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 |
LogarithmicSpiral |
The equiangular spiral |
GoldenSpiral |
The logarithmic spiral that widens by the golden ratio each quarter turn. |
FermatSpiral |
The parabolic spiral |
HyperbolicSpiral |
The reciprocal spiral |
Lituus |
The spiral |
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: |
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 |
0.0
|
b
|
float
|
Radial growth per radian. Every turn is |
10.0
|
Methods:
| Name | Description |
|---|---|
between |
Return the arithmetic spiral running from |
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
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 |
5.0
|
b
|
float
|
Growth rate. The radius multiplies by |
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 |
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
¶
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
¶
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 |
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)
|
clockwise
|
bool
|
Rotational direction of the sweep. Default |
True
|
turns
|
int
|
Extra full revolutions beyond the direct angular sweep from start to
end. |
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)