SuperAPI AI Directory

create-end-card-from-reference

Design & creation

View repository

Updated 10-15 21:54

About this skill

create-end-card-from-reference

Purpose

Generative image models (Nano Banana 2 included) are great at product flat-lays but unreliable at typography — ad-headline text gets mangled. The fix is to split the end-card into two layers:

  • Layer 1 (NB2): product flat-lay on a gradient background, NO text.
  • Layer 2 (HTML hyperframe): all typography, headline, labels, arrows, wordmark.

Both layers compose via Playwright Chromium render → ffmpeg scale-down. The visual language of the resulting end-card matches the supplied reference image (e.g. AMINU skincare's "massive sans headline + outlined word + handwritten micro-labels + product flat-lay" pattern).

This atom is the proven alternative to:

  • create-end-card-html — pure HTML, no AI imagery. Good for type-only cards; weak for lifestyle.
  • create-end-card-branded — auto-selected lifestyle / plain-hero / generated-bg modes. Less precise control over typography placement vs product arrangement.

When to use

  • Brand is lifestyle/beauty/CPG/fragrance with a Pinterest-coded aesthetic.
  • User provides (or you can source) a reference ad image with strong typographic + product-flatlay composition.
  • The end-card needs both: hero product imagery + bold ad typography that AI can't render reliably.
  • Output: 9:16 vertical, 3s mp4 with subtle animation (label/arrow staggered reveals, no zoom).

Skip if:

  • The end-card is type-only (no products) → use create-end-card-html.
  • The end-card needs photoreal motion/CGI → use create-end-card-branded with mode=generated-bg.
  • You don't have a reference image and no time to source one → use create-end-card-html or create-end-card-branded.

Inputs

Required:

  • N product PNGs (transparent or solid background). Touchland-style flat-button atomizer or any consumer SKU.
  • Reference image (jpg/png) showing the target aesthetic. Stored at <project>/source/references/<ref>.jpg.
  • Brand wordmark PNG (high-res, transparent). If not provided, fetch from brand's CDN (e.g. touchland.com/cdn/shop/files/Touchland_Logo__1.png).
  • Headline copy (string, ≤5 words). E.g. "WHICH ONE ARE YOU?", "MATCH YOUR MOOD".
  • Outlined-word index — which word in the headline gets the hollow/outlined treatment (AMINU's signature). Usually the second word or the pivot word.
  • Micro-label per product — handwritten-style labels, one per SKU. E.g. "main character" / "soft girl" / "after dark".
  • Background gradient — two-stop vertical gradient (top color → bottom color). Default: warm blush-cream (#FFF1ED → #FCDDD8) for warm-leaning brands; soft sky-blue (#E8F0FA → #D6E5F5) for cool-leaning.
  • Output target dimensions (default 720×1280 to match MS clips; design at 1080×1920).
  • Output duration (default 3s for end-card overlay; can extend to 5-6s for standalone use).

Workflow

Step 1 — Source / verify reference image

If user-supplied, save to <project>/source/references/<ref>.jpg.

If sourcing fresh:

  • Pinterest search "skincare ad layout" / "GenZ product flat-lay" / "scent wardrobe"
  • Brand's competitor ads
  • Magazine ad scans (i-D, Glossier, Cosmetics Insider)

Look for: massive sans headline + product arrangement + handwritten/script micro-labels. The reference is the visual contract — your output should match its grammar.

Step 2 — Extract aesthetic specs from the reference

Document in <project>/working/endcard/notes.md:

  • Headline font weight + size relative to canvas (e.g. "Inter 900 at ~210px on 1920-tall canvas").
  • Outline treatment (stroke width, transparent fill — common: -webkit-text-stroke: 5px <ink>; color: transparent;).
  • Label font + size (handwritten: Caveat 500 ~84px is the AMINU-equivalent).
  • Background palette (sample with a color picker).
  • Product layout pattern (diagonal cascade, top-down flat-lay, horizontal row).
  • Wordmark placement (centered footer, top-left, bottom-right).

Step 3 — Generate Layer 1 (NB2 product flat-lay)

higgsfield generate create nano_banana_2 \
  --prompt "<product flat-lay description per template below>" \
  --aspect_ratio "9:16" \
  --resolution "2k" \
  --image <product1.jpg> --image <product2.jpg> ... \
  --wait --wait-timeout 5m \
  --json > layer1_result.json

Prompt template (no text):

Editorial top-down flat-lay product photograph of <N> <brand> <product family>
arranged casually on a <background description>. Bottle 1 with <color1 description>
tilted at a 20-degree angle in the <position1>, bottle 2 with <color2 description>
standing upright in the <position2>, ... [continue per product]. The <N> bottles
overlap slightly but each is clearly visible, with soft diffused shadows pooling
beneath them on the <surface>. All bottles have <shared physical features> and the
'<wordmark>' plus '<sub-line>' text on each label exactly matching the imported
product references. Pinterest-style flat-lay, editorial product photography,
soft natural overhead daylight, clean and uncluttered, lots of negative space at
the top of the frame. No text overlays, no typography, no captions, no logos
other than what is printed on the bottles.

Critical phrasing:

  • Pinterest-style flat-lay, editorial product photography — invokes the visual grammar.
  • clean and uncluttered, lots of negative space at the top of the frame — leaves room for the typography overlay.
  • No text overlays, no typography, no captions — keeps NB2 out of the text business.

Cost: ~$0.08 per attempt. Budget 2-3 rerolls if first attempt has wrong angles or label drift. Validated on Touchland Trio 1: NB2 nailed the 3-bottle composite on the first try.

Download the resulting PNG to <project>/working/endcard/layer1.png.

Step 4 — Build Layer 2 (HTML hyperframe)

Create <project>/working/endcard/endcard.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800;900&family=Caveat:wght@500;700&display=swap" rel="stylesheet">
<style>
  :root {
    --ink: #1a1a1a;
    --ink-soft: rgba(26,26,26,0.65);
    --bg-top: #FFF1ED;     /* per brand */
    --bg-bot: #FCDDD8;
  }
  * { box-sizing: border-box; }
  html, body {
    margin: 0; padding: 0;
    width: 1080px; height: 1920px;     /* DESIGN dimensions, not output */
    background: linear-gradient(180deg, var(--bg-top) 0%, var(--bg-bot) 100%);
    font-family: 'Inter', sans-serif;
    color: var(--ink);
    overflow: hidden;
  }

  .product-layer {
    position: absolute; inset: 0;
    z-index: 1;
    background-image: url('layer1.png');     /* relative path */
    background-size: cover;
    background-position: center;
  }

  .type-layer { position: absolute; inset: 0; z-index: 2; }
  .arrows-layer { position: absolute; inset: 0; z-index: 3; pointer-events: none; }

  .headline {
    position: absolute;
    top: 80px; left: 80px; right: 80px;
    font-size: 210px; font-weight: 900;
    line-height: 0.88; letter-spacing: -6px;
    text-transform: uppercase;
    opacity: 0;
    transform: translateY(30px);
    animation: headlineIn 0.5s ease-out 0.2s forwards;
  }
  .headline .outline {
    -webkit-text-stroke: 5px var(--ink);
    color: transparent;
  }

  .label {
    position: absolute;
    font-family: 'Caveat', cursive;
    font-size: 84px; font-weight: 500;
    color: var(--ink);
    white-space: nowrap;
    opacity: 0;
    animation: fadeIn 0.3s ease-out forwards;
  }
  /* per-label position + rotation + staggered animation-delay
     Position each label so its END (for left-anchored) or START (for right-anchored)
     points TOWARD its target bottle. */

  .arrow-path {
    fill: none;
    stroke: var(--ink);
    stroke-width: 7;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-dasharray: 100;
    stroke-dashoffset: 100;
  }
  /* per-arrow animation-delay matching its label */

  .footer {
    position: absolute;
    bottom: 90px; left: 0; right: 0;
    text-align: center;
    opacity: 0;
    transform: translateY(20px);
    animation: footerIn 0.5s ease-out 1.85s forwards;
  }
  .brand-logo {
    display: block; margin: 0 auto 14px;
    height: 96px; width: auto;
  }
  .url {
    font-size: 28px; font-weight: 600;
    letter-spacing: 5px;
    color: var(--ink-soft);
    text-transform: uppercase;
  }

  @keyframes headlineIn { to { opacity: 1; transform: translateY(0); } }
  @keyframes fadeIn { to { opacity: 1; } }
  @keyframes draw { to { stroke-dashoffset: 0; } }
  @keyframes footerIn { to { opacity: 1; transform: translateY(0); } }
</style>
</head>
<body>
  <div class="product-layer"></div>

  <div class="type-layer">
    <div class="headline">
      <!-- Insert each word; wrap the outlined word in <span class="outline"> -->
      WORD1<br>
      <span class="outline">WORD2</span> WORD3<br>
      WORD4?
    </div>
    <!-- One <div class="label label-<sku>"> per product, positioned + rotated to point at its bottle -->
    <div class="footer">
      <img src="brand_logo.png" class="brand-logo" alt="<brand>" />
      <div class="url">brand.com</div>
    </div>
  </div>

  <svg class="arrows-layer" viewBox="0 0 1080 1920" preserveAspectRatio="xMidYMid meet">
    <defs>
      <marker id="arrowhead" viewBox="0 0 12 12" refX="10" refY="6"
              markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <path d="M 0 0 L 12 6 L 0 12 z" fill="#1a1a1a" />
      </marker>
    </defs>
    <!-- One <path class="arrow-path arrow-<sku>"> per SKU, curving from label-end to bottle-edge. -->
    <!-- Each gets a unique animation-delay. -->
  </svg>
</body>
</html>

Animation timeline (3s total)

t event
0.0–0.2s Static base (product layer visible)
0.2–0.7s Headline rises into place
0.75 / 1.05 / 1.35s Labels fade in (staggered ~0.3s apart)
0.95 / 1.25 / 1.55s Arrows draw via stroke-dashoffset (one per label)
1.85–2.35s Footer (wordmark + URL) rises into place
2.35–3.0s Hold final state

Arrow positioning (critical detail)

Each arrow's <path d="..."> is a Q-curve from "just after the label text" to "just outside the bottle's edge":

M <label_end_x> <label_end_y> Q <control_x> <control_y> <bottle_edge_x> <bottle_edge_y>

The arrowhead (marker-end) renders AT the path's endpoint. To avoid the arrowhead overriding the bottle:

  • For a left-side label pointing UP-RIGHT to a top-right bottle: endpoint should be ~15-30px LEFT of the bottle's left edge.
  • For a right-side label pointing DOWN-LEFT to a middle bottle: endpoint should be ~15-30px RIGHT of the bottle's right edge.

Validate by rendering the final frame and inspecting each arrow visually. If an arrowhead sits ON or IN a bottle, shorten the path's endpoint by 30-50px.

Step 5 — Render Layer 1+2 composite

CRITICAL: Render at DESIGN dimensions (1080×1920), then scale down. Do NOT render at output dimensions — Playwright will clip the right 33% of the canvas.

# Render at design dimensions
python3 skills/atoms/end-cards/create-end-card-html/scripts/render_hyperframe.py \
  endcard.html endcard_3s_1080.mp4 3 \
  --fps 24 --width 1080 --height 1920

# Scale to output dimensions
ffmpeg -y -i endcard_3s_1080.mp4 \
  -vf "scale=720:1280:flags=lanczos" \
  -c:v libx264 -pix_fmt yuv420p -crf 18 -preset medium -an \
  endcard_3s.mp4

Step 6 — Verify the final frame

ffmpeg -y -i endcard_3s.mp4 -ss 2.7 -frames:v 1 endcard_final.jpg

Read endcard_final.jpg and check:

  • Headline fully visible (not clipped right edge)
  • Outlined word renders as hollow (not solid black, not invisible)
  • Each label sits near (not overlapping) its target bottle
  • Each arrow tip lands just OUTSIDE its target bottle, not inside the bottle's geometry
  • Brand wordmark renders at the bottom (not clipped, not the wrong logo)
  • Background gradient renders smoothly (no banding)
  • All elements present (compare to count: 1 headline + N labels + N arrows + wordmark + URL)

If any check fails, fix the HTML/CSS and re-render. Do NOT re-render NB2 unless the product layer itself is wrong.

Output

<project>/working/endcard/
├── endcard.html              ← HTML source
├── layer1.png                ← NB2 product flat-lay
├── brand_logo.png            ← official brand wordmark
├── reference.jpg             ← (optional) reference image used
├── endcard_3s_1080.mp4       ← intermediate 1080×1920 render
├── endcard_3s.mp4            ← final 720×1280 mp4 (use in master)
└── endcard_final.jpg         ← QC still

Quality Checks

  • endcard_3s.mp4 exists, has duration ~3.0s ±0.05s, dimensions match target.
  • endcard_final.jpg shows all required elements (headline, N labels, N arrows, wordmark, URL).
  • No element clipped by the canvas edge.
  • No arrow tip inside any product geometry.
  • Animation timeline plays smoothly (no z-fighting, no janky transforms).
  • Brand wordmark is the OFFICIAL wordmark (sourced from brand's CDN), not text-rendered approximation.

Failure Modes

  • Headline clipped on right edge. Root cause: rendered at output dimensions instead of design dimensions. Fix: render at 1080×1920, scale down via ffmpeg.
  • Arrowhead inside bottle. Path endpoint too far into the canvas. Shorten endpoint by 30-50px.
  • Label overlapping bottle. Reposition the label outside the bottle's bounding box.
  • NB2 output has text on the bottles. Prompt is ambiguous — ensure "No text overlays, no typography, no captions" is in the prompt.
  • NB2 bottles in wrong positions. Re-roll NB2 with more explicit per-bottle position instructions.
  • Font not loading. Google Fonts is the standard source — ensure the <link> tag is present and Playwright has network access at render time. Wait 500-800ms after page load for fonts to settle.
  • Animation not firing in screencast. render_hyperframe.py walks document.timeline.currentTime. CSS keyframe animations with explicit animation-delay should work. If not firing, capture in real-time mode (page.wait_for_timeout(<total_anim_duration_ms>)).
  • Brand wordmark looks wrong / pixelated. Source from brand's CDN at highest available resolution; ensure transparent PNG.

Cost + time

Item Cost Wall time
NB2 product layer ~$0.08 (1-3 rerolls) 2-9 min
HTML scaffolding free 15-30 min (first time); 5 min (template reuse)
Render at 1080×1920 + scale free 30 sec
Frame QC free 1-2 min
Total ~$0.08-0.24 ~25 min first time, ~10 min subsequent

References

  • Validated 2026-05-20 on Touchland Trio 1 (AMINU skincare ad as reference, 3-bottle composite, "WHICH ONE ARE YOU?" headline with "ONE" outlined, handwritten labels "main character" / "soft girl" / "after dark").
  • Reference image saved: touchland/Could skincare BE any simpler_.jpeg.
  • Per-project case study: touchland/video-03-third-place-tour/working/endcard/.
  • Related atoms:
    • create-end-card-html (the underlying renderer script)
    • create-end-card-branded (alternative for non-reference workflows)
    • create-end-card-hyperframes (motion-graphics variant)
  • Related memories:
    • feedback_endcard_reference_pattern.md
    • feedback_render_at_design_dim.md