import { useEffect, useRef } from 'react'; /* Matrix code-rain canvas — faithful port of /opt/boolab MatrixRain.jsx. Amber head (#fbbf24) → orange (#f97316) → deep-rust (#7a3d14) trail, COL_WIDTH 14, throttled to ~24fps, dpr capped at 2, visibilitychange-paused, and composited with destination-out fade so the canvas stays transparent and overlays the UI without dimming it. Mounted by ThemeFx only when the bc-anim-on gate is on (toggle ON *and* not prefers-reduced-motion). */ const CHARSET = (() => { let s = ''; for (let c = 0x41; c <= 0x5a; c++) s += String.fromCharCode(c); // A–Z for (let c = 0x30; c <= 0x39; c++) s += String.fromCharCode(c); // 0–9 for (let c = 0xff66; c <= 0xff9d; c++) s += String.fromCharCode(c); // half-width katakana return s; })(); const COL_WIDTH = 14; const TARGET_FPS = 24; const FRAME_BUDGET = 1000 / TARGET_FPS; const HEAD_COLOR = '#fbbf24'; // amber const SECONDARY_COLOR = '#f97316'; // orange const DEEP_COLOR = '#7a3d14'; // deep rust function randChar(): string { return CHARSET.charAt((Math.random() * CHARSET.length) | 0); } interface Column { y: number; step: number; } interface Props { enabled?: boolean; density?: number; speed?: number; opacity?: number; } export function MatrixRain({ enabled = true, density = 0.35, speed = 0.7, opacity = 0.6, }: Props) { const canvasRef = useRef(null); useEffect(() => { if (!enabled) return undefined; const canvas = canvasRef.current; if (!canvas) return undefined; const ctx = canvas.getContext('2d', { alpha: true }); if (!ctx) return undefined; let width = 0; let height = 0; let columns = 0; let cols: Column[] = []; let rafId: number | null = null; let lastTime = performance.now(); let resizeTimer: number | null = null; const setupCanvas = () => { const dpr = Math.min(window.devicePixelRatio || 1, 2); // Measure the element's own CSS box (width:100% + fixed inset:0) rather // than window.innerWidth — innerWidth includes the scrollbar gutter and // would over-size the bitmap on scrollbar-present layouts. width = canvas.clientWidth || window.innerWidth; height = canvas.clientHeight || window.innerHeight; canvas.width = Math.floor(width * dpr); canvas.height = Math.floor(height * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.font = `${COL_WIDTH}px monospace`; ctx.textBaseline = 'top'; columns = Math.max(1, Math.floor((width / COL_WIDTH) * density)); cols = new Array(columns); const rowsApprox = Math.ceil(height / COL_WIDTH) + 2; for (let i = 0; i < columns; i++) { cols[i] = { y: -Math.floor(Math.random() * rowsApprox), step: 0.5 + Math.random() * 0.8, }; } // Keep the canvas transparent so it overlays the UI without dimming it. ctx.clearRect(0, 0, width, height); }; const draw = () => { // Fade older chars toward transparent (destination-out) instead of // filling with a solid trail colour — keeps the canvas alpha-composited. ctx.globalCompositeOperation = 'destination-out'; ctx.fillStyle = 'rgba(0, 0, 0, 0.08)'; ctx.fillRect(0, 0, width, height); ctx.globalCompositeOperation = 'source-over'; const effSpeed = Math.max(0.1, speed); const colPitch = columns > 0 ? width / columns : COL_WIDTH; for (let i = 0; i < columns; i++) { const c = cols[i]; if (!c) continue; const x = Math.floor(i * colPitch); const yPx = Math.floor(c.y) * COL_WIDTH; if (c.y >= 0) { ctx.fillStyle = HEAD_COLOR; ctx.fillText(randChar(), x, yPx); if (c.y >= 1) { ctx.fillStyle = SECONDARY_COLOR; ctx.fillText(randChar(), x, yPx - COL_WIDTH); } if (c.y >= 2) { ctx.fillStyle = DEEP_COLOR; ctx.fillText(randChar(), x, yPx - COL_WIDTH * 2); } } c.y += c.step * effSpeed; const passedBottom = c.y * COL_WIDTH > height; if (passedBottom && (Math.random() < 0.025 || c.y * COL_WIDTH > height + COL_WIDTH * 20)) { c.y = -1 - Math.floor(Math.random() * 8); c.step = 0.5 + Math.random() * 0.8; } } }; const loop = (now: number) => { rafId = window.requestAnimationFrame(loop); if (document.hidden) return; const elapsed = now - lastTime; if (elapsed < FRAME_BUDGET) return; lastTime = now - (elapsed % FRAME_BUDGET); draw(); }; const start = () => { if (rafId != null) return; lastTime = performance.now(); rafId = window.requestAnimationFrame(loop); }; const stop = () => { if (rafId != null) { window.cancelAnimationFrame(rafId); rafId = null; } }; const onVisibility = () => { if (document.hidden) stop(); else start(); }; const onResize = () => { if (resizeTimer) window.clearTimeout(resizeTimer); resizeTimer = window.setTimeout(setupCanvas, 150); }; setupCanvas(); if (!document.hidden) start(); document.addEventListener('visibilitychange', onVisibility); window.addEventListener('resize', onResize); return () => { stop(); if (resizeTimer) window.clearTimeout(resizeTimer); document.removeEventListener('visibilitychange', onVisibility); window.removeEventListener('resize', onResize); }; }, [enabled, density, speed]); if (!enabled) return null; return (