Live demo, running in your browser

Ceramic whale

A cobalt hero with a giant cream DEPTH wordmark where a frame-animated ceramic whale, rippled with a travelling sine warp, swims right to left between the letters while bubbles drift upward.

Assets

Code: MIT license. Imagery generated with Google Gemini · Video generated with Google Veo

<!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>DEPTH - Ceramic whale</title>
    <style>
      :root {
        --cobalt: #1240d0;
        --cream: #f6ecd6;
      }
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }
      html,
      body {
        width: 100%;
        height: 100%;
        overflow: hidden;
        background: var(--cobalt);
      }
      .hero {
        position: relative;
        width: 100vw;
        height: 100vh;
        overflow: hidden;
        background: var(--cobalt);
        color: var(--cream);
        font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
      }
      canvas {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        display: block;
      }
      .sr-only {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
      }
      .nav {
        position: absolute;
        top: 28px;
        left: 4vw;
        right: 4vw;
        display: flex;
        justify-content: space-between;
        z-index: 2;
        font-size: 13px;
        font-weight: 700;
        letter-spacing: 0.16em;
        text-transform: uppercase;
      }
      .copy {
        position: absolute;
        left: 4vw;
        right: 4vw;
        bottom: 5vh;
        display: flex;
        justify-content: space-between;
        align-items: flex-end;
        gap: 24px;
        z-index: 2;
      }
      .copy p {
        max-width: 30ch;
        font-size: clamp(15px, 1.4vw, 20px);
        line-height: 1.45;
      }
      .cta {
        display: inline-block;
        padding: 14px 28px;
        border-radius: 999px;
        background: var(--cream);
        color: var(--cobalt);
        font-weight: 800;
        font-size: 15px;
        letter-spacing: 0.04em;
        text-decoration: none;
        white-space: nowrap;
      }
      .fallback {
        display: none;
        position: absolute;
        inset: 0;
        align-items: center;
        justify-content: center;
        font-weight: 900;
        font-size: 26vw;
        letter-spacing: -0.04em;
        color: var(--cream);
        background: radial-gradient(ellipse at 70% 40%, #2a5cf0, var(--cobalt) 60%);
      }
      .no-canvas .fallback {
        display: flex;
      }
      @media (max-width: 640px) {
        .copy {
          flex-direction: column;
          align-items: flex-start;
          bottom: 6vh;
        }
      }
    </style>
  </head>
  <body>
    <section class="hero" id="hero">
      <canvas
        id="stage"
        aria-label="A glazed ceramic whale swimming across the word DEPTH"
      ></canvas>
      <div class="fallback" aria-hidden="true">DEPTH</div>
      <h1 class="sr-only">DEPTH</h1>
      <div class="nav">
        <span>Kiln &amp; Tide</span>
        <span>Vol. 04</span>
      </div>
      <div class="copy">
        <p>Hand-glazed, slow-fired, made to drift. Sink into something quiet.</p>
        <a class="cta" href="#">Explore the collection</a>
      </div>
    </section>
    <script>
      (function () {
        var P = window.__pattern || { paused: false, reducedMotion: false, maxDpr: 2 };
        var hero = document.getElementById("hero");
        var canvas = document.getElementById("stage");
        var ctx = canvas.getContext("2d");
        if (!ctx) {
          hero.classList.add("no-canvas");
          return;
        }

        var WORD = "DEPTH";
        var FRONT = [0, 2, 4];
        var CREAM = "#f6ecd6";
        var COBALT = "#1240d0";
        var CELL_W = 480;
        var CELL_H = 216;
        var COLS = 5;
        var FRAMES = 48;
        var FPS = 24;
        var TRAVEL_SECONDS = 34;
        var STRIPS = 54;
        var FACES_LEFT = true;
        var STILL_TIME = 17;

        var atlas = new Image();
        var atlasReady = false;
        var dirty = true;
        atlas.crossOrigin = "anonymous";
        atlas.onload = function () {
          atlasReady = true;
          dirty = true;
        };
        atlas.onerror = function () {
          atlasReady = false;
        };
        atlas.src = "https://agents-dev.6to9.ai/visuals/07cc6d0b5ae16ac0c505c3763c8d15af.webp";

        var W = 0;
        var H = 0;
        var dpr = 1;
        var layout = null;
        var bubbles = [];

        function seeded(seed) {
          var s = seed;
          return function () {
            s = (s * 16807) % 2147483647;
            return (s - 1) / 2147483646;
          };
        }

        function resize() {
          dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, P.maxDpr || 2));
          W = hero.clientWidth;
          H = hero.clientHeight;
          canvas.width = Math.round(W * dpr);
          canvas.height = Math.round(H * dpr);
          buildLayout();
          buildBubbles();
          dirty = true;
        }

        function buildLayout() {
          var portrait = W / H < 0.8;
          var yScale = portrait ? 2.2 : 1;
          var targetW = W * 0.94;
          var track = -0.03;
          ctx.font = '900 100px Impact, "Arial Black", "Helvetica Neue", Arial, sans-serif';
          var widths = [];
          var total = 0;
          for (var i = 0; i < WORD.length; i++) {
            var w = ctx.measureText(WORD[i]).width;
            widths.push(w);
            total += w;
            if (i < WORD.length - 1) total += track * 100;
          }
          var fs = (targetW / total) * 100;
          var k = fs / 100;
          var m = ctx.measureText("H");
          var cap = (m.actualBoundingBoxAscent || 72) * k;
          var typeCy = H * (portrait ? 0.4 : 0.46);
          var x = (W - targetW) / 2;
          var letters = [];
          for (var j = 0; j < WORD.length; j++) {
            letters.push({ ch: WORD[j], x: x });
            x += widths[j] * k + track * fs;
          }
          layout = {
            fs: fs,
            yScale: yScale,
            cap: cap,
            typeCy: typeCy,
            baseline: typeCy + (cap * yScale) / 2,
            letters: letters,
            whaleW: portrait ? W * 0.96 : Math.min(W * 0.64, H * 1.5),
          };
        }

        function buildBubbles() {
          var rnd = seeded(42);
          bubbles = [];
          var count = W < 640 ? 9 : 16;
          for (var i = 0; i < count; i++) {
            bubbles.push({
              x: rnd(),
              y0: rnd() * (H + 80),
              r: 4 + rnd() * 16,
              v: 14 + rnd() * 26,
              w: 0.4 + rnd() * 0.8,
              ph: rnd() * 6.28,
              front: i % 4 === 0,
            });
          }
        }

        function drawBubbles(t, front) {
          for (var i = 0; i < bubbles.length; i++) {
            var b = bubbles[i];
            if (b.front !== front) continue;
            var y = H + 40 - ((b.y0 + t * b.v) % (H + 80));
            var x = b.x * W + Math.sin(t * b.w + b.ph) * 12;
            var g = ctx.createRadialGradient(x - b.r * 0.3, y - b.r * 0.3, b.r * 0.1, x, y, b.r);
            g.addColorStop(0, "rgba(255,255,255,0.28)");
            g.addColorStop(1, "rgba(255,255,255,0.04)");
            ctx.beginPath();
            ctx.arc(x, y, b.r, 0, Math.PI * 2);
            ctx.fillStyle = g;
            ctx.fill();
            ctx.lineWidth = 1.5;
            ctx.strokeStyle = "rgba(246,236,214,0.6)";
            ctx.stroke();
            ctx.beginPath();
            ctx.arc(x - b.r * 0.35, y - b.r * 0.35, b.r * 0.18, 0, Math.PI * 2);
            ctx.fillStyle = "rgba(255,255,255,0.7)";
            ctx.fill();
          }
        }

        function drawLetters(indices, shadow) {
          ctx.font =
            "900 " + layout.fs + 'px Impact, "Arial Black", "Helvetica Neue", Arial, sans-serif';
          ctx.textBaseline = "alphabetic";
          ctx.fillStyle = CREAM;
          for (var n = 0; n < indices.length; n++) {
            var L = layout.letters[indices[n]];
            ctx.save();
            if (shadow) {
              ctx.shadowColor = "rgba(0,10,70,0.4)";
              ctx.shadowBlur = 36;
              ctx.shadowOffsetY = 10;
            }
            ctx.translate(L.x, layout.baseline);
            ctx.scale(1, layout.yScale);
            ctx.fillText(L.ch, 0, 0);
            ctx.restore();
          }
        }

        // Mesh warp: the cell is cut into horizontal strips, each offset by a travelling sine.
        function drawWhaleFrame(index, alpha, t, ww, wh) {
          var sx = (index % COLS) * CELL_W;
          var sy = Math.floor(index / COLS) * CELL_H;
          var srcStrip = (CELL_H - 1) / STRIPS;
          var dstStrip = wh / STRIPS;
          var amp = ww * 0.01;
          ctx.globalAlpha = alpha;
          for (var k = 0; k < STRIPS; k++) {
            var v = (k + 0.5) / STRIPS;
            var off = amp * Math.sin(v * 6.0 - t * 1.6) * (0.4 + 0.6 * Math.sin(v * Math.PI));
            ctx.drawImage(
              atlas,
              sx + 0.5,
              sy + 0.5 + k * srcStrip,
              CELL_W - 1,
              srcStrip,
              -ww / 2 + off,
              -wh / 2 + k * dstStrip,
              ww,
              dstStrip + 0.7,
            );
          }
          ctx.globalAlpha = 1;
        }

        function drawWhale(t) {
          if (!atlasReady) return;
          var ww = layout.whaleW;
          var wh = (ww * CELL_H) / CELL_W;
          var p = (t / TRAVEL_SECONDS) % 1;
          var cx = W + ww * 0.4 - p * (W + ww * 0.8);
          var cy = layout.typeCy + H * 0.02 + Math.sin(t * 0.5) * H * 0.018;
          var f = (t * FPS) % FRAMES;
          var i = Math.floor(f);
          var j = (i + 1) % FRAMES;
          var a = f - i;
          ctx.save();
          ctx.translate(cx, cy);
          ctx.rotate(Math.sin(t * 0.4) * 0.025);
          if (!FACES_LEFT) ctx.scale(-1, 1);
          drawWhaleFrame(i, 1, t, ww, wh);
          drawWhaleFrame(j, a, t, ww, wh);
          ctx.restore();
        }

        function draw(t) {
          ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
          ctx.globalAlpha = 1;
          ctx.fillStyle = COBALT;
          ctx.fillRect(0, 0, W, H);
          drawBubbles(t, false);
          drawLetters([0, 1, 2, 3, 4], false);
          drawWhale(t);
          drawLetters(FRONT, true);
          drawBubbles(t, true);
        }

        var clock = 0;
        var last = performance.now();

        function frame(now) {
          var dt = Math.min((now - last) / 1000, 0.1);
          last = now;
          if (P.reducedMotion) {
            if (dirty) {
              draw(STILL_TIME);
              dirty = false;
            }
          } else if (!P.paused) {
            clock += dt;
            draw(clock);
            dirty = false;
          } else if (dirty) {
            draw(clock);
            dirty = false;
          }
          requestAnimationFrame(frame);
        }

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

Made by 6to9. Back to all motion patterns.