Hustler-Pixelwelt T13: Korrekturen Hochhaus (Fokus, Tastatur, weniger Bewegung, Handy-Tippflächen, Schnee per transform, Pause überall, Seite ohne Leerlauf-Blitz)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0924a8d2ae
commit
5e6698f5e0
14 files changed
+994
-52
No files matched your search
@@ -1,21 +1,43 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import HochhausKopf from './HochhausKopf'
|
||||
import HochhausWelt from './HochhausWelt'
|
||||
import HustlerDialog from './HustlerDialog'
|
||||
import RegieEbene from './RegieEbene'
|
||||
|
||||
// Von welchem Bildschirm man zurückkommt -> Etage, über die man gegangen war (Freigabe-Pult über die Etage „freigabe“, Keller über „keller“)
|
||||
const ETAGE_NACH_TAB = Object.freeze({ freigabe: 'freigabe', botleads: 'keller' })
|
||||
|
||||
// Überschrift der Seite, nur für Screenreader (Pult und Keller haben ihre eigene sichtbare Überschrift)
|
||||
function Ueberschrift() {
|
||||
return <h1 className="hk-nur-sr">Hustler – Hochhaus</h1>
|
||||
}
|
||||
|
||||
// Bildschirm „Hochhaus“: Spielwelt mit Regie-Ebene (T5, T8), Kopfzeile (T6) und Detailansicht eines Hustlers (T7). Zustände wie bisher im WorkersBoard.
|
||||
// Reine Darstellung ohne Auth-Bezug: isAdmin, der Feed (feed = { state, error, loaded, refresh }), die Hustler-Tabelle (workers), die Regie-Anbindung
|
||||
// (regieRef, die Seite ruft regieRef.current.push(events) bei neuen Ereignissen) und der Bildschirmwechsel (onOpen(tab)) kommen von der Seite.
|
||||
// (regieRef, die Seite ruft regieRef.current.push(events) bei neuen Ereignissen), der Bildschirmwechsel (onOpen(tab)) und vonTab (der Bildschirm, von dem man
|
||||
// zurückkommt: 'freigabe' | 'botleads' | null) kommen von der Seite. Beim Zurückkommen bekommt die Etage, über die man gegangen war, den Fokus (M2).
|
||||
// Die Kopfzeile steht im DOM vor der Welt, damit die Tab-Reihenfolge oben beginnt (M7); über der Welt liegt sie per z-index.
|
||||
// Kein CSS-Import und keine Bilder: die Stile hängt WorkersPage ein (Regeln dieser Datei in styles/hustler/hustler-ui.css).
|
||||
export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen }) {
|
||||
export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen, vonTab = null }) {
|
||||
const wurzelRef = useRef(null)
|
||||
const weltRef = useRef(null)
|
||||
const [auswahl, setAuswahl] = useState(null) // workerId der offenen Detailansicht (null = keine)
|
||||
const ready = feed.loaded && Boolean(feed.state)
|
||||
|
||||
// Rückweg aus Pult oder Keller: Fokus auf die Etage, über die man ging (erst wenn die Welt steht; die Kamera holt sie bei Tastaturfokus ins Bild)
|
||||
useEffect(() => {
|
||||
if (!ready || !vonTab || !Object.hasOwn(ETAGE_NACH_TAB, vonTab)) return
|
||||
const wurzel = wurzelRef.current
|
||||
if (!wurzel || typeof wurzel.querySelector !== 'function') return
|
||||
const etage = wurzel.querySelector(`[data-hk-etage="${ETAGE_NACH_TAB[vonTab]}"]`)
|
||||
if (etage && typeof etage.focus === 'function') etage.focus({ preventScroll: true })
|
||||
}, [ready, vonTab])
|
||||
|
||||
// Dienst noch nie erreicht: Pixel-Leerzustand mit Fehlertext und „Neu laden“
|
||||
if (!ready && feed.error) {
|
||||
return (
|
||||
<div className="hk-hochhaus">
|
||||
<div className="hk-hochhaus" ref={wurzelRef}>
|
||||
<Ueberschrift />
|
||||
<div className="hk-leer">
|
||||
<strong className="hk-leer-titel">Der Hustler-Dienst ist noch nicht erreichbar</strong>
|
||||
<p className="hk-leer-text">{feed.error}</p>
|
||||
@@ -26,7 +48,8 @@ export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen
|
||||
}
|
||||
if (!ready) {
|
||||
return (
|
||||
<div className="hk-hochhaus">
|
||||
<div className="hk-hochhaus" ref={wurzelRef}>
|
||||
<Ueberschrift />
|
||||
<div className="hk-laden" role="status">Wird geladen …</div>
|
||||
</div>
|
||||
)
|
||||
@@ -35,7 +58,16 @@ export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen
|
||||
// Die Regie-Ebene liegt in der skalierten Bühne (Weltkoordinaten); ihr Handle ist regieRef der Seite, die neue Ereignisse hineinreicht.
|
||||
// Solange die Detailansicht offen ist, ruhen Rad, Ziehen und Tasten der Kamera (gesperrt).
|
||||
return (
|
||||
<div className="hk-hochhaus">
|
||||
<div className="hk-hochhaus" ref={wurzelRef}>
|
||||
<Ueberschrift />
|
||||
<HochhausKopf
|
||||
api={api}
|
||||
isAdmin={isAdmin}
|
||||
state={feed.state}
|
||||
error={feed.error}
|
||||
refresh={feed.refresh}
|
||||
onGanz={() => { if (weltRef.current) weltRef.current.ganz() }}
|
||||
/>
|
||||
<HochhausWelt
|
||||
ref={weltRef}
|
||||
state={feed.state}
|
||||
@@ -46,19 +78,12 @@ export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen
|
||||
>
|
||||
<RegieEbene ref={regieRef} />
|
||||
</HochhausWelt>
|
||||
<HochhausKopf
|
||||
api={api}
|
||||
isAdmin={isAdmin}
|
||||
state={feed.state}
|
||||
error={feed.error}
|
||||
refresh={feed.refresh}
|
||||
onGanz={() => { if (weltRef.current) weltRef.current.ganz() }}
|
||||
/>
|
||||
{auswahl !== null && (
|
||||
<HustlerDialog
|
||||
api={api}
|
||||
workerId={auswahl}
|
||||
workers={workers}
|
||||
paused={feed.state?.paused === true}
|
||||
onClose={() => setAuswahl(null)}
|
||||
onWechsel={setAuswahl}
|
||||
/>
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
// Raumschilder, Bildschirme, Radar, Briefkasten-Zähler, Tresor-/Keller-Text, den Etagen (Knöpfe, Freigabe und Keller sind Links) und den 19 Hustlern; davor der Schnee.
|
||||
// Alles hängt am Dienst-Zustand: Figur und Zusätze nach Status (figurZustand), graue Raum-Ebene samt Papierschild nur für nicht angeschlossene Such-Räume.
|
||||
// Die Kamera (Rad, Ziehen, Pinch, Tasten) steckt in useKamera. `children` (die Regie-Ebene) liegt in der Bühne über den Figuren, in Weltkoordinaten.
|
||||
// Bedienung: Tastaturfokus (:focus-visible) auf Etage oder Figur außerhalb des Bildes holt sie per Kamera herein (beiFokus, kein Schwenk bei Mausklick); das Sichtfenster ist
|
||||
// per tabindex -1 fokussierbar (ein Klick in die Welt gibt ihm den Fokus, damit + - 0 Esc wirken); die Tippflächen der Figuren wachsen mit dem Zoom (--hk-tipp-*, tippRand);
|
||||
// bei offener Detailansicht (gesperrt) trägt das Sichtfenster hk-verdeckt (Schnee aus).
|
||||
// Kein CSS-Import (die Stile hängt WorkersPage ein: styles/hustler/hochhaus.css, hustler-ui.css, hustler-welt.css), keine Bilder, kein innerHTML.
|
||||
import { forwardRef, useImperativeHandle } from 'react'
|
||||
import { anzeigeName, figurZustand, schilder } from '../../../lib/hustlerSicht.js'
|
||||
import { anzeigeName, figurZustand, schilder, tippRand } from '../../../lib/hustlerSicht.js'
|
||||
import { AUFZUG, ETAGEN, ETAGEN_SPALTE, HAUS_BILD, HUSTLER, RAEUME, SCHILDER } from '../../../lib/hustlerWelt.js'
|
||||
import { FLOOR_TITLES, ROLES, ROOM_TITLES } from '../../../lib/workersRoster.js'
|
||||
import useKamera from './useKamera.js'
|
||||
@@ -20,6 +23,11 @@ const HAUS_RECHTECK = Object.freeze({ left: HAUS_BILD.x, top: HAUS_BILD.y, width
|
||||
|
||||
const PX = (n) => `${n}px`
|
||||
|
||||
// Elemente, die Fokus per Tastatur bekommen können und beim Fokus ins Bild geholt werden (M1)
|
||||
const FOKUS_ZIEL = '.hk-etage, .hk-hustler'
|
||||
// Namensschild einer Figur: so viele Bildschirm-Pixel über dem Knopf sollen beim Heranholen mit sichtbar sein
|
||||
const SCHILD_HOEHE = 34
|
||||
|
||||
// Feste, je Figur verschiedene Startverschiebung der Animation (deterministisch aus dem Index: 7 und 19 sind teilerfremd, 19 Figuren -> 19 Werte)
|
||||
const versatz = (index, schritt = 7, anzahl = 19) => `-${(((index * schritt) % anzahl) / 10).toFixed(1)}s`
|
||||
|
||||
@@ -49,6 +57,7 @@ const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige,
|
||||
const grau = new Set(figuren.filter((x) => x.f.raumGrau).map((x) => x.f.raum))
|
||||
|
||||
const { s, x, y } = kam
|
||||
const tipp = tippRand(s)
|
||||
const etikett = Math.min(18, Math.max(8, 15 / s)).toFixed(2)
|
||||
const namePx = Math.min(14, Math.max(4, 12 / s)).toFixed(2)
|
||||
const buehneStil = {
|
||||
@@ -56,6 +65,10 @@ const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige,
|
||||
imageRendering: s * geraeteDichte() < 1 ? 'auto' : 'pixelated',
|
||||
'--hk-etikett': `${etikett}px`,
|
||||
'--hk-name': `${namePx}px`,
|
||||
// Tippfläche der Figuren: wächst mit dem Zoom auf 40 Bildschirm-Pixel (M8, hustler-welt.css)
|
||||
'--hk-tipp-x': `${tipp.x.toFixed(2)}px`,
|
||||
'--hk-tipp-oben': `${tipp.oben.toFixed(2)}px`,
|
||||
'--hk-tipp-unten': `${tipp.unten.toFixed(2)}px`,
|
||||
}
|
||||
|
||||
const zeige = (id) => () => {
|
||||
@@ -70,10 +83,29 @@ const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige,
|
||||
if (typeof onOpen === 'function') onOpen(tab)
|
||||
}
|
||||
|
||||
// Fokus per Tastatur (:focus-visible) auf Etage oder Figur außerhalb des sichtbaren Bereichs: Kamera holt sie ins Bild (M1). Kein Schwenk bei Mausklick.
|
||||
const beiFokus = (e) => {
|
||||
if (gesperrt) return
|
||||
const knoten = e && e.target
|
||||
if (!knoten || typeof knoten.closest !== 'function') return
|
||||
const ziel = knoten.closest(FOKUS_ZIEL)
|
||||
if (!ziel) return
|
||||
let tastatur = false
|
||||
try { tastatur = ziel.matches(':focus-visible') } catch { tastatur = false }
|
||||
if (!tastatur) return
|
||||
// Der Browser darf das Sichtfenster nicht selbst verschieben (alte Browser ohne overflow: clip scrollen beim Fokus)
|
||||
const sicht = e.currentTarget
|
||||
if (sicht && (sicht.scrollLeft || sicht.scrollTop)) { sicht.scrollLeft = 0; sicht.scrollTop = 0 }
|
||||
const figur = typeof ziel.matches === 'function' && ziel.matches('.hk-hustler')
|
||||
kam.zeige({ left: ziel.offsetLeft, top: ziel.offsetTop, width: ziel.offsetWidth, height: ziel.offsetHeight }, figur ? SCHILD_HOEHE : 0)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={kam.view}
|
||||
className={`hk-sicht${kam.ziehen ? ' hk-zieht' : ''}${pausiert ? ' hk-steht' : ''}`}
|
||||
className={`hk-sicht${kam.ziehen ? ' hk-zieht' : ''}${pausiert ? ' hk-steht' : ''}${gesperrt ? ' hk-verdeckt' : ''}`}
|
||||
tabIndex={-1}
|
||||
onFocus={beiFokus}
|
||||
onPointerDown={kam.handlers.onPointerDown}
|
||||
onPointerMove={kam.handlers.onPointerMove}
|
||||
onPointerUp={kam.handlers.onPointerUp}
|
||||
@@ -100,6 +132,7 @@ const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige,
|
||||
<div className="hk-haus hk-schild-b" aria-hidden="true" style={HAUS_RECHTECK} />
|
||||
<div
|
||||
className="hk-neon"
|
||||
aria-hidden="true"
|
||||
style={{ left: SCHILDER.neon.left, top: SCHILDER.neon.top, width: SCHILDER.neon.w, height: SCHILDER.neon.h, fontSize: PX(SCHILDER.neon.schrift), lineHeight: PX(SCHILDER.neon.schrift) }}
|
||||
>
|
||||
HUSTLER
|
||||
@@ -186,13 +219,13 @@ const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige,
|
||||
const link = LINK_ETAGEN[e.id]
|
||||
if (link) {
|
||||
return (
|
||||
<a key={e.id} className="hk-etage" href={`/hustler?tab=${link.tab}`} draggable={false} onClick={linkKlick(link.tab)} aria-label={link.label} style={stil}>
|
||||
<a key={e.id} className="hk-etage" data-hk-etage={e.id} href={`/hustler?tab=${link.tab}`} draggable={false} onClick={linkKlick(link.tab)} aria-label={link.label} style={stil}>
|
||||
<span className="hk-etage-name">{link.text}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<button key={e.id} type="button" className="hk-etage" onClick={() => kam.etage(e.oben, e.unten)} aria-label={`${FLOOR_TITLES[e.id]} heranzoomen`} style={stil}>
|
||||
<button key={e.id} type="button" className="hk-etage" data-hk-etage={e.id} onClick={() => kam.etage(e.oben, e.unten)} aria-label={`${FLOOR_TITLES[e.id]} heranzoomen`} style={stil}>
|
||||
<span className="hk-etage-name">{FLOOR_TITLES[e.id]}</span>
|
||||
</button>
|
||||
)
|
||||
@@ -207,6 +240,7 @@ const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige,
|
||||
type="button"
|
||||
className="hk-hustler"
|
||||
data-hk-hustler={id}
|
||||
aria-haspopup="dialog"
|
||||
onClick={zeige(id)}
|
||||
aria-label={f.label}
|
||||
style={{ left: k.left, top: k.top, width: k.w, height: k.h }}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
// Esc schließt, ←/→ blättern in Roster-Reihenfolge, ein Klick auf den Hintergrund schließt erst 400 ms nach dem Öffnen (Geister-Klick nach
|
||||
// dem Antippen der Figur). Beim Schließen geht der Fokus zurück auf das Element, das beim Öffnen den Fokus hatte (wenn es noch im Dokument
|
||||
// steht), sonst auf die Figur des zuletzt gezeigten Hustlers ([data-hk-hustler="<id>"], die setzt die Welt).
|
||||
import { useEffect, useRef } from 'react'
|
||||
// Blättern wird angesagt (aria-live polite: Name, Status, Nummer), nicht aber das Öffnen. Angehalten (paused): die Animationen im Dialog stehen (hk-steht).
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useWorkerTrace } from '../../../hooks/useWorkersFeed'
|
||||
import { blaettern, detailModell } from '../../../lib/hustlerSicht'
|
||||
@@ -39,9 +40,12 @@ function figurStelle(szene) {
|
||||
return DETAIL.figur.buero
|
||||
}
|
||||
|
||||
// Ansage nach dem Blättern: „Lea, Arbeitet, 9 von 19“
|
||||
const ansageVon = (m) => `${m.name}, ${m.statusText}, ${m.nummer.replace(' / ', ' von ')}`
|
||||
|
||||
// Props: api (Dienst-API für die Spur), workerId (null = nichts anzeigen), workers (Hustler-Tabelle der Seite, Zustandseinträge nach workerId),
|
||||
// onClose(), onWechsel(nextId) (Blättern; die Seite setzt die neue Id)
|
||||
export default function HustlerDialog({ api, workerId, workers, onClose, onWechsel }) {
|
||||
// paused (Dienst angehalten: Animationen stehen), onClose(), onWechsel(nextId) (Blättern; die Seite setzt die neue Id)
|
||||
export default function HustlerDialog({ api, workerId, workers, paused = false, onClose, onWechsel }) {
|
||||
const { trace, loading, error } = useWorkerTrace(api, workerId)
|
||||
const ersterRef = useRef(null) // Knopf „Schließen“ (Fokus beim Öffnen, Anfang der Fokusfalle)
|
||||
const letzterRef = useRef(null) // Knopf „Nächster Hustler“ (Ende der Fokusfalle)
|
||||
@@ -50,6 +54,7 @@ export default function HustlerDialog({ api, workerId, workers, onClose, onWechs
|
||||
const geoeffnetRef = useRef(0)
|
||||
const openerRef = useRef(null)
|
||||
const idRef = useRef(workerId)
|
||||
const [ansage, setAnsage] = useState('') // Text der Live-Region: leer beim Öffnen, nach dem Blättern der neue Hustler
|
||||
const offen = Boolean(workerId)
|
||||
if (workerId) idRef.current = workerId
|
||||
|
||||
@@ -59,6 +64,7 @@ export default function HustlerDialog({ api, workerId, workers, onClose, onWechs
|
||||
const aktiv = typeof document !== 'undefined' ? document.activeElement : null
|
||||
openerRef.current = aktiv && aktiv !== document.body ? aktiv : null
|
||||
geoeffnetRef.current = Date.now()
|
||||
setAnsage('')
|
||||
const erster = ersterRef.current
|
||||
if (erster && typeof erster.focus === 'function') erster.focus({ preventScroll: true })
|
||||
return () => {
|
||||
@@ -78,7 +84,13 @@ export default function HustlerDialog({ api, workerId, workers, onClose, onWechs
|
||||
const m = detailModell(workerId, eintrag, trace, Date.now())
|
||||
const stelle = figurStelle(m.szene)
|
||||
const o = m.overlays
|
||||
const wechsel = (schritt) => onWechsel(blaettern(workerId, schritt))
|
||||
const wechsel = (schritt) => {
|
||||
const naechster = blaettern(workerId, schritt)
|
||||
// Ansage des Ziels (Name, Status, Nummer hängen nicht an der Spur) als Momentaufnahme: spätere Statuswechsel melden sich nicht von selbst
|
||||
const ziel = isRecord(workers) && Object.hasOwn(workers, naechster) ? workers[naechster] : null
|
||||
setAnsage(ansageVon(detailModell(naechster, ziel, null, Date.now())))
|
||||
onWechsel(naechster)
|
||||
}
|
||||
|
||||
// Hintergrund: schließt erst nach der Sperrzeit
|
||||
const hintergrund = () => {
|
||||
@@ -115,7 +127,7 @@ export default function HustlerDialog({ api, workerId, workers, onClose, onWechs
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="hk-dialog-ueber" onKeyDown={taste}>
|
||||
<div className={paused === true ? 'hk-dialog-ueber hk-steht' : 'hk-dialog-ueber'} onKeyDown={taste}>
|
||||
<button type="button" tabIndex={-1} className="hk-dialog-hinten" aria-label="Detailansicht schließen" ref={hintenRef} onClick={hintergrund} />
|
||||
<section role="dialog" aria-modal="true" aria-label={`Hustler ${m.name}`} tabIndex={-1} className="hk-panel hk-dialog" ref={sectionRef}>
|
||||
<div className={m.szene} aria-hidden="true">
|
||||
@@ -132,6 +144,7 @@ export default function HustlerDialog({ api, workerId, workers, onClose, onWechs
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="hk-nur-sr" aria-live="polite" aria-atomic="true">{ansage}</div>
|
||||
<div className="hk-dialog-daten">
|
||||
<div className="hk-dialog-kopf">
|
||||
<div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Kamera der Hochhaus-Welt (Hustler-Pixelwelt T5, Spec E1/E8/E14): Port von logik.js der Zeichenfläche als Hook.
|
||||
// Mausrad zoomt um den Mauspunkt, Ziehen verschiebt, zwei Finger zoomen (Pinch), Doppelklick/Doppeltipp zeigt das ganze Haus,
|
||||
// Tasten +/=/-/0/Esc, Etagen heranzoomen. Gleiten 0,55 s (aus bei „weniger Bewegung“). Nach einem Zug kein versehentlicher Klick (nachZug()).
|
||||
// Tasten +/=/-/0/Esc, Etagen heranzoomen, ein Element ins Bild holen (zeige, für den Tastaturfokus). Gleiten 0,55 s (aus bei „weniger Bewegung“: dann springt die Kamera).
|
||||
// Nach einem Zug kein versehentlicher Klick (nachZug()). Ein Zeigerdruck in die Welt gibt dem Sichtfenster den Fokus.
|
||||
// Gerechnet wird nur mit den reinen Kamera-Funktionen aus lib/hustlerSicht.js; hier steht die Zeiger- und Zustandsverwaltung.
|
||||
// Die Kamera selbst (baueKamera) ist reines JS ohne React und bekommt nur die Meldung „Zustand hat sich geändert“.
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { SICHT_STANDARD, kameraEtage, kameraGrenzen, kameraPassend, kameraZoomAuf } from '../../../lib/hustlerSicht.js'
|
||||
import { SICHT_STANDARD, kameraEtage, kameraGrenzen, kameraPassend, kameraZeige, kameraZoomAuf } from '../../../lib/hustlerSicht.js'
|
||||
import { WELT } from '../../../lib/hustlerWelt.js'
|
||||
|
||||
// Gleiten: die CSS-Klasse hk-gleitet dauert 0,55 s, der Merker bleibt etwas länger (Ende der Bewegung abwarten)
|
||||
export const GLEITEN_MS = 600
|
||||
@@ -14,6 +16,10 @@ const ZUG_SCHWELLE_MAUS = 5
|
||||
const ZUG_SCHWELLE_TOUCH = 10
|
||||
// Beschreibt Elemente, auf denen ein Doppelklick/-tipp nicht „ganzes Haus“ bedeutet
|
||||
const BEDIENBAR = '.hk-etage, .hk-hustler'
|
||||
// Ein Zeigerdruck auf diesen Elementen fokussiert sich selbst; überall sonst bekommt das Sichtfenster den Fokus (damit + - 0 Esc wirken)
|
||||
const SELBST_FOKUS = 'button, a[href], input, select, textarea, [tabindex]'
|
||||
// Obergrenze für die von der Kopfzeile verdeckte Höhe (Anteil der Fensterhöhe), falls viele Meldungen darunter stehen
|
||||
const KOPF_MAX_ANTEIL = 0.5
|
||||
|
||||
const jetzt = () => Date.now()
|
||||
|
||||
@@ -92,8 +98,17 @@ export function baueKamera(melde) {
|
||||
if (m.st.ziehen) setze({ ziehen: false })
|
||||
}
|
||||
|
||||
// Ein Druck in die Welt gibt dem Sichtfenster den Fokus (tabindex -1), sonst erreichen + - 0 Esc die Kamera nach einem Klick nicht mehr
|
||||
const fokusAufSicht = (e) => {
|
||||
const ziel = e && e.target
|
||||
if (ziel && typeof ziel.closest === 'function' && ziel.closest(SELBST_FOKUS)) return
|
||||
if (m.el && typeof m.el.focus === 'function') m.el.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
const unten = (e) => {
|
||||
if (m.gesperrt || m.st.gleiten || !m.st.bereit) return
|
||||
if (m.gesperrt) return
|
||||
fokusAufSicht(e)
|
||||
if (m.st.gleiten || !m.st.bereit) return
|
||||
if (e.pointerType === 'mouse' && (e.button !== 0 || e.ctrlKey)) return
|
||||
if (e.pointerType === 'mouse') m.zeiger.clear() // es gibt nur eine Maus: ein verwaister Eintrag darf keinen Pinch auslösen
|
||||
m.zeiger.set(e.pointerId, { x: e.clientX, y: e.clientY })
|
||||
@@ -186,6 +201,34 @@ export function baueKamera(melde) {
|
||||
gleite(kameraEtage(oben, untenRand, groesse()))
|
||||
}
|
||||
|
||||
// Höhe, die die Kopfzeile oben im Sichtfenster verdeckt: Unterkante von .hk-kopf (Geschwister des Sichtfensters), sonst die feste Höhe der Welt-Daten
|
||||
const kopfHoehe = () => {
|
||||
const g = groesse()
|
||||
let h = WELT.hud
|
||||
try {
|
||||
const wurzel = m.el && m.el.parentElement
|
||||
const kopf = wurzel && typeof wurzel.querySelector === 'function' ? wurzel.querySelector('.hk-kopf') : null
|
||||
if (kopf) {
|
||||
const unterkante = kopf.getBoundingClientRect().bottom - m.el.getBoundingClientRect().top
|
||||
if (Number.isFinite(unterkante)) h = unterkante
|
||||
}
|
||||
} catch { /* Messung nicht möglich: feste Höhe */ }
|
||||
return Math.min(g.h * KOPF_MAX_ANTEIL, Math.max(0, h))
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt ein Rechteck der Welt (left, top, width, height) ins Bild, wenn es nicht ganz sichtbar ist (Tastaturfokus, M1): Kamera gleitet, bei „weniger Bewegung“
|
||||
* springt sie. `obenExtra` = Bildschirm-Pixel über dem Rechteck, die mit sichtbar sein sollen (Namensschild). Gesperrt: nichts.
|
||||
*/
|
||||
const zeige = (rechteck, obenExtra = 0) => {
|
||||
if (m.gesperrt || !m.st.bereit || !rechteck) return
|
||||
const extra = Number.isFinite(obenExtra) && obenExtra > 0 && m.st.s > 0 ? obenExtra / m.st.s : 0
|
||||
const ziel = kameraZeige(m.st, { ...rechteck, top: rechteck.top - extra, height: rechteck.height + extra }, groesse(), { oben: kopfHoehe() })
|
||||
if (!ziel) return
|
||||
m.selbst = true
|
||||
gleite(ziel)
|
||||
}
|
||||
|
||||
// Mausrad (passive: false, sonst scrollt die Seite mit) und Größenänderung am Sichtfenster
|
||||
const verbinden = () => {
|
||||
if (!m.el || m.verbunden) return
|
||||
@@ -229,6 +272,7 @@ export function baueKamera(melde) {
|
||||
loesen,
|
||||
ganz,
|
||||
etage,
|
||||
zeige,
|
||||
nachZug,
|
||||
handlers: {
|
||||
onPointerDown: unten,
|
||||
@@ -243,7 +287,7 @@ export function baueKamera(melde) {
|
||||
|
||||
/**
|
||||
* Hook für die Welt. `gesperrt` = Detailansicht offen: Rad, Ziehen und Tasten wirken dann nicht.
|
||||
* Liefert { view (Ref-Callback für das Sichtfenster), s, x, y, bereit, gleiten, ziehen, handlers, ganz(), etage(oben, unten), nachZug() }.
|
||||
* Liefert { view (Ref-Callback für das Sichtfenster), s, x, y, bereit, gleiten, ziehen, handlers, ganz(), etage(oben, unten), zeige(rechteck, obenExtra), nachZug() }.
|
||||
* s/x/y: Welt-Pixel mal s plus Verschiebung x/y (transform: translate(x, y) scale(s)).
|
||||
*/
|
||||
export default function useKamera({ gesperrt = false } = {}) {
|
||||
@@ -269,6 +313,7 @@ export default function useKamera({ gesperrt = false } = {}) {
|
||||
handlers: kam.handlers,
|
||||
ganz: kam.ganz,
|
||||
etage: kam.etage,
|
||||
zeige: kam.zeige,
|
||||
nachZug: kam.nachZug,
|
||||
}
|
||||
}
|
||||
+61
-2
@@ -258,6 +258,12 @@ const ETAGE_ZOOM_MAX = 3
|
||||
const ETAGE_HOEHE = 3.4 // Etagenhöhe mal dieser Faktor passt in die Höhe unter der Kopfzeile
|
||||
const ETAGE_HOEHE_RAND = 40
|
||||
const OBEN_FREI = 0.35 // so viel der Höhe darf oben über der Welt frei bleiben (Himmel)
|
||||
const ETAGE_MIN_FAKTOR = 1.6 // „Etage heranzoomen“ vergrößert mindestens so viel mehr als „ganzes Haus“
|
||||
const ZEIGE_RAND = 24 // Luft um ein per Tastatur fokussiertes Element, das ins Bild geholt wird
|
||||
const TIPP_PX = 40 // so groß soll die Tippfläche einer Figur am Bildschirm mindestens sein
|
||||
const TIPP_FIGUR = Object.freeze({ w: 24, h: 32 }) // kleinster Figuren-Knopf in Weltpixeln
|
||||
const TIPP_BASIS = Object.freeze({ x: 12, oben: 8, unten: 6 }) // Überstand der Tippfläche bei starkem Zoom (wie bisher in hochhaus.css)
|
||||
const TIPP_MAX = Object.freeze({ x: 36, y: 12 }) // Überstand, ab dem sich Nachbarn überdecken würden
|
||||
|
||||
function sicht(g) {
|
||||
return isRecord(g) && isNum(g.w) && g.w > 0 && isNum(g.h) && g.h > 0 ? g : SICHT_STANDARD
|
||||
@@ -303,11 +309,64 @@ export function kameraZoomAuf(alt, mx, my, neuS, g) {
|
||||
return kameraGrenzen({ s: n.s, x: px - (px - alt.x) * f, y: py - (py - alt.y) * f }, v)
|
||||
}
|
||||
|
||||
/** Eine Etage (Klickbereich oben..unten in Weltpixeln) mittig heranzoomen, höchstens 3-fach. */
|
||||
/**
|
||||
* Eine Etage (Klickbereich oben..unten in Weltpixeln) mittig heranzoomen: höchstens 3-fach, aber nie weniger als das 1,6-fache von „ganzes Haus“
|
||||
* (am Handy hochkant begrenzt die Breite sonst auf „cover“, und „heranzoomen“ ändert gar nichts) und nie über den größten Zoom.
|
||||
*/
|
||||
export function kameraEtage(oben, unten, g) {
|
||||
const v = sicht(g)
|
||||
const hoehe = isNum(oben) && isNum(unten) && unten > oben ? unten - oben : 1
|
||||
const mitte = isNum(oben) && isNum(unten) ? (oben + unten) / 2 : 0
|
||||
const s = Math.max(kameraMinS(v), Math.min(ETAGE_ZOOM_MAX, (v.w - ETAGE_SEITE) / ETAGE_BREITE, (v.h - WELT.hud - ETAGE_HOEHE_RAND) / (hoehe * ETAGE_HOEHE)))
|
||||
const mindestens = Math.min(ZOOM_MAX, kameraPassend(v).s * ETAGE_MIN_FAKTOR)
|
||||
const s = Math.max(kameraMinS(v), mindestens, Math.min(ETAGE_ZOOM_MAX, (v.w - ETAGE_SEITE) / ETAGE_BREITE, (v.h - WELT.hud - ETAGE_HOEHE_RAND) / (hoehe * ETAGE_HOEHE)))
|
||||
return kameraGrenzen({ s, x: v.w / 2 - (ETAGEN_SPALTE.x + ETAGEN_SPALTE.w / 2) * s, y: WELT.hud / 2 + v.h / 2 - mitte * s }, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kamera so verschieben, dass ein Rechteck der Welt (left, top, width, height in Weltpixeln, z. B. eine Etage oder Figur mit Fokus) im sichtbaren
|
||||
* Bereich liegt: das Fenster ohne die Kopfzeile oben (`oben` px, Standard WELT.hud) und mit `rand` px Luft ringsum. Der Zoom bleibt. Liegt das Rechteck
|
||||
* schon ganz im sichtbaren Bereich (oder lässt die Welt keine Verschiebung zu), kommt null; ist es größer als der Bereich, richtet sich seine linke
|
||||
* bzw. obere Kante am Rand aus. Ergebnis { s, x, y } innerhalb der Grenzen.
|
||||
*/
|
||||
export function kameraZeige(kamera, rechteck, g, { oben = WELT.hud, rand = ZEIGE_RAND } = {}) {
|
||||
const v = sicht(g)
|
||||
if (!isRecord(kamera) || !isNum(kamera.s) || kamera.s <= 0 || !isNum(kamera.x) || !isNum(kamera.y)) return null
|
||||
const r = isRecord(rechteck) ? rechteck : {}
|
||||
if (!isNum(r.left) || !isNum(r.top) || !isNum(r.width) || !isNum(r.height)) return null
|
||||
const kopf = isNum(oben) && oben >= 0 ? oben : WELT.hud
|
||||
const luft = isNum(rand) && rand >= 0 ? rand : ZEIGE_RAND
|
||||
const links = kamera.x + r.left * kamera.s
|
||||
const rechts = links + r.width * kamera.s
|
||||
const obenR = kamera.y + r.top * kamera.s
|
||||
const untenR = obenR + r.height * kamera.s
|
||||
const achse = (a, b, min, max) => {
|
||||
if (b - a > max - min) return min - a // größer als der Bereich: Anfang zeigen
|
||||
if (a < min) return min - a
|
||||
if (b > max) return max - b
|
||||
return 0
|
||||
}
|
||||
const dx = achse(links, rechts, luft, v.w - luft)
|
||||
const dy = achse(obenR, untenR, kopf + luft, v.h - luft)
|
||||
if (dx === 0 && dy === 0) return null
|
||||
const neu = kameraGrenzen({ s: kamera.s, x: kamera.x + dx, y: kamera.y + dy }, v)
|
||||
return Math.abs(neu.x - kamera.x) < 0.5 && Math.abs(neu.y - kamera.y) < 0.5 ? null : neu
|
||||
}
|
||||
|
||||
/**
|
||||
* Tippfläche der Figuren (M8): der Knopf ist 24 x 32 Weltpixel, am Handy also nur ~17 x 23 px groß. Die Tippfläche (::after des Knopfs) wächst über den
|
||||
* Knopf hinaus, so dass sie am Bildschirm mindestens 40 px misst: Ergebnis { x, oben, unten } = Überstand in Weltpixeln (CSS: --hk-tipp-x, -oben,
|
||||
* -unten an der Bühne). Nach unten gedeckelt, damit sich Nachbarn nicht überdecken: seitlich 36 (Figuren stehen mindestens 100 px auseinander, 24 + 2 x 36 = 96),
|
||||
* senkrecht 12 (Figuren benachbarter Etagen stehen 56 px untereinander, 32 + 2 x 12 = 56). Mindestens die bisherigen 12/8/6.
|
||||
*/
|
||||
export function tippRand(zoom) {
|
||||
const s = isNum(zoom) && zoom > 0 ? zoom : 1
|
||||
const welt = TIPP_PX / s // 40 Bildschirm-Pixel in Weltpixeln
|
||||
const klemme = (wert, min, max) => Math.min(max, Math.max(min, wert))
|
||||
const seitlich = (welt - TIPP_FIGUR.w) / 2
|
||||
const senkrecht = (welt - TIPP_FIGUR.h) / 2
|
||||
return {
|
||||
x: klemme(seitlich, TIPP_BASIS.x, TIPP_MAX.x),
|
||||
oben: klemme(senkrecht, TIPP_BASIS.oben, TIPP_MAX.y),
|
||||
unten: klemme(senkrecht, TIPP_BASIS.unten, TIPP_MAX.y),
|
||||
}
|
||||
}
|
||||
@@ -36,12 +36,16 @@ const api = workersApi
|
||||
// Auth und Dienst-API kommen von hier. Der Feed (Zustand alle 15 s, Ereignisse alle 5 s) läuft genau einmal pro Seite, egal welcher
|
||||
// Bildschirm offen ist; die Hustler-Tabelle (indexWorkers beim Schnappschuss, reduceEvents bei Ereignissen) gehört ebenfalls hierher.
|
||||
// Ereignisse gehen zusätzlich an die Regie, die nur das Hochhaus über regieRef einhängt.
|
||||
// Die Tabelle liegt nie einen Render hinter dem Schnappschuss zurück (sonst stünden alle Figuren kurz auf „Leerlauf“): sie merkt sich, aus welchem Schnappschuss sie
|
||||
// entstand, und wird im Render neu aus `state` gebaut, sobald der Schnappschuss ein anderer ist. Pause (state.paused) geht an alle drei Bildschirme.
|
||||
// Das Hochhaus bekommt vonTab (der Bildschirm, von dem man zurückkommt) und fokussiert dann die Etage, über die man gegangen war.
|
||||
export default function WorkersPage() {
|
||||
const { isAdmin, user } = useAuth()
|
||||
const [params, setParams] = useSearchParams()
|
||||
const active = pickTab(WORKERS_TABS, params.get('tab'))
|
||||
const bot = params.get('bot') ?? ''
|
||||
const [workers, setWorkers] = useState({})
|
||||
const [tabelle, setTabelle] = useState({ von: null, workers: {} }) // Hustler-Tabelle samt dem Schnappschuss, aus dem sie entstand
|
||||
const [verlauf, setVerlauf] = useState({ aktuell: active.id, vorher: null }) // aktueller und vorheriger Bildschirm
|
||||
const regieRef = useRef(null)
|
||||
|
||||
// Tab-Titel nur solange die Seite offen ist; beim Verlassen kommt der vorige Titel zurück
|
||||
@@ -53,14 +57,20 @@ export default function WorkersPage() {
|
||||
|
||||
// Neue Ereignisse: Tabelle fortschreiben und der Regie vorspielen (die Regie kennt den Rückstau selbst)
|
||||
const onEvents = useCallback((events) => {
|
||||
setWorkers((w) => reduceEvents(w, events))
|
||||
setTabelle((t) => ({ von: t.von, workers: reduceEvents(t.workers, events) }))
|
||||
if (regieRef.current) regieRef.current.push(events)
|
||||
}, [])
|
||||
|
||||
const { state, error, loaded, refresh } = useWorkersFeed(api, { onEvents })
|
||||
useEffect(() => {
|
||||
if (state) setWorkers(indexWorkers(state))
|
||||
}, [state])
|
||||
// Neuer Schnappschuss: Tabelle sofort neu bauen (nicht erst nach einem Effekt) und merken
|
||||
const veraltet = Boolean(state) && tabelle.von !== state
|
||||
const workers = veraltet ? indexWorkers(state) : tabelle.workers
|
||||
if (veraltet) setTabelle({ von: state, workers })
|
||||
// Bildschirmwechsel: der bisherige wird zum vorherigen
|
||||
const gewechselt = verlauf.aktuell !== active.id
|
||||
const vonTab = gewechselt ? verlauf.aktuell : verlauf.vorher
|
||||
if (gewechselt) setVerlauf({ aktuell: active.id, vorher: vonTab })
|
||||
const paused = state?.paused === true
|
||||
|
||||
return (
|
||||
<div className="hk-seite" data-tab={active.id}>
|
||||
@@ -71,6 +81,7 @@ export default function WorkersPage() {
|
||||
feed={{ state, error, loaded, refresh }}
|
||||
workers={workers}
|
||||
regieRef={regieRef}
|
||||
vonTab={vonTab}
|
||||
onOpen={(tab) => setParams({ tab })}
|
||||
/>
|
||||
)}
|
||||
@@ -80,6 +91,7 @@ export default function WorkersPage() {
|
||||
me={{ id: user?.$id, name: user?.name }}
|
||||
kpis={state?.kpis}
|
||||
workers={workers}
|
||||
paused={paused}
|
||||
onBack={() => setParams({ tab: 'hochhaus' })}
|
||||
/>
|
||||
)}
|
||||
@@ -90,6 +102,7 @@ export default function WorkersPage() {
|
||||
onClearBot={() => setParams({ tab: 'botleads' })}
|
||||
kpis={state?.kpis}
|
||||
workers={workers}
|
||||
paused={paused}
|
||||
onBack={() => setParams({ tab: 'hochhaus' })}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
/* ---------- Überlagerung: dunkler Hintergrund-Knopf und das Panel in der Mitte ---------- */
|
||||
.hk-hochhaus .hk-dialog-ueber {
|
||||
position: absolute; inset: 0; z-index: 20;
|
||||
position: absolute; inset: 0; z-index: 9; /* unter Seitenleiste (20) und Abdunklung (15) der Handy-Navigation */
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-sizing: border-box; padding: 16px;
|
||||
font-family: 'Pixelify Sans', ui-monospace, monospace; font-size: 15px; line-height: 20px; color: #e6ebff;
|
||||
@@ -64,8 +64,8 @@
|
||||
.hk-hochhaus .hk-dialog-fuss { margin-top: auto; display: flex; align-items: center; justify-content: space-between; gap: 12px; position: sticky; bottom: 0; padding: 6px 0 2px; background: #0e1430; }
|
||||
.hk-hochhaus .hk-dialog-nummer { font-family: 'Press Start 2P', 'Pixelify Sans', monospace; font-size: 11px; line-height: 14px; color: #9aa6d6; }
|
||||
|
||||
/* ---------- Handy (unter 720 px, Spec E13): Vollbild, Szene oben (auf die Breite verkleinert), Daten darunter scrollbar ---------- */
|
||||
@media (max-width: 719px) {
|
||||
/* ---------- Handy (unter 720 px breit oder höchstens 500 px hoch, also auch quer, Spec E13): Vollbild, Szene oben (auf die Breite verkleinert), Daten darunter scrollbar ---------- */
|
||||
@media (max-width: 719px), (max-height: 500px) {
|
||||
.hk-hochhaus .hk-dialog-ueber { padding: 0; }
|
||||
.hk-hochhaus .hk-dialog {
|
||||
/* Container für die Szenen-Skalierung (cqw = Innenbreite des Panels) */
|
||||
@@ -87,8 +87,8 @@
|
||||
.hk-hochhaus .hk-dialog-fuss { position: sticky; bottom: 0; margin-top: 4px; padding: 6px 0 2px; background: #0e1430; }
|
||||
}
|
||||
|
||||
/* Handy quer (niedrig): Szene links (45 % der Breite), Daten rechts daneben und scrollbar */
|
||||
@media (max-width: 719px) and (max-height: 520px) {
|
||||
/* Handy quer (niedrig, breiter als hoch): Szene links (45 % der Breite), Daten rechts daneben und scrollbar */
|
||||
@media (max-height: 500px) and (orientation: landscape) {
|
||||
.hk-hochhaus .hk-dialog { flex-direction: row; align-items: flex-start; }
|
||||
.hk-hochhaus .hk-dialog .hk-sz { zoom: 0.4; }
|
||||
@supports (zoom: tan(atan2(1px, 1px))) {
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
/* WOMS-Bausteine (Einstellungen-Dialog, ProspectPanel) stehen in der Pixelwelt, sollen aber wie im übrigen WOMS aussehen */
|
||||
.hk-seite .hk-woms { font-family: var(--font); font-size: 14px; line-height: 20px; color: var(--text); letter-spacing: normal; image-rendering: auto; }
|
||||
.hk-seite .hk-woms a { color: var(--accent, #3b5bdb); }
|
||||
/* .hk-hochhaus a:hover (hochhaus.css, gelb) darf im WOMS-Dialog nicht gewinnen: Hover bleibt in der Akzentfarbe des WOMS */
|
||||
.hk-seite .hk-woms a:hover { color: var(--accent); }
|
||||
|
||||
/* Nur für Screenreader (Seitenüberschrift des Hochhauses, Ansage beim Blättern in der Detailansicht): unsichtbar, aber im Lesefluss */
|
||||
.hk-seite .hk-nur-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
||||
|
||||
/* Overseer außerhalb von „arbeitet“: Kristall bleibt sichtbar, nur matt */
|
||||
.hk-seite .hk-gedimmt { filter: saturate(0.35) brightness(0.72); }
|
||||
@@ -35,16 +40,19 @@
|
||||
/* Fehler-Marker: rotes Pixel-Schild mit „!“ über dem Kopf (klein im Haus, groß in der Detailansicht) */
|
||||
.hk-seite .hk-alarm {
|
||||
position: absolute; left: 15px; top: -11px; width: 7px; height: 9px; box-sizing: border-box;
|
||||
background: #e2483d; box-shadow: 0 0 0 1px #1a1630; color: #ffffff;
|
||||
background: #c8372d; box-shadow: 0 0 0 1px #1a1630; color: #ffffff; /* #c8372d: weiß darauf 5,2:1 (mit #e2483d nur 4,0:1) */
|
||||
font-family: 'Press Start 2P', 'Pixelify Sans', monospace; font-size: 6px; line-height: 9px; text-align: center;
|
||||
pointer-events: none; animation: hk-ui-blinken 1s steps(1) infinite;
|
||||
}
|
||||
.hk-seite .hk-alarm-gross { left: auto; right: 8px; top: 8px; width: 28px; height: 36px; font-size: 22px; line-height: 36px; box-shadow: 0 0 0 3px #1a1630; }
|
||||
@keyframes hk-ui-blinken { 50% { opacity: 0.35; } }
|
||||
|
||||
/* Angehalten (state.paused): alles in der Welt steht still */
|
||||
.hk-seite .hk-steht, .hk-seite .hk-steht * { animation-play-state: paused !important; }
|
||||
/* Angehalten (state.paused): alles in der Welt steht still (Bühne, Schnee, Detailansicht); auch Pseudo-Elemente (der Schnee bewegt ::before) */
|
||||
.hk-seite .hk-steht, .hk-seite .hk-steht *, .hk-seite .hk-steht *::before, .hk-seite .hk-steht *::after { animation-play-state: paused !important; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hk-seite .hk-alarm { animation: none; }
|
||||
/* Alle Sprite-Schleifen im Hochhaus und in der Detailansicht stehen auf dem ersten Bild; Pose und Zusätze (Punkte, Zzz, Bildschirm, Dampf …) bleiben sichtbar.
|
||||
Höhere Spezifität als die Regeln mit animation: in hochhaus.css (.hk-hochhaus .hk-fig-…), deshalb mit .hk-seite davor. */
|
||||
.hk-seite .hk-hochhaus .hk-spr { animation: none; }
|
||||
}
|
||||
@@ -13,6 +13,8 @@
|
||||
font-size: 15px; line-height: 20px;
|
||||
}
|
||||
.hk-hochhaus .hk-sicht.hk-zieht { cursor: grabbing; }
|
||||
/* Das Sichtfenster ist per tabindex="-1" nur mit der Maus/Programm fokussierbar (ein Klick in die Welt gibt ihm den Fokus, damit + - 0 Esc wirken): kein Ring */
|
||||
.hk-hochhaus .hk-sicht:focus { outline: none; }
|
||||
.hk-hochhaus .hk-sicht > .hk-himmel,
|
||||
.hk-hochhaus .hk-sicht > .hk-funkeln { position: absolute; inset: 0; }
|
||||
.hk-hochhaus .hk-sicht > .hk-mond { position: absolute; right: 13%; top: 15%; width: 80px; height: 80px; }
|
||||
@@ -22,6 +24,8 @@
|
||||
.hk-hochhaus .hk-buehne.hk-gleitet { transition: transform 0.55s cubic-bezier(0.2, 0.7, 0.2, 1); }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hk-hochhaus .hk-buehne.hk-gleitet { transition: none; }
|
||||
/* Schnee: hochhaus.css blendet ihn aus; falls je eingeblendet, steht er */
|
||||
.hk-hochhaus .hk-sicht > .hk-schnee::before { animation: none; }
|
||||
}
|
||||
|
||||
/* Schriftgrößen der Etagen-Namen und Namensschilder: wachsen beim Herauszoomen, damit sie lesbar bleiben (Werte setzt die Bühne) */
|
||||
@@ -49,7 +53,7 @@
|
||||
.hk-hochhaus .hk-briefkasten {
|
||||
position: absolute; box-sizing: border-box; padding: 0 2px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: #e2483d; color: #ffffff; font-family: 'Press Start 2P', 'Pixelify Sans', monospace;
|
||||
background: #c8372d; color: #ffffff; font-family: 'Press Start 2P', 'Pixelify Sans', monospace; /* #c8372d: weiß darauf 5,2:1 (mit #e2483d nur 4,0:1) */
|
||||
box-shadow: 0 0 0 1px #3a1216, inset 0 1px 0 0 #ff7d70; pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -57,3 +61,39 @@
|
||||
.hk-hochhaus .hk-hustler > .hk-spr { left: 0; top: 0; }
|
||||
.hk-hochhaus .hk-hustler > .hk-punkte { left: 14px; top: -6px; }
|
||||
.hk-hochhaus .hk-hustler > .hk-zzz { left: 15px; top: -11px; }
|
||||
|
||||
/* ---------- Tippflächen der Figuren (M8): wachsen mit dem Zoom auf 40 Bildschirm-Pixel ----------
|
||||
Der Knopf ist 24 x 32 Weltpixel; am Handy (ganzes Haus ~0,7-fach) wäre er nur ~17 x 23 px groß. Die Überstände (--hk-tipp-x/-oben/-unten) setzt die Bühne
|
||||
aus tippRand(zoom) (lib/hustlerSicht.js), gedeckelt, damit sich Nachbarn nicht überdecken. Ersetzt left/right/top/bottom aus hochhaus.css (-12/-12/-8/-6 px). */
|
||||
.hk-hochhaus .hk-buehne .hk-hustler::after {
|
||||
left: calc(-1 * var(--hk-tipp-x, 12px)); right: calc(-1 * var(--hk-tipp-x, 12px));
|
||||
top: calc(-1 * var(--hk-tipp-oben, 8px)); bottom: calc(-1 * var(--hk-tipp-unten, 6px));
|
||||
}
|
||||
|
||||
/* ---------- Schnee (R1): Bewegung per transform statt background-position ----------
|
||||
hochhaus.css (erzeugt) animiert background-position der drei Schnee-Ebenen: das malt 60 mal pro Sekunde drei Fensterflächen neu. Hier läuft stattdessen ein
|
||||
::before, das um genau eine Kachel-Wiederholung größer ist als das Fenster und per transform (Compositor, kein Neu-Malen) um dieselbe Strecke in derselben
|
||||
Richtung in derselben Zeit wandert wie die Vorlage (hk-h-schnee1: +512/+2048 in 72 s, -2: -512/+2048 in 48 s, -3: +768/+6144 in 64 s). Die Kachelgrößen
|
||||
(512/512/768) teilen die Strecke, die Bildfolge ist also identisch. Der Container .hk-schnee schneidet ab (kein Überlauf). */
|
||||
.hk-hochhaus .hk-sicht > .hk-schnee { background: none; animation: none; overflow: hidden; overflow: clip; }
|
||||
.hk-hochhaus .hk-sicht > .hk-schnee::before { content: ''; position: absolute; background-repeat: repeat; image-rendering: pixelated; }
|
||||
.hk-hochhaus .hk-sicht > .hk-schnee-1::before {
|
||||
left: -512px; top: -2048px; width: calc(100% + 512px); height: calc(100% + 2048px);
|
||||
background-image: url(../../assets/hustler/welt/schnee-1.png); background-size: 512px 512px;
|
||||
animation: hk-welt-schnee1 72s linear infinite;
|
||||
}
|
||||
.hk-hochhaus .hk-sicht > .hk-schnee-2::before {
|
||||
left: 0; top: -2048px; width: calc(100% + 512px); height: calc(100% + 2048px);
|
||||
background-image: url(../../assets/hustler/welt/schnee-2.png); background-size: 512px 512px;
|
||||
animation: hk-welt-schnee2 48s linear infinite;
|
||||
}
|
||||
.hk-hochhaus .hk-sicht > .hk-schnee-3::before {
|
||||
left: -768px; top: -6144px; width: calc(100% + 768px); height: calc(100% + 6144px);
|
||||
background-image: url(../../assets/hustler/welt/schnee-3.png); background-size: 768px 768px;
|
||||
animation: hk-welt-schnee3 64s linear infinite;
|
||||
}
|
||||
@keyframes hk-welt-schnee1 { to { transform: translate(512px, 2048px); } }
|
||||
@keyframes hk-welt-schnee2 { to { transform: translate(-512px, 2048px); } }
|
||||
@keyframes hk-welt-schnee3 { to { transform: translate(768px, 6144px); } }
|
||||
/* Detailansicht offen: Schnee weg (die Welt liegt unter dem Abdunkler, die Ebenen würden umsonst laufen) */
|
||||
.hk-hochhaus .hk-sicht.hk-verdeckt > .hk-schnee { display: none; }
|
||||
Reference in new issue
Block a user