All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
317 lines
10 KiB
TypeScript
317 lines
10 KiB
TypeScript
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; |