26f998d243
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>
122 lines
4.7 KiB
TypeScript
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)' },
|
|
});
|