import React, { useMemo } from 'react';
import { SeasonalOverlayProps } from '../types';
import {
animDoodleFloat,
animConfettiTumble,
animWobble,
animRainbowDrift,
animGoogly,
animSparkle,
} from './AprilFools.css';
// Deterministic pseudo-random so the scene is identical on every mount and the
// reduced-motion preview thumbnail is stable. Large primes spread the values.
const rand = (seed: number) => {
const x = Math.sin(seed * 127.1 + 311.7) * 43758.5453;
return x - Math.floor(x);
};
// Bright-but-soft pastel rainbow in oklch. Kept luminous and gentle so the
// doodles read as crayon pastel over chat without ever fighting the text.
const PASTELS = [
'oklch(0.85 0.12 20)', // pink
'oklch(0.88 0.12 90)', // butter yellow
'oklch(0.82 0.12 160)', // mint
'oklch(0.8 0.12 260)', // periwinkle
'oklch(0.84 0.12 320)', // lilac
'oklch(0.86 0.11 50)', // peach
];
// Inline-SVG data-URI doodle glyphs, drawn hand-sketch style (round caps,
// open paths). `enc()` keeps them CSP-safe — no external assets, no base64.
const enc = (svg: string) => `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
// A single rough stroke wrapper helper for the glyph SVGs.
const stroke = (color: string, body: string) =>
``;
// Question mark — the playful "huh?" centerpiece doodle.
const glyphQuestion = (c: string) =>
stroke(
c,
`` +
``,
);
// Exclamation / "bang" — a surprised little doodle.
const glyphBang = (c: string) =>
stroke(c, ``);
// Squiggle — a loopy scribble that adds whimsy.
const glyphSquiggle = (c: string) => stroke(c, ``);
// Five-point doodle star (open-stroke, hand-drawn look).
const glyphStar = (c: string) =>
stroke(
c,
``,
);
// A tiny heart doodle for extra grin.
const glyphHeart = (c: string) =>
stroke(c, ``);
const GLYPHS = [glyphQuestion, glyphBang, glyphSquiggle, glyphStar, glyphHeart, glyphQuestion];
type Doodle = {
left: number;
size: number;
glyph: string;
duration: number;
delay: number;
startTop: number; // used for the static (reduced) scatter
opacity: number;
};
type Confetti = {
left: number;
size: number;
color: string;
duration: number;
delay: number;
startTop: number;
ratio: number; // chip aspect
round: boolean;
};
type Eye = {
left: number;
top: number;
size: number;
duration: number;
delay: number;
};
type Spark = {
left: number;
top: number;
size: number;
color: string;
duration: number;
delay: number;
};
export function AprilFoolsOverlay({ reduced }: SeasonalOverlayProps) {
// ~16 drifting doodles. Built once; per-element timing creates the variety.
const doodles = useMemo(() => {
const count = 16;
const out: Doodle[] = [];
for (let i = 0; i < count; i += 1) {
const color = PASTELS[i % PASTELS.length];
out.push({
left: rand(i + 0.1) * 96 + 2,
size: 18 + rand(i + 0.3) * 22,
glyph: enc(GLYPHS[i % GLYPHS.length](color)),
duration: 16 + rand(i + 0.5) * 12,
delay: -rand(i + 0.7) * 26,
startTop: rand(i + 0.9) * 92 + 4,
opacity: 0.5 + rand(i + 0.2) * 0.32,
});
}
return out;
}, []);
// ~14 confetti chips in a couple of falling bands.
const confetti = useMemo(() => {
const count = 14;
const out: Confetti[] = [];
for (let i = 0; i < count; i += 1) {
out.push({
left: rand(i + 3.1) * 98 + 1,
size: 5 + rand(i + 3.3) * 6,
color: PASTELS[(i + 2) % PASTELS.length],
duration: 10 + rand(i + 3.5) * 9,
delay: -rand(i + 3.7) * 18,
startTop: rand(i + 3.9) * 96 + 2,
ratio: 0.45 + rand(i + 3.2) * 0.8,
round: rand(i + 3.6) > 0.6,
});
}
return out;
}, []);
// A few googly eyes peeking from corners/edges — the cheeky surprise.
const eyes = useMemo(() => {
const anchors = [
{ left: 6, top: 12 },
{ left: 90, top: 20 },
{ left: 80, top: 82 },
{ left: 14, top: 74 },
];
return anchors.map((a, i) => ({
left: a.left,
top: a.top,
size: 22 + rand(i + 5.1) * 12,
duration: 3 + rand(i + 5.3) * 2.5,
delay: -rand(i + 5.5) * 3,
}));
}, []);
// Sly winking sparkles scattered sparsely.
const sparks = useMemo(() => {
const count = 5;
const out: Spark[] = [];
for (let i = 0; i < count; i += 1) {
out.push({
left: rand(i + 7.1) * 90 + 5,
top: rand(i + 7.3) * 84 + 8,
size: 12 + rand(i + 7.5) * 12,
color: PASTELS[(i + 1) % PASTELS.length],
duration: 4 + rand(i + 7.7) * 3,
delay: -rand(i + 7.9) * 5,
});
}
return out;
}, []);
// Four-point glint used for the winking sparkles.
const sparkGlint = (c: string) =>
enc(
``,
);
return (
<>
{/* Soft pastel ambient wash — layered oklch radials for depth. Very low
opacity so chat text keeps WCAG-AA contrast. */}
{/* Faux wobble layer — a near-invisible pastel haze that gently skews so
the whole scene feels playfully "tickled". Tiny amplitude = not
nauseating. backdrop-filter is one cheap layer for a candy bloom. */}
{/* Pastel rainbow aurora high up — soft band of the full palette. */}
{/* Drifting doodles. Motion: rise from below. Reduced: static scatter. */}
{doodles.map((d, i) => {
const common: React.CSSProperties = {
position: 'absolute',
left: `${d.left}%`,
width: `${d.size}px`,
height: `${d.size}px`,
backgroundImage: d.glyph,
backgroundRepeat: 'no-repeat',
backgroundSize: 'contain',
backgroundPosition: 'center',
opacity: d.opacity,
filter: 'drop-shadow(0 1px 1px oklch(0.4 0.05 300 / 0.18))',
};
if (reduced) {
return (
);
}
return (
);
})}
{/* Light confetti — tumbling pastel chips. */}
{confetti.map((c, i) => {
const common: React.CSSProperties = {
position: 'absolute',
left: `${c.left}%`,
width: `${c.size}px`,
height: `${c.size * c.ratio}px`,
background: c.color,
borderRadius: c.round ? '50%' : '1px',
opacity: 0.75,
};
if (reduced) {
return (
);
}
return (
);
})}
{/* Googly eyes peeking from the edges — pupil wanders cheekily. */}
{eyes.map((e, i) => (
))}
{/* Sly winking sparkles. Static (reduced) shows them mid-glint. */}
{sparks.map((s, i) => (
))}
>
);
}