Motion · Interaction

Autolayout Gap Dial

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.

1920×108016:930 fps4.05s · 122 framesremotionLicense: Apache-2.0#shotcraft#remotion#interaction

Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player

When to use

The "change one number, the UI moves" hero shot for design tools/low-code products; category language that borrows design-tool semantics.

Intention

"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").

Duration & energy

  • Duration: ~4s (120f: selection entrance + loosen 38f + hold + spring-back).
  • Energy: Medium (tool-rational; the payoff is the locked sync between numbers and displacement).

Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)

意图

"参数改了、UI 变了"是设计工具的核心承诺,但通常拍成"点面板→切 结果"的前后对比,因果被剪掉了。本卡把因果拍成连续镜头:间距徽章 数字逐格跳,链接块同帧被推开,测量线实时拉长——数字就是缰绳, 布局就是马。框选描边+8 手柄+间距标注全是设计工具的母语,观众一眼 认出"这是在工具里调参数"。回程故意用欠阻尼弹簧过冲(缩到比起点 更紧再回稳),把"归位"做成一次有弹性的收尾。与 type-and-filter 分工:那是"输入内容→列表收敛"的检索因果,本卡是"拨数值→几何 重排"的参数因果。

动效核心

  • gap 一个参数管全部:块位置由 gap 重算(整排保持居中)、测量

线宽=gap、徽章钉缝隙中心——所有元素从同一数值派生,锁定同步 是本卡的全部说服力

  • 参数曲线:去程 12→110 走 38f Easing.inOut(cubic);hold 14f;

回程 spring(damping 9, stiffness 80, mass 1.1) 故意过冲到起点 以下再回稳

  • 徽章数字步进 2 显示(Math.round(gap/2)*2),跳数时 scale 脉冲

1+0.10·|sin|;顶部大 GAP 读数块同步

  • 框选描边 spring 入场,8 手柄(四角+四边中点);间距标注 = 竖直

延长线 ×2 + 水平测量线 + 跟随徽章,8–16f 淡入

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | gap 幅度 | 12→110px(原片 20→24,demo 按裁决加大) | 原片幅度肉眼几乎不可感——可感性判例:看不出就加大做极端版;实战至少 4–5 倍行程 | | 去程时长 | 38f inOut cubic | 快了读作抽搐;慢过 60f 数字跳动和位移的绑定感松掉 | | 回程弹簧 | damping 9 → 过冲到起点以下再回稳 | 过冲是"松手回弹"的手感来源;damping ≥13 直落可惜 | | 数字步进 | 步进 2 + 跳数 scale 脉冲 0.10 | 逐 1 跳太碎读作毫秒表;步进是"拨盘咔哒"的颗粒感 | | 标注几何 | 延长线 62px、测量线钉块底 +56px、徽章钉缝隙中心 | 标注必须随 gap 实时重算——任何一处滞后即穿帮 | | 块宽 | 不等宽(230/190/265/210/245) | 等宽读作示意图;不等宽才像真导航 |

已知坑

  • demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,

首次实战须以真实素材回验

  • 数字与位移必须同源同帧:徽章显示值和块坐标都从同一个 gap 派生,分开动画哪怕差 1f,"参数驱动"的信任即碎
  • demo 顶部大 GAP 读数块是灰阶示意的讲解字幕,实战里若画面已有真实工具面板就删掉,别双重报数
  • 同族扩展:gap 只是一维,radius/padding/rotation 同语法可换参数——但一镜只拨一个参数,两个以上读作乱调
  • 框选描边+手柄是"设计工具语义"借用(figma/framer 母语),非设计工具受众的片子慎用,读不懂框选就只剩块在挪
  • 声音:徽章每跳一格一声极轻 tick(拨盘咔哒),回弹落定一声 pop;tick 密度跟步进走

参考实现

demos/interaction/autolayout-gap-dial/ (AutolayoutGapDial.tsx) 原片出处:framer-ai 4.5–5.5s

Reference implementation (Remotion)

  • demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx

The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.

Everything on this page is also machine-readable. Point your agent at the JSON, or add the gimgs MCP server and ask it to search motion templates.

  1. Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
  2. Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
  3. Register the component as a <Composition> in a Remotion project and render with `npx remotion render`, or re-implement the recipe in your own engine.
curl -s https://gimgs.net/motion/autolayout-gap-dial.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("autolayout-gap-dial")

Prompt

Use the video-shotcraft shot recipe "Autolayout Gap Dial" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/autolayout-gap-dial.json
Raw TSX: https://gimgs.net/motion/autolayout-gap-dial/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 4.05s

1. Read the JSON and follow its "prompt" section (when to use, intention, parameter table, known pitfalls).
2. Adapt it for: <describe what you want>.
3. Either clone the repo, register the component as a <Composition> in a Remotion project and run npx remotion render <CompositionId> out.mp4 — or re-implement the recipe in your own engine (HTML/WAAPI works: keep the same timing and easing).

JSON · Markdown · llms.txt

6,003 chars · Apache-2.0Open raw template ↗
// ===== demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx =====
// autolayout-gap-dial —— framer-ai 4.5–5.5s
// 一排导航链接块带框选描边 + 间距标注徽章,徽章数字逐格跳动,
// 链接块被参数实时推开(flex gap 插值),标注线跟随。
// demo 放大做:间距从紧(16)拉到松(64),再弹簧回弹归位。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, spring, useVideoConfig, Easing } from 'remotion';
import { G } from '../../_fixtures/Fixtures';

const BLOCK_WIDTHS = [230, 190, 265, 210, 245];
const BLOCK_H = 100;
const ROW_Y = 540; // 行中心
const G_MIN = 12;
const G_MAX = 110;

const GROW_START = 14;
const GROW_END = 52;
const HOLD_END = 66;

export const AutolayoutGapDial: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // 间距参数曲线:紧 → 松(easeInOut)→ hold → 弹簧回弹归位(带过冲)
  let gap: number;
  if (frame < HOLD_END) {
    gap = interpolate(frame, [GROW_START, GROW_END], [G_MIN, G_MAX], {
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
      easing: Easing.inOut(Easing.cubic),
    });
  } else {
    const s = spring({ frame: frame - HOLD_END, fps, config: { damping: 9, stiffness: 80, mass: 1.1 } });
    gap = G_MAX + (G_MIN - G_MAX) * s; // damping 9 → 过冲到 G_MIN 以下再回稳
  }
  const gapShown = Math.round(gap / 2) * 2; // 徽章逐格跳数(步进 2)

  // 布局:居中排布
  const total = BLOCK_WIDTHS.reduce((a, b) => a + b, 0) + gap * (BLOCK_WIDTHS.length - 1);
  const startX = (1920 - total) / 2;
  const xs: number[] = [];
  let acc = startX;
  for (const w of BLOCK_WIDTHS) {
    xs.push(acc);
    acc += w + gap;
  }

  // 框选描边入场
  const selIn = spring({ frame: frame - 2, fps, config: { damping: 14, stiffness: 130 } });
  const pad = 26;
  const selX = startX - pad;
  const selW = total + pad * 2;
  const selY = ROW_Y - BLOCK_H / 2 - pad;
  const selH = BLOCK_H + pad * 2;

  // 徽章在数值变动时的每格"跳一下"
  const ticking = Math.abs(gap - (frame < HOLD_END ? G_MIN : G_MIN)) > 0.5 && (frame >= GROW_START && frame < HOLD_END + 40);
  const tickPulse = ticking ? 1 + 0.10 * Math.abs(Math.sin(frame * 0.9)) : 1;

  return (
    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>
      <AbsoluteFill
        style={{
          backgroundImage: `radial-gradient(${G.line} 3px, transparent 3px)`,
          backgroundSize: '52px 52px',
        }}
      />

      {/* 标题字块(讲清这是"参数驱动布局") */}
      <div style={{ position: 'absolute', top: 250, width: '100%', textAlign: 'center', fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 44, color: G.mid, letterSpacing: 2 }}>
        GAP
        <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})` }}>
          {gapShown}
        </span>
      </div>

      {/* 框选描边 + 8 手柄 */}
      <div
        style={{
          position: 'absolute',
          left: selX,
          top: selY,
          width: selW,
          height: selH,
          border: `3px solid ${G.ink}`,
          borderRadius: 6,
          opacity: selIn,
          boxSizing: 'border-box',
        }}
      >
        {[
          [0, 0], [0.5, 0], [1, 0],
          [0, 0.5], [1, 0.5],
          [0, 1], [0.5, 1], [1, 1],
        ].map(([hx, hy], i) => (
          <div
            key={i}
            style={{
              position: 'absolute',
              left: `${hx * 100}%`,
              top: `${hy * 100}%`,
              width: 16,
              height: 16,
              marginLeft: -8,
              marginTop: -8,
              background: '#fff',
              border: `3px solid ${G.ink}`,
              borderRadius: 3,
              boxSizing: 'border-box',
            }}
          />
        ))}
      </div>

      {/* 链接块 */}
      {BLOCK_WIDTHS.map((w, i) => (
        <div
          key={i}
          style={{
            position: 'absolute',
            left: xs[i],
            top: ROW_Y - BLOCK_H / 2,
            width: w,
            height: BLOCK_H,
            background: G.card,
            border: `2px solid ${G.border}`,
            borderRadius: 14,
            boxShadow: '0 3px 12px rgba(0,0,0,0.07)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            boxSizing: 'border-box',
          }}
        >
          <div style={{ height: 16, width: w - 76, background: G.bar, borderRadius: 8 }} />
        </div>
      ))}

      {/* 间距标注:测量线 + 端刺 + 跟随徽章 */}
      {xs.slice(0, -1).map((x, i) => {
        const gx = x + BLOCK_WIDTHS[i]; // 缝隙左缘
        const cy = ROW_Y + BLOCK_H / 2 + 56;
        const badgeOp = interpolate(frame, [8, 16], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
        return (
          <div key={i} style={{ position: 'absolute', left: 0, top: 0, opacity: badgeOp }}>
            {/* 竖直延长线 */}
            <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 }} />
            <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 }} />
            {/* 水平测量线 */}
            <div style={{ position: 'absolute', left: gx, top: cy - 1.5, width: gap, height: 3, background: G.ink }} />
            {/* 徽章(跟随缝隙中心) */}
            <div
              style={{
                position: 'absolute',
                left: gx + gap / 2,
                top: cy + 18,
                transform: `translateX(-50%) scale(${tickPulse})`,
                padding: '5px 14px',
                borderRadius: 10,
                background: G.ink,
                color: '#f2f2f0',
                fontFamily: 'Helvetica, Arial, sans-serif',
                fontWeight: 700,
                fontSize: 24,
                fontVariantNumeric: 'tabular-nums',
              }}
            >
              {gapShown}
            </div>
          </div>
        );
      })}
    </AbsoluteFill>
  );
};


  1. Read the recipe
    Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
  2. Get the code
    tool: git
    git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install
  3. Register + render
    tool: remotion
    npx remotion render <CompositionId> out.mp4 # component: demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx
  4. Or re-implement
    Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
paramtypedefaultdescription
—

More in this category