@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/dist/index.mjs CHANGED
@@ -297,7 +297,7 @@ var flanksBeside = (pic, top, bottom2, m) => [
297
297
  [pic[0] + pic[2] + m, top, 1 - pic[0] - pic[2] - 2 * m, bottom2 - top]
298
298
  ];
299
299
  var ARCH_PIC = [0.22, 0.42, 0.56, 0.58];
300
- var PHONE_ARCH_PIC = [0.2, 0.6, 0.6, 0.4];
300
+ var PHONE_ARCH_PIC = [0.2, 0.47, 0.6, 0.53];
301
301
  var DESK_ARCH_PIC = [0.355, 0.07, 0.32, 0.93];
302
302
  var ARCH_LANDSCAPE = {
303
303
  header: [0.04, 0.06, 0.3, 0.5],
@@ -317,7 +317,7 @@ var LAYOUT_SPECS = {
317
317
  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 }
318
318
  },
319
319
  ["phone-arch" /* PhoneArch */]: {
320
- 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 },
320
+ 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 },
321
321
  landscape: { ...ARCH_LANDSCAPE, header: [0.04, 0.16, 0.3, 0.42], picture: [0.36, 0.16, 0.3, 0.84] }
322
322
  },
323
323
  ["desktop-safe" /* DesktopSafe */]: {
@@ -351,6 +351,9 @@ var PICTURE_SIDE = {
351
351
  ["picture-left" /* PictureLeft */]: DEFAULT_SIDE,
352
352
  ["list" /* List */]: [0.5, 0.36]
353
353
  };
354
+ var SHORT_PORTRAIT = 1.5;
355
+ var SHORT_PICTURE_W = 0.5;
356
+ var SHORT_PICTURE_H = 0.75;
354
357
  var ANCHOR_Y = { ["top" /* Top */]: 0, ["center" /* Center */]: 0.5, ["bottom" /* Bottom */]: 1 };
355
358
  var r2 = Math.round;
356
359
  function side(w, h, fraction) {
@@ -374,6 +377,20 @@ function side(w, h, fraction) {
374
377
  alignX: 1
375
378
  };
376
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
+ }
377
394
  function bottom(w, h, share) {
378
395
  const m = r2(Math.min(w, h) * MARGIN);
379
396
  const gap = r2(Math.min(w, h) * BOTTOM_GAP);
@@ -408,11 +425,12 @@ function geometry(layout, w, h, give = 0) {
408
425
  if (layout === "picture-bottom" /* PictureBottom */) {
409
426
  return bottom(w, h, BOTTOM_PICTURE_W - give);
410
427
  }
411
- const g = side(w, h, PICTURE_SIDE[layout] ?? DEFAULT_SIDE);
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);
412
430
  if (layout !== "picture-left" /* PictureLeft */) {
413
431
  return g;
414
432
  }
415
- 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 };
416
434
  }
417
435
  function placeContain(image, region, fit) {
418
436
  const { anchor, alignX, scale } = fit;
@@ -478,12 +496,33 @@ function itemGroup(item, l, u) {
478
496
  }
479
497
  return { lines, gapAfter: GAP.item * u };
480
498
  }
481
- function eventGroups(doc, l, u, withPrices) {
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];
505
+ }
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)];
508
+ }
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) {
482
514
  const b = doc.blocks;
483
515
  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 }));
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
+ });
487
526
  return groups;
488
527
  }
489
528
  function listGroups(doc, l, u) {
@@ -498,22 +537,23 @@ function flankPriceGroups(doc, p, u) {
498
537
  const l = makers(p, u);
499
538
  return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong(row.amount, SIZE.flankAmount), l.accent(row.label, SIZE.flankLabel)], gapAfter: GAP.row * u }));
500
539
  }
501
- function bodyGroups(doc, p, u, withPrices = true) {
540
+ function bodyGroups(doc, p, u, options = {}) {
502
541
  const l = makers(p, u);
503
542
  const b = doc.blocks;
504
543
  const groups = [];
505
544
  const isList = doc.layout === "list" /* List */;
506
545
  if (b.venue) {
507
546
  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 } });
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 });
509
549
  }
510
- 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));
511
551
  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 });
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, keepWithNext: b.footer !== void 0, attachNext: b.footer !== void 0 });
514
554
  }
515
555
  if (b.footer) {
516
- groups.push({ lines: [l.soft(b.footer, SIZE.small)], gapAfter: 0 });
556
+ groups.push({ lines: wrapped(l.soft(b.footer, SIZE.small), options.wrap), gapAfter: 0 });
517
557
  }
518
558
  return groups;
519
559
  }
@@ -534,12 +574,13 @@ var ICON_SCALE = 1.6;
534
574
  var ICON_GAP = 0.35;
535
575
  var fontOf = (line, size, fonts) => `${line.weight} ${size}px ${fonts[line.role]}`;
536
576
  var emptyFlow = () => ({ texts: [], cards: [], rules: [], bullets: [], icons: [] });
577
+ var indented = (line) => line.bullet !== void 0 || line.hang === true;
537
578
  var iconSize = (group, k) => {
538
579
  const first = group.lines[0];
539
580
  return group.icon && first ? first.size * k * LINE_HEIGHT * ICON_SCALE : 0;
540
581
  };
541
582
  function lineSize(line, width, k, avail) {
542
- const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
583
+ const indent = indented(line) ? line.size * k * BULLET_INDENT : 0;
543
584
  return Math.max(0, Math.min(line.size * k, line.size * (avail - indent) / Math.max(width, 1)));
544
585
  }
545
586
  function groupRatio(m, k, avail) {
@@ -574,7 +615,7 @@ function placeGroup(m, at, out) {
574
615
  const ratio = groupRatio(m, k, avail);
575
616
  group.lines.forEach((line, i) => {
576
617
  const size = ratio === null ? lineSize(line, m.widths[i] ?? 1, k, avail) : line.size * ratio;
577
- const indent = (line.bullet ? line.size * k * BULLET_INDENT : 0) + iconIndent;
618
+ const indent = (indented(line) ? line.size * k * BULLET_INDENT : 0) + iconIndent;
578
619
  const w = (m.widths[i] ?? 0) * size / line.size;
579
620
  const x = col.right ? col.x + col.w - pad - w : col.x + pad + indent;
580
621
  out?.texts.push({ text: line.text, font: "", color: line.color, box: { x, y, w, h: size * LINE_HEIGHT } });
@@ -598,11 +639,15 @@ function placeGroup(m, at, out) {
598
639
  function placeColumn(groups, at, out) {
599
640
  const { k, col, spread = 0 } = at;
600
641
  let h = 0;
601
- for (const g of groups) {
602
- h += placeGroup(g, { k, col, y0: col.y + h }, out) + spread;
603
- }
604
- return h - spread;
642
+ groups.forEach((g, i) => {
643
+ h += placeGroup(g, { k, col, y0: col.y + h }, out);
644
+ if (i < groups.length - 1 && g.group.attachNext !== true) {
645
+ h += spread;
646
+ }
647
+ });
648
+ return h;
605
649
  }
650
+ var spreadGaps = (groups) => groups.slice(0, -1).filter((g) => g.group.attachNext !== true).length;
606
651
  var columnSum = (heights, from, to) => heights.slice(from, to).reduce((a, b) => a + b, 0);
607
652
  var tallest = (heights, cut) => {
608
653
  const left = heights.slice(0, cut).reduce((a, b) => a + b, 0);
@@ -682,7 +727,10 @@ function flow(ctx, groups, zone, fonts) {
682
727
  const out = emptyFlow();
683
728
  const heights = columns.map((c) => placeColumn(c.groups, { k: lo, col: c.col }, null));
684
729
  const end = Math.max(...heights);
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));
730
+ columns.forEach((c, i) => {
731
+ const gaps = spreadGaps(c.groups);
732
+ placeColumn(c.groups, { k: lo, col: c.col, spread: gaps > 0 && columns.length > 1 ? (end - (heights[i] ?? end)) / gaps : 0 }, out);
733
+ });
686
734
  const lines = groups.flatMap((g) => g.lines);
687
735
  out.texts.forEach((t, i) => {
688
736
  const line = lines[i];
@@ -727,35 +775,6 @@ function pictureRegion(layout, geo, body, frame) {
727
775
  return { x: geo.picture.x, y, w: geo.picture.w, h: frame.height - y };
728
776
  }
729
777
 
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
778
  // src/render/fitToFloor.ts
760
779
  var FLOOR_PX = 30;
761
780
  var BASE = 1080;
@@ -763,33 +782,88 @@ var LINE_HEIGHT3 = 1.2;
763
782
  var LOGO_SHARE = 0.26;
764
783
  var GIVE_STEP = 0.025;
765
784
  var GIVE_STEPS = 32;
785
+ var SHIFT_STEP = 0.06;
786
+ var SHIFT_STEPS = 8;
787
+ var MARGIN2 = 0.045;
766
788
  function splitHeader(header, hasLogo) {
767
789
  const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
768
790
  return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
769
791
  }
770
- function flowAll(ctx, input, geo, u) {
792
+ function flowAll(ctx, input, geo, reflow) {
771
793
  const { doc, palette, fonts } = input;
794
+ const u = Math.min(input.width, input.height) / BASE;
772
795
  const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
773
796
  const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
774
797
  const flanks = geo.flanks ?? [];
775
- const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
798
+ const body = flow(ctx, bodyGroups(doc, palette, u, { withPrices: flanks.length === 0, compact: reflow.compact, wrap: reflow.wrap }), geo.body, fonts);
776
799
  const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
777
800
  return { geo, zones, head, body, sides };
778
801
  }
779
802
  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);
803
+ function shiftHeader(geo, shift) {
804
+ const { header, body } = geo;
805
+ const stacked = header.y + header.h <= body.y && header.x < body.x + body.w && body.x < header.x + header.w;
806
+ const d = Math.round(header.h * shift);
807
+ if (!stacked || d === 0 || body.h - d <= 0) {
808
+ return geo;
809
+ }
810
+ return { ...geo, header: { ...header, h: header.h - d }, body: { ...body, y: body.y - d, h: body.h + d } };
782
811
  }
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);
812
+ function liftHeader(geo, top) {
813
+ const { header } = geo;
814
+ return header.y > top ? { ...geo, header: { ...header, y: top, h: header.h + header.y - top } } : geo;
815
+ }
816
+ function fitBottom(ctx, input, floor) {
817
+ const { width, height } = input;
818
+ const none = { compact: false, wrap: false};
819
+ let fitted = flowAll(ctx, input, geometry("picture-bottom" /* PictureBottom */, width, height), none);
788
820
  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);
821
+ fitted = flowAll(ctx, input, geometry("picture-bottom" /* PictureBottom */, width, height, step * GIVE_STEP), none);
790
822
  }
791
823
  return fitted;
792
824
  }
825
+ function yieldPicture(geo, give) {
826
+ const { picture: p, body: b } = geo;
827
+ const d = Math.round(p.w * give);
828
+ return { ...geo, picture: { ...p, x: p.x + d, w: p.w - d }, body: { ...b, w: b.w + d } };
829
+ }
830
+ function fitList(ctx, input, floor, base2) {
831
+ const last = { compact: true, wrap: true};
832
+ let fitted = flowAll(ctx, input, base2, last);
833
+ for (let step = 1; step <= GIVE_STEPS && smallest(fitted) < floor; step++) {
834
+ fitted = flowAll(ctx, input, yieldPicture(base2, step * GIVE_STEP), last);
835
+ }
836
+ return fitted;
837
+ }
838
+ var SHIFTS = [0, ...Array.from({ length: SHIFT_STEPS }, (_v, i) => [(i + 1) * SHIFT_STEP, -(i + 1) * SHIFT_STEP]).flat()];
839
+ var TIERS = [
840
+ { compact: false, wrap: false, lift: false },
841
+ { compact: true, wrap: false, lift: false },
842
+ { compact: true, wrap: true, lift: false },
843
+ { compact: true, wrap: true, lift: true }
844
+ ];
845
+ var REFLOWS = TIERS.flatMap((tier) => SHIFTS.map((shift) => ({ ...tier, shift })));
846
+ function fitToFloor(ctx, input) {
847
+ const { doc, width, height } = input;
848
+ const floor = FLOOR_PX * Math.min(width, height) / BASE;
849
+ if (doc.layout === "picture-bottom" /* PictureBottom */) {
850
+ return fitBottom(ctx, input, floor);
851
+ }
852
+ const base2 = geometry(doc.layout, width, height);
853
+ const top = Math.round(Math.min(width, height) * MARGIN2);
854
+ let best = null;
855
+ for (const reflow of REFLOWS) {
856
+ const fitted = flowAll(ctx, input, shiftHeader(reflow.lift ? liftHeader(base2, top) : base2, reflow.shift), reflow);
857
+ if (smallest(fitted) >= floor) {
858
+ return fitted;
859
+ }
860
+ best = best === null || smallest(fitted) > smallest(best) ? fitted : best;
861
+ }
862
+ if (doc.layout === "list" /* List */) {
863
+ return fitList(ctx, input, floor, base2);
864
+ }
865
+ return best ?? flowAll(ctx, input, base2, { compact: false, wrap: false});
866
+ }
793
867
 
794
868
  // src/render/render.ts
795
869
  var BASE2 = 1080;
@@ -961,7 +1035,7 @@ var LAYOUT_CATALOGUE = [
961
1035
  { id: "picture-bottom" /* PictureBottom */, designId: "picture-bottom", label: "Picture bottom", description: "Titles and details on top, the picture across the bottom half.", formats: POSTERS },
962
1036
  { id: "list" /* List */, designId: "list", label: "Upcoming list", description: "Several dated events as a list beside the picture.", formats: POSTERS },
963
1037
  { 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 */] },
964
- { 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 */] },
1038
+ { 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 */] },
965
1039
  { 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 */] },
966
1040
  { 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 */] }
967
1041
  ];