Merge remote-tracking branch 'origin/development' into poster-design-plugin/land
This commit is contained in:
commit
3f7d489409
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1537,7 +1537,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"
|
||||
],
|
||||
|
|
@ -1556,7 +1556,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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1537,7 +1537,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"
|
||||
],
|
||||
|
|
@ -1556,7 +1556,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