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>
110 lines
4.1 KiB
TypeScript
110 lines
4.1 KiB
TypeScript
import { keyframes } from '@vanilla-extract/css';
|
|
|
|
/**
|
|
* Lunar New Year overlay keyframes — red paper lanterns, drifting gold plum
|
|
* blossoms, and a coiling dragon. Every animation touches ONLY `transform` and
|
|
* `opacity`, so the compositor runs them on the GPU with zero layout/paint.
|
|
* keyframes() returns the generated animation-name string, applied inline by the
|
|
* component. Static structure (gradients, SVG data-URIs, geometry) lives in the
|
|
* component; this module is motion only.
|
|
*/
|
|
|
|
/**
|
|
* Lantern bob — a hung lantern rises a touch and sinks again on a long, lazy
|
|
* cycle, as if buoyed by warm air. translateY + a whisper of scale only; the
|
|
* per-lantern duration/delay desynchronise the swarm.
|
|
*/
|
|
export const animLanternBob = keyframes({
|
|
'0%': { transform: 'translate3d(0, 0, 0) scale(1)' },
|
|
'50%': { transform: 'translate3d(0, -2.2vh, 0) scale(1.015)' },
|
|
'100%': { transform: 'translate3d(0, 0, 0) scale(1)' },
|
|
});
|
|
|
|
/**
|
|
* Lantern pendulum — a gentle rotational sway about the top mount, so each
|
|
* lantern rocks like it hangs from a string. Pairs with the bob on a different
|
|
* period to read as organic drift rather than a metronome.
|
|
*/
|
|
export const animLanternSway = keyframes({
|
|
'0%': { transform: 'rotate(-2.4deg)' },
|
|
'50%': { transform: 'rotate(2.4deg)' },
|
|
'100%': { transform: 'rotate(-2.4deg)' },
|
|
});
|
|
|
|
/**
|
|
* Tassel sway — the silk tassel under a lantern trails its parent's motion with
|
|
* a wider, slightly lagging swing. transformOrigin is the top of the tassel.
|
|
*/
|
|
export const animTasselSway = keyframes({
|
|
'0%': { transform: 'rotate(5deg)' },
|
|
'50%': { transform: 'rotate(-5deg)' },
|
|
'100%': { transform: 'rotate(5deg)' },
|
|
});
|
|
|
|
/**
|
|
* Lantern inner glow — the warm light inside each lantern swells and dims, like
|
|
* a candle breathing. Opacity + scale only.
|
|
*/
|
|
export const animGlowBreathe = keyframes({
|
|
'0%': { transform: 'scale(0.94)', opacity: '0.55' },
|
|
'50%': { transform: 'scale(1.06)', opacity: '0.9' },
|
|
'100%': { transform: 'scale(0.94)', opacity: '0.55' },
|
|
});
|
|
|
|
/**
|
|
* Petal drift — a gold plum-blossom petal falls the full height while spinning
|
|
* and swaying. A tall translateY lets one keyframe set serve every petal;
|
|
* per-petal duration/delay/scale create the parallax variety.
|
|
*/
|
|
export const animPetalFall = keyframes({
|
|
'0%': { transform: 'translate3d(0, -10vh, 0) rotateZ(0deg) rotateY(0deg)', opacity: '0' },
|
|
'10%': { opacity: '0.9' },
|
|
'50%': { transform: 'translate3d(3vw, 52vh, 0) rotateZ(190deg) rotateY(180deg)' },
|
|
'90%': { opacity: '0.8' },
|
|
'100%': {
|
|
transform: 'translate3d(-2.4vw, 114vh, 0) rotateZ(380deg) rotateY(360deg)',
|
|
opacity: '0',
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Lateral petal sway on the wrapper, decoupled from the fall so the two combine
|
|
* into an organic wind-borne path rather than a straight drop.
|
|
*/
|
|
export const animPetalSway = keyframes({
|
|
'0%': { transform: 'translate3d(0, 0, 0)' },
|
|
'50%': { transform: 'translate3d(2.8vw, 0, 0)' },
|
|
'100%': { transform: 'translate3d(0, 0, 0)' },
|
|
});
|
|
|
|
/**
|
|
* Dragon drift — the gold dragon silhouette breathes and undulates almost
|
|
* imperceptibly across the scene. translate + scale + opacity only, very slow.
|
|
*/
|
|
export const animDragonDrift = keyframes({
|
|
'0%': { transform: 'translate3d(-2%, 0, 0) scale(1)', opacity: '0.42' },
|
|
'50%': { transform: 'translate3d(2%, -1%, 0) scale(1.04)', opacity: '0.6' },
|
|
'100%': { transform: 'translate3d(-2%, 0, 0) scale(1)', opacity: '0.42' },
|
|
});
|
|
|
|
/**
|
|
* Lacquer-tint breathing — a barely-there pulse of the warm red ambient wash so
|
|
* the static base feels alive without distracting motion.
|
|
*/
|
|
export const animLacquerPulse = keyframes({
|
|
'0%': { opacity: '0.82' },
|
|
'50%': { opacity: '1' },
|
|
'100%': { opacity: '0.82' },
|
|
});
|
|
|
|
/**
|
|
* Gold ember rise — tiny sparks of lantern light float gently upward and fade,
|
|
* like motes drifting off the flames. translateY + opacity only.
|
|
*/
|
|
export const animEmberRise = keyframes({
|
|
'0%': { transform: 'translate3d(0, 0, 0) scale(0.6)', opacity: '0' },
|
|
'15%': { opacity: '0.85' },
|
|
'80%': { opacity: '0.5' },
|
|
'100%': { transform: 'translate3d(0.6vw, -26vh, 0) scale(1)', opacity: '0' },
|
|
});
|