@dloizides/poster-render 0.2.0 → 0.2.2

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
@@ -312,18 +312,19 @@ function headerGroups(doc, p, u) {
312
312
  return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
313
313
  }
314
314
  function itemGroup(item, l, u) {
315
- const lines = [l.script?.(item.date, 96)];
315
+ const lines = [l.script?.(item.date, 67)];
316
316
  item.schedule.forEach((row) => lines.push(l.strong?.(`${row.label} ${row.time}`, 42)));
317
317
  if (item.price) {
318
318
  lines.push(l.accent?.(item.price, 42));
319
319
  }
320
- return { lines, gapAfter: 26 * u };
320
+ return { lines, gapAfter: 48 * u };
321
321
  }
322
322
  function eventGroups(doc, l, u, withPrices) {
323
323
  const b = doc.blocks;
324
324
  const groups = [];
325
325
  b.schedule?.forEach((row) => groups.push({ lines: [l.strong?.(row.label, 66), l.accent?.(row.time, 60)], gapAfter: 30 * u }));
326
- (withPrices ? b.prices : void 0)?.forEach((row) => groups.push({ lines: [l.strong?.(row.amount, 66), l.accent?.(row.label, 52)], gapAfter: 30 * u }));
326
+ const prices = (withPrices ? b.prices : void 0) ?? [];
327
+ prices.forEach((row, i) => groups.push({ lines: [l.strong?.(row.amount, 66), l.accent?.(row.label, 52)], gapAfter: 30 * u, keepWithNext: i < prices.length - 1 }));
327
328
  return groups;
328
329
  }
329
330
  function listGroups(doc, l, u) {
@@ -344,15 +345,15 @@ function bodyGroups(doc, p, u, withPrices = true) {
344
345
  const groups = [];
345
346
  const isList = doc.layout === "list" /* List */;
346
347
  if (b.venue) {
347
- groups.push({ lines: b.venue.map((v) => l.venue?.(v, isList ? 48 : 58)), gapAfter: 40 * u, rule: { color: p.accent, height: 8 * u } });
348
+ groups.push({ lines: b.venue.map((v) => l.venue?.(v, isList ? 48 : 58)), gapAfter: 40 * u, rule: { color: p.accent, height: 8 * u }, ruleBelow: true, icon: { color: p.accent, glyph: p.background } });
348
349
  }
349
350
  groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
350
351
  if (b.includes) {
351
352
  const lines = [l.accent?.(b.includes.heading, 38), ...b.includes.items.map((i) => l.item?.(i, 38))];
352
- groups.push({ lines, gapAfter: 24 * u, card: { color: p.card, pad: 28 * u }, uniform: true });
353
+ groups.push({ lines, gapAfter: 24 * u, uniform: true });
353
354
  }
354
355
  if (b.footer) {
355
- groups.push({ lines: [l.soft?.(b.footer, 34)], gapAfter: 0 });
356
+ groups.push({ lines: [l.soft?.(b.footer, 38)], gapAfter: 0 });
356
357
  }
357
358
  return groups;
358
359
  }
@@ -388,11 +389,11 @@ var LAYOUT_SPECS = {
388
389
  },
389
390
  ["desktop-safe" /* DesktopSafe */]: {
390
391
  portrait: { header: [0.08, 0.06, 0.84, 0.24], body: [0.08, 0.31, 0.46, 0.58], picture: [0.56, 0.36, 0.44, 0.64], alignX: 1 },
391
- landscape: { header: [0.0625, 0.05, 0.58, 0.44], body: [0.0625, 0.5, 0.58, 0.38], picture: [0.67, 0.0972, 0.29, 0.9028], alignX: 1 }
392
+ landscape: { header: [0.0625, 0.05, 0.58, 0.44], body: [0.0625, 0.5, 0.58, 0.38], picture: [0.67, 0.0972, 0.29, 0.9028], alignX: 1, logo: [0.4, 0.045, 0.09, 0.17] }
392
393
  },
393
394
  ["desktop-arch" /* DesktopArch */]: {
394
395
  portrait: { header: [0.08, 0.06, 0.84, 0.2], body: [0.08, 0.27, 0.84, 0.2], picture: [0.14, 0.48, 0.72, 0.52], alignX: 0.5, arch: 0.05 },
395
- landscape: { header: [0.07, 0.3, 0.26, 0.45], body: [0.7, 0.25, 0.27, 0.65], picture: DESK_ARCH_PIC, alignX: 0.5, logo: [0.88, 0.04, 0.09, 0.14], arch: 0.05 }
396
+ landscape: { header: [0.07, 0.3, 0.26, 0.45], body: [0.7, 0.25, 0.27, 0.65], picture: DESK_ARCH_PIC, alignX: 0.5, logo: [0.86, 0.03, 0.11, 0.18], arch: 0.05 }
396
397
  }
397
398
  };
398
399
  var r = Math.round;
@@ -494,8 +495,14 @@ var RULE_WIDTH = 0.9;
494
495
  var WIDE = 1.6;
495
496
  var MIN_K = 0.02;
496
497
  var STEPS = 22;
498
+ var ICON_SCALE = 1.6;
499
+ var ICON_GAP = 0.35;
497
500
  var fontOf = (line, size, fonts) => `${line.weight} ${size}px ${fonts[line.role]}`;
498
- var emptyFlow = () => ({ texts: [], cards: [], rules: [], bullets: [] });
501
+ var emptyFlow = () => ({ texts: [], cards: [], rules: [], bullets: [], icons: [] });
502
+ var iconSize = (group, k) => {
503
+ const first = group.lines[0];
504
+ return group.icon && first ? first.size * k * LINE_HEIGHT * ICON_SCALE : 0;
505
+ };
499
506
  function lineSize(line, width, k, avail) {
500
507
  const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
501
508
  return Math.max(0, Math.min(line.size * k, line.size * (avail - indent) / Math.max(width, 1)));
@@ -506,6 +513,14 @@ function groupRatio(m, k, avail) {
506
513
  }
507
514
  return Math.min(...m.group.lines.map((line, i) => lineSize(line, m.widths[i] ?? 1, k, avail) / line.size));
508
515
  }
516
+ function placeIcon(group, d, x, textTop, textBottom, out) {
517
+ if (!group.icon || d <= 0) {
518
+ return textBottom;
519
+ }
520
+ const textH = textBottom - textTop;
521
+ out?.icons.push({ rect: { x, y: textTop + Math.max(0, (textH - d) / 2), w: d, h: d }, color: group.icon.color, glyph: group.icon.glyph });
522
+ return textTop + Math.max(textH, d);
523
+ }
509
524
  function placeGroup(m, k, col, y0, out) {
510
525
  const { group } = m;
511
526
  let y = y0;
@@ -516,10 +531,13 @@ function placeGroup(m, k, col, y0, out) {
516
531
  const pad = group.card ? group.card.pad * k : 0;
517
532
  const top = y;
518
533
  y += pad;
519
- const ratio = groupRatio(m, k, col.w - 2 * pad);
534
+ const d = iconSize(group, k);
535
+ const iconIndent = d > 0 ? d * (1 + ICON_GAP) : 0;
536
+ const avail = col.w - 2 * pad - iconIndent;
537
+ const ratio = groupRatio(m, k, avail);
520
538
  group.lines.forEach((line, i) => {
521
- const size = ratio === null ? lineSize(line, m.widths[i] ?? 1, k, col.w - 2 * pad) : line.size * ratio;
522
- const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
539
+ const size = ratio === null ? lineSize(line, m.widths[i] ?? 1, k, avail) : line.size * ratio;
540
+ const indent = (line.bullet ? line.size * k * BULLET_INDENT : 0) + iconIndent;
523
541
  const w = (m.widths[i] ?? 0) * size / line.size;
524
542
  const x = col.right ? col.x + col.w - pad - w : col.x + pad + indent;
525
543
  out?.texts.push({ text: line.text, font: "", color: line.color, box: { x, y, w, h: size * LINE_HEIGHT } });
@@ -528,36 +546,62 @@ function placeGroup(m, k, col, y0, out) {
528
546
  }
529
547
  y += size * LINE_HEIGHT;
530
548
  });
549
+ y = placeIcon(group, d, col.x + pad, top + pad, y, out);
531
550
  y += pad;
551
+ if (group.rule && group.ruleBelow) {
552
+ y += group.rule.height * k * (RULE_GAP - 1);
553
+ out?.rules.push({ rect: { x: col.x, y, w: col.w * RULE_WIDTH, h: group.rule.height * k }, color: group.rule.color });
554
+ y += group.rule.height * k;
555
+ }
532
556
  if (group.card) {
533
557
  out?.cards.unshift({ rect: { x: col.x, y: top, w: col.w, h: y - top }, color: group.card.color });
534
558
  }
535
559
  return y + group.gapAfter * k - y0;
536
560
  }
537
- function placeColumn(groups, k, col, out) {
561
+ function placeColumn(groups, k, col, out, spread = 0) {
538
562
  let h = 0;
539
563
  for (const g of groups) {
540
- h += placeGroup(g, k, col, col.y + h, out);
564
+ h += placeGroup(g, k, col, col.y + h, out) + spread;
565
+ }
566
+ return h - spread;
567
+ }
568
+ var columnSum = (heights, from, to) => heights.slice(from, to).reduce((a, b) => a + b, 0);
569
+ var tallest = (heights, cut) => {
570
+ const left = heights.slice(0, cut).reduce((a, b) => a + b, 0);
571
+ const right = heights.slice(cut).reduce((a, b) => a + b, 0);
572
+ return Math.max(left, right);
573
+ };
574
+ function keepTogether(groups, heights, cut) {
575
+ const breaks = (c) => groups[c - 1]?.group.keepWithNext === true;
576
+ let back = cut;
577
+ while (back > 1 && breaks(back)) {
578
+ back--;
579
+ }
580
+ let fwd = cut;
581
+ while (fwd < groups.length - 1 && breaks(fwd)) {
582
+ fwd++;
583
+ }
584
+ const candidates = [back, fwd].filter((c) => !breaks(c));
585
+ if (candidates.length === 0) {
586
+ return cut;
541
587
  }
542
- return h;
588
+ return candidates.reduce((best, c) => tallest(heights, c) < tallest(heights, best) ? c : best);
543
589
  }
544
590
  function splitColumns(groups, zone) {
545
591
  if (zone.w / Math.max(zone.h, 1) <= WIDE || groups.length < 2) {
546
592
  return [{ col: zone, groups }];
547
593
  }
548
- const heights = groups.map((g) => placeGroup(g, 1, zone, 0, null));
549
- const half = heights.reduce((a, b) => a + b, 0) / 2;
550
- let acc = 0;
551
- let cut = 1;
552
- heights.forEach((h, i) => {
553
- if (acc < half) {
554
- cut = i + 1;
555
- }
556
- acc += h;
557
- });
558
- cut = Math.min(cut, groups.length - 1);
559
594
  const gap = Math.round(zone.w * 0.04);
560
595
  const w = (zone.w - gap) / 2;
596
+ const heights = groups.map((g) => placeGroup(g, 1, { ...zone, w }, 0, null));
597
+ const total = columnSum(heights, 0, heights.length);
598
+ let cut = 1;
599
+ for (let c = 2; c < groups.length; c++) {
600
+ if (Math.abs(total - 2 * columnSum(heights, 0, c)) < Math.abs(total - 2 * columnSum(heights, 0, cut))) {
601
+ cut = c;
602
+ }
603
+ }
604
+ cut = keepTogether(groups, heights, cut);
561
605
  return [
562
606
  { col: { ...zone, w }, groups: groups.slice(0, cut) },
563
607
  { col: { ...zone, x: zone.x + w + gap, w }, groups: groups.slice(cut) }
@@ -587,7 +631,9 @@ function flow(ctx, groups, zone, fonts) {
587
631
  }
588
632
  }
589
633
  const out = emptyFlow();
590
- columns.forEach((c) => placeColumn(c.groups, lo, c.col, out));
634
+ const heights = columns.map((c) => placeColumn(c.groups, lo, c.col, null));
635
+ const end = Math.max(...heights);
636
+ 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));
591
637
  const lines = groups.flatMap((g) => g.lines);
592
638
  out.texts.forEach((t, i) => {
593
639
  const line = lines[i];
@@ -596,11 +642,75 @@ function flow(ctx, groups, zone, fonts) {
596
642
  return out;
597
643
  }
598
644
 
645
+ // src/render/regions.ts
646
+ var LINE_HEIGHT2 = 1.2;
647
+ var BODY_GAP = 40;
648
+ var BOTTOM_SIDE = 0.1;
649
+ function flowFlanks(ctx, groups, flanks, fonts) {
650
+ const half = Math.ceil(groups.length / 2);
651
+ 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
+ }
653
+ function smallestPerLine(groups, results) {
654
+ const sizes = results.flatMap((r3) => r3.texts.map((t) => t.box.h / LINE_HEIGHT2));
655
+ const smallest = [];
656
+ let i = 0;
657
+ groups.forEach(
658
+ (g) => g.lines.forEach((_line, j) => {
659
+ smallest[j] = Math.min(smallest[j] ?? Infinity, sizes[i] ?? Infinity);
660
+ i++;
661
+ })
662
+ );
663
+ return smallest;
664
+ }
665
+ 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) })) }));
668
+ return flowFlanks(ctx, sized, flanks, fonts);
669
+ }
670
+ function measuredEnd(f, fallback) {
671
+ 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);
672
+ return bottoms.length > 0 ? Math.max(...bottoms) : fallback;
673
+ }
674
+ function pictureRegion(layout, geo, body, u, frame) {
675
+ if (layout !== "picture-bottom" /* PictureBottom */) {
676
+ return geo.picture;
677
+ }
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 };
681
+ }
682
+
599
683
  // src/render/render.ts
600
684
  var BASE = 1080;
601
685
  var LOGO_SHARE = 0.26;
602
686
  var CARD_RADIUS = 0.035;
603
687
  var CAP_TOP = 0.15;
688
+ var PIN_HEAD_Y = 0.08;
689
+ var PIN_HEAD_R = 0.19;
690
+ var PIN_TIP_Y = 0.28;
691
+ var PIN_HOLE = 0.42;
692
+ var ARC_START = 0.75;
693
+ var ARC_END = 2.25;
694
+ function drawPin(ctx, r3, color, glyph) {
695
+ const cx = r3.x + r3.w / 2;
696
+ const cy = r3.y + r3.h / 2;
697
+ const hy = cy - r3.w * PIN_HEAD_Y;
698
+ const rad = r3.w * PIN_HEAD_R;
699
+ ctx.fillStyle = color;
700
+ ctx.beginPath();
701
+ ctx.arc(cx, cy, r3.w / 2, 0, Math.PI * 2);
702
+ ctx.fill();
703
+ ctx.fillStyle = glyph;
704
+ ctx.beginPath();
705
+ ctx.moveTo(cx, cy + r3.w * PIN_TIP_Y);
706
+ ctx.arc(cx, hy, rad, Math.PI * ARC_START, Math.PI * ARC_END);
707
+ ctx.closePath();
708
+ ctx.fill();
709
+ ctx.fillStyle = color;
710
+ ctx.beginPath();
711
+ ctx.arc(cx, hy, rad * PIN_HOLE, 0, Math.PI * 2);
712
+ ctx.fill();
713
+ }
604
714
  function roundRect(ctx, r3, radius) {
605
715
  const rad = Math.min(radius, r3.w / 2, r3.h / 2);
606
716
  ctx.beginPath();
@@ -627,6 +737,7 @@ function drawFlow(ctx, f, radius) {
627
737
  ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
628
738
  ctx.fill();
629
739
  });
740
+ f.icons.forEach((i) => drawPin(ctx, i.rect, i.color, i.glyph));
630
741
  ctx.textAlign = "left";
631
742
  ctx.textBaseline = "top";
632
743
  f.texts.forEach((t) => {
@@ -661,10 +772,6 @@ function drawArch(ctx, r3, color) {
661
772
  ctx.arc(r3.x + rad, r3.y + rad, rad, Math.PI, Math.PI * 2);
662
773
  ctx.fill();
663
774
  }
664
- function flankFlows(ctx, groups, flanks, fonts) {
665
- const half = Math.ceil(groups.length / 2);
666
- return flanks.map((zone, i) => i === 0 ? flow(ctx, groups.slice(0, half), zone, fonts) : flow(ctx, groups.slice(half), { ...zone, right: true }, fonts));
667
- }
668
775
  function capAligned(zone, head) {
669
776
  const first = head.texts[0]?.box;
670
777
  const shift = first ? Math.min(Math.max(0, Math.round(first.y + first.h * CAP_TOP) - zone.y), Math.round(zone.h / 2)) : 0;
@@ -698,20 +805,21 @@ function render(input, assets, canvas) {
698
805
  const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
699
806
  const flanks = geo.flanks ?? [];
700
807
  const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
701
- const sides = flanks.length > 0 ? flankFlows(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
808
+ const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
702
809
  const radius = Math.min(width, height) * CARD_RADIUS;
703
810
  [head, body, ...sides].forEach((f) => drawFlow(ctx, f, radius));
704
811
  const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
705
- if (logoImage) {
706
- const r3 = placeContain(logoImage, capAligned(zones.logo, head), "top" /* Top */, 1, 1);
707
- ctx.drawImage(logoImage, r3.x, r3.y, r3.w, r3.h);
812
+ const logo = logoImage ? placeContain(logoImage, geo.logo ? zones.logo : capAligned(zones.logo, head), "top" /* Top */, 1, 1) : null;
813
+ if (logoImage && logo) {
814
+ ctx.drawImage(logoImage, logo.x, logo.y, logo.w, logo.h);
708
815
  }
709
816
  const image = doc.picture ? assets[doc.picture.imageId] : void 0;
710
817
  const backdrop = image ? archRect(geo) : null;
711
818
  if (backdrop) {
712
819
  drawArch(ctx, backdrop, palette.arch);
713
820
  }
714
- const picture = image && doc.picture ? placeContain(image, geo.picture, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
821
+ const region = pictureRegion(doc.layout, geo, body, u, { width, height });
822
+ const picture = image && doc.picture ? placeContain(image, region, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
715
823
  if (image && picture) {
716
824
  ctx.drawImage(image, picture.x, picture.y, picture.w, picture.h);
717
825
  }
@@ -719,9 +827,11 @@ function render(input, assets, canvas) {
719
827
  width,
720
828
  height,
721
829
  picture,
722
- pictureRegion: geo.picture,
830
+ pictureRegion: region,
723
831
  textBoxes: [head, body, ...sides].flatMap((f) => f.texts.map((t) => t.box)),
724
832
  cardBoxes: body.cards.map((c) => c.rect),
833
+ iconBoxes: [head, body, ...sides].flatMap((f) => f.icons.map((i) => i.rect)),
834
+ logo,
725
835
  backdrop
726
836
  };
727
837
  }