@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/dist/index.mjs CHANGED
@@ -29,6 +29,11 @@ var LayoutId = /* @__PURE__ */ ((LayoutId2) => {
29
29
  LayoutId2["PictureLeft"] = "picture-left";
30
30
  LayoutId2["PictureBottom"] = "picture-bottom";
31
31
  LayoutId2["List"] = "list";
32
+ LayoutId2["Arch"] = "arch";
33
+ LayoutId2["PhoneClockSafe"] = "phone-clock-safe";
34
+ LayoutId2["PhoneArch"] = "phone-arch";
35
+ LayoutId2["DesktopSafe"] = "desktop-safe";
36
+ LayoutId2["DesktopArch"] = "desktop-arch";
32
37
  return LayoutId2;
33
38
  })(LayoutId || {});
34
39
 
@@ -60,10 +65,10 @@ var FORMAT_SIZES = {
60
65
 
61
66
  // src/palettes.ts
62
67
  var PALETTES = {
63
- ["sand" /* Sand */]: { background: "#f5f4f2", tint: "#ece8e3", title: "#958a80", accent: "#e9b47f", ink: "#5f564f", soft: "#b3a89e", card: "#ffffff" },
64
- ["night" /* Night */]: { background: "#1c1a17", tint: "#25221e", title: "#e8dccb", accent: "#e9b47f", ink: "#f4efe8", soft: "#9c9284", card: "#2c2824" },
65
- ["sea" /* Sea */]: { background: "#eef4f3", tint: "#e1ecea", title: "#4f7d7a", accent: "#e07a5f", ink: "#2f3e46", soft: "#8aa3a1", card: "#ffffff" },
66
- ["rose" /* Rose */]: { background: "#f8efec", tint: "#f1e2dd", title: "#9b6b63", accent: "#c9846f", ink: "#4a3934", soft: "#b39a92", card: "#ffffff" }
68
+ ["sand" /* Sand */]: { background: "#f5f4f2", tint: "#ece8e3", title: "#958a80", accent: "#e9b47f", ink: "#5f564f", soft: "#b3a89e", card: "#ffffff", arch: "#ede8e2" },
69
+ ["night" /* Night */]: { background: "#1c1a17", tint: "#25221e", title: "#e8dccb", accent: "#e9b47f", ink: "#f4efe8", soft: "#9c9284", card: "#2c2824", arch: "#25221f" },
70
+ ["sea" /* Sea */]: { background: "#eef4f3", tint: "#e1ecea", title: "#4f7d7a", accent: "#e07a5f", ink: "#2f3e46", soft: "#8aa3a1", card: "#ffffff", arch: "#e4ecea" },
71
+ ["rose" /* Rose */]: { background: "#f8efec", tint: "#f1e2dd", title: "#9b6b63", accent: "#c9846f", ink: "#4a3934", soft: "#b39a92", card: "#ffffff", arch: "#f1e5e1" }
67
72
  };
68
73
 
69
74
  // src/fontSets.ts
@@ -307,18 +312,19 @@ function headerGroups(doc, p, u) {
307
312
  return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
308
313
  }
309
314
  function itemGroup(item, l, u) {
310
- const lines = [l.script?.(item.date, 96)];
315
+ const lines = [l.script?.(item.date, 67)];
311
316
  item.schedule.forEach((row) => lines.push(l.strong?.(`${row.label} ${row.time}`, 42)));
312
317
  if (item.price) {
313
318
  lines.push(l.accent?.(item.price, 42));
314
319
  }
315
- return { lines, gapAfter: 26 * u };
320
+ return { lines, gapAfter: 48 * u };
316
321
  }
317
- function eventGroups(doc, l, u) {
322
+ function eventGroups(doc, l, u, withPrices) {
318
323
  const b = doc.blocks;
319
324
  const groups = [];
320
325
  b.schedule?.forEach((row) => groups.push({ lines: [l.strong?.(row.label, 66), l.accent?.(row.time, 60)], gapAfter: 30 * u }));
321
- b.prices?.forEach((row) => groups.push({ lines: [l.strong?.(row.amount, 66), l.accent?.(row.label, 52)], gapAfter: 30 * u }));
326
+ const prices = (withPrices ? b.prices : void 0) ?? [];
327
+ 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 }));
322
328
  return groups;
323
329
  }
324
330
  function listGroups(doc, l, u) {
@@ -329,18 +335,22 @@ function listGroups(doc, l, u) {
329
335
  }
330
336
  return groups;
331
337
  }
332
- function bodyGroups(doc, p, u) {
338
+ function flankPriceGroups(doc, p, u) {
339
+ const l = makers(p, u);
340
+ return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong?.(row.amount, 110), l.accent?.(row.label, 48)], gapAfter: 30 * u }));
341
+ }
342
+ function bodyGroups(doc, p, u, withPrices = true) {
333
343
  const l = makers(p, u);
334
344
  const b = doc.blocks;
335
345
  const groups = [];
336
346
  const isList = doc.layout === "list" /* List */;
337
347
  if (b.venue) {
338
- groups.push({ lines: b.venue.map((v) => l.venue?.(v, isList ? 48 : 58)), gapAfter: 40 * u, rule: { color: p.accent, height: 8 * u } });
348
+ 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 } });
339
349
  }
340
- groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u));
350
+ groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
341
351
  if (b.includes) {
342
- const lines = [l.accent?.(b.includes.heading, 36), ...b.includes.items.map((i) => l.item?.(i, 40))];
343
- groups.push({ lines, gapAfter: 24 * u, card: { color: p.card, pad: 28 * u } });
352
+ const lines = [l.accent?.(b.includes.heading, 38), ...b.includes.items.map((i) => l.item?.(i, 38))];
353
+ groups.push({ lines, gapAfter: 24 * u, uniform: true });
344
354
  }
345
355
  if (b.footer) {
346
356
  groups.push({ lines: [l.soft?.(b.footer, 34)], gapAfter: 0 });
@@ -348,6 +358,51 @@ function bodyGroups(doc, p, u) {
348
358
  return groups;
349
359
  }
350
360
 
361
+ // src/layout/layoutSpecs.ts
362
+ var flanksBeside = (pic, top, bottom2, m) => [
363
+ [m, top, pic[0] - 2 * m, bottom2 - top],
364
+ [pic[0] + pic[2] + m, top, 1 - pic[0] - pic[2] - 2 * m, bottom2 - top]
365
+ ];
366
+ var ARCH_PIC = [0.22, 0.42, 0.56, 0.58];
367
+ var PHONE_ARCH_PIC = [0.2, 0.6, 0.6, 0.4];
368
+ var DESK_ARCH_PIC = [0.355, 0.07, 0.32, 0.93];
369
+ var ARCH_LANDSCAPE = {
370
+ header: [0.04, 0.06, 0.3, 0.5],
371
+ body: [0.04, 0.58, 0.3, 0.36],
372
+ picture: [0.36, 0.08, 0.3, 0.92],
373
+ alignX: 0.5,
374
+ flanks: [[0.68, 0.08, 0.28, 0.42], [0.68, 0.52, 0.28, 0.42]],
375
+ arch: 0.06
376
+ };
377
+ var LAYOUT_SPECS = {
378
+ ["arch" /* Arch */]: {
379
+ 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 },
380
+ landscape: ARCH_LANDSCAPE
381
+ },
382
+ ["phone-clock-safe" /* PhoneClockSafe */]: {
383
+ 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 },
384
+ 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 }
385
+ },
386
+ ["phone-arch" /* PhoneArch */]: {
387
+ 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 },
388
+ landscape: { ...ARCH_LANDSCAPE, header: [0.04, 0.16, 0.3, 0.42], picture: [0.36, 0.16, 0.3, 0.84] }
389
+ },
390
+ ["desktop-safe" /* DesktopSafe */]: {
391
+ 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 },
392
+ 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] }
393
+ },
394
+ ["desktop-arch" /* DesktopArch */]: {
395
+ 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 },
396
+ 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 }
397
+ }
398
+ };
399
+ var r = Math.round;
400
+ var toRect = (f, w, h) => {
401
+ const x = r(f[0] * w);
402
+ const y = r(f[1] * h);
403
+ return { x, y, w: r((f[0] + f[2]) * w) - x, h: r((f[1] + f[3]) * h) - y };
404
+ };
405
+
351
406
  // src/layout/geometry.ts
352
407
  var MARGIN = 0.045;
353
408
  var HEADER_PORTRAIT = 0.3;
@@ -360,12 +415,12 @@ var PICTURE_SIDE = {
360
415
  ["list" /* List */]: [0.5, 0.36]
361
416
  };
362
417
  var ANCHOR_Y = { ["top" /* Top */]: 0, ["center" /* Center */]: 0.5, ["bottom" /* Bottom */]: 1 };
363
- var r = Math.round;
418
+ var r2 = Math.round;
364
419
  function side(w, h, fraction) {
365
- const m = r(Math.min(w, h) * MARGIN);
420
+ const m = r2(Math.min(w, h) * MARGIN);
366
421
  if (h >= w) {
367
- const split2 = r(w * (1 - fraction[0]));
368
- const top2 = r(h * HEADER_PORTRAIT);
422
+ const split2 = r2(w * (1 - fraction[0]));
423
+ const top2 = r2(h * HEADER_PORTRAIT);
369
424
  return {
370
425
  header: { x: m, y: m, w: w - 2 * m, h: top2 - 2 * m },
371
426
  picture: { x: split2, y: top2, w: w - split2, h: h - top2 },
@@ -373,19 +428,19 @@ function side(w, h, fraction) {
373
428
  alignX: 1
374
429
  };
375
430
  }
376
- const split = r(w * (1 - fraction[1]));
377
- const top = r(h * HEADER_LANDSCAPE);
431
+ const split = r2(w * (1 - fraction[1]));
432
+ const top = r2(h * HEADER_LANDSCAPE);
378
433
  return {
379
434
  header: { x: m, y: m, w: split - 2 * m, h: top - m },
380
435
  picture: { x: split, y: 0, w: w - split, h },
381
- body: { x: m, y: top + r(m / 2), w: split - 2 * m, h: h - top - r(m / 2) - m },
436
+ body: { x: m, y: top + r2(m / 2), w: split - 2 * m, h: h - top - r2(m / 2) - m },
382
437
  alignX: 1
383
438
  };
384
439
  }
385
440
  function bottom(w, h) {
386
- const m = r(Math.min(w, h) * MARGIN);
387
- const top = r(h * BOTTOM_HEADER);
388
- const pic = r(h * BOTTOM_PICTURE);
441
+ const m = r2(Math.min(w, h) * MARGIN);
442
+ const top = r2(h * BOTTOM_HEADER);
443
+ const pic = r2(h * BOTTOM_PICTURE);
389
444
  return {
390
445
  header: { x: m, y: m, w: w - 2 * m, h: top - 2 * m },
391
446
  body: { x: m, y: top, w: w - 2 * m, h: pic - top - m },
@@ -393,8 +448,23 @@ function bottom(w, h) {
393
448
  alignX: 0.5
394
449
  };
395
450
  }
451
+ function fromSpec(spec, w, h) {
452
+ return {
453
+ header: toRect(spec.header, w, h),
454
+ body: toRect(spec.body, w, h),
455
+ picture: toRect(spec.picture, w, h),
456
+ alignX: spec.alignX,
457
+ flanks: spec.flanks?.map((f) => toRect(f, w, h)),
458
+ logo: spec.logo ? toRect(spec.logo, w, h) : void 0,
459
+ arch: spec.arch
460
+ };
461
+ }
396
462
  var mirror = (rect, w) => ({ ...rect, x: w - rect.x - rect.w });
397
463
  function geometry(layout, w, h) {
464
+ const oriented = LAYOUT_SPECS[layout];
465
+ if (oriented) {
466
+ return fromSpec(h >= w ? oriented.portrait : oriented.landscape, w, h);
467
+ }
398
468
  if (layout === "picture-bottom" /* PictureBottom */) {
399
469
  return bottom(w, h);
400
470
  }
@@ -425,12 +495,32 @@ var RULE_WIDTH = 0.9;
425
495
  var WIDE = 1.6;
426
496
  var MIN_K = 0.02;
427
497
  var STEPS = 22;
498
+ var ICON_SCALE = 1.6;
499
+ var ICON_GAP = 0.35;
428
500
  var fontOf = (line, size, fonts) => `${line.weight} ${size}px ${fonts[line.role]}`;
429
- var emptyFlow = () => ({ texts: [], cards: [], rules: [], bullets: [] });
501
+ var emptyFlow = () => ({ texts: [], cards: [], rules: [], bullets: [], icons: [] });
502
+ var iconSize = (group, k) => {
503
+ const first = group.lines[0];
504
+ return group.icon && first ? first.size * k * LINE_HEIGHT * ICON_SCALE : 0;
505
+ };
430
506
  function lineSize(line, width, k, avail) {
431
507
  const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
432
508
  return Math.max(0, Math.min(line.size * k, line.size * (avail - indent) / Math.max(width, 1)));
433
509
  }
510
+ function groupRatio(m, k, avail) {
511
+ if (!m.group.uniform) {
512
+ return null;
513
+ }
514
+ return Math.min(...m.group.lines.map((line, i) => lineSize(line, m.widths[i] ?? 1, k, avail) / line.size));
515
+ }
516
+ function placeIcon(group, d, x, textTop, textBottom, out) {
517
+ if (!group.icon || d <= 0) {
518
+ return textBottom;
519
+ }
520
+ const textH = textBottom - textTop;
521
+ 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 });
522
+ return textTop + Math.max(textH, d);
523
+ }
434
524
  function placeGroup(m, k, col, y0, out) {
435
525
  const { group } = m;
436
526
  let y = y0;
@@ -441,17 +531,22 @@ function placeGroup(m, k, col, y0, out) {
441
531
  const pad = group.card ? group.card.pad * k : 0;
442
532
  const top = y;
443
533
  y += pad;
534
+ const d = iconSize(group, k);
535
+ const iconIndent = d > 0 ? d * (1 + ICON_GAP) : 0;
536
+ const avail = col.w - 2 * pad - iconIndent;
537
+ const ratio = groupRatio(m, k, avail);
444
538
  group.lines.forEach((line, i) => {
445
- const size = lineSize(line, m.widths[i] ?? 1, k, col.w - 2 * pad);
446
- const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
447
- const x = col.x + pad + indent;
539
+ const size = ratio === null ? lineSize(line, m.widths[i] ?? 1, k, avail) : line.size * ratio;
540
+ const indent = (line.bullet ? line.size * k * BULLET_INDENT : 0) + iconIndent;
448
541
  const w = (m.widths[i] ?? 0) * size / line.size;
542
+ const x = col.right ? col.x + col.w - pad - w : col.x + pad + indent;
449
543
  out?.texts.push({ text: line.text, font: "", color: line.color, box: { x, y, w, h: size * LINE_HEIGHT } });
450
544
  if (line.bullet && out) {
451
545
  out.bullets.push({ x: col.x + pad + size * BULLET_RADIUS, y: y + size * LINE_HEIGHT / 2, r: size * BULLET_RADIUS, color: line.bullet });
452
546
  }
453
547
  y += size * LINE_HEIGHT;
454
548
  });
549
+ y = placeIcon(group, d, col.x + pad, top + pad, y, out);
455
550
  y += pad;
456
551
  if (group.card) {
457
552
  out?.cards.unshift({ rect: { x: col.x, y: top, w: col.w, h: y - top }, color: group.card.color });
@@ -465,6 +560,27 @@ function placeColumn(groups, k, col, out) {
465
560
  }
466
561
  return h;
467
562
  }
563
+ var tallest = (heights, cut) => {
564
+ const left = heights.slice(0, cut).reduce((a, b) => a + b, 0);
565
+ const right = heights.slice(cut).reduce((a, b) => a + b, 0);
566
+ return Math.max(left, right);
567
+ };
568
+ function keepTogether(groups, heights, cut) {
569
+ const breaks = (c) => groups[c - 1]?.group.keepWithNext === true;
570
+ let back = cut;
571
+ while (back > 1 && breaks(back)) {
572
+ back--;
573
+ }
574
+ let fwd = cut;
575
+ while (fwd < groups.length - 1 && breaks(fwd)) {
576
+ fwd++;
577
+ }
578
+ const candidates = [back, fwd].filter((c) => !breaks(c));
579
+ if (candidates.length === 0) {
580
+ return cut;
581
+ }
582
+ return candidates.reduce((best, c) => tallest(heights, c) < tallest(heights, best) ? c : best);
583
+ }
468
584
  function splitColumns(groups, zone) {
469
585
  if (zone.w / Math.max(zone.h, 1) <= WIDE || groups.length < 2) {
470
586
  return [{ col: zone, groups }];
@@ -479,7 +595,7 @@ function splitColumns(groups, zone) {
479
595
  }
480
596
  acc += h;
481
597
  });
482
- cut = Math.min(cut, groups.length - 1);
598
+ cut = keepTogether(groups, heights, Math.min(cut, groups.length - 1));
483
599
  const gap = Math.round(zone.w * 0.04);
484
600
  const w = (zone.w - gap) / 2;
485
601
  return [
@@ -524,21 +640,48 @@ function flow(ctx, groups, zone, fonts) {
524
640
  var BASE = 1080;
525
641
  var LOGO_SHARE = 0.26;
526
642
  var CARD_RADIUS = 0.035;
527
- function roundRect(ctx, r2, radius) {
528
- const rad = Math.min(radius, r2.w / 2, r2.h / 2);
643
+ var CAP_TOP = 0.15;
644
+ var PIN_HEAD_Y = 0.08;
645
+ var PIN_HEAD_R = 0.19;
646
+ var PIN_TIP_Y = 0.28;
647
+ var PIN_HOLE = 0.42;
648
+ var ARC_START = 0.75;
649
+ var ARC_END = 2.25;
650
+ function drawPin(ctx, r3, color, glyph) {
651
+ const cx = r3.x + r3.w / 2;
652
+ const cy = r3.y + r3.h / 2;
653
+ const hy = cy - r3.w * PIN_HEAD_Y;
654
+ const rad = r3.w * PIN_HEAD_R;
655
+ ctx.fillStyle = color;
656
+ ctx.beginPath();
657
+ ctx.arc(cx, cy, r3.w / 2, 0, Math.PI * 2);
658
+ ctx.fill();
659
+ ctx.fillStyle = glyph;
660
+ ctx.beginPath();
661
+ ctx.moveTo(cx, cy + r3.w * PIN_TIP_Y);
662
+ ctx.arc(cx, hy, rad, Math.PI * ARC_START, Math.PI * ARC_END);
663
+ ctx.closePath();
664
+ ctx.fill();
665
+ ctx.fillStyle = color;
666
+ ctx.beginPath();
667
+ ctx.arc(cx, hy, rad * PIN_HOLE, 0, Math.PI * 2);
668
+ ctx.fill();
669
+ }
670
+ function roundRect(ctx, r3, radius) {
671
+ const rad = Math.min(radius, r3.w / 2, r3.h / 2);
529
672
  ctx.beginPath();
530
- ctx.moveTo(r2.x + rad, r2.y);
531
- ctx.arcTo(r2.x + r2.w, r2.y, r2.x + r2.w, r2.y + r2.h, rad);
532
- ctx.arcTo(r2.x + r2.w, r2.y + r2.h, r2.x, r2.y + r2.h, rad);
533
- ctx.arcTo(r2.x, r2.y + r2.h, r2.x, r2.y, rad);
534
- ctx.arcTo(r2.x, r2.y, r2.x + r2.w, r2.y, rad);
673
+ ctx.moveTo(r3.x + rad, r3.y);
674
+ ctx.arcTo(r3.x + r3.w, r3.y, r3.x + r3.w, r3.y + r3.h, rad);
675
+ ctx.arcTo(r3.x + r3.w, r3.y + r3.h, r3.x, r3.y + r3.h, rad);
676
+ ctx.arcTo(r3.x, r3.y + r3.h, r3.x, r3.y, rad);
677
+ ctx.arcTo(r3.x, r3.y, r3.x + r3.w, r3.y, rad);
535
678
  ctx.closePath();
536
679
  ctx.fill();
537
680
  }
538
681
  function drawFlow(ctx, f, radius) {
539
- f.rules.forEach((r2) => {
540
- ctx.fillStyle = r2.color;
541
- ctx.fillRect(r2.rect.x, r2.rect.y, r2.rect.w, r2.rect.h);
682
+ f.rules.forEach((r3) => {
683
+ ctx.fillStyle = r3.color;
684
+ ctx.fillRect(r3.rect.x, r3.rect.y, r3.rect.w, r3.rect.h);
542
685
  });
543
686
  f.cards.forEach((c) => {
544
687
  ctx.fillStyle = c.color;
@@ -550,6 +693,7 @@ function drawFlow(ctx, f, radius) {
550
693
  ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
551
694
  ctx.fill();
552
695
  });
696
+ f.icons.forEach((i) => drawPin(ctx, i.rect, i.color, i.glyph));
553
697
  ctx.textAlign = "left";
554
698
  ctx.textBaseline = "top";
555
699
  f.texts.forEach((t) => {
@@ -569,6 +713,30 @@ function checked(input, assets) {
569
713
  }
570
714
  return result.doc;
571
715
  }
716
+ function archRect(geo) {
717
+ if (geo.arch === void 0) {
718
+ return null;
719
+ }
720
+ const top = geo.picture.y + Math.round(geo.picture.h * geo.arch);
721
+ return { x: geo.picture.x, y: top, w: geo.picture.w, h: geo.picture.y + geo.picture.h - top };
722
+ }
723
+ function drawArch(ctx, r3, color) {
724
+ const rad = r3.w / 2;
725
+ ctx.fillStyle = color;
726
+ ctx.fillRect(r3.x, r3.y + rad, r3.w, r3.h - rad);
727
+ ctx.beginPath();
728
+ ctx.arc(r3.x + rad, r3.y + rad, rad, Math.PI, Math.PI * 2);
729
+ ctx.fill();
730
+ }
731
+ function flankFlows(ctx, groups, flanks, fonts) {
732
+ const half = Math.ceil(groups.length / 2);
733
+ return flanks.map((zone, i) => i === 0 ? flow(ctx, groups.slice(0, half), zone, fonts) : flow(ctx, groups.slice(half), { ...zone, right: true }, fonts));
734
+ }
735
+ function capAligned(zone, head) {
736
+ const first = head.texts[0]?.box;
737
+ const shift = first ? Math.min(Math.max(0, Math.round(first.y + first.h * CAP_TOP) - zone.y), Math.round(zone.h / 2)) : 0;
738
+ return { ...zone, y: zone.y + shift, h: zone.h - shift };
739
+ }
572
740
  function splitHeader(header, hasLogo) {
573
741
  const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
574
742
  return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
@@ -591,20 +759,25 @@ function render(input, assets, canvas) {
591
759
  ctx.fillRect(0, 0, width, height);
592
760
  if (doc.background === "tint" /* Tint */) {
593
761
  ctx.fillStyle = palette.tint;
594
- [geo.header, geo.body].forEach((r2) => ctx.fillRect(r2.x, r2.y, r2.w, r2.h));
762
+ [geo.header, geo.body].forEach((r3) => ctx.fillRect(r3.x, r3.y, r3.w, r3.h));
595
763
  }
596
- const zones = splitHeader(geo.header, doc.logo !== void 0);
764
+ const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
597
765
  const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
598
- const body = flow(ctx, bodyGroups(doc, palette, u), geo.body, fonts);
766
+ const flanks = geo.flanks ?? [];
767
+ const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
768
+ const sides = flanks.length > 0 ? flankFlows(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
599
769
  const radius = Math.min(width, height) * CARD_RADIUS;
600
- drawFlow(ctx, head, radius);
601
- drawFlow(ctx, body, radius);
770
+ [head, body, ...sides].forEach((f) => drawFlow(ctx, f, radius));
602
771
  const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
603
- if (logoImage) {
604
- const r2 = placeContain(logoImage, zones.logo, "top" /* Top */, 1, 1);
605
- ctx.drawImage(logoImage, r2.x, r2.y, r2.w, r2.h);
772
+ const logo = logoImage ? placeContain(logoImage, geo.logo ? zones.logo : capAligned(zones.logo, head), "top" /* Top */, 1, 1) : null;
773
+ if (logoImage && logo) {
774
+ ctx.drawImage(logoImage, logo.x, logo.y, logo.w, logo.h);
606
775
  }
607
776
  const image = doc.picture ? assets[doc.picture.imageId] : void 0;
777
+ const backdrop = image ? archRect(geo) : null;
778
+ if (backdrop) {
779
+ drawArch(ctx, backdrop, palette.arch);
780
+ }
608
781
  const picture = image && doc.picture ? placeContain(image, geo.picture, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
609
782
  if (image && picture) {
610
783
  ctx.drawImage(image, picture.x, picture.y, picture.w, picture.h);
@@ -614,8 +787,11 @@ function render(input, assets, canvas) {
614
787
  height,
615
788
  picture,
616
789
  pictureRegion: geo.picture,
617
- textBoxes: [...head.texts, ...body.texts].map((t) => t.box),
618
- cardBoxes: body.cards.map((c) => c.rect)
790
+ textBoxes: [head, body, ...sides].flatMap((f) => f.texts.map((t) => t.box)),
791
+ cardBoxes: body.cards.map((c) => c.rect),
792
+ iconBoxes: [head, body, ...sides].flatMap((f) => f.icons.map((i) => i.rect)),
793
+ logo,
794
+ backdrop
619
795
  };
620
796
  }
621
797
 
@@ -634,6 +810,30 @@ function exportPng(canvas) {
634
810
  return new Blob([bytes], { type: "image/png" });
635
811
  }
636
812
 
813
+ // src/layoutCatalogue.ts
814
+ var POSTERS = ["story" /* Story */, "post" /* Post */, "square" /* Square */];
815
+ var LAYOUT_CATALOGUE = [
816
+ { 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 },
817
+ { 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 },
818
+ { 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 },
819
+ { id: "picture-bottom" /* PictureBottom */, designId: "picture-bottom", label: "Picture bottom", description: "Titles and details on top, the picture across the bottom half.", formats: POSTERS },
820
+ { id: "list" /* List */, designId: "list", label: "Upcoming list", description: "Several dated events as a list beside the picture.", formats: POSTERS },
821
+ { 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 */] },
822
+ { 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 */] },
823
+ { 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 */] },
824
+ { 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 */] }
825
+ ];
826
+ var DEFAULTS = {
827
+ ["story" /* Story */]: "picture-right" /* PictureRight */,
828
+ ["post" /* Post */]: "picture-right" /* PictureRight */,
829
+ ["square" /* Square */]: "picture-right" /* PictureRight */,
830
+ ["phone" /* Phone */]: "phone-clock-safe" /* PhoneClockSafe */,
831
+ ["desktop" /* Desktop */]: "desktop-safe" /* DesktopSafe */
832
+ };
833
+ function defaultLayoutFor(format) {
834
+ return DEFAULTS[format];
835
+ }
836
+
637
837
  // src/examples/kizombaWorkshop.ts
638
838
  var WORKSHOPS = [
639
839
  { script: "Saturday 31 October", date: "Sat 31 Oct", first: "20:00 - 21:00", second: "21:00 - 22:00", party: "20\u20AC" },
@@ -685,6 +885,6 @@ var kizombaWorkshop = {
685
885
  };
686
886
  var kizombaWorkshopDocs = [...WORKSHOPS.map(workshopDoc), kizombaWorkshop];
687
887
 
688
- export { BackgroundStyle, FONT_SETS, FORMAT_SIZES, FlierDocError, FontSetId, FormatId, LayoutId, PALETTES, PaletteId, PictureAnchor, exportPng, flierDocSchema, kizombaWorkshop, kizombaWorkshopDocs, render, validateFlierDoc };
888
+ export { BackgroundStyle, FONT_SETS, FORMAT_SIZES, FlierDocError, FontSetId, FormatId, LAYOUT_CATALOGUE, LayoutId, PALETTES, PaletteId, PictureAnchor, defaultLayoutFor, exportPng, flierDocSchema, kizombaWorkshop, kizombaWorkshopDocs, render, validateFlierDoc };
689
889
  //# sourceMappingURL=index.mjs.map
690
890
  //# sourceMappingURL=index.mjs.map