@dloizides/poster-render 0.1.0 → 0.2.0

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ - Layouts `arch`, `phone-clock-safe`, `phone-arch`, `desktop-safe`, `desktop-arch` (FLIER-1 D4 "Keep all layouts"); `LAYOUT_CATALOGUE` + `defaultLayoutFor`.
6
+ - 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.
7
+ - Includes block uses one size for every line; `RenderReport.backdrop`; `Palette.arch`.
8
+
3
9
  ## 0.1.0
4
10
 
5
11
  - 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,7 @@ interface RenderReport {
135
140
  pictureRegion: Rect;
136
141
  textBoxes: Rect[];
137
142
  cardBoxes: Rect[];
143
+ backdrop: Rect | null;
138
144
  }
139
145
  interface Palette {
140
146
  background: string;
@@ -144,6 +150,7 @@ interface Palette {
144
150
  ink: string;
145
151
  soft: string;
146
152
  card: string;
153
+ arch: string;
147
154
  }
148
155
  interface FontSet {
149
156
  display: string;
@@ -200,7 +207,18 @@ interface DataUrlCanvas {
200
207
  /** Encodes the canvas as a PNG Blob of the canvas's exact pixel size. */
201
208
  declare function exportPng(canvas: DataUrlCanvas): Blob;
202
209
 
210
+ interface LayoutCatalogueEntry {
211
+ id: LayoutId;
212
+ designId: string;
213
+ label: string;
214
+ description: string;
215
+ formats: readonly FormatId[];
216
+ }
217
+ declare const LAYOUT_CATALOGUE: readonly LayoutCatalogueEntry[];
218
+ /** The default layout for a format (D4: E-A for posters, P-A for phone, D-A for desktop). */
219
+ declare function defaultLayoutFor(format: FormatId): LayoutId;
220
+
203
221
  declare const kizombaWorkshop: FlierDoc;
204
222
  declare const kizombaWorkshopDocs: FlierDoc[];
205
223
 
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 };
224
+ 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,7 @@ interface RenderReport {
135
140
  pictureRegion: Rect;
136
141
  textBoxes: Rect[];
137
142
  cardBoxes: Rect[];
143
+ backdrop: Rect | null;
138
144
  }
139
145
  interface Palette {
140
146
  background: string;
@@ -144,6 +150,7 @@ interface Palette {
144
150
  ink: string;
145
151
  soft: string;
146
152
  card: string;
153
+ arch: string;
147
154
  }
148
155
  interface FontSet {
149
156
  display: string;
@@ -200,7 +207,18 @@ interface DataUrlCanvas {
200
207
  /** Encodes the canvas as a PNG Blob of the canvas's exact pixel size. */
201
208
  declare function exportPng(canvas: DataUrlCanvas): Blob;
202
209
 
210
+ interface LayoutCatalogueEntry {
211
+ id: LayoutId;
212
+ designId: string;
213
+ label: string;
214
+ description: string;
215
+ formats: readonly FormatId[];
216
+ }
217
+ declare const LAYOUT_CATALOGUE: readonly LayoutCatalogueEntry[];
218
+ /** The default layout for a format (D4: E-A for posters, P-A for phone, D-A for desktop). */
219
+ declare function defaultLayoutFor(format: FormatId): LayoutId;
220
+
203
221
  declare const kizombaWorkshop: FlierDoc;
204
222
  declare const kizombaWorkshopDocs: FlierDoc[];
205
223
 
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 };
224
+ 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
@@ -316,11 +321,11 @@ function itemGroup(item, l, u) {
316
321
  }
317
322
  return { lines, gapAfter: 26 * 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
+ (withPrices ? b.prices : void 0)?.forEach((row) => groups.push({ lines: [l.strong?.(row.amount, 66), l.accent?.(row.label, 52)], gapAfter: 30 * u }));
324
329
  return groups;
325
330
  }
326
331
  function listGroups(doc, l, u) {
@@ -331,7 +336,11 @@ function listGroups(doc, l, u) {
331
336
  }
332
337
  return groups;
333
338
  }
334
- function bodyGroups(doc, p, u) {
339
+ function flankPriceGroups(doc, p, u) {
340
+ const l = makers(p, u);
341
+ return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong?.(row.amount, 110), l.accent?.(row.label, 48)], gapAfter: 30 * u }));
342
+ }
343
+ function bodyGroups(doc, p, u, withPrices = true) {
335
344
  const l = makers(p, u);
336
345
  const b = doc.blocks;
337
346
  const groups = [];
@@ -339,10 +348,10 @@ function bodyGroups(doc, p, u) {
339
348
  if (b.venue) {
340
349
  groups.push({ lines: b.venue.map((v) => l.venue?.(v, isList ? 48 : 58)), gapAfter: 40 * u, rule: { color: p.accent, height: 8 * u } });
341
350
  }
342
- groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u));
351
+ groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
343
352
  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 } });
353
+ const lines = [l.accent?.(b.includes.heading, 38), ...b.includes.items.map((i) => l.item?.(i, 38))];
354
+ groups.push({ lines, gapAfter: 24 * u, card: { color: p.card, pad: 28 * u }, uniform: true });
346
355
  }
347
356
  if (b.footer) {
348
357
  groups.push({ lines: [l.soft?.(b.footer, 34)], gapAfter: 0 });
@@ -350,6 +359,51 @@ function bodyGroups(doc, p, u) {
350
359
  return groups;
351
360
  }
352
361
 
362
+ // src/layout/layoutSpecs.ts
363
+ var flanksBeside = (pic, top, bottom2, m) => [
364
+ [m, top, pic[0] - 2 * m, bottom2 - top],
365
+ [pic[0] + pic[2] + m, top, 1 - pic[0] - pic[2] - 2 * m, bottom2 - top]
366
+ ];
367
+ var ARCH_PIC = [0.22, 0.42, 0.56, 0.58];
368
+ var PHONE_ARCH_PIC = [0.2, 0.6, 0.6, 0.4];
369
+ var DESK_ARCH_PIC = [0.355, 0.07, 0.32, 0.93];
370
+ var ARCH_LANDSCAPE = {
371
+ header: [0.04, 0.06, 0.3, 0.5],
372
+ body: [0.04, 0.58, 0.3, 0.36],
373
+ picture: [0.36, 0.08, 0.3, 0.92],
374
+ alignX: 0.5,
375
+ flanks: [[0.68, 0.08, 0.28, 0.42], [0.68, 0.52, 0.28, 0.42]],
376
+ arch: 0.06
377
+ };
378
+ var LAYOUT_SPECS = {
379
+ ["arch" /* Arch */]: {
380
+ 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 },
381
+ landscape: ARCH_LANDSCAPE
382
+ },
383
+ ["phone-clock-safe" /* PhoneClockSafe */]: {
384
+ 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 },
385
+ 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 }
386
+ },
387
+ ["phone-arch" /* PhoneArch */]: {
388
+ 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 },
389
+ landscape: { ...ARCH_LANDSCAPE, header: [0.04, 0.16, 0.3, 0.42], picture: [0.36, 0.16, 0.3, 0.84] }
390
+ },
391
+ ["desktop-safe" /* DesktopSafe */]: {
392
+ 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 },
393
+ 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 }
394
+ },
395
+ ["desktop-arch" /* DesktopArch */]: {
396
+ 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 },
397
+ 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.88, 0.04, 0.09, 0.14], arch: 0.05 }
398
+ }
399
+ };
400
+ var r = Math.round;
401
+ var toRect = (f, w, h) => {
402
+ const x = r(f[0] * w);
403
+ const y = r(f[1] * h);
404
+ return { x, y, w: r((f[0] + f[2]) * w) - x, h: r((f[1] + f[3]) * h) - y };
405
+ };
406
+
353
407
  // src/layout/geometry.ts
354
408
  var MARGIN = 0.045;
355
409
  var HEADER_PORTRAIT = 0.3;
@@ -362,12 +416,12 @@ var PICTURE_SIDE = {
362
416
  ["list" /* List */]: [0.5, 0.36]
363
417
  };
364
418
  var ANCHOR_Y = { ["top" /* Top */]: 0, ["center" /* Center */]: 0.5, ["bottom" /* Bottom */]: 1 };
365
- var r = Math.round;
419
+ var r2 = Math.round;
366
420
  function side(w, h, fraction) {
367
- const m = r(Math.min(w, h) * MARGIN);
421
+ const m = r2(Math.min(w, h) * MARGIN);
368
422
  if (h >= w) {
369
- const split2 = r(w * (1 - fraction[0]));
370
- const top2 = r(h * HEADER_PORTRAIT);
423
+ const split2 = r2(w * (1 - fraction[0]));
424
+ const top2 = r2(h * HEADER_PORTRAIT);
371
425
  return {
372
426
  header: { x: m, y: m, w: w - 2 * m, h: top2 - 2 * m },
373
427
  picture: { x: split2, y: top2, w: w - split2, h: h - top2 },
@@ -375,19 +429,19 @@ function side(w, h, fraction) {
375
429
  alignX: 1
376
430
  };
377
431
  }
378
- const split = r(w * (1 - fraction[1]));
379
- const top = r(h * HEADER_LANDSCAPE);
432
+ const split = r2(w * (1 - fraction[1]));
433
+ const top = r2(h * HEADER_LANDSCAPE);
380
434
  return {
381
435
  header: { x: m, y: m, w: split - 2 * m, h: top - m },
382
436
  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 },
437
+ body: { x: m, y: top + r2(m / 2), w: split - 2 * m, h: h - top - r2(m / 2) - m },
384
438
  alignX: 1
385
439
  };
386
440
  }
387
441
  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);
442
+ const m = r2(Math.min(w, h) * MARGIN);
443
+ const top = r2(h * BOTTOM_HEADER);
444
+ const pic = r2(h * BOTTOM_PICTURE);
391
445
  return {
392
446
  header: { x: m, y: m, w: w - 2 * m, h: top - 2 * m },
393
447
  body: { x: m, y: top, w: w - 2 * m, h: pic - top - m },
@@ -395,8 +449,23 @@ function bottom(w, h) {
395
449
  alignX: 0.5
396
450
  };
397
451
  }
452
+ function fromSpec(spec, w, h) {
453
+ return {
454
+ header: toRect(spec.header, w, h),
455
+ body: toRect(spec.body, w, h),
456
+ picture: toRect(spec.picture, w, h),
457
+ alignX: spec.alignX,
458
+ flanks: spec.flanks?.map((f) => toRect(f, w, h)),
459
+ logo: spec.logo ? toRect(spec.logo, w, h) : void 0,
460
+ arch: spec.arch
461
+ };
462
+ }
398
463
  var mirror = (rect, w) => ({ ...rect, x: w - rect.x - rect.w });
399
464
  function geometry(layout, w, h) {
465
+ const oriented = LAYOUT_SPECS[layout];
466
+ if (oriented) {
467
+ return fromSpec(h >= w ? oriented.portrait : oriented.landscape, w, h);
468
+ }
400
469
  if (layout === "picture-bottom" /* PictureBottom */) {
401
470
  return bottom(w, h);
402
471
  }
@@ -433,6 +502,12 @@ function lineSize(line, width, k, avail) {
433
502
  const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
434
503
  return Math.max(0, Math.min(line.size * k, line.size * (avail - indent) / Math.max(width, 1)));
435
504
  }
505
+ function groupRatio(m, k, avail) {
506
+ if (!m.group.uniform) {
507
+ return null;
508
+ }
509
+ return Math.min(...m.group.lines.map((line, i) => lineSize(line, m.widths[i] ?? 1, k, avail) / line.size));
510
+ }
436
511
  function placeGroup(m, k, col, y0, out) {
437
512
  const { group } = m;
438
513
  let y = y0;
@@ -443,11 +518,12 @@ function placeGroup(m, k, col, y0, out) {
443
518
  const pad = group.card ? group.card.pad * k : 0;
444
519
  const top = y;
445
520
  y += pad;
521
+ const ratio = groupRatio(m, k, col.w - 2 * pad);
446
522
  group.lines.forEach((line, i) => {
447
- const size = lineSize(line, m.widths[i] ?? 1, k, col.w - 2 * pad);
523
+ const size = ratio === null ? lineSize(line, m.widths[i] ?? 1, k, col.w - 2 * pad) : line.size * ratio;
448
524
  const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
449
- const x = col.x + pad + indent;
450
525
  const w = (m.widths[i] ?? 0) * size / line.size;
526
+ const x = col.right ? col.x + col.w - pad - w : col.x + pad + indent;
451
527
  out?.texts.push({ text: line.text, font: "", color: line.color, box: { x, y, w, h: size * LINE_HEIGHT } });
452
528
  if (line.bullet && out) {
453
529
  out.bullets.push({ x: col.x + pad + size * BULLET_RADIUS, y: y + size * LINE_HEIGHT / 2, r: size * BULLET_RADIUS, color: line.bullet });
@@ -526,21 +602,22 @@ function flow(ctx, groups, zone, fonts) {
526
602
  var BASE = 1080;
527
603
  var LOGO_SHARE = 0.26;
528
604
  var CARD_RADIUS = 0.035;
529
- function roundRect(ctx, r2, radius) {
530
- const rad = Math.min(radius, r2.w / 2, r2.h / 2);
605
+ var CAP_TOP = 0.15;
606
+ function roundRect(ctx, r3, radius) {
607
+ const rad = Math.min(radius, r3.w / 2, r3.h / 2);
531
608
  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);
609
+ ctx.moveTo(r3.x + rad, r3.y);
610
+ ctx.arcTo(r3.x + r3.w, r3.y, r3.x + r3.w, r3.y + r3.h, rad);
611
+ ctx.arcTo(r3.x + r3.w, r3.y + r3.h, r3.x, r3.y + r3.h, rad);
612
+ ctx.arcTo(r3.x, r3.y + r3.h, r3.x, r3.y, rad);
613
+ ctx.arcTo(r3.x, r3.y, r3.x + r3.w, r3.y, rad);
537
614
  ctx.closePath();
538
615
  ctx.fill();
539
616
  }
540
617
  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);
618
+ f.rules.forEach((r3) => {
619
+ ctx.fillStyle = r3.color;
620
+ ctx.fillRect(r3.rect.x, r3.rect.y, r3.rect.w, r3.rect.h);
544
621
  });
545
622
  f.cards.forEach((c) => {
546
623
  ctx.fillStyle = c.color;
@@ -571,6 +648,30 @@ function checked(input, assets) {
571
648
  }
572
649
  return result.doc;
573
650
  }
651
+ function archRect(geo) {
652
+ if (geo.arch === void 0) {
653
+ return null;
654
+ }
655
+ const top = geo.picture.y + Math.round(geo.picture.h * geo.arch);
656
+ return { x: geo.picture.x, y: top, w: geo.picture.w, h: geo.picture.y + geo.picture.h - top };
657
+ }
658
+ function drawArch(ctx, r3, color) {
659
+ const rad = r3.w / 2;
660
+ ctx.fillStyle = color;
661
+ ctx.fillRect(r3.x, r3.y + rad, r3.w, r3.h - rad);
662
+ ctx.beginPath();
663
+ ctx.arc(r3.x + rad, r3.y + rad, rad, Math.PI, Math.PI * 2);
664
+ ctx.fill();
665
+ }
666
+ function flankFlows(ctx, groups, flanks, fonts) {
667
+ const half = Math.ceil(groups.length / 2);
668
+ return flanks.map((zone, i) => i === 0 ? flow(ctx, groups.slice(0, half), zone, fonts) : flow(ctx, groups.slice(half), { ...zone, right: true }, fonts));
669
+ }
670
+ function capAligned(zone, head) {
671
+ const first = head.texts[0]?.box;
672
+ const shift = first ? Math.min(Math.max(0, Math.round(first.y + first.h * CAP_TOP) - zone.y), Math.round(zone.h / 2)) : 0;
673
+ return { ...zone, y: zone.y + shift, h: zone.h - shift };
674
+ }
574
675
  function splitHeader(header, hasLogo) {
575
676
  const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
576
677
  return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
@@ -593,20 +694,25 @@ function render(input, assets, canvas) {
593
694
  ctx.fillRect(0, 0, width, height);
594
695
  if (doc.background === "tint" /* Tint */) {
595
696
  ctx.fillStyle = palette.tint;
596
- [geo.header, geo.body].forEach((r2) => ctx.fillRect(r2.x, r2.y, r2.w, r2.h));
697
+ [geo.header, geo.body].forEach((r3) => ctx.fillRect(r3.x, r3.y, r3.w, r3.h));
597
698
  }
598
- const zones = splitHeader(geo.header, doc.logo !== void 0);
699
+ const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
599
700
  const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
600
- const body = flow(ctx, bodyGroups(doc, palette, u), geo.body, fonts);
701
+ const flanks = geo.flanks ?? [];
702
+ const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
703
+ const sides = flanks.length > 0 ? flankFlows(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
601
704
  const radius = Math.min(width, height) * CARD_RADIUS;
602
- drawFlow(ctx, head, radius);
603
- drawFlow(ctx, body, radius);
705
+ [head, body, ...sides].forEach((f) => drawFlow(ctx, f, radius));
604
706
  const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
605
707
  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);
708
+ const r3 = placeContain(logoImage, capAligned(zones.logo, head), "top" /* Top */, 1, 1);
709
+ ctx.drawImage(logoImage, r3.x, r3.y, r3.w, r3.h);
608
710
  }
609
711
  const image = doc.picture ? assets[doc.picture.imageId] : void 0;
712
+ const backdrop = image ? archRect(geo) : null;
713
+ if (backdrop) {
714
+ drawArch(ctx, backdrop, palette.arch);
715
+ }
610
716
  const picture = image && doc.picture ? placeContain(image, geo.picture, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
611
717
  if (image && picture) {
612
718
  ctx.drawImage(image, picture.x, picture.y, picture.w, picture.h);
@@ -616,8 +722,9 @@ function render(input, assets, canvas) {
616
722
  height,
617
723
  picture,
618
724
  pictureRegion: geo.picture,
619
- textBoxes: [...head.texts, ...body.texts].map((t) => t.box),
620
- cardBoxes: body.cards.map((c) => c.rect)
725
+ textBoxes: [head, body, ...sides].flatMap((f) => f.texts.map((t) => t.box)),
726
+ cardBoxes: body.cards.map((c) => c.rect),
727
+ backdrop
621
728
  };
622
729
  }
623
730
 
@@ -636,6 +743,30 @@ function exportPng(canvas) {
636
743
  return new Blob([bytes], { type: "image/png" });
637
744
  }
638
745
 
746
+ // src/layoutCatalogue.ts
747
+ var POSTERS = ["story" /* Story */, "post" /* Post */, "square" /* Square */];
748
+ var LAYOUT_CATALOGUE = [
749
+ { 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 },
750
+ { 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 },
751
+ { 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 },
752
+ { id: "picture-bottom" /* PictureBottom */, designId: "picture-bottom", label: "Picture bottom", description: "Titles and details on top, the picture across the bottom half.", formats: POSTERS },
753
+ { id: "list" /* List */, designId: "list", label: "Upcoming list", description: "Several dated events as a list beside the picture.", formats: POSTERS },
754
+ { 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 */] },
755
+ { 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 */] },
756
+ { 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 */] },
757
+ { 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 */] }
758
+ ];
759
+ var DEFAULTS = {
760
+ ["story" /* Story */]: "picture-right" /* PictureRight */,
761
+ ["post" /* Post */]: "picture-right" /* PictureRight */,
762
+ ["square" /* Square */]: "picture-right" /* PictureRight */,
763
+ ["phone" /* Phone */]: "phone-clock-safe" /* PhoneClockSafe */,
764
+ ["desktop" /* Desktop */]: "desktop-safe" /* DesktopSafe */
765
+ };
766
+ function defaultLayoutFor(format) {
767
+ return DEFAULTS[format];
768
+ }
769
+
639
770
  // src/examples/kizombaWorkshop.ts
640
771
  var WORKSHOPS = [
641
772
  { script: "Saturday 31 October", date: "Sat 31 Oct", first: "20:00 - 21:00", second: "21:00 - 22:00", party: "20\u20AC" },
@@ -693,10 +824,12 @@ exports.FORMAT_SIZES = FORMAT_SIZES;
693
824
  exports.FlierDocError = FlierDocError;
694
825
  exports.FontSetId = FontSetId;
695
826
  exports.FormatId = FormatId;
827
+ exports.LAYOUT_CATALOGUE = LAYOUT_CATALOGUE;
696
828
  exports.LayoutId = LayoutId;
697
829
  exports.PALETTES = PALETTES;
698
830
  exports.PaletteId = PaletteId;
699
831
  exports.PictureAnchor = PictureAnchor;
832
+ exports.defaultLayoutFor = defaultLayoutFor;
700
833
  exports.exportPng = exportPng;
701
834
  exports.flierDocSchema = flierDocSchema;
702
835
  exports.kizombaWorkshop = kizombaWorkshop;