PageSourceSearch

https://lifedex.pages.dev/js/wrap/build.js

js lifedex.pages.dev collected 2026-10-03 05:40:09 UTC 6,290 bytes, 159 lines download raw bytes

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.