Live demo, running in your browser

Water surface

A sunlit pool floor of sand and drifting caustics with a refracted SUN DIP headline painted into it; a wandering virtual swimmer, random drops and the pointer send damped ripples across the water.

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>Sun Dip</title>
    <style>
      html,
      body {
        margin: 0;
        height: 100%;
        overflow: hidden;
        background: #e6d2a6;
        font-family:
          system-ui,
          -apple-system,
          "Segoe UI",
          Roboto,
          "Helvetica Neue",
          Arial,
          sans-serif;
      }
      .hero {
        position: relative;
        width: 100vw;
        height: 100vh;
        overflow: hidden;
        background: #e6d2a6;
      }
      @supports (height: 100dvh) {
        .hero {
          height: 100dvh;
        }
      }
      #water {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        display: block;
      }
      /* The headline is painted into the pool floor by the canvas; this copy is for screen readers. */
      .headline {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
      }
      .panel {
        position: absolute;
        left: 4vw;
        bottom: 5vh;
        max-width: 400px;
        box-sizing: border-box;
        padding: 22px 24px 24px;
        background: #fffaf0;
        color: #0f4c5c;
        border-radius: 18px;
        box-shadow: 0 14px 40px rgba(14, 70, 84, 0.22);
      }
      .eyebrow {
        margin: 0 0 8px;
        font-size: 12px;
        font-weight: 700;
        letter-spacing: 0.16em;
        text-transform: uppercase;
        color: #c4562f;
      }
      .sub {
        margin: 0 0 18px;
        font-size: 17px;
        line-height: 1.45;
      }
      .btn {
        display: inline-block;
        border: 0;
        cursor: pointer;
        padding: 13px 24px;
        border-radius: 999px;
        background: #0f4c5c;
        color: #fffaf0;
        font: inherit;
        font-weight: 700;
        font-size: 16px;
        transition:
          background 0.25s ease,
          transform 0.25s ease;
      }
      .btn:hover {
        background: #c4562f;
        transform: translateY(-1px);
      }
      .btn:focus-visible {
        outline: 3px solid #ff9b7a;
        outline-offset: 3px;
      }
      @media (max-width: 640px) {
        .panel {
          left: 16px;
          right: 16px;
          bottom: 20px;
          max-width: none;
          padding: 18px 20px 20px;
        }
        .sub {
          font-size: 16px;
          margin-bottom: 14px;
        }
      }
      /* CSS-only fallback when canvas 2D is unavailable */
      .hero.fallback #water {
        display: none;
      }
      .hero.fallback::before {
        content: "";
        position: absolute;
        inset: -20%;
        background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.55), transparent 28%),
          radial-gradient(circle at 70% 60%, rgba(255, 255, 240, 0.45), transparent 30%),
          radial-gradient(circle at 45% 80%, rgba(120, 210, 215, 0.45), transparent 35%);
        animation: drift 14s ease-in-out infinite alternate;
      }
      .hero.fallback .headline {
        position: absolute;
        inset: 0 0 25% 0;
        width: auto;
        height: auto;
        margin: 0;
        clip: auto;
        overflow: visible;
        display: flex;
        align-items: center;
        justify-content: center;
        text-align: center;
        white-space: normal;
        font-family: "Arial Black", "Helvetica Neue", Arial, sans-serif;
        font-weight: 900;
        font-size: clamp(90px, 26vw, 360px);
        line-height: 0.86;
        letter-spacing: -0.03em;
        text-transform: uppercase;
        color: #166880;
      }
      @keyframes drift {
        from {
          transform: translate(-3%, -2%) scale(1);
        }
        to {
          transform: translate(3%, 2%) scale(1.08);
        }
      }
    </style>
  </head>
  <body>
    <main class="hero" id="hero">
      <canvas id="water" aria-hidden="true"></canvas>
      <h1 class="headline">Sun dip</h1>
      <section class="panel" id="panel">
        <p class="eyebrow">Lido Studio</p>
        <p class="sub">Slow, sunlit tools for unhurried teams. Come on in, the water is fine.</p>
        <button class="btn" type="button">Dip in</button>
      </section>
    </main>
    <script>
      (function () {
        var P = window.__pattern || { paused: false, reducedMotion: false, maxDpr: 2 };
        var hero = document.getElementById("hero");
        var canvas = document.getElementById("water");
        var panel = document.getElementById("panel");

        var FONT = "'Arial Black', 'Helvetica Neue', Arial, system-ui, sans-serif";
        var LINES = ["SUN", "DIP"];
        var TILE = 256;
        var REF = 0.6;
        var STILL_T = 4;
        var SUBSTEPS = 2;
        var DAMP = 0.994;
        var INK = [20, 98, 120];

        var ctx = null;
        try {
          ctx = canvas.getContext("2d");
        } catch (e) {
          ctx = null;
        }

        var cssW = 0,
          cssH = 0,
          cell = 2,
          gw = 0,
          gh = 0;
        var gridCanvas, gridCtx, img, px32;
        var cur, prev, mask, shade;
        var causticA, causticB, sand;
        var stillDirty = true;

        var simT = 0;
        var lastFrame = 0;
        var acc = 0;
        var nextDrop = 1.2;

        var userActive = false;
        var lastMoveAt = -99999;
        var lastX = null,
          lastY = null,
          lastT = 0;
        var travel = 0;
        var virtX = null,
          virtY = null;

        function useFallback() {
          hero.classList.add("fallback");
        }

        /* ---------- procedural textures (tileable) ---------- */

        function makeCausticTile(phase) {
          var out = new Float32Array(TILE * TILE);
          var TAU = Math.PI * 2;
          for (var y = 0; y < TILE; y++) {
            for (var x = 0; x < TILE; x++) {
              var px = (x / TILE) * TAU;
              var py = (y / TILE) * TAU;
              // integer wave numbers keep the whole field periodic
              var wx =
                px + 0.9 * Math.sin(py * 2 + phase) + 0.5 * Math.sin(py * 3 - phase * 1.3 + 1);
              var wy =
                py +
                0.9 * Math.sin(px * 2 - phase * 0.8 + 2) +
                0.5 * Math.sin(px * 3 + phase * 1.1);
              var f1 =
                (Math.sin(wx * 2 + wy) +
                  Math.sin(wy * 3 - wx) +
                  Math.sin(wx * 3 + wy * 2 + phase * 0.5)) /
                3;
              var f2 =
                (Math.sin(wx - wy * 2 + 1.7) +
                  Math.sin(wy * 2 + wx * 3 - phase) +
                  Math.sin(wx * 2 - wy * 3 + 0.4)) /
                3;
              var r1 = 1 - Math.abs(f1);
              var r2 = 1 - Math.abs(f2);
              var c = (Math.pow(r1, 6) + Math.pow(r2, 6)) * 0.7;
              out[y * TILE + x] = c > 1 ? 1 : c;
            }
          }
          return out;
        }

        function makeSandTile() {
          var n = new Float32Array(TILE * TILE);
          var i;
          for (i = 0; i < n.length; i++) n[i] = Math.random();
          var out = new Float32Array(TILE * TILE);
          var TAU = Math.PI * 2;
          for (var y = 0; y < TILE; y++) {
            for (var x = 0; x < TILE; x++) {
              var s = 0;
              for (var dy = -1; dy <= 1; dy++) {
                for (var dx = -1; dx <= 1; dx++) {
                  s += n[((y + dy) & 255) * TILE + ((x + dx) & 255)];
                }
              }
              var px = (x / TILE) * TAU;
              var py = (y / TILE) * TAU;
              var mottle = 0.25 * Math.sin(px * 2 + py) * Math.sin(py * 3 - px);
              out[y * TILE + x] = (s / 9 - 0.5) * 2.2 + (n[y * TILE + x] - 0.5) * 0.5 + mottle;
            }
          }
          return out;
        }

        /* ---------- layout / buffers ---------- */

        function buildText() {
          var tc = document.createElement("canvas");
          tc.width = gw;
          tc.height = gh;
          var t2 = tc.getContext("2d", { willReadFrequently: true });
          var pr = panel.getBoundingClientRect();
          var top = cssH * 0.04;
          var bottom = pr.top > cssH * 0.3 ? pr.top - cssH * 0.03 : cssH * 0.9;
          var availH = Math.max(120, bottom - top);

          t2.font = "900 100px " + FONT;
          try {
            t2.letterSpacing = "-0.03em";
          } catch (e) {
            /* older browsers */
          }
          var widest = 1;
          for (var i = 0; i < LINES.length; i++) {
            widest = Math.max(widest, t2.measureText(LINES[i]).width);
          }
          var sizeW = (100 * cssW * 0.94) / widest;
          var sizeH = availH / (LINES.length * 0.84);
          var size = Math.min(sizeW, sizeH);

          t2.clearRect(0, 0, gw, gh);
          t2.font = "900 " + size / cell + "px " + FONT;
          try {
            t2.letterSpacing = "-0.03em";
          } catch (e) {
            /* older browsers */
          }
          t2.textAlign = "center";
          t2.textBaseline = "middle";
          t2.fillStyle = "#fff";
          var lineH = size * 0.86;
          var total = lineH * LINES.length;
          var y0 = top + (availH - total) / 2 + lineH / 2;
          for (var k = 0; k < LINES.length; k++) {
            t2.fillText(LINES[k], cssW / 2 / cell, (y0 + k * lineH) / cell);
          }
          var data = t2.getImageData(0, 0, gw, gh).data;
          mask = new Uint8Array(gw * gh);
          for (var p = 0; p < mask.length; p++) mask[p] = data[p * 4 + 3];
        }

        function buildShade() {
          shade = new Float32Array(gw * gh);
          for (var y = 0; y < gh; y++) {
            var vy = (y / gh - 0.5) * 2;
            for (var x = 0; x < gw; x++) {
              var vx = (x / gw - 0.5) * 2;
              var v2 = (vx * vx + vy * vy) / 2;
              shade[y * gw + x] = 1.06 - 0.24 * v2 + 0.04 * (1 - y / gh);
            }
          }
        }

        function resize() {
          cssW = hero.clientWidth || window.innerWidth;
          cssH = hero.clientHeight || window.innerHeight;
          var dpr = Math.min(window.devicePixelRatio || 1, P.maxDpr || 1);
          canvas.width = Math.max(1, Math.round(cssW * dpr));
          canvas.height = Math.max(1, Math.round(cssH * dpr));

          cell = Math.max(2, cssW / 640);
          gw = Math.ceil(cssW / cell);
          gh = Math.ceil(cssH / cell);
          gridCanvas = document.createElement("canvas");
          gridCanvas.width = gw;
          gridCanvas.height = gh;
          gridCtx = gridCanvas.getContext("2d");
          img = gridCtx.createImageData(gw, gh);
          px32 = new Uint32Array(img.data.buffer);

          cur = new Float32Array(gw * gh);
          prev = new Float32Array(gw * gh);
          virtX = null;
          buildText();
          buildShade();

          if (P.reducedMotion) primeStill();
          stillDirty = true;
          render(P.reducedMotion ? STILL_T : simT);
        }

        /* ---------- wave simulation ---------- */

        function step() {
          var W = gw,
            H = gh;
          var a = cur,
            b = prev;
          for (var y = 1; y < H - 1; y++) {
            var row = y * W;
            for (var x = 1; x < W - 1; x++) {
              var i = row + x;
              var v = (a[i - 1] + a[i + 1] + a[i - W] + a[i + W]) * 0.5 - b[i];
              v *= DAMP;
              if (v < 0.0005 && v > -0.0005) v = 0;
              b[i] = v;
            }
          }
          prev = a;
          cur = b;
        }

        function splash(gx, gy, amp, rad) {
          var cx = Math.round(gx);
          var cy = Math.round(gy);
          var r = Math.ceil(rad);
          for (var dy = -r; dy <= r; dy++) {
            var y = cy + dy;
            if (y < 1 || y >= gh - 1) continue;
            for (var dx = -r; dx <= r; dx++) {
              var x = cx + dx;
              if (x < 1 || x >= gw - 1) continue;
              var d = Math.sqrt(dx * dx + dy * dy);
              if (d > rad) continue;
              var f = 0.5 + 0.5 * Math.cos((Math.PI * d) / rad);
              var i = y * gw + x;
              var v = cur[i] + amp * f;
              cur[i] = v > 120 ? 120 : v < -120 ? -120 : v;
            }
          }
        }

        // Drags a body through the water along a segment (css px). v = speed in px/ms.
        function dragSegment(x0, y0, x1, y1, v) {
          var dist = Math.hypot(x1 - x0, y1 - y0);
          var n = Math.min(60, Math.max(1, Math.ceil(dist / (cell * 1.4))));
          var amp = -(3 + v * 5);
          var rad = 2.2 + v * 1.2;
          if (n === 1) amp *= Math.min(1, dist / (cell * 1.4));
          for (var k = 1; k <= n; k++) {
            var t = k / n;
            splash((x0 + (x1 - x0) * t) / cell, (y0 + (y1 - y0) * t) / cell, amp, rad);
          }
          travel += dist;
          if (travel > 150) {
            travel = 0;
            splash(x1 / cell, y1 / cell, -(16 + v * 6), 5.5);
          }
        }

        function primeStill() {
          splash(gw * 0.3, gh * 0.42, -40, 4);
          splash(gw * 0.7, gh * 0.55, -34, 4);
          splash(gw * 0.52, gh * 0.25, -28, 3.5);
          for (var i = 0; i < 70; i++) step();
        }

        /* ---------- pointer ---------- */

        function onMove(x, y) {
          var now = performance.now();
          userActive = true;
          if (lastX === null) {
            lastX = x;
            lastY = y;
            lastT = now;
            lastMoveAt = now;
            return;
          }
          var dist = Math.hypot(x - lastX, y - lastY);
          if (dist < 0.5) return;
          var dt = Math.max(now - lastT, 8);
          var v = Math.min(dist / dt, 2.5);
          if (!P.paused && !P.reducedMotion) dragSegment(lastX, lastY, x, y, v);
          lastX = x;
          lastY = y;
          lastT = now;
          lastMoveAt = now;
        }

        window.addEventListener("pointermove", function (e) {
          onMove(e.clientX, e.clientY);
        });
        window.addEventListener("mousemove", function (e) {
          onMove(e.clientX, e.clientY);
        });
        window.addEventListener("pointerdown", function (e) {
          userActive = true;
          lastX = e.clientX;
          lastY = e.clientY;
          lastT = performance.now();
          lastMoveAt = lastT;
          if (!P.paused && !P.reducedMotion) splash(e.clientX / cell, e.clientY / cell, -50, 5);
        });

        // gentle autonomous motion: a wandering virtual pointer and occasional drops
        function autonomous() {
          var now = performance.now();
          var wander = !userActive || now - lastMoveAt > 3500;
          if (wander) {
            var x =
              cssW * (0.5 + 0.32 * Math.sin(simT * 0.21 + 1)) + cssW * 0.1 * Math.sin(simT * 0.57);
            var y =
              cssH * (0.45 + 0.26 * Math.sin(simT * 0.17)) +
              cssH * 0.06 * Math.sin(simT * 0.43 + 2);
            if (virtX === null) {
              virtX = x;
              virtY = y;
            }
            if (Math.hypot(x - virtX, y - virtY) > cell * 2.5) {
              dragSegment(virtX, virtY, x, y, 0.12);
              virtX = x;
              virtY = y;
            }
          }
          if (simT > nextDrop) {
            var gx = (0.1 + Math.random() * 0.8) * gw;
            var gy = (0.1 + Math.random() * 0.8) * gh;
            splash(gx, gy, -(28 + Math.random() * 14), 3.5 + Math.random() * 1.5);
            nextDrop = simT + 1.8 + Math.random() * 2.4;
          }
        }

        /* ---------- rendering ---------- */

        function render(t) {
          var W = gw,
            H = gh;
          var A0 = causticA,
            A1 = causticB;
          var k1 = (cell * TILE) / 420;
          var k2 = (cell * TILE) / 300;
          var k3 = cell * 0.6;
          var o1x = t * 9,
            o1y = t * 5;
          var o2x = -t * 6,
            o2y = t * 8;
          var w = 0.5 + 0.5 * Math.sin(t * 0.35);
          var iw = 1 - w;
          var c0 = cur;

          for (var y = 0; y < H; y++) {
            var row = y * W;
            var up = y > 0 ? -W : 0;
            var dn = y < H - 1 ? W : 0;
            for (var x = 0; x < W; x++) {
              var i = row + x;
              var l = x > 0 ? -1 : 0;
              var rr = x < W - 1 ? 1 : 0;
              var hx = c0[i + rr] - c0[i + l];
              var hy = c0[i + dn] - c0[i + up];
              var lap = c0[i + l] + c0[i + rr] + c0[i + up] + c0[i + dn] - 4 * c0[i];

              // refracted sample position for the floor and the caustics
              var sx = x + hx * REF;
              var sy = y + hy * REF;
              var j1 = ((((sy * k1 + o1y) | 0) & 255) << 8) | (((sx * k1 + o1x) | 0) & 255);
              var j2 = ((((sy * k2 + o2y) | 0) & 255) << 8) | (((sx * k2 + o2x) | 0) & 255);
              var c = (A0[j1] * iw + A1[j1] * w + (A0[j2] * iw + A1[j2] * w)) * 0.6 - lap * 0.05;
              if (c < 0) c = 0;

              var g = sand[((((sy * k3) | 0) & 255) << 8) | (((sx * k3) | 0) & 255)];
              var sh = shade[i];
              var r = (226 + g * 26) * sh * 0.88;
              var gr = (205 + g * 24) * sh * 0.97 + 6;
              var b = (158 + g * 24) * sh * 0.96 + 22;

              // the headline lies on the floor, refracted a little more
              var tx = (x + hx * REF * 1.7 + 0.5) | 0;
              var ty = (y + hy * REF * 1.7 + 0.5) | 0;
              tx = tx < 0 ? 0 : tx >= W ? W - 1 : tx;
              ty = ty < 0 ? 0 : ty >= H ? H - 1 : ty;
              var m = mask[ty * W + tx] * (0.92 / 255);
              if (m > 0) {
                r += (INK[0] - r) * m;
                gr += (INK[1] - gr) * m;
                b += (INK[2] - b) * m;
              }

              // caustic light over everything, then ripple glints
              var L = 0.78 + c * 0.6;
              r = r * L + c * 34;
              gr = gr * L + c * 32;
              b = b * L + c * 24;
              var add = -(hx + hy) * 7;
              add = add < -30 ? -30 : add > 80 ? 80 : add;
              r += add;
              gr += add;
              b += add * 1.05;

              r = r > 255 ? 255 : r < 0 ? 0 : r;
              gr = gr > 255 ? 255 : gr < 0 ? 0 : gr;
              b = b > 255 ? 255 : b < 0 ? 0 : b;
              px32[i] = (255 << 24) | ((b | 0) << 16) | ((gr | 0) << 8) | (r | 0);
            }
          }
          gridCtx.putImageData(img, 0, 0);
          ctx.imageSmoothingEnabled = true;
          try {
            ctx.imageSmoothingQuality = "high";
          } catch (e) {
            /* optional */
          }
          ctx.drawImage(gridCanvas, 0, 0, canvas.width, canvas.height);
        }

        /* ---------- loop ---------- */

        function frame(now) {
          requestAnimationFrame(frame);
          try {
            if (P.paused) {
              lastFrame = now;
              return;
            }
            if (P.reducedMotion) {
              if (stillDirty) {
                render(STILL_T);
                stillDirty = false;
              }
              lastFrame = now;
              return;
            }
            var dt = Math.min((now - lastFrame) / 1000, 0.05);
            lastFrame = now;
            simT += dt;
            autonomous();
            acc += dt;
            var n = 0;
            while (acc >= 1 / 60 && n < 3) {
              for (var s = 0; s < SUBSTEPS; s++) step();
              acc -= 1 / 60;
              n++;
            }
            if (acc > 1 / 60) acc = 0;
            render(simT);
          } catch (e) {
            useFallback();
          }
        }

        function start() {
          causticA = makeCausticTile(0);
          causticB = makeCausticTile(2.2);
          sand = makeSandTile();
          resize();
          window.addEventListener("resize", function () {
            try {
              resize();
            } catch (e) {
              useFallback();
            }
          });
          lastFrame = performance.now();
          requestAnimationFrame(frame);
        }

        if (!ctx) {
          useFallback();
        } else {
          try {
            start();
          } catch (e) {
            useFallback();
          }
        }
      })();
    </script>
  </body>
</html>

Made by 6to9. Back to all motion patterns.