Checkpoint line-grid renderer state
This commit is contained in:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user