Physics-based Animations

Tesserax includes a built-in, deterministic, baked physics engine. Unlike real-time game engines that calculate physics during the render loop, Tesserax calculates the entire simulation upfront and “bakes” it into standard KeyframeAnimation tracks.

This architecture offers two major advantages:

  1. Precision: You can simulate at extremely high resolutions (e.g., 1000 steps per second) for accuracy, while playing back at a standard 30 or 60 FPS.
  2. Predictability: The resulting animation is a standard Tesserax object that can be paused, reversed, looped, or composed with other animations.

The Basic

A high-level overview of the workflow is as follows:

  1. Create a World.
  2. Add Shape objects to the world (creates Body wrappers).
  3. Add Fields (like Gravity) and global constraints.
  4. Call world.simulate(duration) to generate an Animation.
  5. Play the animation in your Scene.

Here is a complete example of a ball falling under gravity and bouncing on a static floor. First we will create the scene.

Code
from tesserax import Canvas, Circle, Rect

# 1. Setup the Scene
with Canvas() as canvas:
    floor = Rect(100, 20, fill="#333").translated(0, 200)
    ball = Circle(20, fill="#ff0055").translated(00, 0)

Next, we define the physical world. This involves wrapping the existing shapes that will undergo physical simulation as Bodys and place them in a World. Once set up, we bake the physics animation with world.simulate(...).

Code
from tesserax.physics import World, Gravity, Material, CircleCollider

world = World()
world.fields.append(Gravity())  # Downward acceleration (pixels/s^2)
world.add(floor, static=True, material=Material(restitution=0.8))
world.add(
    ball,
    material=Material(restitution=0.8, friction=0.5),
    collider=CircleCollider(ball.r),
)

simulation = world.simulate(duration=5.0)

Finally, we run the animation as normal. Notice we can simulate at some fixed framerate and render at a completely different rate. The property simulation.bounds has a precomputed approximate bounding box of the entire simulation so we can fit to it.

Code
from tesserax.animation import Scene

canvas.fit(10, bounds=simulation.bounds)
scene = Scene(canvas)
scene.play(simulation, duration=5.0)
scene.display()

Constraints

Tesserax physics engine also supports some simple constraints like Springs (soft constraints) and Rods (hard constraints). These work by applying forces and corrections to bodies during simulation.

Code
from tesserax import Canvas, Rect, Circle, Line, Point
from tesserax.animation import Scene
from tesserax.physics import World, Gravity, Material
from tesserax.physics.constraints import Spring, Rod

with Canvas() as canvas:
    r1 = Rect(20, 20, fill="steelblue").translated(50, 0)
    ball = Circle(15, fill="orange").translated(100, 0)
    r2 = Rect(20, 20, fill="green").translated(150, 0)

    spring_line = Line(
        lambda: r1.anchor("center"),
        lambda: ball.anchor("center"),
        stroke="gray",
        width=3,
    )
    rod_line = Line(
        lambda: ball.anchor("center"),
        lambda: r2.anchor("center"),
        stroke="gray",
        width=3,
    )

world = World()
world.fields.append(Gravity())

body_r1 = world.add(r1)
body_ball = world.add(
    ball,
    material=Material(restitution=0.8),
    collider=CircleCollider(15),
    static=True,
)
body_r2 = world.add(r2)
body_r2.angular_vel = 5.0  # Radians/sec

# Physics Constraint
world.constraint(Spring(body_r1, body_ball, length=30, k=50))
world.constraint(Spring(body_ball, body_r2, length=30, k=50))

# 3. Bake
simulation = world.simulate(duration=5.0)
canvas.fit(10, bounds=simulation.bounds)

# 4. Play
scene = Scene(canvas)
scene.play(simulation, duration=5.0)
scene.display()