Skip to content

Instantly share code, notes, and snippets.

@shshaw
Last active May 17, 2018 19:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shshaw/6db07c4c1dfc6f1b6269d3d75a63ac98 to your computer and use it in GitHub Desktop.
Save shshaw/6db07c4c1dfc6f1b6269d3d75a63ac98 to your computer and use it in GitHub Desktop.
Simple Rope Verlet Physics
<canvas id="canvas" width="900" height="500"></canvas>
"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);
});
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