Motion · Chữ & title card
Ba từ tiếp sức bằng hình học
Ba từ lợi ích mỗi từ mang riêng một bộ hình học tiếp sức — vòng tròn nét đứt tự xoay thu nhỏ → ba vòng tròn nét liền mọc lên lần lượt → ánh kim sheen quét qua rồi thu về trắng thuần; mỗi từ mới viền nét rồi tô đầy.
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
Trình bày lợi ích kiểu ba điểm (nhanh hơn/tốt hơn/mạnh hơn); đoạn giá trị thương hiệu; đoạn tiến triển giữa phim cần "mỗi từ một thế giới riêng"
Ý đồ
Trình bày lợi ích kiểu ba điểm dễ nhất là làm thành ba thẻ chữ cắt cứng. Cách tiếp sức thay vào đó cho mỗi từ một bộ hình học riêng biệt: vòng tròn nét đứt là "đang quay" (nhanh), ba vòng tròn khớp nhau là "ăn khớp lẫn nhau" (tốt), ánh kim quét là "chất liệu cứng" (mạnh) — bản thân hình học đang lập luận thay cho tính từ. Từ thứ ba được đãi ngộ nâng cấp (ánh kim quét + thu về trắng) vì nó là kết luận. 20 hạt bụi nền trôi lên khâu ba nhịp thành một không gian liên tục, nếu không thì vẫn chỉ là ba thẻ.
Thời lượng & năng lượng
- Thời lượng: khoảng 6.0s (180f@30fps: ba đoạn mỗi đoạn cửa sổ thời gian 0.36, chồng lấn giữa các đoạn 0.04)
- Năng lượng: Trung bình cao (ba nhịp tiến triển + chuyển động hình học suốt cảnh + hạt nền, không có khung đứng yê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)
意图
三点式利益陈述最容易做成三张字卡硬切。接力的做法是给每个词配一套 只属于它的几何:虚线圆是"在转"(快)、三圆相扣是"互相咬合"(好)、 金属扫光是"材质硬"(强)——几何本身就在替形容词做论证。第三个词 留了升格待遇(sheen 扫光 + 收白),因为它是结论。背景 20 颗微尘上浮 把三拍缝成一个连续空间,不然还是三张卡。
动效核心
- 三段时段带重叠:
[0, 0.36] / [0.32, 0.68] / [0.64, 1.0]——每段入场窗
0.07(≈13f,outCubic),出场窗 0.05(≈9f,inQuad,落在段末), 重叠 0.04 是交接而非硬切;末段无出场(收在片尾)
- 每词双层文字(56px/800):
outline层-webkit-text-stroke:1px #6a7186
+ 透明填充,fill 层纯白——描边先到位,填充后揭示
- 前两词的填充是横向擦入:
fill.clipPath = inset(-20% (1-fillp)·100% -20% 0),
fillp = seg(t, t0+0.06, t0+0.18, E.inOutCubic),同时 outline 透明度 降到 0.25(1 - fillp·0.75,留一点描边做重影)
- 第一词的虚线圆(r=100、
stroke-dasharray:5 7):scale(lerp(grow,0.4,1))
从 0.4 长到 1,同时 rotate(-90 + t·30)——用全片 t 做自转,所以它在 自己那一段里持续转,不是转完就停;第二词的三实线圆(r=62,x 轴 -110/0/110)走 SVG trim:pathLength=1 + dasharray=1 + dashoffset 从 1 收到 0,三圆延迟 d = 0/0.06/0.12 依次生长,出场时 trim 反向减回去
- Stronger 的 sheen:
linear-gradient(100deg, …#fff 50%…)+background-size:280%
+ background-clip:text,backgroundPosition 从 100% 扫到 0% (窗 t0+0.08→+0.26,inOutCubic),随后 white = seg(t0+0.27, t0+0.34) 把 sheen 换成纯白 fill 并打 18px 白色辉光
- 出场统一
scale(lerp(tout, 1, 0.86))缩小淡出(缩小而非放大,读作
"退场"不是"冲出画面");背景 20 颗微尘 y = (1 - ((t·sp + ph) % 1))·110 - 5(%),速度 sp = 0.5 + rand·0.8 各异
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 三段时段 | 各 0.36(≈65f),重叠 0.04(≈7f) | 每词约 2s 是"读词 + 看几何"的下限;重叠 <0.02 读作硬切,>0.08 两词同屏打架 | | 入/出场窗 | 入 0.07 outCubic / 出 0.05 inQuad | 出场比入场短是对的(退场要决绝);出场用 out 曲线会读作"舍不得走" | | outline→fill | 窗 0.12(≈22f)inOutCubic,outline 留 0.25 | 留一点描边做重影是质感来源,降到 0 字变扁;窗 >0.25 擦入慢得像加载条 | | 虚线圆 | r=100、dash 5 7、scale 0.4→1、自转 t·30° | 自转绑全片 t 所以永不停——这是"快"的证据;scale 起点 >0.7 就没有"长出来" | | trim 生长 | pathLength=1 + dashoffset 1→0,相位差 0.06 | 相位差是"依次相扣"的关键,同时生长读作三个独立圆;窗 0.12 与 fill 同步收敛 | | sheen 扫光 | 100deg 渐变、background-size:280%、位置 100%→0% | 280% 保证白带只占字宽一小段(=金属高光);100% 时整字一起变亮,不是扫光 | | 收白 | seg(t0+0.27, t0+0.34) ≈13f + 18px 辉光 | 这一下是全片唯一的"结论"待遇;删了三个词就是平级,第三点没有落点 | | 字号配色 | 56px/800、描边 #6a7186、几何 #565e78、底 #07080c,微尘 20 颗 | 几何色比描边略暗一档是层级,几何比字亮就抢主角;微尘是把三拍缝成一个空间的胶水,>40 颗读作雪 |
已知坑
- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,
首次实战须以真实素材回验
- 三个词必须是同一句法的三点(demo 都是比较级单词,6–8 字母):
时段是等分的,词长差大时几何(圆半径、间距)都是写死的坐标, 长词会顶出虚线圆或压到相扣圆上——换词表要同步调 CIRC_R 与 x 偏移
-webkit-text-stroke是前缀属性:Chromium/WebKit 才有,Firefox 下描边层
直接是透明(词消失)。Remotion 渲染走 Chromium 无碍,但预览环境要注意
- SVG trim 用的
pathLength=1归一化在部分老版本浏览器上对<circle>
支持不全,退化表现是圆一次性全画出(丢生长感)——移植时先在目标 渲染器上验一帧
- 出场 trim 是"入场 trim 减出场 seg"直接相减:两段窗若靠得太近
(段长 <0.2)会出现还没长完就开始收,圆只画出一小段
- sheen 层与 fill 层是叠着的两个 DOM:切换靠透明度交叉,若把
white窗
与 sh 窗调成重叠不足,会有 1–2 帧两层都半透(字发灰)
- 第三个词的升格待遇只能给一次:全片三个词都上 sheen 就没有结论感
(P4 手法去重精神);声音上三次入场是三个节奏点,第三个词的收白值得 单独一记重音,配法纪律见 sound-design.md
参考实现
demos/typography/word-relay-geometry/ (WordRelayGeometry.tsx)
Mã nguồn tham chiếu (Remotion)
- demos/typography/word-relay-geometry/WordRelayGeometry.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/word-relay-geometry/WordRelayGeometry.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/word-relay-geometry.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/typography/word-relay-geometry/WordRelayGeometry.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("word-relay-geometry")Prompt
Use the video-shotcraft shot recipe "Ba từ tiếp sức bằng hình học" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/word-relay-geometry.json Raw TSX: https://gimgs.net/motion/word-relay-geometry/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/typography/word-relay-geometry/WordRelayGeometry.tsx; imports demos/_fixtures and assets/lib) Spec: 1920x1080, 30 fps, 6.5s 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/typography/word-relay-geometry/WordRelayGeometry.tsx =====
// word-relay-geometry — Word Relay Geometry 利益词几何接力(motion-lab 定稿转原生 Remotion)
// 三个利益词接力:Faster(虚线大圆)→ Better(三实线圆相扣)→ Stronger(金属 sheen
// 从左扫到右变纯白)。旧词与几何淡出缩小,新词描边→填充进场,圆路径带 trim 生长感;
// 背景漂浮微尘粒子。设计坐标 480×270(DesignStage 等比放大),参数以此坐标系标定。
import React from 'react';
import { DesignStage, E, lerp, rand, seg, useT } from '../../_fixtures/Motion';
export const WORD_RELAY_GEOMETRY_DURATION = 180; // 6000ms @30fps
const CIRC_R = 78;
// 微尘粒子(20 个,种子与原 effect.js 完全一致)
const PARTS = Array.from({ length: 20 }, (_, i) => ({
size: 1 + rand(i * 3) * 1.4,
x: rand(i) * 100,
ph: rand(i + 40),
sp: 0.5 + rand(i + 80) * 0.8,
}));
// 每个词一段 slot:几何配置 + 时段(in 0.07 + hold + out 0.05)
type Geom = { x: number; r: number; dash?: boolean; d?: number };
const SLOTS: { label: string; geom: Geom[]; t0: number; t1: number }[] = [
{ label: 'Faster', geom: [{ x: 0, r: CIRC_R + 22, dash: true }], t0: 0.0, t1: 0.36 },
{
label: 'Better',
geom: [
{ x: -110, r: 62, d: 0 },
{ x: 0, r: 62, d: 0.06 },
{ x: 110, r: 62, d: 0.12 },
],
t0: 0.32,
t1: 0.68,
},
{ label: 'Stronger', geom: [], t0: 0.64, t1: 1.0 },
];
export const WordRelayGeometry: React.FC = () => {
const t = useT();
return (
<DesignStage bg="#07080c">
{/* 微尘粒子:缓慢上浮 loop + 呼吸闪烁 */}
{PARTS.map((p, i) => {
const y = (1 - ((t * p.sp + p.ph) % 1)) * 110 - 5;
return (
<div
key={i}
style={{
position: 'absolute',
width: p.size,
height: p.size,
borderRadius: '50%',
background: '#fff',
left: `${p.x}%`,
top: `${y}%`,
opacity: 0.12 + 0.18 * Math.sin((t * 3 + p.ph) * Math.PI * 2) ** 2,
}}
/>
);
})}
{SLOTS.map(({ label, geom, t0, t1 }, i) => {
const isLast = i === SLOTS.length - 1;
const tin = seg(t, t0, t0 + 0.07, E.outCubic);
const tout = isLast ? 0 : seg(t, t1 - 0.05, t1, E.inQuad);
const alive = tin > 0 && tout < 1;
// Faster/Better:outline→fill 左起 clip 揭示
const fillp = seg(t, t0 + 0.06, t0 + 0.18, E.inOutCubic);
// Stronger:描边→sheen 扫光→收纯白
const sh = seg(t, t0 + 0.05, t0 + 0.2, E.outQuad); // 出现(描边→sheen)
const sweep = seg(t, t0 + 0.08, t0 + 0.26, E.inOutCubic); // 扫光位置
const white = seg(t, t0 + 0.27, t0 + 0.34, E.outQuad); // 收为纯白
return (
<div key={label} style={{ position: 'absolute', inset: 0, opacity: alive ? tin * (1 - tout) : 0 }}>
<svg
viewBox="-240 -135 480 270"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}
>
{geom.map((g, k) => {
if (g.dash) {
// 虚线大圆:整体从 0.4 生长到 1 并持续慢转
const grow = seg(t, t0 + 0.01, t0 + 0.14, E.outCubic);
return (
<circle
key={k}
cx={g.x}
cy={0}
r={g.r}
fill="none"
stroke="#565e78"
strokeWidth={1.1}
strokeDasharray="5 7"
opacity={grow * (1 - tout)}
transform={`rotate(${-90 + t * 30}) scale(${lerp(grow, 0.4, 1)})`}
/>
);
}
// 实线圆:pathLength 归一化 trim 生长 / 出场反向消隐
const trim =
seg(t, t0 + 0.02 + (g.d || 0), t0 + 0.14 + (g.d || 0), E.outCubic) -
seg(t, t1 - 0.06, t1, E.inQuad) * (isLast ? 0 : 1);
return (
<circle
key={k}
cx={g.x}
cy={0}
r={g.r}
fill="none"
stroke="#565e78"
strokeWidth={1.1}
pathLength={1}
strokeDasharray="1"
strokeDashoffset={1 - Math.max(0, trim)}
transform="rotate(-90)"
/>
);
})}
</svg>
{/* 词组:outline 打底,fill/sheen 绝对层叠 */}
<div
style={{
position: 'absolute',
left: '50%',
top: '50%',
transform: `translate(-50%,-52%) scale(${lerp(tout, 1, 0.86)})`,
fontFamily: '-apple-system,"Helvetica Neue",sans-serif',
fontSize: 56,
fontWeight: 800,
lineHeight: 1,
}}
>
<div
style={{
color: 'transparent',
WebkitTextStroke: '1px #6a7186',
opacity: isLast ? 1 - sh : 1 - fillp * 0.75,
}}
>
{label}
</div>
{isLast ? (
<>
<div
style={{
position: 'absolute',
inset: 0,
color: '#fff',
opacity: white,
textShadow: `0 0 ${white * 18}px rgba(255,255,255,.3)`,
}}
>
{label}
</div>
{/* 金属 sheen:渐变 background-clip 文字,背景位从右扫到左 */}
<div
style={{
position: 'absolute',
inset: 0,
color: 'transparent',
backgroundImage:
'linear-gradient(100deg,#585f72 0%,#8d95aa 38%,#ffffff 50%,#8d95aa 62%,#585f72 100%)',
backgroundSize: '280% 100%',
WebkitBackgroundClip: 'text',
backgroundClip: 'text',
opacity: sh * (1 - white),
backgroundPosition: `${lerp(sweep, 100, 0)}% 0`,
}}
>
{label}
</div>
</>
) : (
<div
style={{
position: 'absolute',
inset: 0,
color: '#fff',
clipPath: `inset(-20% ${(1 - fillp) * 100}% -20% 0)`,
}}
>
{label}
</div>
)}
</div>
</div>
);
})}
</DesignStage>
);
};
- 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/typography/word-relay-geometry/WordRelayGeometry.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

Tiếp sức từ với cuộn phim trang
Cột trái các thẻ trang đen trắng cuộn theo bước, bên phải từ lớn chữ serif tiếp sức tại chỗ (danh từ cố định + động từ luân phiên) — chỉ khi đổi từ mới cuộn một bước, khớp chính xác điểm giữa thẻ trang hiện tại.

Cuộn từ dọc theo nhịp
Từ cuối câu đổi trên cuộn dọc, 3 lần đổi từ mỗi lần 0.55s, nhanh trước chậm sau kèm hơi overshoot; các dòng lân cận nhòe dọc và xám dần theo khoảng cách, từ ở giữa vừa đậu liền nhuộm từ xám sang màu nhấn.

Hai cách tiết lộ code song song
Cùng một khối code tô cú pháp được tiết lộ hai cách song song — trái: stagger cấp dòng mờ vào nổi lên 8px, phải: gõ từng ký tự nhưng giữ nguyên màu token gốc, con trỏ vuông #3a4468 đệm dưới ký tự hiện tại.

Chữ tập hợp: bốn kiểu
Bốn kiểu tập hợp chữ — split-text-stagger chữ dâng lên vào vị trí, drift-assembly chữ trôi tụ lại, tracking-expand giãn khoảng cách kiểu hơi thở, text-on-path chảy vào theo đường cong.