Playground

Verlet Physics

Wed, 01 May 2024

A custom physics engine using the Verlet integration

method. In case you're curious, here's all the code for the engine:

Vector.js

A simple class used to represent 2D vectors.

TypeScript
1export class Vector {
2  x: number;
3  y: number;
4
5  constructor(x: number, y: number) {
6    this.x = x;
7    this.y = y;
8  }
9
10  static add = (v1: Vector, v2: Vector) => new Vector(v1.x + v2.x, v1.y + v2.y);
11
12  static subtract = (v1: Vector, v2: Vector) =>
13    new Vector(v1.x - v2.x, v1.y - v2.y);
14
15  static multiply = (v: Vector, scalar: number) =>
16    new Vector(v.x * scalar, v.y * scalar);
17
18  static divide = (v: Vector, scalar: number) =>
19    new Vector(v.x / scalar, v.y / scalar);
20
21  static distance = (v1: Vector, v2: Vector) =>
22    Math.sqrt((v1.x - v2.x) ** 2 + (v1.y - v2.y) ** 2);
23
24  get magnitude() {
25    return Math.sqrt(this.x ** 2 + this.y ** 2);
26  }
27
28  add = (v: Vector) => {
29    this.x += v.x;
30    this.y += v.y;
31  };
32
33  multiply = (scalar: number) => {
34    this.x *= scalar;
35    this.y *= scalar;
36  };
37}

Point.ts

This is a fundamental element in the engine. It represents a point in 2D space, with an x and y coordinate. Points can be affected by things like gravity or other forces, and they can collide with other points.

TypeScript
1import { Vector } from "./vector";
2
3interface constructorProps {
4  position: Vector;
5  radius?: number;
6  solid?: boolean;
7  color?: string;
8}
9
10export class Point {
11  public oldPosition: Vector;
12  public position: Vector;
13  public solid: boolean = false;
14  public acceleration: Vector;
15  public bounce: number;
16
17  // Visual properties
18  public color: string;
19  public radius: number;
20
21  constructor({ position, radius, solid, color }: constructorProps) {
22    this.position = position;
23    this.oldPosition = position;
24    this.solid = solid || false;
25
26    this.acceleration = new Vector(0, 0);
27    this.bounce = 0.9;
28
29    this.color = color || "#fff";
30    this.radius = radius || 5;
31  }
32
33  applyForce = (force: Vector) => {
34    if (this.solid) return;
35
36    this.acceleration = Vector.add(this.acceleration, force);
37  };
38
39  update = ({ dt, friction }: { dt: number; friction: number }) => {
40    if (this.solid) return;
41
42    const currentVelocity = Vector.subtract(this.position, this.oldPosition);
43    currentVelocity.multiply(friction);
44    this.oldPosition = this.position;
45    this.position = Vector.add(this.position, currentVelocity);
46    this.position.add(Vector.multiply(this.acceleration, dt * dt));
47    this.acceleration = new Vector(0, 0);
48  };
49}

Constraint.ts

It's a relationship between two points, a constraint keeps two points separated by a precise distance.

TypeScript
1import { Vector } from "./vector";
2import { Point } from "./point";
3
4interface constructorProps {
5  start: Point;
6  end: Point;
7  length: number;
8  color?: string;
9  thickness?: number;
10}
11
12export class Constraint {
13  public start: Point;
14  public end: Point;
15  public length: number;
16
17  // Visual properties
18  public color: string;
19  public thickness: number;
20
21  constructor({ start, end, length, color, thickness }: constructorProps) {
22    this.start = start;
23    this.end = end;
24    this.length = length;
25
26    this.color = color || "#fff";
27    this.thickness = thickness || 1;
28  }
29
30  solve() {
31    const axis = Vector.subtract(this.start.position, this.end.position);
32    const length = axis.magnitude;
33
34    const normal = Vector.divide(axis, length);
35    const delta = this.length - length;
36
37    const correction = Vector.multiply(normal, delta * 0.5);
38    if (!this.start.solid)
39      this.start.position = Vector.add(this.start.position, correction);
40    if (!this.end.solid)
41      this.end.position = Vector.subtract(this.end.position, correction);
42  }
43}

Solver.ts

This is where the magic happens. The solver is in charge of figuring out how each of the points should move over time taking into account the different variables of the system (forces, constraints, etc.).

TypeScript
1import { Vector } from "./vector";
2import { Point } from "./point";
3import { Constraint } from "./constraint";
4
5interface constructorProps {
6  gravity: Vector;
7  dimensions: Vector;
8}
9
10export class VerletSolver {
11  public points: Point[];
12  public constraints: Constraint[];
13  public gravity: Vector;
14  public dimensions: Vector;
15  public friction: number = 0.999;
16
17  constructor({ gravity, dimensions }: constructorProps) {
18    this.points = [];
19    this.constraints = [];
20
21    this.dimensions = dimensions;
22    this.gravity = gravity || new Vector(0, 64);
23  }
24
25  addPoint(point: Point) {
26    this.points.push(point);
27  }
28
29  addConstraint(constraint: Constraint) {
30    this.constraints.push(constraint);
31  }
32
33  update(dt: number) {
34    this.applyGravity();
35
36    this.updatePoints(dt);
37
38    for (let i = 0; i < 3; i++) {
39      this.applyConstraints();
40    }
41
42    this.solveCollisions();
43  }
44
45  applyGravity() {
46    for (const point of this.points) {
47      point.applyForce(this.gravity);
48    }
49  }
50
51  updatePoints(dt: number) {
52    for (const point of this.points) {
53      point.update({ dt, friction: this.friction });
54    }
55  }
56
57  applyConstraints() {
58    for (const p of this.points) {
59      if (!p.solid) {
60        let velocity = Vector.subtract(p.position, p.oldPosition);
61        velocity.multiply(this.friction);
62
63        const maxX = this.dimensions.x - p.radius;
64        const minX = 0 + p.radius;
65        const maxY = this.dimensions.y - p.radius;
66        const minY = 0 + p.radius;
67
68        if (p.position.x > maxX) {
69          p.position.x = maxX;
70          p.oldPosition.x = p.position.x + velocity.x * p.bounce;
71        } else if (p.position.x < minX) {
72          p.position.x = minX;
73          p.oldPosition.x = p.position.x + velocity.x * p.bounce;
74        }
75
76        if (p.position.y > maxY) {
77          p.position.y = maxY;
78          p.oldPosition.y = p.position.y + velocity.y * p.bounce;
79        } else if (p.position.y < minY) {
80          p.position.y = minY;
81          p.oldPosition.y = p.position.y + velocity.y * p.bounce;
82        }
83      }
84    }
85
86    for (const constraint of this.constraints) {
87      constraint.solve();
88    }
89  }
90
91  solveCollisions() {
92    for (const p1 of this.points) {
93      for (const p2 of this.points) {
94        if (p1 === p2) continue;
95
96        const distance = Vector.distance(p1.position, p2.position);
97        const minDistance = p1.radius + p2.radius;
98
99        if (distance < minDistance) {
100          const collisionAxis = Vector.subtract(p1.position, p2.position);
101          const normal = Vector.divide(collisionAxis, distance);
102          const delta = minDistance - distance;
103          const correction = Vector.multiply(normal, delta * 0.5);
104          if (!p1.solid) p1.position = Vector.add(p1.position, correction);
105          if (!p2.solid) p2.position = Vector.subtract(p2.position, correction);
106        }
107      }
108    }
109  }
110}

Vittorio Retrivi

Overview
Let’s Get In Touch-Let’s Get In Touch-Let’s Get In Touch-Let’s Get In Touch-Let’s Get In Touch-Let’s Get In Touch-Let’s Get In Touch-Let’s Get In Touch-Let’s Get In Touch-Let’s Get In Touch-Let’s Get In Touch-Let’s Get In Touch-