Files
cinny/src/app/components/seasonal/themes/LunarNewYear.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

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' },
});