Vai al contenuto principale
Ricetta numero 62

Ricettario · Capitolo 2 · Caratteri e testo

Versi figurati: Easter Wings di Herbert, ogni riga centrata

Easter Wings e The Altar di George Herbert composti come testo centrato, un paragrafo per ogni verso, così le lunghezze delle righe disegnano le due figure.

In questa pagina
Genere
Poesia
Output
Canvas
Livello
Base
Postext
Provata con Postext 1.31.0
Richiede ≥ 1.25.0
Licenza
Aggiornata il 11 ott 2026
Codice MIT · Testo CC BY 4.0

pp. 2–3 di 4

  • Esempio in inglese: non c’è ancora un’edizione italiana
  • Formato 148 × 210 mm
  • 1 colonna
  • IM Fell English 12,4/17
  • IM Fell English SC
  • IM Fell Great Primer
  • 4 pagine
  • Livello
  • Postext 1.29.1
  • Impaginato in 42 ms
  • 99 righe di codice

In breve

Due antiche poesie inglesi i cui versi sono stampati in modo che ogni poesia sembri un’immagine: un altare e un paio d’ali. Ogni verso è semplicemente centrato, e la sua lunghezza disegna la forma.

Che cosa comporrai

Un opuscolo di quattro pagine A5, un foglio A4 piegato una volta, con due carmi figurati da The Temple di George Herbert nella grafia della prima edizione del 1633. Il frontespizio segue quello del 1633, fino all’epigrafe dal Salmo 29. All’interno, The Altar sta di fronte a Easter Wings. Nessun verso delle due poesie è rientrato o riempito di spazi; ciascuno è centrato, così l’altare poggia su un pilastro stretto e ogni strofa delle ali si restringe fino a due parole e si riallarga. Nell’ultima pagina, sotto il motto di Herbert, una nota racconta come gli stampatori del 1633 composero le ali di traverso, e il colophon si accorcia riga dopo riga. Tutto è composto come testo corrente o come uno dei tre livelli di titolo nei caratteri IM Fell, senza stili con nome e senza disegni di pagina. L’edizione spagnola compone una versione verso per verso che conserva entrambe le forme.

Questa ricetta risponde a

  • Come aggiungo spazio verticale tra due blocchi, se le righe vuote non hanno effetto?
  • Come compongo poesia: una riga per verso, spazi tra le strofe, rientri sporgenti per i versi che vanno a capo, niente sillabazione?
  • Come compongo un’epigrafe, una dedica, una firma o una citazione in evidenza con una grande virgoletta?
  • Come forzo un’interruzione di pagina o di colonna, e faccio cominciare ogni capitolo su una pagina destra?

La risposta breve

script.js · righe 26–38nel codice completo
// Postext joins the lines of a Markdown paragraph, so every line of the poem is a paragraph
// of its own, with a blank line after it. Centred, each line keeps its natural width on the
// axis of the measure, and the lengths Herbert gave his lines draw the figure:
//   Lord, who createdst man in wealth and store,
//
//   Though foolishly he lost the same,
//
//   Decaying more and more,
const verse = {
  fontFamily: 'IM Fell English', fontSize: pt(12.4), lineHeight: pt(LEAD), color: col('ink'),
  textAlign: 'center', // ragged on both sides; only justified text is hyphenated by default
  firstLineIndent: pt(0), // the default 1.5 em moves every line 3.3 mm off the titles' axis
};

Ingredienti

Tipo
IM Fell English, IM Fell Great Primer, IM Fell English SC (SIL OFL 1.1)
Risorse
Nessuna: ogni immagine è disegnata nel codice

Preparazione

#1 · Un verso è un paragrafo centrato

Il codice è la risposta breve qui sopra. Postext unisce in una sola le righe di un paragrafo Markdown, quindi ogni verso è un paragrafo, con una riga vuota dopo. textAlign: 'center' compone ogni riga alla sua larghezza naturale sull’asse della giustezza, e poiché solo il testo giustificato viene sillabato a meno che hyphenation.ragged non sia attivo, nessuna parola viene divisa (testo corrente). Ogni riga è la prima del suo paragrafo, quindi il rientro di prima riga predefinito di 1,5 em la spingerebbe 3,3 mm a destra dell’asse dei titoli; firstLineIndent: pt(0) tiene poesie e titoli su un solo asse.

#2 · La giustezza accoglie il verso più lungo

script.js · righe 42–47nel codice completo
// A line too long for the measure wraps, and both halves are centred, which adds a step to
// the outline.
// The widest line, 'Lord, who createdst man in wealth and store,', is 80.7 mm in 12.4 pt
// Fell, so the 110 mm measure takes every line of both poems whole.
const TRIM = { w: 148, h: 210 }; // mm: A5, one A4 sheet folded once
const MARGIN = { top: 22, bottom: 24, inner: 17, outer: 21 }; // mm: the measure is 110 mm

Una riga che non ci sta va a capo, ed entrambe le metà sono centrate, così il profilo acquista un gradino che non c’è nella figura di Herbert. Con una giustezza di 75 mm, tre versi di The Altar e due di Easter Wings vanno a capo, e l’opuscolo arriva a sei pagine. Il verso più largo qui, il primo di Easter Wings, misura 80,7 mm in inglese e 80,0 mm in spagnolo, quindi la giustezza di 110 mm della pagina A5 accoglie ogni verso intero.

#3 · Titoli semplici, centrati

script.js · righe 51–71nel codice completo
// headings.textAlign centres every level. The Fell faces ship weight 400 only, so no level
// asks for bold.
const titles = {
  fontFamily: 'IM Fell Great Primer', fontWeight: 400, color: col('violet'), textAlign: 'center',
  levels: [
    // The keepsake's title opens page 1, where a break changes nothing; a second # title
    // would open on a recto.
    { level: 1, fontSize: pt(40), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
      marginBottom: pt(LEAD), breakBefore: { enabled: true, parity: 'odd' } },
    // A poem opens the next page, recto or verso: The Altar (page 2) faces Easter Wings.
    // A heading that opens a page drops its top margin, so the title is lowered by its
    // line box instead: three lines deep, it sets the title about 5 mm down. The default
    // half-em margin under it rounds up to a whole line, and each poem starts on line 5.
    { level: 2, fontSize: pt(24), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
      breakBefore: { enabled: true, parity: 'any' } },
    // The small-capital labels on pages 1 and 4: the sister face at the body size, with no
    // margins, since the :::space lines place them.
    { level: 3, fontFamily: 'IM Fell English SC', fontSize: pt(12.4), lineHeight: pt(LEAD),
      marginTop: pt(0), marginBottom: pt(0) },
  ],
};

Nessun livello ha un disegno, solo impostazioni tipografiche, e headings.textAlign centra tutti e tre (titoli). La parità 'any' apre ogni poesia sulla pagina successiva, recto o verso, così l’altare a pagina 2 sta di fronte alle ali a pagina 3. Un marginTop non può abbassare quei titoli, perché lo spazio sopra un titolo che apre una pagina viene eliminato. È invece il riquadro di riga del livello 2, alto tre righe, ad abbassare ogni titolo di circa 5 mm, ed entrambe le poesie cominciano alla riga 5 della gabbia del testo. Il livello 1 apre il documento e qui non ha bisogno di un’interruzione; ne porta comunque una, perché un secondo titolo # si apra su un recto. Il livello 3 compone le quattro etichette delle pagine 1 e 4 nel maiuscoletto di IM Fell English SC, al corpo del testo.

#4 · Il Markdown spazia e interrompe le pagine

script.js · righe 75–92nel codice completo
// A blank line adds no space, so every gap on these pages is a :::space{lines=N}, which adds
// N lines of the 17 pt grid (one without the attribute). ## opens a page for each poem, and
// :::pagebreak opens the last page.
const config = () => ({
  colorPalette,
  locale: t({ en: 'en-us', es: 'es' }), // the language of each edition, and of its bundle
  page: { // mirror: left is the inner margin; 150 dpi is for the screen
    sizePreset: 'custom', width: mm(TRIM.w), height: mm(TRIM.h), dpi: 150,
    backgroundColor: col('paper'),
    margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner),
      right: mm(MARGIN.outer), mirror: true },
  },
  layout: { layoutType: 'single' }, // the default is two columns
  bodyText: verse, // a :ref added to these pages takes main-color, the ink, like bold and italic
  headings: titles,
  header: { elements: [] }, // a folded keepsake of four pages: no running heads,
  footer: { elements: [] }, // and no folios
});

Una riga vuota tra i paragrafi non aggiunge spazio, quindi ogni spazio di queste pagine viene da :::space: una riga tra le strofe di Easter Wings, e nel frontespizio cinque righe sopra l’etichetta del Salmo e dieci sotto l’epigrafe (:::space). Con questi conteggi le note tipografiche di pagina 1 finiscono sull’ultima riga della gabbia del testo, e le etichette sopra il Salmo e sopra la nota stanno entrambe alla riga 13, a 94 mm dalla sommità della pagina. :::pagebreak apre l’ultima pagina (:::pagebreak). Anche il colophon è composto un paragrafo per riga, spezzato dove finisce una frase, così le sue quattro righe si assottigliano in un finalino.

La ricetta completa

Sandbox
// ═══ Postext Cookbook · Nº 062 · Shaped verse: Herbert's Easter Wings, centred line by line ═
// https://postext.dev/en/cookbook/shaped-verse
// Code: MIT · Text: G. Herbert, The Temple, 1633 (PD; EEBO-TCP, CC0) · Spanish version: CC BY 4.0
// Fonts: IM Fell English, Great Primer and English SC (SIL OFL 1.1) · Needs postext ≥ 1.25.0
import { buildDocumentWithFonts, renderPageToCanvas } from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'shaped-verse';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = { // every colour in the config links to one of these
  ink: '#241d29', // the text: a violet-black
  violet: '#5c2a6f', // the one accent: the titles and the small-capital labels
  paper: '#fbf7ee', // the sheet: an uncoated cream
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  // Italic and bold take their colour from 'main-color' unless set. With 'main-color' in ink,
  // the italic subtitle, epigraph and motto print in ink instead of the default blue.
  { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
const LEAD = 17; // pt: the leading of every line, verse and prose alike

// #region answer: centred text, one line of verse to a paragraph
// Postext joins the lines of a Markdown paragraph, so every line of the poem is a paragraph
// of its own, with a blank line after it. Centred, each line keeps its natural width on the
// axis of the measure, and the lengths Herbert gave his lines draw the figure:
//   Lord, who createdst man in wealth and store,
//
//   Though foolishly he lost the same,
//
//   Decaying more and more,
const verse = {
  fontFamily: 'IM Fell English', fontSize: pt(12.4), lineHeight: pt(LEAD), color: col('ink'),
  textAlign: 'center', // ragged on both sides; only justified text is hyphenated by default
  firstLineIndent: pt(0), // the default 1.5 em moves every line 3.3 mm off the titles' axis
};
// #endregion

// #region measure: an A5 leaf whose measure holds the longest line whole
// A line too long for the measure wraps, and both halves are centred, which adds a step to
// the outline.
// The widest line, 'Lord, who createdst man in wealth and store,', is 80.7 mm in 12.4 pt
// Fell, so the 110 mm measure takes every line of both poems whole.
const TRIM = { w: 148, h: 210 }; // mm: A5, one A4 sheet folded once
const MARGIN = { top: 22, bottom: 24, inner: 17, outer: 21 }; // mm: the measure is 110 mm
// #endregion

// #region titles: plain headings, centred, in the display face and the accent
// headings.textAlign centres every level. The Fell faces ship weight 400 only, so no level
// asks for bold.
const titles = {
  fontFamily: 'IM Fell Great Primer', fontWeight: 400, color: col('violet'), textAlign: 'center',
  levels: [
    // The keepsake's title opens page 1, where a break changes nothing; a second # title
    // would open on a recto.
    { level: 1, fontSize: pt(40), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
      marginBottom: pt(LEAD), breakBefore: { enabled: true, parity: 'odd' } },
    // A poem opens the next page, recto or verso: The Altar (page 2) faces Easter Wings.
    // A heading that opens a page drops its top margin, so the title is lowered by its
    // line box instead: three lines deep, it sets the title about 5 mm down. The default
    // half-em margin under it rounds up to a whole line, and each poem starts on line 5.
    { level: 2, fontSize: pt(24), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
      breakBefore: { enabled: true, parity: 'any' } },
    // The small-capital labels on pages 1 and 4: the sister face at the body size, with no
    // margins, since the :::space lines place them.
    { level: 3, fontFamily: 'IM Fell English SC', fontSize: pt(12.4), lineHeight: pt(LEAD),
      marginTop: pt(0), marginBottom: pt(0) },
  ],
};
// #endregion

// #region pages: four pages, spaced and broken from the Markdown
// A blank line adds no space, so every gap on these pages is a :::space{lines=N}, which adds
// N lines of the 17 pt grid (one without the attribute). ## opens a page for each poem, and
// :::pagebreak opens the last page.
const config = () => ({
  colorPalette,
  locale: t({ en: 'en-us', es: 'es' }), // the language of each edition, and of its bundle
  page: { // mirror: left is the inner margin; 150 dpi is for the screen
    sizePreset: 'custom', width: mm(TRIM.w), height: mm(TRIM.h), dpi: 150,
    backgroundColor: col('paper'),
    margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner),
      right: mm(MARGIN.outer), mirror: true },
  },
  layout: { layoutType: 'single' }, // the default is two columns
  bodyText: verse, // a :ref added to these pages takes main-color, the ink, like bold and italic
  headings: titles,
  header: { elements: [] }, // a folded keepsake of four pages: no running heads,
  footer: { elements: [] }, // and no folios
});
// #endregion

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Esempio Markdown · 129 righe · content.en.mdtitle: "The Temple" author: "George Herbert" --- # The Temple *Sacred Poems and Private Ejaculations* :::space By Mr. George Herbert :::space{lines=5} ### Psal. 29. *In his Temple doth every man speak of his honour.* :::space{lines=10} ### Two shaped poems in the text of the first edition Cambridge, 1633 ## The Altar A broken ALTAR, Lord, thy servant reares, Made of a heart, and cemented with teares: Whose parts are as thy hand did frame; No workmans tool hath touch’d the same. A HEART alone Is such a stone, As nothing but Thy pow’r doth cut. Wherefore each part Of my hard heart Meets in this frame, To praise thy name. That if I chance to hold my peace, These stones to praise thee may not cease. O let thy blessed SACRIFICE be mine, And sanctifie this ALTAR to be thine. ## Easter Wings Lord, who createdst man in wealth and store, Though foolishly he lost the same, Decaying more and more, Till he became Most poore: With thee O let me rise As larks, harmoniously, And sing this day thy victories: Then shall the fall further the flight in me. :::space My tender age in sorrow did beginne: And still with sicknesses and shame Thou didst so punish sinne, That I became Most thinne. With thee Let me combine, And feel this day thy victorie: For, if I imp my wing on thine, Affliction shall advance the flight in me. :::pagebreak ### Herbert’s motto *Lesse then the least of Gods mercies.* :::space With it, his printers wrote, he used to conclude “all things that might seem to tend any way to his own honour”. :::space{lines=7} ### A note on the text The poems keep the spelling of the first edition, printed at Cambridge in 1633. There “The Altar” stands upright on page 18, and the two stanzas of “Easter Wings” lie sideways on pages 34 and 35. Here they stand upright; give the booklet a quarter turn and each opens into a pair of wings. :::space{lines=5} Set in IM Fell English, English SC and Great Primer, Igino Marini’s revivals of the types John Fell gathered at Oxford. Text from EEBO-TCP.
`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses. Layout measures with the browser's fonts, so the // kit loads them from Fontsource before the first build. const FONTS = { 'IM Fell English': ['400', '400i'], 'IM Fell Great Primer': ['400'], 'IM Fell English SC': ['400'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── const doc = await buildDocumentWithFonts({ markdown }, config(), kitFonts(FONTS)); showPages(doc, { title: t({ en: 'Easter Wings and The Altar', es: 'Alas de Pascua y El altar' }) });
Kit · core, fonts, viewer: uguale in ogni ricetta · 224 righe// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook // Postext loads the faces a config asks for before layout (prepareFonts, // buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the // files the PDF embeds too. /** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts: * the Fontsource resolver, and the faces of FONTS loaded with the ones the * config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */ function kitFonts(FONTS = {}) { const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) => ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' }))); return { resolve: fontsourceResolver, faces }; } /** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek * when `text` needs them and the family ships them (kitSubsetsFor), each with * its unicode-range. Null for a weight or slant Fontsource does not ship. */ async function fontsourceResolver(family, weight, style, { text = '' } = {}) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null; kitStatus('Loading fonts…'); const at = await fontsourceAt(id); return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({ source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`, unicodeRange: kitRanges()[subset], })); } /** The unicode-range of each Fontsource file the kit loads (a function: the * kit is inlined after the pen's code, which runs before a const would be set). */ function kitRanges() { return { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', greek: 'U+0370-03FF', }; } /** For the cjk, arabic and comics blocks, which add their own files: the * faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With * `optional`, a face Fontsource does not ship is skipped instead of failing. * The files are added once all have loaded, in the order asked (latin, then * latin-ext, then greek), whatever order they arrive in: the last one added * wins a character two files hold. Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = kitRanges(); const jobs = []; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal']) .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await const at = fontsourceAt(id); const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null; const subsets = ['latin', ...kitSubsetsFor(text, meta)]; for (const [weight, style] of todo) { if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().catch(() => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return ready.filter((face) => face && document.fonts.add(face)).length; } /** A loaded FontFace covers this family, weight and style (fonts.check() would * also say yes for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** The files beyond latin `text` needs that `meta`'s family ships. */ function kitSubsetsFor(text, meta) { return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']] .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x); } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** A package's folder at the one version @5 names now (#766). */ function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) { return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json()) .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`); } /** The family's Fontsource metadata (weights, styles, subsets), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook /** The pages as spreads on a dark desk, page 1 alone, then verso | recto, * each painted when it scrolls near. Other options go to renderPageToCanvas. */ function showPages(docs, { title, width = 460, ...render } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── /Kit ───────────────────────────────────────────────────────────────────────

Lo script.js composto funziona così com’è: incollalo nello script modulo di qualsiasi pagina, oppure apri la ricetta su CodePen. Cartella della ricetta su GitHub ↗ (si apre in una nuova scheda)

Varianti

#Apri ogni poesia su una pagina destra

Con la parità 'odd' The Altar ed Easter Wings si aprono alle pagine 3 e 5, entrambe recto, e l’opuscolo arriva a sei pagine con le pagine 2 e 4 bianche.

-      breakBefore: { enabled: true, parity: 'any' } },
+      breakBefore: { enabled: true, parity: 'odd' } },

#Allinea i versi a sinistra

Con textAlign: 'left' ogni riga comincia sul margine e le due poesie conservano solo il profilo destro; anche il resto del testo corrente, dal sottotitolo al colophon, va allineato a sinistra, e restano centrati solo i titoli.

-  textAlign: 'center', // ragged on both sides; only justified text is hyphenated by default
+  textAlign: 'left', // ragged right; only justified text is hyphenated by default

Errori frequenti

  • Un verso troppo lungo per la giustezza va a capo, ed entrambe le metà vengono centrate ciascuna per conto suo. Misura il verso più largo prima di scegliere il corpo e i margini. Per versi allineati a sinistra, con righe a capo sporgenti, vedi Poesie composte verso per verso.
  • Gli spazi iniziali vengono tolti da un paragrafo, quindi non puoi dare forma a una riga riempiendola di spazi. La larghezza di ogni riga dipende solo dalle sue parole, e una traduzione deve trovare versi larghi più o meno quanto quelli dell’originale, come fa l’edizione spagnola.
  • La centratura tiene ogni riga alla sua larghezza naturale, quindi la lastra e la base di The Altar non risultano squadrate: la terza e la quarta riga della lastra sono più strette delle prime due. Nemmeno la giustificazione può squadrarle, perché l’ultima riga di un paragrafo giustificato mantiene la sua larghezza naturale e ognuna di queste righe è un paragrafo a sé.
  • Il frontespizio e l’ultima pagina sono pieni fino all’ultima riga della griglia. Una riga in più sul frontespizio, per un’epigrafe più lunga, una riga che va a capo o un :::space più grande, sposta l’etichetta al piede e le due righe sotto di essa su una pagina tutta loro; una riga in più nell’ultima pagina sposta da sola l’ultima riga del colophon su una quinta pagina. Per aggiungere una riga a una delle due pagine, toglila da un :::space sopra di essa.
  • Postext non compone il testo di traverso, come gli stampatori del 1633 composero Easter Wings: ruotano solo figure e tabelle intere. Qui le strofe stanno in piedi, e la nota a pagina 4 lo dice.

Crediti

Testo
  • “The Altar” and “Easter wings”, the title, subtitle, byline and epigraph of the title page, and Herbert’s motto with a clause of the printers’ preface, from the first edition of The Temple (Cambridge, 1633) in the EEBO-TCP transcription (CC0 1.0); the letters and stops it marks as illegible or unknown at six line ends (four in The Altar, two in Easter wings) are supplied · George Herbert; Text Creation Partnership · pubblico dominio
  • Psalm 29:9 in the Spanish edition, from the Reina-Valera Bible of 1909 · Reina-Valera 1909 · pubblico dominio
  • The Spanish versions of both poems, of the subtitle, the motto and the printers’ clause, and the title-page imprint, the note on the text and the colophon of both editions, written for this recipe · Ignacio Ferro · CC BY 4.0
Font
IM Fell English (SIL OFL 1.1) · IM Fell Great Primer (SIL OFL 1.1) · IM Fell English SC (SIL OFL 1.1)
Sandbox