Add texture drop cap pagination

This commit is contained in:
2026-06-06 15:39:53 +02:00
parent 431e305df9
commit 1b8c8f8bce
4 changed files with 40 additions and 7 deletions
+26 -5
View File
@@ -21,6 +21,8 @@ class BookPaginationModule extends BaseModule {
'refreshFromHistory', 'refreshFromHistory',
'buildSpreads', 'buildSpreads',
'layoutTextBlock', 'layoutTextBlock',
'getDropCapText',
'extractDropCapText',
'extractLines', 'extractLines',
'getLineGeometry', 'getLineGeometry',
'getSpread', 'getSpread',
@@ -80,7 +82,7 @@ class BookPaginationModule extends BaseModule {
let blockWordCursor = 0; let blockWordCursor = 0;
cursorLine += layout.topSpaceLines; cursorLine += layout.topSpaceLines;
layout.lines.forEach((line) => { layout.lines.forEach((line, layoutLineIndex) => {
const geometry = this.getLineGeometry(cursorLine); const geometry = this.getLineGeometry(cursorLine);
const lineWordCount = line.nodes.filter(node => node?.type === 'box' && node.value).length; const lineWordCount = line.nodes.filter(node => node?.type === 'box' && node.value).length;
if (!spreads[geometry.spreadIndex]) { if (!spreads[geometry.spreadIndex]) {
@@ -97,7 +99,8 @@ class BookPaginationModule extends BaseModule {
fontPx: layout.fontPx, fontPx: layout.fontPx,
lineHeightPx: layout.lineHeightPx, lineHeightPx: layout.lineHeightPx,
fontStyle: layout.fontStyle, fontStyle: layout.fontStyle,
blockWordStart: blockWordCursor blockWordStart: blockWordCursor,
dropCapText: layoutLineIndex === 0 ? layout.dropCapText : ''
}); });
blockWordCursor += lineWordCount; blockWordCursor += lineWordCount;
cursorLine += 1; cursorLine += 1;
@@ -109,7 +112,10 @@ class BookPaginationModule extends BaseModule {
} }
layoutTextBlock(block = {}, type = 'paragraph') { layoutTextBlock(block = {}, type = 'paragraph') {
const text = String(block.layoutText || block.text || '').trim(); const sourceText = String(block.layoutText || block.text || '').trim();
const dropCap = Boolean(block.dropCap || block.metadata?.dropCap);
const dropCapText = dropCap ? this.getDropCapText(sourceText) : '';
const text = dropCap ? this.extractDropCapText(sourceText) : sourceText;
if (!text || !this.paragraphLayout) return null; if (!text || !this.paragraphLayout) return null;
const typography = this.metrics.typography; const typography = this.metrics.typography;
@@ -119,13 +125,16 @@ class BookPaginationModule extends BaseModule {
const bottomSpaceLines = role === 'chapter-heading' || role === 'section-heading' ? 1 : 0; const bottomSpaceLines = role === 'chapter-heading' || role === 'section-heading' ? 1 : 0;
const lineHeightPx = Math.max(1, Number(this.metrics.typographyLineHeightPx || 1)); const lineHeightPx = Math.max(1, Number(this.metrics.typographyLineHeightPx || 1));
const fontPx = Math.max(1, Number(this.metrics.bodyFontSizePx || lineHeightPx / 1.5)); const fontPx = Math.max(1, Number(this.metrics.bodyFontSizePx || lineHeightPx / 1.5));
const dropCapWidth = dropCap ? lineHeightPx * 1.58 : 0;
const indent = (isHeading || block.isFirstParagraphInChapter || block.metadata?.isFirstParagraphInChapter || block.addTopSpace) const indent = (isHeading || block.isFirstParagraphInChapter || block.metadata?.isFirstParagraphInChapter || block.addTopSpace)
? 0 ? 0
: lineHeightPx * 1.5; : lineHeightPx * 1.5;
const measures = isHeading const measures = isHeading
? [this.metrics.content.width] ? [this.metrics.content.width]
: [Math.max(120, this.metrics.content.width - indent), this.metrics.content.width, this.metrics.content.width]; : dropCap
const lineOffsets = isHeading ? [0] : [indent, 0, 0]; ? [Math.max(120, this.metrics.content.width - dropCapWidth), Math.max(120, this.metrics.content.width - dropCapWidth), this.metrics.content.width]
: [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 layout = this.paragraphLayout.calculateLayout(text, {
measures, measures,
@@ -144,6 +153,8 @@ class BookPaginationModule extends BaseModule {
fontStyle: isHeading ? 'italic' : 'normal', fontStyle: isHeading ? 'italic' : 'normal',
topSpaceLines, topSpaceLines,
bottomSpaceLines, bottomSpaceLines,
dropCapText,
dropCap,
lines: this.extractLines(layout, { lines: this.extractLines(layout, {
measures, measures,
lineOffsets, lineOffsets,
@@ -152,6 +163,16 @@ class BookPaginationModule extends BaseModule {
}; };
} }
getDropCapText(text) {
return String(text || '').trimStart().match(/\S/u)?.[0] || '';
}
extractDropCapText(text) {
const dropCap = this.getDropCapText(text);
if (!dropCap) return String(text || '');
return String(text || '').replace(dropCap, '').trimStart();
}
extractLines(layout, options = {}) { extractLines(layout, options = {}) {
const lines = []; const lines = [];
const breaks = Array.isArray(layout.breaks) ? layout.breaks : []; const breaks = Array.isArray(layout.breaks) ? layout.breaks : [];
+12
View File
@@ -159,6 +159,18 @@ class BookTextureRendererModule extends BaseModule {
let wordIndex = 0; let wordIndex = 0;
ctx.font = `${fontStyle}${fontPx}px ${metrics.typography.fontFamily}`; ctx.font = `${fontStyle}${fontPx}px ${metrics.typography.fontFamily}`;
if (lineRecord.dropCapText) {
ctx.save();
ctx.font = `${Math.round(lineHeightPx * 2.08)}px "EB Garamond Initials", ${metrics.typography.fontFamily}`;
ctx.textBaseline = 'top';
ctx.fillText(
String(lineRecord.dropCapText),
metrics.content.x,
metrics.content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) - (lineHeightPx * 0.08)
);
ctx.restore();
ctx.font = `${fontStyle}${fontPx}px ${metrics.typography.fontFamily}`;
}
nodes.forEach((node, index) => { nodes.forEach((node, index) => {
if (!node) return; if (!node) return;
if (node.type === 'box' && node.value) { if (node.type === 'box' && node.value) {
+1 -1
View File
@@ -24,7 +24,7 @@ const ModuleState = {
ERROR: 'ERROR' ERROR: 'ERROR'
}; };
const MODULE_CACHE_BUSTER = '20260606-webgl-fps-texture-animation'; const MODULE_CACHE_BUSTER = '20260606-webgl-texture-dropcap-animation';
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER; window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
/** /**
+1 -1
View File
@@ -4,7 +4,7 @@ import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postproces
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js'; import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js'; import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js'; import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260606-webgl-fps-texture-animation'; import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260606-webgl-texture-dropcap-animation';
const canvas = document.getElementById('scene'); const canvas = document.getElementById('scene');
canvas.style.cursor = 'grab'; canvas.style.cursor = 'grab';