Motion · Interação
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.
Fonte: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composição Remotion (React/TSX) — vídeo de prévia, sem player no navegador
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.
Tudo nesta página também é legível por máquina. Aponte seu agente para o JSON ou adicione o servidor MCP do gimgs e peça para buscar templates de motion.
- Baixe o JSON — contém a receita do plano (timing, easing, tabela de parâmetros, armadilhas) e o código TSX completo.
- Clone o repositório video-shotcraft; o componente importa fixtures de demos/_fixtures e helpers de assets/lib.
- Registre o componente como <Composition> em um projeto Remotion e renderize com `npx remotion render`, ou reimplemente a receita no seu próprio engine.
curl -s https://gimgs.net/motion/autolayout-gap-dial.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx
npx remotion render <CompositionId> out.mp4claude 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).
// ===== 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>
);
};
- Read the recipePrompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
- Get the codetool: git
git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install - Register + rendertool: remotion
npx remotion render <CompositionId> out.mp4 # component: demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx - Or re-implementPort the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
| param | type | default | description |
|---|---|---|---|
| — | |||
Mais nesta categoria

Live voice waveform
A live waveform in a recording pill — 64 thin bars rise and fall with "speech," spiking tall mid-speech and shrinking to a dotted line on pauses, scrolling right to left; a full speak-pause-speak-submit performance.

Type, filter, drill in
Typing a search on a real UI, the grid converges to a single card on its own, then a click drills through into the detail page.

Theme switch: sweep and ripple
Two theme-switch moves — theme-sweep, a diagonal wipe (the skin changes wherever the boundary passes), and palette-ripple, a combo move (a ⌘K panel collapses to a point, a ripple spreads from it to re-skin).
Segmented thumb hero
A segmented control's thumb shift becomes the star of a close-up — a cursor clicks it and a new icon springs in.