@koda-sl/baker-cli 0.403.0 → 0.404.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/README.md CHANGED
@@ -4049,6 +4049,21 @@ baker canvas validate my-canvas.json
4049
4049
  # Where `hyperframes` is installed, a static ad's composition is laid out with the copy and
4050
4050
  # fonts the run will draw, over a stand-in picture: two lines of copy that collide are refused
4051
4051
  # as `layout/content_overlap` before any picture is paid for.
4052
+ # At render, each static format reads the picture it is drawn on: a line that would sit on
4053
+ # the subject, a device or the seam of two painted panels moves to the nearest calm room
4054
+ # (never past another line, never more than a few of its own heights), takes white or
4055
+ # near-black when its own colour does not read (WCAG 3:1, display type), or gets a frosted
4056
+ # surface when no room is calm. A region's card or pill is drawn with its words; when the
4057
+ # picture painted that shape anyway, the drawn panel is laid over it (a creative scaffolded
4058
+ # before this draws a panel only there, so its plates stay cached). Copy never shrinks
4059
+ # below 72% of its size. The static-ad template in each creative's `layout-*/` folders is
4060
+ # refreshed on every validate and run.
4061
+ # Before the pictures are painted, the scene's subjects are read (Jev, OpenRouter) for
4062
+ # shapes made to hold copy, each matched to a shape the composition draws (or none); the
4063
+ # plate leaves out only the matched ones, and is told which shapes the designer draws
4064
+ # (`drawn_by_designer` in prompt.plate.json); each answer is kept in
4065
+ # `prompt.compose.json` (`scene_reads`), and one Jev could not give is kept as part of the
4066
+ # picture, so the plate spec does not change between runs.
4052
4067
 
4053
4068
  # 2. Run (executes nodes, writes files to ./canvas/<run_id>/)
4054
4069
  baker canvas run my-canvas.json
@@ -24,6 +24,8 @@
24
24
  .fill.highlight { padding: 0.12em 0.4em; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
25
25
  .fill.sticker { transform: rotate(-3deg); }
26
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); }
27
29
  </style>
28
30
  </head>
29
31
  <body>
@@ -49,7 +51,8 @@
49
51
 
50
52
  var SIZE = { x_large: 8.6, large: 6.6, medium: 4.4, small: 3.0 };
51
53
  var WEIGHT = { bold: 800, semibold: 600, regular: 400 };
52
- var CASE = { upper: "uppercase", title: "capitalize", sentence: "none" };
54
+ // Title case is left as written: `capitalize` also raised "and", "of" and "de".
55
+ var CASE = { upper: "uppercase", title: "none", sentence: "none" };
53
56
  var MAX_SHARE = { top: 0.44, middle: 0.3, bottom: 0.36 };
54
57
  var RADIUS = { pill: "999px", rounded: "0.35em", square: "0" };
55
58
 
@@ -69,13 +72,300 @@
69
72
  root.style.background = ground.color;
70
73
  }
71
74
 
72
- function luma(hex) {
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) {
73
232
  var m = /^#?([0-9a-f]{6})$/i.exec(hex || "");
74
233
  if (!m) return null;
75
234
  var v = parseInt(m[1], 16);
76
235
  return 0.2126 * ((v >> 16) & 255) + 0.7152 * ((v >> 8) & 255) + 0.0722 * (v & 255);
77
236
  }
78
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
+
79
369
  function alignSelf(align) {
80
370
  return align === "left" ? "flex-start" : align === "right" ? "flex-end" : "center";
81
371
  }
@@ -121,6 +411,15 @@
121
411
  el.style.top = (at.y[0] / 100) * H + "px";
122
412
  el.style.minHeight = ((at.y[1] - at.y[0]) / 100) * H + "px";
123
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
+ }
124
423
  el.style.gap = 1.2 * U + "px";
125
424
  // Copy rewritten for our brand runs longer than the source's: allow some growth
126
425
  // past the region before the type shrinks.
@@ -146,6 +445,21 @@
146
445
  else el.insertBefore(img, el.firstChild);
147
446
  }
148
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
+
149
463
  function addBlock(b) {
150
464
  var el = document.createElement("div");
151
465
  el.className = "block " + b.role;
@@ -178,14 +492,33 @@
178
492
  el.style.textDecorationThickness = "0.08em";
179
493
  el.style.textUnderlineOffset = "0.18em";
180
494
  }
181
- var dark = (luma(ink) || 255) < 110;
182
- el.dataset.dark = dark ? "1" : "0";
183
- if (hasPlate) el.style.textShadow = dark ? "0 1px 10px rgba(255,255,255,0.35)" : "0 2px 14px rgba(0,0,0,0.45)";
495
+ el.dataset.ink = ink;
184
496
  }
185
- containerFor(b.group, b.band).appendChild(el);
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);
186
518
  }
187
519
 
188
520
  var drawCopy = FRAME.draw_copy !== false;
521
+ if (drawCopy) (AD.cards || []).forEach(addCard);
189
522
  (drawCopy ? AD.copy || [] : []).forEach(addBlock);
190
523
  if (AD.logo) addLogo(AD.logo);
191
524
 
@@ -196,16 +529,41 @@
196
529
  // Shrink a group's type until it fits its room and no line runs past its edge. The
197
530
  // copy is whatever prompt.json says today, so its length is not known when the
198
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
+
199
554
  function fit(el) {
200
555
  var max = Number(el.dataset.max);
201
556
  var width = el.getBoundingClientRect().width;
202
557
  var scale = 1;
203
558
  for (var i = 0; i < 40; i++) {
204
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.
205
561
  var tooWide = Array.prototype.some.call(el.children, function (c) {
206
562
  return c.scrollWidth > width + 1;
207
563
  });
208
- if ((!tooTall && !tooWide) || scale < 0.45) break;
564
+ var cut = tooWide && offFrame(el);
565
+ var grown = tooTall || tooWide;
566
+ if (!keepShrinking(scale, { tall: grown, wide: cut, collides: grown && collides(el) })) break;
209
567
  scale *= 0.94;
210
568
  Array.prototype.forEach.call(el.querySelectorAll(".block"), function (c) {
211
569
  c.style.fontSize = Number(c.dataset.base) * U * scale + "px";
@@ -241,11 +599,216 @@
241
599
  window.__timelines = window.__timelines || {};
242
600
  window.__timelines.main = tl;
243
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
+
244
797
  var ready = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
245
798
  ready.then(function () {
246
- containers().forEach(fit);
247
- ["top", "bottom"].forEach(scrim);
248
- root.setAttribute("data-ready", "1");
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
+ });
249
812
  });
250
813
  })();
251
814
  </script>