Motion · Transição
Circle Match Iris Cut
An iris wipe bursts open from the center of a circular element on the page, and the new page's circular chart lands on that same circle inside the ring — a match cut that gives the iris a semantic anchor.
Fonte: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composição Remotion (React/TSX) — vídeo de prévia, sem player no navegador
When to use
Cuts where the foreground has a circular element (avatar/icon/round button) and the background has a circular subject (donut chart/circular progress); a transition-technique card.
Intention
A plain iris wipe (the vocabulary's original entry) opens from any arbitrary point, and viewers only read "page changed." This card welds it to a match cut: the iris must burst from the center of a real circular element in the foreground, and the new page that grows inside it has a circular chart that lands right on that same circle — what viewers see is "the avatar's circle became the chart's circle," and the circle itself carries the narrative (this person → this person's data). This is a match cut's UI-native form: it doesn't rely on compositional coincidence, it relies on circle-to-circle semantics. Same family as the crossing-three-style cards ("the background is already present inside the foreground"), but the anchor here is an abstract shape rather than a container.
Duration & energy
- Duration: 4.7s (anchor pulse 30f + iris expansion 45f + chart growth 55f + hold 40f).
- Energy: Medium-high
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
普通 iris 光圈(词汇表原条目)从任意点开圈,观众只读到"换页了"。 本卡把它和 match-cut 焊死:光圈必须从前景一个真实圆形元素的圆心 炸开,圈内长出的新页里有个圆形图表恰好接在同一个圆上——观众看到的 是"头像的圆变成了图表的圆",圆形本身完成叙事(这个人→这个人的数据)。 这是 match-cut 在 UI 语境里的落地形态:不靠构图巧合,靠语义圆对圆。 与穿越三式同属"后景在前景里预先在场",但锚点是抽象形状而非容器。
动效核心
- 两景的圆严格同心是命门:锚点圆心写死为一组常量(demo:CX=308,
CY=384.8,手算自布局),clip-path circle、新景圆环、脉冲光环全部 引用同一组常量——坐标各算各的必然错心
- 定睛引导:光圈开圈前锚点元素两次放大脉冲 + 两道扩散光环(30f),
把视线钉在圆上再动
- 光圈
clip-path: circle(r at x y),r 22→2100px(45f inOut cubic);
新景圆环同窗从 22→170px 生长——接圆发生在扩张中途(观众在 光圈还没吃满屏时就看到圆被接住了),不是开完圈再画图
- 圆环 sweep:strokeDasharray = sweep×周长,rotate(-90) 从顶部起笔,
中央数字随 sweep 同步计数
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 锚点脉冲 | 2 次 scale 1→1.15 + 光环扩两道 / 30f | 没有定睛引导,开圈瞬间观众找不到圆心 | | 光圈扩张 | 22→2100px / 45f Easing.inOut(cubic) | 快于 30f 接圆读不到;圆心偏移 >10px 一眼穿帮 | | 接圆时机 | 圆环在光圈扩张中途(~40% 进度)即可见并同心生长 | 开完圈再长图表 = 普通转场+普通图表动画,匹配感归零 | | sweep | 45–100f 到目标值,数字同步计数 | 数字与 sweep 异步读作两个动画 | | 收尾 | sweep 完成后真静止 ≥40f | R1 |
已知坑
- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,
首次实战须以真实素材回验
- 真实素材上锚点坐标从截图量测(devtools/标尺),量到的是元素盒不是
视觉圆心——带内边距的头像取内容圆心;量完渲一帧对不齐就调常量, 别信第一次量的数(同 transition-travel C 腔心判例)
- 前景锚点必须是真圆:圆角方块(border-radius < 50%)当锚点,
开圈瞬间方圆错型一眼假;demo 里就是用白补丁把方块盖成真圆的
- 语义要成对:头像→个人数据、产品图标→产品指标——圆对圆只是形,
"这个圆的内容变成它的展开"才是匹配剪辑成立的因
- 一缝一式规矩同 shot-transitions;本卡自带图表生长段,圈内新景
别再叠入场动效
参考实现
demos/transition/circle-match-iris/ (CircleMatchIris.tsx)
Reference implementation (Remotion)
- demos/transition/circle-match-iris/CircleMatchIris.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/circle-match-iris/CircleMatchIris.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/circle-match-iris.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/transition/circle-match-iris/CircleMatchIris.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("circle-match-iris")Prompt
Use the video-shotcraft shot recipe "Circle Match Iris Cut" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/circle-match-iris.json Raw TSX: https://gimgs.net/motion/circle-match-iris/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/transition/circle-match-iris/CircleMatchIris.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/transition/circle-match-iris/CircleMatchIris.tsx =====
// 圆心匹配光圈切(match-cut × iris-reveal 组合):
// 帧 0–30:景 A(列表面板)hold,第 2 行 44px 圆形头像做两次脉冲 + 扩散光环提示"看这里";
// 帧 30–75:景 B 以 clip-path: circle(r at CX CY) 从 22px 炸开到 2100px(Easing.inOut(cubic)),
// 景 B 是深色圆环图表页,圆环半径同步从 22px 长到 170px——在光圈吃满全屏前就"接住"头像的圆;
// 帧 45–100:圆环描边 sweep 到 78%,中央大数字随之浮现计数;帧 100–140 全属性静止收尾(≥35f)。
// 命门:两景的圆严格同心——CX/CY 写死为 FakeDashboard B 第 2 行头像的屏幕坐标常量。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { FakeDashboard, G } from '../../_fixtures/Fixtures';
// FakeDashboard variant B 第 2 行左侧 44px 头像的圆心(手算自 fixture 布局)
const CX = 308;
const CY = 384.8;
export const CircleMatchIris: React.FC = () => {
const f = useCurrentFrame();
// ---- 景 A:头像脉冲(帧 0–30,两次呼吸) ----
const pulseT = Math.min(f, 30) / 30;
const scale = f < 30 ? 1 + 0.45 * Math.abs(Math.sin(pulseT * Math.PI * 2)) : 1;
// 两道扩散光环
const waves = [0, 14].map((start) => {
const p = interpolate(f, [start, start + 16], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
return { r: 22 + p * 40, o: f < start + 16 ? 0.85 * (1 - p) : 0 };
});
// ---- 光圈:景 B 从同一圆心炸开 ----
const irisR = interpolate(f, [30, 75], [22, 2100], {
easing: Easing.inOut(Easing.cubic),
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
// ---- 景 B 圆环:半径从 22 长到 170,"接住"头像的圆 ----
const ringR = interpolate(f, [30, 70], [22, 170], {
easing: Easing.inOut(Easing.cubic),
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
const ringW = interpolate(f, [30, 70], [12, 40], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
// 描边 sweep 到 78%
const sweep = interpolate(f, [45, 100], [0, 0.78], {
easing: Easing.out(Easing.cubic),
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
const circ = 2 * Math.PI * ringR;
const num = Math.round(sweep * 100);
const numOpacity = interpolate(f, [68, 88], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
const furnitureOpacity = interpolate(f, [60, 85], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
return (
<div style={{ width: 1920, height: 1080, position: 'relative', overflow: 'hidden', background: G.bg }}>
{/* ===== 景 A:列表面板 ===== */}
<FakeDashboard variant="B" />
{/* 白色补丁盖住 fixture 自带的圆角方块,再叠真正的圆形头像 */}
<div style={{ position: 'absolute', left: CX - 23, top: CY - 23, width: 46, height: 46, background: G.card }} />
<div style={{
position: 'absolute', left: CX - 22, top: CY - 22, width: 44, height: 44,
borderRadius: 22, background: G.mid, border: `3px solid ${G.ink}`,
boxSizing: 'border-box', transform: `scale(${scale})`,
}} />
{/* 脉冲扩散光环 */}
<svg width={1920} height={1080} style={{ position: 'absolute', left: 0, top: 0 }}>
{waves.map((w, i) => (
<circle key={i} cx={CX} cy={CY} r={w.r} fill="none" stroke={G.ink} strokeWidth={4} opacity={w.o} />
))}
</svg>
{/* ===== 景 B:深色圆环图表页,从同一圆心以光圈长出 ===== */}
{f >= 30 && (
<div style={{
position: 'absolute', left: 0, top: 0, width: 1920, height: 1080,
background: G.ink,
clipPath: `circle(${irisR}px at ${CX}px ${CY}px)`,
}}>
{/* 圆环 donut:圆心与头像严格同点 */}
<svg width={1920} height={1080} style={{ position: 'absolute', left: 0, top: 0 }}>
{/* 底轨 */}
<circle cx={CX} cy={CY} r={ringR} fill="none" stroke="#5a5a58" strokeWidth={ringW} />
{/* sweep 弧,从正上方起 */}
<circle
cx={CX} cy={CY} r={ringR} fill="none" stroke="#ececea"
strokeWidth={ringW} strokeLinecap="round"
strokeDasharray={`${sweep * circ} ${circ}`}
transform={`rotate(-90 ${CX} ${CY})`}
/>
</svg>
{/* 中央大数字 */}
<div style={{
position: 'absolute', left: CX - 150, top: CY - 80, width: 300, height: 160,
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
opacity: numOpacity,
}}>
<div style={{ fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 96, color: '#f2f2f0', letterSpacing: -2 }}>
{num}%
</div>
<div style={{ marginTop: 6, height: 12, width: 130, background: '#6a6a68', borderRadius: 6 }} />
</div>
{/* 右侧页面家具:标题 + 统计条,证明这是一整页 */}
<div style={{ position: 'absolute', left: 680, top: 260, opacity: furnitureOpacity, display: 'flex', flexDirection: 'column', gap: 30 }}>
<div style={{ height: 34, width: 520, background: '#c2c2c0', borderRadius: 10 }} />
<div style={{ height: 16, width: 780, background: '#5a5a58', borderRadius: 8 }} />
<div style={{ height: 16, width: 640, background: '#5a5a58', borderRadius: 8 }} />
<div style={{ height: 16, width: 700, background: '#5a5a58', borderRadius: 8 }} />
<div style={{ display: 'flex', gap: 28, marginTop: 24 }}>
{[0, 1, 2].map((i) => (
<div key={i} style={{ width: 240, height: 150, background: '#454543', border: '2px solid #5a5a58', borderRadius: 14, padding: 20, boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ height: 12, width: `${55 + i * 12}%`, background: '#8f8f8d', borderRadius: 6 }} />
<div style={{ height: 30, width: '45%', background: '#c2c2c0', borderRadius: 8, marginTop: 'auto' }} />
</div>
))}
</div>
</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/transition/circle-match-iris/CircleMatchIris.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

White flash: logo simplifies
A colorful liquid-gradient wordmark holds with flowing light, then the frame flashes white and overexposes; a flat wordmark fades in on white and settles — one flash completes the downgrade to simplicity.

Geometric wipe transitions
Two geometric wipe transitions — clock-wipe, a radar-hand sweep changing the page, and blinds-slice, 12 vertical strips flipping in staggered waves.

Hidden-cut transitions
Three hidden-cut transitions — foreground occlusion invisible cut, versus-slam opener, and warm light-leak burn; the hard cut hides inside 1-3 frames of occlusion/impact/light peak, so the viewer never sees the scissors.

Travel-through transitions
Two travel-through transitions — shared-element homecoming and letter-cavity pass-through; the camera drills through a real element inside the frame to complete the scene change.