Motion · Nhịp & montage

Gạch chân hóa phổ tần

UI hóa phổ tần — gạch chân tiêu đề tách thành dải cột dọc nhảy theo phổ tần suốt hai nhịp, rồi thu lại thành đường thẳng; hình ảnh hóa nhạc sống trong UI.

1920×108016:930 fps4.72s · 142 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#rhythm#typography

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

Đoạn đồng bộ hình-tiếng cao trào của phim có nhạc nền (điệp khúc BGM/đoạn trống dồn dập); cấu kiện gạch chân/đường phân cách của thẻ tiêu đề, trang chương

Ý đồ

Thư viện đã có type-rhythm-sync A (độ đậm chữ đập theo nhịp) cho đồng bộ hình-tiếng — đó là "chữ tự chuyển động". Thẻ này là "cấu kiện UI biến thành equalizer": gạch chân, cấu kiện khiêm tốn nhất, bị nhạc mượn dùng suốt hai nhịp, tách thành 28 cột dọc nhảy theo phổ tần, rồi thu lại, trả nguyên vẹn. Cấu trúc mượn-rồi-trả chính là toàn bộ sự tinh tế — khán giả trước tiên nhận ra "đây là gạch chân", rồi thấy nó biến thành thứ khác, rồi thấy nó biến trở lại; nếu ngay từ đầu đã là dải phổ tần thì chỉ là một hình ảnh hóa nhạc khác. Nhảy xong phải trở về đúng đường thẳng 8px, không lệch một pixel.

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

  • Thời lượng: ~4.7s (tách 8f + nhảy 64f + thu lại 12f + đứng yên 39f)
  • Năng lượng: Trung bình

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)

意图

声画同步库内已有 type-rhythm-sync A(字重脉冲)——那是"文字自己动"; 本卡是"UI 构件变成均衡器":下划线这个最不起眼的构件被音乐借用两小节, 裂成 28 根竖条跳出频谱,然后收拢还原、完璧归赵。借用-归还结构是 全部优雅所在——观众先认出"这是下划线",再看它变成别的东西,最后 看它变回来;若一开始就是频谱条,只是又一个音乐可视化。跳完必须还原 成那条 8px 直线,一像素不差。

动效核心

  • 28 根条底边锁在原下划线 y 上只向上生长,收拢结束条宽 + gap

恰好拼回 720px 整线(gap 0↔6px 同步过渡,gap<1 时条宽 +0.5px 防细缝)

  • 非跳动段条件挂载回整条直线(摘罩式)——像素级静止靠"根本不渲染

条组",不是条高归零

  • demo 为伪 FFT:|sin(i·0.7+f·0.31)| × 每 4 帧换挡 seed hash 抖动 ×

低频高/高频矮包络 × ramp 幅度包络,帧确定无随机

  • 实战换真 FFT:@remotion/media-utils 的 visualizeAudio() 取

频谱映射条高,伪 FFT 只是灰阶占位的节拍替身

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 条数/条宽 | 28 根、gap 6px 时 ≈20px 宽 | >40 根读作噪声;<16 根读作柱状图 | | 峰值幅度 | AMP=92,实测峰值 75–85px | spec 62 实测只有 ~55px 不够响,已加码 1.5×(可感性判例) | | 包络 | 左高右矮(低频高/高频矮) | 均匀高度像栅栏不像频谱 | | 换挡节拍 | seed 每 4 帧换挡 | 逐帧换读作电噪;8f 以上读作慢动作 | | 时间轴 | 裂开 8f out-cubic / 跳动 64f(两小节)/ 收拢 12f | 跳动 <1 小节读作故障闪了一下 | | 收尾 | 收线后真静止 ≥39f,像素级 diff 为零 | 归还之后再动一下,借用结构就毁了 |

已知坑

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

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

  • 声音强依赖:跳动段必须真的踩在音乐上——无音轨的频谱跳动读作

UI 故障;实战必须 visualizeAudio() 对齐 BGM,伪 FFT 不许上片

  • 全片 ≤1 次,且只借用一个构件——标题下划线在跳、卡片分隔线也在跳,

均衡器就成了壁纸(P4)

  • 与 type-rhythm-sync 同为声画同步族,同一段落二选一——文字在脉冲、

下划线在跳频谱,两个声画主角互相拆台

  • 收拢帧后的静止段是"归还仪式"的一部分,从重给(≥35f),别急着切

参考实现

demos/rhythm/spectrum-morph-ui/ (SpectrumMorphUi.tsx)

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

  • demos/rhythm/spectrum-morph-ui/SpectrumMorphUi.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/spectrum-morph-ui/SpectrumMorphUi.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/spectrum-morph-ui.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/rhythm/spectrum-morph-ui/SpectrumMorphUi.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("spectrum-morph-ui")

Prompt

Use the video-shotcraft shot recipe "Gạch chân hóa phổ tần" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/spectrum-morph-ui.json
Raw TSX: https://gimgs.net/motion/spectrum-morph-ui/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/rhythm/spectrum-morph-ui/SpectrumMorphUi.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 4.72s

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

3,992 chars · Apache-2.0Mở template gốc ↗
// ===== demos/rhythm/spectrum-morph-ui/SpectrumMorphUi.tsx =====
// spectrum-morph-ui —— 频谱化 UI
// 标题 "LAUNCH WEEK" 下方 8px ink 下划线(720px)。25f 起裂成 28 根竖条
// (20px 宽、6px 间隙),条高按伪 FFT 跳动 64f(低频端高、高频端矮的包络),
// 条底对齐原线、向上生长。两小节后 12f 收拢回 8px 直线,收线后真静止 ≥35f。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, TitleBlock } from '../../_fixtures/Fixtures';

// 库内标准 seed hash(帧确定,无 Math.random)
const h = (n: number) => {
  const s = Math.sin(n * 127.3) * 43758.5453;
  return s - Math.floor(s);
};

const LINE_W = 720;
const LINE_H = 8;
const LINE_X = (1920 - LINE_W) / 2; // 600
const LINE_BOTTOM = 620; // 下划线底边 y(条从这里向上长)

const N_BARS = 28;
const GAP_MAX = 6;

const SPLIT = 25; // 裂开起点
const SPLIT_DUR = 8; // 裂开 & 幅度爬升
const DANCE = 64; // 跳动两小节
const COLLAPSE_START = SPLIT + DANCE; // 89
const COLLAPSE_DUR = 12;
const COLLAPSE_END = COLLAPSE_START + COLLAPSE_DUR; // 101 → 之后真静止 39f

const AMP = 92; // 理论峰值 8 + 92 = 100px;wobble×jitter×env 很少同时取满,实测峰值 ~80px

// 低频端高、高频端矮的包络
const env = (i: number) => 0.4 + 0.6 * Math.pow(1 - i / (N_BARS - 1), 1.1);

export const SpectrumMorphUi: React.FC = () => {
  const frame = useCurrentFrame();

  const titleOp = interpolate(frame, [0, 10], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  // 幅度爬升(扩散 → out-cubic)与收拢(12f 归零)
  const rampIn = interpolate(frame, [SPLIT, SPLIT + SPLIT_DUR], [0, 1], {
    easing: Easing.out(Easing.cubic),
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const rampOut = interpolate(frame, [COLLAPSE_START, COLLAPSE_END], [1, 0], {
    easing: Easing.out(Easing.cubic),
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const amp = rampIn * rampOut;

  // 间隙:裂开时 0→6,收拢时 6→0(收拢结束恰好合成整线)
  const gapIn = interpolate(frame, [SPLIT, SPLIT + SPLIT_DUR], [0, GAP_MAX], {
    easing: Easing.out(Easing.cubic),
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const gapOut = interpolate(frame, [COLLAPSE_START, COLLAPSE_END], [GAP_MAX, 0], {
    easing: Easing.out(Easing.cubic),
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const gap = Math.min(gapIn, gapOut);
  const barW = (LINE_W - (N_BARS - 1) * gap) / N_BARS; // gap=6 时 ≈20px

  // 条形阶段以外条件挂载整线(摘罩=条件挂载,保证收尾像素级静止)
  const barsActive = frame >= SPLIT && frame < COLLAPSE_END;

  return (
    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
      <div
        style={{
          position: 'absolute',
          top: 330,
          width: '100%',
          textAlign: 'center',
          opacity: titleOp,
          fontFamily: 'Helvetica, Arial, sans-serif',
          fontWeight: 800,
          fontSize: 120,
          color: G.ink,
          letterSpacing: -1,
        }}
      >
        LAUNCH WEEK
      </div>

      {!barsActive && (
        <div
          style={{
            position: 'absolute',
            left: LINE_X,
            top: LINE_BOTTOM - LINE_H,
            width: LINE_W,
            height: LINE_H,
            background: G.ink,
            borderRadius: 4,
          }}
        />
      )}

      {barsActive &&
        Array.from({ length: N_BARS }).map((_, i) => {
          // 伪 FFT:|sin| 摆动 × 每 4 帧换挡的 seed 随机 × 频段包络 × 幅度包络
          const wobble = Math.abs(Math.sin(i * 0.7 + frame * 0.31));
          const jitter = 0.4 + 0.6 * h(i * 13 + Math.floor(frame / 4));
          const barH = LINE_H + AMP * wobble * jitter * env(i) * amp;
          return (
            <div
              key={i}
              style={{
                position: 'absolute',
                left: LINE_X + i * (barW + gap),
                top: LINE_BOTTOM - barH, // 底边对齐原线,向上长
                width: barW + (gap < 1 ? 0.5 : 0), // gap 收到 0 时补 0.5px 防细缝
                height: barH,
                background: G.ink,
                borderRadius: 3,
              }}
            />
          );
        })}
    </div>
  );
};


  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/rhythm/spectrum-morph-ui/SpectrumMorphUi.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