A very simple Verlet physics engine...
Last active
May 17, 2018 19:33
-
-
Save shshaw/6db07c4c1dfc6f1b6269d3d75a63ac98 to your computer and use it in GitHub Desktop.
Simple Rope Verlet Physics
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
<canvas id="canvas" width="900" height="500"></canvas> |
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
"use strict"; | |
console.clear(); | |
const TWOPI = Math.PI * 2; | |
function distance(x1, y1, x2, y2) { | |
var dx = x1 - x2; | |
var dy = y1 - y2; | |
return Math.sqrt( dx * dx + dy * dy ); | |
} | |
const gravity = 0.5; | |
// VNode class | |
class VNode { | |
constructor(node) { | |
this.x = node.x || 0; | |
this.y = node.y || 0; | |
this.oldX = this.x; | |
this.oldY = this.y; | |
this.w = node.w || 2; | |
this.angle = node.angle || 0; | |
this.gravity = node.gravity || gravity; | |
this.mass = node.mass || 1.0; | |
this.color = node.color; | |
this.letter = node.letter; | |
this.pointerMove = node.pointerMove; | |
this.fixed = node.fixed; | |
} | |
// verlet integration | |
integrate(pointer) { | |
if (this.lock && (!this.lockX || !this.lockY)) { | |
this.lockX = this.x; | |
this.lockY = this.y; | |
} | |
if ( | |
(this.pointerMove) && pointer && | |
distance(this.x, this.y, pointer.x, pointer.y) < | |
(this.w + pointer.w) | |
) { | |
this.x += (pointer.x - this.x) / (this.mass * 18); | |
this.y += (pointer.y - this.y) / (this.mass * 18); | |
} else if (this.lock) { | |
this.x += (this.lockX - this.x) * this.lock; | |
this.y += (this.lockY - this.y) * this.lock; | |
} | |
if (!this.fixed) { | |
const x = this.x; | |
const y = this.y; | |
this.x += this.x - this.oldX; | |
this.y += this.y - this.oldY + this.gravity; | |
this.oldX = x; | |
this.oldY = y; | |
} | |
} | |
set(x, y) { | |
this.oldX = this.x = x; | |
this.oldY = this.y = y; | |
} | |
// draw node | |
draw(ctx) { | |
if (!this.color) { | |
return; | |
} | |
// ctx.globalAlpha = 0.8; | |
ctx.translate(this.x, this.y); | |
ctx.rotate(this.angle); | |
ctx.fillStyle = this.color; | |
ctx.beginPath(); | |
if (this.letter) { | |
ctx.globalAlpha = 1; | |
ctx.rotate(Math.PI / 2); | |
ctx.rect(-7, 0, 14, 1); | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
ctx.font = 'bold 75px "Bebas Neue", monospace'; | |
ctx.fillStyle = '#000'; | |
ctx.fillText(this.letter, 0, (this.w * .25) + 4); | |
ctx.fillStyle = this.color; | |
ctx.fillText(this.letter, 0, this.w * .25); | |
} else { | |
ctx.globalAlpha = 0.2; | |
ctx.rect(-this.w, -this.w, this.w * 2, this.w * 2); | |
// ctx.arc(this.x, this.y, this.w, 0, 2 * Math.PI); | |
} | |
ctx.closePath(); | |
ctx.fill(); | |
ctx.setTransform(1, 0, 0, 1, 0, 0); | |
} | |
} | |
// constraint class | |
class Constraint { | |
constructor(n0, n1, stiffness) { | |
this.n0 = n0; | |
this.n1 = n1; | |
this.dist = distance(n0.x, n0.y, n1.x, n1.y); | |
this.stiffness = stiffness || 0.5; | |
this.firstRun = true; | |
} | |
// solve constraint | |
solve() { | |
let dx = this.n0.x - this.n1.x; | |
let dy = this.n0.y - this.n1.y; | |
let newAngle = Math.atan2(dy, dx); | |
this.n1.angle = newAngle; | |
const currentDist = distance(this.n0.x, this.n0.y, this.n1.x, this.n1.y); | |
const delta = this.stiffness * (currentDist - this.dist) / currentDist; | |
dx *= delta; | |
dy *= delta; | |
if (this.firstRun) { | |
this.firstRun = false; | |
if (!this.n1.fixed) { | |
this.n1.x += dx; | |
this.n1.y += dy; | |
} | |
if (!this.n0.fixed) { | |
this.n0.x -= dx; | |
this.n0.y -= dy; | |
} | |
return; | |
} | |
let m1 = this.n0.mass + this.n1.mass; | |
let m2 = this.n0.mass / m1; | |
m1 = this.n1.mass / m1; | |
if (!this.n1.fixed) { | |
this.n1.x += dx * m2; | |
this.n1.y += dy * m2; | |
} | |
if (!this.n0.fixed) { | |
this.n0.x -= dx * m1; | |
this.n0.y -= dy * m1; | |
} | |
} | |
// draw constraint | |
draw(ctx) { | |
ctx.globalAlpha = 0.9; | |
ctx.beginPath(); | |
ctx.moveTo(this.n0.x, this.n0.y); | |
ctx.lineTo(this.n1.x, this.n1.y); | |
ctx.strokeStyle = "#fff"; | |
ctx.stroke(); | |
} | |
} | |
class Rope { | |
constructor(rope) { | |
const { | |
x, | |
y, | |
length, | |
points, | |
vertical, | |
fixedEnds, | |
startNode, | |
letter, | |
endNode, | |
stiffness, | |
constrain, | |
gravity, | |
pointerMove | |
} = rope; | |
this.stiffness = stiffness || 1; | |
this.nodes = []; | |
this.constraints = []; | |
if (letter === ' ') { | |
return this; | |
} | |
var dist = length / points; | |
for (let i = 0, last = points - 1; i < points; i++) { | |
let size = (letter && i === last ? 15 : 2); | |
let spacing = ((dist * i) + size); | |
let node = new VNode({ | |
w: size, | |
mass: .1, //(i === last ? .5 : .1), | |
fixed: fixedEnds && (i === 0 || i === last), | |
}); | |
node = ( | |
(i === 0 && startNode) || | |
(i === last && endNode) || | |
node | |
); | |
node.gravity = gravity; | |
//node.pointerMove = pointerMove; | |
if (i === last && letter) { | |
node.letter = letter; | |
node.color = '#FFF'; | |
node.pointerMove = true; | |
} | |
node.oldX = node.x = x + (!vertical ? spacing : 0); | |
node.oldY = node.y = y + (vertical ? spacing : 0); | |
this.nodes.push(node); | |
} | |
constrain ? this.makeConstraints() : null; | |
} | |
makeConstraints() { | |
for (let i = 1; i < this.nodes.length; i++) { | |
this.constraints.push( | |
new Constraint(this.nodes[i - 1], this.nodes[i], this.stiffness) | |
); | |
} | |
} | |
run(pointer) { | |
// integration | |
for (let n of this.nodes) { | |
n.integrate(pointer); | |
} | |
// solve constraints | |
for (let i = 0; i < 5; i++) { | |
for (let n of this.constraints) { | |
n.solve(); | |
} | |
} | |
} | |
// draw(ctx) { | |
// // draw constraints | |
// this.constraints.forEach(n => { | |
// n.draw(ctx); | |
// }); | |
// // draw nodes | |
// this.nodes.forEach(n => { | |
// n.draw(ctx); | |
// }) | |
// } | |
draw(ctx) { | |
let vertices = Array.from(this.constraints).reduce((p, c, i, a) => { | |
p.push(c.n0); | |
if (i == a.length - 1) p.push(c.n1); | |
return p; | |
}, []); | |
const h = (x, y) => Math.sqrt(x * x + y * y); | |
const tension = 0.5; | |
if (!vertices.length) return; | |
const controls = vertices.map(() => null); | |
for (let i = 1; i < vertices.length - 1; ++i) { | |
const previous = vertices[i - 1]; | |
const current = vertices[i]; | |
const next = vertices[i + 1]; | |
let rdx = next.x - previous.x, | |
rdy = next.y - previous.y, | |
rd = h(rdx, rdy), | |
dx = rdx / rd, | |
dy = rdy / rd; | |
let dp = h(current.x - previous.x, current.y - previous.y), | |
dn = h(current.x - next.x, current.y - next.y); | |
let cpx = current.x - dx * dp * tension, | |
cpy = current.y - dy * dp * tension, | |
cnx = current.x + dx * dn * tension, | |
cny = current.y + dy * dn * tension; | |
controls[i] = { | |
cp: { | |
x: cpx, | |
y: cpy | |
}, | |
cn: { | |
x: cnx, | |
y: cny | |
} | |
}; | |
} | |
controls[0] = { | |
cn: { | |
x: (vertices[0].x + controls[1].cp.x) / 2, | |
y: (vertices[0].y + controls[1].cp.y) / 2 | |
} | |
}; | |
controls[vertices.length - 1] = { | |
cp: { | |
x: (vertices[vertices.length - 1].x + controls[vertices.length - 2].cn.x) / 2, | |
y: (vertices[vertices.length - 1].y + controls[vertices.length - 2].cn.y) / 2 | |
} | |
}; | |
// Draw vertices & control points | |
// ctx.fillStyle = 'blue'; | |
// ctx.fillRect(vertices[0].x, vertices[0].y, 4, 4); | |
// for (let i = 1; i < vertices.length; ++i) | |
// { | |
// const v = vertices[i]; | |
// const ca = controls[i - 1]; | |
// const cb = controls[i]; | |
// ctx.fillStyle = 'blue'; | |
// ctx.fillRect(v.x, v.y, 4, 4); | |
// ctx.fillStyle = 'green'; | |
// ctx.fillRect(ca.cn.x, ca.cn.y, 4, 4); | |
// ctx.fillRect(cb.cp.x, cb.cp.y, 4, 4); | |
// } | |
ctx.globalAlpha = 0.9; | |
ctx.beginPath(); | |
ctx.moveTo(vertices[0].x, vertices[0].y); | |
for (let i = 1; i < vertices.length; ++i) { | |
const v = vertices[i]; | |
const ca = controls[i - 1]; | |
const cb = controls[i]; | |
ctx.bezierCurveTo( | |
ca.cn.x, ca.cn.y, | |
cb.cp.x, cb.cp.y, | |
v.x, v.y | |
); | |
} | |
ctx.strokeStyle = 'white'; | |
ctx.stroke(); | |
ctx.closePath(); | |
// draw nodes | |
this.nodes.forEach(n => { | |
n.draw(ctx); | |
}) | |
} | |
} | |
// Pointer class | |
class Pointer extends VNode { | |
constructor(canvas) { | |
super({ | |
x: 0, | |
y: 0, | |
w: 8, | |
color: '#F00', | |
fixed: true | |
}); | |
this.elem = canvas; | |
canvas.addEventListener("mousemove", e => this.move(e), false); | |
canvas.addEventListener("touchmove", e => this.move(e), false); | |
} | |
move(e) { | |
const touchMode = e.targetTouches; | |
let pointer = e; | |
if (touchMode) { | |
e.preventDefault(); | |
pointer = touchMode[0]; | |
} | |
var rect = this.elem.getBoundingClientRect(); | |
var cw = this.elem.width; | |
var ch = this.elem.height; | |
// get the scale based on actual width; | |
var sx = cw / this.elem.offsetWidth; | |
var sy = ch / this.elem.offsetHeight; | |
this.x = ( pointer.clientX - rect.left ) * sx; | |
this.y = ( pointer.clientY - rect.top ) * sy; | |
} | |
} | |
class Scene { | |
constructor(canvas) { | |
this.draw = true; | |
this.canvas = canvas; | |
this.ctx = canvas.getContext('2d'); | |
this.nodes = new Set(); | |
this.constraints = new Set(); | |
this.ropes = []; | |
this.pointer = new Pointer(canvas); | |
this.nodes.add(this.pointer); | |
this.run = this.run.bind(this); | |
this.addRope = this.addRope.bind(this); | |
this.add = this.add.bind(this); | |
} | |
// animation loop | |
run() { | |
// if (!canvas.isConnected) { | |
// return; | |
// } | |
requestAnimationFrame(this.run); | |
// clear screen | |
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); | |
this.ropes.forEach(rope => { | |
rope.run(this.pointer); | |
}); | |
this.ropes.forEach(rope => { | |
rope.draw(this.ctx); | |
}) | |
// // integration | |
// for (let n of nodes) { | |
// n.integrate(pointer); | |
// } | |
// solve constraints | |
// for (let i = 0; i < 4; i++) { | |
// for (let n of constraints) { | |
// n.solve(); | |
// } | |
// } | |
// // draw constraints | |
// for (let n of constraints) { | |
// n.draw(ctx); | |
// } | |
// draw nodes | |
for (let n of this.nodes) { | |
n.draw(this.ctx); | |
} | |
} | |
addRope(rope) { | |
this.ropes.push(rope); | |
} | |
add(struct) { | |
// load nodes | |
for (let n in struct.nodes) { | |
this.nodes.add(struct.nodes[n]); | |
/* | |
const node = new Node(struct.nodes[n]); | |
struct.nodes[n].id = node; | |
nodes.add(node); | |
*/ | |
} | |
// load constraints | |
for (let i = 0; i < struct.constraints.length; i++) { | |
let c = struct.constraints[i]; | |
this.constraints.add(c); | |
/* | |
new Constraint( | |
struct.nodes[c[0]].id, | |
struct.nodes[c[1]].id | |
) | |
); | |
*/ | |
} | |
} | |
} | |
var scene = new Scene(document.querySelector('#canvas')); | |
scene.run(); | |
// const pointer = new Pointer(canvas); | |
const phrase = ' get uncomfortable '; | |
var r = new Rope({ | |
x: scene.canvas.width * 0.15, | |
y: 40, | |
length: scene.canvas.width * 0.7, | |
points: phrase.length, | |
vertical: false, | |
dangleEnd: false, | |
fixedEnds: true, | |
stiffness: 1.5, | |
constrain: false, | |
gravity: 0.1, | |
}); | |
var center = r.nodes.length / 2; | |
var ropes = r.nodes.map((n, i) => { | |
n.set(n.x, 60 + 80 * (1 - Math.abs(((center - i) % center) / center))); | |
if (phrase[i] !== ' ') { | |
//if ( i !== 0 && i !== r.nodes.length - 1 ) { | |
return new Rope({ | |
startNode: n, | |
x: n.x, | |
y: n.y, | |
length: 60, | |
points: 4, | |
letter: phrase[i], | |
vertical: true, | |
stiffness: 1, //2.5,, | |
constrain: false, | |
gravity: 0.5, | |
}) | |
} | |
//} | |
}); | |
var first = r.nodes[0]; | |
var last = r.nodes[r.nodes.length - 1]; | |
first.set(2, -2); | |
last.set(scene.canvas.width - 2, -2); | |
r.makeConstraints(); | |
ropes = ropes; | |
scene.addRope(r); | |
ropes.filter(r => r).forEach(r => { | |
r.makeConstraints() | |
scene.addRope(r); | |
}); |
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
html { | |
overflow: hidden; | |
touch-action: none; | |
content-zooming: none; | |
} | |
body { | |
position: absolute; | |
margin: 0; | |
padding: 0; | |
background: #222; | |
width: 100%; | |
height: 100%; | |
} | |
#canvas { | |
position: absolute; | |
top: 0; right: 0; left: 0; | |
margin: auto; | |
margin: auto; | |
max-width: 100%; | |
height: auto; | |
outline: solid 1px red; | |
/* background:#000; */ | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment