@dloizides/poster-render 0.2.3 → 0.2.4
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 +6 -0
- package/dist/index.js +170 -103
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +170 -103
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.4
|
|
4
|
+
|
|
5
|
+
- Type floor: every text run is at least 30px at a 1080 short edge (scaled per format by short edge). When a layout cannot fit its text at the floor, the picture yields space to the body (and a header stacked above it) in 2.5% steps.
|
|
6
|
+
- picture-bottom sizes the picture to the space left once the body holds the floor (about 0.56w for the real tall couple on a story); it stays whole on the bottom edge.
|
|
7
|
+
- Text zones pick two columns when that sets the type at least 15% larger.
|
|
8
|
+
|
|
3
9
|
## 0.2.3
|
|
4
10
|
|
|
5
11
|
- `picture-bottom`: the picture is sized for a tall cut-out (design aspect 0.57): 80% of the width, standing on the bottom edge; the header and body share the top (at least 25% of the height) and shrink to fit. The picture is never cropped.
|
package/dist/index.js
CHANGED
|
@@ -293,73 +293,6 @@ function missingAssets(doc, assets) {
|
|
|
293
293
|
return errors;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
// src/layout/blockGroups.ts
|
|
297
|
-
var BOLD = 700;
|
|
298
|
-
var SEMI = 600;
|
|
299
|
-
var MEDIUM = 500;
|
|
300
|
-
var SCRIPT = 400;
|
|
301
|
-
var makers = (p, u) => ({
|
|
302
|
-
display: (text2, size) => ({ text: text2, role: "display", weight: BOLD, size: size * u, color: p.title }),
|
|
303
|
-
venue: (text2, size) => ({ text: text2, role: "display", weight: MEDIUM, size: size * u, color: p.ink }),
|
|
304
|
-
script: (text2, size) => ({ text: text2, role: "script", weight: SCRIPT, size: size * u, color: p.accent }),
|
|
305
|
-
strong: (text2, size) => ({ text: text2, role: "body", weight: BOLD, size: size * u, color: p.ink }),
|
|
306
|
-
accent: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.accent }),
|
|
307
|
-
item: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.ink, bullet: p.accent }),
|
|
308
|
-
soft: (text2, size) => ({ text: text2, role: "body", weight: MEDIUM, size: size * u, color: p.soft })
|
|
309
|
-
});
|
|
310
|
-
function headerGroups(doc, p, u) {
|
|
311
|
-
const l = makers(p, u);
|
|
312
|
-
const b = doc.blocks;
|
|
313
|
-
const lines = [l.display?.(b.title, 250), b.script ? l.script?.(b.script, 120) : void 0, b.titleBottom ? l.display?.(b.titleBottom, 250) : void 0];
|
|
314
|
-
return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
|
|
315
|
-
}
|
|
316
|
-
function itemGroup(item, l, u) {
|
|
317
|
-
const lines = [l.script?.(item.date, 67)];
|
|
318
|
-
item.schedule.forEach((row) => lines.push(l.strong?.(`${row.label} ${row.time}`, 42)));
|
|
319
|
-
if (item.price) {
|
|
320
|
-
lines.push(l.accent?.(item.price, 42));
|
|
321
|
-
}
|
|
322
|
-
return { lines, gapAfter: 48 * u };
|
|
323
|
-
}
|
|
324
|
-
function eventGroups(doc, l, u, withPrices) {
|
|
325
|
-
const b = doc.blocks;
|
|
326
|
-
const groups = [];
|
|
327
|
-
b.schedule?.forEach((row) => groups.push({ lines: [l.strong?.(row.label, 66), l.accent?.(row.time, 60)], 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 }));
|
|
330
|
-
return groups;
|
|
331
|
-
}
|
|
332
|
-
function listGroups(doc, l, u) {
|
|
333
|
-
const groups = (doc.items ?? []).map((item) => itemGroup(item, l, u));
|
|
334
|
-
const prices = doc.blocks.prices?.map((row) => l.strong?.(`${row.amount} ${row.label}`, 58)) ?? [];
|
|
335
|
-
if (prices.length > 0) {
|
|
336
|
-
groups.push({ lines: prices, gapAfter: 30 * u });
|
|
337
|
-
}
|
|
338
|
-
return groups;
|
|
339
|
-
}
|
|
340
|
-
function flankPriceGroups(doc, p, u) {
|
|
341
|
-
const l = makers(p, u);
|
|
342
|
-
return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong?.(row.amount, 110), l.accent?.(row.label, 48)], gapAfter: 30 * u }));
|
|
343
|
-
}
|
|
344
|
-
function bodyGroups(doc, p, u, withPrices = true) {
|
|
345
|
-
const l = makers(p, u);
|
|
346
|
-
const b = doc.blocks;
|
|
347
|
-
const groups = [];
|
|
348
|
-
const isList = doc.layout === "list" /* List */;
|
|
349
|
-
if (b.venue) {
|
|
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 } });
|
|
351
|
-
}
|
|
352
|
-
groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
|
|
353
|
-
if (b.includes) {
|
|
354
|
-
const lines = [l.accent?.(b.includes.heading, 38), ...b.includes.items.map((i) => l.item?.(i, 38))];
|
|
355
|
-
groups.push({ lines, gapAfter: 24 * u, uniform: true });
|
|
356
|
-
}
|
|
357
|
-
if (b.footer) {
|
|
358
|
-
groups.push({ lines: [l.soft?.(b.footer, 38)], gapAfter: 0 });
|
|
359
|
-
}
|
|
360
|
-
return groups;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
296
|
// src/layout/layoutSpecs.ts
|
|
364
297
|
var flanksBeside = (pic, top, bottom2, m) => [
|
|
365
298
|
[m, top, pic[0] - 2 * m, bottom2 - top],
|
|
@@ -411,7 +344,7 @@ var HEADER_PORTRAIT = 0.3;
|
|
|
411
344
|
var HEADER_LANDSCAPE = 0.36;
|
|
412
345
|
var BOTTOM_PICTURE_W = 0.8;
|
|
413
346
|
var BOTTOM_MIN_TOP = 0.25;
|
|
414
|
-
var BOTTOM_HEADER_SHARE = 0.
|
|
347
|
+
var BOTTOM_HEADER_SHARE = 0.3;
|
|
415
348
|
var BOTTOM_GAP = 40 / 1080;
|
|
416
349
|
var BOTTOM_ASPECT = 0.57;
|
|
417
350
|
var PICTURE_SIDE = {
|
|
@@ -442,11 +375,11 @@ function side(w, h, fraction) {
|
|
|
442
375
|
alignX: 1
|
|
443
376
|
};
|
|
444
377
|
}
|
|
445
|
-
function bottom(w, h) {
|
|
378
|
+
function bottom(w, h, share) {
|
|
446
379
|
const m = r2(Math.min(w, h) * MARGIN);
|
|
447
380
|
const gap = r2(Math.min(w, h) * BOTTOM_GAP);
|
|
448
|
-
const picW = r2(w *
|
|
449
|
-
const picH = Math.min(r2(picW / BOTTOM_ASPECT), h - r2(h * BOTTOM_MIN_TOP));
|
|
381
|
+
const picW = r2(w * share);
|
|
382
|
+
const picH = Math.min(r2(picW / BOTTOM_ASPECT), r2((h - r2(h * BOTTOM_MIN_TOP)) * share / BOTTOM_PICTURE_W));
|
|
450
383
|
const pic = h - picH;
|
|
451
384
|
const top = m + r2((pic - m) * BOTTOM_HEADER_SHARE);
|
|
452
385
|
return {
|
|
@@ -468,13 +401,13 @@ function fromSpec(spec, w, h) {
|
|
|
468
401
|
};
|
|
469
402
|
}
|
|
470
403
|
var mirror = (rect, w) => ({ ...rect, x: w - rect.x - rect.w });
|
|
471
|
-
function geometry(layout, w, h) {
|
|
404
|
+
function geometry(layout, w, h, give = 0) {
|
|
472
405
|
const oriented = LAYOUT_SPECS[layout];
|
|
473
406
|
if (oriented) {
|
|
474
407
|
return fromSpec(h >= w ? oriented.portrait : oriented.landscape, w, h);
|
|
475
408
|
}
|
|
476
409
|
if (layout === "picture-bottom" /* PictureBottom */) {
|
|
477
|
-
return bottom(w, h);
|
|
410
|
+
return bottom(w, h, BOTTOM_PICTURE_W - give);
|
|
478
411
|
}
|
|
479
412
|
const g = side(w, h, PICTURE_SIDE[layout] ?? [0.6, 0.42]);
|
|
480
413
|
if (layout !== "picture-left" /* PictureLeft */) {
|
|
@@ -494,6 +427,73 @@ function placeContain(image, region, anchor, alignX, scale) {
|
|
|
494
427
|
};
|
|
495
428
|
}
|
|
496
429
|
|
|
430
|
+
// src/layout/blockGroups.ts
|
|
431
|
+
var BOLD = 700;
|
|
432
|
+
var SEMI = 600;
|
|
433
|
+
var MEDIUM = 500;
|
|
434
|
+
var SCRIPT = 400;
|
|
435
|
+
var makers = (p, u) => ({
|
|
436
|
+
display: (text2, size) => ({ text: text2, role: "display", weight: BOLD, size: size * u, color: p.title }),
|
|
437
|
+
venue: (text2, size) => ({ text: text2, role: "display", weight: MEDIUM, size: size * u, color: p.ink }),
|
|
438
|
+
script: (text2, size) => ({ text: text2, role: "script", weight: SCRIPT, size: size * u, color: p.accent }),
|
|
439
|
+
strong: (text2, size) => ({ text: text2, role: "body", weight: BOLD, size: size * u, color: p.ink }),
|
|
440
|
+
accent: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.accent }),
|
|
441
|
+
item: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.ink, bullet: p.accent }),
|
|
442
|
+
soft: (text2, size) => ({ text: text2, role: "body", weight: MEDIUM, size: size * u, color: p.soft })
|
|
443
|
+
});
|
|
444
|
+
function headerGroups(doc, p, u) {
|
|
445
|
+
const l = makers(p, u);
|
|
446
|
+
const b = doc.blocks;
|
|
447
|
+
const lines = [l.display?.(b.title, 250), b.script ? l.script?.(b.script, 120) : void 0, b.titleBottom ? l.display?.(b.titleBottom, 250) : void 0];
|
|
448
|
+
return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
|
|
449
|
+
}
|
|
450
|
+
function itemGroup(item, l, u) {
|
|
451
|
+
const lines = [l.script?.(item.date, 67)];
|
|
452
|
+
item.schedule.forEach((row) => lines.push(l.strong?.(`${row.label} ${row.time}`, 42)));
|
|
453
|
+
if (item.price) {
|
|
454
|
+
lines.push(l.accent?.(item.price, 42));
|
|
455
|
+
}
|
|
456
|
+
return { lines, gapAfter: 48 * u };
|
|
457
|
+
}
|
|
458
|
+
function eventGroups(doc, l, u, withPrices) {
|
|
459
|
+
const b = doc.blocks;
|
|
460
|
+
const groups = [];
|
|
461
|
+
b.schedule?.forEach((row) => groups.push({ lines: [l.strong?.(row.label, 66), l.accent?.(row.time, 60)], gapAfter: 30 * u }));
|
|
462
|
+
const prices = (withPrices ? b.prices : void 0) ?? [];
|
|
463
|
+
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 }));
|
|
464
|
+
return groups;
|
|
465
|
+
}
|
|
466
|
+
function listGroups(doc, l, u) {
|
|
467
|
+
const groups = (doc.items ?? []).map((item) => itemGroup(item, l, u));
|
|
468
|
+
const prices = doc.blocks.prices?.map((row) => l.strong?.(`${row.amount} ${row.label}`, 58)) ?? [];
|
|
469
|
+
if (prices.length > 0) {
|
|
470
|
+
groups.push({ lines: prices, gapAfter: 30 * u });
|
|
471
|
+
}
|
|
472
|
+
return groups;
|
|
473
|
+
}
|
|
474
|
+
function flankPriceGroups(doc, p, u) {
|
|
475
|
+
const l = makers(p, u);
|
|
476
|
+
return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong?.(row.amount, 110), l.accent?.(row.label, 48)], gapAfter: 30 * u }));
|
|
477
|
+
}
|
|
478
|
+
function bodyGroups(doc, p, u, withPrices = true) {
|
|
479
|
+
const l = makers(p, u);
|
|
480
|
+
const b = doc.blocks;
|
|
481
|
+
const groups = [];
|
|
482
|
+
const isList = doc.layout === "list" /* List */;
|
|
483
|
+
if (b.venue) {
|
|
484
|
+
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 } });
|
|
485
|
+
}
|
|
486
|
+
groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
|
|
487
|
+
if (b.includes) {
|
|
488
|
+
const lines = [l.accent?.(b.includes.heading, 38), ...b.includes.items.map((i) => l.item?.(i, 38))];
|
|
489
|
+
groups.push({ lines, gapAfter: 24 * u, uniform: true });
|
|
490
|
+
}
|
|
491
|
+
if (b.footer) {
|
|
492
|
+
groups.push({ lines: [l.soft?.(b.footer, 38)], gapAfter: 0 });
|
|
493
|
+
}
|
|
494
|
+
return groups;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
497
|
// src/layout/textFlow.ts
|
|
498
498
|
var LINE_HEIGHT = 1.2;
|
|
499
499
|
var BULLET_INDENT = 1.1;
|
|
@@ -501,6 +501,8 @@ var BULLET_RADIUS = 0.2;
|
|
|
501
501
|
var RULE_GAP = 2.5;
|
|
502
502
|
var RULE_WIDTH = 0.9;
|
|
503
503
|
var WIDE = 1.6;
|
|
504
|
+
var TWO_COLUMN_MIN = 0.75;
|
|
505
|
+
var TWO_COLUMN_GAIN = 1.15;
|
|
504
506
|
var MIN_K = 0.02;
|
|
505
507
|
var STEPS = 22;
|
|
506
508
|
var ICON_SCALE = 1.6;
|
|
@@ -596,9 +598,6 @@ function keepTogether(groups, heights, cut) {
|
|
|
596
598
|
return candidates.reduce((best, c) => tallest(heights, c) < tallest(heights, best) ? c : best);
|
|
597
599
|
}
|
|
598
600
|
function splitColumns(groups, zone) {
|
|
599
|
-
if (zone.w / Math.max(zone.h, 1) <= WIDE || groups.length < 2) {
|
|
600
|
-
return [{ col: zone, groups }];
|
|
601
|
-
}
|
|
602
601
|
const gap = Math.round(zone.w * 0.04);
|
|
603
602
|
const w = (zone.w - gap) / 2;
|
|
604
603
|
const heights = groups.map((g) => placeGroup(g, 1, { ...zone, w }, 0, null));
|
|
@@ -615,16 +614,8 @@ function splitColumns(groups, zone) {
|
|
|
615
614
|
{ col: { ...zone, x: zone.x + w + gap, w }, groups: groups.slice(cut) }
|
|
616
615
|
];
|
|
617
616
|
}
|
|
618
|
-
function
|
|
619
|
-
const
|
|
620
|
-
group,
|
|
621
|
-
widths: group.lines.map((line) => {
|
|
622
|
-
ctx.font = fontOf(line, line.size, fonts);
|
|
623
|
-
return ctx.measureText(line.text).width;
|
|
624
|
-
})
|
|
625
|
-
}));
|
|
626
|
-
const columns = splitColumns(measured, zone);
|
|
627
|
-
const fits = (k) => columns.every((c) => placeColumn(c.groups, k, c.col, null) <= zone.h);
|
|
617
|
+
function largestK(columns, zoneH) {
|
|
618
|
+
const fits = (k) => columns.every((c) => placeColumn(c.groups, k, c.col, null) <= zoneH);
|
|
628
619
|
let lo = MIN_K;
|
|
629
620
|
let hi = 1;
|
|
630
621
|
if (fits(1)) {
|
|
@@ -638,6 +629,28 @@ function flow(ctx, groups, zone, fonts) {
|
|
|
638
629
|
hi = mid;
|
|
639
630
|
}
|
|
640
631
|
}
|
|
632
|
+
return lo;
|
|
633
|
+
}
|
|
634
|
+
function bestColumns(measured, zone) {
|
|
635
|
+
const single = [{ col: zone, groups: measured }];
|
|
636
|
+
const k1 = largestK(single, zone.h);
|
|
637
|
+
if (measured.length < 2 || zone.w / Math.max(zone.h, 1) < TWO_COLUMN_MIN) {
|
|
638
|
+
return { columns: single, k: k1 };
|
|
639
|
+
}
|
|
640
|
+
const split = splitColumns(measured, zone);
|
|
641
|
+
const k2 = largestK(split, zone.h);
|
|
642
|
+
const forced = zone.w / Math.max(zone.h, 1) > WIDE;
|
|
643
|
+
return forced || k2 > k1 * TWO_COLUMN_GAIN ? { columns: split, k: k2 } : { columns: single, k: k1 };
|
|
644
|
+
}
|
|
645
|
+
function flow(ctx, groups, zone, fonts) {
|
|
646
|
+
const measured = groups.map((group) => ({
|
|
647
|
+
group,
|
|
648
|
+
widths: group.lines.map((line) => {
|
|
649
|
+
ctx.font = fontOf(line, line.size, fonts);
|
|
650
|
+
return ctx.measureText(line.text).width;
|
|
651
|
+
})
|
|
652
|
+
}));
|
|
653
|
+
const { columns, k: lo } = bestColumns(measured, zone);
|
|
641
654
|
const out = emptyFlow();
|
|
642
655
|
const heights = columns.map((c) => placeColumn(c.groups, lo, c.col, null));
|
|
643
656
|
const end = Math.max(...heights);
|
|
@@ -659,19 +672,19 @@ function flowFlanks(ctx, groups, flanks, fonts) {
|
|
|
659
672
|
}
|
|
660
673
|
function smallestPerLine(groups, results) {
|
|
661
674
|
const sizes = results.flatMap((r3) => r3.texts.map((t) => t.box.h / LINE_HEIGHT2));
|
|
662
|
-
const
|
|
675
|
+
const smallest2 = [];
|
|
663
676
|
let i = 0;
|
|
664
677
|
groups.forEach(
|
|
665
678
|
(g) => g.lines.forEach((_line, j) => {
|
|
666
|
-
|
|
679
|
+
smallest2[j] = Math.min(smallest2[j] ?? Infinity, sizes[i] ?? Infinity);
|
|
667
680
|
i++;
|
|
668
681
|
})
|
|
669
682
|
);
|
|
670
|
-
return
|
|
683
|
+
return smallest2;
|
|
671
684
|
}
|
|
672
685
|
function equalFlanks(ctx, groups, flanks, fonts) {
|
|
673
|
-
const
|
|
674
|
-
const sized = groups.map((g) => ({ ...g, lines: g.lines.map((line, j) => ({ ...line, size: Math.min(line.size,
|
|
686
|
+
const smallest2 = smallestPerLine(groups, flowFlanks(ctx, groups, flanks, fonts));
|
|
687
|
+
const sized = groups.map((g) => ({ ...g, lines: g.lines.map((line, j) => ({ ...line, size: Math.min(line.size, smallest2[j] ?? line.size) })) }));
|
|
675
688
|
return flowFlanks(ctx, sized, flanks, fonts);
|
|
676
689
|
}
|
|
677
690
|
function measuredEnd(f, fallback) {
|
|
@@ -686,9 +699,72 @@ function pictureRegion(layout, geo, body, u, frame) {
|
|
|
686
699
|
return { x: geo.picture.x, y, w: geo.picture.w, h: frame.height - y };
|
|
687
700
|
}
|
|
688
701
|
|
|
689
|
-
// src/render/
|
|
702
|
+
// src/render/yieldToText.ts
|
|
703
|
+
function yieldToText(geo, give) {
|
|
704
|
+
const { picture: p, body: b } = geo;
|
|
705
|
+
if (give <= 0) {
|
|
706
|
+
return geo;
|
|
707
|
+
}
|
|
708
|
+
if (p.x >= b.x + b.w) {
|
|
709
|
+
const d = Math.round(p.w * give);
|
|
710
|
+
return { ...geo, picture: { ...p, x: p.x + d, w: p.w - d }, body: { ...b, w: b.w + d } };
|
|
711
|
+
}
|
|
712
|
+
if (p.x + p.w <= b.x) {
|
|
713
|
+
const d = Math.round(p.w * give);
|
|
714
|
+
return { ...geo, picture: { ...p, w: p.w - d }, body: { ...b, x: b.x - d, w: b.w + d } };
|
|
715
|
+
}
|
|
716
|
+
if (p.y >= b.y + b.h) {
|
|
717
|
+
const d = Math.round(p.h * give);
|
|
718
|
+
const stacked = geo.header.y + geo.header.h <= b.y;
|
|
719
|
+
const toHeader = stacked ? Math.round(d / 2) : 0;
|
|
720
|
+
return {
|
|
721
|
+
...geo,
|
|
722
|
+
header: { ...geo.header, h: geo.header.h + toHeader },
|
|
723
|
+
picture: { ...p, y: p.y + d, h: p.h - d },
|
|
724
|
+
body: { ...b, y: b.y + toHeader, h: b.h + d - toHeader },
|
|
725
|
+
flanks: geo.flanks?.map((f) => ({ ...f, y: f.y + d, h: f.h - d }))
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
return geo;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/render/fitToFloor.ts
|
|
732
|
+
var FLOOR_PX = 30;
|
|
690
733
|
var BASE = 1080;
|
|
734
|
+
var LINE_HEIGHT3 = 1.2;
|
|
691
735
|
var LOGO_SHARE = 0.26;
|
|
736
|
+
var GIVE_STEP = 0.025;
|
|
737
|
+
var GIVE_STEPS = 32;
|
|
738
|
+
function splitHeader(header, hasLogo) {
|
|
739
|
+
const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
|
|
740
|
+
return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
|
|
741
|
+
}
|
|
742
|
+
function flowAll(ctx, input, geo, u) {
|
|
743
|
+
const { doc, palette, fonts } = input;
|
|
744
|
+
const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
|
|
745
|
+
const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
|
|
746
|
+
const flanks = geo.flanks ?? [];
|
|
747
|
+
const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
|
|
748
|
+
const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
|
|
749
|
+
return { geo, zones, head, body, sides };
|
|
750
|
+
}
|
|
751
|
+
var smallest = (t) => Math.min(Infinity, ...[t.head, t.body, ...t.sides].flatMap((f) => f.texts.map((x) => x.box.h / LINE_HEIGHT3)));
|
|
752
|
+
function geometryWithGive(layout, width, height, give) {
|
|
753
|
+
return layout === "picture-bottom" /* PictureBottom */ ? geometry(layout, width, height, give) : yieldToText(geometry(layout, width, height), give);
|
|
754
|
+
}
|
|
755
|
+
function fitToFloor(ctx, input) {
|
|
756
|
+
const { doc, width, height } = input;
|
|
757
|
+
const u = Math.min(width, height) / BASE;
|
|
758
|
+
const floor = FLOOR_PX * u;
|
|
759
|
+
let fitted = flowAll(ctx, input, geometryWithGive(doc.layout, width, height, 0), u);
|
|
760
|
+
for (let step = 1; step <= GIVE_STEPS && smallest(fitted) < floor; step++) {
|
|
761
|
+
fitted = flowAll(ctx, input, geometryWithGive(doc.layout, width, height, step * GIVE_STEP), u);
|
|
762
|
+
}
|
|
763
|
+
return fitted;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/render/render.ts
|
|
767
|
+
var BASE2 = 1080;
|
|
692
768
|
var CARD_RADIUS = 0.035;
|
|
693
769
|
var CAP_TOP = 0.15;
|
|
694
770
|
var PIN_HEAD_Y = 0.08;
|
|
@@ -783,10 +859,6 @@ function capAligned(zone, head) {
|
|
|
783
859
|
const shift = first ? Math.min(Math.max(0, Math.round(first.y + first.h * CAP_TOP) - zone.y), Math.round(zone.h / 2)) : 0;
|
|
784
860
|
return { ...zone, y: zone.y + shift, h: zone.h - shift };
|
|
785
861
|
}
|
|
786
|
-
function splitHeader(header, hasLogo) {
|
|
787
|
-
const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
|
|
788
|
-
return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
|
|
789
|
-
}
|
|
790
862
|
function render(input, assets, canvas) {
|
|
791
863
|
const doc = checked(input, assets);
|
|
792
864
|
const { width, height } = FORMAT_SIZES[doc.format];
|
|
@@ -798,8 +870,8 @@ function render(input, assets, canvas) {
|
|
|
798
870
|
}
|
|
799
871
|
const palette = PALETTES[doc.palette];
|
|
800
872
|
const fonts = FONT_SETS[doc.fonts];
|
|
801
|
-
const
|
|
802
|
-
const
|
|
873
|
+
const u = Math.min(width, height) / BASE2;
|
|
874
|
+
const { geo, zones, head, body, sides } = fitToFloor(ctx, { doc, palette, fonts, width, height });
|
|
803
875
|
ctx.globalAlpha = 1;
|
|
804
876
|
ctx.fillStyle = palette.background;
|
|
805
877
|
ctx.fillRect(0, 0, width, height);
|
|
@@ -807,11 +879,6 @@ function render(input, assets, canvas) {
|
|
|
807
879
|
ctx.fillStyle = palette.tint;
|
|
808
880
|
[geo.header, geo.body].forEach((r3) => ctx.fillRect(r3.x, r3.y, r3.w, r3.h));
|
|
809
881
|
}
|
|
810
|
-
const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
|
|
811
|
-
const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
|
|
812
|
-
const flanks = geo.flanks ?? [];
|
|
813
|
-
const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
|
|
814
|
-
const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
|
|
815
882
|
const radius = Math.min(width, height) * CARD_RADIUS;
|
|
816
883
|
[head, body, ...sides].forEach((f) => drawFlow(ctx, f, radius));
|
|
817
884
|
const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
|