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, degfrom tesserax.animation import Scenefrom tesserax.color import Colors# 1. Setup the static scenewith Canvas() as canvas: rect = Square(40, fill=Colors.Orange)canvas.fit(10)# 2. Animatescene = Scene(canvas, fps=30)# We manually drive the loopfor i inrange(30): rect.rotated(deg(3)) # Rotate 3 degrees per frame scene.capture() # Snap!# 3. Renderscene.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 Circlewith 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 animationsmove_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 Shapewith 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 animationdef 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 Polylinewith 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 mathfrom tesserax import Pointwith Canvas() as c:# Create a dense line so we have points to warp line = Polyline([Point(x, 0) for x inrange(-50, 50, 2)], stroke=Colors.Blue)c.fit(15)scene = Scene(c)# Define a wave functiondef wave(p, t):# t goes 0 -> 1# We use it to shift the phase phase = t * math.pi *2 amplitude =10 freq =0.1return 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).
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 Circlewith 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 Polylinewith 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 mathwith Canvas() as c: ball = Circle(10, fill=Colors.Purple)c.fit(50)scene = Scene(c)# A custom function to move in a spiraldef 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, linearwith 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 1000.6: 20, # Stay there for 20% of time1.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 end1.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.