A custom physics engine using the Verlet integration method. In case you're curious, here's all the code for the engine:
A simple class used to represent 2D vectors.
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}
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.
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}
It's a relationship between two points, a constraint keeps two points separated by a precise distance.
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}
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.).
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}