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 =
``;
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. */}
{/* 2. The neon perspective grid. A wide, tall plane is tilted away from
the viewer with `perspective` + rotateX so its rule lines recede to
a vanishing point at the top (the horizon). It lives in the lower
half of the screen — the "floor". The inner plane scrolls upward by
one tile via transform translateY, which reads as the grid flowing
toward the viewer. Pure transform; never background-position. */}
{/* 3. Horizon glow + neon horizon line. A soft synthwave sun-bloom sits
where the grid meets the sky, with a thin bright rule on top of it
to seal the vanishing point. Static (no motion) either way. */}
{/* 4. Drifting pixel sparkles / neon coin-burst specks. Tiny square
neon pixels rising off the grid and twinkling. The static scene uses
a small resting set instead. */}
{reduced
? RESTING_SPARKLES.map((s, i) => (
))
: sparkles.map((s, i) => (
))}
{/* 5. Fine CRT scanlines. A repeating 1px dark rule field over the whole
screen, gently rolling downward on the compositor (transform only).
Held faint so text stays crisp. The pattern is in a child taller
than the frame so the roll never reveals an edge. */}
{/* 6. Chromatic-aberration fringe. Two thin edge-glows — magenta and cyan —
offset a sub-pixel apart at the screen border so the frame shimmers
with an RGB split, like a misconverged tube. Animated only; in the
static scene it sits as a steady fringe. */}
{/* 7a. Glowing "INSERT COIN" attract-mode blip, low-opacity, bottom-center.
Static scene shows it steady (no blink). */}
INSERT COIN
{/* 7b. Corner SCORE HUD glyph — a tiny pixel score that blips, top-left,
very low opacity so it reads as ambient chrome, not UI. */}
1UP 000000
{/* 8. CRT vignette + screen-glow. A radial darkening frames the corners,
with a faint magenta tube-glow swell. The vignette protects central
chat-text contrast. Static scene holds it steady; live scene adds a
shallow breathing glow + irregular flicker, both opacity-only. */}
>
);
}