Motion · Nhịp & montage
Ô lưới làm nhịp điệu
Ba kiểu nhịp lưới: chớp lấp đầy lưới, xếp lại cả lưới, hoặc xếp ô chéo nhiều góc máy — tất cả theo nhịp.
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
Biến thể
Lưới 3×3 gắn từng ô mỗi 2f theo thứ tự xáo trộn h(i), vào cứng (mỗi ô scale 1.18→1 trong 3f kèm rung nhấn đậm 2f), lấp đầy tường thì nghỉ một phách, ô trung tâm phóng to 3.28 lần trong 14f với Easing.in(cubic) để nuốt trọn màn hình.
Ma trận tính năng ra mắt toàn bộ trong một giây; dàn dựng trước cao trào
Hai bảng tọa độ được soạn sẵn (xếp hàng ngang / lưới 3×2); mỗi thẻ bay thẳng với delay=i×1.5f trong 16f easing inOut cubic kèm scale 1→1.28 vọt nhẹ 1.02; an vị xong thì rung nhấn brightness 0.78 toàn khung hình trong 6f.
Kể chuyện bằng bố cục: chuyển ngữ nghĩa từ xếp ngang sang phân nhóm
Mỗi ô trong ba ô mang một bản toàn trang cắt clip-path theo cạnh chéo 12° kèm translate/scale đặt góc máy (1x/1.9x/2.6x), bật vào từng ô cách nhau 2f; đứng yên 18f mỗi ô đẩy chậm để giữ sức sống, ô cuối cạnh chéo mở rộng trong 12f với out-cubic nuốt trọn màn hình.
Cùng một chủ thể, nhiều góc nhìn song song; đoạn mang cảm giác truyện tranh
Khi nào dùng
Biến "ô lưới" thành một bộ đếm nhịp: lần lượt xuất hiện từng ô theo phách (A), đổi vị trí tập thể tại điểm nhịp (B), cùng một chủ thể đứng yên ở nhiều góc máy song song (C); cả ba đều bám điểm nhịp
Ý đồ
Khác với wall-reveal-moves ở điểm neo: đó là "cả bức tường vào cảnh thế nào" (một lần tiết lộ), đây là "ô lưới làm bộ đếm nhịp thế nào" — chia ô, xếp lại, xếp song song đều diễn ra theo phách, mỗi lần đổi ô là một tiếng trống. A là lấp đầy: lưới 3×3 theo thứ tự xáo trộn kiểu nốt móc kép, từng ô vào cứng "bốp bốp", lấp đầy nghỉ một phách, ô trung tâm phóng to nuốt trọn màn hình — ma trận tính năng ra mắt trong một giây; B là di chuyển: 6 thẻ đổi vị trí thẳng hàng tập thể tại điểm nhịp (xếp ngang → lưới 3×2), an vị nửa giây kèm rung nhấn đậm hơn để khép lại — bản thân việc xếp lại bố cục là một tiếng trống; C là xếp song song: khung hình cắt cứng thành 3 ô chéo, cùng một sản phẩm đứng yên ở ba góc máy cùng lúc, nghỉ nửa phách rồi một ô mở rộng theo cạnh chéo nuốt trọn màn hình — ngôn ngữ chia màn hình của Scott Pilgrim. Chọn theo nhu cầu: ra mắt đa tính năng dùng A, kể chuyện bố cục dùng B, một sản phẩm nhiều góc nhìn dùng C.
Thời lượng & năng lượng
- Thời lượng: A ~4.7s / B ~4.8s / C ~5s
- Năng lượng: A Cao / B Trung bình / C Trung bình cao
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)
意图
与 wall-reveal-moves 的区别是本卡的定位锚:那是"整墙怎么入场" (一次性揭示),这是"格子怎么当节奏器"——分格、重排、并列都发生 在节拍上,格变即鼓点。A 是填:3×3 格按十六分音符乱序逐格啪啪硬入, 填满停一拍,中心格放大吞掉全屏——功能矩阵一秒全亮相;B 是移:6 张卡 在节拍点集体直线换位(横排→3×2 网格),半秒落定 + 加深脉冲收束—— 布局重排本身当一个鼓点;C 是并:画面咔咔切成 3 个斜线分格,同一产品 三种机位同时定格,停半拍后一格斜边扩张吃掉全屏——Scott Pilgrim 的 分屏语言。按需选:多功能亮相用 A,布局叙事用 B,单品多视角用 C。
三式选型
| 式 | 做法 | 适用 | |----|------|------| | A grid-flash-mosaic | 3×3 格每 2f 一格按 h(i) 乱序条件挂载硬入(入格 3f scale 1.18→1 + 2f 加深脉冲),满墙呼吸一拍,中心格 14f Easing.in(cubic) 放大 3.28x 吞屏 | 功能矩阵一秒全亮相;高潮前铺排 | | B flip-grid-reflow | 预写两套坐标表(横排/3×2 网格),每卡 delay=i×1.5f、16f inOut cubic 直线飞行 + scale 1→1.28 带 1.02 过冲;落定后 6f brightness 0.78 全画面脉冲 | 布局叙事:并列→分组的语义换挡 | | C comic-panel-split | 三格各一份整页 clip-path 12° 斜边裁切 + translate/scale 摆机位(1x/1.9x/2.6x),逐格 2f 间隔弹入;定格 18f 各格缓推保活,末格斜边 12f out-cubic 扩张吃屏 | 同一主体多视角并列;漫画感段落 |
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A 硬入 | 未到拍点直接不渲染(条件挂载),零淡化 | 逐格硬入无淡化——命门;淡入就不是"啪" | | A 描边 | 每格 3px 深色描边(border-box) | 白底提亮不可见判例的反向应用:白卡靠描边分格 | | A 吞屏 | 中心格塞 0.3125x 整页,放大 3.28(>3.2)让描边滑出画外 | 描边残在收尾满屏上就穿帮 | | B 坐标 | 两套表预写(行 y=330 / 网格列 571.6/960/1348.4、行 416.2/663.8),纯 transform | 免真 FLIP 量测;帧确定无随机 | | B 交错 | 偶数索引上排、奇数下排,轨迹互不对穿 | 乱排轨迹会恶性穿帮 | | B 脉冲 | f58–64 brightness 0.78,filter 仅窗口内条件挂载 | 摘罩判例;窗口外挂 filter 毁真静止 | | C 斜缝 | SVG 双线:16px 墨 #2f2f2f 底 + 10px 白核,12° 斜角 | 斜缝白线是漫画感命门;直缝读作分屏工具 | | C 保活 | 定格 18f 各格 scale 缓推 +3%/+5.5%/+8% | 三格全静读作截图拼贴 | | 收尾 | A 68f / B 81f / C 93f 真静止(≥40f) | R1;C 摘罩帧与扩张终值对齐防跳变 |
已知坑
- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,
首次实战须以真实素材回验
- C 三格必须是同一主体的不同机位——三格塞不同内容读作 bento
排版不是漫画分格,语义就散了
- 三式都吃拍点、声音强依赖:A 逐格一声啪、B 起飞与脉冲各一声、
C 逐格咔咔三声(sound-design §4.5);不踩拍的分格只是布局动画
- 同片分格系 ≤1 段——格子语言辨识度太强,两段就撞
- A 与 wall-reveal-moves 别同片连用(都是"墙",观众分不出
入场还是节奏器)
参考实现
demos/rhythm/panel-grid-moves/ (ComicPanelSplit.tsx / FlipGridReflow.tsx / GridFlashMosaic.tsx)
Mã nguồn tham chiếu (Remotion)
- demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx
- demos/rhythm/panel-grid-moves/FlipGridReflow.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/panel-grid-moves/FlipGridReflow.tsx
- demos/rhythm/panel-grid-moves/GridFlashMosaic.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/panel-grid-moves/GridFlashMosaic.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/panel-grid-moves.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx, demos/rhythm/panel-grid-moves/FlipGridReflow.tsx, demos/rhythm/panel-grid-moves/GridFlashMosaic.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("panel-grid-moves")Prompt
Use the video-shotcraft shot recipe "Ô lưới làm nhịp điệu" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/panel-grid-moves.json Raw TSX: https://gimgs.net/motion/panel-grid-moves/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx, demos/rhythm/panel-grid-moves/FlipGridReflow.tsx, demos/rhythm/panel-grid-moves/GridFlashMosaic.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/panel-grid-moves/ComicPanelSplit.tsx =====
// comic-panel-split|漫画分格并列
// FakeDashboard A(加 KPI 数字块)全屏 20f → 咔咔咔切成 3 个斜线分格
// (12° 斜边,10px 白缝 + 墨线描边),逐格 2f 间隔弹入(3f scale 1.06→1
// + 加深脉冲)。三格 = 同页面三机位:全景 1x / 卡片特写 1.9x / 数字区
// 特写 2.6x。定格 18f 各格缓慢微推近保持活 → 第三格斜边框 12f out-cubic
// 扩张吃掉全屏成为下一镜特写。57f 起摘罩(特写直出、分格结构与缝线
// 全部卸载),57–150f 真静止 93f ≥ 40f。帧确定,无随机源。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';
import { FakeDashboard, G } from '../../_fixtures/Fixtures';
const SPLIT = 20; // 全屏 A 结束、开始分格
const POP = 3; // 每格弹入 3f
const STAGGER = 2; // 逐格 2f 间隔
const HOLD_END = 45; // 27f 弹完 + 定格 18f
const EXPAND_END = 57; // 第三格 12f 扩张结束
// 57–150 真静止 93f
// 斜边 12°:tan(12°)×1080 ≈ 230,顶边比底边右移 230
// 缝 10px(水平半宽 5px)
// 边界1:顶 750 / 底 520;边界2:顶 1405 / 底 1175
const outCubic = Easing.out(Easing.cubic);
// 页面 = FakeDashboard A + 左上卡片内 KPI 数字块(给"数字区特写"一个锚点)
const PageA: React.FC = () => (
<div style={{ width: 1920, height: 1080, position: 'relative' }}>
<FakeDashboard variant="A" />
<div style={{
position: 'absolute', left: 328, top: 320, width: 380, height: 160,
background: G.card, borderRadius: 12, display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', gap: 8,
}}>
<div style={{ fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 96, color: G.ink, letterSpacing: -2, lineHeight: 1 }}>
1,284
</div>
<div style={{ height: 10, width: 150, background: G.mid, borderRadius: 5 }} />
</div>
</div>
);
type PanelSpec = {
clip: (f: number) => string; // clip-path polygon
centroidX: number; // 弹入缩放原点
originX: number; originY: number; // 内容变换原点(焦点)
baseScale: number; // 机位倍率
tx: number; ty: number; // 焦点搬到格中心的位移
z: number;
};
export const ComicPanelSplit: React.FC = () => {
const frame = useCurrentFrame();
// ===== 摘罩:扩张完成后特写直出,分格结构 / 缝线全部卸载 =====
if (frame >= EXPAND_END) {
return (
<AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>
<div style={{
width: 1920, height: 1080,
transform: 'translate(442px, 140px) scale(2.6)',
transformOrigin: '518px 400px',
}}>
<PageA />
</div>
</AbsoluteFill>
);
}
// ===== 阶段 1:全屏 A =====
if (frame < SPLIT) {
return (
<AbsoluteFill style={{ background: G.bg }}>
<PageA />
</AbsoluteFill>
);
}
// ===== 阶段 2/3:分格 + 第三格扩张 =====
// 定格期微推近(27–45f 线性,很缓)
const push = interpolate(frame, [SPLIT + 2 * STAGGER + POP, HOLD_END], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
// 第三格扩张进度(扩散用 out-cubic)
const ex = interpolate(frame, [HOLD_END, EXPAND_END], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: outCubic,
});
// 第三格左斜边(= 边界2 吃屏轨迹)
const e3Top = 1410 + ex * (-60 - 1410); // 1410 → -60
const e3Bot = 1180 + ex * (-290 - 1180); // 1180 → -290
const panels: PanelSpec[] = [
{ // 全景 1x
clip: () => 'polygon(0px 0px, 745px 0px, 515px 1080px, 0px 1080px)',
centroidX: 315, originX: 960, originY: 540,
baseScale: 1 + push * 0.03, tx: 0, ty: 0, z: 1,
},
{ // 卡片特写 1.9x(中上卡片)
clip: () => 'polygon(755px 0px, 1400px 0px, 1170px 1080px, 525px 1080px)',
centroidX: 962, originX: 1070, originY: 371,
baseScale: 1.9 + push * 0.055, tx: -108, ty: 169, z: 1,
},
{ // 数字区特写 2.6x(KPI 块),扩张时焦点从格中心搬到屏中心
clip: () => `polygon(${e3Top}px 0px, 1920px 0px, 1920px 1080px, ${e3Bot}px 1080px)`,
centroidX: 1607, originX: 518, originY: 400,
// 扩张时 push 增量退掉,scale 收敛回 2.6(与摘罩帧完全一致)
baseScale: 2.6 + push * 0.08 * (1 - ex), tx: 1089 + ex * (442 - 1089), ty: 140, z: 3,
},
];
// 缝线透明度:边界1 随第 2 格弹入出现、扩张期被吃前线性淡出;
// 边界2 随第 3 格弹入出现、扩张末 4f 线性淡出(消散用线性)
const seam1O = Math.min(
interpolate(frame, [SPLIT + STAGGER, SPLIT + STAGGER + 2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
interpolate(frame, [HOLD_END, HOLD_END + 3], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
);
const seam2O = Math.min(
interpolate(frame, [SPLIT + 2 * STAGGER, SPLIT + 2 * STAGGER + 2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
interpolate(frame, [EXPAND_END - 4, EXPAND_END], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
);
return (
<AbsoluteFill style={{ background: '#ffffff' }}>
{panels.map((p, i) => {
const start = SPLIT + i * STAGGER;
if (frame < start) return null; // 未弹入不渲染
// 弹入:3f scale 1.06→1 + 加深脉冲
const pop = interpolate(frame, [start, start + POP], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: outCubic,
});
const popScale = 1.06 - 0.06 * pop;
const pulse = 0.3 * (1 - pop);
return (
<div key={i} style={{
position: 'absolute', inset: 0, zIndex: p.z,
clipPath: p.clip(frame),
transform: `scale(${popScale})`,
transformOrigin: `${p.centroidX}px 540px`,
}}>
<div style={{
width: 1920, height: 1080,
transform: `translate(${p.tx}px, ${p.ty}px) scale(${p.baseScale})`,
transformOrigin: `${p.originX}px ${p.originY}px`,
}}>
<PageA />
</div>
{pulse > 0.005 && (
<div style={{ position: 'absolute', inset: 0, background: `rgba(0,0,0,${pulse})` }} />
)}
</div>
);
})}
{/* 斜缝:白缝命门 + 墨线描边(暗 16 下、白 10 上 → 两侧各 3px 墨边) */}
{(seam1O > 0.005 || seam2O > 0.005) && (
<svg width={1920} height={1080} style={{ position: 'absolute', inset: 0, zIndex: 5, pointerEvents: 'none' }}>
{seam1O > 0.005 && (
<g opacity={seam1O}>
<line x1={750} y1={-10} x2={520} y2={1090} stroke="#2f2f2f" strokeWidth={16} />
<line x1={750} y1={-10} x2={520} y2={1090} stroke="#ffffff" strokeWidth={10} />
</g>
)}
{seam2O > 0.005 && (
<g opacity={seam2O}>
<line x1={e3Top - 5} y1={-10} x2={e3Bot - 5} y2={1090} stroke="#2f2f2f" strokeWidth={16} />
<line x1={e3Top - 5} y1={-10} x2={e3Bot - 5} y2={1090} stroke="#ffffff" strokeWidth={10} />
</g>
)}
</svg>
)}
</AbsoluteFill>
);
};
// ===== demos/rhythm/panel-grid-moves/FlipGridReflow.tsx =====
// flip-grid-reflow —— 网格 FLIP 重排
// 6 张 Card 初始横排一行(marquee 式,屏心偏上)静止 30f,节拍点集体换位:
// 每卡沿直线飞向 3×2 网格目标位(预写两套坐标表),scale 1→1.28,
// delay = 卡索引×1.5f 微错峰,16f inOut(cubic) + 落定 3f 过冲 1.02。
// 全部落定后整体加深脉冲收束。收尾真静止 ≥40f。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, Card, TitleBlock } from '../../_fixtures/Fixtures';
const CARD_W = 280;
const CARD_H = 170;
const N = 6;
// ---- 坐标表 A:横排一行(卡间 24px,屏心偏上)----
// 总宽 6*280 + 5*24 = 1800,x0 = 60;中心 y = 330
const ROW_Y = 330;
const ROW_CENTERS: Array<[number, number]> = Array.from({ length: N }, (_, i) => [
60 + CARD_W / 2 + i * (CARD_W + 24),
ROW_Y,
]); // x: 200, 504, 808, 1112, 1416, 1720
// ---- 坐标表 B:3×2 网格(卡放大 1.28 → 358.4×217.6,网格间距 30px)----
// 网格总宽 3*358.4 + 2*30 = 1135.2 → x0 = 392.4;总高 2*217.6 + 30 = 465.2 → y0 = 307.4
const GC = [571.6, 960, 1348.4]; // 列中心
const GR = [416.2, 663.8]; // 行中心
// 交错映射:偶数索引去上排、奇数去下排(每卡直线飞行、互不对穿)
const GRID_CENTERS: Array<[number, number]> = [
[GC[0], GR[0]], // card0 → 上左
[GC[0], GR[1]], // card1 → 下左
[GC[1], GR[0]], // card2 → 上中
[GC[1], GR[1]], // card3 → 下中
[GC[2], GR[0]], // card4 → 上右
[GC[2], GR[1]], // card5 → 下右
];
const BEAT = 30; // 节拍点
const STAGGER = 1.5; // 每卡延迟
const MOVE = 16; // 飞行帧数
const SETTLE = 3; // 落定过冲帧数
const SCALE_END = 1.28;
const OVERSHOOT = 1.02;
// 末卡完全落定:30 + 5*1.5 + 16 + 3 = 56.5
const ALL_SETTLED = BEAT + (N - 1) * STAGGER + MOVE + SETTLE; // 56.5
// 加深脉冲:f58 → f61 谷值 → f64 回正(谷值 brightness 0.78,spec 4f 放宽到 6f 保可感)
const PULSE_IN = 58;
const PULSE_MID = 61;
const PULSE_OUT = 64;
const moveEase = Easing.inOut(Easing.cubic);
const FlipCard: React.FC<{ i: number; frame: number }> = ({ i, frame }) => {
const t0 = BEAT + i * STAGGER;
const [x0, y0] = ROW_CENTERS[i];
const [x1, y1] = GRID_CENTERS[i];
const x = interpolate(frame, [t0, t0 + MOVE], [x0, x1], {
easing: moveEase, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
const y = interpolate(frame, [t0, t0 + MOVE], [y0, y1], {
easing: moveEase, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
// scale:16f 冲到 1.28*1.02,随后 3f 回落到 1.28
const sUp = interpolate(frame, [t0, t0 + MOVE], [1, SCALE_END * OVERSHOOT], {
easing: moveEase, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
const sBack = interpolate(frame, [t0 + MOVE, t0 + MOVE + SETTLE], [SCALE_END * OVERSHOOT, SCALE_END], {
easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
const s = frame < t0 + MOVE ? sUp : sBack;
return (
<div
style={{
position: 'absolute',
left: x - CARD_W / 2,
top: y - CARD_H / 2,
width: CARD_W,
height: CARD_H,
transform: `scale(${s})`,
transformOrigin: '50% 50%',
zIndex: i,
}}
>
<Card w={CARD_W} h={CARD_H} seed={i + 1} />
</div>
);
};
export const FlipGridReflow: React.FC = () => {
const frame = useCurrentFrame();
// 加深脉冲:仅脉冲窗口内挂载 filter,窗口外完全不挂(摘罩)
const pulsing = frame >= PULSE_IN && frame <= PULSE_OUT;
const bright = pulsing
? interpolate(frame, [PULSE_IN, PULSE_MID, PULSE_OUT], [1, 0.78, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
})
: 1;
return (
<div
style={{
width: 1920,
height: 1080,
background: G.bg,
position: 'relative',
overflow: 'hidden',
...(pulsing ? { filter: `brightness(${bright})` } : {}),
}}
>
<div style={{ position: 'absolute', left: 70, top: 56 }}>
<TitleBlock text="FLIP GRID REFLOW" size={44} />
</div>
{Array.from({ length: N }).map((_, i) => (
<FlipCard key={i} i={i} frame={frame} />
))}
</div>
);
};
// ===== demos/rhythm/panel-grid-moves/GridFlashMosaic.tsx =====
// grid-flash-mosaic —— 九宫格闪切
// 浅底 → f25 起 3×3 网格按十六分音符(每 2f)逐格啪啪硬入(顺序 h(i) 打乱),
// 每格 = FakeDashboard 不同区域裁切片或灰卡;入格 3f scale 1.18→1 + 2f 加深脉冲。
// 填满后停 14f(整墙微呼吸 1.008) → 中心格 14f Easing.in(cubic) 放大吞掉全屏
// 成为满屏页面。收尾真静止 ≥40f。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, Card, FakeDashboard } from '../../_fixtures/Fixtures';
const h = (n: number) => {
const s = Math.sin(n * 127.3) * 43758.5453;
return s - Math.floor(s);
};
const CELL_W = 600;
const CELL_H = 340;
const GAP = 12;
const GRID_X = (1920 - (CELL_W * 3 + GAP * 2)) / 2; // 48
const GRID_Y = (1080 - (CELL_H * 3 + GAP * 2)) / 2; // 18
const FILL_START = 25; // 第一格落下
const STEP = 2; // 十六分音符:每 2f 一格
// 打乱顺序:索引按 h(i+1) 排序
const ORDER = Array.from({ length: 9 }, (_, i) => i).sort(
(a, b) => h(a + 1) - h(b + 1)
);
const RANK: number[] = [];
ORDER.forEach((cell, k) => (RANK[cell] = k));
const LAST_IN = FILL_START + 8 * STEP + 3; // 最后一格入格动画结束 f44
const HOLD_END = LAST_IN + 14; // 整墙呼吸段结束 f58
const ZOOM_DUR = 14;
const ZOOM_END = HOLD_END + ZOOM_DUR; // f72,之后真静止
// 中心格内是整页 FakeDashboard 缩小版(0.3125),放大后正好成为满屏页面
const MINI_SCALE = CELL_W / 1920; // 0.3125
// 3.2 恰好铺满;加到 3.28 让格子的深色描边完全滑出画外,收尾满屏页面干净
const ZOOM_SCALE = 3.28;
// 非中心格:FakeDashboard 裁切偏移 (x, y, variant) 或灰卡(null)
const CROPS: Array<{ x: number; y: number; v: 'A' | 'B' } | null> = [
{ x: -120, y: -60, v: 'A' },
null, // 灰卡
{ x: -1260, y: -120, v: 'B' },
{ x: -60, y: -520, v: 'B' },
null, // 中心格(单独处理,占位)
{ x: -1300, y: -640, v: 'A' },
{ x: -420, y: -300, v: 'B' },
null, // 灰卡
{ x: -900, y: -680, v: 'A' },
];
const CellContent: React.FC<{ i: number }> = ({ i }) => {
if (i === 4) {
// 中心格:整页 dashboard 缩到格内
return (
<div
style={{
position: 'absolute',
left: 0,
top: (CELL_H - 1080 * MINI_SCALE) / 2,
transform: `scale(${MINI_SCALE})`,
transformOrigin: 'top left',
}}
>
<FakeDashboard variant="A" />
</div>
);
}
const crop = CROPS[i];
if (crop === null) {
// 灰卡格
return (
<div
style={{
width: '100%',
height: '100%',
background: G.panel,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Card w={430} h={240} seed={i * 3 + 2} />
</div>
);
}
// 裁切片:整页 dashboard 以不同偏移塞进格子(相当于 backgroundPosition 各异)
return (
<div style={{ position: 'absolute', left: crop.x, top: crop.y }}>
<FakeDashboard variant={crop.v} />
</div>
);
};
export const GridFlashMosaic: React.FC = () => {
const f = useCurrentFrame();
// 整墙微呼吸:仅在填满后的 14f 停顿段,一个正弦来回 1→1.008→1
const breath =
f >= LAST_IN && f < HOLD_END
? 1 + 0.008 * Math.sin((Math.PI * (f - LAST_IN)) / 14)
: 1;
// 中心格放大:14f Easing.in(cubic),吞掉全屏
const zoom = interpolate(f, [HOLD_END, ZOOM_END], [1, ZOOM_SCALE], {
easing: Easing.in(Easing.cubic),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return (
<div
style={{
width: 1920,
height: 1080,
background: G.bg,
position: 'relative',
overflow: 'hidden',
}}
>
<div
style={{
position: 'absolute',
inset: 0,
transform: `scale(${breath})`,
transformOrigin: '960px 540px',
}}
>
{Array.from({ length: 9 }).map((_, i) => {
const start = FILL_START + RANK[i] * STEP;
if (f < start) return null; // 硬入:未到拍点不渲染,无淡化
const row = Math.floor(i / 3);
const col = i % 3;
// 入格 3f scale 1.18→1
const popScale = interpolate(f, [start, start + 3], [1.18, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// 2f 加深脉冲
const darken = interpolate(f, [start, start + 2], [0.45, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const isCenter = i === 4;
const cellScale = isCenter ? popScale * zoom : popScale;
return (
<div
key={i}
style={{
position: 'absolute',
left: GRID_X + col * (CELL_W + GAP),
top: GRID_Y + row * (CELL_H + GAP),
width: CELL_W,
height: CELL_H,
overflow: 'hidden',
background: G.card,
border: `3px solid ${G.ink}`,
boxSizing: 'border-box',
transform: `scale(${cellScale})`,
transformOrigin: 'center',
zIndex: isCenter ? 10 : 1,
}}
>
<CellContent i={i} />
{darken > 0.001 && (
<div
style={{
position: 'absolute',
inset: 0,
background: '#000',
opacity: darken,
}}
/>
)}
</div>
);
})}
</div>
</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/panel-grid-moves/ComicPanelSplit.tsx, demos/rhythm/panel-grid-moves/FlipGridReflow.tsx, demos/rhythm/panel-grid-moves/GridFlashMosaic.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

Ngữ pháp trailer: ba nhịp
Ba thủ pháp ngữ pháp trailer — trailer-bumper cắt nhanh móc câu mở đầu, card-footage-cadence xen thẻ chữ với cảnh đối thoại, smash-cut đập cắt vào tĩnh lặng.

Đổi tốc độ và đóng băng chú thích
Hai thủ pháp nhịp điệu remap số khung hình phi tuyến — speed-ramp (nhanh → 0.2x nhìn kỹ → nhanh) và freeze-annotate (chảy → đóng băng khoanh chú thích → tan băng).

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.

Ảnh bóng lặp kiểu smear
Ảnh bóng smear — thẻ để lại 4 bản sao bán trong suốt rõ, đếm được khi lướt ngang nhanh, gộp lại làm một lúc dừng; phương án dựng hình thay cho motion blur.