Baktiyar Ablaikhan

Cloudy Blur Reveal

Source
arlan.me/vault ↗
Author
Arlan
Licence
MIT

Loading…

Short maxims — make it obvious, then make it fast, sweat the details, ship it anyway, keep it honest, cut what is dull — dissolve in through a cloudy, blurred front on cycling colour panels.

Code

"use client";

import { createNoise3D } from "simplex-noise";
import { clamp, hex, makeCanvas, mixRGB, mulberry32, paintCloudMask, rgbStr, smooth, useStage, type Scene } from "./stage";

/*
 * Vivid panels, one at a time, each with a short line in its own contrasting
 * colour. The line dissolves in through a soft noisy diagonal front while
 * un-blurring into focus, holds, dissolves out, and the next panel arrives.
 */

const LINES: [string, string, string][] = [
  ["make it obvious", "#1f3cff", "#ffd84d"],
  ["then make it fast", "#ff5a1f", "#1a1033"],
  ["sweat the details", "#0f3d2e", "#9dffb0"],
  ["ship it anyway", "#f2e8d8", "#c4401a"],
  ["keep it honest", "#ffc6e0", "#6a0f4a"],
  ["cut what is dull", "#151515", "#f4f3ef"],
];
const IN = 0.75;
const HOLD = 1.7;
const OUT = 0.5;
const CYCLE = IN + HOLD + OUT;
const GW = 56;
const GH = 35;

function createScene(): Scene {
  const noise = createNoise3D(mulberry32(33));
  let text: { c: HTMLCanvasElement; ctx: CanvasRenderingContext2D } | null = null;
  let tmp: { c: HTMLCanvasElement; ctx: CanvasRenderingContext2D } | null = null;
  const mask = makeCanvas(GW, GH);
  const img = mask.ctx.createImageData(GW, GH);
  let shown = -1;
  let r = 1;
  let W = 0;
  let H = 0;

  return {
    resize(w, h, dpr) {
      r = Math.min(2, dpr);
      W = w;
      H = h;
      text = makeCanvas(w * r, h * r);
      tmp = makeCanvas(w * r, h * r);
      shown = -1;
    },
    frame({ ctx, w, h, t }) {
      if (!text || !tmp) return;
      const n = Math.floor(t / CYCLE);
      const local = t - n * CYCLE;
      const idx = n % LINES.length;
      const [line, bg, fg] = LINES[idx];
      const prevBg = LINES[(idx + LINES.length - 1) % LINES.length][1];
      // background cross-fades during the reveal
      const bgK = smooth(local / (IN * 0.8));
      ctx.fillStyle = rgbStr(mixRGB(hex(prevBg), hex(bg), bgK));
      ctx.fillRect(0, 0, w, h);

      if (shown !== n) {
        const c = text.ctx;
        c.setTransform(r, 0, 0, r, 0, 0);
        c.clearRect(0, 0, W, H);
        const size = Math.min(H * 0.17, W * 0.09);
        c.font = `500 ${size}px "Helvetica Neue", Inter, "Segoe UI", Arial, sans-serif`;
        c.textAlign = "center";
        c.textBaseline = "middle";
        c.fillStyle = fg;
        c.fillText(line, W / 2, H / 2);
        shown = n;
      }
      let prog: number;
      if (local < IN) prog = smooth(local / IN);
      else if (local < IN + HOLD) prog = 1;
      else prog = 1 - smooth((local - IN - HOLD) / OUT);
      if (prog <= 0.001) return;
      if (prog >= 0.999) {
        ctx.drawImage(text.c, 0, 0, w, h);
        return;
      }
      paintCloudMask(mask.ctx, img, noise, prog, t, 0.14, [0.7, 0.3]);
      const T = tmp.ctx;
      const TW = tmp.c.width;
      const TH = tmp.c.height;
      T.globalCompositeOperation = "source-over";
      T.clearRect(0, 0, TW, TH);
      const blur = (1 - prog) * 7 * 2 * r;
      T.save();
      T.shadowColor = fg;
      T.shadowBlur = blur;
      T.shadowOffsetX = TW * 2;
      T.drawImage(text.c, -TW * 2, 0);
      T.restore();
      T.globalAlpha = clamp((prog - 0.4) / 0.6);
      T.drawImage(text.c, 0, 0);
      T.globalAlpha = 1;
      T.globalCompositeOperation = "destination-in";
      T.imageSmoothingEnabled = true;
      T.drawImage(mask.c, 0, 0, TW, TH);
      ctx.drawImage(tmp.c, 0, 0, w, h);
    },
  };
}

export default function RevealPanels() {
  const ref = useStage(createScene, 1.5);
  return <div ref={ref} className="relative h-full w-full overflow-hidden bg-[#f2e8d8]" />;
}
Prompt

Build "Cloudy Blur Reveal", a text animation. What it does: Short maxims — make it obvious, then make it fast, sweat the details, ship it anyway, keep it honest, cut what is dull — dissolve in through a cloudy, blurred front on cycling colour panels. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using simplex-noise. Technique: an HTML canvas, a requestAnimationFrame loop, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://www.arlan.me/vault (Arlan, MIT).