"use client"; import * as React from "react"; import { useInViewOnce } from "./useInViewOnce"; const CELL = 16; const DURATION_MS = 1700; /** * BitReveal: covers whatever lies beneath it with a field of binary digits that * dissolves cell by cell once it scrolls into view, so the picture underneath * appears to assemble itself out of bits. Without JavaScript nothing is covered. */ export const BitReveal: React.FC<{ /** Colour of the covering cells; should match the surface behind the picture. */ surface?: string; }> = ({ surface = "#0f172a" }) => { const { ref, seen, still } = useInViewOnce(0.25); const [done, setDone] = React.useState(false); // Cover the picture as soon as the script runs, before it is scrolled into view. React.useEffect(() => { const canvas = ref.current; const ctx = canvas?.getContext("2d"); if (!canvas || !ctx) return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setDone(true); return; } canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; ctx.fillStyle = surface; ctx.fillRect(0, 0, canvas.width, canvas.height); }, [ref, surface]); React.useEffect(() => { const canvas = ref.current; const ctx = canvas?.getContext("2d"); if (!seen || still || !canvas || !ctx) return; const width = (canvas.width = canvas.offsetWidth); const height = (canvas.height = canvas.offsetHeight); const cols = Math.ceil(width / CELL); const rows = Math.ceil(height / CELL); // Each cell disappears at its own moment; cells further right go a little later. const when = new Float32Array(cols * rows); const bit = new Uint8Array(cols * rows); for (let i = 0; i < when.length; i++) { const col = i % cols; when[i] = Math.random() * 0.7 + (col / cols) * 0.3; bit[i] = Math.random() < 0.5 ? 0 : 1; } ctx.font = `11px ui-monospace, Menlo, monospace`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; let raf = 0; const start = performance.now(); const frame = (now: number) => { const progress = Math.min(1, (now - start) / DURATION_MS); ctx.clearRect(0, 0, width, height); for (let i = 0; i < when.length; i++) { const left = (when[i] ?? 0) - progress; if (left <= 0) continue; const x = (i % cols) * CELL; const y = Math.floor(i / cols) * CELL; ctx.fillStyle = surface; ctx.fillRect(x, y, CELL, CELL); // Cells about to vanish light up first. ctx.fillStyle = left < 0.08 ? "#f8fafc" : "#475569"; if (Math.random() < 0.02) bit[i] = bit[i] === 1 ? 0 : 1; ctx.fillText(String(bit[i] ?? 0), x + CELL / 2, y + CELL / 2); } if (progress < 1) raf = requestAnimationFrame(frame); else setDone(true); }; raf = requestAnimationFrame(frame); return () => cancelAnimationFrame(raf); }, [ref, seen, still, surface]); if (done) return null; return (