render-mosaic-grid-reveal
Render a 'mosaic-grid-reveal' video from a config — a real-DOM FULL-BLEED N×N mosaic of real product tiles that pops in one tile at a time (scatter order, ease-out-back overshoot), the grid clears, then the brand wordmark builds line-by-line followed by a sub-label, tagline, and
Install
npx skills add https://github.com/gooseworks-ai/goose-skills/tree/main/skills/ads/capabilities/render-mosaic-grid-reveal
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install gooseworks-ai-goose-skills@llmmart
git clone https://github.com/gooseworks-ai/goose-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole gooseworks-ai/goose-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
render-mosaic-grid-reveal
Render the mosaic-grid-reveal format from a config. The signature of this format is a
full-bleed N×N mosaic (default 3×3) of the brand's real product stills that pops in
one tile at a time (scatter order, ease-out-back "pop"), building the whole grid; the grid
then clears (scale-up + fade) and a clean end card builds line-by-line — the brand's
real wordmark, then a small sub-label, then the tagline, then a CTA. Silent by design (a
light music bed is muxed separately via create-music-elevenlabs). The hook is RANGE — nine
maximally-distinct variants show how much choice the brand offers.
Everything is a real-DOM HTML scene frame-stepped to PNG via Playwright and encoded with FFmpeg, so the wordmark, tile captions, and CTA stay pixel-crisp (a video model would smear them). No generative video, no i2v, no AI-rendered text. This capability is FREE and deterministic — the only paid step of the format (the music bed) is a separate media cap.
Inputs (config.json)
width,height,fps— canvas + frame rate (default 1080×1920, 30).wordmark— brand logo SVG (full<svg>or bare<path>markup) or PNG path. Real DOM, recolored viapalette.ink. Never AI-render it.wordmark_viewbox,wordmark_widthtune it.palette—bg(warm off-white),accent+accent_deep(brand color),ink,cap.grid—cols/rows(default 3×3),inset(outer margin),gap.tiles— one per cell (length must equalcols*rows):image(real variant still),name(caption),bg(soft pastel echoing the variant),ink(deep caption color).pop_order— scatter order across cells (default corners → center → edges).timing—grid_in0,cadence,pop,hold,clear(seconds).end_card—sub,tagline_top,tagline_bottom(bold),cta,url. Brand's own approved copy only.eyebrow— optional top kicker held during the grid.
scripts/config.example.json is a filled example (the Pair Eyewear "RANGE" worked example);
the tile image/wordmark paths are brand inputs bound by the orchestrator at remix time.
Run
python3 scripts/build_html.py --config config.json --out hyperframe.html # emits scene, writes duration_sec back
python3 scripts/render.py --config config.json --html hyperframe.html --out master-silent.mp4
build_html.py computes the full timeline and writes duration_sec back into the config so
render.py (which reads dims/fps/duration from the config) frame-steps window.renderAt(t)
at fps and FFmpeg-encodes the silent master. Then mux the create-music-elevenlabs bed:
ffmpeg -i master-silent.mp4 -i bed.wav -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -shortest out.mp4.
Rules
- FILL the frame — tiles are full-bleed and edge-to-edge, NOT small cards floating in an empty margin. Tiles pop in one at a time (scatter order), never all-at-once or static.
- Real wordmark + real product stills only. Never AI-render the logo or text.
- Copy slots are the brand's own approved lines — never invent claims, customers, results, prices, or shipping speed.
- Pick the 9 most DISTINCT variants (color-wheel spread) — that IS the RANGE hook.
Files (goose-skills)
-
scripts
-
build_html.py 8.8 KB
#!/usr/bin/env python3 """Build hyperframe.html for the `mosaic-grid-reveal` video format from a config.json. Mechanic: a FULL-BLEED N×N mosaic of real product tiles pops in one tile at a time (scatter order, ease-out-back overshoot), the grid holds then clears, and a clean brand end card builds line-by-line — wordmark → sub → tagline → CTA. Deterministic: window.renderAt(t) drives every element as a pure function of time (Playwright frame-step). Usage: build_html.py --config config.json [--out hyperframe.html] Side effect: writes the computed `duration_sec` back into the config so render.py reads the right length. Paths in the config (wordmark, tile images) are resolved relative to the current working directory. """ import argparse import json import pathlib import re def load_wordmark(path, default_viewbox): """Return (viewBox, inner_svg). Accepts a full <svg>…</svg> or bare <path> markup.""" raw = pathlib.Path(path).read_text() if "<svg" in raw: vb = re.search(r'viewBox="([^"]+)"', raw) inner = re.sub(r"(?s)^.*?<svg[^>]*>", "", raw) inner = re.sub(r"(?s)</svg>.*$", "", inner) return (vb.group(1) if vb else default_viewbox), inner.strip() return default_viewbox, raw.strip() def main(): ap = argparse.ArgumentParser() ap.add_argument("--config", required=True) ap.add_argument("--out", default="hyperframe.html") args = ap.parse_args() cfgp = pathlib.Path(args.config) cfg = json.loads(cfgp.read_text()) W = cfg.get("width", 1080) H = cfg.get("height", 1920) pal = cfg["palette"] grid = cfg.get("grid", {}) cols = grid.get("cols", 3) rows = grid.get("rows", 3) inset = grid.get("inset", 20) gap = grid.get("gap", 14) n = cols * rows tiles = cfg["tiles"] assert len(tiles) == n, f"need {n} tiles for a {cols}x{rows} grid, got {len(tiles)}" pop_order = cfg.get("pop_order") or list(range(n)) ec = cfg["end_card"] vb, wm_inner = load_wordmark(cfg["wordmark"], cfg.get("wordmark_viewbox", "0 0 170 92")) wm_w = cfg.get("wordmark_width", 430) eyebrow = cfg.get("eyebrow", "") # optional top kicker held during the grid # ---- timeline (seconds) — computed here so render.py gets the right duration_sec ---- tm = cfg.get("timing", {}) g0 = tm.get("grid_in0", 0.35) cad = tm.get("cadence", 0.42) pop = tm.get("pop", 0.44) hold = tm.get("hold", 0.80) clear = tm.get("clear", 0.50) last_pop = g0 + (n - 1) * cad HOLD_END = last_pop + pop + hold T = { "GRID_IN0": g0, "CAD": cad, "POP": pop, "CLEAR0": HOLD_END, "CLEAR1": HOLD_END + clear, "END_IN0": HOLD_END + 0.33, } T["WM0"] = T["END_IN0"] + 0.20; T["WM1"] = T["WM0"] + 0.45 T["EY0"] = T["WM0"] + 0.32; T["EY1"] = T["EY0"] + 0.40 T["TAG0"] = T["EY0"] + 0.28; T["TAG1"] = T["TAG0"] + 0.45 T["CTA0"] = T["TAG0"] + 0.42; T["CTA1"] = T["CTA0"] + 0.45 DURATION = round(T["CTA1"] + 1.75, 3) tile_divs = "\n".join( f'''<div class="tile" data-i="{i}" style="--tbg:{t['bg']};--tink:{t['ink']}"> <div class="frame"><img src="{t['image']}" alt=""></div> <div class="cap">{t['name']}</div> </div>''' for i, t in enumerate(tiles) ) eyebrow_div = f'<div id="eyebrow">{eyebrow}</div>' if eyebrow else "" HTML = f"""<!doctype html> <html><head><meta charset="utf-8"> <style> * {{ margin:0; padding:0; box-sizing:border-box; }} :root {{ --accent:{pal['accent']}; --accent-deep:{pal['accent_deep']}; --ink:{pal['ink']}; --bg:{pal['bg']}; --cap:{pal.get('cap', '#9A948A')}; }} html,body {{ width:{W}px; height:{H}px; }} body {{ background:var(--bg); font-family:-apple-system,'Helvetica Neue','Segoe UI',Arial,sans-serif; -webkit-font-smoothing:antialiased; overflow:hidden; }} #stage {{ position:relative; width:{W}px; height:{H}px; }} /* ---------- grid phase : FULL-BLEED mosaic ---------- */ #gridwrap {{ position:absolute; inset:0; }} #eyebrow {{ position:absolute; top:70px; left:0; right:0; text-align:center; font-size:30px; font-weight:600; letter-spacing:11px; color:var(--accent-deep); text-transform:uppercase; }} #grid {{ position:absolute; inset:{inset}px; display:grid; grid-template-columns:repeat({cols},1fr); grid-template-rows:repeat({rows},1fr); gap:{gap}px; transform-origin:center; }} .tile {{ position:relative; background:var(--tbg); border-radius:26px; overflow:hidden; display:flex; flex-direction:column; align-items:center; justify-content:center; will-change:transform,opacity; }} .tile .frame {{ width:94%; height:52%; display:flex; align-items:center; justify-content:center; }} .tile .frame img {{ max-width:100%; max-height:100%; object-fit:contain; filter:drop-shadow(0 10px 14px rgba(0,0,0,.16)); }} .tile .cap {{ position:absolute; bottom:34px; font-size:25px; font-weight:700; letter-spacing:.4px; color:var(--tink); opacity:.92; }} /* ---------- end card ---------- */ #end {{ position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; opacity:0; }} #wordmark {{ color:var(--ink); width:{wm_w}px; }} #wordmark svg {{ width:100%; height:auto; display:block; }} #sub {{ margin-top:20px; font-size:34px; font-weight:600; letter-spacing:17px; color:var(--accent-deep); text-transform:uppercase; }} #tagline {{ margin-top:70px; font-size:41px; font-weight:400; letter-spacing:.3px; color:var(--ink); text-align:center; }} #tagline b {{ font-weight:700; }} #cta {{ margin-top:78px; display:flex; flex-direction:column; align-items:center; }} #shop {{ background:var(--accent); color:#12210F; font-size:34px; font-weight:700; letter-spacing:.5px; padding:26px 66px; border-radius:100px; box-shadow:0 10px 24px rgba(0,0,0,.16); }} #url {{ margin-top:26px; font-size:28px; font-weight:500; letter-spacing:3px; color:var(--cap); }} </style></head> <body> <div id="stage"> <div id="gridwrap"> {eyebrow_div} <div id="grid"> {tile_divs} </div> </div> <div id="end"> <div id="wordmark"><svg viewBox="{vb}" fill="currentColor" xmlns="http://www.w3.org/2000/svg">{wm_inner}</svg></div> <div id="sub">{ec.get('sub','')}</div> <div id="tagline">{ec.get('tagline_top','')}<br><b>{ec.get('tagline_bottom','')}</b></div> <div id="cta"> <div id="shop">{ec.get('cta','Shop Now')}</div> <div id="url">{ec.get('url','')}</div> </div> </div> </div> <script src="_shared.js"></script> <script> const POP_ORDER = {json.dumps(pop_order)}; const T = {json.dumps({k: round(v, 3) for k, v in T.items()})}; const tiles = [...document.querySelectorAll('.tile')]; const popRank = {{}}; POP_ORDER.forEach((cell, seq) => popRank[cell] = seq); const grid = document.getElementById('grid'); const gridwrap = document.getElementById('gridwrap'); const eyebrow = document.getElementById('eyebrow'); const end = document.getElementById('end'); const wm = document.getElementById('wordmark'); const sub = document.getElementById('sub'); const tag = document.getElementById('tagline'); const cta = document.getElementById('cta'); function fadeUp(el, p, dist){{ el.style.opacity=p; el.style.transform=`translateY(${{(1-p)*dist}}px)`; }} function render(t){{ if (eyebrow) {{ let eb = clamp01(tw(t,0.12,0.55)); if (t>T.CLEAR0) eb *= (1-clamp01(tw(t,T.CLEAR0,T.CLEAR1))); eyebrow.style.opacity = eb; }} // per-tile pop tiles.forEach((tile,cell)=>{{ const seq = popRank[cell]; const t0 = T.GRID_IN0 + seq*T.CAD; const p = clamp01(tw(t, t0, t0+T.POP)); const s = p<=0 ? 0.0 : easeOutBack(p, 1.15); tile.style.opacity = clamp01(tw(t, t0, t0+T.POP*0.45)); tile.style.transform = `scale(${{s}})`; }}); // grid clears const clr = clamp01(tw(t, T.CLEAR0, T.CLEAR1)); gridwrap.style.opacity = 1 - clr; grid.style.transform = `scale(${{1 + 0.06*easeOut(clr)}})`; // end card build end.style.opacity = clamp01(tw(t, T.END_IN0, T.END_IN0+0.45)); const wp = clamp01(tw(t, T.WM0, T.WM1)); wm.style.opacity = wp; wm.style.transform = `scale(${{0.86 + 0.14*easeOut(wp)}})`; fadeUp(sub, clamp01(tw(t, T.EY0, T.EY1)), 16); fadeUp(tag, clamp01(tw(t, T.TAG0, T.TAG1)), 24); fadeUp(cta, clamp01(tw(t, T.CTA0, T.CTA1)), 26); }} window.__DURATION = {DURATION}; initRenderer({DURATION}, render); </script> </body></html> """ pathlib.Path(args.out).write_text(HTML) cfg["duration_sec"] = DURATION cfgp.write_text(json.dumps(cfg, indent=2)) print(f"wrote {args.out} (duration_sec={DURATION}, {n} tiles, {cols}x{rows})") if __name__ == "__main__": main() -
config.example.json 2.5 KB
{ "_comment": "Pair Eyewear 'RANGE' mosaic-grid-reveal — the worked example. Copy to config.json (in a dir with an assets/ folder) and edit. Canvas 1080x1920 9:16. FULL-BLEED 3x3 mosaic of REAL product tiles pops in one at a time (scatter order), clears, then the REAL brand wordmark builds line-by-line + tagline + CTA. Real DOM text/SVG only — never AI-render the wordmark or copy. Copy slots = the brand's OWN approved lines; never invent claims/customers/results.", "brand_name": "Pair Eyewear", "width": 1080, "height": 1920, "fps": 30, "wordmark": "assets/pair-wordmark.svg", "wordmark_viewbox": "0 0 170 92", "wordmark_width": 430, "palette": { "bg": "#F4EFE6", "accent": "#92BA8F", "accent_deep": "#5F8A5C", "ink": "#1D1D1B", "cap": "#9A948A" }, "grid": { "cols": 3, "rows": 3, "inset": 20, "gap": 14 }, "pop_order": [0, 8, 2, 6, 4, 1, 7, 3, 5], "timing": { "grid_in0": 0.35, "cadence": 0.42, "pop": 0.44, "hold": 0.80, "clear": 0.50 }, "tiles": [ { "image": "assets/1-aqua-shimmer.png", "name": "Aqua Shimmer", "bg": "#CFE8E5", "ink": "#2E6B65" }, { "image": "assets/2-rose-shimmer.png", "name": "Rose Shimmer", "bg": "#F1D8DE", "ink": "#9A4A5A" }, { "image": "assets/3-forest-tortoise.png", "name": "Forest Tortoise", "bg": "#E6DAC4", "ink": "#6B5A38" }, { "image": "assets/4-cobalt-check.png", "name": "Cobalt Check", "bg": "#D2DDF1", "ink": "#37507F" }, { "image": "assets/5-cherries.png", "name": "Cherries", "bg": "#F0DEDE", "ink": "#9B4340" }, { "image": "assets/6-watermelon.png", "name": "Watermelon", "bg": "#D7E9D6", "ink": "#3B7A4A" }, { "image": "assets/7-butterflies.png", "name": "Beautiful Butterflies", "bg": "#E3DAED", "ink": "#5B4A82" }, { "image": "assets/8-pride.png", "name": "Traditional Pride", "bg": "#ECE5D6", "ink": "#6E6448" }, { "image": "assets/9-sunset-palms.png", "name": "Sunset Palms", "bg": "#F3DEC8", "ink": "#9A6437" } ], "end_card": { "sub": "Eyewear", "tagline_top": "One Pair.", "tagline_bottom": "Infinite Possibilities.", "cta": "Shop Now", "url": "paireyewear.com" }, "music": { "prompt": "Light, upbeat, modern lifestyle bed for a short fashion-eyewear ad. Bright plucky marimba and soft synth mallets, gentle claps, warm and playful, clean and minimal, no vocals, no lyrics, positive and friendly, quick tempo but relaxed, boutique DTC brand feel. Kicks in immediately with no long intro.", "length_ms": 11000 } } -
render.py 2.5 KB
#!/usr/bin/env python3 """Frame-step a promo-card hyperframe.html → silent mp4 (Playwright + ffmpeg). Pure-function-of-time: calls window.renderAt(t) per frame and screenshots — fully deterministic, no paid calls. Reads dims/fps/duration from config.json. Usage: render.py --config config.json --html <run>/hyperframe.html --out <run>/master-silent.mp4 Fallback (no Playwright): screenshot the HTML at N frame times via the chrome-devtools MCP (evaluate window.renderAt(t) → screenshot) and encode with the ffmpeg block below. """ import argparse import asyncio import json import pathlib import shutil import subprocess from playwright.async_api import async_playwright async def run(cfg, html_path, out_path): fps = cfg.get("fps", 25) dur = cfg.get("duration_sec", 10.0) W, H = cfg.get("width", 1080), cfg.get("height", 1920) scratch = out_path.parent / "_scratch_frames" if scratch.exists(): shutil.rmtree(scratch) scratch.mkdir(parents=True) async with async_playwright() as p: b = await p.chromium.launch(args=["--autoplay-policy=no-user-gesture-required"]) ctx = await b.new_context(viewport={"width": W, "height": H}, device_scale_factor=1) page = await ctx.new_page() await page.goto(html_path.resolve().as_uri()) await page.wait_for_function("window.__driverReady === true", timeout=15000) await page.evaluate("document.fonts.ready") n = round(dur * fps) for i in range(n): t = i / fps await page.evaluate(f"window.renderAt({t})") await page.evaluate("new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))") await page.screenshot(path=str(scratch / f"frame_{i:04d}.png"), clip={"x": 0, "y": 0, "width": W, "height": H}) await b.close() subprocess.run(["ffmpeg", "-y", "-framerate", str(fps), "-i", str(scratch / "frame_%04d.png"), "-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "18", "-r", str(fps), "-movflags", "+faststart", str(out_path)], check=True) shutil.rmtree(scratch) print(f"[render] {out_path}") def main(): ap = argparse.ArgumentParser() ap.add_argument("--config", required=True) ap.add_argument("--html", required=True) ap.add_argument("--out", required=True) args = ap.parse_args() cfg = json.loads(pathlib.Path(args.config).read_text()) asyncio.run(run(cfg, pathlib.Path(args.html), pathlib.Path(args.out))) if __name__ == "__main__": main() -
_shared.js 950 B
// Deterministic frame renderer scaffold for pick-05. // Same contract as pick-01: initRenderer(duration_s, renderFn). Pure-function-of-time motion. (function () { let _duration = 0; let _renderFn = null; window.initRenderer = function (durationSeconds, renderFn) { _duration = durationSeconds; _renderFn = renderFn; renderFn(0); window.__driverReady = true; }; window.renderAt = function (t) { if (!_renderFn) return; if (t < 0) t = 0; if (t > _duration) t = _duration; _renderFn(t); }; window.clamp01 = function (x) { return Math.max(0, Math.min(1, x)); }; window.tw = function (t, t0, t1) { if (t <= t0) return 0; if (t >= t1) return 1; return (t - t0) / (t1 - t0); }; window.easeOut = function (x) { return 1 - Math.pow(1 - x, 3); }; window.easeOutBack = function (x, s) { s = s || 1.70158; return 1 + (s + 1) * Math.pow(x - 1, 3) + s * Math.pow(x - 1, 2); }; })();
-
-
tests
-
smoke-test.md 1.3 KB
# Smoke test — render-mosaic-grid-reveal Deterministic, no paid calls. Needs a `config.json` + its referenced assets (a wordmark SVG and `cols*rows` product stills). The runnable worked example (with real assets) lives in the content-goose molecule `one-shot-videos/create-mosaic-grid-reveal-video-from-refs/demo/`; `scripts/config.example.json` here documents the same schema. ```bash # from a dir holding config.json + assets/ + _shared.js: python3 scripts/build_html.py --config config.json --out hyperframe.html python3 scripts/render.py --config config.json --html hyperframe.html --out master-silent.mp4 ``` **Pass:** - `build_html.py` prints `wrote hyperframe.html (duration_sec=…, N tiles, RxC)`. - `render.py` writes `master-silent.mp4` at the config `width`×`height` and `fps`, duration within ±0.2 s of the written-back `duration_sec`. - A frame mid-build shows the full-bleed mosaic filling the frame with some tiles popped and others not yet; the final second shows the wordmark + sub + tagline + CTA end card. **Fail signatures:** - Tiles floating small/centered with empty margins → grid not full-bleed (`#grid{position:absolute;inset:20px}`, `repeat(N,1fr)`). - Blank wordmark → `wordmark` path wrong or the SVG doesn't use `fill="currentColor"`. - All tiles appear at once → `pop_order` / per-tile `renderAt` timing not wired.
-
-
SKILL.md 3.8 KB
--- name: render-mosaic-grid-reveal description: Render a 'mosaic-grid-reveal' video from a config — a real-DOM FULL-BLEED N×N mosaic of real product tiles that pops in one tile at a time (scatter order, ease-out-back overshoot), the grid clears, then the brand wordmark builds line-by-line followed by a sub-label, tagline, and CTA; frame-stepped via Playwright and encoded with FFmpeg — deterministic assembly, FREE (the music bed comes from create-music-elevenlabs), so the wordmark, tile captions, and CTA stay pixel-crisp. Use for the mosaic-grid-reveal format. status: active --- # render-mosaic-grid-reveal Render the `mosaic-grid-reveal` format from a config. The signature of this format is a **full-bleed N×N mosaic** (default 3×3) of the brand's real product stills that **pops in one tile at a time** (scatter order, ease-out-back "pop"), building the whole grid; the grid then **clears** (scale-up + fade) and a clean end card **builds line-by-line** — the brand's real wordmark, then a small sub-label, then the tagline, then a CTA. Silent by design (a light music bed is muxed separately via `create-music-elevenlabs`). The hook is RANGE — nine maximally-distinct variants show how much choice the brand offers. Everything is a real-DOM HTML scene **frame-stepped** to PNG via Playwright and encoded with FFmpeg, so the wordmark, tile captions, and CTA stay **pixel-crisp** (a video model would smear them). No generative video, no i2v, no AI-rendered text. This capability is **FREE and deterministic** — the only paid step of the format (the music bed) is a separate media cap. ## Inputs (`config.json`) - `width`, `height`, `fps` — canvas + frame rate (default 1080×1920, 30). - `wordmark` — brand logo SVG (full `<svg>` or bare `<path>` markup) or PNG path. Real DOM, recolored via `palette.ink`. Never AI-render it. `wordmark_viewbox`, `wordmark_width` tune it. - `palette` — `bg` (warm off-white), `accent` + `accent_deep` (brand color), `ink`, `cap`. - `grid` — `cols`/`rows` (default 3×3), `inset` (outer margin), `gap`. - `tiles` — one per cell (length must equal `cols*rows`): `image` (real variant still), `name` (caption), `bg` (soft pastel echoing the variant), `ink` (deep caption color). - `pop_order` — scatter order across cells (default corners → center → edges). - `timing` — `grid_in0`, `cadence`, `pop`, `hold`, `clear` (seconds). - `end_card` — `sub`, `tagline_top`, `tagline_bottom` (bold), `cta`, `url`. Brand's own approved copy only. - `eyebrow` — optional top kicker held during the grid. `scripts/config.example.json` is a filled example (the Pair Eyewear "RANGE" worked example); the tile `image`/`wordmark` paths are brand inputs bound by the orchestrator at remix time. ## Run ```bash python3 scripts/build_html.py --config config.json --out hyperframe.html # emits scene, writes duration_sec back python3 scripts/render.py --config config.json --html hyperframe.html --out master-silent.mp4 ``` `build_html.py` computes the full timeline and writes `duration_sec` back into the config so `render.py` (which reads dims/fps/duration from the config) frame-steps `window.renderAt(t)` at `fps` and FFmpeg-encodes the silent master. Then mux the `create-music-elevenlabs` bed: `ffmpeg -i master-silent.mp4 -i bed.wav -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -shortest out.mp4`. ## Rules - FILL the frame — tiles are full-bleed and edge-to-edge, NOT small cards floating in an empty margin. Tiles pop in one at a time (scatter order), never all-at-once or static. - Real wordmark + real product stills only. Never AI-render the logo or text. - Copy slots are the brand's own approved lines — never invent claims, customers, results, prices, or shipping speed. - Pick the 9 most DISTINCT variants (color-wheel spread) — that IS the RANGE hook. -
skill.meta.json 327 B
{ "slug": "render-mosaic-grid-reveal", "category": "capabilities", "domain": "ads", "tags": [ "ads" ], "installation": { "base_command": "npx goose-skills install render-mosaic-grid-reveal", "supports": [ "claude", "cursor", "codex" ] }, "requires_skills": [ "watch" ] }
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.