Created
April 24, 2026 12:50
-
-
Save luizbills/5388d79c9b419b3bc1eefeebace57aec to your computer and use it in GitHub Desktop.
Basic physics simulation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //! Based on https://slicker.me/javascript/physics/physics_engine.htm | |
| // Vector utilities | |
| class Vec { | |
| constructor(x=0, y=0) { | |
| this.x = x; | |
| this.y = y; | |
| } | |
| add(v) { | |
| return new Vec(this.x + v.x,this.y + v.y); | |
| } | |
| sub(v) { | |
| return new Vec(this.x - v.x,this.y - v.y); | |
| } | |
| mul(s) { | |
| return new Vec(this.x * s,this.y * s); | |
| } | |
| dot(v) { | |
| return this.x * v.x + this.y * v.y; | |
| } | |
| len() { | |
| return Math.hypot(this.x, this.y); | |
| } | |
| norm() { | |
| let l = this.len() || 1; | |
| return this.mul(1 / l); | |
| } | |
| } | |
| // Circle body | |
| class Circle { | |
| constructor(x, y, r, m=1) { | |
| this.pos = new Vec(x,y); | |
| this.vel = new Vec(0,0); | |
| this.r = r; | |
| this.m = m; | |
| this.invM = 1 / m; | |
| } | |
| applyImpulse(j) { | |
| this.vel = this.vel.add(j.mul(this.invM)); | |
| } | |
| integrate(dt) { | |
| let gravity = new Vec(0,300); | |
| this.vel = this.vel.add(gravity.mul(dt)); | |
| this.pos = this.pos.add(this.vel.mul(dt)); | |
| } | |
| } | |
| // Line segment | |
| class Line { | |
| constructor(x1, y1, x2, y2) { | |
| this.a = new Vec(x1,y1); | |
| this.b = new Vec(x2,y2); | |
| } | |
| closestPoint(p) { | |
| let ab = this.b.sub(this.a); | |
| let t = p.sub(this.a).dot(ab) / ab.dot(ab); | |
| t = Math.max(0, Math.min(1, t)); | |
| return this.a.add(ab.mul(t)); | |
| } | |
| } | |
| // Collision and scene | |
| function resolveCircleLine(circle, line) { | |
| let cp = line.closestPoint(circle.pos); | |
| let diff = circle.pos.sub(cp); | |
| let dist = diff.len(); | |
| if (dist < circle.r) { | |
| let n = diff.norm(); | |
| let penetration = circle.r - dist; | |
| circle.pos = circle.pos.add(n.mul(penetration + 0.01)); | |
| let vn = circle.vel.dot(n); | |
| if (vn < 0) { | |
| let e = 0.8; | |
| circle.vel = circle.vel.sub(n.mul((1 + e) * vn)); | |
| } | |
| } | |
| } | |
| function resolveCircleCircle(a, b) { | |
| let diff = b.pos.sub(a.pos); | |
| let d = diff.len(); | |
| if (d === 0) { | |
| return; | |
| } | |
| if (d < a.r + b.r) { | |
| let n = diff.mul(1 / d); | |
| let penetration = a.r + b.r - d; | |
| let totalInv = a.invM + b.invM; | |
| a.pos = a.pos.add(n.mul(-penetration * (a.invM / totalInv))); | |
| b.pos = b.pos.add(n.mul(penetration * (b.invM / totalInv))); | |
| let rel = b.vel.sub(a.vel); | |
| let vn = rel.dot(n); | |
| if (vn > 0) { | |
| return; | |
| } | |
| let e = 0.8; | |
| let j = -(1 + e) * vn / totalInv; | |
| let impulse = n.mul(j); | |
| a.applyImpulse(impulse.mul(-1)); | |
| b.applyImpulse(impulse); | |
| } | |
| } | |
| let counter = 0; | |
| let circles = [new Circle(250,150,20,1), new Circle(510,120,16,1), new Circle(170,120,10,1)]; | |
| let lines = []; | |
| const lineCount = 16; | |
| for (let i = 0; i < lineCount; i++) { | |
| lines.push(new Line(100,100,200,200)); | |
| } | |
| function step(dt) { | |
| for (let c of circles) { | |
| c.integrate(dt); | |
| } | |
| for (let i = 0; i < circles.length; i++) { | |
| for (let j = i + 1; j < circles.length; j++) { | |
| resolveCircleCircle(circles[i], circles[j]); | |
| } | |
| } | |
| for (let c of circles) { | |
| for (let l of lines) { | |
| resolveCircleLine(c, l); | |
| } | |
| } | |
| } | |
| function draw() { | |
| cls(0) | |
| linewidth(4) | |
| for (let l of lines) { | |
| line(l.a.x, l.a.y, l.b.x, l.b.y, 2) | |
| } | |
| for (let c of circles) { | |
| circfill(c.pos.x, c.pos.y, c.r, 3) | |
| } | |
| } | |
| const armLength = 70; | |
| const speed = 0.009; | |
| function update(dt) { | |
| step(dt); | |
| // Spinning crosses | |
| for (let i = 0; i < lineCount / 2; i++) { | |
| lines[i * 2].a.x = 100 * i + armLength * Math.sin(counter); | |
| lines[i * 2].a.y = 350 + armLength * Math.cos(counter); | |
| lines[i * 2].b.x = 100 * i - armLength * Math.sin(counter); | |
| lines[i * 2].b.y = 350 - armLength * Math.cos(counter); | |
| lines[i * 2 + 1].a.x = 100 * i - armLength * Math.cos(counter); | |
| lines[i * 2 + 1].a.y = 350 + armLength * Math.sin(counter); | |
| lines[i * 2 + 1].b.x = 100 * i + armLength * Math.cos(counter); | |
| lines[i * 2 + 1].b.y = 350 - armLength * Math.sin(counter); | |
| } | |
| counter = counter + speed; | |
| } | |
| function tapped(x, y) { | |
| circles.push(new Circle(x, y, 20, 1)); | |
| } | |
| litecanvas({ | |
| width: 800, | |
| height: 600, | |
| autoscale: false | |
| }) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Live Demo