@dloizides/poster-render 0.2.3 → 0.2.5

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,82 +282,15 @@ 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;
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],
@@ -409,12 +342,13 @@ var HEADER_PORTRAIT = 0.3;
409
342
  var HEADER_LANDSCAPE = 0.36;
410
343
  var BOTTOM_PICTURE_W = 0.8;
411
344
  var BOTTOM_MIN_TOP = 0.25;
412
- var BOTTOM_HEADER_SHARE = 0.45;
345
+ var BOTTOM_HEADER_SHARE = 0.3;
413
346
  var BOTTOM_GAP = 40 / 1080;
414
347
  var BOTTOM_ASPECT = 0.57;
348
+ var DEFAULT_SIDE = [0.6, 0.42];
415
349
  var PICTURE_SIDE = {
416
- ["picture-right" /* PictureRight */]: [0.6, 0.42],
417
- ["picture-left" /* PictureLeft */]: [0.6, 0.42],
350
+ ["picture-right" /* PictureRight */]: DEFAULT_SIDE,
351
+ ["picture-left" /* PictureLeft */]: DEFAULT_SIDE,
418
352
  ["list" /* List */]: [0.5, 0.36]
419
353
  };
420
354
  var ANCHOR_Y = { ["top" /* Top */]: 0, ["center" /* Center */]: 0.5, ["bottom" /* Bottom */]: 1 };
@@ -440,11 +374,11 @@ function side(w, h, fraction) {
440
374
  alignX: 1
441
375
  };
442
376
  }
443
- function bottom(w, h) {
377
+ function bottom(w, h, share) {
444
378
  const m = r2(Math.min(w, h) * MARGIN);
445
379
  const gap = r2(Math.min(w, h) * BOTTOM_GAP);
446
- const picW = r2(w * BOTTOM_PICTURE_W);
447
- const picH = Math.min(r2(picW / BOTTOM_ASPECT), h - r2(h * BOTTOM_MIN_TOP));
380
+ const picW = r2(w * share);
381
+ const picH = Math.min(r2(picW / BOTTOM_ASPECT), r2((h - r2(h * BOTTOM_MIN_TOP)) * share / BOTTOM_PICTURE_W));
448
382
  const pic = h - picH;
449
383
  const top = m + r2((pic - m) * BOTTOM_HEADER_SHARE);
450
384
  return {
@@ -466,21 +400,22 @@ function fromSpec(spec, w, h) {
466
400
  };
467
401
  }
468
402
  var mirror = (rect, w) => ({ ...rect, x: w - rect.x - rect.w });
469
- function geometry(layout, w, h) {
403
+ function geometry(layout, w, h, give = 0) {
470
404
  const oriented = LAYOUT_SPECS[layout];
471
405
  if (oriented) {
472
406
  return fromSpec(h >= w ? oriented.portrait : oriented.landscape, w, h);
473
407
  }
474
408
  if (layout === "picture-bottom" /* PictureBottom */) {
475
- return bottom(w, h);
409
+ return bottom(w, h, BOTTOM_PICTURE_W - give);
476
410
  }
477
- const g = side(w, h, PICTURE_SIDE[layout] ?? [0.6, 0.42]);
411
+ const g = side(w, h, PICTURE_SIDE[layout] ?? DEFAULT_SIDE);
478
412
  if (layout !== "picture-left" /* PictureLeft */) {
479
413
  return g;
480
414
  }
481
415
  return { header: mirror(g.header, w), body: mirror(g.body, w), picture: mirror(g.picture, w), alignX: 1 - g.alignX };
482
416
  }
483
- function placeContain(image, region, anchor, alignX, scale) {
417
+ function placeContain(image, region, fit) {
418
+ const { anchor, alignX, scale } = fit;
484
419
  const s = Math.min(region.w / image.width, region.h / image.height) * scale;
485
420
  const w = Math.floor(image.width * s);
486
421
  const h = Math.floor(image.height * s);
@@ -492,6 +427,97 @@ function placeContain(image, region, anchor, alignX, scale) {
492
427
  };
493
428
  }
494
429
 
430
+ // src/layout/blockGroups.ts
431
+ var BOLD = 700;
432
+ var SEMI = 600;
433
+ var MEDIUM = 500;
434
+ var SCRIPT = 400;
435
+ var SIZE = {
436
+ title: 250,
437
+ headerScript: 120,
438
+ itemDate: 67,
439
+ itemRow: 42,
440
+ scheduleLabel: 66,
441
+ scheduleTime: 60,
442
+ priceAmount: 66,
443
+ priceLabel: 52,
444
+ listPrice: 58,
445
+ flankAmount: 110,
446
+ flankLabel: 48,
447
+ venue: 58,
448
+ listVenue: 48,
449
+ small: 38
450
+ };
451
+ var GAP = {
452
+ item: 48,
453
+ row: 30,
454
+ venue: 40,
455
+ includes: 24,
456
+ rule: 8
457
+ };
458
+ var makers = (p, u) => ({
459
+ display: (text2, size) => ({ text: text2, role: "display", weight: BOLD, size: size * u, color: p.title }),
460
+ venue: (text2, size) => ({ text: text2, role: "display", weight: MEDIUM, size: size * u, color: p.ink }),
461
+ script: (text2, size) => ({ text: text2, role: "script", weight: SCRIPT, size: size * u, color: p.accent }),
462
+ strong: (text2, size) => ({ text: text2, role: "body", weight: BOLD, size: size * u, color: p.ink }),
463
+ accent: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.accent }),
464
+ item: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.ink, bullet: p.accent }),
465
+ soft: (text2, size) => ({ text: text2, role: "body", weight: MEDIUM, size: size * u, color: p.soft })
466
+ });
467
+ function headerGroups(doc, p, u) {
468
+ const l = makers(p, u);
469
+ const b = doc.blocks;
470
+ 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];
471
+ return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
472
+ }
473
+ function itemGroup(item, l, u) {
474
+ const lines = [l.script(item.date, SIZE.itemDate)];
475
+ item.schedule.forEach((row) => lines.push(l.strong(`${row.label} ${row.time}`, SIZE.itemRow)));
476
+ if (item.price) {
477
+ lines.push(l.accent(item.price, SIZE.itemRow));
478
+ }
479
+ return { lines, gapAfter: GAP.item * u };
480
+ }
481
+ function eventGroups(doc, l, u, withPrices) {
482
+ const b = doc.blocks;
483
+ const groups = [];
484
+ b.schedule?.forEach((row) => groups.push({ lines: [l.strong(row.label, SIZE.scheduleLabel), l.accent(row.time, SIZE.scheduleTime)], gapAfter: GAP.row * u }));
485
+ const prices = (withPrices ? b.prices : void 0) ?? [];
486
+ prices.forEach((row, i) => groups.push({ lines: [l.strong(row.amount, SIZE.priceAmount), l.accent(row.label, SIZE.priceLabel)], gapAfter: GAP.row * u, keepWithNext: i < prices.length - 1 }));
487
+ return groups;
488
+ }
489
+ function listGroups(doc, l, u) {
490
+ const groups = (doc.items ?? []).map((item) => itemGroup(item, l, u));
491
+ const prices = doc.blocks.prices?.map((row) => l.strong(`${row.amount} ${row.label}`, SIZE.listPrice)) ?? [];
492
+ if (prices.length > 0) {
493
+ groups.push({ lines: prices, gapAfter: GAP.row * u });
494
+ }
495
+ return groups;
496
+ }
497
+ function flankPriceGroups(doc, p, u) {
498
+ const l = makers(p, u);
499
+ return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong(row.amount, SIZE.flankAmount), l.accent(row.label, SIZE.flankLabel)], gapAfter: GAP.row * u }));
500
+ }
501
+ function bodyGroups(doc, p, u, withPrices = true) {
502
+ const l = makers(p, u);
503
+ const b = doc.blocks;
504
+ const groups = [];
505
+ const isList = doc.layout === "list" /* List */;
506
+ if (b.venue) {
507
+ const size = isList ? SIZE.listVenue : SIZE.venue;
508
+ 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 } });
509
+ }
510
+ groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
511
+ if (b.includes) {
512
+ const lines = [l.accent(b.includes.heading, SIZE.small), ...b.includes.items.map((i) => l.item(i, SIZE.small))];
513
+ groups.push({ lines, gapAfter: GAP.includes * u, uniform: true });
514
+ }
515
+ if (b.footer) {
516
+ groups.push({ lines: [l.soft(b.footer, SIZE.small)], gapAfter: 0 });
517
+ }
518
+ return groups;
519
+ }
520
+
495
521
  // src/layout/textFlow.ts
496
522
  var LINE_HEIGHT = 1.2;
497
523
  var BULLET_INDENT = 1.1;
@@ -499,6 +525,9 @@ var BULLET_RADIUS = 0.2;
499
525
  var RULE_GAP = 2.5;
500
526
  var RULE_WIDTH = 0.9;
501
527
  var WIDE = 1.6;
528
+ var COLUMN_GAP = 0.04;
529
+ var TWO_COLUMN_MIN = 0.75;
530
+ var TWO_COLUMN_GAIN = 1.15;
502
531
  var MIN_K = 0.02;
503
532
  var STEPS = 22;
504
533
  var ICON_SCALE = 1.6;
@@ -519,15 +548,17 @@ function groupRatio(m, k, avail) {
519
548
  }
520
549
  return Math.min(...m.group.lines.map((line, i) => lineSize(line, m.widths[i] ?? 1, k, avail) / line.size));
521
550
  }
522
- function placeIcon(group, d, x, textTop, textBottom, out) {
551
+ function placeIcon(group, box, out) {
552
+ const { d, x, top, bottom: bottom2 } = box;
523
553
  if (!group.icon || d <= 0) {
524
- return textBottom;
554
+ return bottom2;
525
555
  }
526
- const textH = textBottom - textTop;
527
- 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 });
528
- return textTop + Math.max(textH, d);
556
+ const textH = bottom2 - top;
557
+ 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 });
558
+ return top + Math.max(textH, d);
529
559
  }
530
- function placeGroup(m, k, col, y0, out) {
560
+ function placeGroup(m, at, out) {
561
+ const { k, col, y0 } = at;
531
562
  const { group } = m;
532
563
  let y = y0;
533
564
  if (group.rule) {
@@ -552,7 +583,7 @@ function placeGroup(m, k, col, y0, out) {
552
583
  }
553
584
  y += size * LINE_HEIGHT;
554
585
  });
555
- y = placeIcon(group, d, col.x + pad, top + pad, y, out);
586
+ y = placeIcon(group, { d, x: col.x + pad, top: top + pad, bottom: y }, out);
556
587
  y += pad;
557
588
  if (group.rule && group.ruleBelow) {
558
589
  y += group.rule.height * k * (RULE_GAP - 1);
@@ -564,10 +595,11 @@ function placeGroup(m, k, col, y0, out) {
564
595
  }
565
596
  return y + group.gapAfter * k - y0;
566
597
  }
567
- function placeColumn(groups, k, col, out, spread = 0) {
598
+ function placeColumn(groups, at, out) {
599
+ const { k, col, spread = 0 } = at;
568
600
  let h = 0;
569
601
  for (const g of groups) {
570
- h += placeGroup(g, k, col, col.y + h, out) + spread;
602
+ h += placeGroup(g, { k, col, y0: col.y + h }, out) + spread;
571
603
  }
572
604
  return h - spread;
573
605
  }
@@ -594,12 +626,9 @@ function keepTogether(groups, heights, cut) {
594
626
  return candidates.reduce((best, c) => tallest(heights, c) < tallest(heights, best) ? c : best);
595
627
  }
596
628
  function splitColumns(groups, zone) {
597
- if (zone.w / Math.max(zone.h, 1) <= WIDE || groups.length < 2) {
598
- return [{ col: zone, groups }];
599
- }
600
- const gap = Math.round(zone.w * 0.04);
629
+ const gap = Math.round(zone.w * COLUMN_GAP);
601
630
  const w = (zone.w - gap) / 2;
602
- const heights = groups.map((g) => placeGroup(g, 1, { ...zone, w }, 0, null));
631
+ const heights = groups.map((g) => placeGroup(g, { k: 1, col: { ...zone, w }, y0: 0 }, null));
603
632
  const total = columnSum(heights, 0, heights.length);
604
633
  let cut = 1;
605
634
  for (let c = 2; c < groups.length; c++) {
@@ -613,16 +642,8 @@ function splitColumns(groups, zone) {
613
642
  { col: { ...zone, x: zone.x + w + gap, w }, groups: groups.slice(cut) }
614
643
  ];
615
644
  }
616
- function flow(ctx, groups, zone, fonts) {
617
- const measured = groups.map((group) => ({
618
- group,
619
- widths: group.lines.map((line) => {
620
- ctx.font = fontOf(line, line.size, fonts);
621
- return ctx.measureText(line.text).width;
622
- })
623
- }));
624
- const columns = splitColumns(measured, zone);
625
- const fits = (k) => columns.every((c) => placeColumn(c.groups, k, c.col, null) <= zone.h);
645
+ function largestK(columns, zoneH) {
646
+ const fits = (k) => columns.every((c) => placeColumn(c.groups, { k, col: c.col }, null) <= zoneH);
626
647
  let lo = MIN_K;
627
648
  let hi = 1;
628
649
  if (fits(1)) {
@@ -636,10 +657,32 @@ function flow(ctx, groups, zone, fonts) {
636
657
  hi = mid;
637
658
  }
638
659
  }
660
+ return lo;
661
+ }
662
+ function bestColumns(measured, zone) {
663
+ const single = [{ col: zone, groups: measured }];
664
+ const k1 = largestK(single, zone.h);
665
+ if (measured.length < 2 || zone.w / Math.max(zone.h, 1) < TWO_COLUMN_MIN) {
666
+ return { columns: single, k: k1 };
667
+ }
668
+ const split = splitColumns(measured, zone);
669
+ const k2 = largestK(split, zone.h);
670
+ const forced = zone.w / Math.max(zone.h, 1) > WIDE;
671
+ return forced || k2 > k1 * TWO_COLUMN_GAIN ? { columns: split, k: k2 } : { columns: single, k: k1 };
672
+ }
673
+ function flow(ctx, groups, zone, fonts) {
674
+ const measured = groups.map((group) => ({
675
+ group,
676
+ widths: group.lines.map((line) => {
677
+ ctx.font = fontOf(line, line.size, fonts);
678
+ return ctx.measureText(line.text).width;
679
+ })
680
+ }));
681
+ const { columns, k: lo } = bestColumns(measured, zone);
639
682
  const out = emptyFlow();
640
- const heights = columns.map((c) => placeColumn(c.groups, lo, c.col, null));
683
+ const heights = columns.map((c) => placeColumn(c.groups, { k: lo, col: c.col }, null));
641
684
  const end = Math.max(...heights);
642
- 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));
685
+ 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));
643
686
  const lines = groups.flatMap((g) => g.lines);
644
687
  out.texts.forEach((t, i) => {
645
688
  const line = lines[i];
@@ -657,36 +700,99 @@ function flowFlanks(ctx, groups, flanks, fonts) {
657
700
  }
658
701
  function smallestPerLine(groups, results) {
659
702
  const sizes = results.flatMap((r3) => r3.texts.map((t) => t.box.h / LINE_HEIGHT2));
660
- const smallest = [];
703
+ const smallest2 = [];
661
704
  let i = 0;
662
705
  groups.forEach(
663
706
  (g) => g.lines.forEach((_line, j) => {
664
- smallest[j] = Math.min(smallest[j] ?? Infinity, sizes[i] ?? Infinity);
707
+ smallest2[j] = Math.min(smallest2[j] ?? Infinity, sizes[i] ?? Infinity);
665
708
  i++;
666
709
  })
667
710
  );
668
- return smallest;
711
+ return smallest2;
669
712
  }
670
713
  function equalFlanks(ctx, groups, flanks, fonts) {
671
- const smallest = smallestPerLine(groups, flowFlanks(ctx, groups, flanks, fonts));
672
- const sized = groups.map((g) => ({ ...g, lines: g.lines.map((line, j) => ({ ...line, size: Math.min(line.size, smallest[j] ?? line.size) })) }));
714
+ const smallest2 = smallestPerLine(groups, flowFlanks(ctx, groups, flanks, fonts));
715
+ const sized = groups.map((g) => ({ ...g, lines: g.lines.map((line, j) => ({ ...line, size: Math.min(line.size, smallest2[j] ?? line.size) })) }));
673
716
  return flowFlanks(ctx, sized, flanks, fonts);
674
717
  }
675
718
  function measuredEnd(f, fallback) {
676
719
  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);
677
720
  return bottoms.length > 0 ? Math.max(...bottoms) : fallback;
678
721
  }
679
- function pictureRegion(layout, geo, body, u, frame) {
722
+ function pictureRegion(layout, geo, body, frame) {
680
723
  if (layout !== "picture-bottom" /* PictureBottom */) {
681
724
  return geo.picture;
682
725
  }
683
- const y = Math.min(geo.picture.y, Math.ceil(measuredEnd(body, geo.body.y) + BODY_GAP * u));
726
+ const y = Math.min(geo.picture.y, Math.ceil(measuredEnd(body, geo.body.y) + BODY_GAP * frame.u));
684
727
  return { x: geo.picture.x, y, w: geo.picture.w, h: frame.height - y };
685
728
  }
686
729
 
687
- // src/render/render.ts
730
+ // src/render/yieldToText.ts
731
+ function yieldToText(geo, give) {
732
+ const { picture: p, body: b } = geo;
733
+ if (give <= 0) {
734
+ return geo;
735
+ }
736
+ if (p.x >= b.x + b.w) {
737
+ const d = Math.round(p.w * give);
738
+ return { ...geo, picture: { ...p, x: p.x + d, w: p.w - d }, body: { ...b, w: b.w + d } };
739
+ }
740
+ if (p.x + p.w <= b.x) {
741
+ const d = Math.round(p.w * give);
742
+ return { ...geo, picture: { ...p, w: p.w - d }, body: { ...b, x: b.x - d, w: b.w + d } };
743
+ }
744
+ if (p.y >= b.y + b.h) {
745
+ const d = Math.round(p.h * give);
746
+ const stacked = geo.header.y + geo.header.h <= b.y;
747
+ const toHeader = stacked ? Math.round(d / 2) : 0;
748
+ return {
749
+ ...geo,
750
+ header: { ...geo.header, h: geo.header.h + toHeader },
751
+ picture: { ...p, y: p.y + d, h: p.h - d },
752
+ body: { ...b, y: b.y + toHeader, h: b.h + d - toHeader },
753
+ flanks: geo.flanks?.map((f) => ({ ...f, y: f.y + d, h: f.h - d }))
754
+ };
755
+ }
756
+ return geo;
757
+ }
758
+
759
+ // src/render/fitToFloor.ts
760
+ var FLOOR_PX = 30;
688
761
  var BASE = 1080;
762
+ var LINE_HEIGHT3 = 1.2;
689
763
  var LOGO_SHARE = 0.26;
764
+ var GIVE_STEP = 0.025;
765
+ var GIVE_STEPS = 32;
766
+ function splitHeader(header, hasLogo) {
767
+ const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
768
+ return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
769
+ }
770
+ function flowAll(ctx, input, geo, u) {
771
+ const { doc, palette, fonts } = input;
772
+ const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
773
+ const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
774
+ const flanks = geo.flanks ?? [];
775
+ const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
776
+ const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
777
+ return { geo, zones, head, body, sides };
778
+ }
779
+ var smallest = (t) => Math.min(Infinity, ...[t.head, t.body, ...t.sides].flatMap((f) => f.texts.map((x) => x.box.h / LINE_HEIGHT3)));
780
+ function geometryWithGive(layout, width, height, give) {
781
+ return layout === "picture-bottom" /* PictureBottom */ ? geometry(layout, width, height, give) : yieldToText(geometry(layout, width, height), give);
782
+ }
783
+ function fitToFloor(ctx, input) {
784
+ const { doc, width, height } = input;
785
+ const u = Math.min(width, height) / BASE;
786
+ const floor = FLOOR_PX * u;
787
+ let fitted = flowAll(ctx, input, geometryWithGive(doc.layout, width, height, 0), u);
788
+ for (let step = 1; step <= GIVE_STEPS && smallest(fitted) < floor; step++) {
789
+ fitted = flowAll(ctx, input, geometryWithGive(doc.layout, width, height, step * GIVE_STEP), u);
790
+ }
791
+ return fitted;
792
+ }
793
+
794
+ // src/render/render.ts
795
+ var BASE2 = 1080;
690
796
  var CARD_RADIUS = 0.035;
691
797
  var CAP_TOP = 0.15;
692
798
  var PIN_HEAD_Y = 0.08;
@@ -781,10 +887,6 @@ function capAligned(zone, head) {
781
887
  const shift = first ? Math.min(Math.max(0, Math.round(first.y + first.h * CAP_TOP) - zone.y), Math.round(zone.h / 2)) : 0;
782
888
  return { ...zone, y: zone.y + shift, h: zone.h - shift };
783
889
  }
784
- function splitHeader(header, hasLogo) {
785
- const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
786
- return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
787
- }
788
890
  function render(input, assets, canvas) {
789
891
  const doc = checked(input, assets);
790
892
  const { width, height } = FORMAT_SIZES[doc.format];
@@ -796,8 +898,8 @@ function render(input, assets, canvas) {
796
898
  }
797
899
  const palette = PALETTES[doc.palette];
798
900
  const fonts = FONT_SETS[doc.fonts];
799
- const geo = geometry(doc.layout, width, height);
800
- const u = Math.min(width, height) / BASE;
901
+ const u = Math.min(width, height) / BASE2;
902
+ const { geo, zones, head, body, sides } = fitToFloor(ctx, { doc, palette, fonts, width, height });
801
903
  ctx.globalAlpha = 1;
802
904
  ctx.fillStyle = palette.background;
803
905
  ctx.fillRect(0, 0, width, height);
@@ -805,15 +907,10 @@ function render(input, assets, canvas) {
805
907
  ctx.fillStyle = palette.tint;
806
908
  [geo.header, geo.body].forEach((r3) => ctx.fillRect(r3.x, r3.y, r3.w, r3.h));
807
909
  }
808
- const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
809
- const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
810
- const flanks = geo.flanks ?? [];
811
- const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
812
- const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
813
910
  const radius = Math.min(width, height) * CARD_RADIUS;
814
911
  [head, body, ...sides].forEach((f) => drawFlow(ctx, f, radius));
815
912
  const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
816
- const logo = logoImage ? placeContain(logoImage, geo.logo ? zones.logo : capAligned(zones.logo, head), "top" /* Top */, 1, 1) : null;
913
+ const logo = logoImage ? placeContain(logoImage, geo.logo ? zones.logo : capAligned(zones.logo, head), { anchor: "top" /* Top */, alignX: 1, scale: 1 }) : null;
817
914
  if (logoImage && logo) {
818
915
  ctx.drawImage(logoImage, logo.x, logo.y, logo.w, logo.h);
819
916
  }
@@ -822,8 +919,8 @@ function render(input, assets, canvas) {
822
919
  if (backdrop) {
823
920
  drawArch(ctx, backdrop, palette.arch);
824
921
  }
825
- const region = pictureRegion(doc.layout, geo, body, u, { height });
826
- const picture = image && doc.picture ? placeContain(image, region, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
922
+ const region = pictureRegion(doc.layout, geo, body, { height, u });
923
+ const picture = image && doc.picture ? placeContain(image, region, { anchor: doc.picture.anchor, alignX: geo.alignX, scale: doc.picture.scale }) : null;
827
924
  if (image && picture) {
828
925
  ctx.drawImage(image, picture.x, picture.y, picture.w, picture.h);
829
926
  }