This commit is contained in:
317
client/src/FallingLeaves.tsx
Normal file
317
client/src/FallingLeaves.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
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ů
|
||||
* <FallingLeaves />
|
||||
*
|
||||
* @example
|
||||
* // Použití s vlastním počtem listů
|
||||
* <FallingLeaves numLeaves={50} />
|
||||
*
|
||||
* @example
|
||||
* // Použití s vlastní CSS třídou a pouze podzimními barvami
|
||||
* <FallingLeaves
|
||||
* numLeaves={15}
|
||||
* className="autumn-leaves"
|
||||
* leafVariants={['leaf-orange.svg', 'leaf-red.svg', 'leaf-yellow.svg']}
|
||||
* />
|
||||
*/
|
||||
const FallingLeaves: React.FC<FallingLeavesProps> = ({
|
||||
numLeaves = 20,
|
||||
className = 'falling-leaves',
|
||||
leafVariants = LEAF_VARIANTS
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const leafSceneRef = useRef<LeafScene | null>(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 <div ref={containerRef} className={className} />;
|
||||
};
|
||||
|
||||
// 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;
|
||||
Reference in New Issue
Block a user