geomotif
¶
geomotif -- generate and plot geometric designs.
A motif is a parameterized recipe for geometry; applying a transform to what it produced gives a design, which is what you plot or export. That is the whole mental model::
from geomotif import PowerSpacing
from geomotif.motifs import SpiralBetween
design = SpiralBetween(
start=(200, 0), # required — first point (always included)
end=(20, 0), # required — last point (always included)
center=(0, 0), # point the spiral winds around (default shown)
turns=3, # extra full revolutions (default 0)
).generate(
120, spacing=PowerSpacing(2.5)
)
for x, y in design:
...
Point placement is arc-length exact: equal spacing means the same real x,y distance between every consecutive pair of points, however tightly the curve winds. Because resampling is generic over polylines, that applies to every motif -- yours included.
Writing your own takes one method::
from dataclasses import dataclass
from geomotif import Design, Motif, Path, register
@register("my-shape")
@dataclass(frozen=True, slots=True)
class MyShape(Motif):
def build(self) -> Design:
return Design((Path(((0.0, 0.0), (10.0, 10.0))),))
or none at all, if one of the bases in :mod:geomotif.bases already describes
the kind of thing you are drawing -- then you write the maths and nothing else.
Motif classes live in :mod:geomotif.motifs, not here: the catalog is far
too large for a flat namespace. This module exports the core model, the motif
bases, the spacing curves, the transform layer and the registry -- the things
you build with. See :mod:geomotif.core.registry for lookup by name.
Plotting helpers (require matplotlib, pip install 'geomotif[plot]')
live in :mod:geomotif.plotting.
Modules:
| Name | Description |
|---|---|
animate |
Frames: the same design over time, or the same motif over a parameter. |
bases |
Base classes that turn a formula, a grammar or a rule into a motif. |
cli |
The |
compose |
Motifs made out of other motifs. |
core |
The engine: value types, sampling, spacing, transforms and the registry. |
demo |
Demo entry point: generate a few spirals and plot them with matplotlib. |
explore |
An explorable gallery: one page, sliders, and every picture already drawn. |
io |
Getting designs out of Python, and back in again. |
motifs |
The motif catalog. |
plotting |
Matplotlib helpers for looking at a design. |
Classes:
| Name | Description |
|---|---|
Curve |
One parametric strand of a motif, and how densely to measure it. |
LatticeTiling |
Base for a periodic tiling: one cell, repeated on two basis vectors. |
LSystemMotif |
Base for a motif described by a grammar and drawn with a turtle. |
MultiCurveMotif |
Base for a motif made of several parametric strands. |
ParametricMotif |
Base for a motif defined by a single |
PolarMotif |
Base for a motif defined by a single |
PolygonMotif |
Base for a motif drawn as one or more exact corner sequences. |
SegmentMotif |
Base for a motif built from straight segments between indexed points. |
SubstitutionTiling |
Base for an aperiodic tiling: seed tiles, subdivided :attr: |
Motif |
Convenience base: implement :meth: |
SupportsBuild |
Structural contract -- any object with |
Range |
A min/max/step bound for a parameter, as a field-metadata mapping. |
ArcTable |
Cumulative-length table over a polyline, and the inverse of it. |
CircularSpacing |
Circular (quarter-arc) easing -- abrupt at one end, flat at the other. |
CompositeSpacing |
Chain curves, feeding each one's output into the next. |
CubicSpacing |
Classic cubic easing ( |
ExponentialSpacing |
Exponential easing with adjustable |
LinearSpacing |
Equal spacing between every point (the default, a "0 curve"). |
PowerSpacing |
|
QuadraticSpacing |
Classic quadratic easing ( |
ReversedSpacing |
Mirror any curve: dense where the original was sparse, and vice versa. |
SineSpacing |
Sinusoidal easing -- a gentle, natural-feeling bias. |
SmoothstepSpacing |
Hermite smoothstep |
SpacingCurve |
Base class for point-spacing curves. |
TableSpacing |
A curve drawn by hand, from arbitrary |
Style |
How one stroke or one loose point is drawn. |
Affine |
A 2D affine transform, composable with |
Bounds |
An axis-aligned rectangle enclosing some geometry. |
Design |
The universal result: zero or more strokes plus zero or more loose points. |
Path |
One continuous polyline. |
Functions:
| Name | Description |
|---|---|
register |
Register a motif class under |
densify |
Evaluate |
resample |
Return |
resample_path |
Return |
samples_for_turns |
Return a sensible densification count for a curve spanning |
coerce_spacing |
Normalize anything spacing-shaped into a :class: |
by_layer |
Split a design into one sub-design per layer, styles and all. |
layer_names |
Return the layers a design uses, in the order they first appear. |
point_styles_of |
Return one style per loose point, |
styled |
Return |
styles_of |
Return one style per stroke, |
clip_to |
Trim |
fit_to |
Scale and center |
jitter |
Randomly displace every point, for controlled hand-drawn irregularity. |
layer |
Overlay designs into one. Equivalent to repeated |
mirror_axis |
Return |
offset_path |
Return a parallel copy of |
radial_repeat |
Repeat |
snap |
Move every point onto the nearest line of a square grid. |
symmetry_group |
Apply a full cyclic |
tile |
Repeat |
from_spec |
Rebuild the motif a spec describes. |
load_design |
Read a design back from a JSON file written by :func: |
load_spec |
Read a spec file and return the motif it describes. |
save_design |
Write a design, strokes kept apart, and return the path written. |
save_dxf |
Write a design to a DXF file and return the path written. |
save_gif |
Write an animated GIF and return the path written. |
save_jpeg |
Render a design -- or re-encode a raster -- as JPEG and write it. |
save_plotter_svg |
Write a plotter-ready SVG and return the path written. |
save_png |
Render a design -- or re-encode a raster -- as a PNG and write it. |
save_points |
Write points to a file and return the path written. |
save_spec |
Write a motif's recipe to a JSON file and return the path written. |
save_svg |
Write a design to an SVG file and return the path written. |
to_dxf |
Render a design as a DXF R12 document. |
to_gif |
Render a sequence of designs as an animated GIF. |
to_jpeg |
Render a design -- or re-encode a :class: |
to_plotter_svg |
Render a design as an SVG measured in real millimeters. |
to_png |
Render a design -- or re-encode a :class: |
to_spec |
Return the JSON-ready recipe for a motif, or for the design it built. |
to_svg |
Render a design as an SVG document. |
Curve
dataclass
¶
Curve(position: Callable[[float], Point], domain: tuple[float, float] = (0.0, 1.0), closed: bool = False, turns: float = 1.0)
One parametric strand of a motif, and how densely to measure it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
position
|
callable
|
Maps a parameter to a point. Called across |
required |
domain
|
(float, float)
|
Parameter range, inclusive at both ends. May run backwards. |
(0.0, 1.0)
|
closed
|
bool
|
Whether the strand returns to where it started. A closed strand's
final sample is dropped, since :class: |
False
|
turns
|
float
|
How far the strand winds, in whole turns. Only ever used to choose a sample count: a curve that bends more needs measuring more finely. |
1.0
|
LatticeTiling
dataclass
¶
LatticeTiling(*, region: Bounds, clip: bool = True)
Bases: Motif, ABC
Base for a periodic tiling: one cell, repeated on two basis vectors.
Implement :meth:cell (the geometry of one tile) and :meth:basis (the
two translations that repeat it), and give the motif a :attr:region to
fill::
@register("tiling.square", family="tiling")
@dataclass(frozen=True, slots=True)
class SquareTiling(LatticeTiling):
size: float = 10.0
def basis(self) -> tuple[Point, Point]:
return ((self.size, 0.0), (0.0, self.size))
def cell(self) -> Design:
s = self.size
return Design((Path(((0, 0), (s, 0), (s, s), (0, s)), closed=True),))
:meth:basis is a method rather than a field because for most tilings the
vectors follow from the motif's own parameters, as above; a tiling that
genuinely wants caller-supplied vectors can declare a field and return it.
Methods:
| Name | Description |
|---|---|
basis |
Return the two translation vectors that generate the lattice. |
cell |
Return the geometry of a single cell, at lattice origin. |
basis
abstractmethod
¶
LSystemMotif
dataclass
¶
Bases: Motif, ABC
Base for a motif described by a grammar and drawn with a turtle.
Define :attr:axiom, :attr:rules and :attr:angle as class variables;
everything else is a parameter with a sensible default. A concrete
fractal is four lines::
@register("koch", family="fractal")
@dataclass(frozen=True, slots=True)
class KochCurve(LSystemMotif):
axiom = "F"
rules: ClassVar[Mapping[str, str]] = {"F": "F+F--F+F"}
angle = math.pi / 3
The ClassVar annotation on :attr:rules is not decoration: without
it, a mutable class attribute in a dataclass body is ambiguous to reader
and linter alike, and annotating it as anything else would make it a
constructor parameter with a mutable default, which dataclasses reject.
Notes
Both the string expansion and the point count grow exponentially with
:attr:depth; the expansion is capped so that an accidental depth=20
raises instead of exhausting memory.
Methods:
| Name | Description |
|---|---|
expand |
Return the axiom rewritten :attr: |
expand
¶
Return the axiom rewritten :attr:depth times.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/geomotif/bases/lsystem.py
MultiCurveMotif
dataclass
¶
Bases: Motif, ABC
Base for a motif made of several parametric strands.
Implement :meth:curves; :meth:build measures each strand and returns
one :class:~geomotif.Path per strand.
Notes
Subclasses implement hooks rather than overriding :meth:build, and the
bases validate inside :meth:build rather than in __post_init__, both
for the same reason: on Python 3.12 the zero-argument super() does not
work inside a slots=True dataclass, so a subclass cannot reliably
chain up to us. Nothing here ever requires it to.
Every field on a base is keyword-only, so a subclass is free to declare its own parameters positionally without tripping over the "no non-default argument after a default one" rule.
Methods:
| Name | Description |
|---|---|
curves |
Return the strands this motif is made of. At least one. |
ParametricMotif
dataclass
¶
Bases: MultiCurveMotif, ABC
Base for a motif defined by a single position(u).
Set :attr:domain and :attr:closed as class variables to describe the
curve's shape; override :meth:sweep_turns if it winds more than once, so
the sample density keeps up with it.
Examples:
::
@register("astroid", family="curve")
@dataclass(frozen=True, slots=True)
class Astroid(ParametricMotif):
domain = (0.0, math.tau)
closed = True
size: float = 1.0
def position(self, u: float) -> Point:
return (
self.size * math.cos(u) ** 3,
self.size * math.sin(u) ** 3,
)
Methods:
| Name | Description |
|---|---|
position |
Return the point at parameter |
sweep_turns |
Return how far the curve winds, in whole turns. |
position
abstractmethod
¶
sweep_turns
¶
Return how far the curve winds, in whole turns.
Used only to pick a sample count. The default of one turn suits any curve that does not loop repeatedly; a spiral should return its actual revolution count so that a tightly wound one is still measured accurately.
Source code in src/geomotif/bases/parametric.py
PolarMotif
dataclass
¶
PolarMotif(*, resolution: int | None = None, center: Point = (0.0, 0.0), theta_start: float = 0.0, theta_span: float = tau)
Bases: ParametricMotif, ABC
Base for a motif defined by a single radius(theta).
The whole extensibility story in eight lines::
@register("my-flower", family="polar")
@dataclass(frozen=True, slots=True)
class MyFlower(PolarMotif):
k: float = 7.0
def radius(self, theta: float) -> float:
return math.sin(self.k * theta) + 0.4 * math.cos(17 * theta)
Notes
A negative radius reflects: the point is placed on the opposite ray,
at theta + pi. That is what the cartesian conversion does naturally,
it is the convention every plot of r = cos(k*theta) assumes, and it is
what makes the petal count of a rose come out right. Clip the radius
yourself in :meth:radius if you want the other convention.
The hook is named radius, so a subclass cannot also have a field
called radius -- the two would collide in the class body. In practice
that never bites: a shape whose radius is a constant parameter rather
than a function of theta is a circle or an arc, and those are
parametric rather than polar for exactly this reason.
Methods:
| Name | Description |
|---|---|
radius |
Return the radius at angle |
with_turns |
Return a copy sweeping |
radius
abstractmethod
¶
with_turns
¶
Return a copy sweeping turns revolutions in the given direction.
The same thing as setting :attr:theta_span to turns * tau, said
the way a wound curve is usually described::
LogarithmicSpiral(b=0.15).with_turns(5, clockwise=True)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
turns
|
float
|
Revolutions to sweep. Fractional turns are fine. |
required |
clockwise
|
bool
|
Sweep direction. Counter-clockwise by default, matching the standard math convention the rest of the library uses. |
False
|
Source code in src/geomotif/bases/parametric.py
PolygonMotif
dataclass
¶
Bases: Motif, ABC
Base for a motif drawn as one or more exact corner sequences.
Implement :meth:outlines; :meth:build turns each one into a
:class:~geomotif.Path::
@register("rectangle", family="primitive")
@dataclass(frozen=True, slots=True)
class Rectangle(PolygonMotif):
width: float = 1.0
height: float = 1.0
def outlines(self) -> Iterable[Sequence[Point]]:
w, h = self.width / 2.0, self.height / 2.0
yield ((-w, -h), (w, -h), (w, h), (-w, h))
:meth:outlines is plural because one shape is not always one loop: the
star polygon {6/2} is two overlaid triangles, and drawing it as a
single path would invent an edge between them that is not there.
Notes
Corners are emitted as given -- no deduplication, no collinear-point removal. A motif that wants a vertex repeated (to hold a pen, to mark a lattice site) is entitled to it, and guessing otherwise would silently change geometry the author chose.
Methods:
| Name | Description |
|---|---|
outlines |
Return the corner sequences to draw, one per stroke. At least one. |
outlines
abstractmethod
¶
SegmentMotif
dataclass
¶
Bases: Motif, ABC
Base for a motif built from straight segments between indexed points.
Implement :meth:nodes and :meth:edges; :meth:build turns them into
strokes. A ten-line class gets you the whole times-table family::
@register("modular.multiplication", family="graph", example={"modulus": 200})
@dataclass(frozen=True, slots=True)
class ModularMultiplication(SegmentMotif):
modulus: int = 200
factor: int = 2
def nodes(self) -> Sequence[Point]:
step = math.tau / self.modulus
return [(math.cos(i * step), math.sin(i * step)) for i in range(self.modulus)]
def edges(self) -> Iterable[tuple[int, int]]:
return ((i, self.factor * i % self.modulus) for i in range(self.modulus))
Notes
Edges are undirected: (i, j) and (j, i) are the same segment and
the duplicate is dropped, as is any self-loop (i, i). Both are
routine outputs of an arithmetic edge rule rather than mistakes, so
neither is an error -- but drawing them would waste plotter time on
nothing.
Methods:
| Name | Description |
|---|---|
nodes |
Return the points the edges are drawn between. |
edges |
Return index pairs into :meth: |
nodes
abstractmethod
¶
SubstitutionTiling
dataclass
¶
Bases: Motif, ABC
Base for an aperiodic tiling: seed tiles, subdivided :attr:depth times.
The tile type is yours -- a dataclass of three vertices, a rhomb with an orientation, whatever the substitution rule needs. The base only ever passes tiles back to your own methods, so it never has to know.
Implement :meth:seed (the starting tiles), :meth:subdivide (one tile
to its replacements) and :meth:outline (a tile to the strokes that draw
it).
Notes
Tile count grows geometrically -- a rule with three replacements reaches a hundred thousand tiles by depth eleven -- so the expansion is capped and raises rather than exhausting memory.
Shared edges are drawn once per tile that owns them, so a plotter will trace most edges twice. Deduplicating them means comparing floating-point vertices for equality, which is a judgement call about tolerance the base should not be making for you.
Methods:
| Name | Description |
|---|---|
seed |
Return the tiles the subdivision starts from. |
subdivide |
Return the tiles that replace |
outline |
Return the strokes that draw |
tiles |
Return the seed tiles subdivided :attr: |
seed
abstractmethod
¶
subdivide
abstractmethod
¶
tiles
¶
Return the seed tiles subdivided :attr:depth times.
Exposed separately from :meth:build because the tiles themselves are
often what you want -- to count them, to check a substitution rule
preserves area, or to color them by type.
Source code in src/geomotif/bases/tiling.py
Motif
¶
Bases: ABC
Convenience base: implement :meth:build, inherit everything else.
The ABC exists to hand you :meth:generate and registration, not to
police the type -- see :class:SupportsBuild if you would rather not
inherit at all.
Methods:
| Name | Description |
|---|---|
build |
Return the design at its natural/native resolution. |
generate |
Build, then resample to |
generate
¶
generate(count: int | None = None, *, step: float | None = None, spacing: SpacingLike | None = None, distribute: Distribution = 'length', by: Placement = 'length') -> Design
Build, then resample to count points (or a fixed step distance).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
count
|
int
|
Total number of points to return. Must be >= 2. |
None
|
step
|
float
|
Fixed real distance between consecutive points, letting the count
fall out of the geometry. Mutually exclusive with |
None
|
spacing
|
SpacingCurve or callable
|
Distribution of points along the path. Defaults to equal spacing. |
None
|
distribute
|
('length', 'even', 'per_path')
|
How |
"length"
|
by
|
('length', 'parameter')
|
Place points by real distance along the curve (the default), or by even steps through its parametrization. |
"length"
|
Returns:
| Type | Description |
|---|---|
Design
|
The resampled design, ready to plot or export. |
Source code in src/geomotif/core/motif.py
SupportsBuild
¶
Bases: Protocol
Structural contract -- any object with build() works everywhere.
Following the :class:typing.SupportsInt convention, this is the
structural twin of :class:Motif: anything that can build a design is
accepted wherever a motif is, so nobody is ever forced to inherit.
Methods:
| Name | Description |
|---|---|
build |
Return the design at its natural resolution. |
Range
dataclass
¶
Bases: Mapping[str, float | None]
A min/max/step bound for a parameter, as a field-metadata mapping.
Any of the three may be None to leave that bound unset; a consumer
then falls back to its own heuristic for the missing axis. The common
case is to set all three, and the commonest still is Range(lo, hi,
step=1) for an integer count.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min
|
float
|
Smallest sensible value, inclusive. |
None
|
max
|
float
|
Largest sensible value, inclusive. |
None
|
step
|
float
|
Granularity for an integer or quantized parameter. |
None
|
Examples:
>>> from dataclasses import field
>>> field(default=5, metadata=Range(1, 50, step=1)).metadata.get("min")
1
ArcTable
¶
Cumulative-length table over a polyline, and the inverse of it.
Building the table is O(n). A lone "where is the point at distance d?"
costs a binary search and one linear interpolation; asking for a whole run
of increasing distances -- which is what resampling does -- walks the table
once between them all instead, so the run is linear in the table rather
than n log n. See :meth:points_at.
Methods:
| Name | Description |
|---|---|
point_at |
Return the point |
point_at_fraction |
Return the point at fraction |
points_at |
Return the point at each distance, in the order they were asked for. |
segment |
Return the part of the polyline lying between two distances. |
points_at_fractions |
Return the point at each fraction of the total length. |
Attributes:
| Name | Type | Description |
|---|---|---|
total |
float
|
Total length of the polyline. |
vertices |
tuple[Point, ...]
|
The measured points, including the closing vertex if closed. |
Source code in src/geomotif/core/sampling.py
vertices
property
¶
The measured points, including the closing vertex if closed.
point_at
¶
Return the point distance along the polyline, clamped to its ends.
A zero-length polyline (every vertex coincident) always returns its single location rather than dividing by zero -- the degenerate case should collapse gracefully, not explode.
Source code in src/geomotif/core/sampling.py
point_at_fraction
¶
points_at
¶
Return the point at each distance, in the order they were asked for.
Exactly what calling :meth:point_at on each in turn returns, and
several times faster for the run of lookups that resampling actually
performs. Those arrive in increasing order, so the segment holding one
is at or after the segment that held the last, and the whole run walks
the table once between them instead of binary-searching all of it every
time.
Order is exploited, never assumed: a distance that goes backwards seeks again, so this has no precondition to get wrong and no fast and slow version to keep in agreement.
Source code in src/geomotif/core/sampling.py
segment
¶
Return the part of the polyline lying between two distances.
The ends are exact -- interpolated where they fall inside a segment -- and every vertex between them is kept as it was, so this is a piece of the polyline rather than a resampling of one. That is what an animation drawing itself on needs: the geometry so far, at the resolution it was built at.
Distances outside the polyline clamp to its ends, and a range that collapses to a point returns that one point.
Source code in src/geomotif/core/sampling.py
points_at_fractions
¶
Return the point at each fraction of the total length.
CircularSpacing
¶
Bases: _ModalCurve
Circular (quarter-arc) easing -- abrupt at one end, flat at the other.
Source code in src/geomotif/core/spacing.py
CompositeSpacing
¶
Bases: SpacingCurve
Chain curves, feeding each one's output into the next.
CompositeSpacing(a, b)(t) == b(a(t)) -- written in application order,
so it reads left to right. Composition of monotone [0, 1] -> [0, 1] maps
is itself monotone with fixed endpoints, so the result is always a valid
spacing curve.
Source code in src/geomotif/core/spacing.py
CubicSpacing
¶
ExponentialSpacing
¶
Bases: _ModalCurve
Exponential easing with adjustable strength (dramatic bias).
strength (default 10, the CSS/Penner standard) controls how extreme
the clustering is; higher values pack points ever more tightly at the
slow end. The curve is normalized so it still maps 0 -> 0 and 1 -> 1.
Source code in src/geomotif/core/spacing.py
LinearSpacing
¶
PowerSpacing
¶
Bases: _ModalCurve
t ** exponent -- the general-purpose "by how much" control.
exponent == 1-- equal spacing (identical to :class:LinearSpacing)exponent > 1-- with mode"in", spacing gradually increases; larger exponents exaggerate the effect0 < exponent < 1-- the opposite bias
Combine with mode="out" to flip which end is dense.
Source code in src/geomotif/core/spacing.py
QuadraticSpacing
¶
ReversedSpacing
¶
Bases: SpacingCurve
Mirror any curve: dense where the original was sparse, and vice versa.
ReversedSpacing(f)(t) == 1 - f(1 - t), which is exactly the "out"
of a modal curve's "in" -- but this works on curves that have no mode,
including plain callables and :class:TableSpacing.
Source code in src/geomotif/core/spacing.py
SineSpacing
¶
SmoothstepSpacing
¶
Bases: SpacingCurve
Hermite smoothstep 3t^2 - 2t^3 -- inherently ease-in-out.
Spacing grows toward the middle of the path and shrinks again toward the end, with perfectly smooth acceleration.
SpacingCurve
¶
Bases: ABC
Base class for point-spacing curves.
Subclasses must implement :meth:ease, mapping [0, 1] -> [0, 1]
monotonically with the endpoints fixed.
Methods:
| Name | Description |
|---|---|
ease |
Map a fraction of the way along the curve to a fraction of its length. |
TableSpacing
¶
Bases: SpacingCurve
A curve drawn by hand, from arbitrary (t, eased) control points.
Interpolation between control points is linear. That is a deliberate choice over a smoother spline: linear interpolation of monotone data is exactly monotone, whereas cubic fits can overshoot and hand back a curve that walks backwards -- which shows up as points in the wrong order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
sequence of (float, float)
|
Control points in [0, 1] x [0, 1]. |
required |
Examples:
A curve that spends the first half of its length on the first quarter of the path::
TableSpacing([(0.5, 0.25)])
Source code in src/geomotif/core/spacing.py
Style
dataclass
¶
Style(layer: str | None = None, stroke: str | None = None, width: float | None = None, fill: str | None = None)
How one stroke or one loose point is drawn.
Every field is optional and None means "not stated", which is not the
same as a default: an unstated color takes whatever the writer was told
to use, so a style that names only a layer still draws in the document's
own ink.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer
|
str
|
Which layer the geometry belongs on. SVG writes these as the labeled
groups Inkscape and |
None
|
stroke
|
str
|
Line color, as any CSS color string. DXF has no notion of an arbitrary color, so its writer maps the seven it can name and leaves the rest to the layer. |
None
|
width
|
float
|
Stroke width, in the units the writer is working in. Must be > 0. |
None
|
fill
|
str
|
Fill color for closed paths. Line art rarely wants one, which is why it is not the default. |
None
|
Methods:
| Name | Description |
|---|---|
merged |
Return this style with everything |
merged
¶
Return this style with everything other states laid over it.
Silence loses: a field other leaves at None keeps this style's
value, which is what makes styled composable -- setting a color
later must not quietly clear the layer set earlier.
Source code in src/geomotif/core/style.py
Affine
dataclass
¶
Affine(a: float = 1.0, b: float = 0.0, c: float = 0.0, d: float = 1.0, e: float = 0.0, f: float = 0.0)
A 2D affine transform, composable with @ and callable on points.
Defaults to the identity, so Affine() is a no-op you can build on.
Methods:
| Name | Description |
|---|---|
identity |
Return the transform that changes nothing. |
translate |
Move by |
rotate |
Rotate by |
scale |
Scale by |
mirror |
Reflect across the line at |
shear |
Slant by |
inverse |
Return the transform that undoes this one. |
Attributes:
| Name | Type | Description |
|---|---|---|
determinant |
float
|
Signed area scale factor; negative when the transform reflects. |
determinant
property
¶
Signed area scale factor; negative when the transform reflects.
identity
classmethod
¶
translate
classmethod
¶
rotate
classmethod
¶
Rotate by angle radians, counter-clockwise in y-up coordinates.
Source code in src/geomotif/core/transform.py
scale
classmethod
¶
Scale by sx horizontally and sy vertically (sy defaults to sx).
Source code in src/geomotif/core/transform.py
mirror
classmethod
¶
Reflect across the line at angle radians passing through through.
Source code in src/geomotif/core/transform.py
shear
classmethod
¶
inverse
¶
inverse() -> Affine
Return the transform that undoes this one.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the transform is singular (a zero scale factor, say), which collapses the plane onto a line and cannot be undone. |
Source code in src/geomotif/core/transform.py
Bounds
dataclass
¶
An axis-aligned rectangle enclosing some geometry.
Methods:
| Name | Description |
|---|---|
from_points |
Return the tightest bounds containing every point. |
union |
Return the smallest bounds containing both rectangles. |
padded |
Return these bounds grown by |
Attributes:
| Name | Type | Description |
|---|---|---|
width |
float
|
Horizontal extent. |
height |
float
|
Vertical extent. |
center |
Point
|
Midpoint of the rectangle. |
from_points
classmethod
¶
from_points(points: Iterable[Point]) -> Bounds
Return the tightest bounds containing every point.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/geomotif/core/types.py
union
¶
Return the smallest bounds containing both rectangles.
Source code in src/geomotif/core/types.py
Design
dataclass
¶
Design(paths: tuple[Path, ...] = (), points: tuple[Point, ...] = (), meta: Mapping[str, object] = EMPTY_META)
The universal result: zero or more strokes plus zero or more loose points.
meta carries the motif name and its resolved parameters (including any
resolved random seed), which is what makes a design reproducible,
serializable to a spec file, and self-labelling in the gallery.
Methods:
| Name | Description |
|---|---|
transformed |
Return this design with |
flipped_y |
Return this design mirrored about the x-axis. |
resampled |
Return this design resampled to |
snapped |
Return this design with every point moved onto a grid of |
fit |
Return this design scaled and centered inside a |
Attributes:
| Name | Type | Description |
|---|---|---|
bounds |
Bounds
|
Bounds over every point in every path plus the loose points. |
transformed
¶
Return this design with m applied to every point.
Source code in src/geomotif/core/types.py
flipped_y
¶
flipped_y() -> Design
Return this design mirrored about the x-axis.
The y-up/y-down question is a property of the target coordinate space, not of any motif, which is why it lives here rather than as a flag on every builder.
Source code in src/geomotif/core/types.py
resampled
¶
resampled(count: int | None = None, *, step: float | None = None, spacing: SpacingLike | None = None, distribute: Distribution = 'length') -> Design
Return this design resampled to count points (or a fixed step).
See :func:geomotif.core.sampling.resample for the full contract.
Source code in src/geomotif/core/types.py
snapped
¶
snapped(step: float = 1.0, *, mode: SnapMode = 'half-even', drop_duplicates: bool = True) -> Design
Return this design with every point moved onto a grid of step.
Rounding the geometry rather than each file as it is written, so every exporter agrees and a plot shows what the file will hold. Defaults to whole units.
See :func:geomotif.core.transform.snap for the full contract.
Source code in src/geomotif/core/types.py
fit
¶
fit(width: float, height: float, *, padding: float = 0.0, flip_y: bool = False) -> Design
Return this design scaled and centered inside a width x height canvas.
Scaling is uniform, so the design is never distorted; it is centered
in whichever axis has slack. The result sits in [0, width] x
[0, height], with flip_y=True producing y-down (screen/SVG)
coordinates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
width
|
float
|
Canvas size. Both must be positive. |
required |
height
|
float
|
Canvas size. Both must be positive. |
required |
padding
|
float
|
Margin reserved on all four sides. |
0.0
|
flip_y
|
bool
|
Mirror vertically so y increases downward. |
False
|
Returns:
| Type | Description |
|---|---|
Design
|
The fitted design. A design with no extent in either axis (a single point, or a perfectly vertical line) is translated but never scaled, since there is no finite scale that fills a canvas from nothing. |
Source code in src/geomotif/core/types.py
Path
dataclass
¶
One continuous polyline.
closed means the last point connects back to the first. The closing
segment is implied, never stored, so a closed path's points are never
duplicated at the seam.
Points are normalized to a tuple of finite (float, float) pairs at
construction, so any sequence of pairs may be passed in.
Attributes:
| Name | Type | Description |
|---|---|---|
length |
float
|
Total polyline length, including the closing segment if closed. |
bounds |
Bounds
|
Tightest rectangle containing every vertex. |
length
property
¶
Total polyline length, including the closing segment if closed.
A two-point "closed" path is treated as a single open segment: its closing segment retraces the one it already has, and counting that twice reports a length no plotter would ever draw.
register
¶
register(name: str | None = None, *, family: str | None = None, requires: str | None = None, example: Mapping[str, object] | None = None) -> Callable[[type[MotifT]], type[MotifT]]
Register a motif class under name, returning it unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Registry key. Derived from the class name in kebab-case when
omitted, so |
None
|
family
|
str
|
Grouping for |
None
|
requires
|
str
|
Name of an optional dependency the motif needs, e.g. |
None
|
example
|
mapping
|
Constructor arguments producing a representative instance -- what the gallery renders and what the conformance suite exercises. Required for motifs with parameters that have no default, since those cannot be instantiated any other way. |
None
|
Examples:
::
@register("rose", family="polar", example={"k": 5})
@dataclass(frozen=True, slots=True)
class Rose(PolarMotif): ...
Source code in src/geomotif/core/registry.py
densify
¶
densify(fn: Callable[[float], Point], *, samples: int, domain: tuple[float, float] = (0.0, 1.0)) -> tuple[Point, ...]
Evaluate fn at evenly spaced parameters across domain.
Returns samples + 1 points, so both endpoints of the domain are
included and the result contains exactly samples segments.
Source code in src/geomotif/core/sampling.py
resample
¶
resample(design: Design, count: int | None = None, *, step: float | None = None, spacing: SpacingLike | None = None, distribute: Distribution = 'length', by: Placement = 'length') -> Design
Return design resampled across all of its paths.
Loose points are passed through untouched: they are already exactly the points the motif meant, with no curve to redistribute them along.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
The design to resample. |
required |
count
|
int
|
Total number of points. How it is split across paths depends on
|
None
|
step
|
float
|
Fixed distance between consecutive points, applied independently to
every path. |
None
|
spacing
|
SpacingCurve or callable
|
Distribution of points along each path. |
None
|
distribute
|
('length', 'even', 'per_path')
|
How a total
|
"length"
|
by
|
('length', 'parameter')
|
Placement mode along each individual path; see :func: |
"length"
|
Returns:
| Type | Description |
|---|---|
Design
|
A new design with the same loose points and metadata. |
Source code in src/geomotif/core/sampling.py
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 | |
resample_path
¶
resample_path(path: Path, count: int | None = None, *, step: float | None = None, spacing: SpacingLike | None = None, by: Placement = 'length') -> Path
Return path resampled to count points, or at a fixed step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path
|
The polyline to resample. |
required |
count
|
int
|
Total number of points to return. Must be >= 2. Mutually exclusive
with |
None
|
step
|
float
|
Fixed real distance between consecutive points; the count falls out
of the geometry. Any remainder shorter than |
None
|
spacing
|
SpacingCurve or callable
|
Distribution of points along the path. Defaults to equal spacing.
Cannot be combined with |
None
|
by
|
('length', 'parameter')
|
|
"length"
|
Returns:
| Type | Description |
|---|---|
Path
|
The resampled path, preserving |
Source code in src/geomotif/core/sampling.py
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | |
samples_for_turns
¶
Return a sensible densification count for a curve spanning turns.
Sample density has to scale with how much the curve actually bends, or a tightly wound motif is measured by a polyline that cuts every corner. This is the adaptive heuristic the spiral generator used, exposed so every motif can share one answer.
Source code in src/geomotif/core/sampling.py
coerce_spacing
¶
coerce_spacing(spacing: SpacingLike | None) -> SpacingCurve
Normalize anything spacing-shaped into a :class:SpacingCurve.
None means :class:LinearSpacing. This is the single place the
library decides what counts as a spacing curve, so every entry point
accepts exactly the same things and fails the same way.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Source code in src/geomotif/core/spacing.py
by_layer
¶
Split a design into one sub-design per layer, styles and all.
Returns:
| Type | Description |
|---|---|
dict
|
Keyed by layer name, in first-appearance order, with |
Source code in src/geomotif/core/style.py
layer_names
¶
layer_names(design: Design) -> tuple[str, ...]
Return the layers a design uses, in the order they first appear.
First appearance rather than alphabetical: layer order is drawing order, and a plotter changes pens in the order the file lists them.
Source code in src/geomotif/core/style.py
point_styles_of
¶
Return one style per loose point, None where a point has none.
styled
¶
styled(design: Design, style: Style | None = None, *, layer: str | None = None, stroke: str | None = None, width: float | None = None, fill: str | None = None) -> Design
Return design with a style laid over every stroke and loose point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
What to style. Returned unchanged if nothing is actually stated. |
required |
style
|
Style
|
A whole style to apply. |
None
|
layer
|
str | None
|
Individual fields, applied over |
None
|
stroke
|
str | None
|
Individual fields, applied over |
None
|
width
|
str | None
|
Individual fields, applied over |
None
|
fill
|
str | None
|
Individual fields, applied over |
None
|
Returns:
| Type | Description |
|---|---|
Design
|
The same geometry, with styles merged over whatever it already carried -- so a second call that names a color keeps the layer the first one set. |
Source code in src/geomotif/core/style.py
styles_of
¶
Return one style per stroke, None where a stroke has none.
Always exactly as long as design.paths, whatever the metadata says, so
callers can zip the two without checking.
Source code in src/geomotif/core/style.py
clip_to
¶
Trim design to a rectangle, splitting paths that leave and re-enter.
Clipped paths come back open even if they went in closed: a shape whose outline has been cut is no longer a closed loop, and pretending otherwise would draw a chord across the gap. Loose points outside the rectangle are dropped.
Source code in src/geomotif/core/transform.py
fit_to
¶
fit_to(design: Design, width: float, height: float, *, padding: float = 0.0, flip_y: bool = False) -> Design
Scale and center design inside a canvas. See :meth:Design.fit.
Source code in src/geomotif/core/transform.py
jitter
¶
Randomly displace every point, for controlled hand-drawn irregularity.
Each coordinate is offset independently by a uniform value in
[-amount, amount]. The RNG is private to this call -- the global
:mod:random state is never touched -- so a given seed always
reproduces the same result no matter what else the program is doing.
Source code in src/geomotif/core/transform.py
layer
¶
Overlay designs into one. Equivalent to repeated +.
mirror_axis
¶
Return design overlaid with its reflection across a line.
offset_path
¶
Return a parallel copy of path, offset by distance to its left.
Left is relative to the direction of travel in y-up coordinates, so a negative distance offsets to the right. Corners are mitered, with a limit that falls back to a plain bevel on very sharp turns.
This is the "simple parallel stroke" of guilloché and knot outlines, not a CAD offset: self-intersections on tight concave corners are not cleaned up, and the result may cross itself where the offset exceeds the local radius of curvature.
Source code in src/geomotif/core/transform.py
radial_repeat
¶
Repeat design n times evenly around a point -- the mandala workhorse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
The unit to repeat. |
required |
n
|
int
|
Number of copies, including the original. Must be >= 1. |
required |
about
|
(float, float)
|
Center of rotation. |
(0.0, 0.0)
|
mirror
|
bool
|
Also emit a reflected copy in each sector, giving dihedral rather than merely cyclic symmetry. |
False
|
Source code in src/geomotif/core/transform.py
snap
¶
snap(design: Design, step: float = 1.0, *, mode: SnapMode = 'half-even', drop_duplicates: bool = True) -> Design
Move every point onto the nearest line of a square grid.
This is rounding applied to the design rather than to each file as it is
written, which is the difference that matters: every exporter then agrees,
and a plot of the result shows what the file will actually contain.
design.snapped() alone rounds to whole units.
Snapping trades this library's exact arc-length spacing for grid alignment. Points that were an equal real distance apart come out equal only to within half a step, so snap after resampling and choose a step well below the spacing if the evenness is what you are there for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
What to snap. |
required |
step
|
float
|
Grid size, in the design's own units. Must be finite and positive.
|
1.0
|
mode
|
('half-even', 'half-up', 'floor', 'ceil', 'trunc')
|
How a coordinate between two grid lines is resolved. |
"half-even"
|
drop_duplicates
|
bool
|
Remove points that a coarse grid has landed on top of their immediate neighbour, and then any stroke left with fewer than two points. On by default, because those are zero-length segments: ink a plotter cannot draw and a pen-down/pen-up it should not spend the time on. Turn it off to keep the point count exactly as it was, which is what a caller feeding a fixed-size buffer or a per-point parallel array needs. |
True
|
Returns:
| Type | Description |
|---|---|
Design
|
Snapped, with each surviving stroke's style following it across. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from geomotif import Design, Path
>>> square = Design((Path(((0.4, 0.4), (9.6, 0.4), (9.6, 9.6))),))
>>> list(snap(square))
[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]
>>> list(snap(square, 0.25))
[(0.5, 0.5), (9.5, 0.5), (9.5, 9.5)]
Source code in src/geomotif/core/transform.py
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 | |
symmetry_group
¶
Apply a full cyclic Cn or dihedral Dn symmetry group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
The fundamental domain to replicate. |
required |
group
|
str
|
|
required |
Source code in src/geomotif/core/transform.py
tile
¶
Repeat design on a rectangular lattice.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
The unit cell contents. |
required |
cols
|
int
|
Lattice size. Both must be >= 1. |
required |
rows
|
int
|
Lattice size. Both must be >= 1. |
required |
dx
|
float
|
Spacing between columns and rows. |
required |
dy
|
float
|
Spacing between columns and rows. |
required |
stagger
|
float
|
Fraction of |
0.0
|
Source code in src/geomotif/core/transform.py
from_spec
¶
from_spec(data: Mapping[str, object]) -> Motif
Rebuild the motif a spec describes.
The version stamp is not consulted; a spec is data, and data that loaded once should keep loading.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the mapping names no motif, or names a value type this library will not import. |
KeyError
|
If the motif name is not registered -- including when it belongs to a plugin that is not installed. |
Source code in src/geomotif/io/spec.py
load_design
¶
load_design(path: str | PathLike[str]) -> Design
Read a design back from a JSON file written by :func:save_design.
A plain JSON array of pairs -- what :func:save_points writes -- also
loads, as a design of loose points with no strokes. The two shapes are an
array and an object, so there is nothing to guess at.
Returns:
| Type | Description |
|---|---|
Design
|
With |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the file is not one of the two shapes above. |
Source code in src/geomotif/io/points.py
load_spec
¶
load_spec(path: str | PathLike[str]) -> Motif
Read a spec file and return the motif it describes.
Returns:
| Type | Description |
|---|---|
Motif
|
Ready to :meth: |
Source code in src/geomotif/io/spec.py
save_design
¶
save_design(design: Design, path: str | PathLike[str], *, fmt: PointFormat | None = None, precision: int | None = None, meta: bool = True) -> Path
Write a design, strokes kept apart, and return the path written.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
What to write. |
required |
path
|
str or path - like
|
Destination file. |
required |
fmt
|
('csv', 'txt', 'json')
|
Output format, inferred from the suffix when omitted.
|
"csv"
|
precision
|
int
|
Round coordinates to this many decimal places, as for
:func: |
None
|
meta
|
bool
|
Record the design's recipe alongside its points, for the JSON format only. Turn it off for a design whose motif takes a parameter that cannot be written as data. |
True
|
Returns:
| Type | Description |
|---|---|
Path
|
The file that was written. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Source code in src/geomotif/io/points.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
save_dxf
¶
save_dxf(design: Design, path: str | PathLike[str], *, layer: str = '0', precision: int = 4) -> Path
Write a design to a DXF file and return the path written.
See :func:to_dxf for what the options mean.
Source code in src/geomotif/io/dxf.py
save_gif
¶
save_gif(frames: Sequence[Design], path: str | PathLike[str], **kwargs: Any) -> Path
Write an animated GIF and return the path written.
Keyword arguments are passed straight through to :func:to_gif.
Source code in src/geomotif/io/gif.py
save_jpeg
¶
Render a design -- or re-encode a raster -- as JPEG and write it.
Keyword arguments are passed straight through to :func:to_jpeg.
Source code in src/geomotif/io/jpeg.py
save_plotter_svg
¶
save_plotter_svg(design: Design, path: str | PathLike[str], **kwargs: Any) -> Path
Write a plotter-ready SVG and return the path written.
Keyword arguments are passed straight through to :func:to_plotter_svg.
Source code in src/geomotif/io/plotter.py
save_png
¶
Render a design -- or re-encode a raster -- as a PNG and write it.
Keyword arguments are passed straight through to :func:to_png.
Source code in src/geomotif/io/png.py
save_points
¶
save_points(points: Iterable[Point], path: str | PathLike[str], *, fmt: PointFormat | None = None, precision: int | None = None) -> Path
Write points to a file and return the path written.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
iterable of (float, float)
|
The points to export. A :class: |
required |
path
|
str or path - like
|
Destination file. |
required |
fmt
|
('csv', 'txt', 'json')
|
Output format. Inferred from the file suffix when omitted
(
|
"csv"
|
precision
|
int
|
Round coordinates to this many decimal places. This rounds the file rather than the design, so it says nothing about
what the other writers do with the same points.
:meth: |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
The file that was written. |
Source code in src/geomotif/io/points.py
save_spec
¶
save_spec(source: SupportsBuild | Design, path: str | PathLike[str], *, indent: int | None = 2) -> Path
Write a motif's recipe to a JSON file and return the path written.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Motif or Design
|
What to describe; see :func: |
required |
path
|
str or path - like
|
Destination file. |
required |
indent
|
int
|
Passed through to :func: |
2
|
Returns:
| Type | Description |
|---|---|
Path
|
The file that was written. |
Source code in src/geomotif/io/spec.py
save_svg
¶
save_svg(design: Design, path: str | PathLike[str], **kwargs: Any) -> Path
Write a design to an SVG file and return the path written.
Keyword arguments are passed straight through to :func:to_svg.
Source code in src/geomotif/io/svg.py
to_dxf
¶
to_dxf(design: Design, *, layer: str = '0', precision: int = 4) -> str
Render a design as a DXF R12 document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
What to write. Strokes become |
required |
layer
|
str
|
Layer for geometry that does not name one of its own. |
'0'
|
precision
|
int
|
Decimal places for coordinates. |
4
|
Returns:
| Type | Description |
|---|---|
str
|
A complete DXF R12 document. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the design is empty, the precision is negative, or a layer name -- the argument's or a style's -- is not one R12 permits. |
Source code in src/geomotif/io/dxf.py
to_gif
¶
to_gif(frames: Sequence[Design], *, width: int = 480, height: int = 480, padding: float = 8.0, fps: float = 20.0, loop: int = 0, ink: str = '#0b0b0b', background: str = '#ffffff', thickness: int = 1, dot_radius: int | None = None, antialias: bool = False, aa_level: int = 8, dither: bool = True, transparent: bool = False) -> bytes
Render a sequence of designs as an animated GIF.
Every frame is drawn against the same world rectangle -- the union of all of their bounds -- and the same color table, so a drawing that grows stays put instead of swimming about as its own extent changes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frames
|
sequence of Design
|
What to draw, in order. At least one is needed. |
required |
width
|
int
|
Canvas size in pixels. |
480
|
height
|
int
|
Canvas size in pixels. |
480
|
padding
|
float
|
Margin reserved on all four sides, in pixels. |
8.0
|
fps
|
float
|
Frames per second. GIF stores a delay in hundredths of a second, so the rate is rounded to what the format can actually say. |
20.0
|
loop
|
int
|
How many times to play; |
0
|
ink
|
str
|
Default stroke color and the color behind everything. A stroke with a style of its own is drawn in that instead. |
'#0b0b0b'
|
background
|
str
|
Default stroke color and the color behind everything. A stroke with a style of its own is drawn in that instead. |
'#0b0b0b'
|
thickness
|
int
|
Stroke width in pixels. |
1
|
dot_radius
|
int
|
Radius for loose points. Defaults to |
None
|
antialias
|
bool
|
Supersample and blend edges. Off by default, so the output is exactly the hard-edged picture it has always been. |
False
|
aa_level
|
int
|
When antialiasing, how many shades an edge may blend into per color pair, which is what keeps the whole animation inside GIF's 256-color budget. Must be >= 1. |
8
|
dither
|
bool
|
Error-diffuse the round-off onto a gradient, which keeps an antialiased edge on a colored ground from banding inside the palette budget. On by default. |
True
|
transparent
|
bool
|
Leave the background empty. Index 0 is flagged transparent in the file, so the drawing sits over whatever the page shows instead of a painted ground. Off by default, so the picture stays what it has always been. |
False
|
Returns:
| Type | Description |
|---|---|
bytes
|
A complete GIF89a file. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If there are no frames, the rate is not positive, the antialias level is zero, or the designs' own colors (the inks and background) exceed 256 between them -- which is GIF's limit, not this writer's. |
Source code in src/geomotif/io/gif.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | |
to_jpeg
¶
to_jpeg(source: Design | Raster, *, width: int = 480, height: int = 480, padding: float = 8.0, ink: str = '#0b0b0b', background: str = '#ffffff', thickness: int = 1, dot_radius: int | None = None, antialias: bool = False, aa_level: int = 8, quality: int = 85) -> bytes
Render a design -- or re-encode a :class:~geomotif.io.Raster -- as JPEG.
A still is drawn once, exactly the way :func:to_png draws a frame, and
then encoded with baseline JPEG: 4:2:0 chroma subsampling, an 8x8 DCT on
each block, quality-scaled quantization, and Huffman coding against the
reference tables. The styling parameters are shared with the PNG and GIF
writers so one summoning controls all three.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Design or Raster
|
What to write. A design is drawn; a raster is encoded as it is. |
required |
width
|
int
|
Canvas size in pixels. Ignored when |
480
|
height
|
int
|
Canvas size in pixels. Ignored when |
480
|
padding
|
float
|
Margin reserved on all four sides, in pixels. |
8.0
|
ink
|
str
|
Default stroke color and the color behind everything. |
'#0b0b0b'
|
background
|
str
|
Default stroke color and the color behind everything. |
'#0b0b0b'
|
thickness
|
int
|
Stroke width in pixels. |
1
|
dot_radius
|
int
|
Radius for loose points. Defaults to |
None
|
antialias
|
bool
|
Supersample and blend edges. Off by default, so the edges are the same hard Bresenham edges they have always been. |
False
|
aa_level
|
int
|
How many shades an antialiased edge may blend into. A JPEG keeps full color anyway, so this only bounds the drawing, not the encode. |
8
|
quality
|
int
|
From 0 (smallest, most loss) to 100 (closest to the original). This is what the quantization tables are scaled by. |
85
|
Returns:
| Type | Description |
|---|---|
bytes
|
A complete baseline JPEG file. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/geomotif/io/jpeg.py
to_plotter_svg
¶
to_plotter_svg(design: Design, *, paper: str = 'a4', margin: float = 10.0, landscape: bool = False, stroke_width: float = DEFAULT_PEN, **kwargs: Any) -> str
Render a design as an SVG measured in real millimeters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
What to plot. |
required |
paper
|
str
|
A name from :data: |
'a4'
|
margin
|
float
|
Border to leave unplotted, in millimeters. Worth more than you would think: most plotters cannot reach the last few millimeters of a sheet. |
10.0
|
landscape
|
bool
|
Turn the paper on its side. |
False
|
stroke_width
|
float
|
Pen width in millimeters. Only affects how the file looks -- a plotter draws with the pen it has -- but getting it right makes the preview honest. |
DEFAULT_PEN
|
**kwargs
|
Any
|
Passed to :func: |
{}
|
Returns:
| Type | Description |
|---|---|
str
|
An SVG whose |
Source code in src/geomotif/io/plotter.py
to_png
¶
to_png(source: Design | Raster, *, width: int = 480, height: int = 480, padding: float = 8.0, ink: str = '#0b0b0b', background: str = '#ffffff', thickness: int = 1, dot_radius: int | None = None, antialias: bool = False, aa_level: int = 8, color: str = 'rgb', compression: int = 6, transparent: bool = False) -> bytes
Render a design -- or re-encode a :class:~geomotif.io.Raster -- as PNG.
A still is drawn once, exactly the way :func:to_gif draws a frame, and
then written as a PNG instead of being quantized toward a GIF's colors.
The styling parameters are shared with the GIF writer so one summoning
controls both.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Design or Raster
|
What to write. A design is drawn; a raster is encoded as it is. |
required |
width
|
int
|
Canvas size in pixels. Ignored when |
480
|
height
|
int
|
Canvas size in pixels. Ignored when |
480
|
padding
|
float
|
Margin reserved on all four sides, in pixels. |
8.0
|
ink
|
str
|
Default stroke color and the color behind everything. |
'#0b0b0b'
|
background
|
str
|
Default stroke color and the color behind everything. |
'#0b0b0b'
|
thickness
|
int
|
Stroke width in pixels. |
1
|
dot_radius
|
int
|
Radius for loose points. Defaults to |
None
|
antialias
|
bool
|
Supersample and blend edges. Off by default, so the edges are the same hard Bresenham edges they have always been. |
False
|
aa_level
|
int
|
When antialiasing into an indexed frame, how many shades an edge may blend into per color pair. Ignored for the truecolor paths, which keep every level. |
8
|
color
|
str
|
|
'rgb'
|
compression
|
int
|
zlib level from 0 (fast, big) to 9 (slow, small). Must be an integer in that range. |
6
|
transparent
|
bool
|
Leave the background empty instead of painting |
False
|
Returns:
| Type | Description |
|---|---|
bytes
|
A complete PNG file. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/geomotif/io/png.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
to_spec
¶
to_spec(source: SupportsBuild | Design, *, animation: Mapping[str, object] | None = None) -> dict[str, object]
Return the JSON-ready recipe for a motif, or for the design it built.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Motif or Design
|
A motif, or any design whose :attr: |
required |
animation
|
mapping
|
An animation recipe to carry alongside the still, so a moving picture
round-trips through the same file the CLI's |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
TypeError
|
If a parameter cannot be written as data -- see the module docstring. |
Examples:
Source code in src/geomotif/io/spec.py
to_svg
¶
to_svg(design: Design, *, width: float | None = None, height: float | None = None, padding: float = 8.0, stroke: str = '#0b0b0b', stroke_width: float = 1.0, fill: str = 'none', background: str | None = None, dot_radius: float | None = None, flip_y: bool = True, precision: int = 3, group_by_path: bool = True, title: str | None = None, units: str = '') -> str
Render a design as an SVG document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
What to draw. Its strokes become |
required |
width
|
float
|
Canvas size in user units. Give both to fit the design into exactly
that rectangle; give one and the other follows from the design's own
proportions; give neither and the design keeps its own measurements,
with |
None
|
height
|
float
|
Canvas size in user units. Give both to fit the design into exactly
that rectangle; give one and the other follows from the design's own
proportions; give neither and the design keeps its own measurements,
with |
None
|
padding
|
float
|
Margin reserved on all four sides. |
8.0
|
stroke
|
(str, float, str)
|
Applied to the group holding the strokes. |
'#0b0b0b'
|
stroke_width
|
(str, float, str)
|
Applied to the group holding the strokes. |
'#0b0b0b'
|
fill
|
(str, float, str)
|
Applied to the group holding the strokes. |
'#0b0b0b'
|
background
|
str
|
Draw a filled rectangle behind everything. Omitted by default, which leaves the canvas transparent. |
None
|
dot_radius
|
float
|
Radius for the loose points. Defaults to |
None
|
flip_y
|
bool
|
Mirror vertically, so a design drawn y-up appears the right way up in SVG's y-down space. On by default. |
True
|
precision
|
int
|
Decimal places for coordinates. Trailing zeros are dropped, so a whole number costs one character rather than five. |
3
|
group_by_path
|
bool
|
Give every stroke its own |
True
|
title
|
str
|
The document's |
None
|
units
|
str
|
A physical unit for the document's |
''
|
Returns:
| Type | Description |
|---|---|
str
|
A complete SVG document, ending in a newline. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the design has no points, |
Source code in src/geomotif/io/svg.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | |