Wall of People Groups: the poster prints from 20 MB of photos instead of 133 MB, and says how far along it is
The print waits for every card's photo before the print dialog can open. 77 of the 939 photos are IMB originals at 1024 px (~1.5 MB each), 114 MB of the 133 MB: on a home connection a minute or more with nothing on screen but a busy button. - Photos load at the size they print (photoUrl.js): the IMB host's own 320 px copies (1.3 MB for all 77). 320 px covers the largest printed photo (5 x 8 ft, ~0.97 in, 291 px at 300 dpi) and the canvas's cap. A resized copy that fails falls back to the original, in print and on screen. Every other photo (Joshua Project, doxa, already 150-250 px) is untouched. - Measured on the print flow, 3 x 4 ft: 133 MB -> 19 MB; 5 x 8 ft: 20 MB; 0 photos failed. - While it prepares, the shared print card (MapPrintProgress) shows "Preparing the poster: n of N photos" with a clock and a bar, in the middle of the screen. check:wall-photo-weight (gates:pre, headless) pins the mapping on both IMB hosts, the fallback, the untouched hosts, and that the print HTML and the canvas both use it. It fails when either reverts to the originals.
This commit is contained in:
parent
28cb482c79
commit
6bc7d70bf3
10 changed files with 170 additions and 38 deletions
|
|
@ -347,7 +347,7 @@
|
|||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:wall-photo-weight check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
@ -402,6 +402,7 @@
|
|||
"check:wall-print-watch": "node plugin-designs/wall-of-people-groups/scripts/check-wall-print-watch.mjs",
|
||||
"check:wall-no-bg-photos": "node plugin-designs/wall-of-people-groups/scripts/check-wall-no-bg-photos.mjs",
|
||||
"check:wall-pan": "node plugin-designs/wall-of-people-groups/scripts/check-wall-pan.mjs",
|
||||
"check:wall-photo-weight": "node plugin-designs/wall-of-people-groups/scripts/check-wall-photo-weight.mjs",
|
||||
"check:template-i18n": "node plugin-designs/template-bundle/scripts/check-template-i18n.mjs",
|
||||
"check:snippets-draw": "node internal/check-snippets-draw.mjs",
|
||||
"check:snippets-draw:mapbox": "node internal/check-snippets-draw.mjs --mapbox",
|
||||
|
|
@ -427,6 +428,7 @@
|
|||
"check:wall-print-watch": "the Wall's HD poster print never prints silently incomplete: with fake timers and a fake frame, the watch counts photos as they load, counts the ones that could not, and reports both. Two-sided on planted printWatch copies. Headless.",
|
||||
"check:wall-no-bg-photos": "the Wall's store downloads no photos on its own (it used to test all 932, 132 MB, on every visit): the REAL store runs through Vite against the committed fixtures/pray-list-sample.json with fetch stubbed and every Image request counted. Two-sided with the old background test planted. Headless, no network.",
|
||||
"check:wall-pan": "the Wall pans and zooms with the pointer 1:1: the REAL WallCanvas methods (through Vite) with d3-zoom's transform arithmetic, over 12 fits and zooms: a pan moves every card exactly as far as the pointer, a zoom keeps the point under the pointer, toWorld inverts toScreen, and a fling never loses the wall. Two-sided: the old transform-inside-the-fit formulas are planted and caught. Headless.",
|
||||
"check:wall-photo-weight": "the Wall's poster loads its photos at print size: the 77 IMB photos (1024 px, ~1.5 MB each, 114 MB of the print's 133 MB) map to their host's 320 px copies on either host, with the original as the onerror fallback, every other photo untouched, and the print HTML and the screen canvas both go through photoUrl.js. Two-sided (the old full-size URLs are caught). Headless.",
|
||||
"check:template-i18n": "the starter template every new plugin copies renders its text from the catalogue: profile B, server-rendered with the REAL template.json in en, ar and zh, takes its heading, note and sentence from the page's language. Two-sided with the English catalogue served as Arabic. Headless (SSR).",
|
||||
"check:coverage-consistent:chained": "every clause of check:coverage-consistent EXCEPT the link-verification age (VERIFY_MAX_AGE_DAYS set out of reach): the chains run what depends on the code and data, and leave what depends on the calendar to the on-demand run, so the gates cannot turn red on a date with no change.",
|
||||
"check:viewport-meta": "every generated page declares <meta name=\"viewport\"> with width=device-width. Without it a browser lays the page out at the 980px desktop fallback even when the viewport is set to 390, so a mobile probe silently measures a viewport no phone has and still passes (measured: innerWidth 980 without the tag, 390 with it). width=device-width is required specifically, because a meta carrying only initial-scale still lays out at 980. Checked on the BUILT tree, not the generator template. Fixed-size print sheets are exempt by their stated shape (a .stage with a three-digit px width), and a page that must render at a fixed desktop width carries a viewport-meta-ok: reason. 8 self-tests. Source-only, headless.",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-wall-photo-weight — the Wall's poster loads its photos at the size it prints them. Headless.
|
||||
*
|
||||
* The 77 IMB photos (peoplegroupspublic.blob.core.windows.net, images.peoplegroups.org) are 1024 px originals of
|
||||
* ~1.5 MB: 114 MB of the print's 133 MB, measured 2026-09-30, a minute or more before the print dialog can open
|
||||
* on a home connection. photoUrl.js loads a 320 px copy from the host's own resizer instead (19-20 MB in all).
|
||||
* This pins, on the REAL modules:
|
||||
* - an IMB photo on EITHER host maps to images.peoplegroups.org/cdn-cgi/image/width=320 for the same file;
|
||||
* - every other photo (Joshua Project, doxa) is untouched, and a non-URL stays as it is;
|
||||
* - fullPhoto() gives the original back for a resized URL, and null for anything else;
|
||||
* - the PRINT HTML (buildBlessPosterHtml) uses the resized copy, with the original as its onerror fallback,
|
||||
* and leaves the other photos alone; the screen canvas loads through the same lightPhoto().
|
||||
* Two-sided: lightPhoto replaced by the identity (the old behaviour) must fail the mapping and the print arms.
|
||||
* EXIT 0 pass · 1 fail
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../src')
|
||||
const P = await import(pathToFileURL(join(DIR, 'photoUrl.js')).href)
|
||||
const B = await import(pathToFileURL(join(DIR, 'blessPoster.js')).href)
|
||||
const canvasSrc = readFileSync(join(DIR, 'wallCanvas.js'), 'utf8')
|
||||
|
||||
const IMB = ['https://peoplegroupspublic.blob.core.windows.net/imbpeople-featured-images/PG016542.jpg',
|
||||
'https://images.peoplegroups.org/imbpeople-featured-images/PG023972.jpg']
|
||||
const OTHER = ['https://media.joshuaproject.net/public/assets/media/profiles/photos/p10120.jpg',
|
||||
'https://joshuaproject.net/assets/media/profiles/photos/p20298.jpg', 'https://s3.doxa.life/no-photo-images/asia.jpg']
|
||||
const WANT = (file) => `https://images.peoplegroups.org/cdn-cgi/image/width=320,format=jpeg/imbpeople-featured-images/${file}`
|
||||
|
||||
function run(light, full) {
|
||||
const errs = []
|
||||
for (const u of IMB) {
|
||||
const file = u.split('/').pop()
|
||||
if (light(u) !== WANT(file)) errs.push(`map: ${u} → ${light(u)}, want the 320 px copy`)
|
||||
if (full(light(u)) !== `https://images.peoplegroups.org/imbpeople-featured-images/${file}`) errs.push(`full: no original for ${u}`)
|
||||
}
|
||||
for (const u of OTHER) { if (light(u) !== u) errs.push(`map: ${u} was rewritten`); if (full(u) !== null) errs.push(`full: ${u} is not a resized URL`) }
|
||||
if (light('') !== '' || light('not a url') !== 'not a url') errs.push('map: a non-URL was changed')
|
||||
// the print HTML, through the real engine, with this light()
|
||||
const groups = [{ name: 'A', imageUrl: IMB[0] }, { name: 'B', imageUrl: OTHER[0] }, { name: 'C', imageUrl: '' }]
|
||||
const html = B.buildBlessPosterHtml(groups, '3x4ft', {})
|
||||
const imgs = [...html.matchAll(/<img src="([^"]+)"([^>]*)>/g)]
|
||||
if (imgs.length !== 2) errs.push(`print: ${imgs.length} photos in the document, want 2`)
|
||||
else {
|
||||
if (imgs[0][1] !== light(IMB[0]) || !imgs[0][1].includes('/cdn-cgi/image/width=320')) errs.push(`print: the IMB photo prints from ${imgs[0][1]}`)
|
||||
if (!/data-full="https:\/\/images\.peoplegroups\.org\/imbpeople-featured-images\/PG016542\.jpg"/.test(imgs[0][2]) || !/onerror=/.test(imgs[0][2])) errs.push('print: the resized photo has no fallback to its original')
|
||||
if (imgs[1][1] !== OTHER[0] || /data-full/.test(imgs[1][2])) errs.push('print: a Joshua Project photo was changed')
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
const fails = []
|
||||
fails.push(...run(P.lightPhoto, P.fullPhoto))
|
||||
// the canvas loads through the same helper (both draw paths) and retries the original
|
||||
if ((canvasSrc.match(/lightPhoto\(g\.imageUrl\)/g) || []).length < 2) fails.push('canvas: wallCanvas.js does not load every card photo through lightPhoto()')
|
||||
if (!/fullPhoto\(u\)/.test(canvasSrc)) fails.push('canvas: a failed resized photo is not retried from its original')
|
||||
// SELF-TEST: the old behaviour (originals) must fail the mapping arm; the print arm reads the real engine, so
|
||||
// it is checked by running the engine's output against an identity map
|
||||
const planted = run((u) => u, () => null)
|
||||
if (!planted.some((e) => e.startsWith('map:'))) fails.push('SELF-TEST: originals (identity) were not caught by the mapping arm')
|
||||
if (!planted.some((e) => e.startsWith('print:') || e.startsWith('full:'))) fails.push('SELF-TEST: the print/fallback arms did not react to the identity map')
|
||||
|
||||
if (fails.length) { console.log('WALL-PHOTO-WEIGHT: FAIL'); for (const f of fails) console.log(' - ' + f); process.exitCode = 1 }
|
||||
else console.log('WALL-PHOTO-WEIGHT: PASS — IMB photos on both hosts load as 320 px copies with their originals as the fallback, every other photo is untouched, and the print and the canvas both go through photoUrl.js; the old full-size behaviour is caught.')
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
* SAME layout for the screen (screenPoster: what the wall shows is what prints). No Vue.
|
||||
*/
|
||||
import { blessingFor, POSTER, THEMES } from './blessings.js'
|
||||
import { lightPhoto, fullPhoto } from './photoUrl.js'
|
||||
// The sheets, their glue bands and the print marks: the shared panel geometry (also India's large posters).
|
||||
import { planPanels, screenLayout, panelChromeHtml, panelCss } from '../../../library/components/poster/panelSheets.js'
|
||||
|
||||
|
|
@ -252,7 +253,10 @@ function posterParts(groups, presetId, texts, withCards = true) {
|
|||
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>'
|
||||
// The photo at print size (photoUrl.js); a resized copy that fails falls back to the original, so a change on
|
||||
// the photo host costs weight, never a photo.
|
||||
const light = lightPhoto(g.imageUrl), full = fullPhoto(light)
|
||||
const img = g.imageUrl ? `<img src="${esc(light)}" alt=""${full ? ` data-full="${esc(full)}" onerror="this.onerror=null;this.src=this.dataset.full"` : ''}>` : '<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 = cardText(plan, g.name || '').name
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@
|
|||
|
||||
<div ref="canvas" class="wall-canvas"></div>
|
||||
<div v-if="toast" class="wall-toast" role="status">{{ toast }}</div>
|
||||
<!-- While the poster prepares (its photos load before the print dialog can open, a minute or more on a slow
|
||||
connection), the shared print card says so in the middle of the screen: the count and a clock, so a
|
||||
long wait never reads as nothing happening. -->
|
||||
<map-print-progress :visible="printing" :kind="$t('wall.printPoster')" :title="printTitle" :progress="printPercent" />
|
||||
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<div class="spinner"></div>
|
||||
|
|
@ -56,9 +60,11 @@ import { buildBlessPosterHtml, screenPoster, PRESETS, DEFAULT_PRESET } from '../
|
|||
import { openPrintFrame, printDocument, isPhonePrint } from '@map/composables/printInPlace.js'
|
||||
import { watchPrintFrame } from '../printWatch.js'
|
||||
import { useHostedHelp } from '@map/composables/useHostedHelp.js'
|
||||
import MapPrintProgress from '@map/components/poster/MapPrintProgress.vue'
|
||||
// Wall of People Groups: a zoomable canvas wall (wallCanvas.js), Bless or Social cards, and the poster print.
|
||||
export default {
|
||||
name: 'PeopleGroupsWall',
|
||||
components: { MapPrintProgress },
|
||||
// The page's profile-config, for its people-group LIST (library/lists/); handed to the store.
|
||||
inject: { profileConfig: { default: null } },
|
||||
// The wall's "?" asks its HOST for the wall's help (doxa:help); it shows only where a host answers.
|
||||
|
|
@ -75,6 +81,8 @@ export default {
|
|||
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)
|
||||
printProgress: null, // { n, total, failed }: the print frame's photos, counted while they load
|
||||
printStarted: 0, // when the current print was asked for (the progress card's clock)
|
||||
printClock: 0, // seconds since then, ticked once a second while printing
|
||||
toast: '', // a short status line over the wall (e.g. "Copied!"), cleared by flash()
|
||||
cardWidth: 50,
|
||||
cardHeight: 70,
|
||||
|
|
@ -103,6 +111,16 @@ export default {
|
|||
return this.$store?.error || null;
|
||||
},
|
||||
|
||||
// The progress card: "Preparing the poster: n of total photos · m:ss" (the count once the frame reports it).
|
||||
printTitle() {
|
||||
const s = this.printClock, clock = Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0')
|
||||
const p = this.printProgress
|
||||
return p ? this.$t('wall.printPreparing', { n: p.n.toLocaleString('en-US'), total: p.total.toLocaleString('en-US') }) + ' · ' + clock : clock
|
||||
},
|
||||
printPercent() {
|
||||
const p = this.printProgress
|
||||
return { percent: p && p.total ? (100 * p.n) / p.total : 0 }
|
||||
},
|
||||
printLabel() {
|
||||
const p = this.printing && this.printProgress
|
||||
return p ? this.$t('wall.printPreparing', { n: p.n.toLocaleString('en-US'), total: p.total.toLocaleString('en-US') }) : this.$t('wall.printPoster');
|
||||
|
|
@ -121,6 +139,7 @@ export default {
|
|||
beforeUnmount() {
|
||||
// Release the canvas, its listeners and every cached photo (the fleet's leak rule).
|
||||
this._wc?.destroy(); this._wc = null;
|
||||
clearInterval(this._printTick); this._printTick = null;
|
||||
try { const host = this.$el?.getRootNode?.()?.host; if (host) delete host.__wallLod; } catch (_) { /* no host */ }
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -166,6 +185,7 @@ export default {
|
|||
const groups = (this.peopleGroups || []).map((d) => ({ name: d.name || d.post_title || '', imageUrl: d.imageUrl || '' }));
|
||||
if (!groups.length) return;
|
||||
this.printing = true;
|
||||
this.startPrintClock();
|
||||
const texts = this.posterTexts(groups.length);
|
||||
// A PHONE PRINTS THE PAGE (printInPlace.printDocument): there is no frame to watch, and printDocument
|
||||
// itself waits for the photos before it prints.
|
||||
|
|
@ -189,6 +209,15 @@ export default {
|
|||
},
|
||||
});
|
||||
},
|
||||
// The progress card's clock: from the click until the print is done (stops itself once printing ends).
|
||||
startPrintClock() {
|
||||
clearInterval(this._printTick);
|
||||
this.printStarted = Date.now(); this.printClock = 0;
|
||||
this._printTick = setInterval(() => {
|
||||
if (!this.printing) { clearInterval(this._printTick); this._printTick = null; return; }
|
||||
this.printClock = Math.floor((Date.now() - this.printStarted) / 1000);
|
||||
}, 1000);
|
||||
},
|
||||
// The wall in Bless mode IS the poster (WYSIWYG): the same sheets, cards, title, chart and footer that
|
||||
// print, from the same blessPoster layout. Re-planned when the groups, the size or the language change.
|
||||
showPoster() {
|
||||
|
|
|
|||
28
plugin-designs/wall-of-people-groups/src/photoUrl.js
Normal file
28
plugin-designs/wall-of-people-groups/src/photoUrl.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* photoUrl.js — a people group's photo at the size the Wall draws and prints it, not its original.
|
||||
*
|
||||
* Measured on the live list (939 distinct photos, 2026-09-30): the Joshua Project and doxa photos are already
|
||||
* small (150-250 px, 7-64 KB), but the 77 IMB photos (peoplegroupspublic.blob.core.windows.net and
|
||||
* images.peoplegroups.org, one Azure container behind both) are 1024 px originals of ~1.5 MB each: 114 MB of the
|
||||
* print's 133 MB. images.peoplegroups.org resizes on request (/cdn-cgi/image/), and at 320 px the 77 weigh 1.3 MB.
|
||||
* 320 px covers the largest printed photo (5 × 8 ft: ~0.97 in, 291 px at 300 dpi) and the canvas's own cap
|
||||
* (wallCanvas THUMB_PX). Every other URL is returned unchanged.
|
||||
*/
|
||||
|
||||
const IMB_HOSTS = new Set(['peoplegroupspublic.blob.core.windows.net', 'images.peoplegroups.org'])
|
||||
const IMB_PATH = /^\/(imbpeople-featured-images\/[^?#]+)$/
|
||||
export const PHOTO_PX = 320
|
||||
|
||||
/** The URL to load for a photo: a resized copy where the host offers one, else the URL itself. */
|
||||
export function lightPhoto(url) {
|
||||
let u
|
||||
try { u = new URL(url) } catch (_) { return url || '' }
|
||||
const m = IMB_HOSTS.has(u.hostname) && u.pathname.match(IMB_PATH)
|
||||
return m ? `https://images.peoplegroups.org/cdn-cgi/image/width=${PHOTO_PX},format=jpeg/${m[1]}` : url
|
||||
}
|
||||
|
||||
/** For a URL lightPhoto() rewrote, the original to fall back to if the resized copy fails; else null. */
|
||||
export function fullPhoto(light) {
|
||||
const m = String(light || '').match(/^https:\/\/images\.peoplegroups\.org\/cdn-cgi\/image\/[^/]+\/(imbpeople-featured-images\/.+)$/)
|
||||
return m ? `https://images.peoplegroups.org/${m[1]}` : null
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
import { wallD3 } from './wallD3.js'
|
||||
import { wallGrid } from './wallLod.js'
|
||||
import { blessingFor, wrapText } from './blessings.js'
|
||||
import { lightPhoto, fullPhoto } from './photoUrl.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.
|
||||
|
|
@ -289,7 +290,7 @@ export class WallCanvas {
|
|||
const verseH = T.verse ? T.verse.lines.length * vFs * ct.verseLine + ct.refGapPt * pt + rFs * 1.2 : 0
|
||||
const m = ct.imgMarginIn * px
|
||||
const imgY = R.y + pad + nameH + m, imgH = Math.max(0, R.h - 2 * pad - nameH - verseH - 2 * m)
|
||||
const url = g.imageUrl, cached = url && this.images.get(url)
|
||||
const url = lightPhoto(g.imageUrl), cached = url && this.images.get(url)
|
||||
if (cached && cached.ok) {
|
||||
cached.used = performance.now()
|
||||
const im = cached.img, ar = pw(im) / ph(im) || 1, box = iw / (imgH || 1)
|
||||
|
|
@ -342,7 +343,7 @@ export class WallCanvas {
|
|||
ctx.fillStyle = '#fff'; ctx.fillRect(x, y, w, h)
|
||||
ctx.lineWidth = 0.5; ctx.strokeStyle = '#dbdbdb'; ctx.strokeRect(x, y, w, h)
|
||||
const imgY = y + h * 0.18, imgH = h * 0.5
|
||||
const url = g.imageUrl
|
||||
const url = lightPhoto(g.imageUrl)
|
||||
const cached = url && this.images.get(url)
|
||||
if (cached && cached.ok) {
|
||||
cached.used = performance.now()
|
||||
|
|
@ -472,7 +473,9 @@ export class WallCanvas {
|
|||
rec.ok = true; this.loading--; this.request(); this.load([])
|
||||
})
|
||||
}
|
||||
img.onerror = () => { this.loading--; this.load([]) }
|
||||
// A resized copy that fails (photoUrl.js) is retried once from the original, under the same key.
|
||||
const full = fullPhoto(u)
|
||||
img.onerror = () => { if (full && img.src !== full) { img.src = full; return } this.loading--; this.load([]) }
|
||||
img.src = u
|
||||
}
|
||||
if (this.images.size > IMG_CACHE) {
|
||||
|
|
|
|||
|
|
@ -1471,7 +1471,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-EQcPfsC0SN3qR+HvILq9491mHiDQSWOfhK0SVrvNSRaZzQ8hqYlZiy5/fmaiJKNF",
|
||||
"integrity": "sha384-gF3WFa9sSjgu4eH7jHE2GlH/VLwtWi44pLNLzX8iWDqxqc87rZdNF/M9Vsp+UDaU",
|
||||
"elements": [
|
||||
"wall-of-people-groups"
|
||||
],
|
||||
|
|
@ -1490,7 +1490,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-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>"
|
||||
"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-gF3WFa9sSjgu4eH7jHE2GlH/VLwtWi44pLNLzX8iWDqxqc87rZdNF/M9Vsp+UDaU\" 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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1471,7 +1471,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-EQcPfsC0SN3qR+HvILq9491mHiDQSWOfhK0SVrvNSRaZzQ8hqYlZiy5/fmaiJKNF",
|
||||
"integrity": "sha384-gF3WFa9sSjgu4eH7jHE2GlH/VLwtWi44pLNLzX8iWDqxqc87rZdNF/M9Vsp+UDaU",
|
||||
"elements": [
|
||||
"wall-of-people-groups"
|
||||
],
|
||||
|
|
@ -1490,7 +1490,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-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>"
|
||||
"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-gF3WFa9sSjgu4eH7jHE2GlH/VLwtWi44pLNLzX8iWDqxqc87rZdNF/M9Vsp+UDaU\" 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-EQcPfsC0SN3qR+HvILq9491mHiDQSWOfhK0SVrvNSRaZzQ8hqYlZiy5/fmaiJKNF" crossorigin="anonymous"></script>
|
||||
<script src="../wall-of-people-groups.js" integrity="sha384-gF3WFa9sSjgu4eH7jHE2GlH/VLwtWi44pLNLzX8iWDqxqc87rZdNF/M9Vsp+UDaU" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue