Compare commits
11 commits
main
...
poster-des
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8550142a05 | ||
|
|
eff0945ab9 | ||
|
|
dea74fa6ee | ||
|
|
d1a2dc145b | ||
|
|
7a66bd9134 | ||
|
|
2d00df7323 | ||
|
|
7f4f7cd7bf | ||
|
|
be6cd3b55b | ||
|
|
13e22fe649 | ||
|
|
b462d8d155 | ||
|
|
ddefb6f30a |
29 changed files with 1144 additions and 299 deletions
|
|
@ -11,7 +11,24 @@
|
|||
},
|
||||
"sizes": {
|
||||
"heading": "مقاسات الملصقات",
|
||||
"waiting": "سيظهر سُلّم المقاسات (الورق القياسي، و3 × 3 أقدام، و3 × 5 أقدام، و5.5 × 5.5 أقدام، و5 × 8 أقدام) هنا بعد نشر جدول المقاسات المشترك. سيُعرض كل مقاس كمعاينة طباعة، بالاتجاه الطولي فقط."
|
||||
"intro": "كل المقاسات التي يمكن طباعة الملصق بها، مرسومة بمقياس رسم. تُطبع المقاسات الكبيرة على عدة أوراق تتداخل على الجدار؛ والشريط الأحمر هو موضع لصق الورقة التالية.",
|
||||
"lineup": "كل المقاسات جنبًا إلى جنب، بمقياس رسم",
|
||||
"paper": "ورق طابعة قياسي",
|
||||
"paperLetter": "ورق Letter الأمريكي، 8.5 × 11 بوصة",
|
||||
"paperA4": "A4، 210 × 297 مم",
|
||||
"dimsFt": "{w} × {h} قدم",
|
||||
"dimsIn": "{w} × {h} بوصة",
|
||||
"dimsInOne": "{v} بوصة",
|
||||
"rowSheets": "الأوراق",
|
||||
"rowSheet": "كل ورقة",
|
||||
"rowOverlap": "التداخل الملصوق",
|
||||
"rowPhotos": "صور بجانب الأسماء",
|
||||
"rowPrint": "طريقة الطباعة",
|
||||
"yes": "نعم",
|
||||
"no": "لا",
|
||||
"printHome": "أي طابعة منزلية",
|
||||
"printShop": "مطبعة (ملف PDF، صفحة لكل ورقة)",
|
||||
"layoutNote": "تعرض الأوراق تخطيط ملصق الدولة بمقاس 3 × 3 أقدام في كل مقاس: الترويسة والخريطة والأسماء والجدول وتذييل الآية. سيظهر الملصق المرسوم بكل مقاس في الأنماط عند توفره."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "صفحات الخرائط",
|
||||
|
|
@ -30,9 +47,7 @@
|
|||
"mapPrint": "طباعة الخريطة"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "لا يرسم المعرض الحزمة بعد. تذكر التفاصيل مولّدها ونسختها.",
|
||||
"cards": "لا يرسم المعرض البطاقات بعد. تذكر التفاصيل المولّد ومقاس البطاقة.",
|
||||
"bless": "محرّك هذا القالب ليس في المكتبة المشتركة بعد، لذا لا يستطيع المعرض رسمه.",
|
||||
"bless": "الأوراق وشرائط اللصق ومواضع صفوف البطاقات مأخوذة من هندسة الألواح المشتركة؛ وتُعرض البطاقات كتخطيط، لأن جدار البركة يرسم بطاقاته في إضافته الخاصة.",
|
||||
"mapPrint": "يطبع هذا القالب الخريطة الحية التي ينظر إليها القارئ، لذا ليست له صورة نموذجية."
|
||||
},
|
||||
"details": {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,24 @@
|
|||
},
|
||||
"sizes": {
|
||||
"heading": "Postergrößen",
|
||||
"waiting": "Die Größenstufen (Standardpapier, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft und 5 × 8 ft) erscheinen hier, sobald die gemeinsame Größentabelle veröffentlicht ist. Jede Größe wird als Druckvorschau gezeigt, nur im Hochformat."
|
||||
"intro": "Jede Größe, in der ein Poster gedruckt werden kann, maßstabsgetreu gezeichnet. Die großen Größen werden auf mehrere Bögen gedruckt, die sich an der Wand überlappen; der rote Streifen zeigt, wo der nächste Bogen angeklebt wird.",
|
||||
"lineup": "Alle Größen nebeneinander, maßstabsgetreu",
|
||||
"paper": "Normales Druckerpapier",
|
||||
"paperLetter": "US-Letter, 8,5 × 11 in",
|
||||
"paperA4": "A4, 210 × 297 mm",
|
||||
"dimsFt": "{w} × {h} ft",
|
||||
"dimsIn": "{w} × {h} in",
|
||||
"dimsInOne": "{v} in",
|
||||
"rowSheets": "Bögen",
|
||||
"rowSheet": "Jeder Bogen",
|
||||
"rowOverlap": "Geklebte Überlappung",
|
||||
"rowPhotos": "Fotos neben den Namen",
|
||||
"rowPrint": "Druck",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"printHome": "Jeder Drucker zu Hause",
|
||||
"printShop": "Eine Druckerei (ein PDF, eine Seite pro Bogen)",
|
||||
"layoutNote": "Die Bögen zeigen das Layout des 3 × 3 ft Länderposters in jeder Größe: Kopfzeile, Karte, Namen, Tabelle und die Fußzeile mit dem Bibelvers. Das in jeder Größe gezeichnete Poster erscheint unter Stile, sobald es verfügbar ist."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Kartenblätter",
|
||||
|
|
@ -30,9 +47,7 @@
|
|||
"mapPrint": "Kartendruck"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "Die Galerie zeichnet die Mappe noch nicht. Die Details nennen ihren Generator und ihre Variante.",
|
||||
"cards": "Die Galerie zeichnet die Karten noch nicht. Die Details nennen den Generator und das Kartenformat.",
|
||||
"bless": "Die Engine dieser Vorlage ist noch nicht in der gemeinsamen Bibliothek, deshalb kann die Galerie sie nicht zeichnen.",
|
||||
"bless": "Die Bögen, die Klebestreifen und die Lage der Kartenreihen stammen aus der gemeinsamen Tafelgeometrie; die Karten sind als Layout dargestellt, weil die Segnen-Wand ihre Karten in ihrem eigenen Plugin zeichnet.",
|
||||
"mapPrint": "Diese Vorlage druckt die Live-Karte, die der Leser gerade ansieht, und hat deshalb kein Beispielbild."
|
||||
},
|
||||
"details": {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,24 @@
|
|||
},
|
||||
"sizes": {
|
||||
"heading": "Poster sizes",
|
||||
"waiting": "The size ladder (standard paper, 3 × 3 ft, 3 × 5 ft, 5.5 × 5.5 ft and 5 × 8 ft) appears here once the shared size table is published. Each size will be shown as a print preview, portrait only."
|
||||
"intro": "Every size a poster can print at, drawn to scale. The large sizes print on several sheets that overlap on the wall; the red strip is where the next sheet is glued on.",
|
||||
"lineup": "All sizes side by side, to scale",
|
||||
"paper": "Standard printer paper",
|
||||
"paperLetter": "US Letter, 8.5 × 11 in",
|
||||
"paperA4": "A4, 210 × 297 mm",
|
||||
"dimsFt": "{w} × {h} ft",
|
||||
"dimsIn": "{w} × {h} in",
|
||||
"dimsInOne": "{v} in",
|
||||
"rowSheets": "Sheets",
|
||||
"rowSheet": "Each sheet",
|
||||
"rowOverlap": "Glued overlap",
|
||||
"rowPhotos": "Photos beside names",
|
||||
"rowPrint": "How it prints",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"printHome": "Any home printer",
|
||||
"printShop": "A print shop (a PDF, one page per sheet)",
|
||||
"layoutNote": "The sheets show the layout of the 3 × 3 ft country poster at each size: the header, the map, the names, the table and the scripture footer. The poster drawn at each size appears in Styles as it becomes available."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Map sheets",
|
||||
|
|
@ -30,9 +47,7 @@
|
|||
"mapPrint": "Map print"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "The gallery does not draw the packet yet. The details name its renderer and variant.",
|
||||
"cards": "The gallery does not draw the cards yet. The details name the renderer and the card stock.",
|
||||
"bless": "This template's engine is not in the shared library yet, so the gallery cannot draw it.",
|
||||
"bless": "The sheets, the glue strips and where the card rows go come from the shared panel geometry; the cards are shown as a layout, because the Bless wall draws its cards in its own plugin.",
|
||||
"mapPrint": "This template prints the live map the reader is looking at, so it has no sample picture."
|
||||
},
|
||||
"details": {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,24 @@
|
|||
},
|
||||
"sizes": {
|
||||
"heading": "Tamaños de póster",
|
||||
"waiting": "La escala de tamaños (papel estándar, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft y 5 × 8 ft) aparecerá aquí cuando se publique la tabla de tamaños compartida. Cada tamaño se mostrará como vista previa de impresión, solo en vertical."
|
||||
"intro": "Todos los tamaños en que se puede imprimir un póster, dibujados a escala. Los tamaños grandes se imprimen en varias hojas que se superponen en la pared; la franja roja es donde se pega la hoja siguiente.",
|
||||
"lineup": "Todos los tamaños lado a lado, a escala",
|
||||
"paper": "Papel de impresora estándar",
|
||||
"paperLetter": "Carta (EE. UU.), 8,5 × 11 in",
|
||||
"paperA4": "A4, 210 × 297 mm",
|
||||
"dimsFt": "{w} × {h} ft",
|
||||
"dimsIn": "{w} × {h} in",
|
||||
"dimsInOne": "{v} in",
|
||||
"rowSheets": "Hojas",
|
||||
"rowSheet": "Cada hoja",
|
||||
"rowOverlap": "Superposición pegada",
|
||||
"rowPhotos": "Fotos junto a los nombres",
|
||||
"rowPrint": "Cómo se imprime",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"printHome": "Cualquier impresora doméstica",
|
||||
"printShop": "Una imprenta (un PDF, una página por hoja)",
|
||||
"layoutNote": "Las hojas muestran el diseño del póster de país de 3 × 3 ft en cada tamaño: el encabezado, el mapa, los nombres, la tabla y el pie con la Escritura. El póster dibujado en cada tamaño aparecerá en Estilos a medida que esté disponible."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Hojas de mapa",
|
||||
|
|
@ -30,9 +47,7 @@
|
|||
"mapPrint": "Impresión del mapa"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "La galería todavía no dibuja el paquete. Los detalles indican su generador y su variante.",
|
||||
"cards": "La galería todavía no dibuja las tarjetas. Los detalles indican el generador y el tipo de tarjeta.",
|
||||
"bless": "El motor de esta plantilla aún no está en la biblioteca compartida, así que la galería no puede dibujarla.",
|
||||
"bless": "Las hojas, las franjas de pegado y el lugar de las filas de tarjetas vienen de la geometría de paneles compartida; las tarjetas se muestran como un diseño, porque el muro Bendecir dibuja sus tarjetas en su propio complemento.",
|
||||
"mapPrint": "Esta plantilla imprime el mapa en vivo que está mirando el lector, así que no tiene imagen de ejemplo."
|
||||
},
|
||||
"details": {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,24 @@
|
|||
},
|
||||
"sizes": {
|
||||
"heading": "Formats d'affiche",
|
||||
"waiting": "L'échelle des formats (papier standard, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft et 5 × 8 ft) apparaîtra ici dès que la table des formats partagée sera publiée. Chaque format sera présenté en aperçu avant impression, en portrait uniquement."
|
||||
"intro": "Tous les formats d'impression d'une affiche, dessinés à l'échelle. Les grands formats s'impriment sur plusieurs feuilles qui se chevauchent au mur ; la bande rouge indique où coller la feuille suivante.",
|
||||
"lineup": "Tous les formats côte à côte, à l'échelle",
|
||||
"paper": "Papier d'imprimante standard",
|
||||
"paperLetter": "Lettre US, 8,5 × 11 in",
|
||||
"paperA4": "A4, 210 × 297 mm",
|
||||
"dimsFt": "{w} × {h} ft",
|
||||
"dimsIn": "{w} × {h} in",
|
||||
"dimsInOne": "{v} in",
|
||||
"rowSheets": "Feuilles",
|
||||
"rowSheet": "Chaque feuille",
|
||||
"rowOverlap": "Chevauchement collé",
|
||||
"rowPhotos": "Photos à côté des noms",
|
||||
"rowPrint": "Impression",
|
||||
"yes": "Oui",
|
||||
"no": "Non",
|
||||
"printHome": "N'importe quelle imprimante",
|
||||
"printShop": "Un imprimeur (un PDF, une page par feuille)",
|
||||
"layoutNote": "Les feuilles montrent la mise en page de l'affiche pays de 3 × 3 ft à chaque format : l'en-tête, la carte, les noms, le tableau et le pied avec l'Écriture. L'affiche dessinée à chaque format apparaîtra dans Styles dès qu'elle sera disponible."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Feuilles de carte",
|
||||
|
|
@ -30,9 +47,7 @@
|
|||
"mapPrint": "Impression de la carte"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "La galerie ne dessine pas encore le dossier. Les détails indiquent son générateur et sa variante.",
|
||||
"cards": "La galerie ne dessine pas encore les cartes. Les détails indiquent le générateur et le format de carte.",
|
||||
"bless": "Le moteur de ce modèle n'est pas encore dans la bibliothèque partagée, la galerie ne peut donc pas le dessiner.",
|
||||
"bless": "Les feuilles, les bandes de collage et l'emplacement des rangées de cartes viennent de la géométrie de panneaux partagée ; les cartes sont montrées comme une mise en page, car le mur Bénir dessine ses cartes dans son propre module.",
|
||||
"mapPrint": "Ce modèle imprime la carte en direct que le lecteur regarde, il n'a donc pas d'image d'exemple."
|
||||
},
|
||||
"details": {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,24 @@
|
|||
},
|
||||
"sizes": {
|
||||
"heading": "पोस्टर आकार",
|
||||
"waiting": "साझा आकार तालिका प्रकाशित होते ही आकारों की सीढ़ी (मानक कागज़, 3 × 3 फ़ुट, 3 × 5 फ़ुट, 5.5 × 5.5 फ़ुट और 5 × 8 फ़ुट) यहाँ दिखेगी। हर आकार प्रिंट पूर्वावलोकन के रूप में दिखेगा, केवल खड़े (पोर्ट्रेट) रूप में।"
|
||||
"intro": "हर वह आकार जिसमें पोस्टर छापा जा सकता है, पैमाने के अनुसार बनाया गया। बड़े आकार कई शीटों पर छपते हैं जो दीवार पर एक-दूसरे पर चढ़ती हैं; लाल पट्टी वह जगह है जहाँ अगली शीट चिपकाई जाती है।",
|
||||
"lineup": "सभी आकार साथ-साथ, पैमाने के अनुसार",
|
||||
"paper": "सामान्य प्रिंटर कागज़",
|
||||
"paperLetter": "यूएस लेटर, 8.5 × 11 इंच",
|
||||
"paperA4": "A4, 210 × 297 मिमी",
|
||||
"dimsFt": "{w} × {h} फ़ुट",
|
||||
"dimsIn": "{w} × {h} इंच",
|
||||
"dimsInOne": "{v} इंच",
|
||||
"rowSheets": "शीटें",
|
||||
"rowSheet": "हर शीट",
|
||||
"rowOverlap": "चिपकाई जाने वाली ओवरलैप",
|
||||
"rowPhotos": "नामों के पास फ़ोटो",
|
||||
"rowPrint": "कैसे छपता है",
|
||||
"yes": "हाँ",
|
||||
"no": "नहीं",
|
||||
"printHome": "कोई भी घरेलू प्रिंटर",
|
||||
"printShop": "प्रिंटिंग प्रेस (PDF, हर शीट के लिए एक पेज)",
|
||||
"layoutNote": "शीटें हर आकार में 3 × 3 फ़ुट देश पोस्टर का लेआउट दिखाती हैं: शीर्षक, मानचित्र, नाम, तालिका और पवित्रशास्त्र वाला फ़ुटर। हर आकार में बना असली पोस्टर उपलब्ध होते ही शैलियाँ में दिखेगा।"
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "मानचित्र पृष्ठ",
|
||||
|
|
@ -30,9 +47,7 @@
|
|||
"mapPrint": "मानचित्र प्रिंट"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "गैलरी अभी पैकेट नहीं बनाती। विवरण में उसका जनरेटर और संस्करण बताया गया है।",
|
||||
"cards": "गैलरी अभी कार्ड नहीं बनाती। विवरण में जनरेटर और कार्ड का आकार बताया गया है।",
|
||||
"bless": "इस टेम्पलेट का इंजन अभी साझा लाइब्रेरी में नहीं है, इसलिए गैलरी इसे नहीं बना सकती।",
|
||||
"bless": "शीटें, चिपकाने की पट्टियाँ और कार्ड-पंक्तियों की जगह साझा पैनल ज्यामिति से आती हैं; कार्ड लेआउट के रूप में दिखाए गए हैं, क्योंकि आशीष दीवार अपने कार्ड अपने प्लगइन में बनाती है।",
|
||||
"mapPrint": "यह टेम्पलेट वह लाइव मानचित्र प्रिंट करता है जिसे पाठक देख रहा है, इसलिए इसकी कोई नमूना तस्वीर नहीं है।"
|
||||
},
|
||||
"details": {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,24 @@
|
|||
},
|
||||
"sizes": {
|
||||
"heading": "Formati dei poster",
|
||||
"waiting": "La scala dei formati (carta standard, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft e 5 × 8 ft) comparirà qui quando sarà pubblicata la tabella dei formati condivisa. Ogni formato sarà mostrato come anteprima di stampa, solo in verticale."
|
||||
"intro": "Tutti i formati in cui si può stampare un poster, disegnati in scala. I formati grandi si stampano su più fogli che si sovrappongono sulla parete; la striscia rossa indica dove incollare il foglio successivo.",
|
||||
"lineup": "Tutti i formati affiancati, in scala",
|
||||
"paper": "Carta da stampante standard",
|
||||
"paperLetter": "Letter USA, 8,5 × 11 in",
|
||||
"paperA4": "A4, 210 × 297 mm",
|
||||
"dimsFt": "{w} × {h} ft",
|
||||
"dimsIn": "{w} × {h} in",
|
||||
"dimsInOne": "{v} in",
|
||||
"rowSheets": "Fogli",
|
||||
"rowSheet": "Ogni foglio",
|
||||
"rowOverlap": "Sovrapposizione incollata",
|
||||
"rowPhotos": "Foto accanto ai nomi",
|
||||
"rowPrint": "Come si stampa",
|
||||
"yes": "Sì",
|
||||
"no": "No",
|
||||
"printHome": "Qualsiasi stampante di casa",
|
||||
"printShop": "Una tipografia (un PDF, una pagina per foglio)",
|
||||
"layoutNote": "I fogli mostrano l'impaginazione del poster di paese da 3 × 3 ft in ogni formato: intestazione, mappa, nomi, tabella e il piè di pagina con la Scrittura. Il poster disegnato in ogni formato comparirà in Stili appena disponibile."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Fogli mappa",
|
||||
|
|
@ -30,9 +47,7 @@
|
|||
"mapPrint": "Stampa della mappa"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "La galleria non disegna ancora il fascicolo. I dettagli indicano il suo generatore e la variante.",
|
||||
"cards": "La galleria non disegna ancora le schede. I dettagli indicano il generatore e il formato della scheda.",
|
||||
"bless": "Il motore di questo modello non è ancora nella libreria condivisa, quindi la galleria non può disegnarlo.",
|
||||
"bless": "I fogli, le strisce da incollare e la posizione delle file di schede vengono dalla geometria dei pannelli condivisa; le schede sono mostrate come impaginazione, perché il muro Benedici disegna le sue schede nel proprio plugin.",
|
||||
"mapPrint": "Questo modello stampa la mappa dal vivo che il lettore sta guardando, quindi non ha un'immagine di esempio."
|
||||
},
|
||||
"details": {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,24 @@
|
|||
},
|
||||
"sizes": {
|
||||
"heading": "Tamanhos de pôster",
|
||||
"waiting": "A escala de tamanhos (papel padrão, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft e 5 × 8 ft) aparecerá aqui quando a tabela de tamanhos compartilhada for publicada. Cada tamanho será mostrado como pré-visualização de impressão, apenas na vertical."
|
||||
"intro": "Todos os tamanhos em que um pôster pode ser impresso, desenhados em escala. Os tamanhos grandes são impressos em várias folhas que se sobrepõem na parede; a faixa vermelha mostra onde colar a folha seguinte.",
|
||||
"lineup": "Todos os tamanhos lado a lado, em escala",
|
||||
"paper": "Papel de impressora padrão",
|
||||
"paperLetter": "Carta (EUA), 8,5 × 11 in",
|
||||
"paperA4": "A4, 210 × 297 mm",
|
||||
"dimsFt": "{w} × {h} ft",
|
||||
"dimsIn": "{w} × {h} in",
|
||||
"dimsInOne": "{v} in",
|
||||
"rowSheets": "Folhas",
|
||||
"rowSheet": "Cada folha",
|
||||
"rowOverlap": "Sobreposição colada",
|
||||
"rowPhotos": "Fotos ao lado dos nomes",
|
||||
"rowPrint": "Como se imprime",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"printHome": "Qualquer impressora doméstica",
|
||||
"printShop": "Uma gráfica (um PDF, uma página por folha)",
|
||||
"layoutNote": "As folhas mostram o layout do pôster de país de 3 × 3 ft em cada tamanho: o cabeçalho, o mapa, os nomes, a tabela e o rodapé com a Escritura. O pôster desenhado em cada tamanho aparecerá em Estilos quando estiver disponível."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Folhas de mapa",
|
||||
|
|
@ -30,9 +47,7 @@
|
|||
"mapPrint": "Impressão do mapa"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "A galeria ainda não desenha o pacote. Os detalhes indicam o seu gerador e a variante.",
|
||||
"cards": "A galeria ainda não desenha os cartões. Os detalhes indicam o gerador e o formato do cartão.",
|
||||
"bless": "O motor deste modelo ainda não está na biblioteca compartilhada, por isso a galeria não consegue desenhá-lo.",
|
||||
"bless": "As folhas, as faixas de colagem e a posição das fileiras de cartões vêm da geometria de painéis compartilhada; os cartões aparecem como um layout, porque o mural Abençoar desenha os seus cartões no seu próprio plugin.",
|
||||
"mapPrint": "Este modelo imprime o mapa ao vivo que o leitor está vendo, por isso não tem imagem de exemplo."
|
||||
},
|
||||
"details": {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,24 @@
|
|||
},
|
||||
"sizes": {
|
||||
"heading": "Dimensiuni de afiș",
|
||||
"waiting": "Scara dimensiunilor (hârtie standard, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft și 5 × 8 ft) va apărea aici după publicarea tabelului comun de dimensiuni. Fiecare dimensiune va fi afișată ca previzualizare de tipărire, doar pe verticală."
|
||||
"intro": "Toate dimensiunile la care se poate tipări un afiș, desenate la scară. Dimensiunile mari se tipăresc pe mai multe coli care se suprapun pe perete; banda roșie arată unde se lipește coala următoare.",
|
||||
"lineup": "Toate dimensiunile alăturate, la scară",
|
||||
"paper": "Hârtie standard de imprimantă",
|
||||
"paperLetter": "Letter SUA, 8,5 × 11 in",
|
||||
"paperA4": "A4, 210 × 297 mm",
|
||||
"dimsFt": "{w} × {h} ft",
|
||||
"dimsIn": "{w} × {h} in",
|
||||
"dimsInOne": "{v} in",
|
||||
"rowSheets": "Coli",
|
||||
"rowSheet": "Fiecare coală",
|
||||
"rowOverlap": "Suprapunere lipită",
|
||||
"rowPhotos": "Fotografii lângă nume",
|
||||
"rowPrint": "Cum se tipărește",
|
||||
"yes": "Da",
|
||||
"no": "Nu",
|
||||
"printHome": "Orice imprimantă de acasă",
|
||||
"printShop": "O tipografie (un PDF, o pagină pe coală)",
|
||||
"layoutNote": "Colile arată machetarea afișului de țară de 3 × 3 ft la fiecare dimensiune: antetul, harta, numele, tabelul și subsolul cu versetul din Scriptură. Afișul desenat la fiecare dimensiune va apărea în Stiluri când va fi disponibil."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Foi de hartă",
|
||||
|
|
@ -30,9 +47,7 @@
|
|||
"mapPrint": "Tipărirea hărții"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "Galeria nu desenează încă pachetul. Detaliile numesc generatorul și varianta lui.",
|
||||
"cards": "Galeria nu desenează încă cartonașele. Detaliile numesc generatorul și formatul cartonașului.",
|
||||
"bless": "Motorul acestui șablon nu este încă în biblioteca comună, așa că galeria nu îl poate desena.",
|
||||
"bless": "Colile, benzile de lipire și locul rândurilor de cartonașe vin din geometria comună a panourilor; cartonașele sunt arătate ca machetă, pentru că peretele Binecuvântează își desenează cartonașele în propriul modul.",
|
||||
"mapPrint": "Acest șablon tipărește harta live pe care o privește cititorul, așa că nu are imagine de exemplu."
|
||||
},
|
||||
"details": {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,24 @@
|
|||
},
|
||||
"sizes": {
|
||||
"heading": "Размеры плакатов",
|
||||
"waiting": "Шкала размеров (стандартная бумага, 3 × 3 фута, 3 × 5 футов, 5,5 × 5,5 фута и 5 × 8 футов) появится здесь, когда будет опубликована общая таблица размеров. Каждый размер будет показан как предпросмотр печати, только в книжной ориентации."
|
||||
"intro": "Все размеры, в которых можно напечатать плакат, в масштабе. Большие размеры печатаются на нескольких листах, которые на стене заходят друг на друга; красная полоса показывает, где приклеить следующий лист.",
|
||||
"lineup": "Все размеры рядом, в масштабе",
|
||||
"paper": "Обычная бумага для принтера",
|
||||
"paperLetter": "US Letter, 8,5 × 11 дюймов",
|
||||
"paperA4": "A4, 210 × 297 мм",
|
||||
"dimsFt": "{w} × {h} фута",
|
||||
"dimsIn": "{w} × {h} дюймов",
|
||||
"dimsInOne": "{v} дюймов",
|
||||
"rowSheets": "Листы",
|
||||
"rowSheet": "Каждый лист",
|
||||
"rowOverlap": "Склеиваемый нахлёст",
|
||||
"rowPhotos": "Фото рядом с названиями",
|
||||
"rowPrint": "Как печатать",
|
||||
"yes": "Да",
|
||||
"no": "Нет",
|
||||
"printHome": "Любой домашний принтер",
|
||||
"printShop": "Типография (PDF, одна страница на лист)",
|
||||
"layoutNote": "На листах показан макет плаката страны 3 × 3 фута в каждом размере: заголовок, карта, названия, таблица и нижняя полоса со стихом из Писания. Плакат, нарисованный в каждом размере, появится в разделе «Стили», когда будет готов."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Листы карт",
|
||||
|
|
@ -30,9 +47,7 @@
|
|||
"mapPrint": "Печать карты"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "Галерея пока не рисует пакет. В сведениях указаны его генератор и вариант.",
|
||||
"cards": "Галерея пока не рисует карточки. В сведениях указаны генератор и формат карточки.",
|
||||
"bless": "Движка этого шаблона пока нет в общей библиотеке, поэтому галерея не может его нарисовать.",
|
||||
"bless": "Листы, полосы для склейки и места рядов карточек взяты из общей геометрии панелей; карточки показаны как макет, потому что стена «Благословить» рисует свои карточки в собственном плагине.",
|
||||
"mapPrint": "Этот шаблон печатает живую карту, на которую смотрит читатель, поэтому у него нет примерного изображения."
|
||||
},
|
||||
"details": {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,24 @@
|
|||
},
|
||||
"sizes": {
|
||||
"heading": "海报尺寸",
|
||||
"waiting": "共享尺寸表发布后,尺寸阶梯(标准纸张、3 × 3 英尺、3 × 5 英尺、5.5 × 5.5 英尺和 5 × 8 英尺)将显示在这里。每种尺寸都会以打印预览呈现,仅限纵向。"
|
||||
"intro": "海报可打印的所有尺寸,按比例绘制。大尺寸海报分多张纸打印,贴在墙上时彼此重叠;红色条带标出下一张纸的粘贴位置。",
|
||||
"lineup": "所有尺寸并排,按比例",
|
||||
"paper": "标准打印纸",
|
||||
"paperLetter": "美国信纸,8.5 × 11 英寸",
|
||||
"paperA4": "A4,210 × 297 毫米",
|
||||
"dimsFt": "{w} × {h} 英尺",
|
||||
"dimsIn": "{w} × {h} 英寸",
|
||||
"dimsInOne": "{v} 英寸",
|
||||
"rowSheets": "纸张数",
|
||||
"rowSheet": "每张纸",
|
||||
"rowOverlap": "粘贴重叠",
|
||||
"rowPhotos": "名称旁配照片",
|
||||
"rowPrint": "打印方式",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"printHome": "任何家用打印机",
|
||||
"printShop": "印刷店(PDF,每张纸一页)",
|
||||
"layoutNote": "纸张显示的是 3 × 3 英尺国家海报在各尺寸下的版式:标题、地图、名称、表格和经文页脚。各尺寸实际绘制的海报完成后会出现在“样式”中。"
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "地图页",
|
||||
|
|
@ -30,9 +47,7 @@
|
|||
"mapPrint": "地图打印"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "图库暂不绘制资料包。详情中列出了它的生成器和版本。",
|
||||
"cards": "图库暂不绘制祷告卡。详情中列出了生成器和卡片规格。",
|
||||
"bless": "此模板的引擎尚未进入共享组件库,因此图库无法绘制。",
|
||||
"bless": "纸张、粘贴条以及卡片行的位置都来自共享的拼板几何;卡片以版式示意,因为祝福墙在其自己的插件中绘制卡片。",
|
||||
"mapPrint": "此模板打印读者正在查看的实时地图,因此没有示例图片。"
|
||||
},
|
||||
"details": {
|
||||
|
|
|
|||
|
|
@ -349,7 +349,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:script-flags 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: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 check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test",
|
||||
"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-internal-role-nouns 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:side-lanes check:side-lanes: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: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 check:poster-design-catalogue check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes",
|
||||
"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-internal-role-nouns 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: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 check:poster-design-catalogue check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:side-lanes check:side-lanes:self-test",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -117,7 +117,8 @@ useShadowStyles(PANE_GRID_STYLES + `
|
|||
<div class="pdmfe">
|
||||
<header class="hdr">
|
||||
<ViewSelect :views="spec.panes" :current="view" :legend="t('tabs.selectView')" :label="t('tabs.views')" @select="go" />
|
||||
<ShareButton :is-dark="isDark" :embed-path="SHARE_EMBED_PATH" />
|
||||
<!-- A HEADER button, so the header form: a rounded square (Big Picture's DashboardTopBar uses the same). -->
|
||||
<ShareButton :is-dark="isDark" :embed-path="SHARE_EMBED_PATH" shape="rounded-square" />
|
||||
</header>
|
||||
<div class="stage" ref="stage"></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,35 +1,164 @@
|
|||
<script setup>
|
||||
/**
|
||||
* poster-sizes.vue: every poster size, each card a print preview at that size.
|
||||
* poster-sizes.vue: every poster size, drawn to scale, with the sheets each one prints on.
|
||||
*
|
||||
* @element poster-design
|
||||
* @description Every poster size the print picker offers (standard paper up to 5 × 8 ft), each drawn as a
|
||||
* print preview, portrait only, from the one shared size table.
|
||||
* @description Every poster size the print picker offers, from standard paper to 5 × 8 ft, drawn to scale.
|
||||
* A size that prints on several sheets shows them pulled apart with the red strip where the next sheet is
|
||||
* glued on, as the Wall of People Groups previews its 5 × 8 ft poster.
|
||||
*
|
||||
* The sizes come from the shared size table in library/constants/posterSizes.js and nowhere else: a
|
||||
* second list here would drift from the print picker. Until that table carries the size ladder, this
|
||||
* tab says so rather than inventing one.
|
||||
* The sizes are the ONE shared ladder (library/constants/posterSizes.js POSTER_LADDER, in
|
||||
* POSTER_LADDER_ORDER), never a second list: a size the ladder gains appears here by itself. Each size's
|
||||
* `sheets` goes straight into the library's PanelSheets, the same component and the same geometry
|
||||
* (panelSheets.js) the print uses. What fills the sheets is the 3 × 3 country poster's LAYOUT drawn at that
|
||||
* size (src/layoutSvg.js), labelled as a layout, because the renderer does not draw the large sizes yet.
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { useOptionalT } from '@map/composables/useOptionalT.js'
|
||||
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import PanelSheets from '@map/components/poster/PanelSheets.vue'
|
||||
import { planPanels, screenLayout } from '@map/components/poster/panelSheets.js'
|
||||
import { POSTER_LADDER, POSTER_LADDER_ORDER, paperFor } from '@map/constants/posterSizes.js'
|
||||
import { useOptionalT, useOptionalI18n } from '@map/composables/useOptionalT.js'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { layoutSvgUrl } from './src/layoutSvg.js'
|
||||
|
||||
const t = useOptionalT('')
|
||||
const heading = computed(() => t('posterDesign.sizes.heading'))
|
||||
const waiting = computed(() => t('posterDesign.sizes.waiting'))
|
||||
const { locale } = useOptionalI18n('')
|
||||
|
||||
// A number as the sizes are spoken: whole, or one decimal place (5.5).
|
||||
const n = (x) => (Number.isInteger(x) ? String(x) : String(Math.round(x * 10) / 10))
|
||||
// Feet once a side reaches a yard and both sides are whole or half feet (3 × 5.5 ft); inches below that (18 × 24 in).
|
||||
function dims(w, h) {
|
||||
const ft = (x) => x / 12
|
||||
const halfFeet = (x) => Number.isInteger(ft(x) * 2)
|
||||
if (Math.min(w, h) >= 36 && halfFeet(w) && halfFeet(h)) return t('posterDesign.sizes.dimsFt', { w: n(ft(w)), h: n(ft(h)) })
|
||||
return t('posterDesign.sizes.dimsIn', { w: n(w), h: n(h) })
|
||||
}
|
||||
// Standard printer paper is Letter or A4 by the reader's region, as the print picker decides it.
|
||||
const paperIsA4 = computed(() => {
|
||||
const l = locale && (locale.value ?? locale)
|
||||
const tag = (typeof navigator !== 'undefined' && navigator.language) || (typeof l === 'string' ? l : '') || 'en'
|
||||
return paperFor(tag) === 'a4'
|
||||
})
|
||||
|
||||
const sizes = computed(() => POSTER_LADDER_ORDER.map((key) => POSTER_LADDER[key]).filter(Boolean).map((s) => {
|
||||
const { sheetW, sheetH, count, direction, joinIn } = s.sheets
|
||||
return {
|
||||
key: s.id,
|
||||
w: s.inches.w,
|
||||
h: s.inches.h,
|
||||
sheets: { sheetW, sheetH, count, direction, joinIn },
|
||||
title: s.paper ? t('posterDesign.sizes.paper') : dims(s.inches.w, s.inches.h),
|
||||
subtitle: s.paper ? t(paperIsA4.value ? 'posterDesign.sizes.paperA4' : 'posterDesign.sizes.paperLetter') : '',
|
||||
count,
|
||||
sheetDims: dims(sheetW, sheetH),
|
||||
overlap: count > 1 ? t('posterDesign.sizes.dimsInOne', { v: n(joinIn) }) : '',
|
||||
photos: !!s.photos,
|
||||
shop: s.path === 'pdf',
|
||||
src: layoutSvgUrl(s.inches.w, s.inches.h),
|
||||
}
|
||||
}))
|
||||
|
||||
// The box PanelSheets draws, in inches: its own screenLayout of the same plan, so a fit computed here is the fit
|
||||
// it renders (each sheet whole, pulled apart by the gap, which is taller than the finished poster).
|
||||
const LINEUP_GAP_IN = 1.5
|
||||
const CARD_GAP_IN = 0.75
|
||||
function world(s, gapIn) {
|
||||
const L = screenLayout(planPanels(s.sheets), { gapIn, pxPerIn: 1 })
|
||||
return { w: L.worldW, h: L.worldH }
|
||||
}
|
||||
|
||||
// THE LINEUP: every size at ONE scale, standing on one baseline, so 5 × 8 ft looks as big beside paper as it
|
||||
// is. Each size gets an equal column; the scale is the largest at which the tallest drawing is LINEUP_H and
|
||||
// the widest still fits its column, so on a phone every size stays in view and stays to scale.
|
||||
const LINEUP_H = 240 // px for the tallest size, when there is room
|
||||
const COL_GAP = 12
|
||||
const rowEl = ref(null)
|
||||
const rowW = ref(1000)
|
||||
let ro = null
|
||||
onMounted(() => {
|
||||
if (!rowEl.value || typeof ResizeObserver === 'undefined') return
|
||||
ro = new ResizeObserver(([e]) => { if (e && e.contentRect.width) rowW.value = e.contentRect.width })
|
||||
ro.observe(rowEl.value)
|
||||
})
|
||||
onBeforeUnmount(() => { ro?.disconnect(); ro = null })
|
||||
const pxPerIn = computed(() => {
|
||||
const ws = sizes.value.map((s) => world(s, LINEUP_GAP_IN))
|
||||
if (!ws.length) return 1
|
||||
const col = (rowW.value - COL_GAP * (ws.length - 1)) / ws.length
|
||||
return Math.min(LINEUP_H / Math.max(...ws.map((w) => w.h)), col / Math.max(...ws.map((w) => w.w)))
|
||||
})
|
||||
const lineupW = (s) => `${Math.max(4, world(s, LINEUP_GAP_IN).w * pxPerIn.value)}px`
|
||||
|
||||
useShadowStyles(`
|
||||
:host{display:block;width:100%;height:100%;min-height:420px}
|
||||
.pd-sizes{position:absolute;inset:0;overflow-y:auto;padding:28px 20px;background:#f3f3f1;color:#3b463d;
|
||||
font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,system-ui,sans-serif}
|
||||
.pd-sizes__h{margin:0 0 8px;font-size:22px;font-weight:700}
|
||||
.pd-sizes__note{margin:0;max-width:60ch;color:#6d766e}
|
||||
.pd-sizes{position:absolute;inset:0;overflow-y:auto;padding:24px 20px 40px;background:#f3f3f1;color:#3b463d;
|
||||
font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,system-ui,sans-serif;box-sizing:border-box}
|
||||
.pd-sizes__h{margin:0 0 6px;font-size:22px;font-weight:700}
|
||||
.pd-sizes__intro{margin:0 0 18px;max-width:70ch;color:#6d766e}
|
||||
.pd-lineup{background:#fff;border:1px solid #d1d4cd;border-radius:12px;padding:16px 16px 10px;margin:0 0 24px}
|
||||
.pd-lineup__h{margin:0 0 12px;font-size:14px;font-weight:700;color:#6d766e}
|
||||
.pd-lineup__row{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);align-items:end;gap:12px;
|
||||
padding-bottom:6px;border-bottom:2px solid #d1d4cd}
|
||||
.pd-lineup__item{display:flex;flex-direction:column;align-items:center;gap:6px;min-width:0}
|
||||
.pd-lineup__label{font-size:12px;font-weight:600;text-align:center;line-height:1.25;overflow-wrap:anywhere}
|
||||
.pd-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:18px}
|
||||
.pd-card{background:#fff;border:1px solid #d1d4cd;border-radius:12px;padding:14px;display:flex;flex-direction:column;gap:10px;min-width:0}
|
||||
.pd-card__art{display:flex;justify-content:center;align-items:flex-end;height:220px;background:#f3f3f1;border-radius:8px;padding:10px;box-sizing:border-box}
|
||||
.pd-card__art > .pd-card__fit{height:100%;display:flex;align-items:flex-end;justify-content:center;width:100%}
|
||||
.pd-card__title{margin:0;font-size:17px;font-weight:700}
|
||||
.pd-card__sub{margin:-8px 0 0;color:#6d766e;font-size:13px}
|
||||
.pd-card__rows{display:grid;grid-template-columns:max-content 1fr;gap:4px 12px;margin:0;font-size:13px}
|
||||
.pd-card__rows dt{color:#6d766e}
|
||||
.pd-card__rows dd{margin:0}
|
||||
.pd-note{margin:20px 0 0;max-width:70ch;color:#6d766e;font-size:13px}
|
||||
`, 'poster-design-sizes')
|
||||
|
||||
// A card's art box leaves 200 × 222 px for the drawing; it fits by whichever side binds first.
|
||||
const ART_H = 200
|
||||
const ART_W = 222
|
||||
function artWidth(s) {
|
||||
const { w, h } = world(s, CARD_GAP_IN)
|
||||
return `${Math.floor(w * Math.min(ART_H / h, ART_W / w))}px`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pd-sizes">
|
||||
<h2 class="pd-sizes__h">{{ heading }}</h2>
|
||||
<p class="pd-sizes__note">{{ waiting }}</p>
|
||||
<h2 class="pd-sizes__h">{{ t('posterDesign.sizes.heading') }}</h2>
|
||||
<p class="pd-sizes__intro">{{ t('posterDesign.sizes.intro') }}</p>
|
||||
|
||||
<section class="pd-lineup" :aria-label="t('posterDesign.sizes.lineup')">
|
||||
<h3 class="pd-lineup__h">{{ t('posterDesign.sizes.lineup') }}</h3>
|
||||
<div ref="rowEl" class="pd-lineup__row">
|
||||
<div v-for="s in sizes" :key="s.key" class="pd-lineup__item">
|
||||
<div :style="{ width: lineupW(s) }">
|
||||
<PanelSheets v-bind="s.sheets" :gap-in="LINEUP_GAP_IN" :src="s.src" />
|
||||
</div>
|
||||
<span class="pd-lineup__label">{{ s.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="pd-grid">
|
||||
<article v-for="s in sizes" :key="s.key" class="pd-card" :data-size="s.key">
|
||||
<div class="pd-card__art">
|
||||
<div class="pd-card__fit">
|
||||
<div :style="{ width: artWidth(s) }"><PanelSheets v-bind="s.sheets" :gap-in="CARD_GAP_IN" :src="s.src" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="pd-card__title">{{ s.title }}</h3>
|
||||
<p v-if="s.subtitle" class="pd-card__sub">{{ s.subtitle }}</p>
|
||||
<dl class="pd-card__rows">
|
||||
<dt>{{ t('posterDesign.sizes.rowSheets') }}</dt><dd>{{ s.count }}</dd>
|
||||
<template v-if="s.count > 1">
|
||||
<dt>{{ t('posterDesign.sizes.rowSheet') }}</dt><dd>{{ s.sheetDims }}</dd>
|
||||
<dt>{{ t('posterDesign.sizes.rowOverlap') }}</dt><dd>{{ s.overlap }}</dd>
|
||||
</template>
|
||||
<dt>{{ t('posterDesign.sizes.rowPhotos') }}</dt><dd>{{ s.photos ? t('posterDesign.sizes.yes') : t('posterDesign.sizes.no') }}</dd>
|
||||
<dt>{{ t('posterDesign.sizes.rowPrint') }}</dt><dd>{{ s.shop ? t('posterDesign.sizes.printShop') : t('posterDesign.sizes.printHome') }}</dd>
|
||||
</dl>
|
||||
</article>
|
||||
</div>
|
||||
<p class="pd-note">{{ t('posterDesign.sizes.layoutNote') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ import { getApiBaseUrl } from '@map/api/apiBaseUrl.js'
|
|||
import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js'
|
||||
import countryData from '@map/data/country-posters.json'
|
||||
import { makeCountrySheet } from '@map/packet/countrySheet.js'
|
||||
import { loadPacketStrings } from '@map/packet/packetStrings.js'
|
||||
import { firstPageImage } from './src/pageThumb.js'
|
||||
import { panelsSvgUrl } from './src/panelsSvg.js'
|
||||
import { resolvePosterSize } from '@map/constants/posterSizes.js'
|
||||
import { listConfigurations, FAMILY_KEYS, SAMPLE_COUNTRY_ID } from './src/templates.js'
|
||||
import FullDetailsDialog from './src/FullDetailsDialog.vue'
|
||||
|
||||
|
|
@ -64,33 +68,80 @@ const countrySheet = (format, id = SAMPLE_COUNTRY_ID) => {
|
|||
const c = countryById(id)
|
||||
return c ? sheetFor(c, format) : Promise.resolve('')
|
||||
}
|
||||
const ctx = { countrySheet }
|
||||
|
||||
// The sample's people groups and the packet's words, each fetched once for the whole gallery.
|
||||
const lang = () => getRuntimeLocale() || detectLocale() || 'en'
|
||||
let rowsP = null
|
||||
const sampleRows = () => (rowsP ||= (sample ? recordsFor({ type: 'country', values: [sample.iso3] }) : Promise.resolve([]))
|
||||
.then((r) => r || []).catch(() => { rowsP = null; return [] }))
|
||||
let stringsP = null
|
||||
const packetStrings = () => (stringsP ||= loadPacketStrings(assetsBase.value))
|
||||
|
||||
// A card deck exactly as the wall prints one (usePosterPrints printCardDeck: createCardRenderer(...).buildDeck):
|
||||
// the whole deck is the document, its first card the thumbnail.
|
||||
async function cardDeck(stockId) {
|
||||
const [rows, strings, { createCardRenderer }] = await Promise.all([sampleRows(), packetStrings(), import('@map/packet/card-render.js')])
|
||||
if (!rows.length) return null
|
||||
const R = createCardRenderer({ strings, lang: lang(), stock: stockId })
|
||||
return { html: R.buildDeck(sample, rows), page: '.card', width: R.W, height: R.H }
|
||||
}
|
||||
// A packet's PAGE 1, its map page, from the same renderer and the same map sheet the wall's packet uses
|
||||
// (the sample's map-only 'packet' sheet, lifted by the renderer). Only page 1 is built: it is what the
|
||||
// thumbnail shows, and it needs nothing the later pages need (descriptions, the evangelical share).
|
||||
async function packetDocument(variant) {
|
||||
const [rows, strings, { createPacketRenderer }, sheetHtml] = await Promise.all([
|
||||
sampleRows(), packetStrings(), import('@map/packet/packet-render.js'), countrySheet('packet'),
|
||||
])
|
||||
if (!rows.length) return null
|
||||
const R = createPacketRenderer({ strings, lang: lang() })
|
||||
const mapSvg = sheetHtml ? R.liftMap(sheetHtml) : ''
|
||||
const doc = R.buildPacket(sample, rows, new Map(), mapSvg, null, null, variant, null)
|
||||
return { html: doc, page: '.sheet', width: R.W, height: R.H, firstPageOnly: true }
|
||||
}
|
||||
// A multi-sheet poster as its panels, from the shared ladder's sheets for that size.
|
||||
function panels(sizeKey) {
|
||||
const size = resolvePosterSize(sizeKey)
|
||||
return size ? { image: panelsSvgUrl(size.sheets) } : null
|
||||
}
|
||||
const ctx = { countrySheet, cardDeck, packetDocument, panels }
|
||||
|
||||
// The printed document's size, measured when it is drawn: what "file size" means in the details.
|
||||
// render() answers a poster's HTML (a map sheet) or a document { html, page, width, height } (a deck, a packet).
|
||||
const fileBytes = ref({})
|
||||
async function renderDocument(name) {
|
||||
const c = byName.get(name)
|
||||
if (!c || !c.render) return ''
|
||||
const html = await c.render(ctx)
|
||||
if (html) fileBytes.value = { ...fileBytes.value, [name]: new Blob([html]).size }
|
||||
return html || ''
|
||||
if (!c || !c.render) return null
|
||||
const out = await c.render(ctx)
|
||||
if (out && out.image) return out
|
||||
const html = typeof out === 'string' ? out : out && out.html
|
||||
if (!html) return null
|
||||
if (!(out && out.firstPageOnly)) fileBytes.value = { ...fileBytes.value, [name]: new Blob([html]).size }
|
||||
return out
|
||||
}
|
||||
|
||||
// previewSrc: the thumbnail, rasterised from the drawn document at a quarter scale (a 1500px sheet
|
||||
// becomes ~375px, the card's size). A render that comes out blank is refused, so the card shows
|
||||
// "preview unavailable" rather than a black rectangle.
|
||||
// previewSrc: the thumbnail. A map sheet is rasterised at a quarter scale (a 1500px sheet becomes ~375px,
|
||||
// the card's size), and a render that comes out blank is refused, so the card shows "preview unavailable"
|
||||
// rather than a black rectangle. A document's first page is drawn as it prints (src/pageThumb.js).
|
||||
async function previewSrc(item) {
|
||||
const html = await renderDocument(item.id)
|
||||
if (!html) return null
|
||||
const out = await renderDocument(item.id)
|
||||
if (!out) return null
|
||||
if (out.image) return out.image
|
||||
if (typeof out === 'object') return firstPageImage(out.html, out)
|
||||
const { rasterPoster, looksRendered } = await import('@map/packet/poster-raster.js')
|
||||
const blob = await rasterPoster(html, { scale: 0.25 })
|
||||
const blob = await rasterPoster(out, { scale: 0.25 })
|
||||
if (!(await looksRendered(blob))) return null
|
||||
return URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
// The card's print button prints THIS configuration: the format the wall asks for is ignored on
|
||||
// purpose, because the card already names its size.
|
||||
const sheet = (item) => renderDocument(item.id)
|
||||
// The card's print button prints THIS configuration: the format the wall asks for is ignored on purpose,
|
||||
// because the card already names its size. A card deck prints whole, as the wall prints it. A packet is
|
||||
// built here only as far as its page 1, so it is NOT offered for printing from this card: printing a
|
||||
// packet missing its later pages would misrepresent the template.
|
||||
async function sheet(item) {
|
||||
const out = await renderDocument(item.id)
|
||||
if (!out || out.image || (typeof out === 'object' && out.firstPageOnly)) return ''
|
||||
return typeof out === 'string' ? out : out.html
|
||||
}
|
||||
|
||||
// ── "Show full details" ───────────────────────────────────────────────────────────────────────
|
||||
const open = ref(null)
|
||||
|
|
|
|||
55
plugin-designs/poster-design/src/layoutSvg.js
Normal file
55
plugin-designs/poster-design/src/layoutSvg.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* layoutSvg.js: the LAYOUT of the 3 × 3 country poster (a header band, the map, the names down both sides,
|
||||
* a table beneath, and the bold scripture footer), drawn at any finished size as an SVG image.
|
||||
*
|
||||
* The Sizes tab shows how each ladder size prints before the renderer draws that size for real, so this is
|
||||
* a layout, not a poster: blocks in the poster's own colours, no words (nothing to translate, nothing that
|
||||
* reads as real data). Units are INCHES (the viewBox is the finished w × h), so PanelSheets can slice it
|
||||
* across sheets exactly as it slices a real poster.
|
||||
*/
|
||||
const PAPER = '#f7f4ec'
|
||||
const INK = '#3b463d'
|
||||
const LINE = '#c9c3b3'
|
||||
const LAND = '#dcd3ba'
|
||||
const SEA = '#e6eef0'
|
||||
const ACCENT = '#92b195'
|
||||
|
||||
/** @returns {string} an image URL (a data: URL of an SVG) of the layout at w × h inches. */
|
||||
export function layoutSvgUrl(w, h) {
|
||||
const m = Math.min(w, h) * 0.05 // outer margin
|
||||
const headH = h * 0.09
|
||||
const footH = h * 0.08
|
||||
const bodyTop = m + headH + m * 0.6
|
||||
const bodyBottom = h - m - footH - m * 0.6
|
||||
const tableH = (bodyBottom - bodyTop) * 0.22
|
||||
const mapBottom = bodyBottom - tableH - m * 0.5
|
||||
const colW = (w - 2 * m) * 0.22 // a name column on each side of the map
|
||||
const mapX = m + colW + m * 0.4
|
||||
const mapW = w - 2 * mapX
|
||||
const lineGap = Math.max(h / 80, 0.35)
|
||||
const nameLines = (x) => {
|
||||
let out = ''
|
||||
for (let y = bodyTop + lineGap; y < mapBottom - lineGap / 2; y += lineGap) {
|
||||
out += `<rect x="${x.toFixed(2)}" y="${(y - lineGap * 0.28).toFixed(2)}" width="${(colW * (0.55 + 0.4 * ((y * 7) % 1))).toFixed(2)}" height="${(lineGap * 0.4).toFixed(2)}" fill="${LINE}"/>`
|
||||
}
|
||||
return out
|
||||
}
|
||||
let table = ''
|
||||
const rows = 4
|
||||
for (let r = 0; r <= rows; r++) {
|
||||
const y = mapBottom + m * 0.5 + (tableH * r) / rows
|
||||
table += `<line x1="${m}" y1="${y.toFixed(2)}" x2="${(w - m).toFixed(2)}" y2="${y.toFixed(2)}" stroke="${LINE}" stroke-width="${(lineGap * 0.12).toFixed(3)}"/>`
|
||||
}
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none">`
|
||||
+ `<rect width="${w}" height="${h}" fill="${PAPER}"/>`
|
||||
+ `<rect x="${m}" y="${m}" width="${w - 2 * m}" height="${headH}" rx="${(headH * 0.12).toFixed(2)}" fill="${INK}"/>`
|
||||
+ `<rect x="${(m + (w - 2 * m) * 0.25).toFixed(2)}" y="${(m + headH * 0.38).toFixed(2)}" width="${((w - 2 * m) * 0.5).toFixed(2)}" height="${(headH * 0.24).toFixed(2)}" fill="${PAPER}" opacity=".85"/>`
|
||||
+ `<rect x="${mapX.toFixed(2)}" y="${bodyTop.toFixed(2)}" width="${mapW.toFixed(2)}" height="${(mapBottom - bodyTop).toFixed(2)}" fill="${SEA}"/>`
|
||||
+ `<ellipse cx="${(mapX + mapW * 0.5).toFixed(2)}" cy="${((bodyTop + mapBottom) / 2).toFixed(2)}" rx="${(mapW * 0.36).toFixed(2)}" ry="${((mapBottom - bodyTop) * 0.34).toFixed(2)}" fill="${LAND}"/>`
|
||||
+ nameLines(m) + nameLines(w - m - colW)
|
||||
+ table
|
||||
+ `<rect x="${m}" y="${(h - m - footH).toFixed(2)}" width="${w - 2 * m}" height="${footH}" rx="${(footH * 0.12).toFixed(2)}" fill="${ACCENT}"/>`
|
||||
+ `<rect x="${(m + (w - 2 * m) * 0.15).toFixed(2)}" y="${(h - m - footH * 0.62).toFixed(2)}" width="${((w - 2 * m) * 0.7).toFixed(2)}" height="${(footH * 0.24).toFixed(2)}" fill="${INK}" opacity=".8"/>`
|
||||
+ `</svg>`
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
|
||||
}
|
||||
77
plugin-designs/poster-design/src/pageThumb.js
Normal file
77
plugin-designs/poster-design/src/pageThumb.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* pageThumb.js: the FIRST PAGE of a printable HTML document (a card deck, a prayer packet) as an image
|
||||
* a poster card can show.
|
||||
*
|
||||
* The poster rasteriser (library/packet/poster-raster.js) lifts an <svg> out of a poster; a deck or a
|
||||
* packet is HTML pages instead, so it has nothing to lift. This takes the document the reader would
|
||||
* print, keeps its first page, and wraps that page in an SVG <foreignObject>, which a browser draws
|
||||
* through a plain <img>. Nothing is re-laid-out here: the page, its stylesheet and its size all come
|
||||
* from the shared renderer.
|
||||
*
|
||||
* Two things an SVG shown through <img> does differently, both handled here:
|
||||
* - it is an ISOLATED document that loads nothing, so a photo on the page is fetched now and inlined as
|
||||
* a data: URL, but ONLY from this page's own origin: a fetch needs CORS, and the people-group photo host
|
||||
* sends none, so a cross-origin photo is left out WITHOUT trying (trying only fills the console with
|
||||
* refusals). Its frame stays, empty, and the printed document still carries the photo;
|
||||
* - its markup must be XML, so the page is re-serialised with XMLSerializer (void elements closed,
|
||||
* entities resolved) rather than pasted as HTML.
|
||||
*
|
||||
* A pilot in this plugin; if a second surface needs a document thumbnail it belongs in library/.
|
||||
*/
|
||||
const MAX_PHOTOS = 8
|
||||
|
||||
function withTimeout(promise, ms) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('timed out')), ms)
|
||||
promise.then((v) => { clearTimeout(timer); resolve(v) }, (e) => { clearTimeout(timer); reject(e) })
|
||||
})
|
||||
}
|
||||
|
||||
async function asDataUrl(src, timeoutMs) {
|
||||
const res = await withTimeout(fetch(src, { mode: 'cors', cache: 'force-cache' }), timeoutMs)
|
||||
if (!res.ok) throw new Error(`photo ${res.status}`)
|
||||
const blob = await res.blob()
|
||||
return new Promise((resolve, reject) => {
|
||||
const r = new FileReader()
|
||||
r.onload = () => resolve(r.result)
|
||||
r.onerror = () => reject(r.error)
|
||||
r.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html the whole printable document, exactly as the renderer builds it
|
||||
* @param {object} o
|
||||
* page CSS selector of one page (".card", ".sheet"); the first match is kept
|
||||
* width the page's width in CSS px (the renderer's W)
|
||||
* height the page's height in CSS px (the renderer's H)
|
||||
* timeoutMs per photo
|
||||
* @returns {Promise<string|null>} an image URL (image/svg+xml), or null when the document has no page
|
||||
*/
|
||||
export async function firstPageImage(html, { page, width, height, timeoutMs = 8000 } = {}) {
|
||||
if (!html || !page || !width || !height) return null
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||
const first = doc.body && doc.body.querySelector(page)
|
||||
if (!first) return null
|
||||
// Keep only the body child that holds the first page.
|
||||
let top = first
|
||||
while (top.parentElement && top.parentElement !== doc.body) top = top.parentElement
|
||||
for (const n of [...doc.body.childNodes]) if (n !== top) n.remove()
|
||||
|
||||
const sameOrigin = (src) => { try { return new URL(src, document.baseURI).origin === location.origin } catch (_) { return false } }
|
||||
const photos = [...top.querySelectorAll('img[src]')].filter((img) => !/^data:/i.test(img.getAttribute('src')))
|
||||
let fetched = 0
|
||||
for (const img of photos) {
|
||||
const src = img.getAttribute('src')
|
||||
if (fetched < MAX_PHOTOS && sameOrigin(src)) {
|
||||
fetched++
|
||||
try { img.setAttribute('src', await asDataUrl(src, timeoutMs)); continue } catch (_) { /* left out below */ }
|
||||
}
|
||||
img.removeAttribute('src')
|
||||
}
|
||||
|
||||
const xhtml = new XMLSerializer().serializeToString(doc.documentElement)
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`
|
||||
+ `<foreignObject x="0" y="0" width="${width}" height="${height}">${xhtml}</foreignObject></svg>`
|
||||
return URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }))
|
||||
}
|
||||
69
plugin-designs/poster-design/src/panelsSvg.js
Normal file
69
plugin-designs/poster-design/src/panelsSvg.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* panelsSvg.js: a poster printed on several sheets, drawn as the sheets pulled apart, each join a red glue
|
||||
* band, as one SVG image for a poster card's thumbnail.
|
||||
*
|
||||
* The geometry is the shared panel plan (library/components/poster/panelSheets.js planPanels + screenLayout):
|
||||
* the same sheets, joins and content boxes the print uses. Inside each sheet's content box sit rows of cards,
|
||||
* the way the Bless wall fills its 5 × 8 ft panels: a banner on the first sheet, a footer on the last, and
|
||||
* card rows that never cross a join. The cards are a LAYOUT (blocks, no words): the Bless wall draws its real
|
||||
* cards in its own plugin, which a library-only gallery cannot import.
|
||||
*/
|
||||
import { planPanels, screenLayout } from '@map/components/poster/panelSheets.js'
|
||||
|
||||
const PAPER = '#f7f4ec'
|
||||
const INK = '#3b463d'
|
||||
const ACCENT = '#6B4A78' // the Bless wall's purple
|
||||
const PHOTO = '#e9e3d4'
|
||||
const LINE = '#c9c3b3'
|
||||
|
||||
/**
|
||||
* @param {{ sheetW, sheetH, count, direction, joinIn }} sheets a size's `sheets` from the shared ladder
|
||||
* @param {{ borderIn?: number, gapIn?: number, cols?: number }} o
|
||||
* @returns {string} an image URL (a data: URL of an SVG), drawn in inches
|
||||
*/
|
||||
export function panelsSvgUrl(sheets, { borderIn = sheets.joinIn || 2, gapIn = 0.75, cols = 14 } = {}) {
|
||||
const plan = planPanels({ ...sheets, borderIn, joinFill: 'glue' })
|
||||
const L = screenLayout(plan, { gapIn, pxPerIn: 1 })
|
||||
const W = L.worldW, H = L.worldH
|
||||
const s1 = Math.min(plan.sheetW, plan.sheetH)
|
||||
const stroke = s1 / 200
|
||||
let out = ''
|
||||
for (const sh of plan.sheets) {
|
||||
const at = L.sheets[sh.s]
|
||||
const ox = at.x, oy = at.y
|
||||
out += `<rect x="${ox}" y="${oy}" width="${plan.sheetW}" height="${plan.sheetH}" fill="${PAPER}" stroke="${LINE}" stroke-width="${stroke}"/>`
|
||||
const c = sh.content
|
||||
let top = oy + c.y0, bottom = oy + c.y1
|
||||
const cw0 = c.x1 - c.x0
|
||||
if (sh.first) { // the banner: a title band and the prayer-themes table
|
||||
const bh = cw0 * 0.075, th = cw0 * 0.17
|
||||
out += `<rect x="${ox + c.x0}" y="${top}" width="${cw0}" height="${bh}" fill="${INK}"/>`
|
||||
out += `<rect x="${ox + c.x0}" y="${top + bh + cw0 * 0.01}" width="${cw0}" height="${th}" fill="none" stroke="${ACCENT}" stroke-width="${stroke * 2}"/>`
|
||||
top += bh + th + cw0 * 0.025
|
||||
}
|
||||
if (sh.last) { // the footer: the blessing and the closing line
|
||||
const fh = cw0 * 0.09
|
||||
out += `<rect x="${ox + c.x0}" y="${bottom - fh}" width="${cw0}" height="${fh}" fill="${ACCENT}" opacity=".85"/>`
|
||||
bottom -= fh + cw0 * 0.015
|
||||
}
|
||||
// rows of portrait cards (0.8 aspect), centred in the band, never crossing a join
|
||||
const cw = cw0 / cols, ch = cw / 0.8
|
||||
const rows = Math.max(0, Math.floor((bottom - top) / ch))
|
||||
const y0 = top + Math.max(0, (bottom - top) - rows * ch) / 2
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let k = 0; k < cols; k++) {
|
||||
const x = ox + c.x0 + k * cw, y = y0 + r * ch
|
||||
out += `<rect x="${x + cw * 0.08}" y="${y + ch * 0.06}" width="${cw * 0.84}" height="${cw * 0.84}" fill="${PHOTO}"/>`
|
||||
out += `<rect x="${x + cw * 0.15}" y="${y + ch * 0.06 + cw * 0.9}" width="${cw * 0.7}" height="${ch * 0.06}" fill="${INK}" opacity=".7"/>`
|
||||
}
|
||||
}
|
||||
if (sh.band) { // the glue band: red, dashed at its edge, as PanelSheets draws it
|
||||
const b = sh.band
|
||||
const ex = plan.direction === 'vertical' ? `x1="${ox}" y1="${oy + b.h}" x2="${ox + b.w}" y2="${oy + b.h}"` : `x1="${ox + b.w}" y1="${oy}" x2="${ox + b.w}" y2="${oy + b.h}"`
|
||||
out += `<rect x="${ox + b.x}" y="${oy + b.y}" width="${b.w}" height="${b.h}" fill="rgba(176,0,0,.14)"/>`
|
||||
out += `<line ${ex} stroke="#b00" stroke-width="${stroke * 1.5}" stroke-dasharray="${stroke * 6} ${stroke * 4}"/>`
|
||||
}
|
||||
}
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${(W * 4).toFixed(0)}" height="${(H * 4).toFixed(0)}">${out}</svg>`
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
|
||||
}
|
||||
|
|
@ -7,8 +7,11 @@
|
|||
* its name and is marked legacy.
|
||||
*
|
||||
* Each entry says where its renderer lives in library/ and, when the gallery can draw it, how:
|
||||
* render(ctx) returns the template's HTML document drawn from the LIBRARY with the sample data, so a
|
||||
* library change shows up in the gallery the next time it opens. Entries the gallery cannot draw yet
|
||||
* render(ctx) draws the template from the LIBRARY with the sample data, so a library change shows up in
|
||||
* the gallery the next time it opens. It returns either a poster's HTML (a map sheet: an <svg> the poster
|
||||
* rasteriser lifts) or a printable DOCUMENT as { html, page, width, height } (a deck, a packet: HTML pages,
|
||||
* whose first page becomes the thumbnail, src/pageThumb.js) or a ready image as { image } (panels drawn from
|
||||
* the shared panel geometry, src/panelsSvg.js). Entries the gallery cannot draw yet
|
||||
* say so (render: null) and the card shows the wall's "preview unavailable" placeholder; the details
|
||||
* dialog states which part is missing, rather than a picture of something else.
|
||||
*
|
||||
|
|
@ -74,8 +77,8 @@ export function listConfigurations() {
|
|||
renderer: 'library/packet/packet-render.js buildPacket (VARIANTS)',
|
||||
settings: { variant },
|
||||
sample: SAMPLE_COUNTRY_ID,
|
||||
render: null,
|
||||
missing: 'posterDesign.missing.packet',
|
||||
// Page 1 of the packet (its map page) as the thumbnail; ctx.packetDocument builds it.
|
||||
render: (ctx) => ctx.packetDocument(variant),
|
||||
})),
|
||||
...Object.entries(STOCK).map(([id, s]) => ({
|
||||
name: `cards.${id}`,
|
||||
|
|
@ -86,18 +89,19 @@ export function listConfigurations() {
|
|||
renderer: 'library/packet/card-render.js createCardRenderer (STOCK)',
|
||||
settings: { stock: id },
|
||||
sample: SAMPLE_COUNTRY_ID,
|
||||
render: null,
|
||||
missing: 'posterDesign.missing.cards',
|
||||
// The first card of the sample's deck; ctx.cardDeck builds the whole deck the wall would print.
|
||||
render: (ctx) => ctx.cardDeck(id),
|
||||
})),
|
||||
{
|
||||
name: 'wall.bless-panels',
|
||||
family: 'wall',
|
||||
labelKey: 'posterDesign.tpl.blessPanels',
|
||||
size: '5 × 8 ft, 3 panels',
|
||||
renderer: 'plugin-designs/wall-of-people-groups/src/blessPoster.js (not in library/ yet)',
|
||||
settings: {},
|
||||
renderer: 'library/components/poster/panelSheets.js planPanels (the sheets, joins and content boxes); the cards are drawn by plugin-designs/wall-of-people-groups/src/blessPoster.js',
|
||||
settings: { size: '60x96', joinFill: 'glue' },
|
||||
sample: null,
|
||||
render: null,
|
||||
// The panel geometry of the ladder's 5 × 8 ft size, with the card rows as a layout (src/panelsSvg.js).
|
||||
render: (ctx) => ctx.panels('60x96'),
|
||||
missing: 'posterDesign.missing.bless',
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -261,8 +261,22 @@ const FORMATS = {
|
|||
// about 1,600px of height against the letter sheet's ~1,200, which is what lets the names grow.
|
||||
// Output: <zone>.packet.html beside the posters (see FILE_SUFFIX), never over them.
|
||||
packet: { w: 1500, h: 1941, gutter: 330, page: 'letter portrait', pageWidthIn: 8.5, mapOnly: true, header: 250, footer: 90 },
|
||||
// ★ LARGE SHEETS WITH SIDE LANES (`sideLanes`): a whole country's hundreds of groups, every one named horizontally
|
||||
// in two staggered columns per side, ONE leader per cluster of co-located groups, crossing-free by construction
|
||||
// (see sideLanesLayout). Same 41.67 px/in as the 3x3, so the header, pins and footer keep their physical size.
|
||||
// `nameMaxPt` caps the names' print size; the layout solves the largest size that fits the rows and columns.
|
||||
'60x96': { w: 2500, h: 4000, gutter: 572, page: '60in 96in', pageWidthIn: 60, sideLanes: true, nameMaxPt: 20.7, photos: true },
|
||||
'66x66': { w: 2750, h: 2750, gutter: 470, page: '66in 66in', pageWidthIn: 66, sideLanes: true, nameMaxPt: 18, photos: true },
|
||||
'36x60': { w: 1500, h: 2500, gutter: 400, page: '36in 60in', pageWidthIn: 36, sideLanes: true, nameMaxPt: 14, photos: false },
|
||||
};
|
||||
const FMT_KEY = FORMATS[process.env.FORMAT] ? process.env.FORMAT : '3x4';
|
||||
// The side-lane marks' styles: emitted ONLY on a sideLanes sheet, so every other format's page is byte-identical.
|
||||
const SIDE_LANES_CSS = `
|
||||
.slead{stroke:#5a5246;stroke-width:0.9;fill:none;opacity:.5}
|
||||
.sbr{stroke:#5a5246;stroke-width:1.1;fill:none}
|
||||
.srow{stroke:#b9ac8c;stroke-width:.3;fill:none;opacity:.7}
|
||||
.spin{fill:#1c1c17;stroke:#f7f4ec;stroke-width:.8}
|
||||
.snm{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-weight:700;fill:#1c1c17}`;
|
||||
const FMT = FORMATS[FMT_KEY];
|
||||
// Map-only sheets (packet page 1) skip the poster's header and footer furniture — see FORMATS.packet.
|
||||
const MAP_ONLY = FMT.mapOnly === true;
|
||||
|
|
@ -854,7 +868,9 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
const floorDeg = CITIES ? _floor(Math.max(_w, _h)) : 1;
|
||||
// PER-AXIS floor. A collinear cluster (zero span on one axis) must not be padded
|
||||
// as if it were as wide as the other axis — that is what produced a 0.0% spread.
|
||||
const pad = CITIES
|
||||
// A side-lane sheet is the COUNTRY at its largest: a thin margin, since the names, not neighbours, fill the sides.
|
||||
const pad = FMT.sideLanes ? [_w * 0.04, _h * 0.04]
|
||||
: CITIES
|
||||
? [_w * 0.22 + _floor(Math.max(_w, _h * 0.35)), _h * 0.22 + _floor(Math.max(_h, _w * 0.35))]
|
||||
: [_w * 0.22 + floorDeg, _h * 0.22 + floorDeg];
|
||||
const viewBbox = [bbox[0] - pad[0], bbox[1] - pad[1], bbox[2] + pad[0], bbox[3] + pad[1]];
|
||||
|
|
@ -884,12 +900,14 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
// frame is exactly what shows them.
|
||||
// FULL SHEET HEIGHT — the map runs from just under the header to just above the
|
||||
// footer rule, so there is no dead band anywhere on the page.
|
||||
const FRAME_H = MAP_H;
|
||||
// A side-lane sheet fits the frame to the country's own shape and centres it: its names run the sheet's full height
|
||||
// beside it, so the frame need not, and a full-height frame would only shrink the country to show its neighbours.
|
||||
const FRAME_H = FMT.sideLanes ? Math.min(MAP_H, FRAME_W / zoneAspect) : MAP_H;
|
||||
// OPTICAL CENTRING, not geometric. A block centred by arithmetic looks like it
|
||||
// is falling off the bottom of the sheet — the optical centre sits ABOVE the
|
||||
// geometric one, which is why framers cut a bottom mat 1.25-1.4x the top. Here
|
||||
// the remainder is split 42/58 so the bottom margin is the larger one.
|
||||
const FRAME_Y = MAP_Y;
|
||||
const FRAME_Y = FMT.sideLanes ? MAP_Y + (MAP_H - FRAME_H) / 2 : MAP_Y;
|
||||
// PROJECT FROM viewBbox, NOT bbox.
|
||||
// These two had drifted apart: `viewBbox` (bbox + 22% context pad) decides WHICH
|
||||
// countries are drawn, while the projection scaled from the bare `bbox` — so the
|
||||
|
|
@ -1247,7 +1265,8 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
//
|
||||
// This is what makes leaders viable again: leaders can only be routed without
|
||||
// crossing if their endpoints are distinct, and the endpoints are these pins.
|
||||
{
|
||||
// A side-lane sheet keeps every pin at its TRUE position: co-located groups share one leader instead.
|
||||
if (!FMT.sideLanes) {
|
||||
// MIN_SEP MUST TRACK THE BADGE SIZE. It was a hardcoded 25 tuned against the
|
||||
// old r=10.8 disc; leaving it fixed while the badge shrinks would keep pushing
|
||||
// pins just as far apart and defeat the whole point of the smaller badge.
|
||||
|
|
@ -1294,7 +1313,126 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
// Feed the settled pixel positions back as lng/lat-free overrides.
|
||||
const settled = prePins.map((p) => ({ ...p, _fixed: true }));
|
||||
|
||||
const laid = layoutLabels(settled, proj);
|
||||
// ── SIDE LANES: a whole country's groups on one large sheet (FMT.sideLanes) ───────────────────────────
|
||||
// Every name HORIZONTAL, on the left and right of the map, in two STAGGERED columns per side: the rows alternate
|
||||
// inner/outer at half the line pitch, so each column keeps a full pitch and each row holds exactly ONE name.
|
||||
// Co-located groups (within CL_R px) form a CLUSTER with ONE leader, a bracket and a block of names.
|
||||
// CROSSING-FREE BY CONSTRUCTION, which the numbered design below could not reach at this density:
|
||||
// · one vertical split (the weighted median x) sends each cluster to the nearer side, so runs never meet;
|
||||
// · each leader is a short vertical stub to its own LANE, a horizontal run to the map edge, then a straight fan
|
||||
// to its block; lanes are placed FARTHEST-FROM-THE-EDGE first, each clamped between its neighbouring lanes,
|
||||
// so a stub never jumps a lane, and the fan keeps the lanes' top-to-bottom order (ordered points on two
|
||||
// parallel lines, joined in order, cannot cross). The count is measured and returned, never assumed.
|
||||
// A real photo (never the generic placeholder) sits on the OUTSIDE of its name when the format allows photos.
|
||||
function sideLanesLayout(pinsIn) {
|
||||
const X0 = MAP_X, X1 = MAP_X + MAP_W;
|
||||
const BY0 = LABEL_TOP ?? FRAME_Y, BY1 = LABEL_BOT ?? (FRAME_Y + FRAME_H);
|
||||
const PXI = PW / FMT.pageWidthIn;
|
||||
const pins = pinsIn.map((p) => ({ ...p, px: p._px, py: p._py }));
|
||||
// A photo only from the known people-group photo hosts, over https, and never the generic placeholder.
|
||||
const PHOTO_HOSTS = /^https:\/\/(media\.joshuaproject\.net|joshuaproject\.net|images\.peoplegroups\.org|peoplegroupspublic\.blob\.core\.windows\.net)\//;
|
||||
const photoOf = (p) => (FMT.photos && typeof p.image_url === 'string' && PHOTO_HOSTS.test(p.image_url) && !/no-photo-images/.test(p.image_url) ? p.image_url : null);
|
||||
const CL_R = 14, clusters = [];
|
||||
for (const p of pins) {
|
||||
const c = clusters.find((q) => Math.hypot(q.x - p.px, q.y - p.py) <= CL_R);
|
||||
if (c) { c.items.push(p); c.x += (p.px - c.x) / c.items.length; c.y += (p.py - c.y) / c.items.length; }
|
||||
else clusters.push({ x: p.px, y: p.py, items: [p] });
|
||||
}
|
||||
for (const c of clusters) c.items.sort((a, b) => String(a.name).localeCompare(String(b.name), 'en'));
|
||||
const byX = [...clusters].sort((a, b) => a.x - b.x);
|
||||
let acc = 0, split = byX.length ? byX[0].x : 0;
|
||||
for (const c of byX) { acc += c.items.length; if (acc >= pins.length / 2) { split = c.x; break; } }
|
||||
for (const c of clusters) c.side = c.x <= split ? 'L' : 'R';
|
||||
// THE TYPE: the largest size (capped by nameMaxPt) whose rows fit the band and whose two columns fit the gutter.
|
||||
const FAN = 70, GAP = 10, PHOTO_GAP = 4;
|
||||
const room = GUTTER - (SHEET_EDGE + 8) - FAN - GAP;
|
||||
const photoPx = (f) => Math.floor(f * 1.18) - 1;
|
||||
const longest = (f) => Math.max(...pins.map((p) => textW(String(p.name), f) * 1.06 + (photoOf(p) ? photoPx(f) + PHOTO_GAP : 0)));
|
||||
const lineCount = (sd) => clusters.filter((c) => c.side === sd).reduce((a, c) => a + c.items.length, 0);
|
||||
const maxLines = Math.max(1, lineCount('L'), lineCount('R'));
|
||||
let font = Math.min((FMT.nameMaxPt || 14) / 72 * PXI, (BY1 - BY0) / (maxLines * 0.59 * 1.02));
|
||||
while (font > 5 && 2 * (longest(font) + 8) > room) font -= 0.1;
|
||||
const ROW = font * 1.18 / 2, PHOTO = photoPx(font), COLW = longest(font) + 8;
|
||||
// LANES, farthest from the edge first (see above). PIN_CLEAR: within its safe gap a lane prefers a height
|
||||
// that passes no other pin, at most 7.5 px from its own pin.
|
||||
const LANE_GAP = 1.5, PIN_CLEAR = 4.5;
|
||||
let laneJumps = 0;
|
||||
for (const sd of ['L', 'R']) {
|
||||
const L = sd === 'L';
|
||||
const mine = clusters.filter((c) => c.side === sd).sort((a, b) => (L ? b.x - a.x : a.x - b.x));
|
||||
const lanes = [];
|
||||
for (const c of mine) {
|
||||
let lo = -Infinity, hi = Infinity;
|
||||
for (const ly of lanes) { if (ly < c.y && ly > lo) lo = ly; if (ly >= c.y && ly < hi) hi = ly; }
|
||||
let y = Math.min(Math.max(c.y, lo + LANE_GAP), hi - LANE_GAP);
|
||||
if (lo + LANE_GAP > hi - LANE_GAP) { y = (lo + hi) / 2; laneJumps++; }
|
||||
else {
|
||||
const a0 = L ? X0 : c.x, a1 = L ? c.x : X1;
|
||||
const blockers = clusters.filter((o) => o !== c && o.x > a0 + 1 && o.x < a1 - 1);
|
||||
let best = y, bestHits = Infinity;
|
||||
for (let t = Math.max(lo + LANE_GAP, c.y - 7.5); t <= Math.min(hi - LANE_GAP, c.y + 7.5); t += 0.75) {
|
||||
const hits = blockers.filter((o) => Math.abs(o.y - t) < PIN_CLEAR).length;
|
||||
if (hits < bestHits || (hits === bestHits && Math.abs(t - c.y) < Math.abs(best - c.y))) { best = t; bestHits = hits; }
|
||||
}
|
||||
if (bestHits !== Infinity) y = best;
|
||||
}
|
||||
c.lane = y; lanes.push(y);
|
||||
}
|
||||
}
|
||||
// BLOCKS, stacked top to bottom in lane order, each as near its lane as the rows allow.
|
||||
const rowsAvail = Math.floor((BY1 - BY0) / ROW);
|
||||
let leadersS = '', labelsS = '', dotsS = '', overflow = 0;
|
||||
const placed = [];
|
||||
for (const sd of ['L', 'R']) {
|
||||
const L = sd === 'L';
|
||||
const blocks = clusters.filter((c) => c.side === sd).sort((a, b) => a.lane - b.lane)
|
||||
.map((c) => ({ c, n: c.items.length, want: (c.lane - BY0) / ROW }));
|
||||
const pos = []; let cur = 0;
|
||||
for (const b of blocks) { const q = Math.max(cur, Math.round(b.want - b.n / 2)); pos.push(q); cur = q + b.n; }
|
||||
let over = cur - rowsAvail;
|
||||
for (let k = pos.length - 1; k >= 0 && over > 0; k--) {
|
||||
const lim = k ? pos[k - 1] + blocks[k - 1].n : 0, can = Math.min(over, pos[k] - lim);
|
||||
for (let j = k; j < pos.length; j++) pos[j] -= can;
|
||||
over -= can;
|
||||
}
|
||||
overflow += Math.max(0, over);
|
||||
const edgeX = L ? X0 : X1, portX = L ? X0 - FAN : X1 + FAN;
|
||||
const innerX = L ? portX - 6 : portX + 6, outerX = L ? innerX - COLW : innerX + COLW;
|
||||
blocks.forEach((b, k) => {
|
||||
const c = b.c, r0 = pos[k], midY = BY0 + (r0 + b.n / 2) * ROW;
|
||||
leadersS += `<path class="slead" d="M${c.x.toFixed(1)} ${c.y.toFixed(1)}L${c.x.toFixed(1)} ${c.lane.toFixed(2)}L${edgeX.toFixed(1)} ${c.lane.toFixed(2)}L${portX.toFixed(1)} ${midY.toFixed(1)}"/>`;
|
||||
if (b.n > 1) leadersS += `<path class="sbr" d="M${portX.toFixed(1)} ${(BY0 + r0 * ROW + 1).toFixed(1)}V${(BY0 + (r0 + b.n) * ROW - 1).toFixed(1)}"/>`;
|
||||
dotsS += `<circle class="spin" cx="${c.x.toFixed(1)}" cy="${c.y.toFixed(1)}" r="${b.n > 1 ? 4.4 : 3.2}"/>`;
|
||||
c.items.forEach((g, j) => {
|
||||
const r = r0 + j, rowY = BY0 + (r + 0.5) * ROW, x = r % 2 === 0 ? innerX : outerX;
|
||||
if (r % 2 === 1) leadersS += `<path class="srow" d="M${innerX.toFixed(1)} ${rowY.toFixed(1)}H${(L ? outerX + 3 : outerX - 3).toFixed(1)}"/>`;
|
||||
labelsS += `<text class="snm" x="${x.toFixed(1)}" y="${(rowY + font * 0.34).toFixed(1)}" font-size="${font.toFixed(2)}"${L ? ' text-anchor="end"' : ''}>${esc(String(g.name))}</text>`;
|
||||
const ph = photoOf(g);
|
||||
if (ph) {
|
||||
const tw = textW(String(g.name), font) * 1.06;
|
||||
const px = L ? x - tw - PHOTO_GAP - PHOTO : x + tw + PHOTO_GAP;
|
||||
labelsS += `<image href="${esc(ph)}" x="${px.toFixed(1)}" y="${(rowY - PHOTO / 2).toFixed(1)}" width="${PHOTO}" height="${PHOTO}" preserveAspectRatio="xMidYMid slice"/>`;
|
||||
}
|
||||
placed.push({ g, side: sd, r, photo: !!ph });
|
||||
});
|
||||
});
|
||||
}
|
||||
// MEASURED, not assumed: leader crossings (every segment pair of different leaders).
|
||||
const polys = [...leadersS.matchAll(/class="slead" d="M([^"]+)"/g)].map((m) => m[1].split('L').map((q) => q.trim().split(' ').map(Number)));
|
||||
const segsOf = polys.map((pl) => pl.slice(1).map((q, k) => [pl[k], q]));
|
||||
const sx = (p, q, r) => (r[1] - p[1]) * (q[0] - p[0]) - (q[1] - p[1]) * (r[0] - p[0]);
|
||||
const segX = (a, b) => (sx(a[0], a[1], b[0]) > 0) !== (sx(a[0], a[1], b[1]) > 0) && (sx(b[0], b[1], a[0]) > 0) !== (sx(b[0], b[1], a[1]) > 0);
|
||||
let crossingsS = 0;
|
||||
for (let i = 0; i < segsOf.length; i++) for (let j = i + 1; j < segsOf.length; j++) if (segsOf[i].some((a) => segsOf[j].some((b) => segX(a, b)))) crossingsS++;
|
||||
const stats = { names: placed.length, pins: pins.length, clusters: clusters.length, crossings: crossingsS, overflowRows: overflow,
|
||||
laneJumps, photos: placed.filter((p) => p.photo).length, namePt: +(font / PXI * 72).toFixed(1), photoMm: FMT.photos ? +(PHOTO / PXI * 25.4).toFixed(1) : 0 };
|
||||
return { leaders: leadersS, labels: labelsS, pinDots: dotsS, crossings: crossingsS, pins, stats };
|
||||
}
|
||||
|
||||
const laid = FMT.sideLanes ? [] : layoutLabels(settled, proj);
|
||||
const sides = FMT.sideLanes ? sideLanesLayout(settled) : null;
|
||||
// What the city layer must keep clear of: the drawn pins (laid out, or true positions on a side-lane sheet).
|
||||
const avoidPins = sides ? sides.pins : laid;
|
||||
|
||||
// ── CAPITALS + MAJOR CITIES ────────────────────────────────────────────────
|
||||
// Orientation marks, deliberately SUBORDINATE to the people-group pins. Imhof's
|
||||
|
|
@ -1367,7 +1505,7 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
// projected point is not where the badge is drawn. Measuring against the
|
||||
// source coordinate let Saravan's label land 1.4px from a badge while the
|
||||
// guard believed it was clear. This block therefore runs AFTER layoutLabels.
|
||||
for (const pin of laid) {
|
||||
for (const pin of avoidPins) {
|
||||
const px = pin.px, py = pin.py;
|
||||
const dx = px < x0 ? x0 - px : (px > x1 ? px - x1 : 0);
|
||||
const dy = py < yTop ? yTop - py : (py > yBot ? py - yBot : 0);
|
||||
|
|
@ -1697,8 +1835,9 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
const tx = it.laneX + it.dirOut * 5;
|
||||
labels += `<text x="${tx.toFixed(1)}" y="${(it.labelY + it.font * 0.34).toFixed(1)}" text-anchor="${it.tanchor}" font-size="${it.font.toFixed(1)}" class="lbl">${esc(it.name)}</text>`;
|
||||
});
|
||||
if (sides) { leaders += sides.leaders; pinDots += sides.pinDots; labels += sides.labels; }
|
||||
if (!octiOK) console.warn(` ⚠ non-octilinear segment in ${zone.zoneKey}`);
|
||||
const crossings = countPolyCrossings(routes);
|
||||
const crossings = sides ? sides.crossings : countPolyCrossings(routes);
|
||||
const diagFrac = bestD;
|
||||
|
||||
// ── TITLE vs SUBTITLE ──────────────────────────────────────────────────────
|
||||
|
|
@ -2678,8 +2817,8 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
page. Collapse the flow height to the scaled height. */
|
||||
html,body{width:${(FMT.pageWidthIn * 96).toFixed(0)}px;height:${(FMT.pageWidthIn * 96 * FMT.h / FMT.w).toFixed(0)}px;overflow:hidden}
|
||||
}
|
||||
</style></head><body><div class="stage">${svg}</div></body></html>`;
|
||||
return { html, title, octiOK, crossings, diagFrac };
|
||||
${FMT.sideLanes ? SIDE_LANES_CSS + '\n' : ''}</style></head><body><div class="stage">${svg}</div></body></html>`;
|
||||
return sides ? { html, title, octiOK, crossings, diagFrac, sideLanes: sides.stats } : { html, title, octiOK, crossings, diagFrac };
|
||||
}
|
||||
|
||||
return { renderPoster, CITIES, COUNTRIES, FILE_SUFFIX, FMT_KEY, FMT, MAP_ONLY, FORMATS };
|
||||
|
|
|
|||
|
|
@ -259,8 +259,22 @@ const FORMATS = {
|
|||
// about 1,600px of height against the letter sheet's ~1,200, which is what lets the names grow.
|
||||
// Output: <zone>.packet.html beside the posters (see FILE_SUFFIX), never over them.
|
||||
packet: { w: 1500, h: 1941, gutter: 330, page: 'letter portrait', pageWidthIn: 8.5, mapOnly: true, header: 250, footer: 90 },
|
||||
// ★ LARGE SHEETS WITH SIDE LANES (`sideLanes`): a whole country's hundreds of groups, every one named horizontally
|
||||
// in two staggered columns per side, ONE leader per cluster of co-located groups, crossing-free by construction
|
||||
// (see sideLanesLayout). Same 41.67 px/in as the 3x3, so the header, pins and footer keep their physical size.
|
||||
// `nameMaxPt` caps the names' print size; the layout solves the largest size that fits the rows and columns.
|
||||
'60x96': { w: 2500, h: 4000, gutter: 572, page: '60in 96in', pageWidthIn: 60, sideLanes: true, nameMaxPt: 20.7, photos: true },
|
||||
'66x66': { w: 2750, h: 2750, gutter: 470, page: '66in 66in', pageWidthIn: 66, sideLanes: true, nameMaxPt: 18, photos: true },
|
||||
'36x60': { w: 1500, h: 2500, gutter: 400, page: '36in 60in', pageWidthIn: 36, sideLanes: true, nameMaxPt: 14, photos: false },
|
||||
};
|
||||
const FMT_KEY = FORMATS[process.env.FORMAT] ? process.env.FORMAT : '3x4';
|
||||
// The side-lane marks' styles: emitted ONLY on a sideLanes sheet, so every other format's page is byte-identical.
|
||||
const SIDE_LANES_CSS = `
|
||||
.slead{stroke:#5a5246;stroke-width:0.9;fill:none;opacity:.5}
|
||||
.sbr{stroke:#5a5246;stroke-width:1.1;fill:none}
|
||||
.srow{stroke:#b9ac8c;stroke-width:.3;fill:none;opacity:.7}
|
||||
.spin{fill:#1c1c17;stroke:#f7f4ec;stroke-width:.8}
|
||||
.snm{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-weight:700;fill:#1c1c17}`;
|
||||
const FMT = FORMATS[FMT_KEY];
|
||||
// Map-only sheets (packet page 1) skip the poster's header and footer furniture — see FORMATS.packet.
|
||||
const MAP_ONLY = FMT.mapOnly === true;
|
||||
|
|
@ -852,7 +866,9 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
const floorDeg = CITIES ? _floor(Math.max(_w, _h)) : 1;
|
||||
// PER-AXIS floor. A collinear cluster (zero span on one axis) must not be padded
|
||||
// as if it were as wide as the other axis — that is what produced a 0.0% spread.
|
||||
const pad = CITIES
|
||||
// A side-lane sheet is the COUNTRY at its largest: a thin margin, since the names, not neighbours, fill the sides.
|
||||
const pad = FMT.sideLanes ? [_w * 0.04, _h * 0.04]
|
||||
: CITIES
|
||||
? [_w * 0.22 + _floor(Math.max(_w, _h * 0.35)), _h * 0.22 + _floor(Math.max(_h, _w * 0.35))]
|
||||
: [_w * 0.22 + floorDeg, _h * 0.22 + floorDeg];
|
||||
const viewBbox = [bbox[0] - pad[0], bbox[1] - pad[1], bbox[2] + pad[0], bbox[3] + pad[1]];
|
||||
|
|
@ -882,12 +898,14 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
// frame is exactly what shows them.
|
||||
// FULL SHEET HEIGHT — the map runs from just under the header to just above the
|
||||
// footer rule, so there is no dead band anywhere on the page.
|
||||
const FRAME_H = MAP_H;
|
||||
// A side-lane sheet fits the frame to the country's own shape and centres it: its names run the sheet's full height
|
||||
// beside it, so the frame need not, and a full-height frame would only shrink the country to show its neighbours.
|
||||
const FRAME_H = FMT.sideLanes ? Math.min(MAP_H, FRAME_W / zoneAspect) : MAP_H;
|
||||
// OPTICAL CENTRING, not geometric. A block centred by arithmetic looks like it
|
||||
// is falling off the bottom of the sheet — the optical centre sits ABOVE the
|
||||
// geometric one, which is why framers cut a bottom mat 1.25-1.4x the top. Here
|
||||
// the remainder is split 42/58 so the bottom margin is the larger one.
|
||||
const FRAME_Y = MAP_Y;
|
||||
const FRAME_Y = FMT.sideLanes ? MAP_Y + (MAP_H - FRAME_H) / 2 : MAP_Y;
|
||||
// PROJECT FROM viewBbox, NOT bbox.
|
||||
// These two had drifted apart: `viewBbox` (bbox + 22% context pad) decides WHICH
|
||||
// countries are drawn, while the projection scaled from the bare `bbox` — so the
|
||||
|
|
@ -1245,7 +1263,8 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
//
|
||||
// This is what makes leaders viable again: leaders can only be routed without
|
||||
// crossing if their endpoints are distinct, and the endpoints are these pins.
|
||||
{
|
||||
// A side-lane sheet keeps every pin at its TRUE position: co-located groups share one leader instead.
|
||||
if (!FMT.sideLanes) {
|
||||
// MIN_SEP MUST TRACK THE BADGE SIZE. It was a hardcoded 25 tuned against the
|
||||
// old r=10.8 disc; leaving it fixed while the badge shrinks would keep pushing
|
||||
// pins just as far apart and defeat the whole point of the smaller badge.
|
||||
|
|
@ -1292,7 +1311,126 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
// Feed the settled pixel positions back as lng/lat-free overrides.
|
||||
const settled = prePins.map((p) => ({ ...p, _fixed: true }));
|
||||
|
||||
const laid = layoutLabels(settled, proj);
|
||||
// ── SIDE LANES: a whole country's groups on one large sheet (FMT.sideLanes) ───────────────────────────
|
||||
// Every name HORIZONTAL, on the left and right of the map, in two STAGGERED columns per side: the rows alternate
|
||||
// inner/outer at half the line pitch, so each column keeps a full pitch and each row holds exactly ONE name.
|
||||
// Co-located groups (within CL_R px) form a CLUSTER with ONE leader, a bracket and a block of names.
|
||||
// CROSSING-FREE BY CONSTRUCTION, which the numbered design below could not reach at this density:
|
||||
// · one vertical split (the weighted median x) sends each cluster to the nearer side, so runs never meet;
|
||||
// · each leader is a short vertical stub to its own LANE, a horizontal run to the map edge, then a straight fan
|
||||
// to its block; lanes are placed FARTHEST-FROM-THE-EDGE first, each clamped between its neighbouring lanes,
|
||||
// so a stub never jumps a lane, and the fan keeps the lanes' top-to-bottom order (ordered points on two
|
||||
// parallel lines, joined in order, cannot cross). The count is measured and returned, never assumed.
|
||||
// A real photo (never the generic placeholder) sits on the OUTSIDE of its name when the format allows photos.
|
||||
function sideLanesLayout(pinsIn) {
|
||||
const X0 = MAP_X, X1 = MAP_X + MAP_W;
|
||||
const BY0 = LABEL_TOP ?? FRAME_Y, BY1 = LABEL_BOT ?? (FRAME_Y + FRAME_H);
|
||||
const PXI = PW / FMT.pageWidthIn;
|
||||
const pins = pinsIn.map((p) => ({ ...p, px: p._px, py: p._py }));
|
||||
// A photo only from the known people-group photo hosts, over https, and never the generic placeholder.
|
||||
const PHOTO_HOSTS = /^https:\/\/(media\.joshuaproject\.net|joshuaproject\.net|images\.peoplegroups\.org|peoplegroupspublic\.blob\.core\.windows\.net)\//;
|
||||
const photoOf = (p) => (FMT.photos && typeof p.image_url === 'string' && PHOTO_HOSTS.test(p.image_url) && !/no-photo-images/.test(p.image_url) ? p.image_url : null);
|
||||
const CL_R = 14, clusters = [];
|
||||
for (const p of pins) {
|
||||
const c = clusters.find((q) => Math.hypot(q.x - p.px, q.y - p.py) <= CL_R);
|
||||
if (c) { c.items.push(p); c.x += (p.px - c.x) / c.items.length; c.y += (p.py - c.y) / c.items.length; }
|
||||
else clusters.push({ x: p.px, y: p.py, items: [p] });
|
||||
}
|
||||
for (const c of clusters) c.items.sort((a, b) => String(a.name).localeCompare(String(b.name), 'en'));
|
||||
const byX = [...clusters].sort((a, b) => a.x - b.x);
|
||||
let acc = 0, split = byX.length ? byX[0].x : 0;
|
||||
for (const c of byX) { acc += c.items.length; if (acc >= pins.length / 2) { split = c.x; break; } }
|
||||
for (const c of clusters) c.side = c.x <= split ? 'L' : 'R';
|
||||
// THE TYPE: the largest size (capped by nameMaxPt) whose rows fit the band and whose two columns fit the gutter.
|
||||
const FAN = 70, GAP = 10, PHOTO_GAP = 4;
|
||||
const room = GUTTER - (SHEET_EDGE + 8) - FAN - GAP;
|
||||
const photoPx = (f) => Math.floor(f * 1.18) - 1;
|
||||
const longest = (f) => Math.max(...pins.map((p) => textW(String(p.name), f) * 1.06 + (photoOf(p) ? photoPx(f) + PHOTO_GAP : 0)));
|
||||
const lineCount = (sd) => clusters.filter((c) => c.side === sd).reduce((a, c) => a + c.items.length, 0);
|
||||
const maxLines = Math.max(1, lineCount('L'), lineCount('R'));
|
||||
let font = Math.min((FMT.nameMaxPt || 14) / 72 * PXI, (BY1 - BY0) / (maxLines * 0.59 * 1.02));
|
||||
while (font > 5 && 2 * (longest(font) + 8) > room) font -= 0.1;
|
||||
const ROW = font * 1.18 / 2, PHOTO = photoPx(font), COLW = longest(font) + 8;
|
||||
// LANES, farthest from the edge first (see above). PIN_CLEAR: within its safe gap a lane prefers a height
|
||||
// that passes no other pin, at most 7.5 px from its own pin.
|
||||
const LANE_GAP = 1.5, PIN_CLEAR = 4.5;
|
||||
let laneJumps = 0;
|
||||
for (const sd of ['L', 'R']) {
|
||||
const L = sd === 'L';
|
||||
const mine = clusters.filter((c) => c.side === sd).sort((a, b) => (L ? b.x - a.x : a.x - b.x));
|
||||
const lanes = [];
|
||||
for (const c of mine) {
|
||||
let lo = -Infinity, hi = Infinity;
|
||||
for (const ly of lanes) { if (ly < c.y && ly > lo) lo = ly; if (ly >= c.y && ly < hi) hi = ly; }
|
||||
let y = Math.min(Math.max(c.y, lo + LANE_GAP), hi - LANE_GAP);
|
||||
if (lo + LANE_GAP > hi - LANE_GAP) { y = (lo + hi) / 2; laneJumps++; }
|
||||
else {
|
||||
const a0 = L ? X0 : c.x, a1 = L ? c.x : X1;
|
||||
const blockers = clusters.filter((o) => o !== c && o.x > a0 + 1 && o.x < a1 - 1);
|
||||
let best = y, bestHits = Infinity;
|
||||
for (let t = Math.max(lo + LANE_GAP, c.y - 7.5); t <= Math.min(hi - LANE_GAP, c.y + 7.5); t += 0.75) {
|
||||
const hits = blockers.filter((o) => Math.abs(o.y - t) < PIN_CLEAR).length;
|
||||
if (hits < bestHits || (hits === bestHits && Math.abs(t - c.y) < Math.abs(best - c.y))) { best = t; bestHits = hits; }
|
||||
}
|
||||
if (bestHits !== Infinity) y = best;
|
||||
}
|
||||
c.lane = y; lanes.push(y);
|
||||
}
|
||||
}
|
||||
// BLOCKS, stacked top to bottom in lane order, each as near its lane as the rows allow.
|
||||
const rowsAvail = Math.floor((BY1 - BY0) / ROW);
|
||||
let leadersS = '', labelsS = '', dotsS = '', overflow = 0;
|
||||
const placed = [];
|
||||
for (const sd of ['L', 'R']) {
|
||||
const L = sd === 'L';
|
||||
const blocks = clusters.filter((c) => c.side === sd).sort((a, b) => a.lane - b.lane)
|
||||
.map((c) => ({ c, n: c.items.length, want: (c.lane - BY0) / ROW }));
|
||||
const pos = []; let cur = 0;
|
||||
for (const b of blocks) { const q = Math.max(cur, Math.round(b.want - b.n / 2)); pos.push(q); cur = q + b.n; }
|
||||
let over = cur - rowsAvail;
|
||||
for (let k = pos.length - 1; k >= 0 && over > 0; k--) {
|
||||
const lim = k ? pos[k - 1] + blocks[k - 1].n : 0, can = Math.min(over, pos[k] - lim);
|
||||
for (let j = k; j < pos.length; j++) pos[j] -= can;
|
||||
over -= can;
|
||||
}
|
||||
overflow += Math.max(0, over);
|
||||
const edgeX = L ? X0 : X1, portX = L ? X0 - FAN : X1 + FAN;
|
||||
const innerX = L ? portX - 6 : portX + 6, outerX = L ? innerX - COLW : innerX + COLW;
|
||||
blocks.forEach((b, k) => {
|
||||
const c = b.c, r0 = pos[k], midY = BY0 + (r0 + b.n / 2) * ROW;
|
||||
leadersS += `<path class="slead" d="M${c.x.toFixed(1)} ${c.y.toFixed(1)}L${c.x.toFixed(1)} ${c.lane.toFixed(2)}L${edgeX.toFixed(1)} ${c.lane.toFixed(2)}L${portX.toFixed(1)} ${midY.toFixed(1)}"/>`;
|
||||
if (b.n > 1) leadersS += `<path class="sbr" d="M${portX.toFixed(1)} ${(BY0 + r0 * ROW + 1).toFixed(1)}V${(BY0 + (r0 + b.n) * ROW - 1).toFixed(1)}"/>`;
|
||||
dotsS += `<circle class="spin" cx="${c.x.toFixed(1)}" cy="${c.y.toFixed(1)}" r="${b.n > 1 ? 4.4 : 3.2}"/>`;
|
||||
c.items.forEach((g, j) => {
|
||||
const r = r0 + j, rowY = BY0 + (r + 0.5) * ROW, x = r % 2 === 0 ? innerX : outerX;
|
||||
if (r % 2 === 1) leadersS += `<path class="srow" d="M${innerX.toFixed(1)} ${rowY.toFixed(1)}H${(L ? outerX + 3 : outerX - 3).toFixed(1)}"/>`;
|
||||
labelsS += `<text class="snm" x="${x.toFixed(1)}" y="${(rowY + font * 0.34).toFixed(1)}" font-size="${font.toFixed(2)}"${L ? ' text-anchor="end"' : ''}>${esc(String(g.name))}</text>`;
|
||||
const ph = photoOf(g);
|
||||
if (ph) {
|
||||
const tw = textW(String(g.name), font) * 1.06;
|
||||
const px = L ? x - tw - PHOTO_GAP - PHOTO : x + tw + PHOTO_GAP;
|
||||
labelsS += `<image href="${esc(ph)}" x="${px.toFixed(1)}" y="${(rowY - PHOTO / 2).toFixed(1)}" width="${PHOTO}" height="${PHOTO}" preserveAspectRatio="xMidYMid slice"/>`;
|
||||
}
|
||||
placed.push({ g, side: sd, r, photo: !!ph });
|
||||
});
|
||||
});
|
||||
}
|
||||
// MEASURED, not assumed: leader crossings (every segment pair of different leaders).
|
||||
const polys = [...leadersS.matchAll(/class="slead" d="M([^"]+)"/g)].map((m) => m[1].split('L').map((q) => q.trim().split(' ').map(Number)));
|
||||
const segsOf = polys.map((pl) => pl.slice(1).map((q, k) => [pl[k], q]));
|
||||
const sx = (p, q, r) => (r[1] - p[1]) * (q[0] - p[0]) - (q[1] - p[1]) * (r[0] - p[0]);
|
||||
const segX = (a, b) => (sx(a[0], a[1], b[0]) > 0) !== (sx(a[0], a[1], b[1]) > 0) && (sx(b[0], b[1], a[0]) > 0) !== (sx(b[0], b[1], a[1]) > 0);
|
||||
let crossingsS = 0;
|
||||
for (let i = 0; i < segsOf.length; i++) for (let j = i + 1; j < segsOf.length; j++) if (segsOf[i].some((a) => segsOf[j].some((b) => segX(a, b)))) crossingsS++;
|
||||
const stats = { names: placed.length, pins: pins.length, clusters: clusters.length, crossings: crossingsS, overflowRows: overflow,
|
||||
laneJumps, photos: placed.filter((p) => p.photo).length, namePt: +(font / PXI * 72).toFixed(1), photoMm: FMT.photos ? +(PHOTO / PXI * 25.4).toFixed(1) : 0 };
|
||||
return { leaders: leadersS, labels: labelsS, pinDots: dotsS, crossings: crossingsS, pins, stats };
|
||||
}
|
||||
|
||||
const laid = FMT.sideLanes ? [] : layoutLabels(settled, proj);
|
||||
const sides = FMT.sideLanes ? sideLanesLayout(settled) : null;
|
||||
// What the city layer must keep clear of: the drawn pins (laid out, or true positions on a side-lane sheet).
|
||||
const avoidPins = sides ? sides.pins : laid;
|
||||
|
||||
// ── CAPITALS + MAJOR CITIES ────────────────────────────────────────────────
|
||||
// Orientation marks, deliberately SUBORDINATE to the people-group pins. Imhof's
|
||||
|
|
@ -1365,7 +1503,7 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
// projected point is not where the badge is drawn. Measuring against the
|
||||
// source coordinate let Saravan's label land 1.4px from a badge while the
|
||||
// guard believed it was clear. This block therefore runs AFTER layoutLabels.
|
||||
for (const pin of laid) {
|
||||
for (const pin of avoidPins) {
|
||||
const px = pin.px, py = pin.py;
|
||||
const dx = px < x0 ? x0 - px : (px > x1 ? px - x1 : 0);
|
||||
const dy = py < yTop ? yTop - py : (py > yBot ? py - yBot : 0);
|
||||
|
|
@ -1695,8 +1833,9 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
const tx = it.laneX + it.dirOut * 5;
|
||||
labels += `<text x="${tx.toFixed(1)}" y="${(it.labelY + it.font * 0.34).toFixed(1)}" text-anchor="${it.tanchor}" font-size="${it.font.toFixed(1)}" class="lbl">${esc(it.name)}</text>`;
|
||||
});
|
||||
if (sides) { leaders += sides.leaders; pinDots += sides.pinDots; labels += sides.labels; }
|
||||
if (!octiOK) console.warn(` ⚠ non-octilinear segment in ${zone.zoneKey}`);
|
||||
const crossings = countPolyCrossings(routes);
|
||||
const crossings = sides ? sides.crossings : countPolyCrossings(routes);
|
||||
const diagFrac = bestD;
|
||||
|
||||
// ── TITLE vs SUBTITLE ──────────────────────────────────────────────────────
|
||||
|
|
@ -2676,8 +2815,8 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
page. Collapse the flow height to the scaled height. */
|
||||
html,body{width:${(FMT.pageWidthIn * 96).toFixed(0)}px;height:${(FMT.pageWidthIn * 96 * FMT.h / FMT.w).toFixed(0)}px;overflow:hidden}
|
||||
}
|
||||
</style></head><body><div class="stage">${svg}</div></body></html>`;
|
||||
return { html, title, octiOK, crossings, diagFrac };
|
||||
${FMT.sideLanes ? SIDE_LANES_CSS + '\n' : ''}</style></head><body><div class="stage">${svg}</div></body></html>`;
|
||||
return sides ? { html, title, octiOK, crossings, diagFrac, sideLanes: sides.stats } : { html, title, octiOK, crossings, diagFrac };
|
||||
}
|
||||
|
||||
return { renderPoster, CITIES, COUNTRIES, FILE_SUFFIX, FMT_KEY, FMT, MAP_ONLY, FORMATS };
|
||||
|
|
|
|||
BIN
public/dx-maps/assets/posters/country-ind.webp
Normal file
BIN
public/dx-maps/assets/posters/country-ind.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
BIN
public/dx-maps/assets/previews/country-ind.webp
Normal file
BIN
public/dx-maps/assets/previews/country-ind.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -1070,7 +1070,7 @@
|
|||
"bundle": "poster-design",
|
||||
"scriptUrl": "./poster-design/poster-design.js",
|
||||
"output": "dx-maps/poster-design/poster-design.js",
|
||||
"integrity": "sha384-kxKGb9vfzkFOaVMv8rx+eOITZVBLLlZzEFAJGmdK2RqRmG26cjpsPJwk5fNkFIsy",
|
||||
"integrity": "sha384-hw+amWf3FSzpTkOfGjFV8CvZgRPBPMY8q/r5qPK5r2Fc1W4P8Lkh5ZnSmpfVTLTD",
|
||||
"elements": [
|
||||
"poster-design"
|
||||
],
|
||||
|
|
@ -1093,7 +1093,7 @@
|
|||
"scriptUrl": "./poster-design/poster-design.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./poster-design/poster-design-mfe/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=\"./poster-design/poster-design.js\" integrity=\"sha384-kxKGb9vfzkFOaVMv8rx+eOITZVBLLlZzEFAJGmdK2RqRmG26cjpsPJwk5fNkFIsy\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-design-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-hw+amWf3FSzpTkOfGjFV8CvZgRPBPMY8q/r5qPK5r2Fc1W4P8Lkh5ZnSmpfVTLTD\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-design-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1101,7 +1101,7 @@
|
|||
"stage": "experimental",
|
||||
"label": "Poster Design — Sizes",
|
||||
"file": "poster-sizes.vue",
|
||||
"description": "Every poster size the print picker offers (standard paper up to 5 × 8 ft), each drawn as a",
|
||||
"description": "Every poster size the print picker offers, from standard paper to 5 × 8 ft, drawn to scale.",
|
||||
"element": "poster-design",
|
||||
"type": "single",
|
||||
"isMap": false,
|
||||
|
|
@ -1109,7 +1109,7 @@
|
|||
"scriptUrl": "./poster-design/poster-design.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./poster-design/poster-sizes/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=\"./poster-design/poster-design.js\" integrity=\"sha384-kxKGb9vfzkFOaVMv8rx+eOITZVBLLlZzEFAJGmdK2RqRmG26cjpsPJwk5fNkFIsy\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-sizes\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-hw+amWf3FSzpTkOfGjFV8CvZgRPBPMY8q/r5qPK5r2Fc1W4P8Lkh5ZnSmpfVTLTD\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-sizes\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1125,7 +1125,7 @@
|
|||
"scriptUrl": "./poster-design/poster-design.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./poster-design/poster-styles/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=\"./poster-design/poster-design.js\" integrity=\"sha384-kxKGb9vfzkFOaVMv8rx+eOITZVBLLlZzEFAJGmdK2RqRmG26cjpsPJwk5fNkFIsy\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-styles\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-hw+amWf3FSzpTkOfGjFV8CvZgRPBPMY8q/r5qPK5r2Fc1W4P8Lkh5ZnSmpfVTLTD\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-styles\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1070,7 +1070,7 @@
|
|||
"bundle": "poster-design",
|
||||
"scriptUrl": "./poster-design/poster-design.js",
|
||||
"output": "dx-maps/poster-design/poster-design.js",
|
||||
"integrity": "sha384-kxKGb9vfzkFOaVMv8rx+eOITZVBLLlZzEFAJGmdK2RqRmG26cjpsPJwk5fNkFIsy",
|
||||
"integrity": "sha384-hw+amWf3FSzpTkOfGjFV8CvZgRPBPMY8q/r5qPK5r2Fc1W4P8Lkh5ZnSmpfVTLTD",
|
||||
"elements": [
|
||||
"poster-design"
|
||||
],
|
||||
|
|
@ -1093,7 +1093,7 @@
|
|||
"scriptUrl": "./poster-design/poster-design.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./poster-design/poster-design-mfe/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=\"./poster-design/poster-design.js\" integrity=\"sha384-kxKGb9vfzkFOaVMv8rx+eOITZVBLLlZzEFAJGmdK2RqRmG26cjpsPJwk5fNkFIsy\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-design-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-hw+amWf3FSzpTkOfGjFV8CvZgRPBPMY8q/r5qPK5r2Fc1W4P8Lkh5ZnSmpfVTLTD\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-design-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1101,7 +1101,7 @@
|
|||
"stage": "experimental",
|
||||
"label": "Poster Design — Sizes",
|
||||
"file": "poster-sizes.vue",
|
||||
"description": "Every poster size the print picker offers (standard paper up to 5 × 8 ft), each drawn as a",
|
||||
"description": "Every poster size the print picker offers, from standard paper to 5 × 8 ft, drawn to scale.",
|
||||
"element": "poster-design",
|
||||
"type": "single",
|
||||
"isMap": false,
|
||||
|
|
@ -1109,7 +1109,7 @@
|
|||
"scriptUrl": "./poster-design/poster-design.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./poster-design/poster-sizes/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=\"./poster-design/poster-design.js\" integrity=\"sha384-kxKGb9vfzkFOaVMv8rx+eOITZVBLLlZzEFAJGmdK2RqRmG26cjpsPJwk5fNkFIsy\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-sizes\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-hw+amWf3FSzpTkOfGjFV8CvZgRPBPMY8q/r5qPK5r2Fc1W4P8Lkh5ZnSmpfVTLTD\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-sizes\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1125,7 +1125,7 @@
|
|||
"scriptUrl": "./poster-design/poster-design.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./poster-design/poster-styles/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=\"./poster-design/poster-design.js\" integrity=\"sha384-kxKGb9vfzkFOaVMv8rx+eOITZVBLLlZzEFAJGmdK2RqRmG26cjpsPJwk5fNkFIsy\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-styles\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-hw+amWf3FSzpTkOfGjFV8CvZgRPBPMY8q/r5qPK5r2Fc1W4P8Lkh5ZnSmpfVTLTD\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-styles\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <poster-design> custom element. -->
|
||||
<script src="../poster-design.js" integrity="sha384-kxKGb9vfzkFOaVMv8rx+eOITZVBLLlZzEFAJGmdK2RqRmG26cjpsPJwk5fNkFIsy" crossorigin="anonymous"></script>
|
||||
<script src="../poster-design.js" integrity="sha384-hw+amWf3FSzpTkOfGjFV8CvZgRPBPMY8q/r5qPK5r2Fc1W4P8Lkh5ZnSmpfVTLTD" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
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 <poster-design> custom element. -->
|
||||
<script src="../poster-design.js" integrity="sha384-kxKGb9vfzkFOaVMv8rx+eOITZVBLLlZzEFAJGmdK2RqRmG26cjpsPJwk5fNkFIsy" crossorigin="anonymous"></script>
|
||||
<script src="../poster-design.js" integrity="sha384-hw+amWf3FSzpTkOfGjFV8CvZgRPBPMY8q/r5qPK5r2Fc1W4P8Lkh5ZnSmpfVTLTD" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <poster-design> custom element. -->
|
||||
<script src="../poster-design.js" integrity="sha384-kxKGb9vfzkFOaVMv8rx+eOITZVBLLlZzEFAJGmdK2RqRmG26cjpsPJwk5fNkFIsy" crossorigin="anonymous"></script>
|
||||
<script src="../poster-design.js" integrity="sha384-hw+amWf3FSzpTkOfGjFV8CvZgRPBPMY8q/r5qPK5r2Fc1W4P8Lkh5ZnSmpfVTLTD" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue