@dloizides/poster-render 0.2.4 → 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 +7 -0
- package/README.md +40 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +70 -40
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +70 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.5
|
|
4
|
+
|
|
5
|
+
- Own-key checks: the schema validator and the asset lookup use `Object.hasOwn`, so a `__proto__` key is rejected and an imageId such as `constructor` is reported missing.
|
|
6
|
+
- `placeContain(image, region, { anchor, alignX, scale })` takes an options object (was 5 positional parameters).
|
|
7
|
+
- Named size and gap constants; ESLint now lints tests and `src/testing`.
|
|
8
|
+
- README: install, usage, canvas injection, fonts, type floor, schema version policy.
|
|
9
|
+
|
|
3
10
|
## 0.2.4
|
|
4
11
|
|
|
5
12
|
- 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.
|
package/README.md
CHANGED
|
@@ -7,7 +7,46 @@ One JSON document (`FlierDoc`) in, one poster out. Used by Flier Studio (FLIER-1
|
|
|
7
7
|
- `render(doc, assets, canvas)`: validates first (throws `FlierDocError` before touching the canvas), resizes the canvas to the format, draws, returns a `RenderReport` (picture rect, text and card boxes).
|
|
8
8
|
- `exportPng(canvas)`: PNG `Blob` of the canvas size.
|
|
9
9
|
- Formats: story 1080x1920, post 1080x1350, square 1080x1080, phone 1170x2532, desktop 2560x1440.
|
|
10
|
-
- Layouts: `picture-right` (E-A), `picture-left` (E-B), `arch` (E-C), `picture-bottom`, `list`, `phone-clock-safe` (P-A, top kept clear for the lock-screen clock), `phone-arch` (P-B), `desktop-safe` (D-A, text clear of desktop icons and the taskbar), `desktop-arch` (D-B). `LAYOUT_CATALOGUE` lists id, design id, label, description and designed formats (for the app picker and the LLM prompt); `defaultLayoutFor(format)` gives E-A / P-A / D-A.
|
|
10
|
+
- Layouts: `picture-right` (E-A "Picture right"), `picture-left` (E-B "Picture left"), `arch` (E-C "Arch"), `picture-bottom`, `list`, `phone-clock-safe` (P-A "Phone, clock clear": top kept clear for the lock-screen clock), `phone-arch` (P-B "Phone arch"), `desktop-safe` (D-A "Desktop, icons clear": text clear of desktop icons and the taskbar), `desktop-arch` (D-B "Desktop arch"). `LAYOUT_CATALOGUE` lists id, design id, label, description and designed formats (for the app picker and the LLM prompt); `defaultLayoutFor(format)` gives E-A "Picture right" / P-A "Phone, clock clear" / D-A "Desktop, icons clear" (D4 "Layouts: keep all").
|
|
11
11
|
- Arch layouts paint a soft arch (palette `arch`, about 4% off the ground) behind the picture only, inside the picture region; prices sit large on either side. The includes block is set at one size for every line. Palettes: `sand`, `night`, `sea`, `rose`. Font sets: `classic`, `modern`, `elegant` (the app loads the web fonts).
|
|
12
12
|
- The picture is placed contain + anchor in its own region: never cropped, never faded, drawn last, and no text or card is laid out in its region. Text shrinks to fit, never overflows.
|
|
13
13
|
- Example: `kizombaWorkshopDocs` (3 workshops + list flier), `kizombaWorkshop` (the list flier).
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @dloizides/poster-render
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { exportPng, kizombaWorkshop, render, validateFlierDoc } from '@dloizides/poster-render';
|
|
25
|
+
|
|
26
|
+
const checked = validateFlierDoc(kizombaWorkshop);
|
|
27
|
+
if (!checked.ok) {
|
|
28
|
+
throw new Error(checked.errors.join('\n'));
|
|
29
|
+
}
|
|
30
|
+
const canvas = document.createElement('canvas');
|
|
31
|
+
const report = render(checked.doc, { dancers: pictureImage, logo: logoImage }, canvas);
|
|
32
|
+
const png = await exportPng(canvas);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Canvas injection
|
|
36
|
+
|
|
37
|
+
`render` takes any canvas with a 2d context; the package has no canvas dependency.
|
|
38
|
+
|
|
39
|
+
- Browser: an `HTMLCanvasElement` (or `OffscreenCanvas`); assets are loaded `HTMLImageElement` / `ImageBitmap`.
|
|
40
|
+
- Node: `@napi-rs/canvas` (`createCanvas(1, 1)`, `loadImage(path)`); `render` resizes the canvas to the format.
|
|
41
|
+
|
|
42
|
+
## Fonts
|
|
43
|
+
|
|
44
|
+
The font sets name CSS families (`FONT_SETS`). The host loads them before `render`: in a browser with `document.fonts.load(...)` / `@font-face`, in Node with `GlobalFonts.registerFromPath` from `@napi-rs/canvas`. A missing font falls back to the default family and the text still fits.
|
|
45
|
+
|
|
46
|
+
## Type floor
|
|
47
|
+
|
|
48
|
+
Every text run is at least 30px at a 1080 short edge (40px on desktop 2560x1440). When a layout cannot fit its text at the floor, the picture gives space to the text.
|
|
49
|
+
|
|
50
|
+
## Schema version policy
|
|
51
|
+
|
|
52
|
+
`FlierDoc.version` is `1`. A field added as optional keeps version 1; a removed, renamed or retyped field raises the version and `validateFlierDoc` rejects the other version with `version: must be 1 (got N)`. A package major release goes with a new doc version.
|
package/dist/index.d.mts
CHANGED
|
@@ -217,7 +217,7 @@ interface LayoutCatalogueEntry {
|
|
|
217
217
|
formats: readonly FormatId[];
|
|
218
218
|
}
|
|
219
219
|
declare const LAYOUT_CATALOGUE: readonly LayoutCatalogueEntry[];
|
|
220
|
-
/** The default layout for a format (D4: E-A for posters, P-A
|
|
220
|
+
/** The default layout for a format (D4 "Layouts: keep all"): E-A "Picture right" for posters, P-A "Phone, clock clear", D-A "Desktop, icons clear". */
|
|
221
221
|
declare function defaultLayoutFor(format: FormatId): LayoutId;
|
|
222
222
|
|
|
223
223
|
declare const kizombaWorkshop: FlierDoc;
|
package/dist/index.d.ts
CHANGED
|
@@ -217,7 +217,7 @@ interface LayoutCatalogueEntry {
|
|
|
217
217
|
formats: readonly FormatId[];
|
|
218
218
|
}
|
|
219
219
|
declare const LAYOUT_CATALOGUE: readonly LayoutCatalogueEntry[];
|
|
220
|
-
/** The default layout for a format (D4: E-A for posters, P-A
|
|
220
|
+
/** The default layout for a format (D4 "Layouts: keep all"): E-A "Picture right" for posters, P-A "Phone, clock clear", D-A "Desktop, icons clear". */
|
|
221
221
|
declare function defaultLayoutFor(format: FormatId): LayoutId;
|
|
222
222
|
|
|
223
223
|
declare const kizombaWorkshop: FlierDoc;
|
package/dist/index.js
CHANGED
|
@@ -226,7 +226,7 @@ function checkObject(schema, value, path, errors) {
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
for (const [key, child] of Object.entries(value)) {
|
|
229
|
-
const childSchema = props[key];
|
|
229
|
+
const childSchema = Object.hasOwn(props, key) ? props[key] : void 0;
|
|
230
230
|
if (childSchema === void 0) {
|
|
231
231
|
if (schema.additionalProperties === false) {
|
|
232
232
|
errors.push(`${join(path, key)}: is not allowed`);
|
|
@@ -284,10 +284,10 @@ function validateFlierDoc(input) {
|
|
|
284
284
|
}
|
|
285
285
|
function missingAssets(doc, assets) {
|
|
286
286
|
const errors = [];
|
|
287
|
-
if (doc.picture && assets
|
|
287
|
+
if (doc.picture && !Object.hasOwn(assets, doc.picture.imageId)) {
|
|
288
288
|
errors.push(`picture.imageId: no asset named "${doc.picture.imageId}"`);
|
|
289
289
|
}
|
|
290
|
-
if (doc.logo && assets
|
|
290
|
+
if (doc.logo && !Object.hasOwn(assets, doc.logo.imageId)) {
|
|
291
291
|
errors.push(`logo.imageId: no asset named "${doc.logo.imageId}"`);
|
|
292
292
|
}
|
|
293
293
|
return errors;
|
|
@@ -347,9 +347,10 @@ var BOTTOM_MIN_TOP = 0.25;
|
|
|
347
347
|
var BOTTOM_HEADER_SHARE = 0.3;
|
|
348
348
|
var BOTTOM_GAP = 40 / 1080;
|
|
349
349
|
var BOTTOM_ASPECT = 0.57;
|
|
350
|
+
var DEFAULT_SIDE = [0.6, 0.42];
|
|
350
351
|
var PICTURE_SIDE = {
|
|
351
|
-
["picture-right" /* PictureRight */]:
|
|
352
|
-
["picture-left" /* PictureLeft */]:
|
|
352
|
+
["picture-right" /* PictureRight */]: DEFAULT_SIDE,
|
|
353
|
+
["picture-left" /* PictureLeft */]: DEFAULT_SIDE,
|
|
353
354
|
["list" /* List */]: [0.5, 0.36]
|
|
354
355
|
};
|
|
355
356
|
var ANCHOR_Y = { ["top" /* Top */]: 0, ["center" /* Center */]: 0.5, ["bottom" /* Bottom */]: 1 };
|
|
@@ -409,13 +410,14 @@ function geometry(layout, w, h, give = 0) {
|
|
|
409
410
|
if (layout === "picture-bottom" /* PictureBottom */) {
|
|
410
411
|
return bottom(w, h, BOTTOM_PICTURE_W - give);
|
|
411
412
|
}
|
|
412
|
-
const g = side(w, h, PICTURE_SIDE[layout] ??
|
|
413
|
+
const g = side(w, h, PICTURE_SIDE[layout] ?? DEFAULT_SIDE);
|
|
413
414
|
if (layout !== "picture-left" /* PictureLeft */) {
|
|
414
415
|
return g;
|
|
415
416
|
}
|
|
416
417
|
return { header: mirror(g.header, w), body: mirror(g.body, w), picture: mirror(g.picture, w), alignX: 1 - g.alignX };
|
|
417
418
|
}
|
|
418
|
-
function placeContain(image, region,
|
|
419
|
+
function placeContain(image, region, fit) {
|
|
420
|
+
const { anchor, alignX, scale } = fit;
|
|
419
421
|
const s = Math.min(region.w / image.width, region.h / image.height) * scale;
|
|
420
422
|
const w = Math.floor(image.width * s);
|
|
421
423
|
const h = Math.floor(image.height * s);
|
|
@@ -432,6 +434,29 @@ var BOLD = 700;
|
|
|
432
434
|
var SEMI = 600;
|
|
433
435
|
var MEDIUM = 500;
|
|
434
436
|
var SCRIPT = 400;
|
|
437
|
+
var SIZE = {
|
|
438
|
+
title: 250,
|
|
439
|
+
headerScript: 120,
|
|
440
|
+
itemDate: 67,
|
|
441
|
+
itemRow: 42,
|
|
442
|
+
scheduleLabel: 66,
|
|
443
|
+
scheduleTime: 60,
|
|
444
|
+
priceAmount: 66,
|
|
445
|
+
priceLabel: 52,
|
|
446
|
+
listPrice: 58,
|
|
447
|
+
flankAmount: 110,
|
|
448
|
+
flankLabel: 48,
|
|
449
|
+
venue: 58,
|
|
450
|
+
listVenue: 48,
|
|
451
|
+
small: 38
|
|
452
|
+
};
|
|
453
|
+
var GAP = {
|
|
454
|
+
item: 48,
|
|
455
|
+
row: 30,
|
|
456
|
+
venue: 40,
|
|
457
|
+
includes: 24,
|
|
458
|
+
rule: 8
|
|
459
|
+
};
|
|
435
460
|
var makers = (p, u) => ({
|
|
436
461
|
display: (text2, size) => ({ text: text2, role: "display", weight: BOLD, size: size * u, color: p.title }),
|
|
437
462
|
venue: (text2, size) => ({ text: text2, role: "display", weight: MEDIUM, size: size * u, color: p.ink }),
|
|
@@ -444,36 +469,36 @@ var makers = (p, u) => ({
|
|
|
444
469
|
function headerGroups(doc, p, u) {
|
|
445
470
|
const l = makers(p, u);
|
|
446
471
|
const b = doc.blocks;
|
|
447
|
-
const lines = [l.display
|
|
472
|
+
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];
|
|
448
473
|
return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
|
|
449
474
|
}
|
|
450
475
|
function itemGroup(item, l, u) {
|
|
451
|
-
const lines = [l.script
|
|
452
|
-
item.schedule.forEach((row) => lines.push(l.strong
|
|
476
|
+
const lines = [l.script(item.date, SIZE.itemDate)];
|
|
477
|
+
item.schedule.forEach((row) => lines.push(l.strong(`${row.label} ${row.time}`, SIZE.itemRow)));
|
|
453
478
|
if (item.price) {
|
|
454
|
-
lines.push(l.accent
|
|
479
|
+
lines.push(l.accent(item.price, SIZE.itemRow));
|
|
455
480
|
}
|
|
456
|
-
return { lines, gapAfter:
|
|
481
|
+
return { lines, gapAfter: GAP.item * u };
|
|
457
482
|
}
|
|
458
483
|
function eventGroups(doc, l, u, withPrices) {
|
|
459
484
|
const b = doc.blocks;
|
|
460
485
|
const groups = [];
|
|
461
|
-
b.schedule?.forEach((row) => groups.push({ lines: [l.strong
|
|
486
|
+
b.schedule?.forEach((row) => groups.push({ lines: [l.strong(row.label, SIZE.scheduleLabel), l.accent(row.time, SIZE.scheduleTime)], gapAfter: GAP.row * u }));
|
|
462
487
|
const prices = (withPrices ? b.prices : void 0) ?? [];
|
|
463
|
-
prices.forEach((row, i) => groups.push({ lines: [l.strong
|
|
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 }));
|
|
464
489
|
return groups;
|
|
465
490
|
}
|
|
466
491
|
function listGroups(doc, l, u) {
|
|
467
492
|
const groups = (doc.items ?? []).map((item) => itemGroup(item, l, u));
|
|
468
|
-
const prices = doc.blocks.prices?.map((row) => l.strong
|
|
493
|
+
const prices = doc.blocks.prices?.map((row) => l.strong(`${row.amount} ${row.label}`, SIZE.listPrice)) ?? [];
|
|
469
494
|
if (prices.length > 0) {
|
|
470
|
-
groups.push({ lines: prices, gapAfter:
|
|
495
|
+
groups.push({ lines: prices, gapAfter: GAP.row * u });
|
|
471
496
|
}
|
|
472
497
|
return groups;
|
|
473
498
|
}
|
|
474
499
|
function flankPriceGroups(doc, p, u) {
|
|
475
500
|
const l = makers(p, u);
|
|
476
|
-
return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong
|
|
501
|
+
return (doc.blocks.prices ?? []).map((row) => ({ lines: [l.strong(row.amount, SIZE.flankAmount), l.accent(row.label, SIZE.flankLabel)], gapAfter: GAP.row * u }));
|
|
477
502
|
}
|
|
478
503
|
function bodyGroups(doc, p, u, withPrices = true) {
|
|
479
504
|
const l = makers(p, u);
|
|
@@ -481,15 +506,16 @@ function bodyGroups(doc, p, u, withPrices = true) {
|
|
|
481
506
|
const groups = [];
|
|
482
507
|
const isList = doc.layout === "list" /* List */;
|
|
483
508
|
if (b.venue) {
|
|
484
|
-
|
|
509
|
+
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 } });
|
|
485
511
|
}
|
|
486
512
|
groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u, withPrices));
|
|
487
513
|
if (b.includes) {
|
|
488
|
-
const lines = [l.accent
|
|
489
|
-
groups.push({ lines, gapAfter:
|
|
514
|
+
const lines = [l.accent(b.includes.heading, SIZE.small), ...b.includes.items.map((i) => l.item(i, SIZE.small))];
|
|
515
|
+
groups.push({ lines, gapAfter: GAP.includes * u, uniform: true });
|
|
490
516
|
}
|
|
491
517
|
if (b.footer) {
|
|
492
|
-
groups.push({ lines: [l.soft
|
|
518
|
+
groups.push({ lines: [l.soft(b.footer, SIZE.small)], gapAfter: 0 });
|
|
493
519
|
}
|
|
494
520
|
return groups;
|
|
495
521
|
}
|
|
@@ -501,6 +527,7 @@ var BULLET_RADIUS = 0.2;
|
|
|
501
527
|
var RULE_GAP = 2.5;
|
|
502
528
|
var RULE_WIDTH = 0.9;
|
|
503
529
|
var WIDE = 1.6;
|
|
530
|
+
var COLUMN_GAP = 0.04;
|
|
504
531
|
var TWO_COLUMN_MIN = 0.75;
|
|
505
532
|
var TWO_COLUMN_GAIN = 1.15;
|
|
506
533
|
var MIN_K = 0.02;
|
|
@@ -523,15 +550,17 @@ function groupRatio(m, k, avail) {
|
|
|
523
550
|
}
|
|
524
551
|
return Math.min(...m.group.lines.map((line, i) => lineSize(line, m.widths[i] ?? 1, k, avail) / line.size));
|
|
525
552
|
}
|
|
526
|
-
function placeIcon(group,
|
|
553
|
+
function placeIcon(group, box, out) {
|
|
554
|
+
const { d, x, top, bottom: bottom2 } = box;
|
|
527
555
|
if (!group.icon || d <= 0) {
|
|
528
|
-
return
|
|
556
|
+
return bottom2;
|
|
529
557
|
}
|
|
530
|
-
const textH =
|
|
531
|
-
out?.icons.push({ rect: { x, y:
|
|
532
|
-
return
|
|
558
|
+
const textH = bottom2 - top;
|
|
559
|
+
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 });
|
|
560
|
+
return top + Math.max(textH, d);
|
|
533
561
|
}
|
|
534
|
-
function placeGroup(m,
|
|
562
|
+
function placeGroup(m, at, out) {
|
|
563
|
+
const { k, col, y0 } = at;
|
|
535
564
|
const { group } = m;
|
|
536
565
|
let y = y0;
|
|
537
566
|
if (group.rule) {
|
|
@@ -556,7 +585,7 @@ function placeGroup(m, k, col, y0, out) {
|
|
|
556
585
|
}
|
|
557
586
|
y += size * LINE_HEIGHT;
|
|
558
587
|
});
|
|
559
|
-
y = placeIcon(group, d, col.x + pad, top + pad, y, out);
|
|
588
|
+
y = placeIcon(group, { d, x: col.x + pad, top: top + pad, bottom: y }, out);
|
|
560
589
|
y += pad;
|
|
561
590
|
if (group.rule && group.ruleBelow) {
|
|
562
591
|
y += group.rule.height * k * (RULE_GAP - 1);
|
|
@@ -568,10 +597,11 @@ function placeGroup(m, k, col, y0, out) {
|
|
|
568
597
|
}
|
|
569
598
|
return y + group.gapAfter * k - y0;
|
|
570
599
|
}
|
|
571
|
-
function placeColumn(groups,
|
|
600
|
+
function placeColumn(groups, at, out) {
|
|
601
|
+
const { k, col, spread = 0 } = at;
|
|
572
602
|
let h = 0;
|
|
573
603
|
for (const g of groups) {
|
|
574
|
-
h += placeGroup(g, k, col, col.y + h, out) + spread;
|
|
604
|
+
h += placeGroup(g, { k, col, y0: col.y + h }, out) + spread;
|
|
575
605
|
}
|
|
576
606
|
return h - spread;
|
|
577
607
|
}
|
|
@@ -598,9 +628,9 @@ function keepTogether(groups, heights, cut) {
|
|
|
598
628
|
return candidates.reduce((best, c) => tallest(heights, c) < tallest(heights, best) ? c : best);
|
|
599
629
|
}
|
|
600
630
|
function splitColumns(groups, zone) {
|
|
601
|
-
const gap = Math.round(zone.w *
|
|
631
|
+
const gap = Math.round(zone.w * COLUMN_GAP);
|
|
602
632
|
const w = (zone.w - gap) / 2;
|
|
603
|
-
const heights = groups.map((g) => placeGroup(g, 1, { ...zone, w }, 0, null));
|
|
633
|
+
const heights = groups.map((g) => placeGroup(g, { k: 1, col: { ...zone, w }, y0: 0 }, null));
|
|
604
634
|
const total = columnSum(heights, 0, heights.length);
|
|
605
635
|
let cut = 1;
|
|
606
636
|
for (let c = 2; c < groups.length; c++) {
|
|
@@ -615,7 +645,7 @@ function splitColumns(groups, zone) {
|
|
|
615
645
|
];
|
|
616
646
|
}
|
|
617
647
|
function largestK(columns, zoneH) {
|
|
618
|
-
const fits = (k) => columns.every((c) => placeColumn(c.groups, k, c.col, null) <= zoneH);
|
|
648
|
+
const fits = (k) => columns.every((c) => placeColumn(c.groups, { k, col: c.col }, null) <= zoneH);
|
|
619
649
|
let lo = MIN_K;
|
|
620
650
|
let hi = 1;
|
|
621
651
|
if (fits(1)) {
|
|
@@ -652,9 +682,9 @@ function flow(ctx, groups, zone, fonts) {
|
|
|
652
682
|
}));
|
|
653
683
|
const { columns, k: lo } = bestColumns(measured, zone);
|
|
654
684
|
const out = emptyFlow();
|
|
655
|
-
const heights = columns.map((c) => placeColumn(c.groups, lo, c.col, null));
|
|
685
|
+
const heights = columns.map((c) => placeColumn(c.groups, { k: lo, col: c.col }, null));
|
|
656
686
|
const end = Math.max(...heights);
|
|
657
|
-
columns.forEach((c, i) => placeColumn(c.groups, lo, c.col,
|
|
687
|
+
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));
|
|
658
688
|
const lines = groups.flatMap((g) => g.lines);
|
|
659
689
|
out.texts.forEach((t, i) => {
|
|
660
690
|
const line = lines[i];
|
|
@@ -691,11 +721,11 @@ function measuredEnd(f, fallback) {
|
|
|
691
721
|
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);
|
|
692
722
|
return bottoms.length > 0 ? Math.max(...bottoms) : fallback;
|
|
693
723
|
}
|
|
694
|
-
function pictureRegion(layout, geo, body,
|
|
724
|
+
function pictureRegion(layout, geo, body, frame) {
|
|
695
725
|
if (layout !== "picture-bottom" /* PictureBottom */) {
|
|
696
726
|
return geo.picture;
|
|
697
727
|
}
|
|
698
|
-
const y = Math.min(geo.picture.y, Math.ceil(measuredEnd(body, geo.body.y) + BODY_GAP * u));
|
|
728
|
+
const y = Math.min(geo.picture.y, Math.ceil(measuredEnd(body, geo.body.y) + BODY_GAP * frame.u));
|
|
699
729
|
return { x: geo.picture.x, y, w: geo.picture.w, h: frame.height - y };
|
|
700
730
|
}
|
|
701
731
|
|
|
@@ -882,7 +912,7 @@ function render(input, assets, canvas) {
|
|
|
882
912
|
const radius = Math.min(width, height) * CARD_RADIUS;
|
|
883
913
|
[head, body, ...sides].forEach((f) => drawFlow(ctx, f, radius));
|
|
884
914
|
const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
|
|
885
|
-
const logo = logoImage ? placeContain(logoImage, geo.logo ? zones.logo : capAligned(zones.logo, head), "top" /* Top */, 1, 1) : null;
|
|
915
|
+
const logo = logoImage ? placeContain(logoImage, geo.logo ? zones.logo : capAligned(zones.logo, head), { anchor: "top" /* Top */, alignX: 1, scale: 1 }) : null;
|
|
886
916
|
if (logoImage && logo) {
|
|
887
917
|
ctx.drawImage(logoImage, logo.x, logo.y, logo.w, logo.h);
|
|
888
918
|
}
|
|
@@ -891,8 +921,8 @@ function render(input, assets, canvas) {
|
|
|
891
921
|
if (backdrop) {
|
|
892
922
|
drawArch(ctx, backdrop, palette.arch);
|
|
893
923
|
}
|
|
894
|
-
const region = pictureRegion(doc.layout, geo, body,
|
|
895
|
-
const picture = image && doc.picture ? placeContain(image, region, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
|
|
924
|
+
const region = pictureRegion(doc.layout, geo, body, { height, u });
|
|
925
|
+
const picture = image && doc.picture ? placeContain(image, region, { anchor: doc.picture.anchor, alignX: geo.alignX, scale: doc.picture.scale }) : null;
|
|
896
926
|
if (image && picture) {
|
|
897
927
|
ctx.drawImage(image, picture.x, picture.y, picture.w, picture.h);
|
|
898
928
|
}
|