Wall of People Groups: nothing on the poster is cut off, and the size list reads the same in Arabic

The chart's rotated labels are sized from their own text and wrap; its theme headers fit their row. A long group name shrinks to fit (never below 6 pt) or takes two lines, and the photo gives way so the card's content never outgrows the card. The verse reference prints at 6 pt. The size labels are isolated left to right, so an RTL page no longer reorders '5 × 8 ft · 3×'.
This commit is contained in:
Doxa Maps 2026-09-28 23:33:54 +00:00
parent 5de01ce9eb
commit a331f1f20e
2 changed files with 45 additions and 16 deletions

View file

@ -108,14 +108,38 @@ export function buildBlessPosterHtml(groups, presetId = DEFAULT_PRESET, texts =
const upright = /^(zh|ja|ko)/.test(String(texts.lang || '')) // CJK sidebar labels stand upright
const glueHere = texts.glueHere || 'glue here'
const f = (v) => v.toFixed(3) + 'in'
const W = plan.contentW
const vPt = plan.text.versePt, nPt = plan.text.namePt
// The reference prints at the verse's size, never below MIN_READABLE_PT (a reference that can't be read
// is noise); it is only printed on sizes whose verses are readable.
const rPt = Math.max(MIN_READABLE_PT, vPt * 0.8)
// A long name shrinks to fit its line (bold ≈ 0.68 em a character, measured wide on purpose), down to MIN_READABLE_PT (or the
// preset's own name size if that is smaller); a name still too long wraps to two lines.
const nameInnerPt = plan.cw * 0.93 / PT
const nameFit = (name) => {
const want = nameInnerPt / (Math.max(1, name.length) * 0.68)
const pt = Math.max(Math.min(nPt, MIN_READABLE_PT), Math.min(nPt, want))
return { pt: +pt.toFixed(1), wrap: want < pt }
}
// The rotated sidebar labels of the chart are sized from their text: the longest word must fit the row's
// height (a CJK label stands upright, one em a character; other scripts ≈ 0.72 em a capital with the
// letter spacing), and the wrapped lines must fit the sidebar's width.
const railH = plan.tableH * 0.33 - W * 0.006, railW = W * 0.06 - W * 0.004
const railFs = (text) => {
const words = String(text).split(/\s+/), em = upright ? 1.02 : 0.72
const longest = Math.max(...words.map((w) => w.length)) * em
const byH = railH / longest, byW = railW / (Math.min(words.length, 3) * 1.2)
return Math.min(W * 0.0105, byH, byW)
}
const railLabel = (text) => `<span style="font-size:${f(railFs(text))}">${esc(text)}</span>`
const card = (c) => {
const g = groups[c.i], v = blessingFor(c.i)
const img = g.imageUrl ? `<img src="${esc(g.imageUrl)}" alt="">` : '<div class="noimg"></div>'
// Below MIN_READABLE_PT the verse would print as grey noise: that size shows name + photo only.
const verse = plan.text.readable ? `<div class="vs" dir="auto">${esc(v.text)}</div><div class="rf" dir="auto">${esc(v.ref)}</div>` : ''
const nf = nameFit(g.name || '')
return `<div class="card" style="left:${f(c.x)};top:${f(c.y)};width:${f(c.w)};height:${f(c.h)}">`
+ `<div class="nm" dir="auto">${esc(g.name || '')}</div>${img}${verse}</div>`
+ `<div class="nm${nf.wrap ? ' w2' : ''}" dir="auto" style="font-size:${nf.pt}pt">${esc(g.name || '')}</div>${img}${verse}</div>`
}
const pages = plan.bands.map((bd) => {
const s = bd.s, last = s === plan.bands.length - 1
@ -127,11 +151,11 @@ export function buildBlessPosterHtml(groups, presetId = DEFAULT_PRESET, texts =
// every scripture, name and reference on the sheet (cards, chart, blessing, closing).
const cell = (v) => `<p class="tv" dir="auto">${esc(v.text)}</p><p class="tr" dir="auto">${esc(v.ref)}</p>`
const table = `<div class="themes" dir="${dir}">`
+ `<div class="rl rl-h"><span>${esc(lab('themesLabel', 'Prayer themes'))}</span></div>`
+ `<div class="rl rl-h">${railLabel(lab('themesLabel', 'Prayer themes'))}</div>`
+ THEMES.map((t) => `<div class="th"><div class="ic">${ICONS[t.id]}</div><div class="tn">${esc(lab('theme_' + t.id, THEME_EN[t.id]))}</div></div>`).join('')
+ `<div class="rl"><span>${esc(lab('oldTestament', 'Old Testament'))}</span></div>`
+ `<div class="rl">${railLabel(lab('oldTestament', 'Old Testament'))}</div>`
+ THEMES.map((t) => `<div class="td">${cell(t.ot)}</div>`).join('')
+ `<div class="rl"><span>${esc(lab('newTestament', 'New Testament'))}</span></div>`
+ `<div class="rl">${railLabel(lab('newTestament', 'New Testament'))}</div>`
+ THEMES.map((t) => `<div class="td">${cell(t.nt)}</div>`).join('')
+ '</div>'
const head = bd.bannerY != null
@ -149,7 +173,6 @@ export function buildBlessPosterHtml(groups, presetId = DEFAULT_PRESET, texts =
return `<section class="sheet">${glue}${head}${plan.cards.filter((c) => c.s === s).map(card).join('')}${foot}${trim}`
+ `<div class="tag">${s + 1} / ${P.sheets}</div></section>`
})
const W = plan.contentW
const css = `@page{size:${P.sheetW}in ${P.sheetH}in;margin:0}
*{box-sizing:border-box}html,body{margin:0;padding:0}
body{font-family:Georgia,'Times New Roman',serif;color:#1c1c1c;-webkit-print-color-adjust:exact;print-color-adjust:exact}
@ -160,11 +183,11 @@ body{font-family:Georgia,'Times New Roman',serif;color:#1c1c1c;-webkit-print-col
.hd .subt{margin:${f(plan.headH * 0.06)} auto 0;max-width:78%;font:${f(plan.headH * 0.085)}/1.35 -apple-system,'Segoe UI',Arial,sans-serif;color:#333}
.themes{display:grid;grid-template-columns:${f(W * 0.06)} repeat(3,1fr);grid-template-rows:34% 33% 33%;height:100%;border:.03in solid #1c1c1c;border-radius:${f(W * 0.006)};overflow:hidden}
.themes > div{border:.015in solid #cfcfcf;padding:${f(W * 0.006)} ${f(W * 0.008)};overflow:hidden}
.rl{display:flex;align-items:center;justify-content:center;background:#1c1c1c;color:#fff}
.rl span{writing-mode:vertical-rl;${upright ? 'text-orientation:upright;' : 'transform:rotate(180deg);'}font:700 ${f(W * 0.0105)} -apple-system,'Segoe UI',Arial,sans-serif;letter-spacing:.06em;text-transform:uppercase;white-space:nowrap}
.th{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:${f(W * 0.004)};background:#faf8f2}
.th .ic{width:${f(W * 0.05)};height:${f(W * 0.05)};color:#6b4f1d}.th .ic svg{width:100%;height:100%}
.th .tn{font:700 ${f(W * 0.014)} Georgia,serif;letter-spacing:.04em;text-transform:uppercase}
.themes > .rl{display:flex;align-items:center;justify-content:center;background:#1c1c1c;color:#fff;padding:${f(W * 0.002)}}
.rl span{writing-mode:vertical-rl;${upright ? 'text-orientation:upright;' : 'transform:rotate(180deg);'}font-weight:700;font-family:-apple-system,'Segoe UI',Arial,sans-serif;line-height:1.15;letter-spacing:.06em;text-transform:uppercase;text-align:center}
.themes > .th{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:${f(W * 0.003)};background:#faf8f2;padding-top:${f(W * 0.003)};padding-bottom:${f(W * 0.003)}}
.th .ic{width:${f(W * 0.028)};height:${f(W * 0.028)};color:#6b4f1d;flex:none}.th .ic svg{width:100%;height:100%}
.th .tn{font:700 ${f(W * 0.012)}/1.1 Georgia,serif;letter-spacing:.04em;text-transform:uppercase}
.td{display:flex;flex-direction:column;justify-content:center;text-align:center}
.td .tv{margin:0;font-size:${f(W * 0.0078)};line-height:1.3;font-style:italic}
.td .tr{margin:${f(W * 0.002)} 0 0;font-size:${f(W * 0.0062)};color:#666}
@ -179,10 +202,13 @@ body{font-family:Georgia,'Times New Roman',serif;color:#1c1c1c;-webkit-print-col
.st.ctr{background:#faf8f2;border:.03in solid #1c1c1c}.st.ctr .sv{font-size:${f(W * 0.0064)};font-weight:600}.st.ctr .sn{color:#1c1c1c}
.close{margin:${f(W * 0.008)} auto 0;max-width:80%;font:700 ${f(W * 0.0085)}/1.35 -apple-system,'Segoe UI',Arial,sans-serif}
.card{position:absolute;padding:${f(plan.cw * 0.035)};display:flex;flex-direction:column;align-items:center;text-align:center;overflow:hidden}
.card .nm{font:700 ${nPt}pt/1.1 -apple-system,'Segoe UI',Arial,sans-serif;width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:none}
.card img,.card .noimg{width:100%;height:${plan.text.readable ? 52 : 78}%;object-fit:cover;margin:${f(plan.cw * 0.02)} 0;background:#eee;flex:none}
.card .nm{font-weight:700;line-height:1.1;font-family:-apple-system,'Segoe UI',Arial,sans-serif;width:100%;white-space:nowrap;overflow:hidden;flex:none}
.card .nm.w2{white-space:normal;overflow-wrap:anywhere;hyphens:auto;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
/* The photo takes whatever height the text leaves, so a card's content never outgrows the card. */
.card img,.card .noimg{width:100%;flex:1 1 0;min-height:0;object-fit:cover;margin:${f(plan.cw * 0.02)} 0;background:#eee}
.card .vs{font-size:${vPt}pt;line-height:1.15;font-style:italic}
.card .rf{font-size:${(vPt * 0.8).toFixed(1)}pt;color:#777;margin-top:1pt}
.card .vs,.card .rf{flex:none}
.card .rf{font-size:${rPt.toFixed(1)}pt;color:#777;margin-top:1pt}
.glue{position:absolute;left:0;width:100%;border-bottom:.02in dashed #b00;display:flex;align-items:center;justify-content:center;font:700 ${f(Math.min(0.5, b * 0.08))} sans-serif;color:#b00;background:rgba(176,0,0,.035);letter-spacing:.05em;text-transform:uppercase}
.trim{position:absolute;left:0;bottom:0;width:100%;border-bottom:.02in solid #999}
.tag{position:absolute;right:${f(Math.max(0.15, b * 0.1))};top:${f(Math.max(0.1, b * 0.08))};font:600 ${f(Math.max(0.12, b * 0.035))} sans-serif;color:#aaa}`

View file

@ -59,7 +59,9 @@ export default {
return {
mode: 'bless', // 'bless' (default) | 'social'
posterPreset: DEFAULT_PRESET, // blessPoster.js PRESETS: 5×8 ft (3 sheets) by default
posterPresets: Object.entries(PRESETS).map(([id, p]) => ({ id, label: p.label + (p.sheets > 1 ? ' · ' + p.sheets + '×' : '') })),
// Each label is isolated left to right (U+2066 … U+2069): "5 × 8 ft · 3×" would otherwise be reordered
// by an RTL page into "×ft · 3 8 × 5" (an <option> cannot hold a <bdi>).
posterPresets: Object.entries(PRESETS).map(([id, p]) => ({ id, label: '\u2066' + p.label + (p.sheets > 1 ? ' · ' + p.sheets + '×' : '') + '\u2069' })),
printing: false, // the HD poster print is loading photos (drives the button's busy state)
cardWidth: 50,
cardHeight: 70,
@ -149,8 +151,9 @@ export default {
subtitle: this.$t('wall.posterSubtitle', { n: groups.length.toLocaleString('en-US') }),
glueHere: this.$t('wall.glueHere'),
// The chart labels and the closing line are translated; the journey's station names print in
// English (blessPoster defaults) until they have keys, and the verses are WEB English in every
// locale for now (sheet row 103).
// English (blessPoster defaults) until they have keys. The scripture (cards, chart, blessing,
// journey) prints in the WEB's English in EVERY locale on purpose, until per-locale Bibles are
// chosen (sheet row 103): a translated label must never sit on an untranslated verse by accident.
labels: {
themesLabel: this.$t('wall.themesLabel'),
oldTestament: this.$t('wall.oldTestament'),