import React, { useMemo } from 'react';
import { SeasonalOverlayProps } from '../types';
import {
animHeartRise,
animHeartBob,
animPetalTumble,
animBokehBreathe,
animBlushPulse,
animSparkle,
} from './Valentines.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) => {
const x = Math.sin(seed * 127.1 + 311.7) * 43758.5453;
return x - Math.floor(x);
};
// Romantic oklch palette — rose, blush pink, warm red, soft cream. Kept
// luminous and gentle so everything reads as soft ambient glow over chat.
const ROSE = 'oklch(0.7 0.15 10)';
const BLUSH = 'oklch(0.9 0.06 350)';
const WARM_RED = 'oklch(0.6 0.18 20)';
const CREAM = 'oklch(0.96 0.03 60)';
const HEART_COLORS = [ROSE, BLUSH, WARM_RED, 'oklch(0.78 0.13 5)'];
const PETAL_COLORS = [
'oklch(0.66 0.16 12)', // rose
'oklch(0.74 0.13 6)', // lighter rose
'oklch(0.6 0.18 20)', // warm red
];
// Inline SVG (data-URI) so it is fully Tauri/CSP-safe — no external assets.
// A soft heart with a gradient fill and a cream highlight glint.
const heartSvg = (fill: string, glint: string) => {
const svg = ``;
return `url("data:image/svg+xml,${svg.replace(/\n/g, '').replace(/#/g, '%23')}")`;
};
// A single rose petal — a soft teardrop/ovate shape with an inner crease,
// gently asymmetric so the tumble reads as a real petal.
const petalSvg = (fill: string) => {
const svg = ``;
return `url("data:image/svg+xml,${svg.replace(/\n/g, '').replace(/#/g, '%23')}")`;
};
type Heart = {
left: number;
size: number;
duration: number;
delay: number;
bobDuration: number;
opacity: number;
blur: number;
image: string;
restTop: number; // static resting position for reduced scene
};
type Petal = {
left: number;
size: number;
duration: number;
delay: number;
opacity: number;
image: string;
rotate: number;
restTop: number;
};
type Bokeh = {
left: number;
top: number;
size: number;
color: string;
duration: number;
delay: number;
};
type Sparkle = {
left: number;
top: number;
size: number;
duration: number;
delay: number;
};
export function ValentinesOverlay({ reduced }: SeasonalOverlayProps) {
// Three parallax bands of hearts: far (small/slow/dim) -> near (large/fast).
const hearts = useMemo(() => {
const bands = [
{ count: 4, size: [12, 18], dur: [20, 26], op: [0.3, 0.5], blur: 0.8 },
{ count: 4, size: [18, 26], dur: [15, 19], op: [0.5, 0.72], blur: 0.3 },
{ count: 3, size: [26, 38], dur: [12, 15], op: [0.62, 0.85], blur: 0 },
];
const out: Heart[] = [];
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);
const fill = HEART_COLORS[Math.floor(r4 * HEART_COLORS.length) % HEART_COLORS.length];
out.push({
left: r1 * 96 + 2,
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] + 5),
bobDuration: 5 + r2 * 5,
opacity: b.op[0] + r3 * (b.op[1] - b.op[0]),
blur: b.blur,
image: heartSvg(fill, CREAM),
restTop: 6 + r3 * 86,
});
s += 1;
}
});
return out;
}, []);
// Drifting rose petals tumbling down — a gentle counter-motion to the hearts.
const petals = useMemo(() => {
const count = 8;
const out: Petal[] = [];
for (let i = 0; i < count; i += 1) {
const r1 = rand(i + 40);
const r2 = rand(i + 40.5);
const r3 = rand(i + 40.9);
const fill = PETAL_COLORS[i % PETAL_COLORS.length];
out.push({
left: r1 * 98,
size: 9 + r2 * 9,
duration: 14 + r3 * 9,
delay: -r1 * 22,
opacity: 0.45 + r2 * 0.35,
image: petalSvg(fill),
rotate: r3 * 360,
restTop: 4 + r2 * 90,
});
}
return out;
}, []);
// Dreamy blush bokeh orbs scattered across the scene, softly breathing.
const bokeh = useMemo(() => {
const count = 7;
const out: Bokeh[] = [];
for (let i = 0; i < count; i += 1) {
const r1 = rand(i + 70);
const r2 = rand(i + 70.4);
const r3 = rand(i + 70.8);
out.push({
left: r1 * 94 + 3,
top: r2 * 88 + 4,
size: 70 + r3 * 130,
color: i % 2 === 0 ? BLUSH : 'oklch(0.82 0.1 355)',
duration: 9 + r3 * 7,
delay: -r1 * 10,
});
}
return out;
}, []);
// Faint sparkle glints — sparse, never strobing.
const sparkles = useMemo(() => {
const count = 5;
const out: Sparkle[] = [];
for (let i = 0; i < count; i += 1) {
const r1 = rand(i + 200);
const r2 = rand(i + 200.5);
const r3 = rand(i + 200.9);
out.push({
left: r1 * 92 + 4,
top: r2 * 80 + 6,
size: 6 + r3 * 8,
duration: 5 + r3 * 4,
delay: -r1 * 9,
});
}
return out;
}, []);
return (
<>
{/* Warm romantic ambient wash — layered radial + linear oklch gradients
for depth. Low opacity so chat text stays legible (WCAG-AA). */}
{/* Blush vignette frame — soft warm edges, clear center. A single cheap
backdrop-filter layer for a faint dreamy haze around the rim. */}
{/* Dreamy bokeh orbs — soft blurred blush lights that breathe. */}
{bokeh.map((b, i) => (
))}
{/* Floating hearts (motion) — three parallax bands rising and bobbing.
The wrapper carries the lateral bob; the inner carries the rise so the
two combine into a wandering draft. */}
{!reduced &&
hearts.map((h, i) => (
))}
{/* Drifting rose petals (motion) — tumbling down through the scene. */}
{!reduced &&
petals.map((p, i) => (
))}
{/* Faint sparkle glints (motion) — sparse romantic twinkle. */}
{!reduced &&
sparkles.map((s, i) => (
))}
{/* Static reduced-motion / preview scene — settled hearts at rest, a
scatter of fallen petals, and still sparkle glints. Tender and still,
so the judged thumbnail stands on its own without any animation. */}
{reduced &&
hearts.map((h, i) => (
))}
{reduced &&
petals.map((p, i) => (
))}
{reduced &&
sparkles.map((s, i) => (
))}
>
);
}