import React, { useEffect, useRef, useCallback } from 'react'; // Různé barevné varianty listů const LEAF_VARIANTS = [ 'leaf.svg', // Původní tmavě hnědá 'leaf-orange.svg', // Oranžová 'leaf-yellow.svg', // Žlutá 'leaf-red.svg', // Červená 'leaf-brown.svg', // Světle hnědá 'leaf-green.svg', // Zelená ] as const; interface LeafData { el: HTMLDivElement; x: number; y: number; z: number; rotation: { axis: 'X' | 'Y' | 'Z'; value: number; speed: number; x: number; }; xSpeedVariation: number; ySpeed: number; path: { type: number; start: number; }; image: number; } interface WindOptions { magnitude: number; maxSpeed: number; duration: number; start: number; speed: (t: number, y: number) => number; } interface LeafSceneOptions { numLeaves: number; wind: WindOptions; } interface FallingLeavesProps { /** Počet padających listů (výchozí: 20) */ numLeaves?: number; /** CSS třída pro kontejner (výchozí: 'falling-leaves') */ className?: string; /** Barevné varianty listů k použití (výchozí: všechny) */ leafVariants?: readonly string[]; } class LeafScene { private viewport: HTMLElement; private world: HTMLDivElement; private leaves: LeafData[] = []; private options: LeafSceneOptions; private width: number; private height: number; private timer: number = 0; private animationId: number | null = null; private leafVariants: readonly string[]; constructor(el: HTMLElement, numLeaves: number = 20, leafVariants: readonly string[] = LEAF_VARIANTS) { this.viewport = el; this.world = document.createElement('div'); this.leafVariants = leafVariants; this.options = { numLeaves, wind: { magnitude: 1.2, maxSpeed: 12, duration: 300, start: 0, speed: () => 0 }, }; this.width = this.viewport.offsetWidth; this.height = this.viewport.offsetHeight; } private resetLeaf = (leaf: LeafData): LeafData => { // place leaf towards the top left leaf.x = this.width * 2 - Math.random() * this.width * 1.75; leaf.y = -10; leaf.z = Math.random() * 200; if (leaf.x > this.width) { leaf.x = this.width + 10; leaf.y = Math.random() * this.height / 2; } // at the start, the leaf can be anywhere if (this.timer === 0) { leaf.y = Math.random() * this.height; } // Choose axis of rotation. // If axis is not X, chose a random static x-rotation for greater variability leaf.rotation.speed = Math.random() * 10; const randomAxis = Math.random(); if (randomAxis > 0.5) { leaf.rotation.axis = 'X'; } else if (randomAxis > 0.25) { leaf.rotation.axis = 'Y'; leaf.rotation.x = Math.random() * 180 + 90; } else { leaf.rotation.axis = 'Z'; leaf.rotation.x = Math.random() * 360 - 180; // looks weird if the rotation is too fast around this axis leaf.rotation.speed = Math.random() * 3; } // random speed leaf.xSpeedVariation = Math.random() * 0.8 - 0.4; leaf.ySpeed = Math.random() + 1.5; // randomly select leaf color variant const randomVariantIndex = Math.floor(Math.random() * this.leafVariants.length); leaf.image = randomVariantIndex; // apply the background image to the leaf element const leafVariant = this.leafVariants[randomVariantIndex]; leaf.el.style.backgroundImage = `url(${leafVariant})`; return leaf; }; private updateLeaf = (leaf: LeafData): void => { const leafWindSpeed = this.options.wind.speed(this.timer - this.options.wind.start, leaf.y); const xSpeed = leafWindSpeed + leaf.xSpeedVariation; leaf.x -= xSpeed; leaf.y += leaf.ySpeed; leaf.rotation.value += leaf.rotation.speed; const transform = `translateX(${leaf.x}px) translateY(${leaf.y}px) translateZ(${leaf.z}px) rotate${leaf.rotation.axis}(${leaf.rotation.value}deg)${leaf.rotation.axis !== 'X' ? ` rotateX(${leaf.rotation.x}deg)` : '' }`; leaf.el.style.transform = transform; // reset if out of view if (leaf.x < -10 || leaf.y > this.height + 10) { this.resetLeaf(leaf); } }; private updateWind = (): void => { // wind follows a sine curve: asin(b*time + c) + a // where a = wind magnitude as a function of leaf position, b = wind.duration, c = offset // wind duration should be related to wind magnitude, e.g. higher windspeed means longer gust duration if (this.timer === 0 || this.timer > (this.options.wind.start + this.options.wind.duration)) { this.options.wind.magnitude = Math.random() * this.options.wind.maxSpeed; this.options.wind.duration = this.options.wind.magnitude * 50 + (Math.random() * 20 - 10); this.options.wind.start = this.timer; const screenHeight = this.height; this.options.wind.speed = function (t: number, y: number) { // should go from full wind speed at the top, to 1/2 speed at the bottom, using leaf Y const a = this.magnitude / 2 * (screenHeight - 2 * y / 3) / screenHeight; return a * Math.sin(2 * Math.PI / this.duration * t + (3 * Math.PI / 2)) + a; }; } }; public init = (): void => { // Clear existing leaves this.leaves = []; this.world.innerHTML = ''; for (let i = 0; i < this.options.numLeaves; i++) { const leaf: LeafData = { el: document.createElement('div'), x: 0, y: 0, z: 0, rotation: { axis: 'X', value: 0, speed: 0, x: 0 }, xSpeedVariation: 0, ySpeed: 0, path: { type: 1, start: 0, }, image: 1 }; this.resetLeaf(leaf); this.leaves.push(leaf); this.world.appendChild(leaf.el); } this.world.className = 'leaf-scene'; this.viewport.appendChild(this.world); // set perspective this.world.style.perspective = "400px"; // reset window height/width on resize const handleResize = (): void => { this.width = this.viewport.offsetWidth; this.height = this.viewport.offsetHeight; }; window.addEventListener('resize', handleResize); }; public render = (): void => { this.updateWind(); for (let i = 0; i < this.leaves.length; i++) { this.updateLeaf(this.leaves[i]); } this.timer++; this.animationId = requestAnimationFrame(this.render); }; public destroy = (): void => { if (this.animationId) { cancelAnimationFrame(this.animationId); this.animationId = null; } if (this.world && this.world.parentNode) { this.world.parentNode.removeChild(this.world); } window.removeEventListener('resize', () => { this.width = this.viewport.offsetWidth; this.height = this.viewport.offsetHeight; }); }; } /** * Komponenta pro zobrazení padajících listů na pozadí stránky * * @param numLeaves - Počet padajících listů (výchozí: 20) * @param className - CSS třída pro kontejner (výchozí: 'falling-leaves') * @param leafVariants - Barevné varianty listů k použití (výchozí: všechny) * * @example * // Základní použití s výchozím počtem listů * * * @example * // Použití s vlastním počtem listů * * * @example * // Použití s vlastní CSS třídou a pouze podzimními barvami * */ const FallingLeaves: React.FC = ({ numLeaves = 20, className = 'falling-leaves', leafVariants = LEAF_VARIANTS }) => { const containerRef = useRef(null); const leafSceneRef = useRef(null); const initializeLeafScene = useCallback(() => { if (containerRef.current) { leafSceneRef.current = new LeafScene(containerRef.current, numLeaves, leafVariants); leafSceneRef.current.init(); leafSceneRef.current.render(); } }, [numLeaves, leafVariants]); useEffect(() => { initializeLeafScene(); return () => { if (leafSceneRef.current) { leafSceneRef.current.destroy(); leafSceneRef.current = null; } }; }, [initializeLeafScene]); return
; }; // Přednastavení pro různé účely export const LEAF_PRESETS = { LIGHT: 10, // Lehký podzimní efekt NORMAL: 20, // Standardní množství HEAVY: 40, // Silný podzimní vítr BLIZZARD: 80 // Hustý pád listí } as const; // Přednastavené barevné kombinace export const LEAF_COLOR_THEMES = { ALL: LEAF_VARIANTS, // Všechny barvy AUTUMN: ['leaf-orange.svg', 'leaf-red.svg', 'leaf-yellow.svg', 'leaf-brown.svg'] as const, // Podzimní barvy WARM: ['leaf-orange.svg', 'leaf-red.svg', 'leaf-brown.svg'] as const, // Teplé barvy CLASSIC: ['leaf.svg', 'leaf-brown.svg'] as const, // Klasické hnědé odstíny BRIGHT: ['leaf-yellow.svg', 'leaf-orange.svg'] as const, // Světlé barvy } as const; export default FallingLeaves;