feat: HG mode - změna obrázků, animace v modálech
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 27s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 43s
CI / Playwright E2E tests (push) Successful in 2m2s
CI / Build and push Docker image (push) Successful in 45s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 27s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 43s
CI / Playwright E2E tests (push) Successful in 2m2s
CI / Build and push Docker image (push) Successful in 45s
CI / Notify (push) Successful in 2s
This commit is contained in:
1 parent
5147651e7c
commit
2a54a1d41f
7 files changed
+198
-2
No files matched your search
@@ -0,0 +1,24 @@
|
||||
// Plamen stylu Hell Girl — hoří na černé přes obsah dialogu (.modal-content je relative a ve stylu
|
||||
// Hell Girl ořezává přetečení), pak černá odezní a odhalí obsah.
|
||||
.hg-burn {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
background: #020204; // barva pozadí GIFu, aby kolem plamene nebyl vidět obdélník
|
||||
pointer-events: none;
|
||||
animation: hg-burn-reveal 1.5s ease-in forwards;
|
||||
}
|
||||
|
||||
.hg-burn-flame {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
// Dlouhý dialog může přesahovat obrazovku — plamen držet ve viditelné části
|
||||
top: min(50%, 45vh);
|
||||
height: min(85%, 60vh);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@keyframes hg-burn-reveal {
|
||||
0%, 50% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { createSymbolMask, loadHellGirlImages } from "../utils/hellGirlImages";
|
||||
import "./HellGirlFlames.scss";
|
||||
|
||||
/** Plamen vzplane a rozpadne se v jiskry za ~1,2 s, pak GIF drží černý snímek — mezitím se obsah vynoří z černé. */
|
||||
const BURN_MS = 1500;
|
||||
|
||||
/** `target` = obsah dialogu, ve kterém se hoří. */
|
||||
type Burn = { src: string, target: HTMLElement };
|
||||
|
||||
/**
|
||||
* Doplňky stylu Hell Girl: při otevření dialogu jeho obsah zčerná, uprostřed jednou
|
||||
* vzplane plamen („Ippen, shinde miru?") a obsah se pak z černé vynoří. Dále symbol ohně ze znělky
|
||||
* u titulku dne (nastaví CSS proměnnou --hg-symbol, dokud se nenačte, zůstávají v titulku původní květy).
|
||||
*/
|
||||
const HellGirlFlames: React.FC = () => {
|
||||
const [burning, setBurning] = useState<Burn>();
|
||||
const flameBlobRef = useRef<Blob | undefined>(undefined);
|
||||
|
||||
const burn = (target: HTMLElement) => {
|
||||
const blob = flameBlobRef.current;
|
||||
if (!blob || window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return;
|
||||
// Pokaždé nová URL — jinak by prohlížeč GIF (bez opakování) znovu nepřehrál
|
||||
setBurning({ src: URL.createObjectURL(blob), target });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!burning) return;
|
||||
const timeout = setTimeout(() => setBurning(undefined), BURN_MS);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
URL.revokeObjectURL(burning.src);
|
||||
};
|
||||
}, [burning]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const root = document.documentElement;
|
||||
loadHellGirlImages().then(images => {
|
||||
if (!images || cancelled) return;
|
||||
fetch(images.flame)
|
||||
.then(r => r.ok ? r.blob() : undefined)
|
||||
.then(blob => { flameBlobRef.current = blob; })
|
||||
.catch(() => { /* bez plamenů */ });
|
||||
createSymbolMask(images.symbol).then(mask => {
|
||||
if (!mask || cancelled) return;
|
||||
root.style.setProperty('--hg-symbol', `url(${mask})`);
|
||||
root.setAttribute('data-hg-symbol', '');
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
root.style.removeProperty('--hg-symbol');
|
||||
root.removeAttribute('data-hg-symbol');
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Dialogy (react-bootstrap) se vkládají přes portál přímo do <body>
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver(mutations => {
|
||||
const modalContent = mutations
|
||||
.flatMap(m => Array.from(m.addedNodes))
|
||||
.map(node => node instanceof HTMLElement ? node.querySelector<HTMLElement>('.modal-content') : null)
|
||||
.find(element => !!element);
|
||||
if (modalContent) burn(modalContent);
|
||||
});
|
||||
observer.observe(document.body, { childList: true });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
if (!burning) return null;
|
||||
return createPortal(
|
||||
<div className="hg-burn" aria-hidden="true">
|
||||
<img className="hg-burn-flame" src={burning.src} alt="" />
|
||||
</div>,
|
||||
burning.target,
|
||||
);
|
||||
};
|
||||
|
||||
export default HellGirlFlames;
|
||||
Reference in new issue
Block a user