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.
Pip ad with the model holding PRESS and the headline "Tone. Texture. PRESS."LIDO ad: "Vanilla. Salt. Sun." beside a raised Salted Vanilla can and the LIDO cap.Nula ad: "Blend in. Tune out." beside the profile portrait.JACI ad: the street-corner frame set inside a pink border under the JACI masthead.
One ad from each set: Pip, LIDO, Nula and JACI, all at 1080 × 1350.

How a set is made

  1. 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.

  2. 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.

  3. 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
  4. 04

    Source stills

    Chosen from approved finals. No new photography was generated for the LIDO or JACI sets.

  5. 05

    Render in code

    Layouts are SVG or HTML, rendered to exact-size PNGs. Photographs are placed, scaled and cropped, never repainted.

  6. 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
  7. 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.

  8. 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.

AdWhat it testsHeadlineBuilt from
BLACK product heroTasteDeep cocoa. Clean finish.The studio pack shot, cut out
Long lunchThe occasionLet lunch run long.Lifestyle still, full bleed
Type onlyBrand voiceCoffee without the rush.Type and the three flavour colours
Café ticketA native social formatStay for coffee.A vector ticket, no photography
The rangeChoiceThree 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".

OptionComprehensionProduct connectionPullDecision
Deep cocoa. Clean finish.555Selected. Exact product language and a two-beat hierarchy.
Cold brew, cut clean.444Strong, but "cut clean" is less natural customer language.
Black. Slow-steeped.453Accurate, 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.

  1. First version of the JACI wall ad: a serif JACI masthead over a thin rule, the line "The city, in bloom." and a small "Street Bloom / JACI" label.
    Version 1
  2. Second version of the JACI wall ad: the same photograph with only the JACI name in a heavy sans serif.
    Version 2
  3. JACI SS26 ad: full-bleed frame against a white wall with the JACI wordmark and SS26.
    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.

Cutting out a can without repainting it

scripts/extract-lido-black-cutout.py (excerpt)
# 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

tools/qa_ads.py (excerpt)
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

CampaignAdsFormatsRendered withChecks
Pip84:5SVG masters, rendered with sharpHierarchy, mobile readability, brand cohesion, unsupported claims and product accuracy
LIDO74:5SVG masters with embedded fonts, rendered with sharpExact dimensions, a 288 × 360 contact sheet, text clear of labels, matching checksums on a repeat render
Nula114:5, 1:1, 9:16HTML rendered in Chromium with Playwright; one Notes-style ad generated with GPT ImageA contrast script run against the pixels behind each line of text
JACI SS2684:5SVG masters, rendered with sharpPNG 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.