@koda-sl/baker-cli 0.396.0-dev.88120240 → 0.397.0-dev.afb6a264b
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 +14 -6
- package/canvas/static-ad-composition/index.html +119 -63
- package/canvas/static-ad-composition/meta.json +6 -6
- package/dist/cli.js +792 -215
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5621,13 +5621,20 @@ Turn a source/inspiration image into a **runnable, self-validated static-ad canv
|
|
|
5621
5621
|
2. **element selection** (`~google/gemini-flash-latest`) — picks the **main, identity-critical** elements (the brand logo, a showcased product, a trust badge) **plus any foreground/hero person or animal** — the emotional focal point — even a generic one, because a free-generated face/muzzle reads as AI and grows artifacts; the emotional hero always gets a real-reference slot. When the advertiser's logo appears in **more than one lockup** (a square/icon **mark** and a horizontal **wordmark**), each is emitted as its **own** element (e.g. `LOGO_MARK`, `LOGO_WORDMARK`) so you drop the right file in each slot instead of stretching one logo to cover both. The describe pass also records the ad's **typography** under a `fonts` block (each typeface's classification, a best-guess family, and its weight/case) so you know exactly what to drop at the brand-font slot. Background extras are dropped. Each element is stamped back onto its blueprint entry as a `reference_image` label so the JSON self-documents which slot grounds which subject.
|
|
5622
5622
|
3. **global layout** (`~google/gemini-flash-latest`) — produces a structured `layout` block in `prompt.json`: the column/row grid, each region's `x_pct`/`y_pct` bounds, panel splits, background/shape, and every text block's relative size/weight/case/alignment. This is what gives the generator a precise composition to rebuild.
|
|
5623
5623
|
|
|
5624
|
-
It then scaffolds a canvas that
|
|
5624
|
+
It then scaffolds a canvas that draws **only what `prompt.json` holds**; no element is mandatory. The report's `mode` names which of four the ad is:
|
|
5625
|
+
|
|
5626
|
+
- **`composed`**: the image model (`openai/gpt-image-2.5-sunburst`, quality `xhigh`) paints up to **three candidate plates** (`--takes`, default 3; the picture with no overlay text and no logo, keeping calm the areas the copy will occupy), an `image_select` judge scores them on one dominant idea, thumbnail legibility, mechanism kept, space for what is drawn, brand with the logo covered, artifacts, Stories safe zones and text, and every delivered format is drawn by an HTML composition (`layout-4x5/`, `layout-9x16/`, one per format, snapshotted by `hyperframe_snapshot`). Each line sits at its layout region, carried through the format's crop or extension into its safe area (a band only when the layout gives it no position), and is set the way the reference set it: plain, underlined, followed by an arrow, a button (in the brand CTA colour), a sticker or a highlight. A call to action is never forced into a pill.
|
|
5627
|
+
- **`picture_only`**: the concept has no copy, so the plate is the ad and nothing is drawn on it.
|
|
5628
|
+
- **`text_only`**: no picture is generated. The copy is drawn on the brand's lead colour in its fonts, for near-zero credits. On by default when the reference pictures nothing but copy (no subjects, no people); `--text-only` forces it, `--no-text-only` refuses it.
|
|
5629
|
+
- **`baked`**: the copy is painted into the plates, only with `--skip-font` or when no font can load; every other shape is re-framed by `image_aspect_adapt` on `google/gemini-3-pro-image`, never cropped.
|
|
5630
|
+
|
|
5631
|
+
**Fonts** come from the first source that has one: a file in `src/brand/fonts/` (via `src/brand/tokens.json` when present), else the heading and body families `tokens.json` or `BRAND.md`'s typography section name, fetched as web font files from Google Fonts or Fontsource and ingested by URL, else Inter (800 and 400). **The logo is optional**: it is drawn from `src/brand/logos/` only when `prompt.json` keeps a logo (a `logo` layout region, or an `advertiser_brand` entry in `brands_logos`) and a real file exists. Meta shows the page name and picture on every ad, so no logo is never an error, never a slot to fill and never a stand-in; a canvas scaffolded with an unfilled logo slot runs without it. Text that belongs to something in the scene (a phone note, a screenshot, a sign, packaging: `surface: "in_scene"` in `text_content`) is painted by the plate, not overlaid, and listed in the report's `in_scene_text`. The plates read `prompt.plate.json` (the scene, the reserved areas, the visual mechanisms and the in-scene text; no overlay copy, no brand names) and the compositions read `prompt.compose.json`; both are re-derived from `prompt.json` by `validate` and `run`, so a copy edit re-draws the formats from the cached plates without paying for a picture again. A format within 10% of the plate's shape is a free crop; a farther one extends the chosen plate once on the same model. Detected elements get **one `[TODO]` ingest slot** each (never the logo), and the original image is wired in for composition only, left out of the render when it shows a person, because the generator copies that face whatever the prompt says (the blueprint's layout carries the composition instead). Each **animal hero** is additionally fused into a generated **multi-view reference sheet** (`image_reference_sheet`, a turnaround built from the one dropped photo) that the render grounds on instead of the lone flat snapshot. Pass `--skip-actor-sheets` to ground straight on the dropped photo. A **person hero** is cast from a company Avatar with `--avatar <handle>`: the slot ingests its identity sheet and `prompt.json` describes that person in the Avatar's own words. The render keeps the face it is given, so it is never a found photo of a real person. The canvas is validated before it's written. stdout returns `{ ok, canvas_path, prompt_path, models, aspect_ratio, placements, output_formats, layout_regions, stats, checklist }`; the **checklist** carries `mode`, `summary` (what a run does: pictures, judge, fonts, logo), `fonts`, `logo`, `in_scene_text` and every real asset to drop in.
|
|
5625
5632
|
|
|
5626
5633
|
**Placement formats.** The canvas delivers the platform's full placement set, each format at its exact size, collected under `final` with the format as its label. The set is derived from `--platform` (`meta` → 4:5 + 9:16, `google` → 1:1 + 4:5 + 16:9, `linkedin` → 1:1, `tiktok` → 9:16, `youtube` → 16:9); override it with `--placements <preset>` or `--placements none` for a single format. The report's `aspect_ratio` is the ratio the plates really render at (the model's nearest to the lead format), and `format_plan` says how each format is made.
|
|
5627
5634
|
|
|
5628
|
-
**After a run** the CLI reads the finished ad back: every delivered image's pixel shape against its format, and one vision pass that reads each word against the copy and checks the logo,
|
|
5635
|
+
**After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format, and one vision pass that reads each word against the copy `prompt.json` holds and checks one dominant idea, clipping, artifacts, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-plate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed.
|
|
5629
5636
|
|
|
5630
|
-
The
|
|
5637
|
+
The logo is never a slot to fill: use the company's committed logo in `src/brand/logos/` (per `BRAND.md`), never `baker images logo <domain>`, whose Brandfetch CDN often serves an outdated mark.
|
|
5631
5638
|
|
|
5632
5639
|
The plate prompt is **model-aware**: for **gpt-image** the exclusions (no invented badges/watermark) are hoisted to the very *end* of the prompt where the model reads them as pure exclusions rather than composition. In the baked-copy fallback it quotes every line of `prompt.json`'s copy (never the logo's text) with an instruction to render each **once, verbatim**; that block is re-read from `prompt.json` on every `validate` and `run`, which also upgrades canvases scaffolded before this change. Run `baker canvas critique` on the scaffolded canvas for an advisory read before you spend.
|
|
5633
5640
|
|
|
@@ -5648,11 +5655,12 @@ baker canvas run ./static-ad.canvas.json
|
|
|
5648
5655
|
| `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
|
|
5649
5656
|
| `--layout-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the global-layout `text_generate` model. |
|
|
5650
5657
|
| `--gen-model <id>` | `openai/gpt-image-2.5-sunburst` | Override the plate model. |
|
|
5651
|
-
| `--takes <n>` | `3` | Plates rendered and judged (1-3); `1` skips the judge. |
|
|
5658
|
+
| `--takes <n>` | `3` | Plates rendered and judged (1-3); `1` skips the judge. A text-only ad renders none. |
|
|
5659
|
+
| `--text-only` | on when the reference pictures nothing but copy | Draw a typographic ad: no picture generated, the copy on the brand colours in its fonts. `--no-text-only` renders a picture anyway. |
|
|
5652
5660
|
| `--platform <channel>` | `meta` | `_definition.md` channel **and** the default placement set the hero fans out to (`meta`\|`google`\|`linkedin`\|`tiktok`\|`youtube`\|`x`\|`other`; `x`/`other` fan out to nothing). |
|
|
5653
5661
|
| `--placements <preset\|none>` | derived from `--platform` | Override the fan-out set: `meta_feed`\|`meta_feed_stories`\|`meta_all`\|`linkedin`\|`google_demandgen`\|`tiktok`\|`youtube`, or `none` for a single base ad. |
|
|
5654
5662
|
| `--aspect <ratio>` | inferred from the image, else `9:16` | Force the lead format (it leads the placement set when the set has it). |
|
|
5655
|
-
| `--skip-font` | off | Bake the copy into the plates
|
|
5663
|
+
| `--skip-font` | off | Bake the copy into the plates instead of drawing it in a real font (a font file, else the typeface the brand names, else Inter). |
|
|
5656
5664
|
| `--skip-actor-sheets` | off | Ground each animal hero on its lone dropped photo instead of a generated multi-view reference sheet. |
|
|
5657
5665
|
| `--avatar <handles>` | — | Cast company avatars as the image's people, comma-separated in the order they were detected (`marta` or `marta,joan`). Each person slot grounds on that avatar's identity sheet and takes its subject description. Checked before any billed pass; an unknown, not-ready or sheetless avatar fails with code `avatar`. A handle with no person to play is reported in `hints` as `NOT CAST`. |
|
|
5658
5666
|
|
|
@@ -6184,7 +6192,7 @@ baker creatives performance --days 14 --full # + ids, last-7-day figures, on
|
|
|
6184
6192
|
| `--days <n>` | Window ending today, 1-90 (default 30). |
|
|
6185
6193
|
| `--full` | Adds creative and image ids, result type, days live, last-7-day CTR and cost per result, and one row per ad. |
|
|
6186
6194
|
|
|
6187
|
-
Each creative reports `spend`, `impressions`, `ctr` (percent), `cpc`, `results`, `costPerResult`, `frequency`, a `fatigue` verdict (`fresh`, `fading`, `fatigued`, `dead`, `too_early`, `no_delivery`) with its reason, and `definition`: the angle, hook, persona, awareness, hypothesis and reference fields read from the local `src/creatives/<slug>/_definition.md`. `hints` name worn-out creatives, the angle with the lowest cost per result, and unreadable ad accounts.
|
|
6195
|
+
Each creative reports `spend`, `impressions`, `ctr` (percent), `cpc`, `results`, `costPerResult`, `frequency`, a `fatigue` verdict (`fresh`, `fading`, `fatigued`, `dead`, `too_early`, `no_delivery`) with its reason, and `definition`: the angle, hook, mechanism, dominantSlot, persona, awareness, hypothesis and reference fields read from the local `src/creatives/<slug>/_definition.md`. `hints` name worn-out creatives, the angle, mechanism and dominant slot with the lowest cost per result (each ranked once two or more values have results), and unreadable ad accounts.
|
|
6188
6196
|
|
|
6189
6197
|
### `baker creatives feedback [--slug <slug>] [--full]`
|
|
6190
6198
|
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
<meta name="viewport" content="width=__WIDTH__, height=__HEIGHT__" />
|
|
6
6
|
<script src="./gsap.min.js"></script>
|
|
7
7
|
<style>
|
|
8
|
-
/* The
|
|
9
|
-
so a single-weight file is used as it is instead of being
|
|
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). */
|
|
10
12
|
@font-face { font-family: 'AdHeadline'; src: url('./headline.font'); font-weight: 100 900; font-display: block; }
|
|
11
13
|
@font-face { font-family: 'AdBody'; src: url('./body.font'); font-weight: 100 900; font-display: block; }
|
|
12
14
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
@@ -14,22 +16,20 @@
|
|
|
14
16
|
#root { position: absolute; inset: 0; overflow: hidden; }
|
|
15
17
|
.plate { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }
|
|
16
18
|
.scrim { position: absolute; left: 0; right: 0; pointer-events: none; display: none; }
|
|
17
|
-
.
|
|
19
|
+
.group { position: absolute; display: flex; flex-direction: column; gap: 0; }
|
|
18
20
|
.block { font-family: 'AdBody', 'Helvetica Neue', Arial, sans-serif; overflow-wrap: break-word; text-wrap: pretty; }
|
|
19
21
|
.block.headline, .block.subheadline, .block.cta { font-family: 'AdHeadline', 'AdBody', 'Helvetica Neue', Arial, sans-serif; }
|
|
20
22
|
.block.headline { text-wrap: balance; letter-spacing: -0.01em; }
|
|
21
|
-
.
|
|
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); }
|
|
22
26
|
.logo { display: block; width: auto; object-fit: contain; }
|
|
23
27
|
</style>
|
|
24
28
|
</head>
|
|
25
29
|
<body>
|
|
26
30
|
<div id="root" data-composition-id="main" data-start="0" data-duration="1" data-width="__WIDTH__" data-height="__HEIGHT__">
|
|
27
|
-
<img class="plate" id="plate" src="./plate.png" alt="" />
|
|
28
31
|
<div class="scrim" id="scrim-top"></div>
|
|
29
32
|
<div class="scrim" id="scrim-bottom"></div>
|
|
30
|
-
<div class="band" id="band-top" data-role="copy-top"></div>
|
|
31
|
-
<div class="band" id="band-middle" data-role="copy-middle"></div>
|
|
32
|
-
<div class="band" id="band-bottom" data-role="copy-bottom"></div>
|
|
33
33
|
</div>
|
|
34
34
|
|
|
35
35
|
<script>
|
|
@@ -43,11 +43,31 @@
|
|
|
43
43
|
var box = { x: safe.left * W, y: safe.top * H, w: W * (1 - safe.left - safe.right), h: H * (1 - safe.top - safe.bottom) };
|
|
44
44
|
// One unit per percent of the shorter side, so type reads the same size on every format.
|
|
45
45
|
var U = Math.min(W, H) / 100;
|
|
46
|
+
// A text-only ad has no picture (`plate: null`): the copy sits on the brand's ground.
|
|
47
|
+
var hasPlate = FRAME.plate !== null;
|
|
48
|
+
var ground = AD.background || { color: "#111111", ink: "#ffffff" };
|
|
46
49
|
|
|
47
50
|
var SIZE = { x_large: 8.6, large: 6.6, medium: 4.4, small: 3.0 };
|
|
48
51
|
var WEIGHT = { bold: 800, semibold: 600, regular: 400 };
|
|
49
52
|
var CASE = { upper: "uppercase", title: "capitalize", sentence: "none" };
|
|
50
53
|
var MAX_SHARE = { top: 0.44, middle: 0.3, bottom: 0.36 };
|
|
54
|
+
var RADIUS = { pill: "999px", rounded: "0.35em", square: "0" };
|
|
55
|
+
|
|
56
|
+
// With no face staged, the text never names the ad's faces, so the browser never asks
|
|
57
|
+
// for a file that is not there (a 404 the pre-render check refuses).
|
|
58
|
+
var FALLBACK_FACE = "'Helvetica Neue', Arial, sans-serif";
|
|
59
|
+
var facesStaged = FRAME.fonts !== false;
|
|
60
|
+
|
|
61
|
+
if (hasPlate) {
|
|
62
|
+
var plate = document.createElement("img");
|
|
63
|
+
plate.className = "plate";
|
|
64
|
+
plate.id = "plate";
|
|
65
|
+
plate.src = "./plate.png";
|
|
66
|
+
plate.alt = "";
|
|
67
|
+
root.insertBefore(plate, root.firstChild);
|
|
68
|
+
} else {
|
|
69
|
+
root.style.background = ground.color;
|
|
70
|
+
}
|
|
51
71
|
|
|
52
72
|
function luma(hex) {
|
|
53
73
|
var m = /^#?([0-9a-f]{6})$/i.exec(hex || "");
|
|
@@ -56,18 +76,23 @@
|
|
|
56
76
|
return 0.2126 * ((v >> 16) & 255) + 0.7152 * ((v >> 8) & 255) + 0.0722 * (v & 255);
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
function alignSelf(align) {
|
|
80
|
+
return align === "left" ? "flex-start" : align === "right" ? "flex-end" : "center";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Every line is drawn in its group: the layout region the reference put it in (placed
|
|
84
|
+
// at that region's box on this format), or a band when the layout gave no position.
|
|
85
|
+
var groups = {};
|
|
86
|
+
var bands = { top: null, middle: null, bottom: null };
|
|
87
|
+
(AD.groups || []).forEach(function (g) {
|
|
88
|
+
groups[g.id] = g;
|
|
63
89
|
});
|
|
64
90
|
|
|
65
91
|
function bandEl(name) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
var el = bandEl(name);
|
|
92
|
+
if (bands[name]) return bands[name];
|
|
93
|
+
var el = document.createElement("div");
|
|
94
|
+
el.className = "group band";
|
|
95
|
+
el.dataset.band = name;
|
|
71
96
|
el.style.left = box.x + "px";
|
|
72
97
|
el.style.width = box.w + "px";
|
|
73
98
|
el.style.gap = 1.6 * U + "px";
|
|
@@ -77,80 +102,110 @@
|
|
|
77
102
|
el.style.top = box.y + box.h / 2 + "px";
|
|
78
103
|
el.style.transform = "translateY(-50%)";
|
|
79
104
|
}
|
|
105
|
+
el.dataset.max = String(box.h * MAX_SHARE[name]);
|
|
106
|
+
root.appendChild(el);
|
|
107
|
+
bands[name] = el;
|
|
108
|
+
return el;
|
|
80
109
|
}
|
|
81
110
|
|
|
82
|
-
|
|
83
|
-
|
|
111
|
+
var placed = {};
|
|
112
|
+
function containerFor(groupId, band) {
|
|
113
|
+
var g = groups[groupId];
|
|
114
|
+
var at = g && g.at && g.at[FRAME.format];
|
|
115
|
+
if (!at) return bandEl((g && g.band) || band || "top");
|
|
116
|
+
if (placed[groupId]) return placed[groupId];
|
|
117
|
+
var el = document.createElement("div");
|
|
118
|
+
el.className = "group placed";
|
|
119
|
+
el.style.left = (at.x[0] / 100) * W + "px";
|
|
120
|
+
el.style.width = ((at.x[1] - at.x[0]) / 100) * W + "px";
|
|
121
|
+
el.style.top = (at.y[0] / 100) * H + "px";
|
|
122
|
+
el.style.minHeight = ((at.y[1] - at.y[0]) / 100) * H + "px";
|
|
123
|
+
el.style.justifyContent = "center";
|
|
124
|
+
el.style.gap = 1.2 * U + "px";
|
|
125
|
+
// Copy rewritten for our brand runs longer than the source's: allow some growth
|
|
126
|
+
// past the region before the type shrinks.
|
|
127
|
+
el.dataset.max = String(((at.y[1] - at.y[0]) / 100) * H * 1.35);
|
|
128
|
+
root.appendChild(el);
|
|
129
|
+
placed[groupId] = el;
|
|
130
|
+
return el;
|
|
84
131
|
}
|
|
85
132
|
|
|
86
|
-
function addLogo(
|
|
133
|
+
function addLogo(logo) {
|
|
87
134
|
var img = document.createElement("img");
|
|
88
135
|
img.className = "logo";
|
|
89
136
|
img.src = "./logo.png";
|
|
90
137
|
img.alt = "";
|
|
91
138
|
img.style.height = 5.4 * U + "px";
|
|
92
139
|
img.style.maxWidth = box.w * 0.42 + "px";
|
|
93
|
-
img.style.alignSelf = alignSelf(align);
|
|
140
|
+
img.style.alignSelf = alignSelf(logo.align);
|
|
94
141
|
img.onerror = function () {
|
|
95
142
|
img.remove();
|
|
96
143
|
};
|
|
97
|
-
var el =
|
|
98
|
-
if (
|
|
144
|
+
var el = containerFor(logo.group, logo.band);
|
|
145
|
+
if (el.dataset.band === "bottom") el.appendChild(img);
|
|
99
146
|
else el.insertBefore(img, el.firstChild);
|
|
100
147
|
}
|
|
101
148
|
|
|
102
|
-
function addBlock(
|
|
149
|
+
function addBlock(b) {
|
|
103
150
|
var el = document.createElement("div");
|
|
104
151
|
el.className = "block " + b.role;
|
|
105
|
-
el.
|
|
106
|
-
var size = b.role === "fine_print" ? 2.4 :
|
|
152
|
+
if (!facesStaged) el.style.fontFamily = FALLBACK_FACE;
|
|
153
|
+
var size = b.role === "fine_print" ? 2.4 : SIZE[b.size] || SIZE.medium;
|
|
107
154
|
el.dataset.base = String(size);
|
|
108
155
|
el.style.fontSize = size * U + "px";
|
|
109
|
-
el.style.fontWeight = String(b.role === "cta" ? 700 :
|
|
156
|
+
el.style.fontWeight = String(WEIGHT[b.weight] || (b.role === "cta" ? 700 : 400));
|
|
110
157
|
el.style.lineHeight = b.role === "headline" ? "1.04" : "1.22";
|
|
111
158
|
el.style.textTransform = CASE[b.case] || "none";
|
|
112
159
|
el.style.textAlign = b.align || "center";
|
|
113
160
|
el.style.alignSelf = alignSelf(b.align);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
161
|
+
var t = b.treatment || { kind: b.role === "cta" && !AD.groups ? "button" : "plain" };
|
|
162
|
+
var ink = b.color || (hasPlate ? "#ffffff" : ground.ink);
|
|
163
|
+
var text = b.text;
|
|
164
|
+
if (t.kind === "arrow" && !/[→›»]\s*$/.test(text)) text = text + " →";
|
|
165
|
+
if (t.kind === "button" || t.kind === "sticker" || t.kind === "highlight") {
|
|
166
|
+
var fill = document.createElement("span");
|
|
167
|
+
fill.className = "fill " + t.kind;
|
|
168
|
+
fill.textContent = text;
|
|
169
|
+
fill.style.background = t.background || (AD.cta && AD.cta.background) || "#111111";
|
|
170
|
+
fill.style.color = t.color || (AD.cta && AD.cta.color) || "#ffffff";
|
|
171
|
+
fill.style.borderRadius = RADIUS[t.shape] || RADIUS.rounded;
|
|
172
|
+
el.appendChild(fill);
|
|
122
173
|
} else {
|
|
123
|
-
el.textContent =
|
|
124
|
-
var ink = b.color || "#ffffff";
|
|
174
|
+
el.textContent = text;
|
|
125
175
|
el.style.color = ink;
|
|
176
|
+
if (t.kind === "underline") {
|
|
177
|
+
el.style.textDecoration = "underline";
|
|
178
|
+
el.style.textDecorationThickness = "0.08em";
|
|
179
|
+
el.style.textUnderlineOffset = "0.18em";
|
|
180
|
+
}
|
|
126
181
|
var dark = (luma(ink) || 255) < 110;
|
|
127
|
-
el.style.textShadow = dark ? "0 1px 10px rgba(255,255,255,0.35)" : "0 2px 14px rgba(0,0,0,0.45)";
|
|
128
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)";
|
|
129
184
|
}
|
|
130
|
-
|
|
185
|
+
containerFor(b.group, b.band).appendChild(el);
|
|
131
186
|
}
|
|
132
187
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Shrink a
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
function fit(
|
|
145
|
-
var
|
|
146
|
-
var
|
|
188
|
+
var drawCopy = FRAME.draw_copy !== false;
|
|
189
|
+
(drawCopy ? AD.copy || [] : []).forEach(addBlock);
|
|
190
|
+
if (AD.logo) addLogo(AD.logo);
|
|
191
|
+
|
|
192
|
+
function containers() {
|
|
193
|
+
return Array.prototype.slice.call(root.querySelectorAll(".group"));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Shrink a group's type until it fits its room and no line runs past its edge. The
|
|
197
|
+
// copy is whatever prompt.json says today, so its length is not known when the
|
|
198
|
+
// layout is written.
|
|
199
|
+
function fit(el) {
|
|
200
|
+
var max = Number(el.dataset.max);
|
|
201
|
+
var width = el.getBoundingClientRect().width;
|
|
147
202
|
var scale = 1;
|
|
148
203
|
for (var i = 0; i < 40; i++) {
|
|
149
204
|
var tooTall = el.scrollHeight > max;
|
|
150
205
|
var tooWide = Array.prototype.some.call(el.children, function (c) {
|
|
151
|
-
return c.scrollWidth >
|
|
206
|
+
return c.scrollWidth > width + 1;
|
|
152
207
|
});
|
|
153
|
-
if (!tooTall && !tooWide) break;
|
|
208
|
+
if ((!tooTall && !tooWide) || scale < 0.45) break;
|
|
154
209
|
scale *= 0.94;
|
|
155
210
|
Array.prototype.forEach.call(el.querySelectorAll(".block"), function (c) {
|
|
156
211
|
c.style.fontSize = Number(c.dataset.base) * U * scale + "px";
|
|
@@ -158,15 +213,16 @@
|
|
|
158
213
|
}
|
|
159
214
|
}
|
|
160
215
|
|
|
161
|
-
// A soft scrim behind a band of light type (or a light one behind
|
|
162
|
-
// copy reads on any plate
|
|
216
|
+
// A soft scrim behind a band of light type over the picture (or a light one behind
|
|
217
|
+
// dark type), so copy with no position of its own reads on any plate. Copy placed
|
|
218
|
+
// by the layout sits on the calm area the plate kept for it.
|
|
163
219
|
function scrim(name) {
|
|
164
|
-
|
|
165
|
-
|
|
220
|
+
var el = bands[name];
|
|
221
|
+
if (!hasPlate || !el) return;
|
|
166
222
|
var s = document.getElementById("scrim-" + name);
|
|
167
|
-
var firstText = el.querySelector(".block
|
|
168
|
-
|
|
169
|
-
var tint = dark ? "255,255,255" : "0,0,0";
|
|
223
|
+
var firstText = el.querySelector(".block[data-dark]");
|
|
224
|
+
if (!firstText) return;
|
|
225
|
+
var tint = firstText.dataset.dark === "1" ? "255,255,255" : "0,0,0";
|
|
170
226
|
var rect = el.getBoundingClientRect();
|
|
171
227
|
var reach = name === "top" ? rect.bottom + 6 * U : H - rect.top + 6 * U;
|
|
172
228
|
s.style.display = "block";
|
|
@@ -187,7 +243,7 @@
|
|
|
187
243
|
|
|
188
244
|
var ready = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
|
|
189
245
|
ready.then(function () {
|
|
190
|
-
|
|
246
|
+
containers().forEach(fit);
|
|
191
247
|
["top", "bottom"].forEach(scrim);
|
|
192
248
|
root.setAttribute("data-ready", "1");
|
|
193
249
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "static-ad",
|
|
3
|
-
"title": "Static ad:
|
|
4
|
-
"description": "Written by `baker canvas scaffold-static-ad`, one copy per delivered format.
|
|
3
|
+
"title": "Static ad: the picture (or the brand colours) 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
5
|
"width": 1080,
|
|
6
6
|
"height": 1350,
|
|
7
7
|
"fps": 1,
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
"inputs": {
|
|
10
10
|
"plate": {
|
|
11
11
|
"kind": "image",
|
|
12
|
-
"required":
|
|
12
|
+
"required": false,
|
|
13
13
|
"staged_as": "plate.png",
|
|
14
|
-
"description": "The picture, with no text and no logo."
|
|
14
|
+
"description": "The picture, with no overlay text and no logo. Absent for a text-only ad."
|
|
15
15
|
},
|
|
16
16
|
"logo": {
|
|
17
17
|
"kind": "image",
|
|
18
18
|
"required": false,
|
|
19
19
|
"staged_as": "logo.png",
|
|
20
|
-
"description": "The brand
|
|
20
|
+
"description": "The brand logo file, wired only when the concept shows a logo."
|
|
21
21
|
},
|
|
22
22
|
"font_headline": {
|
|
23
23
|
"kind": "font",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"frame": {
|
|
48
48
|
"kind": "json",
|
|
49
49
|
"required": true,
|
|
50
|
-
"description": "This format
|
|
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
51
|
}
|
|
52
52
|
}
|
|
53
53
|
}
|