Campaigns · Self-initiated concepts
Static ads
Thirty-four ads for Pip, LIDO, Nula and JACI, made from approved stills. Each set starts from a brief, with one idea per frame. In all but one ad, the type is set in code rather than drawn by an image model, and the product photography is placed, not regenerated.
At a glance
Role
Art direction, concepts, copy and final selection. Built the ad renderers and checks with Claude Code and Codex.
Tools
- Node.js
- SVG
- sharp
- Python
- OpenCV
- Playwright
Outcomes
- Thirty-four ads across four campaigns, in 4:5, 1:1 and 9:16.
- Copy sticks to confirmed product facts. LIDO's "3 for $10" sticker is marked as a placeholder, and JACI's Sale versions are only for use during a real sale.
- Masters stay editable as SVG or HTML, and the LIDO notes record a repeat render matching its SVG and PNG checksums exactly.




How a set is made
-
01
Brief
Placement, format, the product facts that can be used and the claims that cannot. LIDO and JACI were briefed for the Meta feed at 1080 × 1350.
-
02
One idea per frame
Each ad gives a different reason to look. The first five LIDO ads test taste, an occasion, the brand voice, a café ticket and the range.
-
03
Copy against the facts
Headlines are checked against the confirmed product facts before layout. For LIDO and JACI, several lines per ad were compared and the reason for each choice written down.
Human approval -
04
Source stills
Chosen from approved finals. No new photography was generated for the LIDO or JACI sets.
-
05
Render in code
Layouts are SVG or HTML, rendered to exact-size PNGs. Photographs are placed, scaled and cropped, never repainted.
-
06
Feed-size review
LIDO and JACI were reviewed on contact sheets at feed-preview size. On LIDO this removed small support lines and a receipt footer, and enlarged the range CTA.
Human approval -
07
Checks
Pixel sizes read back from the files, type kept clear of faces and packaging, and for Nula, a script that measures contrast on the pixels behind every line of text.
-
08
Editable masters
SVG or HTML masters are kept beside the PNG exports, so any ad can be corrected and rendered again.
Human approval
Five ideas for LIDO
The LIDO set was planned as five different reasons to care, not five layouts of one idea.
| Ad | What it tests | Headline | Built from |
|---|---|---|---|
| BLACK product hero | Taste | Deep cocoa. Clean finish. | The studio pack shot, cut out |
| Long lunch | The occasion | Let lunch run long. | Lifestyle still, full bleed |
| Type only | Brand voice | Coffee without the rush. | Type and the three flavour colours |
| Café ticket | A native social format | Stay for coffee. | A vector ticket, no photography |
| The range | Choice | Three ways to take it cold. | The three-can family still |
The ads set their headlines in capitals. Two more lifestyle ads were added later from the same stills.





Choosing a headline
Options for the BLACK product hero, scored out of 5 in the copy notes. As the notes put it, the scores are "directional creative judgement, not performance predictions".
| Option | Comprehension | Product connection | Pull | Decision |
|---|---|---|---|---|
| Deep cocoa. Clean finish. | 5 | 5 | 5 | Selected. Exact product language and a two-beat hierarchy. |
| Cold brew, cut clean. | 4 | 4 | 4 | Strong, but "cut clean" is less natural customer language. |
| Black. Slow-steeped. | 4 | 5 | 3 | Accurate, but reads like the label and is less distinctive. |
The rules each set followed
- Pip
No text over packaging
One dominant idea per frame, generous negative space, and only approved product facts: no medical, clinical, review or dermatologist claims. Production labels such as "01 / WASH" never appear on finished ads.
- LIDO
Readable at thumbnail size
Headlines of three to seven words at 86–132 px on the 1080 × 1350 canvas, a 64 px safe edge, and a hierarchy that still reads at 324 × 405.
- JACI SS26
Few words, all factual
Only the brand and SS26 or Sale. Dense portraits, movement and detail shots sit in a pink frame so the type stays off the photograph; frames with open wall, sky or road run full bleed.
- Nula
Quiet in a loud category
Avenir Next for copy and a Futura wordmark that matches the mark on the case lid. Colours sampled from the product, and no shadows on cut-outs, no badges and no buttons.
One photograph, three versions
The same approved JACI frame across three rounds of the ad set.

Version 1 
Version 2 
Version 3
Version 1 carried a campaign line, an editorial serif and decorative rules. Version 2 removed every rule, panel, caption and micro-label, leaving one image and at most two text elements. Version 3 added SS26 and Sale markers and a pink frame for dense images, across 13 assets.
One system, three formats
The Nula set keeps one palette, one type pairing and one wordmark across story, square and feed placements.



Cutting out a can without repainting it
# The source is an approved exact packshot on a near-white sweep. Give
# GrabCut a narrow probable-foreground region and a conservative sure-
# foreground core, preserving source pixels and changing alpha only.
cv2.rectangle(mask, (245, 165), (780, 1385), cv2.GC_PR_FGD, -1)
cv2.rectangle(mask, (345, 285), (680, 1250), cv2.GC_FGD, -1)
…
cv2.grabCut(image, mask, None, background, foreground, 8, cv2.GC_INIT_WITH_MASK)
…
alpha = cv2.GaussianBlur(silhouette, (0, 0), 0.75)
alpha[alpha < 8] = 0
…
rgba = cv2.cvtColor(image, cv2.COLOR_BGR2BGRA)
rgba[:, :, 3] = alpha
The approved pack shot goes in and only the alpha channel is written, so the can's colour, label and proportions come out exactly as they went in.
Measuring contrast behind the type
pg.add_style_tag(content="h1,p,.mark{visibility:hidden!important}")
pg.wait_for_timeout(120)
bg_png=pg.screenshot()
…
for bx in boxes:
…
patch=bg[y0:y1,x0:x1].reshape(-1,3)
…
c=contrast(tl,pl)
# worst 2% of the box, so a small bright blowout is not averaged away
worst=float(np.percentile(c,2)); med=float(np.median(c))
need=3.0 if bx["size"]>=24 else 4.5
Each Nula ad is rendered once with its text hidden. The pixels behind every line are sampled, and the worst 2% is compared with WCAG contrast thresholds: 3:1 for text of 24 px and above, 4.5:1 below that. Anything under is flagged LOW.
Every set
| Campaign | Ads | Formats | Rendered with | Checks |
|---|---|---|---|---|
| Pip | 8 | 4:5 | SVG masters, rendered with sharp | Hierarchy, mobile readability, brand cohesion, unsupported claims and product accuracy |
| LIDO | 7 | 4:5 | SVG masters with embedded fonts, rendered with sharp | Exact dimensions, a 288 × 360 contact sheet, text clear of labels, matching checksums on a repeat render |
| Nula | 11 | 4:5, 1:1, 9:16 | HTML rendered in Chromium with Playwright; one Notes-style ad generated with GPT Image | A contrast script run against the pixels behind each line of text |
| JACI SS26 | 8 | 4:5 | SVG masters, rendered with sharp | PNG dimensions, a contact sheet of every asset, headlines clear of faces and garment details |
JACI's third version rendered 13 assets; eight made the final set.