Animation & Video

Tesserax isn’t just for static images. It includes a lightweight, code-first animation engine capable of exporting GIFs and MP4s.

Note: To use animation features, ensure you installed Tesserax with the export dependencies: pip install tesserax[export].

The Scene Object

The Scene is the director of your animation. It wraps a Canvas and manages the render loop, frame capture, and file export.

At its lowest level, you can animate simply by changing shapes in a loop and calling scene.capture(). This gives you total control over every frame.

Code
from tesserax import Canvas, Square, deg
from tesserax.animation import Scene
from tesserax.color import Colors

# 1. Setup the static scene
with Canvas() as canvas:
    rect = Square(40, fill=Colors.Orange)

canvas.fit(10)

# 2. Animate
scene = Scene(canvas, fps=30)

# We manually drive the loop
for i in range(30):
    rect.rotated(deg(3)) # Rotate 3 degrees per frame
    scene.capture()      # Snap!

# 3. Render
scene.display()

Declarative Animations

While manual loops are powerful, they get messy quickly. Tesserax provides a declarative API where you define what happens, not how.

The Scene.play() method accepts one or more Animation objects.

  • Parallel: Arguments passed to play(a, b) run simultaneously.
  • Sequential: Animations added with a + b run one after another.

We use the .animate property on shapes to quickly generate these objects.

Code
from tesserax import Circle

with Canvas() as canvas:
    box = Square(30, fill=Colors.LightBlue).translated(-40, 0)
    ball = Circle(15, fill=Colors.Salmon).translated(40, 0)

canvas.fit(10)
scene = Scene(canvas, fps=30)

# Define animations
move_box = box.animate.rotate(deg(90)) | box.animate.translate(20)
fade_ball = ball.animate.scale(0.5)

# Run them together (box moves sequentially, ball scales in parallel)
scene.play(move_box.looping(), fade_ball.looping(), duration=2.0)

scene.display()

Animation Modifiers

You can tweak the timing and behavior of any animation using fluent modifiers:

  • .reversed(): Plays backwards.
  • .looping(): Plays forward then backward (yoyo).
  • .repeating(n): Repeats n times within the duration.
  • .delayed(t): Waits for t (0.0 to 1.0) before starting.
  • .smoothed(): Applies an ease-in-out curve (default is linear).
Code
from tesserax import Shape

with Canvas() as c:
    b1 = Circle(10, fill=Colors.Red).translated(-30, 0)
    b2 = Circle(10, fill=Colors.Blue).translated(0, 0)
    b3 = Circle(10, fill=Colors.Green).translated(30, 0)

c.fit(40)
scene = Scene(c)

# Create a jump animation
def jump(shape: Shape):
    return (
        shape.animate.translate(0, -40).smoothed() |
        shape.animate.translate(0, 0).smoothed()
    )

scene.play(
    jump(b1),
    jump(b2).delayed(0.2), # Start 20% later
    jump(b3).delayed(0.4), # Start 40% later
    duration=1.5
)

scene.display()

Morphing and Warping

For Polyline shapes, Tesserax offers advanced vertex manipulation.

Morphing

The .morph(target) animation smoothly interpolates the points of one shape into another.

Code
from tesserax import Polyline

with Canvas() as c:
    # Start as a Triangle
    shape = Polyline.poly(3, 40, fill=Colors.Gold).subdivide()
    # Target is a Hexagon
    target = Polyline.poly(6, 40).hide()

c.fit(10)
scene = Scene(c)

scene.play(
    shape.animate.morph(target).smoothed().looping(),
    duration=2.0
)

scene.display()

Warping

The .warp(func) animation allows you to apply a function to every point in a shape over time. This is perfect for wave effects.

Code
import math
from tesserax import Point

with Canvas() as c:
    # Create a dense line so we have points to warp
    line = Polyline([Point(x, 0) for x in range(-50, 50, 2)], stroke=Colors.Blue)

c.fit(15)
scene = Scene(c)

# Define a wave function
def wave(p, t):
    # t goes 0 -> 1
    # We use it to shift the phase
    phase = t * math.pi * 2
    amplitude = 10
    freq = 0.1
    return Point(p.x, math.sin(p.x * freq + phase) * amplitude)

scene.play(
    line.animate.warp(wave).repeating(2),
    duration=2.0
)

canvas.fit(10)
scene.display()

Text Effects

Text objects have their own special animator with effects like write (typewriter style) and scramble (hacker style).

Code
from tesserax import Text

with Canvas() as c:
    t1 = Text("Hello World", size=24).translated(0, -20)
    t2 = Text("Encryption", size=24, fill=Colors.Green, font="monospace").translated(0, 20)

c.fit(10)
scene = Scene(c)

scene.play(
    t1.animate.write(),
    t2.animate.scramble(),
    duration=2.0
)
scene.wait(1.0)

scene.display()

Animating Arbitrary Properties

Sometimes you need to animate a property that Tesserax doesn’t have a built-in method for, like the radius of a circle, the gap of a layout, or a custom attribute you added to a subclass.

The property Method

The animator.property(name, value) method allows you to interpolate any numeric attribute on the shape from its current value to a target value.

Code
from tesserax import Circle

with Canvas() as c:
    # A circle starts with radius 10
    ball = Circle(10, fill=Colors.Red)

c.fit(15)
scene = Scene(c)

scene.play(
    # Explicitly animate the 'r' attribute (radius) to 50
    ball.animate.property("r", 20).looping(),
    duration=1.0
)
scene.display()

Magic Property Access

For even cleaner code, the animator supports dynamic property access. If you try to call a method on .animate that doesn’t exist (e.g., .radius(...)), Tesserax assumes you want to animate the property of that name.

And since any property can be animated (as long as the underlying value supports scalar arithmetics), you can funny things like animating the smoothness of a Polyline.

Code
from tesserax import Polyline

with Canvas() as c:
    # Square defined by 'size'
    line = Polyline.poly(5, 50)

c.fit(10)
scene = Scene(c)

scene.play(
    line.animate.width(5).looping(),
    line.animate.smoothness(1).looping(),
    line.animate.rotate(deg(360/5)).smoothed(),
    duration=1.0
)
scene.display()

Custom Animation Logic

When standard interpolation isn’t enough, the .custom() method gives you a direct hook into the animation loop. You provide a callback function that receives the shape and the time (from 0 to 1), and you can do whatever you want.

This is perfect for complex math, interdependent properties, or non-linear behaviors. The fun part is that you add .looping() or .smoothed() or any other standard Animation modifier even to these custom animations.

Code
import math

with Canvas() as c:
    ball = Circle(10, fill=Colors.Purple)

c.fit(50)
scene = Scene(c)

# A custom function to move in a spiral
def spiral(shape, t):
    # t goes 0 -> 1
    angle = t * math.pi * 4  # 2 full turns
    radius = t * 50          # Spiral out to 50px

    # Update position manually
    shape.transform.tx = math.cos(angle) * radius
    shape.transform.ty = math.sin(angle) * radius

    # We can also change other properties!
    shape.fill = Colors.Purple.lerp(Colors.Yellow, t)

scene.play(
    ball.animate.custom(spiral).smoothed().looping(),
    duration=3.0
)

scene.display()

Keyframe Animation

For complex motion graphics where an object needs to follow a specific “script” (e.g., “move right, wait, rotate, then shrink”), chaining simple animations becomes tedious.

The .keyframes() method allows you to define a timeline for multiple properties simultaneously in a single call.

The Timeline API

You pass properties as keyword arguments. Each argument accepts a dictionary mapping a normalized time (\(0.0 \to 1.0\)) to a target value.

  • If \(0.0\) is missing, Tesserax infers it from the current state.
  • Tesserax automatically detects if the property belongs to the Shape (like fill) or its Transform (like tx, rotation).
Code
from tesserax.animation import ease_out, ease_in_out_cubic, linear

with Canvas() as c:
    box = Square(30, fill=Colors.Orange)

c.fit(50)
scene = Scene(c)

scene.play(
    box.animate.keyframes(
        # Track 1: Movement (Transform property)
        tx={
            0.0: -20,
            0.4: 20, # Move fast to 100
            0.6: 20, # Stay there for 20% of time
            1.0: -20,    # Return home
        },
        # Track 2: Rotation (Transform property)
        # We can use (value, easing) tuples for precise control
        rotation={
            0.0: 0,
            1.0: (deg(360), ease_in_out_cubic) # Spin fully with smooth accel/decel
        },
        # Track 3: Color (Shape property)
        fill={
            0.5: Colors.Orange,
            0.8: Colors.Red, # Turn red near the end
            1.0: Colors.Orange
        }
    ),
    duration=3.0
)

scene.display()

By default, Tesserax interpolates linearly between keyframes. However, you often want specific segments to feel different (e.g., a “snap” effect).

You can pass a tuple (value, easing_function) instead of just a raw value. The easing function applies to the time interval leading up to that keyframe.