@dloizides/poster-render 0.2.3 → 0.2.5
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 +13 -0
- package/README.md +40 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +225 -128
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +225 -128
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -224,7 +224,7 @@ function checkObject(schema, value, path, errors) {
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
for (const [key, child] of Object.entries(value)) {
|
|
227
|
-
const childSchema = props[key];
|
|
227
|
+
const childSchema = Object.hasOwn(props, key) ? props[key] : void 0;
|
|
228
228
|
if (childSchema === void 0) {
|
|
229
229
|
if (schema.additionalProperties === false) {
|
|
230
230
|
errors.push(`${join(path, key)}: is not allowed`);
|
|
@@ -282,82 +282,15 @@ function validateFlierDoc(input) {
|
|
|
282
282
|
}
|
|
283
283
|
function missingAssets(doc, assets) {
|
|
284
284
|
const errors = [];
|
|
285
|
-
if (doc.picture && assets
|
|
285
|
+
if (doc.picture && !Object.hasOwn(assets, doc.picture.imageId)) {
|
|
286
286
|
errors.push(`picture.imageId: no asset named "${doc.picture.imageId}"`);
|
|
287
287
|
}
|
|
288
|
-
if (doc.logo && assets
|
|
288
|
+
if (doc.logo && !Object.hasOwn(assets, doc.logo.imageId)) {
|
|
289
289
|
errors.push(`logo.imageId: no asset named "${doc.logo.imageId}"`);
|
|
290
290
|
}
|
|
291
291
|
return errors;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
// src/layout/blockGroups.ts
|
|
295
|
-
var BOLD = 700;
|
|
296
|
-
var SEMI = 600;
|
|
297
|
-
var MEDIUM = 500;
|
|
298
|
-
var SCRIPT = 400;
|
|
299
|
-
var makers = (p, u) => ({
|
|
300
|
-
display: (text2, size) => ({ text: text2, role: "display", weight: BOLD, size: size * u, color: p.title }),
|
|
301
|
-
venue: (text2, size) => ({ text: text2, role: "display", weight: MEDIUM, size: size * u, color: p.ink }),
|
|
302
|
-
script: (text2, size) => ({ text: text2, role: "script", weight: SCRIPT, size: size * u, color: p.accent }),
|
|
303
|
-
strong: (text2, size) => ({ text: text2, role: "body", weight: BOLD, size: size * u, color: p.ink }),
|
|
304
|
-
accent: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.accent }),
|
|
305
|
-
item: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.ink, bullet: p.accent }),
|
|
306
|
-
soft: (text2, size) => ({ text: text2, role: "body", weight: MEDIUM, size: size * u, color: p.soft })
|
|
307
|
-
});
|
|
308
|
-
function headerGroups(doc, p, u) {
|
|
309
|
-
const l = makers(p, u);
|
|
310
|
-
const b = doc.blocks;
|
|
311
|
-
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];
|
|
312
|
-
return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
|
|
313
|
-
}
|
|
314
|
-
function itemGroup(item, l, u) {
|
|
315
|
-
const lines = [l.script?.(item.date, 67)];
|
|
316
|
-
item.schedule.forEach((row) => lines.push(l.strong?.(`${row.label} ${row.time}`, 42)));
|
|
317
|
-
if (item.price) {
|
|
318
|
-
lines.push(l.accent?.(item.price, 42));
|
|
319
|
-
}
|
|
320
|
-
return { lines, gapAfter: 48 * u };
|
|
321
|
-
}
|
|
322
|
-
function eventGroups(doc, l, u, withPrices) {
|
|
323
|
-
const b = doc.blocks;
|
|
324
|
-
const groups = [];
|
|
325
|
-
b.schedule?.forEach((row) => groups.push({ lines: [l.strong?.(row.label, 66), l.accent?.(row.time, 60)], gapAfter: 30 * u }));
|
|
326
|
-
const prices = (withPrices ? b.prices : void 0) ?? [];
|
|
327
|
-
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 }));
|
|
328
|
-
return groups;
|
|
329
|
-
}
|
|
330
|
-
function listGroups(doc, l, u) {
|
|
331
|
-
const groups = (doc.items ?? []).map((item) => itemGroup(item, l, u));
|
|
332
|
-
const prices = doc.blocks.prices?.map((row) => l.strong?.(`${row.amount} ${row.label}`, 58)) ?? [];
|
|
333
|
-
if (prices.length > 0) {
|
|
334
|
-
groups.push({ lines: prices, gapAfter: 30 * u });
|
|
335
|
-
}
|
|
336
|
-
return groups;
|
|
337
|
-
}
|
|
338
|
-
function flankPriceGroups(doc, p, u) {
|
|
339
|
-
const l = makers(p, u);
|
|
340
|
-
return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong?.(row.amount, 110), l.accent?.(row.label, 48)], gapAfter: 30 * u }));
|
|
341
|
-
}
|
|
342
|
-
function bodyGroups(doc, p, u, withPrices = true) {
|
|
343
|
-
const l = makers(p, u);
|
|
344
|
-
const b = doc.blocks;
|
|
345
|
-
const groups = [];
|
|
346
|
-
const isList = doc.layout === "list" /* List */;
|
|
347
|
-
if (b.venue) {
|
|
348
|
-
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 } });
|
|
349
|
-
}
|
|
350
|
-
groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
|
|
351
|
-
if (b.includes) {
|
|
352
|
-
const lines = [l.accent?.(b.includes.heading, 38), ...b.includes.items.map((i) => l.item?.(i, 38))];
|
|
353
|
-
groups.push({ lines, gapAfter: 24 * u, uniform: true });
|
|
354
|
-
}
|
|
355
|
-
if (b.footer) {
|
|
356
|
-
groups.push({ lines: [l.soft?.(b.footer, 38)], gapAfter: 0 });
|
|
357
|
-
}
|
|
358
|
-
return groups;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
294
|
// src/layout/layoutSpecs.ts
|
|
362
295
|
var flanksBeside = (pic, top, bottom2, m) => [
|
|
363
296
|
[m, top, pic[0] - 2 * m, bottom2 - top],
|
|
@@ -409,12 +342,13 @@ var HEADER_PORTRAIT = 0.3;
|
|
|
409
342
|
var HEADER_LANDSCAPE = 0.36;
|
|
410
343
|
var BOTTOM_PICTURE_W = 0.8;
|
|
411
344
|
var BOTTOM_MIN_TOP = 0.25;
|
|
412
|
-
var BOTTOM_HEADER_SHARE = 0.
|
|
345
|
+
var BOTTOM_HEADER_SHARE = 0.3;
|
|
413
346
|
var BOTTOM_GAP = 40 / 1080;
|
|
414
347
|
var BOTTOM_ASPECT = 0.57;
|
|
348
|
+
var DEFAULT_SIDE = [0.6, 0.42];
|
|
415
349
|
var PICTURE_SIDE = {
|
|
416
|
-
["picture-right" /* PictureRight */]:
|
|
417
|
-
["picture-left" /* PictureLeft */]:
|
|
350
|
+
["picture-right" /* PictureRight */]: DEFAULT_SIDE,
|
|
351
|
+
["picture-left" /* PictureLeft */]: DEFAULT_SIDE,
|
|
418
352
|
["list" /* List */]: [0.5, 0.36]
|
|
419
353
|
};
|
|
420
354
|
var ANCHOR_Y = { ["top" /* Top */]: 0, ["center" /* Center */]: 0.5, ["bottom" /* Bottom */]: 1 };
|
|
@@ -440,11 +374,11 @@ function side(w, h, fraction) {
|
|
|
440
374
|
alignX: 1
|
|
441
375
|
};
|
|
442
376
|
}
|
|
443
|
-
function bottom(w, h) {
|
|
377
|
+
function bottom(w, h, share) {
|
|
444
378
|
const m = r2(Math.min(w, h) * MARGIN);
|
|
445
379
|
const gap = r2(Math.min(w, h) * BOTTOM_GAP);
|
|
446
|
-
const picW = r2(w *
|
|
447
|
-
const picH = Math.min(r2(picW / BOTTOM_ASPECT), h - r2(h * BOTTOM_MIN_TOP));
|
|
380
|
+
const picW = r2(w * share);
|
|
381
|
+
const picH = Math.min(r2(picW / BOTTOM_ASPECT), r2((h - r2(h * BOTTOM_MIN_TOP)) * share / BOTTOM_PICTURE_W));
|
|
448
382
|
const pic = h - picH;
|
|
449
383
|
const top = m + r2((pic - m) * BOTTOM_HEADER_SHARE);
|
|
450
384
|
return {
|
|
@@ -466,21 +400,22 @@ function fromSpec(spec, w, h) {
|
|
|
466
400
|
};
|
|
467
401
|
}
|
|
468
402
|
var mirror = (rect, w) => ({ ...rect, x: w - rect.x - rect.w });
|
|
469
|
-
function geometry(layout, w, h) {
|
|
403
|
+
function geometry(layout, w, h, give = 0) {
|
|
470
404
|
const oriented = LAYOUT_SPECS[layout];
|
|
471
405
|
if (oriented) {
|
|
472
406
|
return fromSpec(h >= w ? oriented.portrait : oriented.landscape, w, h);
|
|
473
407
|
}
|
|
474
408
|
if (layout === "picture-bottom" /* PictureBottom */) {
|
|
475
|
-
return bottom(w, h);
|
|
409
|
+
return bottom(w, h, BOTTOM_PICTURE_W - give);
|
|
476
410
|
}
|
|
477
|
-
const g = side(w, h, PICTURE_SIDE[layout] ??
|
|
411
|
+
const g = side(w, h, PICTURE_SIDE[layout] ?? DEFAULT_SIDE);
|
|
478
412
|
if (layout !== "picture-left" /* PictureLeft */) {
|
|
479
413
|
return g;
|
|
480
414
|
}
|
|
481
415
|
return { header: mirror(g.header, w), body: mirror(g.body, w), picture: mirror(g.picture, w), alignX: 1 - g.alignX };
|
|
482
416
|
}
|
|
483
|
-
function placeContain(image, region,
|
|
417
|
+
function placeContain(image, region, fit) {
|
|
418
|
+
const { anchor, alignX, scale } = fit;
|
|
484
419
|
const s = Math.min(region.w / image.width, region.h / image.height) * scale;
|
|
485
420
|
const w = Math.floor(image.width * s);
|
|
486
421
|
const h = Math.floor(image.height * s);
|
|
@@ -492,6 +427,97 @@ function placeContain(image, region, anchor, alignX, scale) {
|
|
|
492
427
|
};
|
|
493
428
|
}
|
|
494
429
|
|
|
430
|
+
// src/layout/blockGroups.ts
|
|
431
|
+
var BOLD = 700;
|
|
432
|
+
var SEMI = 600;
|
|
433
|
+
var MEDIUM = 500;
|
|
434
|
+
var SCRIPT = 400;
|
|
435
|
+
var SIZE = {
|
|
436
|
+
title: 250,
|
|
437
|
+
headerScript: 120,
|
|
438
|
+
itemDate: 67,
|
|
439
|
+
itemRow: 42,
|
|
440
|
+
scheduleLabel: 66,
|
|
441
|
+
scheduleTime: 60,
|
|
442
|
+
priceAmount: 66,
|
|
443
|
+
priceLabel: 52,
|
|
444
|
+
listPrice: 58,
|
|
445
|
+
flankAmount: 110,
|
|
446
|
+
flankLabel: 48,
|
|
447
|
+
venue: 58,
|
|
448
|
+
listVenue: 48,
|
|
449
|
+
small: 38
|
|
450
|
+
};
|
|
451
|
+
var GAP = {
|
|
452
|
+
item: 48,
|
|
453
|
+
row: 30,
|
|
454
|
+
venue: 40,
|
|
455
|
+
includes: 24,
|
|
456
|
+
rule: 8
|
|
457
|
+
};
|
|
458
|
+
var makers = (p, u) => ({
|
|
459
|
+
display: (text2, size) => ({ text: text2, role: "display", weight: BOLD, size: size * u, color: p.title }),
|
|
460
|
+
venue: (text2, size) => ({ text: text2, role: "display", weight: MEDIUM, size: size * u, color: p.ink }),
|
|
461
|
+
script: (text2, size) => ({ text: text2, role: "script", weight: SCRIPT, size: size * u, color: p.accent }),
|
|
462
|
+
strong: (text2, size) => ({ text: text2, role: "body", weight: BOLD, size: size * u, color: p.ink }),
|
|
463
|
+
accent: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.accent }),
|
|
464
|
+
item: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.ink, bullet: p.accent }),
|
|
465
|
+
soft: (text2, size) => ({ text: text2, role: "body", weight: MEDIUM, size: size * u, color: p.soft })
|
|
466
|
+
});
|
|
467
|
+
function headerGroups(doc, p, u) {
|
|
468
|
+
const l = makers(p, u);
|
|
469
|
+
const b = doc.blocks;
|
|
470
|
+
const lines = [l.display(b.title, SIZE.title), b.script ? l.script(b.script, SIZE.headerScript) : void 0, b.titleBottom ? l.display(b.titleBottom, SIZE.title) : void 0];
|
|
471
|
+
return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
|
|
472
|
+
}
|
|
473
|
+
function itemGroup(item, l, u) {
|
|
474
|
+
const lines = [l.script(item.date, SIZE.itemDate)];
|
|
475
|
+
item.schedule.forEach((row) => lines.push(l.strong(`${row.label} ${row.time}`, SIZE.itemRow)));
|
|
476
|
+
if (item.price) {
|
|
477
|
+
lines.push(l.accent(item.price, SIZE.itemRow));
|
|
478
|
+
}
|
|
479
|
+
return { lines, gapAfter: GAP.item * u };
|
|
480
|
+
}
|
|
481
|
+
function eventGroups(doc, l, u, withPrices) {
|
|
482
|
+
const b = doc.blocks;
|
|
483
|
+
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 }));
|
|
487
|
+
return groups;
|
|
488
|
+
}
|
|
489
|
+
function listGroups(doc, l, u) {
|
|
490
|
+
const groups = (doc.items ?? []).map((item) => itemGroup(item, l, u));
|
|
491
|
+
const prices = doc.blocks.prices?.map((row) => l.strong(`${row.amount} ${row.label}`, SIZE.listPrice)) ?? [];
|
|
492
|
+
if (prices.length > 0) {
|
|
493
|
+
groups.push({ lines: prices, gapAfter: GAP.row * u });
|
|
494
|
+
}
|
|
495
|
+
return groups;
|
|
496
|
+
}
|
|
497
|
+
function flankPriceGroups(doc, p, u) {
|
|
498
|
+
const l = makers(p, u);
|
|
499
|
+
return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong(row.amount, SIZE.flankAmount), l.accent(row.label, SIZE.flankLabel)], gapAfter: GAP.row * u }));
|
|
500
|
+
}
|
|
501
|
+
function bodyGroups(doc, p, u, withPrices = true) {
|
|
502
|
+
const l = makers(p, u);
|
|
503
|
+
const b = doc.blocks;
|
|
504
|
+
const groups = [];
|
|
505
|
+
const isList = doc.layout === "list" /* List */;
|
|
506
|
+
if (b.venue) {
|
|
507
|
+
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 } });
|
|
509
|
+
}
|
|
510
|
+
groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
|
|
511
|
+
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 });
|
|
514
|
+
}
|
|
515
|
+
if (b.footer) {
|
|
516
|
+
groups.push({ lines: [l.soft(b.footer, SIZE.small)], gapAfter: 0 });
|
|
517
|
+
}
|
|
518
|
+
return groups;
|
|
519
|
+
}
|
|
520
|
+
|
|
495
521
|
// src/layout/textFlow.ts
|
|
496
522
|
var LINE_HEIGHT = 1.2;
|
|
497
523
|
var BULLET_INDENT = 1.1;
|
|
@@ -499,6 +525,9 @@ var BULLET_RADIUS = 0.2;
|
|
|
499
525
|
var RULE_GAP = 2.5;
|
|
500
526
|
var RULE_WIDTH = 0.9;
|
|
501
527
|
var WIDE = 1.6;
|
|
528
|
+
var COLUMN_GAP = 0.04;
|
|
529
|
+
var TWO_COLUMN_MIN = 0.75;
|
|
530
|
+
var TWO_COLUMN_GAIN = 1.15;
|
|
502
531
|
var MIN_K = 0.02;
|
|
503
532
|
var STEPS = 22;
|
|
504
533
|
var ICON_SCALE = 1.6;
|
|
@@ -519,15 +548,17 @@ function groupRatio(m, k, avail) {
|
|
|
519
548
|
}
|
|
520
549
|
return Math.min(...m.group.lines.map((line, i) => lineSize(line, m.widths[i] ?? 1, k, avail) / line.size));
|
|
521
550
|
}
|
|
522
|
-
function placeIcon(group,
|
|
551
|
+
function placeIcon(group, box, out) {
|
|
552
|
+
const { d, x, top, bottom: bottom2 } = box;
|
|
523
553
|
if (!group.icon || d <= 0) {
|
|
524
|
-
return
|
|
554
|
+
return bottom2;
|
|
525
555
|
}
|
|
526
|
-
const textH =
|
|
527
|
-
out?.icons.push({ rect: { x, y:
|
|
528
|
-
return
|
|
556
|
+
const textH = bottom2 - top;
|
|
557
|
+
out?.icons.push({ rect: { x, y: top + Math.max(0, (textH - d) / 2), w: d, h: d }, color: group.icon.color, glyph: group.icon.glyph });
|
|
558
|
+
return top + Math.max(textH, d);
|
|
529
559
|
}
|
|
530
|
-
function placeGroup(m,
|
|
560
|
+
function placeGroup(m, at, out) {
|
|
561
|
+
const { k, col, y0 } = at;
|
|
531
562
|
const { group } = m;
|
|
532
563
|
let y = y0;
|
|
533
564
|
if (group.rule) {
|
|
@@ -552,7 +583,7 @@ function placeGroup(m, k, col, y0, out) {
|
|
|
552
583
|
}
|
|
553
584
|
y += size * LINE_HEIGHT;
|
|
554
585
|
});
|
|
555
|
-
y = placeIcon(group, d, col.x + pad, top + pad, y, out);
|
|
586
|
+
y = placeIcon(group, { d, x: col.x + pad, top: top + pad, bottom: y }, out);
|
|
556
587
|
y += pad;
|
|
557
588
|
if (group.rule && group.ruleBelow) {
|
|
558
589
|
y += group.rule.height * k * (RULE_GAP - 1);
|
|
@@ -564,10 +595,11 @@ function placeGroup(m, k, col, y0, out) {
|
|
|
564
595
|
}
|
|
565
596
|
return y + group.gapAfter * k - y0;
|
|
566
597
|
}
|
|
567
|
-
function placeColumn(groups,
|
|
598
|
+
function placeColumn(groups, at, out) {
|
|
599
|
+
const { k, col, spread = 0 } = at;
|
|
568
600
|
let h = 0;
|
|
569
601
|
for (const g of groups) {
|
|
570
|
-
h += placeGroup(g, k, col, col.y + h, out) + spread;
|
|
602
|
+
h += placeGroup(g, { k, col, y0: col.y + h }, out) + spread;
|
|
571
603
|
}
|
|
572
604
|
return h - spread;
|
|
573
605
|
}
|
|
@@ -594,12 +626,9 @@ function keepTogether(groups, heights, cut) {
|
|
|
594
626
|
return candidates.reduce((best, c) => tallest(heights, c) < tallest(heights, best) ? c : best);
|
|
595
627
|
}
|
|
596
628
|
function splitColumns(groups, zone) {
|
|
597
|
-
|
|
598
|
-
return [{ col: zone, groups }];
|
|
599
|
-
}
|
|
600
|
-
const gap = Math.round(zone.w * 0.04);
|
|
629
|
+
const gap = Math.round(zone.w * COLUMN_GAP);
|
|
601
630
|
const w = (zone.w - gap) / 2;
|
|
602
|
-
const heights = groups.map((g) => placeGroup(g, 1, { ...zone, w }, 0, null));
|
|
631
|
+
const heights = groups.map((g) => placeGroup(g, { k: 1, col: { ...zone, w }, y0: 0 }, null));
|
|
603
632
|
const total = columnSum(heights, 0, heights.length);
|
|
604
633
|
let cut = 1;
|
|
605
634
|
for (let c = 2; c < groups.length; c++) {
|
|
@@ -613,16 +642,8 @@ function splitColumns(groups, zone) {
|
|
|
613
642
|
{ col: { ...zone, x: zone.x + w + gap, w }, groups: groups.slice(cut) }
|
|
614
643
|
];
|
|
615
644
|
}
|
|
616
|
-
function
|
|
617
|
-
const
|
|
618
|
-
group,
|
|
619
|
-
widths: group.lines.map((line) => {
|
|
620
|
-
ctx.font = fontOf(line, line.size, fonts);
|
|
621
|
-
return ctx.measureText(line.text).width;
|
|
622
|
-
})
|
|
623
|
-
}));
|
|
624
|
-
const columns = splitColumns(measured, zone);
|
|
625
|
-
const fits = (k) => columns.every((c) => placeColumn(c.groups, k, c.col, null) <= zone.h);
|
|
645
|
+
function largestK(columns, zoneH) {
|
|
646
|
+
const fits = (k) => columns.every((c) => placeColumn(c.groups, { k, col: c.col }, null) <= zoneH);
|
|
626
647
|
let lo = MIN_K;
|
|
627
648
|
let hi = 1;
|
|
628
649
|
if (fits(1)) {
|
|
@@ -636,10 +657,32 @@ function flow(ctx, groups, zone, fonts) {
|
|
|
636
657
|
hi = mid;
|
|
637
658
|
}
|
|
638
659
|
}
|
|
660
|
+
return lo;
|
|
661
|
+
}
|
|
662
|
+
function bestColumns(measured, zone) {
|
|
663
|
+
const single = [{ col: zone, groups: measured }];
|
|
664
|
+
const k1 = largestK(single, zone.h);
|
|
665
|
+
if (measured.length < 2 || zone.w / Math.max(zone.h, 1) < TWO_COLUMN_MIN) {
|
|
666
|
+
return { columns: single, k: k1 };
|
|
667
|
+
}
|
|
668
|
+
const split = splitColumns(measured, zone);
|
|
669
|
+
const k2 = largestK(split, zone.h);
|
|
670
|
+
const forced = zone.w / Math.max(zone.h, 1) > WIDE;
|
|
671
|
+
return forced || k2 > k1 * TWO_COLUMN_GAIN ? { columns: split, k: k2 } : { columns: single, k: k1 };
|
|
672
|
+
}
|
|
673
|
+
function flow(ctx, groups, zone, fonts) {
|
|
674
|
+
const measured = groups.map((group) => ({
|
|
675
|
+
group,
|
|
676
|
+
widths: group.lines.map((line) => {
|
|
677
|
+
ctx.font = fontOf(line, line.size, fonts);
|
|
678
|
+
return ctx.measureText(line.text).width;
|
|
679
|
+
})
|
|
680
|
+
}));
|
|
681
|
+
const { columns, k: lo } = bestColumns(measured, zone);
|
|
639
682
|
const out = emptyFlow();
|
|
640
|
-
const heights = columns.map((c) => placeColumn(c.groups, lo, c.col, null));
|
|
683
|
+
const heights = columns.map((c) => placeColumn(c.groups, { k: lo, col: c.col }, null));
|
|
641
684
|
const end = Math.max(...heights);
|
|
642
|
-
columns.forEach((c, i) => placeColumn(c.groups, lo, c.col,
|
|
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));
|
|
643
686
|
const lines = groups.flatMap((g) => g.lines);
|
|
644
687
|
out.texts.forEach((t, i) => {
|
|
645
688
|
const line = lines[i];
|
|
@@ -657,36 +700,99 @@ function flowFlanks(ctx, groups, flanks, fonts) {
|
|
|
657
700
|
}
|
|
658
701
|
function smallestPerLine(groups, results) {
|
|
659
702
|
const sizes = results.flatMap((r3) => r3.texts.map((t) => t.box.h / LINE_HEIGHT2));
|
|
660
|
-
const
|
|
703
|
+
const smallest2 = [];
|
|
661
704
|
let i = 0;
|
|
662
705
|
groups.forEach(
|
|
663
706
|
(g) => g.lines.forEach((_line, j) => {
|
|
664
|
-
|
|
707
|
+
smallest2[j] = Math.min(smallest2[j] ?? Infinity, sizes[i] ?? Infinity);
|
|
665
708
|
i++;
|
|
666
709
|
})
|
|
667
710
|
);
|
|
668
|
-
return
|
|
711
|
+
return smallest2;
|
|
669
712
|
}
|
|
670
713
|
function equalFlanks(ctx, groups, flanks, fonts) {
|
|
671
|
-
const
|
|
672
|
-
const sized = groups.map((g) => ({ ...g, lines: g.lines.map((line, j) => ({ ...line, size: Math.min(line.size,
|
|
714
|
+
const smallest2 = smallestPerLine(groups, flowFlanks(ctx, groups, flanks, fonts));
|
|
715
|
+
const sized = groups.map((g) => ({ ...g, lines: g.lines.map((line, j) => ({ ...line, size: Math.min(line.size, smallest2[j] ?? line.size) })) }));
|
|
673
716
|
return flowFlanks(ctx, sized, flanks, fonts);
|
|
674
717
|
}
|
|
675
718
|
function measuredEnd(f, fallback) {
|
|
676
719
|
const bottoms = [...f.texts.map((t) => t.box), ...f.rules.map((r3) => r3.rect), ...f.icons.map((i) => i.rect)].map((r3) => r3.y + r3.h);
|
|
677
720
|
return bottoms.length > 0 ? Math.max(...bottoms) : fallback;
|
|
678
721
|
}
|
|
679
|
-
function pictureRegion(layout, geo, body,
|
|
722
|
+
function pictureRegion(layout, geo, body, frame) {
|
|
680
723
|
if (layout !== "picture-bottom" /* PictureBottom */) {
|
|
681
724
|
return geo.picture;
|
|
682
725
|
}
|
|
683
|
-
const y = Math.min(geo.picture.y, Math.ceil(measuredEnd(body, geo.body.y) + BODY_GAP * u));
|
|
726
|
+
const y = Math.min(geo.picture.y, Math.ceil(measuredEnd(body, geo.body.y) + BODY_GAP * frame.u));
|
|
684
727
|
return { x: geo.picture.x, y, w: geo.picture.w, h: frame.height - y };
|
|
685
728
|
}
|
|
686
729
|
|
|
687
|
-
// src/render/
|
|
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
|
+
// src/render/fitToFloor.ts
|
|
760
|
+
var FLOOR_PX = 30;
|
|
688
761
|
var BASE = 1080;
|
|
762
|
+
var LINE_HEIGHT3 = 1.2;
|
|
689
763
|
var LOGO_SHARE = 0.26;
|
|
764
|
+
var GIVE_STEP = 0.025;
|
|
765
|
+
var GIVE_STEPS = 32;
|
|
766
|
+
function splitHeader(header, hasLogo) {
|
|
767
|
+
const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
|
|
768
|
+
return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
|
|
769
|
+
}
|
|
770
|
+
function flowAll(ctx, input, geo, u) {
|
|
771
|
+
const { doc, palette, fonts } = input;
|
|
772
|
+
const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
|
|
773
|
+
const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
|
|
774
|
+
const flanks = geo.flanks ?? [];
|
|
775
|
+
const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
|
|
776
|
+
const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
|
|
777
|
+
return { geo, zones, head, body, sides };
|
|
778
|
+
}
|
|
779
|
+
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);
|
|
782
|
+
}
|
|
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);
|
|
788
|
+
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);
|
|
790
|
+
}
|
|
791
|
+
return fitted;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/render/render.ts
|
|
795
|
+
var BASE2 = 1080;
|
|
690
796
|
var CARD_RADIUS = 0.035;
|
|
691
797
|
var CAP_TOP = 0.15;
|
|
692
798
|
var PIN_HEAD_Y = 0.08;
|
|
@@ -781,10 +887,6 @@ function capAligned(zone, head) {
|
|
|
781
887
|
const shift = first ? Math.min(Math.max(0, Math.round(first.y + first.h * CAP_TOP) - zone.y), Math.round(zone.h / 2)) : 0;
|
|
782
888
|
return { ...zone, y: zone.y + shift, h: zone.h - shift };
|
|
783
889
|
}
|
|
784
|
-
function splitHeader(header, hasLogo) {
|
|
785
|
-
const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
|
|
786
|
-
return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
|
|
787
|
-
}
|
|
788
890
|
function render(input, assets, canvas) {
|
|
789
891
|
const doc = checked(input, assets);
|
|
790
892
|
const { width, height } = FORMAT_SIZES[doc.format];
|
|
@@ -796,8 +898,8 @@ function render(input, assets, canvas) {
|
|
|
796
898
|
}
|
|
797
899
|
const palette = PALETTES[doc.palette];
|
|
798
900
|
const fonts = FONT_SETS[doc.fonts];
|
|
799
|
-
const
|
|
800
|
-
const
|
|
901
|
+
const u = Math.min(width, height) / BASE2;
|
|
902
|
+
const { geo, zones, head, body, sides } = fitToFloor(ctx, { doc, palette, fonts, width, height });
|
|
801
903
|
ctx.globalAlpha = 1;
|
|
802
904
|
ctx.fillStyle = palette.background;
|
|
803
905
|
ctx.fillRect(0, 0, width, height);
|
|
@@ -805,15 +907,10 @@ function render(input, assets, canvas) {
|
|
|
805
907
|
ctx.fillStyle = palette.tint;
|
|
806
908
|
[geo.header, geo.body].forEach((r3) => ctx.fillRect(r3.x, r3.y, r3.w, r3.h));
|
|
807
909
|
}
|
|
808
|
-
const zones = geo.logo ? { text: geo.header, logo: geo.logo } : splitHeader(geo.header, doc.logo !== void 0);
|
|
809
|
-
const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
|
|
810
|
-
const flanks = geo.flanks ?? [];
|
|
811
|
-
const body = flow(ctx, bodyGroups(doc, palette, u, flanks.length === 0), geo.body, fonts);
|
|
812
|
-
const sides = flanks.length > 0 ? equalFlanks(ctx, flankPriceGroups(doc, palette, u), flanks, fonts) : [emptyFlow()];
|
|
813
910
|
const radius = Math.min(width, height) * CARD_RADIUS;
|
|
814
911
|
[head, body, ...sides].forEach((f) => drawFlow(ctx, f, radius));
|
|
815
912
|
const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
|
|
816
|
-
const logo = logoImage ? placeContain(logoImage, geo.logo ? zones.logo : capAligned(zones.logo, head), "top" /* Top */, 1, 1) : null;
|
|
913
|
+
const logo = logoImage ? placeContain(logoImage, geo.logo ? zones.logo : capAligned(zones.logo, head), { anchor: "top" /* Top */, alignX: 1, scale: 1 }) : null;
|
|
817
914
|
if (logoImage && logo) {
|
|
818
915
|
ctx.drawImage(logoImage, logo.x, logo.y, logo.w, logo.h);
|
|
819
916
|
}
|
|
@@ -822,8 +919,8 @@ function render(input, assets, canvas) {
|
|
|
822
919
|
if (backdrop) {
|
|
823
920
|
drawArch(ctx, backdrop, palette.arch);
|
|
824
921
|
}
|
|
825
|
-
const region = pictureRegion(doc.layout, geo, body,
|
|
826
|
-
const picture = image && doc.picture ? placeContain(image, region, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
|
|
922
|
+
const region = pictureRegion(doc.layout, geo, body, { height, u });
|
|
923
|
+
const picture = image && doc.picture ? placeContain(image, region, { anchor: doc.picture.anchor, alignX: geo.alignX, scale: doc.picture.scale }) : null;
|
|
827
924
|
if (image && picture) {
|
|
828
925
|
ctx.drawImage(image, picture.x, picture.y, picture.w, picture.h);
|
|
829
926
|
}
|