@dloizides/poster-render 0.2.2 → 0.2.4

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
@@ -291,73 +291,6 @@ function missingAssets(doc, assets) {
291
291
  return errors;
292
292
  }
293
293
 
294
- // src/layout/blockGroups.ts
295
- var BOLD = 700;
296
- var SEMI = 600;
297
- var MEDIUM = 500;
298
- var SCRIPT = 400;
299
- var makers = (p, u) => ({
300
- display: (text2, size) => ({ text: text2, role: "display", weight: BOLD, size: size * u, color: p.title }),
301
- venue: (text2, size) => ({ text: text2, role: "display", weight: MEDIUM, size: size * u, color: p.ink }),
302
- script: (text2, size) => ({ text: text2, role: "script", weight: SCRIPT, size: size * u, color: p.accent }),
303
- strong: (text2, size) => ({ text: text2, role: "body", weight: BOLD, size: size * u, color: p.ink }),
304
- accent: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.accent }),
305
- item: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.ink, bullet: p.accent }),
306
- soft: (text2, size) => ({ text: text2, role: "body", weight: MEDIUM, size: size * u, color: p.soft })
307
- });
308
- function headerGroups(doc, p, u) {
309
- const l = makers(p, u);
310
- const b = doc.blocks;
311
- const lines = [l.display?.(b.title, 250), b.script ? l.script?.(b.script, 120) : void 0, b.titleBottom ? l.display?.(b.titleBottom, 250) : void 0];
312
- return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
313
- }
314
- function itemGroup(item, l, u) {
315
- const lines = [l.script?.(item.date, 67)];
316
- item.schedule.forEach((row) => lines.push(l.strong?.(`${row.label} ${row.time}`, 42)));
317
- if (item.price) {
318
- lines.push(l.accent?.(item.price, 42));
319
- }
320
- return { lines, gapAfter: 48 * u };
321
- }
322
- function eventGroups(doc, l, u, withPrices) {
323
- const b = doc.blocks;
324
- const groups = [];
325
- b.schedule?.forEach((row) => groups.push({ lines: [l.strong?.(row.label, 66), l.accent?.(row.time, 60)], 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 }));
328
- return groups;
329
- }
330
- function listGroups(doc, l, u) {
331
- const groups = (doc.items ?? []).map((item) => itemGroup(item, l, u));
332
- const prices = doc.blocks.prices?.map((row) => l.strong?.(`${row.amount} ${row.label}`, 58)) ?? [];
333
- if (prices.length > 0) {
334
- groups.push({ lines: prices, gapAfter: 30 * u });
335
- }
336
- return groups;
337
- }
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) {
343
- const l = makers(p, u);
344
- const b = doc.blocks;
345
- const groups = [];
346
- const isList = doc.layout === "list" /* List */;
347
- if (b.venue) {
348
- groups.push({ lines: b.venue.map((v) => l.venue?.(v, isList ? 48 : 58)), gapAfter: 40 * u, rule: { color: p.accent, height: 8 * u }, ruleBelow: true, icon: { color: p.accent, glyph: p.background } });
349
- }
350
- groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
351
- if (b.includes) {
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 });
354
- }
355
- if (b.footer) {
356
- groups.push({ lines: [l.soft?.(b.footer, 38)], gapAfter: 0 });
357
- }
358
- return groups;
359
- }
360
-
361
294
  // src/layout/layoutSpecs.ts
362
295
  var flanksBeside = (pic, top, bottom2, m) => [
363
296
  [m, top, pic[0] - 2 * m, bottom2 - top],
@@ -407,8 +340,11 @@ var toRect = (f, w, h) => {
407
340
  var MARGIN = 0.045;
408
341
  var HEADER_PORTRAIT = 0.3;
409
342
  var HEADER_LANDSCAPE = 0.36;
410
- var BOTTOM_HEADER = 0.2;
411
- var BOTTOM_PICTURE = 0.52;
343
+ var BOTTOM_PICTURE_W = 0.8;
344
+ var BOTTOM_MIN_TOP = 0.25;
345
+ var BOTTOM_HEADER_SHARE = 0.3;
346
+ var BOTTOM_GAP = 40 / 1080;
347
+ var BOTTOM_ASPECT = 0.57;
412
348
  var PICTURE_SIDE = {
413
349
  ["picture-right" /* PictureRight */]: [0.6, 0.42],
414
350
  ["picture-left" /* PictureLeft */]: [0.6, 0.42],
@@ -437,14 +373,17 @@ function side(w, h, fraction) {
437
373
  alignX: 1
438
374
  };
439
375
  }
440
- function bottom(w, h) {
376
+ function bottom(w, h, share) {
441
377
  const m = r2(Math.min(w, h) * MARGIN);
442
- const top = r2(h * BOTTOM_HEADER);
443
- const pic = r2(h * BOTTOM_PICTURE);
378
+ const gap = r2(Math.min(w, h) * BOTTOM_GAP);
379
+ const picW = r2(w * share);
380
+ const picH = Math.min(r2(picW / BOTTOM_ASPECT), r2((h - r2(h * BOTTOM_MIN_TOP)) * share / BOTTOM_PICTURE_W));
381
+ const pic = h - picH;
382
+ const top = m + r2((pic - m) * BOTTOM_HEADER_SHARE);
444
383
  return {
445
- header: { x: m, y: m, w: w - 2 * m, h: top - 2 * m },
446
- body: { x: m, y: top, w: w - 2 * m, h: pic - top - m },
447
- picture: { x: 0, y: pic, w, h: h - pic },
384
+ header: { x: m, y: m, w: w - 2 * m, h: top - m },
385
+ body: { x: m, y: top, w: w - 2 * m, h: pic - gap - top },
386
+ picture: { x: r2((w - picW) / 2), y: pic, w: picW, h: picH },
448
387
  alignX: 0.5
449
388
  };
450
389
  }
@@ -460,13 +399,13 @@ function fromSpec(spec, w, h) {
460
399
  };
461
400
  }
462
401
  var mirror = (rect, w) => ({ ...rect, x: w - rect.x - rect.w });
463
- function geometry(layout, w, h) {
402
+ function geometry(layout, w, h, give = 0) {
464
403
  const oriented = LAYOUT_SPECS[layout];
465
404
  if (oriented) {
466
405
  return fromSpec(h >= w ? oriented.portrait : oriented.landscape, w, h);
467
406
  }
468
407
  if (layout === "picture-bottom" /* PictureBottom */) {
469
- return bottom(w, h);
408
+ return bottom(w, h, BOTTOM_PICTURE_W - give);
470
409
  }
471
410
  const g = side(w, h, PICTURE_SIDE[layout] ?? [0.6, 0.42]);
472
411
  if (layout !== "picture-left" /* PictureLeft */) {
@@ -486,6 +425,73 @@ function placeContain(image, region, anchor, alignX, scale) {
486
425
  };
487
426
  }
488
427
 
428
+ // src/layout/blockGroups.ts
429
+ var BOLD = 700;
430
+ var SEMI = 600;
431
+ var MEDIUM = 500;
432
+ var SCRIPT = 400;
433
+ var makers = (p, u) => ({
434
+ display: (text2, size) => ({ text: text2, role: "display", weight: BOLD, size: size * u, color: p.title }),
435
+ venue: (text2, size) => ({ text: text2, role: "display", weight: MEDIUM, size: size * u, color: p.ink }),
436
+ script: (text2, size) => ({ text: text2, role: "script", weight: SCRIPT, size: size * u, color: p.accent }),
437
+ strong: (text2, size) => ({ text: text2, role: "body", weight: BOLD, size: size * u, color: p.ink }),
438
+ accent: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.accent }),
439
+ item: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.ink, bullet: p.accent }),
440
+ soft: (text2, size) => ({ text: text2, role: "body", weight: MEDIUM, size: size * u, color: p.soft })
441
+ });
442
+ function headerGroups(doc, p, u) {
443
+ const l = makers(p, u);
444
+ const b = doc.blocks;
445
+ const lines = [l.display?.(b.title, 250), b.script ? l.script?.(b.script, 120) : void 0, b.titleBottom ? l.display?.(b.titleBottom, 250) : void 0];
446
+ return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
447
+ }
448
+ function itemGroup(item, l, u) {
449
+ const lines = [l.script?.(item.date, 67)];
450
+ item.schedule.forEach((row) => lines.push(l.strong?.(`${row.label} ${row.time}`, 42)));
451
+ if (item.price) {
452
+ lines.push(l.accent?.(item.price, 42));
453
+ }
454
+ return { lines, gapAfter: 48 * u };
455
+ }
456
+ function eventGroups(doc, l, u, withPrices) {
457
+ const b = doc.blocks;
458
+ const groups = [];
459
+ b.schedule?.forEach((row) => groups.push({ lines: [l.strong?.(row.label, 66), l.accent?.(row.time, 60)], gapAfter: 30 * u }));
460
+ const prices = (withPrices ? b.prices : void 0) ?? [];
461
+ 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 }));
462
+ return groups;
463
+ }
464
+ function listGroups(doc, l, u) {
465
+ const groups = (doc.items ?? []).map((item) => itemGroup(item, l, u));
466
+ const prices = doc.blocks.prices?.map((row) => l.strong?.(`${row.amount} ${row.label}`, 58)) ?? [];
467
+ if (prices.length > 0) {
468
+ groups.push({ lines: prices, gapAfter: 30 * u });
469
+ }
470
+ return groups;
471
+ }
472
+ function flankPriceGroups(doc, p, u) {
473
+ const l = makers(p, u);
474
+ return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong?.(row.amount, 110), l.accent?.(row.label, 48)], gapAfter: 30 * u }));
475
+ }
476
+ function bodyGroups(doc, p, u, withPrices = true) {
477
+ const l = makers(p, u);
478
+ const b = doc.blocks;
479
+ const groups = [];
480
+ const isList = doc.layout === "list" /* List */;
481
+ if (b.venue) {
482
+ groups.push({ lines: b.venue.map((v) => l.venue?.(v, isList ? 48 : 58)), gapAfter: 40 * u, rule: { color: p.accent, height: 8 * u }, ruleBelow: true, icon: { color: p.accent, glyph: p.background } });
483
+ }
484
+ groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
485
+ if (b.includes) {
486
+ const lines = [l.accent?.(b.includes.heading, 38), ...b.includes.items.map((i) => l.item?.(i, 38))];
487
+ groups.push({ lines, gapAfter: 24 * u, uniform: true });
488
+ }
489
+ if (b.footer) {
490
+ groups.push({ lines: [l.soft?.(b.footer, 38)], gapAfter: 0 });
491
+ }
492
+ return groups;
493
+ }
494
+
489
495
  // src/layout/textFlow.ts
490
496
  var LINE_HEIGHT = 1.2;
491
497
  var BULLET_INDENT = 1.1;
@@ -493,6 +499,8 @@ var BULLET_RADIUS = 0.2;
493
499
  var RULE_GAP = 2.5;
494
500
  var RULE_WIDTH = 0.9;
495
501
  var WIDE = 1.6;
502
+ var TWO_COLUMN_MIN = 0.75;
503
+ var TWO_COLUMN_GAIN = 1.15;
496
504
  var MIN_K = 0.02;
497
505
  var STEPS = 22;
498
506
  var ICON_SCALE = 1.6;
@@ -588,9 +596,6 @@ function keepTogether(groups, heights, cut) {
588
596
  return candidates.reduce((best, c) => tallest(heights, c) < tallest(heights, best) ? c : best);
589
597
  }
590
598
  function splitColumns(groups, zone) {
591
- if (zone.w / Math.max(zone.h, 1) <= WIDE || groups.length < 2) {
592
- return [{ col: zone, groups }];
593
- }
594
599
  const gap = Math.round(zone.w * 0.04);
595
600
  const w = (zone.w - gap) / 2;
596
601
  const heights = groups.map((g) => placeGroup(g, 1, { ...zone, w }, 0, null));
@@ -607,16 +612,8 @@ function splitColumns(groups, zone) {
607
612
  { col: { ...zone, x: zone.x + w + gap, w }, groups: groups.slice(cut) }
608
613
  ];
609
614
  }
610
- function flow(ctx, groups, zone, fonts) {
611
- const measured = groups.map((group) => ({
612
- group,
613
- widths: group.lines.map((line) => {
614
- ctx.font = fontOf(line, line.size, fonts);
615
- return ctx.measureText(line.text).width;
616
- })
617
- }));
618
- const columns = splitColumns(measured, zone);
619
- const fits = (k) => columns.every((c) => placeColumn(c.groups, k, c.col, null) <= zone.h);
615
+ function largestK(columns, zoneH) {
616
+ const fits = (k) => columns.every((c) => placeColumn(c.groups, k, c.col, null) <= zoneH);
620
617
  let lo = MIN_K;
621
618
  let hi = 1;
622
619
  if (fits(1)) {
@@ -630,6 +627,28 @@ function flow(ctx, groups, zone, fonts) {
630
627
  hi = mid;
631
628
  }
632
629
  }
630
+ return lo;
631
+ }
632
+ function bestColumns(measured, zone) {
633
+ const single = [{ col: zone, groups: measured }];
634
+ const k1 = largestK(single, zone.h);
635
+ if (measured.length < 2 || zone.w / Math.max(zone.h, 1) < TWO_COLUMN_MIN) {
636
+ return { columns: single, k: k1 };
637
+ }
638
+ const split = splitColumns(measured, zone);
639
+ const k2 = largestK(split, zone.h);
640
+ const forced = zone.w / Math.max(zone.h, 1) > WIDE;
641
+ return forced || k2 > k1 * TWO_COLUMN_GAIN ? { columns: split, k: k2 } : { columns: single, k: k1 };
642
+ }
643
+ function flow(ctx, groups, zone, fonts) {
644
+ const measured = groups.map((group) => ({
645
+ group,
646
+ widths: group.lines.map((line) => {
647
+ ctx.font = fontOf(line, line.size, fonts);
648
+ return ctx.measureText(line.text).width;
649
+ })
650
+ }));
651
+ const { columns, k: lo } = bestColumns(measured, zone);
633
652
  const out = emptyFlow();
634
653
  const heights = columns.map((c) => placeColumn(c.groups, lo, c.col, null));
635
654
  const end = Math.max(...heights);
@@ -645,26 +664,25 @@ function flow(ctx, groups, zone, fonts) {
645
664
  // src/render/regions.ts
646
665
  var LINE_HEIGHT2 = 1.2;
647
666
  var BODY_GAP = 40;
648
- var BOTTOM_SIDE = 0.1;
649
667
  function flowFlanks(ctx, groups, flanks, fonts) {
650
668
  const half = Math.ceil(groups.length / 2);
651
669
  return flanks.map((zone, i) => i === 0 ? flow(ctx, groups.slice(0, half), zone, fonts) : flow(ctx, groups.slice(half), { ...zone, right: true }, fonts));
652
670
  }
653
671
  function smallestPerLine(groups, results) {
654
672
  const sizes = results.flatMap((r3) => r3.texts.map((t) => t.box.h / LINE_HEIGHT2));
655
- const smallest = [];
673
+ const smallest2 = [];
656
674
  let i = 0;
657
675
  groups.forEach(
658
676
  (g) => g.lines.forEach((_line, j) => {
659
- smallest[j] = Math.min(smallest[j] ?? Infinity, sizes[i] ?? Infinity);
677
+ smallest2[j] = Math.min(smallest2[j] ?? Infinity, sizes[i] ?? Infinity);
660
678
  i++;
661
679
  })
662
680
  );
663
- return smallest;
681
+ return smallest2;
664
682
  }
665
683
  function equalFlanks(ctx, groups, flanks, fonts) {
666
- const smallest = smallestPerLine(groups, flowFlanks(ctx, groups, flanks, fonts));
667
- const sized = groups.map((g) => ({ ...g, lines: g.lines.map((line, j) => ({ ...line, size: Math.min(line.size, smallest[j] ?? line.size) })) }));
684
+ const smallest2 = smallestPerLine(groups, flowFlanks(ctx, groups, flanks, fonts));
685
+ const sized = groups.map((g) => ({ ...g, lines: g.lines.map((line, j) => ({ ...line, size: Math.min(line.size, smallest2[j] ?? line.size) })) }));
668
686
  return flowFlanks(ctx, sized, flanks, fonts);
669
687
  }
670
688
  function measuredEnd(f, fallback) {
@@ -675,14 +693,76 @@ function pictureRegion(layout, geo, body, u, frame) {
675
693
  if (layout !== "picture-bottom" /* PictureBottom */) {
676
694
  return geo.picture;
677
695
  }
678
- const y = Math.ceil(measuredEnd(body, geo.body.y) + BODY_GAP * u);
679
- const x = Math.round(frame.width * BOTTOM_SIDE);
680
- return { x, y, w: frame.width - 2 * x, h: frame.height - y };
696
+ const y = Math.min(geo.picture.y, Math.ceil(measuredEnd(body, geo.body.y) + BODY_GAP * u));
697
+ return { x: geo.picture.x, y, w: geo.picture.w, h: frame.height - y };
681
698
  }
682
699
 
683
- // src/render/render.ts
700
+ // src/render/yieldToText.ts
701
+ function yieldToText(geo, give) {
702
+ const { picture: p, body: b } = geo;
703
+ if (give <= 0) {
704
+ return geo;
705
+ }
706
+ if (p.x >= b.x + b.w) {
707
+ const d = Math.round(p.w * give);
708
+ return { ...geo, picture: { ...p, x: p.x + d, w: p.w - d }, body: { ...b, w: b.w + d } };
709
+ }
710
+ if (p.x + p.w <= b.x) {
711
+ const d = Math.round(p.w * give);
712
+ return { ...geo, picture: { ...p, w: p.w - d }, body: { ...b, x: b.x - d, w: b.w + d } };
713
+ }
714
+ if (p.y >= b.y + b.h) {
715
+ const d = Math.round(p.h * give);
716
+ const stacked = geo.header.y + geo.header.h <= b.y;
717
+ const toHeader = stacked ? Math.round(d / 2) : 0;
718
+ return {
719
+ ...geo,
720
+ header: { ...geo.header, h: geo.header.h + toHeader },
721
+ picture: { ...p, y: p.y + d, h: p.h - d },
722
+ body: { ...b, y: b.y + toHeader, h: b.h + d - toHeader },
723
+ flanks: geo.flanks?.map((f) => ({ ...f, y: f.y + d, h: f.h - d }))
724
+ };
725
+ }
726
+ return geo;
727
+ }
728
+
729
+ // src/render/fitToFloor.ts
730
+ var FLOOR_PX = 30;
684
731
  var BASE = 1080;
732
+ var LINE_HEIGHT3 = 1.2;
685
733
  var LOGO_SHARE = 0.26;
734
+ var GIVE_STEP = 0.025;
735
+ var GIVE_STEPS = 32;
736
+ function splitHeader(header, hasLogo) {
737
+ const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
738
+ return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
739
+ }
740
+ function flowAll(ctx, input, geo, u) {
741
+ const { doc, palette, fonts } = input;
742
+ const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
743
+ const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
744
+ const flanks = geo.flanks ?? [];
745
+ const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
746
+ const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
747
+ return { geo, zones, head, body, sides };
748
+ }
749
+ var smallest = (t) => Math.min(Infinity, ...[t.head, t.body, ...t.sides].flatMap((f) => f.texts.map((x) => x.box.h / LINE_HEIGHT3)));
750
+ function geometryWithGive(layout, width, height, give) {
751
+ return layout === "picture-bottom" /* PictureBottom */ ? geometry(layout, width, height, give) : yieldToText(geometry(layout, width, height), give);
752
+ }
753
+ function fitToFloor(ctx, input) {
754
+ const { doc, width, height } = input;
755
+ const u = Math.min(width, height) / BASE;
756
+ const floor = FLOOR_PX * u;
757
+ let fitted = flowAll(ctx, input, geometryWithGive(doc.layout, width, height, 0), u);
758
+ for (let step = 1; step <= GIVE_STEPS && smallest(fitted) < floor; step++) {
759
+ fitted = flowAll(ctx, input, geometryWithGive(doc.layout, width, height, step * GIVE_STEP), u);
760
+ }
761
+ return fitted;
762
+ }
763
+
764
+ // src/render/render.ts
765
+ var BASE2 = 1080;
686
766
  var CARD_RADIUS = 0.035;
687
767
  var CAP_TOP = 0.15;
688
768
  var PIN_HEAD_Y = 0.08;
@@ -777,10 +857,6 @@ function capAligned(zone, head) {
777
857
  const shift = first ? Math.min(Math.max(0, Math.round(first.y + first.h * CAP_TOP) - zone.y), Math.round(zone.h / 2)) : 0;
778
858
  return { ...zone, y: zone.y + shift, h: zone.h - shift };
779
859
  }
780
- function splitHeader(header, hasLogo) {
781
- const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
782
- return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
783
- }
784
860
  function render(input, assets, canvas) {
785
861
  const doc = checked(input, assets);
786
862
  const { width, height } = FORMAT_SIZES[doc.format];
@@ -792,8 +868,8 @@ function render(input, assets, canvas) {
792
868
  }
793
869
  const palette = PALETTES[doc.palette];
794
870
  const fonts = FONT_SETS[doc.fonts];
795
- const geo = geometry(doc.layout, width, height);
796
- const u = Math.min(width, height) / BASE;
871
+ const u = Math.min(width, height) / BASE2;
872
+ const { geo, zones, head, body, sides } = fitToFloor(ctx, { doc, palette, fonts, width, height });
797
873
  ctx.globalAlpha = 1;
798
874
  ctx.fillStyle = palette.background;
799
875
  ctx.fillRect(0, 0, width, height);
@@ -801,11 +877,6 @@ function render(input, assets, canvas) {
801
877
  ctx.fillStyle = palette.tint;
802
878
  [geo.header, geo.body].forEach((r3) => ctx.fillRect(r3.x, r3.y, r3.w, r3.h));
803
879
  }
804
- const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
805
- const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
806
- const flanks = geo.flanks ?? [];
807
- const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
808
- const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
809
880
  const radius = Math.min(width, height) * CARD_RADIUS;
810
881
  [head, body, ...sides].forEach((f) => drawFlow(ctx, f, radius));
811
882
  const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
@@ -818,7 +889,7 @@ function render(input, assets, canvas) {
818
889
  if (backdrop) {
819
890
  drawArch(ctx, backdrop, palette.arch);
820
891
  }
821
- const region = pictureRegion(doc.layout, geo, body, u, { width, height });
892
+ const region = pictureRegion(doc.layout, geo, body, u, { height });
822
893
  const picture = image && doc.picture ? placeContain(image, region, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
823
894
  if (image && picture) {
824
895
  ctx.drawImage(image, picture.x, picture.y, picture.w, picture.h);