Contact Me

GTM MOTION STYLE LIBRARY

Ten animation styles for GTM video, ready to paste into Claude

Each style has a copy-ready prompt that carries the code behind its preview, so what Claude builds matches what you see here. Every preview runs on made-up sample data for a common sales or marketing story, so swap in your own numbers and go.

How to use a style

  1. Pick a style

    Filter by the play you're running and watch the previews. Each one tells a different sample story, so you can see how the style handles that kind of message.
  2. Fill in the prompt

    Copy it, then add your message, numbers, brand colors and format at the top. Leave the reference code at the bottom as it is.
  3. Paste it into Claude

    Free, Pro and Max return an HTML artifact that plays in the browser. On Team or Enterprise, choose Motion as the output and the same prompt works there.
  4. Get the MP4

    In Motion, click Export and choose MP4. Anywhere else, open Record mode, start a screen recording of the stage (Cmd+Shift+5 on a Mac, or Loom) and press Space. It plays once and holds the last frame.

THE LIBRARY

Ten styles, ten sample stories

Pipeline reviews, webinar results, a lead handoff, a case study and more. Each preview uses sample data, and the prompt shows where your own goes.

  • 01 Data stories

    Boardroom clean

    Use it for Pipeline or QBR story

    Read the prompt
    Build a 15-second animated video in the "Boardroom clean" style: a white slide where a bar chart builds in, one bar is spotlighted, and three KPI tiles count up beside it.
    
    It's for a pipeline or QBR story: one chart that builds, then the three numbers that matter.
    
    MY CONTENT (fill these in; delete a line to keep the default)
    - What the video says, in one sentence: [e.g. "Inbound drove 45% of new pipeline this quarter"]
    - Numbers and labels: [a hero number plus 2 or 3 supporting numbers, each with a short label]
    - Source line: [where the numbers come from]
    - Brand colors: [hex codes for background, text and 2 or 3 accents]. Default: #6688ad, #545454, #292424, #7ba69d, #dc685a
    - Font: [font name]. Default: Poppins
    - Format: [16:9, 1:1 or 9:16]. Default: 16:9
    
    HOW TO BUILD IT
    1. Start from the reference code at the end of this message. It already works. Keep its structure: the CONFIG block, the fixed-size stage, build(), render(t) and the player at the bottom. Put my content, colors, font and format into CONFIG, then adjust the layout so everything fits.
    2. Every frame is a pure function of time: render(t) draws the frame for t seconds. Don't add CSS animations, transitions, setTimeout or setInterval. That's what lets the video be scrubbed, recorded cleanly and captured frame by frame.
    3. If you're in Claude Motion, make it a Motion animation and use the reference for the look and the timing. Anywhere else, return one self-contained HTML artifact with no external scripts. The Google Fonts link is the only outside request.
    4. Keep record mode working. The Record mode button (or the R key) hides every control and holds the first frame. Clicking the stage or pressing Space plays it once and holds the last frame. Esc exits.
    5. No sound. About 15 seconds, with the final frame held for at least 2 seconds.
    6. Text has to read on a phone. On the 1920x1080 stage nothing goes under 30px, labels are 40px or larger, and headline numbers 120px or larger. No text overlaps other text, runs off the stage, or leaves one word alone on the last line of a heading. If my content doesn't fit, cut words or add a scene. Don't shrink the type.
    7. Keep it to one chart and up to three KPI tiles. Spotlight the one bar that carries the message and dim the rest. The bar data lives in CONFIG.bars.
    
    Optional: before writing any code, give me a five-line storyboard (time, what's on screen, what moves) and wait for my OK.
    
    When it's done, tell me in two lines how to get an MP4: open Record mode, start a screen recording of the stage (Cmd+Shift+5 on a Mac, or Loom), then press Space.
    
    REFERENCE CODE: Boardroom clean
    ```html
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Boardroom clean</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap">
    <style>
    /* Player: fixed-size stage, scaled to fit the window. Keep as is. */
    html,body{margin:0;height:100%;background:#111;overflow:hidden}
    #stage{position:absolute;left:50%;top:50%;overflow:hidden;box-sizing:border-box;background:var(--bg);color:var(--ink);font-family:var(--font)}
    #stage *{box-sizing:border-box}
    #stage::after{content:"";position:absolute;inset:0;background:var(--bg);opacity:var(--fade,0);pointer-events:none;z-index:99}
    #ui{position:fixed;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px;align-items:center;padding:8px 12px;border-radius:10px;background:rgba(0,0,0,.7);font:13px system-ui,sans-serif;color:#eee;z-index:9}
    #ui button{font:inherit;padding:5px 10px;border:0;border-radius:6px;cursor:pointer}
    body.rec #ui{display:none} body.rec{cursor:none}
    
    /* Style */
    #stage{padding:90px 110px 70px;display:flex;flex-direction:column}
    .eyebrow{font-size:36px;font-weight:600;letter-spacing:.08em;color:var(--c1)}
    h1{margin:14px 0 0;font-size:72px;line-height:1.1;font-weight:600;text-wrap:balance}
    .body{flex:1;display:grid;grid-template-columns:1.25fr 1fr;grid-template-rows:minmax(0,1fr);gap:90px;margin-top:56px;min-height:0}
    .chart{display:grid;grid-template-rows:minmax(0,1fr) auto auto;min-height:0}
    .plot{display:flex;align-items:flex-end;gap:36px;border-bottom:4px solid var(--ink);position:relative}
    .col{flex:1;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center}
    .val{font-size:52px;font-weight:700;margin-bottom:12px}
    .bar{width:100%;background:var(--grey);border-radius:10px 10px 0 0}
    .col.hi .bar{background:var(--c1)} .col.hi .val{color:var(--c1)}
    .labels{display:flex;gap:36px;margin-top:16px}
    .labels div{flex:1;text-align:center;font-size:38px;font-weight:600;color:var(--muted)}
    .axis{font-size:36px;color:var(--muted);text-align:center;margin-top:8px}
    .callout{position:absolute;right:0;top:0;background:var(--ink);color:var(--bg);font-size:40px;font-weight:600;padding:16px 28px;border-radius:12px;white-space:nowrap}
    .kpis{display:flex;flex-direction:column;justify-content:center;gap:40px}
    .kpis h2{margin:0;font-size:44px;font-weight:600;color:var(--muted)}
    .kpi{display:flex;align-items:center;gap:32px;border-left:10px solid var(--c2);padding-left:32px}
    .kpi b{flex:none;width:270px;font-size:96px;line-height:1;font-weight:700}
    .kpi span{font-size:40px;line-height:1.25}
    .source{font-size:32px;color:var(--muted);margin-top:36px}
    .square .kpis{gap:22px} .square .kpi b{font-size:76px;width:220px}
    .tall .body,.square .body{grid-template-columns:1fr;grid-template-rows:minmax(0,1fr) auto;gap:70px}
    </style>
    </head>
    <body>
    <div id="stage">
    <div class="eyebrow" id="eyebrow"></div>
      <h1 id="title"></h1>
      <div class="body">
        <div class="chart">
          <div class="plot" id="plot"><div class="callout" id="callout"></div></div>
          <div class="labels" id="labels"></div>
          <div class="axis" id="axis"></div>
        </div>
        <div class="kpis" id="kpis"><h2 id="kpiHead"></h2></div>
      </div>
      <div class="source" id="source"></div>
    </div>
    <div id="ui"><button id="b-play">Play / Pause</button><button id="b-replay">Replay</button><input id="scrub" type="range" min="0" max="1000" value="0"><button id="b-rec" title="Hides the controls. Click the stage or press Space to play once. Esc exits.">Record mode</button></div>
    
    <script>
    /* ---------- Your content: edit here ---------- */
    const CONFIG = {
      format: "16:9",            // "16:9" | "1:1" | "9:16"
      duration: 15,              // seconds
      font: "Poppins",
      colors: { bg: "#ffffff", ink: "#292424", muted: "#545454", grey: "#d9dee6", c1: "#6688ad", c2: "#7ba69d", c3: "#dc685a" },
      eyebrow: "Q3 PIPELINE REVIEW",
      title: "Where new pipeline came from this quarter",
      axis: "New opportunities by source",
      bars: [ { label: "Inbound", value: 142, highlight: true }, { label: "Outbound", value: 96 },
              { label: "Partner", value: 48 }, { label: "Events", value: 31 } ],
      callout: "45% of new opportunities",
      kpiHead: "This quarter:",
      kpis: [ { value: 24, suffix: "%", text: "win rate on new opportunities" },
              { value: 38, prefix: "$", suffix: "K", text: "average deal size" },
              { value: 46, text: "days in the average sales cycle" } ],
      source: "Sample data",
    };
    
    /* ---------- Helpers ---------- */
    const $ = (id) => document.getElementById(id);
    const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
    const prog = (t, start, dur) => clamp((t - start) / dur);      // 0 to 1 between start and start+dur
    const ease = (x) => 1 - Math.pow(1 - x, 3);                     // ease-out cubic
    const show = (el, x, dy = 30) => { el.style.opacity = x; el.style.transform = `translateY(${(1 - x) * dy}px)`; };
    
    /* ---------- Build the DOM once from CONFIG ---------- */
    function build() {
      $("eyebrow").textContent = CONFIG.eyebrow; $("title").textContent = CONFIG.title;
      $("axis").textContent = CONFIG.axis; $("callout").textContent = CONFIG.callout;
      $("kpiHead").textContent = CONFIG.kpiHead; $("source").textContent = CONFIG.source;
      for (const b of CONFIG.bars) {
        $("plot").insertAdjacentHTML("beforeend", `<div class="col${b.highlight ? " hi" : ""}"><div class="val"></div><div class="bar"></div></div>`);
        $("labels").insertAdjacentHTML("beforeend", `<div>${b.label}</div>`);
      }
      for (const k of CONFIG.kpis) $("kpis").insertAdjacentHTML("beforeend", `<div class="kpi"><b></b><span>${k.text}</span></div>`);
    }
    
    /* ---------- Draw one frame for time t (seconds). Pure: same t, same frame. ---------- */
    function render(t) {
      show($("eyebrow"), ease(prog(t, 0.2, 0.8))); show($("title"), ease(prog(t, 0.4, 0.9)));
      const max = Math.max(...CONFIG.bars.map((b) => b.value));
      const cols = [...document.querySelectorAll(".col")];
      cols.forEach((c, i) => {
        const p = ease(prog(t, 1.6 + i * 0.35, 1.3));
        c.querySelector(".bar").style.height = (p * CONFIG.bars[i].value / max * 74) + "%";
        c.querySelector(".val").textContent = Math.round(p * CONFIG.bars[i].value);
        c.querySelector(".val").style.opacity = p > 0 ? 1 : 0;
        const dim = CONFIG.bars[i].highlight ? 0 : prog(t, 4.6, 0.6);  // spotlight the highlighted bar
        c.style.opacity = 1 - dim * 0.55;
      });
      show($("labels"), prog(t, 1.4, 0.6), 10); show($("axis"), prog(t, 1.6, 0.6), 10);
      const cp = ease(prog(t, 5.0, 0.7)); $("callout").style.opacity = cp;
      $("callout").style.transform = `scale(${0.85 + 0.15 * cp})`;
      show($("kpiHead"), ease(prog(t, 6.4, 0.7)));
      [...document.querySelectorAll(".kpi")].forEach((k, i) => {
        const p = ease(prog(t, 7.0 + i * 0.9, 0.8));
        k.style.opacity = p; k.style.transform = `translateX(${(1 - p) * 60}px)`;
        k.querySelector("b").textContent = (CONFIG.kpis[i].prefix || "") + Math.round(CONFIG.kpis[i].value * ease(prog(t, 7.0 + i * 0.9, 1.2))) + (CONFIG.kpis[i].suffix || "");
      });
      show($("source"), prog(t, 2.0, 0.8), 0);
    }
    
    /* ---------- Player: keep as is ---------- */
    const SIZES = { "16:9": [1920, 1080], "1:1": [1440, 1440], "9:16": [1080, 1920] };
    const [W, H] = SIZES[CONFIG.format] || SIZES["16:9"], D = CONFIG.duration;
    const stage = $("stage"), scrub = $("scrub");
    stage.style.width = W + "px"; stage.style.height = H + "px";
    stage.classList.toggle("tall", H > W); stage.classList.toggle("square", H === W);
    for (const [k, v] of Object.entries(CONFIG.colors)) stage.style.setProperty("--" + k, v);
    stage.style.setProperty("--font", `"${CONFIG.font}", system-ui, sans-serif`);
    let t = 0, playing = true, rec = false, start = performance.now();
    const fit = () => { stage.style.transform = `translate(-50%,-50%) scale(${Math.min(innerWidth / W, innerHeight / H)})`; };
    function draw(time) { t = time; render(t); stage.style.setProperty("--fade", rec ? 0 : clamp((t - D + 0.5) / 0.5)); scrub.value = t / D * 1000; }
    window.seek = (time) => { playing = false; draw(time); };   // frame-by-frame capture: seek(0), seek(1/30), ...
    function play(from = 0) { start = performance.now() - from * 1000; playing = true; }
    function tick(now) {
      if (playing) { let x = (now - start) / 1000; if (x >= D) { if (rec) { x = D; playing = false; } else { x = 0; play(0); } } draw(x); }
      requestAnimationFrame(tick);
    }
    function setRec(on) { rec = on; document.body.classList.toggle("rec", on); seek(0); }
    $("b-play").onclick = () => (playing ? (playing = false) : play(t >= D ? 0 : t));
    $("b-replay").onclick = () => play(0);
    $("b-rec").onclick = () => setRec(true);
    scrub.oninput = () => seek(scrub.value / 1000 * D);
    stage.onclick = () => { if (rec) play(0); };
    addEventListener("keydown", (e) => {
      if (e.code === "Space") { e.preventDefault(); if (rec) play(0); else if (playing) playing = false; else play(t >= D ? 0 : t); }
      if (e.key === "r" || e.key === "R") setRec(!rec);
      if (e.key === "Escape") { setRec(false); play(0); }
    });
    addEventListener("resize", fit); fit(); build(); draw(0); requestAnimationFrame(tick);
    </script>
    </body>
    </html>
    ```
    
  • 02 Data stories

    Kinetic type

    Use it for Proprietary stat reveal

    Read the prompt
    Build a 15-second animated video in the "Kinetic type" style: big numbers that count up and words that rise in one at a time on a dark background.
    
    It's for revealing a proprietary stat, one beat at a time.
    
    MY CONTENT (fill these in; delete a line to keep the default)
    - What the video says, in one sentence: [e.g. "Inbound drove 45% of new pipeline this quarter"]
    - Numbers and labels: [a hero number plus 2 or 3 supporting numbers, each with a short label]
    - Source line: [where the numbers come from]
    - Brand colors: [hex codes for background, text and 2 or 3 accents]. Default: #6688ad, #545454, #292424, #7ba69d, #dc685a
    - Font: [font name]. Default: Poppins
    - Format: [16:9, 1:1 or 9:16]. Default: 16:9
    
    HOW TO BUILD IT
    1. Start from the reference code at the end of this message. It already works. Keep its structure: the CONFIG block, the fixed-size stage, build(), render(t) and the player at the bottom. Put my content, colors, font and format into CONFIG, then adjust the layout so everything fits.
    2. Every frame is a pure function of time: render(t) draws the frame for t seconds. Don't add CSS animations, transitions, setTimeout or setInterval. That's what lets the video be scrubbed, recorded cleanly and captured frame by frame.
    3. If you're in Claude Motion, make it a Motion animation and use the reference for the look and the timing. Anywhere else, return one self-contained HTML artifact with no external scripts. The Google Fonts link is the only outside request.
    4. Keep record mode working. The Record mode button (or the R key) hides every control and holds the first frame. Clicking the stage or pressing Space plays it once and holds the last frame. Esc exits.
    5. No sound. About 15 seconds, with the final frame held for at least 2 seconds.
    6. Text has to read on a phone. On the 1920x1080 stage nothing goes under 30px, labels are 40px or larger, and headline numbers 120px or larger. No text overlaps other text, runs off the stage, or leaves one word alone on the last line of a heading. If my content doesn't fit, cut words or add a scene. Don't shrink the type.
    7. Use three or four scenes of four to eight words each, with one number per scene. Only type and color blocks move. No charts.
    
    Optional: before writing any code, give me a five-line storyboard (time, what's on screen, what moves) and wait for my OK.
    
    When it's done, tell me in two lines how to get an MP4: open Record mode, start a screen recording of the stage (Cmd+Shift+5 on a Mac, or Loom), then press Space.
    
    REFERENCE CODE: Kinetic type
    ```html
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Kinetic type</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap">
    <style>
    /* Player: fixed-size stage, scaled to fit the window. Keep as is. */
    html,body{margin:0;height:100%;background:#111;overflow:hidden}
    #stage{position:absolute;left:50%;top:50%;overflow:hidden;box-sizing:border-box;background:var(--bg);color:var(--ink);font-family:var(--font)}
    #stage *{box-sizing:border-box}
    #stage::after{content:"";position:absolute;inset:0;background:var(--bg);opacity:var(--fade,0);pointer-events:none;z-index:99}
    #ui{position:fixed;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px;align-items:center;padding:8px 12px;border-radius:10px;background:rgba(0,0,0,.7);font:13px system-ui,sans-serif;color:#eee;z-index:9}
    #ui button{font:inherit;padding:5px 10px;border:0;border-radius:6px;cursor:pointer}
    body.rec #ui{display:none} body.rec{cursor:none}
    
    /* Style */
    .scene{position:absolute;inset:0;padding:100px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:10px}
    .lead{font-size:80px;font-weight:600}
    .big{font-size:380px;line-height:.95;font-weight:700;color:var(--c3);letter-spacing:-.02em}
    .hl{position:relative;display:inline-block;font-size:140px;line-height:1.15;font-weight:700;padding:6px 36px;margin-top:20px;color:var(--bg)}
    .hl::before{content:"";position:absolute;inset:0;background:var(--c2);border-radius:14px;transform:scaleX(var(--wipe,0));transform-origin:left;z-index:-1}
    .hl span{position:relative}
    .line{font-size:84px;line-height:1.2;font-weight:600;max-width:1600px;text-wrap:balance}
    .w{display:inline-block}
    .source{position:absolute;left:0;right:0;bottom:56px;text-align:center;font-size:32px;color:var(--muted)}
    .tall .big{font-size:300px} .tall .line,.tall .lead{font-size:76px}
    </style>
    </head>
    <body>
    <div id="stage">
    <div id="scenes"></div>
    <div class="source" id="source"></div>
    </div>
    <div id="ui"><button id="b-play">Play / Pause</button><button id="b-replay">Replay</button><input id="scrub" type="range" min="0" max="1000" value="0"><button id="b-rec" title="Hides the controls. Click the stage or press Space to play once. Esc exits.">Record mode</button></div>
    
    <script>
    /* ---------- Your content: edit here ---------- */
    const CONFIG = {
      format: "16:9",            // "16:9" | "1:1" | "9:16"
      duration: 15,              // seconds
      font: "Poppins",
      colors: { bg: "#292424", ink: "#ffffff", muted: "#b9b2b2", c1: "#6688ad", c2: "#7ba69d", c3: "#dc685a" },
      // Each scene holds the screen from `at` until the next scene starts. Use any of: lead, big, hl, line.
      // A number inside `big` counts up.
      scenes: [
        { at: 0.3, big: "1,240", line: "people signed up for our pricing webinar" },
        { at: 4.0, lead: "and 46% of them", hl: "showed up live" },
        { at: 7.6, big: "84", line: "meetings booked in the two weeks after" },
        { at: 11.2, big: "$410K", line: "in new pipeline from those meetings" },
      ],
      source: "Sample data",
    };
    
    /* ---------- Helpers ---------- */
    const $ = (id) => document.getElementById(id);
    const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
    const prog = (t, start, dur) => clamp((t - start) / dur);      // 0 to 1 between start and start+dur
    const ease = (x) => 1 - Math.pow(1 - x, 3);                     // ease-out cubic
    const show = (el, x, dy = 30) => { el.style.opacity = x; el.style.transform = `translateY(${(1 - x) * dy}px)`; };
    const words = (text) => text.split(" ").map((w) => `<span class="w">${w}</span>`).join(" ");
    
    /* ---------- Build the DOM once from CONFIG ---------- */
    function build() {
      $("source").textContent = CONFIG.source;
      for (const s of CONFIG.scenes) {
        let h = "";
        if (s.lead) h += `<div class="lead part">${words(s.lead)}</div>`;
        if (s.big) h += `<div class="big part"></div>`;
        if (s.hl) h += `<div class="hl part"><span>${words(s.hl)}</span></div>`;
        if (s.line) h += `<div class="line part">${words(s.line)}</div>`;
        $("scenes").insertAdjacentHTML("beforeend", `<div class="scene">${h}</div>`);
      }
    }
    
    /* ---------- Draw one frame for time t (seconds). Pure: same t, same frame. ---------- */
    function render(t) {
      const scenes = [...document.querySelectorAll(".scene")];
      scenes.forEach((el, i) => {
        const s = CONFIG.scenes[i], next = CONFIG.scenes[i + 1];
        const out = next ? ease(prog(t, next.at - 0.45, 0.45)) : 0;          // slide up and out before the next scene
        el.style.opacity = t < s.at ? 0 : 1 - out;
        el.style.transform = `translateY(${-out * 120}px)`;
        [...el.querySelectorAll(".part")].forEach((part, j) => {
          const start = s.at + j * 0.6;
          if (part.classList.contains("big")) {                                // count the number up, then settle
            const p = ease(prog(t, start, 1.3)), m = s.big.match(/[\d,.]+/);
            const n = m ? Math.round(parseFloat(m[0].replace(/,/g, "")) * p).toLocaleString("en-US") : "";
            part.textContent = m ? s.big.replace(m[0], n) : s.big;
            part.style.opacity = prog(t, start, 0.3);
            part.style.transform = `scale(${0.8 + 0.2 * p})`;
          }
          if (part.classList.contains("hl")) part.style.setProperty("--wipe", ease(prog(t, start, 0.6)));
          [...part.querySelectorAll(".w")].forEach((w, k) => {                  // words rise in one after another
            const p = ease(prog(t, start + 0.15 + k * 0.11, 0.5));
            w.style.opacity = p; w.style.transform = `translateY(${(1 - p) * 60}px)`;
          });
        });
      });
      $("source").style.opacity = prog(t, 1.0, 0.8);
    }
    
    /* ---------- Player: keep as is ---------- */
    const SIZES = { "16:9": [1920, 1080], "1:1": [1440, 1440], "9:16": [1080, 1920] };
    const [W, H] = SIZES[CONFIG.format] || SIZES["16:9"], D = CONFIG.duration;
    const stage = $("stage"), scrub = $("scrub");
    stage.style.width = W + "px"; stage.style.height = H + "px";
    stage.classList.toggle("tall", H > W); stage.classList.toggle("square", H === W);
    for (const [k, v] of Object.entries(CONFIG.colors)) stage.style.setProperty("--" + k, v);
    stage.style.setProperty("--font", `"${CONFIG.font}", system-ui, sans-serif`);
    let t = 0, playing = true, rec = false, start = performance.now();
    const fit = () => { stage.style.transform = `translate(-50%,-50%) scale(${Math.min(innerWidth / W, innerHeight / H)})`; };
    function draw(time) { t = time; render(t); stage.style.setProperty("--fade", rec ? 0 : clamp((t - D + 0.5) / 0.5)); scrub.value = t / D * 1000; }
    window.seek = (time) => { playing = false; draw(time); };   // frame-by-frame capture: seek(0), seek(1/30), ...
    function play(from = 0) { start = performance.now() - from * 1000; playing = true; }
    function tick(now) {
      if (playing) { let x = (now - start) / 1000; if (x >= D) { if (rec) { x = D; playing = false; } else { x = 0; play(0); } } draw(x); }
      requestAnimationFrame(tick);
    }
    function setRec(on) { rec = on; document.body.classList.toggle("rec", on); seek(0); }
    $("b-play").onclick = () => (playing ? (playing = false) : play(t >= D ? 0 : t));
    $("b-replay").onclick = () => play(0);
    $("b-rec").onclick = () => setRec(true);
    scrub.oninput = () => seek(scrub.value / 1000 * D);
    stage.onclick = () => { if (rec) play(0); };
    addEventListener("keydown", (e) => {
      if (e.code === "Space") { e.preventDefault(); if (rec) play(0); else if (playing) playing = false; else play(t >= D ? 0 : t); }
      if (e.key === "r" || e.key === "R") setRec(!rec);
      if (e.key === "Escape") { setRec(false); play(0); }
    });
    addEventListener("resize", fit); fit(); build(); draw(0); requestAnimationFrame(tick);
    </script>
    </body>
    </html>
    ```
    
  • 03 Account plays

    Map build

    Use it for Territory or coverage

    Read the prompt
    Build a 15-second animated video in the "Map build" style: a US tile map that sweeps in and fills state by state, with counters stacking up beside it.
    
    It's for a territory or coverage story.
    
    MY CONTENT (fill these in; delete a line to keep the default)
    - What the video says, in one sentence: [e.g. "Inbound drove 45% of new pipeline this quarter"]
    - Numbers and labels: [a hero number plus 2 or 3 supporting numbers, each with a short label]
    - Source line: [where the numbers come from]
    - Brand colors: [hex codes for background, text and 2 or 3 accents]. Default: #6688ad, #545454, #292424, #7ba69d, #dc685a
    - Font: [font name]. Default: Poppins
    - Format: [16:9, 1:1 or 9:16]. Default: 16:9
    
    HOW TO BUILD IT
    1. Start from the reference code at the end of this message. It already works. Keep its structure: the CONFIG block, the fixed-size stage, build(), render(t) and the player at the bottom. Put my content, colors, font and format into CONFIG, then adjust the layout so everything fits.
    2. Every frame is a pure function of time: render(t) draws the frame for t seconds. Don't add CSS animations, transitions, setTimeout or setInterval. That's what lets the video be scrubbed, recorded cleanly and captured frame by frame.
    3. If you're in Claude Motion, make it a Motion animation and use the reference for the look and the timing. Anywhere else, return one self-contained HTML artifact with no external scripts. The Google Fonts link is the only outside request.
    4. Keep record mode working. The Record mode button (or the R key) hides every control and holds the first frame. Clicking the stage or pressing Space plays it once and holds the last frame. Esc exits.
    5. No sound. About 15 seconds, with the final frame held for at least 2 seconds.
    6. Text has to read on a phone. On the 1920x1080 stage nothing goes under 30px, labels are 40px or larger, and headline numbers 120px or larger. No text overlaps other text, runs off the stage, or leaves one word alone on the last line of a heading. If my content doesn't fit, cut words or add a scene. Don't shrink the type.
    7. CONFIG.shade sets each state from 0 to 3. If I don't give you numbers per state, ask for them or label the shading as sample data. For a region outside the US, swap the tile grid for one that fits and keep the same tile approach.
    
    Optional: before writing any code, give me a five-line storyboard (time, what's on screen, what moves) and wait for my OK.
    
    When it's done, tell me in two lines how to get an MP4: open Record mode, start a screen recording of the stage (Cmd+Shift+5 on a Mac, or Loom), then press Space.
    
    REFERENCE CODE: Map build
    ```html
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Map build</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap">
    <style>
    /* Player: fixed-size stage, scaled to fit the window. Keep as is. */
    html,body{margin:0;height:100%;background:#111;overflow:hidden}
    #stage{position:absolute;left:50%;top:50%;overflow:hidden;box-sizing:border-box;background:var(--bg);color:var(--ink);font-family:var(--font)}
    #stage *{box-sizing:border-box}
    #stage::after{content:"";position:absolute;inset:0;background:var(--bg);opacity:var(--fade,0);pointer-events:none;z-index:99}
    #ui{position:fixed;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px;align-items:center;padding:8px 12px;border-radius:10px;background:rgba(0,0,0,.7);font:13px system-ui,sans-serif;color:#eee;z-index:9}
    #ui button{font:inherit;padding:5px 10px;border:0;border-radius:6px;cursor:pointer}
    body.rec #ui{display:none} body.rec{cursor:none}
    
    /* Style */
    #stage{padding:90px 100px 60px;display:grid;grid-template-columns:640px 1fr;grid-template-rows:minmax(0,1fr) auto;gap:30px 80px;align-items:center}
    .panel{display:flex;flex-direction:column;gap:30px}
    .eyebrow{font-size:36px;font-weight:600;letter-spacing:.08em;color:var(--c1)}
    .stat b{display:block;font-size:130px;line-height:1;font-weight:700}
    .stat span{display:block;font-size:42px;line-height:1.25;margin-top:6px;text-wrap:balance}
    .stat.hero b{color:var(--c3)} .stat.alt b{color:var(--c1)}
    .map{display:grid;grid-template-columns:repeat(11,1fr);gap:8px;width:100%;max-width:1040px;justify-self:center}
    .tile{aspect-ratio:1;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:30px;font-weight:600;color:var(--muted);background:var(--grey);position:relative}
    .ring{position:absolute;inset:-6px;border:5px solid var(--c3);border-radius:14px;opacity:0}
    .foot{grid-column:1/-1;display:flex;justify-content:space-between;gap:40px;font-size:30px;color:var(--muted)}
    #stage.tall{grid-template-columns:1fr;grid-template-rows:auto auto auto;align-content:center}
    .tall .map{grid-row:1} .tall .foot{flex-direction:column;gap:6px}
    .tall .panel{gap:26px} .tall .stat b{font-size:120px}
    </style>
    </head>
    <body>
    <div id="stage">
    <div class="panel" id="panel"><div class="eyebrow" id="eyebrow"></div></div>
    <div class="map" id="map"></div>
    <div class="foot"><span id="note"></span><span id="source"></span></div>
    </div>
    <div id="ui"><button id="b-play">Play / Pause</button><button id="b-replay">Replay</button><input id="scrub" type="range" min="0" max="1000" value="0"><button id="b-rec" title="Hides the controls. Click the stage or press Space to play once. Esc exits.">Record mode</button></div>
    
    <script>
    /* ---------- Your content: edit here ---------- */
    const CONFIG = {
      format: "16:9",            // "16:9" | "1:1" | "9:16"
      duration: 15,              // seconds
      font: "Poppins",
      colors: { bg: "#ffffff", ink: "#292424", muted: "#545454", grey: "#e4e8ee", c1: "#6688ad", c2: "#7ba69d", c3: "#dc685a" },
      eyebrow: "NEW PIPELINE BY REGION, Q3",
      // style: "hero" (accent), "alt" (second accent) or leave it out
      stats: [ { value: 318, text: "new opportunities this quarter" },
               { value: 142, text: "came from the West region", style: "hero" },
               { value: 27, suffix: "%", text: "growth in the Northeast since Q2", style: "alt" } ],
      // State shading, 0 to 3. Swap in your own counts per state or region.
      shade: { CA: 3, WA: 3, OR: 2, CO: 2, UT: 2, AZ: 2, NV: 1, ID: 1, NY: 3, MA: 2, NJ: 2, PA: 1, CT: 1, TX: 2, IL: 1, GA: 1, FL: 1, NC: 1, MN: 1 },
      rings: ["CA", "NY"],       // states that get a pulsing ring
      note: "Sample data",
      source: "",
    };
    
    /* ---------- Helpers ---------- */
    const $ = (id) => document.getElementById(id);
    const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
    const prog = (t, start, dur) => clamp((t - start) / dur);      // 0 to 1 between start and start+dur
    const ease = (x) => 1 - Math.pow(1 - x, 3);                     // ease-out cubic
    const show = (el, x, dy = 30) => { el.style.opacity = x; el.style.transform = `translateY(${(1 - x) * dy}px)`; };
    // US tile grid: state, column, row (11 x 8).
    const GRID = "AK 0 0,ME 10 0,WI 5 1,VT 9 1,NH 10 1,WA 0 2,ID 1 2,MT 2 2,ND 3 2,MN 4 2,IL 5 2,MI 6 2,NY 8 2,MA 9 2,OR 0 3,NV 1 3,WY 2 3,SD 3 3,IA 4 3,IN 5 3,OH 6 3,PA 7 3,NJ 8 3,CT 9 3,RI 10 3,CA 0 4,UT 1 4,CO 2 4,NE 3 4,MO 4 4,KY 5 4,WV 6 4,VA 7 4,MD 8 4,DE 9 4,AZ 1 5,NM 2 5,KS 3 5,AR 4 5,TN 5 5,NC 6 5,SC 7 5,DC 8 5,OK 3 6,LA 4 6,MS 5 6,AL 6 6,GA 7 6,HI 0 7,TX 3 7,FL 8 7"
      .split(",").map((s) => { const [id, c, r] = s.split(" "); return { id, c: +c, r: +r }; });
    const mix = (hex, a) => `color-mix(in srgb, ${hex} ${Math.round(a * 100)}%, var(--grey))`;
    
    /* ---------- Build the DOM once from CONFIG ---------- */
    function build() {
      $("eyebrow").textContent = CONFIG.eyebrow; $("note").textContent = CONFIG.note; $("source").textContent = CONFIG.source;
      for (const s of CONFIG.stats) $("panel").insertAdjacentHTML("beforeend", `<div class="stat ${s.style || ""}"><b></b><span>${s.text}</span></div>`);
      for (const g of GRID) $("map").insertAdjacentHTML("beforeend",
        `<div class="tile" data-id="${g.id}" style="grid-column:${g.c + 1};grid-row:${g.r + 1}">${g.id}${CONFIG.rings.includes(g.id) ? '<i class="ring"></i>' : ""}</div>`);
    }
    
    /* ---------- Draw one frame for time t (seconds). Pure: same t, same frame. ---------- */
    function render(t) {
      show($("eyebrow"), ease(prog(t, 0.2, 0.7)));
      [...document.querySelectorAll(".tile")].forEach((el, i) => {
        const g = GRID[i], lvl = CONFIG.shade[g.id] || 0;
        const p = ease(prog(t, 0.4 + (g.c + g.r) * 0.1, 0.6));            // tiles sweep in from the north-west
        el.style.opacity = p; el.style.transform = `scale(${0.6 + 0.4 * p})`;
        const f = lvl ? ease(prog(t, 2.6 + ((i * 7) % 19) * 0.12, 0.5)) : 0; // shaded states fill in, scattered
        el.style.background = mix(CONFIG.colors.c1, f * (0.25 + 0.25 * lvl));
        el.style.color = lvl >= 2 && f > 0.6 ? "#fff" : "";
        const ring = el.querySelector(".ring");
        if (ring) { const ph = Math.max(0, t - 5.4) % 1.6 / 1.6; ring.style.opacity = t > 5.4 ? 1 - ph : 0; ring.style.transform = `scale(${1 + ph * 0.5})`; }
      });
      [...document.querySelectorAll(".stat")].forEach((el, i) => {
        const start = 1.0 + i * 3.2, s = CONFIG.stats[i];
        show(el, ease(prog(t, start, 0.7)), 40);
        el.querySelector("b").textContent = Math.round(s.value * ease(prog(t, start, 1.4))) + (s.suffix || "");
      });
      $("note").style.opacity = $("source").style.opacity = prog(t, 3.0, 0.8);
    }
    
    /* ---------- Player: keep as is ---------- */
    const SIZES = { "16:9": [1920, 1080], "1:1": [1440, 1440], "9:16": [1080, 1920] };
    const [W, H] = SIZES[CONFIG.format] || SIZES["16:9"], D = CONFIG.duration;
    const stage = $("stage"), scrub = $("scrub");
    stage.style.width = W + "px"; stage.style.height = H + "px";
    stage.classList.toggle("tall", H > W); stage.classList.toggle("square", H === W);
    for (const [k, v] of Object.entries(CONFIG.colors)) stage.style.setProperty("--" + k, v);
    stage.style.setProperty("--font", `"${CONFIG.font}", system-ui, sans-serif`);
    let t = 0, playing = true, rec = false, start = performance.now();
    const fit = () => { stage.style.transform = `translate(-50%,-50%) scale(${Math.min(innerWidth / W, innerHeight / H)})`; };
    function draw(time) { t = time; render(t); stage.style.setProperty("--fade", rec ? 0 : clamp((t - D + 0.5) / 0.5)); scrub.value = t / D * 1000; }
    window.seek = (time) => { playing = false; draw(time); };   // frame-by-frame capture: seek(0), seek(1/30), ...
    function play(from = 0) { start = performance.now() - from * 1000; playing = true; }
    function tick(now) {
      if (playing) { let x = (now - start) / 1000; if (x >= D) { if (rec) { x = D; playing = false; } else { x = 0; play(0); } } draw(x); }
      requestAnimationFrame(tick);
    }
    function setRec(on) { rec = on; document.body.classList.toggle("rec", on); seek(0); }
    $("b-play").onclick = () => (playing ? (playing = false) : play(t >= D ? 0 : t));
    $("b-replay").onclick = () => play(0);
    $("b-rec").onclick = () => setRec(true);
    scrub.oninput = () => seek(scrub.value / 1000 * D);
    stage.onclick = () => { if (rec) play(0); };
    addEventListener("keydown", (e) => {
      if (e.code === "Space") { e.preventDefault(); if (rec) play(0); else if (playing) playing = false; else play(t >= D ? 0 : t); }
      if (e.key === "r" || e.key === "R") setRec(!rec);
      if (e.key === "Escape") { setRec(false); play(0); }
    });
    addEventListener("resize", fit); fit(); build(); draw(0); requestAnimationFrame(tick);
    </script>
    </body>
    </html>
    ```
    
  • 04 Account plays

    Ranked countdown

    Use it for Top accounts list

    Read the prompt
    Build a 15-second animated video in the "Ranked countdown" style: a top-five list revealed from #5 up to #1, each row with a bar that grows and a number that counts, then #1 gets a spotlight.
    
    It's for a top accounts list, revealed from five to one.
    
    MY CONTENT (fill these in; delete a line to keep the default)
    - What the video says, in one sentence: [e.g. "Inbound drove 45% of new pipeline this quarter"]
    - Numbers and labels: [a hero number plus 2 or 3 supporting numbers, each with a short label]
    - Source line: [where the numbers come from]
    - Brand colors: [hex codes for background, text and 2 or 3 accents]. Default: #6688ad, #545454, #292424, #7ba69d, #dc685a
    - Font: [font name]. Default: Poppins
    - Format: [16:9, 1:1 or 9:16]. Default: 16:9
    
    HOW TO BUILD IT
    1. Start from the reference code at the end of this message. It already works. Keep its structure: the CONFIG block, the fixed-size stage, build(), render(t) and the player at the bottom. Put my content, colors, font and format into CONFIG, then adjust the layout so everything fits.
    2. Every frame is a pure function of time: render(t) draws the frame for t seconds. Don't add CSS animations, transitions, setTimeout or setInterval. That's what lets the video be scrubbed, recorded cleanly and captured frame by frame.
    3. If you're in Claude Motion, make it a Motion animation and use the reference for the look and the timing. Anywhere else, return one self-contained HTML artifact with no external scripts. The Google Fonts link is the only outside request.
    4. Keep record mode working. The Record mode button (or the R key) hides every control and holds the first frame. Clicking the stage or pressing Space plays it once and holds the last frame. Esc exits.
    5. No sound. About 15 seconds, with the final frame held for at least 2 seconds.
    6. Text has to read on a phone. On the 1920x1080 stage nothing goes under 30px, labels are 40px or larger, and headline numbers 120px or larger. No text overlaps other text, runs off the stage, or leaves one word alone on the last line of a heading. If my content doesn't fit, cut words or add a scene. Don't shrink the type.
    7. Up to five rows, ranked #1 first in CONFIG.items. Keep labels under 32 characters so each stays on one line. For accounts, the label is the account name and the value can be a fit score or a signal count.
    
    Optional: before writing any code, give me a five-line storyboard (time, what's on screen, what moves) and wait for my OK.
    
    When it's done, tell me in two lines how to get an MP4: open Record mode, start a screen recording of the stage (Cmd+Shift+5 on a Mac, or Loom), then press Space.
    
    REFERENCE CODE: Ranked countdown
    ```html
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Ranked countdown</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap">
    <style>
    /* Player: fixed-size stage, scaled to fit the window. Keep as is. */
    html,body{margin:0;height:100%;background:#111;overflow:hidden}
    #stage{position:absolute;left:50%;top:50%;overflow:hidden;box-sizing:border-box;background:var(--bg);color:var(--ink);font-family:var(--font)}
    #stage *{box-sizing:border-box}
    #stage::after{content:"";position:absolute;inset:0;background:var(--bg);opacity:var(--fade,0);pointer-events:none;z-index:99}
    #ui{position:fixed;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px;align-items:center;padding:8px 12px;border-radius:10px;background:rgba(0,0,0,.7);font:13px system-ui,sans-serif;color:#eee;z-index:9}
    #ui button{font:inherit;padding:5px 10px;border:0;border-radius:6px;cursor:pointer}
    body.rec #ui{display:none} body.rec{cursor:none}
    
    /* Style */
    #stage{padding:80px 110px 60px;display:flex;flex-direction:column}
    h1{margin:0;font-size:62px;line-height:1.1;font-weight:700;text-wrap:balance}
    .sub{font-size:38px;color:var(--muted);margin-top:12px}
    .list{flex:1;min-height:0;display:flex;flex-direction:column;justify-content:center;gap:14px;margin-top:24px}
    .row{display:flex;align-items:center;gap:40px;padding:10px 28px;border-radius:18px;background:var(--row)}
    .rank{flex:none;width:92px;height:92px;border-radius:50%;background:var(--ink);color:var(--bg);display:flex;align-items:center;justify-content:center;font-size:54px;line-height:1;font-weight:700}
    .mid{flex:1;min-width:0}
    .label{font-size:46px;font-weight:600;line-height:1.2}
    .track{height:20px;margin-top:12px;border-radius:10px;background:var(--grey)}
    .fill{height:100%;border-radius:10px;background:var(--c1)}
    .pct{flex:none;width:200px;text-align:right;font-size:84px;line-height:1;font-weight:700}
    .row.top .rank{background:var(--c3)} .row.top .fill{background:var(--c3)}
    .source{font-size:30px;color:var(--muted);margin-top:18px}
    .tall .row{gap:24px;padding:14px 20px} .tall .rank{width:84px;height:84px;font-size:44px} .tall .pct{width:150px;font-size:64px} .tall .label{font-size:40px}
    </style>
    </head>
    <body>
    <div id="stage">
    <h1 id="title"></h1>
    <div class="sub" id="sub"></div>
    <div class="list" id="list"></div>
    <div class="source" id="source"></div>
    </div>
    <div id="ui"><button id="b-play">Play / Pause</button><button id="b-replay">Replay</button><input id="scrub" type="range" min="0" max="1000" value="0"><button id="b-rec" title="Hides the controls. Click the stage or press Space to play once. Esc exits.">Record mode</button></div>
    
    <script>
    /* ---------- Your content: edit here ---------- */
    const CONFIG = {
      format: "16:9",            // "16:9" | "1:1" | "9:16"
      duration: 15,              // seconds
      font: "Poppins",
      colors: { bg: "#ffffff", ink: "#292424", muted: "#545454", grey: "#e4e8ee", row: "#f4f6f9", c1: "#6688ad", c2: "#7ba69d", c3: "#dc685a" },
      title: "Top 5 industries by win rate",
      sub: "Closed opportunities, last 12 months",
      // Ranked #1 first. The countdown reveals them from the last one up.
      items: [ { label: "Healthcare", value: 34 },
               { label: "Financial services", value: 31 },
               { label: "Manufacturing", value: 27 },
               { label: "Software", value: 22 },
               { label: "Retail", value: 18 } ],
      suffix: "%",
      source: "Sample data",
    };
    
    /* ---------- Helpers ---------- */
    const $ = (id) => document.getElementById(id);
    const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
    const prog = (t, start, dur) => clamp((t - start) / dur);      // 0 to 1 between start and start+dur
    const ease = (x) => 1 - Math.pow(1 - x, 3);                     // ease-out cubic
    const show = (el, x, dy = 30) => { el.style.opacity = x; el.style.transform = `translateY(${(1 - x) * dy}px)`; };
    
    /* ---------- Build the DOM once from CONFIG ---------- */
    function build() {
      $("title").textContent = CONFIG.title; $("sub").textContent = CONFIG.sub; $("source").textContent = CONFIG.source;
      CONFIG.items.forEach((it, i) => $("list").insertAdjacentHTML("beforeend",
        `<div class="row${i === 0 ? " top" : ""}"><div class="rank">${i + 1}</div><div class="mid"><div class="label">${it.label}</div><div class="track"><div class="fill"></div></div></div><div class="pct"></div></div>`));
    }
    
    /* ---------- Draw one frame for time t (seconds). Pure: same t, same frame. ---------- */
    function render(t) {
      show($("title"), ease(prog(t, 0.2, 0.8))); show($("sub"), ease(prog(t, 0.5, 0.8)));
      const n = CONFIG.items.length, max = Math.max(...CONFIG.items.map((i) => i.value));
      [...document.querySelectorAll(".row")].forEach((row, i) => {
        const start = 1.5 + (n - 1 - i) * 1.6;                          // last place first, #1 last
        const p = ease(prog(t, start, 0.7));
        row.style.opacity = p; row.style.transform = `translateX(${(1 - p) * 140}px)`;
        row.querySelector(".rank").style.transform = `scaleY(${Math.max(0.05, ease(prog(t, start + 0.1, 0.5)))})`;
        const b = ease(prog(t, start + 0.3, 1.1));
        row.querySelector(".fill").style.width = (b * CONFIG.items[i].value / max * 100) + "%";
        row.querySelector(".pct").textContent = Math.round(b * CONFIG.items[i].value) + CONFIG.suffix;
        if (i === 0) {                                                    // spotlight #1 once the list is complete
          const h = ease(prog(t, start + 1.6, 0.6));
          row.style.boxShadow = `0 0 0 ${h * 6}px ${CONFIG.colors.c3}`;
          row.style.transform += ` scale(${1 + 0.025 * Math.sin(Math.PI * h)})`;
        }
      });
      $("source").style.opacity = prog(t, 2.0, 0.8);
    }
    
    /* ---------- Player: keep as is ---------- */
    const SIZES = { "16:9": [1920, 1080], "1:1": [1440, 1440], "9:16": [1080, 1920] };
    const [W, H] = SIZES[CONFIG.format] || SIZES["16:9"], D = CONFIG.duration;
    const stage = $("stage"), scrub = $("scrub");
    stage.style.width = W + "px"; stage.style.height = H + "px";
    stage.classList.toggle("tall", H > W); stage.classList.toggle("square", H === W);
    for (const [k, v] of Object.entries(CONFIG.colors)) stage.style.setProperty("--" + k, v);
    stage.style.setProperty("--font", `"${CONFIG.font}", system-ui, sans-serif`);
    let t = 0, playing = true, rec = false, start = performance.now();
    const fit = () => { stage.style.transform = `translate(-50%,-50%) scale(${Math.min(innerWidth / W, innerHeight / H)})`; };
    function draw(time) { t = time; render(t); stage.style.setProperty("--fade", rec ? 0 : clamp((t - D + 0.5) / 0.5)); scrub.value = t / D * 1000; }
    window.seek = (time) => { playing = false; draw(time); };   // frame-by-frame capture: seek(0), seek(1/30), ...
    function play(from = 0) { start = performance.now() - from * 1000; playing = true; }
    function tick(now) {
      if (playing) { let x = (now - start) / 1000; if (x >= D) { if (rec) { x = D; playing = false; } else { x = 0; play(0); } } draw(x); }
      requestAnimationFrame(tick);
    }
    function setRec(on) { rec = on; document.body.classList.toggle("rec", on); seek(0); }
    $("b-play").onclick = () => (playing ? (playing = false) : play(t >= D ? 0 : t));
    $("b-replay").onclick = () => play(0);
    $("b-rec").onclick = () => setRec(true);
    scrub.oninput = () => seek(scrub.value / 1000 * D);
    stage.onclick = () => { if (rec) play(0); };
    addEventListener("keydown", (e) => {
      if (e.code === "Space") { e.preventDefault(); if (rec) play(0); else if (playing) playing = false; else play(t >= D ? 0 : t); }
      if (e.key === "r" || e.key === "R") setRec(!rec);
      if (e.key === "Escape") { setRec(false); play(0); }
    });
    addEventListener("resize", fit); fit(); build(); draw(0); requestAnimationFrame(tick);
    </script>
    </body>
    </html>
    ```
    
  • 05 Explainers

    Whiteboard sketch

    Use it for Process explainer

    Read the prompt
    Build a 15-second animated video in the "Whiteboard sketch" style: boxes, arrows and a circle drawn on with a marker, with the words writing in after each stroke.
    
    It's for a process explainer, step by step.
    
    MY CONTENT (fill these in; delete a line to keep the default)
    - What the video says, in one sentence: [e.g. "Inbound drove 45% of new pipeline this quarter"]
    - Numbers and labels: [a hero number plus 2 or 3 supporting numbers, each with a short label]
    - Source line: [where the numbers come from]
    - Brand colors: [hex codes for background, text and 2 or 3 accents]. Default: #6688ad, #545454, #292424, #7ba69d, #dc685a
    - Font: [font name]. Default: Poppins
    - Format: [16:9, 1:1 or 9:16]. Default: 16:9
    
    HOW TO BUILD IT
    1. Start from the reference code at the end of this message. It already works. Keep its structure: the CONFIG block, the fixed-size stage, build(), render(t) and the player at the bottom. Put my content, colors, font and format into CONFIG, then adjust the layout so everything fits.
    2. Every frame is a pure function of time: render(t) draws the frame for t seconds. Don't add CSS animations, transitions, setTimeout or setInterval. That's what lets the video be scrubbed, recorded cleanly and captured frame by frame.
    3. If you're in Claude Motion, make it a Motion animation and use the reference for the look and the timing. Anywhere else, return one self-contained HTML artifact with no external scripts. The Google Fonts link is the only outside request.
    4. Keep record mode working. The Record mode button (or the R key) hides every control and holds the first frame. Clicking the stage or pressing Space plays it once and holds the last frame. Esc exits.
    5. No sound. About 15 seconds, with the final frame held for at least 2 seconds.
    6. Text has to read on a phone. On the 1920x1080 stage nothing goes under 30px, labels are 40px or larger, and headline numbers 120px or larger. No text overlaps other text, runs off the stage, or leaves one word alone on the last line of a heading. If my content doesn't fit, cut words or add a scene. Don't shrink the type.
    7. Three steps reads best, and four is the limit in 16:9. Keep each step to a number or short title plus one line. The marker strokes are generated from each box's size, so leave sizeShapes() and SHAPES as they are.
    
    Optional: before writing any code, give me a five-line storyboard (time, what's on screen, what moves) and wait for my OK.
    
    When it's done, tell me in two lines how to get an MP4: open Record mode, start a screen recording of the stage (Cmd+Shift+5 on a Mac, or Loom), then press Space.
    
    REFERENCE CODE: Whiteboard sketch
    ```html
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Whiteboard sketch</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap">
    <style>
    /* Player: fixed-size stage, scaled to fit the window. Keep as is. */
    html,body{margin:0;height:100%;background:#111;overflow:hidden}
    #stage{position:absolute;left:50%;top:50%;overflow:hidden;box-sizing:border-box;background:var(--bg);color:var(--ink);font-family:var(--font)}
    #stage *{box-sizing:border-box}
    #stage::after{content:"";position:absolute;inset:0;background:var(--bg);opacity:var(--fade,0);pointer-events:none;z-index:99}
    #ui{position:fixed;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px;align-items:center;padding:8px 12px;border-radius:10px;background:rgba(0,0,0,.7);font:13px system-ui,sans-serif;color:#eee;z-index:9}
    #ui button{font:inherit;padding:5px 10px;border:0;border-radius:6px;cursor:pointer}
    body.rec #ui{display:none} body.rec{cursor:none}
    
    /* Style */
    #stage{padding:90px 90px 60px;display:flex;flex-direction:column;align-items:center}
    h1{margin:0;font-size:66px;font-weight:700;align-self:flex-start;text-wrap:balance}
    .flow{flex:1;display:flex;align-items:center;justify-content:center;gap:10px;width:100%}
    .box{position:relative;flex:1;max-width:540px;min-height:430px;padding:44px 44px;display:flex;flex-direction:column;justify-content:center}
    .box>b{position:relative;align-self:flex-start;font-size:150px;line-height:1;font-weight:700;margin-bottom:12px}
    .box>span{font-size:46px;line-height:1.25;margin-top:16px;text-wrap:balance}
    .arrow{flex:none;width:130px;height:90px}
    svg{position:absolute;overflow:visible;pointer-events:none}
    .box>svg{inset:0;width:100%;height:100%}
    b svg{left:-44px;top:-22px;width:calc(100% + 88px);height:calc(100% + 40px)}
    .arrow svg{position:static;width:100%;height:100%}
    path{fill:none;stroke:var(--ink);stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:1 1}
    .c3 path,path.c3{stroke:var(--c3)} .c1 path{stroke:var(--c1)}
    .note{position:relative;font-size:52px;font-weight:600;margin-bottom:30px;color:var(--c1)}
    .note svg{left:0;right:0;bottom:-26px;width:100%;height:30px}
    .source{align-self:flex-start;font-size:30px;color:var(--muted)}
    .write{clip-path:inset(0 100% 0 0)}
    .tall .flow,.square .flow{flex-direction:column;gap:0}
    .tall .box,.square .box{width:860px;max-width:none;min-height:0;flex:none;padding:30px 44px}
    .tall .arrow,.square .arrow{transform:rotate(90deg);height:70px} .tall .note{text-align:center;font-size:46px}
    .square .box{padding:22px 44px} .square .box>b{font-size:100px} .square .box>span{font-size:40px;margin-top:6px} .square .note{font-size:44px}
    </style>
    </head>
    <body>
    <div id="stage">
    <h1 id="title" class="write"></h1>
    <div class="flow" id="flow"></div>
    <div class="note write" id="note"></div>
    <div class="source" id="source"></div>
    </div>
    <div id="ui"><button id="b-play">Play / Pause</button><button id="b-replay">Replay</button><input id="scrub" type="range" min="0" max="1000" value="0"><button id="b-rec" title="Hides the controls. Click the stage or press Space to play once. Esc exits.">Record mode</button></div>
    
    <script>
    /* ---------- Your content: edit here ---------- */
    const CONFIG = {
      format: "16:9",            // "16:9" | "1:1" | "9:16"
      duration: 15,              // seconds
      font: "Poppins",           // try "Caveat" (add it to the font link) for a hand-lettered look
      colors: { bg: "#fbfaf6", ink: "#292424", muted: "#545454", c1: "#6688ad", c2: "#7ba69d", c3: "#dc685a" },
      title: "How an inbound lead becomes a meeting",
      steps: [ { value: "Form", text: "A lead asks for a demo on the website" },
               { value: "SDR", text: "An SDR qualifies it within 15 minutes" },
               { value: "AE", text: "The AE runs the first meeting", circle: true } ],
      note: "Form to meeting: 2 days, median",
      source: "Sample data",
    };
    
    /* ---------- Helpers ---------- */
    const $ = (id) => document.getElementById(id);
    const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
    const prog = (t, start, dur) => clamp((t - start) / dur);      // 0 to 1 between start and start+dur
    const ease = (x) => 1 - Math.pow(1 - x, 3);                     // ease-out cubic
    const show = (el, x, dy = 30) => { el.style.opacity = x; el.style.transform = `translateY(${(1 - x) * dy}px)`; };
    // Marker strokes: wobbly paths drawn on with stroke-dashoffset. Each SVG is sized in real pixels and
    // redrawn only when its box changes size; seeded, so every frame of a given size is identical.
    function wobble(pts, amp, seed) {
      const rnd = () => (seed = seed * 16807 % 2147483647) / 2147483647, j = () => (rnd() - 0.5) * amp;
      let d = `M${pts[0][0] + j()} ${pts[0][1] + j()}`;
      for (let i = 1; i < pts.length; i++) {
        const [x0, y0] = pts[i - 1], [x, y] = pts[i];
        d += ` Q${(x0 + x) / 2 + j() * 2} ${(y0 + y) / 2 + j() * 2} ${x + j()} ${y + j()}`;
      }
      return d;
    }
    const SHAPES = {
      rect: (w, h, s) => wobble([[6, 10], [w - 4, 4], [w - 8, h - 6], [8, h - 4], [5, 8], [40, 6]], 8, s),
      loop: (w, h, s) => wobble(Array.from({ length: 15 }, (_, i) => { const a = -2.4 + i / 13 * 7.2; return [w / 2 + w / 2 * Math.cos(a), h / 2 + h / 2 * Math.sin(a)]; }), 10, s),
      arrow: (w, h, s) => wobble([[4, h / 2], [w - 6, h / 2 + 2]], 4, s) + ` M${w * 0.68} ${h * 0.18} L${w - 4} ${h / 2 + 2} L${w * 0.68} ${h * 0.85}`,
      line: (w, h, s) => wobble([[0, h * 0.4], [w / 2, h * 0.6], [w, h * 0.3]], 6, s),
    };
    const svg = (shape, cls = "") => `<svg data-shape="${shape}" class="${cls}"><path pathLength="1"/></svg>`;
    function sizeShapes() {
      document.querySelectorAll("svg[data-shape]").forEach((el, i) => {
        const w = el.clientWidth, h = el.clientHeight, key = w + "x" + h;
        if (el.dataset.key === key) return;
        el.dataset.key = key; el.setAttribute("viewBox", `0 0 ${w} ${h}`);
        el.querySelector("path").setAttribute("d", SHAPES[el.dataset.shape](w, h, i * 97 + 13));
      });
    }
    const drawOn = (el, p) => el.querySelectorAll("path").forEach((path) => (path.style.strokeDashoffset = 1 - p));
    const writeOn = (el, p) => (el.style.clipPath = `inset(0 ${(1 - p) * 100}% 0 0)`);
    
    /* ---------- Build the DOM once from CONFIG ---------- */
    function build() {
      $("title").textContent = CONFIG.title; $("source").textContent = CONFIG.source;
      $("note").innerHTML = CONFIG.note + svg("line", "c3");
      CONFIG.steps.forEach((s, i) => {
        if (i) $("flow").insertAdjacentHTML("beforeend", `<div class="arrow">${svg("arrow")}</div>`);
        $("flow").insertAdjacentHTML("beforeend",
          `<div class="box">${svg("rect")}<b class="write">${s.value}${s.circle ? svg("loop", "c3") : ""}</b><span class="write">${s.text}</span></div>`);
      });
    }
    
    /* ---------- Draw one frame for time t (seconds). Pure: same t, same frame. ---------- */
    function render(t) {
      sizeShapes();
      writeOn($("title"), prog(t, 0.2, 1.1));
      const boxes = [...document.querySelectorAll(".box")], arrows = [...document.querySelectorAll(".arrow")];
      boxes.forEach((box, i) => {
        const start = 1.3 + i * 2.5;
        drawOn(box.querySelector(":scope>svg"), ease(prog(t, start, 1.0)));
        writeOn(box.querySelector("b"), prog(t, start + 0.8, 0.6));
        writeOn(box.querySelector("span"), prog(t, start + 1.2, 0.9));
        if (arrows[i]) drawOn(arrows[i], ease(prog(t, start + 1.8, 0.6)));
        const loop = box.querySelector("b svg");
        if (loop) { drawOn(loop, ease(prog(t, 9.0, 0.9))); box.querySelector("b").style.clipPath = t > 9 ? "none" : ""; }
      });
      writeOn($("note"), prog(t, 10.3, 1.3)); if (t > 11.6) $("note").style.clipPath = "none";
      drawOn($("note").querySelector("svg"), ease(prog(t, 11.6, 0.7)));
      $("source").style.opacity = prog(t, 2.0, 0.8);
    }
    
    /* ---------- Player: keep as is ---------- */
    const SIZES = { "16:9": [1920, 1080], "1:1": [1440, 1440], "9:16": [1080, 1920] };
    const [W, H] = SIZES[CONFIG.format] || SIZES["16:9"], D = CONFIG.duration;
    const stage = $("stage"), scrub = $("scrub");
    stage.style.width = W + "px"; stage.style.height = H + "px";
    stage.classList.toggle("tall", H > W); stage.classList.toggle("square", H === W);
    for (const [k, v] of Object.entries(CONFIG.colors)) stage.style.setProperty("--" + k, v);
    stage.style.setProperty("--font", `"${CONFIG.font}", system-ui, sans-serif`);
    let t = 0, playing = true, rec = false, start = performance.now();
    const fit = () => { stage.style.transform = `translate(-50%,-50%) scale(${Math.min(innerWidth / W, innerHeight / H)})`; };
    function draw(time) { t = time; render(t); stage.style.setProperty("--fade", rec ? 0 : clamp((t - D + 0.5) / 0.5)); scrub.value = t / D * 1000; }
    window.seek = (time) => { playing = false; draw(time); };   // frame-by-frame capture: seek(0), seek(1/30), ...
    function play(from = 0) { start = performance.now() - from * 1000; playing = true; }
    function tick(now) {
      if (playing) { let x = (now - start) / 1000; if (x >= D) { if (rec) { x = D; playing = false; } else { x = 0; play(0); } } draw(x); }
      requestAnimationFrame(tick);
    }
    function setRec(on) { rec = on; document.body.classList.toggle("rec", on); seek(0); }
    $("b-play").onclick = () => (playing ? (playing = false) : play(t >= D ? 0 : t));
    $("b-replay").onclick = () => play(0);
    $("b-rec").onclick = () => setRec(true);
    scrub.oninput = () => seek(scrub.value / 1000 * D);
    stage.onclick = () => { if (rec) play(0); };
    addEventListener("keydown", (e) => {
      if (e.code === "Space") { e.preventDefault(); if (rec) play(0); else if (playing) playing = false; else play(t >= D ? 0 : t); }
      if (e.key === "r" || e.key === "R") setRec(!rec);
      if (e.key === "Escape") { setRec(false); play(0); }
    });
    addEventListener("resize", fit); fit(); build(); draw(0); requestAnimationFrame(tick);
    </script>
    </body>
    </html>
    ```
    
  • 06 Explainers

    Split-screen before/after

    Use it for Sales enablement

    Read the prompt
    Build a 15-second animated video in the "Split-screen before/after" style: a dark 'before' panel that a divider slides across to reveal the 'after', ending side by side.
    
    It's for sales enablement: the problem on one side, what changes on the other.
    
    MY CONTENT (fill these in; delete a line to keep the default)
    - What the video says, in one sentence: [e.g. "Inbound drove 45% of new pipeline this quarter"]
    - Numbers and labels: [a hero number plus 2 or 3 supporting numbers, each with a short label]
    - Source line: [where the numbers come from]
    - Brand colors: [hex codes for background, text and 2 or 3 accents]. Default: #6688ad, #545454, #292424, #7ba69d, #dc685a
    - Font: [font name]. Default: Poppins
    - Format: [16:9, 1:1 or 9:16]. Default: 16:9
    
    HOW TO BUILD IT
    1. Start from the reference code at the end of this message. It already works. Keep its structure: the CONFIG block, the fixed-size stage, build(), render(t) and the player at the bottom. Put my content, colors, font and format into CONFIG, then adjust the layout so everything fits.
    2. Every frame is a pure function of time: render(t) draws the frame for t seconds. Don't add CSS animations, transitions, setTimeout or setInterval. That's what lets the video be scrubbed, recorded cleanly and captured frame by frame.
    3. If you're in Claude Motion, make it a Motion animation and use the reference for the look and the timing. Anywhere else, return one self-contained HTML artifact with no external scripts. The Google Fonts link is the only outside request.
    4. Keep record mode working. The Record mode button (or the R key) hides every control and holds the first frame. Clicking the stage or pressing Space plays it once and holds the last frame. Esc exits.
    5. No sound. About 15 seconds, with the final frame held for at least 2 seconds.
    6. Text has to read on a phone. On the 1920x1080 stage nothing goes under 30px, labels are 40px or larger, and headline numbers 120px or larger. No text overlaps other text, runs off the stage, or leaves one word alone on the last line of a heading. If my content doesn't fit, cut words or add a scene. Don't shrink the type.
    7. Three items per side, each a number plus a label under 35 characters. Say on screen what each side's numbers are a share of.
    
    Optional: before writing any code, give me a five-line storyboard (time, what's on screen, what moves) and wait for my OK.
    
    When it's done, tell me in two lines how to get an MP4: open Record mode, start a screen recording of the stage (Cmd+Shift+5 on a Mac, or Loom), then press Space.
    
    REFERENCE CODE: Split-screen before/after
    ```html
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Split-screen before/after</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap">
    <style>
    /* Player: fixed-size stage, scaled to fit the window. Keep as is. */
    html,body{margin:0;height:100%;background:#111;overflow:hidden}
    #stage{position:absolute;left:50%;top:50%;overflow:hidden;box-sizing:border-box;background:var(--bg);color:var(--ink);font-family:var(--font)}
    #stage *{box-sizing:border-box}
    #stage::after{content:"";position:absolute;inset:0;background:var(--bg);opacity:var(--fade,0);pointer-events:none;z-index:99}
    #ui{position:fixed;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px;align-items:center;padding:8px 12px;border-radius:10px;background:rgba(0,0,0,.7);font:13px system-ui,sans-serif;color:#eee;z-index:9}
    #ui button{font:inherit;padding:5px 10px;border:0;border-radius:6px;cursor:pointer}
    body.rec #ui{display:none} body.rec{cursor:none}
    
    /* Style */
    .panel{position:absolute;inset:0}
    #before{background:var(--dark);color:#fff}
    #after{background:var(--bg);color:var(--ink)}
    .content{position:absolute;left:0;top:0;width:50%;height:100%;padding:100px 90px 140px;display:flex;flex-direction:column;justify-content:center;gap:34px}
    #after .content{left:50%}
    .chip{align-self:flex-start;font-size:34px;font-weight:700;letter-spacing:.1em;padding:10px 26px;border-radius:999px;color:#fff;background:var(--c3)}
    #after .chip{background:var(--c2-dark)}
    h2{margin:0;font-size:50px;line-height:1.2;font-weight:600;text-wrap:balance}
    .item{display:flex;align-items:center;gap:30px}
    .item b{flex:none;width:210px;font-size:100px;line-height:1;font-weight:700}
    #before .item b{color:var(--c3)} #after .item b{color:var(--c1)}
    .item span{font-size:42px;line-height:1.2;text-wrap:balance}
    .note{font-size:30px;line-height:1.35;color:var(--muted);text-wrap:balance}
    .divider{position:absolute;top:0;bottom:0;width:10px;margin-left:-5px;background:var(--ink)}
    .divider i{position:absolute;top:50%;left:50%;width:84px;height:84px;margin:-42px;border-radius:50%;background:var(--ink);color:#fff;font:700 44px/84px sans-serif;text-align:center;font-style:normal}
    .tall .content,.square .content{width:100%;height:50%;padding:70px 80px;gap:22px} .tall #after .content,.square #after .content{left:0;top:50%}
    .tall .divider,.square .divider{top:auto;left:0 !important;right:0;width:auto;height:10px;margin:-5px 0 0} .tall .divider i,.square .divider i{transform:rotate(90deg)}
    .square .item b{font-size:84px;width:180px} .square h2{font-size:46px}
    </style>
    </head>
    <body>
    <div id="stage">
    <div class="panel" id="before"><div class="content" id="bc"></div></div>
    <div class="panel" id="after"><div class="content" id="ac"></div></div>
    <div class="divider" id="divider"><i>&rarr;</i></div>
    </div>
    <div id="ui"><button id="b-play">Play / Pause</button><button id="b-replay">Replay</button><input id="scrub" type="range" min="0" max="1000" value="0"><button id="b-rec" title="Hides the controls. Click the stage or press Space to play once. Esc exits.">Record mode</button></div>
    
    <script>
    /* ---------- Your content: edit here ---------- */
    const CONFIG = {
      format: "16:9",            // "16:9" | "1:1" | "9:16"
      duration: 15,              // seconds
      font: "Poppins",
      colors: { bg: "#ffffff", dark: "#292424", ink: "#292424", muted: "#8a8484", "c2-dark": "#5c7c76", c1: "#6688ad", c2: "#7ba69d", c3: "#dc685a" },
      before: { chip: "BEFORE", heading: "Manual prospect research",
                items: [ { value: "6h", text: "Research per rep, every week" },
                         { value: "40", text: "Accounts researched per week" },
                         { value: "4%", text: "Reply rate on outbound" } ],
                note: "Per rep. Sample data" },
      after:  { chip: "AFTER", heading: "With an AI research assistant",
                items: [ { value: "1h", text: "Research per rep, every week" },
                         { value: "120", text: "Accounts researched per week" },
                         { value: "9%", text: "Reply rate on outbound" } ],
                note: "Same team, eight weeks later. Sample data" },
    };
    
    /* ---------- Helpers ---------- */
    const $ = (id) => document.getElementById(id);
    const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
    const prog = (t, start, dur) => clamp((t - start) / dur);      // 0 to 1 between start and start+dur
    const ease = (x) => 1 - Math.pow(1 - x, 3);                     // ease-out cubic
    const show = (el, x, dy = 30) => { el.style.opacity = x; el.style.transform = `translateY(${(1 - x) * dy}px)`; };
    const inout = (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2);
    const side = (s) => `<div class="chip">${s.chip}</div><h2>${s.heading}</h2>` + s.items.map((i) => `<div class="item"><b>${i.value}</b><span>${i.text}</span></div>`).join("") + `<div class="note">${s.note}</div>`;
    function reveal(el, t, start) {                                       // chip, heading, then items one by one
      [...el.children].forEach((c, i) => show(c, ease(prog(t, start + (i < 2 ? i * 0.3 : 0.6 + (i - 2) * 0.8), 0.7)), 40));
    }
    
    /* ---------- Build the DOM once from CONFIG ---------- */
    function build() { $("bc").innerHTML = side(CONFIG.before); $("ac").innerHTML = side(CONFIG.after); }
    
    /* ---------- Draw one frame for time t (seconds). Pure: same t, same frame. ---------- */
    function render(t) {
      const tall = H >= W, split = inout(prog(t, 5.0, 1.4));               // 0: before fills the stage, 1: half and half
      const edge = 100 - 50 * split;                                       // where the after panel starts, in %
      $("after").style.clipPath = tall ? `inset(${edge}% 0 0 0)` : `inset(0 0 0 ${edge}%)`;
      $("divider").style[tall ? "top" : "left"] = edge + "%";
      $("divider").style.opacity = t > 5.0 ? 1 : 0;
      $("bc").style.transform = tall ? `translateY(${(1 - split) * 50}%)` : `translateX(${(1 - split) * 50}%)`;
      reveal($("bc"), t, 0.3); reveal($("ac"), t, 6.4);
    }
    
    /* ---------- Player: keep as is ---------- */
    const SIZES = { "16:9": [1920, 1080], "1:1": [1440, 1440], "9:16": [1080, 1920] };
    const [W, H] = SIZES[CONFIG.format] || SIZES["16:9"], D = CONFIG.duration;
    const stage = $("stage"), scrub = $("scrub");
    stage.style.width = W + "px"; stage.style.height = H + "px";
    stage.classList.toggle("tall", H > W); stage.classList.toggle("square", H === W);
    for (const [k, v] of Object.entries(CONFIG.colors)) stage.style.setProperty("--" + k, v);
    stage.style.setProperty("--font", `"${CONFIG.font}", system-ui, sans-serif`);
    let t = 0, playing = true, rec = false, start = performance.now();
    const fit = () => { stage.style.transform = `translate(-50%,-50%) scale(${Math.min(innerWidth / W, innerHeight / H)})`; };
    function draw(time) { t = time; render(t); stage.style.setProperty("--fade", rec ? 0 : clamp((t - D + 0.5) / 0.5)); scrub.value = t / D * 1000; }
    window.seek = (time) => { playing = false; draw(time); };   // frame-by-frame capture: seek(0), seek(1/30), ...
    function play(from = 0) { start = performance.now() - from * 1000; playing = true; }
    function tick(now) {
      if (playing) { let x = (now - start) / 1000; if (x >= D) { if (rec) { x = D; playing = false; } else { x = 0; play(0); } } draw(x); }
      requestAnimationFrame(tick);
    }
    function setRec(on) { rec = on; document.body.classList.toggle("rec", on); seek(0); }
    $("b-play").onclick = () => (playing ? (playing = false) : play(t >= D ? 0 : t));
    $("b-replay").onclick = () => play(0);
    $("b-rec").onclick = () => setRec(true);
    scrub.oninput = () => seek(scrub.value / 1000 * D);
    stage.onclick = () => { if (rec) play(0); };
    addEventListener("keydown", (e) => {
      if (e.code === "Space") { e.preventDefault(); if (rec) play(0); else if (playing) playing = false; else play(t >= D ? 0 : t); }
      if (e.key === "r" || e.key === "R") setRec(!rec);
      if (e.key === "Escape") { setRec(false); play(0); }
    });
    addEventListener("resize", fit); fit(); build(); draw(0); requestAnimationFrame(tick);
    </script>
    </body>
    </html>
    ```
    
  • 07 Explainers

    Dashboard UI mock

    Use it for Product loop

    Read the prompt
    Build a 15-second animated video in the "Dashboard UI mock" style: a product screen where a cursor clicks a filter and the KPI cards and bars update, then a toast confirms the result.
    
    It's for a product loop: one click, and the numbers change.
    
    MY CONTENT (fill these in; delete a line to keep the default)
    - What the video says, in one sentence: [e.g. "Inbound drove 45% of new pipeline this quarter"]
    - Numbers and labels: [a hero number plus 2 or 3 supporting numbers, each with a short label]
    - Source line: [where the numbers come from]
    - Brand colors: [hex codes for background, text and 2 or 3 accents]. Default: #6688ad, #545454, #292424, #7ba69d, #dc685a
    - Font: [font name]. Default: Poppins
    - Format: [16:9, 1:1 or 9:16]. Default: 16:9
    
    HOW TO BUILD IT
    1. Start from the reference code at the end of this message. It already works. Keep its structure: the CONFIG block, the fixed-size stage, build(), render(t) and the player at the bottom. Put my content, colors, font and format into CONFIG, then adjust the layout so everything fits.
    2. Every frame is a pure function of time: render(t) draws the frame for t seconds. Don't add CSS animations, transitions, setTimeout or setInterval. That's what lets the video be scrubbed, recorded cleanly and captured frame by frame.
    3. If you're in Claude Motion, make it a Motion animation and use the reference for the look and the timing. Anywhere else, return one self-contained HTML artifact with no external scripts. The Google Fonts link is the only outside request.
    4. Keep record mode working. The Record mode button (or the R key) hides every control and holds the first frame. Clicking the stage or pressing Space plays it once and holds the last frame. Esc exits.
    5. No sound. About 15 seconds, with the final frame held for at least 2 seconds.
    6. Text has to read on a phone. On the 1920x1080 stage nothing goes under 30px, labels are 40px or larger, and headline numbers 120px or larger. No text overlaps other text, runs off the stage, or leaves one word alone on the last line of a heading. If my content doesn't fit, cut words or add a scene. Don't shrink the type.
    7. If I give you my product's screen names and labels, use them. Keep it to three KPI cards and three bars. The cursor clicks the second filter chip at 5.7 seconds; if you move the click, move the number change with it.
    
    Optional: before writing any code, give me a five-line storyboard (time, what's on screen, what moves) and wait for my OK.
    
    When it's done, tell me in two lines how to get an MP4: open Record mode, start a screen recording of the stage (Cmd+Shift+5 on a Mac, or Loom), then press Space.
    
    REFERENCE CODE: Dashboard UI mock
    ```html
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Dashboard UI mock</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap">
    <style>
    /* Player: fixed-size stage, scaled to fit the window. Keep as is. */
    html,body{margin:0;height:100%;background:#111;overflow:hidden}
    #stage{position:absolute;left:50%;top:50%;overflow:hidden;box-sizing:border-box;background:var(--bg);color:var(--ink);font-family:var(--font)}
    #stage *{box-sizing:border-box}
    #stage::after{content:"";position:absolute;inset:0;background:var(--bg);opacity:var(--fade,0);pointer-events:none;z-index:99}
    #ui{position:fixed;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px;align-items:center;padding:8px 12px;border-radius:10px;background:rgba(0,0,0,.7);font:13px system-ui,sans-serif;color:#eee;z-index:9}
    #ui button{font:inherit;padding:5px 10px;border:0;border-radius:6px;cursor:pointer}
    body.rec #ui{display:none} body.rec{cursor:none}
    
    /* Style */
    #stage{padding:56px;background:var(--desk)}
    .win{height:100%;display:flex;flex-direction:column;background:var(--bg);border-radius:24px;overflow:hidden;box-shadow:0 30px 80px rgba(0,0,0,.18)}
    .top{flex:none;height:84px;display:flex;align-items:center;gap:14px;padding:0 32px;background:var(--ink);color:#fff;font-size:34px;font-weight:600}
    .top i{width:20px;height:20px;border-radius:50%;background:#ffffff40}
    .top span{margin-left:20px}
    .top .sample{margin-left:auto;font-size:30px;font-weight:400;font-style:normal;opacity:.75}
    .app{flex:1;display:flex;min-height:0}
    .side{flex:none;width:300px;padding:40px 24px;background:var(--panel);display:flex;flex-direction:column;gap:10px;font-size:34px;color:var(--muted)}
    .side div{padding:14px 20px;border-radius:12px} .side .on{background:var(--c1);color:#fff;font-weight:600}
    .main{flex:1;min-width:0;padding:44px 56px;display:flex;flex-direction:column;gap:34px}
    .head{display:flex;align-items:center;gap:20px;flex-wrap:wrap}
    .head h2{margin:0 auto 0 0;font-size:56px;font-weight:700}
    .chip{font-size:34px;font-weight:600;padding:12px 28px;border-radius:999px;border:3px solid var(--line);color:var(--muted)}
    .chip.on{background:var(--ink);border-color:var(--ink);color:#fff}
    .kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:28px}
    .kpi{padding:26px 32px;border:3px solid var(--line);border-radius:18px}
    .kpi span{display:block;font-size:34px;color:var(--muted)}
    .kpi b{display:block;font-size:104px;line-height:1.05;font-weight:700}
    .card{flex:1;min-height:0;padding:28px 32px;border:3px solid var(--line);border-radius:18px;display:flex;flex-direction:column;gap:22px}
    .card h3{margin:0;font-size:36px;font-weight:600;color:var(--muted)}
    .bar{display:grid;grid-template-columns:700px 1fr 120px;align-items:center;gap:24px;font-size:40px}
    .track{height:30px;border-radius:15px;background:var(--panel)} .fill{height:100%;border-radius:15px;background:var(--c1)}
    .bar b{text-align:right}
    .cursor{position:absolute;left:0;top:0;width:56px;z-index:5}
    .click{position:absolute;width:120px;height:120px;margin:-60px;border-radius:50%;border:6px solid var(--c3);opacity:0;z-index:4}
    .toast{position:absolute;right:110px;bottom:100px;padding:28px 38px;border-radius:18px;background:var(--ink);color:#fff;font-size:42px;font-weight:600;z-index:3}
    .toast b{color:var(--c3-light)}
    .tall .side,.square .side{display:none} .tall .kpis{grid-template-columns:1fr} .tall .kpi b{font-size:96px}
    .tall .bar,.square .bar{grid-template-columns:1fr 100px} .tall .bar .track,.square .bar .track{grid-column:1/-1;grid-row:2}
    .tall .toast,.square .toast{left:110px;right:110px}
    </style>
    </head>
    <body>
    <div id="stage">
    <div class="win">
      <div class="top"><i></i><i></i><i></i><span id="app"></span><em class="sample" id="sample"></em></div>
      <div class="app">
        <div class="side" id="side"></div>
        <div class="main">
          <div class="head"><h2 id="h"></h2><div class="chip" id="chipA"></div><div class="chip" id="chipB"></div></div>
          <div class="kpis" id="kpis"></div>
          <div class="card"><h3 id="cardTitle"></h3><div id="bars"></div></div>
        </div>
      </div>
    </div>
    <div class="click" id="click"></div>
    <svg class="cursor" id="cursor" viewBox="0 0 24 24"><path d="M3 2l17 9-7.5 1.8L9 21z" fill="#fff" stroke="#292424" stroke-width="1.6" stroke-linejoin="round"/></svg>
    <div class="toast" id="toast"></div>
    </div>
    <div id="ui"><button id="b-play">Play / Pause</button><button id="b-replay">Replay</button><input id="scrub" type="range" min="0" max="1000" value="0"><button id="b-rec" title="Hides the controls. Click the stage or press Space to play once. Esc exits.">Record mode</button></div>
    
    <script>
    /* ---------- Your content: edit here ---------- */
    const CONFIG = {
      format: "16:9",            // "16:9" | "1:1" | "9:16"
      duration: 15,              // seconds
      font: "Poppins",
      colors: { bg: "#ffffff", desk: "#e9edf2", panel: "#f1f3f6", line: "#e1e5eb", ink: "#292424", muted: "#545454", c1: "#6688ad", c2: "#7ba69d", c3: "#dc685a", "c3-light": "#f2a197" },
      app: "Campaign Dashboard",
      nav: ["Overview", "Campaigns", "Channels", "Reports"],
      heading: "Overview",
      filters: ["Last 7 days", "Last 30 days"],   // the cursor clicks the second one
      // Each number animates from its "all" value to its "filtered" value after the click.
      kpis: [ { label: "MQLs", all: 96, filtered: 412 },
              { label: "Pipeline", all: 210, filtered: 940, pre: "$", suf: "K" },
              { label: "Cost per lead", all: 142, filtered: 118, pre: "$" } ],
      cardTitle: "MQLs by channel, share",
      bars: [ { label: "Paid search", all: 44, filtered: 38 },
              { label: "LinkedIn ads", all: 31, filtered: 34 },
              { label: "Webinars", all: 25, filtered: 28 } ],
      toast: "<b>+18%</b> MQLs vs. the previous 30 days",
      sample: "Sample data",
    };
    
    /* ---------- Helpers ---------- */
    const $ = (id) => document.getElementById(id);
    const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
    const prog = (t, start, dur) => clamp((t - start) / dur);      // 0 to 1 between start and start+dur
    const ease = (x) => 1 - Math.pow(1 - x, 3);                     // ease-out cubic
    const show = (el, x, dy = 30) => { el.style.opacity = x; el.style.transform = `translateY(${(1 - x) * dy}px)`; };
    const lerp = (a, b, x) => a + (b - a) * x;
    const inout = (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2);
    function spot(el, fx = 0.5, fy = 0.5) {                               // a point on an element, in stage pixels
      const s = $("stage").getBoundingClientRect(), r = el.getBoundingClientRect(), k = s.width / W;
      return [(r.left - s.left + r.width * fx) / k, (r.top - s.top + r.height * fy) / k];
    }
    
    /* ---------- Build the DOM once from CONFIG ---------- */
    function build() {
      $("app").textContent = CONFIG.app; $("sample").textContent = CONFIG.sample || ""; $("h").textContent = CONFIG.heading; $("cardTitle").textContent = CONFIG.cardTitle;
      $("chipA").textContent = CONFIG.filters[0]; $("chipB").textContent = CONFIG.filters[1]; $("toast").innerHTML = CONFIG.toast;
      $("side").innerHTML = CONFIG.nav.map((n, i) => `<div class="${i ? "" : "on"}">${n}</div>`).join("");
      $("kpis").innerHTML = CONFIG.kpis.map((k) => `<div class="kpi"><span>${k.label}</span><b></b></div>`).join("");
      $("bars").innerHTML = CONFIG.bars.map((b) => `<div class="bar"><span>${b.label}</span><div class="track"><div class="fill"></div></div><b></b></div>`).join("");
    }
    
    /* ---------- Draw one frame for time t (seconds). Pure: same t, same frame. ---------- */
    function render(t) {
      const win = ease(prog(t, 0, 0.8));
      document.querySelector(".win").style.cssText = `opacity:${win};transform:scale(${0.94 + 0.06 * win})`;
      const filt = ease(prog(t, 5.9, 1.3)), clicked = t >= 5.7;           // the click lands at 5.7s
      $("chipA").classList.toggle("on", !clicked); $("chipB").classList.toggle("on", clicked);
      [...document.querySelectorAll(".kpi b")].forEach((el, i) => {
        const k = CONFIG.kpis[i], v = lerp(k.all, k.filtered, filt) * ease(prog(t, 1.0 + i * 0.25, 1.4));
        el.textContent = (k.pre || "") + Math.round(v) + (k.suf || "");
      });
      [...document.querySelectorAll(".bar")].forEach((el, i) => {
        const b = CONFIG.bars[i], v = lerp(b.all, b.filtered, filt) * ease(prog(t, 1.8 + i * 0.25, 1.2));
        el.querySelector(".fill").style.width = v + "%"; el.querySelector("b").textContent = Math.round(v) + "%";
      });
      // Cursor: drifts in, travels to the second filter, clicks, then rests.
      const [cx, cy] = spot($("chipB"), 0.55, 0.6), m = inout(prog(t, 3.6, 1.8));
      const x = lerp(W * 0.82, cx, m), y = lerp(H * 0.92, cy, m), press = t > 5.55 && t < 5.8 ? 0.85 : 1;
      $("cursor").style.cssText = `opacity:${prog(t, 3.2, 0.4)};transform:translate(${x}px,${y}px) scale(${press})`;
      const r = prog(t, 5.7, 0.6);
      $("click").style.cssText = `left:${cx}px;top:${cy}px;opacity:${r > 0 && r < 1 ? 1 - r : 0};transform:scale(${0.3 + r})`;
      const toast = ease(prog(t, 8.2, 0.7));
      $("toast").style.opacity = toast; $("toast").style.transform = `translateY(${(1 - toast) * 60}px)`;
    }
    
    /* ---------- Player: keep as is ---------- */
    const SIZES = { "16:9": [1920, 1080], "1:1": [1440, 1440], "9:16": [1080, 1920] };
    const [W, H] = SIZES[CONFIG.format] || SIZES["16:9"], D = CONFIG.duration;
    const stage = $("stage"), scrub = $("scrub");
    stage.style.width = W + "px"; stage.style.height = H + "px";
    stage.classList.toggle("tall", H > W); stage.classList.toggle("square", H === W);
    for (const [k, v] of Object.entries(CONFIG.colors)) stage.style.setProperty("--" + k, v);
    stage.style.setProperty("--font", `"${CONFIG.font}", system-ui, sans-serif`);
    let t = 0, playing = true, rec = false, start = performance.now();
    const fit = () => { stage.style.transform = `translate(-50%,-50%) scale(${Math.min(innerWidth / W, innerHeight / H)})`; };
    function draw(time) { t = time; render(t); stage.style.setProperty("--fade", rec ? 0 : clamp((t - D + 0.5) / 0.5)); scrub.value = t / D * 1000; }
    window.seek = (time) => { playing = false; draw(time); };   // frame-by-frame capture: seek(0), seek(1/30), ...
    function play(from = 0) { start = performance.now() - from * 1000; playing = true; }
    function tick(now) {
      if (playing) { let x = (now - start) / 1000; if (x >= D) { if (rec) { x = D; playing = false; } else { x = 0; play(0); } } draw(x); }
      requestAnimationFrame(tick);
    }
    function setRec(on) { rec = on; document.body.classList.toggle("rec", on); seek(0); }
    $("b-play").onclick = () => (playing ? (playing = false) : play(t >= D ? 0 : t));
    $("b-replay").onclick = () => play(0);
    $("b-rec").onclick = () => setRec(true);
    scrub.oninput = () => seek(scrub.value / 1000 * D);
    stage.onclick = () => { if (rec) play(0); };
    addEventListener("keydown", (e) => {
      if (e.code === "Space") { e.preventDefault(); if (rec) play(0); else if (playing) playing = false; else play(t >= D ? 0 : t); }
      if (e.key === "r" || e.key === "R") setRec(!rec);
      if (e.key === "Escape") { setRec(false); play(0); }
    });
    addEventListener("resize", fit); fit(); build(); draw(0); requestAnimationFrame(tick);
    </script>
    </body>
    </html>
    ```
    
  • 08 Account plays

    Signal feed / news ticker

    Use it for Top-account video

    Read the prompt
    Build a 15-second animated video in the "Signal feed / news ticker" style: signal cards that drop into a live feed and push the older ones down, with a ticker running along the bottom.
    
    It's for a top-account video: the signals that make one account worth a call this week.
    
    MY CONTENT (fill these in; delete a line to keep the default)
    - What the video says, in one sentence: [e.g. "Inbound drove 45% of new pipeline this quarter"]
    - Numbers and labels: [a hero number plus 2 or 3 supporting numbers, each with a short label]
    - Source line: [where the numbers come from]
    - Brand colors: [hex codes for background, text and 2 or 3 accents]. Default: #6688ad, #545454, #292424, #7ba69d, #dc685a
    - Font: [font name]. Default: Poppins
    - Format: [16:9, 1:1 or 9:16]. Default: 16:9
    
    HOW TO BUILD IT
    1. Start from the reference code at the end of this message. It already works. Keep its structure: the CONFIG block, the fixed-size stage, build(), render(t) and the player at the bottom. Put my content, colors, font and format into CONFIG, then adjust the layout so everything fits.
    2. Every frame is a pure function of time: render(t) draws the frame for t seconds. Don't add CSS animations, transitions, setTimeout or setInterval. That's what lets the video be scrubbed, recorded cleanly and captured frame by frame.
    3. If you're in Claude Motion, make it a Motion animation and use the reference for the look and the timing. Anywhere else, return one self-contained HTML artifact with no external scripts. The Google Fonts link is the only outside request.
    4. Keep record mode working. The Record mode button (or the R key) hides every control and holds the first frame. Clicking the stage or pressing Space plays it once and holds the last frame. Esc exits.
    5. No sound. About 15 seconds, with the final frame held for at least 2 seconds.
    6. Text has to read on a phone. On the 1920x1080 stage nothing goes under 30px, labels are 40px or larger, and headline numbers 120px or larger. No text overlaps other text, runs off the stage, or leaves one word alone on the last line of a heading. If my content doesn't fit, cut words or add a scene. Don't shrink the type.
    7. Four or five signals, each with a tag, a headline under 45 characters and a short meta line. For a single account, put the account name in the title and one signal per card.
    
    Optional: before writing any code, give me a five-line storyboard (time, what's on screen, what moves) and wait for my OK.
    
    When it's done, tell me in two lines how to get an MP4: open Record mode, start a screen recording of the stage (Cmd+Shift+5 on a Mac, or Loom), then press Space.
    
    REFERENCE CODE: Signal feed / news ticker
    ```html
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Signal feed / news ticker</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap">
    <style>
    /* Player: fixed-size stage, scaled to fit the window. Keep as is. */
    html,body{margin:0;height:100%;background:#111;overflow:hidden}
    #stage{position:absolute;left:50%;top:50%;overflow:hidden;box-sizing:border-box;background:var(--bg);color:var(--ink);font-family:var(--font)}
    #stage *{box-sizing:border-box}
    #stage::after{content:"";position:absolute;inset:0;background:var(--bg);opacity:var(--fade,0);pointer-events:none;z-index:99}
    #ui{position:fixed;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px;align-items:center;padding:8px 12px;border-radius:10px;background:rgba(0,0,0,.7);font:13px system-ui,sans-serif;color:#eee;z-index:9}
    #ui button{font:inherit;padding:5px 10px;border:0;border-radius:6px;cursor:pointer}
    body.rec #ui{display:none} body.rec{cursor:none}
    
    /* Style */
    #stage{display:grid;grid-template-columns:680px 1fr;grid-template-rows:minmax(0,1fr) 110px}
    .intro{padding:90px 40px 60px 100px;display:flex;flex-direction:column;gap:22px}
    .live{display:flex;align-items:center;gap:16px;font-size:34px;font-weight:700;letter-spacing:.12em;color:var(--c3)}
    .live i{width:24px;height:24px;border-radius:50%;background:var(--c3)}
    h1{margin:0;font-size:84px;line-height:1.05;font-weight:700}
    .sub{font-size:42px;line-height:1.3;color:var(--muted);text-wrap:balance}
    .count{margin-top:auto;font-size:34px;color:var(--muted)} .count b{display:block;font-size:110px;line-height:1;color:var(--ink)}
    .feed{position:relative;overflow:hidden;margin:50px 90px 0 0}
    .card{position:absolute;left:0;right:0;top:0;height:206px;padding:22px 34px;background:#fff;border-radius:20px;border-left:12px solid var(--c1);box-shadow:0 8px 30px rgba(0,0,0,.07);display:flex;flex-direction:column;justify-content:center;gap:8px}
    .card .tag{font-size:30px;font-weight:700;letter-spacing:.1em;color:var(--c1)}
    .card b{font-size:46px;line-height:1.2;font-weight:600}
    .card span{font-size:32px;color:var(--muted)}
    .ticker{grid-column:1/-1;background:var(--ink);color:#fff;display:flex;align-items:center;overflow:hidden;font-size:40px;font-weight:600;white-space:nowrap}
    .ticker .label{flex:none;align-self:stretch;display:flex;align-items:center;padding:0 36px;background:var(--c3);z-index:1}
    .ticker .run{display:flex}
    .ticker .run span{padding-right:80px}
    #stage.tall,#stage.square{grid-template-columns:1fr;grid-template-rows:auto minmax(0,1fr) 110px}
    .tall .intro,.square .intro{padding:80px 70px 20px} .tall .count,.square .count{margin-top:10px} .tall .feed,.square .feed{margin:20px 60px 40px}
    .square .intro{display:grid;grid-template-columns:1fr auto;column-gap:60px} .square .count{grid-column:2;grid-row:1/4;align-self:end}
    .tall .card{height:230px}
    </style>
    </head>
    <body>
    <div id="stage">
    <div class="intro">
      <div class="live"><i id="dot"></i><span id="live"></span></div>
      <h1 id="title"></h1>
      <div class="sub" id="sub"></div>
      <div class="count" id="countBox"><b id="count"></b><span id="countLabel"></span></div>
    </div>
    <div class="feed" id="feed"></div>
    <div class="ticker"><div class="label" id="tickerLabel"></div><div class="run" id="run"></div></div>
    </div>
    <div id="ui"><button id="b-play">Play / Pause</button><button id="b-replay">Replay</button><input id="scrub" type="range" min="0" max="1000" value="0"><button id="b-rec" title="Hides the controls. Click the stage or press Space to play once. Esc exits.">Record mode</button></div>
    
    <script>
    /* ---------- Your content: edit here ---------- */
    const CONFIG = {
      format: "16:9",            // "16:9" | "1:1" | "9:16"
      duration: 15,              // seconds
      font: "Poppins",
      colors: { bg: "#eef1f5", ink: "#292424", muted: "#545454", c1: "#6688ad", c2: "#7ba69d", c3: "#dc685a" },
      live: "LIVE",
      title: "Ostrafield",
      sub: "Logistics software, 240 employees. Signals this month",
      count: { value: 92, label: "account fit score" },
      // Newest arrives on top. `color` is one of the colors above.
      signals: [ { at: 0.8, tag: "NEW EXEC", color: "c1", title: "Hired a new VP of Sales", meta: "Started three weeks ago" },
                 { at: 3.0, tag: "FUNDING", color: "c2", title: "Raised a $24M Series B", meta: "Announced two weeks ago" },
                 { at: 5.2, tag: "HIRING", color: "c1", title: "Four open SDR roles", meta: "Posted in the last 10 days" },
                 { at: 7.4, tag: "INTENT", color: "c3", title: "Pricing page visits up 3x", meta: "Six visitors in the last week" },
                 { at: 9.6, tag: "ENGAGED", color: "c3", title: "Downloaded the ROI guide", meta: "Two people, yesterday" } ],
      tickerLabel: "ACCOUNT WATCH",
      ticker: ["Series B: $24M", "Employees: 240", "Open SDR roles: 4", "Pricing page: 3x visits", "Fit score: 92"],
      source: "Sample data",
    };
    
    /* ---------- Helpers ---------- */
    const $ = (id) => document.getElementById(id);
    const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
    const prog = (t, start, dur) => clamp((t - start) / dur);      // 0 to 1 between start and start+dur
    const ease = (x) => 1 - Math.pow(1 - x, 3);                     // ease-out cubic
    const show = (el, x, dy = 30) => { el.style.opacity = x; el.style.transform = `translateY(${(1 - x) * dy}px)`; };
    
    /* ---------- Build the DOM once from CONFIG ---------- */
    let loopWidth = 1;
    function build() {
      $("live").textContent = CONFIG.live; $("title").textContent = CONFIG.title; $("sub").textContent = CONFIG.sub;
      $("countLabel").textContent = CONFIG.count.label; $("tickerLabel").textContent = CONFIG.tickerLabel;
      for (const s of CONFIG.signals) $("feed").insertAdjacentHTML("beforeend",
        `<div class="card" style="border-color:var(--${s.color})"><div class="tag" style="color:var(--${s.color})">${s.tag}</div><b>${s.title}</b><span>${s.meta}</span></div>`);
      const once = [...CONFIG.ticker, CONFIG.source].map((x) => `<span>${x}</span>`).join("");
      $("run").innerHTML = once + once + once;
      loopWidth = [...$("run").children].slice(0, CONFIG.ticker.length + 1).reduce((w, s) => w + s.offsetWidth, 0);
    }
    
    /* ---------- Draw one frame for time t (seconds). Pure: same t, same frame. ---------- */
    function render(t) {
      show($("title"), ease(prog(t, 0.2, 0.7))); show($("sub"), ease(prog(t, 0.4, 0.7))); show($("live").parentNode, prog(t, 0.1, 0.5), 0);
      $("dot").style.opacity = 0.35 + 0.65 * Math.abs(Math.cos(t * Math.PI * 0.9));   // pulsing live dot
      show($("countBox"), ease(prog(t, 1.0, 0.7)));
      $("count").textContent = Math.round(CONFIG.count.value * ease(prog(t, 1.0, 1.6)));
      const cards = [...document.querySelectorAll(".card")], step = cards[0].offsetHeight + 26;
      cards.forEach((card, i) => {
        const s = CONFIG.signals[i], p = ease(prog(t, s.at, 0.6));
        // Each newer signal pushes this card down one slot.
        const slot = CONFIG.signals.slice(i + 1).reduce((n, o) => n + ease(prog(t, o.at, 0.6)), 0);
        card.style.opacity = p; card.style.transform = `translate(${(1 - p) * 120}px, ${slot * step}px) scale(${0.96 + 0.04 * p})`;
      });
      $("run").style.transform = `translateX(${-((t * 110) % loopWidth)}px)`;
    }
    
    /* ---------- Player: keep as is ---------- */
    const SIZES = { "16:9": [1920, 1080], "1:1": [1440, 1440], "9:16": [1080, 1920] };
    const [W, H] = SIZES[CONFIG.format] || SIZES["16:9"], D = CONFIG.duration;
    const stage = $("stage"), scrub = $("scrub");
    stage.style.width = W + "px"; stage.style.height = H + "px";
    stage.classList.toggle("tall", H > W); stage.classList.toggle("square", H === W);
    for (const [k, v] of Object.entries(CONFIG.colors)) stage.style.setProperty("--" + k, v);
    stage.style.setProperty("--font", `"${CONFIG.font}", system-ui, sans-serif`);
    let t = 0, playing = true, rec = false, start = performance.now();
    const fit = () => { stage.style.transform = `translate(-50%,-50%) scale(${Math.min(innerWidth / W, innerHeight / H)})`; };
    function draw(time) { t = time; render(t); stage.style.setProperty("--fade", rec ? 0 : clamp((t - D + 0.5) / 0.5)); scrub.value = t / D * 1000; }
    window.seek = (time) => { playing = false; draw(time); };   // frame-by-frame capture: seek(0), seek(1/30), ...
    function play(from = 0) { start = performance.now() - from * 1000; playing = true; }
    function tick(now) {
      if (playing) { let x = (now - start) / 1000; if (x >= D) { if (rec) { x = D; playing = false; } else { x = 0; play(0); } } draw(x); }
      requestAnimationFrame(tick);
    }
    function setRec(on) { rec = on; document.body.classList.toggle("rec", on); seek(0); }
    $("b-play").onclick = () => (playing ? (playing = false) : play(t >= D ? 0 : t));
    $("b-replay").onclick = () => play(0);
    $("b-rec").onclick = () => setRec(true);
    scrub.oninput = () => seek(scrub.value / 1000 * D);
    stage.onclick = () => { if (rec) play(0); };
    addEventListener("keydown", (e) => {
      if (e.code === "Space") { e.preventDefault(); if (rec) play(0); else if (playing) playing = false; else play(t >= D ? 0 : t); }
      if (e.key === "r" || e.key === "R") setRec(!rec);
      if (e.key === "Escape") { setRec(false); play(0); }
    });
    addEventListener("resize", fit); fit(); build(); draw(0); requestAnimationFrame(tick);
    </script>
    </body>
    </html>
    ```
    
  • 09 Data stories

    Editorial

    Use it for 20-second case study

    Read the prompt
    Build a 15-second animated video in the "Editorial" style: bold type and solid color blocks that wipe across the frame, magazine style, one number per scene.
    
    It's for a 20-second case study.
    
    MY CONTENT (fill these in; delete a line to keep the default)
    - What the video says, in one sentence: [e.g. "Inbound drove 45% of new pipeline this quarter"]
    - Numbers and labels: [a hero number plus 2 or 3 supporting numbers, each with a short label]
    - Source line: [where the numbers come from]
    - Brand colors: [hex codes for background, text and 2 or 3 accents]. Default: #6688ad, #545454, #292424, #7ba69d, #dc685a
    - Font: [font name]. Default: Poppins
    - Format: [16:9, 1:1 or 9:16]. Default: 16:9
    
    HOW TO BUILD IT
    1. Start from the reference code at the end of this message. It already works. Keep its structure: the CONFIG block, the fixed-size stage, build(), render(t) and the player at the bottom. Put my content, colors, font and format into CONFIG, then adjust the layout so everything fits.
    2. Every frame is a pure function of time: render(t) draws the frame for t seconds. Don't add CSS animations, transitions, setTimeout or setInterval. That's what lets the video be scrubbed, recorded cleanly and captured frame by frame.
    3. If you're in Claude Motion, make it a Motion animation and use the reference for the look and the timing. Anywhere else, return one self-contained HTML artifact with no external scripts. The Google Fonts link is the only outside request.
    4. Keep record mode working. The Record mode button (or the R key) hides every control and holds the first frame. Clicking the stage or pressing Space plays it once and holds the last frame. Esc exits.
    5. No sound. About 15 seconds, with the final frame held for at least 2 seconds.
    6. Text has to read on a phone. On the 1920x1080 stage nothing goes under 30px, labels are 40px or larger, and headline numbers 120px or larger. No text overlaps other text, runs off the stage, or leaves one word alone on the last line of a heading. If my content doesn't fit, cut words or add a scene. Don't shrink the type.
    7. Three scenes, one number and one sentence each, alternating the block's side. For a case study: the situation, what changed, the result. Set duration to 20 if you need the extra beat.
    
    Optional: before writing any code, give me a five-line storyboard (time, what's on screen, what moves) and wait for my OK.
    
    When it's done, tell me in two lines how to get an MP4: open Record mode, start a screen recording of the stage (Cmd+Shift+5 on a Mac, or Loom), then press Space.
    
    REFERENCE CODE: Editorial
    ```html
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Editorial</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap">
    <style>
    /* Player: fixed-size stage, scaled to fit the window. Keep as is. */
    html,body{margin:0;height:100%;background:#111;overflow:hidden}
    #stage{position:absolute;left:50%;top:50%;overflow:hidden;box-sizing:border-box;background:var(--bg);color:var(--ink);font-family:var(--font)}
    #stage *{box-sizing:border-box}
    #stage::after{content:"";position:absolute;inset:0;background:var(--bg);opacity:var(--fade,0);pointer-events:none;z-index:99}
    #ui{position:fixed;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px;align-items:center;padding:8px 12px;border-radius:10px;background:rgba(0,0,0,.7);font:13px system-ui,sans-serif;color:#eee;z-index:9}
    #ui button{font:inherit;padding:5px 10px;border:0;border-radius:6px;cursor:pointer}
    body.rec #ui{display:none} body.rec{cursor:none}
    
    /* Style */
    .scene{position:absolute;inset:0;background:var(--bg)}
    .block{position:absolute;top:0;bottom:0;display:flex;align-items:center;justify-content:center;overflow:hidden}
    .num{font-size:300px;font-weight:700;line-height:1;color:#fff;letter-spacing:-.03em;white-space:nowrap}
    .copy{position:absolute;top:0;bottom:0;width:55%;padding:90px 100px;display:flex;flex-direction:column;justify-content:center;gap:28px}
    .kicker{font-size:36px;font-weight:700;letter-spacing:.14em}
    .text{font-size:76px;line-height:1.15;font-weight:700;text-wrap:balance}
    .rule{height:12px;width:160px}
    .source{position:absolute;bottom:50px;max-width:860px;font-size:30px;color:var(--muted);z-index:50}
    .tall .copy{top:auto;left:0 !important;right:0;width:auto;height:58%;padding:70px 80px}
    .tall .block{left:0 !important;right:0 !important;width:auto !important;top:0;bottom:auto}
    .tall .text{font-size:70px} .tall .num{font-size:280px} .square .num{font-size:230px}
    </style>
    </head>
    <body>
    <div id="stage">
    <div id="scenes"></div>
    <div class="source" id="source"></div>
    </div>
    <div id="ui"><button id="b-play">Play / Pause</button><button id="b-replay">Replay</button><input id="scrub" type="range" min="0" max="1000" value="0"><button id="b-rec" title="Hides the controls. Click the stage or press Space to play once. Esc exits.">Record mode</button></div>
    
    <script>
    /* ---------- Your content: edit here ---------- */
    const CONFIG = {
      format: "16:9",            // "16:9" | "1:1" | "9:16"
      duration: 15,              // seconds
      font: "Poppins",
      colors: { bg: "#ffffff", ink: "#292424", muted: "#545454", c1: "#6688ad", c2: "#7ba69d", c3: "#dc685a" },
      // Each scene: a color block wipes across the stage, settles on one side, and the copy sets on the other.
      scenes: [ { at: 0.2, color: "c3", side: "left", number: "48h", kicker: "BEFORE",
                  text: "to answer a demo request, on average" },
                { at: 5.0, color: "c1", side: "right", number: "2h", kicker: "AFTER",
                  text: "once routing and rep alerts went live" },
                { at: 9.8, color: "ink", side: "left", number: "+38%", kicker: "THE RESULT",
                  text: "more meetings booked the next quarter" } ],
      blockSize: 45,             // % of the stage the block settles at
      source: "Sample data",
    };
    
    /* ---------- Helpers ---------- */
    const $ = (id) => document.getElementById(id);
    const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
    const prog = (t, start, dur) => clamp((t - start) / dur);      // 0 to 1 between start and start+dur
    const ease = (x) => 1 - Math.pow(1 - x, 3);                     // ease-out cubic
    const show = (el, x, dy = 30) => { el.style.opacity = x; el.style.transform = `translateY(${(1 - x) * dy}px)`; };
    
    /* ---------- Build the DOM once from CONFIG ---------- */
    function build() {
      $("source").textContent = CONFIG.source;
      CONFIG.scenes.forEach((s, i) => $("scenes").insertAdjacentHTML("beforeend",
        `<div class="scene" style="z-index:${i + 1}"><div class="block" style="background:var(--${s.color});${s.side}:0"><div class="num">${s.number}</div></div>` +
        `<div class="copy" style="${s.side === "left" ? "right" : "left"}:0"><div class="kicker" style="color:var(--${s.color})">${s.kicker}</div>` +
        `<div class="text">${s.text}</div><div class="rule" style="background:var(--${s.color})"></div></div></div>`));
    }
    
    /* ---------- Draw one frame for time t (seconds). Pure: same t, same frame. ---------- */
    function render(t) {
      const tall = H > W;
      [...document.querySelectorAll(".scene")].forEach((el, i) => {
        const s = CONFIG.scenes[i], cover = ease(prog(t, s.at, 0.5)), settle = ease(prog(t, s.at + 0.55, 0.65));
        const size = cover < 1 ? 100 * cover : 100 - (100 - CONFIG.blockSize) * settle;  // wipe across, then pull back
        el.style.visibility = t < s.at ? "hidden" : "visible";
        el.style.background = settle > 0 ? "var(--bg)" : "transparent";
        const block = el.querySelector(".block");
        block.style[tall ? "height" : "width"] = size + "%";
        const n = ease(prog(t, s.at + 0.8, 0.6));
        block.firstChild.style.cssText = `opacity:${n};transform:scale(${0.85 + 0.15 * n})`;
        const kids = [...el.querySelector(".copy").children];
        kids.forEach((k, j) => show(k, ease(prog(t, s.at + 1.0 + j * 0.25, 0.7)), 50));
        kids[2].style.width = 160 * ease(prog(t, s.at + 1.5, 0.6)) + "px";
      });
      const last = [...CONFIG.scenes].reverse().find((s) => t >= s.at) || CONFIG.scenes[0];  // source sits on the copy side
      const away = Math.min(...CONFIG.scenes.map((s) => clamp(Math.abs(t - s.at - 0.6) / 0.25 - 2.6)));  // hidden during each wipe
      $("source").style.cssText = `opacity:${Math.min(prog(t, 1.5, 0.8), away)};${H > W ? "left:80px" : last.side === "left" ? `left:calc(${CONFIG.blockSize}% + 100px)` : "left:100px"}`;
    }
    
    /* ---------- Player: keep as is ---------- */
    const SIZES = { "16:9": [1920, 1080], "1:1": [1440, 1440], "9:16": [1080, 1920] };
    const [W, H] = SIZES[CONFIG.format] || SIZES["16:9"], D = CONFIG.duration;
    const stage = $("stage"), scrub = $("scrub");
    stage.style.width = W + "px"; stage.style.height = H + "px";
    stage.classList.toggle("tall", H > W); stage.classList.toggle("square", H === W);
    for (const [k, v] of Object.entries(CONFIG.colors)) stage.style.setProperty("--" + k, v);
    stage.style.setProperty("--font", `"${CONFIG.font}", system-ui, sans-serif`);
    let t = 0, playing = true, rec = false, start = performance.now();
    const fit = () => { stage.style.transform = `translate(-50%,-50%) scale(${Math.min(innerWidth / W, innerHeight / H)})`; };
    function draw(time) { t = time; render(t); stage.style.setProperty("--fade", rec ? 0 : clamp((t - D + 0.5) / 0.5)); scrub.value = t / D * 1000; }
    window.seek = (time) => { playing = false; draw(time); };   // frame-by-frame capture: seek(0), seek(1/30), ...
    function play(from = 0) { start = performance.now() - from * 1000; playing = true; }
    function tick(now) {
      if (playing) { let x = (now - start) / 1000; if (x >= D) { if (rec) { x = D; playing = false; } else { x = 0; play(0); } } draw(x); }
      requestAnimationFrame(tick);
    }
    function setRec(on) { rec = on; document.body.classList.toggle("rec", on); seek(0); }
    $("b-play").onclick = () => (playing ? (playing = false) : play(t >= D ? 0 : t));
    $("b-replay").onclick = () => play(0);
    $("b-rec").onclick = () => setRec(true);
    scrub.oninput = () => seek(scrub.value / 1000 * D);
    stage.onclick = () => { if (rec) play(0); };
    addEventListener("keydown", (e) => {
      if (e.code === "Space") { e.preventDefault(); if (rec) play(0); else if (playing) playing = false; else play(t >= D ? 0 : t); }
      if (e.key === "r" || e.key === "R") setRec(!rec);
      if (e.key === "Escape") { setRec(false); play(0); }
    });
    addEventListener("resize", fit); fit(); build(); draw(0); requestAnimationFrame(tick);
    </script>
    </body>
    </html>
    ```
    
  • 10 Explainers

    Blueprint

    Use it for “How it works”

    Read the prompt
    Build a 15-second animated video in the "Blueprint" style: a system diagram drawn in white lines on a blueprint grid, with signal pulses running along the wires.
    
    It's for a “how it works” explainer.
    
    MY CONTENT (fill these in; delete a line to keep the default)
    - What the video says, in one sentence: [e.g. "Inbound drove 45% of new pipeline this quarter"]
    - Numbers and labels: [a hero number plus 2 or 3 supporting numbers, each with a short label]
    - Source line: [where the numbers come from]
    - Brand colors: [hex codes for background, text and 2 or 3 accents]. Default: #6688ad, #545454, #292424, #7ba69d, #dc685a
    - Font: [font name]. Default: Poppins
    - Format: [16:9, 1:1 or 9:16]. Default: 16:9
    
    HOW TO BUILD IT
    1. Start from the reference code at the end of this message. It already works. Keep its structure: the CONFIG block, the fixed-size stage, build(), render(t) and the player at the bottom. Put my content, colors, font and format into CONFIG, then adjust the layout so everything fits.
    2. Every frame is a pure function of time: render(t) draws the frame for t seconds. Don't add CSS animations, transitions, setTimeout or setInterval. That's what lets the video be scrubbed, recorded cleanly and captured frame by frame.
    3. If you're in Claude Motion, make it a Motion animation and use the reference for the look and the timing. Anywhere else, return one self-contained HTML artifact with no external scripts. The Google Fonts link is the only outside request.
    4. Keep record mode working. The Record mode button (or the R key) hides every control and holds the first frame. Clicking the stage or pressing Space plays it once and holds the last frame. Esc exits.
    5. No sound. About 15 seconds, with the final frame held for at least 2 seconds.
    6. Text has to read on a phone. On the 1920x1080 stage nothing goes under 30px, labels are 40px or larger, and headline numbers 120px or larger. No text overlaps other text, runs off the stage, or leaves one word alone on the last line of a heading. If my content doesn't fit, cut words or add a scene. Don't shrink the type.
    7. Node positions are in pixels, for 16:9 and 9:16, in CONFIG.nodes. If you add or move a node, set both positions and keep the wires running between box edges. Up to six nodes reads well.
    
    Optional: before writing any code, give me a five-line storyboard (time, what's on screen, what moves) and wait for my OK.
    
    When it's done, tell me in two lines how to get an MP4: open Record mode, start a screen recording of the stage (Cmd+Shift+5 on a Mac, or Loom), then press Space.
    
    REFERENCE CODE: Blueprint
    ```html
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Blueprint</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap">
    <style>
    /* Player: fixed-size stage, scaled to fit the window. Keep as is. */
    html,body{margin:0;height:100%;background:#111;overflow:hidden}
    #stage{position:absolute;left:50%;top:50%;overflow:hidden;box-sizing:border-box;background:var(--bg);color:var(--ink);font-family:var(--font)}
    #stage *{box-sizing:border-box}
    #stage::after{content:"";position:absolute;inset:0;background:var(--bg);opacity:var(--fade,0);pointer-events:none;z-index:99}
    #ui{position:fixed;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px;align-items:center;padding:8px 12px;border-radius:10px;background:rgba(0,0,0,.7);font:13px system-ui,sans-serif;color:#eee;z-index:9}
    #ui button{font:inherit;padding:5px 10px;border:0;border-radius:6px;cursor:pointer}
    body.rec #ui{display:none} body.rec{cursor:none}
    
    /* Style */
    #stage{background-color:var(--bg);background-image:linear-gradient(var(--grid) 2px,transparent 2px),linear-gradient(90deg,var(--grid) 2px,transparent 2px),linear-gradient(var(--grid2) 1px,transparent 1px),linear-gradient(90deg,var(--grid2) 1px,transparent 1px);background-size:200px 200px,200px 200px,40px 40px,40px 40px;color:var(--line)}
    #sheet{position:absolute;left:0;top:0;transform-origin:0 0}
    .head{position:absolute;left:110px;right:110px;top:80px}
    h1{margin:0;font-size:60px;line-height:1.15;font-weight:600;text-wrap:balance}
    .fig{margin-top:10px;font-size:30px;letter-spacing:.14em;color:var(--dim)}
    .tall .head{left:80px;right:80px}
    svg{position:absolute;inset:0;overflow:visible}
    svg .ln{fill:none;stroke:var(--line);stroke-width:4;stroke-dasharray:1 1}
    svg .hot{stroke:var(--c3)}
    svg circle{fill:var(--c2)}
    .node{position:absolute;display:flex;flex-direction:column;justify-content:center;padding:20px 32px}
    .node b{font-size:84px;line-height:1;font-weight:700}
    .node span{font-size:38px;line-height:1.2;margin-top:8px}
    .node.out b{font-size:64px} .node.out span{font-size:36px;margin-top:4px}
    .node.hot b{color:var(--c3)}
    .node.gate{align-items:center;text-align:center;padding:0}
    .node.gate span{font-size:32px;line-height:1.15;font-weight:600;margin:0;padding:0 60px}
    .stamp{position:absolute;right:90px;bottom:70px;display:flex;border:3px solid var(--line);font-size:30px;letter-spacing:.08em}
    .stamp div{padding:14px 22px;border-left:3px solid var(--line)} .stamp div:first-child{border:0}
    </style>
    </head>
    <body>
    <div id="stage">
    <div id="sheet">
      <div class="head"><h1 id="title"></h1><div class="fig" id="fig"></div></div>
      <svg id="wires"></svg>
      <div id="nodes"></div>
      <div class="stamp" id="stamp"></div>
    </div>
    </div>
    <div id="ui"><button id="b-play">Play / Pause</button><button id="b-replay">Replay</button><input id="scrub" type="range" min="0" max="1000" value="0"><button id="b-rec" title="Hides the controls. Click the stage or press Space to play once. Esc exits.">Record mode</button></div>
    
    <script>
    /* ---------- Your content: edit here ---------- */
    const CONFIG = {
      format: "16:9",            // "16:9" | "1:1" | "9:16"
      duration: 15,              // seconds
      font: "Poppins",
      colors: { bg: "#1f3b5a", line: "#ffffff", dim: "#a9bfd6", grid: "rgba(255,255,255,.10)", grid2: "rgba(255,255,255,.05)", c1: "#6688ad", c2: "#7ba69d", c3: "#f08a7e" },
      title: "How inbound lead routing works",
      fig: "FIG. 1   FROM FORM FILL TO REP ALERT",
      // Boxes in stage pixels for 16:9 (wide) and 9:16 (tall). kind: "box", "gate" (diamond) or "out".
      nodes: [
        { id: "all",  kind: "box",  value: "Form", text: "A demo request comes in", wide: [110, 440, 420, 260], tall: [180, 330, 720, 220] },
        { id: "gate", kind: "gate", text: "Enrich and score",                     wide: [600, 440, 260, 260], tall: [410, 620, 260, 260] },
        { id: "icp",  kind: "box",  value: "CRM", text: "Lead created, owner set",  wide: [920, 440, 360, 260], tall: [240, 950, 600, 200] },
        { id: "p1",   kind: "out",  value: "Alert", text: "Rep gets a Slack ping in 2 min", hot: true, wide: [1400, 250, 430, 190], tall: [200, 1200, 760, 160] },
        { id: "p2",   kind: "out",  value: "Task", text: "Call task due within the hour", wide: [1400, 475, 430, 190], tall: [200, 1390, 760, 160] },
        { id: "p3",   kind: "out",  value: "Nurture", text: "Low scores go to email", wide: [1400, 700, 430, 190], tall: [200, 1580, 760, 160] },
      ],
      wires: [["all", "gate"], ["gate", "icp"], ["icp", "p1"], ["icp", "p2"], ["icp", "p3"]],
      stamp: ["INBOUND LEAD ROUTING", "SAMPLE DATA", "REV A"],
    };
    
    /* ---------- Helpers ---------- */
    const $ = (id) => document.getElementById(id);
    const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
    const prog = (t, start, dur) => clamp((t - start) / dur);      // 0 to 1 between start and start+dur
    const ease = (x) => 1 - Math.pow(1 - x, 3);                     // ease-out cubic
    const show = (el, x, dy = 30) => { el.style.opacity = x; el.style.transform = `translateY(${(1 - x) * dy}px)`; };
    const NS = "http://www.w3.org/2000/svg";
    const ORDER = ["all", "gate", "icp", "p1", "p2", "p3"];                 // drawing order
    const at = (id) => 1.0 + ORDER.indexOf(id) * 1.15;                       // when each node starts drawing
    const drawOn = (el, p) => (el.style.strokeDashoffset = 1 - p);
    
    /* ---------- Build the DOM once from CONFIG ---------- */
    let tallSheet = false;
    function build() {
      tallSheet = H > W;
      const sw = tallSheet ? 1080 : 1920, sh = tallSheet ? 1920 : 1080, k = Math.min(W / sw, H / sh);
      Object.assign($("sheet").style, { width: sw + "px", height: sh + "px", transform: `translate(${(W - sw * k) / 2}px,${(H - sh * k) / 2}px) scale(${k})` });
      $("title").textContent = CONFIG.title; $("fig").textContent = CONFIG.fig;
      $("stamp").innerHTML = CONFIG.stamp.map((s) => `<div>${s}</div>`).join("");
      const box = {};
      for (const n of CONFIG.nodes) {
        const [x, y, w, h] = n[tallSheet ? "tall" : "wide"]; box[n.id] = { x, y, w, h, kind: n.kind };
        $("nodes").insertAdjacentHTML("beforeend", `<div class="node ${n.kind}${n.hot ? " hot" : ""}" data-id="${n.id}" style="left:${x}px;top:${y}px;width:${w}px;height:${h}px">${n.value ? `<b>${n.value}</b>` : ""}<span>${n.text}</span></div>`);
        const shape = n.kind === "gate"
          ? `<polygon class="ln" pathLength="1" points="${x + w / 2},${y} ${x + w},${y + h / 2} ${x + w / 2},${y + h} ${x},${y + h / 2}"/>`
          : `<rect class="ln${n.hot ? " hot" : ""}" pathLength="1" x="${x}" y="${y}" width="${w}" height="${h}"/>`;
        $("wires").insertAdjacentHTML("beforeend", shape.replace(/^<(\w+)/, `<$1 data-id="${n.id}"`));
      }
      for (const [a, b] of CONFIG.wires) {                                  // elbow connectors between box edges
        const A = box[a], B = box[b], side = tallSheet && B.kind === "out";
        const d = side                                                      // tall stack: run down the left, enter from the side
          ? `M${A.x + A.w / 2} ${A.y + A.h} V${A.y + A.h + 30} H${B.x - 50} V${B.y + B.h / 2} H${B.x}`
          : tallSheet
          ? `M${A.x + A.w / 2} ${A.y + A.h} V${(A.y + A.h + B.y) / 2} H${B.x + B.w / 2} V${B.y}`
          : `M${A.x + A.w} ${A.y + A.h / 2} H${(A.x + A.w + B.x) / 2} V${B.y + B.h / 2} H${B.x}`;
        $("wires").insertAdjacentHTML("beforeend", `<path class="ln wire" pathLength="1" data-to="${b}" d="${d}"/><circle r="12" data-to="${b}"/>`);
      }
    }
    
    /* ---------- Draw one frame for time t (seconds). Pure: same t, same frame. ---------- */
    function render(t) {
      show($("title"), ease(prog(t, 0.2, 0.7))); show($("fig"), ease(prog(t, 0.5, 0.7)));
      $("stamp").style.opacity = prog(t, 1.0, 0.8);
      for (const el of document.querySelectorAll("#wires [data-id]")) drawOn(el, ease(prog(t, at(el.dataset.id), 0.8)));
      for (const el of document.querySelectorAll(".node")) {
        const p = ease(prog(t, at(el.dataset.id) + 0.5, 0.6)); show(el, p, 20);
      }
      for (const w of document.querySelectorAll("#wires .wire")) drawOn(w, ease(prog(t, at(w.dataset.to) - 0.45, 0.5)));
      for (const c of document.querySelectorAll("#wires circle")) {          // signal pulses run along every wire
        const w = c.previousElementSibling, f = (t - 9 + at(c.dataset.to) * 0.1) % 1.4 / 1.4, len = w.getTotalLength();
        const pt = w.getPointAtLength(f * len);
        c.setAttribute("cx", pt.x); c.setAttribute("cy", pt.y); c.style.opacity = t > 9 ? Math.sin(f * Math.PI) : 0;
      }
    }
    
    /* ---------- Player: keep as is ---------- */
    const SIZES = { "16:9": [1920, 1080], "1:1": [1440, 1440], "9:16": [1080, 1920] };
    const [W, H] = SIZES[CONFIG.format] || SIZES["16:9"], D = CONFIG.duration;
    const stage = $("stage"), scrub = $("scrub");
    stage.style.width = W + "px"; stage.style.height = H + "px";
    stage.classList.toggle("tall", H > W); stage.classList.toggle("square", H === W);
    for (const [k, v] of Object.entries(CONFIG.colors)) stage.style.setProperty("--" + k, v);
    stage.style.setProperty("--font", `"${CONFIG.font}", system-ui, sans-serif`);
    let t = 0, playing = true, rec = false, start = performance.now();
    const fit = () => { stage.style.transform = `translate(-50%,-50%) scale(${Math.min(innerWidth / W, innerHeight / H)})`; };
    function draw(time) { t = time; render(t); stage.style.setProperty("--fade", rec ? 0 : clamp((t - D + 0.5) / 0.5)); scrub.value = t / D * 1000; }
    window.seek = (time) => { playing = false; draw(time); };   // frame-by-frame capture: seek(0), seek(1/30), ...
    function play(from = 0) { start = performance.now() - from * 1000; playing = true; }
    function tick(now) {
      if (playing) { let x = (now - start) / 1000; if (x >= D) { if (rec) { x = D; playing = false; } else { x = 0; play(0); } } draw(x); }
      requestAnimationFrame(tick);
    }
    function setRec(on) { rec = on; document.body.classList.toggle("rec", on); seek(0); }
    $("b-play").onclick = () => (playing ? (playing = false) : play(t >= D ? 0 : t));
    $("b-replay").onclick = () => play(0);
    $("b-rec").onclick = () => setRec(true);
    scrub.oninput = () => seek(scrub.value / 1000 * D);
    stage.onclick = () => { if (rec) play(0); };
    addEventListener("keydown", (e) => {
      if (e.code === "Space") { e.preventDefault(); if (rec) play(0); else if (playing) playing = false; else play(t >= D ? 0 : t); }
      if (e.key === "r" || e.key === "R") setRec(!rec);
      if (e.key === "Escape") { setRec(false); play(0); }
    });
    addEventListener("resize", fit); fit(); build(); draw(0); requestAnimationFrame(tick);
    </script>
    </body>
    </html>
    ```
    

All numbers, regions and the company name in the previews are made up for illustration.

More like this in Approachable AI

A weekly newsletter for B2B GTM leaders on putting AI to work, with the prompts and builds I use with client teams.

Subscribe on Substack