@dloizides/poster-render 0.2.2 → 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 +10 -0
- package/dist/index.js +183 -112
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +183 -112
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.2.3
|
|
10
|
+
|
|
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.
|
|
12
|
+
|
|
3
13
|
## 0.2.2
|
|
4
14
|
|
|
5
15
|
- Arch flanks: both flank price groups use one size per line, so "per hour" and "Party pass" match.
|
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],
|
|
@@ -409,8 +342,11 @@ var toRect = (f, w, h) => {
|
|
|
409
342
|
var MARGIN = 0.045;
|
|
410
343
|
var HEADER_PORTRAIT = 0.3;
|
|
411
344
|
var HEADER_LANDSCAPE = 0.36;
|
|
412
|
-
var
|
|
413
|
-
var
|
|
345
|
+
var BOTTOM_PICTURE_W = 0.8;
|
|
346
|
+
var BOTTOM_MIN_TOP = 0.25;
|
|
347
|
+
var BOTTOM_HEADER_SHARE = 0.3;
|
|
348
|
+
var BOTTOM_GAP = 40 / 1080;
|
|
349
|
+
var BOTTOM_ASPECT = 0.57;
|
|
414
350
|
var PICTURE_SIDE = {
|
|
415
351
|
["picture-right" /* PictureRight */]: [0.6, 0.42],
|
|
416
352
|
["picture-left" /* PictureLeft */]: [0.6, 0.42],
|
|
@@ -439,14 +375,17 @@ function side(w, h, fraction) {
|
|
|
439
375
|
alignX: 1
|
|
440
376
|
};
|
|
441
377
|
}
|
|
442
|
-
function bottom(w, h) {
|
|
378
|
+
function bottom(w, h, share) {
|
|
443
379
|
const m = r2(Math.min(w, h) * MARGIN);
|
|
444
|
-
const
|
|
445
|
-
const
|
|
380
|
+
const gap = r2(Math.min(w, h) * BOTTOM_GAP);
|
|
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));
|
|
383
|
+
const pic = h - picH;
|
|
384
|
+
const top = m + r2((pic - m) * BOTTOM_HEADER_SHARE);
|
|
446
385
|
return {
|
|
447
|
-
header: { x: m, y: m, w: w - 2 * m, h: top -
|
|
448
|
-
body: { x: m, y: top, w: w - 2 * m, h: pic -
|
|
449
|
-
picture: { x:
|
|
386
|
+
header: { x: m, y: m, w: w - 2 * m, h: top - m },
|
|
387
|
+
body: { x: m, y: top, w: w - 2 * m, h: pic - gap - top },
|
|
388
|
+
picture: { x: r2((w - picW) / 2), y: pic, w: picW, h: picH },
|
|
450
389
|
alignX: 0.5
|
|
451
390
|
};
|
|
452
391
|
}
|
|
@@ -462,13 +401,13 @@ function fromSpec(spec, w, h) {
|
|
|
462
401
|
};
|
|
463
402
|
}
|
|
464
403
|
var mirror = (rect, w) => ({ ...rect, x: w - rect.x - rect.w });
|
|
465
|
-
function geometry(layout, w, h) {
|
|
404
|
+
function geometry(layout, w, h, give = 0) {
|
|
466
405
|
const oriented = LAYOUT_SPECS[layout];
|
|
467
406
|
if (oriented) {
|
|
468
407
|
return fromSpec(h >= w ? oriented.portrait : oriented.landscape, w, h);
|
|
469
408
|
}
|
|
470
409
|
if (layout === "picture-bottom" /* PictureBottom */) {
|
|
471
|
-
return bottom(w, h);
|
|
410
|
+
return bottom(w, h, BOTTOM_PICTURE_W - give);
|
|
472
411
|
}
|
|
473
412
|
const g = side(w, h, PICTURE_SIDE[layout] ?? [0.6, 0.42]);
|
|
474
413
|
if (layout !== "picture-left" /* PictureLeft */) {
|
|
@@ -488,6 +427,73 @@ function placeContain(image, region, anchor, alignX, scale) {
|
|
|
488
427
|
};
|
|
489
428
|
}
|
|
490
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
|
+
|
|
491
497
|
// src/layout/textFlow.ts
|
|
492
498
|
var LINE_HEIGHT = 1.2;
|
|
493
499
|
var BULLET_INDENT = 1.1;
|
|
@@ -495,6 +501,8 @@ var BULLET_RADIUS = 0.2;
|
|
|
495
501
|
var RULE_GAP = 2.5;
|
|
496
502
|
var RULE_WIDTH = 0.9;
|
|
497
503
|
var WIDE = 1.6;
|
|
504
|
+
var TWO_COLUMN_MIN = 0.75;
|
|
505
|
+
var TWO_COLUMN_GAIN = 1.15;
|
|
498
506
|
var MIN_K = 0.02;
|
|
499
507
|
var STEPS = 22;
|
|
500
508
|
var ICON_SCALE = 1.6;
|
|
@@ -590,9 +598,6 @@ function keepTogether(groups, heights, cut) {
|
|
|
590
598
|
return candidates.reduce((best, c) => tallest(heights, c) < tallest(heights, best) ? c : best);
|
|
591
599
|
}
|
|
592
600
|
function splitColumns(groups, zone) {
|
|
593
|
-
if (zone.w / Math.max(zone.h, 1) <= WIDE || groups.length < 2) {
|
|
594
|
-
return [{ col: zone, groups }];
|
|
595
|
-
}
|
|
596
601
|
const gap = Math.round(zone.w * 0.04);
|
|
597
602
|
const w = (zone.w - gap) / 2;
|
|
598
603
|
const heights = groups.map((g) => placeGroup(g, 1, { ...zone, w }, 0, null));
|
|
@@ -609,16 +614,8 @@ function splitColumns(groups, zone) {
|
|
|
609
614
|
{ col: { ...zone, x: zone.x + w + gap, w }, groups: groups.slice(cut) }
|
|
610
615
|
];
|
|
611
616
|
}
|
|
612
|
-
function
|
|
613
|
-
const
|
|
614
|
-
group,
|
|
615
|
-
widths: group.lines.map((line) => {
|
|
616
|
-
ctx.font = fontOf(line, line.size, fonts);
|
|
617
|
-
return ctx.measureText(line.text).width;
|
|
618
|
-
})
|
|
619
|
-
}));
|
|
620
|
-
const columns = splitColumns(measured, zone);
|
|
621
|
-
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);
|
|
622
619
|
let lo = MIN_K;
|
|
623
620
|
let hi = 1;
|
|
624
621
|
if (fits(1)) {
|
|
@@ -632,6 +629,28 @@ function flow(ctx, groups, zone, fonts) {
|
|
|
632
629
|
hi = mid;
|
|
633
630
|
}
|
|
634
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);
|
|
635
654
|
const out = emptyFlow();
|
|
636
655
|
const heights = columns.map((c) => placeColumn(c.groups, lo, c.col, null));
|
|
637
656
|
const end = Math.max(...heights);
|
|
@@ -647,26 +666,25 @@ function flow(ctx, groups, zone, fonts) {
|
|
|
647
666
|
// src/render/regions.ts
|
|
648
667
|
var LINE_HEIGHT2 = 1.2;
|
|
649
668
|
var BODY_GAP = 40;
|
|
650
|
-
var BOTTOM_SIDE = 0.1;
|
|
651
669
|
function flowFlanks(ctx, groups, flanks, fonts) {
|
|
652
670
|
const half = Math.ceil(groups.length / 2);
|
|
653
671
|
return flanks.map((zone, i) => i === 0 ? flow(ctx, groups.slice(0, half), zone, fonts) : flow(ctx, groups.slice(half), { ...zone, right: true }, fonts));
|
|
654
672
|
}
|
|
655
673
|
function smallestPerLine(groups, results) {
|
|
656
674
|
const sizes = results.flatMap((r3) => r3.texts.map((t) => t.box.h / LINE_HEIGHT2));
|
|
657
|
-
const
|
|
675
|
+
const smallest2 = [];
|
|
658
676
|
let i = 0;
|
|
659
677
|
groups.forEach(
|
|
660
678
|
(g) => g.lines.forEach((_line, j) => {
|
|
661
|
-
|
|
679
|
+
smallest2[j] = Math.min(smallest2[j] ?? Infinity, sizes[i] ?? Infinity);
|
|
662
680
|
i++;
|
|
663
681
|
})
|
|
664
682
|
);
|
|
665
|
-
return
|
|
683
|
+
return smallest2;
|
|
666
684
|
}
|
|
667
685
|
function equalFlanks(ctx, groups, flanks, fonts) {
|
|
668
|
-
const
|
|
669
|
-
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) })) }));
|
|
670
688
|
return flowFlanks(ctx, sized, flanks, fonts);
|
|
671
689
|
}
|
|
672
690
|
function measuredEnd(f, fallback) {
|
|
@@ -677,14 +695,76 @@ function pictureRegion(layout, geo, body, u, frame) {
|
|
|
677
695
|
if (layout !== "picture-bottom" /* PictureBottom */) {
|
|
678
696
|
return geo.picture;
|
|
679
697
|
}
|
|
680
|
-
const y = Math.ceil(measuredEnd(body, geo.body.y) + BODY_GAP * u);
|
|
681
|
-
|
|
682
|
-
return { x, y, w: frame.width - 2 * x, h: frame.height - y };
|
|
698
|
+
const y = Math.min(geo.picture.y, Math.ceil(measuredEnd(body, geo.body.y) + BODY_GAP * u));
|
|
699
|
+
return { x: geo.picture.x, y, w: geo.picture.w, h: frame.height - y };
|
|
683
700
|
}
|
|
684
701
|
|
|
685
|
-
// 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;
|
|
686
733
|
var BASE = 1080;
|
|
734
|
+
var LINE_HEIGHT3 = 1.2;
|
|
687
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;
|
|
688
768
|
var CARD_RADIUS = 0.035;
|
|
689
769
|
var CAP_TOP = 0.15;
|
|
690
770
|
var PIN_HEAD_Y = 0.08;
|
|
@@ -779,10 +859,6 @@ function capAligned(zone, head) {
|
|
|
779
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;
|
|
780
860
|
return { ...zone, y: zone.y + shift, h: zone.h - shift };
|
|
781
861
|
}
|
|
782
|
-
function splitHeader(header, hasLogo) {
|
|
783
|
-
const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
|
|
784
|
-
return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
|
|
785
|
-
}
|
|
786
862
|
function render(input, assets, canvas) {
|
|
787
863
|
const doc = checked(input, assets);
|
|
788
864
|
const { width, height } = FORMAT_SIZES[doc.format];
|
|
@@ -794,8 +870,8 @@ function render(input, assets, canvas) {
|
|
|
794
870
|
}
|
|
795
871
|
const palette = PALETTES[doc.palette];
|
|
796
872
|
const fonts = FONT_SETS[doc.fonts];
|
|
797
|
-
const
|
|
798
|
-
const
|
|
873
|
+
const u = Math.min(width, height) / BASE2;
|
|
874
|
+
const { geo, zones, head, body, sides } = fitToFloor(ctx, { doc, palette, fonts, width, height });
|
|
799
875
|
ctx.globalAlpha = 1;
|
|
800
876
|
ctx.fillStyle = palette.background;
|
|
801
877
|
ctx.fillRect(0, 0, width, height);
|
|
@@ -803,11 +879,6 @@ function render(input, assets, canvas) {
|
|
|
803
879
|
ctx.fillStyle = palette.tint;
|
|
804
880
|
[geo.header, geo.body].forEach((r3) => ctx.fillRect(r3.x, r3.y, r3.w, r3.h));
|
|
805
881
|
}
|
|
806
|
-
const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
|
|
807
|
-
const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
|
|
808
|
-
const flanks = geo.flanks ?? [];
|
|
809
|
-
const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
|
|
810
|
-
const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
|
|
811
882
|
const radius = Math.min(width, height) * CARD_RADIUS;
|
|
812
883
|
[head, body, ...sides].forEach((f) => drawFlow(ctx, f, radius));
|
|
813
884
|
const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
|
|
@@ -820,7 +891,7 @@ function render(input, assets, canvas) {
|
|
|
820
891
|
if (backdrop) {
|
|
821
892
|
drawArch(ctx, backdrop, palette.arch);
|
|
822
893
|
}
|
|
823
|
-
const region = pictureRegion(doc.layout, geo, body, u, {
|
|
894
|
+
const region = pictureRegion(doc.layout, geo, body, u, { height });
|
|
824
895
|
const picture = image && doc.picture ? placeContain(image, region, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
|
|
825
896
|
if (image && picture) {
|
|
826
897
|
ctx.drawImage(image, picture.x, picture.y, picture.w, picture.h);
|