Hustler-Pixelwelt T8: Regie (Ereignisse als Blasen, Icons, Flüge) in der Hochhaus-Welt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
94b88462c7
commit
4c5074f4f6
4 files changed
+1429
No files matched your search
@@ -0,0 +1,135 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { createRegie } from '../../../lib/hustlerRegie.js'
|
||||
|
||||
// Regie-Ebene der Hochhaus-Welt (Spec E9): zeichnet die Effekte der Regie (src/lib/hustlerRegie.js) als absolut positionierte Elemente in
|
||||
// Weltkoordinaten über der Welt. Gehört als Kind in die skalierte Bühne (Wurzel mit position: absolute; inset: 0), nimmt keine Klicks an und
|
||||
// ist für Bildschirmleser versteckt (der Status steht schon an den Figuren). Keine Props: die Seite spielt über das Handle
|
||||
// { push(events), flush() } (regieRef) neue Ereignisse vor. Kein CSS-Import und keine Bilder: die Regeln stehen in styles/hustler/hustler-regie.css.
|
||||
// Ein Zeitgeber läuft nur, solange Effekte sichtbar sind oder gleich starten (setTimeout bis zur nächsten Änderung, kein Dauer-Intervall);
|
||||
// beim Abbau werden Zeitgeber und Beobachter aufgeräumt. „Weniger Bewegung“ (matchMedia, auch bei Änderung) zeigt nur Blasen,
|
||||
// ein verdeckter Tab leert die Ebene (visibilitychange) und nimmt keine neuen Ereignisse an.
|
||||
|
||||
const WENIGER_BEWEGUNG = '(prefers-reduced-motion: reduce)'
|
||||
|
||||
// Fenster und Dokument nur, wo es sie gibt (Tests unter Node, Vorschau ohne Browser)
|
||||
function bewegungMedium() {
|
||||
try {
|
||||
return typeof window !== 'undefined' && typeof window.matchMedia === 'function' ? window.matchMedia(WENIGER_BEWEGUNG) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
const verdeckt = () => typeof document !== 'undefined' && document.hidden === true
|
||||
|
||||
// Gleiche Effekte (nach Kennung und Reihenfolge)? Dann bleibt der Zustand unverändert und React zeichnet nicht neu.
|
||||
const gleich = (a, b) => a.length === b.length && a.every((e, i) => e.id === b[i].id)
|
||||
|
||||
// Regie, Zeitgeber und Handle: einmal pro Ebene, gehalten in einem Ref (kein Zustand, damit Aufrufe von außen nie veraltete Werte sehen)
|
||||
function baueSteuerung(setEffekte) {
|
||||
const s = { regie: createRegie(), timer: null, lebt: false }
|
||||
const stoppen = () => {
|
||||
if (s.timer !== null) {
|
||||
clearTimeout(s.timer)
|
||||
s.timer = null
|
||||
}
|
||||
}
|
||||
// Liste neu holen, zeigen und den Zeitgeber auf die nächste Änderung stellen (nur wenn etwas läuft oder wartet)
|
||||
const aktualisieren = () => {
|
||||
stoppen()
|
||||
if (!s.lebt) return
|
||||
const liste = s.regie.tick()
|
||||
setEffekte((alt) => (gleich(alt, liste) ? alt : liste))
|
||||
const bis = s.regie.naechsteIn()
|
||||
if (bis !== null) s.timer = setTimeout(aktualisieren, Math.max(1, bis))
|
||||
}
|
||||
s.aktualisieren = aktualisieren
|
||||
s.stoppen = stoppen
|
||||
s.push = (events) => {
|
||||
if (!s.lebt || verdeckt()) return
|
||||
s.regie.push(events)
|
||||
aktualisieren()
|
||||
}
|
||||
s.flush = () => {
|
||||
s.regie.flush()
|
||||
aktualisieren()
|
||||
}
|
||||
s.setReduziert = (wert) => {
|
||||
s.regie.setReduziert(wert)
|
||||
aktualisieren()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Lage in Weltkoordinaten (Mittelpunkt bzw. Spitze der Blase) und Dauer für die CSS
|
||||
const stil = (e) => ({ left: `${e.x}px`, top: `${e.y}px`, '--dauer': `${e.dauer}ms` })
|
||||
|
||||
function effektKnoten(e) {
|
||||
switch (e.art) {
|
||||
case 'blase':
|
||||
return (
|
||||
<div key={e.id} className="hk-regie-blase" style={stil(e)}>
|
||||
<span className="hk-regie-blase-text">{e.text}</span>
|
||||
</div>
|
||||
)
|
||||
case 'icon':
|
||||
return <div key={e.id} className={`hk-regie-icon hk-regie-bild-${e.icon}`} style={stil(e)} />
|
||||
case 'flug':
|
||||
return (
|
||||
<div
|
||||
key={e.id}
|
||||
className={`hk-regie-flug hk-regie-bild-${e.icon}`}
|
||||
style={{ ...stil(e), '--dx': `${e.x2 - e.x}px`, '--dy': `${e.y2 - e.y}px` }}
|
||||
/>
|
||||
)
|
||||
case 'jubel':
|
||||
return <div key={e.id} className={`hk-regie-jubel hk-regie-bild-${e.icon}`} style={stil(e)} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const RegieEbene = forwardRef(function RegieEbene(_props, ref) {
|
||||
const [effekte, setEffekte] = useState([])
|
||||
const steuerung = useRef(null)
|
||||
if (steuerung.current === null) steuerung.current = baueSteuerung(setEffekte)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
push: (events) => steuerung.current.push(events),
|
||||
flush: () => steuerung.current.flush(),
|
||||
}), [])
|
||||
|
||||
useEffect(() => {
|
||||
const s = steuerung.current
|
||||
s.lebt = true
|
||||
const medium = bewegungMedium()
|
||||
const beiBewegung = () => s.setReduziert(Boolean(medium && medium.matches))
|
||||
if (medium) {
|
||||
s.regie.setReduziert(medium.matches)
|
||||
if (typeof medium.addEventListener === 'function') medium.addEventListener('change', beiBewegung)
|
||||
else if (typeof medium.addListener === 'function') medium.addListener(beiBewegung)
|
||||
}
|
||||
const beiSicht = () => {
|
||||
if (verdeckt()) s.flush()
|
||||
}
|
||||
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', beiSicht)
|
||||
s.aktualisieren() // nach einem erneuten Einhängen (StrictMode) steht keine alte Liste mehr da
|
||||
return () => {
|
||||
s.lebt = false
|
||||
s.stoppen()
|
||||
s.regie.flush()
|
||||
if (medium) {
|
||||
if (typeof medium.removeEventListener === 'function') medium.removeEventListener('change', beiBewegung)
|
||||
else if (typeof medium.removeListener === 'function') medium.removeListener(beiBewegung)
|
||||
}
|
||||
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', beiSicht)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="hk-regie" aria-hidden="true">
|
||||
{effekte.map(effektKnoten)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default RegieEbene
|
||||
@@ -0,0 +1,205 @@
|
||||
// Regie der Hustler-Pixelwelt (Spec E9): macht neue Ereignisse in der Hochhaus-Welt kurz sichtbar.
|
||||
// Rein: kein React, kein window/document, kein Zeitgeber. Grundlage sind die Schritte aus eventToSteps (workersEvents.js); die Regie
|
||||
// übersetzt sie in Effekte mit Ort (Weltkoordinaten aus hustlerWelt.js), Start und Dauer. Gezeichnet wird in RegieEbene.jsx.
|
||||
// blase Sprechblase über dem Kopf (say, 2,5 s) icon Symbol neben der Figur oder am Platz (fx, 1,2 s)
|
||||
// flug Umschlag oder Münze von A nach B (give, fx mit to, 0,9 s) jubel Stern-Pop neben der Figur (cheer, 1,2 s)
|
||||
// task und pose erzeugen nichts (Status und Text folgen schon über reduceEvents). Die Regie läuft nach einer Uhr, die der Aufrufer
|
||||
// liefert (jetzt), und hält selbst keinen Zeitgeber: tick() holt, was gerade zu sehen ist, naechsteIn() sagt, wann sich das ändert.
|
||||
import { eventToSteps, SPOTS } from './workersEvents.js'
|
||||
import { HUSTLER } from './hustlerWelt.js'
|
||||
|
||||
// Dauer je Art in Millisekunden (die CSS in hustler-regie.css läuft mit derselben Dauer über --dauer)
|
||||
export const DAUER = Object.freeze({ blase: 2500, icon: 1200, flug: 900, jubel: 1200 })
|
||||
|
||||
// Symbol je fx am Ort (fx mit Ziel, also envelope und coin, sind Flüge)
|
||||
export const ICON_NACH_FX = Object.freeze({ paper: 'akte', stampOk: 'haken', stampNo: 'stopp', drawer: 'akte', alert: 'alarm' })
|
||||
|
||||
// Flugsymbol je fx
|
||||
const FLUG_NACH_FX = Object.freeze({ envelope: 'umschlag', coin: 'muenze' })
|
||||
|
||||
// Plätze ohne Figur in Weltkoordinaten (Mittelpunkt): Briefkasten, Tresor-Tür, Keller, vor dem Eingang links am Haus
|
||||
export const PLAETZE = Object.freeze({
|
||||
pult: Object.freeze({ x: 1240, y: 920 }),
|
||||
tresor: Object.freeze({ x: 1375, y: 975 }),
|
||||
archiv: Object.freeze({ x: 1250, y: 1040 }),
|
||||
aussen: Object.freeze({ x: 900, y: 995 }),
|
||||
})
|
||||
|
||||
const KOPF_ABSTAND = 36 // Spitze der Blase: so weit über dem Fußpunkt f (Kopf der Figur plus etwas Luft)
|
||||
const OTTO_ZUSATZ = 8 // der Overseer ist höher (Kristall)
|
||||
const KOERPER_ABSTAND = 16 // Mitte der Figur, Anfang und Ende der Flüge
|
||||
const SEITE_DX = 22 // Symbole stehen rechts neben der Figur, der Jubel links
|
||||
const SEITE_DY = 26 // Mittelpunkt der Symbole neben der Figur: so weit über f
|
||||
|
||||
const MAX_STANDARD = 4
|
||||
const RUECKSTAU_STANDARD = 12
|
||||
|
||||
const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
||||
const isNum = (n) => typeof n === 'number' && Number.isFinite(n)
|
||||
const hatFigur = (id) => typeof id === 'string' && Object.hasOwn(HUSTLER, id)
|
||||
const istPlatz = (id) => typeof id === 'string' && SPOTS.includes(id) && Object.hasOwn(PLAETZE, id)
|
||||
|
||||
/** Punkt über dem Kopf der Figur (Spitze der Sprechblase); unbekannter Worker -> null. */
|
||||
export function kopfPunkt(id) {
|
||||
if (!hatFigur(id)) return null
|
||||
const h = HUSTLER[id]
|
||||
return { x: h.a, y: h.f - KOPF_ABSTAND - (id === 'otto' ? OTTO_ZUSATZ : 0) }
|
||||
}
|
||||
|
||||
/** Mitte der Figur (Anfang und Ende der Flüge); unbekannter Worker -> null. */
|
||||
export function koerperPunkt(id) {
|
||||
if (!hatFigur(id)) return null
|
||||
const h = HUSTLER[id]
|
||||
return { x: h.a, y: h.f - KOERPER_ABSTAND }
|
||||
}
|
||||
|
||||
// Mittelpunkt neben der Figur: richtung 1 rechts (Symbole), -1 links (Jubel)
|
||||
function seitePunkt(id, richtung) {
|
||||
if (!hatFigur(id)) return null
|
||||
const h = HUSTLER[id]
|
||||
return { x: h.a + richtung * SEITE_DX, y: h.f - SEITE_DY }
|
||||
}
|
||||
|
||||
// Ort eines Schritts: Worker oder Platz -> Punkt (Flüge), sonst null
|
||||
function endpunkt(id) {
|
||||
if (hatFigur(id)) return koerperPunkt(id)
|
||||
if (istPlatz(id)) return PLAETZE[id]
|
||||
return null
|
||||
}
|
||||
|
||||
function flugEntwurf(icon, von, nach, delay) {
|
||||
const a = endpunkt(von)
|
||||
const b = endpunkt(nach)
|
||||
if (!a || !b) return null
|
||||
// Start und Ziel am selben Ort: kein Flug
|
||||
if (von === nach || (a.x === b.x && a.y === b.y)) return null
|
||||
return { art: 'flug', icon, x: a.x, y: a.y, x2: b.x, y2: b.y, dauer: DAUER.flug, delay: isNum(delay) && delay > 0 ? delay * 1000 : 0 }
|
||||
}
|
||||
|
||||
// Schritt (eventToSteps) -> Entwurf eines Effekts oder null (task, pose, Unbekanntes, Leeres)
|
||||
function entwurfVon(step) {
|
||||
if (!isRecord(step)) return null
|
||||
switch (step.op) {
|
||||
case 'say': {
|
||||
const p = kopfPunkt(step.who)
|
||||
if (!p || typeof step.text !== 'string' || step.text === '') return null
|
||||
return { art: 'blase', wer: step.who, text: step.text, x: p.x, y: p.y, dauer: DAUER.blase, delay: 0 }
|
||||
}
|
||||
case 'give':
|
||||
return flugEntwurf('umschlag', step.from, step.to, 0)
|
||||
case 'cheer': {
|
||||
const p = seitePunkt(step.who, -1)
|
||||
return p ? { art: 'jubel', icon: 'stern', wer: step.who, x: p.x, y: p.y, dauer: DAUER.jubel, delay: 0 } : null
|
||||
}
|
||||
case 'fx': {
|
||||
if (Object.hasOwn(FLUG_NACH_FX, step.fx)) {
|
||||
return Object.hasOwn(step, 'to') ? flugEntwurf(FLUG_NACH_FX[step.fx], step.at, step.to, step.delay) : null
|
||||
}
|
||||
if (!Object.hasOwn(ICON_NACH_FX, step.fx)) return null
|
||||
const p = hatFigur(step.at) ? seitePunkt(step.at, 1) : istPlatz(step.at) ? PLAETZE[step.at] : null
|
||||
return p ? { art: 'icon', icon: ICON_NACH_FX[step.fx], x: p.x, y: p.y, dauer: DAUER.icon, delay: 0 } : null
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function ganzzahl(wert, standard, min) {
|
||||
return isNum(wert) && Math.floor(wert) >= min ? Math.floor(wert) : standard
|
||||
}
|
||||
|
||||
/**
|
||||
* createRegie({ jetzt = () => Date.now(), max = 4, rueckstau = 12, reduziert = false })
|
||||
* -> { push(events), tick(), flush(), setReduziert(bool), naechsteIn(), wartende() }
|
||||
* push(events): Liste neuer Ereignisse (Reihenfolge = Alter); kaputte Eingaben, unbekannte Worker und Plätze erzeugen nichts.
|
||||
* tick(): die jetzt sichtbaren Effekte { id, art, x, y, x2?, y2?, text?, icon?, wer?, start, dauer } (neues Feld je Aufruf, die Effekte
|
||||
* selbst bleiben dieselben Objekte, solange sie laufen). Höchstens `max` zugleich; wartende rücken nach, sobald einer abläuft.
|
||||
* Münzen mit delay werden erst zu ihrem Zeitpunkt sichtbar. Je Worker höchstens eine Blase: eine neue ersetzt die alte (auch die laufende,
|
||||
* ohne einen weiteren Platz zu brauchen).
|
||||
* Mehr als `rueckstau` Wartende: die ältesten fallen weg. reduziert: nur Blasen. flush(): leert Warteschlange und laufende Effekte
|
||||
* (der Tab war verdeckt). naechsteIn(): Millisekunden bis sich die Liste ändert (Ablauf oder Start eines Verzögerten), null = nichts los.
|
||||
* wartende(): Zahl der Wartenden (für Tests und Anzeigen).
|
||||
*/
|
||||
export function createRegie({ jetzt = () => Date.now(), max = MAX_STANDARD, rueckstau = RUECKSTAU_STANDARD, reduziert = false } = {}) {
|
||||
const hoechstens = ganzzahl(max, MAX_STANDARD, 1)
|
||||
const stau = ganzzahl(rueckstau, RUECKSTAU_STANDARD, 0)
|
||||
let nurBlasen = Boolean(reduziert)
|
||||
let zaehler = 0
|
||||
let laufend = [] // sichtbare Effekte
|
||||
let wartend = [] // Entwürfe mit bereit = frühester Start (Millisekunden der Uhr)
|
||||
|
||||
const uhr = () => {
|
||||
const t = jetzt()
|
||||
return isNum(t) ? t : 0
|
||||
}
|
||||
|
||||
// Abgelaufene entfernen, bereite Wartende starten (in Reihenfolge); eine Blase ersetzt die laufende Blase desselben Workers
|
||||
function fuellen(t) {
|
||||
laufend = laufend.filter((e) => e.start + e.dauer > t)
|
||||
let i = 0
|
||||
while (i < wartend.length) {
|
||||
const w = wartend[i]
|
||||
if (w.bereit > t) { i += 1; continue }
|
||||
const alt = w.art === 'blase' ? laufend.find((e) => e.art === 'blase' && e.wer === w.wer) : undefined
|
||||
if (!alt && laufend.length >= hoechstens) { i += 1; continue }
|
||||
if (alt) laufend = laufend.filter((e) => e !== alt)
|
||||
wartend.splice(i, 1)
|
||||
zaehler += 1
|
||||
const { bereit, delay, ...rest } = w
|
||||
laufend.push(Object.freeze({ id: `r${zaehler}`, ...rest, start: t }))
|
||||
}
|
||||
}
|
||||
|
||||
function einreihen(entwurf, t) {
|
||||
// eine neue Blase macht eine noch wartende Blase desselben Workers überflüssig
|
||||
if (entwurf.art === 'blase') wartend = wartend.filter((w) => !(w.art === 'blase' && w.wer === entwurf.wer))
|
||||
wartend.push({ ...entwurf, bereit: t + entwurf.delay })
|
||||
fuellen(t)
|
||||
// Rückstau: die ältesten Wartenden fallen weg
|
||||
if (wartend.length > stau) wartend = wartend.slice(wartend.length - stau)
|
||||
}
|
||||
|
||||
return {
|
||||
push(events) {
|
||||
if (!Array.isArray(events)) return
|
||||
const t = uhr()
|
||||
for (const event of events) {
|
||||
let steps
|
||||
try {
|
||||
steps = eventToSteps(event)
|
||||
} catch {
|
||||
continue // die Regie ist Zierde und darf den Ereignisstrom nie stören
|
||||
}
|
||||
for (const step of steps) {
|
||||
const entwurf = entwurfVon(step)
|
||||
if (!entwurf || (nurBlasen && entwurf.art !== 'blase')) continue
|
||||
einreihen(entwurf, t)
|
||||
}
|
||||
}
|
||||
},
|
||||
tick() {
|
||||
fuellen(uhr())
|
||||
return laufend.slice()
|
||||
},
|
||||
flush() {
|
||||
laufend = []
|
||||
wartend = []
|
||||
},
|
||||
setReduziert(wert) {
|
||||
nurBlasen = Boolean(wert)
|
||||
if (!nurBlasen) return
|
||||
laufend = laufend.filter((e) => e.art === 'blase')
|
||||
wartend = wartend.filter((w) => w.art === 'blase')
|
||||
},
|
||||
naechsteIn() {
|
||||
const t = uhr()
|
||||
let naechste = Infinity
|
||||
for (const e of laufend) naechste = Math.min(naechste, e.start + e.dauer)
|
||||
for (const w of wartend) if (w.bereit > t) naechste = Math.min(naechste, w.bereit)
|
||||
return naechste === Infinity ? null : Math.max(0, naechste - t)
|
||||
},
|
||||
wartende() {
|
||||
return wartend.length
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/* Regie der Hochhaus-Welt (T8, Spec E9): Sprechblase, Symbol-Pop, Flug (Umschlag, Münze) und Jubel-Stern, gezeichnet von RegieEbene.jsx.
|
||||
Handgeschrieben, nicht erzeugt. Alle Klassen beginnen mit hk-regie- und stehen unter der Wurzel .hk-hochhaus; alle Keyframes heißen hk-regie-….
|
||||
Die Ebene liegt in der skalierten Bühne (Weltkoordinaten, 1 Einheit = 1 Kunstpixel): left/top sind der Mittelpunkt (Blase: Spitze des Dreiecks),
|
||||
--dauer (und beim Flug --dx/--dy, der Weg zum Ziel) kommen inline aus dem Effekt. Die Bilder sind die vorhandenen PNGs aus assets/hustler
|
||||
(nichts Neues gezeichnet), 16 × 16 in der Welt; bei Sprite-Blättern (Münze, Stern) zeigt die Regel nur das erste Bild.
|
||||
„Weniger Bewegung“: keine Animation (die Regie zeigt dann ohnehin nur Blasen). Wird in WorkersPage.jsx importiert (CSS-Importe nur dort). */
|
||||
|
||||
.hk-hochhaus .hk-regie {
|
||||
position: absolute; inset: 0; z-index: 50;
|
||||
pointer-events: none; overflow: visible;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
.hk-hochhaus .hk-regie * { pointer-events: none; }
|
||||
|
||||
/* ---------- Sprechblase: dunkler Pixel-Rand ohne Ecken, kleines Dreieck nach unten; die Spitze sitzt auf left/top ---------- */
|
||||
.hk-hochhaus .hk-regie-blase {
|
||||
position: absolute;
|
||||
margin-top: -6px; /* Dreieck (2 px Rand + 4 px Spitze) hängt unter dem Kasten: die Spitze landet auf top */
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
.hk-hochhaus .hk-regie-blase-text {
|
||||
display: block; position: relative; box-sizing: border-box;
|
||||
padding: 2px 4px; white-space: nowrap;
|
||||
font-family: 'Pixelify Sans', ui-monospace, monospace; font-size: 8px; line-height: 10px; font-weight: 400; letter-spacing: normal;
|
||||
color: #1a1630; background: #fffbe8;
|
||||
box-shadow: 0 -2px 0 0 #1a1630, 0 2px 0 0 #1a1630, -2px 0 0 0 #1a1630, 2px 0 0 0 #1a1630;
|
||||
transform-origin: 50% 100%;
|
||||
animation: hk-regie-blase var(--dauer, 2500ms) steps(1, end) both;
|
||||
}
|
||||
.hk-hochhaus .hk-regie-blase-text::after {
|
||||
content: ''; position: absolute; left: 50%; top: 100%; margin: 2px 0 0 -3px;
|
||||
width: 0; height: 0;
|
||||
border-left: 3px solid transparent; border-right: 3px solid transparent; border-top: 4px solid #1a1630;
|
||||
}
|
||||
|
||||
/* ---------- Symbole: Haken, Stopp, Akte, Alarm, Umschlag, Münze, Stern (Mittelpunkt auf left/top) ---------- */
|
||||
.hk-hochhaus .hk-regie-icon,
|
||||
.hk-hochhaus .hk-regie-flug,
|
||||
.hk-hochhaus .hk-regie-jubel {
|
||||
position: absolute; width: 16px; height: 16px; margin: -8px 0 0 -8px;
|
||||
background-repeat: no-repeat; background-position: 0 0; background-size: auto 16px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
.hk-hochhaus .hk-regie-bild-umschlag { background-image: url(../../assets/hustler/pult/ui/art-mail.png); }
|
||||
.hk-hochhaus .hk-regie-bild-haken { background-image: url(../../assets/hustler/keller/ui/haken.png); }
|
||||
.hk-hochhaus .hk-regie-bild-stopp { background-image: url(../../assets/hustler/keller/ui/stopp.png); }
|
||||
.hk-hochhaus .hk-regie-bild-akte { background-image: url(../../assets/hustler/keller/ui/akte-icon.png); }
|
||||
.hk-hochhaus .hk-regie-bild-alarm { background-image: url(../../assets/hustler/pult/ui/art-beschwerde.png); }
|
||||
.hk-hochhaus .hk-regie-bild-muenze { background-image: url(../../assets/hustler/ui/muenze-gruen.png); }
|
||||
.hk-hochhaus .hk-regie-bild-stern { background-image: url(../../assets/hustler/ui/stern.png); }
|
||||
|
||||
/* Icon-Pop: kurz aufploppen, stehen, am Ende ausblenden */
|
||||
.hk-hochhaus .hk-regie-icon { animation: hk-regie-pop var(--dauer, 1200ms) steps(1, end) both; }
|
||||
|
||||
/* Flug: Bogen von der Mitte des Absenders zum Ziel (--dx/--dy), fünf Stützpunkte auf einer Parabel, 14 px Bogenhöhe */
|
||||
.hk-hochhaus .hk-regie-flug { animation: hk-regie-flug var(--dauer, 900ms) linear both; }
|
||||
|
||||
/* Jubel: Stern ploppt auf und hüpft */
|
||||
.hk-hochhaus .hk-regie-jubel { animation: hk-regie-jubel var(--dauer, 1200ms) steps(1, end) both; }
|
||||
|
||||
@keyframes hk-regie-blase {
|
||||
0% { transform: scale(0.5); opacity: 1; }
|
||||
4% { transform: scale(1.12); opacity: 1; }
|
||||
8% { transform: scale(1); opacity: 1; }
|
||||
90% { transform: scale(1); opacity: 0.6; }
|
||||
96% { transform: scale(1); opacity: 0.3; }
|
||||
100% { transform: scale(1); opacity: 0; }
|
||||
}
|
||||
@keyframes hk-regie-pop {
|
||||
0% { transform: scale(0.4); opacity: 1; }
|
||||
10% { transform: scale(1.35); opacity: 1; }
|
||||
22% { transform: scale(1); opacity: 1; }
|
||||
85% { transform: scale(1); opacity: 0.5; }
|
||||
100% { transform: scale(1); opacity: 0; }
|
||||
}
|
||||
@keyframes hk-regie-flug {
|
||||
0% { transform: translate(0, 0) scale(0.7); opacity: 1; }
|
||||
25% { transform: translate(calc(var(--dx, 0px) * 0.25), calc(var(--dy, 0px) * 0.25 - 10.5px)) scale(1); opacity: 1; }
|
||||
50% { transform: translate(calc(var(--dx, 0px) * 0.5), calc(var(--dy, 0px) * 0.5 - 14px)) scale(1); opacity: 1; }
|
||||
75% { transform: translate(calc(var(--dx, 0px) * 0.75), calc(var(--dy, 0px) * 0.75 - 10.5px)) scale(1); opacity: 1; }
|
||||
100% { transform: translate(var(--dx, 0px), var(--dy, 0px)) scale(0.8); opacity: 0; }
|
||||
}
|
||||
@keyframes hk-regie-jubel {
|
||||
0% { transform: translateY(0) scale(0.4); opacity: 1; }
|
||||
12% { transform: translateY(-4px) scale(1.4); opacity: 1; }
|
||||
24% { transform: translateY(-8px) scale(1); opacity: 1; }
|
||||
40% { transform: translateY(-3px) scale(1); opacity: 1; }
|
||||
56% { transform: translateY(-8px) scale(1); opacity: 1; }
|
||||
72% { transform: translateY(-3px) scale(1); opacity: 1; }
|
||||
88% { transform: translateY(-8px) scale(1); opacity: 0.6; }
|
||||
100% { transform: translateY(-8px) scale(1); opacity: 0; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hk-hochhaus .hk-regie-blase-text,
|
||||
.hk-hochhaus .hk-regie-icon,
|
||||
.hk-hochhaus .hk-regie-flug,
|
||||
.hk-hochhaus .hk-regie-jubel { animation: none; }
|
||||
}
|
||||
Reference in new issue
Block a user