Files
ai.interactive.fiction/public/js/book-page-format-module.js
T
2026-06-07 16:42:09 +02:00

175 lines
5.7 KiB
JavaScript

/**
* 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-reveal-clock';
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;