1import { ensureFontsReady, drawGridBackground, drawBookletFrame, drawBindingStrip, drawMrzBand } from './draw.js'; 2import { measurePaneHeight, drawPane, drawCoverContent, buildMrzLine } from './layout.js'; 3import { THEME, DIM, getCurrentTheme } from './theme.js'; 4 5const MIN_SCALE = 0.55; 6const ENTRY_GAP = 28; 7 8function makeCanvas(w, h) { 9 const canvas = document.createElement('canvas'); 10 canvas.width = w; 11 canvas.height = h; 12 return canvas; 13} 14 15// Grid background + booklet frame + the two dashed binding strips. Returns 16// the inner "pane" rect (excluding margin and strips) that content draws into. 17function drawBookletShell(ctx, totalW, totalH, theme = THEME) { 18 drawGridBackground(ctx, totalW, totalH, theme.page, theme.pageDark, 42); 19 20 const m = DIM.bookletMargin; 21 const bw = totalW - m * 2; 22 const bh = totalH - m * 2; 23 drawBookletFrame(ctx, m, m, bw, bh, theme, DIM.bookletBorder); 24 25 const stripW = DIM.stripW; 26 drawBindingStrip(ctx, m, m, stripW, bh, theme, 'right'); 27 drawBindingStrip(ctx, m + bw - stripW, m, stripW, bh, theme, 'left'); 28 29 return { x: m + stripW, y: m, w: bw - stripW * 2, h: bh }; 30} 31 32function entryCounts(entries) { 33 const count = type => entries.filter(e => e.type === type).length; 34 return { book: count('book'), movie: count('movie'), place: count('place') }; 35} 36 37export async function buildCoverSlide(entries, theme = null) { 38 await ensureFontsReady(); 39 const activeTheme = theme || getCurrentTheme(); 40 const canvas = makeCanvas(DIM.slideW, DIM.slideH); 41 const ctx = canvas.getContext('2d'); 42 const pane = drawBookletShell(ctx, DIM.slideW, DIM.slideH, activeTheme); 43 44 const stats = { 45 entryCount: entries.length, 46 counts: entryCounts(entries), 47 year: new Date().getFullYear(), 48 }; 49 50 const mrzLines = [ 51 buildMrzLine(['PASSPORT', 'LIFEDEX APP']), 52 buildMrzLine(['LIFEDEX.APP', `${entries.length}ENTRIES`]), 53 ]; 54 const mrzH = DIM.mrzPadY * 2 + mrzLines.length * DIM.mrzLineHeight; 55 const availableH = pane.h - mrzH; 56 57 // Measure on a throwaway canvas first so short content (few entries) can 58 // be centered in the available area instead of sitting at the top with 59 // a big empty gap before the MRZ band. 60 const measureCtx = makeCanvas(1, 1).getContext('2d'); 61 const contentH = drawCoverContent(measureCtx, 0, 0, pane.w, activeTheme, stats); 62 const offsetY = Math.max(0, (availableH - contentH) / 2); 63 64 drawCoverContent(ctx, pane.x, pane.y + offsetY, pane.w, activeTheme, stats); 65 drawMrzBand(ctx, pane.x, pane.y + pane.h - mrzH, pane.w, mrzLines, activeTheme, DIM.mrzPadX, DIM.mrzPadY, DIM.mrzLineHeight); 66 67 return canvas; 68} 69 70export async function buildSlide(entry, index, total, theme = null) { 71 await ensureFontsReady(); 72 const activeTheme = theme || getCurrentTheme(); 73 const canvas = makeCanvas(DIM.slideW, DIM.slideH); 74 const ctx = canvas.getContext('2d'); 75 const pane = drawBookletShell(ctx, DIM.slideW, DIM.slideH, activeTheme); 76 77 const mrzLines = entry.type === 'title' 78 ? [buildMrzLine(['LIFEDEX', 'CHAPTER']), buildMrzLine([entry.text])] 79 : [buildMrzLine(['LIFEDEX', entry.type]), buildMrzLine([entry.title || entry.name])]; 80 const mrzH = DIM.mrzPadY * 2 + mrzLines.length * DIM.mrzLineHeight; 81 82 const contentY = pane.y; 83 const contentW = pane.w; 84 const availableH = pane.h - mrzH; 85 86 const measureCtx = makeCanvas(1, 1).getContext('2d'); 87 let scale = 1; 88 let contentH = await measurePaneHeight(measureCtx, entry, contentW, scale); 89 if (contentH > availableH) { 90 scale = Math.max(MIN_SCALE, availableH / contentH); 91 contentH = await measurePaneHeight(measureCtx, entry, contentW, scale); 92 } 93 94 const indexLabel = `No. ${String(index + 1).padStart(2, '0')} / ${String(total).padStart(2, '0')}`; 95 96 // Center shorter content (e.g. a chapter title) in the available area 97 // instead of leaving it pinned to the top with empty space below. 98 const offsetY = Math.max(0, (availableH - contentH) / 2); 99 100 ctx.save(); 101 ctx.beginPath(); 102 ctx.rect(pane.x, contentY, pane.w, availableH); 103 ctx.clip(); 104 await drawPane(ctx, entry, pane.x, contentY + offsetY, contentW, activeTheme, scale, indexLabel);
105 ctx.restore(); 106 107 drawMrzBand(ctx, pane.x, pane.y + pane.h - mrzH, pane.w, mrzLines, activeTheme, DIM.mrzPadX, DIM.mrzPadY, DIM.mrzLineHeight); 108 109 return canvas; 110} 111 112export async function buildInfographic(entries, theme = null) { 113 await ensureFontsReady(); 114 const activeTheme = theme || getCurrentTheme(); 115 116 const measureCtx = makeCanvas(1, 1).getContext('2d'); 117 const paneW = DIM.infographicW - DIM.bookletMargin * 2 - DIM.stripW * 2; 118 119 const headerH = drawCoverContent(measureCtx, 0, 0, paneW, activeTheme, { 120 entryCount: entries.length, 121 counts: entryCounts(entries), 122 year: new Date().getFullYear(), 123 }); 124 125 const heights = []; 126 for (const entry of entries) { 127 heights.push(await measurePaneHeight(measureCtx, entry, paneW, 1)); 128 } 129 130 const mrzLines = [ 131 buildMrzLine(['PASSPORT', 'LIFEDEX APP']), 132 buildMrzLine(['LIFEDEX.APP', `${entries.length}ENTRIES`]), 133 ]; 134 const mrzH = DIM.mrzPadY * 2 + mrzLines.length * DIM.mrzLineHeight; 135 136 const bookletH = headerH + heights.reduce((sum, h) => sum + h + ENTRY_GAP, 0) + mrzH; 137 const totalH = bookletH + DIM.bookletMargin * 2; 138 139 const canvas = makeCanvas(DIM.infographicW, totalH); 140 const ctx = canvas.getContext('2d'); 141 const pane = drawBookletShell(ctx, DIM.infographicW, totalH, activeTheme); 142 143 drawCoverContent(ctx, pane.x, pane.y, pane.w, activeTheme, { 144 entryCount: entries.length, 145 counts: entryCounts(entries), 146 year: new Date().getFullYear(), 147 }); 148 149 let cursorY = pane.y + headerH; 150 for (let i = 0; i < entries.length; i++) { 151 const indexLabel = `No. ${String(i + 1).padStart(2, '0')} / ${String(entries.length).padStart(2, '0')}`; 152 await drawPane(ctx, entries[i], pane.x, cursorY, pane.w, activeTheme, 1, indexLabel); 153 cursorY += heights[i] + ENTRY_GAP; 154 } 155 156 drawMrzBand(ctx, pane.x, pane.y + pane.h - mrzH, pane.w, mrzLines, activeTheme, DIM.mrzPadX, DIM.mrzPadY, DIM.mrzLineHeight); 157 158 return canvas; 159}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.