Wall of People Groups: the Bless poster takes the house poster style, full width, with nothing clipped
The poster now reads like the UPG 100 List posters (poster-render's approved template): - warm paper, hairline rules instead of black boxes and sidebars, Georgia for the title and the scripture; - the sans for names and labels, tracked capitals for the small labels, and the purple ink for every scripture reference; - a quiet boxed centrepiece in the journey. The screen canvas draws the same paper and ink, so the wall still shows what prints. Layout: - The header, the chart, the grid and the footer share ONE full content width and ONE inner padding, so text edges and rules line up. The subtitle, the blessing's intro lines and the closing line no longer wrap inside an 80% column. - The grid's spare height is shared equally above and below it (it pooled as one gap above the footer), and a gap now separates the chart from the first row. Fixed, measured on the print in en and ar at every size (0 clipped text blocks, was 12-15): - the Old/New Testament rail labels overran their rows; - the longest verse cell overran its row (the chart's verse size now fits its longest cell); - the title's descenders were clipped; - two real names were cut by the line clamp (0 of the live 2,107 now): a name's lines are counted by whole words, and a long name may take four lines at the floor size.
This commit is contained in:
parent
ce93047669
commit
7f2dec8600
6 changed files with 175 additions and 119 deletions
|
|
@ -38,6 +38,11 @@ export const STRIP_PT = 9 // the one-verse strip under each row, on the
|
|||
// A preset as panels: its sheets stacked top to bottom, each join a blank GLUE band as deep as the border.
|
||||
const panelsOf = (P) => planPanels({ sheetW: P.sheetW, sheetH: P.sheetH, count: P.sheets, direction: 'vertical', joinIn: P.border, borderIn: P.border, joinFill: 'glue' })
|
||||
|
||||
// The poster's colours, shared with the screen canvas (wallCanvas.js) so the wall shows what prints: the UPG 100
|
||||
// List template's paper, ink and purple, with a warm grey under a photo that has not loaded.
|
||||
export const PAPER = '#f7f4ec', INK = '#1c1c17', NAME_INK = '#2A2721', ACCENT = '#6B4A78', PHOTO_BG = '#e9e3d4'
|
||||
export const VERSE_INK = '#3a362c', REF_INK = '#8a8470'
|
||||
|
||||
const CARD_ASPECT = 0.8 // card width / height: a portrait card (name, photo, verse)
|
||||
const PT = 1 / 72 // one point in inches
|
||||
|
||||
|
|
@ -51,14 +56,16 @@ export function planPoster(presetId, n) {
|
|||
const W = P.sheetW - 2 * b
|
||||
// Top of sheet 1: the title + DOXA line, then the prayer-themes table. Bottom of the last sheet: the
|
||||
// priestly blessing, the journey from Abraham to the church and the closing line. All scale with the width.
|
||||
const headH = W * 0.075, tableH = W * 0.17
|
||||
const bannerH = headH + tableH, footerH = footerHeight(W)
|
||||
// gapH: air between the chart and the first row of cards, and between the last row and the footer (the grid
|
||||
// used to butt against the chart's border).
|
||||
const headH = W * 0.075, tableH = W * 0.17, gapH = W * 0.012
|
||||
const bannerH = headH + tableH + gapH, footerH = footerHeight(W)
|
||||
// Each sheet's content band, in the sheet's own coordinates: y0..y1. The panels give each sheet's content box:
|
||||
// sheet 1 starts below its outer border, the others below their glue band; only the last has a bottom border
|
||||
// (the others' bottom edge is trimmed flat and glued over the next sheet's band).
|
||||
const panels = panelsOf(P)
|
||||
const bands = panels.sheets.map(({ s, last, content: { y0, y1 } }) => {
|
||||
const top = s === 0 ? bannerH : 0, bottom = last ? footerH : 0
|
||||
const top = s === 0 ? bannerH : 0, bottom = last ? footerH + gapH : 0
|
||||
return { s, y0: y0 + top, y1: y1 - bottom, bannerY: s === 0 ? y0 : null, footerY: last ? y1 - footerH : null }
|
||||
})
|
||||
// The largest card (fewest columns) whose grid holds n when rows are fitted to each band; `strip` is the
|
||||
|
|
@ -76,12 +83,16 @@ export function planPoster(presetId, n) {
|
|||
const stripLayout = versePtOf(grid.cw) < MIN_READABLE_PT
|
||||
if (stripLayout) grid = fit((STRIP_PT * 1.35 + 4) * PT) // one line at STRIP_PT, plus air
|
||||
const { cols, cw, ch, rowsPer, strip } = grid
|
||||
// Place cards band by band, row by row; in the strip layout each row's strip sits under it.
|
||||
// Place cards band by band, row by row; in the strip layout each row's strip sits under it. The rows a band
|
||||
// actually uses are centred in it, so its spare height is shared equally above and below the grid (it used
|
||||
// to pool as one wide gap above the footer).
|
||||
const cards = [], strips = []
|
||||
let i = 0, row = 0
|
||||
for (let s = 0; s < bands.length && i < n; s++) {
|
||||
const used = Math.min(rowsPer[s], Math.ceil((n - i) / cols))
|
||||
const top = bands[s].y0 + Math.max(0, (bands[s].y1 - bands[s].y0) - used * (ch + strip)) / 2
|
||||
for (let r = 0; r < rowsPer[s] && i < n; r++, row++) {
|
||||
const y = bands[s].y0 + r * (ch + strip)
|
||||
const y = top + r * (ch + strip)
|
||||
for (let c = 0; c < cols && i < n; c++, i++) cards.push({ i, s, x: b + c * cw, y, w: cw, h: ch })
|
||||
if (strip) strips.push({ row, s, x: b, y: y + ch, w: W, h: strip })
|
||||
}
|
||||
|
|
@ -90,7 +101,7 @@ export function planPoster(presetId, n) {
|
|||
return {
|
||||
preset: P, presetId: PRESETS[presetId] ? presetId : DEFAULT_PRESET, n, cols, rowsPer, cw, ch, bands, cards,
|
||||
layout: stripLayout ? 'strip' : 'cards', strips,
|
||||
bannerH, footerH, headH, tableH, contentW: W,
|
||||
bannerH, footerH, headH, tableH, gapH, contentW: W,
|
||||
posterW: panels.finished.w, posterH: panels.finished.h, // after gluing: 60 × 96 for 5x8ft
|
||||
text: { versePt: +versePt.toFixed(1), namePt: +(versePt * 1.25).toFixed(1), readable: !stripLayout, stripPt: STRIP_PT },
|
||||
}
|
||||
|
|
@ -102,18 +113,21 @@ export function planPoster(presetId, n) {
|
|||
// word breaks; the footer is never shallower than a fifth of the width. The same font sizes as the CSS.
|
||||
// The journey's passages are the long text: never below 6 pt (2 × 3 ft), ~12 pt on the 5 × 8 ft poster, which
|
||||
// keeps the 5 × 8's verse under every card at 6 pt (a larger footer would shrink the cards below it).
|
||||
// ONE inner padding for every block (header, chart cells, footer), so all the text shares the same left and
|
||||
// right edges across the full content width.
|
||||
const padOf = (W) => W * 0.008
|
||||
export const FOOTER_FONTS = (W) => ({ small: W * 0.0065, line: W * 0.012, ref: W * 0.0075, name: W * 0.0062, station: Math.max(6 / 72, W * 0.0034), close: W * 0.0085 })
|
||||
function footerHeight(W) {
|
||||
const F = FOOTER_FONTS(W), gap = W * 0.006
|
||||
const lines = (text, width, fs) => Math.max(1, Math.ceil((String(text).length * 0.5 * fs * 1.12) / width))
|
||||
const B = POSTER.blessing
|
||||
const blessH = [...B.intro, ...B.outro].reduce((a, t) => a + lines(t, W * 0.8, F.small) * F.small * 1.35, 0)
|
||||
const blessH = [...B.intro, ...B.outro].reduce((a, t) => a + lines(t, W - 2 * padOf(W), F.small) * F.small * 1.35, 0)
|
||||
+ B.lines.reduce((a, t) => a + lines(t, W, F.line) * F.line * 1.25, 0) + F.ref * 1.6
|
||||
const colW = (W - 6 * gap) / 7 - W * 0.008
|
||||
const stationH = Math.max(...POSTER.journey.map((v) => lines(v.text, colW, F.station) * F.station * 1.3)) + F.name * 1.6 + F.ref * 1.6 + W * 0.006
|
||||
// the closing line's English sizes its row (its translations run within a few words of it); CLOSING_EN is
|
||||
// declared below, which is fine: this runs when a poster is planned, long after the module has loaded
|
||||
const closeH = lines(CLOSING_EN, W * 0.8, F.close) * F.close * 1.35
|
||||
const closeH = lines(CLOSING_EN, W - 2 * padOf(W), F.close) * F.close * 1.35
|
||||
return Math.max(W * 0.2, (blessH + stationH + closeH + W * 0.03) * 1.05)
|
||||
}
|
||||
|
||||
|
|
@ -136,17 +150,22 @@ const ICONS = {
|
|||
* be read is noise); verse and reference are only printed on sizes whose verses are readable;
|
||||
* · a long name shrinks to fit its line (bold ≈ 0.72 em a character: measured wide on purpose, rega found
|
||||
* 0.68 left "Kummara, Kumbara" 2 px over), down to MIN_READABLE_PT (or the preset's own name size if that
|
||||
* is smaller); still too long, it wraps, at the size that fits two lines, or three at the floor.
|
||||
* is smaller); still too long, it wraps, at the size that fits two lines, or more (up to four) at the floor.
|
||||
*/
|
||||
export function cardText(plan, name) {
|
||||
const vPt = plan.text.versePt, nPt = plan.text.namePt
|
||||
const EM = 0.72, inner = plan.cw * 0.93 / PT, len = Math.max(1, String(name).length)
|
||||
const floor = Math.min(nPt, MIN_READABLE_PT), want = inner / (len * EM)
|
||||
const wrap = want < floor
|
||||
// Wrapped: the size that fits two lines (not below the floor), and as many lines (≤ 3) as that size needs.
|
||||
const pt = wrap ? Math.max(floor, Math.min(nPt, (2 * inner) / (len * EM))) : Math.min(nPt, want)
|
||||
// (× 1.2: a line breaks at a word, so it is rarely full; rega measured a 3-line name the plain sum called 2)
|
||||
const lines = wrap ? Math.min(3, Math.ceil((len * EM * pt * 1.2) / inner)) : 1
|
||||
// Wrapped: the size that fits two lines (not below the floor), and as many lines (≤ 4) as that size needs.
|
||||
// A line breaks only between words, so the longest word must fit a line too ("Liechtensteiners" did not, and
|
||||
// "Deaf Liechtensteiners" printed its third line under the clamp).
|
||||
const words = String(name).split(/\s+/).filter(Boolean), longest = Math.max(1, ...words.map((w) => w.length))
|
||||
const pt = wrap ? Math.max(floor, Math.min(nPt, (2 * inner) / (len * EM), inner / (longest * EM))) : Math.min(nPt, want)
|
||||
// Lines counted the way the browser breaks them: whole words, greedily (a word longer than a line is cut), with
|
||||
// 15% slack on the character width (rega measured bold names wider than the plain sum). Up to 4 lines: a long
|
||||
// name at the floor size ("Northern Conchucos Ancash Quechua") needs 4, and the photo gives up the height.
|
||||
const lines = wrap ? Math.min(4, wrapLines(words, inner / (EM * pt * 1.15))) : 1
|
||||
return {
|
||||
readable: plan.text.readable, versePt: vPt, refPt: +Math.max(MIN_READABLE_PT, vPt * 0.8).toFixed(1),
|
||||
name: { pt: Math.floor(pt * 10) / 10, wrap, lines },
|
||||
|
|
@ -154,6 +173,19 @@ export function cardText(plan, name) {
|
|||
}
|
||||
}
|
||||
|
||||
// How many lines a list of words takes at perLine characters a line, broken at spaces (a longer word is cut).
|
||||
function wrapLines(words, perLine) {
|
||||
let lines = 1, cur = 0
|
||||
for (const w of words) {
|
||||
const need = cur ? cur + 1 + w.length : w.length
|
||||
if (need <= perLine) { cur = need; continue }
|
||||
if (cur) lines++
|
||||
cur = w.length
|
||||
while (cur > perLine) { lines++; cur -= Math.floor(perLine) || 1 }
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
||||
|
||||
/**
|
||||
|
|
@ -201,15 +233,22 @@ function posterParts(groups, presetId, texts, withCards = true) {
|
|||
const W = plan.contentW, FF = FOOTER_FONTS(W) // the footer's sizes: the same numbers footerHeight() plans with
|
||||
const vPt = plan.text.versePt, rPt = cardText(plan, '').refPt
|
||||
// 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.
|
||||
// height (a CJK label stands upright, one em a character; other scripts ≈ 0.84 em a capital: 0.72 for the
|
||||
// letter, 0.06 of letter spacing, and slack, since "TESTAMENT" at 0.72 overran its row), 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 words = String(text).split(/\s+/), em = upright ? 1.02 : 0.84
|
||||
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)
|
||||
}
|
||||
// The chart's verse cells: one size for all six, the largest (up to W × 0.0078) at which the longest cell
|
||||
// (verse at line-height 1.3, then its reference) fits its row. Characters ≈ 0.5 em with 15% slack for breaks.
|
||||
const cellW = (W - W * 0.06) / 3 - 2 * W * 0.008, cellH = plan.tableH * 0.33 - 2 * W * 0.006 - W * 0.002 - W * 0.0062 * 1.3
|
||||
const cellFits = (fs) => THEMES.every((t) => [t.ot, t.nt].every((v) => Math.ceil((String(v.text).length * 0.5 * fs * 1.15) / cellW) * fs * 1.3 <= cellH))
|
||||
let tvFs = W * 0.0078
|
||||
while (tvFs > W * 0.005 && !cellFits(tvFs)) tvFs *= 0.97
|
||||
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)
|
||||
|
|
@ -256,46 +295,55 @@ function posterParts(groups, presetId, texts, withCards = true) {
|
|||
return `<div class="vstrip" dir="auto" style="left:${f(t.x)};top:${f(t.y)};width:${f(t.w)};height:${f(t.h)}">${esc(v.text)}<span class="vref">${esc(v.ref)}</span></div>` }).join('')
|
||||
return { cards: withCards ? plan.cards.filter((c) => c.s === s).map(card).join('') : '', chrome: `${marks.before}${head}${strips}${foot}${marks.after}` }
|
||||
})
|
||||
// THE HOUSE POSTER STYLE, from the UPG 100 List template (library/packet/poster-render.js, the approved v5):
|
||||
// warm paper, rules instead of boxes, Georgia for titles and scripture, the sans for labels and names,
|
||||
// tracked capitals for the small labels, and the purple ink for references and emphasis.
|
||||
const PAD = padOf(W)
|
||||
const SANS = "Lato,-apple-system,'Segoe UI',system-ui,Arial,sans-serif"
|
||||
const css = `@page{size:${P.sheetW}in ${P.sheetH}in;margin:0}
|
||||
*{box-sizing:border-box}html,body{margin:0;padding:0}
|
||||
body,.poster{font-family:Georgia,'Times New Roman',serif;color:#1c1c1c;-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
||||
.sheet{position:relative;width:${P.sheetW}in;height:${P.sheetH}in;overflow:hidden;page-break-after:always;break-after:page}
|
||||
body,.poster{font-family:Georgia,'Times New Roman',serif;color:${INK};-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
||||
.sheet{position:relative;width:${P.sheetW}in;height:${P.sheetH}in;overflow:hidden;page-break-after:always;break-after:page;background:${PAPER}}
|
||||
.ban{position:absolute;left:${f(b)};width:${f(W)};text-align:center}
|
||||
.hd{display:flex;flex-direction:column;justify-content:center;align-items:center}
|
||||
.hd h1{margin:0;font-size:${f(plan.headH * 0.44)};line-height:1.05;font-weight:700}
|
||||
.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}
|
||||
.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}
|
||||
.hd{padding:0 ${f(PAD)}}
|
||||
.hd{display:flex;flex-direction:column;justify-content:center;align-items:center;border-bottom:.015in solid #C9BFA4}
|
||||
.hd h1{margin:0;font-size:${f(plan.headH * 0.44)};line-height:1.15;font-weight:700;letter-spacing:-.02em;color:#1c1c17}
|
||||
.hd .subt{margin:${f(plan.headH * 0.06)} 0 0;font:${f(plan.headH * 0.085)}/1.35 ${SANS};color:#7E7562;letter-spacing:.01em}
|
||||
.themes{display:grid;grid-template-columns:${f(W * 0.06)} repeat(3,1fr);grid-template-rows:34% 33% 33%;height:100%;border-bottom:.02in solid #C9BFA4;overflow:hidden}
|
||||
.themes > div{padding:${f(W * 0.006)} ${f(PAD)};overflow:hidden;border-inline-start:.01in solid #D6CCB4}
|
||||
.themes > .rl{display:flex;align-items:center;justify-content:center;color:#8a8470;padding:${f(W * 0.002)};border-inline-start:0}
|
||||
.themes > .td,.themes > .rl:not(.rl-h){border-top:.01in solid #D6CCB4}
|
||||
.rl span{writing-mode:vertical-rl;${upright ? 'text-orientation:upright;' : 'transform:rotate(180deg);'}font-weight:700;font-family:${SANS};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)};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:${ACCENT};flex:none}.th .ic svg{width:100%;height:100%}
|
||||
.th .tn{font:700 ${f(W * 0.012)}/1.1 Georgia,serif;letter-spacing:.02em;color:${ACCENT}}
|
||||
.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}
|
||||
.ftr{position:absolute;left:${f(b)};width:${f(W)};display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;border-top:.03in solid #1c1c1c}
|
||||
.bless p{margin:0;font-size:${f(FF.line)};line-height:1.25;font-weight:600}
|
||||
.bless p.bi{max-width:80%;margin:${f(W * 0.001)} auto;font-size:${f(FF.small)};line-height:1.35;font-weight:400;font-style:italic;color:#444}
|
||||
.bless .br{font-size:${f(FF.ref)};font-weight:400;color:#666;margin-top:${f(W * 0.002)}}
|
||||
.jny{display:grid;grid-template-columns:repeat(7,1fr);gap:${f(W * 0.006)};margin-top:${f(W * 0.01)};width:100%;align-items:stretch}
|
||||
.st{display:flex;flex-direction:column;justify-content:center;padding:${f(W * 0.003)} ${f(W * 0.004)};border-top:.02in solid #c9b58a}
|
||||
.st .sn{margin:0 0 ${f(W * 0.002)};font:700 ${f(FF.name)} Georgia,serif;letter-spacing:.05em;text-transform:uppercase;color:#6b4f1d}
|
||||
.st .sv{margin:0;font-size:${f(FF.station)};font-style:italic;line-height:1.3}
|
||||
.st .sr{margin:${f(W * 0.002)} 0 0;font-size:${f(Math.max(6 / 72, W * 0.0048))};color:#666}
|
||||
.st.ctr{background:#faf8f2;border:.03in solid #1c1c1c}.st.ctr .sv{font-weight:600}.st.ctr .sn{color:#1c1c1c}
|
||||
.close{margin:${f(W * 0.008)} auto 0;max-width:80%;font:700 ${f(FF.close)}/1.35 -apple-system,'Segoe UI',Arial,sans-serif}
|
||||
.td .tv{margin:0;font-size:${f(tvFs)};line-height:1.3;font-style:italic;color:#3a362c}
|
||||
.td .tr{margin:${f(W * 0.002)} 0 0;font:700 ${f(W * 0.0055)} ${SANS};letter-spacing:.12em;text-transform:uppercase;color:${ACCENT}}
|
||||
.ftr{position:absolute;left:${f(b)};width:${f(W)};padding:0 ${f(PAD)};display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;border-top:.02in solid #C9BFA4}
|
||||
.bless p{margin:0;font-size:${f(FF.line)};line-height:1.25;font-weight:700;font-style:italic;color:#2A2721}
|
||||
.bless p.bi{margin:${f(W * 0.001)} 0;font-size:${f(FF.small)};line-height:1.35;font-weight:400;font-style:italic;color:#6a6450}
|
||||
.bless .br{margin-top:${f(W * 0.002)};font:700 ${f(FF.ref)} ${SANS};letter-spacing:.14em;text-transform:uppercase;color:${ACCENT};font-style:normal}
|
||||
/* Rules run the full content width, like the chart's; the padding belongs to the text. */
|
||||
.jny{display:grid;grid-template-columns:repeat(7,1fr);gap:${f(W * 0.006)};margin:${f(W * 0.01)} ${f(-PAD)} 0;width:${f(W)};align-items:stretch;border-top:.01in solid #D6CCB4;padding-top:${f(W * 0.004)}}
|
||||
.st{display:flex;flex-direction:column;justify-content:flex-start;padding:${f(W * 0.003)} ${f(W * 0.004)}}
|
||||
.st + .st{border-inline-start:.01in solid #D6CCB4}
|
||||
.st .sn{margin:0 0 ${f(W * 0.002)};font:700 ${f(FF.name)} ${SANS};letter-spacing:.08em;text-transform:uppercase;color:#8a8470}
|
||||
.st .sv{margin:0;font-size:${f(FF.station)};font-style:italic;line-height:1.3;color:#4a4636}
|
||||
.st .sr{margin:${f(W * 0.002)} 0 0;font:700 ${f(Math.max(6 / 72, W * 0.0048))} ${SANS};letter-spacing:.1em;text-transform:uppercase;color:${ACCENT}}
|
||||
.st.ctr{background:#F2EFE6;border:.02in solid #cdbf9e}.st.ctr + .st{border-inline-start:0}.st.ctr .sv{font-weight:600;color:#2A2721}.st.ctr .sn{color:${ACCENT}}
|
||||
.close{margin:${f(W * 0.008)} 0 0;font:italic 700 ${f(FF.close)}/1.35 Georgia,'Times New Roman',serif;color:#2A2721}
|
||||
.vstrip{position:absolute;display:flex;align-items:center;justify-content:center;gap:.6em;font-size:${STRIP_PT}pt;font-style:italic;
|
||||
color:#2d2d2d;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-top:.01in solid #e6e6e6}
|
||||
.vstrip .vref{font-style:normal;color:#777;font-size:.85em}
|
||||
color:#3a362c;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-top:.01in solid #D6CCB4}
|
||||
.vstrip .vref{font:700 .8em ${SANS};letter-spacing:.12em;text-transform:uppercase;color:${ACCENT}}
|
||||
.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-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{font-weight:700;line-height:1.1;font-family:${SANS};color:${NAME_INK};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 img,.card .noimg{width:100%;flex:1 1 0;min-height:0;object-fit:cover;margin:${f(plan.cw * 0.02)} 0;background:${PHOTO_BG}}
|
||||
.card .vs{font-size:${vPt}pt;line-height:1.15;font-style:italic;color:${VERSE_INK}}
|
||||
.card .vs,.card .rf{flex:none}
|
||||
.card .rf{font-size:${rPt.toFixed(1)}pt;color:#777;margin-top:1pt}
|
||||
.card .rf{font-size:${rPt.toFixed(1)}pt;font-family:${SANS};color:${REF_INK};margin-top:1pt}
|
||||
${panelCss(panels)}`
|
||||
return { plan, css, pages, banner }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@
|
|||
import { wallD3 } from './wallD3.js'
|
||||
import { wallGrid } from './wallLod.js'
|
||||
import { blessingFor, wrapText } from './blessings.js'
|
||||
import { cardText } from './blessPoster.js'
|
||||
import { cardText, PAPER, NAME_INK, VERSE_INK, REF_INK, PHOTO_BG } from './blessPoster.js'
|
||||
|
||||
// The print's sans (blessPoster SANS), for the names and references drawn on the canvas.
|
||||
const SANS = 'Lato, -apple-system, "Segoe UI", system-ui, Arial, sans-serif'
|
||||
|
||||
// 'rtl' when a string's first strong character is Hebrew/Arabic-script, else 'ltr' (canvas has no dir="auto").
|
||||
const RTL_FIRST = /^[^A-Za-z\u00C0-\u024F\u0370-\u052F\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]*[\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]/
|
||||
|
|
@ -121,9 +124,10 @@ export class WallCanvas {
|
|||
this.host.appendChild(this.ov)
|
||||
this.ovRoot = this.ov.attachShadow ? this.ov.attachShadow({ mode: 'open' }) : this.ov
|
||||
}
|
||||
// The print CSS (its @page rule is ignored on screen), scoped by the shadow root. The overlay starts at
|
||||
// The print CSS (its @page rule is ignored on screen), scoped by the shadow root. The overlay's sheets are
|
||||
// transparent: the canvas under them paints the paper and the cards (the print's paper colour would hide them). The overlay starts at
|
||||
// scale(0) and takes the zoom's transform on the next frame, so it never flashes at full print size.
|
||||
this.ovRoot.innerHTML = `<style>${sp.css}\n.ovw{position:absolute;left:0;top:0;transform-origin:0 0;pointer-events:none}.ovw a{pointer-events:auto}.ovw .sheet{content-visibility:auto}</style>`
|
||||
this.ovRoot.innerHTML = `<style>${sp.css}\n.ovw{position:absolute;left:0;top:0;transform-origin:0 0;pointer-events:none}.ovw a{pointer-events:auto}.ovw .sheet{content-visibility:auto;background:none}</style>`
|
||||
+ `<div class="ovw poster" style="width:${sp.worldW}px;height:${sp.worldH}px;transform:scale(0)">${sp.chromeHtml}</div>`
|
||||
this.ovw = this.ovRoot.querySelector('.ovw')
|
||||
this.showPoster(); this.resize()
|
||||
|
|
@ -232,7 +236,7 @@ export class WallCanvas {
|
|||
ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.fillStyle = '#d4d4d4'; ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); ctx.restore()
|
||||
ctx.fillStyle = 'rgba(0,0,0,.16)'
|
||||
for (const R of sp.sheets) ctx.fillRect(R.x + 3 / s, R.y + 3 / s, R.w, R.h)
|
||||
ctx.fillStyle = '#fff'
|
||||
ctx.fillStyle = PAPER
|
||||
for (const R of sp.sheets) ctx.fillRect(R.x, R.y, R.w, R.h)
|
||||
const [x0, y0] = this.toWorld(0, 0), [x1, y1] = this.toWorld(this.cssW, this.cssH)
|
||||
const cardPx = (sp.cards[0]?.w || 0) * s, want = []
|
||||
|
|
@ -241,7 +245,7 @@ export class WallCanvas {
|
|||
for (const R of sp.cards) {
|
||||
if (R.x + R.w < x0 || R.x > x1 || R.y + R.h < y0 || R.y > y1) continue
|
||||
if (R.i >= this.groups.length) continue
|
||||
if (cardPx < RECT_PX) { ctx.fillStyle = '#e6e6e6'; ctx.fillRect(R.x + R.w * 0.04, R.y + R.h * 0.04, R.w * 0.92, R.h * 0.92); continue }
|
||||
if (cardPx < RECT_PX) { ctx.fillStyle = PHOTO_BG; ctx.fillRect(R.x + R.w * 0.04, R.y + R.h * 0.04, R.w * 0.92, R.h * 0.92); continue }
|
||||
this.drawPrintCard(ctx, R, cardPx, want)
|
||||
}
|
||||
if (want.length) this.load(want)
|
||||
|
|
@ -271,7 +275,7 @@ export class WallCanvas {
|
|||
} else if (!T) {
|
||||
this.measureBudget--; this.stats.measured++
|
||||
// Line breaks measured once per card (the fonts are the print's), then cached.
|
||||
ctx.font = `700 ${nameFs}px -apple-system, "Segoe UI", Arial, sans-serif`
|
||||
ctx.font = `700 ${nameFs}px ${SANS}`
|
||||
const nameLines = ct.name.wrap ? this.breakLines(ctx, name, iw, ct.name.lines) : [name]
|
||||
let verse = null
|
||||
if (ct.readable) {
|
||||
|
|
@ -293,21 +297,21 @@ export class WallCanvas {
|
|||
if (ar > box) { sw = sh * box; sx = (pw(im) - sw) / 2 } else { sh = sw / box; sy = (ph(im) - sh) / 2 }
|
||||
ctx.drawImage(im, sx, sy, sw, sh, x, imgY, iw, imgH) // object-fit: cover
|
||||
} else {
|
||||
ctx.fillStyle = '#eee'; ctx.fillRect(x, imgY, iw, imgH)
|
||||
ctx.fillStyle = PHOTO_BG; ctx.fillRect(x, imgY, iw, imgH)
|
||||
if (url && cardPx >= IMG_PX) want.push(url)
|
||||
}
|
||||
if (cardPx < TEXT_PX || T.estimate) return // no text yet (below text zoom, or waiting its turn to be measured)
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'top'
|
||||
ctx.font = `700 ${nameFs}px -apple-system, "Segoe UI", Arial, sans-serif`
|
||||
ctx.fillStyle = '#1c1c1c' // as printed; a linked name shows the pointer cursor instead of link blue
|
||||
ctx.font = `700 ${nameFs}px ${SANS}`
|
||||
ctx.fillStyle = NAME_INK // as printed; a linked name shows the pointer cursor instead of link blue
|
||||
ctx.direction = textDir(name)
|
||||
T.nameLines.forEach((ln, k) => ctx.fillText(ln, cx, R.y + pad + k * nameFs * ct.nameLine, iw))
|
||||
if (T.verse) {
|
||||
let y = imgY + imgH + m
|
||||
ctx.font = `italic ${vFs}px Georgia, "Times New Roman", serif`; ctx.fillStyle = '#1c1c1c'
|
||||
ctx.font = `italic ${vFs}px Georgia, "Times New Roman", serif`; ctx.fillStyle = VERSE_INK
|
||||
ctx.direction = textDir(T.verse.lines[0])
|
||||
for (const ln of T.verse.lines) { ctx.fillText(ln, cx, y, iw); y += vFs * ct.verseLine }
|
||||
ctx.font = `${rFs}px Georgia, "Times New Roman", serif`; ctx.fillStyle = '#777'
|
||||
ctx.font = `${rFs}px ${SANS}`; ctx.fillStyle = REF_INK
|
||||
ctx.direction = textDir(T.verse.ref)
|
||||
ctx.fillText(T.verse.ref, cx, y + ct.refGapPt * pt, iw)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1473,7 +1473,7 @@
|
|||
"bundle": "wall-of-people-groups",
|
||||
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
|
||||
"output": "dx-maps/wall-of-people-groups/wall-of-people-groups.js",
|
||||
"integrity": "sha384-+0FF2Yam1UsHfo2turc9M9IORYEfg/eKrOjl3VLUymNUK7P8/AnL8/p9zlrf/NEh",
|
||||
"integrity": "sha384-EQcPfsC0SN3qR+HvILq9491mHiDQSWOfhK0SVrvNSRaZzQ8hqYlZiy5/fmaiJKNF",
|
||||
"elements": [
|
||||
"wall-of-people-groups"
|
||||
],
|
||||
|
|
@ -1492,7 +1492,7 @@
|
|||
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./wall-of-people-groups/wall-of-people-groups/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-+0FF2Yam1UsHfo2turc9M9IORYEfg/eKrOjl3VLUymNUK7P8/AnL8/p9zlrf/NEh\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-EQcPfsC0SN3qR+HvILq9491mHiDQSWOfhK0SVrvNSRaZzQ8hqYlZiy5/fmaiJKNF\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
|
||||
},
|
||||
"bundle": "wall-of-people-groups"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1473,7 +1473,7 @@
|
|||
"bundle": "wall-of-people-groups",
|
||||
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
|
||||
"output": "dx-maps/wall-of-people-groups/wall-of-people-groups.js",
|
||||
"integrity": "sha384-+0FF2Yam1UsHfo2turc9M9IORYEfg/eKrOjl3VLUymNUK7P8/AnL8/p9zlrf/NEh",
|
||||
"integrity": "sha384-EQcPfsC0SN3qR+HvILq9491mHiDQSWOfhK0SVrvNSRaZzQ8hqYlZiy5/fmaiJKNF",
|
||||
"elements": [
|
||||
"wall-of-people-groups"
|
||||
],
|
||||
|
|
@ -1492,7 +1492,7 @@
|
|||
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./wall-of-people-groups/wall-of-people-groups/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-+0FF2Yam1UsHfo2turc9M9IORYEfg/eKrOjl3VLUymNUK7P8/AnL8/p9zlrf/NEh\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-EQcPfsC0SN3qR+HvILq9491mHiDQSWOfhK0SVrvNSRaZzQ8hqYlZiy5/fmaiJKNF\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
|
||||
},
|
||||
"bundle": "wall-of-people-groups"
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <wall-of-people-groups> custom element. -->
|
||||
<script src="../wall-of-people-groups.js" integrity="sha384-+0FF2Yam1UsHfo2turc9M9IORYEfg/eKrOjl3VLUymNUK7P8/AnL8/p9zlrf/NEh" crossorigin="anonymous"></script>
|
||||
<script src="../wall-of-people-groups.js" integrity="sha384-EQcPfsC0SN3qR+HvILq9491mHiDQSWOfhK0SVrvNSRaZzQ8hqYlZiy5/fmaiJKNF" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue