Wall of People Groups: in Bless mode, Print poster prints all the people groups as one 8 by 5 foot poster on three overlapping sheets
This commit is contained in:
parent
f4c3767784
commit
3f3494eac9
2 changed files with 96 additions and 1 deletions
79
plugin-designs/wall-of-people-groups/src/blessPoster.js
Normal file
79
plugin-designs/wall-of-people-groups/src/blessPoster.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* blessPoster.js — the "Bless the nations in Jesus' name" poster: every people group (photo, name and a
|
||||
* blessing verse) on ONE 8 × 5 ft poster, printed as THREE 3 × 5 ft sheets that overlap by 6 inches.
|
||||
*
|
||||
* 3 + 2.5 + 2.5 = 8 ft wide, 5 ft tall: sheet 2 is glued over the right-hand 6 in of sheet 1, and sheet 3
|
||||
* over the right-hand 6 in of sheet 2. Each sheet is a 36-inch WINDOW onto one 96 × 60 in layout, so the
|
||||
* content in an overlap strip is identical on both sheets and registers when glued.
|
||||
*
|
||||
* Pure: layout maths + an HTML string for the shared in-place print frame (library printInPlace). No Vue.
|
||||
* Only the cards that touch a sheet are placed on it, so the print document holds ~2,300 cards in total,
|
||||
* not 3 × 2,107.
|
||||
*/
|
||||
import { blessingFor, POSTER } from './blessings.js'
|
||||
|
||||
export const SHEET = { w: 36, h: 60, overlap: 6, count: 3 } // inches
|
||||
export const POSTER_W = SHEET.w * SHEET.count - SHEET.overlap * (SHEET.count - 1) // 96
|
||||
export const POSTER_H = SHEET.h // 60
|
||||
|
||||
const MARGIN = 0.5, BANNER_H = 5.2, FOOTER_H = 3
|
||||
|
||||
/** Where each of n cards goes, in inches on the 96 × 60 poster. */
|
||||
export function posterLayout(n, { aspect = 1.18 } = {}) {
|
||||
const gx = MARGIN, gy = MARGIN + BANNER_H, gw = POSTER_W - 2 * MARGIN, gh = POSTER_H - 2 * MARGIN - BANNER_H - FOOTER_H
|
||||
// Columns so the cells come out near the target width/height aspect, then enough rows for n.
|
||||
let cols = Math.max(1, Math.round(Math.sqrt((n * gw) / (gh * aspect))))
|
||||
let rows = Math.ceil(n / cols)
|
||||
while (rows * (gw / cols / aspect) > gh && cols < n) { cols++; rows = Math.ceil(n / cols) }
|
||||
const cw = gw / cols, ch = Math.min(gh / rows, cw / aspect * 1.25)
|
||||
const cards = []
|
||||
for (let i = 0; i < n; i++) cards.push({ i, x: gx + (i % cols) * cw, y: gy + Math.floor(i / cols) * ch, w: cw, h: ch })
|
||||
return { cols, rows, cw, ch, grid: { x: gx, y: gy, w: gw, h: gh }, cards }
|
||||
}
|
||||
|
||||
/** The x-window (in poster inches) that sheet s prints. */
|
||||
export const sheetX = (s) => s * (SHEET.w - SHEET.overlap)
|
||||
|
||||
const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
||||
|
||||
/** The full print document: 3 pages of 36 × 60 in. groups: [{ name, imageUrl }]. */
|
||||
export function buildBlessPosterHtml(groups, { footer = POSTER.footer, banner = POSTER.banner, subBanner = POSTER.subBanner } = {}) {
|
||||
const L = posterLayout(groups.length)
|
||||
const f = (v) => v.toFixed(3) + 'in'
|
||||
const card = (c) => {
|
||||
const g = groups[c.i], b = blessingFor(c.i)
|
||||
const img = g.imageUrl ? `<img src="${esc(g.imageUrl)}" alt="">` : '<div class="noimg"></div>'
|
||||
return `<div class="card" style="left:${f(c.x)};top:${f(c.y)};width:${f(c.w)};height:${f(c.h)}">`
|
||||
+ `<div class="nm">${esc(g.name || '')}</div>${img}<div class="vs">${esc(b.text)}</div><div class="rf">${esc(b.ref)}</div></div>`
|
||||
}
|
||||
const sheets = []
|
||||
for (let s = 0; s < SHEET.count; s++) {
|
||||
const x0 = sheetX(s), x1 = x0 + SHEET.w
|
||||
const onSheet = L.cards.filter((c) => c.x < x1 && c.x + c.w > x0)
|
||||
const glue = s < SHEET.count - 1
|
||||
? `<div class="glue" style="left:${f(x1 - SHEET.overlap)}">▶ ${s + 2} / ${SHEET.count}</div>` : ''
|
||||
sheets.push(`<section class="sheet"><div class="poster" style="left:${f(-x0)}">`
|
||||
+ `<header class="ban"><h1>${esc(banner)}</h1><p>${esc(subBanner.text)} <span>${esc(subBanner.ref)}</span></p></header>`
|
||||
+ onSheet.map(card).join('')
|
||||
+ `<footer class="ftr"><p>${esc(footer.text)} <span>${esc(footer.ref)}</span></p></footer>`
|
||||
+ glue + `</div><div class="tag">${s + 1} / ${SHEET.count}</div></section>`)
|
||||
}
|
||||
const css = `@page{size:${SHEET.w}in ${SHEET.h}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}
|
||||
.sheet{position:relative;width:${SHEET.w}in;height:${SHEET.h}in;overflow:hidden;page-break-after:always;break-after:page}
|
||||
.poster{position:absolute;top:0;width:${POSTER_W}in;height:${POSTER_H}in}
|
||||
.ban{position:absolute;left:${MARGIN}in;top:${MARGIN}in;width:${POSTER_W - 2 * MARGIN}in;height:${BANNER_H}in;text-align:center}
|
||||
.ban h1{margin:0;font-size:2.35in;line-height:1.05;letter-spacing:.02em;font-weight:700}
|
||||
.ban p{margin:.25in 0 0;font-size:.62in;font-style:italic;color:#444}.ban p span,.ftr p span{font-style:normal;font-size:.7em;color:#777}
|
||||
.card{position:absolute;padding:.04in;display:flex;flex-direction:column;align-items:center;text-align:center;overflow:hidden}
|
||||
.card .nm{font:700 .105in/1.1 -apple-system,'Segoe UI',Arial,sans-serif;width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.card img,.card .noimg{width:100%;height:58%;object-fit:cover;margin:.02in 0;background:#eee;flex:none}
|
||||
.card .vs{font-size:.068in;line-height:1.15;font-style:italic}
|
||||
.card .rf{font-size:.056in;color:#777;margin-top:.01in}
|
||||
.ftr{position:absolute;left:${MARGIN}in;bottom:${MARGIN}in;width:${POSTER_W - 2 * MARGIN}in;height:${FOOTER_H - 0.4}in;display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.ftr p{margin:0;font-size:.95in;font-style:italic}
|
||||
.glue{position:absolute;top:0;width:${SHEET.overlap}in;height:${POSTER_H}in;border-left:.02in dashed #b00;display:flex;align-items:flex-end;justify-content:center;padding-bottom:.2in;font:700 .3in sans-serif;color:#b00;background:rgba(176,0,0,.03)}
|
||||
.tag{position:absolute;right:.15in;bottom:.1in;font:600 .18in sans-serif;color:#999}`
|
||||
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc(banner)}</title><style>${css}</style></head><body>${sheets.join('')}</body></html>`
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
</button>
|
||||
<!-- Loading every card's HD photo can take a while: the button shows it is busy and cannot
|
||||
start a second load while the first is still running. -->
|
||||
<button @click="printWall" class="control-btn" :class="{ busy: printing }" :disabled="printing" :aria-busy="printing ? 'true' : 'false'">{{ $t('wall.printPoster') }}</button>
|
||||
<button @click="onPrint" class="control-btn" :class="{ busy: printing }" :disabled="printing" :aria-busy="printing ? 'true' : 'false'">{{ $t('wall.printPoster') }}</button>
|
||||
<button @click="resetZoom" class="control-btn">{{ $t('wall.resetZoom') }}</button>
|
||||
<!-- BLESS (default): name, photo and a blessing verse under every group, made to print.
|
||||
SOCIAL: the interactive card (pray, comments, share). -->
|
||||
|
|
@ -44,6 +44,8 @@
|
|||
import { lodPlan } from '../wallLod.js'
|
||||
import { wallD3 } from '../wallD3.js'
|
||||
import { blessingFor, wrapText } from '../blessings.js'
|
||||
import { buildBlessPosterHtml } from '../blessPoster.js'
|
||||
import { openPrintFrame } from '@map/composables/printInPlace.js'
|
||||
// Wall of People Groups - D3 Zoomable Whiteboard
|
||||
export default {
|
||||
name: 'PeopleGroupsWall',
|
||||
|
|
@ -472,6 +474,20 @@ export default {
|
|||
|
||||
},
|
||||
|
||||
// BLESS prints the 8 × 5 ft poster (3 sheets of 3 × 5 ft, 6-inch overlaps: blessPoster.js);
|
||||
// SOCIAL keeps the HD print of the wall as it is on screen.
|
||||
onPrint() { return this.mode === 'bless' ? this.printBlessPoster() : this.printWall(); },
|
||||
printBlessPoster() {
|
||||
if (this.printing) return;
|
||||
const groups = (this.peopleGroups || []).map((d) => ({ name: d.name || d.post_title || '', imageUrl: d.imageUrl || '' }));
|
||||
if (!groups.length) return;
|
||||
this.printing = true;
|
||||
const win = openPrintFrame(buildBlessPosterHtml(groups), { autoprint: true });
|
||||
// Busy until the photos have loaded and the print dialog is up (the frame prints on its own 'load').
|
||||
const done = () => { this.printing = false; };
|
||||
if (win) { try { win.addEventListener('load', () => setTimeout(done, 300), { once: true }); } catch (_) { done(); } setTimeout(done, 120000); }
|
||||
else done();
|
||||
},
|
||||
setMode(m) {
|
||||
if (this.mode === m) return;
|
||||
this.mode = m;
|
||||
|
|
|
|||
Loading…
Reference in a new issue