From 1b8c8f8bce76d3c371c5e0fc75dea089f6f5661a Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Sat, 6 Jun 2026 15:39:53 +0200 Subject: [PATCH] Add texture drop cap pagination --- public/js/book-pagination-module.js | 31 +++++++++++++++++++---- public/js/book-texture-renderer-module.js | 12 +++++++++ public/js/loader.js | 2 +- public/js/webgl-book-lab.js | 2 +- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/public/js/book-pagination-module.js b/public/js/book-pagination-module.js index 1298dea..f567c9d 100644 --- a/public/js/book-pagination-module.js +++ b/public/js/book-pagination-module.js @@ -21,6 +21,8 @@ class BookPaginationModule extends BaseModule { 'refreshFromHistory', 'buildSpreads', 'layoutTextBlock', + 'getDropCapText', + 'extractDropCapText', 'extractLines', 'getLineGeometry', 'getSpread', @@ -80,7 +82,7 @@ class BookPaginationModule extends BaseModule { let blockWordCursor = 0; cursorLine += layout.topSpaceLines; - layout.lines.forEach((line) => { + layout.lines.forEach((line, layoutLineIndex) => { const geometry = this.getLineGeometry(cursorLine); const lineWordCount = line.nodes.filter(node => node?.type === 'box' && node.value).length; if (!spreads[geometry.spreadIndex]) { @@ -97,7 +99,8 @@ class BookPaginationModule extends BaseModule { fontPx: layout.fontPx, lineHeightPx: layout.lineHeightPx, fontStyle: layout.fontStyle, - blockWordStart: blockWordCursor + blockWordStart: blockWordCursor, + dropCapText: layoutLineIndex === 0 ? layout.dropCapText : '' }); blockWordCursor += lineWordCount; cursorLine += 1; @@ -109,7 +112,10 @@ class BookPaginationModule extends BaseModule { } 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; const typography = this.metrics.typography; @@ -119,13 +125,16 @@ 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.58 : 0; const indent = (isHeading || block.isFirstParagraphInChapter || block.metadata?.isFirstParagraphInChapter || block.addTopSpace) ? 0 : lineHeightPx * 1.5; const measures = isHeading ? [this.metrics.content.width] - : [Math.max(120, this.metrics.content.width - indent), this.metrics.content.width, this.metrics.content.width]; - const lineOffsets = isHeading ? [0] : [indent, 0, 0]; + : dropCap + ? [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, { measures, @@ -144,6 +153,8 @@ class BookPaginationModule extends BaseModule { fontStyle: isHeading ? 'italic' : 'normal', topSpaceLines, bottomSpaceLines, + dropCapText, + dropCap, lines: this.extractLines(layout, { measures, 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 = {}) { const lines = []; const breaks = Array.isArray(layout.breaks) ? layout.breaks : []; diff --git a/public/js/book-texture-renderer-module.js b/public/js/book-texture-renderer-module.js index c46b864..edce3b6 100644 --- a/public/js/book-texture-renderer-module.js +++ b/public/js/book-texture-renderer-module.js @@ -159,6 +159,18 @@ class BookTextureRendererModule extends BaseModule { let wordIndex = 0; 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) => { if (!node) return; if (node.type === 'box' && node.value) { diff --git a/public/js/loader.js b/public/js/loader.js index 5211b60..68ec8ea 100644 --- a/public/js/loader.js +++ b/public/js/loader.js @@ -24,7 +24,7 @@ const ModuleState = { 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; /** diff --git a/public/js/webgl-book-lab.js b/public/js/webgl-book-lab.js index ba38f2c..afa8547 100644 --- a/public/js/webgl-book-lab.js +++ b/public/js/webgl-book-lab.js @@ -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 { 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 { 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'); canvas.style.cursor = 'grab';