Skip to content

geomotif.motifs.spirals

Spiral motifs.

Most of the family is a radius that grows with the angle, so most of it is a one-line :meth:~geomotif.PolarMotif.radius on top of :class:SpiralBase. The four that are not -- Fibonacci (quarter arcs), Theodorus (a chain of triangles), Euler (Fresnel integrals) and the circle involute (an unwinding string) -- are not polar functions of theta at all, and say so by using a different base.

Angles follow the standard math convention: counter-clockwise positive, with the y-axis pointing up. For a y-down (screen/raster) coordinate system, call :meth:~geomotif.Design.flipped_y on the result rather than looking for a flag here -- which way y points is a property of the target space, not of the spiral.

Classes:

Name Description
SpiralBase

Shared ground for spirals defined by a radius that grows with theta.

ArchimedeanSpiral

The arithmetic spiral r = a + b*theta.

LogarithmicSpiral

The equiangular spiral r = a * exp(b*theta).

GoldenSpiral

The logarithmic spiral that widens by the golden ratio each quarter turn.

FermatSpiral

The parabolic spiral r = a * sqrt(theta), both branches.

HyperbolicSpiral

The reciprocal spiral r = a / theta.

Lituus

The spiral r = a / sqrt(theta), named for the Roman augur's staff.

TheodorusSpiral

The square-root spiral: a chain of right triangles, drawn exactly.

FibonacciSpiral

Quarter circles inscribed in a chain of Fibonacci squares.

EulerSpiral

The clothoid: curvature increasing linearly with distance traveled.

CircleInvolute

The path of a string unwinding, taut, from a circle.

SpiralBetween

The endpoint-constrained arithmetic spiral: r = a + b*theta.

SpiralBase dataclass

SpiralBase(*, resolution: int | None = None, center: Point = (0.0, 0.0), theta_start: float = 0.0, theta_span: float = 3.0 * tau)

Bases: PolarMotif, ABC

Shared ground for spirals defined by a radius that grows with theta.

Everything a spiral needs is already on :class:~geomotif.PolarMotif -- :attr:~geomotif.PolarMotif.center, theta_start and theta_span. This adds only the family's default sweep, three turns rather than one, since a single revolution of a spiral is barely recognizable as one.

Say the sweep in revolutions with :meth:~geomotif.PolarMotif.with_turns::

LogarithmicSpiral(b=0.2).with_turns(5, clockwise=True)

A concrete spiral is then its formula and nothing else::

@register("spiral.archimedean", family="spiral")
@dataclass(frozen=True, slots=True)
class ArchimedeanSpiral(SpiralBase):
    a: float = 0.0
    b: float = 10.0

    def radius(self, theta: float) -> float:
        return self.a + self.b * theta

ArchimedeanSpiral dataclass

ArchimedeanSpiral(a: float = 0.0, b: float = 10.0, *, resolution: int | None = None, center: Point = (0.0, 0.0), theta_start: float = 0.0, theta_span: float = 3.0 * tau)

Bases: SpiralBase

The arithmetic spiral r = a + b*theta.

Successive turns are a constant b*tau apart, which is what makes this the spiral of coiled rope, clock springs and vinyl records -- and the one to reach for when even spacing between the windings is the point.

Parameters:

Name Type Description Default
a float

Radius at theta = 0: the size of the hole in the middle.

0.0
b float

Radial growth per radian. Every turn is b * tau further out.

10.0

Methods:

Name Description
between

Return the arithmetic spiral running from start to end.

between staticmethod

between(start: Point, end: Point, *, center: Point = (0.0, 0.0), clockwise: bool = True, turns: int = 0) -> SpiralBetween

Return the arithmetic spiral running from start to end.

The same curve as this class, constrained by where it has to begin and end rather than by its growth rate -- which is the useful form when the endpoints are given and b is whatever it has to be. See :class:SpiralBetween.

Source code in src/geomotif/motifs/spirals.py
@staticmethod
def between(
    start: Point,
    end: Point,
    *,
    center: Point = (0.0, 0.0),
    clockwise: bool = True,
    turns: int = 0,
) -> SpiralBetween:
    """Return the arithmetic spiral running from ``start`` to ``end``.

    The same curve as this class, constrained by where it has to begin and
    end rather than by its growth rate -- which is the useful form when
    the endpoints are given and ``b`` is whatever it has to be. See
    :class:`SpiralBetween`.
    """
    return SpiralBetween(start, end, center=center, clockwise=clockwise, turns=turns)

LogarithmicSpiral dataclass

LogarithmicSpiral(a: float = 5.0, b: float = 0.15, *, resolution: int | None = None, center: Point = (0.0, 0.0), theta_start: float = 0.0, theta_span: float = 3.0 * tau)

Bases: SpiralBase

The equiangular spiral r = a * exp(b*theta).

Every turn is a constant multiple of the one inside it, so the shape is the same at every scale -- the nautilus shell, the spiral galaxy, the low pressure system. The tangent meets the radius at the same angle everywhere, which is where "equiangular" comes from.

Parameters:

Name Type Description Default
a float

Radius at theta = 0.

5.0
b float

Growth rate. The radius multiplies by exp(b * tau) each turn; 0 degenerates to a circle.

0.15

GoldenSpiral dataclass

GoldenSpiral(a: float = 2.0, *, resolution: int | None = None, center: Point = (0.0, 0.0), theta_start: float = 0.0, theta_span: float = 2.0 * tau)

Bases: SpiralBase

The logarithmic spiral that widens by the golden ratio each quarter turn.

A preset rather than a new curve: :class:LogarithmicSpiral with b fixed at ln(phi) / (pi/2). Distinct from :class:FibonacciSpiral, which is the quarter-circle approximation usually drawn in its place -- they are close enough to be mistaken for each other and different enough to be worth having both.

Parameters:

Name Type Description Default
a float

Radius at theta = 0.

2.0

FermatSpiral dataclass

FermatSpiral(a: float = 30.0, *, resolution: int | None = None, center: Point = (0.0, 0.0), theta_start: float = 0.0, theta_span: float = 3.0 * tau, both_branches: bool = True)

Bases: SpiralBase

The parabolic spiral r = a * sqrt(theta), both branches.

Defined by r**2 = a**2 * theta, so every angle has a positive and a negative root and the true curve is two arms meeting at the origin, each the other reflected through it. That symmetry is the whole character of the shape -- it is the arrangement sunflower seeds settle into -- so both arms are drawn by default.

Because area grows linearly with theta, the windings crowd together as they go out, packing points at constant density rather than constant spacing.

Parameters:

Name Type Description Default
a float

Radial scale.

30.0
both_branches bool

Draw the second arm. Turn off for the single arm alone.

True

HyperbolicSpiral dataclass

HyperbolicSpiral(a: float = 200.0, *, resolution: int | None = None, center: Point = (0.0, 0.0), theta_start: float = pi / 6.0, theta_span: float = 3.0 * tau)

Bases: SpiralBase

The reciprocal spiral r = a / theta.

Runs the other way from most of the family: it starts far out and winds inward toward the origin, which it approaches without ever arriving. Outward it is asymptotic to the line y = a.

theta = 0 is a pole, so the sweep must not cross it -- :attr:~geomotif.PolarMotif.theta_start therefore defaults to a small positive angle rather than zero.

Parameters:

Name Type Description Default
a float

Radial scale, and the height of the horizontal asymptote.

200.0

Lituus dataclass

Lituus(a: float = 200.0, *, resolution: int | None = None, center: Point = (0.0, 0.0), theta_start: float = pi / 6.0, theta_span: float = 3.0 * tau)

Bases: SpiralBase

The spiral r = a / sqrt(theta), named for the Roman augur's staff.

The reciprocal of :class:FermatSpiral, and its complement: it sweeps equal areas in equal angles, so where Fermat's arms crowd outward this one's crowd inward. Like :class:HyperbolicSpiral it has a pole at theta = 0 and its sweep must avoid it.

Parameters:

Name Type Description Default
a float

Radial scale.

200.0

TheodorusSpiral dataclass

TheodorusSpiral(triangles: int = 16, size: float = 20.0, center: Point = (0.0, 0.0))

Bases: PolygonMotif

The square-root spiral: a chain of right triangles, drawn exactly.

Each triangle stands on the hypotenuse of the last with a new leg of length size, so the n-th vertex sits at distance size * sqrt(n) from the center. The result is a polyline by nature, not a sampled curve -- there is nothing between the vertices to measure -- which is why this is a :class:~geomotif.PolygonMotif.

Parameters:

Name Type Description Default
triangles int

How many triangles to stack. The classic figure stops at 16, where the spiral has just about closed its first turn.

16
size float

Length of each triangle's new leg.

20.0
center (float, float)

Point the chain winds around.

(0.0, 0.0)

FibonacciSpiral dataclass

FibonacciSpiral(quarters: int = 9, size: float = 10.0)

Bases: Motif

Quarter circles inscribed in a chain of Fibonacci squares.

The spiral everyone draws when they mean :class:GoldenSpiral, and not the same curve: it is a sequence of circular arcs that jump in curvature at every join, where the golden spiral is smooth throughout. The two run within a couple of percent of each other, which is exactly why the distinction is worth drawing.

Parameters:

Name Type Description Default
quarters int

Number of quarter-circle arcs, one per Fibonacci square.

9
size float

Side of the first square.

10.0

EulerSpiral dataclass

EulerSpiral(scale: float = 200.0, extent: float = 2.5, center: Point = (0.0, 0.0), *, resolution: int | None = None)

Bases: ParametricMotif

The clothoid: curvature increasing linearly with distance traveled.

Drive at constant speed and turn the wheel at a constant rate and this is the path you take, which is why it is the transition curve between a straight railway and a curved one. Both arms wind into their own limit point, and the whole curve is one continuous sweep through the origin.

Parameters:

Name Type Description Default
scale float

Size of the figure: the two limit points sit scale/2 from the center along each axis.

200.0
extent float

How far along the curve to travel in each direction. Past about 3 the arms have all but reached their limit points.

2.5
center (float, float)

Midpoint of the figure.

(0.0, 0.0)
Notes

The Fresnel integrals are evaluated by power series, which is accurate to machine precision out to extent = 4. Beyond that the series loses too many digits to cancellation and a standard rational approximation takes over, good to about 2e-3 * scale. That is the price of a dependency-free implementation, and it is charged only where the curve has already spiralled down to a dot.

CircleInvolute dataclass

CircleInvolute(radius: float = 10.0, turns: float = 3.0, center: Point = (0.0, 0.0), *, resolution: int | None = None)

Bases: ParametricMotif

The path of a string unwinding, taut, from a circle.

Neighbouring turns stay exactly one circumference apart, which is what makes this the profile of very nearly every gear tooth in existence: two involutes rolling against each other transmit motion at a constant ratio however far apart their axes drift.

Not a polar motif, despite looking like one -- r and theta are both functions of how much string has unwound, and neither is a function of the other in closed form.

Parameters:

Name Type Description Default
radius float

Radius of the circle being unwound from.

10.0
turns float

Revolutions of string to unwind.

3.0
center (float, float)

Center of that circle.

(0.0, 0.0)

SpiralBetween dataclass

SpiralBetween(start: Point, end: Point, center: Point = (0.0, 0.0), clockwise: bool = True, turns: int = 0, resolution: int | None = None)

Bases: Motif

The endpoint-constrained arithmetic spiral: r = a + b*theta.

Winds from start to end around center, interpolating the radius linearly against a linearly interpolated angle. Both endpoints are hit exactly, which is what makes this the useful form when you know where the curve has to begin and end rather than what its growth rate should be. :class:ArchimedeanSpiral is the same curve parameterized the other way.

Parameters:

Name Type Description Default
start (float, float)

First point of the spiral.

required
end (float, float)

Last point of the spiral.

required
center (float, float)

Point the spiral winds around. Default (0, 0).

(0.0, 0.0)
clockwise bool

Rotational direction of the sweep. Default True.

True
turns int

Extra full revolutions beyond the direct angular sweep from start to end. 0 (default) takes the shortest sweep in the chosen direction.

0
resolution int

Number of segments used to measure the curve. Defaults to a density that scales with the number of turns, which is nearly always right.

None

Examples:

::

SpiralBetween((200, 0), (20, 0), turns=3).generate(120)