Blob spiral
- Source
- arlan.me/vault ↗
- Author
- Arlan
- Licence
- MIT
Loading…
A three-by-three grid of soft black blobs swells open, bends into rings and a leaning spiral, then folds back into the grid.
Code
"use client";
import { CanvasStudy, easeInOut, lerp, type Frame, type Painter } from "./stage";
export const BG = "#f5f4f0";
const INK = "17,17,17";
type Blob = { x: number; y: number; a: number; b: number; rot: number };
type Layout = (r: number, c: number) => Blob; // in units of the short side, relative to centre
const grid =
(gap: number, size: number): Layout =>
(r, c) => ({ x: (c - 1) * gap, y: (r - 1) * gap, a: size, b: size, rot: 0 });
const rings: Layout = (r, c) => {
const R = 0.11 + r * 0.13;
const th = -Math.PI / 2 + (c * Math.PI * 2) / 3 + r * 0.35;
return { x: Math.cos(th) * R, y: Math.sin(th) * R, a: R * 0.72, b: 0.062, rot: th + Math.PI / 2 };
};
const spiral: Layout = (r, c) => {
const R = 0.07 + r * 0.14;
const th = -Math.PI / 2 + (c * Math.PI * 2) / 3 + R * 6.5;
return {
x: 0.08 + Math.cos(th) * R * 1.12,
y: -0.03 + Math.sin(th) * R,
a: 0.05 + R * 0.8,
b: 0.07 * (1 - r * 0.42),
rot: th + Math.PI / 2 + 0.35,
};
};
// [layout, seconds spent travelling into it, seconds resting on it]
const SEQ: [Layout, number, number][] = [
[grid(0.2, 0.072), 2.2, 1.2],
[grid(0.3, 0.095), 1.2, 0.2],
[rings, 1.6, 0.6],
[spiral, 2.6, 0.8],
];
const TOTAL = SEQ.reduce((s, [, a, b]) => s + a + b, 0);
function angleLerp(a: number, b: number, k: number) {
let d = (b - a) % Math.PI; // ellipses are symmetric under a half turn
if (d > Math.PI / 2) d -= Math.PI;
if (d < -Math.PI / 2) d += Math.PI;
return a + d * k;
}
export function create(): Painter {
return ({ ctx, w, h, t }: Frame) => {
ctx.fillStyle = BG;
ctx.fillRect(0, 0, w, h);
const S = Math.min(w, h) * 1.05;
let u = (t + 2.2) % TOTAL;
let from = SEQ[SEQ.length - 1][0];
let to = SEQ[0][0];
let k = 1;
for (let i = 0; i < SEQ.length; i++) {
const [lay, travel, rest] = SEQ[i];
if (u < travel + rest) {
from = SEQ[(i + SEQ.length - 1) % SEQ.length][0];
to = lay;
k = easeInOut(Math.min(1, u / travel));
break;
}
u -= travel + rest;
}
const grad = ctx.createRadialGradient(0, 0, 0, 0, 0, 1);
grad.addColorStop(0, `rgba(${INK},1)`);
grad.addColorStop(0.55, `rgba(${INK},0.96)`);
grad.addColorStop(0.8, `rgba(${INK},0.45)`);
grad.addColorStop(1, `rgba(${INK},0)`);
ctx.fillStyle = grad;
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
const A = from(r, c);
const B = to(r, c);
const wob = 1 + Math.sin(t * 1.3 + r * 2 + c) * 0.03;
const x = w / 2 + lerp(A.x, B.x, k) * S;
const y = h / 2 + lerp(A.y, B.y, k) * S;
const a = lerp(A.a, B.a, k) * S * wob * 1.25;
const b = lerp(A.b, B.b, k) * S * 1.25;
const rot = A.a === A.b ? B.rot : B.a === B.b ? A.rot : angleLerp(A.rot, B.rot, k);
ctx.save();
ctx.translate(x, y);
ctx.rotate(rot);
ctx.scale(Math.max(0.5, a), Math.max(0.5, b));
ctx.beginPath();
ctx.arc(0, 0, 1, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
};
}
export default function BlobSpiral() {
return (
<CanvasStudy
create={create}
background={BG}
still={8.5}
label="Nine soft black blobs swell open from a grid, bend into rings and a leaning spiral, then fold back into the grid."
/>
);
}
Prompt
Build "Blob spiral", a canvas animation. What it does: A three-by-three grid of soft black blobs swells open, bends into rings and a leaning spiral, then folds back into the grid. 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).