Baktiyar Ablaikhan

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