From 2e0422f6fa67d0881e8418967dd047d3ccdc712a Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Fri, 2 Oct 2026 22:49:49 +0000 Subject: [PATCH] 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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- library/packet/poster-render.js | 64 +++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/library/packet/poster-render.js b/library/packet/poster-render.js index ac5e30be3..748b2fa66 100644 --- a/library/packet/poster-render.js +++ b/library/packet/poster-render.js @@ -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 += `${esc(head)}`; + frags += `${esc(head)}`; 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 += ``; headY += 25; - headBlock += `` - + `\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` - + ` MATTHEW 9:36` - + ``; + 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 += `${esc(l1)}`; + headY += HC_VERSE_LH; + headBlock += `${esc(l2)} MATTHEW 9:36`; + } else { + headBlock += `` + + VERSE + + ` MATTHEW 9:36` + + ``; + } headY += 12; headBlock += ``; @@ -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 += `Start with Prayer!`; - 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' : ''}
${svg}
`; +${FMT.sideLanes ? SIDE_LANES_CSS + '\n' : ''}${HEAD_COLUMNS_ON_SERIES ? HEAD_COLUMNS_CSS + '\n' : ''}${HEAD_COLUMNS ? HEAD_COLUMNS_FULL_CSS + '\n' : ''}
${svg}
`; return sides ? { html, title, octiOK, crossings, diagFrac, sideLanes: sides.stats } : { html, title, octiOK, crossings, diagFrac }; }