Checkpoint line-grid renderer state

This commit is contained in:
2026-05-16 15:57:03 +02:00
parent fe33e4f0ab
commit b9ae7f71c5
5 changed files with 49 additions and 190 deletions
+12 -7
View File
@@ -116,6 +116,9 @@ body.switched {
--book-width: 2727px;
--book-height: 1691px;
--book-scale: 1;
--page-line-count: 25;
--story-line-height: calc((var(--book-height) * 0.85) / var(--page-line-count));
--story-font-size: calc(var(--story-line-height) / 1.45);
font-size: calc(var(--book-height)/(34 * 1.5));
--img-aspect-ratio: 1.613;
--aspect-ratio: min(var(--viewport-aspect-ratio), var(--img-aspect-ratio));
@@ -403,7 +406,6 @@ ol.choice {
padding: 0 3rem 1rem 1rem;
/* border: 1px dotted rgba(200,200,200,1); */
overflow: visible;
overflow-y: auto;
opacity: 0.95;
mix-blend-mode: darken;
}
@@ -415,7 +417,8 @@ ol.choice {
text-align: justify;
text-justify: inter-word;
margin-bottom: 0;
line-height: 1.5;
font-size: var(--story-font-size);
line-height: var(--story-line-height);
will-change: transform;
transform: translateY(0);
}
@@ -485,6 +488,7 @@ ol.choice {
#page_left {
left: 11.5%;
overflow-y: auto;
}
#page_right {
@@ -938,17 +942,18 @@ html[data-process-state="playing-ready"] * {
#story p {
text-align: justify;
text-justify: inter-word;
margin-bottom: 1.2em;
line-height: 1.45;
font-size: var(--story-font-size);
margin: 0;
line-height: var(--story-line-height);
}
#story p.story-chapter-heading {
position: relative;
height: auto;
text-align: center;
font-size: 1.2rem;
font-size: var(--story-font-size);
font-style: italic;
line-height: 1.45;
line-height: var(--story-line-height);
}
#story p.story-section-heading {
@@ -956,7 +961,7 @@ html[data-process-state="playing-ready"] * {
height: auto;
text-align: center;
font-style: normal;
line-height: 1.45;
line-height: var(--story-line-height);
}
/* Typography for word elements in rendered paragraphs */
+10 -2
View File
@@ -95,22 +95,30 @@ class LayoutRendererModule extends BaseModule {
return null;
}
const lineHeight = lineHeightPx || parseFloat(window.getComputedStyle(paragraph).lineHeight) || 24;
if (!Number.isFinite(Number(lineHeightPx)) || Number(lineHeightPx) <= 0) {
throw new Error('LayoutRenderer: Missing canonical lineHeightPx for story layout.');
}
const lineHeight = Number(lineHeightPx);
let marginLines = 0;
if (layoutData.role === 'chapter-heading') {
paragraph.style.marginTop = `${lineHeight * 2}px`;
paragraph.style.marginBottom = `${lineHeight}px`;
marginLines = 3;
} else if (layoutData.role === 'section-heading') {
paragraph.style.marginTop = `${lineHeight}px`;
paragraph.style.marginBottom = `${lineHeight}px`;
marginLines = 2;
} else if (layoutData.addTopSpace) {
paragraph.style.marginTop = `${lineHeight}px`;
marginLines = 1;
}
const maxLineWidth = Array.isArray(measures) && measures.length > 0
? Math.max(...measures)
: storyElement.clientWidth;
// Height should include all lines (breaks.length represents number of lines)
const numLines = breaks.length - 1;
const numLines = Math.max(1, breaks.length - 1);
paragraph.style.height = `${lineHeight * numLines}px`;
paragraph.dataset.heightLines = String(numLines + marginLines);
console.log(`LayoutRenderer: Rendering paragraph ${id} - ${breaks.length} breaks (${numLines} lines), lineHeight: ${lineHeight}px, total height: ${lineHeight * numLines}px`);
+2 -9
View File
@@ -751,15 +751,8 @@ class SentenceQueueModule extends BaseModule {
await document.fonts.ready;
}
const probe = document.createElement('p');
probe.style.visibility = 'hidden';
probe.style.position = 'absolute';
probe.style.left = '-8000px';
probe.style.top = '-8000px';
storyElement.appendChild(probe);
const computedStyle = window.getComputedStyle(probe);
const computedStyle = window.getComputedStyle(storyElement);
const lineHeight = parseFloat(computedStyle.lineHeight) || 24;
probe.remove();
const pageWidth = storyElement.clientWidth;
const requestedSize = String(metadata.size || 'landscape').toLowerCase();
@@ -782,7 +775,7 @@ class SentenceQueueModule extends BaseModule {
if (isPortrait) {
this.activeImageWrap = {
lines: lineCount + 1,
lines: lineCount,
width: width + imageGap,
imageWidth: width,
gap: imageGap,
-2
View File
@@ -156,8 +156,6 @@ class StoryHistoryModule extends BaseModule {
...record,
lineStart,
lineCount,
heightPx: Math.max(0, Number(metrics.heightPx || record.heightPx || 0)),
measuredLineHeightPx: Math.max(0, Number(metrics.lineHeightPx || record.measuredLineHeightPx || 0)),
metricsUpdatedAt: Date.now()
};
+25 -170
View File
@@ -4,6 +4,8 @@
*/
import { BaseModule } from './base-module.js';
const PAGE_LINE_COUNT = 25;
class UIDisplayHandlerModule extends BaseModule {
constructor() {
super('ui-display-handler', 'UI Display Handler');
@@ -22,11 +24,11 @@ class UIDisplayHandlerModule extends BaseModule {
this.lastStoryMetrics = null;
this.visibleBlockLimit = 41;
this.historyBufferBlocks = 20;
this.pageLineCount = PAGE_LINE_COUNT;
this.historyWindowStartId = 1;
this.historyWindowEndId = 0;
this.loadingHistoryPage = false;
this.draggingStoryScrollbar = false;
this.pendingHistoryWindowRequest = null;
this.historyWheelAccumulator = 0;
this.storyTopLine = 0;
this.storyOffsetPx = 0;
@@ -57,11 +59,6 @@ class UIDisplayHandlerModule extends BaseModule {
'renderStoredItem',
'renderHistoryWindow',
'renderHistoryWindowForTurn',
'loadPreviousHistoryPage',
'loadNextHistoryPage',
'loadHistoryWindowAt',
'shiftHistoryWindow',
'loadAdjacentHistoryBlock',
'insertStoredElement',
'trimVirtualWindow',
'handleHistoryWheel',
@@ -87,7 +84,6 @@ class UIDisplayHandlerModule extends BaseModule {
'readFirstFiniteNumber',
'waitForSkippablePause',
'scrollStoryToEnd',
'animateStoryOffset',
'scrollToTurn',
'handleStoryScroll',
'rerenderStory',
@@ -683,13 +679,7 @@ class UIDisplayHandlerModule extends BaseModule {
}
scrollStoryToEnd(smooth = true) {
this.measureStoryLineHeight();
const targetTopLine = this.getMaxStoryTopLine();
const storyHeight = this.paragraphContainer?.offsetHeight || 0;
const viewportHeight = this.pageRight?.clientHeight || 0;
const targetOffset = Math.min(0, viewportHeight - storyHeight);
this.storyTopLine = targetTopLine;
return this.animateStoryOffset(targetOffset, smooth ? 720 : 0, { mode: 'auto-bottom' });
return this.setStoryTopLine(this.getMaxStoryTopLine(), smooth, { mode: 'auto-bottom', ensure: false });
}
async restoreFromHistory(saveRecord = {}) {
@@ -825,32 +815,6 @@ class UIDisplayHandlerModule extends BaseModule {
return result.targetBlockId;
}
async loadPreviousHistoryPage() {
if (!this.storyHistory || !this.paragraphContainer || this.loadingHistoryPage) return;
const beforeBlockId = this.historyWindowStartId || Number(this.paragraphContainer.querySelector('[data-story-block-id]')?.dataset?.storyBlockId || 0);
if (!beforeBlockId || beforeBlockId <= 1) return;
await this.loadAdjacentHistoryBlock(-1);
}
async loadNextHistoryPage() {
if (!this.storyHistory || !this.paragraphContainer || this.loadingHistoryPage || !this.hasNewerHistory()) return;
await this.loadAdjacentHistoryBlock(1);
}
async shiftHistoryWindow(deltaBlocks, scrollTarget = 'top') {
if (!this.storyHistory) return;
if (Math.abs(Number(deltaBlocks) || 0) === 1 && scrollTarget === 'preserve') {
await this.loadAdjacentHistoryBlock(deltaBlocks);
return;
}
const latest = this.getLatestHistoryBlockId();
const visible = Math.min(this.visibleBlockLimit, latest || this.visibleBlockLimit);
const maxStart = Math.max(1, latest - visible + 1);
const start = Math.max(1, Math.min(maxStart, (this.historyWindowStartId || 1) + deltaBlocks));
if (start === this.historyWindowStartId) return;
await this.loadHistoryWindowAt(start, scrollTarget);
}
handleHistoryWheel(event) {
if (!event.target?.closest?.('#page_right') || !this.pageRight) return;
event.preventDefault();
@@ -860,48 +824,6 @@ class UIDisplayHandlerModule extends BaseModule {
this.scrollStoryByLines(lineDelta, true);
}
async loadAdjacentHistoryBlock(direction) {
if (!this.storyHistory || !this.paragraphContainer || this.loadingHistoryPage) {
if (direction) {
const startBlockId = Math.max(1, (this.historyWindowStartId || 1) + Math.sign(direction));
this.pendingHistoryWindowRequest = { startBlockId, scrollTarget: 'preserve' };
}
return;
}
const step = Math.sign(Number(direction) || 0);
if (!step) return;
const latest = this.getLatestHistoryBlockId();
const targetBlockId = step < 0
? (this.historyWindowStartId || 1) - 1
: (this.historyWindowEndId || 0) + 1;
if (targetBlockId < 1 || targetBlockId > latest) return;
this.loadingHistoryPage = true;
try {
const blocks = await this.storyHistory.getBlocksRange(this.storyHistory.currentGameId, targetBlockId, targetBlockId);
const block = blocks[0];
if (!block) return;
await this.renderStoredItem(block, step < 0 ? 'prepend' : 'append');
this.historyWindowStartId = Math.min(this.historyWindowStartId || targetBlockId, targetBlockId);
this.historyWindowEndId = Math.max(this.historyWindowEndId || targetBlockId, targetBlockId);
await new Promise(resolve => window.requestAnimationFrame(resolve));
await this.trimVirtualWindow(step);
this.setVirtualPadding();
this.updateStoryScrollbar();
} finally {
this.loadingHistoryPage = false;
const pending = this.pendingHistoryWindowRequest;
this.pendingHistoryWindowRequest = null;
if (pending && Number(pending.startBlockId) !== this.historyWindowStartId) {
await this.loadAdjacentHistoryBlock(Number(pending.startBlockId) < this.historyWindowStartId ? -1 : 1);
}
}
}
async trimVirtualWindow(direction = 1) {
if (!this.paragraphContainer) return;
const excess = this.renderedItems.length - this.visibleBlockLimit;
@@ -928,33 +850,6 @@ class UIDisplayHandlerModule extends BaseModule {
this.setVirtualPadding();
}
async loadHistoryWindowAt(startBlockId, scrollTarget = 'top') {
if (!this.storyHistory) return;
if (this.loadingHistoryPage) {
this.pendingHistoryWindowRequest = { startBlockId, scrollTarget };
return;
}
const latest = this.getLatestHistoryBlockId();
const maxStart = Math.max(1, latest - this.visibleBlockLimit + 1);
const start = Math.max(1, Math.min(maxStart, Number(startBlockId || 1)));
const end = Math.min(latest, start + this.visibleBlockLimit - 1);
this.loadingHistoryPage = true;
try {
const blocks = await this.storyHistory.getBlocksRange(this.storyHistory.currentGameId, start, end);
if (blocks.length) {
await this.renderHistoryWindow(blocks, scrollTarget);
}
} finally {
this.loadingHistoryPage = false;
const pending = this.pendingHistoryWindowRequest;
this.pendingHistoryWindowRequest = null;
if (pending && Number(pending.startBlockId) !== this.historyWindowStartId) {
await this.loadHistoryWindowAt(pending.startBlockId, pending.scrollTarget);
}
}
}
markBlockRendered(blockId) {
if (this.storyHistory && typeof this.storyHistory.markRendered === 'function') {
const latestRenderedBlockId = this.storyHistory.markRendered(blockId);
@@ -1032,26 +927,26 @@ class UIDisplayHandlerModule extends BaseModule {
}
measureStoryLineHeight() {
const element = this.paragraphContainer || this.container || this.pageRight;
const computed = element ? window.getComputedStyle(element) : null;
const parsed = parseFloat(computed?.lineHeight || '');
const fontSize = parseFloat(computed?.fontSize || '');
this.lineHeightPx = Number.isFinite(parsed)
? parsed
: (Number.isFinite(fontSize) ? fontSize * 1.45 : 24);
this.viewportLineCount = Math.max(1, Math.floor((this.pageRight?.clientHeight || this.lineHeightPx) / this.lineHeightPx));
const pageHeight = this.pageRight?.clientHeight || 0;
const lineHeight = pageHeight > 0
? pageHeight / this.pageLineCount
: this.lineHeightPx || 24;
this.lineHeightPx = lineHeight;
this.viewportLineCount = this.pageLineCount;
document.documentElement.style.setProperty('--page-line-count', String(this.pageLineCount));
document.documentElement.style.setProperty('--story-line-height', `${lineHeight}px`);
document.documentElement.style.setProperty('--story-font-size', `${lineHeight / 1.45}px`);
return this.lineHeightPx;
}
measureBlockLines(element, fallbackLineCount = 1) {
if (!element) return { lineCount: Math.max(1, fallbackLineCount), heightPx: 0, lineHeightPx: this.measureStoryLineHeight() };
const lineHeight = this.measureStoryLineHeight();
const style = window.getComputedStyle(element);
const marginTop = parseFloat(style.marginTop || '0') || 0;
const marginBottom = parseFloat(style.marginBottom || '0') || 0;
const heightPx = Math.max(0, element.offsetHeight + marginTop + marginBottom);
const lineCount = Math.max(1, Math.ceil((heightPx || lineHeight * fallbackLineCount) / Math.max(1, lineHeight)));
return { lineCount, heightPx, lineHeightPx: lineHeight };
const declaredLines = Number(element?.dataset?.heightLines);
if (Number.isFinite(declaredLines) && declaredLines > 0) {
const lineCount = Math.max(1, Math.round(declaredLines));
return { lineCount, heightPx: lineCount * lineHeight, lineHeightPx: lineHeight };
}
throw new Error(`UIDisplayHandler: Rendered story block ${element?.id || '(unknown)'} has no data-height-lines declaration.`);
}
async recordRenderedMetrics(blockId, element, fallbackLineCount = 1) {
@@ -1215,39 +1110,6 @@ class UIDisplayHandlerModule extends BaseModule {
this.updateStoryScrollbar();
}
animateStoryOffset(targetOffset, duration = 720, options = {}) {
const target = Number(targetOffset || 0);
const start = Number(this.storyOffsetPx || 0);
const delta = target - start;
const animationId = ++this.storyScrollAnimationId;
if (!duration || Math.abs(delta) < 1) {
this.setStoryOffset(target);
this.updateStoryScrollbar({ mode: options.mode });
return Promise.resolve();
}
return new Promise(resolve => {
const startedAt = performance.now();
const ease = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
const step = (now) => {
if (animationId !== this.storyScrollAnimationId) {
resolve();
return;
}
const progress = Math.min(1, (now - startedAt) / duration);
this.setStoryOffset(start + (delta * ease(progress)));
if (progress < 1) {
requestAnimationFrame(step);
return;
}
this.setStoryOffset(target);
resolve();
};
requestAnimationFrame(step);
});
}
scrollToTurn(turnId) {
if (!this.pageRight || turnId == null) return;
const escapedTurnId = CSS.escape(String(turnId));
@@ -1257,8 +1119,6 @@ class UIDisplayHandlerModule extends BaseModule {
const targetLine = Number(target.dataset.lineStart);
if (Number.isFinite(targetLine)) {
this.setStoryTopLine(targetLine, true, { mode: 'jump-to-turn' });
} else {
this.setStoryTopLine((target.offsetTop || 0) / Math.max(1, this.measureStoryLineHeight()), true, { mode: 'jump-to-turn' });
}
return true;
};
@@ -1282,9 +1142,10 @@ class UIDisplayHandlerModule extends BaseModule {
blocks.forEach((block) => {
const lineStart = Number(block.dataset.lineStart);
const lineCount = Number(block.dataset.lineCount);
const blockMiddle = Number.isFinite(lineStart) && Number.isFinite(lineCount)
? ((lineStart + (lineCount / 2)) * this.lineHeightPx)
: block.offsetTop + (block.offsetHeight / 2);
if (!Number.isFinite(lineStart) || !Number.isFinite(lineCount)) {
return;
}
const blockMiddle = (lineStart + (lineCount / 2)) * this.lineHeightPx;
const distance = Math.abs(blockMiddle - viewportMiddle);
if (distance < bestDistance) {
bestDistance = distance;
@@ -1429,6 +1290,7 @@ class UIDisplayHandlerModule extends BaseModule {
figure.style.height = `${metrics.height}px`;
figure.style.marginTop = `${metrics.verticalMargin || 0}px`;
figure.style.marginBottom = `${metrics.verticalMargin || 0}px`;
figure.dataset.heightLines = String(Math.max(1, Math.round(metrics.lineCount || 1)));
if ((metrics.size || metadata.size) === 'portrait') {
const gap = metrics.gap || 0;
figure.style.shapeMargin = `${gap}px`;
@@ -1484,14 +1346,7 @@ class UIDisplayHandlerModule extends BaseModule {
calculateImageMetrics(size = 'landscape') {
const storyElement = document.getElementById('story');
const pageWidth = storyElement?.clientWidth || 600;
const probe = document.createElement('p');
probe.style.visibility = 'hidden';
probe.style.position = 'absolute';
probe.style.left = '-8000px';
probe.style.top = '-8000px';
(storyElement || document.body).appendChild(probe);
const lineHeight = parseFloat(window.getComputedStyle(probe).lineHeight) || 24;
probe.remove();
const lineHeight = this.measureStoryLineHeight();
const normalizedSize = String(size || 'landscape').toLowerCase() === 'widescreen'
? 'landscape'