Motion · Rhythm
Spectrum-morph underline
Spectrum UI morph — the title underline splits into vertical bars that pulse like a spectrum for two bars, then closes back into a straight line; music visualization living inside the UI.
Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player
When to use
Audio-synced highlight passages in tracks with music (BGM chorus entry / dense drum hits); underline/divider elements on title cards and chapter pages
Intention
The library already has type-rhythm-sync A (font-weight pulse) for audio sync — that's "the text itself moving." This card is "a UI element turning into an equalizer": the underline, the most unassuming element, is borrowed by the music for two bars, splitting into 28 vertical bars that jump as a spectrum, then closing back and returning intact. The borrow-and-return structure is the whole elegance — the viewer first recognizes "this is the underline," then watches it become something else, then watches it become that again; if it started as spectrum bars, it would just be another music visualizer. It must return to that exact 8px straight line, pixel for pixel, when the pulsing ends.
Duration & energy
- Duration: ~4.7s (8f split + 64f pulsing + 12f close + 39f still)
- Energy: Medium
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
声画同步库内已有 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)
Reference implementation (Remotion)
- demos/rhythm/spectrum-morph-ui/SpectrumMorphUi.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/spectrum-morph-ui/SpectrumMorphUi.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.
- Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
- Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
- 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/spectrum-morph-ui.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/rhythm/spectrum-morph-ui/SpectrumMorphUi.tsx
npx remotion render <CompositionId> out.mp4claude 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 "Spectrum-morph underline" (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).
// ===== 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>
);
};
- 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/rhythm/spectrum-morph-ui/SpectrumMorphUi.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 |
|---|---|---|---|
| — | |||
More in this category

Trailer grammar: three beats
Three trailer-grammar moves — trailer-bumper, a fast-cut hook up front; card-footage-cadence, text cards interleaved with dialogue footage; smash-cut, a slam into stillness.

Speed ramp and freeze-annotate
Two frame-remap pacing moves — speed-ramp (fast → 0.2x gaze → fast) and freeze-annotate (flowing → freeze with a circled callout → unfreeze).

Smear-frame afterimages
Afterimage smear — a card drags 4 crisp, countable clones while panning fast, merging into one on landing; an animated take on motion blur.

Sakuga timing shift
On-3s cuts to on-1s — an element moves in choppy 3-frame steps, then snaps to buttery frame-by-frame speed at the climax.