Skip to content

Animation & Rendering

Animations add motion to entities. Renderers convert scenes (with or without animations) into output formats.

See also

For a tutorial introduction, see Animation.


Entity Animation Methods

All entities inherit these methods from the base Entity class. Each returns self for chaining.

Universal Methods

animate_fade class-attribute instance-attribute

animate_fade = animate_fade

animate_move

animate_move(to: RelCoordLike | None = None, *, by: RelCoordLike | None = None, duration: float = 1.0, delay: float = 0.0, easing: EasingLike = 'ease-in-out', hold: bool = True, repeat: RepeatLike = False, bounce: bool = False) -> Entity

Animate position using relative coordinates.

Two modes:

  • Absolute: entity.animate_move(to=(0.8, 0.5))
  • Relative: entity.animate_move(by=(0.1, 0))

Parameters:

Name Type Description Default
to RelCoordLike | None

Target position as RelCoord or (rx, ry) tuple. Mutually exclusive with by.

None
by RelCoordLike | None

Movement offset as RelCoord or (drx, dry) tuple. Mutually exclusive with to.

None
duration float

Duration in seconds.

1.0
delay float

Seconds before animation starts.

0.0
easing EasingLike

Speed curve (default "ease-in-out" for natural motion).

'ease-in-out'
hold bool

Hold final value after completion.

True
repeat RepeatLike

False = play once (default), True = loop forever, int = play N times.

False
bounce bool

If True, alternate direction each cycle.

False

Returns:

Type Description
Entity

Self, for method chaining.

Raises:

Type Description
ValueError

If both to and by are given, or neither.

animate_spin

animate_spin(angle: float = 360, *, duration: float = 1.0, delay: float = 0.0, easing: EasingLike = 'linear', hold: bool = True, repeat: RepeatLike = False, bounce: bool = False, pivot: RelCoordLike | None = None) -> Entity

Animate rotation.

Parameters:

Name Type Description Default
angle float

Total rotation in degrees (default 360 = full turn).

360
duration float

Duration in seconds.

1.0
delay float

Seconds before animation starts.

0.0
easing EasingLike

Speed curve (default "linear" for constant rotation).

'linear'
hold bool

Hold final value after completion.

True
repeat RepeatLike

False = play once (default), True = loop forever, int = play N times.

False
bounce bool

If True, alternate direction each cycle.

False
pivot RelCoordLike | None

Custom rotation center as (rx, ry) surface-relative fractions. Defaults to the entity's natural rotation_center. Useful for orbits (pivot=sun_pos) or spinning from an end point. Does not follow the entity if it is also being moved.

None

Returns:

Type Description
Entity

Self, for method chaining.

animate_scale

animate_scale(to: float, *, duration: float = 1.0, delay: float = 0.0, easing: EasingLike = 'ease-in-out', hold: bool = True, repeat: RepeatLike = False, bounce: bool = False, pivot: RelCoordLike | None = None) -> Entity

Animate scale factor.

Parameters:

Name Type Description Default
to float

Target scale factor (1.0 = original, 2.0 = double, 0.5 = half).

required
duration float

Duration in seconds.

1.0
delay float

Seconds before animation starts.

0.0
easing EasingLike

Speed curve (default "ease-in-out").

'ease-in-out'
hold bool

Hold final value after completion.

True
repeat RepeatLike

False = play once (default), True = loop forever, int = play N times.

False
bounce bool

If True, alternate direction each cycle.

False
pivot RelCoordLike | None

Custom scale origin as (rx, ry) surface-relative fractions. Defaults to the entity's natural rotation_center. Does not follow the entity if it is also being moved.

None

Returns:

Type Description
Entity

Self, for method chaining.

animate_follow

animate_follow(path: FullPathable, *, duration: float = 1.0, delay: float = 0.0, easing: EasingLike = 'linear', hold: bool = True, rotate: bool | float = False, repeat: RepeatLike = False, bounce: bool = False) -> Entity

Animate along a path.

Parameters:

Name Type Description Default
path FullPathable

A Pathable object (Wave, Spiral, etc.) to follow.

required
duration float

Duration in seconds.

1.0
delay float

Seconds before animation starts.

0.0
easing EasingLike

Speed curve.

'linear'
hold bool

Hold final value after completion.

True
rotate bool | float

True for auto-rotation along tangent, float for fixed angle.

False
repeat RepeatLike

False = play once (default), True = loop forever, int = play N times.

False
bounce bool

If True, alternate direction each cycle.

False

Returns:

Type Description
Entity

Self, for method chaining.

animate

animate(prop: str, *, to: Any | None = None, keyframes: dict[float, Any] | list[Any] | None = None, duration: float = 1.0, delay: float = 0.0, easing: EasingLike = 'linear', hold: bool = True, repeat: RepeatLike = False, bounce: bool = False) -> Entity

Animate any property.

Two modes:

  • Simple: entity.animate("opacity", to=0.0, duration=2.0)
  • Keyframes (dict): entity.animate("opacity", keyframes={0: 1, 1: 0.3, 2: 1})
  • Keyframes (list): entity.animate("fill", keyframes=["red", "blue", "red"], duration=2.0)

Parameters:

Name Type Description Default
prop str

Property name (e.g., "opacity", "r", "fill", "width").

required
to Any | None

Target value (simple mode).

None
keyframes dict[float, Any] | list[Any] | None

Dict of {time_seconds: value}, or list of values (evenly spaced over duration).

None
duration float

Duration for simple mode, or total duration when keyframes is a list.

1.0
delay float

Seconds before animation starts.

0.0
easing EasingLike

Speed curve.

'linear'
hold bool

Hold final value after completion.

True
repeat RepeatLike

False = play once (default), True = loop forever, int = play N times.

False
bounce bool

If True, alternate direction each cycle.

False

Returns:

Type Description
Entity

Self, for method chaining.

Raises:

Type Description
ValueError

If neither to nor keyframes is provided.

then

then(gap: float = 0) -> Entity

Chain: start the next animation after all current ones finish.

Computes when all current animations end, and offsets the next animation call by that amount (plus an optional gap).

Parameters:

Name Type Description Default
gap float

Optional pause in seconds between the end of current animations and the start of the next one.

0

Returns:

Type Description
Entity

Self, for method chaining.

Example::

dot.animate_fade(to=0.0, duration=1.0).then().animate_spin(360, duration=1.0)
dot.animate_fade(to=0.0, duration=1.0).then(0.5).animate_spin(360, duration=1.0)
# Bounce the whole chain with .loop():
dot.animate_fade(to=0.3, duration=1.5).then().animate_spin(360, duration=2.0)
dot.loop(bounce=True)

loop

loop(*, bounce: bool = False, times: RepeatLike = True) -> None

Loop all animations on this entity.

Terminal method — returns None. Call this as the last step after building your animation chain, not in the middle.

Stamps bounce and repeat onto every animation currently on the entity. Its primary use case is finishing a .then() chain so the whole sequence repeats as one unit::

# Natural fit — the chain [move → fade] loops together:
dot.animate_move(...).then().animate_fade(...).loop(bounce=True)

For a single animation, prefer passing repeat= and bounce= directly to the animate_* call — it is self-contained and impossible to accidentally apply to the wrong animation::

# Preferred:
dot.animate_fade(to=0.0, duration=1.0, repeat=True, bounce=True)

# Works but unnecessary indirection:
dot.animate_fade(to=0.0, duration=1.0).loop(bounce=True)

.. warning:: If an entity has multiple animations with different desired settings, use per-animation repeat=/bounce= rather than .loop(). Because .loop() stamps every animation on the entity, it can silently apply bounce to an animation you didn't intend — for example, reversing an animate_follow while only meaning to bounce an animate_radius.

Parameters:

Name Type Description Default
bounce bool

If True, alternate direction each cycle (forward → backward → forward ...). Default False.

False
times RepeatLike

True = loop forever (default); int = play N times.

True

Raises:

Type Description
ValueError

If times is False or a negative integer.

ValueError

If no animations have been added yet.

Note

For bounced finite loops (bounce=True, times=N), the value held after completion depends on parity: odd N freezes at the end value, even N freezes at the start value (the last bounce cycle reverses back). Only relevant when hold=True (the animate_* default).

clear_animations

clear_animations() -> Entity

Remove all animations from this entity.

Returns:

Type Description
Entity

Self, for method chaining.

animations property

animations: list

Current animations on this entity (copy).

Typed Property Methods

Each entity subclass exposes only the animate_* methods that match its constructor parameters. These are factory-generated with typed signatures — IDE autocomplete shows exactly what's available.

Color propertiesto accepts ColorLike (str | tuple[int, int, int]), keyframes accepts dict[float, ColorLike] or list[ColorLike]:

Method Dot Rect Polygon Ellipse Line/Curve Path Text
animate_color Yes Yes Yes Yes
animate_fill Yes Yes Yes Yes
animate_stroke Yes Yes Yes

Numeric propertiesto accepts float, keyframes accepts dict[float, float] or list[float]:

Method Dot Rect Polygon Ellipse Line/Curve Path Text
animate_radius Yes
animate_width Yes Yes Yes
animate_height Yes
animate_stroke_width Yes Yes Yes
animate_rx Yes
animate_ry Yes
animate_font_size Yes
animate_fill_opacity Yes Yes Yes
animate_stroke_opacity Yes Yes Yes

Stroke Draw

Lines, curves, and paths have a stroke-reveal animation method:

animate_draw

animate_draw(*, duration: float = 1.0, delay: float = 0.0, easing: EasingLike = 'ease-in-out', hold: bool = True, reverse: bool = False, repeat: RepeatLike = False, bounce: bool = False) -> Line

Animate the line drawing itself from start to end.

The stroke progressively reveals as if being drawn by a pen.

Parameters:

Name Type Description Default
duration float

Animation duration in seconds.

1.0
delay float

Seconds to wait before starting.

0.0
easing EasingLike

Easing function name or (x1,y1,x2,y2) tuple.

'ease-in-out'
hold bool

Hold final value after animation ends.

True
reverse bool

Draw from end to start instead.

False
repeat RepeatLike

False = play once (default), True = loop forever, int = play N times.

False
bounce bool

If True, alternate direction each cycle.

False

Returns:

Type Description
Line

self, for method chaining.

animate_draw

animate_draw(*, duration: float = 1.0, delay: float = 0.0, easing: EasingLike = 'ease-in-out', hold: bool = True, reverse: bool = False, repeat: RepeatLike = False, bounce: bool = False) -> Curve

Animate the curve drawing itself from start to end.

The stroke progressively reveals as if being drawn by a pen.

Parameters:

Name Type Description Default
duration float

Animation duration in seconds.

1.0
delay float

Seconds to wait before starting.

0.0
easing EasingLike

Easing function name or (x1,y1,x2,y2) tuple.

'ease-in-out'
hold bool

Hold final value after animation ends.

True
reverse bool

Draw from end to start instead.

False
repeat RepeatLike

False = play once (default), True = loop forever, int = play N times.

False
bounce bool

If True, alternate direction each cycle.

False

Returns:

Type Description
Curve

self, for method chaining.

animate_draw

animate_draw(*, duration: float = 1.0, delay: float = 0.0, easing: EasingLike = 'ease-in-out', hold: bool = True, reverse: bool = False, repeat: RepeatLike = False, bounce: bool = False) -> Path

Animate the path drawing itself from start to end.

The stroke progressively reveals as if being drawn by a pen.

Parameters:

Name Type Description Default
duration float

Animation duration in seconds.

1.0
delay float

Seconds to wait before starting.

0.0
easing EasingLike

Easing function name or (x1,y1,x2,y2) tuple.

'ease-in-out'
hold bool

Hold final value after animation ends.

True
reverse bool

Draw from end to start instead.

False
repeat RepeatLike

False = play once (default), True = loop forever, int = play N times.

False
bounce bool

If True, alternate direction each cycle.

False

Returns:

Type Description
Path

self, for method chaining.


Connection Animation Methods

Connections support a subset of animation methods:

animate_fade class-attribute instance-attribute

animate_fade = animate_fade

animate_draw

animate_draw(*, duration: float = 1.0, delay: float = 0.0, easing: EasingLike = 'ease-in-out', hold: bool = True, reverse: bool = False, repeat: RepeatLike = False, bounce: bool = False) -> Connection

Animate the connection drawing itself.

The stroke progressively reveals as if being drawn by a pen.

Parameters:

Name Type Description Default
duration float

Duration in seconds.

1.0
delay float

Seconds before animation starts.

0.0
easing EasingLike

Speed curve (default "ease-in-out").

'ease-in-out'
hold bool

Hold final value after completion.

True
reverse bool

If True, draw from end to start.

False
repeat RepeatLike

False = play once, True = loop forever, int = play N times.

False
bounce bool

If True, alternate direction each cycle.

False

Returns:

Type Description
Connection

Self, for method chaining.

animate

animate(prop: str, *, to: Any | None = None, keyframes: dict[float, Any] | list[Any] | None = None, duration: float = 1.0, delay: float = 0.0, easing: EasingLike = 'linear', hold: bool = True, repeat: RepeatLike = False, bounce: bool = False) -> Connection

Animate any property.

then

then(gap: float = 0) -> Connection

Chain: start the next animation after all current ones finish.

Parameters:

Name Type Description Default
gap float

Optional pause in seconds after current animations end.

0

Returns:

Type Description
Connection

Self, for method chaining.

loop

loop(*, bounce: bool = False, times: RepeatLike = True) -> None

Loop all animations on this connection.

Terminal method — returns None. Call this as the last step after building your animation chain, not in the middle.

Stamps bounce and repeat onto every animation on the connection. Its primary use case is finishing a .then() chain::

conn.animate_fade(to=0.2).then().animate_draw(duration=1.5).loop()

For a single animation, prefer inline repeat=/bounce= parameters instead::

# Preferred:
conn.animate_draw(duration=2.0, repeat=True, bounce=True)

Parameters:

Name Type Description Default
bounce bool

If True, alternate direction each cycle. Default False.

False
times RepeatLike

True = loop forever (default); int = play N times.

True

Raises:

Type Description
ValueError

If times is False or a negative integer.

ValueError

If no animations have been added yet.

clear_animations

clear_animations() -> Connection

Remove all animations.


Utility Functions

stagger

stagger(*entities: Entity | Connection, offset: float = 0.1, each: Callable[[Entity | Connection], Entity | Connection]) -> list[Entity | Connection]

Apply an animation to entities with staggered timing.

Calls each(entity) for every entity, then adjusts the delay of newly added animations by i * offset seconds.

Parameters:

Name Type Description Default
*entities Entity | Connection

Entities (or connections) to animate.

()
offset float

Seconds between each entity's animation start.

0.1
each Callable[[Entity | Connection], Entity | Connection]

Callable that applies animation(s) to an entity.

required

Returns:

Type Description
list[Entity | Connection]

List of entities (same objects, with animations applied).

Example::

stagger(*dots, offset=0.2, each=lambda d: d.animate_fade(to=0.0, duration=1.0))

Animation Data Model

These classes live in pyfreeform.animation and represent animation data. You rarely need to create them directly — the entity methods do it for you. They're useful for inspection and custom renderers.

bounce / repeat fields

The bounce and repeat fields on animation model classes are storage — they are stamped by .loop(), not passed directly to animate_* methods.

Easing dataclass

Easing(x1: float = 0.0, y1: float = 0.0, x2: float = 1.0, y2: float = 1.0)

Cubic-bezier easing function.

Defines the speed curve of an animation using four control points of a cubic Bezier curve (x1, y1, x2, y2). The curve maps input time (0–1) to output progress (0–1).

Predefined easings are available as class attributes::

Easing.LINEAR       # constant speed
Easing.EASE_IN      # slow start
Easing.EASE_OUT     # slow end
Easing.EASE_IN_OUT  # slow start and end

Custom easing via constructor::

Easing(0.68, -0.55, 0.27, 1.55)  # back-overshoot

x1 class-attribute instance-attribute

x1: float = 0.0

y1 class-attribute instance-attribute

y1: float = 0.0

x2 class-attribute instance-attribute

x2: float = 1.0

y2 class-attribute instance-attribute

y2: float = 1.0

LINEAR class-attribute

LINEAR: Easing

EASE_IN class-attribute

EASE_IN: Easing

EASE_OUT class-attribute

EASE_OUT: Easing

EASE_IN_OUT class-attribute

EASE_IN_OUT: Easing

evaluate

evaluate(t: float) -> float

Map input time (0–1) to eased progress (0–1).

Uses De Casteljau subdivision to solve the cubic-bezier curve.

Keyframe dataclass

Keyframe(time: float, value: Any)

A single value at a point in time.

Attributes:

Name Type Description
time float

Time in seconds.

value Any

The property value at this time.

PropertyAnimation dataclass

PropertyAnimation(prop: str, keyframes: list[Keyframe] = list(), easing: Easing = (lambda: LINEAR)(), hold: bool = True, repeat: RepeatLike = False, bounce: bool = False, delay: float = 0.0, pivot: RelCoord | None = None, chain_id: int | None = None, chain_seq: int = 0)

Animation of a single entity property over time.

Stores the property name (pyfreeform name, NOT SVG attribute), keyframes, and playback parameters. The renderer decides how to output this — SVG SMIL emits <animate>, a game renderer calls evaluate(t).

Attributes:

Name Type Description
prop str

Property name (e.g., "opacity", "r", "fill").

keyframes list[Keyframe]

Ordered list of (time, value) pairs.

easing Easing

Speed curve between keyframes.

hold bool

If True, hold final value after animation ends.

repeat RepeatLike

False=once, True=forever, int=N times.

bounce bool

If True, alternate direction each cycle.

delay float

Seconds before animation starts.

pivot RelCoord | None

Custom transform origin for rotation/scale animations. Surface-relative RelCoord. None = use entity's natural center.

chain_id int | None

Shared ID for all animations in one .then() sequence. None = independent animation (default behaviour).

chain_seq int

Position within the chain (0, 1, 2, …).

prop instance-attribute

prop: str

keyframes class-attribute instance-attribute

keyframes: list[Keyframe] = field(default_factory=list)

easing class-attribute instance-attribute

easing: Easing = field(default_factory=lambda: LINEAR)

hold class-attribute instance-attribute

hold: bool = True

repeat class-attribute instance-attribute

repeat: RepeatLike = False

bounce class-attribute instance-attribute

bounce: bool = False

delay class-attribute instance-attribute

delay: float = 0.0

duration property

duration: float

Duration from first to last keyframe (seconds).

evaluate

evaluate(t: float) -> Any

Compute the interpolated value at time t (seconds).

Handles delay, repeat, bounce, and easing. Works for numeric values (float, int) and color strings (interpolated in RGB).

Parameters:

Name Type Description Default
t float

Absolute time in seconds since animation start.

required

Returns:

Type Description
Any

Interpolated property value at time t.

MotionAnimation dataclass

MotionAnimation(path: FullPathable, duration: float, easing: Easing = (lambda: LINEAR)(), hold: bool = True, repeat: RepeatLike = False, bounce: bool = False, delay: float = 0.0, rotate: bool | float = False, reverse: bool = False, chain_id: int | None = None, chain_seq: int = 0)

Animation that moves an entity along a path.

The path is stored as a Pathable object. The renderer extracts geometry as needed — SVG SMIL uses <animateMotion>, a game renderer calls path.point_at(t).

Attributes:

Name Type Description
path FullPathable

The path to follow.

duration float

Total duration in seconds.

easing Easing

Speed curve along the path.

hold bool

If True, hold final position.

repeat RepeatLike

False=once, True=forever, int=N times.

bounce bool

If True, alternate direction each cycle.

delay float

Seconds before animation starts.

rotate bool | float

True for auto-rotation along tangent, float for fixed angle.

reverse bool

If True, traverse the path backwards (end → start instead of start → end). Used internally by the chain renderer to produce the backward pass of a bounced .then() chain.

chain_id int | None

Shared ID for all animations in one .then() sequence. None = independent animation (default behaviour).

chain_seq int

Position within the chain (0, 1, 2, …).

path instance-attribute

path: FullPathable

duration instance-attribute

duration: float

easing class-attribute instance-attribute

easing: Easing = field(default_factory=lambda: LINEAR)

hold class-attribute instance-attribute

hold: bool = True

repeat class-attribute instance-attribute

repeat: RepeatLike = False

bounce class-attribute instance-attribute

bounce: bool = False

delay class-attribute instance-attribute

delay: float = 0.0

rotate class-attribute instance-attribute

rotate: bool | float = False

evaluate

evaluate(t: float) -> tuple[float, float]

Compute the (x, y) position at time t.

Parameters:

Name Type Description Default
t float

Absolute time in seconds.

required

Returns:

Type Description
tuple[float, float]

(x, y) position on the path.

DrawAnimation dataclass

DrawAnimation(duration: float, easing: Easing = (lambda: EASE_IN_OUT)(), hold: bool = True, repeat: RepeatLike = False, bounce: bool = False, delay: float = 0.0, reverse: bool = False, chain_id: int | None = None, chain_seq: int = 0)

Animation that draws a path/connection progressively.

Uses stroke-dashoffset technique: the path appears to be drawn from start to end (or end to start if reversed).

Attributes:

Name Type Description
duration float

Total duration in seconds.

easing Easing

Speed curve of the drawing.

hold bool

If True, hold final state (path fully drawn).

repeat RepeatLike

False=once, True=forever, int=N times.

bounce bool

If True, alternate draw/erase each cycle.

delay float

Seconds before animation starts.

reverse bool

If True, draw from end to start.

chain_id int | None

Shared ID for all animations in one .then() sequence.

chain_seq int

Position within the chain (0, 1, 2, …).

duration instance-attribute

duration: float

easing class-attribute instance-attribute

easing: Easing = field(default_factory=lambda: EASE_IN_OUT)

hold class-attribute instance-attribute

hold: bool = True

repeat class-attribute instance-attribute

repeat: RepeatLike = False

bounce class-attribute instance-attribute

bounce: bool = False

delay class-attribute instance-attribute

delay: float = 0.0

reverse class-attribute instance-attribute

reverse: bool = False

evaluate

evaluate(t: float) -> float

Compute the draw progress (0.0 = hidden, 1.0 = fully drawn).

Parameters:

Name Type Description Default
t float

Absolute time in seconds.

required

Returns:

Type Description
float

Progress from 0.0 to 1.0.


Renderers

Renderers convert a scene into an output format. The base class defines the interface; concrete renderers implement it.

Renderer

Bases: ABC

Abstract base class for PyFreeform renderers.

A renderer converts a Scene (model) into an output string. Subclasses implement type-specific rendering for each entity kind.

The default render_entity dispatches to render_<typename> methods (e.g., render_dot, render_rect) based on the entity's class name.

render_scene abstractmethod

render_scene(scene: Scene) -> str

Render a complete scene to output string.

render_entity

render_entity(entity: Entity) -> str

Dispatch to type-specific render method.

Looks up render_<classname> (lowercase) on this renderer.

render_connection abstractmethod

render_connection(conn: Connection) -> str

Render a connection to output string.

SVGRenderer

Bases: Renderer

Renders PyFreeform scenes as static SVG.

This renderer produces output identical to the original inline to_svg() methods. It ignores any animations on entities — use :class:SMILRenderer for animated SVG output.

render_scene

render_scene(scene: Scene) -> str

Render a complete SVG document.

render_connection

render_connection(conn: Connection) -> str

Render Connection as SVG <line> or <path>.

SMILRenderer

Bases: SVGRenderer

SVG renderer with SMIL animation support.

Extends :class:SVGRenderer. For entities with no animations, produces identical output. For entities with animations, wraps SVG elements with <animate>, <animateTransform>, and <animateMotion> children.

render_scene

render_scene(scene: Scene) -> str

Render a complete animated SVG with fill-layer batching.

Extends the parent renderer with a pre-scan pass that detects entities sharing the same fill animation timing. Overlays from those entities are grouped into shared <g> elements with a single <animate>, reducing the total number of SMIL animation elements the browser must evaluate.

render_connection

render_connection(conn: Connection) -> str

Renderer Selection

Scenario Renderer Notes
scene.save("out.svg") SMILRenderer (auto) Default — includes animations if present
scene.render() SMILRenderer (auto) Same as save
scene.render(SVGRenderer()) SVGRenderer Forces static SVG, ignores animations
scene.render(SMILRenderer()) SMILRenderer Explicit animated SVG

Non-animated entities produce identical output from both renderers.