Fix story page scrolling and ellipsis spacing

This commit is contained in:
2026-05-15 07:35:27 +02:00
parent 74be77b267
commit b8fe8535aa
5 changed files with 60 additions and 21 deletions
+16
View File
@@ -12,6 +12,15 @@ introduction: |
Now you stand beyond the wrought iron gate, with rain cooling your face and the hill rising before you. 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. 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 # Room definitions
rooms: rooms:
# Starting area # Starting area
@@ -22,6 +31,13 @@ rooms:
The rain softens to a drizzle, and moonlight peeks through gaps in the clouds. 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. 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. 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. 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: exits:
- direction: north - direction: north
+12 -3
View File
@@ -401,19 +401,26 @@ ol.choice {
padding: 0 3rem 1rem 1rem; padding: 0 3rem 1rem 1rem;
/* border: 1px dotted rgba(200,200,200,1); */ /* border: 1px dotted rgba(200,200,200,1); */
overflow: visible; overflow: visible;
overflow-y: scroll; overflow-y: auto;
opacity: 0.95; opacity: 0.95;
mix-blend-mode: darken; mix-blend-mode: darken;
} }
#story { #story {
overflow-x: visible; overflow-x: visible;
box-sizing: border-box;
overflow-anchor: none;
text-align: justify; text-align: justify;
text-justify: inter-word; text-justify: inter-word;
margin-bottom: 1.2em; margin-bottom: 0;
line-height: 1.5; line-height: 1.5;
} }
#paragraphs {
box-sizing: border-box;
overflow-anchor: none;
}
/* #story p span { /* #story p span {
font-feature-settings: 'kern' on, 'liga' on, 'onum' on, 'clig' on, 'hlig' on; font-feature-settings: 'kern' on, 'liga' on, 'onum' on, 'clig' on, 'hlig' on;
} */ } */
@@ -425,8 +432,10 @@ ol.choice {
#page_right { #page_right {
/* background-color: rgba(200,200,200,0.5); */ /* background-color: rgba(200,200,200,0.5); */
right: 7%; right: 7%;
height: calc(28 * 1.45 * 1.2rem);
padding-bottom: 0; padding-bottom: 0;
scroll-behavior: smooth;
overscroll-behavior: contain;
overflow-anchor: none;
/* transform: translateX(-1%) translateY(2%) rotateX(0deg) rotateY(-1deg) rotateZ(0deg); */ /* transform: translateX(-1%) translateY(2%) rotateX(0deg) rotateY(-1deg) rotateZ(0deg); */
} }
+3 -3
View File
@@ -8,7 +8,7 @@ function kap(text, measureText, measure, hyphenation) {
let spaceWidth = measureText('\u00A0'); let spaceWidth = measureText('\u00A0');
let nodes = []; let nodes = [];
text.split(/([.,:;!?] |\s|\||<.*?>)/u).forEach(function (fragment) { text.split(/([.,:;!?] |\s|\||<.*?>)/u).forEach(function (fragment) {
let fragmentWidth = measureText(fragment); let fragmentWidth = measureText(fragment);
if (fragment === ' ') { if (fragment === ' ') {
@@ -21,8 +21,8 @@ function kap(text, measureText, measure, hyphenation) {
nodes.push(linebreak.penalty(hyphenWidth * 0.25, 100, 1)); nodes.push(linebreak.penalty(hyphenWidth * 0.25, 100, 1));
} else if (fragment.match(/(<.*?>)/u)) { } else if (fragment.match(/(<.*?>)/u)) {
nodes.push(linebreak.tag(fragmentWidth, fragment)); nodes.push(linebreak.tag(fragmentWidth, fragment));
} else if (fragment.match(/[.,:;!?] /u)) { } else if (fragment.match(/[.,:;!?] /u)) {
let punctuation = fragment.match(/([.,:;!?])( )/u); let punctuation = fragment.match(/([.,:;!?])( )/u);
let punctuationSymbolWidth = measureText(punctuation[1]) * 0.25; let punctuationSymbolWidth = measureText(punctuation[1]) * 0.25;
let punctuationWidth = measureText(punctuation[1]) * 0.75 + spaceWidth; let punctuationWidth = measureText(punctuation[1]) * 0.75 + spaceWidth;
nodes.push(linebreak.box(punctuationSymbolWidth, punctuation[1])); nodes.push(linebreak.box(punctuationSymbolWidth, punctuation[1]));
+1 -1
View File
@@ -150,7 +150,7 @@ class LayoutRendererModule extends BaseModule {
if (node.type === 'box' && node.value !== '' && j < currentBreak.position) { if (node.type === 'box' && node.value !== '' && j < currentBreak.position) {
const followsGlue = j > 0 && nodes[j - 1].type === 'glue'; 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) // Check if this box follows a penalty (hyphenation point)
if (lastChild && isTrailingPunctuation) { if (lastChild && isTrailingPunctuation) {
+19 -5
View File
@@ -34,6 +34,7 @@ class UIDisplayHandlerModule extends BaseModule {
'displayText', 'displayText',
'renderSentence', 'renderSentence',
'handleDeferredMediaBlock', 'handleDeferredMediaBlock',
'scrollStoryToEnd',
'rerenderStory', 'rerenderStory',
'clear', 'clear',
'scheduleRerender', 'scheduleRerender',
@@ -397,14 +398,11 @@ class UIDisplayHandlerModule extends BaseModule {
} }
}); });
this.scrollStoryToEnd(true);
// Start coordinated playback (animation + TTS), including chapter headings. // Start coordinated playback (animation + TTS), including chapter headings.
await this.playbackCoordinator.play(sentence); await this.playbackCoordinator.play(sentence);
// Scroll to bottom
if (this.pageRight) {
this.pageRight.scrollTop = this.pageRight.scrollHeight;
}
// Call completion callback // Call completion callback
if (sentence.onComplete) { if (sentence.onComplete) {
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) { async handleDeferredMediaBlock(sentence) {
document.dispatchEvent(new CustomEvent('story:media-block', { document.dispatchEvent(new CustomEvent('story:media-block', {
detail: { detail: {
@@ -496,6 +507,9 @@ class UIDisplayHandlerModule extends BaseModule {
this.container.appendChild(this.paragraphContainer); this.container.appendChild(this.paragraphContainer);
} }
this.renderedItems = []; this.renderedItems = [];
if (this.pageRight) {
this.pageRight.scrollTop = 0;
}
} }
/** /**