UPG100 poster header: the three columns end at about the same height (WIP: verifying)

This commit is contained in:
Doxa Maps 2026-10-03 08:46:36 +00:00
parent cee32f2732
commit 4ec5905492

View file

@ -875,15 +875,24 @@ function boldLines(text, width) {
}
function headColumnsBand(top, facts, approxLists) {
const GRID_L = BAND_MARGIN_G, GRID_W = PW - 2 * BAND_MARGIN_G;
const CW = (GRID_W - 2 * HC.gut) / 3;
const AVAIL = GRID_W - 2 * HC.gut;
// The measure leaves room for a fallback font (see HC_FALLBACK): a block set to it stays inside its column.
const TW = (CW - 10 - BAND_TEXT_PAD / 2) * 0.92;
const measure = (cw) => (cw - 10 - BAND_TEXT_PAD / 2) * 0.92;
// ★ EVEN COLUMNS (2026-10-03): when the facts column runs longer than the two text columns it is WIDENED (and
// shorter, it is narrowed) so the three end at about the same height. The two side columns stay equal, so they
// still mirror each other round the centre. The centre's share is searched from 26% to 46% of the three-column
// width; the split whose TALLEST column is shortest wins, and a tie goes to the split nearest equal widths, so a
// zone that is already balanced keeps equal columns. measureHeader calls this same function, so the map below
// gets exactly the height that is left.
// Uppercase, letterspaced keys: measured as capitals plus the .14em tracking.
const keyW = (k) => textW(k.toUpperCase(), HC.keyFs) + k.length * HC.keyFs * 0.14;
// Each column's height at a given measure (the same arithmetic the drawing below advances y by).
const proseH = (col, tw) => HC.headLh + col[0].paras.reduce((h, t, k) => h + (k ? HC.gap : 0) + boldLines(t, tw).length * HC.lh, 0);
const factsH = (tw) => facts.reduce((h, [, value], k) => h + (k ? HC.gap : 0) + HC.keyLh + wrapPx(value, HC.fs, tw).length * HC.lh, 0);
// Column order (2026-10-02): Jesus' strategy | the facts | what the series is.
const cols = [
[{ head: 'Jesus’ Strategy to Feed the 5000', paras: strategyParas() }],
facts.map(([key, value]) => ({ key, lines: wrapPx(value, HC.fs, TW) })),
null, // the facts column, wrapped below at the centre's measure
[{ head: POSTER_TEXT.sideBoxHeading || 'What is a UPG100 List?', paras: [
(POSTER_TEXT.sideBoxBody
|| 'This map is one of a {approxLists}-map set called the {seriesName} — a large, overwhelming target in the thousands, organised into clear groupings and displayed on maps and prayer lists.')
@ -891,9 +900,22 @@ function headColumnsBand(top, facts, approxLists) {
...whatIsMore(),
] }],
];
// The split: equal thirds first, then centre shares on a 1% step ordered by their distance from a third, so
// among splits whose tallest column is equally short, the one nearest equal widths is found first and kept.
const shares = Array.from({ length: 21 }, (_, k) => 0.26 + k * 0.01).sort((a, b) => Math.abs(a - 1 / 3) - Math.abs(b - 1 / 3));
let best = null;
for (const cwC of [1 / 3, ...shares].map((f) => AVAIL * f)) {
const cwS = (AVAIL - cwC) / 2;
const tall = Math.max(proseH(cols[0], measure(cwS)), factsH(measure(cwC)), proseH(cols[2], measure(cwS)));
if (!best || tall < best.tall - 0.5) best = { cwC, cwS, tall };
}
const widths = [best.cwS, best.cwC, best.cwS];
const lefts = [GRID_L, GRID_L + best.cwS + HC.gut, GRID_L + best.cwS + HC.gut + best.cwC + HC.gut];
cols[1] = facts.map(([key, value]) => ({ key, lines: wrapPx(value, HC.fs, measure(best.cwC)) }));
let frags = '', bottom = top;
cols.forEach((col, i) => {
const centre = GRID_L + i * (CW + HC.gut) + CW / 2;
const CW = widths[i], TW = measure(CW);
const centre = lefts[i] + CW / 2;
if (col[0] && col[0].key !== undefined) { // the facts column: a bold key above each value
const blockW = Math.min(TW, Math.max(...col.map((r) => Math.max(keyW(r.key), ...r.lines.map((ln) => textW(ln, HC.fs))))));
const x = (centre - blockW * HC_FALLBACK / 2).toFixed(1);
@ -922,7 +944,7 @@ function headColumnsBand(top, facts, approxLists) {
}
});
for (let i = 1; i < 3; i++) {
const rx = (GRID_L + i * (CW + HC.gut) - HC.gut / 2).toFixed(1);
const rx = (lefts[i] - HC.gut / 2).toFixed(1);
frags += `<line x1="${rx}" y1="${top - 14}" x2="${rx}" y2="${bottom - 12}" class="hcolrule"/>`;
}
return { frags, bottom };
@ -2368,7 +2390,7 @@ async function renderPoster(zone, pins, zoneFC) {
let cy0 = bodyTop;
strategyParas().forEach((text, k) => {
if (k) cy0 += 5;
cy0 = paraInto(push, text, { x: b1x[0], y: cy0, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
cy0 = paraInto(push, String(text).replace(/\*\*/g, ''), { x: b1x[0], y: cy0, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
});
let cy1 = bodyTop;
@ -2379,9 +2401,11 @@ async function renderPoster(zone, pins, zoneFC) {
|| 'This map is one of a {approxLists}-map set called the {seriesName} — a large, overwhelming target in the thousands, organised into clear groupings and displayed on maps and prayer lists.')
.replace('{approxLists}', String(approxLists))
.replace('{seriesName}', SERIES_NAME);
// A **...** bold marker is the band's (boldLines); this classic header prints plain, so the markers are dropped.
const plain = (t) => String(t).replace(/\*\*/g, '');
[whatIs, ...whatIsMore()].forEach((text, k) => {
if (k) cy1 += 5;
cy1 = paraInto(push, text, { x: b1x[1], y: cy1, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
cy1 = paraInto(push, plain(text), { x: b1x[1], y: cy1, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
});
const b1Bot = Math.max(cy0, cy1);