Skip to content

Draggable Spring ​

A bob hanging from a spring, pulled by gravity and by Hooke's law, with some damping. Drag the bob and let it go.

js
const replay = Inputs.button("Replay");
js
replay;

const width = 640;
const height = 200;
const spring = {k: 0.1, length: 100};
const anchor = {location: cm.vec(0, 0)};
const bob = {location: cm.vec(120, 160), velocity: cm.vec(), acceleration: cm.vec(), mass: 1};
const renderer = cm.canvas({width, height});
const root = cm.flow().renderer(renderer);
const node = renderer.node();
let dragging = false;

node.style.border = "solid black 1px";
node.style.touchAction = "none";

// The pointer in canvas coordinates, also when the canvas is scaled to fit the page.
const pointer = (event) => [(event.offsetX / node.clientWidth) * width, (event.offsetY / node.clientHeight) * height];
const onpointerup = () => (dragging = false);

node.addEventListener("pointerdown", (event) => {
  dragging = true;
  drag(event);
});
node.addEventListener("pointermove", (event) => dragging && drag(event));
window.addEventListener("pointerup", onpointerup);
invalidation.then(() => window.removeEventListener("pointerup", onpointerup));

function drag(event) {
  const [x, y] = pointer(event);
  bob.location.set(x - width / 2, y);
  bob.velocity.mult(0);
}

function applyForce(d, force) {
  d.acceleration.add(cm.vecDiv(force, d.mass));
}

function gravity(d) {
  applyForce(d, cm.vec(0, d.mass));
}

function pull(d) {
  const force = cm.vecSub(d.location, anchor.location);
  const stretch = force.mag() - spring.length;
  applyForce(d, force.mag(-1 * spring.k * stretch));
}

function move(d) {
  d.velocity.add(d.acceleration).mult(0.98);
  d.location.add(d.velocity);
  d.acceleration.mult(0);
}

requestAnimationFrame(function frame() {
  root.append(cm.clear, {fill: "white"});
  const group = root.append(cm.group, {x: width / 2, y: 0});
  const flow = dragging
    ? group.datum(bob)
    : group.datum(bob).process(cm.each, gravity).process(cm.each, pull).process(cm.each, move);
  const x = (d) => d.location.x;
  const y = (d) => d.location.y;
  flow.append(cm.link, {x: 0, y: 0, x1: x, y1: y, stroke: "black"});
  flow.append(cm.circle, {x, y, r: 20, fill: dragging ? "black" : "rgb(175, 175, 175)", stroke: "black"});
  group.append(cm.rect, {x: -5, y: 0, width: 10, height: 10, fill: "rgb(175, 175, 175)", stroke: "black"});

  root.render();
  requestAnimationFrame(frame);
});

document.body.append(node);