import React, { useMemo } from 'react'; import { SeasonalOverlayProps } from '../types'; import { animLanternBob, animLanternSway, animTasselSway, animGlowBreathe, animPetalFall, animPetalSway, animDragonDrift, animLacquerPulse, animEmberRise, } from './LunarNewYear.css'; // Deterministic pseudo-random so the scene is identical every mount (no React // state per frame). Large primes keep the distribution well spread. const rand = (seed: number): number => { const x = Math.sin(seed * 127.1 + 311.7) * 43758.5453; return x - Math.floor(x); }; // Core oklch palette — auspicious crimson/vermilion lanterns, imperial gold // trim and blossoms, over a deep lacquer-red ambient tint. Kept luminous and // gentle so everything reads as soft ambient glow, never solid paint. const CRIMSON = 'oklch(0.50 0.20 25)'; const VERMILION = 'oklch(0.58 0.21 30)'; const GOLD = 'oklch(0.82 0.14 85)'; const GOLD_HI = 'oklch(0.92 0.10 92)'; // A coiling dragon silhouette in imperial gold, rendered once as an inline SVG // data-URI so it costs a single GPU-composited layer (no DOM weight). The curve // is intentionally abstract and very subtle — a calligraphic ribbon-body with a // suggestion of a head, mane and tail arcing across the upper scene. const dragonUri = ((): string => { const svg = `` + `` + `` + `` + `` + `` + `` + `` + `` + // Sinuous body — a thick tapering serpentine ribbon. `` + // Inner highlight running along the body for a calligraphic sheen. `` + // Head + horn flourish at the leading end. `` + // Mane / whisker strokes flaring back from the head. `` + // Tail wisps. `` + ``; return `url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}")`; })(); type Lantern = { left: number; top: number; scale: number; bobDuration: number; swayDuration: number; delay: number; opacity: number; }; type Petal = { left: number; size: number; duration: number; delay: number; swayDuration: number; opacity: number; blur: number; hue: number; }; type Ember = { left: number; bottom: number; size: number; duration: number; delay: number; }; // A single five-petal plum blossom (gold), inline SVG so each petal sliver is // one cheap element. Returned as a data-URI background painted on a square. const blossomUri = ((): string => { const petals = Array.from({ length: 5 }, (_, i) => { const a = (i * 72 * Math.PI) / 180; const cx = 16 + Math.cos(a - Math.PI / 2) * 8; const cy = 16 + Math.sin(a - Math.PI / 2) * 8; return ``; }).join(''); const svg = `` + `${petals}` + `` + ``; return `url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}")`; })(); export function LunarNewYearOverlay({ reduced }: SeasonalOverlayProps) { // Paper lanterns strung across the upper third, gently staggered in depth. const lanterns = useMemo(() => { const slots = [ { left: 9, top: 7, scale: 1.0 }, { left: 27, top: 13, scale: 0.82 }, { left: 46, top: 6, scale: 1.12 }, { left: 64, top: 15, scale: 0.78 }, { left: 82, top: 9, scale: 0.95 }, { left: 92, top: 20, scale: 0.7 }, ]; return slots.map((s, i) => ({ left: s.left, top: s.top, scale: s.scale, bobDuration: 7 + rand(i + 1) * 4, swayDuration: 5.5 + rand(i + 4) * 3, delay: -rand(i + 7) * 6, opacity: 0.78 + rand(i + 2) * 0.18, })); }, []); // Drifting gold plum-blossom petals — two parallax bands (far small/dim/slow, // near large/bright/fast) for depth. const petals = useMemo(() => { const bands = [ { count: 9, size: [9, 14], dur: [15, 21], op: [0.4, 0.6], blur: 0.6 }, { count: 8, size: [15, 24], dur: [10, 14], op: [0.6, 0.85], blur: 0 }, ]; const out: Petal[] = []; let s = 1; bands.forEach((b) => { for (let i = 0; i < b.count; i += 1) { const r1 = rand(s); const r2 = rand(s + 0.37); const r3 = rand(s + 0.71); const r4 = rand(s + 0.91); out.push({ left: r1 * 100, size: b.size[0] + r2 * (b.size[1] - b.size[0]), duration: b.dur[0] + r3 * (b.dur[1] - b.dur[0]), delay: -r4 * (b.dur[1] + 4), swayDuration: 5 + r2 * 5, opacity: b.op[0] + r3 * (b.op[1] - b.op[0]), blur: b.blur, hue: 82 + r4 * 10, }); s += 1; } }); return out; }, []); // A few gold embers rising from the lanterns (motion scene only). const embers = useMemo( () => Array.from({ length: 7 }, (_, i) => ({ left: 8 + rand(i + 11) * 84, bottom: 8 + rand(i + 21) * 30, size: 1.6 + rand(i + 31) * 2.2, duration: 9 + rand(i + 41) * 6, delay: -rand(i + 51) * 12, })), [], ); return ( <> {/* Deep lacquer-red ambient wash — layered radial + linear oklch gradients for depth and a warm crimson lantern-glow from above. Low-opacity so chat text stays legible (WCAG-AA). */} {/* Imperial-gold dragon silhouette arcing across the upper scene — a single composited SVG layer, blurred and screen-blended so it reads as an ethereal gilt apparition, never a hard graphic. */} {/* Warm vignette frame — crimson edges, clear center, with a faint cheap backdrop-filter for a silken haze around the rim. */} {/* The garland string the lanterns hang from — a faint warm line. */} {/* Paper lanterns. Each is a hung group: a sway wrapper rotating about its mount, an inner bob, then the lantern body (glow + ribs + caps) and a trailing tassel. */} {lanterns.map((l, i) => { const W = 30 * l.scale; const H = 38 * l.scale; const cap = Math.max(8, W * 0.5); return ( {/* short cord from the string to the top cap */} {/* top gold cap */} {/* lantern body */} {/* breathing inner candle glow */} {/* vertical paper ribs */} {/* bottom gold cap */} {/* swaying silk tassel */} ); })} {/* Drifting gold plum-blossom petals (motion only). Static settled blossoms render below for the reduced/preview scene. */} {!reduced && petals.map((p, i) => ( ))} {/* Static settled blossoms for the reduced-motion / preview scene — a serene scatter so the thumbnail still reads as a blossom drift. */} {reduced && petals.slice(0, 12).map((p, i) => { const py = rand(i + 0.5) * 92 + 4; return ( ); })} {/* Gold embers rising off the lanterns (motion only). */} {!reduced && embers.map((e, i) => ( ))} > ); }