@dloizides/poster-render 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,707 @@
1
+ 'use strict';
2
+
3
+ // src/enums/BackgroundStyle.ts
4
+ var BackgroundStyle = /* @__PURE__ */ ((BackgroundStyle2) => {
5
+ BackgroundStyle2["Solid"] = "solid";
6
+ BackgroundStyle2["Tint"] = "tint";
7
+ return BackgroundStyle2;
8
+ })(BackgroundStyle || {});
9
+
10
+ // src/enums/FontSetId.ts
11
+ var FontSetId = /* @__PURE__ */ ((FontSetId2) => {
12
+ FontSetId2["Classic"] = "classic";
13
+ FontSetId2["Modern"] = "modern";
14
+ FontSetId2["Elegant"] = "elegant";
15
+ return FontSetId2;
16
+ })(FontSetId || {});
17
+
18
+ // src/enums/FormatId.ts
19
+ var FormatId = /* @__PURE__ */ ((FormatId2) => {
20
+ FormatId2["Story"] = "story";
21
+ FormatId2["Post"] = "post";
22
+ FormatId2["Square"] = "square";
23
+ FormatId2["Phone"] = "phone";
24
+ FormatId2["Desktop"] = "desktop";
25
+ return FormatId2;
26
+ })(FormatId || {});
27
+
28
+ // src/enums/LayoutId.ts
29
+ var LayoutId = /* @__PURE__ */ ((LayoutId2) => {
30
+ LayoutId2["PictureRight"] = "picture-right";
31
+ LayoutId2["PictureLeft"] = "picture-left";
32
+ LayoutId2["PictureBottom"] = "picture-bottom";
33
+ LayoutId2["List"] = "list";
34
+ return LayoutId2;
35
+ })(LayoutId || {});
36
+
37
+ // src/enums/PaletteId.ts
38
+ var PaletteId = /* @__PURE__ */ ((PaletteId2) => {
39
+ PaletteId2["Sand"] = "sand";
40
+ PaletteId2["Night"] = "night";
41
+ PaletteId2["Sea"] = "sea";
42
+ PaletteId2["Rose"] = "rose";
43
+ return PaletteId2;
44
+ })(PaletteId || {});
45
+
46
+ // src/enums/PictureAnchor.ts
47
+ var PictureAnchor = /* @__PURE__ */ ((PictureAnchor2) => {
48
+ PictureAnchor2["Top"] = "top";
49
+ PictureAnchor2["Center"] = "center";
50
+ PictureAnchor2["Bottom"] = "bottom";
51
+ return PictureAnchor2;
52
+ })(PictureAnchor || {});
53
+
54
+ // src/formats.ts
55
+ var FORMAT_SIZES = {
56
+ ["story" /* Story */]: { width: 1080, height: 1920 },
57
+ ["post" /* Post */]: { width: 1080, height: 1350 },
58
+ ["square" /* Square */]: { width: 1080, height: 1080 },
59
+ ["phone" /* Phone */]: { width: 1170, height: 2532 },
60
+ ["desktop" /* Desktop */]: { width: 2560, height: 1440 }
61
+ };
62
+
63
+ // src/palettes.ts
64
+ var PALETTES = {
65
+ ["sand" /* Sand */]: { background: "#f5f4f2", tint: "#ece8e3", title: "#958a80", accent: "#e9b47f", ink: "#5f564f", soft: "#b3a89e", card: "#ffffff" },
66
+ ["night" /* Night */]: { background: "#1c1a17", tint: "#25221e", title: "#e8dccb", accent: "#e9b47f", ink: "#f4efe8", soft: "#9c9284", card: "#2c2824" },
67
+ ["sea" /* Sea */]: { background: "#eef4f3", tint: "#e1ecea", title: "#4f7d7a", accent: "#e07a5f", ink: "#2f3e46", soft: "#8aa3a1", card: "#ffffff" },
68
+ ["rose" /* Rose */]: { background: "#f8efec", tint: "#f1e2dd", title: "#9b6b63", accent: "#c9846f", ink: "#4a3934", soft: "#b39a92", card: "#ffffff" }
69
+ };
70
+
71
+ // src/fontSets.ts
72
+ var FONT_SETS = {
73
+ ["classic" /* Classic */]: { display: '"Oswald", "Arial Narrow", sans-serif', script: '"Sacramento", cursive', body: '"Fredoka", sans-serif' },
74
+ ["modern" /* Modern */]: { display: '"Bebas Neue", "Arial Narrow", sans-serif', script: '"Great Vibes", cursive', body: '"Montserrat", sans-serif' },
75
+ ["elegant" /* Elegant */]: { display: '"Playfair Display", serif', script: '"Allura", cursive', body: '"Lato", sans-serif' }
76
+ };
77
+
78
+ // src/schema/flierDocSchema.ts
79
+ var SHORT = 60;
80
+ var LINE = 120;
81
+ var MAX_ROWS = 6;
82
+ var MAX_ITEMS = 8;
83
+ var MAX_PRICES = 4;
84
+ var MAX_VENUE = 3;
85
+ var MIN_SCALE = 0.3;
86
+ var text = (maxLength) => ({ type: "string", minLength: 1, maxLength });
87
+ var scheduleRow = {
88
+ type: "object",
89
+ required: ["label", "time"],
90
+ additionalProperties: false,
91
+ properties: { label: text(SHORT), time: text(SHORT) }
92
+ };
93
+ var scheduleList = { type: "array", items: scheduleRow, minItems: 1, maxItems: MAX_ROWS };
94
+ var blocks = {
95
+ type: "object",
96
+ required: ["title"],
97
+ additionalProperties: false,
98
+ properties: {
99
+ title: text(SHORT),
100
+ titleBottom: text(SHORT),
101
+ script: text(SHORT),
102
+ venue: { type: "array", items: text(SHORT), minItems: 1, maxItems: MAX_VENUE },
103
+ schedule: scheduleList,
104
+ prices: {
105
+ type: "array",
106
+ minItems: 1,
107
+ maxItems: MAX_PRICES,
108
+ items: {
109
+ type: "object",
110
+ required: ["amount", "label"],
111
+ additionalProperties: false,
112
+ properties: { amount: text(SHORT), label: text(SHORT) }
113
+ }
114
+ },
115
+ includes: {
116
+ type: "object",
117
+ required: ["heading", "items"],
118
+ additionalProperties: false,
119
+ properties: { heading: text(SHORT), items: { type: "array", items: text(LINE), minItems: 1, maxItems: MAX_ROWS } }
120
+ },
121
+ footer: text(LINE)
122
+ }
123
+ };
124
+ var flierDocSchema = {
125
+ $schema: "https://json-schema.org/draft/2020-12/schema",
126
+ $id: "https://dloizides.com/schemas/flier-doc-v1.json",
127
+ title: "FlierDoc",
128
+ description: "One poster or wallpaper: format, layout, palette, fonts, picture slot and text blocks.",
129
+ type: "object",
130
+ required: ["version", "format", "layout", "palette", "fonts", "background", "blocks"],
131
+ additionalProperties: false,
132
+ properties: {
133
+ version: { const: 1 },
134
+ format: { type: "string", enum: Object.values(FormatId) },
135
+ layout: { type: "string", enum: Object.values(LayoutId) },
136
+ palette: { type: "string", enum: Object.values(PaletteId) },
137
+ fonts: { type: "string", enum: Object.values(FontSetId) },
138
+ background: { type: "string", enum: Object.values(BackgroundStyle) },
139
+ picture: {
140
+ type: "object",
141
+ required: ["imageId", "anchor", "scale"],
142
+ additionalProperties: false,
143
+ properties: {
144
+ imageId: text(SHORT),
145
+ anchor: { type: "string", enum: Object.values(PictureAnchor) },
146
+ scale: { type: "number", minimum: MIN_SCALE, maximum: 1 }
147
+ }
148
+ },
149
+ logo: { type: "object", required: ["imageId"], additionalProperties: false, properties: { imageId: text(SHORT) } },
150
+ blocks,
151
+ items: {
152
+ type: "array",
153
+ minItems: 1,
154
+ maxItems: MAX_ITEMS,
155
+ items: {
156
+ type: "object",
157
+ required: ["date", "schedule"],
158
+ additionalProperties: false,
159
+ properties: { date: text(SHORT), schedule: scheduleList, price: text(SHORT) }
160
+ }
161
+ }
162
+ }
163
+ };
164
+
165
+ // src/schema/validate.ts
166
+ var typeName = (v) => {
167
+ if (v === null) {
168
+ return "null";
169
+ }
170
+ return Array.isArray(v) ? "array" : typeof v;
171
+ };
172
+ var join = (path, key) => path === "" ? key : `${path}.${key}`;
173
+ var show = (v) => JSON.stringify(v) ?? typeName(v);
174
+ var ARTICLE = { object: "an object", array: "an array", string: "a string", number: "a number", integer: "an integer" };
175
+ function matchesType(schema, value) {
176
+ switch (schema.type) {
177
+ case void 0:
178
+ return true;
179
+ case "integer":
180
+ return Number.isInteger(value);
181
+ case "number":
182
+ return typeof value === "number" && Number.isFinite(value);
183
+ default:
184
+ return typeName(value) === schema.type;
185
+ }
186
+ }
187
+ function checkString(schema, value, path, errors) {
188
+ if (schema.minLength !== void 0 && value.trim().length < schema.minLength) {
189
+ errors.push(`${path}: must not be empty`);
190
+ }
191
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
192
+ errors.push(`${path}: must be at most ${schema.maxLength} characters (got ${value.length})`);
193
+ }
194
+ }
195
+ function checkNumber(schema, value, path, errors) {
196
+ if (schema.minimum !== void 0 && value < schema.minimum) {
197
+ errors.push(`${path}: must be at least ${schema.minimum} (got ${value})`);
198
+ }
199
+ if (schema.maximum !== void 0 && value > schema.maximum) {
200
+ errors.push(`${path}: must be at most ${schema.maximum} (got ${value})`);
201
+ }
202
+ }
203
+ function checkArray(schema, value, path, errors) {
204
+ if (schema.minItems !== void 0 && value.length < schema.minItems) {
205
+ errors.push(`${path}: must have at least ${schema.minItems} entries (got ${value.length})`);
206
+ }
207
+ if (schema.maxItems !== void 0 && value.length > schema.maxItems) {
208
+ errors.push(`${path}: must have at most ${schema.maxItems} entries (got ${value.length})`);
209
+ return;
210
+ }
211
+ const itemSchema = schema.items;
212
+ if (itemSchema) {
213
+ value.forEach((item, i) => validateAgainst(itemSchema, item, `${path}[${i}]`, errors));
214
+ }
215
+ }
216
+ function checkObject(schema, value, path, errors) {
217
+ const props = schema.properties ?? {};
218
+ for (const key of schema.required ?? []) {
219
+ if (value[key] === void 0) {
220
+ errors.push(`${join(path, key)}: is required`);
221
+ }
222
+ }
223
+ for (const [key, child] of Object.entries(value)) {
224
+ const childSchema = props[key];
225
+ if (childSchema === void 0) {
226
+ if (schema.additionalProperties === false) {
227
+ errors.push(`${join(path, key)}: is not allowed`);
228
+ }
229
+ } else if (child !== void 0) {
230
+ validateAgainst(childSchema, child, join(path, key), errors);
231
+ }
232
+ }
233
+ }
234
+ function validateAgainst(schema, value, path, errors) {
235
+ if (schema.const !== void 0) {
236
+ if (value !== schema.const) {
237
+ errors.push(`${path}: must be ${schema.const} (got ${show(value)})`);
238
+ }
239
+ return;
240
+ }
241
+ if (!matchesType(schema, value)) {
242
+ errors.push(`${path}: must be ${ARTICLE[schema.type ?? "object"]} (got ${typeName(value)})`);
243
+ return;
244
+ }
245
+ if (schema.enum && !schema.enum.includes(value)) {
246
+ errors.push(`${path}: must be one of ${schema.enum.join(", ")} (got ${show(value)})`);
247
+ return;
248
+ }
249
+ if (typeof value === "string") {
250
+ checkString(schema, value, path, errors);
251
+ } else if (typeof value === "number") {
252
+ checkNumber(schema, value, path, errors);
253
+ } else if (Array.isArray(value)) {
254
+ checkArray(schema, value, path, errors);
255
+ } else if (typeName(value) === "object") {
256
+ checkObject(schema, value, path, errors);
257
+ }
258
+ }
259
+
260
+ // src/schema/validateFlierDoc.ts
261
+ var FlierDocError = class extends Error {
262
+ constructor(errors) {
263
+ super(`Invalid flier: ${errors.join("; ")}`);
264
+ this.name = "FlierDocError";
265
+ this.errors = errors;
266
+ }
267
+ };
268
+ function validateFlierDoc(input) {
269
+ if (typeName(input) !== "object") {
270
+ return { ok: false, errors: ["doc: must be an object"] };
271
+ }
272
+ const errors = [];
273
+ validateAgainst(flierDocSchema, input, "", errors);
274
+ const doc = input;
275
+ if (doc.layout === "list" /* List */ && (doc.items === void 0 || doc.items.length === 0)) {
276
+ errors.push('items: layout "list" needs at least one item');
277
+ }
278
+ return errors.length === 0 ? { ok: true, doc } : { ok: false, errors };
279
+ }
280
+ function missingAssets(doc, assets) {
281
+ const errors = [];
282
+ if (doc.picture && assets[doc.picture.imageId] === void 0) {
283
+ errors.push(`picture.imageId: no asset named "${doc.picture.imageId}"`);
284
+ }
285
+ if (doc.logo && assets[doc.logo.imageId] === void 0) {
286
+ errors.push(`logo.imageId: no asset named "${doc.logo.imageId}"`);
287
+ }
288
+ return errors;
289
+ }
290
+
291
+ // src/layout/blockGroups.ts
292
+ var BOLD = 700;
293
+ var SEMI = 600;
294
+ var MEDIUM = 500;
295
+ var SCRIPT = 400;
296
+ var makers = (p, u) => ({
297
+ display: (text2, size) => ({ text: text2, role: "display", weight: BOLD, size: size * u, color: p.title }),
298
+ venue: (text2, size) => ({ text: text2, role: "display", weight: MEDIUM, size: size * u, color: p.ink }),
299
+ script: (text2, size) => ({ text: text2, role: "script", weight: SCRIPT, size: size * u, color: p.accent }),
300
+ strong: (text2, size) => ({ text: text2, role: "body", weight: BOLD, size: size * u, color: p.ink }),
301
+ accent: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.accent }),
302
+ item: (text2, size) => ({ text: text2, role: "body", weight: SEMI, size: size * u, color: p.ink, bullet: p.accent }),
303
+ soft: (text2, size) => ({ text: text2, role: "body", weight: MEDIUM, size: size * u, color: p.soft })
304
+ });
305
+ function headerGroups(doc, p, u) {
306
+ const l = makers(p, u);
307
+ const b = doc.blocks;
308
+ 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];
309
+ return [{ lines: lines.filter((x) => x !== void 0), gapAfter: 0 }];
310
+ }
311
+ function itemGroup(item, l, u) {
312
+ const lines = [l.script?.(item.date, 96)];
313
+ item.schedule.forEach((row) => lines.push(l.strong?.(`${row.label} ${row.time}`, 42)));
314
+ if (item.price) {
315
+ lines.push(l.accent?.(item.price, 42));
316
+ }
317
+ return { lines, gapAfter: 26 * u };
318
+ }
319
+ function eventGroups(doc, l, u) {
320
+ const b = doc.blocks;
321
+ const groups = [];
322
+ b.schedule?.forEach((row) => groups.push({ lines: [l.strong?.(row.label, 66), l.accent?.(row.time, 60)], gapAfter: 30 * u }));
323
+ b.prices?.forEach((row) => groups.push({ lines: [l.strong?.(row.amount, 66), l.accent?.(row.label, 52)], gapAfter: 30 * u }));
324
+ return groups;
325
+ }
326
+ function listGroups(doc, l, u) {
327
+ const groups = (doc.items ?? []).map((item) => itemGroup(item, l, u));
328
+ const prices = doc.blocks.prices?.map((row) => l.strong?.(`${row.amount} ${row.label}`, 58)) ?? [];
329
+ if (prices.length > 0) {
330
+ groups.push({ lines: prices, gapAfter: 30 * u });
331
+ }
332
+ return groups;
333
+ }
334
+ function bodyGroups(doc, p, u) {
335
+ const l = makers(p, u);
336
+ const b = doc.blocks;
337
+ const groups = [];
338
+ const isList = doc.layout === "list" /* List */;
339
+ if (b.venue) {
340
+ groups.push({ lines: b.venue.map((v) => l.venue?.(v, isList ? 48 : 58)), gapAfter: 40 * u, rule: { color: p.accent, height: 8 * u } });
341
+ }
342
+ groups.push(...isList ? listGroups(doc, l, u) : eventGroups(doc, l, u));
343
+ if (b.includes) {
344
+ const lines = [l.accent?.(b.includes.heading, 36), ...b.includes.items.map((i) => l.item?.(i, 40))];
345
+ groups.push({ lines, gapAfter: 24 * u, card: { color: p.card, pad: 28 * u } });
346
+ }
347
+ if (b.footer) {
348
+ groups.push({ lines: [l.soft?.(b.footer, 34)], gapAfter: 0 });
349
+ }
350
+ return groups;
351
+ }
352
+
353
+ // src/layout/geometry.ts
354
+ var MARGIN = 0.045;
355
+ var HEADER_PORTRAIT = 0.3;
356
+ var HEADER_LANDSCAPE = 0.36;
357
+ var BOTTOM_HEADER = 0.2;
358
+ var BOTTOM_PICTURE = 0.52;
359
+ var PICTURE_SIDE = {
360
+ ["picture-right" /* PictureRight */]: [0.6, 0.42],
361
+ ["picture-left" /* PictureLeft */]: [0.6, 0.42],
362
+ ["list" /* List */]: [0.5, 0.36]
363
+ };
364
+ var ANCHOR_Y = { ["top" /* Top */]: 0, ["center" /* Center */]: 0.5, ["bottom" /* Bottom */]: 1 };
365
+ var r = Math.round;
366
+ function side(w, h, fraction) {
367
+ const m = r(Math.min(w, h) * MARGIN);
368
+ if (h >= w) {
369
+ const split2 = r(w * (1 - fraction[0]));
370
+ const top2 = r(h * HEADER_PORTRAIT);
371
+ return {
372
+ header: { x: m, y: m, w: w - 2 * m, h: top2 - 2 * m },
373
+ picture: { x: split2, y: top2, w: w - split2, h: h - top2 },
374
+ body: { x: m, y: top2, w: split2 - 2 * m, h: h - top2 - m },
375
+ alignX: 1
376
+ };
377
+ }
378
+ const split = r(w * (1 - fraction[1]));
379
+ const top = r(h * HEADER_LANDSCAPE);
380
+ return {
381
+ header: { x: m, y: m, w: split - 2 * m, h: top - m },
382
+ picture: { x: split, y: 0, w: w - split, h },
383
+ body: { x: m, y: top + r(m / 2), w: split - 2 * m, h: h - top - r(m / 2) - m },
384
+ alignX: 1
385
+ };
386
+ }
387
+ function bottom(w, h) {
388
+ const m = r(Math.min(w, h) * MARGIN);
389
+ const top = r(h * BOTTOM_HEADER);
390
+ const pic = r(h * BOTTOM_PICTURE);
391
+ return {
392
+ header: { x: m, y: m, w: w - 2 * m, h: top - 2 * m },
393
+ body: { x: m, y: top, w: w - 2 * m, h: pic - top - m },
394
+ picture: { x: 0, y: pic, w, h: h - pic },
395
+ alignX: 0.5
396
+ };
397
+ }
398
+ var mirror = (rect, w) => ({ ...rect, x: w - rect.x - rect.w });
399
+ function geometry(layout, w, h) {
400
+ if (layout === "picture-bottom" /* PictureBottom */) {
401
+ return bottom(w, h);
402
+ }
403
+ const g = side(w, h, PICTURE_SIDE[layout] ?? [0.6, 0.42]);
404
+ if (layout !== "picture-left" /* PictureLeft */) {
405
+ return g;
406
+ }
407
+ return { header: mirror(g.header, w), body: mirror(g.body, w), picture: mirror(g.picture, w), alignX: 1 - g.alignX };
408
+ }
409
+ function placeContain(image, region, anchor, alignX, scale) {
410
+ const s = Math.min(region.w / image.width, region.h / image.height) * scale;
411
+ const w = Math.floor(image.width * s);
412
+ const h = Math.floor(image.height * s);
413
+ return {
414
+ x: region.x + Math.floor((region.w - w) * alignX),
415
+ y: region.y + Math.floor((region.h - h) * ANCHOR_Y[anchor]),
416
+ w,
417
+ h
418
+ };
419
+ }
420
+
421
+ // src/layout/textFlow.ts
422
+ var LINE_HEIGHT = 1.2;
423
+ var BULLET_INDENT = 1.1;
424
+ var BULLET_RADIUS = 0.2;
425
+ var RULE_GAP = 2.5;
426
+ var RULE_WIDTH = 0.9;
427
+ var WIDE = 1.6;
428
+ var MIN_K = 0.02;
429
+ var STEPS = 22;
430
+ var fontOf = (line, size, fonts) => `${line.weight} ${size}px ${fonts[line.role]}`;
431
+ var emptyFlow = () => ({ texts: [], cards: [], rules: [], bullets: [] });
432
+ function lineSize(line, width, k, avail) {
433
+ const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
434
+ return Math.max(0, Math.min(line.size * k, line.size * (avail - indent) / Math.max(width, 1)));
435
+ }
436
+ function placeGroup(m, k, col, y0, out) {
437
+ const { group } = m;
438
+ let y = y0;
439
+ if (group.rule) {
440
+ out?.rules.push({ rect: { x: col.x, y, w: col.w * RULE_WIDTH, h: group.rule.height * k }, color: group.rule.color });
441
+ y += group.rule.height * k * RULE_GAP;
442
+ }
443
+ const pad = group.card ? group.card.pad * k : 0;
444
+ const top = y;
445
+ y += pad;
446
+ group.lines.forEach((line, i) => {
447
+ const size = lineSize(line, m.widths[i] ?? 1, k, col.w - 2 * pad);
448
+ const indent = line.bullet ? line.size * k * BULLET_INDENT : 0;
449
+ const x = col.x + pad + indent;
450
+ const w = (m.widths[i] ?? 0) * size / line.size;
451
+ out?.texts.push({ text: line.text, font: "", color: line.color, box: { x, y, w, h: size * LINE_HEIGHT } });
452
+ if (line.bullet && out) {
453
+ out.bullets.push({ x: col.x + pad + size * BULLET_RADIUS, y: y + size * LINE_HEIGHT / 2, r: size * BULLET_RADIUS, color: line.bullet });
454
+ }
455
+ y += size * LINE_HEIGHT;
456
+ });
457
+ y += pad;
458
+ if (group.card) {
459
+ out?.cards.unshift({ rect: { x: col.x, y: top, w: col.w, h: y - top }, color: group.card.color });
460
+ }
461
+ return y + group.gapAfter * k - y0;
462
+ }
463
+ function placeColumn(groups, k, col, out) {
464
+ let h = 0;
465
+ for (const g of groups) {
466
+ h += placeGroup(g, k, col, col.y + h, out);
467
+ }
468
+ return h;
469
+ }
470
+ function splitColumns(groups, zone) {
471
+ if (zone.w / Math.max(zone.h, 1) <= WIDE || groups.length < 2) {
472
+ return [{ col: zone, groups }];
473
+ }
474
+ const heights = groups.map((g) => placeGroup(g, 1, zone, 0, null));
475
+ const half = heights.reduce((a, b) => a + b, 0) / 2;
476
+ let acc = 0;
477
+ let cut = 1;
478
+ heights.forEach((h, i) => {
479
+ if (acc < half) {
480
+ cut = i + 1;
481
+ }
482
+ acc += h;
483
+ });
484
+ cut = Math.min(cut, groups.length - 1);
485
+ const gap = Math.round(zone.w * 0.04);
486
+ const w = (zone.w - gap) / 2;
487
+ return [
488
+ { col: { ...zone, w }, groups: groups.slice(0, cut) },
489
+ { col: { ...zone, x: zone.x + w + gap, w }, groups: groups.slice(cut) }
490
+ ];
491
+ }
492
+ function flow(ctx, groups, zone, fonts) {
493
+ const measured = groups.map((group) => ({
494
+ group,
495
+ widths: group.lines.map((line) => {
496
+ ctx.font = fontOf(line, line.size, fonts);
497
+ return ctx.measureText(line.text).width;
498
+ })
499
+ }));
500
+ const columns = splitColumns(measured, zone);
501
+ const fits = (k) => columns.every((c) => placeColumn(c.groups, k, c.col, null) <= zone.h);
502
+ let lo = MIN_K;
503
+ let hi = 1;
504
+ if (fits(1)) {
505
+ lo = 1;
506
+ }
507
+ for (let i = 0; i < STEPS && lo < hi; i++) {
508
+ const mid = (lo + hi) / 2;
509
+ if (fits(mid)) {
510
+ lo = mid;
511
+ } else {
512
+ hi = mid;
513
+ }
514
+ }
515
+ const out = emptyFlow();
516
+ columns.forEach((c) => placeColumn(c.groups, lo, c.col, out));
517
+ const lines = groups.flatMap((g) => g.lines);
518
+ out.texts.forEach((t, i) => {
519
+ const line = lines[i];
520
+ t.font = line ? fontOf(line, t.box.h / LINE_HEIGHT, fonts) : t.font;
521
+ });
522
+ return out;
523
+ }
524
+
525
+ // src/render/render.ts
526
+ var BASE = 1080;
527
+ var LOGO_SHARE = 0.26;
528
+ var CARD_RADIUS = 0.035;
529
+ function roundRect(ctx, r2, radius) {
530
+ const rad = Math.min(radius, r2.w / 2, r2.h / 2);
531
+ ctx.beginPath();
532
+ ctx.moveTo(r2.x + rad, r2.y);
533
+ ctx.arcTo(r2.x + r2.w, r2.y, r2.x + r2.w, r2.y + r2.h, rad);
534
+ ctx.arcTo(r2.x + r2.w, r2.y + r2.h, r2.x, r2.y + r2.h, rad);
535
+ ctx.arcTo(r2.x, r2.y + r2.h, r2.x, r2.y, rad);
536
+ ctx.arcTo(r2.x, r2.y, r2.x + r2.w, r2.y, rad);
537
+ ctx.closePath();
538
+ ctx.fill();
539
+ }
540
+ function drawFlow(ctx, f, radius) {
541
+ f.rules.forEach((r2) => {
542
+ ctx.fillStyle = r2.color;
543
+ ctx.fillRect(r2.rect.x, r2.rect.y, r2.rect.w, r2.rect.h);
544
+ });
545
+ f.cards.forEach((c) => {
546
+ ctx.fillStyle = c.color;
547
+ roundRect(ctx, c.rect, radius);
548
+ });
549
+ f.bullets.forEach((b) => {
550
+ ctx.fillStyle = b.color;
551
+ ctx.beginPath();
552
+ ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
553
+ ctx.fill();
554
+ });
555
+ ctx.textAlign = "left";
556
+ ctx.textBaseline = "top";
557
+ f.texts.forEach((t) => {
558
+ ctx.font = t.font;
559
+ ctx.fillStyle = t.color;
560
+ ctx.fillText(t.text, t.box.x, t.box.y);
561
+ });
562
+ }
563
+ function checked(input, assets) {
564
+ const result = validateFlierDoc(input);
565
+ if (!result.ok) {
566
+ throw new FlierDocError(result.errors);
567
+ }
568
+ const missing = missingAssets(result.doc, assets);
569
+ if (missing.length > 0) {
570
+ throw new FlierDocError(missing);
571
+ }
572
+ return result.doc;
573
+ }
574
+ function splitHeader(header, hasLogo) {
575
+ const logoW = hasLogo ? Math.round(header.w * LOGO_SHARE) : 0;
576
+ return { text: { ...header, w: header.w - logoW * 1.1 }, logo: { ...header, x: header.x + header.w - logoW, w: logoW } };
577
+ }
578
+ function render(input, assets, canvas) {
579
+ const doc = checked(input, assets);
580
+ const { width, height } = FORMAT_SIZES[doc.format];
581
+ canvas.width = width;
582
+ canvas.height = height;
583
+ const ctx = canvas.getContext("2d");
584
+ if (!ctx) {
585
+ throw new Error("poster-render: the canvas has no 2d context");
586
+ }
587
+ const palette = PALETTES[doc.palette];
588
+ const fonts = FONT_SETS[doc.fonts];
589
+ const geo = geometry(doc.layout, width, height);
590
+ const u = Math.min(width, height) / BASE;
591
+ ctx.globalAlpha = 1;
592
+ ctx.fillStyle = palette.background;
593
+ ctx.fillRect(0, 0, width, height);
594
+ if (doc.background === "tint" /* Tint */) {
595
+ ctx.fillStyle = palette.tint;
596
+ [geo.header, geo.body].forEach((r2) => ctx.fillRect(r2.x, r2.y, r2.w, r2.h));
597
+ }
598
+ const zones = splitHeader(geo.header, doc.logo !== void 0);
599
+ const head = flow(ctx, headerGroups(doc, palette, u), zones.text, fonts);
600
+ const body = flow(ctx, bodyGroups(doc, palette, u), geo.body, fonts);
601
+ const radius = Math.min(width, height) * CARD_RADIUS;
602
+ drawFlow(ctx, head, radius);
603
+ drawFlow(ctx, body, radius);
604
+ const logoImage = doc.logo ? assets[doc.logo.imageId] : void 0;
605
+ if (logoImage) {
606
+ const r2 = placeContain(logoImage, zones.logo, "top" /* Top */, 1, 1);
607
+ ctx.drawImage(logoImage, r2.x, r2.y, r2.w, r2.h);
608
+ }
609
+ const image = doc.picture ? assets[doc.picture.imageId] : void 0;
610
+ const picture = image && doc.picture ? placeContain(image, geo.picture, doc.picture.anchor, geo.alignX, doc.picture.scale) : null;
611
+ if (image && picture) {
612
+ ctx.drawImage(image, picture.x, picture.y, picture.w, picture.h);
613
+ }
614
+ return {
615
+ width,
616
+ height,
617
+ picture,
618
+ pictureRegion: geo.picture,
619
+ textBoxes: [...head.texts, ...body.texts].map((t) => t.box),
620
+ cardBoxes: body.cards.map((c) => c.rect)
621
+ };
622
+ }
623
+
624
+ // src/exportPng.ts
625
+ var PREFIX = "data:image/png;base64,";
626
+ function exportPng(canvas) {
627
+ const url = canvas.toDataURL("image/png");
628
+ if (!url.startsWith(PREFIX)) {
629
+ throw new Error("poster-render: the canvas did not return a PNG data URL");
630
+ }
631
+ const binary = atob(url.slice(PREFIX.length));
632
+ const bytes = new Uint8Array(binary.length);
633
+ for (let i = 0; i < binary.length; i++) {
634
+ bytes[i] = binary.charCodeAt(i);
635
+ }
636
+ return new Blob([bytes], { type: "image/png" });
637
+ }
638
+
639
+ // src/examples/kizombaWorkshop.ts
640
+ var WORKSHOPS = [
641
+ { script: "Saturday 31 October", date: "Sat 31 Oct", first: "20:00 - 21:00", second: "21:00 - 22:00", party: "20\u20AC" },
642
+ { script: "Saturday 7 November", date: "Sat 7 Nov", first: "20:00 - 21:00", second: "21:00 - 22:00", party: "15\u20AC" },
643
+ { script: "Sunday 29 November", date: "Sun 29 Nov", first: "17:00 - 18:00", second: "18:00 - 19:00", party: "10\u20AC" }
644
+ ];
645
+ var INCLUDES = { heading: "Party pass includes", items: ["Salsa & Bachata room", "Kizomba dedicated room", "2 drinks"] };
646
+ var schedule = (w) => [
647
+ { label: "Level 1", time: w.first },
648
+ { label: "Level 2", time: w.second }
649
+ ];
650
+ var base = {
651
+ version: 1,
652
+ format: "story" /* Story */,
653
+ palette: "sand" /* Sand */,
654
+ fonts: "classic" /* Classic */,
655
+ background: "solid" /* Solid */,
656
+ picture: { imageId: "dancers", anchor: "bottom" /* Bottom */, scale: 1 },
657
+ logo: { imageId: "logo" }
658
+ };
659
+ var shared = {
660
+ venue: ["Bailemos", "Dance School"],
661
+ includes: INCLUDES,
662
+ footer: "with Jimmy & Ismini"
663
+ };
664
+ var workshopDoc = (w) => ({
665
+ ...base,
666
+ layout: "picture-right" /* PictureRight */,
667
+ blocks: {
668
+ ...shared,
669
+ title: "KIZOMBA",
670
+ titleBottom: "WORKSHOP",
671
+ script: w.script,
672
+ schedule: schedule(w),
673
+ prices: [{ amount: "5\u20AC", label: "per hour" }, { amount: w.party, label: "Party pass" }]
674
+ }
675
+ });
676
+ var kizombaWorkshop = {
677
+ ...base,
678
+ layout: "list" /* List */,
679
+ blocks: {
680
+ ...shared,
681
+ title: "UPCOMING",
682
+ titleBottom: "WORKSHOPS",
683
+ script: "Kizomba",
684
+ prices: [{ amount: "5\u20AC", label: "per hour" }]
685
+ },
686
+ items: WORKSHOPS.map((w) => ({ date: w.date, schedule: schedule(w), price: `Party pass ${w.party}` }))
687
+ };
688
+ var kizombaWorkshopDocs = [...WORKSHOPS.map(workshopDoc), kizombaWorkshop];
689
+
690
+ exports.BackgroundStyle = BackgroundStyle;
691
+ exports.FONT_SETS = FONT_SETS;
692
+ exports.FORMAT_SIZES = FORMAT_SIZES;
693
+ exports.FlierDocError = FlierDocError;
694
+ exports.FontSetId = FontSetId;
695
+ exports.FormatId = FormatId;
696
+ exports.LayoutId = LayoutId;
697
+ exports.PALETTES = PALETTES;
698
+ exports.PaletteId = PaletteId;
699
+ exports.PictureAnchor = PictureAnchor;
700
+ exports.exportPng = exportPng;
701
+ exports.flierDocSchema = flierDocSchema;
702
+ exports.kizombaWorkshop = kizombaWorkshop;
703
+ exports.kizombaWorkshopDocs = kizombaWorkshopDocs;
704
+ exports.render = render;
705
+ exports.validateFlierDoc = validateFlierDoc;
706
+ //# sourceMappingURL=index.js.map
707
+ //# sourceMappingURL=index.js.map