Motion · Tương tác
Sóng âm giọng nói trực tiếp
Sóng âm trực tiếp trên viên nang ghi âm — 64 cột dọc mảnh nhấp nhô theo "lời nói", giữa lúc nói vươn cao, ngừng thì co lại thành dòng chấm, sóng cuộn từ phải sang trái; trọn màn nói→ngừng→nói→gửi.
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 "đang nghe bạn nói" cho tính năng nhập liệu bằng giọng nói/trợ lý AI; đoạn không có nội dung UI để trình diễn nhưng cần độ sống động liên tục để giữ khung hình
Ý đồ
Sóng âm duy nhất trong thư viện trước nay là spectrum-morph-ui — đó là hình ảnh hóa nhạc trang trí, gạch chân tiêu đề tách thành dải phổ tần. Thẻ này là sóng âm chức năng: biên nhận trực tiếp "đang nghe bạn nói". Khác biệt nằm ở nhân quả: spectrum nhảy theo BGM, là lớp vỏ bọc; thẻ này nhảy theo "người dùng đang nói", chính là tính năng sản phẩm. Sóng vươn cao lúc nói, co lại thành hàng chấm lúc ngừng, lịch sử cuộn ra ngoài về bên trái — khán giả đọc được "nó thực sự đang nghe" từ độ nhấp nhô của sóng, một cảnh 6s+ dựa hoàn toàn vào độ sống động này mà không cần nội dung nào khác. Phân vai với gauge-readout-moves: đồng hồ đo là nghi thức cơ khí "báo một con số", thẻ này là dấu hiệu sinh tồn "lắng nghe liên tục", không có giá trị cuối.
Thời lượng & năng lượng
- Thời lượng: khoảng 5s (150f: vào hình 12f + nói 1.4s + ngừng 0.8s + nói 1.4s + gửi thu gọn 0.8s)
- Năng lượng: Trung bình (sống động mang tính chức năng, không phải phô diễ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)
意图
库里唯一的波形是 spectrum-morph-ui——那是标题下划线裂成频谱条的 装饰性音乐可视化。本卡是功能性声纹:"正在听你说"的实时 回执。区别在因果:spectrum 跟 BGM 跳、是包装;本卡跟"用户说话" 跳、是产品功能本身。说话时波形高耸、停顿时缩成一排点线、历史向左 滚出——观众从波形的起伏读出"它真的在听",一段 6s+ 的画面全靠这份 活性撑住,不需要任何别的内容。与 gauge-readout-moves 分工:仪表是 "报一个数值"的机械仪式,本卡是"持续监听"的生命体征,没有终值。
动效核心
- 帧确定的伪随机是命门:mulberry32 按整数采样点取值 + smoothstep
相邻插值(值噪声),无 Math.random——渲染可复现且相邻帧连续
- 滚动感:第 i 条采样时刻 = f - (N-1-i)·1.6,最右条是"现在",
历史向左流;不是条在平移,是每条在读不同时刻的信号
- 条高 = 说话包络(梯形 seg 说/停两档 + 音节噪声 0.55+0.45·noise)
× 中部空间权重 sin^0.8 × 逐条 jitter(0.35+0.65·noise); 静默钳到 5px 点线
- 提交:按钮 3f 压缩 scale 1→0.82→1 回弹,波形 12f Easing.in 塌缩
到 0.06,麦克风辉光随包络亮灭
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 条数/间隙 | 64 条,gap 6px(1320px 宽胶囊) | 少于 40 条读作均衡器;密过 90 条单条读不出起伏 | | 滚动速率 | 1.6 帧/采样点 | 快了历史刷太快像故障;慢了"滚动感"消失变成原地抖 | | 说停包络 | seg 梯形 rise 5f / fall 7f;demo 说 15–57f、停 80–124f | 停顿段必须真的塌成点线——没有"停"的对比,"说"就不成立 | | 音节噪声 | 0.55+0.45·noiseAt(t/4.5) | 去掉音节层波形是平滑山丘,读不出"一句话的顿挫" | | 中部权重 | sin^0.8 空间包络 | 无权重全场等高像均衡器;指数 >1.5 中间独秀像单根跳 | | 静默钳位 | max 5px 点线 | 钳 0 波形消失读作断线;点线是"在听但你没说" | | 提交塌缩 | 12f Easing.in 到 0.06 + 按钮压缩回弹 3f | 塌缩是"这段话被收走了";淡出读不出提交语义 |
已知坑
- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,
首次实战须以真实素材回验
- 有音轨时波形必须跟人声走:片子若配了真旁白/人声,包络的说停段要对齐音轨的说停,错位一秒就穿帮;无人声配乐片才可用纯合成包络
- 与 spectrum-morph-ui 同片可共存(一个是功能一个是装饰),但别用同一种条形视觉——高度/颜色/圆角至少差一档,否则观众读作同一个东西乱入
- demo 的玻璃拟态胶囊(backdropFilter blur 24px)是 raycast 暗场风格,亮色片子换成实底胶囊即可,声纹参数不变
- 麦克风用的 emoji 加 grayscale 滤镜是占位——实战换 SVG 图标
- 声音:本卡画面就是"声音的形状",配人声最顺;提交钮一声轻 pop + 塌缩配短促下滑音
参考实现
demos/interaction/voice-waveform-live/ (VoiceWaveformLive.tsx) 原片出处:raycast-teams 19.5–26.0s
Mã nguồn tham chiếu (Remotion)
- demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/voice-waveform-live/VoiceWaveformLive.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.
- 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.
- Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
- Đă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/voice-waveform-live.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("voice-waveform-live")Prompt
Use the video-shotcraft shot recipe "Sóng âm giọng nói trực tiếp" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/voice-waveform-live.json Raw TSX: https://gimgs.net/motion/voice-waveform-live/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx; imports demos/_fixtures and assets/lib) Spec: 1920x1080, 30 fps, 5.06s 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/voice-waveform-live/VoiceWaveformLive.tsx =====
// voice-waveform-live —— raycast-teams 19.5–26s:
// 录音胶囊内实时声纹:细竖条随"说话"起伏(种子随机+相邻插值),
// 说话时中部高耸、停顿缩成点线,波形从右往左滚动;右端提交钮。
// 演:说(0.5–1.9s) → 停(1.9–2.7s) → 说(2.7–4.1s) → 提交(4.1–5s)。
import React from 'react';
import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';
const mulberry32 = (a: number) => () => {
let t = (a += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
// 值噪声:整数采样点取种子随机值,采样点之间平滑插值
const noiseAt = (x: number) => {
const i = Math.floor(x);
const fr = x - i;
const a = mulberry32(i * 7919 + 13)();
const b = mulberry32((i + 1) * 7919 + 13)();
const s = fr * fr * (3 - 2 * fr); // smoothstep
return a + (b - a) * s;
};
// 说话包络(按"声音发生时刻"计):说→停→说
const envelope = (t: number) => {
const seg = (a: number, b: number, rise = 5, fall = 7) =>
interpolate(t, [a, a + rise, b - fall, b], [0, 1, 1, 0], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
// 两段说话内部再叠音节起伏
const talk = Math.max(seg(15, 57), seg(80, 124));
const syllable = 0.55 + 0.45 * noiseAt(t / 4.5 + 200);
return talk * syllable;
};
const N_BARS = 64;
export const VoiceWaveformLive: React.FC = () => {
const f = useCurrentFrame();
// 提交动作
const submitAt = 126;
const submitted = f >= submitAt;
const btnPress = interpolate(f, [submitAt, submitAt + 3, submitAt + 9], [1, 0.82, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.out(Easing.ease),
});
// 提交后波形整体塌缩 + 胶囊微缩离场感
const collapse = interpolate(f, [submitAt, submitAt + 12], [1, 0.06], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.in(Easing.ease),
});
const capsuleScale = interpolate(f, [submitAt, submitAt + 20], [1, 0.96], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.out(Easing.ease),
});
// 胶囊入场
const inOp = interpolate(f, [0, 12], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.out(Easing.ease),
});
const inScale = interpolate(f, [0, 14], [1.04, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const SCROLL = 1.6; // 帧→采样时间比:滚动速度
const bars = Array.from({ length: N_BARS }).map((_, i) => {
// 从右往左滚动:最右条是"现在",越靠左越旧
const sampleT = f - (N_BARS - 1 - i) * SCROLL;
const env = sampleT < 0 ? 0 : envelope(sampleT);
// 空间权重:中部高耸
const center = Math.pow(Math.sin((i / (N_BARS - 1)) * Math.PI), 0.8);
const jitter = 0.35 + 0.65 * noiseAt(sampleT * 1.7 + i * 0.13);
const hRaw = env * center * jitter;
const h = Math.max(5, hRaw * 235 * collapse); // 静默=5px 点线
return h;
});
const nowEnv = envelope(f);
const micGlow = submitted ? 0 : nowEnv;
return (
<AbsoluteFill style={{ background: '#08080a', overflow: 'hidden' }}>
{/* 暗场绸缎底光 */}
<div style={{
position: 'absolute', left: -300, top: -200, width: 2600, height: 1700,
background: 'radial-gradient(closest-side, rgba(130,131,140,0.16), rgba(0,0,0,0) 70%)',
transform: `translate(${f * 0.6}px, ${f * 0.25}px)`,
}} />
<AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>
<div
style={{
width: 1320, height: 300, borderRadius: 150,
opacity: inOp,
transform: `scale(${inScale * capsuleScale})`,
background:
'linear-gradient(180deg, rgba(255,255,255,0.5), rgba(255,255,255,0.08) 40%, rgba(0,0,0,0.3))',
padding: 2.5, boxSizing: 'border-box',
}}
>
<div
style={{
width: '100%', height: '100%', borderRadius: 148,
background: 'rgba(24,25,29,0.72)',
backdropFilter: 'blur(24px)',
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.10), 0 40px 100px rgba(0,0,0,0.55)',
display: 'flex', alignItems: 'center', gap: 36,
padding: '0 44px', boxSizing: 'border-box',
}}
>
{/* 麦克风圆钮:说话时发亮 */}
<div
style={{
width: 96, height: 96, borderRadius: 48, flexShrink: 0,
background: `rgba(255,255,255,${0.08 + micGlow * 0.14})`,
border: '2.5px solid rgba(255,255,255,0.28)',
boxShadow: `0 0 ${28 * micGlow}px rgba(235,235,245,${micGlow * 0.5})`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 46, filter: 'grayscale(1)', boxSizing: 'border-box',
}}
>
🎙️
</div>
{/* 声纹条区 */}
<div style={{
flex: 1, height: 244, display: 'flex', alignItems: 'center',
gap: 6, overflow: 'hidden',
}}>
{bars.map((h, i) => (
<div
key={i}
style={{
flex: 1, height: h, borderRadius: 4,
background: `rgba(240,240,248,${0.4 + (h / 235) * 0.6})`,
}}
/>
))}
</div>
{/* 提交钮:白圆 + 上箭头 */}
<div
style={{
width: 96, height: 96, borderRadius: 48, flexShrink: 0,
background: submitted ? '#ffffff' : 'rgba(255,255,255,0.92)',
transform: `scale(${btnPress})`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: submitted
? '0 0 60px rgba(255,255,255,0.55)'
: '0 8px 24px rgba(0,0,0,0.4)',
}}
>
<svg width="44" height="44" viewBox="0 0 24 24">
<path
d="M12 20V5M12 5l-6.5 6.5M12 5l6.5 6.5"
stroke="#111114" strokeWidth="3" strokeLinecap="round"
strokeLinejoin="round" fill="none"
/>
</svg>
</div>
</div>
</div>
</AbsoluteFill>
</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/voice-waveform-live/VoiceWaveformLive.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 |
|---|---|---|---|
| — | |||
Cùng danh mục

Gõ, lọc, đi vào chi tiết
Gõ tìm kiếm trên UI thật, lưới tự thu về một thẻ duy nhất, rồi cú click xuyên vào trang chi tiết.

Đổi theme: quét dọc và gợn sóng
Hai kiểu đổi theme — theme-sweep quét chéo (biên quét qua đâu đổi da đến đó) và palette-ripple kiểu kết hợp (panel ⌘K co lại thành điểm, gợn sóng lan ra từ điểm đó đổi da).
Thumb điều khiển phân đoạn làm nhân vật chính
Cú dịch chuyển thumb trong control phân đoạn thành nhân vật chính cận cảnh — con trỏ bấm vào, thumb trắng trượt qua, icon mới bật lên.

Picker cuộn chọn tính năng
Bộ chọn dọc kiểu di động — viên thuốc tiêu điểm đứng yên, nội dung trôi qua nó, mỗi mục hãm phanh rồi dừng hẳn.