import React, { useMemo } from 'react'; import { SeasonalOverlayProps } from '../types'; import { animGridScroll, animScanRoll, animScreenGlow, animCrtFlicker, animChromaShift, animSparkleDrift, animSparkleTwinkle, animCoinBlink, animScoreBlip, } from './Arcade.css'; /** * ArcadeOverlay — retro synthwave CRT. * * A full-screen, pointer-events:none ambient decoration. The parent supplies a * fixed inset:0 overflow:hidden pointer-events:none container at the correct * z-index, so this component only returns absolutely-positioned aria-hidden * children and never sets position:fixed / z-index / pointer-events. * * Composition (back to front): * 1. near-black synthwave ambient wash (magenta sky-glow up top, cyan/purple * pool toward the floor) — layered oklch gradients for depth * 2. a neon perspective grid receding to a vanishing point on the horizon, * scrolling toward the viewer via transform translateY (never bg-position) * 3. a soft horizon sun-glow + thin neon horizon line where the grid meets sky * 4. drifting pixel sparkles / neon coin-burst specks rising off the grid * 5. fine CRT scanlines, gently rolling * 6. a faint chromatic-aberration fringe at the screen edges * 7. a glowing "INSERT COIN" blip + a corner SCORE HUD glyph * 8. a CRT vignette + screen-glow that frames and protects central text * * All motion is transform/opacity only (compositor-friendly). When `reduced` is * true we render a static-but-gorgeous scene: a still neon grid, steady * scanlines + vignette, and a steady "INSERT COIN" — no `animation` anywhere, * no flicker. The settings preview always passes reduced=true, so the still * form stands on its own. */ // Synthwave neon palette in oklch. Saturated where it glows, but every layer is // held at low opacity so it tints rather than takes over the chat beneath. const NEON_MAGENTA = 'oklch(0.65 0.25 350)'; const NEON_CYAN = 'oklch(0.80 0.15 200)'; const GRID_PURPLE = 'oklch(0.45 0.18 300)'; // The receding grid as an inline SVG data-URI (CSP-safe, no external assets). // It is a 1x2 vertical tile of horizontal rule lines + a single set of vertical // lines fanning toward a top-center vanishing point. The plane is then placed // under a CSS `perspective` rotateX so the lines genuinely recede. Scrolling the // tile up by one tile-height (animGridScroll → translateY 50%) loops seamlessly. function gridDataUri(): string { const lines: string[] = []; // Horizontal rules — denser toward the top (the horizon) for a perspective // feel even before the CSS rotateX is applied. const rows = [0, 16, 34, 54, 76, 100, 126, 156, 190, 228, 270, 316, 366, 420, 478, 540]; rows.forEach((y) => { lines.push( ``, ); }); // Vertical lines fanning out from the top-center vanishing point. for (let i = -7; i <= 7; i += 1) { const topX = 300 + i * 6; // tight near the horizon const botX = 300 + i * 95; // wide at the foreground lines.push( ``, ); } const svg = `${lines.join('')}`; return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`; } type Sparkle = { left: number; // vw bottom: number; // % up from floor where it spawns size: number; // px duration: number; // s delay: number; // s twinkle: number; // s hue: 'magenta' | 'cyan'; opacity: number; }; // Hand-placed still sparkles for the reduced/static scene — a few neon specks // resting low over the grid, away from the busy chat center. const RESTING_SPARKLES: ReadonlyArray<{ left: number; bottom: number; size: number; hue: 'magenta' | 'cyan'; opacity: number; }> = [ { left: 12, bottom: 18, size: 4, hue: 'cyan', opacity: 0.5 }, { left: 26, bottom: 30, size: 3, hue: 'magenta', opacity: 0.42 }, { left: 78, bottom: 22, size: 4, hue: 'magenta', opacity: 0.5 }, { left: 88, bottom: 34, size: 3, hue: 'cyan', opacity: 0.4 }, { left: 50, bottom: 14, size: 3, hue: 'cyan', opacity: 0.38 }, ]; const GRID_URI = gridDataUri(); export function ArcadeOverlay({ reduced }: SeasonalOverlayProps) { // Deterministic sparkle field, computed ONCE. No per-frame state. const sparkles = useMemo(() => { const COUNT = 16; return Array.from({ length: COUNT }, (_, i) => ({ left: (i * 6.27 + 4) % 100, bottom: (i * 3.7) % 28, // spawn in the lower third (over the grid) size: 2 + (i % 3), // 2..4 px pixels duration: 14 + (i % 6) * 2.2, delay: -((i * 1.83) % 16), twinkle: 1.4 + (i % 4) * 0.5, hue: i % 2 === 0 ? 'cyan' : 'magenta', opacity: 0.45 + (i % 3) * 0.12, })); }, []); const sparkleColor = (hue: 'magenta' | 'cyan') => (hue === 'cyan' ? NEON_CYAN : NEON_MAGENTA); return ( <> {/* 1. Near-black synthwave ambient wash. Magenta sky-glow up top, a cyan/purple pool toward the floor, and an overall dark vertical grade. Layered oklch gradients give depth at very low opacity. */}