Motion · Data
Particle celebration hits
Confetti cannons fire from both sides, or sparks fizz off the counter each time it ticks past a thousand.
Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player
Variants
Each of the two cannons fires 50 rectangular confetti pieces: initial velocity 90–150px/f (at 0.9 decay, needing ~900–1500px total travel to cross the centerline), 55° spread, tumbling 8–15° per frame; after ~90f they've all fallen off-screen and are conditionally unmounted.
The one-time climax of a final-value reveal or announcement
Tick frames are derived from the same interpolate as the counter; each tick fires 6–10 2px sparks (initial upward velocity 4–6px/f, falling and extinguishing over 12–18f of gravity), with the final-value tick doubling to 20 sparks plus a 1.1x number bounce.
Rhythm accents during a counting-up climb
When to use
Milestone number/KPI reveal/achievement sections; A for a one-time big celebration, B for continuous small ticks
Intention
Give the "number reveal" a visual beat you can almost hear: A is the full stop of the reveal moment — two cannons at bottom-left and bottom-right fire on the same frame, hundreds of tumbling confetti pieces arc and cross over the full screen before falling off, pure celebration semantics; B is the comma along the way — every time the counter ticks past a thousand, a small burst of sparks fizzes off the top of the number and falls out dark, with the final value tick doubling into a bigger burst plus a 1.1x number bounce. Both use closed-form ballistics (initial velocity + gravity + decay, computed frame by frame) — no physics engine, fully frame-deterministic.
Duration & energy
- Duration: A 3–4s / B 4–5s
- Energy: Climax-accent type (must settle back to clean stillness after the burst)
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
给"数字揭晓"配可听感的视觉打点:A 是揭晓瞬间的句号——画面左下右下 两门炮同帧开火,上百颗翻转彩屑抛物线交叉飞过全屏再落出画外,纯庆祝 语义;B 是过程中的逗号——计数器每跳过一个整千,数字顶部"叮"地溅一撮 小火星坠落熄灭,终值那跳翻倍爆一大撮+数字弹 1.1x。两式都是闭式弹道 (初速+重力+衰减逐帧公式),免物理引擎、帧确定。
两式选型
| 式 | 做法 | 适用 | |----|------|------| | A confetti-crossfire | 双炮各 50 颗矩形彩屑:初速 90–150px/f(decay 0.9 下总程 ~900–1500px 才能交叉过中线)、spread 55°、每帧翻转 8–15°;~90f 全部落出画外后条件卸载 | 终值揭晓/发布宣言的一次性高潮 | | B counter-tick-sparks | tick 帧由计数器同一 interpolate 派生;每 tick 6–10 颗 2px 火星(初速向上 4–6px/f、重力 12–18f 坠灭),终值跳翻倍 20 颗+数字弹 1.1x | 计数爬升过程的节奏打点 |
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A 初速 | 90–150px/f | 词汇原案 18px/f 在 decay 下只飞 180px 不可感——弹道类幅度按总位移倒推 | | A 配色 | 灰阶为主 + 1/3 强调色 | 全彩读作 B 端不合品牌;全灰读不出庆祝 | | B 火星量 | 常规 6–10、终值 20 | 默认档最含蓄,嫌弱可 3 倍量做极端版 | | 弹道 | 闭式公式(seed 伪随机初速角度) | 每帧独立可算,禁 Math.random | | 收尾 | 粒子寿命耗尽条件卸载 | 结尾必须真静止,残留粒子=脏帧 |
已知坑
- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,
首次实战须以真实素材回验
- 同批组合款 ring-confetti-blowout(合环×礼炮)、lights-out-tick-burst
(灭灯×溅火)双双淘汰——爆发×爆发同质叠加读作过载; 本卡两式与其他爆发型手法(slam/impact 系)同帧叠用需谨慎
- A 只配"值得开炮"的时刻,一片一次;连开读作廉价
- 声音:A 双炮"砰砰"错开 1–2f 更有层次;B 每 tick 一声细"叮"
参考实现
demos/data/particle-celebrate-hits/ (ConfettiCrossfire.tsx / CounterTickSparks.tsx)
Reference implementation (Remotion)
- demos/data/particle-celebrate-hits/ConfettiCrossfire.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/particle-celebrate-hits/ConfettiCrossfire.tsx
- demos/data/particle-celebrate-hits/CounterTickSparks.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/particle-celebrate-hits/CounterTickSparks.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/particle-celebrate-hits.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/data/particle-celebrate-hits/ConfettiCrossfire.tsx, demos/data/particle-celebrate-hits/CounterTickSparks.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("particle-celebrate-hits")Prompt
Use the video-shotcraft shot recipe "Particle celebration hits" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/particle-celebrate-hits.json Raw TSX: https://gimgs.net/motion/particle-celebrate-hits/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/data/particle-celebrate-hits/ConfettiCrossfire.tsx, demos/data/particle-celebrate-hits/CounterTickSparks.tsx; imports demos/_fixtures and assets/lib) Spec: 1920x1080, 30 fps, 5.38s 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/data/particle-celebrate-hits/ConfettiCrossfire.tsx =====
// confetti-crossfire —— 双侧礼炮交叉喷洒
// 中央大 KPI 卡 scale 落定(f0–14),揭晓帧 f16 左下+右下两门炮各射 50 颗矩形彩屑:
// 闭式弹道(初速 ~18px/f + spread 55° + 重力 + decay 0.9 的位移闭式解),每帧翻转 8–15°,
// 灰阶为主 + 1/3 琥珀。全部彩屑 ~f100 前落出画外(越界即条件卸载),结尾真静止 ≥55f。
// 帧确定性:sin 散列伪随机派生每颗初速/角度/翻转率,弹道 = 纯 age 的函数。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, TitleBlock } from '../../_fixtures/Fixtures';
const AMBER = '#b45309';
const FIRE = 16; // 揭晓帧 = 发射帧
const DECAY = 0.9;
const GRAV = 1.5; // px/f² (等效重力;decay 下终端落速 = GRAV/(1-d) = 15px/f)
const frac = (x: number) => x - Math.floor(x);
const rnd = (i: number, salt: number) => frac(Math.sin(i * 12.9898 + salt * 78.233) * 43758.5453);
// decay 弹道闭式解:v 每帧 ×0.9 → 位移 = v0·(1-d^age)/(1-d);重力项同样按衰减序列累积
const decaySum = (age: number) => (1 - Math.pow(DECAY, age)) / (1 - DECAY);
type Confetto = {
vx: number; vy: number; w: number; h: number;
spin: number; phase: number; amber: boolean; shade: string;
};
const makeGun = (originDeg: number, saltBase: number): Confetto[] =>
Array.from({ length: 50 }).map((_, i) => {
const ang = ((originDeg + (rnd(i, saltBase) - 0.5) * 55) * Math.PI) / 180;
// decay 0.9 下初速总位移只有 10×v0(velocity 6.6f 减半),18px/f 只够 180px 完全不可感
// → 加码到 70–95px/f:最陡颗峰值升到 y≈390(画面上 1/3),弹幕跨越中央卡,~f96 全部落出
const speed = 70 + rnd(i, saltBase + 1) * 25;
const grays = ['#6d6d6b', '#8f8f8d', '#4a4a48', '#b0b0ae'];
return {
vx: Math.cos(ang) * speed,
vy: -Math.sin(ang) * speed, // 屏幕坐标向下为正,射向斜上
w: 14 + rnd(i, saltBase + 2) * 12,
h: 8 + rnd(i, saltBase + 3) * 8,
spin: 8 + rnd(i, saltBase + 4) * 7, // 8–15°/f
phase: rnd(i, saltBase + 5) * 360,
amber: rnd(i, saltBase + 6) < 1 / 3,
shade: grays[Math.floor(rnd(i, saltBase + 7) * 4)],
};
});
const LEFT_GUN = makeGun(60, 3); // 左下炮朝 60°(偏右上)
const RIGHT_GUN = makeGun(120, 9); // 右下炮朝 120°(偏左上)
const LEFT_POS = { x: 140, y: 1040 };
const RIGHT_POS = { x: 1780, y: 1040 };
export const ConfettiCrossfire: React.FC = () => {
const frame = useCurrentFrame();
const age = frame - FIRE;
// KPI 卡 scale 入场落定
const cardScale = interpolate(frame, [0, 14], [0.6, 1], {
easing: Easing.out(Easing.back(1.8)),
extrapolateRight: 'clamp',
});
const cardOp = interpolate(frame, [0, 8], [0, 1], { extrapolateRight: 'clamp' });
const renderGun = (gun: Confetto[], origin: { x: number; y: number }, keyBase: string) =>
gun.map((c, i) => {
if (age <= 0) return null;
const s = decaySum(age);
const x = origin.x + c.vx * s;
// 重力:每帧速度 +GRAV 再统一衰减 → 位移闭式 Σ 可整理为 GRAV·(age - s·d)/(1-d) 的近似;
// 直接用精确序列和:g 项位移 = GRAV · Σ_{k=1..age} (1-d^k)/(1-d) = GRAV·(age-(d-d^{age+1})/(1-d))/(1-d)
const gDisp = (GRAV * (age - (DECAY - Math.pow(DECAY, age + 1)) / (1 - DECAY))) / (1 - DECAY);
const y = origin.y + c.vy * s + gDisp;
// 落出画外即卸载(左右也裁)
if (y > 1140 || x < -80 || x > 2000) return null;
const rot = c.phase + c.spin * age;
return (
<div
key={`${keyBase}${i}`}
style={{
position: 'absolute', left: x, top: y, width: c.w, height: c.h,
background: c.amber ? AMBER : c.shade,
borderRadius: 2,
// rotateX 造翻牌式 3D 翻转(宽度不变、高度压扁),再加平面 rotate
transform: `rotate(${rot}deg) rotateX(${rot * 2.3}deg)`,
}}
/>
);
});
return (
<div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 110, width: '100%', textAlign: 'center' }}>
<TitleBlock text="CONFETTI CROSSFIRE" size={72} />
</div>
{/* 中央 KPI 卡 */}
<div style={{
position: 'absolute', left: 660, top: 400, width: 600, height: 320,
background: G.card, border: `2px solid ${G.border}`, borderRadius: 16,
boxSizing: 'border-box', padding: 36, boxShadow: '0 4px 14px rgba(0,0,0,0.08)',
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
transform: `scale(${cardScale})`, opacity: cardOp,
}}>
<div style={{ height: 14, width: 220, background: G.bar, borderRadius: 7 }} />
<div style={{
fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 150,
color: AMBER, letterSpacing: -3, lineHeight: 1.1, fontVariantNumeric: 'tabular-nums',
}}>
98.5%
</div>
<div style={{ height: 10, width: 150, background: G.line, borderRadius: 5 }} />
</div>
{renderGun(LEFT_GUN, LEFT_POS, 'L')}
{renderGun(RIGHT_GUN, RIGHT_POS, 'R')}
</div>
);
};
// ===== demos/data/particle-celebrate-hits/CounterTickSparks.tsx =====
// counter-tick-sparks —— 数字跳动溅火
// 中央大计数器 0 → 12,847(easeOut 逐位跳动),每逢跳过整千的 tick 帧,
// 数字顶部溅 6–10 颗大火星(初速向上 9–13px/f、重力下坠 14–18f 内坠灭);
// 终值揭晓那跳翻倍 20 颗 + 数字弹 1.1x 回落。
// 结尾所有火星寿命耗尽条件卸载,真静止 ≥40f。
// 帧确定性:tick 帧从同一 easeOut 曲线预解析(模块级求出),火星 = 纯 age 闭式弹道。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, TitleBlock } from '../../_fixtures/Fixtures';
const AMBER = '#b45309';
const TARGET = 12847;
const COUNT_END = 78; // 计数结束帧
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);
const valueAt = (f: number) => Math.round(TARGET * easeOutCubic(Math.min(Math.max(f / COUNT_END, 0), 1)));
const frac = (x: number) => x - Math.floor(x);
const rnd = (i: number, salt: number) => frac(Math.sin(i * 12.9898 + salt * 78.233) * 43758.5453);
// 预解析 tick 帧:跳过整千的那一帧 + 终值揭晓帧
const TICKS: { f: number; big: boolean }[] = (() => {
const out: { f: number; big: boolean }[] = [];
let prev = 0;
for (let f = 1; f <= COUNT_END; f++) {
const v = valueAt(f);
if (Math.floor(v / 1000) > Math.floor(prev / 1000)) out.push({ f, big: false });
prev = v;
}
out.push({ f: COUNT_END, big: true }); // 终值揭晓
return out;
})();
const SPARK_LIFE = 18;
const GRAV = 0.9;
export const CounterTickSparks: React.FC = () => {
const frame = useCurrentFrame();
const value = valueAt(frame);
// 终值揭晓弹 1.1x 回落
const popScale =
1 +
0.1 *
interpolate(frame, [COUNT_END, COUNT_END + 5, COUNT_END + 16], [0, 1, 0], {
easing: Easing.out(Easing.quad),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const TOP_Y = 452; // 数字顶缘(火星发射线)
const CX = 960;
return (
<div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 110, width: '100%', textAlign: 'center' }}>
<TitleBlock text="COUNTER TICK SPARKS" size={72} />
</div>
{/* 计数卡 */}
<div style={{
position: 'absolute', left: 560, top: 400, width: 800, height: 330,
background: G.card, border: `2px solid ${G.border}`, borderRadius: 16,
boxSizing: 'border-box', boxShadow: '0 4px 14px rgba(0,0,0,0.08)',
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 20,
}}>
<div style={{ height: 14, width: 220, background: G.bar, borderRadius: 7 }} />
<div style={{
fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 168,
color: G.ink, letterSpacing: -3, lineHeight: 1, fontVariantNumeric: 'tabular-nums',
transform: `scale(${popScale})`,
}}>
{value.toLocaleString('en-US')}
</div>
<div style={{ height: 10, width: 150, background: G.line, borderRadius: 5 }} />
</div>
{/* 火星:每个 tick 一簇,寿命耗尽条件卸载 */}
{TICKS.map((tick, t) => {
const age = frame - tick.f;
if (age <= 0 || age >= SPARK_LIFE) return null;
const n = tick.big ? 20 : 6 + Math.floor(rnd(t, 21) * 5); // 6–10,终跳 20
return Array.from({ length: n }).map((_, i) => {
const salt = t * 31 + i;
// 初速:向上 9–13px/f,水平 ±4.5px/f 扇形铺开(终跳更宽)
const vy = -(9 + rnd(salt, 2) * 4);
const vx = (rnd(salt, 3) - 0.5) * (tick.big ? 13 : 9);
const x0 = CX + (rnd(salt, 4) - 0.5) * (tick.big ? 560 : 380); // 沿数字顶缘散布
const x = x0 + vx * age;
const y = TOP_Y + vy * age + 0.5 * GRAV * age * age;
const life = 1 - age / SPARK_LIFE;
const size = (tick.big ? 7 : 5.5) * (0.4 + 0.6 * life);
return (
<div key={`${t}-${i}`} style={{
position: 'absolute', left: x, top: y, width: size, height: size,
borderRadius: size / 2, background: AMBER, opacity: life,
boxShadow: `0 0 ${6 * life}px ${AMBER}`,
}} />
);
});
})}
</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/data/particle-celebrate-hits/ConfettiCrossfire.tsx, demos/data/particle-celebrate-hits/CounterTickSparks.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

Timeline travel pan
Timeline travel — the camera accelerates along a horizontal ruler axis past version tick marks, a card springs upright and pauses briefly at each tick, and an abrupt stop pushes in on the final tick.

Scroll brake moves
A long scroll decelerates to a precise stop on the latest item; a variant adds a reticle lock on the same frame.

Ring diagram annotation reveal
A full-screen subject is squeezed into a ring diagram by a circular window, then the group shrinks left to reveal annotations.

Particle sand-fill bars
Bar charts don't grow tall — they "rain into being": square particles fall one by one and pile up into bars, then solidify.