/** * Book Page Format Module * Defines the canonical page geometry used by the WebGL book renderer. */ import { BaseModule } from './base-module.js'; import { calculateProceduralBookThickness, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-shader-reveal'; export const BOOK_TEXTURE_WIDTH = 3072; class BookPageFormatModule extends BaseModule { constructor() { super('book-page-format', 'Book Page Format'); this.dependencies = []; this.format = Object.freeze({ id: 'us-mass-market-paperback', trim: Object.freeze({ widthIn: 4.25, heightIn: 6.87 }), margins: Object.freeze({ topIn: 0.46, bottomIn: 0.58, innerBaseIn: 0.42, innerMinIn: 0.48, innerMaxIn: 0.74, innerThicknessFactor: 0.32, outerBaseIn: 0.36, outerThicknessFactor: 0.02, outerMaxIn: 0.42 }), typography: Object.freeze({ fontFamily: '"EB Garamond", "EB Garamond 12", serif', linesPerPage: 25, bodyLineRatio: 1.5, headingScale: 1, dropCapLines: 2 }) }); this.pageCount = snapProceduralPageCount(window.WebGLBookInitialState?.pageCount ?? 300); this.bindMethods([ 'getFormat', 'getAspectRatio', 'getTextureWidth', 'getTextureMetrics', 'setPageCount', 'getPageCount', 'getDynamicMargins', 'inchesToTexture' ]); } async initialize() { this.addEventListener(document, 'webgl-book:page-count-changed', (event) => { this.setPageCount(event.detail?.pageCount); }); this.addEventListener(document, 'preference-updated', (event) => { const detail = event.detail || {}; if (detail.category === 'webgl' && detail.key === 'bookPageCount') this.setPageCount(detail.value); }); this.reportProgress(100, 'Book page format ready'); return true; } getFormat() { return this.format; } getAspectRatio() { return this.format.trim.widthIn / this.format.trim.heightIn; } getTextureWidth() { return BOOK_TEXTURE_WIDTH; } inchesToTexture(valueIn, textureHeight) { return (Number(valueIn) / this.format.trim.heightIn) * textureHeight; } setPageCount(value) { const nextPageCount = snapProceduralPageCount(value ?? this.pageCount); if (nextPageCount === this.pageCount) return this.pageCount; this.pageCount = nextPageCount; return this.pageCount; } getPageCount() { return this.pageCount; } getDynamicMargins(pageCount = this.pageCount) { const marginConfig = this.format.margins; const thickness = calculateProceduralBookThickness(pageCount); const innerIn = Math.min( marginConfig.innerMaxIn, Math.max( marginConfig.innerMinIn, marginConfig.innerBaseIn + thickness.textBlockThicknessIn * marginConfig.innerThicknessFactor ) ); const outerIn = Math.min( marginConfig.outerMaxIn, marginConfig.outerBaseIn + thickness.textBlockThicknessIn * marginConfig.outerThicknessFactor ); return { topIn: 0.46, bottomIn: 0.58, innerIn, outerIn, thickness }; } getTextureMetrics(textureWidth = BOOK_TEXTURE_WIDTH, pageCount = this.pageCount) { const width = Math.max(1, Math.round(Number(textureWidth) || 1280)); const height = Math.round(width / this.getAspectRatio()); const dynamicMargins = this.getDynamicMargins(pageCount); const margins = { top: this.inchesToTexture(dynamicMargins.topIn, height), bottom: this.inchesToTexture(dynamicMargins.bottomIn, height), inner: this.inchesToTexture(dynamicMargins.innerIn, height), outer: this.inchesToTexture(dynamicMargins.outerIn, height) }; const content = { x: margins.outer, y: margins.top, width: Math.max(1, width - margins.outer - margins.inner), height: Math.max(1, height - margins.top - margins.bottom) }; const contentBySide = { left: { ...content, x: margins.outer }, right: { ...content, x: margins.inner } }; const linesPerPage = Math.max(1, Number(this.format.typography.linesPerPage || 25)); const typographyLineHeightPx = content.height / linesPerPage; const bodyFontSizePx = typographyLineHeightPx / Math.max(1, Number(this.format.typography.bodyLineRatio || 1.5)); return { width, height, aspectRatio: this.getAspectRatio(), margins, content, contentBySide, marginsIn: { top: dynamicMargins.topIn, bottom: dynamicMargins.bottomIn, inner: dynamicMargins.innerIn, outer: dynamicMargins.outerIn }, thickness: dynamicMargins.thickness, linesPerPage, bodyFontSizePx, typographyLineHeightPx, typography: this.format.typography }; } } const bookPageFormat = new BookPageFormatModule(); export { bookPageFormat as BookPageFormat }; if (window.moduleRegistry) { window.moduleRegistry.register(bookPageFormat); } window.BookPageFormat = bookPageFormat;