Motion · Ritmo
Panel grid rhythm moves
Three grid-rhythm techniques: flash-fill a grid, reshuffle it, or line up angled panels — all riding the beat.
Fonte: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composição Remotion (React/TSX) — vídeo de prévia, sem player no navegador
Variantes
3×3 格每 2f 一格按 h(i) 乱序条件挂载硬入(入格 3f scale 1.18→1 + 2f 加深脉冲),满墙呼吸一拍,中心格 14f Easing.in(cubic) 放大 3.28x 吞屏
功能矩阵一秒全亮相;高潮前铺排
预写两套坐标表(横排/3×2 网格),每卡 delay=i×1.5f、16f inOut cubic 直线飞行 + scale 1→1.28 带 1.02 过冲;落定后 6f brightness 0.78 全画面脉冲
布局叙事:并列→分组的语义换挡
三格各一份整页 clip-path 12° 斜边裁切 + translate/scale 摆机位(1x/1.9x/2.6x),逐格 2f 间隔弹入;定格 18f 各格缓推保活,末格斜边 12f out-cubic 扩张吃屏
同一主体多视角并列;漫画感段落
When to use
Using the "grid" itself as a rhythm device: cell-by-cell reveal on the beat (A), collective repositioning on the beat (B), same-subject multi-angle freeze layout (C); all three ride the beat
Intention
The difference from wall-reveal-moves is the anchor point: that card is about "how the whole wall enters" (a one-time reveal), this one is about "how the grid becomes a rhythm device" — splitting, reshuffling, and lining up panels all happen on the beat, each grid change is a drum hit. A fills: a 3×3 grid hard-mounts cell by cell in sixteenth-note scrambled order, holds a beat once full, then the center cell scales up 3.28x over 14f with Easing.in(cubic) to swallow the screen — a full feature matrix reveal in one second. B moves: six cards collectively swap straight-line positions on the beat (row → 3×2 grid), settling in half a second plus a deepening pulse to close it out — the layout reshuffle itself is a drum hit. C aligns: the frame cuts hard into 3 diagonal panels, the same product freezes in three different camera angles at once, holds half a beat, then one panel's diagonal edge expands to swallow the screen — Scott Pilgrim's split-screen language. Choose by need: multi-feature reveal uses A, layout narrative uses B, single-product multi-angle uses C.
Duration & energy
- Duration: A ~4.7s / B ~4.8s / C ~5s
- Energy: A High / B Medium / C Medium-high
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
与 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)
Reference implementation (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
The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.
Tudo nesta página também é legível por máquina. Aponte seu agente para o JSON ou adicione o servidor MCP do gimgs e peça para buscar templates de motion.
- Baixe o JSON — contém a receita do plano (timing, easing, tabela de parâmetros, armadilhas) e o código TSX completo.
- Clone o repositório video-shotcraft; o componente importa fixtures de demos/_fixtures e helpers de assets/lib.
- Registre o componente como <Composition> em um projeto Remotion e renderize com `npx remotion render`, ou reimplemente a receita no seu próprio engine.
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 "Panel grid rhythm moves" (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 |
|---|---|---|---|
| — | |||
Mais nesta categoria

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).

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.

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.