import React from 'react'; import { SeasonalOverlayProps } from '../types'; import { animBatBob, animBatFlap, animBatFlight, animCandle, animGhost, animGhostWobble, animSpiderBob, animSpiderClimb, animSpiderSway, animSwarmFlight, animWispPulse, animWispWander, animWitch, halloweenRoot, } from './Halloween.css'; /** * HalloweenOverlay — cobwebs, a dangling spider and bats, over the app as is. * * The parent supplies a fixed inset:0 overflow:hidden pointer-events:none * container; everything here is aria-hidden and absolutely positioned. * * [#239] No full-screen purple wash or blurred "moon": they tinted the whole * app and the moon glow sat over the member list, making it unreadable. * Colours are theme-aware via --hw-ink / --hw-web (see Halloween.css.ts). * * Composition: * 1. cobwebs in the two top corners (thin threads, readable in both themes) * 2. a spider on a thread in the empty space under the member list, bobbing * 3. every 30 s a small group of bats flaps across the top of the screen * `reduced`: webs and the spider at rest, no bats flying. * `preview`: a night-sky swatch for Settings. */ /** Corner web (corner at 0,0): radial threads + sagging catch-rings. */ function webPath(size = 100): string { const angles = [0, 16, 32, 48, 64, 80, 90].map((a) => (a * Math.PI) / 180); const spokes = angles .map((a) => `M0 0 L${(Math.cos(a) * size).toFixed(1)} ${(Math.sin(a) * size).toFixed(1)}`) .join(' '); const rings = [0.28, 0.48, 0.68, 0.88].map((f) => { const r = f * size; const pts = angles.map((a) => [Math.cos(a) * r, Math.sin(a) * r]); let d = `M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`; for (let i = 1; i < pts.length; i += 1) { const [x0, y0] = pts[i - 1]; const [x1, y1] = pts[i]; // Control point pulled toward the corner so each segment sags. const cx = ((x0 + x1) / 2) * 0.86; const cy = ((y0 + y1) / 2) * 0.86; d += ` Q${cx.toFixed(1)} ${cy.toFixed(1)} ${x1.toFixed(1)} ${y1.toFixed(1)}`; } return d; }); return `${spokes} ${rings.join(' ')}`; } const WEB = webPath(); function Cobweb({ corner, size }: { corner: 'left' | 'right'; size: number }) { return ( ); } // Classic bat silhouette, wings spread (viewBox 0 0 100 50). const BAT_PATH = 'M50 18 L46 10 L45 17 C40 17 36 19 33 23 C28 15 18 11 4 14 C12 18 14 23 12 29 ' + 'C7 27 3 28 0 31 C10 31 17 35 21 42 C25 36 31 34 37 36 C41 41 46 43 50 46 ' + 'C54 43 59 41 63 36 C69 34 75 36 79 42 C83 35 90 31 100 31 C97 28 93 27 88 29 ' + 'C86 23 88 18 96 14 C82 11 72 15 67 23 C64 19 60 17 55 17 L54 10 Z'; function Bat({ width, flap }: { width: number; flap?: number }) { return ( ); } function Spider({ size }: { size: number }) { const legs = [-1, 1] .flatMap((side) => [0, 1, 2, 3].map((i) => { const y = 38 + i * 7; const kx = 50 + side * (16 + i * 2); const fx = 50 + side * (30 + (i === 1 || i === 2 ? 6 : 0)); const fy = y + (i < 2 ? -8 : 10); return `M${50 + side * 7} ${y} Q${kx} ${y - 12} ${fx} ${fy}`; }), ) .join(' '); return ( ); } /** Spider hanging from the top edge; thread and body move together. */ function HangingSpider({ left, length, animate, size = 30, climb = false, }: { left: string; length: string; animate: boolean; size?: number; climb?: boolean; }) { let motion: string | undefined; if (animate) { motion = climb ? `${animSpiderClimb} 24s ease-in-out 6s infinite` : `${animSpiderBob} 7s ease-in-out infinite`; } return (