VibePen

Sheet

Dustin Adkison · 2 hours ago · v1

Open standalone ↗

Code written by a VibePen user, not by VibePen. It runs sandboxed on an isolated origin with no network access. Nobody at VibePen has reviewed it — treat it like any page from a stranger, and never enter a password or payment details.

A cloth of 900 point masses held together by springs, pinned along the top edge. Move the pointer to push it around, or leave it alone and watch it settle. Verlet integration, no physics library — the whole simulation is about sixty lines.

Source

5.0 KB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sheet</title>
<style>
  html, body { margin: 0; height: 100%; overflow: hidden; background: #070a10; }
  canvas { display: block; width: 100%; height: 100%; touch-action: none; cursor: crosshair; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
(() => {
  "use strict";

  var cv = document.getElementById("c");
  var ctx = cv.getContext("2d", { alpha: false });

  var COLS = 30, ROWS = 30;
  var GRAVITY = 0.28;
  var DAMP = 0.985;
  var RELAX = 3;          // constraint passes per frame
  var REACH = 90;         // pointer influence radius, css px

  var w = 0, h = 0, dpr = 1, gap = 0, ox = 0, oy = 0;
  var N = COLS * ROWS;
  var x = new Float32Array(N), y = new Float32Array(N);
  var lx = new Float32Array(N), ly = new Float32Array(N);
  var pinned = new Uint8Array(N);
  var links = [];
  var pointer = { x: -1e6, y: -1e6, active: false };

  function idx(c, r) { return r * COLS + c; }

  function build() {
    gap = Math.min(w / (COLS + 3), h / (ROWS + 4));
    ox = (w - gap * (COLS - 1)) / 2;
    oy = gap * 1.4;
    links.length = 0;
    for (var r = 0; r < ROWS; r++) {
      for (var c = 0; c < COLS; c++) {
        var i = idx(c, r);
        x[i] = lx[i] = ox + c * gap;
        y[i] = ly[i] = oy + r * gap;
        pinned[i] = (r === 0 && (c % 4 === 0 || c === COLS - 1)) ? 1 : 0;
        if (c > 0) links.push(idx(c - 1, r), i, gap);
        if (r > 0) links.push(idx(c, r - 1), i, gap);
      }
    }
  }

  function resize() {
    dpr = Math.min(window.devicePixelRatio || 1, 1.5);
    var nw = Math.max(1, Math.floor(cv.clientWidth * dpr));
    var nh = Math.max(1, Math.floor(cv.clientHeight * dpr));
    if (nw === w && nh === h) return;
    w = nw; h = nh; cv.width = w; cv.height = h;
    build();
  }

  function step() {
    var i, k;
    var pr = REACH * dpr, pr2 = pr * pr;

    for (i = 0; i < N; i++) {
      if (pinned[i]) continue;
      var vx = (x[i] - lx[i]) * DAMP;
      var vy = (y[i] - ly[i]) * DAMP;
      lx[i] = x[i]; ly[i] = y[i];
      x[i] += vx; y[i] += vy + GRAVITY * dpr;

      if (pointer.active) {
        var dx = x[i] - pointer.x, dy = y[i] - pointer.y;
        var d2 = dx * dx + dy * dy;
        if (d2 < pr2 && d2 > 0.01) {
          var f = (1 - Math.sqrt(d2) / pr) * 6 * dpr;
          var d = Math.sqrt(d2);
          x[i] += (dx / d) * f; y[i] += (dy / d) * f;
        }
      }
    }

    for (k = 0; k < RELAX; k++) {
      for (i = 0; i < links.length; i += 3) {
        var a = links[i], b = links[i + 1], rest = links[i + 2];
        var ddx = x[b] - x[a], ddy = y[b] - y[a];
        var dist = Math.sqrt(ddx * ddx + ddy * ddy) || 0.0001;
        var diff = ((dist - rest) / dist) * 0.5;
        var mx = ddx * diff, my = ddy * diff;
        if (!pinned[a]) { x[a] += mx; y[a] += my; }
        if (!pinned[b]) { x[b] -= mx; y[b] -= my; }
      }
    }
  }

  function draw() {
    ctx.fillStyle = "#070a10";
    ctx.fillRect(0, 0, w, h);
    ctx.lineWidth = dpr * 0.7;

    for (var r = 0; r < ROWS; r++) {
      for (var c = 0; c < COLS; c++) {
        var i = idx(c, r);
        // hue drifts down the sheet; stretched links glow brighter
        if (c < COLS - 1) link(i, idx(c + 1, r), r);
        if (r < ROWS - 1) link(i, idx(c, r + 1), r);
      }
    }
  }

  function link(a, b, row) {
    var dx = x[b] - x[a], dy = y[b] - y[a];
    var d = Math.sqrt(dx * dx + dy * dy);
    var strain = Math.min(1, Math.abs(d - gap) / (gap * 0.9));
    var f = row / ROWS;
    var rr = 70 + strain * 185 + f * 40;
    var gg = 150 - strain * 60 + f * 30;
    var bb = 235 - f * 60;
    ctx.strokeStyle = "rgba(" + (rr | 0) + "," + (gg | 0) + "," + (bb | 0) + "," + (0.25 + strain * 0.55).toFixed(3) + ")";
    ctx.beginPath();
    ctx.moveTo(x[a], y[a]);
    ctx.lineTo(x[b], y[b]);
    ctx.stroke();
  }

  function pos(e) {
    var r = cv.getBoundingClientRect();
    pointer.x = (e.clientX - r.left) * dpr;
    pointer.y = (e.clientY - r.top) * dpr;
    pointer.active = true;
  }
  cv.addEventListener("pointermove", pos);
  cv.addEventListener("pointerdown", pos);
  cv.addEventListener("pointerleave", function () { pointer.active = false; });

  var raf = 0, running = false;
  function tick() { resize(); step(); draw(); raf = requestAnimationFrame(tick); }
  function play() { if (!running) { running = true; raf = requestAnimationFrame(tick); } }
  function stop() { running = false; cancelAnimationFrame(raf); }

  resize();

  var reduced = typeof window.matchMedia === "function" &&
    window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  if (reduced) {
    for (var s = 0; s < 140; s++) step();
    draw();
  } else {
    if (typeof IntersectionObserver === "function") {
      new IntersectionObserver(function (e) {
        e[0].isIntersecting ? play() : stop();
      }, { threshold: 0 }).observe(cv);
    }
    document.addEventListener("visibilitychange", function () {
      document.hidden ? stop() : play();
    });
    play();
  }

  window.addEventListener("resize", resize);
})();
</script>
</body>
</html>

Back to the gallery