import React, { useMemo } from 'react'; import { SeasonalOverlayProps } from '../types'; import { animGust, animLeafFall, animLeafFlutter, animLeafSway, leafBlade } from './Autumn.css'; /** * AutumnOverlay — falling leaves over the app as it is. * * A full-screen, pointer-events:none ambient decoration. The parent supplies a * fixed inset:0 overflow:hidden pointer-events:none container at the correct * z-index, so this component only returns absolutely-positioned aria-hidden * children and never sets position:fixed / z-index / pointer-events. * * [#237] Deliberately no full-screen tint, light shafts or vignette: they * turned the user's theme beige (muddy brown on dark) and lowered contrast * everywhere. The leaves carry the season on their own. * * Composition: * 1. maple / oak / aspen leaves drifting down with sway and a soft flutter * 2. every ~42 s a gust: a small group of leaves blows across the screen * All motion is transform/opacity only. When `reduced` is true: a few leaves * at rest in empty parts of the layout, no animation, no gust. The settings * preview always passes reduced=true, so the still form stands alone. */ // [#237] A real autumn palette: [body, tip highlight, vein]. The previous // muted tans read as beige smudges on the light theme. const LEAF_TONES: [string, string, string][] = [ ['oklch(0.56 0.19 30)', 'oklch(0.68 0.17 45)', 'oklch(0.38 0.12 28)'], // crimson ['oklch(0.68 0.17 52)', 'oklch(0.8 0.15 75)', 'oklch(0.45 0.12 45)'], // pumpkin orange ['oklch(0.76 0.15 80)', 'oklch(0.85 0.13 92)', 'oklch(0.52 0.1 65)'], // gold ['oklch(0.5 0.1 50)', 'oklch(0.62 0.11 60)', 'oklch(0.34 0.06 45)'], // russet brown ['oklch(0.62 0.18 40)', 'oklch(0.74 0.16 60)', 'oklch(0.42 0.12 35)'], // burnt orange ]; type LeafKind = 'maple' | 'oak' | 'aspen'; // Leaf silhouettes (viewBox 0 0 100 100, stem at the bottom). The maple is the // flag-style silhouette on purpose: its bold lobes stay recognisable at 30 px, // where the previous star-like outline read as a splat. const LEAF_SHAPES: Record = { maple: { body: 'M50 5 L54 20 L62 14 L60 30 L74 22 L70 36 L86 34 L78 46 L90 52 L74 56 L78 66 ' + 'L60 62 L56 72 L52 64 L52 94 L48 94 L48 64 L44 72 L40 62 L22 66 L26 56 L10 52 ' + 'L22 46 L14 34 L30 36 L26 22 L40 30 L38 14 L46 20 Z', veins: 'M50 66 L50 14 M50 58 L80 40 M50 58 L20 40 M50 64 L70 60 M50 64 L30 60', }, oak: { body: 'M50 4 C58 14 56 22 64 24 C74 22 74 32 68 36 C78 38 76 48 68 50 ' + 'C78 54 74 64 66 64 C70 74 60 78 54 72 C54 80 52 86 52 96 L48 96 ' + 'C48 86 46 80 46 72 C40 78 30 74 34 64 C26 64 22 54 32 50 ' + 'C24 48 22 38 32 36 C26 32 26 22 36 24 C44 22 42 14 50 4 Z', veins: 'M50 76 L50 12 M50 34 L64 28 M50 34 L36 28 M50 50 L66 44 M50 50 L34 44 M50 64 L62 60 M50 64 L38 60', }, aspen: { body: 'M50 8 C74 22 80 54 52 84 L52 96 L48 96 L48 84 C20 54 26 22 50 8 Z', veins: 'M50 84 L50 16 M50 40 L64 32 M50 40 L36 32 M50 58 L66 50 M50 58 L34 50', }, }; /** Build a CSS-ready data-URI of one leaf: gradient body, darker veins. */ function leafDataUri(kind: LeafKind, [body, tip, vein]: [string, string, string]): string { const shape = LEAF_SHAPES[kind]; const svg = `` + `` + `` + `` + `` + `` + ``; return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`; } type Leaf = { uri: string; left: number; size: number; duration: number; // s — fall time delay: number; // s swayDuration: number; flutterDuration: number; opacity: number; }; type GustLeaf = { uri: string; top: number; // % of height where it enters size: number; delay: number; // s within the gust opacity: number; }; /** Seconds between gusts, and when the first one comes after load. */ const GUST_PERIOD = 42; const GUST_FIRST = 8; // Leaves at rest for the reduced/static scene, in areas that are empty in the // layout (middle of the left icon rail, the space under the member list), // partly off-edge — never on the sidebar, members, composer or controls. const RESTING_LEAVES: ReadonlyArray<{ kind: LeafKind; left: number; top: number; size: number; tilt: number; tone: number; opacity: number; }> = [ { kind: 'maple', left: 0.5, top: 44, size: 60, tilt: 24, tone: 0, opacity: 0.62 }, { kind: 'aspen', left: 1.5, top: 54, size: 44, tilt: -30, tone: 2, opacity: 0.6 }, { kind: 'oak', left: 99, top: 62, size: 54, tilt: 160, tone: 3, opacity: 0.6 }, { kind: 'maple', left: 97.5, top: 72, size: 46, tilt: 200, tone: 4, opacity: 0.6 }, ]; // Composition for the Settings swatch (sizes are % of the tile width). const PREVIEW_LEAVES: ReadonlyArray<{ kind: LeafKind; left: number; top: number; size: number; tilt: number; tone: number; }> = [ { kind: 'maple', left: 30, top: 38, size: 42, tilt: -18, tone: 0 }, { kind: 'oak', left: 70, top: 30, size: 30, tilt: 34, tone: 3 }, { kind: 'aspen', left: 62, top: 70, size: 30, tilt: -60, tone: 2 }, { kind: 'maple', left: 86, top: 80, size: 24, tilt: 20, tone: 1 }, { kind: 'maple', left: 12, top: 82, size: 20, tilt: 40, tone: 4 }, ]; export function AutumnOverlay({ reduced, preview }: SeasonalOverlayProps) { // Deterministic field, computed ONCE. No per-frame state. const { leaves, gust } = useMemo(() => { const kinds = ['maple', 'oak', 'maple', 'aspen'] as const; const builtLeaves: Leaf[] = Array.from({ length: 12 }, (_, i) => { const depth = i % 4; // 0..3 → parallax bucket return { uri: leafDataUri(kinds[i % 4], LEAF_TONES[i % LEAF_TONES.length]), left: (i * 8.3 + 4) % 100, size: 28 + depth * 8, // 28..52 px // Nearer leaves fall a touch faster; all slow and calm. duration: 16 - depth * 1.6 + (i % 3) * 1.3, delay: -((i * 1.37) % 16), // negative → already mid-fall on load swayDuration: 5 + (i % 5) * 0.8, flutterDuration: 2 + (i % 4) * 0.5, // Nearer → bolder. Small, moving and semi-transparent, so text under a // passing leaf stays readable. opacity: 0.58 + depth * 0.05, }; }); const builtGust: GustLeaf[] = Array.from({ length: 5 }, (_, i) => ({ uri: leafDataUri(kinds[(i + 1) % 4], LEAF_TONES[(i * 2) % LEAF_TONES.length]), top: 30 + ((i * 13) % 40), size: 30 + (i % 3) * 10, delay: GUST_FIRST + i * 0.35, opacity: 0.7, })); return { leaves: builtLeaves, gust: builtGust }; }, []); if (preview) { // The Settings swatch: a warm backdrop (fine inside the tile, unlike over // the app) with a few leaves arranged across it. return ( <>