/** * Book Texture Renderer Module * Draws the virtual book pages directly into texture-space canvases. */ import { BaseModule } from './base-module.js'; class BookTextureRendererModule extends BaseModule { constructor() { super('book-texture-renderer', 'Book Texture Renderer'); this.dependencies = ['book-page-format', 'localization']; this.pageFormat = null; this.localization = null; this.metrics = null; this.canvases = { left: null, right: null }; this.contexts = { left: null, right: null }; this.hitMaps = { left: [], right: [] }; this.bindMethods([ 'initialize', 'createPageCanvases', 'drawEmptySpread', 'drawPageBase', 'drawDebugText', 'publishSpread', 'getPageCanvas', 'getHitMap', 'handleSceneReady' ]); } async initialize() { this.pageFormat = this.getModule('book-page-format'); this.localization = this.getModule('localization'); this.reportProgress(20, 'Preparing page texture canvases'); this.createPageCanvases(); this.drawEmptySpread(); this.addEventListener(document, 'webgl-book:scene-ready', this.handleSceneReady); this.reportProgress(100, 'Book texture renderer ready'); return true; } createPageCanvases(textureWidth = 1280) { this.metrics = this.pageFormat.getTextureMetrics(textureWidth); ['left', 'right'].forEach((side) => { const canvas = document.createElement('canvas'); canvas.width = this.metrics.width; canvas.height = this.metrics.height; this.canvases[side] = canvas; this.contexts[side] = canvas.getContext('2d'); }); } drawEmptySpread() { this.drawPageBase('left'); this.drawPageBase('right'); this.drawDebugText('right', 'Book canvas renderer ready'); this.publishSpread(); } drawPageBase(side) { const canvas = this.canvases[side]; const ctx = this.contexts[side]; if (!canvas || !ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#fff7dc'; ctx.fillRect(0, 0, canvas.width, canvas.height); const shade = ctx.createLinearGradient(0, 0, canvas.width, 0); if (side === 'left') { shade.addColorStop(0, 'rgba(255, 255, 255, 0.10)'); shade.addColorStop(0.78, 'rgba(255, 255, 255, 0)'); shade.addColorStop(1, 'rgba(82, 42, 14, 0.16)'); } else { shade.addColorStop(0, 'rgba(82, 42, 14, 0.16)'); shade.addColorStop(0.22, 'rgba(255, 255, 255, 0)'); shade.addColorStop(1, 'rgba(255, 255, 255, 0.10)'); } ctx.fillStyle = shade; ctx.fillRect(0, 0, canvas.width, canvas.height); this.hitMaps[side] = []; } drawDebugText(side, text) { const ctx = this.contexts[side]; const metrics = this.metrics; if (!ctx || !metrics) return; ctx.save(); ctx.fillStyle = 'rgba(31, 19, 10, 0.82)'; ctx.font = `${Math.round(metrics.typography.bodyFontSizePt * 1.55)}px ${metrics.typography.fontFamily}`; ctx.textBaseline = 'alphabetic'; ctx.fillText(String(text || ''), metrics.content.x, metrics.content.y + 44); ctx.restore(); } publishSpread() { document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', { detail: { left: this.canvases.left, right: this.canvases.right, metrics: this.metrics, hitMaps: this.hitMaps } })); } getPageCanvas(side) { return this.canvases[side] || null; } getHitMap(side) { return this.hitMaps[side] || []; } handleSceneReady() { this.publishSpread(); } } const bookTextureRenderer = new BookTextureRendererModule(); export { bookTextureRenderer as BookTextureRenderer }; if (window.moduleRegistry) { window.moduleRegistry.register(bookTextureRenderer); } window.BookTextureRenderer = bookTextureRenderer;