geomotif.animate
¶
Frames: the same design over time, or the same motif over a parameter.
A still image says what a design is. An animation says how it is made, which for most of this catalog is the more interesting half -- watching a Hilbert curve fill its square, or a rose's petal count climb, tells you something the finished picture cannot.
Each function here returns a plain tuple of :class:~geomotif.Design s, one
per frame, so they compose with everything else: transform them, restyle them,
export one as SVG, or hand the lot to
:func:~geomotif.io.gif.save_gif::
from geomotif.animate import draw_on, keyframes, spin, sweep
from geomotif.io.gif import save_gif
from geomotif.motifs import HilbertCurve, Rose
save_gif(draw_on(HilbertCurve(depth=5).build(), frames=60), "hilbert.gif")
save_gif(spin(Rose(n=5).build(), frames=48), "rose.gif")
save_gif(sweep(Rose(), "n", range(2, 12)), "petals.gif")
save_gif(keyframes(Rose(), {"n": [(0.0, 3), (1.0, 9)]}, frames=48), "grow.gif")
Nothing here is expensive: a frame is the same geometry seen differently, not
the motif built again -- except in :func:sweep and :func:keyframes, where
building it again is precisely the point.
:func:keyframes animates several parameters at once across arbitrary time
points, and is what the 1.3.0 web explorer's animation editor is built on;
:func:compose chains the post-passes (:func:draw_on, :func:spin) onto a
run of frames so a single --animation flag reproduces whatever the web
app produced.
Functions:
| Name | Description |
|---|---|
draw_on |
Return frames revealing a design progressively, as a pen would draw it. |
spin |
Return frames of a design turning about a point. |
sweep |
Return one frame per value of one of a motif's parameters. |
keyframes |
Return one design per frame, with named parameters eased across keyframes. |
compose |
Chain motion post-passes onto a run of frames. |
draw_on_overlay |
Return a motion that reveals each frame progressively, in step with the timeline. |
spin_overlay |
Return a motion that turns each frame in step with the timeline. |
draw_on
¶
draw_on(design: Design, frames: int = 48, *, trail: float | None = None, hold: int = 0) -> tuple[Design, ...]
Return frames revealing a design progressively, as a pen would draw it.
Progress is measured in arc length, not in vertices, so the pen moves at a constant speed rather than racing through the sparse parts of the geometry and crawling through the dense ones. Strokes are drawn in the order the design holds them, and loose points appear in step with the strokes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
What to draw. |
required |
frames
|
int
|
How many frames to return, before |
48
|
trail
|
float
|
Draw only the last |
None
|
hold
|
int
|
Extra copies of the finished drawing to append, so an animation that loops pauses on the result instead of restarting the instant it arrives. |
0
|
Returns:
| Type | Description |
|---|---|
tuple of Design
|
|
Source code in src/geomotif/animate.py
spin
¶
spin(design: Design, frames: int = 36, *, turns: float = 1.0, about: Point | None = None, hold: int = 0) -> tuple[Design, ...]
Return frames of a design turning about a point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
What to turn. |
required |
frames
|
int
|
How many frames make up the whole rotation. Must be >= 1. |
36
|
turns
|
float
|
Revolutions across the whole animation. Negative turns clockwise. |
1.0
|
about
|
(float, float)
|
center of rotation. Defaults to the middle of the design's bounds, which is what keeps it inside the canvas. |
None
|
hold
|
int
|
Extra copies of the finished drawing to append, so an animation that loops pauses on the result instead of restarting the instant it arrives. |
0
|
Returns:
| Type | Description |
|---|---|
tuple of Design
|
|
Source code in src/geomotif/animate.py
sweep
¶
sweep(motif: SupportsBuild, parameter: str, values: Iterable[object]) -> tuple[Design, ...]
Return one frame per value of one of a motif's parameters.
The motif is rebuilt for each value, which is the only way to animate a parameter -- and cheap enough, since the whole catalog is built rather than loaded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
motif
|
Motif
|
A dataclass motif, which every builtin one is. |
required |
parameter
|
str
|
Which parameter to vary. |
required |
values
|
iterable
|
What to set it to, in order. |
required |
Returns:
| Type | Description |
|---|---|
tuple of Design
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If the motif is not a dataclass, so there is nothing to vary by name. |
ValueError
|
If it has no such parameter. The message lists the ones it does have. |
Examples:
Source code in src/geomotif/animate.py
keyframes
¶
keyframes(motif: SupportsBuild, tracks: Mapping[str, object], *, frames: int = 48, fps: float = 20.0, hold: int = 0, easing: str = 'linear') -> tuple[Design, ...]
Return one design per frame, with named parameters eased across keyframes.
Where :func:sweep varies a single parameter across a list of values,
:func:keyframes varies several at once, each across its own time points.
It is the primitive the 1.3.0 web explorer's animation editor is built on,
so an animation a user shares from the browser reproduces in the CLI
byte-for-byte.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
motif
|
Motif
|
A dataclass motif, which every builtin one is. The motif is rebuilt for each frame with that frame's interpolated parameters. |
required |
tracks
|
mapping of str to track
|
One entry per parameter to animate. A track is either a sequence of
|
required |
frames
|
int
|
How many frames to return, before |
48
|
fps
|
float
|
Recorded for the spec round-trip; it does not change the frames
produced. The GIF writer's frame rate comes from the spec (or the
CLI's |
20.0
|
hold
|
int
|
Extra copies of the finished frame to append, so a looping animation pauses on the result instead of restarting the instant it arrives. |
0
|
easing
|
str
|
Top-level (playback) interpolation curve applied as a final layer on
top of every track's keyframe program: the frame time is warped by
this curve before each track's per-keyframe easing runs. One of
|
'linear'
|
Returns:
| Type | Description |
|---|---|
tuple of Design
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If the motif is not a dataclass, so there is nothing to vary by name. |
ValueError
|
If a track names a parameter the motif does not have, a keyframe time
is outside |
Examples:
>>> from geomotif.animate import keyframes
>>> from geomotif.motifs import Rose
>>> len(keyframes(Rose(), {"n": [(0.0, 3), (1.0, 9)]}, frames=6))
6
Source code in src/geomotif/animate.py
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 337 338 339 340 341 342 343 344 345 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 | |
compose
¶
compose(motions: Iterable[Callable[[tuple[Design, ...]], tuple[Design, ...]]], frames: tuple[Design, ...]) -> tuple[Design, ...]
Chain motion post-passes onto a run of frames.
:func:keyframes only animates motif parameters. The existing
:func:draw_on and :func:spin operate on a built design and are applied
as post-passes -- one frame at a time, in step with the timeline -- so a
Hilbert curve can draw itself on while its depth sweeps from 3 to 6.
:func:draw_on_overlay and :func:spin_overlay adapt them to a whole run
of frames; this helper chains any number of such motions in order, so the
CLI's --animation flag is a single spec rather than a tangle of nested
flags.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
motions
|
iterable of callables
|
Each |
required |
frames
|
tuple of Design
|
The run to transform, typically the result of :func: |
required |
Returns:
| Type | Description |
|---|---|
tuple of Design
|
The run after every motion has been applied in turn. |
Source code in src/geomotif/animate.py
draw_on_overlay
¶
draw_on_overlay(*, trail: float | None = None) -> Callable[[tuple[Design, ...]], tuple[Design, ...]]
Return a motion that reveals each frame progressively, in step with the timeline.
Frame i of n shows the first (i + 1) / n of that frame's arc
length -- a pen drawing alongside the parameter sweep, so a Hilbert curve
can grow from depth 3 to 6 while it draws itself on. trail passes
straight through to the same per-path reveal :func:draw_on uses.
Source code in src/geomotif/animate.py
spin_overlay
¶
spin_overlay(*, turns: float = 1.0, about: Point | None = None) -> Callable[[tuple[Design, ...]], tuple[Design, ...]]
Return a motion that turns each frame in step with the timeline.
Frame i of n is rotated by turns * i / (n - 1) revolutions, so
the final frame has completed exactly turns revolutions (fractions
stay fractional: the last frame is a full turns turn on from the
first, which is back at the starting orientation whenever turns is a
whole number, and partway around otherwise). about defaults to each
frame's own center, which is what keeps it on the canvas; give a point to
rotate about a fixed one instead.