Vai al contenuto principale
Ricetta numero 29

Ricettario · Capitolo 3 · Titoli e aperture

Prontuario di ancoraggio: manifesto di elementi concatenati

Un manifesto A3 con elementi agganciati all’abbondanza, alla pagina, al loro slot o tra loro, e un foglio che li incornicia e ne etichetta tredici.

In questa pagina
Output
Canvas
Livello
Intermedio
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. 1–2 di 2

  • Esempio in inglese: non c’è ancora un’edizione italiana
  • Formato 297 × 420 mm
  • 2 colonne, Intercolonna di 7 mm
  • Funnel Sans 12/16
  • Funnel Display
  • Martian Mono
  • 2 pagine
  • Livello
  • Postext 1.29.1
  • Impaginato in 54 ms
  • 179 righe di codice

In breve

Un manifesto per conferenze di astronomia prima di un’eclissi di Sole. Mostra come collocare ogni elemento misurando dal bordo della pagina o da un altro elemento.

Che cosa comporrai

Il manifesto di un circolo di astronomia immaginario per quattro conferenze del giovedì prima dell’eclissi totale di Sole del 2 agosto 2027. Un cielo blu oltremare occupa i due terzi superiori di un foglio A3. A destra una Luna nera copre un Sole giallo limone tagliato dal rifilo, e una corona disegnata via codice si allarga dietro entrambi. Il titolo, The Shadow Returns in Funnel Display da 120 pt, poggia sul suo sommario al piede del cielo. Sotto l’orizzonte quattro date blu aprono le conferenze, e le note in corpo minore corrono su due colonne sopra una fascia blu. Ogni elemento è agganciato all’abbondanza, alla pagina, al suo slot o a un altro elemento, e i suoi scostamenti si contano da lì. Il secondo foglio ripete il manifesto con una cornice rosa attorno al riquadro di ogni elemento e ai contenitori di intestazione e piè di pagina, e un’etichetta su tredici elementi che stampa la collocazione di ciascuno.

Questa ricetta risponde a

  • Come colloco gli elementi grafici rispetto alla pagina, all’abbondanza o tra loro invece che per coordinate?
  • Come aggiungo una filigrana, una tinta di fondo o un’immagine decorativa su ogni pagina?

La risposta breve

script.js · righe 56–72nel codice completo
const SKY = 272; // mm from the trim's top edge to the horizon, the sky's lower edge
const INSET = BLEED + MARGIN; // from the bleed's edge to the text's edge
const AIR = 14; // mm between the standfirst and the horizon
const poster = () => ({ enabled: true, slot: { elements: [ // array order is paint order
  // 'bleed' and 'page' are frames of the sheet; 'container' would be this heading's own box.
  { kind: 'box', id: 'sky', style: { backgroundColor: col('sky') }, // no width: fills the bleed
    placement: at('bleed', 'top-left', 0, 0, { height: mm(BLEED + SKY) }) },
  ...eclipse(), // the Sun hangs from the trim's corner, the Moon and the corona from the Sun
  // The headline stands on the horizon and grows upwards: 'above' puts an element's
  // bottom-left corner on the top-left corner of its anchor, so a longer title lifts the kicker.
  text('deck', '{attr.deck}', FACE.deck, { ...at('#sky', 'align-bottom', INSET, -AIR),
    size: { maxWidth: mm(153) } }), // shrink-wraps its lines, but wraps at 153 mm
  text('title', '{titleText}', FACE.title, at('#deck', 'above', 0, -9)), // breaks at the \\
  text('kicker', '{attr.kicker}', FACE.kicker, at('#title', 'above', 0, -7)),
  ...programme(), // four columns chained off the horizon
] } }); // hook-up: headings.levels[0] = { span: 'page', breakBefore, advancedDesign: poster() }
// {titleText} is the heading's text; each {attr.*} is written on its line in content.md.

Ingredienti

Tipo
Funnel Display, Funnel Sans, Martian Mono (SIL OFL 1.1)
Risorse
Nessuna: ogni immagine è disegnata nel codice

Preparazione

#1 · Fissa due elementi al foglio e aggancia il resto a loro

Il codice è la risposta breve qui sopra. Lì anchor.to indica a che cosa è agganciato un elemento: 'bleed' e 'page' sono cornici del foglio (lo stesso riquadro finché page.cutLines è disattivato), 'container' è il riquadro dello slot stesso e '#id' è un altro elemento (posizionamento degli elementi). Un riquadro senza larghezza arriva fino al bordo opposto della sua cornice, così il cielo riempie l’abbondanza da un lato all’altro. Un elemento collocato rispetto al cielo misura dal bordo sinistro del cielo, che è quello dell’abbondanza, perciò il sommario prende INSET, 18 mm, come x. Il titolo si costruisce verso l’alto a partire dal bordo inferiore del cielo. 'align-bottom' mette il piede del riquadro del sommario AIR, 14 mm, sopra quel bordo, e 'above' appoggia il piede di ogni elemento sulla sommità di quello sottostante, così un titolo che occupa più righe solleva l’occhiello invece di coprire il sommario. Sotto il progetto grafico, le note in corpo minore partono dalla prima riga della griglia da 16 pt che si trova almeno 3,2 mm sotto l’elemento più basso, la riga dei relatori; quei 3,2 mm sono il margine inferiore predefinito del titolo.

#2 · Aggancia la Luna e la corona al Sole

script.js · righe 76–87nel codice completo
const SUN = 150; // mm across; the Moon is drawn the same size
const CORONA = 300; // mm, the square picture of the corona
const HALO = (CORONA - SUN) / 2; // how far the corona reaches past the Sun on every side
const BITE = { x: 2, y: 2.5 }; // mm the Moon sits off the Sun: a sliver is left at upper left
const disc = (id, colour, placement) => ({ kind: 'box', id, placement: { ...placement,
  size: square(SUN) }, style: { backgroundColor: col(colour), borderRadius: mm(SUN / 2) } });
const eclipse = () => [
  { kind: 'image', id: 'corona', resourceId: 'corona', // listed before the discs: painted behind
    placement: at('#sun', 'align-top', -HALO, -HALO, square(CORONA)) },
  disc('sun', 'sun', at('page', 'top-right', 30, 25)), // 30 mm of it past the right edge
  disc('moon', 'ink', at('#sun', 'align-top', BITE.x, BITE.y)),
];

Il Sole è l’unico elemento fissato alla pagina, il riquadro del formato rifilato. Misurata dall’angolo 'top-right', una x positiva va oltre il bordo destro, così 30 mm del disco restano tagliati. La Luna e la corona sono agganciate al Sole con 'align-top', quindi spostando il Sole si spostano entrambe. I loro scostamenti si contano dall’angolo superiore sinistro del Sole: la Luna sta 2 mm a destra e 2,5 mm in basso, il che lascia una falce di Sole in alto a sinistra, e la corona parte HALO, 75 mm, in alto e a sinistra, il che centra il suo quadrato di 300 mm sul disco di 150 mm. La corona è un elemento immagine che prende le dimensioni del suo riquadro (elementi immagine) e nell’elenco viene prima dei dischi, quindi viene dipinta dietro di loro.

#3 · Concatena le serate in colonne

script.js · righe 91–100nel codice completo
const GUT = 7; // mm between the evenings, and between the two columns of text under them
const COL = (A3.width - 2 * MARGIN - 3 * GUT) / 4; // four columns across the text width: 60 mm
const DROP = 12; // mm from the horizon to the dates
const programme = () => [1, 2, 3, 4].flatMap((n) => [
  text(`day${n}`, `{attr.d${n}}`, FACE.day, n === 1
    ? at('#sky', 'below', INSET, DROP, wide(COL)) // the first date hangs from the horizon
    : at(`#day${n - 1}`, 'right-of', GUT, 0, wide(COL))), // the others from the one before
  text(`talk${n}`, `{attr.t${n}}`, FACE.talk, at(`#day${n}`, 'below', 0, 3, wide(COL))),
  text(`who${n}`, `{attr.s${n}}`, FACE.who, at(`#talk${n}`, 'below', 0, 2.5, wide(COL))),
]);

Ogni data, conferenza e riga dei relatori ha una size.width fissa pari a COL, e 'right-of' più GUT mette la data successiva una colonna più in là. Senza larghezza una data si restringe alle sue cifre (l’«8» è largo 17 mm), quindi il «15» comincerebbe 7 mm dopo e ogni conferenza correrebbe su una sola riga fino alla colonna seguente. COL divide la larghezza del testo in quattro con lo stesso intercolonna di 7 mm delle due colonne di note qui sotto, il che mette la seconda colonna di note sotto il 22. Ogni conferenza è agganciata 'below' alla sua data e ogni riga dei relatori sotto la sua conferenza, così una conferenza che andasse a capo su una terza riga spingerebbe in basso il proprio relatore e nessun altro.

#4 · Metti la fascia nel piè di pagina, su ogni pagina

script.js · righe 104–115nel codice completo
const MARK = 10; // mm, the club's mark
const PAD = (FOOT - MARK) / 2; // centres the mark in the bottom margin
const markTall = { height: mm(MARK) }; // as tall as the mark: the line is centred on it
const footer = { elements: [
  { kind: 'box', id: 'band', style: { backgroundColor: col('sky') }, // the bottom margin, bled
    placement: at('bleed', 'bottom-left', 0, 0, { height: mm(BLEED + FOOT) }) },
  // The container runs from the text down to the trim: 'bottom-*' counts up, 'top-*' down.
  { kind: 'image', id: 'mark', resourceId: 'mark',
    placement: at('container', 'bottom-left', 0, -PAD, square(MARK)) },
  text('club', '{attr.club}', FACE.foot, at('#mark', 'right-of', 3, 0, markTall)),
  text('free', '{attr.free}', FACE.foot, at('container', 'top-right', 0, PAD, markTall)),
] };

Tutto ciò che deve stamparsi su ogni pagina, come questa fascia, va nello slot dell’intestazione o del piè di pagina, e uno stile di titolo con un’intestazione o un piè di pagina propri sostituisce quello slot nelle pagine della sua sezione. Il piè di pagina dello stile guides ripete questi quattro elementi con le loro cornici e le loro etichette, ed è per questo che la fascia e il marchio si stampano su entrambi i fogli. Il contenitore del piè di pagina è il margine inferiore, dal testo fino al rifilo: 'bottom-left' conta verso l’alto dal rifilo e 'top-right' verso il basso dal testo, e il −10 del marchio e il +10 della riga «Free entry» cadono sulla stessa striscia di 10 mm al centro della fascia (intestazioni e piè di pagina). Il piè di pagina viene dipinto dopo il testo e non gli toglie spazio, quindi FOOT stabilisce sia il margine inferiore sia l’altezza della fascia, e la fascia resta lontana dalle righe sopra di lei.

#5 · Disegna le linee di costruzione dalla stessa configurazione

script.js · righe 119–146nel codice completo
const GUIDE = { borderColor: col('guide'), borderWidth: pt(1) };
const signed = ({ value }) => `${value < 0 ? '−' : '+'}${Math.abs(value)}`; // all offsets in mm
const describe = ({ id, placement: { anchor: { to, edge }, offset = {} } }) => [`#${id}`,
  `${edge} ${to}`, // the two words of anchor: { to, edge }
  ['x', 'y'].filter((k) => offset[k]?.value).map((k) => `${k} ${signed(offset[k])}`).join(' '),
].filter(Boolean).join(' · ');
// A border never changes an element's size (padding does), so each frame traces its box.
// An image or a rounded box gets a square frame of its size, hung from its top-left corner.
const frame = (el) => (el.kind === 'text' ? [{ ...el, box: { ...el.box, ...GUIDE } }]
  : el.kind === 'box' && !el.style.borderRadius ? [{ ...el, style: { ...el.style, ...GUIDE } }]
    : [el, { kind: 'box', id: `${el.id}-box`, style: GUIDE,
      placement: at(`#${el.id}`, 'align-top', 0, 0, el.placement.size) }]);
const tag = (el, [edge, x = 0, y = 0, note]) => text(`${el.id}-tag`, note ?? describe(el),
  face('Martian Mono', 8.5, 500, 'ink', { box: { backgroundColor: col('guide'),
    padding: { top: pt(1.6), bottom: pt(1.2), left: pt(3), right: pt(3) } } }),
  at(`#${el.id}`, edge, x, y));
const guides = (elements, tags) => [...elements.flatMap(frame),
  ...elements.filter((el) => tags[el.id]).map((el) => tag(el, tags[el.id]))];
const container = (id) => ({ kind: 'box', id, style: GUIDE, // the slot's own box, drawn
  placement: at('container', 'top-left', 0, 0, { width: 'fill', height: 'fill' }) });
const TAGS = { // where each tag sits against its element: [edge, x, y, text]
  sky: ['align-bottom', INSET, -3], corona: ['align-bottom', 90, -2], sun: ['above'],
  moon: ['below', 20, 2], kicker: ['above'], title: ['above'], deck: ['above'],
  day1: ['above', 0, -6], day2: ['above'], who1: ['below'],
  mark: ['above', 0, -2], club: ['above', 69, -2], free: ['above', -30, -2],
  header: ['align-bottom', 0, -2, t({ en: 'header container', es: 'contenedor de la cabecera' })],
  footer: ['align-bottom', 0, -2, t({ en: 'footer container', es: 'contenedor del pie' })],
};

guides() percorre gli elementi del manifesto. Un testo riceve un bordo sul suo riquadro e un riquadro quadrato un bordo sul suo stile; nessuno dei due sposta nulla, perché solo il padding ingrandisce un riquadro. Un’immagine o un disco tondo riceve un riquadro quadrato delle sue dimensioni agganciato al suo angolo superiore sinistro, così viene disegnato anche l’angolo del Sole a cui sono agganciate la Luna e la corona. Ogni etichetta è un testo agganciato al suo elemento, scritto da describe() a partire dalla placement di quell’elemento, quindi un’etichetta non può contraddire la configurazione che descrive. {style="guides"} sul secondo titolo attiva questo progetto grafico, con un’intestazione e un piè di pagina propri (stili dei titoli). Un’etichetta senza larghezza ha a disposizione solo lo spazio tra la sua ancora e il bordo destro del contenitore, ed è per questo che l’etichetta di #free comincia 30 mm a sinistra del suo testo. pageIndexOffset: 1, nella continuation del contenuto, fa del manifesto un verso, così il visualizzatore mette i due fogli uno accanto all’altro.

La ricetta completa

Sandbox
// ═══ Postext Cookbook · Nº 029 · Anchoring cheat sheet: a poster built from chained elements ═══
// https://postext.dev/en/cookbook/anchoring-cheat-sheet
// Code: MIT · Text: original (CC BY 4.0) · Drawings: generated in code (CC BY 4.0)
// Fonts: Funnel Display, Funnel Sans, Martian Mono (SIL OFL 1.1) · Needs postext ≥ 1.25.0
// An A3 poster whose elements hang from the bleed, the page, their slot or one another, never
// from coordinates; the second sheet is the same poster with each element framed and tagged.
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage,
} from 'https://esm.sh/postext';

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

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = {
  ink: '#0b0b0c', // text, and the Moon
  sky: '#1b3bff', // the sky, the foot band and the dates
  sun: '#ffe53b', // the Sun and the kicker
  paper: '#ffffff', // type on the sky
  muted: '#5c5f66', // speakers and the colophon
  guide: '#ff2d9b', // frames and tags on the construction sheet
};
// col(id): a colour linked to its palette entry, so a swapped palette recolours the sheet.
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' } })),
  { id: 'main-color', name: 'sky (defaults)', value: { hex: palette.sky, model: 'hex' } },
];

const A3 = { width: 297, height: 420 }; // mm, the trim
const MARGIN = 18; // mm at the top and sides: the body's edges, and the header container's
const FOOT = 30; // mm, the bottom margin: the footer container and its band
const BLEED = 0; // mm; 3 for the printer, which switches on page.cutLines below
// at(): a placement. Offsets are distances from the anchor point, never page coordinates.
const at = (to, edge, x = 0, y = 0, size) => ({ anchor: { to, edge },
  offset: { x: mm(x), y: mm(y) }, ...(size && { size }) });
const wide = (w) => ({ width: mm(w) }); // fixed, so a text is as wide as its column
const square = (d) => ({ width: mm(d), height: mm(d) });
// Design text is centred by default, and in a footer it is cut with '…': these wrap, set left.
const text = (id, content, face, placement) => ({ kind: 'text', id, content, overflow: 'wrap',
  align: 'left', ...face, placement });
const face = (fontFamily, size, fontWeight, colour, more) => ({ fontFamily, fontSize: pt(size),
  fontWeight, color: col(colour), ...more });
const caps = (tracking) => ({ letterSpacing: pt(tracking), textTransform: 'uppercase' });
const FACE = { // lineHeight as a multiple of each size
  kicker: face('Martian Mono', 12, 500, 'sun', caps(1.8)),
  title: face('Funnel Display', 120, 800, 'paper', { lineHeight: 0.86 }),
  deck: face('Funnel Sans', 22, 400, 'paper', { lineHeight: 1.22 }),
  day: face('Funnel Display', 84, 800, 'sky', { lineHeight: 0.9 }),
  talk: face('Funnel Sans', 19, 600, 'ink', { lineHeight: 1.14 }),
  who: face('Funnel Sans', 12.5, 400, 'muted', { lineHeight: 1.3, italic: true }),
  foot: face('Martian Mono', 9, 500, 'paper', caps(0.9)),
};

// #region answer: two elements pinned to frames of the sheet, every other one to an element
const SKY = 272; // mm from the trim's top edge to the horizon, the sky's lower edge
const INSET = BLEED + MARGIN; // from the bleed's edge to the text's edge
const AIR = 14; // mm between the standfirst and the horizon
const poster = () => ({ enabled: true, slot: { elements: [ // array order is paint order
  // 'bleed' and 'page' are frames of the sheet; 'container' would be this heading's own box.
  { kind: 'box', id: 'sky', style: { backgroundColor: col('sky') }, // no width: fills the bleed
    placement: at('bleed', 'top-left', 0, 0, { height: mm(BLEED + SKY) }) },
  ...eclipse(), // the Sun hangs from the trim's corner, the Moon and the corona from the Sun
  // The headline stands on the horizon and grows upwards: 'above' puts an element's
  // bottom-left corner on the top-left corner of its anchor, so a longer title lifts the kicker.
  text('deck', '{attr.deck}', FACE.deck, { ...at('#sky', 'align-bottom', INSET, -AIR),
    size: { maxWidth: mm(153) } }), // shrink-wraps its lines, but wraps at 153 mm
  text('title', '{titleText}', FACE.title, at('#deck', 'above', 0, -9)), // breaks at the \\
  text('kicker', '{attr.kicker}', FACE.kicker, at('#title', 'above', 0, -7)),
  ...programme(), // four columns chained off the horizon
] } }); // hook-up: headings.levels[0] = { span: 'page', breakBefore, advancedDesign: poster() }
// {titleText} is the heading's text; each {attr.*} is written on its line in content.md.
// #endregion

// #region eclipse: the Sun pinned to the trim's corner, the Moon and the corona to the Sun
const SUN = 150; // mm across; the Moon is drawn the same size
const CORONA = 300; // mm, the square picture of the corona
const HALO = (CORONA - SUN) / 2; // how far the corona reaches past the Sun on every side
const BITE = { x: 2, y: 2.5 }; // mm the Moon sits off the Sun: a sliver is left at upper left
const disc = (id, colour, placement) => ({ kind: 'box', id, placement: { ...placement,
  size: square(SUN) }, style: { backgroundColor: col(colour), borderRadius: mm(SUN / 2) } });
const eclipse = () => [
  { kind: 'image', id: 'corona', resourceId: 'corona', // listed before the discs: painted behind
    placement: at('#sun', 'align-top', -HALO, -HALO, square(CORONA)) },
  disc('sun', 'sun', at('page', 'top-right', 30, 25)), // 30 mm of it past the right edge
  disc('moon', 'ink', at('#sun', 'align-top', BITE.x, BITE.y)),
];
// #endregion

// #region programme: four evenings in fixed-width columns, chained right-of and below
const GUT = 7; // mm between the evenings, and between the two columns of text under them
const COL = (A3.width - 2 * MARGIN - 3 * GUT) / 4; // four columns across the text width: 60 mm
const DROP = 12; // mm from the horizon to the dates
const programme = () => [1, 2, 3, 4].flatMap((n) => [
  text(`day${n}`, `{attr.d${n}}`, FACE.day, n === 1
    ? at('#sky', 'below', INSET, DROP, wide(COL)) // the first date hangs from the horizon
    : at(`#day${n - 1}`, 'right-of', GUT, 0, wide(COL))), // the others from the one before
  text(`talk${n}`, `{attr.t${n}}`, FACE.talk, at(`#day${n}`, 'below', 0, 3, wide(COL))),
  text(`who${n}`, `{attr.s${n}}`, FACE.who, at(`#talk${n}`, 'below', 0, 2.5, wide(COL))),
]);
// #endregion

// #region footer: a band and the club's mark in the footer slot, which prints on every page
const MARK = 10; // mm, the club's mark
const PAD = (FOOT - MARK) / 2; // centres the mark in the bottom margin
const markTall = { height: mm(MARK) }; // as tall as the mark: the line is centred on it
const footer = { elements: [
  { kind: 'box', id: 'band', style: { backgroundColor: col('sky') }, // the bottom margin, bled
    placement: at('bleed', 'bottom-left', 0, 0, { height: mm(BLEED + FOOT) }) },
  // The container runs from the text down to the trim: 'bottom-*' counts up, 'top-*' down.
  { kind: 'image', id: 'mark', resourceId: 'mark',
    placement: at('container', 'bottom-left', 0, -PAD, square(MARK)) },
  text('club', '{attr.club}', FACE.foot, at('#mark', 'right-of', 3, 0, markTall)),
  text('free', '{attr.free}', FACE.foot, at('container', 'top-right', 0, PAD, markTall)),
] };
// #endregion

// #region guides: the same slots, each element framed and tagged with its own placement
const GUIDE = { borderColor: col('guide'), borderWidth: pt(1) };
const signed = ({ value }) => `${value < 0 ? '−' : '+'}${Math.abs(value)}`; // all offsets in mm
const describe = ({ id, placement: { anchor: { to, edge }, offset = {} } }) => [`#${id}`,
  `${edge} ${to}`, // the two words of anchor: { to, edge }
  ['x', 'y'].filter((k) => offset[k]?.value).map((k) => `${k} ${signed(offset[k])}`).join(' '),
].filter(Boolean).join(' · ');
// A border never changes an element's size (padding does), so each frame traces its box.
// An image or a rounded box gets a square frame of its size, hung from its top-left corner.
const frame = (el) => (el.kind === 'text' ? [{ ...el, box: { ...el.box, ...GUIDE } }]
  : el.kind === 'box' && !el.style.borderRadius ? [{ ...el, style: { ...el.style, ...GUIDE } }]
    : [el, { kind: 'box', id: `${el.id}-box`, style: GUIDE,
      placement: at(`#${el.id}`, 'align-top', 0, 0, el.placement.size) }]);
const tag = (el, [edge, x = 0, y = 0, note]) => text(`${el.id}-tag`, note ?? describe(el),
  face('Martian Mono', 8.5, 500, 'ink', { box: { backgroundColor: col('guide'),
    padding: { top: pt(1.6), bottom: pt(1.2), left: pt(3), right: pt(3) } } }),
  at(`#${el.id}`, edge, x, y));
const guides = (elements, tags) => [...elements.flatMap(frame),
  ...elements.filter((el) => tags[el.id]).map((el) => tag(el, tags[el.id]))];
const container = (id) => ({ kind: 'box', id, style: GUIDE, // the slot's own box, drawn
  placement: at('container', 'top-left', 0, 0, { width: 'fill', height: 'fill' }) });
const TAGS = { // where each tag sits against its element: [edge, x, y, text]
  sky: ['align-bottom', INSET, -3], corona: ['align-bottom', 90, -2], sun: ['above'],
  moon: ['below', 20, 2], kicker: ['above'], title: ['above'], deck: ['above'],
  day1: ['above', 0, -6], day2: ['above'], who1: ['below'],
  mark: ['above', 0, -2], club: ['above', 69, -2], free: ['above', -30, -2],
  header: ['align-bottom', 0, -2, t({ en: 'header container', es: 'contenedor de la cabecera' })],
  footer: ['align-bottom', 0, -2, t({ en: 'footer container', es: 'contenedor del pie' })],
};
// #endregion

const LEAD = 16; // body leading in pt
const config = () => ({
  colorPalette,
  locale: t({ en: 'en-us', es: 'es' }), // the language of each edition, and of its bundle
  page: { width: mm(A3.width), height: mm(A3.height), dpi: 150, // 150 dpi is for the screen
    cutLines: { enabled: BLEED > 0, bleed: mm(BLEED) },
    margins: { top: mm(MARGIN), bottom: mm(FOOT), left: mm(MARGIN), right: mm(MARGIN) } },
  layout: { layoutType: 'double', gutterWidth: mm(GUT) }, // the text columns under the evenings
  bodyText: { fontFamily: 'Funnel Sans', fontSize: pt(12), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), textAlign: 'left', firstLineIndent: pt(0),
    paragraphSpacing: true },
  // The hidden title is still measured, so it needs a face FONTS loads.
  headings: { fontFamily: 'Funnel Display', fontWeight: 800,
    levels: [{ level: 1, span: 'page', advancedDesign: poster(),
      breakBefore: { enabled: true, parity: 'any' } }] }, // each poster on a new sheet
  // {style="guides"}: the same design framed and tagged, and a header and footer of its own.
  headingStyles: [{ id: 'guides',
    advancedDesign: { enabled: true, slot: { elements: guides(poster().slot.elements, TAGS) } },
    header: { elements: guides([container('header')], TAGS) },
    footer: { elements: guides([...footer.elements, container('footer')], TAGS) } }],
  paragraphStyles: [{ id: 'colophon', fontFamily: 'Martian Mono', fontSize: pt(8),
    lineHeight: pt(LEAD * 0.75), color: col('muted') }],
  header: { elements: [] }, // the poster has none; the guides page draws the empty container
  footer,
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`# The\\Shadow\\Returns {kicker="April 2027 · Thursdays at 19:30" deck="Four talks to get ready for the total eclipse of 2 August 2027, when the Moon’s shadow crosses the Strait of Gibraltar." d1="8" t1="Why the Moon fits the Sun" s1="Marta Iribarren, astronomer" d2="15" t2="Where the shadow will fall" s2="Óscar Beltrán, meteorologist" d3="22" t3="What the corona tells us" s3="Lucía Ferrándiz, physicist" d4="29" t4="Looking at the Sun without harm" s4="Tomás Rey, optometrist" club="Umbra Circle · amateur astronomers since 1987" free="Free entry · no booking"}
Esempio Markdown · 14 righe · content.en.md Every lecture begins at 19:30 and lasts about an hour, questions included. The Umbra Circle hall is at 14 Lighthouse Road; the doors open at 19:00, entry is free and there is no booking, so the 120 seats go to whoever comes first. On 29 April every listener takes home a pair of eclipse glasses made to ISO 12312-2: wear them through each partial phase, and take them off only while the Sun is completely covered. On eclipse morning the Circle sets up filtered telescopes on the harbour wall. # The\\Shadow\\Returns {style="guides" kicker="April 2027 · Thursdays at 19:30" deck="Four talks to get ready for the total eclipse of 2 August 2027, when the Moon’s shadow crosses the Strait of Gibraltar." d1="8" t1="Why the Moon fits the Sun" s1="Marta Iribarren, astronomer" d2="15" t2="Where the shadow will fall" s2="Óscar Beltrán, meteorologist" d3="22" t3="What the corona tells us" s3="Lucía Ferrándiz, physicist" d4="29" t4="Looking at the Sun without harm" s4="Tomás Rey, optometrist" club="Umbra Circle · amateur astronomers since 1987" free="Free entry · no booking"} **How to read this sheet.** Each pink frame is an element’s box as laid out, and a tag gives an element’s id, the edge it is pinned by, what it is pinned to and any offset in millimetres. Above the footer only two elements hang from the sheet itself: the sky from the bleed and the Sun from the page, which is the trim. Every other one hangs from an element, so a longer title lifts the kicker, and the four evenings move with the horizon. The frames in the top and bottom margins are the header and footer containers. The footer container runs from the text down to the trim, so its bottom anchors count up from the trim and its top anchors down from the text. :::paragraphs{style="colophon"} Set in Funnel Display, Funnel Sans and Martian Mono (SIL OFL) · Text and drawings: original, CC BY 4.0 · The Umbra Circle and its speakers are fictional. :::
`; // content.<lang>.md, inlined by the Cookbook // #region art: the corona and the club's mark, drawn in code, and their resources function mulberry32(seed) { // a seeded generator: the same corona on every run return () => { seed = (seed + 0x6d2b79f5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function coronaSvg() { // a glow, then 14 streamers, 70 polar plumes and 160 fine rays const rand = mulberry32(20270802); const R = (500 * SUN) / CORONA; // the Sun's radius in the 1000-unit square const TILT = Math.PI / 4; // the Sun's equator: long streamers go up-left, clear of the kicker const TYPE = [2.2, 3.05]; // radians from the centre where the type is: rays there stop at 9 mm const p = (r, a) => `${(500 + r * Math.cos(a)).toFixed(1)} ${(500 + r * Math.sin(a)).toFixed(1)}`; const ray = (a, full, half, opacity) => { // a petal from the limb, tapered to a point const turn = ((a % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI); const length = turn > TYPE[0] && turn < TYPE[1] ? Math.min(full, R * 0.12) : full; const bend = (rand() - 0.5) * 0.06; return `<path d="M${p(R * 0.97, a - half)}C${p(R + length * 0.35, a - half * 1.1)} ` + `${p(R + length * 0.7, a + bend - half * 0.25)} ${p(R + length, a + bend)}` + `C${p(R + length * 0.7, a + bend + half * 0.25)} ${p(R + length * 0.35, a + half * 1.1)} ` + `${p(R * 0.97, a + half)}Z" fill="${palette.paper}" fill-opacity="${opacity.toFixed(3)}"/>`; }; const glow = Array.from({ length: 28 }, (_, i) => `<circle cx="500" cy="500" ` + `r="${(R * (1.01 + i * 0.022)).toFixed(1)}" fill="${palette.paper}" fill-opacity="0.022"/>`) .reverse().join(''); const out = []; for (let i = 0; i < 14; i++) { // helmet streamers, two fans across the equator const a = TILT + (i % 2) * Math.PI + (rand() - 0.5) * 1.1; out.push(ray(a, R * (0.55 + 0.45 * rand()), 0.1 + 0.14 * rand(), 0.06 + 0.06 * rand())); } for (let i = 0; i < 70; i++) { // polar plumes, shorter and thinner than the streamers const a = TILT + Math.PI / 2 + (i % 2) * Math.PI + (rand() - 0.5) * 1.3; out.push(ray(a, R * (0.18 + 0.3 * rand()), 0.008 + 0.012 * rand(), 0.1 + 0.12 * rand())); } for (let i = 0; i < 160; i++) { // fine rays all round const a = (i / 160) * 2 * Math.PI + (rand() - 0.5) * 0.04; out.push(ray(a, R * (0.12 + 0.3 * rand()), 0.006 + 0.014 * rand(), 0.05 + 0.07 * rand())); } return `<svg xmlns="http://www.w3.org/2000/svg" width="1500" height="1500" ` + `viewBox="0 0 1000 1000">${glow}${out.join('')}</svg>`; } function markSvg() { // the Umbra Circle's mark: a ring round an eclipsed Sun return `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 100 100">` + `<circle cx="50" cy="50" r="45" fill="none" stroke="${palette.paper}" stroke-width="6"/>` + `<circle cx="50" cy="50" r="27" fill="${palette.sun}"/>` + `<circle cx="56" cy="45" r="27" fill="${palette.sky}"/></svg>`; } const svg = (id, size, altText) => ({ id, typeId: 'figure', kind: 'svg', altText, svg: { fileId: `${id}.svg`, width: size, height: size }, createdAt: 0, updatedAt: 0 }); // Image elements draw resources. Nothing cites them, so neither is placed or numbered as a figure. const resources = [ svg('corona', 1500, t({ en: 'The solar corona round the eclipsed Sun', es: 'La corona solar alrededor del Sol eclipsado' })), svg('mark', 200, t({ en: 'The Umbra Circle’s mark', es: 'El emblema del Círculo Umbra' })), ]; // #endregion // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the pages paint, loaded before the first build. const FONTS = { 'Funnel Display': ['800'], 'Funnel Sans': ['400', '400i', '600', '700'], 'Martian Mono': ['400', '500'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadSvg('corona.svg', coronaSvg()); await loadSvg('mark.svg', markSvg()); // pageIndexOffset 1 makes the poster a verso, so the viewer sets the two sheets side by side. const content = { markdown, resources, continuation: { pageIndexOffset: 1 } }; const doc = await buildDocumentWithFonts(content, config(), kitFonts(FONTS)); showPages(doc, { title: t({ en: 'Anchoring cheat sheet', es: 'Chuleta de anclajes' }) });
Kit · core, fonts, viewer, images: uguale in ogni ricetta · 270 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 · images v3 ── recipes with pictures · postext.dev/cookbook /** Registers a photo or PNG for the canvas and keeps its bytes for the PDF. * fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */ async function loadImage(fileId, url) { const res = await fetch(url); if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`); const bytes = new Uint8Array(await res.arrayBuffer()); registerResourceImage(fileId, await createImageBitmap(new Blob([bytes]))); (loadImage.bytes ??= new Map()).set(fileId, bytes); } /** Registers SVG markup as a vector image, its fonts inside (inlineSvgFonts, ≥ 1.25). * `options` go to registerResourceImage: { singleInk: true } tints it on the canvas. */ async function loadSvg(fileId, svg, options) { if (typeof inlineSvgFonts === 'function') svg = await inlineSvgFonts(svg, svgFaces); const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img, options); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** A face's Fontsource files: nearest weight; latin + what the SVG sets. */ async function svgFaces(family, weight, style, { codePoints = [] } = {}) { const [id, m] = [fontsourceId(family), await fontsourceMeta(family)]; const w = m?.weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)) ?? weight; const s = m?.styles.includes(style) === false ? 'normal' : style; return Promise.all(['latin', ...kitSubsetsFor(String.fromCodePoint(...codePoints), m)].map(async (x) => new Uint8Array( await (await fetch(`${await fontsourceAt(id)}files/${id}-${x}-${w}-${s}.woff2`)).arrayBuffer()))); } /** renderToPdf({ resourceBytes: imageBytes }) */ function imageBytes(fileId) { return loadImage.bytes?.get(fileId); } /** renderToHtml({ resourceImageUrl: imageUrl }) */ function imageUrl(fileId) { const bytes = imageBytes(fileId); if (!bytes) return undefined; imageUrl.urls ??= new Map(); if (!imageUrl.urls.has(fileId)) { const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg'; imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type }))); } return imageUrl.urls.get(fileId); } // ─── /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

#Aggiungi 3 mm di abbondanza e i segni di taglio

Tre millimetri di BLEED attivano le linee di taglio e spingono il cielo e la fascia di piede 3 mm dentro l’abbondanza a cui sono agganciati, mentre INSET tiene il testo sul margine; il Sole, la Luna e la corona sono agganciati alla pagina, e tutto ciò che una pagina dipinge viene ritagliato all’abbondanza, così il Sole finisce 3 mm oltre il rifilo e la zona esterna contiene solo i segni di taglio.

-const BLEED = 0; // mm; 3 for the printer, which switches on page.cutLines below
+const BLEED = 3; // mm; 3 for the printer, which switches on page.cutLines below

#Alza l’orizzonte

Togliere 20 mm a SKY solleva il bordo del cielo e con esso il sommario, il titolo, l’occhiello e i dodici testi del programma, mentre le note in corpo minore partono da una riga della griglia più alta; il Sole è agganciato alla pagina, quindi la seconda modifica lo solleva degli stessi 20 mm, altrimenti la «w» di Shadow toccherebbe la falce.

-const SKY = 272; // mm from the trim's top edge to the horizon, the sky's lower edge
+const SKY = 252; // mm from the trim's top edge to the horizon, the sky's lower edge
-  disc('sun', 'sun', at('page', 'top-right', 30, 25)), // 30 mm of it past the right edge
+  disc('sun', 'sun', at('page', 'top-right', 30, 5)), // 30 mm of it past the right edge

Errori frequenti

Errore frequente

Gli elementi di intestazione e piè di pagina dipingono sopra il testo

Gli elementi di intestazione e piè di pagina vengono dipinti sopra la pagina e l’area del testo non fa loro spazio. Tienili entro i margini, che sono ciò che ne riserva lo spazio. Testatine e folio →

Errore frequente

Valori degli attributi: niente { o }; virgolette semplici per un valore con "

Il valore di un attributo finisce alla graffa di chiusura, quindi non può contenere { o }. Un valore che contiene una virgoletta doppia va tra virgolette semplici; il simbolo del dollaro va bene. Attributi dei titoli →

Verifica della Sandbox · designCyclicAnchor

Riferimento di ancora ciclico

Perché. Gli elementi grafici si ancorano l’uno all’altro in un ciclo, quindi nessuno di essi può essere collocato.

Soluzione. Ancora un elemento della catena al contenitore, alla pagina o all’abbondanza. Documentazione →

Verifica della Sandbox · designDanglingAnchor

Riferimento di ancora orfano

Perché. Un elemento grafico si ancora a un #id che nessun elemento dello slot ha.

Soluzione. Correggi l’id, oppure aggiungi l’elemento a cui si riferisce. Documentazione →

  • Un pen non riceve alcun resoconto di un’ancora circolare o sospesa. Il pannello Verifiche della Sandbox le elenca nell’intestazione, nel piè di pagina, nelle pagine di parte e nei livelli di titolo, ma non negli stili dei titoli come guides. Un elemento fissato a un #id che non esiste viene collocato rispetto al contenitore: un bordo di elemento come 'below' ripiega sull’angolo superiore sinistro del contenitore, scostamento compreso, e un bordo di contenitore come 'top-right' su quell’angolo. In un ciclo, un elemento ripiega allo stesso modo e gli altri si concatenano a partire da lui.
  • 'align-left' e 'align-top' cadono sullo stesso punto: entrambi mettono l’angolo superiore sinistro dell’elemento sull’angolo superiore sinistro della sua ancora.
  • Un bordo viene dipinto centrato sul margine del riquadro, quindi ogni cornice rosa da 1 pt sporge di 0,5 pt fuori dal riquadro che traccia, anche se la documentazione della configurazione dice che i tratti vengono dipinti all’interno.

Crediti

Testo
Testo originale, CC BY 4.0
Font
Funnel Display (SIL OFL 1.1) · Funnel Sans (SIL OFL 1.1) · Martian Mono (SIL OFL 1.1)
Sandbox