Baktiyar Ablaikhan

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).