Blurred Glow Words
- Source
- arlan.me/vault ↗
- Author
- Arlan
- Licence
- MIT
Loading…
Soft glowing, slightly blurred text that cross-dissolves through hello, pookie and monkey, each in its own colour world, with breathing bloom, grain and a pointer hot-spot.
Code
"use client";
import { approach, clamp, drawBlurred, makeCanvas, mulberry32, smooth, useStage, type Scene } from "./stage";
/*
* A crisp-ish word sitting in a soft gradient-mapped glow on light paper,
* cycling hello -> pookie -> monkey, each in its own colour world. The bloom
* breathes, the ends of the word sit out of focus, the pointer lights a local
* hot-spot and film grain sits over everything.
*/
const WORDS = [
{ word: "hello", paper: "#f3f6ff", ink: "#1f3ad6", glow: ["#ffd36b", "#8fb0ff", "#3550ff"] },
{ word: "pookie", paper: "#fff3f5", ink: "#d3134f", glow: ["#ffdf8a", "#ff97b8", "#ff2e6e"] },
{ word: "monkey", paper: "#fdf2f6", ink: "#9c1c98", glow: ["#ffc75a", "#ff7ad5", "#c41ab9"] },
];
const HOLD = 2.6;
const FADE = 0.75;
function createScene(): Scene {
let grain: HTMLCanvasElement | null = null;
const rnd = mulberry32(9);
const hot = { x: 0, y: 0, a: 0 };
const drawWord = (ctx: CanvasRenderingContext2D, wi: number, alpha: number, w: number, h: number, t: number) => {
const d = WORDS[wi];
const size = Math.min(h * 0.34, w * 0.2);
ctx.font = `600 ${size}px "Avenir Next", "Nunito", "Segoe UI", "Helvetica Neue", Arial, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const breathe = 1 + 0.18 * Math.sin(t * 1.3);
const cx = w / 2;
const cy = h / 2;
const paint = (c: CanvasRenderingContext2D) => {
c.fillStyle = "#000";
c.fillText(d.word, cx, cy);
};
ctx.globalAlpha = alpha * 0.9;
drawBlurred(ctx, size * 0.55 * breathe, d.glow[0], paint);
ctx.globalAlpha = alpha * 0.75;
drawBlurred(ctx, size * 0.22 * breathe, d.glow[1], paint);
ctx.globalAlpha = alpha * 0.85;
drawBlurred(ctx, size * 0.07 * breathe, d.glow[2], paint);
// ink: sharp in the middle, softer toward both ends
ctx.globalAlpha = alpha;
const full = ctx.measureText(d.word).width;
ctx.textAlign = "left";
let x = cx - full / 2;
for (const ch of d.word) {
const cw = ctx.measureText(ch).width;
const u = clamp(Math.abs(x + cw / 2 - cx) / (full / 2));
const blur = size * (0.006 + 0.05 * u * u);
const px = x;
drawBlurred(ctx, blur, d.ink, (c) => {
c.fillStyle = "#000";
c.fillText(ch, px, cy);
});
x += cw;
}
ctx.textAlign = "center";
ctx.globalAlpha = 1;
};
return {
frame({ ctx, w, h, t, dt, p }) {
if (!grain) {
const g = makeCanvas(192, 192);
const img = g.ctx.createImageData(192, 192);
for (let i = 0; i < img.data.length; i += 4) {
const v = (rnd() * 255) | 0;
img.data[i] = v;
img.data[i + 1] = v;
img.data[i + 2] = v;
img.data[i + 3] = 255;
}
g.ctx.putImageData(img, 0, 0);
grain = g.c;
}
const period = HOLD + FADE;
const k = t / period;
const i0 = Math.floor(k) % WORDS.length;
const i1 = (i0 + 1) % WORDS.length;
const f = smooth(((k % 1) * period - HOLD) / FADE);
// paper cross-fades with the word
ctx.fillStyle = WORDS[i0].paper;
ctx.fillRect(0, 0, w, h);
if (f > 0) {
ctx.globalAlpha = f;
ctx.fillStyle = WORDS[i1].paper;
ctx.fillRect(0, 0, w, h);
ctx.globalAlpha = 1;
}
// pointer hot-spot
hot.x = approach(hot.x, p.x, 8, dt);
hot.y = approach(hot.y, p.y, 8, dt);
hot.a = approach(hot.a, p.inside ? 1 : 0, 4, dt);
if (hot.a > 0.01) {
const g = ctx.createRadialGradient(hot.x, hot.y, 0, hot.x, hot.y, h * 0.45);
const c = WORDS[f > 0.5 ? i1 : i0].glow[0];
g.addColorStop(0, c);
g.addColorStop(1, "rgba(255,255,255,0)");
ctx.globalAlpha = 0.55 * hot.a;
ctx.fillStyle = g;
ctx.fillRect(0, 0, w, h);
ctx.globalAlpha = 1;
}
if (f < 1) drawWord(ctx, i0, 1 - f, w, h, t);
if (f > 0) drawWord(ctx, i1, f, w, h, t);
// film grain, soft-light
ctx.globalCompositeOperation = "soft-light";
ctx.globalAlpha = 0.5;
const ox = -((t * 997) % 192);
const oy = -((t * 613) % 192);
for (let y = oy; y < h; y += 192) for (let x = ox; x < w; x += 192) ctx.drawImage(grain, x, y);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
},
};
}
export default function GlowWords() {
const ref = useStage(createScene, 1);
return <div ref={ref} className="relative h-full w-full overflow-hidden bg-[#fdf2f6]" />;
}
Prompt
Build "Blurred Glow Words", a text animation. What it does: Soft glowing, slightly blurred text that cross-dissolves through hello, pookie and monkey, each in its own colour world, with breathing bloom, grain and a pointer hot-spot. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS. 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).