Implement WebGL book spread flip groundwork
This commit is contained in:
@@ -39,6 +39,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.lastAnimationFrameAt = 0;
|
||||
this.targetFrameDurationMs = 1000 / 30;
|
||||
this.pipelineTimings = [];
|
||||
this.imageCache = new Map();
|
||||
|
||||
this.bindMethods([
|
||||
'initialize',
|
||||
@@ -50,7 +51,14 @@ class BookTextureRendererModule extends BaseModule {
|
||||
'getDrawSignature',
|
||||
'cloneCanvas',
|
||||
'drawPageBase',
|
||||
'drawPageMeta',
|
||||
'drawTitlePage',
|
||||
'drawPageNumber',
|
||||
'drawPageLines',
|
||||
'drawImageRecord',
|
||||
'resolveImageSource',
|
||||
'getCachedImage',
|
||||
'drawImageFitted',
|
||||
'drawLine',
|
||||
'drawWord',
|
||||
'recordRevealRect',
|
||||
@@ -194,7 +202,9 @@ class BookTextureRendererModule extends BaseModule {
|
||||
if (!this.canvases[side]) return;
|
||||
this.drawPageBase(side);
|
||||
if (hasReveal) this.revealBaseCanvases[side] = this.cloneCanvas(this.canvases[side]);
|
||||
this.drawPageMeta(side, 'before-lines');
|
||||
this.drawPageLines(side, this.currentSpread?.[side] || []);
|
||||
this.drawPageMeta(side, 'after-lines');
|
||||
});
|
||||
const published = this.publishSpread(sidesToDraw, options);
|
||||
this.markPipelineTiming('drawSpread:end', {
|
||||
@@ -214,8 +224,9 @@ class BookTextureRendererModule extends BaseModule {
|
||||
const source = spread || {};
|
||||
return sides.map(side => {
|
||||
const lines = Array.isArray(source[side]) ? source[side] : [];
|
||||
const ids = lines.map(line => `${line.blockId ?? ''}:${line.lineIndex ?? ''}:${line.pageLine ?? ''}:${line.line?.nodes?.length || 0}`).join(',');
|
||||
return `${side}[${ids}]`;
|
||||
const meta = source.pageMeta?.[side] || {};
|
||||
const ids = lines.map(line => `${line.type || 'line'}:${line.blockId ?? ''}:${line.lineIndex ?? ''}:${line.pageLine ?? ''}:${line.lineCount ?? ''}:${line.line?.nodes?.length || 0}`).join(',');
|
||||
return `${side}:${meta.kind || ''}:${meta.pageIndex ?? ''}:${meta.pageNumber ?? ''}:${meta.omitPageNumber === true}[${ids}]`;
|
||||
}).join('|');
|
||||
}
|
||||
|
||||
@@ -254,6 +265,69 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.hitMaps[side] = [];
|
||||
}
|
||||
|
||||
drawPageMeta(side, phase = 'after-lines') {
|
||||
const meta = this.currentSpread?.pageMeta?.[side] || null;
|
||||
if (!meta) return;
|
||||
if (phase === 'before-lines' && meta.kind === 'title') this.drawTitlePage(side);
|
||||
if (phase === 'after-lines') this.drawPageNumber(side, meta);
|
||||
}
|
||||
|
||||
drawTitlePage(side) {
|
||||
const ctx = this.contexts[side];
|
||||
if (!ctx || !this.metrics) return;
|
||||
const content = this.getPageContent(side);
|
||||
const titleText = document.getElementById('game_title')?.textContent?.trim() || '';
|
||||
const authorText = document.getElementById('game_author')?.textContent?.trim() || '';
|
||||
const subtitleText = document.getElementById('game_subtitle')?.textContent?.trim() || '';
|
||||
const ornamentText = document.querySelector('#start_prompt .separator, #start_prompt .ornament, #start_prompt [class*="separator"]')?.textContent?.trim() || '';
|
||||
const legalText = document.getElementById('game_legal_text')?.textContent?.trim() || '';
|
||||
const centerX = content.x + content.width * 0.5;
|
||||
const font = this.metrics.typography.fontFamily;
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = 'rgba(31, 19, 10, 0.9)';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
if ('fontKerning' in ctx) ctx.fontKerning = 'normal';
|
||||
if (authorText) {
|
||||
ctx.font = `italic ${Math.round(this.metrics.bodyFontSizePx * 0.86)}px ${font}`;
|
||||
ctx.fillText(authorText, centerX, content.y + content.height * 0.18);
|
||||
}
|
||||
if (titleText) {
|
||||
ctx.font = `${Math.round(this.metrics.bodyFontSizePx * 1.55)}px ${font}`;
|
||||
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = 'small-caps';
|
||||
ctx.fillText(titleText, centerX, content.y + content.height * 0.28);
|
||||
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = 'normal';
|
||||
}
|
||||
if (subtitleText) {
|
||||
ctx.font = `${Math.round(this.metrics.bodyFontSizePx * 0.94)}px ${font}`;
|
||||
ctx.fillText(subtitleText, centerX, content.y + content.height * 0.39);
|
||||
}
|
||||
if (ornamentText) {
|
||||
ctx.font = `${Math.round(this.metrics.bodyFontSizePx * 1.3)}px ${font}`;
|
||||
ctx.fillText(ornamentText, centerX, content.y + content.height * 0.52);
|
||||
}
|
||||
if (legalText) {
|
||||
ctx.font = `${Math.round(this.metrics.bodyFontSizePx * 0.62)}px ${font}`;
|
||||
ctx.fillText(legalText, centerX, content.y + content.height * 0.96);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
drawPageNumber(side, meta = {}) {
|
||||
if (meta.omitPageNumber || meta.pageNumber == null) return;
|
||||
const ctx = this.contexts[side];
|
||||
if (!ctx || !this.metrics) return;
|
||||
const content = this.getPageContent(side);
|
||||
ctx.save();
|
||||
ctx.fillStyle = 'rgba(31, 19, 10, 0.74)';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = `${Math.round(this.metrics.bodyFontSizePx * 0.68)}px ${this.metrics.typography.fontFamily}`;
|
||||
ctx.fillText(String(meta.pageNumber), content.x + content.width * 0.5, content.y + content.height + this.metrics.margin.bottom * 0.48);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
drawPageLines(side, lines = []) {
|
||||
const ctx = this.contexts[side];
|
||||
if (!ctx || !this.metrics || !Array.isArray(lines)) return;
|
||||
@@ -263,10 +337,73 @@ class BookTextureRendererModule extends BaseModule {
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
if ('fontKerning' in ctx) ctx.fontKerning = 'normal';
|
||||
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = 'normal';
|
||||
lines.forEach(line => this.drawLine(ctx, line, side));
|
||||
lines.forEach(line => {
|
||||
if (line?.type === 'image' || line?.kind === 'image') this.drawImageRecord(ctx, line, side);
|
||||
else this.drawLine(ctx, line, side);
|
||||
});
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
drawImageRecord(ctx, lineRecord = {}, side = 'left') {
|
||||
const content = this.getPageContent(side);
|
||||
const layout = lineRecord.metadata?.imageLayout || {};
|
||||
const rect = layout.textureRect || {};
|
||||
const x = content.x + Number(rect.x || 0);
|
||||
const y = content.y + Number(rect.y || 0);
|
||||
const width = Math.max(1, Number(rect.width || content.width));
|
||||
const height = Math.max(1, Number(rect.height || this.metrics.typographyLineHeightPx));
|
||||
const src = this.resolveImageSource(lineRecord.metadata || {});
|
||||
|
||||
ctx.save();
|
||||
if (src) {
|
||||
const image = this.getCachedImage(src);
|
||||
if (image?.complete && image.naturalWidth > 0) {
|
||||
this.drawImageFitted(ctx, image, x, y, width, height);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
resolveImageSource(metadata = {}) {
|
||||
const explicit = String(metadata.url || metadata.src || '').trim();
|
||||
if (explicit) return explicit;
|
||||
const filename = String(metadata.filename || '').trim();
|
||||
if (!filename) return '';
|
||||
if (/^(https?:|data:|blob:|\/)/i.test(filename)) return filename;
|
||||
return `/images/${filename.replace(/^images[\\/]/i, '').replace(/\\/g, '/')}`;
|
||||
}
|
||||
|
||||
getCachedImage(src) {
|
||||
if (!src) return null;
|
||||
if (this.imageCache.has(src)) return this.imageCache.get(src);
|
||||
const image = new Image();
|
||||
image.decoding = 'async';
|
||||
image.onload = () => this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.());
|
||||
image.onerror = () => this.markPipelineTiming('image:load-error', { src });
|
||||
image.src = src;
|
||||
this.imageCache.set(src, image);
|
||||
return image;
|
||||
}
|
||||
|
||||
drawImageFitted(ctx, image, x, y, width, height) {
|
||||
const sourceWidth = image.naturalWidth || image.width || 1;
|
||||
const sourceHeight = image.naturalHeight || image.height || 1;
|
||||
const sourceAspect = sourceWidth / sourceHeight;
|
||||
const targetAspect = width / height;
|
||||
let sx = 0;
|
||||
let sy = 0;
|
||||
let sw = sourceWidth;
|
||||
let sh = sourceHeight;
|
||||
if (sourceAspect > targetAspect) {
|
||||
sw = sourceHeight * targetAspect;
|
||||
sx = (sourceWidth - sw) * 0.5;
|
||||
} else if (sourceAspect < targetAspect) {
|
||||
sh = sourceWidth / targetAspect;
|
||||
sy = (sourceHeight - sh) * 0.5;
|
||||
}
|
||||
ctx.drawImage(image, sx, sy, sw, sh, x, y, width, height);
|
||||
}
|
||||
|
||||
drawLine(ctx, lineRecord = {}, side = 'left') {
|
||||
const metrics = this.metrics;
|
||||
const content = this.getPageContent(side);
|
||||
@@ -708,7 +845,8 @@ class BookTextureRendererModule extends BaseModule {
|
||||
const detail = {
|
||||
metrics: this.metrics,
|
||||
hitMaps: this.hitMaps,
|
||||
sides: sidesToPublish
|
||||
sides: sidesToPublish,
|
||||
pageMeta: this.currentSpread?.pageMeta || {}
|
||||
};
|
||||
if (options.preloadOnly) detail.preloadOnly = true;
|
||||
if (sidesToPublish.includes('left')) {
|
||||
|
||||
Reference in New Issue
Block a user