feat: chytání motýlků síťkou s počítadlem
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 23s
CI / Build server (push) Successful in 35s
CI / Build client (push) Successful in 42s
CI / Playwright E2E tests (push) Successful in 1m27s
CI / Build and push Docker image (push) Successful in 42s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 23s
CI / Build server (push) Successful in 35s
CI / Build client (push) Successful in 42s
CI / Playwright E2E tests (push) Successful in 1m27s
CI / Build and push Docker image (push) Successful in 42s
CI / Notify (push) Successful in 2s
Přidána síťka, kterou lze uchopit myší a lovit poletující motýly. Po chycení přiletí nový (počet létajících se nemění), počet ulovených se zobrazuje jako odznak a ukládá do local storage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
146c31b775
commit
c8c5ecc60c
@@ -0,0 +1,106 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
width="100"
|
||||||
|
height="100">
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="netFill" x1="0" y1="0" x2="0.4" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#E4EDF5" stop-opacity="0.18"/>
|
||||||
|
<stop offset="100%" stop-color="#B9C4CF" stop-opacity="0.08"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Celý tvar síťového vaku -->
|
||||||
|
<clipPath id="netBagClip">
|
||||||
|
<path d="
|
||||||
|
M 36 8
|
||||||
|
C 21 8, 9 20, 9 35
|
||||||
|
C 9 51, 13 70, 19 84
|
||||||
|
C 22 92, 27 94, 31 87
|
||||||
|
C 40 72, 54 51, 63 35
|
||||||
|
C 63 20, 51 8, 36 8
|
||||||
|
Z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Síťový vak za obručí -->
|
||||||
|
<path d="
|
||||||
|
M 36 8
|
||||||
|
C 21 8, 9 20, 9 35
|
||||||
|
C 9 51, 13 70, 19 84
|
||||||
|
C 22 92, 27 94, 31 87
|
||||||
|
C 40 72, 54 51, 63 35
|
||||||
|
C 63 20, 51 8, 36 8
|
||||||
|
Z"
|
||||||
|
fill="url(#netFill)"
|
||||||
|
stroke="#AEBBC8"
|
||||||
|
stroke-width="1"
|
||||||
|
opacity="0.7"/>
|
||||||
|
|
||||||
|
<!-- Výplet síťky -->
|
||||||
|
<g clip-path="url(#netBagClip)"
|
||||||
|
fill="none"
|
||||||
|
stroke="#AEBBC8"
|
||||||
|
stroke-width="0.8"
|
||||||
|
opacity="0.6"
|
||||||
|
stroke-linecap="round">
|
||||||
|
|
||||||
|
<!-- Vlákna začínající na horní a boční obruči -->
|
||||||
|
<path d="M 16 17 C 15 42, 19 69, 25 90"/>
|
||||||
|
<path d="M 25 10 C 23 39, 24 69, 25 90"/>
|
||||||
|
<path d="M 36 8 C 34 39, 30 69, 25 90"/>
|
||||||
|
<path d="M 47 10 C 44 40, 36 70, 25 90"/>
|
||||||
|
<path d="M 56 18 C 51 44, 41 72, 25 90"/>
|
||||||
|
<path d="M 62 30 C 55 51, 43 75, 25 90"/>
|
||||||
|
|
||||||
|
<!-- Vlákna od levého okraje obruče -->
|
||||||
|
<path d="M 9 29 C 13 49, 18 72, 25 90"/>
|
||||||
|
<path d="M 10 42 C 14 60, 19 78, 25 90"/>
|
||||||
|
|
||||||
|
<!-- Příčná oka -->
|
||||||
|
<path d="M 16 17 C 28 22, 45 22, 56 18"/>
|
||||||
|
<path d="M 10 28 C 25 34, 47 34, 62 29"/>
|
||||||
|
<path d="M 9 40 C 25 46, 45 47, 59 42"/>
|
||||||
|
<path d="M 11 52 C 24 58, 41 59, 53 54"/>
|
||||||
|
<path d="M 13 64 C 24 69, 36 70, 45 66"/>
|
||||||
|
<path d="M 16 75 C 24 79, 31 80, 38 77"/>
|
||||||
|
<path d="M 20 84 C 24 87, 28 87, 32 84"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Rukojeť -->
|
||||||
|
<line x1="57" y1="57"
|
||||||
|
x2="93" y2="93"
|
||||||
|
stroke="#8B5A2B"
|
||||||
|
stroke-width="6"
|
||||||
|
stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<line x1="57" y1="57"
|
||||||
|
x2="93" y2="93"
|
||||||
|
stroke="#A9713A"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Kovové napojení rukojeti -->
|
||||||
|
<line x1="56" y1="55"
|
||||||
|
x2="61" y2="61"
|
||||||
|
stroke="#71808E"
|
||||||
|
stroke-width="6"
|
||||||
|
stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<line x1="56" y1="55"
|
||||||
|
x2="61" y2="61"
|
||||||
|
stroke="#B8C4CF"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Hlavní obruč – nezměněná -->
|
||||||
|
<circle cx="36" cy="35" r="27"
|
||||||
|
fill="none"
|
||||||
|
stroke="#7A8896"
|
||||||
|
stroke-width="4.5"/>
|
||||||
|
|
||||||
|
<circle cx="36" cy="35" r="27"
|
||||||
|
fill="none"
|
||||||
|
stroke="#AEBBC8"
|
||||||
|
stroke-width="2"/>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -48,9 +48,137 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Síťka na chytání --------------------------------------------------------
|
||||||
|
|
||||||
|
.butterfly-net {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: grab;
|
||||||
|
z-index: 4;
|
||||||
|
will-change: transform;
|
||||||
|
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.25));
|
||||||
|
transition: filter 0.15s ease;
|
||||||
|
// zabrání označování/scrollování při tažení na dotykových zařízeních
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.grabbed {
|
||||||
|
cursor: grabbing;
|
||||||
|
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.35));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.catch-pop {
|
||||||
|
animation: butterfly-net-pop 0.3s ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes butterfly-net-pop {
|
||||||
|
0% {
|
||||||
|
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.25)) brightness(1);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
filter: drop-shadow(0 2px 8px rgba(120, 200, 120, 0.7)) brightness(1.25);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.25)) brightness(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prchavé „+1" v místě chycení
|
||||||
|
.butterfly-catch-fx {
|
||||||
|
position: fixed;
|
||||||
|
margin: -12px 0 0 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #2f9e44;
|
||||||
|
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 5;
|
||||||
|
animation: butterfly-catch-float 0.75s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes butterfly-catch-float {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(0) scale(0.9);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-6px) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-40px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Počítadlo chycených motýlů ----------------------------------------------
|
||||||
|
|
||||||
|
.butterfly-counter {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
left: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px 6px 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #333;
|
||||||
|
z-index: 5;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.bump {
|
||||||
|
animation: butterfly-counter-bump 0.3s ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.butterfly-counter-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-image: url(butterfly-orange.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.butterfly-counter-value {
|
||||||
|
min-width: 1ch;
|
||||||
|
text-align: center;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes butterfly-counter-bump {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1.18);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ohleduplnost k uživatelům, kteří nechtějí pohyb
|
// Ohleduplnost k uživatelům, kteří nechtějí pohyb
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.butterfly-sprite {
|
.butterfly-sprite {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.butterfly-net.catch-pop,
|
||||||
|
.butterfly-counter.bump {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useCallback } from 'react';
|
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
|
|
||||||
// Různé barevné varianty motýlů
|
// Různé barevné varianty motýlů
|
||||||
const BUTTERFLY_VARIANTS = [
|
const BUTTERFLY_VARIANTS = [
|
||||||
@@ -8,6 +8,9 @@ const BUTTERFLY_VARIANTS = [
|
|||||||
'butterfly-pink.svg', // Růžová
|
'butterfly-pink.svg', // Růžová
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
// Klíč pro uložení počtu chycených motýlů do local storage
|
||||||
|
const CAUGHT_STORAGE_KEY = 'flyingButterfliesCaught';
|
||||||
|
|
||||||
interface ButterflyData {
|
interface ButterflyData {
|
||||||
/** Vnější element – nese pozici, natočení a velikost */
|
/** Vnější element – nese pozici, natočení a velikost */
|
||||||
el: HTMLDivElement;
|
el: HTMLDivElement;
|
||||||
@@ -37,6 +40,10 @@ interface FlyingButterfliesProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
/** Barevné varianty motýlů k použití (výchozí: všechny) */
|
/** Barevné varianty motýlů k použití (výchozí: všechny) */
|
||||||
butterflyVariants?: readonly string[];
|
butterflyVariants?: readonly string[];
|
||||||
|
/** Zapne síťku na chytání motýlů (výchozí: true) */
|
||||||
|
enableNet?: boolean;
|
||||||
|
/** Callback při chycení motýla (obdrží nový celkový počet) */
|
||||||
|
onCatch?: (total: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ButterflyScene {
|
class ButterflyScene {
|
||||||
@@ -51,17 +58,42 @@ class ButterflyScene {
|
|||||||
private animationId: number | null = null;
|
private animationId: number | null = null;
|
||||||
private handleResize: () => void;
|
private handleResize: () => void;
|
||||||
|
|
||||||
|
// Síťka na chytání
|
||||||
|
private net: HTMLDivElement;
|
||||||
|
private netEnabled: boolean;
|
||||||
|
private netGrabbed: boolean = false;
|
||||||
|
private netX: number = 0;
|
||||||
|
private netY: number = 0;
|
||||||
|
private onCatch?: () => void;
|
||||||
|
|
||||||
// Základní velikost sprite v px (dále se násobí náhodným měřítkem)
|
// Základní velikost sprite v px (dále se násobí náhodným měřítkem)
|
||||||
private static readonly BASE_SIZE = 34;
|
private static readonly BASE_SIZE = 34;
|
||||||
|
|
||||||
constructor(el: HTMLElement, numButterflies: number = 9, variants: readonly string[] = BUTTERFLY_VARIANTS) {
|
// Rozměry síťky (px) a poloha obruče v rámci SVG (viewBox 100×100, střed 36×35)
|
||||||
|
private static readonly NET_SIZE = 120;
|
||||||
|
private static readonly HOOP_OFFSET_X = ButterflyScene.NET_SIZE * 0.36;
|
||||||
|
private static readonly HOOP_OFFSET_Y = ButterflyScene.NET_SIZE * 0.35;
|
||||||
|
// Dosah chytání kolem středu obruče
|
||||||
|
private static readonly CATCH_RADIUS = 40;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
el: HTMLElement,
|
||||||
|
numButterflies: number = 9,
|
||||||
|
variants: readonly string[] = BUTTERFLY_VARIANTS,
|
||||||
|
netEnabled: boolean = true,
|
||||||
|
onCatch?: () => void,
|
||||||
|
) {
|
||||||
this.viewport = el;
|
this.viewport = el;
|
||||||
this.world = document.createElement('div');
|
this.world = document.createElement('div');
|
||||||
this.numButterflies = numButterflies;
|
this.numButterflies = numButterflies;
|
||||||
this.variants = variants;
|
this.variants = variants;
|
||||||
|
this.netEnabled = netEnabled;
|
||||||
|
this.onCatch = onCatch;
|
||||||
this.width = this.viewport.offsetWidth;
|
this.width = this.viewport.offsetWidth;
|
||||||
this.height = this.viewport.offsetHeight;
|
this.height = this.viewport.offsetHeight;
|
||||||
|
|
||||||
|
this.net = document.createElement('div');
|
||||||
|
|
||||||
this.handleResize = () => {
|
this.handleResize = () => {
|
||||||
this.width = this.viewport.offsetWidth;
|
this.width = this.viewport.offsetWidth;
|
||||||
this.height = this.viewport.offsetHeight;
|
this.height = this.viewport.offsetHeight;
|
||||||
@@ -124,6 +156,100 @@ class ButterflyScene {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Síťka na chytání ---------------------------------------------------
|
||||||
|
|
||||||
|
private updateNetTransform = (): void => {
|
||||||
|
this.net.style.transform =
|
||||||
|
`translate(${this.netX - ButterflyScene.HOOP_OFFSET_X}px, ` +
|
||||||
|
`${this.netY - ButterflyScene.HOOP_OFFSET_Y}px)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onPointerDown = (e: PointerEvent): void => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.netGrabbed = true;
|
||||||
|
this.net.classList.add('grabbed');
|
||||||
|
this.netX = e.clientX;
|
||||||
|
this.netY = e.clientY;
|
||||||
|
this.updateNetTransform();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onPointerMove = (e: PointerEvent): void => {
|
||||||
|
if (!this.netGrabbed) return;
|
||||||
|
this.netX = e.clientX;
|
||||||
|
this.netY = e.clientY;
|
||||||
|
this.updateNetTransform();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onPointerUp = (): void => {
|
||||||
|
if (!this.netGrabbed) return;
|
||||||
|
this.netGrabbed = false;
|
||||||
|
this.net.classList.remove('grabbed');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zobrazí prchavé „+1" v místě chycení
|
||||||
|
private spawnCatchFx = (): void => {
|
||||||
|
const fx = document.createElement('div');
|
||||||
|
fx.className = 'butterfly-catch-fx';
|
||||||
|
fx.textContent = '+1';
|
||||||
|
fx.style.left = `${this.netX}px`;
|
||||||
|
fx.style.top = `${this.netY}px`;
|
||||||
|
this.viewport.appendChild(fx);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (fx.parentNode) fx.parentNode.removeChild(fx);
|
||||||
|
}, 750);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zkontroluje, zda obruč překrývá nějakého motýla – a chytí ho
|
||||||
|
private checkCatches = (): void => {
|
||||||
|
const r2 = ButterflyScene.CATCH_RADIUS * ButterflyScene.CATCH_RADIUS;
|
||||||
|
const half = (ButterflyScene.BASE_SIZE / 2);
|
||||||
|
let caughtAny = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.butterflies.length; i++) {
|
||||||
|
const b = this.butterflies[i];
|
||||||
|
const cx = b.x + half * b.size;
|
||||||
|
const cy = b.y + half * b.size;
|
||||||
|
const dx = cx - this.netX;
|
||||||
|
const dy = cy - this.netY;
|
||||||
|
|
||||||
|
if (dx * dx + dy * dy < r2) {
|
||||||
|
caughtAny = true;
|
||||||
|
if (this.onCatch) this.onCatch();
|
||||||
|
// Chycený motýl je nahrazen novým – celkový počet létajících se nemění
|
||||||
|
this.resetButterfly(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caughtAny) {
|
||||||
|
this.spawnCatchFx();
|
||||||
|
this.net.classList.remove('catch-pop');
|
||||||
|
// vynucení reflow pro restart animace
|
||||||
|
void this.net.offsetWidth;
|
||||||
|
this.net.classList.add('catch-pop');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private initNet = (): void => {
|
||||||
|
this.net.className = 'butterfly-net';
|
||||||
|
this.net.style.width = `${ButterflyScene.NET_SIZE}px`;
|
||||||
|
this.net.style.height = `${ButterflyScene.NET_SIZE}px`;
|
||||||
|
this.net.style.backgroundImage = 'url(butterfly-net.svg)';
|
||||||
|
|
||||||
|
// Výchozí poloha – vpravo dole
|
||||||
|
this.netX = this.width * 0.82;
|
||||||
|
this.netY = this.height * 0.72;
|
||||||
|
this.updateNetTransform();
|
||||||
|
|
||||||
|
this.net.addEventListener('pointerdown', this.onPointerDown);
|
||||||
|
window.addEventListener('pointermove', this.onPointerMove);
|
||||||
|
window.addEventListener('pointerup', this.onPointerUp);
|
||||||
|
window.addEventListener('pointercancel', this.onPointerUp);
|
||||||
|
|
||||||
|
this.viewport.appendChild(this.net);
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
public init = (): void => {
|
public init = (): void => {
|
||||||
this.butterflies = [];
|
this.butterflies = [];
|
||||||
this.world.innerHTML = '';
|
this.world.innerHTML = '';
|
||||||
@@ -157,6 +283,11 @@ class ButterflyScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.viewport.appendChild(this.world);
|
this.viewport.appendChild(this.world);
|
||||||
|
|
||||||
|
if (this.netEnabled) {
|
||||||
|
this.initNet();
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', this.handleResize);
|
window.addEventListener('resize', this.handleResize);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,6 +296,10 @@ class ButterflyScene {
|
|||||||
this.updateButterfly(this.butterflies[i]);
|
this.updateButterfly(this.butterflies[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.netEnabled && this.netGrabbed) {
|
||||||
|
this.checkCatches();
|
||||||
|
}
|
||||||
|
|
||||||
this.timer++;
|
this.timer++;
|
||||||
this.animationId = requestAnimationFrame(this.render);
|
this.animationId = requestAnimationFrame(this.render);
|
||||||
};
|
};
|
||||||
@@ -179,6 +314,14 @@ class ButterflyScene {
|
|||||||
this.world.parentNode.removeChild(this.world);
|
this.world.parentNode.removeChild(this.world);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.net.removeEventListener('pointerdown', this.onPointerDown);
|
||||||
|
window.removeEventListener('pointermove', this.onPointerMove);
|
||||||
|
window.removeEventListener('pointerup', this.onPointerUp);
|
||||||
|
window.removeEventListener('pointercancel', this.onPointerUp);
|
||||||
|
if (this.net.parentNode) {
|
||||||
|
this.net.parentNode.removeChild(this.net);
|
||||||
|
}
|
||||||
|
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -186,26 +329,62 @@ class ButterflyScene {
|
|||||||
/**
|
/**
|
||||||
* Komponenta pro zobrazení poletujících motýlů na pozadí stránky.
|
* 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.
|
* Motýli plachtí vodorovně po měkkých zvlněných křivkách a mávají křídly.
|
||||||
|
* Volitelná síťka umožňuje motýly chytat – po chycení přiletí nový a počet
|
||||||
|
* chycených se ukládá do local storage.
|
||||||
*
|
*
|
||||||
* @param numButterflies - Počet motýlů (výchozí: 9)
|
* @param numButterflies - Počet motýlů (výchozí: 9)
|
||||||
* @param className - CSS třída pro kontejner (výchozí: 'flying-butterflies')
|
* @param className - CSS třída pro kontejner (výchozí: 'flying-butterflies')
|
||||||
* @param butterflyVariants - Barevné varianty motýlů (výchozí: všechny)
|
* @param butterflyVariants - Barevné varianty motýlů (výchozí: všechny)
|
||||||
|
* @param enableNet - Zapne síťku na chytání (výchozí: true)
|
||||||
|
* @param onCatch - Callback při chycení motýla (obdrží nový celkový počet)
|
||||||
*/
|
*/
|
||||||
const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
||||||
numButterflies = 9,
|
numButterflies = 9,
|
||||||
className = 'flying-butterflies',
|
className = 'flying-butterflies',
|
||||||
butterflyVariants = BUTTERFLY_VARIANTS,
|
butterflyVariants = BUTTERFLY_VARIANTS,
|
||||||
|
enableNet = true,
|
||||||
|
onCatch,
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const sceneRef = useRef<ButterflyScene | null>(null);
|
const sceneRef = useRef<ButterflyScene | null>(null);
|
||||||
|
const badgeRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [caught, setCaught] = useState<number>(() => {
|
||||||
|
try {
|
||||||
|
const stored = Number(localStorage.getItem(CAUGHT_STORAGE_KEY));
|
||||||
|
return Number.isFinite(stored) && stored > 0 ? stored : 0;
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stabilní callback – scéna se kvůli změně počtu nemusí přegenerovat
|
||||||
|
const handleCatch = useCallback(() => {
|
||||||
|
setCaught((prev) => {
|
||||||
|
const next = prev + 1;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(CAUGHT_STORAGE_KEY, String(next));
|
||||||
|
} catch {
|
||||||
|
/* local storage nedostupné – počet se prostě neuloží */
|
||||||
|
}
|
||||||
|
if (onCatch) onCatch(next);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [onCatch]);
|
||||||
|
|
||||||
const initialize = useCallback(() => {
|
const initialize = useCallback(() => {
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
sceneRef.current = new ButterflyScene(containerRef.current, numButterflies, butterflyVariants);
|
sceneRef.current = new ButterflyScene(
|
||||||
|
containerRef.current,
|
||||||
|
numButterflies,
|
||||||
|
butterflyVariants,
|
||||||
|
enableNet,
|
||||||
|
handleCatch,
|
||||||
|
);
|
||||||
sceneRef.current.init();
|
sceneRef.current.init();
|
||||||
sceneRef.current.render();
|
sceneRef.current.render();
|
||||||
}
|
}
|
||||||
}, [numButterflies, butterflyVariants]);
|
}, [numButterflies, butterflyVariants, enableNet, handleCatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initialize();
|
initialize();
|
||||||
@@ -218,7 +397,26 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
};
|
};
|
||||||
}, [initialize]);
|
}, [initialize]);
|
||||||
|
|
||||||
return <div ref={containerRef} className={className} />;
|
// Krátké „poskočení" počítadla při každém chycení
|
||||||
|
useEffect(() => {
|
||||||
|
const node = badgeRef.current;
|
||||||
|
if (!node || caught === 0) return;
|
||||||
|
node.classList.remove('bump');
|
||||||
|
void node.offsetWidth;
|
||||||
|
node.classList.add('bump');
|
||||||
|
}, [caught]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={containerRef} className={className} />
|
||||||
|
{enableNet && (
|
||||||
|
<div ref={badgeRef} className="butterfly-counter" title="Chycení motýli">
|
||||||
|
<span className="butterfly-counter-icon" aria-hidden="true" />
|
||||||
|
<span className="butterfly-counter-value">{caught}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Přednastavení množství motýlů pro různé účely
|
// Přednastavení množství motýlů pro různé účely
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Chytání motýlků: uchop myší síťku a lov poletující motýly – počet ulovených se ti počítá a ukládá"
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user