本文へスキップ
レシピ番号136

レシピ集 · 第10章 · 出力と組み込み

写真が動く、生きている新聞

6つの動画リソースを4段組みのタブロイド紙に報道写真として組みます。autoplay、loop、exclusive: falseで、Folioではクリップがそろって再生されます。

1ページ(全4ページ)

  • 英語の見本:日本語版はまだありません
  • 仕上がり280 × 430 mm
  • 4段, 段間4.5 mm
  • Libre Caslon Text 8.8/11.6
  • Courier Prime
  • Oswald
  • 4ページ
  • レベル
  • Postext 1.18.0
  • 組版時間530 ms
  • コード250行

かんたんな説明

1930年代風の地方紙で、どの写真も短い無声映画です。紙の上では静止画ですが、3Dビューアーではすべてが同時に、何度でも動きます。おとぎ話に出てくる新聞のようです。

できあがり

『The Wexcombe Lantern』は、1933年6月の号という設定の架空の海辺の週刊紙です。280 × 430 mmのタブロイド判のページを、罫で区切った4段に組みます。長体の題字、3段にまたがる写真を添えたトップ記事、2段の写真を添えたもう1本の記事、潮汐表、案内広告、スポーツ面が入ります。写真はどれも5秒の白黒映画です。印刷では、またビューアーの下の平面のページでは、どれも最初のフレームです。Folioで淡い新聞用紙の折った新聞として開くと、開いたページの写真がいっせいに動き、動き続けます。市長が話し、飛行士が手を振り、漁師が笑い、ゴールキーパーが飛び込んではまた立ち上がります。ページは何も描いていません。レイアウトはふつうの新聞で、動きはクリップが受け持ちます。

このレシピが答える質問

  • 1ページにある無音のループ動画をいくつも同時に再生し、そのページを開くたびに始まるようにするには?
  • 3Dビューアーで新聞を折った紙として見せ、経済面をサーモンピンクの紙にするには?

手短な答え

script.js · 46–60行コード全体で見る
// A video resource prints its poster (the first frame) on the flat page. In Folio, and in the
// HTML viewer, the poster comes alive: autoplay starts it muted when its page is on show, loop
// keeps it going, and exclusive: false lets it run alongside the other clips instead of
// pausing them, as one exclusive player at a time would.
const videoStyle = {
  playMark: { enabled: false }, // a press photograph, not a play button
  qr: { enabled: false }, // the clips live on the page, not behind a link
  linkPoster: false,
  player: { autoplay: true, loop: true, muted: true, exclusive: false, controls: false },
};
const clip = (id, url, [w, h], placement, caption, altText) => ({ id, typeId: 'picture',
  kind: 'video', createdAt: 0, updatedAt: 0, caption, altText, placement,
  video: { source: 'file', fileId: `${id}-loop.mp4`, url, // url: where the clip plays from
    format: 'mp4', width: w, height: h, duration: 5,
    poster: { fileId: `${id}-poster.jpg`, format: 'jpeg', width: w, height: h } } });

材料

種類
Libre Caslon Text, Oswald, Courier Prime (SIL OFL 1.1)
素材
  • actress-loop.mp4
  • actress-poster.jpg
  • aviator-loop.mp4
  • aviator-poster.jpg
  • clock-loop.mp4
  • clock-poster.jpg
  • fisherman-loop.mp4
  • fisherman-poster.jpg
  • keeper-loop.mp4
  • keeper-poster.jpg
  • mayor-loop.mp4
  • mayor-poster.jpg
  • Mrs Edith Rowe speaks at the opening of the Harbour line: a black-and-white loop of 5 s, a still generated from a text prompt and animated with the same first and last frame, encoded with ffmpeg (Generated With Diffusion Models, CC BY 4.0)
  • The first frame of the mayor loop, its printed picture (Generated With Diffusion Models, CC BY 4.0)
  • The Guildhall clock tower: a black-and-white loop of 5 s, a still generated from a text prompt and animated with the same first and last frame, encoded with ffmpeg (Generated With Diffusion Models, CC BY 4.0)
  • The first frame of the clock loop, its printed picture (Generated With Diffusion Models, CC BY 4.0)
  • Skipper Jacob Hosking with the cod: a black-and-white loop of 5 s, a still generated from a text prompt and animated with the same first and last frame, encoded with ffmpeg (Generated With Diffusion Models, CC BY 4.0)
  • The first frame of the fisherman loop, its printed picture (Generated With Diffusion Models, CC BY 4.0)
  • Miss Constance Lacey in the cockpit: a black-and-white loop of 5 s, a still generated from a text prompt and animated with the same first and last frame, encoded with ffmpeg (Generated With Diffusion Models, CC BY 4.0)
  • The first frame of the aviator loop, its printed picture (Generated With Diffusion Models, CC BY 4.0)
  • Miss Rosalind Avery on the stage: a black-and-white loop of 5 s, a still generated from a text prompt and animated with the same first and last frame, encoded with ffmpeg (Generated With Diffusion Models, CC BY 4.0)
  • The first frame of the actress loop, its printed picture (Generated With Diffusion Models, CC BY 4.0)
  • Tom Rundle in goal: a black-and-white loop of 5 s, a still generated from a text prompt and animated with the same first and last frame, encoded with ffmpeg (Generated With Diffusion Models, CC BY 4.0)
  • The first frame of the keeper loop, its printed picture (Generated With Diffusion Models, CC BY 4.0)

作り方

#1 · どの写真もほかと一緒にループするクリップにする

コードは前の手短な答えにあります。動画リソースはポスター画像を図版として印刷するので、クリップは写真の代わりになり、写真と同じようにキャプションが付きます。ページを動かすのはプレーヤーの3つのオプションです。autoplayはページが表示されたときにクリップを始め(ブラウザーの要件どおり常にミュート)、loopは最初から再生し直し、exclusive: falseはほかのクリップと並行して再生させます。既定では動画は排他的で、再生を始めるとほかに再生中のものはすべて一時停止します。講義には合っていますが、動く写真を並べたページには向きません。再生マークとQRコードはオフにしてあるので、紙の上のクリップは報道写真として読めます。

#2 · ループごとにグリッド上の位置を決める

script.js · 64–90行コード全体で見る
const resources = [
  clip('mayor', asset('mayor-loop.mp4'), [640, 480],
    { position: 'top', span: 'column', columns: 3 },
    '**Mrs Edith Rowe** speaks from the platform at the foot of Quay Hill before cutting '
      + 'the ribbon.',
    'A woman in a dark coat and small hat speaks at a draped podium, one hand raised.'),
  clip('clock', asset('clock-loop.mp4'), [480, 640],
    { position: 'top', span: 'column', columns: 2 },
    '**Twelve o’clock** on the Guildhall tower, for the first time since 1924.',
    'A stone clock tower against clouds, pigeons on its ledge.'),
  clip('fisherman', asset('fisherman-loop.mp4'), [640, 480],
    { position: 'auto', span: 'column', columns: 2 },
    '**Skipper Jacob Hosking** and the 82-pound cod on the quay.',
    'An old fisherman in an oilskin laughs, holding up a huge cod.'),
  clip('aviator', asset('aviator-loop.mp4'), [640, 480],
    { position: 'top', span: 'column', columns: 2 },
    '**Miss Constance Lacey** waves from the cockpit on Wexcombe Common.',
    'A young woman in a flying helmet waves from the open cockpit of a biplane.'),
  clip('actress', asset('actress-loop.mp4'), [480, 640],
    { position: 'top', span: 'column', columns: 2 },
    '**Miss Rosalind Avery** as Hester Lane, on the garden steps in Act III.',
    'An actress in a long gown on a garden stage set, one arm outstretched.'),
  clip('keeper', asset('keeper-loop.mp4'), [640, 480],
    { position: 'top', span: 'column', columns: 3 },
    '**Tom Rundle** at Gannet Park, before the penalty he saved.',
    'A goalkeeper in a flat cap crouches on his line in front of a packed terrace.'),
];

レイアウトはlayoutType: 'multiple'で、columnCount: 4です。フロートのcolumnsが占める段数を決めます。市長とゴールキーパーは3段、飛行士、時計塔、漁師、女優は2段です。複数の段にまたがるフロートは、上端のそろった空いた段の並びの頭に入るか、それを参照した段とそれに続く空いた段の下端に入ります。そのため、::resourceの行をどこに置くかで空き位置が決まります。市長は最初の段落の後で参照されるので、1段目は本文に残り、写真は2段目から4段目の上に入ります。

#3 · 題字と各面の欄名を組む

script.js · 130–146行コード全体で見る
const nameplate = { enabled: true, slot: { elements: [
  depth(10), // the masthead is ten grid lines deep; the rules and the dateline hang from it
  { kind: 'rule', id: 'foot', thickness: pt(2.5), color: col('ink'),
    placement: { ...at('#depth', 'align-bottom', 0, -1.5), size: { width: 'fill' } } },
  ...[['left', '{attr.issue}'], ['center', '{publishDate}'], ['right', '{attr.price}']].map(
    ([align, content]) => text(`date-${align}`, content, { ...typed(9, { fontWeight: 700 }),
      align }, { ...at('#foot', 'above', 0, -1.6), size: { width: 'fill' } })),
  rule('thin', '#date-left', 0.6, 1.8),
  text('name', '{titleText}', { ...oswald(66, 700, { textTransform: 'uppercase',
    letterSpacing: pt(1.5) }), lineHeight: 1, align: 'center' },
  { ...at('#thin', 'above', 0, -2), size: { width: 'fill' } }),
  // Two ears over the name: the paper's age on the left, the weather on the right.
  text('since', '{attr.since}', { ...caps(8, 600, 'flag'), align: 'left' },
    { ...at('container', 'top-left', 0, 1), size: { width: mm(90) } }),
  text('weather', 'Weather: {attr.weather}', { ...typed(8.5, { italic: true }), align: 'right' },
    { ...at('container', 'top-right', 0, 1), size: { width: mm(110) } }),
] } };

第1面のレベル1見出しは新聞の名前で、デザインスロットから描きます。上に2つの耳、Oswaldの大文字の題字、細い罫と太い罫に挟まれた日付行です。中面は3 ptの帯の上の赤い欄名で始まり(pagesリージョン)、第1面を除く各ページの頭にノンブルの行が付きます。

#4 · Folioで新聞を開く

script.js · 566–575行コード全体で見る
const stage = Object.assign(document.createElement('section'),
  { id: 'folio', ariaLabel: 'The newspaper in 3D' });
stage.style.cssText = 'height: min(82vh, 820px); margin: 0 auto; max-width: 1280px';
document.getElementById('pages').before(stage);
createFolioFromDocument(stage, doc, {
  // Folio plays each clip from its video.url, muted, while its page is on show.
  appearance: { textureBaseUrl: 'https://postext.dev/folio/textures' },
  onChange: ({ pages }) => kitStatus(`${doc.pages.length} pages · open at `
    + pages.map((i) => doc.pages[i].pageLabel || i + 1).join('–')),
});

config.folioは新聞を昼の光の中でオークの机に置きます。45 g/m²の新聞用紙には次のページが透けて見えます。binding.type: 'folded'は、紙を一度だけ折って一方を他方の中に重ねたもので、ホチキスも背もありません。新聞はこう組まれています。Folioは開いた見開きのクリップをすべて一つのテクスチャーに描き(一度に8本まで)、ページめくりがそのページで止まるたびに自動再生のループを始め、ページがめくられると止めます。排他的な動画をクリックすれば、これまでどおり音声付きで単独で再生され、ループはそれが止まるまで待ちます。

レシピの全体

Sandbox
// ═══ Postext Cookbook · Nº 136 · A living newspaper of video loops ══════════════
// https://postext.dev/en/cookbook/living-newspaper-video-loops
// Code: MIT · Text: original (CC BY 4.0) · Clips: generated with diffusion models (CC BY 4.0)
// Fonts: Oswald, Libre Caslon Text, Courier Prime (SIL OFL 1.1) · Needs postext ≥ 1.18.0
import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage }
  from 'https://esm.sh/postext';
import { createFolioFromDocument } from 'https://esm.sh/postext-folio';

const LANG = 'en'; // @lang: the language of the sample document ('en')
const RECIPE = 'living-newspaper-video-loops';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: newsprint, a warm black and a dark red for the flags
const palette = {
  ink: '#16130f', // text, rules and the nameplate
  paper: '#f3efe4', // newsprint, and type reversed out of ink or red
  flag: '#8e1b14', // the one accent: section flags, kickers, the jump lines
  tint: '#e6dfcf', // the In brief strip
  rule: '#8f897c', // hairlines: the column rules
  muted: '#5b554b', // bylines, captions' credits, the imprint
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [ // defaults link to 'main-color': point it at the ink, never blue
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
// #endregion
const MARGIN = { top: 12, bottom: 12, side: 12 }; // mm: a tabloid's narrow margins
const GUTTER = 4.5; // mm between the four columns
const LEAD = 11.6; // body leading in pt: the baseline grid
const oswald = (size, weight, look = {}) => ({ fontFamily: 'Oswald', fontSize: pt(size),
  fontWeight: weight, color: col('ink'), ...look });
const caps = (size, weight, colour = 'ink') => oswald(size, weight, { color: col(colour),
  textTransform: 'uppercase', letterSpacing: pt(size * 0.12) });
const typed = (size, look = {}) => ({ fontFamily: 'Courier Prime', fontSize: pt(size),
  color: col('ink'), ...look }); // the dateline and the captions: a typewriter
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });
const text = (id, content, look, placement) => ({ kind: 'text', id, content, align: 'left',
  overflow: 'wrap', color: col('ink'), ...look, placement }); // never the ellipsis default
const rule = (id, under, weight, gap = 0) => ({ kind: 'rule', id, thickness: pt(weight),
  color: col('ink'), placement: { ...at(under, 'above', 0, -gap), size: { width: 'fill' } } });
const depth = (lines) => ({ kind: 'box', id: 'depth', style: {}, placement: {
  ...at('container', 'top-left'), size: { width: 'fill', height: pt(lines * LEAD) } } });

// #region answer: every picture is a clip that loops, autoplays and plays with the others
// A video resource prints its poster (the first frame) on the flat page. In Folio, and in the
// HTML viewer, the poster comes alive: autoplay starts it muted when its page is on show, loop
// keeps it going, and exclusive: false lets it run alongside the other clips instead of
// pausing them, as one exclusive player at a time would.
const videoStyle = {
  playMark: { enabled: false }, // a press photograph, not a play button
  qr: { enabled: false }, // the clips live on the page, not behind a link
  linkPoster: false,
  player: { autoplay: true, loop: true, muted: true, exclusive: false, controls: false },
};
const clip = (id, url, [w, h], placement, caption, altText) => ({ id, typeId: 'picture',
  kind: 'video', createdAt: 0, updatedAt: 0, caption, altText, placement,
  video: { source: 'file', fileId: `${id}-loop.mp4`, url, // url: where the clip plays from
    format: 'mp4', width: w, height: h, duration: 5,
    poster: { fileId: `${id}-poster.jpg`, format: 'jpeg', width: w, height: h } } });
// #endregion

// #region clips: where each loop sits on the four-column grid
const resources = [
  clip('mayor', asset('mayor-loop.mp4'), [640, 480],
    { position: 'top', span: 'column', columns: 3 },
    '**Mrs Edith Rowe** speaks from the platform at the foot of Quay Hill before cutting '
      + 'the ribbon.',
    'A woman in a dark coat and small hat speaks at a draped podium, one hand raised.'),
  clip('clock', asset('clock-loop.mp4'), [480, 640],
    { position: 'top', span: 'column', columns: 2 },
    '**Twelve o’clock** on the Guildhall tower, for the first time since 1924.',
    'A stone clock tower against clouds, pigeons on its ledge.'),
  clip('fisherman', asset('fisherman-loop.mp4'), [640, 480],
    { position: 'auto', span: 'column', columns: 2 },
    '**Skipper Jacob Hosking** and the 82-pound cod on the quay.',
    'An old fisherman in an oilskin laughs, holding up a huge cod.'),
  clip('aviator', asset('aviator-loop.mp4'), [640, 480],
    { position: 'top', span: 'column', columns: 2 },
    '**Miss Constance Lacey** waves from the cockpit on Wexcombe Common.',
    'A young woman in a flying helmet waves from the open cockpit of a biplane.'),
  clip('actress', asset('actress-loop.mp4'), [480, 640],
    { position: 'top', span: 'column', columns: 2 },
    '**Miss Rosalind Avery** as Hester Lane, on the garden steps in Act III.',
    'An actress in a long gown on a garden stage set, one arm outstretched.'),
  clip('keeper', asset('keeper-loop.mp4'), [640, 480],
    { position: 'top', span: 'column', columns: 3 },
    '**Tom Rundle** at Gannet Park, before the penalty he saved.',
    'A goalkeeper in a flat cap crouches on his line in front of a packed terrace.'),
];
// #endregion

// The three small tables: the week's tides, the picture houses and the cricket.
const cell = (content, align = 'left') => ({ content, align });
const head = (content, align = 'left') => ({ content, align, isHeader: true });
const table = (id, caption, columnWidths, rows, placement) => ({ id, typeId: 'picture',
  kind: 'table', createdAt: 0, updatedAt: 0, caption, placement, table: { model: {
    headerRowCount: 1, columnWidths, rows: [rows[0].map((h, i) => head(h, i ? 'right' : 'left')),
      ...rows.slice(1).map((r) => r.map((c, i) => cell(c, i ? 'right' : 'left')))] } } });
resources.push(
  table('tides', '**High water at Wexcombe Quay**', [1.2, 1, 1, 0.9], [
    ['Day', 'Morning', 'Evening', 'Feet'], ['Sat 10', '5.12', '5.38', '16.4'],
    ['Sun 11', '5.58', '6.21', '16.9'], ['Mon 12', '6.41', '7.03', '17.2'],
    ['Tue 13', '7.23', '7.44', '17.3'], ['Wed 14', '8.04', '8.25', '17.0']],
  { position: 'here' }),
  table('pictures', '**This week at the picture houses**', [1, 2.2, 1.3], [
    ['House', 'Programme', 'Shows'], ['Regal', 'All Aboard for Lisbon', '2.30, 6, 8.30'],
    ['Electric', 'The Lone Rider of Red Mesa', '6.15, 8.40'],
    ['Both', 'Children’s matinée', 'Sat 10.30']], { position: 'here' }),
  table('wireless', '**The Regional programme**', [0.8, 2.6], [
    ['Time', 'Programme'], ['6.0', 'The Children’s Hour'], ['6.40', 'News and weather'],
    ['7.0', 'Cornish Songs, by the Wexcombe Male Voice Choir'],
    ['7.45', 'Talk: The Coasting Trade, by a ship’s master'],
    ['8.15', 'Dance Music from the Assembly Rooms'], ['10.0', 'Second news; shipping']],
  { position: 'here' }),
  table('fixtures', '**This week’s fixtures**', [1.1, 2.3, 1.1], [
    ['Day', 'Match', 'Ground'], ['Sat 10', 'Cricket: Wexcombe v. Truro', 'The Rec'],
    ['Wed 14', 'Bowls: Park v. Hayle', 'Park green'],
    ['Sat 17', 'Cricket: Redruth v. Wexcombe', 'Redruth'],
    ['Sat 24', 'County Cup final: Athletic v. Rovers', 'Gannet Park']], { position: 'here' }),
  table('wheelers', '**Fifty miles: the first five**', [0.3, 1.2, 1.9], [
    ['', 'Rider', 'Time'], ['1', 'L. Uren', '2 h 11 m 40 s'], ['2', 'T. Bray', '2 h 14 m 05 s'],
    ['3', 'S. Harvey', '2 h 15 m 52 s'], ['4', 'C. Nance', '2 h 19 m 31 s'],
    ['5', 'R. Jenkin', '2 h 20 m 18 s']], { position: 'here' }),
  table('scores', '**Wexcombe v. St Austell**', [1.6, 0.7, 1.7], [
    ['Innings', 'Runs', 'Best'], ['Wexcombe', '168', 'Dunstan 64'],
    ['St Austell', '127', 'Carne 5 for 22']], { position: 'here' }));

// #region nameplate: the front page's H1 is the paper's name, ruled above and below
const nameplate = { enabled: true, slot: { elements: [
  depth(10), // the masthead is ten grid lines deep; the rules and the dateline hang from it
  { kind: 'rule', id: 'foot', thickness: pt(2.5), color: col('ink'),
    placement: { ...at('#depth', 'align-bottom', 0, -1.5), size: { width: 'fill' } } },
  ...[['left', '{attr.issue}'], ['center', '{publishDate}'], ['right', '{attr.price}']].map(
    ([align, content]) => text(`date-${align}`, content, { ...typed(9, { fontWeight: 700 }),
      align }, { ...at('#foot', 'above', 0, -1.6), size: { width: 'fill' } })),
  rule('thin', '#date-left', 0.6, 1.8),
  text('name', '{titleText}', { ...oswald(66, 700, { textTransform: 'uppercase',
    letterSpacing: pt(1.5) }), lineHeight: 1, align: 'center' },
  { ...at('#thin', 'above', 0, -2), size: { width: 'fill' } }),
  // Two ears over the name: the paper's age on the left, the weather on the right.
  text('since', '{attr.since}', { ...caps(8, 600, 'flag'), align: 'left' },
    { ...at('container', 'top-left', 0, 1), size: { width: mm(90) } }),
  text('weather', 'Weather: {attr.weather}', { ...typed(8.5, { italic: true }), align: 'right' },
    { ...at('container', 'top-right', 0, 1), size: { width: mm(110) } }),
] } };
// #endregion

// #region pages: a section flag on each inside page, a folio line at the head
const section = { enabled: true, slot: { elements: [
  depth(3),
  { kind: 'rule', id: 'bar', thickness: pt(3), color: col('ink'),
    placement: { ...at('#depth', 'align-bottom', 0, -2), size: { width: 'fill' } } },
  text('flag', '{titleText}', { ...caps(13, 700, 'paper'), box: { backgroundColor: col('flag'),
    padding: { top: mm(1.4), right: mm(3.2), bottom: mm(1.2), left: mm(3.2) } } },
  at('#bar', 'above')),
] } };
const folio = (parity, side, s) => [
  text(`n-${parity}`, '{pageNumber}', { ...oswald(11, 700), align: side },
    at('page', `top-${side}`, s * MARGIN.side, 5)),
  text(`t-${parity}`, '{title} · {publishDate}', { ...typed(8), align: side },
    at(`#n-${parity}`, s > 0 ? 'right-of' : 'left-of', s * 3, 0.7)),
].map((element) => ({ ...element, parity }));
const header = { elements: [...folio('even', 'left', 1), ...folio('odd', 'right', -1)] };
// #endregion

const banner = { id: 'banner', backgroundEnabled: false, marginTop: pt(0),
  padding: { top: pt(0), right: pt(0), bottom: pt(0), left: pt(0) },
  marginBottom: pt(LEAD / 2), titleStyle: { ...caps(9, 700, 'flag'), gap: mm(1.4) },
  body: { fontFamily: 'Libre Caslon Text', fontSize: pt(12), lineHeight: pt(15),
    textAlign: 'center', firstLineIndent: pt(0) } };
const briefs = { id: 'briefs', background: col('tint'), columnGap: mm(GUTTER),
  padding: { top: mm(3), right: mm(4), bottom: mm(4), left: mm(4) },
  titleStyle: { ...caps(10, 700, 'flag'), gap: mm(2) },
  body: { fontFamily: 'Libre Caslon Text', fontSize: pt(8.4), lineHeight: pt(11),
    textAlign: 'left', firstLineIndent: pt(0) } };
const headline = (id, size, look = {}) => ({ id, fontFamily: 'Oswald', fontWeight: 600,
  fontSize: pt(size), lineHeight: pt(size * 1.05), marginBottom: pt(size * 0.3), ...look });
// A box in a column (the tram fares), and the small advertisements across the page's foot.
const info = { id: 'info', background: col('tint'), marginTop: pt(LEAD), marginBottom: pt(LEAD),
  padding: { top: mm(2.5), right: mm(3), bottom: mm(3), left: mm(3) },
  stripe: { enabled: true, side: 'top', width: pt(2.5), color: col('flag') },
  titleStyle: { ...oswald(11, 600), gap: mm(1.5) },
  body: { fontFamily: 'Libre Caslon Text', fontSize: pt(8.4), lineHeight: pt(11),
    textAlign: 'left', firstLineIndent: pt(0) } };
const ads = { ...briefs, id: 'ads', background: col('paper'),
  border: { enabled: true, width: pt(0.6), color: col('ink') },
  body: { ...typed(8), lineHeight: pt(10.6), textAlign: 'left', firstLineIndent: pt(0) } };
const label = { fontFamily: 'Courier Prime', fontSize: pt(8), firstLineIndent: pt(0) };

const config = () => ({ // a factory: the engine caches resolved configs per object
  colorPalette, resourceTypes, videoStyle,
  page: { sizePreset: 'tabloid', dpi: 150, backgroundColor: col('paper'), // 280 × 430 mm
    margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.side),
      right: mm(MARGIN.side) } },
  layout: { layoutType: 'multiple', columnCount: 4, gutterWidth: mm(GUTTER),
    columnRule: { enabled: true, color: col('rule'), lineWidth: pt(0.4) } },
  bodyText: { fontFamily: 'Libre Caslon Text', fontSize: pt(8.8), lineHeight: pt(LEAD),
    color: col('ink'), boldColor: col('ink'), textAlign: 'justify', firstLineIndent: mm(3),
    indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true,
    minWordSpacing: 0.75, maxWordSpacing: 1.6, avoidWidows: true, avoidOrphans: true,
    maxJustifyTracking: 15 }, // ‰ of an em: narrow columns justify with a little tracking
  headings: { fontFamily: 'Oswald', fontWeight: 600, marginBottom: pt(0), levels: [
    // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break).
    { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'any' },
      advancedDesign: section },
    { level: 2, fontSize: pt(20), lineHeight: pt(22), marginTop: pt(LEAD) },
    { level: 3, fontSize: pt(10.5), lineHeight: pt(LEAD), fontWeight: 700,
      marginTop: pt(LEAD / 2) },
  ] },
  headingStyles: [
    { id: 'front', advancedDesign: nameplate, header: { elements: [] } },
    { id: 'section' }, // inside pages: the flag of the level-1 design
    headline('lead', 44, { fontWeight: 700 }), headline('second', 20),
    headline('third', 12, { marginBottom: pt(2) }),
  ],
  calloutStyles: [banner, briefs, info, ads],
  tableStyle: { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
    headerBackground: col('ink'), headerColor: col('paper'), headerFontFamily: 'Oswald',
    headerFontSize: pt(7.5), bodyFontFamily: 'Courier Prime', bodyFontSize: pt(7.8),
    cellPadding: mm(1.1) },
  paragraphStyles: [{ ...label, id: 'byline', textAlign: 'left', color: col('muted') },
    { id: 'flush', firstLineIndent: pt(0) },
    { ...label, id: 'jump', textAlign: 'right', color: col('flag') },
    { id: 'imprint', ...label, fontSize: pt(6.8), lineHeight: pt(9), textAlign: 'left',
      color: col('muted'), marginTop: pt(LEAD) }],
  captionStyle: { fontFamily: 'Courier Prime', fontSize: pt(7.8), gap: mm(1.4),
    note: { fontSize: pt(6.6), color: col('muted') } },
  header, footer: { elements: [] },
  folio: { tilt: 24, paper: { type: 'newsprint', grammage: 45, shade: '#f9f7f1' },
    binding: { type: 'folded', cover: 'pages' }, surface: { type: 'oak' },
    lighting: { environment: 'daylight' } },
});
// An unnumbered type: captions print without "Figure 1".
const resourceTypes = [{ id: 'picture', name: 'Picture', shortLabel: 'Picture',
  captionPrefix: '', numberingTemplate: '', resetOn: 'never', counterFormat: 'decimal' }];

// ─── 2 · Content ────────────────────────────────────────────────────────────
const front = String.raw`---
Markdownの見本 · 147行 · content.en.mdtitle: "The Wexcombe Lantern" author: "The Wexcombe Lantern" publishDate: "Saturday 10 June 1933" --- # The Wexcombe Lantern {style="front" issue="No. 4,117" since="Established 1871" price="Twopence" weather="Fair, a fresh breeze from the west"} :::callout{type="banner" span="page" title="The Harbour Line"} ## Trams run to the sea at last {style="lead"} *Mayor Edith Rowe opens the new line before four thousand people, and drives the first car down Quay Hill herself* ::: :::paragraphs{style="byline"} By **Arthur Penhale** · Municipal Correspondent ::: :::paragraphs{style="flush"} The trams reached the harbour yesterday, eleven years after the Corporation first promised them. At a quarter past eleven the Mayor, Mrs Edith Rowe, cut a ribbon of green silk across the rails at the foot of Quay Hill, and a crowd that stretched back past the Fish Market cheered loud enough to lift the gulls off the warehouse roofs. ::: ::resource{id="mayor"} “It has taken us longer than the Romans took to build their roads,” Mrs Rowe told the crowd from a platform dressed in bunting, “and our road is only a mile and three furlongs long. But it is ours, it is paid for, and from Monday a docker in Saltings Row can be at the Town Hall in nine minutes for a penny.” The Harbour line leaves the main route at Market Cross and runs down Quay Hill, along the Strand and out to a new turning loop beside the lifeboat house. Four cars, rebuilt in the Corporation’s own works at Fennick Lane, will run every eight minutes from six in the morning until half past eleven at night. The line cost £41,300, of which £12,000 came from the government’s grant for public works. Some two hundred men of the town were taken on to lay it, most of them from the register at the Labour Exchange, and the Borough Engineer, Mr Lionel Cardew, said that not one day had been lost to accident since the first sett was lifted in October. :::paragraphs{style="jump"} A hill too steep, and the fares: **page 2** ::: ::resource{id="aviator"} ## Miss Lacey lands on the Common {style="second"} :::paragraphs{style="flush"} The aviator Miss Constance Lacey brought her biplane down on Wexcombe Common at twenty past six on Wednesday evening, at the end of a flight from Croydon of three hours and fifty minutes. She came in low over the cricket pavilion, scattering a game of the Wednesday Eleven, and rolled to a stop forty yards from the bandstand. ::: Miss Lacey, who is twenty-four, is flying round the coast of England and Wales in stages to raise money for the lifeboat fund, and has so far raised £1,108. “The Common is the best field I have met since Kent,” she said, “though the cows took some persuading.” Her machine is a two-seater of 1929 with a Gipsy engine, bought second-hand from a flying club in Hampshire and named *Kittiwake*. She navigates with a road map folded on her knee, and follows the railway lines where the coast turns misty. “The Great Western has never once let me down,” she said. “I cannot say the same for the weather.” She stayed the night at the Mermaid, where the landlord refused to take her money, and left at seven the next morning for Penzance, waving to a crowd of schoolchildren who had been let out of lessons to see her off. Two of them had sat up all night on the pavilion roof to be sure of a good view. The pavilion has asked for a signed photograph for its wall, and the Wednesday Eleven for a replay. ### The weather Fair at first, with a fresh breeze from the west; cloud later and a little rain in the evening, clearing overnight. Sea moderate, with a swell on the bar at low water. Sunrise 4.58, sunset 9.31. Yesterday’s highest temperature in the town was 68 degrees, at the Fish Market at three o’clock, and the lowest 51, at the coastguard station at dawn. The Harbour Master reports the best visibility of the year from Gannet Point, where the Stags could be seen plainly at noon and the smoke of the Scilly packet an hour before she was due. *Tomorrow:* bright, a light northerly air, good drying weather. Bathers are reminded that the red flag on West Sands means what it says. :::callout{type="briefs" span="page" placement="bottom" title="In brief"} :::columns{count=4 breaks="2,3,4"} **Lighthouse dark for a night.** The Gannet Point light will be shut down on Tuesday from dusk to midnight while its new lamp is fitted. Ships are warned by the Harbour Master. **Bathing tents return.** The striped bathing tents are back on West Sands for the summer, at threepence the hour. Mixed bathing is allowed east of the groyne only. **Choir to Cardiff.** The Wexcombe Male Voice Choir sails on Thursday for the Eisteddfod. Forty-two singers, one harmonium and the conductor’s spaniel will make the crossing. **Lost umbrella.** A lady’s umbrella with an ivory handle in the shape of a heron was left in car No. 4 on Monday. It may be claimed at the Depot, Fennick Lane. ::: ::: # Town & Harbour {style="section"} ## Trams: a hill too steep, they said {style="second"} :::paragraphs{style="byline"} Continued from page 1 ::: Many doubted that a tram could manage Quay Hill at all. The gradient is one in eleven at its worst, and a petition of 1928 warned of runaway cars and ruin in the Fish Market. Mr Cardew’s answer was a track brake of his own design, tried on the hill for six weeks this spring with cars loaded with sandbags. Mrs Rowe silenced the doubters in the simplest way. After the ribbon was cut she climbed to the front platform of car No. 9, took the controller from the motorman and drove the first car down the hill herself, at a steady walking pace and to a roar from the pavements. She is believed to be the first Mayor of Wexcombe to drive anything at all. ## The clock that stopped for nine years strikes again {style="second"} :::paragraphs{style="byline"} By **Mary Tregarthen** ::: :::paragraphs{style="flush"} For nine years the clock on the Guildhall tower has said twenty to four. Yesterday, at noon precisely, it said twelve, and the bell in the cupola struck the hour for the first time since the great gale of 1924 bent its hammer and cracked the escapement wheel. ::: ::resource{id="clock"} The work was done by Mr Samuel Opie, clockmaker, of Chapel Street, and his two sons, who have climbed the tower every evening since February. “The wheel was cast in 1799, and nobody casts them like that now,” said Mr Opie. “So we cast it like that ourselves.” The new hammer was forged at the Harbour Works from an old capstan bar. A small crowd had gathered in Guildhall Square by a quarter to twelve, many of them with their watches out. When the first stroke sounded there was a moment of silence, and then a cheer, and Mr Opie’s younger son was seen to wave his cap from the louvres of the belfry. The Town Clerk said that the old joke, that Wexcombe was the only town in England where it was always teatime, would now have to be retired. The pigeons, he added, would have to get used to the bell. ## A cod the size of a boy {style="second"} :::paragraphs{style="flush"} The largest cod landed at Wexcombe in living memory came ashore on Thursday in the hold of the *Morning Star*, skipper Jacob Hosking, 71. It weighed 82 pounds on the Fish Market scales and measured five feet and an inch from its nose to the fork of its tail. ::: ::resource{id="fisherman"} “I’ve fished these grounds since I was eleven,” Mr Hosking said, “and I never saw his like. He fought for three-quarters of an hour, and I reckon he’d have won if my grandson hadn’t been there to hold my belt.” The fish was sold to Messrs Kitto of the Strand for a guinea and is to be shown in their window, packed in ice, until Monday. A photograph has been sent to the Natural History Museum, which has asked whether the skipper might part with the head. Mr Hosking says he will think about it. ## Council votes for an open-air bathing pool {style="second"} The Town Council on Tuesday voted by eighteen votes to seven to build an open-air bathing pool at Gannet Point, in the hollow of the old quarry below the coastguard cottages. The pool will be filled by the tide through a sluice, and will measure fifty yards by twenty, with diving stages and a café. The Borough Engineer puts the cost at £9,600. Councillor Bastian, who led the opposition, said the town already had a perfectly good sea. Councillor Mrs Pascoe replied that the sea was perfectly good only in August, and that children had been drowned in it. Work is to begin in October, so that the pool may open for the summer of 1934. Seventy men will be employed. ## Lifeboat out twice in one night {style="second"} The Wexcombe lifeboat *Sarah Jolly* was launched twice on Tuesday night, the first time at a quarter to eleven to the yacht *Petrel*, whose engine had failed off Gannet Point with three men aboard, and the second at half past two to a fishing boat reported overdue from the mackerel grounds. The *Petrel* was towed into harbour shortly after midnight. The fishing boat, the *Brothers’ Pride*, was found at anchor in the lee of the Stags, her crew asleep. “They had a good catch and saw no reason to hurry,” said the coxswain, Mr William Bassett. “I told them what I thought of that, and then I gave them a tow, because the tide was turning.” The crew of twelve were back in their beds by five. It was the boat’s fourteenth service this year. ## Fire at the rope works {style="second"} A fire broke out in the tarring shed of Messrs Pengelly’s rope works on Fennick Lane shortly after four on Thursday afternoon, and for an hour threatened the whole of the long rope walk beside it. The alarm was given by a boy delivering newspapers, who saw smoke coming from under the door and ran to the fire station in Market Street, a distance of three hundred yards, in what the Chief Officer called the best time he had ever seen. The brigade had two jets on the shed within ten minutes. The roof fell in at a quarter to five, but the fire was kept from the rope walk, where some forty coils of new manila rope were stored for the harbour, and the walls of the shed are standing. Mr Pengelly puts the damage at £300, and says that work will go on as usual on Monday, in the open air if it must. The boy, Jimmy Rowse, aged twelve, has been given a reward of five shillings by the firm and a ride on the engine by the brigade. He said he preferred the ride. :::callout{type="info" title="Harbour line: what you need to know"} Cars run every eight minutes from Market Cross to the Lifeboat House, calling at Quay Hill, the Fish Market, the Custom House and Strand Corner. The fare is one penny all the way, a halfpenny for children under fourteen. Workmen’s returns at three-halfpence are sold before eight in the morning. On Sundays cars start at nine. ::: ::resource{id="tides"} ## From the Guildhall {style="third"} The Watch Committee has fixed the speed of the trams on Quay Hill at four miles an hour going down and eight coming up, and a man is to stand at the Fish Market corner with a red flag on market days until the public is used to the cars. The Finance Committee reports that the rates will not rise this year, for the first time since 1927, and the Libraries Committee that the new reading room at Saltings Row has lent 2,140 books in its first month, chiefly detective stories. A proposal to ring the restored Guildhall bell at midnight on New Year’s Eve was carried with one vote against, from the councillor who lives next door. ## Shipping {style="third"} **Arrived:** *Mary Jane*, ketch, from Swansea with coal; *Goldfinch*, steamer, from Rotterdam with timber; *Ellen*, schooner, from Fowey with china clay for the potteries; *Pride of Wexcombe*, trawler, from the Smalls with a full hold. **Sailed:** *Morning Star* and *Brothers’ Pride* for the mackerel grounds; *Lynher*, barge, for Plymouth with pit props; *Goldfinch*, in ballast, for Cardiff. **In harbour:** the training ship *Exe*, until Wednesday; visitors on board each afternoon from two, and the band of the ship plays on the quarterdeck at four. **Fish Market:** mackerel 9s. the box, pilchards 6s., hake scarce at 2s. 6d. the stone; crab plentiful and cheap.
`; // content.en.md: the front page and Town & Harbour const arts = String.raw`# Stage & Screen {style="section"}
Markdownの見本 · 86行 · content.stage.en.md ## A Lady of the Garden at the Pavilion {style="second"} :::paragraphs{style="byline"} Reviewed by **Harold Vosper** ::: :::paragraphs{style="flush"} The Pavilion Players opened their summer season on Monday with *A Lady of the Garden*, a comedy in three acts by a playwright who prefers to remain anonymous, and whose modesty the second act fully justifies. The first and third, however, are a delight, and the evening belongs to Miss Rosalind Avery. ::: ::resource{id="actress"} As Miss Hester Lane, a gardener’s daughter who inherits the great house where her father kept the roses, Miss Avery is by turns wistful, imperious and very funny. Her long speech to the assembled relations in the third act, delivered from the top of the garden steps with one arm flung out towards the vegetable beds, stopped the play for a full minute of applause on the first night. Mr Frank Polglase is a fine, glowering butler, who conveys more with one raised eyebrow than the second act manages with its whole cast, and Miss Daisy Kitto, as the cousin who expected to inherit, is spiteful in the best possible way. The painted garden by Mr Tom Rule, the company’s carpenter, is so convincing that a lady in the second row was heard to ask where she might buy the delphiniums. The second act is another matter. It is set in a solicitor’s office, it lasts fifty minutes, and it consists almost entirely of people reading a will aloud to one another. Miss Avery does her best with a scene in which she must faint on to a filing cabinet, but the audience on Monday was plainly waiting for the garden to come back, and when it did, so did the play. The Pavilion itself has been painted for the season, cream and green, and the management has kept its promise of last summer: the seats in the back six rows have been raised on a new floor, so that the shilling public can see the stage over the hats in front. The orchestra of five, under Mr Leonard Jewell, plays a selection from *Bitter Sweet* in the intervals and plays it well. *A Lady of the Garden* runs nightly at eight until the 24th, with a matinée on Wednesdays at half past two. Seats from sixpence to three shillings; the box office at the Pavilion is open from ten. The Players follow it on the 26th with a thriller, *The Lamp in the Lighthouse*, for which the management promises a real storm. ## Band concert on the Parade {style="third"} The Wexcombe Silver Band gives the first of its Sunday concerts on the Parade bandstand tomorrow at three, under its new conductor, Mr Alfred Penna, late of the Royal Marines. The programme includes a selection from *The Gondoliers*, the overture to *Poet and Peasant* and a march of Mr Penna’s own, *The Harbour Line*, written for the opening of the trams and played for the first time in public. Deck chairs twopence; the collection is for the hospital. ## Roses and sweet peas at the Drill Hall {style="third"} The Horticultural Society’s summer show filled the Drill Hall on Thursday with three hundred and forty exhibits, sixty more than last year, and with the scent of sweet peas, which took the whole of the north wall. The challenge cup for roses went to Mr George Nance of Trevenna Lane for six blooms of *Ophelia* that the judges called the best seen in the town since the war, and the cup for vegetables to Mrs Annie Pascoe, whose onions weighed two pounds each. The prize for the best-kept allotment, judged in May, went to the Saltings Row allotments for the second year. In the children’s class for a wild flower collection, every entry received a prize, by order of the President. ## Harbour paintings at the Institute {style="third"} The Wexcombe Art Society’s summer exhibition opened at the Literary Institute on Wednesday, with a hundred and twelve pictures by forty-one members, the largest show in the society’s history. The harbour, as ever, is the favourite subject: there are nineteen views of it, at every hour and in every weather, and one, by Miss Phyllis Body, painted from the top of the Guildhall tower while the clockmakers were at work. The prize for the best picture in oils went to Mr Ernest Lugg for *Pilchard Boats, Evening*, and the prize for water-colours to Mrs Jane Treloar, whose *Quay Hill in Rain* shows the new tram lines glistening, two days before they were finished. The exhibition is open daily from ten until six, until the 1st of July. Admission threepence; members free. ## On the screen this week {style="second"} ::resource{id="pictures"} At the **Regal**, the picture of the week is a musical comedy of shipboard life, with the news and a coloured cartoon. At the **Electric**, a western in the old style, and a serial whose heroine has now been tied to a railway line for three weeks running. Children’s matinée on Saturday mornings at both houses, price twopence. ## Wireless this evening {style="third"} ::resource{id="wireless"} ## Letters to the Editor {style="second"} **Sir,** — Now that we have trams to the harbour, may I ask when we are to have a seat at the top of Quay Hill? Those of us who walk up it, as I have done every morning for forty years, would be glad of somewhere to recover. — *Yours, &c.,* **J. Trevenna**, Saltings Row. **Sir,** — Your correspondent of last week complains of the noise of the gulls on the Strand. I would remind him that the gulls were here first, and that they do not, so far as I know, write to the newspapers about him. — *Yours, &c.,* **Ellen Rodda**, Fore Street. **Sir,** — I see the Guildhall clock is going again. Might the Council now turn its attention to the one at the station, which has been five minutes fast since the Coronation and has caused me to miss nothing, but has caused my wife to wait for a great many trains? — *Yours, &c.,* **H. B. Clemo**, The Parade. **Sir,** — May I, through your columns, thank the young man who carried my shopping basket from Market Cross to the top of Chapel Street on Tuesday and refused to give his name? I hope he reads the *Lantern*. — *Yours, &c.,* **Mary Tonkin**, Chapel Street. **Sir,** — I have read in your paper that the new trams are painted green and cream. Mine, I see, is painted green and cream also: the colour came through the hedge from the Corporation’s works in Fennick Lane, where they paint the cars with the windows open. Who pays for my washing? — *Yours, &c.,* **E. J. Hosken**, Fennick Lane. **Sir,** — Your report of the Harbour line says the cars will run until half past eleven at night. Will somebody tell the Regal, whose second house does not come out until twenty to twelve? — *Yours, &c.,* **A Picturegoer**, Fennick Lane. :::callout{type="ads" span="page" placement="bottom" title="Small advertisements"} :::columns{count=4 breaks="4,7,10"} **TO LET.** Furnished rooms, sea view, use of piano. Suit quiet gentleman. Apply Mrs Jose, 3 Marine Terrace. **BICYCLE** for sale, lady’s, three-speed, little used, £2 10s. or near offer. Box 112, Lantern Office. **WANTED,** a smart boy to deliver groceries, must be able to ride. Apply Hicks & Son, Market Street. **DANCING** every Saturday at the Assembly Rooms, eight till midnight, Billy Rowse and his Melodians. Admission one shilling. **PIANO TUNING** by an experienced man, late of Broadwood’s. Terms moderate. Write J. Kemp, 9 Chapel Street. **LOST** on the Common, Wednesday evening, a child’s red woollen glove. Finder please return to the Mermaid. Reward. **MOTOR COACH** to St Ives every Thursday, leaving Market Cross at 9.30, returning by 7. Fare 3s. 6d. Book at Rodda’s Garage. **FOR SALE,** pram, navy blue, good springs, 25s. Also cot. Apply after six, 21 Saltings Row. **WIRELESS SETS** repaired and recharged. Accumulators called for and delivered weekly. Tregenza, 4 Fore Street. **HOLIDAY APARTMENTS,** two bedrooms and sitting room, near sands, July vacant. Mrs Bray, Seaview, The Parade. **GENTLEMAN’S** gold watch, lost between the Rec and the Mermaid on Saturday. Sentimental value. Reward at this office. **DRESSMAKING** and alterations, ladies’ and children’s, at moderate charges. Miss Uren, 12 Lemon Street. ::: :::
`; // content.stage.en.md: Stage & Screen const sport = String.raw`# Sport {style="section"}
Markdownの見本 · 74行 · content.sport.en.md ## Rundle saves the Cup {style="second"} :::paragraphs{style="byline"} By **Walter Jago** · at Gannet Park ::: :::paragraphs{style="flush"} Wexcombe Athletic are in the final of the County Cup, and they owe it to Tom Rundle. With the score at one goal each and four minutes left on a pitch like a ploughed field, the referee gave a penalty against the home side for a handball nobody else in the ground saw. ::: ::resource{id="keeper"} Penrose of Treviskey placed the ball, took four paces back and struck it hard and low towards the right-hand post. Rundle, who is a carpenter by trade and the shortest goalkeeper in the league, went full length into the mud and held it against his chest. The crowd of 6,200, the largest at Gannet Park since the war, threw caps in the air, and one man was seen to throw his wife’s hat. From the clearance Athletic broke down the left, and young Billy Trounce, who is seventeen, ran half the length of the field before shooting past the Treviskey goalkeeper for the winner. He was carried off the pitch at the final whistle by supporters who had climbed the railings, and did not get his boots back until Sunday. The first half had belonged to Treviskey. Their centre-forward, Hendra, scored after nine minutes with a header from a corner, and only a fine save by Rundle from the same player, low down at the near post, kept the score at one goal to nil at the interval. Athletic were level ten minutes after the restart, when Opie, son of the clockmaker, turned in a cross from Trounce at the second attempt. The final, against Penhallow Rovers, will be played at Gannet Park on the 24th, kick-off at three. The club asks supporters to come early and to leave their wives’ hats at home. ## Rovers come by the long road {style="third"} Penhallow Rovers, Athletic’s opponents in the final, have needed three replays to get there. They drew with St Just twice, the second time after extra time in a thunderstorm, and beat them at the third attempt at Camborne, where the match was stopped for a quarter of an hour while a horse was led off the pitch. Their semi-final against Porthleven was settled by a goal from their captain, Arthur Kitto, the Penhallow blacksmith, who is forty-one and says this will be his last season, as he has said for the past four. Rovers have the stronger side on paper, with three men who have played for the county, and they beat Athletic twice in the league this winter. But they have never won the Cup, and they have never played at Gannet Park, where the slope from the town end is worth a goal to anyone who knows it. Athletic’s trainer, Mr Sam Rowe, has asked the groundsman not to cut the grass this week. Tickets for the stand, at a shilling, go on sale at Hicks & Son on Monday morning, and the club expects them to be gone by dinner time. ## The Harbour Mile {style="third"} The Harbour Mile, run on Whit Monday along the Strand from the Custom House to the lifeboat slip and back, was won for the third year running by Jack Penrose of the Wexcombe Harriers in 4 minutes 31 seconds, a record for the course. He was followed home by his brother Harry, eleven yards behind, and by a visiting runner from Truro, who said that he had been told the course was flat. Forty-four ran, the youngest fourteen and the oldest sixty-two, and every one of them finished, though two had to be helped up the slip. The new trams were stopped for twenty minutes while the race passed, the first time, said the Borough Engineer, that the line has given way to anybody. ## Swimming club’s new raft {style="third"} The Wexcombe Amateur Swimming Club has moored a new diving raft off West Sands, forty yards out and built by members over the winter from timber and oil drums given by the Harbour Board. It was christened on Sunday by the club’s oldest member, Mrs Grace Tonkin, who is seventy-three and swam out to it. The club’s summer gala is on the 15th of July; races for juniors, ladies and veterans, and a novelty race in fancy dress. ## Fixtures {style="third"} ::resource{id="fixtures"} ## Cricket {style="third"} ::resource{id="scores"} Wexcombe beat St Austell by 41 runs at the Rec on Saturday. Dunstan’s 64, his first fifty of the season, carried the town to 168, and the Rev. A. Carne took five wickets for 22 with his slow left-arm bowling, which St Austell found as hard to read as his sermons. ## Regatta and bowls {style="third"} The Harbour Regatta is fixed for Saturday the 1st of July, with gig racing, a swimming race across the harbour mouth and the greasy pole. Entries to the Secretary at the Harbour Office by the 24th. At bowls, Wexcombe Park beat Camborne by nine shots in the County League; Mr W. Oates, aged eighty-one, was again the best player on the green, and was presented afterwards with a new set of woods to mark his sixtieth season with the club. He said that he would keep the old ones for wet days. ## Wheelers’ fifty-mile trial {style="third"} The Wexcombe Wheelers held their fifty-mile time trial on Sunday morning over the coast road to Hayle and back, starting at six o’clock to have the road to themselves. Twenty-three riders started at one-minute intervals and twenty finished; two punctured at Gwithian and one stopped, by his own account, for breakfast. The winner, Mr Leonard Uren, a postman, beat the club record by four minutes on a machine he built himself from parts, and was back at his round by nine. ::resource{id="wheelers"} The club’s next event is the hill climb up Quay Hill on the 25th, which the Watch Committee has allowed on the condition that the riders keep clear of the tram lines, and the trams of the riders. ## Gigs on the bay {style="third"} The pilot gigs were out on the bay on Wednesday evening for the first time this summer, six boats in a practice race from the lifeboat slip round the Stags and back. The Wexcombe crew in the old *Bonnet*, built in 1838 and still the fastest boat on this coast in a flat sea, led from the start and won by four lengths from Penhallow. The crews race for the Harbour Cup at the Regatta on the 1st of July, when nine gigs are expected. ## Angling {style="third"} Sea fishing from the pier has been good all week, with mackerel taken on feathers at every tide and a bass of nine pounds by Mr Fred Curnow on Tuesday evening, the best of the season so far. The Sea Angling Club’s monthly competition on Sunday was won by Mr Curnow again, with a catch of eleven pounds. Visitors may fish from the pier on payment of twopence to the pier master. ## Racing at Penhallow {style="third"} The Whitsun meeting at Penhallow Downs drew a crowd of nine thousand on Monday, many of whom came by the special trains from Wexcombe. The big race, the Downs Plate over a mile and a half, went to *Harbour Lights*, owned by Mr Jory of Trelask and ridden by his nephew, at a hundred to eight. The favourite unseated his rider at the second hurdle and finished the race alone, to great applause, a good length ahead of the field. The bookmakers had a poor afternoon. Three favourites were beaten, and the last race, the Farmers’ Handicap, was won by a grey mare from a farm above Wexcombe that had been ploughing on Saturday and had never run on a course before. Her owner, Mr Tom Oliver, said that he had entered her for a lark and backed her with half a crown, and that he would now buy a new plough with the winnings. The meeting closes on Wednesday with the Hunters’ Steeplechase, and the Great Western will again run special trains from Wexcombe at half past eleven and a quarter past twelve, returning after the last race. The Lantern’s tipster, who has not had a winner since Easter, refuses to name a horse. :::paragraphs{style="imprint"} Printed and published by the Wexcombe Lantern Company, Ltd., at their offices, 14 Market Street, Wexcombe. Every Saturday. The town, its people and its news are invented. The pictures are films generated with diffusion models. :::
`; // content.sport.en.md: Sport const markdown = [front, arts, sport].join('\n\n'); // one document, four pages // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { 'Libre Caslon Text': ['400', '400i', '700'], Oswald: ['600', '700'], 'Courier Prime': ['400', '400i', '700'] }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); await Promise.all(resources.filter((r) => r.kind === 'video').map(({ video: { poster } }) => loadImage(poster.fileId, asset(poster.fileId)))); // the printed frames const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: 'The Wexcombe Lantern · a living newspaper' }); // #region folio: the newspaper in 3D, every clip playing on the open pages at once const stage = Object.assign(document.createElement('section'), { id: 'folio', ariaLabel: 'The newspaper in 3D' }); stage.style.cssText = 'height: min(82vh, 820px); margin: 0 auto; max-width: 1280px'; document.getElementById('pages').before(stage); createFolioFromDocument(stage, doc, { // Folio plays each clip from its video.url, muted, while its page is on show. appearance: { textureBaseUrl: 'https://postext.dev/folio/textures' }, onChange: ({ pages }) => kitStatus(`${doc.pages.length} pages · open at ` + pages.map((i) => doc.pages[i].pageLabel || i + 1).join('–')), }); // #endregion
キット · core, fonts, viewer, images:全レシピ共通 · 270行// ─── 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 v1 ── the same in every recipe · postext.dev/cookbook ──────── // Postext measures text with the faces the browser has loaded, and caches the // widths, so every face must be ready before the first build. Faces come from // Fontsource: the same static files the PDF embeds, so screen and PDF agree. /** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With * `optional`, a face Fontsource does not ship is skipped instead of failing. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { 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', }; const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin']; const jobs = []; let added = 0; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const meta = optional ? await fontsourceMeta(family) : null; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; if (hasFace(family, weight, style)) continue; if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/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().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` (a buildDocument or buildBundle call) and checks the faces * the pages use. A regular face missing from FONTS is loaded with a warning; * bold and italic variants are loaded when the family ships them. Then the * measurement caches are cleared and the build runs again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout. `base` marks a block's own face; its * bold, italic and bold-italic variants are listed whether or not used. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** True when a loaded FontFace covers exactly this family, weight and style * (document.fonts.check() is also true 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; } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), 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 v1 ── the same in every recipe · postext.dev/cookbook ─────── /** Shows the pages as facing spreads on a dark desk: the first page is a * recto on its own, then verso | recto pairs, as in a bound book. Pages * are painted when they scroll near the screen. */ function showPages(docs, { title, width = 460 } = {}) { 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, { 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 v1 ── 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 (drawn in code, or fetched) as a vector image. */ async function loadSvg(fileId, svg) { const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** 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 ───────────────────────────────────────────────────────────────────────

組み立てたscript.jsはそのまま動きます。任意のページのモジュールスクリプトに貼り付けるか、レシピをCodePenで開いてください。 GitHub上のレシピのフォルダー ↗ (新しいタブで開きます)

アレンジ

#クリップを読者に始めさせる

autoplayをなくすと、読者がクリックするまで写真は止まったままです。一度始まれば、ループはやはりそろって再生されます。

-  player: { autoplay: true, loop: true, muted: true, exclusive: false, controls: false },
+  player: { loop: true, muted: true, exclusive: false, controls: false },

#音声付きの排他的なクリップを1本

1本の動画だけを既定のままにして、単独で再生させることもできます。そのリソースのvideoにplayer: { exclusive: true, muted: false }を設定すると、それを再生し始めたとき、ループはそれが終わるまで止まります。

#6段組みのブロードシート

2段組みの第1面は静止写真で小さめの新聞を組みます。同じグリッドで、sizePreset: 'broadsheet'のブロードシート判に6段を組めます。

よくあるつまずき

つまずき

headingsオブジェクトを渡すとH1の改ページが消える

既定ではH1は奇数ページへ改ページします(always-odd)。ところがheadingsオブジェクトを渡すと中身にかかわらずこの既定がリセットされ、章は改ページせずに続けて組まれ、span: 'page'も効かなくなります。どの設定でもheadings.levels[0].breakBefore: { enabled: true, parity }を書き直してください。 奇数ページから始まる章 →

つまずき

レイアウトの前にすべてのフォントを読み込む

レイアウトはブラウザーが読み込んだフォントで文字を計測し、その幅をキャッシュします。最初のビルドのあとに届いたフォントがあると改行位置が狂い、PDFも画面と一致しなくなります。すべてのウェイトとスタイルを先に読み込み、遅れて届いたときは再ビルドの前にclearMeasurementCache()を呼んでください。 レイアウト前のフォント読み込み →

つまずき

デザインのテキストのoverflowの既定値は'ellipsis-end'

幅に収まらないデザインのテキスト要素は、既定では省略記号で終わります。複数行に折り返したいタイトルにはoverflow: 'wrap'を設定してください。 ページデザインのテキスト・罫・ボックス →

つまずき

:::columnsは囲みの中でしか効かず、分割されない

:::columnsは囲みの外では無視されます。また、囲みが分割されても、columnsグループの内部で切れることはありません。breaks属性は子ブロックを数え、入れ子の囲みは1つと数えます。 囲みの中の段組み →

新聞のセクションは新しいページを始めるレベル1見出しで、フロートはそれを越えません。セクションの最後のページに空き位置のない写真は、次のセクションの前に単独のページに組まれます。そのため原稿をページに合わせてください。ここでは、飛行士を第1面から押し出す代わりに、路面電車の記事を2ページ目へ送っています。

クレジット

レシピ
Ignacio Ferro
本文
書き下ろしの文章, CC BY 4.0
画像
  • Mrs Edith Rowe speaks at the opening of the Harbour line: a black-and-white loop of 5 s, a still generated from a text prompt and animated with the same first and last frame, encoded with ffmpeg · Generated With Diffusion Models · CC BY 4.0
  • The first frame of the mayor loop, its printed picture · Generated With Diffusion Models · CC BY 4.0
  • The Guildhall clock tower: a black-and-white loop of 5 s, a still generated from a text prompt and animated with the same first and last frame, encoded with ffmpeg · Generated With Diffusion Models · CC BY 4.0
  • The first frame of the clock loop, its printed picture · Generated With Diffusion Models · CC BY 4.0
  • Skipper Jacob Hosking with the cod: a black-and-white loop of 5 s, a still generated from a text prompt and animated with the same first and last frame, encoded with ffmpeg · Generated With Diffusion Models · CC BY 4.0
  • The first frame of the fisherman loop, its printed picture · Generated With Diffusion Models · CC BY 4.0
  • Miss Constance Lacey in the cockpit: a black-and-white loop of 5 s, a still generated from a text prompt and animated with the same first and last frame, encoded with ffmpeg · Generated With Diffusion Models · CC BY 4.0
  • The first frame of the aviator loop, its printed picture · Generated With Diffusion Models · CC BY 4.0
  • Miss Rosalind Avery on the stage: a black-and-white loop of 5 s, a still generated from a text prompt and animated with the same first and last frame, encoded with ffmpeg · Generated With Diffusion Models · CC BY 4.0
  • The first frame of the actress loop, its printed picture · Generated With Diffusion Models · CC BY 4.0
  • Tom Rundle in goal: a black-and-white loop of 5 s, a still generated from a text prompt and animated with the same first and last frame, encoded with ffmpeg · Generated With Diffusion Models · CC BY 4.0
  • The first frame of the keeper loop, its printed picture · Generated With Diffusion Models · CC BY 4.0
フォント
Libre Caslon Text (SIL OFL 1.1) · Oswald (SIL OFL 1.1) · Courier Prime (SIL OFL 1.1)
Sandbox