Skip to content

geomotif.compose

Motifs made out of other motifs.

Everything in :mod:geomotif.motifs draws a shape. Everything here arranges shapes somebody else drew: a mandala is rings of a repeated unit, a kaleidoscope is one unit under a symmetry group, a snowflake is one arm reflected and turned six ways.

They are motifs like any other -- they subclass :class:~geomotif.Motif, they build a :class:~geomotif.Design, they resample and export and register like the rest -- so a composed figure can itself be the unit of another composition. What makes them different is only that their most interesting parameter is another motif::

from geomotif.compose import Mandala, Ring
from geomotif.motifs import RegularPolygon, Rose

Mandala(rings=(
    Ring(Rose(n=5, size=30.0), count=8, radius=60.0),
    Ring(RegularPolygon(sides=3, radius=18.0), count=16, radius=120.0),
))

Anything with a build() method works as that parameter, which is the whole point of :class:~geomotif.SupportsBuild.

That parameter is called unit rather than motif throughout, because motif is the key :func:~geomotif.core.registry.spec reserves for a design's own name in :attr:~geomotif.Design.meta -- a field of that name would overwrite it and the design could not be rebuilt from its own spec.

Modules:

Name Description
mandala

Rings, symmetry groups and snowflakes: arranging a motif rather than drawing one.

Classes:

Name Description
Kaleidoscope

One motif, repeated under a cyclic or dihedral symmetry group.

LayeredRings

Concentric circles: the other half of a mandala's scaffolding.

Mandala

Concentric rings of repeated motifs.

Ring

One ring of a :class:Mandala: what to repeat, how often, how far out.

Snowflake

Six identical arms, each mirrored in its own axis.

SpokePattern

Radial lines from an inner circle to an outer one.

Kaleidoscope dataclass

Kaleidoscope(unit: SupportsBuild, group: str = 'D6', center: Point = (0.0, 0.0))

Bases: Motif

One motif, repeated under a cyclic or dihedral symmetry group.

"C6" turns the motif six ways; "D6" turns it six ways and mirrors each, giving twelve copies and the look of a real kaleidoscope, where every second image is reflected because it has bounced off a mirror an odd number of times.

Parameters:

Name Type Description Default
unit Motif or anything with ``build()``

The fundamental domain: the one wedge everything else is made from.

required
group str

"Cn" for n-fold rotation, "Dn" for rotation plus mirrors.

'D6'
center (float, float)

Point the symmetry is about.

(0.0, 0.0)

LayeredRings dataclass

LayeredRings(count: int = 7, inner: float = 24.0, step: float = 20.0, growth: float = 1.0, center: Point = (0.0, 0.0))

Bases: Motif

Concentric circles: the other half of a mandala's scaffolding.

Parameters:

Name Type Description Default
count int

How many circles.

7
inner float

Radius of the innermost.

24.0
step float

Radial gap between the first two circles.

20.0
growth float

Multiplier applied to the gap each time out. 1 gives evenly spaced rings; above 1 they spread as they go, which reads as depth.

1.0
center (float, float)

Middle of the figure.

(0.0, 0.0)

Methods:

Name Description
radii

Return each circle's radius, innermost first.

radii

radii() -> tuple[float, ...]

Return each circle's radius, innermost first.

Source code in src/geomotif/compose/mandala.py
def radii(self) -> tuple[float, ...]:
    """Return each circle's radius, innermost first."""
    out: list[float] = []
    radius, gap = self.inner, self.step
    for _ in range(self.count):
        out.append(radius)
        radius += gap
        gap *= self.growth
    return tuple(out)

Mandala dataclass

Mandala(rings: tuple[Ring, ...], center: Point = (0.0, 0.0))

Bases: Motif

Concentric rings of repeated motifs.

The workhorse. Each :class:Ring is built once and then placed by an affine transform per copy, so a hundred-fold ring costs one build and a hundred cheap transforms rather than a hundred builds.

Parameters:

Name Type Description Default
rings tuple of Ring

The rings, innermost first by convention. Nothing enforces an order, and rings are free to overlap.

required
center (float, float)

Middle of the figure.

(0.0, 0.0)

Ring dataclass

Ring(unit: SupportsBuild, count: int, radius: float, phase: float = 0.0, spin: float = 0.0, face: bool = True, mirror: bool = False)

One ring of a :class:Mandala: what to repeat, how often, how far out.

Parameters:

Name Type Description Default
unit Motif or anything with ``build()``

The unit to repeat.

required
count int

How many copies to place around the ring.

required
radius float

Distance from the mandala's middle to each copy's own origin.

required
phase float

Angle of the first copy, in radians.

0.0
spin float

Extra rotation applied to every copy, on top of whichever way :attr:face leaves it pointing.

0.0
face bool

Turn each copy to face outward, so a petal drawn along the x-axis points away from the middle wherever it lands. Turn this off to keep every copy at its original angle, which is what you want for a shape that has to stay upright.

True
mirror bool

Also place each copy's reflection in its own ray, giving the ring mirror symmetry as well as rotational.

False

Methods:

Name Description
placements

Return the transform that puts each copy where it belongs.

placements

placements(center: Point = (0.0, 0.0)) -> tuple[Affine, ...]

Return the transform that puts each copy where it belongs.

Source code in src/geomotif/compose/mandala.py
def placements(self, center: Point = (0.0, 0.0)) -> tuple[Affine, ...]:
    """Return the transform that puts each copy where it belongs."""
    cx, cy = center
    out: list[Affine] = []
    for index in range(self.count):
        theta = self.phase + math.tau * index / self.count
        # Carry the unit out along the x-axis, then swing the whole arm
        # round to its own angle. A copy that is not meant to face
        # outward has that swing undone on the unit itself.
        spin = self.spin if self.face else self.spin - theta
        placed = (
            Affine.translate(cx, cy)
            @ Affine.rotate(theta)
            @ Affine.translate(self.radius, 0.0)
            @ Affine.rotate(spin)
        )
        out.append(placed)
        if self.mirror:
            out.append(Affine.mirror(theta, through=center) @ placed)
    return tuple(out)

Snowflake dataclass

Snowflake(unit: SupportsBuild | None = None, size: float = 150.0, branches: int = 4, depth: int = 2, seed: int = 0, *, center: Point = (0.0, 0.0))

Bases: Motif

Six identical arms, each mirrored in its own axis.

A real snowflake is sixfold because a water molecule is, and identical across its six arms because every arm grew in the same air. Both facts are in the construction: one arm is grown, then reflected in its own axis and turned six ways.

Give it a unit to use that as the arm. Otherwise it grows a dendrite -- a spine with side branches at sixty degrees, each of which may branch again -- from :attr:seed, so the same seed always yields the same crystal and a different one never does.

Parameters:

Name Type Description Default
unit Motif or anything with ``build()``

The arm, drawn along the positive x-axis and mirrored in it.

None
size float

Length of the grown arm's spine. Ignored when a unit is given.

150.0
branches int

Side branches per spine. Ignored when a unit is given.

4
depth int

How many times a branch may branch again. Ignored when a unit is given.

2
seed int

Fixes the growth. The generator is private to the call, so nothing else in the program can change what you get.

0
center (float, float)

Middle of the crystal.

(0.0, 0.0)

Methods:

Name Description
arm

Return the single arm, before it is mirrored and repeated.

arm

arm() -> Design

Return the single arm, before it is mirrored and repeated.

Source code in src/geomotif/compose/mandala.py
def arm(self) -> Design:
    """Return the single arm, before it is mirrored and repeated."""
    if self.unit is not None:
        return self.unit.build()
    rng = random.Random(self.seed)
    return Design(tuple(self._grow(rng, self.center, 0.0, self.size, self.depth)))

SpokePattern dataclass

SpokePattern(count: int = 24, inner: float = 40.0, outer: float = 140.0, stagger: float = 0.0, rotation: float = 0.0, center: Point = (0.0, 0.0))

Bases: Motif

Radial lines from an inner circle to an outer one.

The bones of most mandalas, and a decent motif on its own. With :attr:stagger set, every other spoke stops short, which is the ticked dial of a compass rose.

Parameters:

Name Type Description Default
count int

How many spokes.

24
inner float

Where each spoke starts and ends.

40.0
outer float

Where each spoke starts and ends.

40.0
stagger float

How far short every second spoke stops, as a fraction of its length. 0 draws every spoke full length; 0.5 halves the alternates.

0.0
rotation float

Angle of the first spoke, in radians.

0.0
center (float, float)

Point they radiate from.

(0.0, 0.0)