106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
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<HTMLCanvasElement | null> }) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current!;
|
|
const ctx = canvas.getContext("2d")!;
|
|
let animId: number;
|
|
const leaves: Leaf[] = [];
|
|
const MAX_LEAVES = 50;
|
|
|
|
function resize() {
|
|
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));
|
|
}
|
|
resize();
|
|
window.addEventListener("resize", resize);
|
|
|
|
function spawnLeaf(): Leaf {
|
|
const treeRect = treeRef.current?.getBoundingClientRect();
|
|
const canvasRect = canvas.getBoundingClientRect();
|
|
const spawnY = treeRect ? treeRect.top - canvasRect.top + TRUNK_TOP_CSS : canvas.height * 0.4;
|
|
const spawnX = treeRect
|
|
? treeRect.left - canvasRect.left + Math.random() * treeRect.width * 0.8 + treeRect.width * 0.1
|
|
: Math.random() * canvas.width * 0.6 + canvas.width * 0.2;
|
|
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() {
|
|
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);
|
|
}
|
|
}
|
|
|
|
animId = requestAnimationFrame(draw);
|
|
}
|
|
|
|
draw();
|
|
|
|
return () => {
|
|
cancelAnimationFrame(animId);
|
|
window.removeEventListener("resize", resize);
|
|
};
|
|
}, []);
|
|
|
|
return <canvas ref={canvasRef} className="falling-leaves" />;
|
|
}
|