Packet page 1 reads on paper: the full UPG-100 sheet's header band and footer at ~8pt with darker ink, the verse on two lines
Only the full layout (HEAD_COLUMNS, the packet's page 1, printed on letter at 92%): the column text 14 → 22px (~8.3pt), the fact keys and the verse reference 19px (~7pt), the column headings 23px, the footer's three blocks 13.5 → 22px with their lines re-measured; the Matthew 9:36 verse is set on two balanced lines at 19.5px. The band's measure fills its column (0.96), a country list longer than twelve prints ten and '+N' (the Deaf bloc names 66), and the full footer band is 400px. All 28 zones render; each map is 858px (it was 1,206). The series-wide rules are unchanged, so the SPARSE posters are byte-identical.
This commit is contained in:
parent
360e361614
commit
2e0422f6fa
1 changed files with 50 additions and 14 deletions
|
|
@ -282,6 +282,19 @@ const HEAD_COLUMNS_CSS = `
|
|||
.hcolk{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12.5px;fill:#8a8470;font-weight:700;letter-spacing:.14em;text-transform:uppercase}
|
||||
.hcolt{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#5f5849;letter-spacing:.01em}
|
||||
.hsrc{font-size:13.5px}`;
|
||||
// ★ READABLE ON PAPER (2026-10-02): the full sheet is the packet's page 1, printed on letter at 92% (its margins),
|
||||
// where 14px column text came out near 5pt in light grey. The header band and the footer's three blocks are set
|
||||
// at 22 / 21px with darker ink: about 8pt on the printed page. Only the full layout (HEAD_COLUMNS) gets this; the
|
||||
// series-wide rules above stay as they were, so the series' SPARSE posters are byte-identical.
|
||||
// Print size = sheet px x 0.92 (the packet's margins) x 0.408 (1500px = 8.5in): 22px ≈ 8.3pt, 19px ≈ 7.1pt.
|
||||
const HEAD_COLUMNS_FULL_CSS = `
|
||||
.hcolk{font-size:19px;fill:#6b6553}
|
||||
.hcolt{font-size:22px;fill:#3a362c}
|
||||
.hcolh{font-family:Georgia,'Times New Roman',serif;font-size:23px;font-weight:700;fill:#2f2a22;letter-spacing:.04em;font-variant:small-caps}
|
||||
.hvref{font-size:19px}
|
||||
.hverse{font-size:19.5px}
|
||||
.hsrc{font-size:22px;fill:#4a4636}
|
||||
.hsrc-em{fill:#2f2a22}`;
|
||||
const FMT = FORMATS[FMT_KEY];
|
||||
// Map-only sheets (packet page 1) skip the poster's header and footer furniture — see FORMATS.packet.
|
||||
const MAP_ONLY = FMT.mapOnly === true;
|
||||
|
|
@ -357,6 +370,8 @@ const POSTER_TEXT = SERIES_DEF.poster || {};
|
|||
const HEAD_COLUMNS = POSTER_TEXT.headColumns === true && !SPARSE && !MAP_ONLY;
|
||||
// The series-wide part (the footer prose at its measured size) applies to its SPARSE sheets too.
|
||||
const HEAD_COLUMNS_ON_SERIES = POSTER_TEXT.headColumns === true && !MAP_ONLY;
|
||||
// The full sheet's footer band at the larger type (21px blocks): measured on the 28 zones, see HEAD_COLUMNS_FULL_CSS.
|
||||
const FULL_FOOTER = 400;
|
||||
const SERIES_NAME = process.env.SERIES_NAME
|
||||
|| (process.env.CITIES === '1' ? 'Key City Maps'
|
||||
: POSTER_TEXT.seriesName
|
||||
|
|
@ -414,7 +429,7 @@ const HEADER_BASE = SPARSE ? 355 : 460;
|
|||
// no city poster rendered at all. 280 clears it with ~8px of slack for the
|
||||
// per-zone wrapping the block's height depends on. This costs 25px of map height,
|
||||
// which is the documented trade this constant exists to make.
|
||||
const FOOTER = MAP_ONLY ? FMT.footer : SPARSE ? 316 : 280;
|
||||
const FOOTER = MAP_ONLY ? FMT.footer : SPARSE ? 316 : HEAD_COLUMNS ? FULL_FOOTER : 280;
|
||||
// GUTTER was 300px A SIDE — 600 of 1350 (44% of the sheet) given to text, leaving
|
||||
// the map only 750px wide. The names are short and do not need that; the MAP does.
|
||||
// At 210 a side the map grows 750 -> 930 (+24% width, +24% linear scale), which is
|
||||
|
|
@ -815,11 +830,12 @@ function paraInto(push, text, { x, y, w, fs, lh, cls, lead }) {
|
|||
// 1 the facts: a small key above each value (countries, administrative areas, people groups, series)
|
||||
// 2 Jesus' strategy to feed the 5000
|
||||
// 3 what the series is
|
||||
const HC = { gut: 40, fs: 14, lh: 18.5, keyFs: 12.5, keyLh: 17, headLh: 22, gap: 7 };
|
||||
const HC = { gut: 32, fs: 22, lh: 27, keyFs: 19, keyLh: 24, headLh: 30, gap: 8, headFs: 23 };
|
||||
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 TW = (CW - 10 - BAND_TEXT_PAD / 2) * 0.86;
|
||||
// The measure nearly fills the column (its inset is BAND_TEXT_PAD), so the larger type takes fewer lines.
|
||||
const TW = (CW - 10 - BAND_TEXT_PAD / 2) * 0.96;
|
||||
// Uppercase, letterspaced keys: measured as capitals plus the .14em tracking.
|
||||
const keyW = (k) => textW(k.toUpperCase(), HC.keyFs) + k.length * HC.keyFs * 0.14;
|
||||
const cols = [
|
||||
|
|
@ -852,9 +868,9 @@ function headColumnsBand(top, facts, approxLists) {
|
|||
} else {
|
||||
const { head, paras } = col[0];
|
||||
const wrapped = paras.map((t) => wrapPx(t, HC.fs, TW));
|
||||
const blockW = Math.min(TW, Math.max(textW(head, 15), ...wrapped.flat().map((ln) => textW(ln, HC.fs))));
|
||||
const blockW = Math.min(TW, Math.max(textW(head, HC.headFs), ...wrapped.flat().map((ln) => textW(ln, HC.fs))));
|
||||
const x = (centre - blockW / 2).toFixed(1);
|
||||
frags += `<text x="${x}" y="${top}" class="hvh">${esc(head)}</text>`;
|
||||
frags += `<text x="${x}" y="${top}" class="hcolh">${esc(head)}</text>`;
|
||||
let y = top + HC.headLh;
|
||||
wrapped.forEach((lines, k) => {
|
||||
if (k) y += HC.gap;
|
||||
|
|
@ -876,7 +892,9 @@ function headFacts(zone, pins) {
|
|||
...(Array.isArray(zone.adm0) ? zone.adm0.map((c) => ISO3_NAME[c] || c) : []),
|
||||
].filter(Boolean))].sort((a, b) => a.localeCompare(b, 'en-US'));
|
||||
return [
|
||||
['Countries', ctry.join(' \u00b7 ') || '\u2014'],
|
||||
// A list of countries longer than a column can carry (the Deaf bloc names 66) prints its first ten and the
|
||||
// count of the rest, so one sheet's facts do not take its map's height.
|
||||
['Countries', (ctry.length > 12 ? `${ctry.slice(0, 10).join(' \u00b7 ')} \u00b7 +${ctry.length - 10}` : ctry.join(' \u00b7 ')) || '\u2014'],
|
||||
['Administrative Areas', provincesOf(zone).join(' \u00b7 ') || '\u2014'],
|
||||
['People groups', `${pins.length} UPG`],
|
||||
['Series', `${SERIES_NAME} \u00b7 map ${SERIES.index} of ${SERIES.totalLists}`],
|
||||
|
|
@ -884,7 +902,15 @@ function headFacts(zone, pins) {
|
|||
}
|
||||
// The band's first baseline: the title flow (136), the scripture block (16 + 25 + 12), and the 34 the
|
||||
// renderer leaves before its columns. The bottom adds the renderer's 4 + 14.
|
||||
const HC_TOP = 136 + 16 + 25 + 12 + 34;
|
||||
// The verse's second line (HEAD_COLUMNS sets it on two) and the split, by words, nearest the middle of the text.
|
||||
const HC_VERSE_LH = 26;
|
||||
function VERSE_LINES(v) {
|
||||
const w = v.split(' '), half = v.length / 2;
|
||||
let best = 1, d = Infinity, run = 0;
|
||||
for (let i = 0; i < w.length - 1; i++) { run += w[i].length + 1; if (Math.abs(run - half) < d) { d = Math.abs(run - half); best = i + 1 } }
|
||||
return [w.slice(0, best).join(' '), w.slice(best).join(' ')];
|
||||
}
|
||||
const HC_TOP = 136 + 16 + 25 + HC_VERSE_LH + 12 + 34;
|
||||
|
||||
async function renderPoster(zone, pins, zoneFC) {
|
||||
// Per-zone override for the badge, so a zone with unusually crowded or sparse
|
||||
|
|
@ -2117,10 +2143,20 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
headY += 16;
|
||||
headBlock += `<line x1="${VRULE_L}" y1="${headY}" x2="${VRULE_R}" y2="${headY}" class="hrule"/>`;
|
||||
headY += 25;
|
||||
headBlock += `<text x="${CX}" y="${headY}" text-anchor="middle" class="hverse">`
|
||||
+ `\u201cBut when he saw the multitudes, he was moved with compassion for them because they were harassed and scattered, like sheep without a shepherd.\u201d`
|
||||
+ ` <tspan class="hvref">MATTHEW 9:36</tspan>`
|
||||
+ `</text>`;
|
||||
const VERSE = `\u201cBut when he saw the multitudes, he was moved with compassion for them because they were harassed and scattered, like sheep without a shepherd.\u201d`;
|
||||
if (HEAD_COLUMNS) {
|
||||
// At the full sheet's print size the verse is set on TWO balanced lines (one line at a readable size would
|
||||
// run past the band); the reference ends the second line.
|
||||
const [l1, l2] = VERSE_LINES(VERSE);
|
||||
headBlock += `<text x="${CX}" y="${headY}" text-anchor="middle" class="hverse">${esc(l1)}</text>`;
|
||||
headY += HC_VERSE_LH;
|
||||
headBlock += `<text x="${CX}" y="${headY}" text-anchor="middle" class="hverse">${esc(l2)} <tspan class="hvref">MATTHEW 9:36</tspan></text>`;
|
||||
} else {
|
||||
headBlock += `<text x="${CX}" y="${headY}" text-anchor="middle" class="hverse">`
|
||||
+ VERSE
|
||||
+ ` <tspan class="hvref">MATTHEW 9:36</tspan>`
|
||||
+ `</text>`;
|
||||
}
|
||||
headY += 12;
|
||||
headBlock += `<line x1="${VRULE_L}" y1="${headY}" x2="${VRULE_R}" y2="${headY}" class="hrule"/>`;
|
||||
|
||||
|
|
@ -2254,7 +2290,7 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
const GRID_W = PW - 2 * BAND_MARGIN_G;
|
||||
// The type is slightly larger now that there is more room to work with.
|
||||
// 12.5 -> 13.5 with leading kept proportional (~1.30).
|
||||
const FS = 13.5, LH = 17.5;
|
||||
const FS = HEAD_COLUMNS ? 22 : 13.5, LH = HEAD_COLUMNS ? 28 : 17.5;
|
||||
// The two vision columns sit BELOW the ruled scripture, so they read as its
|
||||
// supporting detail, not as headline copy. Slimmer type + tighter leading than
|
||||
// the sheet's body size: less height for the same words, and the contrast in
|
||||
|
|
@ -2375,7 +2411,7 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
// networks answer, so it carries more weight than an ordinary column heading,
|
||||
// and sizing it here does not move every other heading on the sheet.
|
||||
footBlock += `<text x="${CX}" y="${fy}" text-anchor="middle" class="hcta">Start with Prayer!</text>`;
|
||||
fy += SPARSE ? 25 : 22; // clears the larger .hcta line
|
||||
fy += SPARSE ? 25 : HEAD_COLUMNS ? 38 : 22; // clears the larger .hcta line (and, full, the larger blocks below)
|
||||
|
||||
const B2_GUT = 40;
|
||||
const B2_CW = (GRID_W - 2 * B2_GUT) / 3;
|
||||
|
|
@ -2930,7 +2966,7 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
page. Collapse the flow height to the scaled height. */
|
||||
html,body{width:${(FMT.pageWidthIn * 96).toFixed(0)}px;height:${(FMT.pageWidthIn * 96 * FMT.h / FMT.w).toFixed(0)}px;overflow:hidden}
|
||||
}
|
||||
${FMT.sideLanes ? SIDE_LANES_CSS + '\n' : ''}${HEAD_COLUMNS_ON_SERIES ? HEAD_COLUMNS_CSS + '\n' : ''}</style></head><body><div class="stage">${svg}</div></body></html>`;
|
||||
${FMT.sideLanes ? SIDE_LANES_CSS + '\n' : ''}${HEAD_COLUMNS_ON_SERIES ? HEAD_COLUMNS_CSS + '\n' : ''}${HEAD_COLUMNS ? HEAD_COLUMNS_FULL_CSS + '\n' : ''}</style></head><body><div class="stage">${svg}</div></body></html>`;
|
||||
return sides ? { html, title, octiOK, crossings, diagFrac, sideLanes: sides.stats } : { html, title, octiOK, crossings, diagFrac };
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue