@dloizides/poster-render 0.2.4 → 0.2.6

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
@@ -224,7 +224,7 @@ function checkObject(schema, value, path, errors) {
224
224
  }
225
225
  }
226
226
  for (const [key, child] of Object.entries(value)) {
227
- const childSchema = props[key];
227
+ const childSchema = Object.hasOwn(props, key) ? props[key] : void 0;
228
228
  if (childSchema === void 0) {
229
229
  if (schema.additionalProperties === false) {
230
230
  errors.push(`${join(path, key)}: is not allowed`);
@@ -282,10 +282,10 @@ function validateFlierDoc(input) {
282
282
  }
283
283
  function missingAssets(doc, assets) {
284
284
  const errors = [];
285
- if (doc.picture && assets[doc.picture.imageId] === void 0) {
285
+ if (doc.picture && !Object.hasOwn(assets, doc.picture.imageId)) {
286
286
  errors.push(`picture.imageId: no asset named "${doc.picture.imageId}"`);
287
287
  }
288
- if (doc.logo && assets[doc.logo.imageId] === void 0) {
288
+ if (doc.logo && !Object.hasOwn(assets, doc.logo.imageId)) {
289
289
  errors.push(`logo.imageId: no asset named "${doc.logo.imageId}"`);
290
290
  }
291
291
  return errors;
@@ -345,11 +345,15 @@ var BOTTOM_MIN_TOP = 0.25;
345
345
  var BOTTOM_HEADER_SHARE = 0.3;
346
346
  var BOTTOM_GAP = 40 / 1080;
347
347
  var BOTTOM_ASPECT = 0.57;
348
+ var DEFAULT_SIDE = [0.6, 0.42];
348
349
  var PICTURE_SIDE = {
349
- ["picture-right" /* PictureRight */]: [0.6, 0.42],
350
- ["picture-left" /* PictureLeft */]: [0.6, 0.42],
350
+ ["picture-right" /* PictureRight */]: DEFAULT_SIDE,
351
+ ["picture-left" /* PictureLeft */]: DEFAULT_SIDE,
351
352
  ["list" /* List */]: [0.5, 0.36]
352
353
  };
354
+ var SHORT_PORTRAIT = 1.5;
355
+ var SHORT_PICTURE_W = 0.5;
356
+ var SHORT_PICTURE_H = 0.75;
353
357
  var ANCHOR_Y = { ["top" /* Top */]: 0, ["center" /* Center */]: 0.5, ["bottom" /* Bottom */]: 1 };
354
358
  var r2 = Math.round;
355
359
  function side(w, h, fraction) {
@@ -373,6 +377,20 @@ function side(w, h, fraction) {
373
377
  alignX: 1
374
378
  };
375
379
  }
380
+ function shortSide(w, h) {
381
+ const m = r2(Math.min(w, h) * MARGIN);
382
+ const picW = r2(w * SHORT_PICTURE_W);
383
+ const picH = r2(h * SHORT_PICTURE_H);
384
+ const top = r2(h * HEADER_PORTRAIT);
385
+ const textW = w - picW - 2 * m;
386
+ return {
387
+ header: { x: m, y: m, w: textW, h: top - 2 * m },
388
+ body: { x: m, y: top, w: textW, h: h - top - m },
389
+ picture: { x: w - picW, y: h - picH, w: picW, h: picH },
390
+ logo: { x: w - picW + m, y: m, w: picW - 2 * m, h: h - picH - 2 * m },
391
+ alignX: 1
392
+ };
393
+ }
376
394
  function bottom(w, h, share) {
377
395
  const m = r2(Math.min(w, h) * MARGIN);
378
396
  const gap = r2(Math.min(w, h) * BOTTOM_GAP);
@@ -407,13 +425,15 @@ function geometry(layout, w, h, give = 0) {
407
425
  if (layout === "picture-bottom" /* PictureBottom */) {
408
426
  return bottom(w, h, BOTTOM_PICTURE_W - give);
409
427
  }
410
- const g = side(w, h, PICTURE_SIDE[layout] ?? [0.6, 0.42]);
428
+ const isSide = layout === "picture-right" /* PictureRight */ || layout === "picture-left" /* PictureLeft */;
429
+ const g = isSide && h >= w && h < w * SHORT_PORTRAIT ? shortSide(w, h) : side(w, h, PICTURE_SIDE[layout] ?? DEFAULT_SIDE);
411
430
  if (layout !== "picture-left" /* PictureLeft */) {
412
431
  return g;
413
432
  }
414
- return { header: mirror(g.header, w), body: mirror(g.body, w), picture: mirror(g.picture, w), alignX: 1 - g.alignX };
433
+ return { header: mirror(g.header, w), body: mirror(g.body, w), picture: mirror(g.picture, w), logo: g.logo ? mirror(g.logo, w) : void 0, alignX: 1 - g.alignX };
415
434
  }
416
- function placeContain(image, region, anchor, alignX, scale) {
435
+ function placeContain(image, region, fit) {
436
+ const { anchor, alignX, scale } = fit;
417
437
  const s = Math.min(region.w / image.width, region.h / image.height) * scale;
418
438
  const w = Math.floor(image.width * s);
419
439
  const h = Math.floor(image.height * s);
@@ -430,6 +450,29 @@ var BOLD = 700;
430
450
  var SEMI = 600;
431
451
  var MEDIUM = 500;
432
452
  var SCRIPT = 400;
453
+ var SIZE = {
454
+ title: 250,
455
+ headerScript: 120,
456
+ itemDate: 67,
457
+ itemRow: 42,
458
+ scheduleLabel: 66,
459
+ scheduleTime: 60,
460
+ priceAmount: 66,
461
+ priceLabel: 52,
462
+ listPrice: 58,
463
+ flankAmount: 110,
464
+ flankLabel: 48,
465
+ venue: 58,
466
+ listVenue: 48,
467
+ small: 38
468
+ };
469
+ var GAP = {
470
+ item: 48,
471
+ row: 30,
472
+ venue: 40,
473
+ includes: 24,
474
+ rule: 8
475
+ };
433
476
  var makers = (p, u) => ({
434
477
  display: (text2, size) => ({ text: text2, role: "display", weight: BOLD, size: size * u, color: p.title }),
435
478
  venue: (text2, size) => ({ text: text2, role: "display", weight: MEDIUM, size: size * u, color: p.ink }),
@@ -442,52 +485,75 @@ var makers = (p, u) => ({
442
485
  function headerGroups(doc, p, u) {
443
486
  const l = makers(p, u);
444
487
  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];
488
+ const lines = [l.display(b.title, SIZE.title), b.script ? l.script(b.script, SIZE.headerScript) : void 0, b.titleBottom ? l.display(b.titleBottom, SIZE.title) : void 0];
446
489
  return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
447
490
  }
448
491
  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)));
492
+ const lines = [l.script(item.date, SIZE.itemDate)];
493
+ item.schedule.forEach((row) => lines.push(l.strong(`${row.label} ${row.time}`, SIZE.itemRow)));
451
494
  if (item.price) {
452
- lines.push(l.accent?.(item.price, 42));
495
+ lines.push(l.accent(item.price, SIZE.itemRow));
496
+ }
497
+ return { lines, gapAfter: GAP.item * u };
498
+ }
499
+ var WRAP_FROM = 14;
500
+ function halves(text2) {
501
+ const mid = text2.length / 2;
502
+ const spaces = [...text2].flatMap((c, i) => c === " " ? [i] : []);
503
+ if (text2.length < WRAP_FROM || spaces.length === 0) {
504
+ return [text2];
453
505
  }
454
- return { lines, gapAfter: 48 * u };
506
+ const cut = spaces.reduce((best, i) => Math.abs(i - mid) < Math.abs(best - mid) ? i : best);
507
+ return [text2.slice(0, cut), text2.slice(cut + 1)];
455
508
  }
456
- function eventGroups(doc, l, u, withPrices) {
509
+ function wrapped(line, wrap) {
510
+ const parts = wrap ? halves(line.text) : [line.text];
511
+ return parts.map((text2, i) => i === 0 ? { ...line, text: text2 } : { ...line, text: text2, bullet: void 0, hang: line.bullet !== void 0 });
512
+ }
513
+ function eventGroups(doc, l, u, options) {
457
514
  const b = doc.blocks;
458
515
  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 }));
516
+ const schedule2 = b.schedule ?? [];
517
+ schedule2.forEach((row, i) => {
518
+ const lines = options.compact ? [l.strong(`${row.label} ${row.time}`, SIZE.scheduleLabel)] : [l.strong(row.label, SIZE.scheduleLabel), l.accent(row.time, SIZE.scheduleTime)];
519
+ groups.push({ lines, gapAfter: GAP.row * u, keepWithNext: i < schedule2.length - 1 });
520
+ });
521
+ const prices = (options.withPrices === false ? void 0 : b.prices) ?? [];
522
+ prices.forEach((row, i) => {
523
+ const lines = options.compact ? [l.strong(`${row.amount} ${row.label}`, SIZE.priceAmount)] : [l.strong(row.amount, SIZE.priceAmount), l.accent(row.label, SIZE.priceLabel)];
524
+ groups.push({ lines, gapAfter: GAP.row * u, keepWithNext: i < prices.length - 1 });
525
+ });
462
526
  return groups;
463
527
  }
464
528
  function listGroups(doc, l, u) {
465
529
  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)) ?? [];
530
+ const prices = doc.blocks.prices?.map((row) => l.strong(`${row.amount} ${row.label}`, SIZE.listPrice)) ?? [];
467
531
  if (prices.length > 0) {
468
- groups.push({ lines: prices, gapAfter: 30 * u });
532
+ groups.push({ lines: prices, gapAfter: GAP.row * u });
469
533
  }
470
534
  return groups;
471
535
  }
472
536
  function flankPriceGroups(doc, p, u) {
473
537
  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 }));
538
+ return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong(row.amount, SIZE.flankAmount), l.accent(row.label, SIZE.flankLabel)], gapAfter: GAP.row * u }));
475
539
  }
476
- function bodyGroups(doc, p, u, withPrices = true) {
540
+ function bodyGroups(doc, p, u, options = {}) {
477
541
  const l = makers(p, u);
478
542
  const b = doc.blocks;
479
543
  const groups = [];
480
544
  const isList = doc.layout === "list" /* List */;
481
545
  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 } });
546
+ const size = isList ? SIZE.listVenue : SIZE.venue;
547
+ const keepWithNext = !isList && (b.schedule?.length ?? 0) > 0;
548
+ groups.push({ lines: b.venue.map((v) => l.venue(v, size)), gapAfter: GAP.venue * u, rule: { color: p.accent, height: GAP.rule * u }, ruleBelow: true, icon: { color: p.accent, glyph: p.background }, keepWithNext });
483
549
  }
484
- groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
550
+ groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, options));
485
551
  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 });
552
+ const lines = [l.accent(b.includes.heading, SIZE.small), ...b.includes.items.map((i) => l.item(i, SIZE.small))].flatMap((line) => wrapped(line, options.wrap));
553
+ groups.push({ lines, gapAfter: GAP.includes * u, uniform: true });
488
554
  }
489
555
  if (b.footer) {
490
- groups.push({ lines: [l.soft?.(b.footer, 38)], gapAfter: 0 });
556
+ groups.push({ lines: wrapped(l.soft(b.footer, SIZE.small), options.wrap), gapAfter: 0 });
491
557
  }
492
558
  return groups;
493
559
  }
@@ -499,6 +565,7 @@ var BULLET_RADIUS = 0.2;
499
565
  var RULE_GAP = 2.5;
500
566
  var RULE_WIDTH = 0.9;
501
567
  var WIDE = 1.6;
568
+ var COLUMN_GAP = 0.04;
502
569
  var TWO_COLUMN_MIN = 0.75;
503
570
  var TWO_COLUMN_GAIN = 1.15;
504
571
  var MIN_K = 0.02;
@@ -507,12 +574,13 @@ var ICON_SCALE = 1.6;
507
574
  var ICON_GAP = 0.35;
508
575
  var fontOf = (line, size, fonts) => `${line.weight} ${size}px ${fonts[line.role]}`;
509
576
  var emptyFlow = () => ({ texts: [], cards: [], rules: [], bullets: [], icons: [] });
577
+ var indented = (line) => line.bullet !== void 0 || line.hang === true;
510
578
  var iconSize = (group, k) => {
511
579
  const first = group.lines[0];
512
580
  return group.icon && first ? first.size * k * LINE_HEIGHT * ICON_SCALE : 0;
513
581
  };
514
582
  function lineSize(line, width, k, avail) {
515
- const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
583
+ const indent = indented(line) ? line.size * k * BULLET_INDENT : 0;
516
584
  return Math.max(0, Math.min(line.size * k, line.size * (avail - indent) / Math.max(width, 1)));
517
585
  }
518
586
  function groupRatio(m, k, avail) {
@@ -521,15 +589,17 @@ function groupRatio(m, k, avail) {
521
589
  }
522
590
  return Math.min(...m.group.lines.map((line, i) => lineSize(line, m.widths[i] ?? 1, k, avail) / line.size));
523
591
  }
524
- function placeIcon(group, d, x, textTop, textBottom, out) {
592
+ function placeIcon(group, box, out) {
593
+ const { d, x, top, bottom: bottom2 } = box;
525
594
  if (!group.icon || d <= 0) {
526
- return textBottom;
595
+ return bottom2;
527
596
  }
528
- const textH = textBottom - textTop;
529
- 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 });
530
- return textTop + Math.max(textH, d);
597
+ const textH = bottom2 - top;
598
+ out?.icons.push({ rect: { x, y: top + Math.max(0, (textH - d) / 2), w: d, h: d }, color: group.icon.color, glyph: group.icon.glyph });
599
+ return top + Math.max(textH, d);
531
600
  }
532
- function placeGroup(m, k, col, y0, out) {
601
+ function placeGroup(m, at, out) {
602
+ const { k, col, y0 } = at;
533
603
  const { group } = m;
534
604
  let y = y0;
535
605
  if (group.rule) {
@@ -545,7 +615,7 @@ function placeGroup(m, k, col, y0, out) {
545
615
  const ratio = groupRatio(m, k, avail);
546
616
  group.lines.forEach((line, i) => {
547
617
  const size = ratio === null ? lineSize(line, m.widths[i] ?? 1, k, avail) : line.size * ratio;
548
- const indent = (line.bullet ? line.size * k * BULLET_INDENT : 0) + iconIndent;
618
+ const indent = (indented(line) ? line.size * k * BULLET_INDENT : 0) + iconIndent;
549
619
  const w = (m.widths[i] ?? 0) * size / line.size;
550
620
  const x = col.right ? col.x + col.w - pad - w : col.x + pad + indent;
551
621
  out?.texts.push({ text: line.text, font: "", color: line.color, box: { x, y, w, h: size * LINE_HEIGHT } });
@@ -554,7 +624,7 @@ function placeGroup(m, k, col, y0, out) {
554
624
  }
555
625
  y += size * LINE_HEIGHT;
556
626
  });
557
- y = placeIcon(group, d, col.x + pad, top + pad, y, out);
627
+ y = placeIcon(group, { d, x: col.x + pad, top: top + pad, bottom: y }, out);
558
628
  y += pad;
559
629
  if (group.rule && group.ruleBelow) {
560
630
  y += group.rule.height * k * (RULE_GAP - 1);
@@ -566,10 +636,11 @@ function placeGroup(m, k, col, y0, out) {
566
636
  }
567
637
  return y + group.gapAfter * k - y0;
568
638
  }
569
- function placeColumn(groups, k, col, out, spread = 0) {
639
+ function placeColumn(groups, at, out) {
640
+ const { k, col, spread = 0 } = at;
570
641
  let h = 0;
571
642
  for (const g of groups) {
572
- h += placeGroup(g, k, col, col.y + h, out) + spread;
643
+ h += placeGroup(g, { k, col, y0: col.y + h }, out) + spread;
573
644
  }
574
645
  return h - spread;
575
646
  }
@@ -596,9 +667,9 @@ function keepTogether(groups, heights, cut) {
596
667
  return candidates.reduce((best, c) => tallest(heights, c) < tallest(heights, best) ? c : best);
597
668
  }
598
669
  function splitColumns(groups, zone) {
599
- const gap = Math.round(zone.w * 0.04);
670
+ const gap = Math.round(zone.w * COLUMN_GAP);
600
671
  const w = (zone.w - gap) / 2;
601
- const heights = groups.map((g) => placeGroup(g, 1, { ...zone, w }, 0, null));
672
+ const heights = groups.map((g) => placeGroup(g, { k: 1, col: { ...zone, w }, y0: 0 }, null));
602
673
  const total = columnSum(heights, 0, heights.length);
603
674
  let cut = 1;
604
675
  for (let c = 2; c < groups.length; c++) {
@@ -613,7 +684,7 @@ function splitColumns(groups, zone) {
613
684
  ];
614
685
  }
615
686
  function largestK(columns, zoneH) {
616
- const fits = (k) => columns.every((c) => placeColumn(c.groups, k, c.col, null) <= zoneH);
687
+ const fits = (k) => columns.every((c) => placeColumn(c.groups, { k, col: c.col }, null) <= zoneH);
617
688
  let lo = MIN_K;
618
689
  let hi = 1;
619
690
  if (fits(1)) {
@@ -650,9 +721,9 @@ function flow(ctx, groups, zone, fonts) {
650
721
  }));
651
722
  const { columns, k: lo } = bestColumns(measured, zone);
652
723
  const out = emptyFlow();
653
- const heights = columns.map((c) => placeColumn(c.groups, lo, c.col, null));
724
+ const heights = columns.map((c) => placeColumn(c.groups, { k: lo, col: c.col }, null));
654
725
  const end = Math.max(...heights);
655
- columns.forEach((c, i) => placeColumn(c.groups, lo, c.col, out, c.groups.length > 1 && columns.length > 1 ? (end - (heights[i] ?? end)) / (c.groups.length - 1) : 0));
726
+ columns.forEach((c, i) => placeColumn(c.groups, { k: lo, col: c.col, spread: c.groups.length > 1 && columns.length > 1 ? (end - (heights[i] ?? end)) / (c.groups.length - 1) : 0 }, out));
656
727
  const lines = groups.flatMap((g) => g.lines);
657
728
  out.texts.forEach((t, i) => {
658
729
  const line = lines[i];
@@ -689,43 +760,14 @@ function measuredEnd(f, fallback) {
689
760
  const bottoms = [...f.texts.map((t) => t.box), ...f.rules.map((r3) => r3.rect), ...f.icons.map((i) => i.rect)].map((r3) => r3.y + r3.h);
690
761
  return bottoms.length > 0 ? Math.max(...bottoms) : fallback;
691
762
  }
692
- function pictureRegion(layout, geo, body, u, frame) {
763
+ function pictureRegion(layout, geo, body, frame) {
693
764
  if (layout !== "picture-bottom" /* PictureBottom */) {
694
765
  return geo.picture;
695
766
  }
696
- const y = Math.min(geo.picture.y, Math.ceil(measuredEnd(body, geo.body.y) + BODY_GAP * u));
767
+ const y = Math.min(geo.picture.y, Math.ceil(measuredEnd(body, geo.body.y) + BODY_GAP * frame.u));
697
768
  return { x: geo.picture.x, y, w: geo.picture.w, h: frame.height - y };
698
769
  }
699
770
 
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
771
  // src/render/fitToFloor.ts
730
772
  var FLOOR_PX = 30;
731
773
  var BASE = 1080;
@@ -733,33 +775,88 @@ var LINE_HEIGHT3 = 1.2;
733
775
  var LOGO_SHARE = 0.26;
734
776
  var GIVE_STEP = 0.025;
735
777
  var GIVE_STEPS = 32;
778
+ var SHIFT_STEP = 0.06;
779
+ var SHIFT_STEPS = 8;
780
+ var MARGIN2 = 0.045;
736
781
  function splitHeader(header, hasLogo) {
737
782
  const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
738
783
  return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
739
784
  }
740
- function flowAll(ctx, input, geo, u) {
785
+ function flowAll(ctx, input, geo, reflow) {
741
786
  const { doc, palette, fonts } = input;
787
+ const u = Math.min(input.width, input.height) / BASE;
742
788
  const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
743
789
  const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
744
790
  const flanks = geo.flanks ?? [];
745
- const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
791
+ const body = flow(ctx, bodyGroups(doc, palette, u, { withPrices: flanks.length === 0, compact: reflow.compact, wrap: reflow.wrap }), geo.body, fonts);
746
792
  const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
747
793
  return { geo, zones, head, body, sides };
748
794
  }
749
795
  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);
796
+ function shiftHeader(geo, shift) {
797
+ const { header, body } = geo;
798
+ const stacked = header.y + header.h <= body.y && header.x < body.x + body.w && body.x < header.x + header.w;
799
+ const d = Math.round(header.h * shift);
800
+ if (!stacked || d === 0 || body.h - d <= 0) {
801
+ return geo;
802
+ }
803
+ return { ...geo, header: { ...header, h: header.h - d }, body: { ...body, y: body.y - d, h: body.h + d } };
752
804
  }
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);
805
+ function liftHeader(geo, top) {
806
+ const { header } = geo;
807
+ return header.y > top ? { ...geo, header: { ...header, y: top, h: header.h + header.y - top } } : geo;
808
+ }
809
+ function fitBottom(ctx, input, floor) {
810
+ const { width, height } = input;
811
+ const none = { compact: false, wrap: false};
812
+ let fitted = flowAll(ctx, input, geometry("picture-bottom" /* PictureBottom */, width, height), none);
813
+ for (let step = 1; step <= GIVE_STEPS && smallest(fitted) < floor; step++) {
814
+ fitted = flowAll(ctx, input, geometry("picture-bottom" /* PictureBottom */, width, height, step * GIVE_STEP), none);
815
+ }
816
+ return fitted;
817
+ }
818
+ function yieldPicture(geo, give) {
819
+ const { picture: p, body: b } = geo;
820
+ const d = Math.round(p.w * give);
821
+ return { ...geo, picture: { ...p, x: p.x + d, w: p.w - d }, body: { ...b, w: b.w + d } };
822
+ }
823
+ function fitList(ctx, input, floor, base2) {
824
+ const last = { compact: true, wrap: true};
825
+ let fitted = flowAll(ctx, input, base2, last);
758
826
  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);
827
+ fitted = flowAll(ctx, input, yieldPicture(base2, step * GIVE_STEP), last);
760
828
  }
761
829
  return fitted;
762
830
  }
831
+ var SHIFTS = [0, ...Array.from({ length: SHIFT_STEPS }, (_v, i) => [(i + 1) * SHIFT_STEP, -(i + 1) * SHIFT_STEP]).flat()];
832
+ var TIERS = [
833
+ { compact: false, wrap: false, lift: false },
834
+ { compact: true, wrap: false, lift: false },
835
+ { compact: true, wrap: true, lift: false },
836
+ { compact: true, wrap: true, lift: true }
837
+ ];
838
+ var REFLOWS = TIERS.flatMap((tier) => SHIFTS.map((shift) => ({ ...tier, shift })));
839
+ function fitToFloor(ctx, input) {
840
+ const { doc, width, height } = input;
841
+ const floor = FLOOR_PX * Math.min(width, height) / BASE;
842
+ if (doc.layout === "picture-bottom" /* PictureBottom */) {
843
+ return fitBottom(ctx, input, floor);
844
+ }
845
+ const base2 = geometry(doc.layout, width, height);
846
+ const top = Math.round(Math.min(width, height) * MARGIN2);
847
+ let best = null;
848
+ for (const reflow of REFLOWS) {
849
+ const fitted = flowAll(ctx, input, shiftHeader(reflow.lift ? liftHeader(base2, top) : base2, reflow.shift), reflow);
850
+ if (smallest(fitted) >= floor) {
851
+ return fitted;
852
+ }
853
+ best = best === null || smallest(fitted) > smallest(best) ? fitted : best;
854
+ }
855
+ if (doc.layout === "list" /* List */) {
856
+ return fitList(ctx, input, floor, base2);
857
+ }
858
+ return best ?? flowAll(ctx, input, base2, { compact: false, wrap: false});
859
+ }
763
860
 
764
861
  // src/render/render.ts
765
862
  var BASE2 = 1080;
@@ -880,7 +977,7 @@ function render(input, assets, canvas) {
880
977
  const radius = Math.min(width, height) * CARD_RADIUS;
881
978
  [head, body, ...sides].forEach((f) => drawFlow(ctx, f, radius));
882
979
  const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
883
- const logo = logoImage ? placeContain(logoImage, geo.logo ? zones.logo : capAligned(zones.logo, head), "top" /* Top */, 1, 1) : null;
980
+ const logo = logoImage ? placeContain(logoImage, geo.logo ? zones.logo : capAligned(zones.logo, head), { anchor: "top" /* Top */, alignX: 1, scale: 1 }) : null;
884
981
  if (logoImage && logo) {
885
982
  ctx.drawImage(logoImage, logo.x, logo.y, logo.w, logo.h);
886
983
  }
@@ -889,8 +986,8 @@ function render(input, assets, canvas) {
889
986
  if (backdrop) {
890
987
  drawArch(ctx, backdrop, palette.arch);
891
988
  }
892
- const region = pictureRegion(doc.layout, geo, body, u, { height });
893
- const picture = image && doc.picture ? placeContain(image, region, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
989
+ const region = pictureRegion(doc.layout, geo, body, { height, u });
990
+ const picture = image && doc.picture ? placeContain(image, region, { anchor: doc.picture.anchor, alignX: geo.alignX, scale: doc.picture.scale }) : null;
894
991
  if (image && picture) {
895
992
  ctx.drawImage(image, picture.x, picture.y, picture.w, picture.h);
896
993
  }