import React, { useEffect, useRef } from "react"; import { TRUNK_TOP_CSS } from "./Tree"; interface Leaf { x: number; y: number; size: number; speed: number; drift: number; rotation: number; rotationSpeed: number; opacity: number; sway: number; swaySpeed: number; phase: number; } export default function FallingLeaves({ treeRef }: { treeRef: React.RefObject }) { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current!; const ctx = canvas.getContext("2d")!; let animId: number; const leaves: Leaf[] = []; const MAX_LEAVES = 36; const frameInterval = 1000 / 30; let lastFrame = 0; let isVisible = true; let isRunning = false; let treeBounds = { left: 0, top: 0, width: 0 }; let canvasBounds = { left: 0, top: 0 }; const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; function measure() { const parent = canvas.parentElement; const rect = parent?.getBoundingClientRect(); canvas.width = Math.max(1, Math.round(rect?.width || window.innerWidth)); canvas.height = Math.max(1, Math.round(rect?.height || window.innerHeight)); const treeRect = treeRef.current?.getBoundingClientRect(); const canvasRect = canvas.getBoundingClientRect(); treeBounds = treeRect ? { left: treeRect.left, top: treeRect.top, width: treeRect.width } : { left: canvasRect.left + canvas.width * 0.2, top: canvasRect.top + canvas.height * 0.4, width: canvas.width * 0.6 }; canvasBounds = { left: canvasRect.left, top: canvasRect.top }; } measure(); const resizeObserver = new ResizeObserver(measure); if (canvas.parentElement) resizeObserver.observe(canvas.parentElement); if (treeRef.current) resizeObserver.observe(treeRef.current); function spawnLeaf(): Leaf { const spawnY = treeBounds.top - canvasBounds.top + TRUNK_TOP_CSS; const spawnX = treeBounds.left - canvasBounds.left + Math.random() * treeBounds.width * 0.8 + treeBounds.width * 0.1; return { x: spawnX, y: spawnY, size: Math.random() * 4 + 2, speed: Math.random() * 0.3 + 0.15, drift: Math.random() * 0.5 - 0.25, rotation: Math.random() * Math.PI * 2, rotationSpeed: (Math.random() - 0.5) * 0.02, opacity: Math.random() * 0.25 + 0.08, sway: Math.random() * 30 + 15, swaySpeed: Math.random() * 0.008 + 0.003, phase: Math.random() * Math.PI * 2, }; } let spawnTimer = 0; function draw(timestamp: number) { if (!isRunning) return; animId = requestAnimationFrame(draw); if (timestamp - lastFrame < frameInterval) return; lastFrame = timestamp; ctx.clearRect(0, 0, canvas.width, canvas.height); spawnTimer++; if (spawnTimer > 30 && leaves.length < MAX_LEAVES) { leaves.push(spawnLeaf()); spawnTimer = 0; } for (let i = leaves.length - 1; i >= 0; i--) { const l = leaves[i]; l.y += l.speed; l.x += l.drift + Math.sin(l.phase) * l.swaySpeed * l.sway * 0.05; l.phase += l.swaySpeed; l.rotation += l.rotationSpeed; ctx.save(); ctx.translate(l.x, l.y); ctx.rotate(l.rotation); ctx.globalAlpha = l.opacity; ctx.fillStyle = "#fdb068"; const s = Math.round(l.size); ctx.fillRect(-s / 2, -s / 2, s, s); ctx.restore(); if (l.y > canvas.height + 10) { leaves.splice(i, 1); } } } function start() { if (reducedMotion || isRunning || !isVisible || document.hidden) return; isRunning = true; animId = requestAnimationFrame(draw); } function stop() { isRunning = false; if (animId) cancelAnimationFrame(animId); } const intersectionObserver = new IntersectionObserver(([entry]) => { isVisible = entry.isIntersecting; if (isVisible) start(); else stop(); }); const onVisibilityChange = () => { if (document.hidden) stop(); else start(); }; intersectionObserver.observe(canvas); document.addEventListener("visibilitychange", onVisibilityChange); start(); return () => { stop(); intersectionObserver.disconnect(); resizeObserver.disconnect(); document.removeEventListener("visibilitychange", onVisibilityChange); }; }, []); return ; }