// Ported from bmo-preview (https://github.com/PageZ948/bmo-preview). Visuals kept 1:1.
/**
* SeasonalOverlay — renders ground-level seasonal decorations as an SVG group.
*
* Supported overlays: autumn-leaves, snow, cherry-blossoms.
* Positions are deterministic (index-based). No Math.random().
* Cherry blossom petals in the air use a gentle drift animation
* (garden-pollen-drift) — same keyframe, different colors.
*/
import { memo } from "react";
interface SeasonalOverlayProps {
overlay: string;
sceneWidth: number;
sceneHeight: number;
}
/** Deterministic value in [0, range) for a given index + seed. */
function det(i: number, range: number, seed = 37): number {
return ((i * seed + i * i * 13) % 1000) / 1000 * range;
}
function detRange(i: number, min: number, max: number, seed = 37): number {
return min + det(i, max - min, seed);
}
// ---- Autumn leaves ----
const AUTUMN_COLORS = ["#f59e0b", "#ea580c", "#dc2626", "#92400e", "#d97706", "#b45309"];
function leafPath(): string {
return "M0,-3 C1.5,-1.5 3,0 1.5,3 C0,4.5 -1.5,3 0,0 C-1.5,-1.5 -3,0 0,-3Z";
}
function AutumnLeaves({
count,
sceneWidth,
sceneHeight,
}: {
count: number;
sceneWidth: number;
sceneHeight: number;
}) {
const groundMinY = sceneHeight * 0.60;
const groundMaxY = sceneHeight * 0.70;
return (
<>
{Array.from({ length: count }, (_, i) => {
const x = detRange(i, 10, sceneWidth - 10, 41);
const y = detRange(i, groundMinY, groundMaxY, 59);
const rotate = detRange(i, -45, 45, 73);
const scale = detRange(i, 0.6, 1.2, 89);
const color = AUTUMN_COLORS[i % AUTUMN_COLORS.length];
return (
);
})}
>
);
}
// ---- Snow ----
function SnowOverlay({
sceneWidth,
sceneHeight,
}: {
sceneWidth: number;
sceneHeight: number;
}) {
// Wavy ground cover path
const groundY = sceneHeight * 0.63;
const amplitude = 3;
const segments = 12;
const segW = sceneWidth / segments;
let d = `M0,${groundY}`;
for (let i = 0; i <= segments; i++) {
const x = i * segW;
const y = groundY + (i % 2 === 0 ? -amplitude : amplitude);
if (i === 0) d += ` L${x},${y}`;
else d += ` Q${x - segW / 2},${y + amplitude * 0.5} ${x},${y}`;
}
d += ` L${sceneWidth},${sceneHeight} L0,${sceneHeight} Z`;
// Small snow pile ellipses near ground features
const pilePositions = Array.from({ length: 8 }, (_, i) => ({
cx: detRange(i, 15, sceneWidth - 15, 43),
cy: detRange(i, sceneHeight * 0.60, sceneHeight * 0.67, 61),
rx: detRange(i, 6, 14, 73),
ry: detRange(i, 2, 5, 89),
}));
return (
<>
{/* Wavy ground cover */}
{/* Snow piles */}
{pilePositions.map((p, i) => (
))}
>
);
}
// ---- Cherry blossoms ----
const BLOSSOM_COLORS = ["#fbb6ce", "#f9a8d4", "#f472b6", "#fbcfe8"];
function CherryBlossoms({
count,
sceneWidth,
sceneHeight,
}: {
count: number;
sceneWidth: number;
sceneHeight: number;
}) {
const airCount = Math.floor(count * 0.6);
const groundCount = count - airCount;
return (
<>
{/* Air petals — animated drift */}
{Array.from({ length: airCount }, (_, i) => {
const cx = detRange(i, 5, sceneWidth - 5, 43);
const cy = detRange(i, sceneHeight * 0.1, sceneHeight * 0.55, 59);
const r = detRange(i, 1, 2.5, 71);
const color = BLOSSOM_COLORS[i % BLOSSOM_COLORS.length];
const duration = detRange(i, 5, 10, 83);
const delay = detRange(i, 0, 7, 97);
return (
);
})}
{/* Ground petals — static */}
{Array.from({ length: groundCount }, (_, i) => {
const cx = detRange(i, 10, sceneWidth - 10, 101);
const cy = detRange(i, sceneHeight * 0.60, sceneHeight * 0.68, 113);
const r = detRange(i, 0.8, 2, 127);
const color = BLOSSOM_COLORS[i % BLOSSOM_COLORS.length];
return (
);
})}
>
);
}
// ---- Main component ----
export const SeasonalOverlay = memo(function SeasonalOverlay({
overlay,
sceneWidth,
sceneHeight,
}: SeasonalOverlayProps) {
if (overlay === "none" || !overlay) return null;
if (overlay === "autumn-leaves") {
return (
);
}
if (overlay === "snow") {
return (
);
}
if (overlay === "cherry-blossoms") {
return (
);
}
return null;
});