From b8fe8535aa893a6339c02f226e3b07fb2f683633 Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Fri, 15 May 2026 07:35:27 +0200 Subject: [PATCH] Fix story page scrolling and ellipsis spacing --- data/worlds/example_world.yml | 32 +++++++++++++++++++------- public/css/style.css | 17 ++++++++++---- public/js/knuth-and-plass.js | 6 ++--- public/js/layout-renderer-module.js | 2 +- public/js/ui-display-handler-module.js | 24 +++++++++++++++---- 5 files changed, 60 insertions(+), 21 deletions(-) diff --git a/data/worlds/example_world.yml b/data/worlds/example_world.yml index e2841d7..d01d257 100644 --- a/data/worlds/example_world.yml +++ b/data/worlds/example_world.yml @@ -9,20 +9,36 @@ introduction: | The last thing you remember is the letter: heavy paper, black wax, your name written in a hand you almost recognized. It asked you to come after dusk, alone, and promised that the house would answer what the sender could not. - Now you stand beyond the wrought iron gate, with rain cooling your face and the hill rising before you. - At its crest waits the old Victorian mansion, every dark window turned toward the path as if the building has been expecting you. - -# Room definitions -rooms: + Now you stand beyond the wrought iron gate, with rain cooling your face and the hill rising before you. + At its crest waits the old Victorian mansion, every dark window turned toward the path as if the building has been expecting you. + + The gate gives under your hand with no protest, though its ironwork is wet enough to shine black. + Gravel shifts beneath your boots as you pass between the pillars, and the garden closes behind you with the soft finality of a curtain. + + Halfway up the path, you stop and listen. + The rain has thinned to a whisper, but the house answers with other sounds: timber settling, gutters ticking, and something deep inside the walls that might be machinery or breath. + + For a heartbeat you think the mansion is about to speak ... but only the wind moves through the ivy. + It drags the leaves across the brickwork in slow strokes, as if wiping dust from an old name. + +# Room definitions +rooms: # Starting area front_yard: name: Front Yard description: | You follow the gravel path up the hill. The rain softens to a drizzle, and moonlight peeks through gaps in the clouds. - Ancient oak trees frame the property, their branches swaying in the gentle breeze. - At the top of three worn stone steps, the mansion's front door waits under a sagging porch roof. - When you reach for the handle, it turns before your fingers touch it, and the door opens {{sfx:squeaky-door.ogg}} with a long, complaining squeak. + Ancient oak trees frame the property, their branches swaying in the gentle breeze. + At the top of three worn stone steps, the mansion's front door waits under a sagging porch roof. + The porch boards are swollen with rain, each one bending under your weight before it remembers its shape. + A brass knocker hangs at eye level, polished bright at the edges where countless hands have touched it and left no warmth behind. + The letter in your pocket presses against your ribs. + You remember the last line now: come before the clocks learn your name. + Somewhere above you, behind a blind upper window, a pale shape passes from left to right and is gone. + You tell yourself it was a reflection, then look back at the path and find no light behind you bright enough to make one. + The house waits. + When you reach for the handle, it turns before your fingers touch it, and the door opens {{sfx:squeaky-door.ogg}} with a long, complaining squeak. exits: - direction: north targetRoomId: entrance_hall diff --git a/public/css/style.css b/public/css/style.css index 04bb08a..47f46c0 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -401,19 +401,26 @@ ol.choice { padding: 0 3rem 1rem 1rem; /* border: 1px dotted rgba(200,200,200,1); */ overflow: visible; - overflow-y: scroll; + overflow-y: auto; opacity: 0.95; mix-blend-mode: darken; } #story { - overflow-x: visible; + overflow-x: visible; + box-sizing: border-box; + overflow-anchor: none; text-align: justify; text-justify: inter-word; - margin-bottom: 1.2em; + margin-bottom: 0; line-height: 1.5; } +#paragraphs { + box-sizing: border-box; + overflow-anchor: none; +} + /* #story p span { font-feature-settings: 'kern' on, 'liga' on, 'onum' on, 'clig' on, 'hlig' on; } */ @@ -425,8 +432,10 @@ ol.choice { #page_right { /* background-color: rgba(200,200,200,0.5); */ right: 7%; - height: calc(28 * 1.45 * 1.2rem); padding-bottom: 0; + scroll-behavior: smooth; + overscroll-behavior: contain; + overflow-anchor: none; /* transform: translateX(-1%) translateY(2%) rotateX(0deg) rotateY(-1deg) rotateZ(0deg); */ } diff --git a/public/js/knuth-and-plass.js b/public/js/knuth-and-plass.js index 9e0808a..3a9fb8b 100644 --- a/public/js/knuth-and-plass.js +++ b/public/js/knuth-and-plass.js @@ -8,7 +8,7 @@ function kap(text, measureText, measure, hyphenation) { let spaceWidth = measureText('\u00A0'); let nodes = []; - text.split(/([.,:;!?] |\s|\||<.*?>)/u).forEach(function (fragment) { + text.split(/([.,:;!?…] |\s|\||<.*?>)/u).forEach(function (fragment) { let fragmentWidth = measureText(fragment); if (fragment === ' ') { @@ -21,8 +21,8 @@ function kap(text, measureText, measure, hyphenation) { nodes.push(linebreak.penalty(hyphenWidth * 0.25, 100, 1)); } else if (fragment.match(/(<.*?>)/u)) { nodes.push(linebreak.tag(fragmentWidth, fragment)); - } else if (fragment.match(/[.,:;!?] /u)) { - let punctuation = fragment.match(/([.,:;!?])( )/u); + } else if (fragment.match(/[.,:;!?…] /u)) { + let punctuation = fragment.match(/([.,:;!?…])( )/u); let punctuationSymbolWidth = measureText(punctuation[1]) * 0.25; let punctuationWidth = measureText(punctuation[1]) * 0.75 + spaceWidth; nodes.push(linebreak.box(punctuationSymbolWidth, punctuation[1])); diff --git a/public/js/layout-renderer-module.js b/public/js/layout-renderer-module.js index e928765..a325c0e 100644 --- a/public/js/layout-renderer-module.js +++ b/public/js/layout-renderer-module.js @@ -150,7 +150,7 @@ class LayoutRendererModule extends BaseModule { if (node.type === 'box' && node.value !== '' && j < currentBreak.position) { const followsGlue = j > 0 && nodes[j - 1].type === 'glue'; - const isTrailingPunctuation = /^[,.;:!?)]$/.test(node.value) && !followsGlue; + const isTrailingPunctuation = /^[,.;:!?…)]$/.test(node.value) && !followsGlue; // Check if this box follows a penalty (hyphenation point) if (lastChild && isTrailingPunctuation) { diff --git a/public/js/ui-display-handler-module.js b/public/js/ui-display-handler-module.js index 0446d43..ca7c56c 100644 --- a/public/js/ui-display-handler-module.js +++ b/public/js/ui-display-handler-module.js @@ -34,6 +34,7 @@ class UIDisplayHandlerModule extends BaseModule { 'displayText', 'renderSentence', 'handleDeferredMediaBlock', + 'scrollStoryToEnd', 'rerenderStory', 'clear', 'scheduleRerender', @@ -397,14 +398,11 @@ class UIDisplayHandlerModule extends BaseModule { } }); + this.scrollStoryToEnd(true); + // Start coordinated playback (animation + TTS), including chapter headings. await this.playbackCoordinator.play(sentence); - // Scroll to bottom - if (this.pageRight) { - this.pageRight.scrollTop = this.pageRight.scrollHeight; - } - // Call completion callback if (sentence.onComplete) { sentence.onComplete(); @@ -464,6 +462,19 @@ class UIDisplayHandlerModule extends BaseModule { } } + scrollStoryToEnd(smooth = true) { + if (!this.pageRight) { + return; + } + + window.requestAnimationFrame(() => { + this.pageRight.scrollTo({ + top: Math.max(0, this.pageRight.scrollHeight - this.pageRight.clientHeight), + behavior: smooth ? 'smooth' : 'auto' + }); + }); + } + async handleDeferredMediaBlock(sentence) { document.dispatchEvent(new CustomEvent('story:media-block', { detail: { @@ -496,6 +507,9 @@ class UIDisplayHandlerModule extends BaseModule { this.container.appendChild(this.paragraphContainer); } this.renderedItems = []; + if (this.pageRight) { + this.pageRight.scrollTop = 0; + } } /**