@dloizides/poster-render 0.2.5 → 0.2.7

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.7
4
+
5
+ - P-B "Phone arch": the title block starts at the top margin (0.05h) instead of under a clock zone; the arch picture starts at 0.47h and grows.
6
+ - The footer stays attached under the includes list: columns never spread space between them, and both stay in one column.
7
+
3
8
  ## 0.2.5
4
9
 
5
10
  - Own-key checks: the schema validator and the asset lookup use `Object.hasOwn`, so a `__proto__` key is rejected and an imageId such as `constructor` is reported missing.
package/dist/index.js CHANGED
@@ -299,7 +299,7 @@ var flanksBeside = (pic, top, bottom2, m) => [
299
299
  [pic[0] + pic[2] + m, top, 1 - pic[0] - pic[2] - 2 * m, bottom2 - top]
300
300
  ];
301
301
  var ARCH_PIC = [0.22, 0.42, 0.56, 0.58];
302
- var PHONE_ARCH_PIC = [0.2, 0.6, 0.6, 0.4];
302
+ var PHONE_ARCH_PIC = [0.2, 0.47, 0.6, 0.53];
303
303
  var DESK_ARCH_PIC = [0.355, 0.07, 0.32, 0.93];
304
304
  var ARCH_LANDSCAPE = {
305
305
  header: [0.04, 0.06, 0.3, 0.5],
@@ -319,7 +319,7 @@ var LAYOUT_SPECS = {
319
319
  landscape: { header: [0.04, 0.2, 0.52, 0.3], body: [0.04, 0.52, 0.52, 0.42], picture: [0.6, 0.12, 0.4, 0.88], alignX: 1 }
320
320
  },
321
321
  ["phone-arch" /* PhoneArch */]: {
322
- portrait: { header: [0.045, 0.3, 0.91, 0.13], body: [0.045, 0.44, 0.91, 0.14], picture: PHONE_ARCH_PIC, alignX: 0.5, flanks: flanksBeside(PHONE_ARCH_PIC, 0.63, 0.94, 0.03), arch: 0.08 },
322
+ portrait: { header: [0.045, 0.05, 0.91, 0.16], body: [0.045, 0.22, 0.91, 0.21], picture: PHONE_ARCH_PIC, alignX: 0.5, flanks: flanksBeside(PHONE_ARCH_PIC, 0.5, 0.94, 0.03), arch: 0.08 },
323
323
  landscape: { ...ARCH_LANDSCAPE, header: [0.04, 0.16, 0.3, 0.42], picture: [0.36, 0.16, 0.3, 0.84] }
324
324
  },
325
325
  ["desktop-safe" /* DesktopSafe */]: {
@@ -353,6 +353,9 @@ var PICTURE_SIDE = {
353
353
  ["picture-left" /* PictureLeft */]: DEFAULT_SIDE,
354
354
  ["list" /* List */]: [0.5, 0.36]
355
355
  };
356
+ var SHORT_PORTRAIT = 1.5;
357
+ var SHORT_PICTURE_W = 0.5;
358
+ var SHORT_PICTURE_H = 0.75;
356
359
  var ANCHOR_Y = { ["top" /* Top */]: 0, ["center" /* Center */]: 0.5, ["bottom" /* Bottom */]: 1 };
357
360
  var r2 = Math.round;
358
361
  function side(w, h, fraction) {
@@ -376,6 +379,20 @@ function side(w, h, fraction) {
376
379
  alignX: 1
377
380
  };
378
381
  }
382
+ function shortSide(w, h) {
383
+ const m = r2(Math.min(w, h) * MARGIN);
384
+ const picW = r2(w * SHORT_PICTURE_W);
385
+ const picH = r2(h * SHORT_PICTURE_H);
386
+ const top = r2(h * HEADER_PORTRAIT);
387
+ const textW = w - picW - 2 * m;
388
+ return {
389
+ header: { x: m, y: m, w: textW, h: top - 2 * m },
390
+ body: { x: m, y: top, w: textW, h: h - top - m },
391
+ picture: { x: w - picW, y: h - picH, w: picW, h: picH },
392
+ logo: { x: w - picW + m, y: m, w: picW - 2 * m, h: h - picH - 2 * m },
393
+ alignX: 1
394
+ };
395
+ }
379
396
  function bottom(w, h, share) {
380
397
  const m = r2(Math.min(w, h) * MARGIN);
381
398
  const gap = r2(Math.min(w, h) * BOTTOM_GAP);
@@ -410,11 +427,12 @@ function geometry(layout, w, h, give = 0) {
410
427
  if (layout === "picture-bottom" /* PictureBottom */) {
411
428
  return bottom(w, h, BOTTOM_PICTURE_W - give);
412
429
  }
413
- const g = side(w, h, PICTURE_SIDE[layout] ?? DEFAULT_SIDE);
430
+ const isSide = layout === "picture-right" /* PictureRight */ || layout === "picture-left" /* PictureLeft */;
431
+ const g = isSide && h >= w && h < w * SHORT_PORTRAIT ? shortSide(w, h) : side(w, h, PICTURE_SIDE[layout] ?? DEFAULT_SIDE);
414
432
  if (layout !== "picture-left" /* PictureLeft */) {
415
433
  return g;
416
434
  }
417
- return { header: mirror(g.header, w), body: mirror(g.body, w), picture: mirror(g.picture, w), alignX: 1 - g.alignX };
435
+ 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 };
418
436
  }
419
437
  function placeContain(image, region, fit) {
420
438
  const { anchor, alignX, scale } = fit;
@@ -480,12 +498,33 @@ function itemGroup(item, l, u) {
480
498
  }
481
499
  return { lines, gapAfter: GAP.item * u };
482
500
  }
483
- function eventGroups(doc, l, u, withPrices) {
501
+ var WRAP_FROM = 14;
502
+ function halves(text2) {
503
+ const mid = text2.length / 2;
504
+ const spaces = [...text2].flatMap((c, i) => c === " " ? [i] : []);
505
+ if (text2.length < WRAP_FROM || spaces.length === 0) {
506
+ return [text2];
507
+ }
508
+ const cut = spaces.reduce((best, i) => Math.abs(i - mid) < Math.abs(best - mid) ? i : best);
509
+ return [text2.slice(0, cut), text2.slice(cut + 1)];
510
+ }
511
+ function wrapped(line, wrap) {
512
+ const parts = wrap ? halves(line.text) : [line.text];
513
+ return parts.map((text2, i) => i === 0 ? { ...line, text: text2 } : { ...line, text: text2, bullet: void 0, hang: line.bullet !== void 0 });
514
+ }
515
+ function eventGroups(doc, l, u, options) {
484
516
  const b = doc.blocks;
485
517
  const groups = [];
486
- b.schedule?.forEach((row) => groups.push({ lines: [l.strong(row.label, SIZE.scheduleLabel), l.accent(row.time, SIZE.scheduleTime)], gapAfter: GAP.row * u }));
487
- const prices = (withPrices ? b.prices : void 0) ?? [];
488
- 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 }));
518
+ const schedule2 = b.schedule ?? [];
519
+ schedule2.forEach((row, i) => {
520
+ const lines = options.compact ? [l.strong(`${row.label} ${row.time}`, SIZE.scheduleLabel)] : [l.strong(row.label, SIZE.scheduleLabel), l.accent(row.time, SIZE.scheduleTime)];
521
+ groups.push({ lines, gapAfter: GAP.row * u, keepWithNext: i < schedule2.length - 1 });
522
+ });
523
+ const prices = (options.withPrices === false ? void 0 : b.prices) ?? [];
524
+ prices.forEach((row, i) => {
525
+ const lines = options.compact ? [l.strong(`${row.amount} ${row.label}`, SIZE.priceAmount)] : [l.strong(row.amount, SIZE.priceAmount), l.accent(row.label, SIZE.priceLabel)];
526
+ groups.push({ lines, gapAfter: GAP.row * u, keepWithNext: i < prices.length - 1 });
527
+ });
489
528
  return groups;
490
529
  }
491
530
  function listGroups(doc, l, u) {
@@ -500,22 +539,23 @@ function flankPriceGroups(doc, p, u) {
500
539
  const l = makers(p, u);
501
540
  return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong(row.amount, SIZE.flankAmount), l.accent(row.label, SIZE.flankLabel)], gapAfter: GAP.row * u }));
502
541
  }
503
- function bodyGroups(doc, p, u, withPrices = true) {
542
+ function bodyGroups(doc, p, u, options = {}) {
504
543
  const l = makers(p, u);
505
544
  const b = doc.blocks;
506
545
  const groups = [];
507
546
  const isList = doc.layout === "list" /* List */;
508
547
  if (b.venue) {
509
548
  const size = isList ? SIZE.listVenue : SIZE.venue;
510
- 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 } });
549
+ const keepWithNext = !isList && (b.schedule?.length ?? 0) > 0;
550
+ 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 });
511
551
  }
512
- groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
552
+ groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, options));
513
553
  if (b.includes) {
514
- const lines = [l.accent(b.includes.heading, SIZE.small), ...b.includes.items.map((i) => l.item(i, SIZE.small))];
515
- groups.push({ lines, gapAfter: GAP.includes * u, uniform: true });
554
+ 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));
555
+ groups.push({ lines, gapAfter: GAP.includes * u, uniform: true, keepWithNext: b.footer !== void 0, attachNext: b.footer !== void 0 });
516
556
  }
517
557
  if (b.footer) {
518
- groups.push({ lines: [l.soft(b.footer, SIZE.small)], gapAfter: 0 });
558
+ groups.push({ lines: wrapped(l.soft(b.footer, SIZE.small), options.wrap), gapAfter: 0 });
519
559
  }
520
560
  return groups;
521
561
  }
@@ -536,12 +576,13 @@ var ICON_SCALE = 1.6;
536
576
  var ICON_GAP = 0.35;
537
577
  var fontOf = (line, size, fonts) => `${line.weight} ${size}px ${fonts[line.role]}`;
538
578
  var emptyFlow = () => ({ texts: [], cards: [], rules: [], bullets: [], icons: [] });
579
+ var indented = (line) => line.bullet !== void 0 || line.hang === true;
539
580
  var iconSize = (group, k) => {
540
581
  const first = group.lines[0];
541
582
  return group.icon && first ? first.size * k * LINE_HEIGHT * ICON_SCALE : 0;
542
583
  };
543
584
  function lineSize(line, width, k, avail) {
544
- const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
585
+ const indent = indented(line) ? line.size * k * BULLET_INDENT : 0;
545
586
  return Math.max(0, Math.min(line.size * k, line.size * (avail - indent) / Math.max(width, 1)));
546
587
  }
547
588
  function groupRatio(m, k, avail) {
@@ -576,7 +617,7 @@ function placeGroup(m, at, out) {
576
617
  const ratio = groupRatio(m, k, avail);
577
618
  group.lines.forEach((line, i) => {
578
619
  const size = ratio === null ? lineSize(line, m.widths[i] ?? 1, k, avail) : line.size * ratio;
579
- const indent = (line.bullet ? line.size * k * BULLET_INDENT : 0) + iconIndent;
620
+ const indent = (indented(line) ? line.size * k * BULLET_INDENT : 0) + iconIndent;
580
621
  const w = (m.widths[i] ?? 0) * size / line.size;
581
622
  const x = col.right ? col.x + col.w - pad - w : col.x + pad + indent;
582
623
  out?.texts.push({ text: line.text, font: "", color: line.color, box: { x, y, w, h: size * LINE_HEIGHT } });
@@ -600,11 +641,15 @@ function placeGroup(m, at, out) {
600
641
  function placeColumn(groups, at, out) {
601
642
  const { k, col, spread = 0 } = at;
602
643
  let h = 0;
603
- for (const g of groups) {
604
- h += placeGroup(g, { k, col, y0: col.y + h }, out) + spread;
605
- }
606
- return h - spread;
644
+ groups.forEach((g, i) => {
645
+ h += placeGroup(g, { k, col, y0: col.y + h }, out);
646
+ if (i < groups.length - 1 && g.group.attachNext !== true) {
647
+ h += spread;
648
+ }
649
+ });
650
+ return h;
607
651
  }
652
+ var spreadGaps = (groups) => groups.slice(0, -1).filter((g) => g.group.attachNext !== true).length;
608
653
  var columnSum = (heights, from, to) => heights.slice(from, to).reduce((a, b) => a + b, 0);
609
654
  var tallest = (heights, cut) => {
610
655
  const left = heights.slice(0, cut).reduce((a, b) => a + b, 0);
@@ -684,7 +729,10 @@ function flow(ctx, groups, zone, fonts) {
684
729
  const out = emptyFlow();
685
730
  const heights = columns.map((c) => placeColumn(c.groups, { k: lo, col: c.col }, null));
686
731
  const end = Math.max(...heights);
687
- 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));
732
+ columns.forEach((c, i) => {
733
+ const gaps = spreadGaps(c.groups);
734
+ placeColumn(c.groups, { k: lo, col: c.col, spread: gaps > 0 && columns.length > 1 ? (end - (heights[i] ?? end)) / gaps : 0 }, out);
735
+ });
688
736
  const lines = groups.flatMap((g) => g.lines);
689
737
  out.texts.forEach((t, i) => {
690
738
  const line = lines[i];
@@ -729,35 +777,6 @@ function pictureRegion(layout, geo, body, frame) {
729
777
  return { x: geo.picture.x, y, w: geo.picture.w, h: frame.height - y };
730
778
  }
731
779
 
732
- // src/render/yieldToText.ts
733
- function yieldToText(geo, give) {
734
- const { picture: p, body: b } = geo;
735
- if (give <= 0) {
736
- return geo;
737
- }
738
- if (p.x >= b.x + b.w) {
739
- const d = Math.round(p.w * give);
740
- return { ...geo, picture: { ...p, x: p.x + d, w: p.w - d }, body: { ...b, w: b.w + d } };
741
- }
742
- if (p.x + p.w <= b.x) {
743
- const d = Math.round(p.w * give);
744
- return { ...geo, picture: { ...p, w: p.w - d }, body: { ...b, x: b.x - d, w: b.w + d } };
745
- }
746
- if (p.y >= b.y + b.h) {
747
- const d = Math.round(p.h * give);
748
- const stacked = geo.header.y + geo.header.h <= b.y;
749
- const toHeader = stacked ? Math.round(d / 2) : 0;
750
- return {
751
- ...geo,
752
- header: { ...geo.header, h: geo.header.h + toHeader },
753
- picture: { ...p, y: p.y + d, h: p.h - d },
754
- body: { ...b, y: b.y + toHeader, h: b.h + d - toHeader },
755
- flanks: geo.flanks?.map((f) => ({ ...f, y: f.y + d, h: f.h - d }))
756
- };
757
- }
758
- return geo;
759
- }
760
-
761
780
  // src/render/fitToFloor.ts
762
781
  var FLOOR_PX = 30;
763
782
  var BASE = 1080;
@@ -765,33 +784,88 @@ var LINE_HEIGHT3 = 1.2;
765
784
  var LOGO_SHARE = 0.26;
766
785
  var GIVE_STEP = 0.025;
767
786
  var GIVE_STEPS = 32;
787
+ var SHIFT_STEP = 0.06;
788
+ var SHIFT_STEPS = 8;
789
+ var MARGIN2 = 0.045;
768
790
  function splitHeader(header, hasLogo) {
769
791
  const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
770
792
  return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
771
793
  }
772
- function flowAll(ctx, input, geo, u) {
794
+ function flowAll(ctx, input, geo, reflow) {
773
795
  const { doc, palette, fonts } = input;
796
+ const u = Math.min(input.width, input.height) / BASE;
774
797
  const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
775
798
  const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
776
799
  const flanks = geo.flanks ?? [];
777
- const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
800
+ const body = flow(ctx, bodyGroups(doc, palette, u, { withPrices: flanks.length === 0, compact: reflow.compact, wrap: reflow.wrap }), geo.body, fonts);
778
801
  const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
779
802
  return { geo, zones, head, body, sides };
780
803
  }
781
804
  var smallest = (t) => Math.min(Infinity, ...[t.head, t.body, ...t.sides].flatMap((f) => f.texts.map((x) => x.box.h / LINE_HEIGHT3)));
782
- function geometryWithGive(layout, width, height, give) {
783
- return layout === "picture-bottom" /* PictureBottom */ ? geometry(layout, width, height, give) : yieldToText(geometry(layout, width, height), give);
805
+ function shiftHeader(geo, shift) {
806
+ const { header, body } = geo;
807
+ const stacked = header.y + header.h <= body.y && header.x < body.x + body.w && body.x < header.x + header.w;
808
+ const d = Math.round(header.h * shift);
809
+ if (!stacked || d === 0 || body.h - d <= 0) {
810
+ return geo;
811
+ }
812
+ return { ...geo, header: { ...header, h: header.h - d }, body: { ...body, y: body.y - d, h: body.h + d } };
784
813
  }
785
- function fitToFloor(ctx, input) {
786
- const { doc, width, height } = input;
787
- const u = Math.min(width, height) / BASE;
788
- const floor = FLOOR_PX * u;
789
- let fitted = flowAll(ctx, input, geometryWithGive(doc.layout, width, height, 0), u);
814
+ function liftHeader(geo, top) {
815
+ const { header } = geo;
816
+ return header.y > top ? { ...geo, header: { ...header, y: top, h: header.h + header.y - top } } : geo;
817
+ }
818
+ function fitBottom(ctx, input, floor) {
819
+ const { width, height } = input;
820
+ const none = { compact: false, wrap: false};
821
+ let fitted = flowAll(ctx, input, geometry("picture-bottom" /* PictureBottom */, width, height), none);
790
822
  for (let step = 1; step <= GIVE_STEPS && smallest(fitted) < floor; step++) {
791
- fitted = flowAll(ctx, input, geometryWithGive(doc.layout, width, height, step * GIVE_STEP), u);
823
+ fitted = flowAll(ctx, input, geometry("picture-bottom" /* PictureBottom */, width, height, step * GIVE_STEP), none);
792
824
  }
793
825
  return fitted;
794
826
  }
827
+ function yieldPicture(geo, give) {
828
+ const { picture: p, body: b } = geo;
829
+ const d = Math.round(p.w * give);
830
+ return { ...geo, picture: { ...p, x: p.x + d, w: p.w - d }, body: { ...b, w: b.w + d } };
831
+ }
832
+ function fitList(ctx, input, floor, base2) {
833
+ const last = { compact: true, wrap: true};
834
+ let fitted = flowAll(ctx, input, base2, last);
835
+ for (let step = 1; step <= GIVE_STEPS && smallest(fitted) < floor; step++) {
836
+ fitted = flowAll(ctx, input, yieldPicture(base2, step * GIVE_STEP), last);
837
+ }
838
+ return fitted;
839
+ }
840
+ var SHIFTS = [0, ...Array.from({ length: SHIFT_STEPS }, (_v, i) => [(i + 1) * SHIFT_STEP, -(i + 1) * SHIFT_STEP]).flat()];
841
+ var TIERS = [
842
+ { compact: false, wrap: false, lift: false },
843
+ { compact: true, wrap: false, lift: false },
844
+ { compact: true, wrap: true, lift: false },
845
+ { compact: true, wrap: true, lift: true }
846
+ ];
847
+ var REFLOWS = TIERS.flatMap((tier) => SHIFTS.map((shift) => ({ ...tier, shift })));
848
+ function fitToFloor(ctx, input) {
849
+ const { doc, width, height } = input;
850
+ const floor = FLOOR_PX * Math.min(width, height) / BASE;
851
+ if (doc.layout === "picture-bottom" /* PictureBottom */) {
852
+ return fitBottom(ctx, input, floor);
853
+ }
854
+ const base2 = geometry(doc.layout, width, height);
855
+ const top = Math.round(Math.min(width, height) * MARGIN2);
856
+ let best = null;
857
+ for (const reflow of REFLOWS) {
858
+ const fitted = flowAll(ctx, input, shiftHeader(reflow.lift ? liftHeader(base2, top) : base2, reflow.shift), reflow);
859
+ if (smallest(fitted) >= floor) {
860
+ return fitted;
861
+ }
862
+ best = best === null || smallest(fitted) > smallest(best) ? fitted : best;
863
+ }
864
+ if (doc.layout === "list" /* List */) {
865
+ return fitList(ctx, input, floor, base2);
866
+ }
867
+ return best ?? flowAll(ctx, input, base2, { compact: false, wrap: false});
868
+ }
795
869
 
796
870
  // src/render/render.ts
797
871
  var BASE2 = 1080;
@@ -963,7 +1037,7 @@ var LAYOUT_CATALOGUE = [
963
1037
  { id: "picture-bottom" /* PictureBottom */, designId: "picture-bottom", label: "Picture bottom", description: "Titles and details on top, the picture across the bottom half.", formats: POSTERS },
964
1038
  { id: "list" /* List */, designId: "list", label: "Upcoming list", description: "Several dated events as a list beside the picture.", formats: POSTERS },
965
1039
  { id: "phone-clock-safe" /* PhoneClockSafe */, designId: "P-A", label: "Phone, clock clear", description: "Lock-screen wallpaper: top kept clear for the clock, details on the left, picture on the right.", formats: ["phone" /* Phone */] },
966
- { id: "phone-arch" /* PhoneArch */, designId: "P-B", label: "Phone arch", description: "Lock-screen wallpaper: top kept clear for the clock, details in the middle, the picture in an arch at the bottom.", formats: ["phone" /* Phone */] },
1040
+ { id: "phone-arch" /* PhoneArch */, designId: "P-B", label: "Phone arch", description: "Lock-screen wallpaper: titles at the top, details under them, the picture in an arch at the bottom.", formats: ["phone" /* Phone */] },
967
1041
  { id: "desktop-safe" /* DesktopSafe */, designId: "D-A", label: "Desktop, icons clear", description: "Desktop wallpaper: text on the left clear of desktop icons and the taskbar, the picture on the right.", formats: ["desktop" /* Desktop */] },
968
1042
  { id: "desktop-arch" /* DesktopArch */, designId: "D-B", label: "Desktop arch", description: "Desktop wallpaper: titles left, the picture in a centred arch, details right.", formats: ["desktop" /* Desktop */] }
969
1043
  ];