Files
cinny/src/app/components/seasonal/themes/Arcade.css.ts
T
jared 26f998d243
CI / Build & Quality Checks (push) Successful in 11m7s
CI / Trigger Desktop Build (push) Successful in 12s
feat(seasonal): redesign all 11 seasonal themes as modular per-theme overlays
Split the 808-line SeasonalEffect monolith into one self-contained module per
theme under seasonal/themes/ (<Theme>.tsx + <Theme>.css.ts), and gave every
theme a premium, research-backed redesign (one Opus agent per theme against a
shared brief). SeasonalEffect now just imports the 11 overlays and dispatches;
the orphaned shared Seasonal.css.ts is removed (each theme owns its keyframes).

Each overlay: layered oklch palettes, GPU-only animation (transform/opacity),
`contain: layout paint style` to kill repaint flicker, ≤~40-element perf budget,
particles seeded once via useMemo (no per-frame state), a gorgeous STATIC
prefers-reduced-motion form (the settings preview thumbnail), WCAG-AA-preserving
low opacities, and no new deps / no external assets (inline SVG data-URIs,
Tauri/CSP-safe).

Themes: Halloween, Christmas, New Year, Autumn, April Fools, Lunar New Year,
Valentines, St. Patrick's, Earth Day, Deep Space, Arcade.

Gates: tsc clean, ESLint clean, Prettier clean, build OK, 551 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 19:41:58 -04:00

122 lines
4.7 KiB
TypeScript

import { keyframes } from '@vanilla-extract/css';
/**
* Arcade overlay keyframes — retro synthwave CRT.
*
* Every animation touches ONLY `transform` and `opacity` so the compositor can
* run them on the GPU without triggering layout or paint. keyframes() returns
* the generated animation-name string, which is applied inline in Arcade.tsx.
*
* Motion philosophy: a neon perspective grid scrolls toward the viewer, a soft
* CRT scanline field breathes, the whole screen glows and flickers ever so
* faintly, sparse pixel sparkles drift up, and an "INSERT COIN" blip pulses.
* The grid scroll is done with a translateY on a tiled, perspective-projected
* plane — never background-position — so it rides the compositor.
*/
/**
* The neon grid plane is laid out twice its visible height and tiled with the
* horizontal rule lines. Translating it up by exactly one tile makes the lines
* appear to flow continuously toward the viewer (the horizon). Because the
* plane sits under a `perspective` transform, the lines also accelerate as they
* approach, giving a true receding-grid illusion. Pure transform.
*/
export const animGridScroll = keyframes({
'0%': { transform: 'translateZ(0) translateY(0)' },
'100%': { transform: 'translateZ(0) translateY(50%)' },
});
/**
* Slow vertical drift of the fine scanline field — a couple of pixels so the
* raster looks like it's gently rolling, the way a real CRT does. Transform
* only; the line texture itself never moves on the GPU's paint layer.
*/
export const animScanRoll = keyframes({
'0%': { transform: 'translate3d(0, 0, 0)' },
'100%': { transform: 'translate3d(0, 4px, 0)' },
});
/**
* The overall CRT screen-glow breathes: a barely-there opacity swell that keeps
* the static neon tint feeling alive and powered-on. Opacity only.
*/
export const animScreenGlow = keyframes({
'0%': { opacity: '0.72' },
'50%': { opacity: '1' },
'100%': { opacity: '0.72' },
});
/**
* A faint, irregular CRT brightness flicker laid over the glow — the classic
* unstable-tube shimmer. Kept extremely shallow so it never distracts or harms
* readability. Opacity only.
*/
export const animCrtFlicker = keyframes({
'0%': { opacity: '0.94' },
'12%': { opacity: '1' },
'20%': { opacity: '0.9' },
'34%': { opacity: '0.98' },
'52%': { opacity: '0.92' },
'70%': { opacity: '1' },
'83%': { opacity: '0.95' },
'100%': { opacity: '0.94' },
});
/**
* Chromatic-aberration twin: the magenta/cyan fringe layers nudge a sub-pixel
* apart and back so the edges shimmer with RGB split, like a misconverged tube.
* transform + opacity only.
*/
export const animChromaShift = keyframes({
'0%': { transform: 'translate3d(0, 0, 0)', opacity: '0.5' },
'50%': { transform: 'translate3d(1.5px, 0, 0)', opacity: '0.8' },
'100%': { transform: 'translate3d(0, 0, 0)', opacity: '0.5' },
});
/**
* Pixel sparkle drift: a tiny neon speck rises and twinkles like a coin-burst
* particle floating up off the grid. transform + opacity, single tall path.
*/
export const animSparkleDrift = keyframes({
'0%': { transform: 'translate3d(0, 0, 0) scale(0.6)', opacity: '0' },
'12%': { opacity: '1' },
'50%': { transform: 'translate3d(8px, -42vh, 0) scale(1)', opacity: '0.85' },
'78%': { transform: 'translate3d(-6px, -70vh, 0) scale(0.8)', opacity: '0.5' },
'92%': { opacity: '0.18' },
'100%': { transform: 'translate3d(6px, -92vh, 0) scale(0.55)', opacity: '0' },
});
/**
* Independent pixel twinkle layered on the drift so specks blink on/off like a
* low-res sprite. Stepped opacity for a crisp 8-bit feel.
*/
export const animSparkleTwinkle = keyframes({
'0%, 44%': { opacity: '1' },
'50%, 94%': { opacity: '0.35' },
'100%': { opacity: '1' },
});
/**
* "INSERT COIN" blink: the classic attract-mode pulse. Stepped so it reads as a
* hard retro blink rather than a soft fade, but with a brief bright swell.
* Opacity + a hair of scale for a CRT bloom feel.
*/
export const animCoinBlink = keyframes({
'0%': { opacity: '0.85', transform: 'translateX(-50%) scale(1)' },
'6%': { opacity: '1', transform: 'translateX(-50%) scale(1.015)' },
'12%': { opacity: '0.85', transform: 'translateX(-50%) scale(1)' },
'49%': { opacity: '0.85', transform: 'translateX(-50%) scale(1)' },
'50%': { opacity: '0', transform: 'translateX(-50%) scale(1)' },
'100%': { opacity: '0', transform: 'translateX(-50%) scale(1)' },
});
/**
* Score-blip pulse for the corner HUD glyph: a quick pop then settle, like a
* counter ticking up. transform + opacity.
*/
export const animScoreBlip = keyframes({
'0%': { opacity: '0.4', transform: 'scale(1)' },
'50%': { opacity: '0.85', transform: 'scale(1.12)' },
'100%': { opacity: '0.4', transform: 'scale(1)' },
});