Blinking flowers
- Source
- arlan.me/vault ↗
- Author
- Arlan
- Licence
- MIT
Loading…
A lattice of rust-red line flowers in a heavy red frame blinks in unison while the top row drifts right and the bottom row left.
Code
"use client";
import { CanvasStudy, easeInOut, easeOut, type Frame, type Painter } from "./stage";
export const BG = "#f6efe2";
const RUST = "#b8432a";
const BLINK = 2.6;
const PETALS = 6;
/** Openness of every flower: 1 open, 0 closed. Flood closed fast, sweep open slower. */
function openness(t: number) {
const u = (t % BLINK) / BLINK;
if (u < 0.55) return 1;
if (u < 0.68) return 1 - easeOut((u - 0.55) / 0.13);
if (u < 0.74) return 0;
return easeInOut((u - 0.74) / 0.26);
}
function flower(ctx: CanvasRenderingContext2D, x: number, y: number, R: number, open: number, lw: number) {
const width = R * (0.2 + 0.24 * open);
const sweep = (1 - open) * (Math.PI / PETALS) * 0.9;
const flood = 1 - open;
ctx.save();
ctx.translate(x, y);
for (let i = 0; i < PETALS; i++) {
ctx.save();
ctx.rotate((i / PETALS) * Math.PI * 2 + sweep);
const petal = new Path2D();
petal.moveTo(0, -R * 0.14);
petal.bezierCurveTo(width, -R * 0.3, width * 0.9, -R * 0.85, 0, -R);
petal.bezierCurveTo(-width * 0.9, -R * 0.85, -width, -R * 0.3, 0, -R * 0.14);
if (flood > 0.01) {
ctx.save();
ctx.beginPath();
ctx.rect(-R, -R * 0.14 - flood * R * 0.9, R * 2, flood * R * 0.9 + 1);
ctx.clip();
ctx.fillStyle = RUST;
ctx.fill(petal);
ctx.restore();
}
ctx.stroke(petal);
// a vein down the petal
ctx.beginPath();
ctx.moveTo(0, -R * 0.22);
ctx.lineTo(0, -R * (0.45 + 0.3 * open));
ctx.stroke();
ctx.restore();
}
ctx.beginPath();
ctx.arc(0, 0, R * 0.12, 0, Math.PI * 2);
ctx.fillStyle = RUST;
ctx.fill();
ctx.lineWidth = lw;
ctx.restore();
}
export function create(): Painter {
return ({ ctx, w, h, t }: Frame) => {
ctx.fillStyle = RUST;
ctx.fillRect(0, 0, w, h);
const frame = Math.min(w, h) * 0.05;
const iw = w - frame * 2;
const ih = h - frame * 2;
ctx.fillStyle = BG;
ctx.fillRect(frame, frame, iw, ih);
const rows = 3;
const rowH = ih / rows;
const sp = Math.max(rowH * 1.02, iw / 5.5);
const R = Math.min(sp, rowH) * 0.4;
const lw = Math.max(1, R * 0.045);
const open = openness(t);
const speed = sp * 0.22; // px per second
ctx.save();
ctx.beginPath();
ctx.rect(frame, frame, iw, ih);
ctx.clip();
ctx.strokeStyle = RUST;
ctx.lineWidth = lw;
ctx.lineJoin = "round";
ctx.lineCap = "round";
for (let r = 0; r < rows; r++) {
const drift = r === 0 ? (t * speed) % sp : r === rows - 1 ? -((t * speed) % sp) : 0;
const stagger = r % 2 ? sp / 2 : 0;
const y = frame + rowH * (r + 0.5);
for (let x = frame - sp * 2 + stagger + drift; x < frame + iw + sp; x += sp) {
flower(ctx, x, y, R, open, lw);
}
}
ctx.restore();
};
}
export default function RustFlowers() {
return (
<CanvasStudy
create={create}
background={RUST}
still={0.6}
label="A lattice of rust-red line flowers on cream in a heavy red frame blinks in unison while the top row drifts right and the bottom row left."
/>
);
}
Prompt
Build "Blinking flowers", a canvas animation. What it does: A lattice of rust-red line flowers in a heavy red frame blinks in unison while the top row drifts right and the bottom row left. 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).