# Hotel Dusk — Known Issues

## ISSUE: per-line animations show onion-skin / ghost trails in the browser
**Status:** OPEN (parked June 14 2026). Everything else for Hotel Dusk works
(bilingual DB, speaker attribution + JP/EN realignment, expr→anim mapping,
extracted animations, gallery, per-line wiring).

### Symptom
In the line-visual panel, character animations render with a lighter, larger
ghost silhouette behind the figure (the white sticker outline of multiple
frames accumulated). Persists after the format/decoder fixes below and after
(reported) app relaunches.

### What's been confirmed
- The **extracted frames are correct.** Each frame, decoded individually, is a
  clean white-outline sticker cutout (no trails, no holes) matching the
  in-game look. Verified by rendering single frames AND by coalescing the
  deployed APNGs honoring disposal (constant alpha area per frame, no
  accumulation).
- The ghost is **onion-skinning at playback time**: a renderer that BLENDS
  each transparent frame over the previous (no canvas clear between frames)
  accumulates the figures. Reproduced exactly by alpha-compositing the frames.

### Fixes already applied (kept)
- Decoder: frames are partial deltas over an opaque WHITE paper base; decode
  the game way (frame 0 keyframe fills white; later frames keep b==0), then
  derive alpha as a sticker cutout: `ink = lum<245` → despeckle →
  `binary_dilation(ink, 6)` (white outline ring) → `binary_fill_holes`.
  (`extract_animations.py::cut_bg`)
- Format: switched WebP → **APNG**. Pillow CANNOT store a frame-disposal flag
  in animated WebP (reads back `disposal=None`), so Chromium blended frames.
  APNG saves with `disposal=1` (OP_BACKGROUND) + `blend=0` (OP_SOURCE),
  verified stored as `disposal=[1,1,...]`, `duration=100`.

### Remaining hypothesis
APNG file is spec-correct but the user's QtWebEngine/Chromium build apparently
still doesn't clear between frames (or the running process wasn't reloading the
startup-loaded `anim_resolve.json`). Inconclusive.

### Recommended next step (renderer-independent)
Replace the animated-image approach with a **JS frame-flipbook**: export each
frame as an independent still PNG; in `AnimImg` (terminal/main.jsx) hold the
frame URL list and swap `<img src>` on a `setInterval` (play intro frames
once, then loop the settled idle frames ping-ponged). A static PNG fully
replaces on each swap, so onion-skin is impossible in ANY renderer. Needs:
extractor to emit per-frame PNGs + frame lists in anim_resolve.json; backend
`get_line_visual` to return the frame URL list; AnimImg flipbook + preload.

### Note
`anim_resolve.json` is loaded at process startup, so backend changes here
require an app relaunch to take effect.
