Skip to content

geomotif.compose.mandala

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

Four composers and one value type, which between them cover most of what people mean by "mandala".

:class:Mandala is the general one -- a list of :class:Ring s, each saying what to repeat, how many times, and how far out. :class:Kaleidoscope is the single-unit case, under a named symmetry group. :class:SpokePattern and :class:LayeredRings are the scaffolding a mandala is usually hung on, worth having because they are what you reach for first and neither deserves ten lines at the call site. :class:Snowflake is the odd one out: it grows its own arm if you do not give it one, because a snowflake's arm is a random dendrite and there is no other motif in the library that is one.

Classes:

Name Description
Ring

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

Mandala

Concentric rings of repeated motifs.

Kaleidoscope

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

SpokePattern

Radial lines from an inner circle to an outer one.

LayeredRings

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

Snowflake

Six identical arms, each mirrored in its own axis.

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)

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)

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)

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)

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)

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)))