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
Bodyhandle. If radius isNoneit 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
Bodyor(x, y)point.
- add_repulsion(target, strength=50000, max_dist=500)¶
Add repulsion away from a
Bodyor(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.inffor static bodies.restitution (float) – Bounciness coefficient (0–1).
friction (float) – Friction coefficient applied during collisions.
radius (float) – Collision radius (auto-detected from
Circle/DotifNone).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
Bodyinstances 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)