Live demo, running in your browser

Ink mist

A warm paper-textured hero where indigo and coral ink drifts through a live fluid simulation, wandering on its own and swirling into new eddies as the pointer moves, with a serif headline over it.

Code: MIT license.

<!doctype html>
<html lang="en">
  <head
    ><script>
      /* 6to9 pattern harness */ (function () {
        var parentPaused = false,
          hidden = document.hidden,
          p = (window.__pattern = { paused: false, reducedMotion: false, maxDpr: 2 });
        function sync() {
          p.paused = parentPaused || hidden;
        }
        sync();
        try {
          p.reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
        } catch (e) {}
        document.addEventListener("visibilitychange", function () {
          hidden = document.hidden;
          sync();
        });
        addEventListener("message", function (e) {
          var d = e.data || {};
          if (d.pattern === "pause") {
            parentPaused = true;
            sync();
          }
          if (d.pattern === "play") {
            parentPaused = false;
            sync();
          }
        });
      })();
    </script>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Ombra - ink mist</title>
    <style>
      :root {
        --paper: #f2eee4;
        --ink: #16143a;
        --coral: #ff6a52;
      }
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }
      html,
      body {
        height: 100%;
      }
      body {
        overflow: hidden;
        background: var(--paper);
        color: var(--ink);
        font-family: Georgia, "Iowan Old Style", "Palatino Linotype", "Times New Roman", serif;
        touch-action: none;
      }
      .hero {
        position: relative;
        width: 100vw;
        height: 100vh;
        height: 100dvh;
        overflow: hidden;
      }
      canvas {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        display: block;
      }
      .fallback {
        display: none;
        position: absolute;
        inset: 0;
        background: radial-gradient(40% 50% at 25% 30%, rgba(40, 38, 120, 0.55), transparent 70%),
          radial-gradient(45% 45% at 75% 70%, rgba(255, 106, 82, 0.5), transparent 70%),
          radial-gradient(30% 35% at 60% 25%, rgba(70, 60, 160, 0.3), transparent 70%), var(--paper);
        background-size: 140% 140%;
        animation: drift 18s ease-in-out infinite alternate;
      }
      .no-canvas .fallback {
        display: block;
      }
      @keyframes drift {
        from {
          background-position: 0% 0%;
        }
        to {
          background-position: 100% 100%;
        }
      }
      .vignette {
        position: absolute;
        inset: 0;
        pointer-events: none;
        background: radial-gradient(
          ellipse at center,
          transparent 55%,
          rgba(60, 50, 30, 0.14) 100%
        );
      }
      .brand {
        position: absolute;
        z-index: 2;
        top: clamp(20px, 3.5vw, 44px);
        left: clamp(24px, 7vw, 120px);
        font-size: 1.05rem;
        letter-spacing: 0.22em;
        text-transform: uppercase;
        text-shadow: 0 0 14px var(--paper);
      }
      .content {
        position: relative;
        z-index: 2;
        height: 100%;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: flex-start;
        padding: 0 clamp(24px, 7vw, 120px);
        pointer-events: none;
      }
      h1 {
        font-weight: 500;
        font-size: clamp(2.7rem, 8.4vw, 8rem);
        line-height: 0.98;
        letter-spacing: -0.03em;
        max-width: 10.5em;
        text-wrap: balance;
        text-shadow:
          0 0 22px var(--paper),
          0 0 8px var(--paper);
      }
      h1 em {
        font-style: italic;
        color: #2a2790;
      }
      .sub {
        margin-top: clamp(20px, 3vw, 34px);
        max-width: 29em;
        font-size: clamp(1rem, 1.35vw, 1.22rem);
        line-height: 1.55;
        text-shadow:
          0 0 16px var(--paper),
          0 0 6px var(--paper);
      }
      .cta {
        pointer-events: auto;
        margin-top: clamp(22px, 3vw, 36px);
        display: inline-flex;
        align-items: center;
        gap: 12px;
        padding: 16px 30px;
        border: 0;
        border-radius: 999px;
        background: var(--ink);
        color: var(--paper);
        font-family: inherit;
        font-size: 1.02rem;
        letter-spacing: 0.02em;
        cursor: pointer;
        transition:
          transform 0.3s ease,
          background 0.3s ease;
      }
      .cta:hover,
      .cta:focus-visible {
        transform: translateY(-3px);
        background: #2a2790;
        outline: none;
      }
    </style>
  </head>
  <body>
    <main class="hero">
      <div class="fallback"></div>
      <canvas id="mist"></canvas>
      <div class="vignette"></div>
      <div class="brand">Ombra</div>
      <section class="content">
        <h1>Let the quiet thing <em>move.</em></h1>
        <p class="sub"
          >A calm workspace for notes, drafts and slow ideas. Stir the page and watch the thinking
          bloom.</p
        >
        <button class="cta" type="button"
          >Start writing <span aria-hidden="true">&rarr;</span></button
        >
      </section>
    </main>
    <script>
      (function () {
        var canvas = document.getElementById("mist");
        var ctx = null;
        try {
          ctx = canvas.getContext("2d");
        } catch (err) {
          ctx = null;
        }
        if (!ctx) {
          document.body.classList.add("no-canvas");
          return;
        }

        function getP() {
          return window.__pattern || { paused: false, reducedMotion: false, maxDpr: 2 };
        }

        // ---- paper grain tile (generated in JS) ----
        var grain = null;
        function buildGrain() {
          var size = 384;
          var tile = document.createElement("canvas");
          tile.width = size;
          tile.height = size;
          var tc = tile.getContext("2d");
          var data = tc.createImageData(size, size);
          for (var i = 0; i < size * size; i++) {
            var v = 232 + Math.random() * 23;
            data.data[i * 4] = v;
            data.data[i * 4 + 1] = v - 2;
            data.data[i * 4 + 2] = v - 6;
            data.data[i * 4 + 3] = 255;
          }
          tc.putImageData(data, 0, 0);
          tc.lineWidth = 1;
          for (var f = 0; f < 90; f++) {
            var x = Math.random() * size;
            var y = Math.random() * size;
            var a = Math.random() * Math.PI * 2;
            var l = 6 + Math.random() * 18;
            tc.strokeStyle = "rgba(120,108,86," + (0.04 + Math.random() * 0.05) + ")";
            tc.beginPath();
            tc.moveTo(x, y);
            tc.quadraticCurveTo(
              x + Math.cos(a + 0.6) * l * 0.5,
              y + Math.sin(a + 0.6) * l * 0.5,
              x + Math.cos(a) * l,
              y + Math.sin(a) * l,
            );
            tc.stroke();
          }
          grain = ctx.createPattern(tile, "repeat");
        }
        buildGrain();

        // ---- fluid state ----
        var W = 0,
          H = 0,
          dpr = 1,
          gw = 0,
          gh = 0;
        var u, v, u2, v2, d1, d1b, d2, d2b, p, div, curl;
        var off = document.createElement("canvas");
        var offCtx = off.getContext("2d");
        var img = null;
        var simT = 0;
        var idle = 0.8;
        var hasPointer = false;
        var lastX = 0,
          lastY = 0;
        var needStill = true;

        var PAPER = [242, 238, 228];
        var INDIGO = [0.12, 0.13, 0.45];
        var CORAL = [1.0, 0.38, 0.3];

        function clamp(x, a, b) {
          return x < a ? a : x > b ? b : x;
        }

        function splat(cx, cy, r, fx, fy, a1, a2) {
          var reach = Math.ceil(r * 2.6);
          var x0 = Math.max(1, Math.floor(cx - reach));
          var x1 = Math.min(gw - 2, Math.ceil(cx + reach));
          var y0 = Math.max(1, Math.floor(cy - reach));
          var y1 = Math.min(gh - 2, Math.ceil(cy + reach));
          var rr = r * r;
          for (var y = y0; y <= y1; y++) {
            for (var x = x0; x <= x1; x++) {
              var dx = x - cx;
              var dy = y - cy;
              var g = Math.exp(-(dx * dx + dy * dy) / rr);
              var i = y * gw + x;
              u[i] += fx * g;
              v[i] += fy * g;
              d1[i] += a1 * g;
              d2[i] += a2 * g;
            }
          }
        }

        function wander(t) {
          return [
            0.5 + 0.38 * Math.sin(0.11 * t + 0.5) + 0.08 * Math.sin(0.37 * t),
            0.5 + 0.34 * Math.sin(0.083 * t + 2.0) + 0.07 * Math.cos(0.29 * t),
          ];
        }

        function emit() {
          var a = wander(simT);
          var b = wander(simT + 0.2);
          var tx = b[0] * gw - a[0] * gw;
          var ty = b[1] * gh - a[1] * gh;
          var len = Math.sqrt(tx * tx + ty * ty) + 1e-6;
          tx /= len;
          ty /= len;
          var target = hasPointer ? 0.25 : 0.8;
          idle += (target - idle) * 0.01;
          var cx = a[0] * gw;
          var cy = a[1] * gh;
          var side = Math.sin(simT * 0.2) > 0 ? 1 : -1;
          var px = -ty * 2.0 * side;
          var py = tx * 2.0 * side;
          splat(cx + px, cy + py, 4.5, tx * 0.9 * idle, ty * 0.9 * idle, 0.06 * idle, 0);
          splat(cx - px, cy - py, 4.5, tx * 0.9 * idle, ty * 0.9 * idle, 0, 0.06 * idle);
        }

        function vorticity() {
          var x, y, i;
          for (y = 1; y < gh - 1; y++) {
            for (x = 1; x < gw - 1; x++) {
              i = y * gw + x;
              curl[i] = (v[i + 1] - v[i - 1] - (u[i + gw] - u[i - gw])) * 0.5;
            }
          }
          for (y = 2; y < gh - 2; y++) {
            for (x = 2; x < gw - 2; x++) {
              i = y * gw + x;
              var dx = (Math.abs(curl[i + 1]) - Math.abs(curl[i - 1])) * 0.5;
              var dy = (Math.abs(curl[i + gw]) - Math.abs(curl[i - gw])) * 0.5;
              var l = Math.sqrt(dx * dx + dy * dy) + 1e-5;
              u[i] += 0.2 * (dy / l) * curl[i];
              v[i] -= 0.2 * (dx / l) * curl[i];
            }
          }
        }

        function current() {
          for (var y = 1; y < gh - 1; y++) {
            for (var x = 1; x < gw - 1; x++) {
              var i = y * gw + x;
              u[i] += 0.0035 * Math.sin(y * 0.09 + simT * 0.45 + x * 0.02);
              v[i] += 0.0035 * Math.sin(x * 0.08 - simT * 0.35 + y * 0.03);
            }
          }
        }

        function project() {
          var x, y, i, k;
          for (y = 1; y < gh - 1; y++) {
            for (x = 1; x < gw - 1; x++) {
              i = y * gw + x;
              div[i] = -0.5 * (u[i + 1] - u[i - 1] + v[i + gw] - v[i - gw]);
            }
          }
          for (k = 0; k < 18; k++) {
            for (y = 1; y < gh - 1; y++) {
              for (x = 1; x < gw - 1; x++) {
                i = y * gw + x;
                p[i] = (div[i] + p[i - 1] + p[i + 1] + p[i - gw] + p[i + gw]) * 0.25;
              }
            }
            for (x = 0; x < gw; x++) {
              p[x] = p[x + gw];
              p[(gh - 1) * gw + x] = p[(gh - 2) * gw + x];
            }
            for (y = 0; y < gh; y++) {
              p[y * gw] = p[y * gw + 1];
              p[y * gw + gw - 1] = p[y * gw + gw - 2];
            }
          }
          for (y = 1; y < gh - 1; y++) {
            for (x = 1; x < gw - 1; x++) {
              i = y * gw + x;
              u[i] -= 0.5 * (p[i + 1] - p[i - 1]);
              v[i] -= 0.5 * (p[i + gw] - p[i - gw]);
            }
          }
          for (y = 0; y < gh; y++) {
            u[y * gw] = 0;
            u[y * gw + gw - 1] = 0;
          }
          for (x = 0; x < gw; x++) {
            v[x] = 0;
            v[(gh - 1) * gw + x] = 0;
          }
        }

        function advect(dst, src, diss) {
          var maxX = gw - 1.001;
          var maxY = gh - 1.001;
          for (var y = 0; y < gh; y++) {
            for (var x = 0; x < gw; x++) {
              var i = y * gw + x;
              var px = clamp(x - u[i], 0, maxX);
              var py = clamp(y - v[i], 0, maxY);
              var x0 = px | 0;
              var y0 = py | 0;
              var fx = px - x0;
              var fy = py - y0;
              var j = y0 * gw + x0;
              var top = src[j] * (1 - fx) + src[j + 1] * fx;
              var bot = src[j + gw] * (1 - fx) + src[j + gw + 1] * fx;
              dst[i] = (top * (1 - fy) + bot * fy) * diss;
            }
          }
        }

        function step() {
          simT += 1 / 60;
          emit();
          current();
          vorticity();
          project();
          advect(u2, u, 0.997);
          advect(v2, v, 0.997);
          var t = u;
          u = u2;
          u2 = t;
          t = v;
          v = v2;
          v2 = t;
          for (var i = 0; i < u.length; i++) {
            u[i] = clamp(u[i], -3, 3);
            v[i] = clamp(v[i], -3, 3);
          }
          advect(d1b, d1, 0.992);
          advect(d2b, d2, 0.992);
          t = d1;
          d1 = d1b;
          d1b = t;
          t = d2;
          d2 = d2b;
          d2b = t;
        }

        function render() {
          var data = img.data;
          var n = gw * gh;
          for (var i = 0; i < n; i++) {
            var a1 = Math.min(0.96, 1 - Math.exp(-2.0 * d1[i]));
            var a2 = Math.min(0.92, 1 - Math.exp(-2.0 * d2[i]));
            a1 = a1 * a1 * (3 - 2 * a1) * 0.6 + a1 * 0.4;
            a2 = a2 * a2 * (3 - 2 * a2) * 0.6 + a2 * 0.4;
            var o = i * 4;
            data[o] = PAPER[0] * (1 - a1 * (1 - INDIGO[0])) * (1 - a2 * (1 - CORAL[0]));
            data[o + 1] = PAPER[1] * (1 - a1 * (1 - INDIGO[1])) * (1 - a2 * (1 - CORAL[1]));
            data[o + 2] = PAPER[2] * (1 - a1 * (1 - INDIGO[2])) * (1 - a2 * (1 - CORAL[2]));
            data[o + 3] = 255;
          }
          offCtx.putImageData(img, 0, 0);
          ctx.setTransform(1, 0, 0, 1, 0, 0);
          ctx.globalCompositeOperation = "source-over";
          ctx.imageSmoothingEnabled = true;
          ctx.imageSmoothingQuality = "high";
          ctx.drawImage(off, 0, 0, canvas.width, canvas.height);
          if (grain) {
            ctx.globalCompositeOperation = "multiply";
            ctx.fillStyle = grain;
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            ctx.globalCompositeOperation = "source-over";
          }
        }

        function init() {
          W = window.innerWidth;
          H = window.innerHeight;
          var maxDpr = getP().maxDpr || 1.5;
          dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
          canvas.width = Math.max(1, Math.round(W * dpr));
          canvas.height = Math.max(1, Math.round(H * dpr));
          var s = 170 / Math.max(W, H);
          gw = Math.max(48, Math.round(W * s));
          gh = Math.max(48, Math.round(H * s));
          var n = gw * gh;
          u = new Float32Array(n);
          v = new Float32Array(n);
          u2 = new Float32Array(n);
          v2 = new Float32Array(n);
          d1 = new Float32Array(n);
          d1b = new Float32Array(n);
          d2 = new Float32Array(n);
          d2b = new Float32Array(n);
          p = new Float32Array(n);
          div = new Float32Array(n);
          curl = new Float32Array(n);
          off.width = gw;
          off.height = gh;
          img = offCtx.createImageData(gw, gh);
          simT = 0;
          var warm = getP().reducedMotion ? 300 : 160;
          for (var k = 0; k < warm; k++) {
            step();
          }
          render();
          needStill = true;
        }

        function onMove(e) {
          var P = getP();
          if (P.paused || P.reducedMotion) {
            return;
          }
          var x = e.clientX;
          var y = e.clientY;
          if (typeof x !== "number" || typeof y !== "number") {
            return;
          }
          if (!hasPointer) {
            hasPointer = true;
            lastX = x;
            lastY = y;
            return;
          }
          var sx = gw / W;
          var sy = gh / H;
          var cdx = (x - lastX) * sx;
          var cdy = (y - lastY) * sy;
          var dist = Math.sqrt(cdx * cdx + cdy * cdy);
          if (dist < 0.01) {
            return;
          }
          var n = Math.max(1, Math.ceil(dist / 1.5));
          var speed = Math.min(dist, 8);
          var dirx = cdx / dist;
          var diry = cdy / dist;
          var fx = dirx * Math.min(dist, 10) * 0.09;
          var fy = diry * Math.min(dist, 10) * 0.09;
          var amt = Math.min(0.1 + speed * 0.02, 0.35);
          var r = 3 + speed * 0.35;
          var side = Math.sin(simT * 0.4) > 0 ? 1 : -1;
          var px = -diry * 1.6 * side;
          var py = dirx * 1.6 * side;
          for (var k = 1; k <= n; k++) {
            var f = k / n;
            var gx = (lastX + (x - lastX) * f) * sx;
            var gy = (lastY + (y - lastY) * f) * sy;
            splat(gx + px, gy + py, r, fx, fy, amt, 0);
            splat(gx - px, gy - py, r, fx, fy, 0, amt * 1.1);
          }
          lastX = x;
          lastY = y;
        }

        window.addEventListener("pointermove", onMove, { passive: true });
        window.addEventListener("mousemove", onMove, { passive: true });
        window.addEventListener("resize", init);

        var lastTs = 0;
        var acc = 0;
        function frame(ts) {
          requestAnimationFrame(frame);
          var P = getP();
          if (!lastTs) {
            lastTs = ts;
          }
          if (P.paused) {
            lastTs = ts;
            return;
          }
          if (P.reducedMotion) {
            if (needStill) {
              render();
              needStill = false;
            }
            lastTs = ts;
            return;
          }
          acc += Math.min(ts - lastTs, 50);
          lastTs = ts;
          var n = 0;
          while (acc >= 16.67 && n < 3) {
            step();
            acc -= 16.67;
            n++;
          }
          if (n > 0) {
            render();
          }
        }

        try {
          init();
        } catch (err) {
          document.body.classList.add("no-canvas");
          return;
        }
        requestAnimationFrame(frame);
      })();
    </script>
  </body>
</html>

Made by 6to9. Back to all motion patterns.