Physics Engine

Pre-computed 2D physics simulation that maps trajectories onto time-varying VObject attributes. Physics bodies are stepped via semi-implicit Euler integration at a configurable dt, then positions are baked as time functions so the SVG renderer can sample any frame.

Example: Basic bouncing ball

from vectormation.objects import *
from vectormation._physics import PhysicsSpace

canvas = VectorMathAnim()
canvas.set_background()

space = PhysicsSpace(gravity=(0, 980), dt=1/120)
ball = Circle(r=20, cx=960, cy=100, fill='#58C4DD')
b = space.add_body(ball, mass=1, restitution=0.7)
space.add_wall(y=900)     # floor
space.simulate(duration=5)

canvas.add_objects(ball)
canvas.browser_display(fps=60)

PhysicsSpace

class PhysicsSpace(gravity=(0, 980), dt=1 / 120, start=0.0)

The simulation container. Manages bodies, walls, springs, and forces.

Parameters:
  • gravity (tuple) – Gravity vector in px/s2. Default (0, 980) points downward in SVG coordinates.

  • dt (float) – Simulation timestep in seconds.

  • start (float) – Animation start time (offsets baked trajectories).

add_body(obj, mass=1.0, restitution=0.8, friction=0.0, radius=None, vx=0.0, vy=0.0, fixed=False, angle=0.0, angular_velocity=0.0, moment_of_inertia=None) Body

Register a VObject as a physics body and return a Body handle. If radius is None it is auto-detected from the object (e.g. Circle.r). Set fixed=True for immovable obstacles.

Parameters:
  • angle (float) – Initial rotation angle in radians.

  • angular_velocity (float) – Initial angular velocity (rad/s).

  • moment_of_inertia (float) – Rotational inertia (auto-computed if None).

add_wall(x=None, y=None, restitution=0.9, friction=1.0) Wall

Add an infinite axis-aligned wall. Specify x for a vertical wall or y for a horizontal wall.

add_walls(left=40, right=1880, top=40, bottom=1040, restitution=0.9)

Convenience method: add four walls forming a bounding box.

add_spring(a, b, stiffness=0.5, rest_length=None, damping=0.02) Spring

Add a spring constraint between two bodies, or between a body and a fixed (x, y) anchor point.

add_force(func)

Add a global force function func(body, t) -> (fx, fy) that is evaluated every timestep for every non-fixed body.

simulate(duration=5.0)

Run the simulation for duration seconds and bake trajectories onto each body’s VObject. After calling this, each VObject will have its position attributes set as time functions.

add_drag(coefficient=0.01)

Add velocity-proportional drag to all bodies.

add_attraction(target, strength=50000)

Add gravitational attraction towards a Body or (x, y) point.

add_repulsion(target, strength=50000, max_dist=500)

Add repulsion away from a Body or (x, y) point.

add_mutual_repulsion(strength=5000, max_dist=300)

Add pairwise repulsion between all bodies.


Body

class Body(obj, mass=1.0, restitution=0.8, friction=0.0, radius=None, vx=0.0, vy=0.0, fixed=False, angle=0.0, angular_velocity=0.0, moment_of_inertia=None)

A physics body wrapping a VObject. Created via PhysicsSpace.add_body().

Parameters:
  • obj (VObject) – The visual object to move.

  • mass (float) – Mass in kg. Use math.inf for static bodies.

  • restitution (float) – Bounciness coefficient (0–1).

  • friction (float) – Friction coefficient applied during collisions.

  • radius (float) – Collision radius (auto-detected from Circle/Dot if None).

  • vx (float) – Initial horizontal velocity (px/s).

  • vy (float) – Initial vertical velocity (px/s).

  • fixed (bool) – If True, the body does not move.

obj

The wrapped VObject.

x: float

Current x position (updated during simulation).

y: float

Current y position.

apply_force(fx, fy)

Accumulate a force for the current simulation step.

apply_torque(torque)

Accumulate a torque for the current simulation step.


Wall

class Wall(x=None, y=None, restitution=0.9)

An infinite axis-aligned wall for collision. Specify x for a vertical wall or y for a horizontal wall.

Parameters:
  • x (float) – X-coordinate of a vertical wall.

  • y (float) – Y-coordinate of a horizontal wall.

  • restitution (float) – Bounciness coefficient.


Spring

class Spring(a, b, stiffness=0.5, rest_length=None, damping=0.02)

A spring constraint between two Body instances or between a body and a fixed (x, y) anchor.

Parameters:
  • a – First body or (x, y) anchor.

  • b – Second body or (x, y) anchor.

  • stiffness (float) – Spring constant k (N/px).

  • rest_length (float) – Natural length in pixels (uses initial distance if None).

  • damping (float) – Damping coefficient.

Example: Spring

"""PhysicsSpace: spring pendulum with visual spring line."""
from vectormation.objects import *

v = VectorMathAnim()
v.set_background()

duration = 5

# Anchor point (fixed in space, top center)
anchor_x, anchor_y = 960, 150
anchor_dot = Dot(r=8, cx=anchor_x, cy=anchor_y, fill='#aaa', stroke='#fff', stroke_width=2)

# The bob: a colored circle attached by a spring
bob = Circle(r=28, cx=1300, cy=250,
             fill='#FF6B6B', fill_opacity=0.9,
             stroke=lighten('#FF6B6B', 0.3), stroke_width=3)

# Connecting line (spring visual) between anchor and bob
spring_line = Line(x1=anchor_x, y1=anchor_y,
                   x2=1300, y2=250,
                   stroke='#FFFF00', stroke_width=2,
                   stroke_dasharray='8 4')

# Physics space
space = PhysicsSpace(gravity=(0, 500), dt=1/120)

# Add the bob as a physics body
body = space.add_body(bob, mass=1.5, restitution=0.5, vx=-150, vy=0)

# Spring connecting the bob to the fixed anchor point
space.add_spring(
    (anchor_x, anchor_y),  # fixed anchor
    body,
    stiffness=8,
    rest_length=250,
    damping=0.3,
)

space.add_drag(coefficient=0.001)
space.add_wall(y=1000)
space.simulate(duration=duration)

# Bake the spring line endpoints to track the bob
traj = body._trajectory
n = len(traj)
dt_sim = space.dt
start_t = space.start

def bob_pos(t, _traj=traj, _start=start_t, _dt=dt_sim, _n=n):
    elapsed = t - _start
    if elapsed <= 0:
        return _traj[0]
    idx = elapsed / _dt
    i = int(idx)
    if i >= _n - 1:
        return _traj[-1]
    frac = idx - i
    x1, y1 = _traj[i]
    x2, y2 = _traj[i + 1]
    return (x1 + (x2 - x1) * frac, y1 + (y2 - y1) * frac)

spring_line.p2.set_onward(0, bob_pos)

v.add(spring_line, anchor_dot, bob)

v.show(end=duration)

Cloth

class Cloth(x=560, y=200, width=800, height=500, cols=15, rows=10, pin_top=True, stiffness=2.0, color='#58C4DD', creation=0)

A 2D cloth simulation using a grid of particles connected by springs. The top row can be pinned (fixed) to create a hanging curtain effect.

Parameters:
  • x (float) – Top-left x position.

  • y (float) – Top-left y position.

  • width (float) – Cloth width in pixels.

  • height (float) – Cloth height in pixels.

  • cols (int) – Number of columns in the particle grid.

  • rows (int) – Number of rows in the particle grid.

  • pin_top (bool) – Pin the top row of particles.

  • stiffness (float) – Spring constant for structural springs.

simulate(duration=5.0)

Run the cloth simulation and bake all particle and line trajectories.

objects()

Return a list of all VObjects (lines and dots) for adding to the canvas.

Example: Cloth

"""PhysicsSpace: cloth simulation with wind."""
import math
from vectormation.objects import *

v = VectorMathAnim()
v.set_background()

duration = 5

# Create a cloth: top row pinned, draping under gravity
cloth = Cloth(
    x=510, y=120,
    width=900, height=300,
    cols=18, rows=10,
    pin_top=True,
    stiffness=18,
    color='#58C4DD',
)

# Add a gentle sideways wind force
def wind_force(body, t):
    wind_x = 30 * math.sin(t * 1.2) + 10 * math.sin(t * 3.7)
    wind_y = 8 * math.cos(t * 2.5)
    return (wind_x, wind_y)

cloth.space.add_force(wind_force)
cloth.space.add_drag(coefficient=0.02)

cloth.simulate(duration=duration)

v.add(*cloth.objects())

v.show(end=duration)

Example: Bouncing balls

"""PhysicsSpace: bouncing objects with walls."""
import random
from vectormation.objects import *

v = VectorMathAnim()
v.set_background()

duration = 6
rng = random.Random(42)

space = PhysicsSpace(gravity=(0, 800), dt=1/120)
space.add_walls(left=60, right=1860, top=60, bottom=1020)

walls = [
    Line(x1=60, y1=1020, x2=1860, y2=1020, stroke='#555', stroke_width=2),
    Line(x1=60, y1=60, x2=1860, y2=60, stroke='#555', stroke_width=2),
    Line(x1=60, y1=60, x2=60, y2=1020, stroke='#555', stroke_width=2),
    Line(x1=1860, y1=60, x2=1860, y2=1020, stroke='#555', stroke_width=2),
]

colors = [
    '#FF6B6B', '#58C4DD', '#83C167', '#FFFF00', '#9B59B6',
    '#FF9F43', '#E74C3C', '#1ABC9C', '#E84393', '#6C5CE7',
]

objects = []
for i in range(30):
    size = rng.randint(15, 35)
    cx = rng.randint(80 + size, 1840 - size)
    cy = rng.randint(80 + size, 500)
    vx, vy = rng.randint(-300, 300), rng.randint(-400, 100)
    color = colors[i % len(colors)]
    style = dict(fill=color, fill_opacity=0.85, stroke=lighten(color, 0.3), stroke_width=2)
    obj = Circle(r=size, cx=cx, cy=cy, **style)
    space.add_body(obj, mass=(size / 20) ** 2, restitution=0.8, friction=0.02, vx=vx, vy=vy)
    objects.append(obj)

space.add_drag(coefficient=0.002)
space.simulate(duration=duration)

v.add(*walls, *objects)

v.show(end=duration)