@dloizides/poster-render 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ - Venue pin: an accent disc with a pin glyph beside the venue lines; `RenderReport.iconBoxes`.
6
+ - Includes block drawn as a heading plus bullets, with no card.
7
+ - Price groups stay in one column when a wide body splits into two (`FlowGroup.keepWithNext`).
8
+ - `desktop-safe` landscape gets an explicit logo rect beside the title; `desktop-arch` logo enlarged and no longer pushed down to the title cap; `RenderReport.logo`.
9
+ - List rows: date at 0.7x, 48 px between dated rows.
10
+
11
+ ## 0.2.0
12
+
13
+ - Layouts `arch`, `phone-clock-safe`, `phone-arch`, `desktop-safe`, `desktop-arch` (FLIER-1 D4 "Keep all layouts"); `LAYOUT_CATALOGUE` + `defaultLayoutFor`.
14
+ - Critic r2 nits: arch prices moved up beside the arch top, lighter arch fill, phone arch top at 63% height, desktop arch 40 px right, logo top on the title cap top.
15
+ - Includes block uses one size for every line; `RenderReport.backdrop`; `Palette.arch`.
16
+
3
17
  ## 0.1.0
4
18
 
5
19
  - FlierDoc schema + validator, render, exportPng, 4 layouts, 5 formats, palettes, font sets, Kizomba example.
package/README.md CHANGED
@@ -7,6 +7,7 @@ One JSON document (`FlierDoc`) in, one poster out. Used by Flier Studio (FLIER-1
7
7
  - `render(doc, assets, canvas)`: validates first (throws `FlierDocError` before touching the canvas), resizes the canvas to the format, draws, returns a `RenderReport` (picture rect, text and card boxes).
8
8
  - `exportPng(canvas)`: PNG `Blob` of the canvas size.
9
9
  - Formats: story 1080x1920, post 1080x1350, square 1080x1080, phone 1170x2532, desktop 2560x1440.
10
- - Layouts: `picture-right`, `picture-left`, `picture-bottom`, `list`. Palettes: `sand`, `night`, `sea`, `rose`. Font sets: `classic`, `modern`, `elegant` (the app loads the web fonts).
10
+ - Layouts: `picture-right` (E-A), `picture-left` (E-B), `arch` (E-C), `picture-bottom`, `list`, `phone-clock-safe` (P-A, top kept clear for the lock-screen clock), `phone-arch` (P-B), `desktop-safe` (D-A, text clear of desktop icons and the taskbar), `desktop-arch` (D-B). `LAYOUT_CATALOGUE` lists id, design id, label, description and designed formats (for the app picker and the LLM prompt); `defaultLayoutFor(format)` gives E-A / P-A / D-A.
11
+ - Arch layouts paint a soft arch (palette `arch`, about 4% off the ground) behind the picture only, inside the picture region; prices sit large on either side. The includes block is set at one size for every line. Palettes: `sand`, `night`, `sea`, `rose`. Font sets: `classic`, `modern`, `elegant` (the app loads the web fonts).
11
12
  - The picture is placed contain + anchor in its own region: never cropped, never faded, drawn last, and no text or card is laid out in its region. Text shrinks to fit, never overflows.
12
13
  - Example: `kizombaWorkshopDocs` (3 workshops + list flier), `kizombaWorkshop` (the list flier).
package/dist/index.d.mts CHANGED
@@ -21,7 +21,12 @@ declare enum LayoutId {
21
21
  PictureRight = "picture-right",
22
22
  PictureLeft = "picture-left",
23
23
  PictureBottom = "picture-bottom",
24
- List = "list"
24
+ List = "list",
25
+ Arch = "arch",
26
+ PhoneClockSafe = "phone-clock-safe",
27
+ PhoneArch = "phone-arch",
28
+ DesktopSafe = "desktop-safe",
29
+ DesktopArch = "desktop-arch"
25
30
  }
26
31
 
27
32
  declare enum PaletteId {
@@ -135,6 +140,9 @@ interface RenderReport {
135
140
  pictureRegion: Rect;
136
141
  textBoxes: Rect[];
137
142
  cardBoxes: Rect[];
143
+ iconBoxes: Rect[];
144
+ logo: Rect | null;
145
+ backdrop: Rect | null;
138
146
  }
139
147
  interface Palette {
140
148
  background: string;
@@ -144,6 +152,7 @@ interface Palette {
144
152
  ink: string;
145
153
  soft: string;
146
154
  card: string;
155
+ arch: string;
147
156
  }
148
157
  interface FontSet {
149
158
  display: string;
@@ -200,7 +209,18 @@ interface DataUrlCanvas {
200
209
  /** Encodes the canvas as a PNG Blob of the canvas's exact pixel size. */
201
210
  declare function exportPng(canvas: DataUrlCanvas): Blob;
202
211
 
212
+ interface LayoutCatalogueEntry {
213
+ id: LayoutId;
214
+ designId: string;
215
+ label: string;
216
+ description: string;
217
+ formats: readonly FormatId[];
218
+ }
219
+ declare const LAYOUT_CATALOGUE: readonly LayoutCatalogueEntry[];
220
+ /** The default layout for a format (D4: E-A for posters, P-A for phone, D-A for desktop). */
221
+ declare function defaultLayoutFor(format: FormatId): LayoutId;
222
+
203
223
  declare const kizombaWorkshop: FlierDoc;
204
224
  declare const kizombaWorkshopDocs: FlierDoc[];
205
225
 
206
- export { BackgroundStyle, type DataUrlCanvas, FONT_SETS, FORMAT_SIZES, type FlierBlocks, type FlierDoc, FlierDocError, type FlierDocResult, type FontSet, FontSetId, FormatId, type FormatSize, type IncludesBlock, type JsonSchema, LayoutId, type ListItem, type LogoSlot, PALETTES, type Palette, PaletteId, PictureAnchor, type PictureSlot, type PosterAssets, type PosterCanvas, type PosterContext, type PosterImage, type PriceRow, type Rect, type RenderReport, type ScheduleRow, type TextMetricsLike, exportPng, flierDocSchema, kizombaWorkshop, kizombaWorkshopDocs, render, validateFlierDoc };
226
+ export { BackgroundStyle, type DataUrlCanvas, FONT_SETS, FORMAT_SIZES, type FlierBlocks, type FlierDoc, FlierDocError, type FlierDocResult, type FontSet, FontSetId, FormatId, type FormatSize, type IncludesBlock, type JsonSchema, LAYOUT_CATALOGUE, type LayoutCatalogueEntry, LayoutId, type ListItem, type LogoSlot, PALETTES, type Palette, PaletteId, PictureAnchor, type PictureSlot, type PosterAssets, type PosterCanvas, type PosterContext, type PosterImage, type PriceRow, type Rect, type RenderReport, type ScheduleRow, type TextMetricsLike, defaultLayoutFor, exportPng, flierDocSchema, kizombaWorkshop, kizombaWorkshopDocs, render, validateFlierDoc };
package/dist/index.d.ts CHANGED
@@ -21,7 +21,12 @@ declare enum LayoutId {
21
21
  PictureRight = "picture-right",
22
22
  PictureLeft = "picture-left",
23
23
  PictureBottom = "picture-bottom",
24
- List = "list"
24
+ List = "list",
25
+ Arch = "arch",
26
+ PhoneClockSafe = "phone-clock-safe",
27
+ PhoneArch = "phone-arch",
28
+ DesktopSafe = "desktop-safe",
29
+ DesktopArch = "desktop-arch"
25
30
  }
26
31
 
27
32
  declare enum PaletteId {
@@ -135,6 +140,9 @@ interface RenderReport {
135
140
  pictureRegion: Rect;
136
141
  textBoxes: Rect[];
137
142
  cardBoxes: Rect[];
143
+ iconBoxes: Rect[];
144
+ logo: Rect | null;
145
+ backdrop: Rect | null;
138
146
  }
139
147
  interface Palette {
140
148
  background: string;
@@ -144,6 +152,7 @@ interface Palette {
144
152
  ink: string;
145
153
  soft: string;
146
154
  card: string;
155
+ arch: string;
147
156
  }
148
157
  interface FontSet {
149
158
  display: string;
@@ -200,7 +209,18 @@ interface DataUrlCanvas {
200
209
  /** Encodes the canvas as a PNG Blob of the canvas's exact pixel size. */
201
210
  declare function exportPng(canvas: DataUrlCanvas): Blob;
202
211
 
212
+ interface LayoutCatalogueEntry {
213
+ id: LayoutId;
214
+ designId: string;
215
+ label: string;
216
+ description: string;
217
+ formats: readonly FormatId[];
218
+ }
219
+ declare const LAYOUT_CATALOGUE: readonly LayoutCatalogueEntry[];
220
+ /** The default layout for a format (D4: E-A for posters, P-A for phone, D-A for desktop). */
221
+ declare function defaultLayoutFor(format: FormatId): LayoutId;
222
+
203
223
  declare const kizombaWorkshop: FlierDoc;
204
224
  declare const kizombaWorkshopDocs: FlierDoc[];
205
225
 
206
- export { BackgroundStyle, type DataUrlCanvas, FONT_SETS, FORMAT_SIZES, type FlierBlocks, type FlierDoc, FlierDocError, type FlierDocResult, type FontSet, FontSetId, FormatId, type FormatSize, type IncludesBlock, type JsonSchema, LayoutId, type ListItem, type LogoSlot, PALETTES, type Palette, PaletteId, PictureAnchor, type PictureSlot, type PosterAssets, type PosterCanvas, type PosterContext, type PosterImage, type PriceRow, type Rect, type RenderReport, type ScheduleRow, type TextMetricsLike, exportPng, flierDocSchema, kizombaWorkshop, kizombaWorkshopDocs, render, validateFlierDoc };
226
+ export { BackgroundStyle, type DataUrlCanvas, FONT_SETS, FORMAT_SIZES, type FlierBlocks, type FlierDoc, FlierDocError, type FlierDocResult, type FontSet, FontSetId, FormatId, type FormatSize, type IncludesBlock, type JsonSchema, LAYOUT_CATALOGUE, type LayoutCatalogueEntry, LayoutId, type ListItem, type LogoSlot, PALETTES, type Palette, PaletteId, PictureAnchor, type PictureSlot, type PosterAssets, type PosterCanvas, type PosterContext, type PosterImage, type PriceRow, type Rect, type RenderReport, type ScheduleRow, type TextMetricsLike, defaultLayoutFor, exportPng, flierDocSchema, kizombaWorkshop, kizombaWorkshopDocs, render, validateFlierDoc };
package/dist/index.js CHANGED
@@ -31,6 +31,11 @@ var LayoutId = /* @__PURE__ */ ((LayoutId2) => {
31
31
  LayoutId2["PictureLeft"] = "picture-left";
32
32
  LayoutId2["PictureBottom"] = "picture-bottom";
33
33
  LayoutId2["List"] = "list";
34
+ LayoutId2["Arch"] = "arch";
35
+ LayoutId2["PhoneClockSafe"] = "phone-clock-safe";
36
+ LayoutId2["PhoneArch"] = "phone-arch";
37
+ LayoutId2["DesktopSafe"] = "desktop-safe";
38
+ LayoutId2["DesktopArch"] = "desktop-arch";
34
39
  return LayoutId2;
35
40
  })(LayoutId || {});
36
41
 
@@ -62,10 +67,10 @@ var FORMAT_SIZES = {
62
67
 
63
68
  // src/palettes.ts
64
69
  var PALETTES = {
65
- ["sand" /* Sand */]: { background: "#f5f4f2", tint: "#ece8e3", title: "#958a80", accent: "#e9b47f", ink: "#5f564f", soft: "#b3a89e", card: "#ffffff" },
66
- ["night" /* Night */]: { background: "#1c1a17", tint: "#25221e", title: "#e8dccb", accent: "#e9b47f", ink: "#f4efe8", soft: "#9c9284", card: "#2c2824" },
67
- ["sea" /* Sea */]: { background: "#eef4f3", tint: "#e1ecea", title: "#4f7d7a", accent: "#e07a5f", ink: "#2f3e46", soft: "#8aa3a1", card: "#ffffff" },
68
- ["rose" /* Rose */]: { background: "#f8efec", tint: "#f1e2dd", title: "#9b6b63", accent: "#c9846f", ink: "#4a3934", soft: "#b39a92", card: "#ffffff" }
70
+ ["sand" /* Sand */]: { background: "#f5f4f2", tint: "#ece8e3", title: "#958a80", accent: "#e9b47f", ink: "#5f564f", soft: "#b3a89e", card: "#ffffff", arch: "#ede8e2" },
71
+ ["night" /* Night */]: { background: "#1c1a17", tint: "#25221e", title: "#e8dccb", accent: "#e9b47f", ink: "#f4efe8", soft: "#9c9284", card: "#2c2824", arch: "#25221f" },
72
+ ["sea" /* Sea */]: { background: "#eef4f3", tint: "#e1ecea", title: "#4f7d7a", accent: "#e07a5f", ink: "#2f3e46", soft: "#8aa3a1", card: "#ffffff", arch: "#e4ecea" },
73
+ ["rose" /* Rose */]: { background: "#f8efec", tint: "#f1e2dd", title: "#9b6b63", accent: "#c9846f", ink: "#4a3934", soft: "#b39a92", card: "#ffffff", arch: "#f1e5e1" }
69
74
  };
70
75
 
71
76
  // src/fontSets.ts
@@ -309,18 +314,19 @@ function headerGroups(doc, p, u) {
309
314
  return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
310
315
  }
311
316
  function itemGroup(item, l, u) {
312
- const lines = [l.script?.(item.date, 96)];
317
+ const lines = [l.script?.(item.date, 67)];
313
318
  item.schedule.forEach((row) => lines.push(l.strong?.(`${row.label} ${row.time}`, 42)));
314
319
  if (item.price) {
315
320
  lines.push(l.accent?.(item.price, 42));
316
321
  }
317
- return { lines, gapAfter: 26 * u };
322
+ return { lines, gapAfter: 48 * u };
318
323
  }
319
- function eventGroups(doc, l, u) {
324
+ function eventGroups(doc, l, u, withPrices) {
320
325
  const b = doc.blocks;
321
326
  const groups = [];
322
327
  b.schedule?.forEach((row) => groups.push({ lines: [l.strong?.(row.label, 66), l.accent?.(row.time, 60)], gapAfter: 30 * u }));
323
- b.prices?.forEach((row) => groups.push({ lines: [l.strong?.(row.amount, 66), l.accent?.(row.label, 52)], gapAfter: 30 * u }));
328
+ const prices = (withPrices ? b.prices : void 0) ?? [];
329
+ prices.forEach((row, i) => groups.push({ lines: [l.strong?.(row.amount, 66), l.accent?.(row.label, 52)], gapAfter: 30 * u, keepWithNext: i < prices.length - 1 }));
324
330
  return groups;
325
331
  }
326
332
  function listGroups(doc, l, u) {
@@ -331,18 +337,22 @@ function listGroups(doc, l, u) {
331
337
  }
332
338
  return groups;
333
339
  }
334
- function bodyGroups(doc, p, u) {
340
+ function flankPriceGroups(doc, p, u) {
341
+ const l = makers(p, u);
342
+ return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong?.(row.amount, 110), l.accent?.(row.label, 48)], gapAfter: 30 * u }));
343
+ }
344
+ function bodyGroups(doc, p, u, withPrices = true) {
335
345
  const l = makers(p, u);
336
346
  const b = doc.blocks;
337
347
  const groups = [];
338
348
  const isList = doc.layout === "list" /* List */;
339
349
  if (b.venue) {
340
- groups.push({ lines: b.venue.map((v) => l.venue?.(v, isList ? 48 : 58)), gapAfter: 40 * u, rule: { color: p.accent, height: 8 * u } });
350
+ groups.push({ lines: b.venue.map((v) => l.venue?.(v, isList ? 48 : 58)), gapAfter: 40 * u, rule: { color: p.accent, height: 8 * u }, icon: { color: p.accent, glyph: p.background } });
341
351
  }
342
- groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u));
352
+ groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
343
353
  if (b.includes) {
344
- const lines = [l.accent?.(b.includes.heading, 36), ...b.includes.items.map((i) => l.item?.(i, 40))];
345
- groups.push({ lines, gapAfter: 24 * u, card: { color: p.card, pad: 28 * u } });
354
+ const lines = [l.accent?.(b.includes.heading, 38), ...b.includes.items.map((i) => l.item?.(i, 38))];
355
+ groups.push({ lines, gapAfter: 24 * u, uniform: true });
346
356
  }
347
357
  if (b.footer) {
348
358
  groups.push({ lines: [l.soft?.(b.footer, 34)], gapAfter: 0 });
@@ -350,6 +360,51 @@ function bodyGroups(doc, p, u) {
350
360
  return groups;
351
361
  }
352
362
 
363
+ // src/layout/layoutSpecs.ts
364
+ var flanksBeside = (pic, top, bottom2, m) => [
365
+ [m, top, pic[0] - 2 * m, bottom2 - top],
366
+ [pic[0] + pic[2] + m, top, 1 - pic[0] - pic[2] - 2 * m, bottom2 - top]
367
+ ];
368
+ var ARCH_PIC = [0.22, 0.42, 0.56, 0.58];
369
+ var PHONE_ARCH_PIC = [0.2, 0.6, 0.6, 0.4];
370
+ var DESK_ARCH_PIC = [0.355, 0.07, 0.32, 0.93];
371
+ var ARCH_LANDSCAPE = {
372
+ header: [0.04, 0.06, 0.3, 0.5],
373
+ body: [0.04, 0.58, 0.3, 0.36],
374
+ picture: [0.36, 0.08, 0.3, 0.92],
375
+ alignX: 0.5,
376
+ flanks: [[0.68, 0.08, 0.28, 0.42], [0.68, 0.52, 0.28, 0.42]],
377
+ arch: 0.06
378
+ };
379
+ var LAYOUT_SPECS = {
380
+ ["arch" /* Arch */]: {
381
+ portrait: { header: [0.045, 0.025, 0.91, 0.18], body: [0.045, 0.215, 0.91, 0.19], picture: ARCH_PIC, alignX: 0.5, flanks: flanksBeside(ARCH_PIC, 0.44, 0.97, 0.03), arch: 0.04 },
382
+ landscape: ARCH_LANDSCAPE
383
+ },
384
+ ["phone-clock-safe" /* PhoneClockSafe */]: {
385
+ portrait: { header: [0.045, 0.28, 0.91, 0.2], body: [0.045, 0.5, 0.42, 0.44], picture: [0.48, 0.5, 0.52, 0.5], alignX: 1 },
386
+ landscape: { header: [0.04, 0.2, 0.52, 0.3], body: [0.04, 0.52, 0.52, 0.42], picture: [0.6, 0.12, 0.4, 0.88], alignX: 1 }
387
+ },
388
+ ["phone-arch" /* PhoneArch */]: {
389
+ portrait: { header: [0.045, 0.3, 0.91, 0.13], body: [0.045, 0.44, 0.91, 0.14], picture: PHONE_ARCH_PIC, alignX: 0.5, flanks: flanksBeside(PHONE_ARCH_PIC, 0.63, 0.94, 0.03), arch: 0.08 },
390
+ landscape: { ...ARCH_LANDSCAPE, header: [0.04, 0.16, 0.3, 0.42], picture: [0.36, 0.16, 0.3, 0.84] }
391
+ },
392
+ ["desktop-safe" /* DesktopSafe */]: {
393
+ portrait: { header: [0.08, 0.06, 0.84, 0.24], body: [0.08, 0.31, 0.46, 0.58], picture: [0.56, 0.36, 0.44, 0.64], alignX: 1 },
394
+ landscape: { header: [0.0625, 0.05, 0.58, 0.44], body: [0.0625, 0.5, 0.58, 0.38], picture: [0.67, 0.0972, 0.29, 0.9028], alignX: 1, logo: [0.4, 0.045, 0.09, 0.17] }
395
+ },
396
+ ["desktop-arch" /* DesktopArch */]: {
397
+ portrait: { header: [0.08, 0.06, 0.84, 0.2], body: [0.08, 0.27, 0.84, 0.2], picture: [0.14, 0.48, 0.72, 0.52], alignX: 0.5, arch: 0.05 },
398
+ landscape: { header: [0.07, 0.3, 0.26, 0.45], body: [0.7, 0.25, 0.27, 0.65], picture: DESK_ARCH_PIC, alignX: 0.5, logo: [0.86, 0.03, 0.11, 0.18], arch: 0.05 }
399
+ }
400
+ };
401
+ var r = Math.round;
402
+ var toRect = (f, w, h) => {
403
+ const x = r(f[0] * w);
404
+ const y = r(f[1] * h);
405
+ return { x, y, w: r((f[0] + f[2]) * w) - x, h: r((f[1] + f[3]) * h) - y };
406
+ };
407
+
353
408
  // src/layout/geometry.ts
354
409
  var MARGIN = 0.045;
355
410
  var HEADER_PORTRAIT = 0.3;
@@ -362,12 +417,12 @@ var PICTURE_SIDE = {
362
417
  ["list" /* List */]: [0.5, 0.36]
363
418
  };
364
419
  var ANCHOR_Y = { ["top" /* Top */]: 0, ["center" /* Center */]: 0.5, ["bottom" /* Bottom */]: 1 };
365
- var r = Math.round;
420
+ var r2 = Math.round;
366
421
  function side(w, h, fraction) {
367
- const m = r(Math.min(w, h) * MARGIN);
422
+ const m = r2(Math.min(w, h) * MARGIN);
368
423
  if (h >= w) {
369
- const split2 = r(w * (1 - fraction[0]));
370
- const top2 = r(h * HEADER_PORTRAIT);
424
+ const split2 = r2(w * (1 - fraction[0]));
425
+ const top2 = r2(h * HEADER_PORTRAIT);
371
426
  return {
372
427
  header: { x: m, y: m, w: w - 2 * m, h: top2 - 2 * m },
373
428
  picture: { x: split2, y: top2, w: w - split2, h: h - top2 },
@@ -375,19 +430,19 @@ function side(w, h, fraction) {
375
430
  alignX: 1
376
431
  };
377
432
  }
378
- const split = r(w * (1 - fraction[1]));
379
- const top = r(h * HEADER_LANDSCAPE);
433
+ const split = r2(w * (1 - fraction[1]));
434
+ const top = r2(h * HEADER_LANDSCAPE);
380
435
  return {
381
436
  header: { x: m, y: m, w: split - 2 * m, h: top - m },
382
437
  picture: { x: split, y: 0, w: w - split, h },
383
- body: { x: m, y: top + r(m / 2), w: split - 2 * m, h: h - top - r(m / 2) - m },
438
+ body: { x: m, y: top + r2(m / 2), w: split - 2 * m, h: h - top - r2(m / 2) - m },
384
439
  alignX: 1
385
440
  };
386
441
  }
387
442
  function bottom(w, h) {
388
- const m = r(Math.min(w, h) * MARGIN);
389
- const top = r(h * BOTTOM_HEADER);
390
- const pic = r(h * BOTTOM_PICTURE);
443
+ const m = r2(Math.min(w, h) * MARGIN);
444
+ const top = r2(h * BOTTOM_HEADER);
445
+ const pic = r2(h * BOTTOM_PICTURE);
391
446
  return {
392
447
  header: { x: m, y: m, w: w - 2 * m, h: top - 2 * m },
393
448
  body: { x: m, y: top, w: w - 2 * m, h: pic - top - m },
@@ -395,8 +450,23 @@ function bottom(w, h) {
395
450
  alignX: 0.5
396
451
  };
397
452
  }
453
+ function fromSpec(spec, w, h) {
454
+ return {
455
+ header: toRect(spec.header, w, h),
456
+ body: toRect(spec.body, w, h),
457
+ picture: toRect(spec.picture, w, h),
458
+ alignX: spec.alignX,
459
+ flanks: spec.flanks?.map((f) => toRect(f, w, h)),
460
+ logo: spec.logo ? toRect(spec.logo, w, h) : void 0,
461
+ arch: spec.arch
462
+ };
463
+ }
398
464
  var mirror = (rect, w) => ({ ...rect, x: w - rect.x - rect.w });
399
465
  function geometry(layout, w, h) {
466
+ const oriented = LAYOUT_SPECS[layout];
467
+ if (oriented) {
468
+ return fromSpec(h >= w ? oriented.portrait : oriented.landscape, w, h);
469
+ }
400
470
  if (layout === "picture-bottom" /* PictureBottom */) {
401
471
  return bottom(w, h);
402
472
  }
@@ -427,12 +497,32 @@ var RULE_WIDTH = 0.9;
427
497
  var WIDE = 1.6;
428
498
  var MIN_K = 0.02;
429
499
  var STEPS = 22;
500
+ var ICON_SCALE = 1.6;
501
+ var ICON_GAP = 0.35;
430
502
  var fontOf = (line, size, fonts) => `${line.weight} ${size}px ${fonts[line.role]}`;
431
- var emptyFlow = () => ({ texts: [], cards: [], rules: [], bullets: [] });
503
+ var emptyFlow = () => ({ texts: [], cards: [], rules: [], bullets: [], icons: [] });
504
+ var iconSize = (group, k) => {
505
+ const first = group.lines[0];
506
+ return group.icon && first ? first.size * k * LINE_HEIGHT * ICON_SCALE : 0;
507
+ };
432
508
  function lineSize(line, width, k, avail) {
433
509
  const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
434
510
  return Math.max(0, Math.min(line.size * k, line.size * (avail - indent) / Math.max(width, 1)));
435
511
  }
512
+ function groupRatio(m, k, avail) {
513
+ if (!m.group.uniform) {
514
+ return null;
515
+ }
516
+ return Math.min(...m.group.lines.map((line, i) => lineSize(line, m.widths[i] ?? 1, k, avail) / line.size));
517
+ }
518
+ function placeIcon(group, d, x, textTop, textBottom, out) {
519
+ if (!group.icon || d <= 0) {
520
+ return textBottom;
521
+ }
522
+ const textH = textBottom - textTop;
523
+ out?.icons.push({ rect: { x, y: textTop + Math.max(0, (textH - d) / 2), w: d, h: d }, color: group.icon.color, glyph: group.icon.glyph });
524
+ return textTop + Math.max(textH, d);
525
+ }
436
526
  function placeGroup(m, k, col, y0, out) {
437
527
  const { group } = m;
438
528
  let y = y0;
@@ -443,17 +533,22 @@ function placeGroup(m, k, col, y0, out) {
443
533
  const pad = group.card ? group.card.pad * k : 0;
444
534
  const top = y;
445
535
  y += pad;
536
+ const d = iconSize(group, k);
537
+ const iconIndent = d > 0 ? d * (1 + ICON_GAP) : 0;
538
+ const avail = col.w - 2 * pad - iconIndent;
539
+ const ratio = groupRatio(m, k, avail);
446
540
  group.lines.forEach((line, i) => {
447
- const size = lineSize(line, m.widths[i] ?? 1, k, col.w - 2 * pad);
448
- const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
449
- const x = col.x + pad + indent;
541
+ const size = ratio === null ? lineSize(line, m.widths[i] ?? 1, k, avail) : line.size * ratio;
542
+ const indent = (line.bullet ? line.size * k * BULLET_INDENT : 0) + iconIndent;
450
543
  const w = (m.widths[i] ?? 0) * size / line.size;
544
+ const x = col.right ? col.x + col.w - pad - w : col.x + pad + indent;
451
545
  out?.texts.push({ text: line.text, font: "", color: line.color, box: { x, y, w, h: size * LINE_HEIGHT } });
452
546
  if (line.bullet && out) {
453
547
  out.bullets.push({ x: col.x + pad + size * BULLET_RADIUS, y: y + size * LINE_HEIGHT / 2, r: size * BULLET_RADIUS, color: line.bullet });
454
548
  }
455
549
  y += size * LINE_HEIGHT;
456
550
  });
551
+ y = placeIcon(group, d, col.x + pad, top + pad, y, out);
457
552
  y += pad;
458
553
  if (group.card) {
459
554
  out?.cards.unshift({ rect: { x: col.x, y: top, w: col.w, h: y - top }, color: group.card.color });
@@ -467,6 +562,27 @@ function placeColumn(groups, k, col, out) {
467
562
  }
468
563
  return h;
469
564
  }
565
+ var tallest = (heights, cut) => {
566
+ const left = heights.slice(0, cut).reduce((a, b) => a + b, 0);
567
+ const right = heights.slice(cut).reduce((a, b) => a + b, 0);
568
+ return Math.max(left, right);
569
+ };
570
+ function keepTogether(groups, heights, cut) {
571
+ const breaks = (c) => groups[c - 1]?.group.keepWithNext === true;
572
+ let back = cut;
573
+ while (back > 1 && breaks(back)) {
574
+ back--;
575
+ }
576
+ let fwd = cut;
577
+ while (fwd < groups.length - 1 && breaks(fwd)) {
578
+ fwd++;
579
+ }
580
+ const candidates = [back, fwd].filter((c) => !breaks(c));
581
+ if (candidates.length === 0) {
582
+ return cut;
583
+ }
584
+ return candidates.reduce((best, c) => tallest(heights, c) < tallest(heights, best) ? c : best);
585
+ }
470
586
  function splitColumns(groups, zone) {
471
587
  if (zone.w / Math.max(zone.h, 1) <= WIDE || groups.length < 2) {
472
588
  return [{ col: zone, groups }];
@@ -481,7 +597,7 @@ function splitColumns(groups, zone) {
481
597
  }
482
598
  acc += h;
483
599
  });
484
- cut = Math.min(cut, groups.length - 1);
600
+ cut = keepTogether(groups, heights, Math.min(cut, groups.length - 1));
485
601
  const gap = Math.round(zone.w * 0.04);
486
602
  const w = (zone.w - gap) / 2;
487
603
  return [
@@ -526,21 +642,48 @@ function flow(ctx, groups, zone, fonts) {
526
642
  var BASE = 1080;
527
643
  var LOGO_SHARE = 0.26;
528
644
  var CARD_RADIUS = 0.035;
529
- function roundRect(ctx, r2, radius) {
530
- const rad = Math.min(radius, r2.w / 2, r2.h / 2);
645
+ var CAP_TOP = 0.15;
646
+ var PIN_HEAD_Y = 0.08;
647
+ var PIN_HEAD_R = 0.19;
648
+ var PIN_TIP_Y = 0.28;
649
+ var PIN_HOLE = 0.42;
650
+ var ARC_START = 0.75;
651
+ var ARC_END = 2.25;
652
+ function drawPin(ctx, r3, color, glyph) {
653
+ const cx = r3.x + r3.w / 2;
654
+ const cy = r3.y + r3.h / 2;
655
+ const hy = cy - r3.w * PIN_HEAD_Y;
656
+ const rad = r3.w * PIN_HEAD_R;
657
+ ctx.fillStyle = color;
658
+ ctx.beginPath();
659
+ ctx.arc(cx, cy, r3.w / 2, 0, Math.PI * 2);
660
+ ctx.fill();
661
+ ctx.fillStyle = glyph;
662
+ ctx.beginPath();
663
+ ctx.moveTo(cx, cy + r3.w * PIN_TIP_Y);
664
+ ctx.arc(cx, hy, rad, Math.PI * ARC_START, Math.PI * ARC_END);
665
+ ctx.closePath();
666
+ ctx.fill();
667
+ ctx.fillStyle = color;
668
+ ctx.beginPath();
669
+ ctx.arc(cx, hy, rad * PIN_HOLE, 0, Math.PI * 2);
670
+ ctx.fill();
671
+ }
672
+ function roundRect(ctx, r3, radius) {
673
+ const rad = Math.min(radius, r3.w / 2, r3.h / 2);
531
674
  ctx.beginPath();
532
- ctx.moveTo(r2.x + rad, r2.y);
533
- ctx.arcTo(r2.x + r2.w, r2.y, r2.x + r2.w, r2.y + r2.h, rad);
534
- ctx.arcTo(r2.x + r2.w, r2.y + r2.h, r2.x, r2.y + r2.h, rad);
535
- ctx.arcTo(r2.x, r2.y + r2.h, r2.x, r2.y, rad);
536
- ctx.arcTo(r2.x, r2.y, r2.x + r2.w, r2.y, rad);
675
+ ctx.moveTo(r3.x + rad, r3.y);
676
+ ctx.arcTo(r3.x + r3.w, r3.y, r3.x + r3.w, r3.y + r3.h, rad);
677
+ ctx.arcTo(r3.x + r3.w, r3.y + r3.h, r3.x, r3.y + r3.h, rad);
678
+ ctx.arcTo(r3.x, r3.y + r3.h, r3.x, r3.y, rad);
679
+ ctx.arcTo(r3.x, r3.y, r3.x + r3.w, r3.y, rad);
537
680
  ctx.closePath();
538
681
  ctx.fill();
539
682
  }
540
683
  function drawFlow(ctx, f, radius) {
541
- f.rules.forEach((r2) => {
542
- ctx.fillStyle = r2.color;
543
- ctx.fillRect(r2.rect.x, r2.rect.y, r2.rect.w, r2.rect.h);
684
+ f.rules.forEach((r3) => {
685
+ ctx.fillStyle = r3.color;
686
+ ctx.fillRect(r3.rect.x, r3.rect.y, r3.rect.w, r3.rect.h);
544
687
  });
545
688
  f.cards.forEach((c) => {
546
689
  ctx.fillStyle = c.color;
@@ -552,6 +695,7 @@ function drawFlow(ctx, f, radius) {
552
695
  ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
553
696
  ctx.fill();
554
697
  });
698
+ f.icons.forEach((i) => drawPin(ctx, i.rect, i.color, i.glyph));
555
699
  ctx.textAlign = "left";
556
700
  ctx.textBaseline = "top";
557
701
  f.texts.forEach((t) => {
@@ -571,6 +715,30 @@ function checked(input, assets) {
571
715
  }
572
716
  return result.doc;
573
717
  }
718
+ function archRect(geo) {
719
+ if (geo.arch === void 0) {
720
+ return null;
721
+ }
722
+ const top = geo.picture.y + Math.round(geo.picture.h * geo.arch);
723
+ return { x: geo.picture.x, y: top, w: geo.picture.w, h: geo.picture.y + geo.picture.h - top };
724
+ }
725
+ function drawArch(ctx, r3, color) {
726
+ const rad = r3.w / 2;
727
+ ctx.fillStyle = color;
728
+ ctx.fillRect(r3.x, r3.y + rad, r3.w, r3.h - rad);
729
+ ctx.beginPath();
730
+ ctx.arc(r3.x + rad, r3.y + rad, rad, Math.PI, Math.PI * 2);
731
+ ctx.fill();
732
+ }
733
+ function flankFlows(ctx, groups, flanks, fonts) {
734
+ const half = Math.ceil(groups.length / 2);
735
+ return flanks.map((zone, i) => i === 0 ? flow(ctx, groups.slice(0, half), zone, fonts) : flow(ctx, groups.slice(half), { ...zone, right: true }, fonts));
736
+ }
737
+ function capAligned(zone, head) {
738
+ const first = head.texts[0]?.box;
739
+ const shift = first ? Math.min(Math.max(0, Math.round(first.y + first.h * CAP_TOP) - zone.y), Math.round(zone.h / 2)) : 0;
740
+ return { ...zone, y: zone.y + shift, h: zone.h - shift };
741
+ }
574
742
  function splitHeader(header, hasLogo) {
575
743
  const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
576
744
  return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
@@ -593,20 +761,25 @@ function render(input, assets, canvas) {
593
761
  ctx.fillRect(0, 0, width, height);
594
762
  if (doc.background === "tint" /* Tint */) {
595
763
  ctx.fillStyle = palette.tint;
596
- [geo.header, geo.body].forEach((r2) => ctx.fillRect(r2.x, r2.y, r2.w, r2.h));
764
+ [geo.header, geo.body].forEach((r3) => ctx.fillRect(r3.x, r3.y, r3.w, r3.h));
597
765
  }
598
- const zones = splitHeader(geo.header, doc.logo !== void 0);
766
+ const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
599
767
  const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
600
- const body = flow(ctx, bodyGroups(doc, palette, u), geo.body, fonts);
768
+ const flanks = geo.flanks ?? [];
769
+ const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
770
+ const sides = flanks.length > 0 ? flankFlows(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
601
771
  const radius = Math.min(width, height) * CARD_RADIUS;
602
- drawFlow(ctx, head, radius);
603
- drawFlow(ctx, body, radius);
772
+ [head, body, ...sides].forEach((f) => drawFlow(ctx, f, radius));
604
773
  const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
605
- if (logoImage) {
606
- const r2 = placeContain(logoImage, zones.logo, "top" /* Top */, 1, 1);
607
- ctx.drawImage(logoImage, r2.x, r2.y, r2.w, r2.h);
774
+ const logo = logoImage ? placeContain(logoImage, geo.logo ? zones.logo : capAligned(zones.logo, head), "top" /* Top */, 1, 1) : null;
775
+ if (logoImage && logo) {
776
+ ctx.drawImage(logoImage, logo.x, logo.y, logo.w, logo.h);
608
777
  }
609
778
  const image = doc.picture ? assets[doc.picture.imageId] : void 0;
779
+ const backdrop = image ? archRect(geo) : null;
780
+ if (backdrop) {
781
+ drawArch(ctx, backdrop, palette.arch);
782
+ }
610
783
  const picture = image && doc.picture ? placeContain(image, geo.picture, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
611
784
  if (image && picture) {
612
785
  ctx.drawImage(image, picture.x, picture.y, picture.w, picture.h);
@@ -616,8 +789,11 @@ function render(input, assets, canvas) {
616
789
  height,
617
790
  picture,
618
791
  pictureRegion: geo.picture,
619
- textBoxes: [...head.texts, ...body.texts].map((t) => t.box),
620
- cardBoxes: body.cards.map((c) => c.rect)
792
+ textBoxes: [head, body, ...sides].flatMap((f) => f.texts.map((t) => t.box)),
793
+ cardBoxes: body.cards.map((c) => c.rect),
794
+ iconBoxes: [head, body, ...sides].flatMap((f) => f.icons.map((i) => i.rect)),
795
+ logo,
796
+ backdrop
621
797
  };
622
798
  }
623
799
 
@@ -636,6 +812,30 @@ function exportPng(canvas) {
636
812
  return new Blob([bytes], { type: "image/png" });
637
813
  }
638
814
 
815
+ // src/layoutCatalogue.ts
816
+ var POSTERS = ["story" /* Story */, "post" /* Post */, "square" /* Square */];
817
+ var LAYOUT_CATALOGUE = [
818
+ { id: "picture-right" /* PictureRight */, designId: "E-A", label: "Picture right", description: "Titles across the top, details on the left, the picture on the right standing on the bottom edge.", formats: POSTERS },
819
+ { id: "picture-left" /* PictureLeft */, designId: "E-B", label: "Picture left", description: "Titles across the top, the picture on the left standing on the bottom edge, details on the right.", formats: POSTERS },
820
+ { id: "arch" /* Arch */, designId: "E-C", label: "Arch", description: "Titles and details on top, the picture centred in a soft arch at the bottom, prices either side.", formats: POSTERS },
821
+ { id: "picture-bottom" /* PictureBottom */, designId: "picture-bottom", label: "Picture bottom", description: "Titles and details on top, the picture across the bottom half.", formats: POSTERS },
822
+ { id: "list" /* List */, designId: "list", label: "Upcoming list", description: "Several dated events as a list beside the picture.", formats: POSTERS },
823
+ { id: "phone-clock-safe" /* PhoneClockSafe */, designId: "P-A", label: "Phone, clock clear", description: "Lock-screen wallpaper: top kept clear for the clock, details on the left, picture on the right.", formats: ["phone" /* Phone */] },
824
+ { id: "phone-arch" /* PhoneArch */, designId: "P-B", label: "Phone arch", description: "Lock-screen wallpaper: top kept clear for the clock, details in the middle, the picture in an arch at the bottom.", formats: ["phone" /* Phone */] },
825
+ { id: "desktop-safe" /* DesktopSafe */, designId: "D-A", label: "Desktop, icons clear", description: "Desktop wallpaper: text on the left clear of desktop icons and the taskbar, the picture on the right.", formats: ["desktop" /* Desktop */] },
826
+ { id: "desktop-arch" /* DesktopArch */, designId: "D-B", label: "Desktop arch", description: "Desktop wallpaper: titles left, the picture in a centred arch, details right.", formats: ["desktop" /* Desktop */] }
827
+ ];
828
+ var DEFAULTS = {
829
+ ["story" /* Story */]: "picture-right" /* PictureRight */,
830
+ ["post" /* Post */]: "picture-right" /* PictureRight */,
831
+ ["square" /* Square */]: "picture-right" /* PictureRight */,
832
+ ["phone" /* Phone */]: "phone-clock-safe" /* PhoneClockSafe */,
833
+ ["desktop" /* Desktop */]: "desktop-safe" /* DesktopSafe */
834
+ };
835
+ function defaultLayoutFor(format) {
836
+ return DEFAULTS[format];
837
+ }
838
+
639
839
  // src/examples/kizombaWorkshop.ts
640
840
  var WORKSHOPS = [
641
841
  { script: "Saturday 31 October", date: "Sat 31 Oct", first: "20:00 - 21:00", second: "21:00 - 22:00", party: "20\u20AC" },
@@ -693,10 +893,12 @@ exports.FORMAT_SIZES = FORMAT_SIZES;
693
893
  exports.FlierDocError = FlierDocError;
694
894
  exports.FontSetId = FontSetId;
695
895
  exports.FormatId = FormatId;
896
+ exports.LAYOUT_CATALOGUE = LAYOUT_CATALOGUE;
696
897
  exports.LayoutId = LayoutId;
697
898
  exports.PALETTES = PALETTES;
698
899
  exports.PaletteId = PaletteId;
699
900
  exports.PictureAnchor = PictureAnchor;
901
+ exports.defaultLayoutFor = defaultLayoutFor;
700
902
  exports.exportPng = exportPng;
701
903
  exports.flierDocSchema = flierDocSchema;
702
904
  exports.kizombaWorkshop = kizombaWorkshop;