diff --git a/client/public/butterfly-blue.svg b/client/public/butterfly-blue.svg
new file mode 100644
index 0000000..2201c61
--- /dev/null
+++ b/client/public/butterfly-blue.svg
@@ -0,0 +1,24 @@
+
diff --git a/client/public/butterfly-orange.svg b/client/public/butterfly-orange.svg
new file mode 100644
index 0000000..2a7f359
--- /dev/null
+++ b/client/public/butterfly-orange.svg
@@ -0,0 +1,24 @@
+
diff --git a/client/public/butterfly-pink.svg b/client/public/butterfly-pink.svg
new file mode 100644
index 0000000..60ac55e
--- /dev/null
+++ b/client/public/butterfly-pink.svg
@@ -0,0 +1,24 @@
+
diff --git a/client/public/butterfly-yellow.svg b/client/public/butterfly-yellow.svg
new file mode 100644
index 0000000..6565b8a
--- /dev/null
+++ b/client/public/butterfly-yellow.svg
@@ -0,0 +1,24 @@
+
diff --git a/client/src/AppRoutes.tsx b/client/src/AppRoutes.tsx
index 30e707e..08574ba 100644
--- a/client/src/AppRoutes.tsx
+++ b/client/src/AppRoutes.tsx
@@ -1,7 +1,7 @@
import { Routes, Route } from "react-router-dom";
import { ProvideSettings } from "./context/settings";
-// import Snowfall from "react-snowfall";
-import { SnowOverlay } from 'react-snow-overlay';
+import FlyingButterflies, { BUTTERFLY_PRESETS } from "./FlyingButterflies";
+import "./FlyingButterflies.scss";
import { ToastContainer } from "react-toastify";
import { SocketContext, socket } from "./context/socket";
import StatsPage from "./pages/StatsPage";
@@ -34,13 +34,7 @@ export default function AppRoutes() {
<>
- {/* */}
-
+
>
diff --git a/client/src/FlyingButterflies.scss b/client/src/FlyingButterflies.scss
new file mode 100644
index 0000000..9507914
--- /dev/null
+++ b/client/src/FlyingButterflies.scss
@@ -0,0 +1,56 @@
+.flying-butterflies {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ pointer-events: none;
+ z-index: 2;
+ overflow: hidden;
+}
+
+.butterfly-scene {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+
+ // Vnější element – pozici a natočení nastavuje JS přes transform
+ > div {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 34px;
+ height: 34px;
+ will-change: transform;
+ }
+}
+
+// Vnitřní element se sprite motýla – máchá křídly (scaleX kolem svislé osy těla)
+.butterfly-sprite {
+ width: 100%;
+ height: 100%;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ animation: butterfly-flap 0.45s ease-in-out infinite;
+ filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.15));
+}
+
+@keyframes butterfly-flap {
+ 0%,
+ 100% {
+ transform: scaleX(1);
+ }
+ 50% {
+ transform: scaleX(0.35);
+ }
+}
+
+// Ohleduplnost k uživatelům, kteří nechtějí pohyb
+@media (prefers-reduced-motion: reduce) {
+ .butterfly-sprite {
+ animation: none;
+ }
+}
diff --git a/client/src/FlyingButterflies.tsx b/client/src/FlyingButterflies.tsx
new file mode 100644
index 0000000..3c02303
--- /dev/null
+++ b/client/src/FlyingButterflies.tsx
@@ -0,0 +1,238 @@
+import React, { useEffect, useRef, useCallback } from 'react';
+
+// Různé barevné varianty motýlů
+const BUTTERFLY_VARIANTS = [
+ 'butterfly-orange.svg', // Oranžová (monarcha)
+ 'butterfly-blue.svg', // Modrá
+ 'butterfly-yellow.svg', // Žlutá (otakárek)
+ 'butterfly-pink.svg', // Růžová
+] as const;
+
+interface ButterflyData {
+ /** Vnější element – nese pozici, natočení a velikost */
+ el: HTMLDivElement;
+ /** Vnitřní element – nese CSS animaci mávání křídly */
+ sprite: HTMLDivElement;
+ x: number;
+ y: number;
+ /** Směr letu vodorovně: +1 doprava, -1 doleva */
+ dir: number;
+ /** Vodorovná rychlost (px/snímek) */
+ speed: number;
+ /** Velikost (měřítko sprite) */
+ size: number;
+ // Dvě sinusovky pro přirozené zvlněné stoupání a klesání
+ bobFreq1: number;
+ bobPhase1: number;
+ bobAmp1: number;
+ bobFreq2: number;
+ bobPhase2: number;
+ bobAmp2: number;
+}
+
+interface FlyingButterfliesProps {
+ /** Počet poletujících motýlů (výchozí: 9) */
+ numButterflies?: number;
+ /** CSS třída pro kontejner (výchozí: 'flying-butterflies') */
+ className?: string;
+ /** Barevné varianty motýlů k použití (výchozí: všechny) */
+ butterflyVariants?: readonly string[];
+}
+
+class ButterflyScene {
+ private viewport: HTMLElement;
+ private world: HTMLDivElement;
+ private butterflies: ButterflyData[] = [];
+ private numButterflies: number;
+ private variants: readonly string[];
+ private width: number;
+ private height: number;
+ private timer: number = 0;
+ private animationId: number | null = null;
+ private handleResize: () => void;
+
+ // Základní velikost sprite v px (dále se násobí náhodným měřítkem)
+ private static readonly BASE_SIZE = 34;
+
+ constructor(el: HTMLElement, numButterflies: number = 9, variants: readonly string[] = BUTTERFLY_VARIANTS) {
+ this.viewport = el;
+ this.world = document.createElement('div');
+ this.numButterflies = numButterflies;
+ this.variants = variants;
+ this.width = this.viewport.offsetWidth;
+ this.height = this.viewport.offsetHeight;
+
+ this.handleResize = () => {
+ this.width = this.viewport.offsetWidth;
+ this.height = this.viewport.offsetHeight;
+ };
+ }
+
+ // Nastaví motýlovi nové náhodné parametry a umístí ho na okraj obrazovky
+ private resetButterfly = (b: ButterflyData): void => {
+ // Vletí zleva nebo zprava
+ b.dir = Math.random() > 0.5 ? 1 : -1;
+ b.speed = Math.random() * 1.1 + 0.7; // 0.7 – 1.8 px/snímek
+
+ // Na začátku (timer 0) rozprostřeme motýly po celé šířce, ať není obrazovka prázdná
+ if (this.timer === 0) {
+ b.x = Math.random() * this.width;
+ } else {
+ b.x = b.dir === 1 ? -40 : this.width + 40;
+ }
+ b.y = Math.random() * (this.height - 60) + 30;
+
+ b.size = Math.random() * 0.6 + 0.6; // 0.6 – 1.2
+
+ // Dvě sinusovky s různou frekvencí → nepravidelné, měkké křivky letu
+ b.bobFreq1 = Math.random() * 0.02 + 0.03;
+ b.bobPhase1 = Math.random() * Math.PI * 2;
+ b.bobAmp1 = Math.random() * 0.8 + 0.6;
+ b.bobFreq2 = Math.random() * 0.015 + 0.01;
+ b.bobPhase2 = Math.random() * Math.PI * 2;
+ b.bobAmp2 = Math.random() * 0.6 + 0.4;
+
+ // Náhodná varianta a rychlost mávání křídly
+ const variant = this.variants[Math.floor(Math.random() * this.variants.length)];
+ b.sprite.style.backgroundImage = `url(${variant})`;
+ b.sprite.style.animationDuration = `${Math.random() * 0.25 + 0.3}s`; // 0.3 – 0.55 s
+ };
+
+ private updateButterfly = (b: ButterflyData): void => {
+ const vx = b.dir * b.speed;
+ // Svislá rychlost jako součet dvou sinusovek – přirozené plachtění
+ const vy =
+ Math.sin(this.timer * b.bobFreq1 + b.bobPhase1) * b.bobAmp1 +
+ Math.sin(this.timer * b.bobFreq2 + b.bobPhase2) * b.bobAmp2;
+
+ b.x += vx;
+ b.y += vy;
+
+ // Sprite natočíme po směru letu (motýl je nakreslený hlavou nahoru)
+ const angle = Math.atan2(vy, vx) * (180 / Math.PI) + 90;
+
+ b.el.style.transform = `translate(${b.x}px, ${b.y}px) rotate(${angle}deg) scale(${b.size})`;
+
+ // Reset po opuštění obrazovky (s rezervou na natočení)
+ if (
+ (b.dir === 1 && b.x > this.width + 50) ||
+ (b.dir === -1 && b.x < -50) ||
+ b.y < -60 ||
+ b.y > this.height + 60
+ ) {
+ this.resetButterfly(b);
+ }
+ };
+
+ public init = (): void => {
+ this.butterflies = [];
+ this.world.innerHTML = '';
+ this.world.className = 'butterfly-scene';
+
+ for (let i = 0; i < this.numButterflies; i++) {
+ const el = document.createElement('div');
+ const sprite = document.createElement('div');
+ sprite.className = 'butterfly-sprite';
+ el.appendChild(sprite);
+
+ const b: ButterflyData = {
+ el,
+ sprite,
+ x: 0,
+ y: 0,
+ dir: 1,
+ speed: 1,
+ size: 1,
+ bobFreq1: 0,
+ bobPhase1: 0,
+ bobAmp1: 0,
+ bobFreq2: 0,
+ bobPhase2: 0,
+ bobAmp2: 0,
+ };
+
+ this.resetButterfly(b);
+ this.butterflies.push(b);
+ this.world.appendChild(el);
+ }
+
+ this.viewport.appendChild(this.world);
+ window.addEventListener('resize', this.handleResize);
+ };
+
+ public render = (): void => {
+ for (let i = 0; i < this.butterflies.length; i++) {
+ this.updateButterfly(this.butterflies[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.handleResize);
+ };
+}
+
+/**
+ * Komponenta pro zobrazení poletujících motýlů na pozadí stránky.
+ * Motýli plachtí vodorovně po měkkých zvlněných křivkách a mávají křídly.
+ *
+ * @param numButterflies - Počet motýlů (výchozí: 9)
+ * @param className - CSS třída pro kontejner (výchozí: 'flying-butterflies')
+ * @param butterflyVariants - Barevné varianty motýlů (výchozí: všechny)
+ */
+const FlyingButterflies: React.FC = ({
+ numButterflies = 9,
+ className = 'flying-butterflies',
+ butterflyVariants = BUTTERFLY_VARIANTS,
+}) => {
+ const containerRef = useRef(null);
+ const sceneRef = useRef(null);
+
+ const initialize = useCallback(() => {
+ if (containerRef.current) {
+ sceneRef.current = new ButterflyScene(containerRef.current, numButterflies, butterflyVariants);
+ sceneRef.current.init();
+ sceneRef.current.render();
+ }
+ }, [numButterflies, butterflyVariants]);
+
+ useEffect(() => {
+ initialize();
+
+ return () => {
+ if (sceneRef.current) {
+ sceneRef.current.destroy();
+ sceneRef.current = null;
+ }
+ };
+ }, [initialize]);
+
+ return ;
+};
+
+// Přednastavení množství motýlů pro různé účely
+export const BUTTERFLY_PRESETS = {
+ LIGHT: 5, // Pár motýlů
+ NORMAL: 9, // Standardní množství
+ HEAVY: 16, // Rušná letní louka
+} as const;
+
+// Přednastavené barevné kombinace
+export const BUTTERFLY_COLOR_THEMES = {
+ ALL: BUTTERFLY_VARIANTS,
+ WARM: ['butterfly-orange.svg', 'butterfly-yellow.svg', 'butterfly-pink.svg'] as const,
+ COOL: ['butterfly-blue.svg'] as const,
+} as const;
+
+export default FlyingButterflies;
diff --git a/server/changelogs/2026-07-07.json b/server/changelogs/2026-07-07.json
index 355a51b..10c43e3 100644
--- a/server/changelogs/2026-07-07.json
+++ b/server/changelogs/2026-07-07.json
@@ -1,5 +1,6 @@
[
"Oprava: uložení preferovaného času odchodu i u podniku bez načteného menu",
"Oprava: načítání denního menu podniku TechTower po změně jejich webu",
- "Oprava: rychlé klikání na šipky přepínání dní už nepřeskočí mimo pracovní týden"
+ "Oprava: rychlé klikání na šipky přepínání dní už nepřeskočí mimo pracovní týden",
+ "Letní vzhled: místo padajícího sněhu teď po stránce poletují motýli"
]