Add texture drop cap pagination
This commit is contained in:
@@ -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 : [];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user