@koda-sl/baker-cli 0.394.0-dev.d28cef496 → 0.395.0-dev.f8ecbaa42

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.
@@ -0,0 +1,404 @@
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; white-space: nowrap; }
24
+ .fill.highlight { padding: 0.12em 0.4em; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
25
+ .fill.sticker { transform: rotate(-3deg); }
26
+ .fill.outline { background: transparent !important; border: 0.09em solid currentColor; }
27
+ .logo { display: block; width: auto; object-fit: contain; }
28
+ .shape { position: absolute; pointer-events: none; }
29
+ .shape svg { display: block; height: 100%; width: auto; }
30
+ .shape.chrome { display: flex; align-items: center; justify-content: space-between; }
31
+ .chrome-back { display: flex; align-items: center; gap: 0.2em; font-family: 'AdBody', 'Helvetica Neue', Arial, sans-serif; }
32
+ .chrome-more { display: flex; align-items: center; justify-content: center; border-radius: 999px; }
33
+ .block.mark { display: flex; align-items: flex-start; gap: 0.45em; }
34
+ .block.mark svg { flex: none; width: 1.05em; height: 1.05em; margin-top: 0.1em; }
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <div id="root" data-composition-id="main" data-start="0" data-duration="1" data-width="__WIDTH__" data-height="__HEIGHT__">
39
+ <div class="scrim" id="scrim-top"></div>
40
+ <div class="scrim" id="scrim-bottom"></div>
41
+ </div>
42
+
43
+ <script>
44
+ (function () {
45
+ var AD = {{ad}};
46
+ var FRAME = {{frame}};
47
+ var W = __WIDTH__;
48
+ var H = __HEIGHT__;
49
+ var root = document.getElementById("root");
50
+ var safe = FRAME.safe || { top: 0.05, bottom: 0.05, left: 0.06, right: 0.06 };
51
+ var box = { x: safe.left * W, y: safe.top * H, w: W * (1 - safe.left - safe.right), h: H * (1 - safe.top - safe.bottom) };
52
+ // One unit per percent of the shorter side, so type reads the same size on every format.
53
+ var U = Math.min(W, H) / 100;
54
+ // A text-only ad has no picture (`plate: null`): the copy sits on the brand's ground.
55
+ var hasPlate = FRAME.plate !== null;
56
+ var ground = AD.background || { color: "#111111", ink: "#ffffff" };
57
+
58
+ var SIZE = { x_large: 8.6, large: 6.6, medium: 4.4, small: 3.0 };
59
+ var WEIGHT = { bold: 800, semibold: 600, regular: 400 };
60
+ var CASE = { upper: "uppercase", title: "capitalize", sentence: "none" };
61
+ var MAX_SHARE = { top: 0.44, middle: 0.3, bottom: 0.36 };
62
+ var RADIUS = { pill: "999px", rounded: "0.35em", square: "0" };
63
+
64
+ // With no face staged, the text never names the ad's faces, so the browser never asks
65
+ // for a file that is not there (a 404 the pre-render check refuses).
66
+ var FALLBACK_FACE = "'Helvetica Neue', Arial, sans-serif";
67
+ var facesStaged = FRAME.fonts !== false;
68
+
69
+ if (hasPlate) {
70
+ var plate = document.createElement("img");
71
+ plate.className = "plate";
72
+ plate.id = "plate";
73
+ plate.src = "./plate.png";
74
+ plate.alt = "";
75
+ root.insertBefore(plate, root.firstChild);
76
+ } else {
77
+ root.style.background = ground.color;
78
+ }
79
+
80
+ function luma(hex) {
81
+ var m = /^#?([0-9a-f]{6})$/i.exec(hex || "");
82
+ if (!m) return null;
83
+ var v = parseInt(m[1], 16);
84
+ return 0.2126 * ((v >> 16) & 255) + 0.7152 * ((v >> 8) & 255) + 0.0722 * (v & 255);
85
+ }
86
+
87
+ function alignSelf(align) {
88
+ return align === "left" ? "flex-start" : align === "right" ? "flex-end" : "center";
89
+ }
90
+
91
+ // Every line is drawn in its group: the layout region the reference put it in (placed
92
+ // at that region's box on this format), or a band when the layout gave no position.
93
+ var groups = {};
94
+ var bands = { top: null, middle: null, bottom: null };
95
+ (AD.groups || []).forEach(function (g) {
96
+ groups[g.id] = g;
97
+ });
98
+
99
+ function bandEl(name) {
100
+ if (bands[name]) return bands[name];
101
+ var el = document.createElement("div");
102
+ el.className = "group band";
103
+ el.dataset.band = name;
104
+ el.style.left = box.x + "px";
105
+ el.style.width = box.w + "px";
106
+ el.style.gap = 1.6 * U + "px";
107
+ if (name === "top") el.style.top = box.y + "px";
108
+ if (name === "bottom") el.style.bottom = H - (box.y + box.h) + "px";
109
+ if (name === "middle") {
110
+ el.style.top = box.y + box.h / 2 + "px";
111
+ el.style.transform = "translateY(-50%)";
112
+ }
113
+ el.dataset.max = String(box.h * MAX_SHARE[name]);
114
+ root.appendChild(el);
115
+ bands[name] = el;
116
+ return el;
117
+ }
118
+
119
+ var placed = {};
120
+ function containerFor(groupId, band) {
121
+ var g = groups[groupId];
122
+ var at = g && g.at && g.at[FRAME.format];
123
+ if (!at) return bandEl((g && g.band) || band || "top");
124
+ if (placed[groupId]) return placed[groupId];
125
+ var el = document.createElement("div");
126
+ el.className = "group placed";
127
+ el.style.left = (at.x[0] / 100) * W + "px";
128
+ el.style.width = ((at.x[1] - at.x[0]) / 100) * W + "px";
129
+ el.style.top = (at.y[0] / 100) * H + "px";
130
+ el.style.minHeight = ((at.y[1] - at.y[0]) / 100) * H + "px";
131
+ el.style.justifyContent = "center";
132
+ el.style.gap = 1.2 * U + "px";
133
+ // A card's lines keep off its edges.
134
+ if (g.inset) el.style.padding = 2.6 * U + "px " + 3.2 * U + "px";
135
+ // Copy rewritten for our brand runs longer than the source's: allow some growth
136
+ // past the region before the type shrinks.
137
+ el.dataset.max = String(((at.y[1] - at.y[0]) / 100) * H * 1.35);
138
+ root.appendChild(el);
139
+ placed[groupId] = el;
140
+ return el;
141
+ }
142
+
143
+ function addLogo(logo) {
144
+ var img = document.createElement("img");
145
+ img.className = "logo";
146
+ img.src = "./logo.png";
147
+ img.alt = "";
148
+ img.style.height = 5.4 * U + "px";
149
+ img.style.maxWidth = box.w * 0.42 + "px";
150
+ img.style.alignSelf = alignSelf(logo.align);
151
+ img.onerror = function () {
152
+ img.remove();
153
+ };
154
+ var el = containerFor(logo.group, logo.band);
155
+ if (el.dataset.band === "bottom") el.appendChild(img);
156
+ else el.insertBefore(img, el.firstChild);
157
+ }
158
+
159
+ // The structures of a text-only ad, each at its layout region's box on this format:
160
+ // cards and panels, dividers, icons and an app's chrome. Drawn before the copy, under it.
161
+ var ICON_PATHS = {
162
+ check: '<path d="M5 12.5l4.5 4.5L19 7.5" fill="none" stroke="C" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>',
163
+ cross: '<path d="M6.5 6.5l11 11M17.5 6.5l-11 11" fill="none" stroke="C" stroke-width="3" stroke-linecap="round"/>',
164
+ dots: '<circle cx="5" cy="12" r="2" fill="C"/><circle cx="12" cy="12" r="2" fill="C"/><circle cx="19" cy="12" r="2" fill="C"/>',
165
+ chevron_left: '<path d="M15 4.5L7.5 12 15 19.5" fill="none" stroke="C" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>',
166
+ arrow_right: '<path d="M4 12h15M13 6l6 6-6 6" fill="none" stroke="C" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>',
167
+ star: '<path d="M12 3l2.7 5.6 6.1.9-4.4 4.3 1 6.1L12 17l-5.4 2.9 1-6.1-4.4-4.3 6.1-.9z" fill="C"/>',
168
+ heart: '<path d="M12 20s-7-4.4-7-10a4 4 0 017-2.6A4 4 0 0119 10c0 5.6-7 10-7 10z" fill="C"/>',
169
+ };
170
+ function icon(name, color) {
171
+ var path = ICON_PATHS[name] || "";
172
+ return '<svg viewBox="0 0 24 24" aria-hidden="true">' + path.split('"C"').join('"' + color + '"') + "</svg>";
173
+ }
174
+ var SHAPE_RADIUS = { pill: "999px", rounded: 2.4 * U + "px", square: "0" };
175
+ function addShape(s) {
176
+ var at = s.at && s.at[FRAME.format];
177
+ if (!at) return;
178
+ var left = (at.x[0] / 100) * W;
179
+ var top = (at.y[0] / 100) * H;
180
+ var width = ((at.x[1] - at.x[0]) / 100) * W;
181
+ var height = ((at.y[1] - at.y[0]) / 100) * H;
182
+ var el = document.createElement("div");
183
+ el.className = "shape " + s.kind;
184
+ el.setAttribute("data-layout-ignore", "");
185
+ el.style.left = left + "px";
186
+ el.style.top = top + "px";
187
+ el.style.width = width + "px";
188
+ el.style.height = height + "px";
189
+ if (s.kind === "box") {
190
+ el.style.background = s.fill || "transparent";
191
+ if (s.border) el.style.border = Math.max(2, 0.28 * U) + "px solid " + s.border;
192
+ el.style.borderRadius = SHAPE_RADIUS[s.radius] || SHAPE_RADIUS.rounded;
193
+ if (s.shadow) el.style.boxShadow = "0 " + 0.8 * U + "px " + 2.6 * U + "px rgba(0,0,0,0.18)";
194
+ } else if (s.kind === "rule") {
195
+ var thick = Math.max(2, 0.32 * U);
196
+ el.style.background = s.color;
197
+ if (width >= height) {
198
+ el.style.top = top + height / 2 - thick / 2 + "px";
199
+ el.style.height = thick + "px";
200
+ } else {
201
+ el.style.left = left + width / 2 - thick / 2 + "px";
202
+ el.style.width = thick + "px";
203
+ }
204
+ } else if (s.kind === "icon") {
205
+ el.style.display = "flex";
206
+ el.style.justifyContent = "center";
207
+ el.style.alignItems = "center";
208
+ if (s.ring) {
209
+ // A circled icon (a note's "more" button): the ring is as tall as the region.
210
+ var ring = Math.min(width, height);
211
+ var circle = document.createElement("div");
212
+ circle.className = "chrome-more";
213
+ circle.style.width = ring + "px";
214
+ circle.style.height = ring + "px";
215
+ circle.style.border = Math.max(2, ring * 0.07) + "px solid " + s.color;
216
+ circle.innerHTML = '<span style="display:block;height:' + ring * 0.6 + 'px">' + icon(s.icon, s.color) + "</span>";
217
+ el.appendChild(circle);
218
+ } else {
219
+ el.innerHTML = icon(s.icon, s.color);
220
+ }
221
+ } else if (s.kind === "chrome") {
222
+ var size = Math.min(height * 0.62, 5 * U);
223
+ var back = document.createElement("div");
224
+ back.className = "chrome-back";
225
+ if (!facesStaged) back.style.fontFamily = FALLBACK_FACE;
226
+ back.style.color = s.color;
227
+ back.style.fontSize = size + "px";
228
+ back.innerHTML = '<span style="display:block;height:1.1em">' + icon("chevron_left", s.color) + "</span>";
229
+ var label = document.createElement("span");
230
+ label.textContent = s.label;
231
+ back.appendChild(label);
232
+ var more = document.createElement("div");
233
+ more.className = "chrome-more";
234
+ more.style.width = size * 1.35 + "px";
235
+ more.style.height = size * 1.35 + "px";
236
+ more.style.border = Math.max(2, size * 0.09) + "px solid " + s.color;
237
+ more.innerHTML = '<span style="display:block;height:' + size * 0.8 + 'px">' + icon("dots", s.color) + "</span>";
238
+ el.appendChild(back);
239
+ if (s.more) el.appendChild(more);
240
+ }
241
+ root.appendChild(el);
242
+ }
243
+
244
+ function addBlock(b) {
245
+ var el = document.createElement("div");
246
+ el.className = "block " + b.role;
247
+ if (!facesStaged) el.style.fontFamily = FALLBACK_FACE;
248
+ var size = b.role === "fine_print" ? 2.4 : SIZE[b.size] || SIZE.medium;
249
+ el.dataset.base = String(size);
250
+ el.style.fontSize = size * U + "px";
251
+ el.style.fontWeight = String(WEIGHT[b.weight] || (b.role === "cta" ? 700 : 400));
252
+ // A staged face is one weight, so a line prompt.json sets bold (a bold paragraph, a
253
+ // sign-off) is drawn in the heavy face whatever its role.
254
+ if (facesStaged && WEIGHT[b.weight] >= 600) {
255
+ el.style.fontFamily = "'AdHeadline', 'AdBody', 'Helvetica Neue', Arial, sans-serif";
256
+ }
257
+ el.style.lineHeight = b.role === "headline" ? "1.04" : "1.22";
258
+ el.style.textTransform = CASE[b.case] || "none";
259
+ el.style.textAlign = b.align || "center";
260
+ el.style.alignSelf = alignSelf(b.align);
261
+ var t = b.treatment || { kind: b.role === "cta" && !AD.groups ? "button" : "plain" };
262
+ var ink = b.color || (hasPlate ? "#ffffff" : ground.ink);
263
+ var text = b.text;
264
+ if (t.kind === "arrow" && !/[→›»]\s*$/.test(text)) text = text + " →";
265
+ if (t.kind === "button" || t.kind === "sticker" || t.kind === "highlight") {
266
+ var fill = document.createElement("span");
267
+ fill.className = "fill " + t.kind;
268
+ fill.textContent = text;
269
+ fill.style.background = t.background || (AD.cta && AD.cta.background) || "#111111";
270
+ fill.style.color = t.color || (AD.cta && AD.cta.color) || "#ffffff";
271
+ fill.style.borderRadius = RADIUS[t.shape] || RADIUS.rounded;
272
+ if (t.outline) fill.classList.add("outline");
273
+ el.appendChild(fill);
274
+ } else {
275
+ if (t.kind === "mark") {
276
+ // A tick or cross list item: the mark in its own colour, then the line.
277
+ el.classList.add("mark");
278
+ el.innerHTML = icon(t.mark, t.color);
279
+ var words = document.createElement("span");
280
+ words.textContent = text;
281
+ el.appendChild(words);
282
+ } else {
283
+ el.textContent = text;
284
+ }
285
+ el.style.color = ink;
286
+ if (t.kind === "underline") {
287
+ el.style.textDecoration = "underline";
288
+ el.style.textDecorationThickness = "0.08em";
289
+ el.style.textUnderlineOffset = "0.18em";
290
+ }
291
+ if (t.kind === "strike") {
292
+ el.style.textDecoration = "line-through";
293
+ el.style.textDecorationThickness = "0.08em";
294
+ }
295
+ var dark = (luma(ink) || 255) < 110;
296
+ el.dataset.dark = dark ? "1" : "0";
297
+ if (hasPlate) el.style.textShadow = dark ? "0 1px 10px rgba(255,255,255,0.35)" : "0 2px 14px rgba(0,0,0,0.45)";
298
+ }
299
+ containerFor(b.group, b.band).appendChild(el);
300
+ }
301
+
302
+ var drawCopy = FRAME.draw_copy !== false;
303
+ (hasPlate ? [] : AD.shapes || []).forEach(addShape);
304
+ (drawCopy ? AD.copy || [] : []).forEach(addBlock);
305
+ if (AD.logo) addLogo(AD.logo);
306
+
307
+ function containers() {
308
+ return Array.prototype.slice.call(root.querySelectorAll(".group"));
309
+ }
310
+
311
+ // Shrink a group's type until it fits its room and no line runs past its edge. The
312
+ // copy is whatever prompt.json says today, so its length is not known when the
313
+ // layout is written.
314
+ function fit(el) {
315
+ var max = Number(el.dataset.max);
316
+ // The room inside a card's padding, not the card's outer width.
317
+ var pad = getComputedStyle(el);
318
+ var width = el.getBoundingClientRect().width - parseFloat(pad.paddingLeft) - parseFloat(pad.paddingRight);
319
+ var scale = 1;
320
+ for (var i = 0; i < 40; i++) {
321
+ var tooTall = el.scrollHeight > max;
322
+ var tooWide = Array.prototype.some.call(el.children, function (c) {
323
+ return c.scrollWidth > width + 1;
324
+ });
325
+ if ((!tooTall && !tooWide) || scale < 0.45) break;
326
+ scale *= 0.94;
327
+ Array.prototype.forEach.call(el.querySelectorAll(".block"), function (c) {
328
+ c.style.fontSize = Number(c.dataset.base) * U * scale + "px";
329
+ c.dataset.scale = String(scale);
330
+ });
331
+ }
332
+ }
333
+
334
+ // A region's copy may run past its box, but never into the next region down the same
335
+ // column: the room it has ends where that one starts.
336
+ function keepClearOfNext(el) {
337
+ var r = el.getBoundingClientRect();
338
+ var room = Infinity;
339
+ Array.prototype.forEach.call(root.querySelectorAll(".group.placed"), function (other) {
340
+ if (other === el) return;
341
+ var q = other.getBoundingClientRect();
342
+ var sameColumn = Math.min(r.right, q.right) - Math.max(r.left, q.left) > 0;
343
+ if (sameColumn && q.top > r.top + 1) room = Math.min(room, q.top - r.top - 0.8 * U);
344
+ });
345
+ // Two regions the format already stacks on each other are left as they are: holding
346
+ // one to less than its own box would only shrink its type to nothing.
347
+ if (room === Infinity || room < parseFloat(el.style.minHeight)) return;
348
+ el.dataset.max = String(Math.min(Number(el.dataset.max), room));
349
+ }
350
+
351
+ // Lines set at one size in prompt.json stay one size in the ad: every one is drawn at
352
+ // the smallest scale any of them needed to fit.
353
+ function evenSizes() {
354
+ var least = {};
355
+ var blocks = Array.prototype.slice.call(root.querySelectorAll(".block"));
356
+ blocks.forEach(function (b) {
357
+ var s = Number(b.dataset.scale || 1);
358
+ least[b.dataset.base] = Math.min(least[b.dataset.base] || 1, s);
359
+ });
360
+ blocks.forEach(function (b) {
361
+ b.style.fontSize = Number(b.dataset.base) * U * least[b.dataset.base] + "px";
362
+ });
363
+ }
364
+
365
+ // A soft scrim behind a band of light type over the picture (or a light one behind
366
+ // dark type), so copy with no position of its own reads on any plate. Copy placed
367
+ // by the layout sits on the calm area the plate kept for it.
368
+ function scrim(name) {
369
+ var el = bands[name];
370
+ if (!hasPlate || !el) return;
371
+ var s = document.getElementById("scrim-" + name);
372
+ var firstText = el.querySelector(".block[data-dark]");
373
+ if (!firstText) return;
374
+ var tint = firstText.dataset.dark === "1" ? "255,255,255" : "0,0,0";
375
+ var rect = el.getBoundingClientRect();
376
+ var reach = name === "top" ? rect.bottom + 6 * U : H - rect.top + 6 * U;
377
+ s.style.display = "block";
378
+ s.style.height = Math.min(H * 0.6, reach) + "px";
379
+ if (name === "top") {
380
+ s.style.top = "0";
381
+ s.style.background = "linear-gradient(to bottom, rgba(" + tint + ",0.42), rgba(" + tint + ",0))";
382
+ } else {
383
+ s.style.bottom = "0";
384
+ s.style.background = "linear-gradient(to top, rgba(" + tint + ",0.42), rgba(" + tint + ",0))";
385
+ }
386
+ }
387
+
388
+ var tl = gsap.timeline({ paused: true });
389
+ tl.to({}, { duration: 1 });
390
+ window.__timelines = window.__timelines || {};
391
+ window.__timelines.main = tl;
392
+
393
+ var ready = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
394
+ ready.then(function () {
395
+ Array.prototype.forEach.call(root.querySelectorAll(".group.placed"), keepClearOfNext);
396
+ containers().forEach(fit);
397
+ evenSizes();
398
+ ["top", "bottom"].forEach(scrim);
399
+ root.setAttribute("data-ready", "1");
400
+ });
401
+ })();
402
+ </script>
403
+ </body>
404
+ </html>
@@ -0,0 +1,53 @@
1
+ {
2
+ "id": "static-ad",
3
+ "title": "Static ad: the picture (or, text-only, the layout drawn from prompt.json) with what the concept holds drawn on top",
4
+ "description": "Written by `baker canvas scaffold-static-ad`, one copy per delivered format. Draws only what prompt.compose.json (derived from prompt.json) holds: each line of copy where the source layout placed it, set the way the reference set it (plain, underlined, with an arrow, as a button or sticker), in the staged fonts, and the logo file only when the concept shows one. The plate is the image model's clean picture; a text-only ad has none and sits on the brand colour. Width and height are this format's delivered size.",
5
+ "width": 1080,
6
+ "height": 1350,
7
+ "fps": 1,
8
+ "default_duration": 1,
9
+ "inputs": {
10
+ "plate": {
11
+ "kind": "image",
12
+ "required": false,
13
+ "staged_as": "plate.png",
14
+ "description": "The picture, with no overlay text and no logo. Absent for a text-only ad."
15
+ },
16
+ "logo": {
17
+ "kind": "image",
18
+ "required": false,
19
+ "staged_as": "logo.png",
20
+ "description": "The brand logo file, wired only when the concept shows a logo."
21
+ },
22
+ "font_headline": {
23
+ "kind": "font",
24
+ "required": false,
25
+ "staged_as": "headline.font",
26
+ "description": "The brand's headline face."
27
+ },
28
+ "font_body": {
29
+ "kind": "font",
30
+ "required": false,
31
+ "staged_as": "body.font",
32
+ "description": "The brand's body face."
33
+ },
34
+ "compose": {
35
+ "kind": "json",
36
+ "required": true,
37
+ "staged_as": "compose.json",
38
+ "description": "prompt.compose.json, also passed inline as the `ad` param."
39
+ }
40
+ },
41
+ "params": {
42
+ "ad": {
43
+ "kind": "json",
44
+ "required": true,
45
+ "description": "The copy blocks, logo placement and CTA colours (prompt.compose.json)."
46
+ },
47
+ "frame": {
48
+ "kind": "json",
49
+ "required": true,
50
+ "description": "This format: its safe area, whether copy is drawn, the picture it is cut from (plate: null for none), and whether fonts are staged."
51
+ }
52
+ }
53
+ }