Motion · Kết
Kết phim kiểu phép trừ
Kết phim kiểu phép trừ — bấm Publish xong, toàn bộ UI trình soạn bốc hơi từng lớp lệch nhịp từ ngoài vào trong, chỉ còn lại nút vừa bấm trượt vào tâm màn hình phóng to, rồi mờ đi trao lại cho chữ hiệu định vị.
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
Kết phim mang ngữ nghĩa "phát hành/hoàn tất"; kết sản phẩm muốn nói "sau một cú bấm mọi phức tạp biến mất"
Ý đồ
Kết phim trong thư viện toàn thuộc "phép cộng": outro-group-photo-launch cho các phần tử bay tới chụp ảnh chung, logo sting là đứng vững rồi thêm trứng phục sinh. Thẻ này là con đường "phép trừ" duy nhất — một cú bấm kích nổ toàn bộ UI thoát cảnh, nền đen chỉ để lại tiêu điểm ngữ nghĩa (chiếc nút vừa bấm), dịch "phát hành = độ phức tạp về không" thành ngôn ngữ chuyển động. Có hai mấu chốt: bốc hơi phải có trật tự — mỗi 4f một lớp từ ngoài vào trong, mỗi lớp mang dịch chuyển có hướng riêng (mỗi phần tử tản ra một hướng ra ngoài khung), lộn xộn hoặc tắt cùng khung đọc thành sự cố mất điện; nút là kẻ sống sót duy nhất và phải di chuyển — trong lúc bốc hơi nó trượt từ góc thanh công cụ vào tâm màn hình phóng to 1.5 lần, quầng trắng mạnh dần theo nền đen, hoàn tất việc thăng cấp từ "điều khiển UI" thành "nhân vật chính nghi thức", cuối cùng mờ đi trao nền đen lại cho chữ hiệu.
Thời lượng & năng lượng
- Thời lượng: khoảng 4.3s (130f: con trỏ vào vị trí 34f → bốc hơi khoảng 40f → nút độc chiếm → chữ hiệu tiếp quản)
- Năng lượng: Trung bình (đoạn đầu thao tác lặng lẽ, đoạn giữa thoát cảnh liên tục, không có cú va chạm tức thời)
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)
意图
库内收尾全是"加法":outro-group-photo-launch 元素飞来合影,logo sting 是定住再彩蛋。本卡是唯一的"减法"路线——点击引爆全 UI 退场, 黑场只留语义焦点(那颗被点的按钮),把"发布 = 复杂性归零"翻译成 动效叙事。命门有二:蒸发要有秩序——从外围到中心每 4f 一层、 每层带方向性位移(各自往画外散),乱序或同帧齐灭读作断电故障; 按钮是唯一幸存者且必须迁移——蒸发期间从工具条角落滑到屏心 放大 1.5 倍、白晕随黑场增强,完成从"UI 控件"到"仪式主角"的升格, 最后淡出把黑场交给字标。
动效核心
- 6 个蒸发层:侧栏→属性面板→画布卡→工具条两端→画布底→工具条中段,
点击后 +4f 起每 4f 一层;每层 STRIP_DUR 14f,Easing.in(quad) 离场加速,fade + 方向性位移(侧栏 −140,0 / 面板 −90,20 / 工具条两端 ∓80,−60 / 画布底 0,40 / 工具条中段 0,−50)
- 画布内 4 张卡再各错峰 3f,位移左右交替(±70px)——大块整体消失
会读作图层被删,碎开走才是"蒸发"
- 背景压黑:随画布底层蒸发(+20f 起 20f)灰底 → #111110
- 按钮:点击 spring(damping 12, stiffness 220) 压缩脉冲;蒸发期
位置/尺寸插值滑向屏心(18f,inOut cubic),scale ×1.5,白晕 30→70px 随黑场增强;+52f 起 12f 淡出
- 字标接棒:+62f spring(damping 14, stiffness 90) 淡入 +
86%→100% 缩放,黑场定版
- 光标点击后 10f 内淡出——主角换人,光标必须退场
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 层间错峰 | 4f × 6 层 | 同帧齐灭读作断电;>8f 拖成慢拆迁,仪式感散 | | 单层时长 | 14f,ease-in 加速 | ease-out 离场读作"飘走"不读作"蒸发";>20f 黏 | | 蒸发顺序 | 外围 → 中心,按钮所在层最后 | 中心先空读作故障;按钮层先走全卡崩塌 | | 位移幅度 | 50–140px,方向各自向画外 | 无位移纯 fade 读作调透明度;同方向读作整屏平移 | | 按钮迁移 | 18f 滑到屏心 + scale ×1.5 + 晕光增强 | 不迁移则黑场上一颗角落小钮,读作漏删;先迁后灭顺序不可反 | | 字标接棒 | 按钮淡出后 ~10f 进,spring 定版 | 与按钮同屏抢戏;黑场空窗 >20f 像放完了 |
已知坑
- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,
首次实战须以真实素材回验
- 与 outro-group-photo-launch 分工:全家福是"加法"峰值终镜(多功能
产品全员谢幕),本卡是"减法"收尾(单一动作语义、越走越静)——按 产品气质二选一,不可同片双收尾
- 与 edit-hook-moves B(logo-sting-button)衔接:本卡字标定版后可接
sting 彩蛋,但 sting 的"突插"会打破减法的静——接不接看全片性格
- 蒸发的每层必须是语义完整的 UI 区块(侧栏/面板/工具条),按
像素区域切层会在错峰时露出结构不合理的残缺态
- 按钮迁移全程 zIndex 压最顶——任何蒸发层从按钮前面飘过都是穿帮
- 触发动作必须是"终结性"语义(Publish/Ship/Done);拿 Save 或普通
确认触发全 UI 蒸发,语义配不上排场
- 实战按钮用品牌高亮色(原片是 Framer 蓝),demo 的白钮只是灰阶占位
——黑场上按钮与晕光的对比是主角光环,颜色弱则升格失败
参考实现
demos/outro/ui-strip-away-outro/ (UiStripAwayOutro.tsx) 原片出处:framer-ai 33–36.5s
Mã nguồn tham chiếu (Remotion)
- demos/outro/ui-strip-away-outro/UiStripAwayOutro.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/outro/ui-strip-away-outro/UiStripAwayOutro.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/ui-strip-away-outro.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/outro/ui-strip-away-outro/UiStripAwayOutro.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("ui-strip-away-outro")Prompt
Use the video-shotcraft shot recipe "Kết phim kiểu phép trừ" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/ui-strip-away-outro.json Raw TSX: https://gimgs.net/motion/ui-strip-away-outro/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/outro/ui-strip-away-outro/UiStripAwayOutro.tsx; imports demos/_fixtures and assets/lib) Spec: 1920x1080, 30 fps, 4.39s 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/outro/ui-strip-away-outro/UiStripAwayOutro.tsx =====
// ui-strip-away-outro —— framer-ai 33–36.5s
// 满屏灰阶"编辑器"里光标点击高亮 Publish 按钮 → UI 层层错峰蒸发退场
// (每层 fade + 轻微位移,从外围到中心)→ 黑场只剩按钮 → 按钮淡出交棒字标。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, spring, useVideoConfig, Easing } from 'remotion';
import { G, Card } from '../../_fixtures/Fixtures';
const CLICK = 34; // 点击时刻
// 蒸发层级(点击后延迟,外围先走)
const STRIP = {
sidebar: CLICK + 4,
leftPanel: CLICK + 8,
canvasCards: CLICK + 12,
topbarEnds: CLICK + 16,
canvasBg: CLICK + 20,
toolbarShell: CLICK + 24,
};
const STRIP_DUR = 14;
const BTN_FADE = CLICK + 52;
const LOGO_IN = CLICK + 62;
// 某层的蒸发进度 → {opacity, offset}
const useStrip = (frame: number, start: number, dx: number, dy: number) => {
const p = interpolate(frame, [start, start + STRIP_DUR], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad), // 离场加速
});
return {
opacity: 1 - p,
transform: `translate(${dx * p}px, ${dy * p}px)`,
};
};
export const UiStripAwayOutro: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// 背景:编辑器灰底 → 黑场(随 canvasBg 层蒸发压黑)
const bgDark = interpolate(frame, [STRIP.canvasBg, STRIP.canvasBg + STRIP_DUR + 6], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.quad),
});
const sidebar = useStrip(frame, STRIP.sidebar, -140, 0);
const leftPanel = useStrip(frame, STRIP.leftPanel, -90, 20);
const topLeft = useStrip(frame, STRIP.topbarEnds, -80, -60);
const topRight = useStrip(frame, STRIP.topbarEnds, 80, -60);
const toolbarShell = useStrip(frame, STRIP.toolbarShell, 0, -50);
const canvasFrame = useStrip(frame, STRIP.canvasBg, 0, 40);
// 画布卡片错峰蒸发
const cardStrip = (i: number) => useStripStatic(frame, STRIP.canvasCards + i * 3, (i % 2 ? 70 : -70), 50 + i * 10);
// 按钮:点击脉冲 + 最后淡出
const press = spring({ frame: frame - CLICK, fps, config: { damping: 12, stiffness: 220 } });
const pressScale = frame < CLICK ? 1 : 1 - 0.12 * Math.sin(Math.min(1, press) * Math.PI);
const btnOp = interpolate(frame, [BTN_FADE, BTN_FADE + 12], [1, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// 蒸发期间按钮从工具条位置滑向屏幕中心,独占黑场
const btnCenter = interpolate(frame, [STRIP.toolbarShell, STRIP.toolbarShell + 18], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.cubic),
});
const btnX = 1560 + (960 - 88 - 1560) * btnCenter;
const btnY = 30 + (540 - 30 - 30) * btnCenter;
const btnScale = 1 + 0.5 * btnCenter;
// 字标接棒
const logoP = spring({ frame: frame - LOGO_IN, fps, config: { damping: 14, stiffness: 90 } });
// 光标移向按钮
const curX = interpolate(frame, [4, CLICK - 2], [820, 1636], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.inOut(Easing.quad),
});
const curY = interpolate(frame, [4, CLICK - 2], [640, 64], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.inOut(Easing.quad),
});
const curOp = interpolate(frame, [CLICK + 6, CLICK + 16], [1, 0], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
return (
<AbsoluteFill style={{ background: '#111110', overflow: 'hidden' }}>
{/* 编辑器灰底(作为一层可蒸发的背景) */}
<AbsoluteFill style={{ background: G.bg, opacity: 1 - bgDark }} />
{/* 左侧栏(图层面板) */}
<div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: 240, background: G.side, padding: '90px 24px', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 20, ...sidebar }}>
{Array.from({ length: 9 }).map((_, i) => (
<div key={i} style={{ height: 13, width: `${55 + ((i * 31) % 40)}%`, background: G.sideBar, borderRadius: 6 }} />
))}
</div>
{/* 右侧属性面板 */}
<div style={{ position: 'absolute', right: 0, top: 60, bottom: 0, width: 300, background: G.panel, borderLeft: `2px solid ${G.line}`, padding: 28, boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 18, ...leftPanel }}>
{Array.from({ length: 4 }).map((_, i) => (
<React.Fragment key={i}>
<div style={{ height: 12, width: '45%', background: G.bar, borderRadius: 6 }} />
<div style={{ height: 34, background: '#fff', border: `2px solid ${G.line}`, borderRadius: 8, boxSizing: 'border-box' }} />
</React.Fragment>
))}
</div>
{/* 顶部工具条左半(logo + 工具 chips) */}
<div style={{ position: 'absolute', left: 0, top: 0, width: 760, height: 60, background: G.panel, borderBottom: `2px solid ${G.line}`, display: 'flex', alignItems: 'center', gap: 16, padding: '0 24px', boxSizing: 'border-box', ...topLeft }}>
<div style={{ width: 32, height: 32, borderRadius: 8, background: G.mid }} />
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} style={{ width: 30, height: 30, borderRadius: 8, background: G.line }} />
))}
</div>
{/* 顶部工具条中段(标题) */}
<div style={{ position: 'absolute', left: 760, top: 0, right: 400, height: 60, background: G.panel, borderBottom: `2px solid ${G.line}`, display: 'flex', alignItems: 'center', justifyContent: 'center', boxSizing: 'border-box', ...toolbarShell }}>
<div style={{ height: 14, width: 220, background: G.bar, borderRadius: 7 }} />
</div>
{/* 顶部工具条右段底板(Invite 假按钮;Publish 单独渲染在最上层) */}
<div style={{ position: 'absolute', right: 0, top: 0, width: 400, height: 60, background: G.panel, borderBottom: `2px solid ${G.line}`, display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: 14, padding: '0 24px', boxSizing: 'border-box', ...topRight }}>
<div style={{ height: 36, width: 100, borderRadius: 18, border: `2px solid ${G.bar}`, boxSizing: 'border-box' }} />
</div>
{/* 画布区:一张浏览器式大卡 + 两张小卡 */}
<div style={{ position: 'absolute', left: 320, top: 130, width: 1180, height: 850, ...canvasFrame }}>
<div style={{ position: 'absolute', inset: 0, background: G.card, border: `2px solid ${G.border}`, borderRadius: 18, boxShadow: '0 10px 40px rgba(0,0,0,0.08)' }}>
<div style={{ height: 46, borderBottom: `2px solid ${G.line}`, display: 'flex', alignItems: 'center', gap: 8, padding: '0 18px' }}>
{[0, 1, 2].map((i) => (
<div key={i} style={{ width: 14, height: 14, borderRadius: 7, background: G.line }} />
))}
<div style={{ marginLeft: 16, height: 20, width: 380, background: G.bg, borderRadius: 10 }} />
</div>
</div>
{[0, 1, 2, 3].map((i) => {
const s = cardStrip(i);
return (
<div key={i} style={{ position: 'absolute', left: 70 + (i % 2) * 560, top: 120 + Math.floor(i / 2) * 340, ...s }}>
<Card w={480} h={280} seed={i + 2} />
</div>
);
})}
</div>
{/* Publish 按钮(高亮层,最后退场) */}
<div
style={{
position: 'absolute',
left: btnX,
top: btnY,
width: 176,
height: 44,
opacity: btnOp,
transform: `scale(${pressScale})`,
zIndex: 30,
}}
>
<div
style={{
width: 176 * btnScale,
height: 44 * btnScale,
marginLeft: -((176 * btnScale - 176) / 2),
marginTop: -((44 * btnScale - 44) / 2),
borderRadius: 22 * btnScale,
background: '#f2f2f0',
boxShadow: `0 0 ${30 + 40 * btnCenter}px rgba(255,255,255,${0.25 + 0.3 * btnCenter * bgDark})`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'Helvetica, Arial, sans-serif',
fontWeight: 700,
fontSize: 20 * btnScale,
color: '#161615',
}}
>
Publish
</div>
</div>
{/* 字标接棒 */}
{frame >= LOGO_IN && (
<AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>
<div
style={{
opacity: logoP,
transform: `scale(${0.86 + 0.14 * logoP})`,
fontFamily: 'Helvetica, Arial, sans-serif',
fontWeight: 800,
fontSize: 110,
letterSpacing: 6,
color: '#f2f2f0',
}}
>
WORDMARK
</div>
</AbsoluteFill>
)}
{/* 光标 */}
<svg width={40} height={44} viewBox="0 0 20 22" style={{ position: 'absolute', left: curX, top: curY, opacity: curOp, zIndex: 40 }}>
<path d="M2 1 L2 17 L6.5 13.2 L9.4 20 L12.4 18.7 L9.5 12 L15 11.6 Z" fill={G.ink} stroke="#fff" strokeWidth="1.4" />
</svg>
</AbsoluteFill>
);
};
// hook 规则外的静态版本(供 map 内调用)
const useStripStatic = (frame: number, start: number, dx: number, dy: number) => {
const p = interpolate(frame, [start, start + STRIP_DUR], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad),
});
return {
opacity: 1 - p,
transform: `translate(${dx * p}px, ${dy * p}px)`,
};
};
- 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/outro/ui-strip-away-outro/UiStripAwayOutro.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

UI biến hình thành logo thương hiệu
Hai kiểu UI biến thành logo — icon-flip-bloom icon lật phẳng trục Y nở thành hình hoa mark + wordmark vào từng chữ, và input-morph-assemble ô nhập liệu co lại thành viên nang, ba hạt hình học tụ thành một cánh logo.

Outro ảnh nhóm ra mắt
Mọi yếu tố trong phim bay vào từ mọi hướng vây quanh wordmark chụp ảnh nhóm, dàn dựng như cảnh chốt của một sự kiện ra mắt.

Ba hàng đèn neon chạy chữ
Ba hàng đèn neon chạy chữ ngược chiều (BETTER/FASTER/STRONGER) cuộn vô tận, lần lượt sáng theo pha rồi mờ dần.

Logo thu nhỏ ráp thành lockup
Vòng tròn neon cắt hở khổng lồ nhanh chóng thu về thành một chữ O trắng đặc nhỏ ở giữa kèm vượt ngưỡng rồi phanh lại, icon dịch trái nhường chỗ, các chữ cái lần lượt trượt vào hoàn thành lockup, khẩu hiệu kết thúc.