Refine line-based story scrolling

This commit is contained in:
2026-05-16 21:40:36 +02:00
parent b9ae7f71c5
commit e368d252ad
10 changed files with 1238 additions and 755 deletions
+67 -7
View File
@@ -4,7 +4,7 @@ This file is the single living technical specification, implementation checklist
## Product Goal
Build an AI-assisted interactive fiction client that feels like a carefully typeset illustrated novel rather than a chat window. The game server owns game state and narrative generation. The client renders incoming narrative as synchronized animated prose with optional speech, sound effects, music, future image blocks, and persistent player options.
Build an AI-assisted interactive fiction client that feels like a carefully typeset illustrated novel rather than a chat window. The game server owns game state and narrative generation. The client renders incoming narrative as synchronized animated prose with optional speech, sound effects, music, image blocks, and persistent player options.
The production client must tolerate TTS being unavailable. The safe default TTS provider is `none`; a game, user preference, or explicit option can select another provider.
@@ -25,8 +25,8 @@ The production client must tolerate TTS being unavailable. The safe default TTS
- Done: mouse cursor state reporting by process state.
- Done: placeholder game API for new/load/save/running state.
- Done: sound effect and music folders, sound effect playback, music playback, and music ducking during TTS.
- Partial: image markup is parsed and queued, but actual image rendering is still future work.
- Partial: save-game API is a placeholder; saves are per socket session and do not survive reload.
- Done: image markup is parsed, persisted in history, restored from save/history, and rendered as line-snapped page blocks.
- Partial: save-game API restores story state and Ink state, but the broader save/storage model still needs hardening for all engines.
- Pending: deeper automated tests for layout, playback timing, TTS provider switching, and media cue timing.
## Module System Specification
@@ -58,8 +58,8 @@ The loader is deliberately the conductor, not the orchestra. Module-specific con
- `markup-parser-module.js`: converts story text into text blocks, inline styled spans, image blocks, sound cues, and music cues.
- `text-processor-module.js`: applies SmartyPants and Hyphenopoly according to active language.
- `paragraph-layout-module.js`: measures text and computes Knuth-Plass layout.
- `layout-renderer-module.js`: turns layout data into page DOM with stable word positions and animation metadata.
- `sentence-queue-module.js`: prepares sentence objects and coordinates layout/TTS readiness.
- `layout-renderer-module.js`: turns line-coordinate layout data into absolutely positioned page DOM with stable word positions and animation metadata.
- `sentence-queue-module.js`: prepares speech/media readiness. It must not own page layout, image wrapping, or history rendering state.
- `playback-coordinator-module.js`: starts synchronized text/audio playback in the right order.
- `animation-queue-module.js`: schedules and fast-forwards visual text animation.
- `audio-manager-module.js`: owns sound effects, music tracks, music ducking, volume application, and speech audio playback helpers.
@@ -73,7 +73,7 @@ The loader is deliberately the conductor, not the orchestra. Module-specific con
- `localization-module.js`: language state used by UI, hyphenation, and TTS selection.
- `options-ui-module.js`: options modal, persisted controls, provider status displays.
- `ui-controller-module.js`: top-bar commands, global input behavior, game API control wiring.
- `ui-display-handler-module.js`: book page display, startup prompt, text insertion, and media block dispatch.
- `ui-display-handler-module.js`: book page display, startup prompt, unified live/history rendering, line-coordinate scrolling, image placement, and media block dispatch.
- `ui-input-handler-module.js`: command entry, history, fast-forward key handling.
- `socket-client-module.js`: socket connection and game API request wrapper.
- `game-loop-module.js`: high-level client/game flow.
@@ -95,6 +95,66 @@ The right page must look like typeset book text:
- Punctuation and short marks should not visually break the measure; optical margin handling is desirable future polish.
- The page must scale as a fixed-aspect book page. Font sizes and word positions scale with page size, preserving the composition when the window is resized.
## Right Page History And Scrolling Specification
The right page uses one virtual, line-addressed content pane. It must not behave like browser pagination and must not rely on native scrolling inside `#page_right`, `#story`, or story blocks.
Line model invariants:
- `#page_right` has a size relative to the browser window.
- There is exactly one story line-height value.
- The page height is divided into a fixed number of lines; currently `PAGE_LINE_COUNT = 25`.
- `lineHeight = pageRightHeight / PAGE_LINE_COUNT`.
- All rendered content has a height that is an exact multiple of line height, including margins, internal spacing, drop cap space, image vertical spacing, and section/chapter spacing.
- All virtual content coordinates and pixel positions are derived mathematically from line coordinates.
- Stored content does not change line numbers after creation.
- Visible content is never inserted between already existing blocks; new live content is appended at the end of the virtual history.
- Therefore cumulative pixel measurements from the browser DOM are not authoritative and cumulative line starts should not need updating after a block has been assigned coordinates.
- In portrait-image cases, text and image blocks may occupy overlapping cumulative line ranges, but every block edge still lands on a line boundary.
Scroll positioning:
- Scrolling means translating the content pane vertically with an ease-in/ease-out animation.
- Every finished scroll position must snap to the nearest position where page edges align with line edges.
- Scrolling to the top means the top edge of the first line of the first block aligns with the top edge of the page.
- Scrolling to the bottom means the bottom edge of the last line of the last rendered block aligns with the bottom edge of the page.
- Scrolling to the bottom to insert new content uses the same bottom rule, but the new block is first added invisibly to block history, advancing the block counter and line history. The page scrolls to the resulting bottom position, then the block reveal animation starts.
- If playback continues while the user is viewing older history, the view must first return to the live bottom insertion position before revealing new content.
- If manual scrolling moves currently animating content out of focus, active text animation and TTS playback must be fast-forwarded through the same path used by page click/space, including TTS fade/stop.
Active line and active block model:
- The 41-block retention target is not pagination.
- There is one active line representing the current view position.
- If enough content exists above it, the active line is considered to be the last visible line of the page, line 25.
- The block containing that active line is the active block.
- The DOM should normally contain 20 blocks before the active block, the active block itself, and 20 blocks after the active block, when those blocks exist.
- When normal scrolling shifts the active line into a different block, load one block in the direction of travel and unload one block from the opposite side.
- This one-block exchange should happen as soon as the active block changes, not after the viewport reaches a DOM edge.
- Mouse wheel, arrow key, and scrollbar interactions must drive this active-line model rather than loading page-sized chunks.
Random-position and scrollbar jumps:
- Scrolling to a random target first identifies the target line and target block.
- If the target is reached by traversal, the one-block exchange model applies.
- If the target is jumped to, the page first loads:
- 20 blocks before the current/starting active block, as available,
- the current/starting active block,
- all blocks between the starting block and the target block,
- the target block,
- 20 blocks after the target block, as available.
- The whole loaded range can then be traversed smoothly from the starting position to the target position.
- The final target aligns so the bottom edge of the requested line aligns with the bottom edge of the page when enough content exists above it; otherwise it uses the top rule.
- After the scroll finishes, blocks farther than the retained margin are unloaded.
- If the required loaded range would exceed a sensible DOM budget, currently 150 blocks total, all visible page content fades out, the old DOM content is unloaded, the target block plus 20 blocks before and after it are loaded, and the page fades in at the target position.
Scrollbar behavior:
- The custom scrollbar represents virtual history position and history size in line coordinates, not native DOM scroll state.
- Dragging the scrollbar thumb should move the thumb preview freely without scrolling content or loading history during the drag.
- On pointer release, the target line/block is resolved, the required block range is loaded according to the random-position rules, and then the content scroll animation runs.
- Scrollbar pointer events must not bubble into story fast-forward/continue handlers.
Processing order:
1. Parse block and inline story markup.
@@ -342,7 +402,7 @@ Longer-term goal:
- [x] Added chapter heading and dropcap markup.
- [x] Added section/textblock markup.
- [x] Added Markdown emphasis parsing.
- [x] Added image markup parsing for future image rendering.
- [x] Added image markup parsing, line-snapped rendering, and history/save restoration.
- [x] Added sound effect markup and playback.
- [x] Added music markup, playback modes, loop/once, and lead-in.
- [x] Added music ducking during TTS.