Motion · Tương tác

Núm xoay khoảng cách bố cục

Núm xoay khoảng cách điều khiển bố cục — một hàng khối liên kết có viền chọn + chú thích khoảng cách, số trên badge nhảy từng nấc, khối bị đẩy giãn ra theo thời gian thực rồi bật lò xo về vị trí.

1920×108016:930 fps4.05s · 122 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#interaction

Nguồn: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composition Remotion (React/TSX) — có video preview, không có player trong trình duyệt

Khi nào dùng

Cảnh cốt lõi "đổi một con số, giao diện chuyển động theo" của công cụ thiết kế/low-code; ngôn ngữ chuyên ngành mượn từ vựng công cụ thiết kế.

Ý đồ

"Đổi tham số, UI thay đổi" là lời hứa cốt lõi của công cụ thiết kế, nhưng thường được quay kiểu "bấm panel → cắt sang kết quả", cắt mất mối nhân quả. Thẻ này quay nhân quả thành một cảnh liên tục: số trên badge khoảng cách nhảy từng nấc, khối liên kết bị đẩy giãn trong cùng một khung hình, đường đo kéo dài theo thời gian thực — con số là dây cương, bố cục là con ngựa. Viền chọn + 8 tay cầm + chú thích khoảng cách đều là ngôn ngữ mẹ đẻ của công cụ thiết kế, người xem nhận ra ngay "đây là đang chỉnh tham số trong công cụ". Đoạn quay về cố tình dùng lò xo thiếu giảm chấn để vọt quá (co chặt hơn điểm xuất phát rồi mới ổn định), biến "về vị trí" thành một cú kết có độ đàn hồi. Phân công với type-and-filter: đó là nhân quả tìm kiếm ("gõ nội dung → danh sách thu hẹp"), thẻ này là nhân quả tham số ("xoay giá trị → hình học sắp xếp lại").

Thời lượng & năng lượng

  • Thời lượng: ~4s (120f: vào khung chọn + nới giãn 38f + giữ + bật lò xo về).
  • Năng lượng: Trung bình (kiểu duy lý công cụ, điểm sướng là sự đồng bộ khóa chặt giữa con số và dịch chuyển).

Công thức cảnh quay (bản gốc tiếng Trung — timing, easing, bảng tham số, lỗi hay gặp)

意图

"参数改了、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

Mã nguồn tham chiếu (Remotion)

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

Component import fixture chung từ demos/_fixtures và helper từ assets/lib của repo; xem SKILL.md trong repo để biết quy trình render.

Mọi thứ trên trang này đều có bản máy đọc. Đưa agent link JSON, hoặc thêm MCP server gimgs rồi bảo nó tìm motion template.

  1. Tải JSON — trong đó có công thức cảnh quay (timing, easing, bảng tham số, lỗi hay gặp) và toàn bộ source TSX.
  2. Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
  3. Đăng ký component thành <Composition> trong project Remotion rồi render bằng `npx remotion render`, hoặc dựng lại công thức bằng engine của bạn.
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 "Núm xoay khoảng cách bố cục" (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.0Mở template gốc ↗
// ===== 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
—

Cùng danh mục