{
  "slug": "autolayout-gap-dial",
  "title": "Autolayout Gap Dial",
  "tagline": "A gap dial drives layout — a row of link blocks with selection outlines and spacing callouts, badge numbers tick up as blocks are pushed apart in real time then spring back.",
  "description": "A gap dial drives layout — a row of link blocks with selection outlines and spacing callouts, badge numbers tick up as blocks are pushed apart in real time th",
  "category": "interaction",
  "tags": [
    "shotcraft",
    "remotion",
    "interaction"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.05,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/autolayout-gap-dial/poster-ab039c5f422d.jpg",
    "video": "https://gimgs.net/motion/media/autolayout-gap-dial/video-a52730d3d81a.mp4"
  },
  "variants": [
    {
      "key": "autolayout-gap-dial",
      "label": "autolayout-gap-dial",
      "description": "A gap dial drives layout — a row of link blocks with selection outlines and spacing callouts, badge numbers tick up as blocks are pushed apart in real time then spring back.",
      "use": "",
      "video": "https://gimgs.net/motion/media/autolayout-gap-dial/video-a52730d3d81a.mp4",
      "poster": "https://gimgs.net/motion/media/autolayout-gap-dial/poster-ab039c5f422d.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/autolayout-gap-dial.html",
  "json_url": "https://gimgs.net/motion/autolayout-gap-dial.json",
  "template_url": "https://gimgs.net/motion/autolayout-gap-dial/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/autolayout-gap-dial.md",
  "updated_at": "2026-09-24T07:39:41.937Z",
  "prompt": "## When to use\n\nThe \"change one number, the UI moves\" hero shot for design tools/low-code products; category language that borrows design-tool semantics.\n\n## Intention\n\n\"Change a parameter, the UI changes\" is the core promise of design tools, but it's usually shot as a before/after cut with the causality edited out. This card shoots the causality as one continuous take: the gap badge ticks up digit by digit, link blocks get pushed apart in the same frame, and the measurement line stretches in real time — the number is the rein, the layout is the horse. The selection outline, 8 handles, and gap callout are all native design-tool vocabulary, so viewers instantly recognize \"this is tuning a parameter inside the tool.\" The return deliberately overshoots with an underdamped spring (tightening past the start point before settling), turning \"settling back\" into an elastic finish. Division of labor with type-and-filter: that card is search causality (\"type → list narrows\"); this card is parameter causality (\"turn a value → geometry rearranges\").\n\n## Duration & energy\n\n- Duration: ~4s (120f: selection entrance + loosen 38f + hold + spring-back).\n- Energy: Medium (tool-rational; the payoff is the locked sync between numbers and displacement).\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n\"参数改了、UI 变了\"是设计工具的核心承诺，但通常拍成\"点面板→切\n结果\"的前后对比，因果被剪掉了。本卡把因果拍成连续镜头：间距徽章\n数字逐格跳，链接块**同帧**被推开，测量线实时拉长——数字就是缰绳，\n布局就是马。框选描边+8 手柄+间距标注全是设计工具的母语，观众一眼\n认出\"这是在工具里调参数\"。回程故意用欠阻尼弹簧过冲（缩到比起点\n更紧再回稳），把\"归位\"做成一次有弹性的收尾。与 type-and-filter\n分工：那是\"输入内容→列表收敛\"的检索因果，本卡是\"拨数值→几何\n重排\"的参数因果。\n\n## 动效核心\n- **gap 一个参数管全部**：块位置由 gap 重算（整排保持居中）、测量\n  线宽=gap、徽章钉缝隙中心——所有元素从同一数值派生，锁定同步\n  是本卡的全部说服力\n- 参数曲线：去程 12→110 走 38f Easing.inOut(cubic)；hold 14f；\n  回程 spring(damping 9, stiffness 80, mass 1.1) 故意过冲到起点\n  以下再回稳\n- 徽章数字步进 2 显示（Math.round(gap/2)*2），跳数时 scale 脉冲\n  1+0.10·|sin|；顶部大 GAP 读数块同步\n- 框选描边 spring 入场，8 手柄（四角+四边中点）；间距标注 = 竖直\n  延长线 ×2 + 水平测量线 + 跟随徽章，8–16f 淡入\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| gap 幅度 | 12→110px（原片 20→24，demo 按裁决加大） | 原片幅度肉眼几乎不可感——可感性判例：看不出就加大做极端版；实战至少 4–5 倍行程 |\n| 去程时长 | 38f inOut cubic | 快了读作抽搐；慢过 60f 数字跳动和位移的绑定感松掉 |\n| 回程弹簧 | damping 9 → 过冲到起点以下再回稳 | 过冲是\"松手回弹\"的手感来源；damping ≥13 直落可惜 |\n| 数字步进 | 步进 2 + 跳数 scale 脉冲 0.10 | 逐 1 跳太碎读作毫秒表；步进是\"拨盘咔哒\"的颗粒感 |\n| 标注几何 | 延长线 62px、测量线钉块底 +56px、徽章钉缝隙中心 | 标注必须随 gap 实时重算——任何一处滞后即穿帮 |\n| 块宽 | 不等宽（230/190/265/210/245） | 等宽读作示意图；不等宽才像真导航 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 数字与位移必须同源同帧：徽章显示值和块坐标都从同一个 gap 派生，分开动画哪怕差 1f，\"参数驱动\"的信任即碎\n- demo 顶部大 GAP 读数块是灰阶示意的讲解字幕，实战里若画面已有真实工具面板就删掉，别双重报数\n- 同族扩展：gap 只是一维，radius/padding/rotation 同语法可换参数——但一镜只拨一个参数，两个以上读作乱调\n- 框选描边+手柄是\"设计工具语义\"借用（figma/framer 母语），非设计工具受众的片子慎用，读不懂框选就只剩块在挪\n- 声音：徽章每跳一格一声极轻 tick（拨盘咔哒），回弹落定一声 pop；tick 密度跟步进走\n\n## 参考实现\ndemos/interaction/autolayout-gap-dial/\n（AutolayoutGapDial.tsx）\n原片出处：framer-ai 4.5–5.5s\n\n## Reference implementation (Remotion)\n\n- demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "npx remotion render <CompositionId> out.mp4   # component: demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 122,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx =====\n// autolayout-gap-dial —— framer-ai 4.5–5.5s\n// 一排导航链接块带框选描边 + 间距标注徽章，徽章数字逐格跳动，\n// 链接块被参数实时推开（flex gap 插值），标注线跟随。\n// demo 放大做：间距从紧(16)拉到松(64)，再弹簧回弹归位。\nimport React from 'react';\nimport { AbsoluteFill, useCurrentFrame, interpolate, spring, useVideoConfig, Easing } from 'remotion';\nimport { G } from '../../_fixtures/Fixtures';\n\nconst BLOCK_WIDTHS = [230, 190, 265, 210, 245];\nconst BLOCK_H = 100;\nconst ROW_Y = 540; // 行中心\nconst G_MIN = 12;\nconst G_MAX = 110;\n\nconst GROW_START = 14;\nconst GROW_END = 52;\nconst HOLD_END = 66;\n\nexport const AutolayoutGapDial: React.FC = () => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  // 间距参数曲线：紧 → 松（easeInOut）→ hold → 弹簧回弹归位（带过冲）\n  let gap: number;\n  if (frame < HOLD_END) {\n    gap = interpolate(frame, [GROW_START, GROW_END], [G_MIN, G_MAX], {\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n      easing: Easing.inOut(Easing.cubic),\n    });\n  } else {\n    const s = spring({ frame: frame - HOLD_END, fps, config: { damping: 9, stiffness: 80, mass: 1.1 } });\n    gap = G_MAX + (G_MIN - G_MAX) * s; // damping 9 → 过冲到 G_MIN 以下再回稳\n  }\n  const gapShown = Math.round(gap / 2) * 2; // 徽章逐格跳数（步进 2）\n\n  // 布局：居中排布\n  const total = BLOCK_WIDTHS.reduce((a, b) => a + b, 0) + gap * (BLOCK_WIDTHS.length - 1);\n  const startX = (1920 - total) / 2;\n  const xs: number[] = [];\n  let acc = startX;\n  for (const w of BLOCK_WIDTHS) {\n    xs.push(acc);\n    acc += w + gap;\n  }\n\n  // 框选描边入场\n  const selIn = spring({ frame: frame - 2, fps, config: { damping: 14, stiffness: 130 } });\n  const pad = 26;\n  const selX = startX - pad;\n  const selW = total + pad * 2;\n  const selY = ROW_Y - BLOCK_H / 2 - pad;\n  const selH = BLOCK_H + pad * 2;\n\n  // 徽章在数值变动时的每格\"跳一下\"\n  const ticking = Math.abs(gap - (frame < HOLD_END ? G_MIN : G_MIN)) > 0.5 && (frame >= GROW_START && frame < HOLD_END + 40);\n  const tickPulse = ticking ? 1 + 0.10 * Math.abs(Math.sin(frame * 0.9)) : 1;\n\n  return (\n    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>\n      <AbsoluteFill\n        style={{\n          backgroundImage: `radial-gradient(${G.line} 3px, transparent 3px)`,\n          backgroundSize: '52px 52px',\n        }}\n      />\n\n      {/* 标题字块（讲清这是\"参数驱动布局\"） */}\n      <div style={{ position: 'absolute', top: 250, width: '100%', textAlign: 'center', fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 44, color: G.mid, letterSpacing: 2 }}>\n        GAP\n        <span style={{ display: 'inline-block', minWidth: 130, marginLeft: 24, padding: '4px 22px', borderRadius: 12, background: G.ink, color: '#f2f2f0', fontVariantNumeric: 'tabular-nums', transform: `scale(${tickPulse})` }}>\n          {gapShown}\n        </span>\n      </div>\n\n      {/* 框选描边 + 8 手柄 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: selX,\n          top: selY,\n          width: selW,\n          height: selH,\n          border: `3px solid ${G.ink}`,\n          borderRadius: 6,\n          opacity: selIn,\n          boxSizing: 'border-box',\n        }}\n      >\n        {[\n          [0, 0], [0.5, 0], [1, 0],\n          [0, 0.5], [1, 0.5],\n          [0, 1], [0.5, 1], [1, 1],\n        ].map(([hx, hy], i) => (\n          <div\n            key={i}\n            style={{\n              position: 'absolute',\n              left: `${hx * 100}%`,\n              top: `${hy * 100}%`,\n              width: 16,\n              height: 16,\n              marginLeft: -8,\n              marginTop: -8,\n              background: '#fff',\n              border: `3px solid ${G.ink}`,\n              borderRadius: 3,\n              boxSizing: 'border-box',\n            }}\n          />\n        ))}\n      </div>\n\n      {/* 链接块 */}\n      {BLOCK_WIDTHS.map((w, i) => (\n        <div\n          key={i}\n          style={{\n            position: 'absolute',\n            left: xs[i],\n            top: ROW_Y - BLOCK_H / 2,\n            width: w,\n            height: BLOCK_H,\n            background: G.card,\n            border: `2px solid ${G.border}`,\n            borderRadius: 14,\n            boxShadow: '0 3px 12px rgba(0,0,0,0.07)',\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'center',\n            boxSizing: 'border-box',\n          }}\n        >\n          <div style={{ height: 16, width: w - 76, background: G.bar, borderRadius: 8 }} />\n        </div>\n      ))}\n\n      {/* 间距标注：测量线 + 端刺 + 跟随徽章 */}\n      {xs.slice(0, -1).map((x, i) => {\n        const gx = x + BLOCK_WIDTHS[i]; // 缝隙左缘\n        const cy = ROW_Y + BLOCK_H / 2 + 56;\n        const badgeOp = interpolate(frame, [8, 16], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });\n        return (\n          <div key={i} style={{ position: 'absolute', left: 0, top: 0, opacity: badgeOp }}>\n            {/* 竖直延长线 */}\n            <div style={{ position: 'absolute', left: gx - 1.5, top: ROW_Y + BLOCK_H / 2 + 6, width: 3, height: 62, background: G.mid, opacity: 0.55 }} />\n            <div style={{ position: 'absolute', left: gx + gap - 1.5, top: ROW_Y + BLOCK_H / 2 + 6, width: 3, height: 62, background: G.mid, opacity: 0.55 }} />\n            {/* 水平测量线 */}\n            <div style={{ position: 'absolute', left: gx, top: cy - 1.5, width: gap, height: 3, background: G.ink }} />\n            {/* 徽章（跟随缝隙中心） */}\n            <div\n              style={{\n                position: 'absolute',\n                left: gx + gap / 2,\n                top: cy + 18,\n                transform: `translateX(-50%) scale(${tickPulse})`,\n                padding: '5px 14px',\n                borderRadius: 10,\n                background: G.ink,\n                color: '#f2f2f0',\n                fontFamily: 'Helvetica, Arial, sans-serif',\n                fontWeight: 700,\n                fontSize: 24,\n                fontVariantNumeric: 'tabular-nums',\n              }}\n            >\n              {gapShown}\n            </div>\n          </div>\n        );\n      })}\n    </AbsoluteFill>\n  );\n};\n\n\n"
}