Pause stoppt die Animationen nicht: Kreide-Wackeln, Schal und Partikel-Zeit laufen weiter #7

Closed
opened 2026-08-31 21:01:55 +02:00 by gilli · 1 comment
Owner

⁨⁩⁨Pause stoppt die Animationen nicht: Kreide-Wackeln, Schal und Partikel-Zeit laufen weiter⁩ ⁨⁩#⁨7⁩⁨⁩"
By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩
accessibility
bug
medium
pause
spec-gap

Komponente: GAME (update)
Betroffene Version: main @ 238ed66, index.html Zeilen 2589–2604

Beschreibung

AGENTS.md verlangt für die Pause:

Während der Pause müssen Animation, Physik, Partikel und Audio angehalten werden.

Physik, Partikel und Simulationsschritte stehen korrekt still, die Animation nicht:
S.visualTime und S.tick werden vor der State-Prüfung hochgezählt und damit auch im
pauseierten Zustand weitergeführt. render() läuft ohnehin weiter und verwendet genau diese
Werte.

Gemessen (PAUSED, 500 ms Realzeit zugewartet):

{
  "t0": { "state": "PAUSED", "vt": 6.802, "tick": 68, "run": 2.820 },
  "t1": {                                "vt": 7.306, "tick": 73, "run": 2.820 },
  "visualTimeAdvancedWhilePaused": 0.505,
  "runTimeAdvancedWhilePaused": 0.000
}

Ursache

// index.html:2589-2604
function update(dt) {
  S.visualTime += dt;                 // <-- vor der State-Prüfung
  S.tick = Math.floor(S.visualTime * 10);
  INPUT.tickTimers(dt);
  ...
  if (S.state === "PAUSED" || S.state === "GAME_OVER") return;   // zu spät

Betroffene Zeichenorte, die visualTime/tick verwenden:

  • Linien-Unruhe / „Gekritzel“ (chalk(..., tick)) – Wackeln aller Linien, inkl. Boden und Figur
  • drawScarf (Schalwellen hinter der Figur)
  • Fragment-Rotation (drawFragment, spin = t * 1.9)
  • Parallax-Bewegung und Fokus-Effekt

Bei reducedMotion (S.settings.reduced) ist das Wackeln reduziert, Schal/Fragmente laufen
aber ebenfalls weiter.

Auswirkung

  • Sichtbar lebendes Standbild im Pause-Overlay – wirkt wie ein Flackern hinter dem Dialog.
  • Vorgabe aus AGENTS.md nicht erfüllt.
  • Bei prefers-reduced-motion: reduce unnötige Dauerbewegung hinter dem Pause-Dialog.

Lösungsvorschlag

Animationszeit nur in aktiven Zuständen fortschreiben und den Frühausstieg nach oben ziehen:

function update(dt) {
  if (S.state === "MENU" || S.state === "SETTINGS" || S.state === "HELP") { ...demo...; return; }
  if (S.state === "PAUSED" || S.state === "GAME_OVER") { renderStatisch(); return; }
  S.visualTime += dt;
  S.tick = Math.floor(S.visualTime * 10);
  ...
}

Alternativ visualTime an eine eigene, vom Simulations-State unabhängige Uhr binden und im
Pause-Zweig einfrieren. Dazu ein Test: nach pauseGame() darf sich S.visualTime über
mehrere Frames nicht ändern, und zwei aufeinanderfolgende Screenshots im Pausenmenü müssen
pixelidentisch sein.


Dokumentiert in issues/07-pause-stoppt-animation-nicht.md (Commit c171694).

1 comments

⁨⁩⁨Pause stoppt die Animationen nicht: Kreide-Wackeln, Schal und Partikel-Zeit laufen weiter⁩ ⁨⁩#⁨7⁩⁨⁩" By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩ accessibility bug medium pause spec-gap > **Komponente:** GAME (`update`) > **Betroffene Version:** `main` @ `238ed66`, `index.html` Zeilen 2589–2604 > > ## Beschreibung > > AGENTS.md verlangt für die Pause: > > > Während der Pause müssen Animation, Physik, Partikel und Audio angehalten werden. > > Physik, Partikel und Simulationsschritte stehen korrekt still, die **Animation nicht**: > `S.visualTime` und `S.tick` werden vor der State-Prüfung hochgezählt und damit auch im > pauseierten Zustand weitergeführt. `render()` läuft ohnehin weiter und verwendet genau diese > Werte. > > Gemessen (`PAUSED`, 500 ms Realzeit zugewartet): > > ```json > { > "t0": { "state": "PAUSED", "vt": 6.802, "tick": 68, "run": 2.820 }, > "t1": { "vt": 7.306, "tick": 73, "run": 2.820 }, > "visualTimeAdvancedWhilePaused": 0.505, > "runTimeAdvancedWhilePaused": 0.000 > } > ``` > > ## Ursache > > ```js > // index.html:2589-2604 > function update(dt) { > S.visualTime += dt; // <-- vor der State-Prüfung > S.tick = Math.floor(S.visualTime * 10); > INPUT.tickTimers(dt); > ... > if (S.state === "PAUSED" || S.state === "GAME_OVER") return; // zu spät > ``` > > Betroffene Zeichenorte, die `visualTime`/`tick` verwenden: > > * Linien-Unruhe / „Gekritzel“ (`chalk(..., tick)`) – Wackeln aller Linien, inkl. Boden und Figur > * `drawScarf` (Schalwellen hinter der Figur) > * Fragment-Rotation (`drawFragment`, `spin = t * 1.9`) > * Parallax-Bewegung und Fokus-Effekt > > Bei `reducedMotion` (`S.settings.reduced`) ist das Wackeln reduziert, Schal/Fragmente laufen > aber ebenfalls weiter. > > ## Auswirkung > > * Sichtbar lebendes Standbild im Pause-Overlay – wirkt wie ein Flackern hinter dem Dialog. > * Vorgabe aus AGENTS.md nicht erfüllt. > * Bei `prefers-reduced-motion: reduce` unnötige Dauerbewegung hinter dem Pause-Dialog. > > ## Lösungsvorschlag > > Animationszeit nur in aktiven Zuständen fortschreiben und den Frühausstieg nach oben ziehen: > > ```js > function update(dt) { > if (S.state === "MENU" || S.state === "SETTINGS" || S.state === "HELP") { ...demo...; return; } > if (S.state === "PAUSED" || S.state === "GAME_OVER") { renderStatisch(); return; } > S.visualTime += dt; > S.tick = Math.floor(S.visualTime * 10); > ... > } > ``` > > Alternativ `visualTime` an eine eigene, vom Simulations-State unabhängige Uhr binden und im > Pause-Zweig einfrieren. Dazu ein Test: nach `pauseGame()` darf sich `S.visualTime` über > mehrere Frames nicht ändern, und zwei aufeinanderfolgende Screenshots im Pausenmenü müssen > pixelidentisch sein. > > --- > > *Dokumentiert in `issues/07-pause-stoppt-animation-nicht.md` (Commit `c171694`).* 1 comments
Author
Owner

Fixed in 9a1fd56 (main).

Geändert

  • update(dt): der Frühausstieg für PAUSED/GAME_OVER sitzt jetzt über der
    Animationsuhr. S.visualTime += dt / S.tick und INPUT.tickTimers(dt) laufen nur noch in
    aktiven Zuständen – render() malt in der Pause damit dasselbe Bild (Linien-Wackeln, Schal,
    Fragment-Rotation, Parallax, Fokus-Effekt stehen). beim Fortsetzen läuft die Uhr nahtlos weiter,
    es sammelt sich kein Restakku an.
  • Die Toast-Meldung bleibt bewusst vor dem Frühausstieg: sie ist eine DOM-Auskunft
    (role="status" / aria-live), kein Bildanteil – ihr Timer durfte nicht mit einfrieren, sonst
    hinge sie dauerhaft hinter dem Dialog.
  • AUDIO.freeze(bool) (neu) friert den Audio-Context ein; setState ruft es mit
    next === "PAUSED". Ein langer Klang (Fokus 0,7 s, Sturz 0,85 s, Rekord-Fanfare) verklingt
    nicht ins Pausenmenü, sondern wartet bis „Fortsetzen“. Im eingefrorenen Zustand werden keine
    neuen Klänge geplant (tone/noise), auch nicht über die Gestik-Aufrufe von AUDIO.resume().
    Freigegeben wird in jedem anderen Zustand, damit die Menü-Töne nach „Zum Menü“ nicht stumm sind.
  • setState stellt beim Wechsel in die Pause camShake/flash auf 0: beide werden in update
    heruntergezählt und wirken in render() mit Math.random() bzw. als Tönung – eingefroren wäre
    das Standbild wackelnd oder getönt geblieben.

Physik, Partikel und Simulationsschritte standen bereits vorher still – unverändert.

Messung

Vor dem Fix (PAUSED, 500 ms Realzeit): visualTime +0,525, tick +5, zwei Screenshots
uneins. Nach dem Fix: 0,0 / 0 / pixelidentisch. Aktive Zustände unverändert: Menü-Demo
+0,41 und Lauf +0,41 visualTime pro 400 ms.

Test

tests/browser_issue7.py (neu, in tests/run-all.sh), im echten requestAnimationFrame-Loop –
nichts stillgelegt, sonst könnte die Animationsuhr gar nicht weiterlaufen:

  • Menü-Demo und Lauf schreiben die Uhr weiter (Gegenprüfung gegen „alles eingefroren“)
  • Pause per echtem Escape: visualTime/tick, runTime, Körper und Partikel unverändert
  • zwei Screenshots im Pausenmenü (Abstand 350 ms) sind bytegleich
  • Audio: suspend-Aufruf bei Pause gezählt, im Pausenmenü wird kein einziger Oszillator mehr
    erzeugt, bei Fortsetzen resume (geprüft über Aufrufzähler, nicht über Backend-Zustände –
    Autoplay-Sperre sonst nicht aussagekräftig)
  • Gleiches Bild mit settings.reduced (prefers-reduced-motion) und nach einem Sturz im
    Game-Over-Bildschirm; nach Fortsetzen und nach „Nochmal spielen“ läuft die Uhr wieder
  • Nachlauf gegen git show HEAD:index.html via PULSE_HTML: 6 Prüfungen rot, nach dem Fix grün
    (dreimal wiederholt). ./tests/run-all.sh 1 komplett grün.
Fixed in `9a1fd56` (main). **Geändert** * `update(dt)`: der Frühausstieg für `PAUSED`/`GAME_OVER` sitzt jetzt **über** der Animationsuhr. `S.visualTime += dt` / `S.tick` und `INPUT.tickTimers(dt)` laufen nur noch in aktiven Zuständen – `render()` malt in der Pause damit dasselbe Bild (Linien-Wackeln, Schal, Fragment-Rotation, Parallax, Fokus-Effekt stehen). beim Fortsetzen läuft die Uhr nahtlos weiter, es sammelt sich kein Restakku an. * Die Toast-Meldung bleibt bewusst vor dem Frühausstieg: sie ist eine DOM-Auskunft (`role="status"` / `aria-live`), kein Bildanteil – ihr Timer durfte nicht mit einfrieren, sonst hinge sie dauerhaft hinter dem Dialog. * `AUDIO.freeze(bool)` (neu) friert den Audio-Context ein; `setState` ruft es mit `next === "PAUSED"`. Ein langer Klang (Fokus 0,7 s, Sturz 0,85 s, Rekord-Fanfare) verklingt nicht ins Pausenmenü, sondern wartet bis „Fortsetzen“. Im eingefrorenen Zustand werden keine neuen Klänge geplant (`tone`/`noise`), auch nicht über die Gestik-Aufrufe von `AUDIO.resume()`. Freigegeben wird in jedem anderen Zustand, damit die Menü-Töne nach „Zum Menü“ nicht stumm sind. * `setState` stellt beim Wechsel in die Pause `camShake`/`flash` auf 0: beide werden in `update` heruntergezählt und wirken in `render()` mit `Math.random()` bzw. als Tönung – eingefroren wäre das Standbild wackelnd oder getönt geblieben. Physik, Partikel und Simulationsschritte standen bereits vorher still – unverändert. **Messung** Vor dem Fix (PAUSED, 500 ms Realzeit): `visualTime` +0,525, `tick` +5, zwei Screenshots uneins. Nach dem Fix: 0,0 / 0 / pixelidentisch. Aktive Zustände unverändert: Menü-Demo +0,41 und Lauf +0,41 `visualTime` pro 400 ms. **Test** `tests/browser_issue7.py` (neu, in `tests/run-all.sh`), im echten `requestAnimationFrame`-Loop – nichts stillgelegt, sonst könnte die Animationsuhr gar nicht weiterlaufen: * Menü-Demo und Lauf schreiben die Uhr weiter (Gegenprüfung gegen „alles eingefroren“) * Pause per echtem `Escape`: `visualTime`/`tick`, `runTime`, Körper und Partikel unverändert * zwei Screenshots im Pausenmenü (Abstand 350 ms) sind bytegleich * Audio: `suspend`-Aufruf bei Pause gezählt, im Pausenmenü wird kein einziger Oszillator mehr erzeugt, bei Fortsetzen `resume` (geprüft über Aufrufzähler, nicht über Backend-Zustände – Autoplay-Sperre sonst nicht aussagekräftig) * Gleiches Bild mit `settings.reduced` (prefers-reduced-motion) und nach einem Sturz im Game-Over-Bildschirm; nach Fortsetzen und nach „Nochmal spielen“ läuft die Uhr wieder * Nachlauf gegen `git show HEAD:index.html` via `PULSE_HTML`: 6 Prüfungen rot, nach dem Fix grün (dreimal wiederholt). `./tests/run-all.sh 1` komplett grün.
gilli closed this issue 2026-09-01 10:12:28 +02:00
Sign in to join this conversation.
No description provided.