@koda-sl/baker-cli 0.406.0 → 0.408.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.
@@ -1,816 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=__WIDTH__, height=__HEIGHT__" />
6
- <script src="./gsap.min.js"></script>
7
- <style>
8
- /* The ad's own faces, staged beside this file by the render (a brand file or a web font
9
- file). One range per face, so a single-weight file is used as it is instead of being
10
- faked bolder. The browser fetches a face only when text uses it, and text uses it
11
- only when a face was staged (see FRAME.fonts below). */
12
- @font-face { font-family: 'AdHeadline'; src: url('./headline.font'); font-weight: 100 900; font-display: block; }
13
- @font-face { font-family: 'AdBody'; src: url('./body.font'); font-weight: 100 900; font-display: block; }
14
- * { margin: 0; padding: 0; box-sizing: border-box; }
15
- html, body { width: __WIDTH__px; height: __HEIGHT__px; overflow: hidden; background: #000; }
16
- #root { position: absolute; inset: 0; overflow: hidden; }
17
- .plate { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }
18
- .scrim { position: absolute; left: 0; right: 0; pointer-events: none; display: none; }
19
- .group { position: absolute; display: flex; flex-direction: column; gap: 0; }
20
- .block { font-family: 'AdBody', 'Helvetica Neue', Arial, sans-serif; overflow-wrap: break-word; text-wrap: pretty; }
21
- .block.headline, .block.subheadline, .block.cta { font-family: 'AdHeadline', 'AdBody', 'Helvetica Neue', Arial, sans-serif; }
22
- .block.headline { text-wrap: balance; letter-spacing: -0.01em; }
23
- .fill { display: inline-block; line-height: 1.1; padding: 0.5em 1.2em; }
24
- .fill.highlight { padding: 0.12em 0.4em; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
25
- .fill.sticker { transform: rotate(-3deg); }
26
- .logo { display: block; width: auto; object-fit: contain; }
27
- .group.panel { box-sizing: border-box; }
28
- .backing { position: absolute; pointer-events: none; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
29
- </style>
30
- </head>
31
- <body>
32
- <div id="root" data-composition-id="main" data-start="0" data-duration="1" data-width="__WIDTH__" data-height="__HEIGHT__">
33
- <div class="scrim" id="scrim-top"></div>
34
- <div class="scrim" id="scrim-bottom"></div>
35
- </div>
36
-
37
- <script>
38
- (function () {
39
- var AD = {{ad}};
40
- var FRAME = {{frame}};
41
- var W = __WIDTH__;
42
- var H = __HEIGHT__;
43
- var root = document.getElementById("root");
44
- var safe = FRAME.safe || { top: 0.05, bottom: 0.05, left: 0.06, right: 0.06 };
45
- var box = { x: safe.left * W, y: safe.top * H, w: W * (1 - safe.left - safe.right), h: H * (1 - safe.top - safe.bottom) };
46
- // One unit per percent of the shorter side, so type reads the same size on every format.
47
- var U = Math.min(W, H) / 100;
48
- // A text-only ad has no picture (`plate: null`): the copy sits on the brand's ground.
49
- var hasPlate = FRAME.plate !== null;
50
- var ground = AD.background || { color: "#111111", ink: "#ffffff" };
51
-
52
- var SIZE = { x_large: 8.6, large: 6.6, medium: 4.4, small: 3.0 };
53
- var WEIGHT = { bold: 800, semibold: 600, regular: 400 };
54
- // Title case is left as written: `capitalize` also raised "and", "of" and "de".
55
- var CASE = { upper: "uppercase", title: "none", sentence: "none" };
56
- var MAX_SHARE = { top: 0.44, middle: 0.3, bottom: 0.36 };
57
- var RADIUS = { pill: "999px", rounded: "0.35em", square: "0" };
58
-
59
- // With no face staged, the text never names the ad's faces, so the browser never asks
60
- // for a file that is not there (a 404 the pre-render check refuses).
61
- var FALLBACK_FACE = "'Helvetica Neue', Arial, sans-serif";
62
- var facesStaged = FRAME.fonts !== false;
63
-
64
- if (hasPlate) {
65
- var plate = document.createElement("img");
66
- plate.className = "plate";
67
- plate.id = "plate";
68
- plate.src = "./plate.png";
69
- plate.alt = "";
70
- root.insertBefore(plate, root.firstChild);
71
- } else {
72
- root.style.background = ground.color;
73
- }
74
-
75
- // <plate-layout>
76
- // Where the copy can go, read off the picture this format actually shows. The layout
77
- // placed each line where the REFERENCE had room; the picture model does not always
78
- // leave that room, and an extended format moves it. So each line group is checked
79
- // against the picture under it and, when it sits on something (a subject, a device,
80
- // a painted edge), moved to the nearest calm room clear of the other lines, or given
81
- // a surface to sit on when no such room exists. Pure: a grid of cells in, a plan out.
82
- // Calibrated on production plates: the grain of a generated picture reads 0.1-0.25,
83
- // a faint icon pattern up to ~0.4, a device, a product or a photo of shelves 0.5-1.
84
- var BUSY_CELL = 0.4;
85
- // The share of any one line that may sit on busy cells before it moves: a tenth, since an
86
- // edge through the foot of a line already reads as the line crossing the object.
87
- var OK_CLUTTER = 0.1;
88
- // Mean-luma step between neighbouring cells that marks the edge of a painted panel.
89
- var SEAM_LUMA = 28;
90
-
91
- // Per cell of `cell` pixels: how much edge detail it holds (0 calm .. 1 cluttered) and
92
- // its mean luma. `px` is RGBA, `w`x`h` pixels, drawn at `scale` page px per pixel.
93
- function busyMap(px, w, h, cell, scale) {
94
- var cols = Math.ceil(w / cell);
95
- var rows = Math.ceil(h / cell);
96
- var L = new Float32Array(w * h);
97
- for (var i = 0; i < w * h; i++) L[i] = 0.2126 * px[i * 4] + 0.7152 * px[i * 4 + 1] + 0.0722 * px[i * 4 + 2];
98
- var busy = [];
99
- var luma = [];
100
- var rgb = [];
101
- for (var r = 0; r < rows; r++) {
102
- for (var c = 0; c < cols; c++) {
103
- var g = 0;
104
- var sum = 0;
105
- var n = 0;
106
- var cr = 0;
107
- var cg = 0;
108
- var cb = 0;
109
- for (var y = r * cell; y < Math.min(h, (r + 1) * cell); y++) {
110
- for (var x = c * cell; x < Math.min(w, (c + 1) * cell); x++) {
111
- var v = L[y * w + x];
112
- sum += v;
113
- cr += px[(y * w + x) * 4];
114
- cg += px[(y * w + x) * 4 + 1];
115
- cb += px[(y * w + x) * 4 + 2];
116
- if (x + 1 < w) g += Math.abs(L[y * w + x + 1] - v);
117
- if (y + 1 < h) g += Math.abs(L[(y + 1) * w + x] - v);
118
- n++;
119
- }
120
- }
121
- busy.push(n ? Math.min(1, g / n / 40) : 0);
122
- luma.push(n ? sum / n : 0);
123
- rgb.push(n ? [cr / n, cg / n, cb / n] : [0, 0, 0]);
124
- }
125
- }
126
- return { cols: cols, rows: rows, cellW: cell * scale, cellH: cell * scale, busy: busy, luma: luma, rgb: rgb };
127
- }
128
-
129
- // The cells a rect covers, each with the share of the rect's area it holds.
130
- function coverage(map, rect) {
131
- var out = [];
132
- var area = Math.max(1, rect.w * rect.h);
133
- var c0 = Math.max(0, Math.floor(rect.x / map.cellW));
134
- var c1 = Math.min(map.cols - 1, Math.floor((rect.x + rect.w - 0.001) / map.cellW));
135
- var r0 = Math.max(0, Math.floor(rect.y / map.cellH));
136
- var r1 = Math.min(map.rows - 1, Math.floor((rect.y + rect.h - 0.001) / map.cellH));
137
- for (var r = r0; r <= r1; r++) {
138
- for (var c = c0; c <= c1; c++) {
139
- var ox = Math.min(rect.x + rect.w, (c + 1) * map.cellW) - Math.max(rect.x, c * map.cellW);
140
- var oy = Math.min(rect.y + rect.h, (r + 1) * map.cellH) - Math.max(rect.y, r * map.cellH);
141
- if (ox > 0 && oy > 0) out.push({ i: r * map.cols + c, share: (ox * oy) / area });
142
- }
143
- }
144
- return out;
145
- }
146
-
147
- // The share of a rect that sits on cluttered picture.
148
- function clutter(map, rect) {
149
- return coverage(map, rect).reduce(function (acc, k) {
150
- return acc + (map.busy[k.i] > BUSY_CELL ? k.share : 0);
151
- }, 0);
152
- }
153
-
154
- // Whether a rect straddles a seam: two neighbouring flat cells under it whose grounds
155
- // differ sharply (the edge of a painted panel). A smooth gradient never does, and a
156
- // photograph's light and shade is not flat, so neither counts.
157
- function seam(map, rect) {
158
- var cells = {};
159
- coverage(map, rect).forEach(function (k) {
160
- cells[k.i] = true;
161
- });
162
- return Object.keys(cells).some(function (key) {
163
- var i = Number(key);
164
- var right = (i + 1) % map.cols !== 0 && cells[i + 1] ? i + 1 : -1;
165
- var down = cells[i + map.cols] ? i + map.cols : -1;
166
- return [right, down].some(function (j) {
167
- if (j < 0 || map.busy[i] >= FLAT_CELL || map.busy[j] >= FLAT_CELL) return false;
168
- return Math.abs(map.luma[i] - map.luma[j]) > SEAM_LUMA;
169
- });
170
- });
171
- }
172
-
173
- // The worst line of an item moved by `dy`: a group reads only if every line does.
174
- function worst(map, parts, dy) {
175
- return parts.reduce(function (acc, p) {
176
- var r = { x: p.x, y: p.y + dy, w: p.w, h: p.h };
177
- return Math.max(acc, seam(map, r) ? 1 : clutter(map, r));
178
- }, 0);
179
- }
180
-
181
- function overlaps(a, b, gap) {
182
- return a.x < b.x + b.w + gap && b.x < a.x + a.w + gap && a.y < b.y + b.h + gap && b.y < a.y + a.h + gap;
183
- }
184
-
185
- function inside(r, safe) {
186
- return r.y >= safe.y - 0.5 && r.y + r.h <= safe.y + safe.h + 0.5;
187
- }
188
-
189
- // Whether `cand` keeps its place in the reading order: every group sharing its column
190
- // stays on the same side of it as before, a unit clear.
191
- function keepsOrder(cand, from, other, gap) {
192
- if (cand.x >= other.x + other.w || other.x >= cand.x + cand.w) return !overlaps(cand, other, gap);
193
- return other.y >= from.y ? cand.y + cand.h + gap <= other.y : cand.y >= other.y + other.h + gap;
194
- }
195
-
196
- // One plan per item, in order: stay, move (dy) to the nearest calm room inside the safe
197
- // area that keeps the reading order, or stay with a surface behind it. Each item is
198
- // judged by its lines (`parts`), not the region box around them. A panel or a button is
199
- // its own surface, so it may move but is never given a second one; a pinned item (a
200
- // logo alone in its corner) never moves and gets a surface instead.
201
- function placeGroups(map, frame, items) {
202
- var rects = items.map(function (it) {
203
- return { x: it.rect.x, y: it.rect.y, w: it.rect.w, h: it.rect.h };
204
- });
205
- return items.map(function (it, i) {
206
- var here = rects[i];
207
- var parts = it.parts && it.parts.length ? it.parts : [it.rect];
208
- if (worst(map, parts, 0) <= OK_CLUTTER) return { id: it.id, dx: 0, dy: 0, backing: false };
209
- if (it.pinned) return { id: it.id, dx: 0, dy: 0, backing: !it.panel };
210
- // Moved further than a few of its own heights, a line leaves what it names (a label
211
- // under a plate ended up beside the headline): past that it stays, on a surface.
212
- var step = Math.max(1, frame.U);
213
- var reach = Math.ceil(Math.max(3 * here.h, 0.06 * frame.H) / step);
214
- for (var d = 1; d <= reach; d++) {
215
- for (var s = -1; s <= 1; s += 2) {
216
- var dy = s * d * step;
217
- var cand = { x: here.x, y: here.y + dy, w: here.w, h: here.h };
218
- if (!inside(cand, frame.safe)) continue;
219
- var clear = rects.every(function (other, j) {
220
- return j === i || keepsOrder(cand, here, other, frame.U);
221
- });
222
- if (!clear || worst(map, parts, dy) > OK_CLUTTER) continue;
223
- rects[i] = cand;
224
- return { id: it.id, dx: 0, dy: dy, backing: false };
225
- }
226
- }
227
- return { id: it.id, dx: 0, dy: 0, backing: !it.panel };
228
- });
229
- }
230
-
231
- function hexLuma(hex) {
232
- var m = /^#?([0-9a-f]{6})$/i.exec(hex || "");
233
- if (!m) return null;
234
- var v = parseInt(m[1], 16);
235
- return 0.2126 * ((v >> 16) & 255) + 0.7152 * ((v >> 8) & 255) + 0.0722 * (v & 255);
236
- }
237
-
238
- // WCAG relative luminance of a colour, from its own channels.
239
- function luminance(hex) {
240
- var m = /^#?([0-9a-f]{6})$/i.exec(hex || "");
241
- if (!m) return null;
242
- var v = parseInt(m[1], 16);
243
- var lin = function (c) {
244
- var x = c / 255;
245
- return x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
246
- };
247
- return 0.2126 * lin((v >> 16) & 255) + 0.7152 * lin((v >> 8) & 255) + 0.0722 * lin(v & 255);
248
- }
249
-
250
- function contrast(x, y) {
251
- return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
252
- }
253
-
254
- // The ink a line should take on the picture under it: its own colour when it reads as
255
- // display type (WCAG 3:1 for large text, against the mean of what is under it), else
256
- // whichever of white and near-black reads better. A flat ground always has one.
257
- function inkOn(map, rect, ink) {
258
- var under = coverage(map, rect).reduce(function (acc, k) {
259
- return acc + Math.pow(map.luma[k.i] / 255, 2.2) * k.share;
260
- }, 0);
261
- var own = luminance(ink);
262
- if (own !== null && contrast(own, under) >= 3) return ink;
263
- return contrast(1, under) >= contrast(luminance("#111111"), under) ? "#ffffff" : "#111111";
264
- }
265
- // A cell flat enough to be part of a painted shape, and how close two colours are.
266
- var FLAT_CELL = 0.15;
267
- var SAME_COLOUR = 36;
268
- // How far a painted shape may be from its panel's colour and still be taken for it.
269
- var PANEL_COLOUR = 90;
270
-
271
- function hexRgb(hex) {
272
- var m = /^#?([0-9a-f]{6})$/i.exec(hex || "");
273
- if (!m) return null;
274
- var v = parseInt(m[1], 16);
275
- return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
276
- }
277
-
278
- function colourGap(a, b) {
279
- return Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2));
280
- }
281
-
282
- // The flat region of one colour grown from `seed`, as a box in page px with a key (its
283
- // first cell); null once it outgrows `limit` cells, which only the page's ground does.
284
- function flatComponent(map, seed, limit, seen) {
285
- var colour = map.rgb[seed];
286
- var queue = [seed];
287
- var mine = {};
288
- mine[seed] = true;
289
- seen[seed] = true;
290
- var c0 = map.cols;
291
- var c1 = -1;
292
- var r0 = map.rows;
293
- var r1 = -1;
294
- var count = 0;
295
- var key = seed;
296
- while (queue.length) {
297
- var i = queue.shift();
298
- var c = i % map.cols;
299
- var r = Math.floor(i / map.cols);
300
- count++;
301
- key = Math.min(key, i);
302
- c0 = Math.min(c0, c);
303
- c1 = Math.max(c1, c);
304
- r0 = Math.min(r0, r);
305
- r1 = Math.max(r1, r);
306
- [c > 0 ? i - 1 : -1, c < map.cols - 1 ? i + 1 : -1, i - map.cols, i + map.cols].forEach(function (j) {
307
- if (j < 0 || j >= map.cols * map.rows || mine[j]) return;
308
- if (map.busy[j] >= FLAT_CELL || colourGap(map.rgb[j], colour) >= SAME_COLOUR) return;
309
- mine[j] = true;
310
- seen[j] = true;
311
- queue.push(j);
312
- });
313
- }
314
- if (count > limit) return null;
315
- return {
316
- key: String(key),
317
- box: { x: c0 * map.cellW, y: r0 * map.cellH, w: (c1 - c0 + 1) * map.cellW, h: (r1 - r0 + 1) * map.cellH },
318
- };
319
- }
320
-
321
- // The flat shape the picture painted for a panel (the reference's bar, card or pill,
322
- // which the picture model copies however it is told not to, and places its own way):
323
- // the nearest one near the panel's place of a comparable size that no other panel has
324
- // `claimed`, or null when the picture painted none. The picture copies the reference's
325
- // shape in the reference's colour, which the panel carries too, so a shape far from the
326
- // panel's `colour` (a calm patch of the page) is refused, as is the page's own ground
327
- // for being far larger than any panel, and any shape under another group's words (`avoid`).
328
- function snapPanel(map, rect, claimed, colour, avoid) {
329
- if (!map.rgb) return null;
330
- claimed = claimed || {};
331
- var want = colour ? hexRgb(colour) : null;
332
- var near = { x: rect.x - rect.w * 0.3, y: rect.y - rect.h * 2, w: rect.w * 1.6, h: rect.h * 5 };
333
- var limit = Math.ceil((3 * rect.w * rect.h) / (map.cellW * map.cellH));
334
- var seen = {};
335
- var best = null;
336
- coverage(map, near).forEach(function (k) {
337
- if (seen[k.i] || map.busy[k.i] >= FLAT_CELL) return;
338
- var comp = flatComponent(map, k.i, limit, seen);
339
- if (!comp || claimed[comp.key]) return;
340
- if (want && colourGap(map.rgb[k.i], want) > PANEL_COLOUR) return;
341
- var b = comp.box;
342
- var ratio = (b.w * b.h) / (rect.w * rect.h);
343
- // Only a shape's flat interior is found (its edges are not flat), and the panel's box
344
- // has already grown with its copy: size is a loose check, width the telling one.
345
- if (b.w < rect.w * 0.4 || ratio < 0.15 || ratio > 3) return;
346
- var covers = (avoid || []).some(function (a) {
347
- return overlaps(b, a, 0);
348
- });
349
- if (covers) return;
350
- var dist = Math.abs(b.y + b.h / 2 - (rect.y + rect.h / 2)) + 0.5 * Math.abs(b.x + b.w / 2 - (rect.x + rect.w / 2));
351
- if (!best || dist < best.dist) best = { dist: dist, comp: comp };
352
- });
353
- if (!best) return null;
354
- claimed[best.comp.key] = true;
355
- return best.comp.box;
356
- }
357
- // Whether copy that does not fit its room shrinks another step. It stops at MIN_SCALE
358
- // when it is only taller than its region (it grows past it into free room), and goes
359
- // on to FLOOR_SCALE when it would be clipped at its width or run into another group:
360
- // small is better than cut off or overlapping.
361
- var MIN_SCALE = 0.72;
362
- var FLOOR_SCALE = 0.45;
363
- function keepShrinking(scale, room) {
364
- if (room.wide || room.collides) return scale >= FLOOR_SCALE;
365
- return Boolean(room.tall) && scale >= MIN_SCALE;
366
- }
367
- // </plate-layout>
368
-
369
- function alignSelf(align) {
370
- return align === "left" ? "flex-start" : align === "right" ? "flex-end" : "center";
371
- }
372
-
373
- // Every line is drawn in its group: the layout region the reference put it in (placed
374
- // at that region's box on this format), or a band when the layout gave no position.
375
- var groups = {};
376
- var bands = { top: null, middle: null, bottom: null };
377
- (AD.groups || []).forEach(function (g) {
378
- groups[g.id] = g;
379
- });
380
-
381
- function bandEl(name) {
382
- if (bands[name]) return bands[name];
383
- var el = document.createElement("div");
384
- el.className = "group band";
385
- el.dataset.band = name;
386
- el.style.left = box.x + "px";
387
- el.style.width = box.w + "px";
388
- el.style.gap = 1.6 * U + "px";
389
- if (name === "top") el.style.top = box.y + "px";
390
- if (name === "bottom") el.style.bottom = H - (box.y + box.h) + "px";
391
- if (name === "middle") {
392
- el.style.top = box.y + box.h / 2 + "px";
393
- el.style.transform = "translateY(-50%)";
394
- }
395
- el.dataset.max = String(box.h * MAX_SHARE[name]);
396
- root.appendChild(el);
397
- bands[name] = el;
398
- return el;
399
- }
400
-
401
- var placed = {};
402
- function containerFor(groupId, band) {
403
- var g = groups[groupId];
404
- var at = g && g.at && g.at[FRAME.format];
405
- if (!at) return bandEl((g && g.band) || band || "top");
406
- if (placed[groupId]) return placed[groupId];
407
- var el = document.createElement("div");
408
- el.className = "group placed";
409
- el.style.left = (at.x[0] / 100) * W + "px";
410
- el.style.width = ((at.x[1] - at.x[0]) / 100) * W + "px";
411
- el.style.top = (at.y[0] / 100) * H + "px";
412
- el.style.minHeight = ((at.y[1] - at.y[0]) / 100) * H + "px";
413
- el.style.justifyContent = "center";
414
- // The region's own card or pill, drawn here so it stays with its words.
415
- if (g.card) el.dataset.card = g.card;
416
- if (g.panel) {
417
- el.dataset.panel = g.panel.background;
418
- el.dataset.shape = g.panel.shape;
419
- // An older creative's panel is drawn only over the shape its picture painted.
420
- if (g.panel.if_painted) el.classList.add("panel-candidate");
421
- else paintPanel(el);
422
- }
423
- el.style.gap = 1.2 * U + "px";
424
- // Copy rewritten for our brand runs longer than the source's: allow some growth
425
- // past the region before the type shrinks.
426
- el.dataset.max = String(((at.y[1] - at.y[0]) / 100) * H * 1.35);
427
- root.appendChild(el);
428
- placed[groupId] = el;
429
- return el;
430
- }
431
-
432
- function addLogo(logo) {
433
- var img = document.createElement("img");
434
- img.className = "logo";
435
- img.src = "./logo.png";
436
- img.alt = "";
437
- img.style.height = 5.4 * U + "px";
438
- img.style.maxWidth = box.w * 0.42 + "px";
439
- img.style.alignSelf = alignSelf(logo.align);
440
- img.onerror = function () {
441
- img.remove();
442
- };
443
- var el = containerFor(logo.group, logo.band);
444
- if (el.dataset.band === "bottom") el.appendChild(img);
445
- else el.insertBefore(img, el.firstChild);
446
- }
447
-
448
- function paintPanel(el) {
449
- el.classList.remove("panel-candidate");
450
- el.classList.add("panel");
451
- el.style.background = el.dataset.panel;
452
- el.style.borderRadius = RADIUS[el.dataset.shape] || RADIUS.rounded;
453
- el.style.padding = 1.4 * U + "px " + 2.2 * U + "px";
454
- }
455
-
456
- // A soft shadow on the side away from the ink, so a line keeps its edge on the picture.
457
- function shade(el) {
458
- var dark = (hexLuma(el.dataset.ink) || 255) < 110;
459
- el.dataset.dark = dark ? "1" : "0";
460
- el.style.textShadow = dark ? "0 1px 10px rgba(255,255,255,0.35)" : "0 2px 14px rgba(0,0,0,0.45)";
461
- }
462
-
463
- function addBlock(b) {
464
- var el = document.createElement("div");
465
- el.className = "block " + b.role;
466
- if (!facesStaged) el.style.fontFamily = FALLBACK_FACE;
467
- var size = b.role === "fine_print" ? 2.4 : SIZE[b.size] || SIZE.medium;
468
- el.dataset.base = String(size);
469
- el.style.fontSize = size * U + "px";
470
- el.style.fontWeight = String(WEIGHT[b.weight] || (b.role === "cta" ? 700 : 400));
471
- el.style.lineHeight = b.role === "headline" ? "1.04" : "1.22";
472
- el.style.textTransform = CASE[b.case] || "none";
473
- el.style.textAlign = b.align || "center";
474
- el.style.alignSelf = alignSelf(b.align);
475
- var t = b.treatment || { kind: b.role === "cta" && !AD.groups ? "button" : "plain" };
476
- var ink = b.color || (hasPlate ? "#ffffff" : ground.ink);
477
- var text = b.text;
478
- if (t.kind === "arrow" && !/[→›»]\s*$/.test(text)) text = text + " →";
479
- if (t.kind === "button" || t.kind === "sticker" || t.kind === "highlight") {
480
- var fill = document.createElement("span");
481
- fill.className = "fill " + t.kind;
482
- fill.textContent = text;
483
- fill.style.background = t.background || (AD.cta && AD.cta.background) || "#111111";
484
- fill.style.color = t.color || (AD.cta && AD.cta.color) || "#ffffff";
485
- fill.style.borderRadius = RADIUS[t.shape] || RADIUS.rounded;
486
- el.appendChild(fill);
487
- } else {
488
- el.textContent = text;
489
- el.style.color = ink;
490
- if (t.kind === "underline") {
491
- el.style.textDecoration = "underline";
492
- el.style.textDecorationThickness = "0.08em";
493
- el.style.textUnderlineOffset = "0.18em";
494
- }
495
- el.dataset.ink = ink;
496
- }
497
- var container = containerFor(b.group, b.band);
498
- if (hasPlate && el.dataset.ink && !container.classList.contains("panel") && !container.dataset.card) shade(el);
499
- container.appendChild(el);
500
- }
501
-
502
- // A card that several regions of copy sit on (a testimonial's), drawn where the layout
503
- // put it, under the copy, so it can never drift from its words or be left empty.
504
- function addCard(card) {
505
- var at = (card.at && card.at[FRAME.format]) || card.box;
506
- var el = document.createElement("div");
507
- el.className = "card";
508
- el.style.position = "absolute";
509
- el.style.left = (at.x[0] / 100) * W + "px";
510
- el.style.width = ((at.x[1] - at.x[0]) / 100) * W + "px";
511
- el.style.top = (at.y[0] / 100) * H + "px";
512
- el.style.height = ((at.y[1] - at.y[0]) / 100) * H + "px";
513
- el.style.background = card.background;
514
- // Not RADIUS: a card holds several lines (a quote and its name), and a full pill radius
515
- // on a block that tall reads as a stadium. "pill" rounds it more, never into one.
516
- el.style.borderRadius = card.shape === "pill" ? 4 * U + "px" : card.shape === "square" ? "0" : 2 * U + "px";
517
- root.appendChild(el);
518
- }
519
-
520
- var drawCopy = FRAME.draw_copy !== false;
521
- if (drawCopy) (AD.cards || []).forEach(addCard);
522
- (drawCopy ? AD.copy || [] : []).forEach(addBlock);
523
- if (AD.logo) addLogo(AD.logo);
524
-
525
- function containers() {
526
- return Array.prototype.slice.call(root.querySelectorAll(".group"));
527
- }
528
-
529
- // Shrink a group's type until it fits its room and no line runs past its edge. The
530
- // copy is whatever prompt.json says today, so its length is not known when the
531
- // layout is written.
532
- // Whether a group's words, at their current size, overlap another group's words. Words,
533
- // not boxes: neighbouring regions overlap in many layouts where no letter touches.
534
- function collides(el) {
535
- var mine = partsOf(el);
536
- return containers().some(function (other) {
537
- if (other === el) return false;
538
- var theirs = partsOf(other);
539
- return mine.some(function (a) {
540
- return theirs.some(function (b) {
541
- return overlaps(a, b, 0);
542
- });
543
- });
544
- });
545
- }
546
-
547
- // Whether any of a group's words run past the frame's left or right edge, where they are cut.
548
- function offFrame(el) {
549
- return partsOf(el).some(function (p) {
550
- return p.x < -1 || p.x + p.w > W + 1;
551
- });
552
- }
553
-
554
- function fit(el) {
555
- var max = Number(el.dataset.max);
556
- var width = el.getBoundingClientRect().width;
557
- var scale = 1;
558
- for (var i = 0; i < 40; i++) {
559
- var tooTall = el.scrollHeight > max;
560
- // Wider than its region is only room it grows into; past the frame's edge it is cut.
561
- var tooWide = Array.prototype.some.call(el.children, function (c) {
562
- return c.scrollWidth > width + 1;
563
- });
564
- var cut = tooWide && offFrame(el);
565
- var grown = tooTall || tooWide;
566
- if (!keepShrinking(scale, { tall: grown, wide: cut, collides: grown && collides(el) })) break;
567
- scale *= 0.94;
568
- Array.prototype.forEach.call(el.querySelectorAll(".block"), function (c) {
569
- c.style.fontSize = Number(c.dataset.base) * U * scale + "px";
570
- });
571
- }
572
- }
573
-
574
- // A soft scrim behind a band of light type over the picture (or a light one behind
575
- // dark type), so copy with no position of its own reads on any plate. Copy placed
576
- // by the layout sits on the calm area the plate kept for it.
577
- function scrim(name) {
578
- var el = bands[name];
579
- if (!hasPlate || !el) return;
580
- var s = document.getElementById("scrim-" + name);
581
- var firstText = el.querySelector(".block[data-dark]");
582
- if (!firstText) return;
583
- var tint = firstText.dataset.dark === "1" ? "255,255,255" : "0,0,0";
584
- var rect = el.getBoundingClientRect();
585
- var reach = name === "top" ? rect.bottom + 6 * U : H - rect.top + 6 * U;
586
- s.style.display = "block";
587
- s.style.height = Math.min(H * 0.6, reach) + "px";
588
- if (name === "top") {
589
- s.style.top = "0";
590
- s.style.background = "linear-gradient(to bottom, rgba(" + tint + ",0.42), rgba(" + tint + ",0))";
591
- } else {
592
- s.style.bottom = "0";
593
- s.style.background = "linear-gradient(to top, rgba(" + tint + ",0.42), rgba(" + tint + ",0))";
594
- }
595
- }
596
-
597
- var tl = gsap.timeline({ paused: true });
598
- tl.to({}, { duration: 1 });
599
- window.__timelines = window.__timelines || {};
600
- window.__timelines.main = tl;
601
-
602
- // The picture as this format shows it (cover, centred, like `.plate`), reduced to a
603
- // grid. Read through a blob so the canvas stays readable; the render opens the page
604
- // with file access for exactly this. Where it cannot be read (a check that opens the
605
- // page without that access), the copy stays where the layout put it.
606
- function readPlate(done) {
607
- if (!hasPlate) return done(null);
608
- try {
609
- var xhr = new XMLHttpRequest();
610
- xhr.open("GET", "./plate.png");
611
- xhr.responseType = "blob";
612
- xhr.onerror = function () {
613
- done(null);
614
- };
615
- xhr.onload = function () {
616
- if (!xhr.response || !xhr.response.size) return done(null);
617
- var img = new Image();
618
- img.onerror = function () {
619
- done(null);
620
- };
621
- img.onload = function () {
622
- try {
623
- var scale = 4;
624
- var cw = Math.round(W / scale);
625
- var ch = Math.round(H / scale);
626
- var canvas = document.createElement("canvas");
627
- canvas.width = cw;
628
- canvas.height = ch;
629
- var cx = canvas.getContext("2d");
630
- var k = Math.max(cw / img.naturalWidth, ch / img.naturalHeight);
631
- var dw = img.naturalWidth * k;
632
- var dh = img.naturalHeight * k;
633
- cx.drawImage(img, (cw - dw) / 2, (ch - dh) / 2, dw, dh);
634
- done(busyMap(cx.getImageData(0, 0, cw, ch).data, cw, ch, 6, scale));
635
- } catch (e) {
636
- done(null);
637
- }
638
- };
639
- img.src = URL.createObjectURL(xhr.response);
640
- };
641
- xhr.send();
642
- } catch (e) {
643
- done(null);
644
- }
645
- }
646
-
647
- function rectOf(el) {
648
- var r = el.getBoundingClientRect();
649
- var o = root.getBoundingClientRect();
650
- return { x: r.left - o.left, y: r.top - o.top, w: r.width, h: r.height };
651
- }
652
-
653
- // A frosted surface behind a group that has nowhere calm to sit, tinted away from its
654
- // ink. A logo has no ink to read: its surface follows the ground it was chosen for.
655
- function addBacking(el, map) {
656
- var r = rectOf(el);
657
- var first = el.querySelector(".block[data-ink]");
658
- var darkInk = first
659
- ? (hexLuma(first.dataset.ink) || 255) < 110
660
- : coverage(map, r).reduce(function (acc, k) {
661
- return acc + map.luma[k.i] * k.share;
662
- }, 0) > 128;
663
- var pad = { x: 2 * U, y: 1.2 * U };
664
- var b = document.createElement("div");
665
- b.className = "backing";
666
- b.style.left = Math.max(0, r.x - pad.x) + "px";
667
- b.style.top = Math.max(0, r.y - pad.y) + "px";
668
- b.style.width = Math.min(W, r.w + 2 * pad.x) + "px";
669
- b.style.height = Math.min(H, r.h + 2 * pad.y) + "px";
670
- b.style.borderRadius = 1.4 * U + "px";
671
- b.style.background = darkInk ? "rgba(255,255,255,0.62)" : "rgba(0,0,0,0.5)";
672
- // Under every group and card: a surface blurs what is behind it, and that must only
673
- // ever be the picture, never another line (a button beside it went unreadable).
674
- root.insertBefore(b, root.querySelector(".card, .group") || el);
675
- Array.prototype.forEach.call(el.querySelectorAll(".block[data-ink]"), function (c) {
676
- c.style.textShadow = "none";
677
- });
678
- }
679
-
680
- // Move each group off what the picture shows, set each line in an ink that reads on
681
- // what ends up under it, and give a surface to whatever still cannot read.
682
- // Where the words (and the logo) of a group actually are, not the region box around them.
683
- function partsOf(el) {
684
- var parts = [];
685
- Array.prototype.forEach.call(el.children, function (c) {
686
- if (c.classList.contains("logo")) return parts.push(rectOf(c));
687
- var fill = c.querySelector(".fill");
688
- if (fill) return parts.push(rectOf(fill));
689
- var range = document.createRange();
690
- range.selectNodeContents(c);
691
- Array.prototype.forEach.call(range.getClientRects(), function (r) {
692
- var o = root.getBoundingClientRect();
693
- if (r.width > 0 && r.height > 0) parts.push({ x: r.left - o.left, y: r.top - o.top, w: r.width, h: r.height });
694
- });
695
- });
696
- return parts;
697
- }
698
-
699
- // A card, or a group whose every line is a button or a sticker, carries its own surface.
700
- function surfaced(el) {
701
- if (el.classList.contains("panel") || el.dataset.card) return true;
702
- var blocks = el.querySelectorAll(".block");
703
- return blocks.length > 0 && el.querySelectorAll(".block .fill").length === blocks.length;
704
- }
705
-
706
- function arrange(map) {
707
- var els = containers();
708
- var items = els.map(function (el, i) {
709
- // A logo alone in its corner, or copy on a drawn card, stays where the layout put it.
710
- var logoOnly = el.children.length === 1 && el.firstChild.classList.contains("logo");
711
- // A drawn panel is an opaque surface in the layout's place: moving it off a busy spot
712
- // only uncovered what it was drawn over.
713
- var pinned = logoOnly || Boolean(el.dataset.card) || el.classList.contains("panel");
714
- return { id: String(i), rect: rectOf(el), parts: partsOf(el), panel: surfaced(el), pinned: pinned };
715
- });
716
- var safeRect = { x: box.x, y: box.y, w: box.w, h: box.h };
717
- var plans = placeGroups(map, { W: W, H: H, safe: safeRect, U: U }, items);
718
- var report = [];
719
- plans.forEach(function (plan, i) {
720
- var el = els[i];
721
- if (plan.dy) el.style.transform = (el.style.transform || "") + " translateY(" + plan.dy + "px)";
722
- var backing = plan.backing;
723
- if (!backing && !items[i].panel) {
724
- Array.prototype.forEach.call(el.querySelectorAll(".block[data-ink]"), function (c) {
725
- var ink = inkOn(map, rectOf(c), c.dataset.ink);
726
- if (ink !== c.dataset.ink) {
727
- c.dataset.ink = ink;
728
- c.style.color = ink;
729
- shade(c);
730
- }
731
- });
732
- }
733
- if (backing) addBacking(el, map);
734
- report.push({ group: el.className, dy: plan.dy, backing: backing });
735
- });
736
- window.__layout = { read: true, groups: report };
737
- }
738
-
739
- // A panel whose shape the picture painted anyway is laid over that shape, half a cell
740
- // proud on every side so no painted edge shows past it; its copy then fits inside.
741
- function snapPanels(map) {
742
- var snapped = 0;
743
- var claimed = {};
744
- var words = containers()
745
- .filter(function (el) {
746
- return !el.dataset.panel;
747
- })
748
- .reduce(function (acc, el) {
749
- return acc.concat(partsOf(el));
750
- }, []);
751
- containers().forEach(function (el) {
752
- if (!el.dataset.panel) return;
753
- var box = snapPanel(map, rectOf(el), claimed, el.dataset.panel, words);
754
- if (!box) return;
755
- if (el.classList.contains("panel-candidate")) {
756
- paintPanel(el);
757
- inkForPanel(el);
758
- }
759
- el.dataset.snapped = "1";
760
- var pad = map.cellW / 2;
761
- el.style.left = Math.max(0, box.x - pad) + "px";
762
- el.style.top = Math.max(0, box.y - pad) + "px";
763
- el.style.width = Math.min(W, box.w + 2 * pad) + "px";
764
- el.style.minHeight = box.h + 2 * pad + "px";
765
- el.style.height = box.h + 2 * pad + "px";
766
- el.style.paddingTop = "0";
767
- el.style.paddingBottom = "0";
768
- el.dataset.max = String(box.h + 2 * pad);
769
- snapped++;
770
- });
771
- return snapped;
772
- }
773
-
774
- // An older creative's line keeps its reference ink until its panel is drawn: then it takes
775
- // an ink that reads on the panel (display type, 3:1) and drops its picture shadow.
776
- function inkForPanel(el) {
777
- var ground = luminance(el.dataset.panel);
778
- Array.prototype.forEach.call(el.querySelectorAll(".block[data-ink]"), function (c) {
779
- c.style.textShadow = "none";
780
- var own = luminance(c.dataset.ink);
781
- if (own !== null && ground !== null && contrast(own, ground) >= 3) return;
782
- var ink = contrast(1, ground) >= contrast(luminance("#111111"), ground) ? "#ffffff" : "#111111";
783
- c.dataset.ink = ink;
784
- c.style.color = ink;
785
- });
786
- }
787
-
788
- // A snapped panel is the painted shape's height; copy that still does not fit at the
789
- // smallest size grows it (over the shape, never out of it) instead of spilling below.
790
- function growSnapped() {
791
- containers().forEach(function (el) {
792
- if (el.dataset.snapped !== "1" || el.scrollHeight <= el.clientHeight + 1) return;
793
- el.style.height = "auto";
794
- });
795
- }
796
-
797
- var ready = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
798
- ready.then(function () {
799
- readPlate(function (map) {
800
- var snapped = map ? snapPanels(map) : 0;
801
- containers().forEach(fit);
802
- growSnapped();
803
- if (map) {
804
- arrange(map);
805
- window.__layout.snapped = snapped;
806
- } else {
807
- ["top", "bottom"].forEach(scrim);
808
- window.__layout = { read: false };
809
- }
810
- root.setAttribute("data-ready", "1");
811
- });
812
- });
813
- })();
814
- </script>
815
- </body>
816
- </html>