From a708eda4217c4bbb6414680fa645c81789f43004 Mon Sep 17 00:00:00 2001 From: Martin Berka Date: Tue, 7 Jul 2026 13:29:28 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20letn=C3=AD=20vzhled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/public/butterfly-blue.svg | 24 +++ client/public/butterfly-orange.svg | 24 +++ client/public/butterfly-pink.svg | 24 +++ client/public/butterfly-yellow.svg | 24 +++ client/src/AppRoutes.tsx | 12 +- client/src/FlyingButterflies.scss | 56 +++++++ client/src/FlyingButterflies.tsx | 238 +++++++++++++++++++++++++++++ server/changelogs/2026-07-07.json | 3 +- 8 files changed, 395 insertions(+), 10 deletions(-) create mode 100644 client/public/butterfly-blue.svg create mode 100644 client/public/butterfly-orange.svg create mode 100644 client/public/butterfly-pink.svg create mode 100644 client/public/butterfly-yellow.svg create mode 100644 client/src/FlyingButterflies.scss create mode 100644 client/src/FlyingButterflies.tsx 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" ]