{
  "slug": "kinetic-words",
  "title": "Kinetic words",
  "tagline": "Words pop in one after another; *starred* words take the accent colour and get underlined. 5 s, 16:9.",
  "description": "Free CC0 kinetic typography motion template: a sentence animates word by word with highlighted keywords. Self-contained HTML, deterministic, agent-friendly.",
  "category": "kinetic-text",
  "tags": [
    "typography",
    "text",
    "quote",
    "hook"
  ],
  "tier": "free",
  "license": "CC0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5,
    "aspect": "16:9"
  },
  "kind": "html",
  "params_schema": {
    "type": "object",
    "properties": {
      "text": {
        "type": "string",
        "default": "Ship *faster* than your *competitors*",
        "maxLength": 120,
        "description": "The sentence. Wrap a word in *asterisks* to highlight it. ~3–9 words works best"
      },
      "accent": {
        "type": "string",
        "format": "color",
        "default": "#ffb224",
        "description": "Highlight colour"
      },
      "fg": {
        "type": "string",
        "format": "color",
        "default": "#ffffff",
        "description": "Text colour"
      },
      "bg": {
        "type": "string",
        "format": "color",
        "default": "#111111",
        "description": "Background"
      },
      "size": {
        "type": "integer",
        "minimum": 60,
        "maximum": 260,
        "default": 150,
        "description": "Font size in px (auto-wraps)"
      },
      "stagger": {
        "type": "number",
        "minimum": 0.03,
        "maximum": 0.5,
        "default": 0.12,
        "description": "Seconds between consecutive words"
      },
      "startAt": {
        "type": "number",
        "minimum": 0,
        "maximum": 3,
        "default": 0.4,
        "description": "Delay before the first word (seconds)"
      }
    }
  },
  "default_params": {
    "text": "Ship *faster* than your *competitors*",
    "accent": "#ffb224",
    "fg": "#ffffff",
    "bg": "#111111",
    "size": 150,
    "stagger": 0.12,
    "startAt": 0.4
  },
  "preview": {
    "poster": "https://gimgs.net/motion/media/kinetic-words/poster-00356d1d0af9.jpg",
    "video": ""
  },
  "variants": [],
  "credits": {
    "author": "gimgs",
    "url": "https://gimgs.net/motion"
  },
  "page_url": "https://gimgs.net/motion/kinetic-words.html",
  "json_url": "https://gimgs.net/motion/kinetic-words.json",
  "template_url": "https://gimgs.net/motion/kinetic-words/template.html",
  "play_url": "https://gimgs.net/motion/kinetic-words/play",
  "markdown_url": "https://gimgs.net/motion/kinetic-words.md",
  "updated_at": "2026-09-24T07:24:50.351Z",
  "prompt": "## When to use\nA hook or a quote for social video, a section title, a punchline. One idea per clip.\n\n## How to adapt\n- Put the sentence in `text`; star the 1–2 words that carry the meaning (`*faster*`). Everything else stays in `fg`.\n- Total run time before the last word lands = `startAt + (words − 1) × stagger + 0.55`. Keep it under `duration` (5 s) or the tail is cut; for 12+ words lower `stagger`.\n- Long sentences: lower `size` to 110–120 so the block fits two or three lines.\n- Chain several clips with different `text` for a whole script; keep `accent`/`bg` identical for continuity.\n\n## Do not change\n`window.motion`, `#stage` size, `animation-fill-mode: both`. The word spans are rebuilt on every `apply()` — that is expected.",
  "flow": [
    {
      "step": "Write the line",
      "note": "Short. Star the key words."
    },
    {
      "step": "Render",
      "cmd": "frames 0…149 at t=i/30 → ffmpeg -framerate 30 -i frame-%04d.png -c:v libx264 -pix_fmt yuv420p out.mp4"
    },
    {
      "step": "Chain",
      "cmd": "ffmpeg -f concat -safe 0 -i list.txt -c copy script.mp4"
    }
  ],
  "files": [],
  "contract": "Self-contained HTML. Declares window.motion = { width, height, fps, duration, params, apply(params) [, seek(t)] }; root element #stage sized width×height; animations are CSS/WAAPI with animation-fill-mode: both so any t can be seeked via document.getAnimations(). The host runtime (injected by /play) merges ?p params, calls apply(), scales #stage to the viewport and, when ?t is given, pauses and seeks deterministically.",
  "render": {
    "frames": 150,
    "frame_url": "https://gimgs.net/motion/kinetic-words/play?p=<base64url(JSON params)>&t=<seconds>",
    "note": "Open frame_url in a headless browser with viewport width×height (deviceScaleFactor 1), wait for document.documentElement.dataset.motionReady === '1', screenshot; repeat for t = i/fps (i = 0…frames-1); then `ffmpeg -framerate <fps> -i frame-%04d.png -c:v libx264 -pix_fmt yuv420p out.mp4`. Or drop ?t to play it live."
  },
  "source": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Kinetic words</title>\n<style>\n  @font-face{font-family:'Bricolage Grotesque';font-weight:600 800;font-display:block;src:url(https://gimgs.net/fonts/bricolage-600-latin.woff2) format('woff2')}\n  html,body{margin:0;background:#000}\n  #stage{position:relative;width:1920px;height:1080px;overflow:hidden;background:var(--bg,#111);color:var(--fg,#fff);\n    font-family:'Bricolage Grotesque',Inter,system-ui,sans-serif;font-weight:800;letter-spacing:-.03em}\n  .wrap{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:0 120px;text-align:center}\n  .line{display:flex;flex-wrap:wrap;justify-content:center;gap:0 .28em;font-size:var(--size,150px);line-height:1.02}\n  .w{display:inline-block;transform:translateY(60%) rotate(4deg);opacity:0;\n    animation:in .55s var(--d) cubic-bezier(.2,.9,.2,1.1) both}\n  .w.hi{color:var(--accent,#ffb224)}\n  .w.hi::after{content:\"\";display:block;height:.08em;margin-top:-.06em;background:var(--accent,#ffb224);transform:scaleX(0);transform-origin:0 50%;\n    animation:ul .4s calc(var(--d) + .35s) cubic-bezier(.2,.9,.2,1) both}\n  @keyframes in{to{transform:none;opacity:1}}\n  @keyframes ul{to{transform:scaleX(1)}}\n  .dot{position:absolute;width:26px;height:26px;border-radius:50%;background:var(--accent,#ffb224);left:50%;top:50%;transform:translate(-50%,-50%) scale(0);\n    animation:dot .9s cubic-bezier(.2,.9,.2,1) both}\n  @keyframes dot{0%{transform:translate(-50%,-50%) scale(0)}40%{transform:translate(-50%,-50%) scale(1.6)}100%{transform:translate(-50%,-50%) scale(0)}}\n</style>\n</head>\n<body>\n<div id=\"stage\">\n  <div class=\"dot\"></div>\n  <div class=\"wrap\"><div class=\"line\" id=\"line\"></div></div>\n</div>\n<script>\nwindow.motion = {\n  width: 1920, height: 1080, fps: 30, duration: 5,\n  params: { text: \"Ship *faster* than your *competitors*\", accent: \"#ffb224\", fg: \"#ffffff\", bg: \"#111111\", size: 150, stagger: 0.12, startAt: 0.4 },\n  apply: function (p) {\n    var s = document.getElementById(\"stage\");\n    s.style.setProperty(\"--accent\", p.accent);\n    s.style.setProperty(\"--fg\", p.fg);\n    s.style.setProperty(\"--bg\", p.bg);\n    s.style.setProperty(\"--size\", (+p.size || 150) + \"px\");\n    var line = document.getElementById(\"line\");\n    while (line.firstChild) line.removeChild(line.firstChild);\n    // words wrapped in *asterisks* get the accent colour + underline\n    var words = String(p.text || \"\").split(/\\s+/).filter(Boolean);\n    var stagger = Math.max(0.02, +p.stagger || 0.12), start = Math.max(0, +p.startAt || 0);\n    words.forEach(function (w, i) {\n      var hi = /^\\*.+\\*$/.test(w);\n      var span = document.createElement(\"span\");\n      span.className = \"w\" + (hi ? \" hi\" : \"\");\n      span.textContent = hi ? w.slice(1, -1) : w;\n      span.style.setProperty(\"--d\", (start + i * stagger).toFixed(3) + \"s\");\n      line.appendChild(span);\n    });\n  }\n};\n</script>\n</body>\n</html>\n"
}