Motion · Interaction
Hashtag to pill materialize
"#word" types itself in, hard-cuts on 1 frame into a large pill label, holds, then shrinks and moves left into position, another hard cut reveals the finished page — two hard cuts around one slide.
Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player
When to use
Demo segments for tag/category/keyword features (tagging in a notes app, topic aggregation); a three-act narrative of "type it → it becomes a UI object → it settles into the finished product".
Intention
The conventional way to materialize text is a fade/morph/expand. Frame-by-frame breakdown of the original footage proves Bear does the opposite: materializing happens on a 1-frame hard cut — one frame it's still text+cursor, the next it's a complete pill, no expansion, no cross-fade, no rebound. A hard cut delivers the certainty of "snap, done" — any gradual transition would soften "object" into "effect". The whole segment's skeleton is two hard cuts bracketing one slide: hard-cut materialize → smooth shrink-move into place → hard-cut reveal of the finished page. The only continuous motion (the shrink-move) is framed by two hard cuts, which is exactly what makes it read as fast and stable. This rhythm skeleton is the crux — change the nature of any one of the three beats and the whole card collapses.
Duration & energy
- Duration: Typing ~40f + hard-cut pill hold ~18f + shrink-move 14f + hard-cut reveal then rest; whole segment about 3.5s (18–21.5s in the source footage)
- Energy: Medium (crisp and clean, driven by the hard cuts, not by bounce)
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
文字变实体的常规做法是渐变/morph/展开,原片帧级拆解证明 Bear 反着 来:实体化是 1 帧硬切——上一帧还是文字+光标,下一帧就是完整 胶囊,无展开无 cross-fade 无回弹。硬切给的是"啪、成了"的确定感, 任何渐变都会把"实体"软化成"特效"。全段骨架是两次硬切夹一次 滑动:硬切实体化 → 平滑缩移归位 → 硬切揭示成品页。唯一的连续 运动(缩移)被两记硬切框住,才显得又快又稳。这个节奏骨架是命门, 三段挪动任何一段的性质(把硬切改渐变、把滑动改硬切)整卡就塌。
动效核心
- 打字段:几何无衬线(Futura 气质)居中打 "#word",**红实心光标
恒亮不闪**(原片实测,别加 blink);人手节奏 4–6f/字带确定性抖动
- 实体化 = 1 帧硬切:文字+光标整层消失,同帧出现无描边浅灰
胶囊 + 图标 + 词(字号不变、原位等大,# 被图标替换);仅允许 3f 1.03→1 微落定防死板
- 胶囊 hold ~0.6s → 一段 bezier easeInOut 缩小左移(~0.55x、
~14f)落到页面标签位,位置/缩放同曲线同起止,中途不分段
- 落位后再 1 帧硬切揭示成品页:底色/标题/正文/胶囊配色同帧
全变,之后真静止收尾
- 实现要点:胶囊按大字号绘制、整体 transform 缩放,且
transformOrigin 必须 0 0 + translate 先行,否则落位中心漂移
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 打字节奏 | 4–6f/字(原片 ~6 字/秒),mulberry32 抖动 | 均匀间隔读作机打;光标必须恒亮(原片实测不闪烁) | | 实体化 | 1 帧硬切(原片帧级实测无任何过渡)+ 3f 1.03→1 微落定 | 加淡入/展开立刻降级成普通 morph;微落定 >4f 读作弹跳 | | 胶囊几何 | 740×236@1920(原片实测 493×157@720p ×1.5),无描边 | 胶囊要"宽大过头"才有实体感;加描边读作按钮 | | hold | ~18f(0.6s,原片实测) | <12f 观众没看清胶囊长啥样就飞了 | | 缩移 | ~0.55x(原片实测 273/493)、14f、bezier(0.5,0,0.25,1) | 缩放与位移必须同曲线;分开调会读作两个动画 | | 揭示 | 落位 +3f 再 1 帧硬切全页 | 揭示若做转场,第二记硬切没了,骨架塌一半 |
已知坑
- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,
首次实战须以真实素材回验
- 与 typewriter-moves/ai-stream-response(打字呈现)、
morph-from-primitive(图元渐变成形)撞领域:本卡命门是硬切 实体化非渐变变形——只要你想加过渡,就该去用那几张卡而不是改这张
- 早期版本曾杜撰"胶囊飞入下方滑入笔记卡"段,原片对照证伪已砍;
别复活飞行段
- transformOrigin 默认 50% 50% 会让缩放落位中心漂移 (1−s)×半宽,
必须 origin 0 0 + translate 到目标中心再 scale(demo 内注释)
- 与原片残余差距:音符图标手绘 SVG 曲线、Futura 回退字重、揭示帧
正文间距 ~10px 级别偏差;节奏与硬切时点已密帧对齐
参考实现
demos/interaction/hashtag-to-pill-materialize/ (HashtagToPillMaterialize.tsx) 原片出处:bear-app.mp4 18–21.5s
Reference implementation (Remotion)
- demos/interaction/hashtag-to-pill-materialize/HashtagToPillMaterialize.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/hashtag-to-pill-materialize/HashtagToPillMaterialize.tsx
The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.
Everything on this page is also machine-readable. Point your agent at the JSON, or add the gimgs MCP server and ask it to search motion templates.
- Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
- Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
- Register the component as a <Composition> in a Remotion project and render with `npx remotion render`, or re-implement the recipe in your own engine.
curl -s https://gimgs.net/motion/hashtag-to-pill-materialize.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/interaction/hashtag-to-pill-materialize/HashtagToPillMaterialize.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("hashtag-to-pill-materialize")Prompt
Use the video-shotcraft shot recipe "Hashtag to pill materialize" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/hashtag-to-pill-materialize.json Raw TSX: https://gimgs.net/motion/hashtag-to-pill-materialize/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/interaction/hashtag-to-pill-materialize/HashtagToPillMaterialize.tsx; imports demos/_fixtures and assets/lib) Spec: 1920x1080, 30 fps, 4.46s 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/interaction/hashtag-to-pill-materialize/HashtagToPillMaterialize.tsx =====
// hashtag-to-pill-materialize(bear-app 18–21.5s 重做版,按原片密帧逐帧对照)
// 原片实测节奏(25fps 逐帧):
// 1) 白底居中打字 "#music":几何无衬线(Futura 气质)、中灰墨色、红色实心光标恒亮不闪、人手节奏
// 2) 实体化 = 1 帧硬切:文字+光标 → 宽大浅灰无描边胶囊 + 灰色双八分音符图标 + "music"(字号不变,#被图标替换)
// 3) 停约 0.6s → 整体平滑缩小(→~0.55x)并左移落到页面标签位(约 0.55s,easeInOut)
// 4) 再 1 帧硬切揭示成品笔记页:奶油底、墨绿大标题 "My favorite bands"、胶囊换成鼠尾草绿、正文三行——
// 原片没有"胶囊飞入下方滑入卡片"的段落(批次 8 的飞行段为杜撰,已砍)
import React from 'react';
import { AbsoluteFill, interpolate, useCurrentFrame, Easing } from 'remotion';
const FONT = "Futura, 'Century Gothic', 'Avenir Next', 'Trebuchet MS', sans-serif";
const C = {
bgWhite: '#fcfcfb',
bgCream: '#f4f1e5',
ink: '#454543',
cursor: '#e0453f',
pillGray: '#e9e9e7',
pillTextGray: '#4b4b49',
iconGray: '#7e7e7c',
pillSage: '#d5e0cf',
iconSage: '#5c7a63',
titleGreen: '#2d5c47',
pillSageText: '#3f5e4c',
body: '#4c4b43',
};
// ---- mulberry32(仅用于打字节奏的人手抖动,确定性)----
function mulberry32(seed: number) {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const TEXT = '#music';
// 打字起点 & 每字间隔(帧),4–8 帧带抖动,模拟原片真人节奏
const TYPE_START = 8;
const rand = mulberry32(20260717);
const TYPE_AT: number[] = (() => {
const at: number[] = [];
let f = TYPE_START;
for (let i = 0; i < TEXT.length; i++) {
at.push(f);
f += 4 + Math.floor(rand() * 3); // 4–6 帧(原片 ~6字/秒)
}
return at;
})();
// ---- 时间轴(30fps,共 132 帧,节奏对齐原片 18–21.5s)----
const MORPH = 48; // 1 帧硬切实体化(打完 hold ~0.5s,原片 0.45s)
const MOVE_START = 66; // 胶囊开始缩小左移(morph 后 0.6s,原片同)
const MOVE_END = 80; // 落位(0.47s,原片 ~0.45s)
const REVEAL = 83; // 1 帧硬切揭示成品页,之后静置收尾
// ---- 几何(1920x1080,等比换算自原片 1280x720 实测像素)----
const FS = 132; // 打字/胶囊文字字号(原片 glyph 等高换算)
const HERO = { x: 960, y: 540 }; // 大胶囊中心
const PILL_W = 740, PILL_H = 236; // 原片实测 493x157 @720p ×1.5
const END_SCALE = 0.554; // 落位缩放(原片 273/493)
const SLOT = { x: 361, y: 473 }; // 标签位中心(原片灰胶囊落点 (244.5,317.5)×1.5 与揭示位折中)
// 灰色双八分音符图标(原片是 beamed 双音符,非 ♪)
const NoteIcon: React.FC<{ size: number; color: string }> = ({ size, color }) => (
<svg width={size} height={size} viewBox="0 0 48 48" style={{ display: 'block' }}>
<ellipse cx="11" cy="39" rx="7.2" ry="5.6" fill={color} transform="rotate(-18 11 39)" />
<ellipse cx="35" cy="35" rx="7.2" ry="5.6" fill={color} transform="rotate(-18 35 35)" />
<rect x="15.4" y="12.5" width="3" height="27" fill={color} />
<rect x="39.4" y="8.5" width="3" height="27" fill={color} />
<polygon points="15.4,12.5 42.4,8.5 42.4,16.5 15.4,20.5" fill={color} />
</svg>
);
// 胶囊(大字号绘制,整体 transform 缩放,保证实体化前后文字原位等大)
const Pill: React.FC<{ bg: string; iconColor: string; textColor: string }> = ({ bg, iconColor, textColor }) => (
<div style={{
width: PILL_W, height: PILL_H, borderRadius: PILL_H / 2, background: bg,
display: 'flex', alignItems: 'center', paddingLeft: 96, boxSizing: 'border-box', gap: 66,
}}>
<NoteIcon size={104} color={iconColor} />
<span style={{ fontSize: FS, fontWeight: 500, color: textColor, letterSpacing: 2 }}>music</span>
</div>
);
export const HashtagToPillMaterialize: React.FC = () => {
const frame = useCurrentFrame();
// ---- 打字 ----
const typedCount = TYPE_AT.filter((t) => frame >= t).length;
const typed = TEXT.slice(0, typedCount);
// ---- 缩小左移 ----
const moveT = interpolate(frame, [MOVE_START, MOVE_END], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.bezier(0.5, 0, 0.25, 1),
});
const px = interpolate(moveT, [0, 1], [HERO.x, SLOT.x]);
const py = interpolate(moveT, [0, 1], [HERO.y, SLOT.y]);
const ps = interpolate(moveT, [0, 1], [1, END_SCALE]);
// 实体化瞬间极轻微落定(原片近乎硬切,仅 3 帧 1.03→1,避免死板)
const settle = interpolate(frame, [MORPH, MORPH + 3], [1.03, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.quad),
});
const revealed = frame >= REVEAL;
return (
<AbsoluteFill style={{ background: revealed ? C.bgCream : C.bgWhite, fontFamily: FONT }}>
{/* 成品页(硬切揭示,之后全静) */}
{revealed && (
<>
<div style={{
position: 'absolute', left: 160, top: 168,
fontSize: 122, fontWeight: 700, color: C.titleGreen, letterSpacing: 0.5,
}}>
My favorite bands
</div>
<div style={{
position: 'absolute', left: 152, top: 618,
fontSize: 70, fontWeight: 500, color: C.body, lineHeight: 1.33, letterSpacing: 0.3,
}}>
I want to share a few of my favorite bands<br />
and the song that I always listen when driving<br />
to home. Welcome. Bring headphones.
</div>
</>
)}
{/* 打字层:文字 + 恒亮红光标(原片光标不闪烁),实体化帧整体消失 */}
{frame < MORPH && (
<AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ fontSize: FS, fontWeight: 500, color: C.ink, letterSpacing: 2, whiteSpace: 'pre' }}>
{typed}
</span>
<span style={{
display: 'inline-block', width: 7, height: 150,
background: C.cursor, marginLeft: 8, borderRadius: 2,
}} />
</div>
</AbsoluteFill>
)}
{/* 胶囊层:实体化 1 帧硬切出现 → hold → 缩小左移落位 → 揭示帧换鼠尾草绿 */}
{frame >= MORPH && (
<div style={{
position: 'absolute', left: 0, top: 0,
// origin 必须是 0 0:translate 先把原点送到目标中心,scale 绕该点缩放,
// 否则默认 50% 50% 会让落位时中心漂移 (1-s)*半宽
transformOrigin: '0 0',
transform: `translate(${px}px, ${py}px) scale(${ps * settle})`,
}}>
<div style={{ transform: 'translate(-50%, -50%)' }}>
{revealed
? <Pill bg={C.pillSage} iconColor={C.iconSage} textColor={C.pillSageText} />
: <Pill bg={C.pillGray} iconColor={C.iconGray} textColor={C.pillTextGray} />}
</div>
</div>
)}
</AbsoluteFill>
);
};
- 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/interaction/hashtag-to-pill-materialize/HashtagToPillMaterialize.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 |
|---|---|---|---|
| — | |||
More in this category

Live voice waveform
A live waveform in a recording pill — 64 thin bars rise and fall with "speech," spiking tall mid-speech and shrinking to a dotted line on pauses, scrolling right to left; a full speak-pause-speak-submit performance.

Type, filter, drill in
Typing a search on a real UI, the grid converges to a single card on its own, then a click drills through into the detail page.

Theme switch: sweep and ripple
Two theme-switch moves — theme-sweep, a diagonal wipe (the skin changes wherever the boundary passes), and palette-ripple, a combo move (a ⌘K panel collapses to a point, a ripple spreads from it to re-skin).
Segmented thumb hero
A segmented control's thumb shift becomes the star of a close-up — a cursor clicks it and a new icon springs in.