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