Correct WebGL book page projection
This commit is contained in:
@@ -25,6 +25,11 @@ class BookPaginationModule extends BaseModule {
|
||||
'layoutTextBlock',
|
||||
'getDropCapText',
|
||||
'extractDropCapText',
|
||||
'measureDropCapReservation',
|
||||
'measureNormalTextGap',
|
||||
'calculateDropCapLayout',
|
||||
'extractLayoutLine',
|
||||
'extractRemainingLayoutText',
|
||||
'extractLines',
|
||||
'countLineWords',
|
||||
'getLineGeometry',
|
||||
@@ -136,7 +141,7 @@ class BookPaginationModule extends BaseModule {
|
||||
const bottomSpaceLines = role === 'chapter-heading' || role === 'section-heading' ? 1 : 0;
|
||||
const lineHeightPx = Math.max(1, Number(this.metrics.typographyLineHeightPx || 1));
|
||||
const fontPx = Math.max(1, Number(this.metrics.bodyFontSizePx || lineHeightPx / 1.5));
|
||||
const dropCapWidth = dropCap ? lineHeightPx * 1.72 : 0;
|
||||
const dropCapWidth = dropCap ? this.measureDropCapReservation(dropCapText, fontPx, lineHeightPx) : 0;
|
||||
const indent = (isHeading || block.isFirstParagraphInChapter || block.metadata?.isFirstParagraphInChapter || block.addTopSpace)
|
||||
? 0
|
||||
: lineHeightPx * 1.5;
|
||||
@@ -147,14 +152,17 @@ class BookPaginationModule extends BaseModule {
|
||||
: [Math.max(120, this.metrics.content.width - indent), this.metrics.content.width, this.metrics.content.width];
|
||||
const lineOffsets = isHeading ? [0] : dropCap ? [dropCapWidth, dropCapWidth, 0] : [indent, 0, 0];
|
||||
|
||||
const layout = this.paragraphLayout.calculateLayout(text, {
|
||||
const layoutOptions = {
|
||||
measures,
|
||||
fontSize: `${fontPx}px`,
|
||||
fontFamily: typography.fontFamily,
|
||||
fontFeatureSettings: '"kern" on, "liga" on, "onum" on, "pnum" on, "dlig" on, "clig" on, "calt" on',
|
||||
lineHeightPx,
|
||||
lineHeight: lineHeightPx / fontPx
|
||||
});
|
||||
};
|
||||
const layout = dropCap
|
||||
? this.calculateDropCapLayout(text, measures, lineOffsets, layoutOptions)
|
||||
: this.paragraphLayout.calculateLayout(text, layoutOptions);
|
||||
if (!layout) return null;
|
||||
|
||||
return {
|
||||
@@ -184,7 +192,118 @@ class BookPaginationModule extends BaseModule {
|
||||
return String(text || '').replace(dropCap, '').trimStart();
|
||||
}
|
||||
|
||||
measureDropCapReservation(dropCapText, fontPx, lineHeightPx) {
|
||||
if (!dropCapText) return lineHeightPx * 1.34;
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) return lineHeightPx * 1.34;
|
||||
|
||||
const dropCapFontPx = Math.round(fontPx * 2.68);
|
||||
context.font = `${dropCapFontPx}px "EB Garamond Initials", ${this.metrics.typography.fontFamily}`;
|
||||
const metrics = context.measureText(dropCapText);
|
||||
const inkRight = Number.isFinite(metrics.actualBoundingBoxRight) && metrics.actualBoundingBoxRight > 0
|
||||
? metrics.actualBoundingBoxRight
|
||||
: (metrics.width || 0);
|
||||
return Math.max(inkRight, lineHeightPx * 1.08) + this.measureNormalTextGap(fontPx);
|
||||
}
|
||||
|
||||
measureNormalTextGap(fontPx) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) return fontPx * 0.75;
|
||||
context.font = `${fontPx}px ${this.metrics.typography.fontFamily}`;
|
||||
const gap = context.measureText('\u2002').width;
|
||||
return Number.isFinite(gap) && gap > 0 ? gap : fontPx * 0.75;
|
||||
}
|
||||
|
||||
calculateDropCapLayout(text, measures, lineOffsets, layoutOptions) {
|
||||
const firstLineOptions = {
|
||||
...layoutOptions,
|
||||
measures: [measures[0], Math.max(measures[0] * 20, 10000)],
|
||||
fontVariantCaps: 'all-small-caps',
|
||||
fontFeatureSettings: '"smcp" on, "c2sc" on, "kern" on, "liga" on, "onum" on, "pnum" on'
|
||||
};
|
||||
const firstLayout = this.paragraphLayout.calculateLayout(text, firstLineOptions);
|
||||
if (!firstLayout?.breaks || firstLayout.breaks.length < 2) {
|
||||
return this.paragraphLayout.calculateLayout(text, layoutOptions);
|
||||
}
|
||||
|
||||
const firstLine = this.extractLayoutLine(firstLayout, 0, {
|
||||
measure: measures[0],
|
||||
offset: lineOffsets[0],
|
||||
smallCaps: true
|
||||
});
|
||||
const remainingText = this.extractRemainingLayoutText(firstLayout, firstLayout.breaks[1].position);
|
||||
const remainingLayout = this.paragraphLayout.calculateLayout(remainingText, {
|
||||
...layoutOptions,
|
||||
measures: [measures[1], ...measures.slice(2)]
|
||||
});
|
||||
const remainingLines = [];
|
||||
if (remainingLayout?.breaks?.length > 1) {
|
||||
for (let lineIndex = 0; lineIndex < remainingLayout.breaks.length - 1; lineIndex += 1) {
|
||||
remainingLines.push(this.extractLayoutLine(remainingLayout, lineIndex, {
|
||||
measure: measures[Math.min(lineIndex + 1, measures.length - 1)],
|
||||
offset: lineOffsets[Math.min(lineIndex + 1, lineOffsets.length - 1)] || 0,
|
||||
smallCaps: false
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
lines: [firstLine, ...remainingLines].filter(Boolean),
|
||||
processedText: text,
|
||||
lineHeight: layoutOptions.lineHeight,
|
||||
lineHeightPx: layoutOptions.lineHeightPx,
|
||||
fontSize: layoutOptions.fontSize,
|
||||
fontFamily: layoutOptions.fontFamily
|
||||
};
|
||||
}
|
||||
|
||||
extractLayoutLine(layout, lineIndex, metadata = {}) {
|
||||
const startBreak = layout.breaks?.[lineIndex];
|
||||
const endBreak = layout.breaks?.[lineIndex + 1];
|
||||
if (!startBreak || !endBreak || !Array.isArray(layout.nodes)) return null;
|
||||
const nodes = [];
|
||||
for (let index = startBreak.position; index <= endBreak.position; index += 1) {
|
||||
const node = layout.nodes[index];
|
||||
if (!node) continue;
|
||||
if (node.type === 'glue' && (index === startBreak.position || index === endBreak.position)) continue;
|
||||
const forcedBreak = window.linebreak?.infinity ? -window.linebreak.infinity : -100000;
|
||||
if (node.type === 'penalty' && node.penalty <= forcedBreak) continue;
|
||||
nodes.push({ ...node });
|
||||
}
|
||||
const endNode = layout.nodes[endBreak.position];
|
||||
return {
|
||||
nodes,
|
||||
measure: metadata.measure,
|
||||
offset: metadata.offset || 0,
|
||||
ratio: endBreak.ratio || 0,
|
||||
isFinal: lineIndex === layout.breaks.length - 2,
|
||||
smallCaps: Boolean(metadata.smallCaps),
|
||||
hyphenated: Boolean(endNode?.type === 'penalty' && endNode.penalty === 100),
|
||||
align: 'justify'
|
||||
};
|
||||
}
|
||||
|
||||
extractRemainingLayoutText(layout, breakPosition) {
|
||||
if (!Array.isArray(layout.nodes)) return '';
|
||||
const fragments = [];
|
||||
for (let index = breakPosition + 1; index < layout.nodes.length; index += 1) {
|
||||
const node = layout.nodes[index];
|
||||
if (!node) continue;
|
||||
if (node.type === 'box' || node.type === 'tag') {
|
||||
fragments.push(node.value || '');
|
||||
} else if (node.type === 'glue' && node.width > 0) {
|
||||
fragments.push(' ');
|
||||
} else if (node.type === 'penalty' && node.penalty === 100) {
|
||||
fragments.push('|');
|
||||
}
|
||||
}
|
||||
return fragments.join('').replace(/\s+/g, ' ').trimStart();
|
||||
}
|
||||
|
||||
extractLines(layout, options = {}) {
|
||||
if (Array.isArray(layout?.lines)) return layout.lines;
|
||||
const lines = [];
|
||||
const breaks = Array.isArray(layout.breaks) ? layout.breaks : [];
|
||||
const nodes = Array.isArray(layout.nodes) ? layout.nodes : [];
|
||||
|
||||
Reference in New Issue
Block a user