2026-06-14 00:33:04 -04:00
|
|
|
|
import React, { useMemo } from 'react';
|
|
|
|
|
|
import { useAtomValue } from 'jotai';
|
|
|
|
|
|
import { settingsAtom } from '../../state/settings';
|
|
|
|
|
|
import {
|
|
|
|
|
|
animSeasonFall,
|
|
|
|
|
|
animLeafFall,
|
|
|
|
|
|
animFloatUp,
|
|
|
|
|
|
animBob,
|
|
|
|
|
|
animTasselSway,
|
|
|
|
|
|
animGlitch,
|
|
|
|
|
|
animGlitchColor,
|
|
|
|
|
|
animGlitchScan,
|
|
|
|
|
|
animBurst,
|
|
|
|
|
|
animWarp,
|
|
|
|
|
|
animScanline,
|
|
|
|
|
|
animPixelBlink,
|
|
|
|
|
|
animGoldShimmer,
|
|
|
|
|
|
animCloverDrift,
|
|
|
|
|
|
animEarthLeafDrift,
|
|
|
|
|
|
} from './Seasonal.css';
|
|
|
|
|
|
|
|
|
|
|
|
export type SeasonTheme =
|
|
|
|
|
|
| 'halloween'
|
|
|
|
|
|
| 'christmas'
|
|
|
|
|
|
| 'newyear'
|
|
|
|
|
|
| 'autumn'
|
|
|
|
|
|
| 'aprilfools'
|
|
|
|
|
|
| 'lunar'
|
|
|
|
|
|
| 'valentines'
|
|
|
|
|
|
| 'stpatricks'
|
|
|
|
|
|
| 'earthday'
|
|
|
|
|
|
| 'deepspace'
|
|
|
|
|
|
| 'arcade';
|
|
|
|
|
|
|
|
|
|
|
|
function getActiveSeason(now: Date): SeasonTheme | null {
|
|
|
|
|
|
const m = now.getMonth() + 1; // 1-12
|
|
|
|
|
|
const d = now.getDate();
|
|
|
|
|
|
|
|
|
|
|
|
// New Year takes highest priority (Dec 31 – Jan 2)
|
|
|
|
|
|
if ((m === 12 && d === 31) || (m === 1 && d <= 2)) return 'newyear';
|
|
|
|
|
|
// Valentine's Day (Feb 10–15)
|
|
|
|
|
|
if (m === 2 && d >= 10 && d <= 15) return 'valentines';
|
|
|
|
|
|
// St. Patrick's Day (March 15–18)
|
|
|
|
|
|
if (m === 3 && d >= 15 && d <= 18) return 'stpatricks';
|
|
|
|
|
|
// April Fool's (April 1)
|
|
|
|
|
|
if (m === 4 && d === 1) return 'aprilfools';
|
|
|
|
|
|
// Earth Day (April 20–23)
|
|
|
|
|
|
if (m === 4 && d >= 20 && d <= 23) return 'earthday';
|
|
|
|
|
|
// Lunar New Year (Jan 22 – Feb 5, approximate fixed window)
|
|
|
|
|
|
if ((m === 1 && d >= 22) || (m === 2 && d <= 5)) return 'lunar';
|
|
|
|
|
|
// International Video Game Day (Sept 12)
|
|
|
|
|
|
if (m === 9 && d === 12) return 'arcade';
|
|
|
|
|
|
// World Space Week (Oct 4–10)
|
|
|
|
|
|
if (m === 10 && d >= 4 && d <= 10) return 'deepspace';
|
|
|
|
|
|
// Halloween (Oct 15 – Nov 1)
|
|
|
|
|
|
if ((m === 10 && d >= 15) || (m === 11 && d === 1)) return 'halloween';
|
|
|
|
|
|
// Christmas (Dec 10–30)
|
|
|
|
|
|
if (m === 12 && d >= 10) return 'christmas';
|
|
|
|
|
|
// Autumn (Sept 21 – Oct 31, excluding Halloween/Deep Space windows above)
|
|
|
|
|
|
if ((m === 9 && d >= 21) || (m === 10 && d <= 14)) return 'autumn';
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Individual theme overlays ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function HalloweenOverlay({ reduced }: { reduced: boolean }) {
|
|
|
|
|
|
const particles = Array.from({ length: 22 });
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Dark purple ambient tint */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundColor: 'rgba(25,0,45,0.22)',
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
'radial-gradient(ellipse at 50% 50%, rgba(100,0,180,0.08) 0%, transparent 70%)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Spider web corners */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
width: '160px',
|
|
|
|
|
|
height: '160px',
|
|
|
|
|
|
backgroundImage: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'><g stroke='rgba(180,120,255,0.35)' stroke-width='0.7' fill='none'><line x1='0' y1='0' x2='80' y2='80'/><line x1='40' y1='0' x2='80' y2='80'/><line x1='80' y1='0' x2='80' y2='80'/><line x1='0' y1='40' x2='80' y2='80'/><line x1='0' y1='80' x2='80' y2='80'/><ellipse cx='80' cy='80' rx='20' ry='20'/><ellipse cx='80' cy='80' rx='40' ry='40'/><ellipse cx='80' cy='80' rx='60' ry='60'/><ellipse cx='80' cy='80' rx='80' ry='80'/></g></svg>")`,
|
|
|
|
|
|
backgroundRepeat: 'no-repeat',
|
|
|
|
|
|
opacity: 0.7,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Falling purple/orange particles */}
|
|
|
|
|
|
{!reduced &&
|
|
|
|
|
|
particles.map((_, i) => {
|
|
|
|
|
|
const isOrange = i % 3 === 0;
|
|
|
|
|
|
const size = 4 + (i % 3) * 2;
|
|
|
|
|
|
const left = ((i * 4597 + 137) % 100);
|
|
|
|
|
|
const duration = 8 + (i % 7) * 1.5;
|
|
|
|
|
|
const delay = (i * 0.45) % 7;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={i}
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: '-8px',
|
|
|
|
|
|
left: `${left}%`,
|
|
|
|
|
|
width: `${size}px`,
|
|
|
|
|
|
height: `${size}px`,
|
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
|
backgroundColor: isOrange ? 'rgba(255,100,0,0.75)' : 'rgba(160,0,255,0.7)',
|
|
|
|
|
|
boxShadow: isOrange
|
|
|
|
|
|
? '0 0 8px rgba(255,100,0,0.5)'
|
|
|
|
|
|
: '0 0 8px rgba(160,0,255,0.5)',
|
|
|
|
|
|
animation: `${animSeasonFall} ${duration}s ease-in ${delay}s infinite`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ChristmasOverlay({ reduced }: { reduced: boolean }) {
|
|
|
|
|
|
const flakes = Array.from({ length: 28 });
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
'radial-gradient(ellipse at 50% 0%, rgba(220,240,255,0.06) 0%, transparent 60%)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{!reduced &&
|
|
|
|
|
|
flakes.map((_, i) => {
|
|
|
|
|
|
const size = 3 + (i % 4) * 2;
|
|
|
|
|
|
const left = ((i * 3571 + 251) % 100);
|
|
|
|
|
|
const duration = 10 + (i % 8) * 2;
|
|
|
|
|
|
const delay = (i * 0.55) % 10;
|
|
|
|
|
|
const drift = ((i % 5) - 2) * 12;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={i}
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: '-10px',
|
|
|
|
|
|
left: `${left}%`,
|
|
|
|
|
|
width: `${size}px`,
|
|
|
|
|
|
height: `${size}px`,
|
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
|
backgroundColor: 'rgba(255,255,255,0.82)',
|
|
|
|
|
|
boxShadow: '0 0 4px rgba(200,230,255,0.6)',
|
|
|
|
|
|
animation: `${animSeasonFall} ${duration}s ease-in ${delay}s infinite`,
|
|
|
|
|
|
transform: `translateX(${drift}px)`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function NewYearOverlay({ reduced }: { reduced: boolean }) {
|
|
|
|
|
|
const bursts = [
|
|
|
|
|
|
{ x: 20, y: 25, color: '#ffd700', delay: 0 },
|
|
|
|
|
|
{ x: 75, y: 15, color: '#ff4466', delay: 1.2 },
|
|
|
|
|
|
{ x: 50, y: 35, color: '#00d4ff', delay: 2.4 },
|
|
|
|
|
|
{ x: 15, y: 60, color: '#ffd700', delay: 3.6 },
|
|
|
|
|
|
{ x: 85, y: 45, color: '#aa44ff', delay: 0.8 },
|
|
|
|
|
|
{ x: 40, y: 20, color: '#ff8800', delay: 2.0 },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const petals = Array.from({ length: 8 });
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundColor: 'rgba(10,5,0,0.15)',
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
'radial-gradient(ellipse at 50% 50%, rgba(255,200,0,0.04) 0%, transparent 70%)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{!reduced &&
|
|
|
|
|
|
bursts.map((b, bi) =>
|
|
|
|
|
|
petals.map((_, pi) => {
|
|
|
|
|
|
const angle = (pi / petals.length) * 360;
|
|
|
|
|
|
const dist = 80 + (pi % 3) * 30;
|
|
|
|
|
|
const duration = 1.6 + (bi % 3) * 0.4;
|
|
|
|
|
|
const period = 3.5 + bi * 0.8;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={`${bi}-${pi}`}
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
left: `${b.x}%`,
|
|
|
|
|
|
top: `${b.y}%`,
|
|
|
|
|
|
width: `${dist}px`,
|
|
|
|
|
|
height: '2px',
|
|
|
|
|
|
backgroundColor: b.color,
|
|
|
|
|
|
boxShadow: `0 0 6px ${b.color}`,
|
|
|
|
|
|
transformOrigin: '0 50%',
|
|
|
|
|
|
transform: `rotate(${angle}deg)`,
|
|
|
|
|
|
animation: `${animBurst} ${duration}s ease-out ${b.delay + pi * 0.05}s ${period}s infinite`,
|
|
|
|
|
|
borderRadius: '1px',
|
|
|
|
|
|
opacity: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
})
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* Gold shimmer overlay */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
'linear-gradient(105deg, transparent 30%, rgba(255,215,0,0.06) 50%, transparent 70%)',
|
|
|
|
|
|
backgroundSize: '200% 100%',
|
|
|
|
|
|
animation: reduced ? 'none' : `${animGoldShimmer} 4s linear infinite`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function AutumnOverlay({ reduced }: { reduced: boolean }) {
|
|
|
|
|
|
const leaves = Array.from({ length: 18 });
|
|
|
|
|
|
const colors = [
|
|
|
|
|
|
'rgba(220,80,20,0.75)',
|
|
|
|
|
|
'rgba(200,120,0,0.7)',
|
|
|
|
|
|
'rgba(180,50,10,0.7)',
|
|
|
|
|
|
'rgba(230,150,0,0.65)',
|
|
|
|
|
|
'rgba(160,80,0,0.6)',
|
|
|
|
|
|
];
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
'radial-gradient(ellipse at 50% 100%, rgba(180,80,0,0.06) 0%, transparent 60%)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{!reduced &&
|
|
|
|
|
|
leaves.map((_, i) => {
|
|
|
|
|
|
const left = ((i * 5381 + 179) % 100);
|
|
|
|
|
|
const duration = 12 + (i % 6) * 2;
|
|
|
|
|
|
const delay = (i * 0.65) % 12;
|
|
|
|
|
|
const size = 10 + (i % 4) * 4;
|
|
|
|
|
|
const color = colors[i % colors.length];
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={i}
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: '-15px',
|
|
|
|
|
|
left: `${left}%`,
|
|
|
|
|
|
width: `${size}px`,
|
|
|
|
|
|
height: `${size * 0.7}px`,
|
|
|
|
|
|
borderRadius: '50% 0 50% 0',
|
|
|
|
|
|
backgroundColor: color,
|
|
|
|
|
|
boxShadow: `0 0 4px ${color}`,
|
|
|
|
|
|
animation: `${animLeafFall} ${duration}s ease-in ${delay}s infinite`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function AprilFoolsOverlay({ reduced }: { reduced: boolean }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* RGB channel separation layers */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundColor: 'rgba(255,0,0,0.04)',
|
|
|
|
|
|
transform: 'translateX(2px)',
|
|
|
|
|
|
mixBlendMode: 'multiply',
|
|
|
|
|
|
animation: reduced ? 'none' : `${animGlitch} 5s step-end infinite`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundColor: 'rgba(0,255,255,0.04)',
|
|
|
|
|
|
transform: 'translateX(-2px)',
|
|
|
|
|
|
mixBlendMode: 'multiply',
|
|
|
|
|
|
animation: reduced ? 'none' : `${animGlitch} 5s step-end 0.3s infinite`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Color corruption */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
animation: reduced ? 'none' : `${animGlitchColor} 7s step-end infinite`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Sweeping scanline */}
|
|
|
|
|
|
{!reduced && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
right: 0,
|
|
|
|
|
|
height: '3px',
|
|
|
|
|
|
backgroundColor: 'rgba(0,255,136,0.35)',
|
|
|
|
|
|
boxShadow: '0 0 8px rgba(0,255,136,0.5)',
|
|
|
|
|
|
animation: `${animGlitchScan} 2.8s linear infinite`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* "ERROR" watermark */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: '50%',
|
|
|
|
|
|
left: '50%',
|
|
|
|
|
|
transform: 'translate(-50%, -50%) rotate(-15deg)',
|
|
|
|
|
|
fontSize: '80px',
|
|
|
|
|
|
fontWeight: 900,
|
|
|
|
|
|
fontFamily: 'monospace',
|
|
|
|
|
|
color: 'rgba(255,0,0,0.07)',
|
|
|
|
|
|
letterSpacing: '0.1em',
|
|
|
|
|
|
pointerEvents: 'none',
|
|
|
|
|
|
userSelect: 'none',
|
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
SIGNAL LOST
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function LunarNewYearOverlay({ reduced }: { reduced: boolean }) {
|
|
|
|
|
|
const lanterns = Array.from({ length: 9 });
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Silk-like texture overlay */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundColor: 'rgba(140,0,0,0.08)',
|
|
|
|
|
|
backgroundImage: [
|
|
|
|
|
|
'repeating-linear-gradient(45deg, rgba(200,20,0,0.03) 0px, rgba(200,20,0,0.03) 1px, transparent 1px, transparent 8px)',
|
|
|
|
|
|
'repeating-linear-gradient(135deg, rgba(200,20,0,0.03) 0px, rgba(200,20,0,0.03) 1px, transparent 1px, transparent 8px)',
|
|
|
|
|
|
].join(','),
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Gold shimmer sweep */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
'linear-gradient(100deg, transparent 25%, rgba(255,200,0,0.07) 45%, rgba(255,220,50,0.1) 50%, rgba(255,200,0,0.07) 55%, transparent 75%)',
|
|
|
|
|
|
backgroundSize: '300% 100%',
|
|
|
|
|
|
animation: reduced ? 'none' : `${animGoldShimmer} 5s linear infinite`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Floating paper lanterns */}
|
|
|
|
|
|
{lanterns.map((_, i) => {
|
|
|
|
|
|
const left = 5 + ((i * 4603 + 311) % 90);
|
|
|
|
|
|
const top = 8 + ((i * 2311 + 97) % 55);
|
|
|
|
|
|
const duration = 3.5 + (i % 4) * 0.7;
|
|
|
|
|
|
const delay = i * 0.5;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={i}
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
left: `${left}%`,
|
|
|
|
|
|
top: `${top}%`,
|
|
|
|
|
|
animation: reduced ? 'none' : `${animBob} ${duration}s ease-in-out ${delay}s infinite`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Lantern top cap */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '18px',
|
|
|
|
|
|
height: '5px',
|
|
|
|
|
|
backgroundColor: '#ffd700',
|
|
|
|
|
|
borderRadius: '2px',
|
|
|
|
|
|
margin: '0 auto',
|
|
|
|
|
|
boxShadow: '0 0 4px rgba(255,215,0,0.6)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Lantern body */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '24px',
|
|
|
|
|
|
height: '32px',
|
|
|
|
|
|
backgroundColor: '#cc0000',
|
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
|
border: '1.5px solid #ffd700',
|
|
|
|
|
|
boxShadow: '0 0 14px rgba(200,0,0,0.5), inset 0 0 10px rgba(255,200,0,0.2)',
|
|
|
|
|
|
margin: '1px auto',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Lantern bottom cap */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '18px',
|
|
|
|
|
|
height: '5px',
|
|
|
|
|
|
backgroundColor: '#ffd700',
|
|
|
|
|
|
borderRadius: '2px',
|
|
|
|
|
|
margin: '0 auto',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Tassel */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '2px',
|
|
|
|
|
|
height: '14px',
|
|
|
|
|
|
backgroundColor: '#ffd700',
|
|
|
|
|
|
margin: '0 auto',
|
|
|
|
|
|
animation: reduced ? 'none' : `${animTasselSway} ${duration * 0.6}s ease-in-out ${delay}s infinite`,
|
|
|
|
|
|
transformOrigin: 'top center',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ValentinesOverlay({ reduced }: { reduced: boolean }) {
|
|
|
|
|
|
const hearts = Array.from({ length: 18 });
|
|
|
|
|
|
const colors = [
|
|
|
|
|
|
'rgba(255,100,140,0.8)',
|
|
|
|
|
|
'rgba(255,150,180,0.65)',
|
|
|
|
|
|
'rgba(220,70,110,0.7)',
|
|
|
|
|
|
'rgba(255,180,200,0.55)',
|
|
|
|
|
|
];
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
'radial-gradient(ellipse at 50% 100%, rgba(255,100,140,0.06) 0%, transparent 55%)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{!reduced &&
|
|
|
|
|
|
hearts.map((_, i) => {
|
|
|
|
|
|
const left = 3 + ((i * 6271 + 443) % 94);
|
|
|
|
|
|
const duration = 9 + (i % 6) * 1.8;
|
|
|
|
|
|
const delay = (i * 0.6) % 9;
|
|
|
|
|
|
const size = 14 + (i % 4) * 5;
|
|
|
|
|
|
const color = colors[i % colors.length];
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={i}
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
bottom: '-20px',
|
|
|
|
|
|
left: `${left}%`,
|
|
|
|
|
|
fontSize: `${size}px`,
|
|
|
|
|
|
color,
|
|
|
|
|
|
filter: 'drop-shadow(0 0 4px rgba(255,100,140,0.4))',
|
|
|
|
|
|
animation: `${animFloatUp} ${duration}s ease-in ${delay}s infinite`,
|
|
|
|
|
|
userSelect: 'none',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
♥
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function StPatricksOverlay({ reduced }: { reduced: boolean }) {
|
|
|
|
|
|
const clovers = Array.from({ length: 18 });
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundImage: [
|
|
|
|
|
|
'radial-gradient(ellipse at 50% 0%, rgba(0,160,60,0.07) 0%, transparent 50%)',
|
|
|
|
|
|
'radial-gradient(ellipse at 50% 100%, rgba(0,130,50,0.05) 0%, transparent 40%)',
|
|
|
|
|
|
].join(','),
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Moving metallic gold shimmer on the accent border at top */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
right: 0,
|
|
|
|
|
|
height: '3px',
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
'linear-gradient(90deg, transparent 0%, #ffd700 20%, #fff4a0 40%, #ffd700 60%, transparent 100%)',
|
|
|
|
|
|
backgroundSize: '300% 100%',
|
|
|
|
|
|
animation: reduced ? 'none' : `${animGoldShimmer} 3s linear infinite`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{!reduced &&
|
|
|
|
|
|
clovers.map((_, i) => {
|
|
|
|
|
|
const left = ((i * 4129 + 223) % 100);
|
|
|
|
|
|
const duration = 14 + (i % 6) * 2;
|
|
|
|
|
|
const delay = (i * 0.7) % 12;
|
|
|
|
|
|
const size = 14 + (i % 3) * 6;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={i}
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: '-20px',
|
|
|
|
|
|
left: `${left}%`,
|
|
|
|
|
|
fontSize: `${size}px`,
|
|
|
|
|
|
opacity: 0.45 + (i % 3) * 0.1,
|
|
|
|
|
|
filter: 'drop-shadow(0 0 3px rgba(0,180,60,0.3))',
|
|
|
|
|
|
animation: `${animCloverDrift} ${duration}s linear ${delay}s infinite`,
|
|
|
|
|
|
userSelect: 'none',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
☘
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function EarthDayOverlay({ reduced }: { reduced: boolean }) {
|
|
|
|
|
|
const leaves = Array.from({ length: 16 });
|
|
|
|
|
|
const leafEmoji = ['🌿', '🍃', '🌱', '🍀'];
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundImage: [
|
|
|
|
|
|
'radial-gradient(ellipse at 30% 70%, rgba(60,160,60,0.07) 0%, transparent 50%)',
|
|
|
|
|
|
'radial-gradient(ellipse at 70% 30%, rgba(100,180,80,0.05) 0%, transparent 45%)',
|
|
|
|
|
|
].join(','),
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Vine line along the left edge */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
bottom: 0,
|
|
|
|
|
|
width: '3px',
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
'linear-gradient(180deg, transparent 0%, rgba(60,160,60,0.4) 20%, rgba(80,180,60,0.6) 50%, rgba(60,160,60,0.4) 80%, transparent 100%)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{!reduced &&
|
|
|
|
|
|
leaves.map((_, i) => {
|
|
|
|
|
|
const left = 3 + ((i * 5023 + 317) % 92);
|
|
|
|
|
|
const duration = 13 + (i % 5) * 2;
|
|
|
|
|
|
const delay = (i * 0.75) % 11;
|
|
|
|
|
|
const size = 14 + (i % 3) * 5;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={i}
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: '-20px',
|
|
|
|
|
|
left: `${left}%`,
|
|
|
|
|
|
fontSize: `${size}px`,
|
|
|
|
|
|
opacity: 0.5 + (i % 3) * 0.1,
|
|
|
|
|
|
animation: `${animEarthLeafDrift} ${duration}s ease-in ${delay}s infinite`,
|
|
|
|
|
|
userSelect: 'none',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{leafEmoji[i % leafEmoji.length]}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function DeepSpaceOverlay({ reduced }: { reduced: boolean }) {
|
|
|
|
|
|
const stars = Array.from({ length: 24 });
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Deep space ambient */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundColor: 'rgba(0,0,8,0.3)',
|
|
|
|
|
|
backgroundImage: [
|
|
|
|
|
|
'radial-gradient(ellipse at 30% 40%, rgba(80,0,180,0.10) 0%, transparent 50%)',
|
|
|
|
|
|
'radial-gradient(ellipse at 70% 60%, rgba(0,60,180,0.10) 0%, transparent 50%)',
|
|
|
|
|
|
'radial-gradient(ellipse at 50% 20%, rgba(120,0,200,0.07) 0%, transparent 40%)',
|
|
|
|
|
|
].join(','),
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Warp streak particles emanating from center */}
|
|
|
|
|
|
{!reduced &&
|
|
|
|
|
|
stars.map((_, i) => {
|
|
|
|
|
|
const angle = (i / stars.length) * 360;
|
|
|
|
|
|
const duration = 2.5 + (i % 5) * 0.4;
|
|
|
|
|
|
const delay = (i * 0.18) % 2.5;
|
|
|
|
|
|
const period = 3 + (i % 4) * 0.5;
|
|
|
|
|
|
const size = 1 + (i % 3);
|
|
|
|
|
|
const colors = ['rgba(200,180,255,0.9)', 'rgba(150,200,255,0.8)', 'rgba(255,255,255,0.7)'];
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={i}
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
left: '50%',
|
|
|
|
|
|
top: '50%',
|
|
|
|
|
|
width: `${80 + i * 6}px`,
|
|
|
|
|
|
height: `${size}px`,
|
|
|
|
|
|
backgroundColor: colors[i % colors.length],
|
|
|
|
|
|
transformOrigin: '0 50%',
|
|
|
|
|
|
transform: `rotate(${angle}deg)`,
|
|
|
|
|
|
boxShadow: `0 0 ${size * 2}px ${colors[i % colors.length]}`,
|
|
|
|
|
|
animation: `${animWarp} ${duration}s ease-out ${delay}s ${period}s infinite`,
|
|
|
|
|
|
opacity: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ArcadeOverlay({ reduced }: { reduced: boolean }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* CRT scanlines */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
'repeating-linear-gradient(0deg, rgba(0,0,0,0.12) 0px, rgba(0,0,0,0.12) 1px, transparent 1px, transparent 3px)',
|
|
|
|
|
|
animation: reduced ? 'none' : `${animScanline} 3s ease-in-out infinite`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Pixel corner decorations */}
|
|
|
|
|
|
{(['0,0', '0,auto', 'auto,0', 'auto,auto'] as const).map((corner, i) => {
|
|
|
|
|
|
const [t, b] = corner.split(',');
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={i}
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: t === '0' ? '8px' : undefined,
|
|
|
|
|
|
bottom: b === '0' ? '8px' : undefined,
|
|
|
|
|
|
left: i % 2 === 0 ? '8px' : undefined,
|
|
|
|
|
|
right: i % 2 === 1 ? '8px' : undefined,
|
|
|
|
|
|
fontFamily: 'monospace',
|
|
|
|
|
|
fontSize: '11px',
|
|
|
|
|
|
color: 'rgba(0,255,136,0.5)',
|
|
|
|
|
|
letterSpacing: '0.05em',
|
|
|
|
|
|
animation: reduced ? 'none' : `${animPixelBlink} ${1 + i * 0.3}s step-end infinite`,
|
|
|
|
|
|
userSelect: 'none',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{['[■]', '[■]', '[■]', '[■]'][i]}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
{/* "INSERT COIN" prompt */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
bottom: '16px',
|
|
|
|
|
|
left: '50%',
|
|
|
|
|
|
transform: 'translateX(-50%)',
|
|
|
|
|
|
fontFamily: 'monospace',
|
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
|
letterSpacing: '0.2em',
|
|
|
|
|
|
color: 'rgba(255,220,0,0.4)',
|
|
|
|
|
|
animation: reduced ? 'none' : `${animPixelBlink} 1.2s step-end infinite`,
|
|
|
|
|
|
userSelect: 'none',
|
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
— INSERT COIN —
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/* Vignette */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
'radial-gradient(ellipse at 50% 50%, transparent 60%, rgba(0,0,0,0.35) 100%)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Wrapper ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function SeasonalOverlay({
|
|
|
|
|
|
theme,
|
|
|
|
|
|
reduced,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
theme: SeasonTheme;
|
|
|
|
|
|
reduced: boolean;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const overlayMap: Record<SeasonTheme, React.ReactNode> = {
|
|
|
|
|
|
halloween: <HalloweenOverlay reduced={reduced} />,
|
|
|
|
|
|
christmas: <ChristmasOverlay reduced={reduced} />,
|
|
|
|
|
|
newyear: <NewYearOverlay reduced={reduced} />,
|
|
|
|
|
|
autumn: <AutumnOverlay reduced={reduced} />,
|
|
|
|
|
|
aprilfools: <AprilFoolsOverlay reduced={reduced} />,
|
|
|
|
|
|
lunar: <LunarNewYearOverlay reduced={reduced} />,
|
|
|
|
|
|
valentines: <ValentinesOverlay reduced={reduced} />,
|
|
|
|
|
|
stpatricks: <StPatricksOverlay reduced={reduced} />,
|
|
|
|
|
|
earthday: <EarthDayOverlay reduced={reduced} />,
|
|
|
|
|
|
deepspace: <DeepSpaceOverlay reduced={reduced} />,
|
|
|
|
|
|
arcade: <ArcadeOverlay reduced={reduced} />,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
pointerEvents: 'none',
|
2026-06-15 00:09:54 -04:00
|
|
|
|
zIndex: 9999,
|
2026-06-14 00:33:04 -04:00
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{overlayMap[theme]}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function SeasonalEffect() {
|
|
|
|
|
|
const settings = useAtomValue(settingsAtom);
|
|
|
|
|
|
const reduced =
|
|
|
|
|
|
typeof window !== 'undefined' &&
|
|
|
|
|
|
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
|
|
|
|
|
|
|
|
|
|
const theme = useMemo<SeasonTheme | null>(() => {
|
|
|
|
|
|
const override = settings.seasonalThemeOverride ?? 'auto';
|
|
|
|
|
|
if (override === 'off') return null;
|
|
|
|
|
|
if (override === 'auto') return getActiveSeason(new Date());
|
|
|
|
|
|
return override as SeasonTheme;
|
|
|
|
|
|
}, [settings.seasonalThemeOverride]);
|
|
|
|
|
|
|
|
|
|
|
|
if (!theme) return null;
|
|
|
|
|
|
return <SeasonalOverlay theme={theme} reduced={reduced} />;
|
|
|
|
|
|
}
|