@dloizides/poster-render 0.2.5 → 0.2.6

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
@@ -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))];
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));
513
553
  groups.push({ lines, gapAfter: GAP.includes * u, uniform: true });
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 } });
@@ -727,35 +768,6 @@ function pictureRegion(layout, geo, body, frame) {
727
768
  return { x: geo.picture.x, y, w: geo.picture.w, h: frame.height - y };
728
769
  }
729
770
 
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
771
  // src/render/fitToFloor.ts
760
772
  var FLOOR_PX = 30;
761
773
  var BASE = 1080;
@@ -763,33 +775,88 @@ var LINE_HEIGHT3 = 1.2;
763
775
  var LOGO_SHARE = 0.26;
764
776
  var GIVE_STEP = 0.025;
765
777
  var GIVE_STEPS = 32;
778
+ var SHIFT_STEP = 0.06;
779
+ var SHIFT_STEPS = 8;
780
+ var MARGIN2 = 0.045;
766
781
  function splitHeader(header, hasLogo) {
767
782
  const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
768
783
  return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
769
784
  }
770
- function flowAll(ctx, input, geo, u) {
785
+ function flowAll(ctx, input, geo, reflow) {
771
786
  const { doc, palette, fonts } = input;
787
+ const u = Math.min(input.width, input.height) / BASE;
772
788
  const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
773
789
  const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
774
790
  const flanks = geo.flanks ?? [];
775
- const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
791
+ const body = flow(ctx, bodyGroups(doc, palette, u, { withPrices: flanks.length === 0, compact: reflow.compact, wrap: reflow.wrap }), geo.body, fonts);
776
792
  const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
777
793
  return { geo, zones, head, body, sides };
778
794
  }
779
795
  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);
796
+ function shiftHeader(geo, shift) {
797
+ const { header, body } = geo;
798
+ const stacked = header.y + header.h <= body.y && header.x < body.x + body.w && body.x < header.x + header.w;
799
+ const d = Math.round(header.h * shift);
800
+ if (!stacked || d === 0 || body.h - d <= 0) {
801
+ return geo;
802
+ }
803
+ return { ...geo, header: { ...header, h: header.h - d }, body: { ...body, y: body.y - d, h: body.h + d } };
782
804
  }
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);
805
+ function liftHeader(geo, top) {
806
+ const { header } = geo;
807
+ return header.y > top ? { ...geo, header: { ...header, y: top, h: header.h + header.y - top } } : geo;
808
+ }
809
+ function fitBottom(ctx, input, floor) {
810
+ const { width, height } = input;
811
+ const none = { compact: false, wrap: false};
812
+ let fitted = flowAll(ctx, input, geometry("picture-bottom" /* PictureBottom */, width, height), none);
788
813
  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);
814
+ fitted = flowAll(ctx, input, geometry("picture-bottom" /* PictureBottom */, width, height, step * GIVE_STEP), none);
790
815
  }
791
816
  return fitted;
792
817
  }
818
+ function yieldPicture(geo, give) {
819
+ const { picture: p, body: b } = geo;
820
+ const d = Math.round(p.w * give);
821
+ return { ...geo, picture: { ...p, x: p.x + d, w: p.w - d }, body: { ...b, w: b.w + d } };
822
+ }
823
+ function fitList(ctx, input, floor, base2) {
824
+ const last = { compact: true, wrap: true};
825
+ let fitted = flowAll(ctx, input, base2, last);
826
+ for (let step = 1; step <= GIVE_STEPS && smallest(fitted) < floor; step++) {
827
+ fitted = flowAll(ctx, input, yieldPicture(base2, step * GIVE_STEP), last);
828
+ }
829
+ return fitted;
830
+ }
831
+ var SHIFTS = [0, ...Array.from({ length: SHIFT_STEPS }, (_v, i) => [(i + 1) * SHIFT_STEP, -(i + 1) * SHIFT_STEP]).flat()];
832
+ var TIERS = [
833
+ { compact: false, wrap: false, lift: false },
834
+ { compact: true, wrap: false, lift: false },
835
+ { compact: true, wrap: true, lift: false },
836
+ { compact: true, wrap: true, lift: true }
837
+ ];
838
+ var REFLOWS = TIERS.flatMap((tier) => SHIFTS.map((shift) => ({ ...tier, shift })));
839
+ function fitToFloor(ctx, input) {
840
+ const { doc, width, height } = input;
841
+ const floor = FLOOR_PX * Math.min(width, height) / BASE;
842
+ if (doc.layout === "picture-bottom" /* PictureBottom */) {
843
+ return fitBottom(ctx, input, floor);
844
+ }
845
+ const base2 = geometry(doc.layout, width, height);
846
+ const top = Math.round(Math.min(width, height) * MARGIN2);
847
+ let best = null;
848
+ for (const reflow of REFLOWS) {
849
+ const fitted = flowAll(ctx, input, shiftHeader(reflow.lift ? liftHeader(base2, top) : base2, reflow.shift), reflow);
850
+ if (smallest(fitted) >= floor) {
851
+ return fitted;
852
+ }
853
+ best = best === null || smallest(fitted) > smallest(best) ? fitted : best;
854
+ }
855
+ if (doc.layout === "list" /* List */) {
856
+ return fitList(ctx, input, floor, base2);
857
+ }
858
+ return best ?? flowAll(ctx, input, base2, { compact: false, wrap: false});
859
+ }
793
860
 
794
861
  // src/render/render.ts
795
862
  var BASE2 = 1080;