@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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2
4
+
5
+ - Arch flanks: both flank price groups use one size per line, so "per hour" and "Party pass" match.
6
+ - Two-column bodies: the break is chosen from heights measured at the column width, and the shorter column spreads its group gaps so both columns end together.
7
+ - Footer base size 38 (was 34): includes and footer stay at 30 px or more on story and phone arch layouts.
8
+ - `picture-bottom`: the picture region starts 40u under the body's measured end, spans the middle 80% of the width and runs to the frame edge.
9
+ - Venue gets a rule below as well as above (`FlowGroup.ruleBelow`).
10
+
11
+ ## 0.2.1
12
+
13
+ - Venue pin: an accent disc with a pin glyph beside the venue lines; `RenderReport.iconBoxes`.
14
+ - Includes block drawn as a heading plus bullets, with no card.
15
+ - Price groups stay in one column when a wide body splits into two (`FlowGroup.keepWithNext`).
16
+ - `desktop-safe` landscape gets an explicit logo rect beside the title; `desktop-arch` logo enlarged and no longer pushed down to the title cap; `RenderReport.logo`.
17
+ - List rows: date at 0.7x, 48 px between dated rows.
18
+
3
19
  ## 0.2.0
4
20
 
5
21
  - Layouts `arch`, `phone-clock-safe`, `phone-arch`, `desktop-safe`, `desktop-arch` (FLIER-1 D4 "Keep all layouts"); `LAYOUT_CATALOGUE` + `defaultLayoutFor`.
package/dist/index.d.mts CHANGED
@@ -140,6 +140,8 @@ interface RenderReport {
140
140
  pictureRegion: Rect;
141
141
  textBoxes: Rect[];
142
142
  cardBoxes: Rect[];
143
+ iconBoxes: Rect[];
144
+ logo: Rect | null;
143
145
  backdrop: Rect | null;
144
146
  }
145
147
  interface Palette {
package/dist/index.d.ts CHANGED
@@ -140,6 +140,8 @@ interface RenderReport {
140
140
  pictureRegion: Rect;
141
141
  textBoxes: Rect[];
142
142
  cardBoxes: Rect[];
143
+ iconBoxes: Rect[];
144
+ logo: Rect | null;
143
145
  backdrop: Rect | null;
144
146
  }
145
147
  interface Palette {
package/dist/index.js CHANGED
@@ -314,18 +314,19 @@ function headerGroups(doc, p, u) {
314
314
  return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
315
315
  }
316
316
  function itemGroup(item, l, u) {
317
- const lines = [l.script?.(item.date, 96)];
317
+ const lines = [l.script?.(item.date, 67)];
318
318
  item.schedule.forEach((row) => lines.push(l.strong?.(`${row.label} ${row.time}`, 42)));
319
319
  if (item.price) {
320
320
  lines.push(l.accent?.(item.price, 42));
321
321
  }
322
- return { lines, gapAfter: 26 * u };
322
+ return { lines, gapAfter: 48 * u };
323
323
  }
324
324
  function eventGroups(doc, l, u, withPrices) {
325
325
  const b = doc.blocks;
326
326
  const groups = [];
327
327
  b.schedule?.forEach((row) => groups.push({ lines: [l.strong?.(row.label, 66), l.accent?.(row.time, 60)], gapAfter: 30 * u }));
328
- (withPrices ? b.prices : void 0)?.forEach((row) => groups.push({ lines: [l.strong?.(row.amount, 66), l.accent?.(row.label, 52)], 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 }));
329
330
  return groups;
330
331
  }
331
332
  function listGroups(doc, l, u) {
@@ -346,15 +347,15 @@ function bodyGroups(doc, p, u, withPrices = true) {
346
347
  const groups = [];
347
348
  const isList = doc.layout === "list" /* List */;
348
349
  if (b.venue) {
349
- groups.push({ lines: b.venue.map((v) => l.venue?.(v, isList ? 48 : 58)), gapAfter: 40 * u, rule: { color: p.accent, height: 8 * u } });
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 } });
350
351
  }
351
352
  groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
352
353
  if (b.includes) {
353
354
  const lines = [l.accent?.(b.includes.heading, 38), ...b.includes.items.map((i) => l.item?.(i, 38))];
354
- groups.push({ lines, gapAfter: 24 * u, card: { color: p.card, pad: 28 * u }, uniform: true });
355
+ groups.push({ lines, gapAfter: 24 * u, uniform: true });
355
356
  }
356
357
  if (b.footer) {
357
- groups.push({ lines: [l.soft?.(b.footer, 34)], gapAfter: 0 });
358
+ groups.push({ lines: [l.soft?.(b.footer, 38)], gapAfter: 0 });
358
359
  }
359
360
  return groups;
360
361
  }
@@ -390,11 +391,11 @@ var LAYOUT_SPECS = {
390
391
  },
391
392
  ["desktop-safe" /* DesktopSafe */]: {
392
393
  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 },
393
- 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 }
394
+ 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] }
394
395
  },
395
396
  ["desktop-arch" /* DesktopArch */]: {
396
397
  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 },
397
- 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 }
398
+ 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 }
398
399
  }
399
400
  };
400
401
  var r = Math.round;
@@ -496,8 +497,14 @@ var RULE_WIDTH = 0.9;
496
497
  var WIDE = 1.6;
497
498
  var MIN_K = 0.02;
498
499
  var STEPS = 22;
500
+ var ICON_SCALE = 1.6;
501
+ var ICON_GAP = 0.35;
499
502
  var fontOf = (line, size, fonts) => `${line.weight} ${size}px ${fonts[line.role]}`;
500
- var emptyFlow = () => ({ texts: [], cards: [], rules: [], bullets: [] });
503
+ var emptyFlow = () => ({ texts: [], cards: [], rules: [], bullets: [], icons: [] });
504
+ var iconSize = (group, k) => {
505
+ const first = group.lines[0];
506
+ return group.icon && first ? first.size * k * LINE_HEIGHT * ICON_SCALE : 0;
507
+ };
501
508
  function lineSize(line, width, k, avail) {
502
509
  const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
503
510
  return Math.max(0, Math.min(line.size * k, line.size * (avail - indent) / Math.max(width, 1)));
@@ -508,6 +515,14 @@ function groupRatio(m, k, avail) {
508
515
  }
509
516
  return Math.min(...m.group.lines.map((line, i) => lineSize(line, m.widths[i] ?? 1, k, avail) / line.size));
510
517
  }
518
+ function placeIcon(group, d, x, textTop, textBottom, out) {
519
+ if (!group.icon || d <= 0) {
520
+ return textBottom;
521
+ }
522
+ const textH = textBottom - textTop;
523
+ 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 });
524
+ return textTop + Math.max(textH, d);
525
+ }
511
526
  function placeGroup(m, k, col, y0, out) {
512
527
  const { group } = m;
513
528
  let y = y0;
@@ -518,10 +533,13 @@ function placeGroup(m, k, col, y0, out) {
518
533
  const pad = group.card ? group.card.pad * k : 0;
519
534
  const top = y;
520
535
  y += pad;
521
- const ratio = groupRatio(m, k, col.w - 2 * pad);
536
+ const d = iconSize(group, k);
537
+ const iconIndent = d > 0 ? d * (1 + ICON_GAP) : 0;
538
+ const avail = col.w - 2 * pad - iconIndent;
539
+ const ratio = groupRatio(m, k, avail);
522
540
  group.lines.forEach((line, i) => {
523
- const size = ratio === null ? lineSize(line, m.widths[i] ?? 1, k, col.w - 2 * pad) : line.size * ratio;
524
- const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
541
+ const size = ratio === null ? lineSize(line, m.widths[i] ?? 1, k, avail) : line.size * ratio;
542
+ const indent = (line.bullet ? line.size * k * BULLET_INDENT : 0) + iconIndent;
525
543
  const w = (m.widths[i] ?? 0) * size / line.size;
526
544
  const x = col.right ? col.x + col.w - pad - w : col.x + pad + indent;
527
545
  out?.texts.push({ text: line.text, font: "", color: line.color, box: { x, y, w, h: size * LINE_HEIGHT } });
@@ -530,36 +548,62 @@ function placeGroup(m, k, col, y0, out) {
530
548
  }
531
549
  y += size * LINE_HEIGHT;
532
550
  });
551
+ y = placeIcon(group, d, col.x + pad, top + pad, y, out);
533
552
  y += pad;
553
+ if (group.rule && group.ruleBelow) {
554
+ y += group.rule.height * k * (RULE_GAP - 1);
555
+ out?.rules.push({ rect: { x: col.x, y, w: col.w * RULE_WIDTH, h: group.rule.height * k }, color: group.rule.color });
556
+ y += group.rule.height * k;
557
+ }
534
558
  if (group.card) {
535
559
  out?.cards.unshift({ rect: { x: col.x, y: top, w: col.w, h: y - top }, color: group.card.color });
536
560
  }
537
561
  return y + group.gapAfter * k - y0;
538
562
  }
539
- function placeColumn(groups, k, col, out) {
563
+ function placeColumn(groups, k, col, out, spread = 0) {
540
564
  let h = 0;
541
565
  for (const g of groups) {
542
- h += placeGroup(g, k, col, col.y + h, out);
566
+ h += placeGroup(g, k, col, col.y + h, out) + spread;
567
+ }
568
+ return h - spread;
569
+ }
570
+ var columnSum = (heights, from, to) => heights.slice(from, to).reduce((a, b) => a + b, 0);
571
+ var tallest = (heights, cut) => {
572
+ const left = heights.slice(0, cut).reduce((a, b) => a + b, 0);
573
+ const right = heights.slice(cut).reduce((a, b) => a + b, 0);
574
+ return Math.max(left, right);
575
+ };
576
+ function keepTogether(groups, heights, cut) {
577
+ const breaks = (c) => groups[c - 1]?.group.keepWithNext === true;
578
+ let back = cut;
579
+ while (back > 1 && breaks(back)) {
580
+ back--;
581
+ }
582
+ let fwd = cut;
583
+ while (fwd < groups.length - 1 && breaks(fwd)) {
584
+ fwd++;
585
+ }
586
+ const candidates = [back, fwd].filter((c) => !breaks(c));
587
+ if (candidates.length === 0) {
588
+ return cut;
543
589
  }
544
- return h;
590
+ return candidates.reduce((best, c) => tallest(heights, c) < tallest(heights, best) ? c : best);
545
591
  }
546
592
  function splitColumns(groups, zone) {
547
593
  if (zone.w / Math.max(zone.h, 1) <= WIDE || groups.length < 2) {
548
594
  return [{ col: zone, groups }];
549
595
  }
550
- const heights = groups.map((g) => placeGroup(g, 1, zone, 0, null));
551
- const half = heights.reduce((a, b) => a + b, 0) / 2;
552
- let acc = 0;
553
- let cut = 1;
554
- heights.forEach((h, i) => {
555
- if (acc < half) {
556
- cut = i + 1;
557
- }
558
- acc += h;
559
- });
560
- cut = Math.min(cut, groups.length - 1);
561
596
  const gap = Math.round(zone.w * 0.04);
562
597
  const w = (zone.w - gap) / 2;
598
+ const heights = groups.map((g) => placeGroup(g, 1, { ...zone, w }, 0, null));
599
+ const total = columnSum(heights, 0, heights.length);
600
+ let cut = 1;
601
+ for (let c = 2; c < groups.length; c++) {
602
+ if (Math.abs(total - 2 * columnSum(heights, 0, c)) < Math.abs(total - 2 * columnSum(heights, 0, cut))) {
603
+ cut = c;
604
+ }
605
+ }
606
+ cut = keepTogether(groups, heights, cut);
563
607
  return [
564
608
  { col: { ...zone, w }, groups: groups.slice(0, cut) },
565
609
  { col: { ...zone, x: zone.x + w + gap, w }, groups: groups.slice(cut) }
@@ -589,7 +633,9 @@ function flow(ctx, groups, zone, fonts) {
589
633
  }
590
634
  }
591
635
  const out = emptyFlow();
592
- columns.forEach((c) => placeColumn(c.groups, lo, c.col, out));
636
+ const heights = columns.map((c) => placeColumn(c.groups, lo, c.col, null));
637
+ const end = Math.max(...heights);
638
+ 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));
593
639
  const lines = groups.flatMap((g) => g.lines);
594
640
  out.texts.forEach((t, i) => {
595
641
  const line = lines[i];
@@ -598,11 +644,75 @@ function flow(ctx, groups, zone, fonts) {
598
644
  return out;
599
645
  }
600
646
 
647
+ // src/render/regions.ts
648
+ var LINE_HEIGHT2 = 1.2;
649
+ var BODY_GAP = 40;
650
+ var BOTTOM_SIDE = 0.1;
651
+ function flowFlanks(ctx, groups, flanks, fonts) {
652
+ const half = Math.ceil(groups.length / 2);
653
+ 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
+ }
655
+ function smallestPerLine(groups, results) {
656
+ const sizes = results.flatMap((r3) => r3.texts.map((t) => t.box.h / LINE_HEIGHT2));
657
+ const smallest = [];
658
+ let i = 0;
659
+ groups.forEach(
660
+ (g) => g.lines.forEach((_line, j) => {
661
+ smallest[j] = Math.min(smallest[j] ?? Infinity, sizes[i] ?? Infinity);
662
+ i++;
663
+ })
664
+ );
665
+ return smallest;
666
+ }
667
+ 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) })) }));
670
+ return flowFlanks(ctx, sized, flanks, fonts);
671
+ }
672
+ function measuredEnd(f, fallback) {
673
+ 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);
674
+ return bottoms.length > 0 ? Math.max(...bottoms) : fallback;
675
+ }
676
+ function pictureRegion(layout, geo, body, u, frame) {
677
+ if (layout !== "picture-bottom" /* PictureBottom */) {
678
+ return geo.picture;
679
+ }
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 };
683
+ }
684
+
601
685
  // src/render/render.ts
602
686
  var BASE = 1080;
603
687
  var LOGO_SHARE = 0.26;
604
688
  var CARD_RADIUS = 0.035;
605
689
  var CAP_TOP = 0.15;
690
+ var PIN_HEAD_Y = 0.08;
691
+ var PIN_HEAD_R = 0.19;
692
+ var PIN_TIP_Y = 0.28;
693
+ var PIN_HOLE = 0.42;
694
+ var ARC_START = 0.75;
695
+ var ARC_END = 2.25;
696
+ function drawPin(ctx, r3, color, glyph) {
697
+ const cx = r3.x + r3.w / 2;
698
+ const cy = r3.y + r3.h / 2;
699
+ const hy = cy - r3.w * PIN_HEAD_Y;
700
+ const rad = r3.w * PIN_HEAD_R;
701
+ ctx.fillStyle = color;
702
+ ctx.beginPath();
703
+ ctx.arc(cx, cy, r3.w / 2, 0, Math.PI * 2);
704
+ ctx.fill();
705
+ ctx.fillStyle = glyph;
706
+ ctx.beginPath();
707
+ ctx.moveTo(cx, cy + r3.w * PIN_TIP_Y);
708
+ ctx.arc(cx, hy, rad, Math.PI * ARC_START, Math.PI * ARC_END);
709
+ ctx.closePath();
710
+ ctx.fill();
711
+ ctx.fillStyle = color;
712
+ ctx.beginPath();
713
+ ctx.arc(cx, hy, rad * PIN_HOLE, 0, Math.PI * 2);
714
+ ctx.fill();
715
+ }
606
716
  function roundRect(ctx, r3, radius) {
607
717
  const rad = Math.min(radius, r3.w / 2, r3.h / 2);
608
718
  ctx.beginPath();
@@ -629,6 +739,7 @@ function drawFlow(ctx, f, radius) {
629
739
  ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
630
740
  ctx.fill();
631
741
  });
742
+ f.icons.forEach((i) => drawPin(ctx, i.rect, i.color, i.glyph));
632
743
  ctx.textAlign = "left";
633
744
  ctx.textBaseline = "top";
634
745
  f.texts.forEach((t) => {
@@ -663,10 +774,6 @@ function drawArch(ctx, r3, color) {
663
774
  ctx.arc(r3.x + rad, r3.y + rad, rad, Math.PI, Math.PI * 2);
664
775
  ctx.fill();
665
776
  }
666
- function flankFlows(ctx, groups, flanks, fonts) {
667
- const half = Math.ceil(groups.length / 2);
668
- return flanks.map((zone, i) => i === 0 ? flow(ctx, groups.slice(0, half), zone, fonts) : flow(ctx, groups.slice(half), { ...zone, right: true }, fonts));
669
- }
670
777
  function capAligned(zone, head) {
671
778
  const first = head.texts[0]?.box;
672
779
  const shift = first ? Math.min(Math.max(0, Math.round(first.y + first.h * CAP_TOP) - zone.y), Math.round(zone.h / 2)) : 0;
@@ -700,20 +807,21 @@ function render(input, assets, canvas) {
700
807
  const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
701
808
  const flanks = geo.flanks ?? [];
702
809
  const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
703
- const sides = flanks.length > 0 ? flankFlows(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
810
+ const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
704
811
  const radius = Math.min(width, height) * CARD_RADIUS;
705
812
  [head, body, ...sides].forEach((f) => drawFlow(ctx, f, radius));
706
813
  const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
707
- if (logoImage) {
708
- const r3 = placeContain(logoImage, capAligned(zones.logo, head), "top" /* Top */, 1, 1);
709
- ctx.drawImage(logoImage, r3.x, r3.y, r3.w, r3.h);
814
+ const logo = logoImage ? placeContain(logoImage, geo.logo ? zones.logo : capAligned(zones.logo, head), "top" /* Top */, 1, 1) : null;
815
+ if (logoImage && logo) {
816
+ ctx.drawImage(logoImage, logo.x, logo.y, logo.w, logo.h);
710
817
  }
711
818
  const image = doc.picture ? assets[doc.picture.imageId] : void 0;
712
819
  const backdrop = image ? archRect(geo) : null;
713
820
  if (backdrop) {
714
821
  drawArch(ctx, backdrop, palette.arch);
715
822
  }
716
- const picture = image && doc.picture ? placeContain(image, geo.picture, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
823
+ const region = pictureRegion(doc.layout, geo, body, u, { width, height });
824
+ const picture = image && doc.picture ? placeContain(image, region, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
717
825
  if (image && picture) {
718
826
  ctx.drawImage(image, picture.x, picture.y, picture.w, picture.h);
719
827
  }
@@ -721,9 +829,11 @@ function render(input, assets, canvas) {
721
829
  width,
722
830
  height,
723
831
  picture,
724
- pictureRegion: geo.picture,
832
+ pictureRegion: region,
725
833
  textBoxes: [head, body, ...sides].flatMap((f) => f.texts.map((t) => t.box)),
726
834
  cardBoxes: body.cards.map((c) => c.rect),
835
+ iconBoxes: [head, body, ...sides].flatMap((f) => f.icons.map((i) => i.rect)),
836
+ logo,
727
837
  backdrop
728
838
  };
729
839
  }