# 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 `Field`s (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.

```{python}
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 `Body`s and place them in a `World`. Once set up, we bake the physics animation with `world.simulate(...)`.

```{python}
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.

```{python}
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 `Spring`s (soft constraints) and `Rod`s (hard constraints). These work by applying forces and corrections to bodies during simulation.

```{python}
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()
```