Published
Edited
Sep 4, 2020
1 star
Insert cell
md`# Deformables`
Insert cell
import { Vec } from "@yalmar/2d-geometry-utils"
Insert cell
height = width * 9 / 16
Insert cell
context = DOM.context2d(width, height)
Insert cell
gravity = Vec(0,0.5)
Insert cell
defaults = ({mass:1.0, restitution:0.5, iterations:10})
Insert cell
class Particle {
constructor (pos) {
this.pos = pos;
this.prev = pos.clone();
this.mass = defaults.mass;
this.pinned = false; // Whether to stay in place or move with the simulation
}
timeStep () { // Do one time step integration
if (!this.pinned) {
var tmp = this.pos;
this.pos = this.pos.add(this.pos.sub(this.prev)).add(gravity)
this.prev = tmp;
}
}
move (newPos) {
if (!this.pinned) {
this.pos.set(newPos);
}
}
draw(ctx) { // Draw this particle on a canvas
ctx.beginPath();
ctx.arc(this.pos.x,this.pos.y,2,0,2*Math.PI);
ctx.fill();
}
}
Insert cell
/**
* Constraint to keep the distance between two particles constant
*/
class LinearConstraint {
constructor (p0,p1,restitution) {
[this.p0,this.p1,this.s,this.r] = [p0,p1,p0.pos.dist(p1.pos),restitution||defaults.restitution];
this.relax()
}
relax() {
let v = this.p1.pos.sub (this.p0.pos);
let d = v.mag();
let diff = (d-this.s)/d*this.r;
let ratio = this.p1.mass / (this.p0.mass + this.p1.mass);
this.p0.move (this.p0.pos.add(v.scale(ratio*diff)));
this.p1.move (this.p1.pos.add(v.scale((ratio-1)*diff)));
}
draw (ctx) { // Draws constraint on a canvas
ctx.beginPath();
ctx.moveTo(this.p0.pos.x,this.p0.pos.y);
ctx.lineTo(this.p1.pos.x,this.p1.pos.y);
ctx.stroke();
}
}
Insert cell
class Body {
constructor (particles,constraints) {
[this.particles,this.constraints] = [particles,constraints];
}
timeStep () { // Do one time step of the simulation
for (let p of this.particles) p.timeStep(); // Integrate velocities
for (let i = 0; i < defaults.iterations; i++) {
for (let p of this.particles) { // Constrain particles to remain in the world
let x = Math.max(Math.min(p.pos.x, width),0);
let y = Math.max(Math.min(p.pos.y, height),0);
p.pos = p.pos.mix(Vec(x,y),defaults.restitution);
}
for (let c of this.constraints) c.relax(); // Enforce all constraints by relaxing one at a time
}
}
draw (ctx) { // Draw this body
ctx.lineWidth=0.8;
//ctx.lineWidth=0.5;
ctx.strokeStyle = "black";
for (let c of this.constraints) c.draw(ctx);
for (let p of this.particles) {
ctx.fillStyle = p.pinned ? "blue" : "red";
p.draw(ctx);
}
}
}
Insert cell
function trelis (corner,size,nx,ny, stiff,reinforce) {
let p = [];
let c = [];
let dx = Vec(size.x/nx,0);
let dy = Vec(0,size.y/ny);
let m = nx+1;
for (let i = 0; i <= ny; i++) {
let q = corner.add (dy.scale (i));
for (let j = 0; j <= nx; j++) {
p.push (new Particle(q.add (dx.scale(j))));
let k = i*m+j;
if (j > 0) c.push (new LinearConstraint (p[k], p[k-1]));
if (i > 0) c.push (new LinearConstraint (p[k], p[k-m]));
if (i > 0 && j > 0 && stiff) c.push (new LinearConstraint (p[k-1], p[k-m]),
new LinearConstraint (p[k], p[k-m-1]));
}
}
if (nx > 1 && ny > 1 && reinforce) {
c.push (new LinearConstraint(p[0],p[p.length-1]),
new LinearConstraint(p[0],p[m-1]),
new LinearConstraint(p[0],p[p.length-m]),
new LinearConstraint(p[p.length-m],p[m-1]),
new LinearConstraint(p[m-1],p[p.length-1]),
new LinearConstraint(p[p.length-m],p[p.length-1])
);
}
return new Body(p,c);
}
Insert cell
bodies = {
let b0 = trelis(Vec(100,100),Vec(100,100),4,4,true,true);
b0.particles[0].pinned = true;
let b1 = trelis(Vec(200,200),Vec(150,100),3,2,true);
b1.particles[2].pinned = true;
let b2 = trelis(Vec(400,100),Vec(100,100),8,8);
b2.particles[0].pinned = b2.particles[8].pinned = true;
return [b0,b1,b2]
}
Insert cell
simulationStep = function () {
context.fillStyle = "#f2f2f2";
context.fillRect(0, 0, width, height);
for (let b of bodies) {
b.timeStep();
b.draw(context);
}
}
Insert cell
interaction = {
var dragged = false;
var sel = null;
// Finds the closest particle to the mouse. Returns null if none close enough
function selectParticle (mouse) {
let closest = null;
let closestDist = 1e10;
for (let b of bodies) {
for (let p of b.particles) {
let d = p.pos.dist(mouse);
if (d < closestDist) {
closest = p;
closestDist = d;
}
}
}
if (closestDist < 20) {
return closest;
}
return null;
}
// Picks the closest particle for dragging
context.canvas.onmousedown = function (event) {
let closest = selectParticle(Vec(event.offsetX,event.offsetY));
if (closest) {
sel = closest;
closest.pinned = true;
dragged = false;
}
}
// Drags picked particle
context.canvas.onmousemove = function (event) {
let mouse = Vec(event.offsetX,event.offsetY);
if (event.buttons && sel && sel.pinned) sel.pos = mouse;
dragged = true;
}
// Releases the particle
context.canvas.onmouseup = function (event) {
if (sel) {
if (!dragged) sel.pinned = false;
}
}
}
Insert cell
context.canvas
Insert cell
simulation = {
for (let i = 0; ; i++) {
simulationStep();
yield i;
}
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more