"use client"; import { useEffect, useRef, type ReactNode } from "react"; type WindowBlindsProps = { children?: ReactNode; className?: string; /** Number of slats in the light. */ bands?: number; /** Colour of the wall in shade. */ wall?: string; /** Colour of the sunlight. */ light?: string; /** Show the shadow of a plant in the light. */ plant?: boolean; /** 1 is the default pace; 2 is twice as fast. */ speed?: number; }; /* One leaf as a lens from the stem out to its tip. */ function leaf(g: CanvasRenderingContext2D, x: number, y: number, angle: number, size: number) { g.save(); g.translate(x, y); g.rotate(angle); g.beginPath(); g.moveTo(0, 0); g.quadraticCurveTo(size * 0.5, -size * 0.2, size, 0); g.quadraticCurveTo(size * 0.5, size * 0.2, 0, 0); g.fill(); g.restore(); } /* WindowBlinds: late sun through a slatted blind, thrown slanted across a wall with the shadow of a plant in it. The sun creeps, the slats tilt on their own, and the pointer works the cord. */ export function WindowBlinds({ children, className = "", bands = 14, wall = "#2c211b", light = "#ffc97a", plant = true, speed = 1, }: WindowBlindsProps) { const rootRef = useRef(null); const canvasRef = useRef(null); useEffect(() => { const root = rootRef.current; const canvas = canvasRef.current; const ctx = canvas?.getContext("2d"); if (!root || !canvas || !ctx) return; const still = window.matchMedia("(prefers-reduced-motion: reduce)").matches; let w = 0; let h = 0; let raf = 0; let last = 0; let time = 0; let open = 0.62; let cord: number | null = null; const draw = () => { ctx.globalCompositeOperation = "source-over"; ctx.fillStyle = wall; ctx.fillRect(0, 0, w, h); // The whole patch of light is sheared and turned, the way sun lands on a wall. const creep = still ? 0 : Math.sin(time * 0.06) * w * 0.05; const patchW = Math.max(w * 0.66, h * 0.9); const patchH = h * 1.7; const pitch = patchH / bands; ctx.save(); ctx.translate(w * 0.63 + creep, h * 0.5); ctx.rotate(-0.1); ctx.transform(1, 0, -0.42, 1, 0, 0); const sun = ctx.createLinearGradient(-patchW / 2, 0, patchW / 2, 0); sun.addColorStop(0, `${light}66`); sun.addColorStop(0.55, `${light}e6`); sun.addColorStop(1, light); ctx.fillStyle = sun; const bar = patchW * 0.035; for (let i = 0; i < bands; i++) { const y = -patchH / 2 + i * pitch; ctx.fillRect(-patchW / 2, y, patchW / 2 - bar, pitch * open); ctx.fillRect(bar, y, patchW / 2 - bar, pitch * open); } ctx.restore(); if (plant) { const sway = still ? 0 : Math.sin(time * 0.7) * 0.05 + Math.sin(time * 1.9) * 0.015; const stem = Math.min(w, h) * 0.62; ctx.save(); ctx.translate(w * 0.86, h * 1.04); ctx.rotate(-0.28 + sway); ctx.fillStyle = wall; ctx.strokeStyle = wall; ctx.globalAlpha = 0.9; ctx.lineCap = "round"; ctx.lineWidth = stem * 0.015; ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(stem * 0.06, -stem * 0.5, 0, -stem); ctx.stroke(); for (let i = 0; i < 9; i++) { const t = 0.2 + i * 0.09; const y = -stem * t; const x = stem * 0.12 * t * (1 - t) * 2; const size = stem * (0.34 - t * 0.17); leaf(ctx, x, y, -0.55 + sway * 2, size); leaf(ctx, x, y, -Math.PI + 0.55 - sway * 2, size); } leaf(ctx, 0, -stem, -Math.PI / 2, stem * 0.16); ctx.restore(); } }; const frame = (now: number) => { const dt = last ? Math.min((now - last) / 1000, 0.05) * speed : 0; last = now; time += dt; const aim = cord ?? 0.5 + 0.24 * Math.sin(time * 0.22); open += (aim - open) * Math.min(1, dt * 5); draw(); raf = requestAnimationFrame(frame); }; const start = () => { if (still || raf) return; last = 0; raf = requestAnimationFrame(frame); }; const stop = () => { cancelAnimationFrame(raf); raf = 0; }; const resize = () => { w = canvas.clientWidth; h = canvas.clientHeight; if (!w || !h) return; // A quarter of the size on purpose: stretched back up, every shadow edge comes out soft. canvas.width = Math.max(1, Math.round(w / 4)); canvas.height = Math.max(1, Math.round(h / 4)); ctx.setTransform(1 / 4, 0, 0, 1 / 4, 0, 0); draw(); }; const pull = (e: PointerEvent) => { const box = canvas.getBoundingClientRect(); cord = 0.12 + 0.76 * (1 - (e.clientY - box.top) / box.height); }; const release = () => { cord = null; }; // Redraw on size change; only animate while on screen. const ro = new ResizeObserver(resize); const io = new IntersectionObserver(([e]) => (e.isIntersecting ? start() : stop())); ro.observe(canvas); io.observe(canvas); if (!still) { root.addEventListener("pointermove", pull); root.addEventListener("pointerleave", release); } return () => { stop(); ro.disconnect(); io.disconnect(); root.removeEventListener("pointermove", pull); root.removeEventListener("pointerleave", release); }; }, [bands, wall, light, plant, speed]); return (
{children}
); }