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