diff --git a/client/public/leaf-brown.svg b/client/public/leaf-brown.svg
new file mode 100644
index 0000000..f7458ac
--- /dev/null
+++ b/client/public/leaf-brown.svg
@@ -0,0 +1,22 @@
+
+
+
+
\ No newline at end of file
diff --git a/client/public/leaf-green.svg b/client/public/leaf-green.svg
new file mode 100644
index 0000000..084a95b
--- /dev/null
+++ b/client/public/leaf-green.svg
@@ -0,0 +1,22 @@
+
+
+
+
\ No newline at end of file
diff --git a/client/public/leaf-orange.svg b/client/public/leaf-orange.svg
new file mode 100644
index 0000000..b1a3d4a
--- /dev/null
+++ b/client/public/leaf-orange.svg
@@ -0,0 +1,22 @@
+
+
+
+
\ No newline at end of file
diff --git a/client/public/leaf-red.svg b/client/public/leaf-red.svg
new file mode 100644
index 0000000..5b44239
--- /dev/null
+++ b/client/public/leaf-red.svg
@@ -0,0 +1,22 @@
+
+
+
+
\ No newline at end of file
diff --git a/client/public/leaf-yellow.svg b/client/public/leaf-yellow.svg
new file mode 100644
index 0000000..aff490d
--- /dev/null
+++ b/client/public/leaf-yellow.svg
@@ -0,0 +1,22 @@
+
+
+
+
\ No newline at end of file
diff --git a/client/public/leaf.svg b/client/public/leaf.svg
new file mode 100644
index 0000000..d064de2
--- /dev/null
+++ b/client/public/leaf.svg
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 9fab6f0..035ee83 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -20,6 +20,8 @@ import NoteModal from './components/modals/NoteModal';
import { useEasterEgg } from './context/eggs';
import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime } from '../../types';
import { getLunchChoiceName } from './enums';
+import FallingLeaves, { LEAF_PRESETS, LEAF_COLOR_THEMES } from './FallingLeaves';
+import './FallingLeaves.scss';
const EVENT_CONNECT = "connect"
@@ -684,6 +686,10 @@ function App() {
> || "Jejda, něco se nám nepovedlo :("}
+
setNoteModalOpen(false)} onSave={saveNote} />
diff --git a/client/src/FallingLeaves.scss b/client/src/FallingLeaves.scss
new file mode 100644
index 0000000..704a769
--- /dev/null
+++ b/client/src/FallingLeaves.scss
@@ -0,0 +1,31 @@
+.falling-leaves {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ pointer-events: none;
+ z-index: 0;
+ overflow: hidden;
+}
+
+.leaf-scene {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ transform-style: preserve-3d;
+
+ div {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 20px;
+ height: 20px;
+ background-repeat: no-repeat;
+ background-size: 100%;
+ transform-style: preserve-3d;
+ backface-visibility: visible;
+ }
+}
\ No newline at end of file
diff --git a/client/src/FallingLeaves.tsx b/client/src/FallingLeaves.tsx
new file mode 100644
index 0000000..876ff75
--- /dev/null
+++ b/client/src/FallingLeaves.tsx
@@ -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ů
+ *
+ *
+ * @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;
\ No newline at end of file