Motion · Dữ liệu

Đếm số nổ pháo giấy

Con số lớn đếm tăng theo easeOutQuart kèm vọt quá về scale; trước khi về đích một nhịp, 52 mảnh pháo giấy nổ vào từ hai bên theo quỹ đạo parabol, vòng va chạm lan tỏa, nhãn chữ siết khoảng cách kết thúc.

1920×108016:930 fps5.1s · 153 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#data

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

Nhịp ăn mừng cho các con số cột mốc/thành tích: số người dùng, doanh thu, lượt tải — mọi chỉ số "đáng mở champagne".

Ý đồ

Con số không phải "hiển thị" ra, mà là "về đích": đường cong đếm nhanh trước chậm sau như chạy nước rút 100m chuyển sang slow-motion về đích, pháo giấy nổ ra trước khi con số về đích — ăn mừng đến trước kết quả nửa nhịp, cảm xúc lấn át thông tin, người xem phấn khích trước rồi mới nhìn rõ con số.

Thời lượng & năng lượng

  • Thời lượng: Khoảng 4.6s (138f@30fps; đếm 0.3–2.6s · pháo giấy từ 2.4s · ổn định lúc 3.3s).
  • Năng lượng: Cao (dồn lực khi đếm + giải phóng bùng nổ, một cảnh đỉnh cảm xúc kinh điể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)

意图

数字不是"显示"出来的,是"冲线"的:计数曲线前快后慢像百米冲刺进入慢镜,纸屑在数字到位之前抢拍爆开——庆祝比结果先到半拍,情绪压过信息,观众先兴奋再看清数字。

动效核心

  • 计数走 easeOutQuart(t=0.06–0.56):前 1/3 时间跑完 2/3 数值,符合"冲刺-减速-压线"的体感;toLocaleString 千分位格式
  • 数字 scale 两段:0.2→1.3 冲入(outCubic),计数到位后 1.3→1 回落(outBack 轻弹)——大一号入场、落定收正
  • 抢拍:纸屑 BURST=0.52 早于计数到位 0.56——庆祝先于结果 0.04(约 5 帧),这半拍是这张卡的灵魂
  • 52 片纸屑真物理:从画面两侧 ±250px 出发,初速 vx 150–450 向中冲 / vy −230~−450 上抛,重力 g 900–1420 拉回,自转 ±750°,每片错峰 ≤0.06
  • 到位冲击环:0.545 起 scale 0.35→2.95 扩散淡出(outQuart);标签 0.62 淡入且字距 11px→5px 收紧——"落章"感

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 计数曲线 | easeOutQuart,0.06–0.56 | 换 outCubic 冲刺感减弱;线性计数是最常见的"没有情绪"写法 | | 抢拍量 | BURST 提前 0.04 | 提前 0–0.02 读作同拍;>0.08 纸屑像"别人的庆祝" | | 纸屑数量 | 52 片(8 色调色板) | <30 稀疏不成雨;>80 遮数字。PAL 是"演出彩纸",一般不随品牌换色 | | 重力/初速比 | g≈2.5×|vy| | 决定抛物线顶点在画面上 1/3 处;g 太小纸屑飘出画顶 | | scale 过冲 | 1.3 峰值 + outBack 回落 | 数字是主角,过冲 >1.5 会撞到标签排版 | | 终值 | p*1000(演示值) | 换真实数字:保持"从 0 滚到终值",跳变起点会丢冲刺感 |

已知坑

  • 纸屑物理用 life = u*1.1 的归一化秒——改 dur 后重力体感会变(同 u 对应不同真实时长),需重调 g/vy
  • 抢拍差 0.04 是硬编码在 BURST 与计数终点两处的常量,改计数窗口时同步平移 BURST 保持提前量
  • 纸屑 opacity 尾段 0.74 起衰减到 0.05——别让纸屑活到 t=1 还在飞,落定段(标签收字距)需要干净背景
  • 配 SFX:计数段 tick 渐密 + BURST 拍 pop/confetti + 到位拍 impact——三层缺一情绪都塌(对照 sound-design.md counter 类)

参考实现

demos/data/counter-confetti/ (CounterConfetti.tsx)

Mã nguồn tham chiếu (Remotion)

  • demos/data/counter-confetti/CounterConfetti.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/counter-confetti/CounterConfetti.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.

  1. 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.
  2. Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
  3. Đă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/counter-confetti.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/data/counter-confetti/CounterConfetti.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("counter-confetti")

Prompt

Use the video-shotcraft shot recipe "Đếm số nổ pháo giấy" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/counter-confetti.json
Raw TSX: https://gimgs.net/motion/counter-confetti/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/data/counter-confetti/CounterConfetti.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.1s

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).

JSON · Markdown · llms.txt

5,144 chars · Apache-2.0Mở template gốc ↗
// ===== demos/data/counter-confetti/CounterConfetti.tsx =====
// counter-confetti — Counter Confetti 数字冲刺纸屑(motion-lab 定稿转原生 Remotion)
// 大数字 0 → 1000 用 easeOutQuart 冲刺,scale 走 [0.2,1.3,1.3,1] 过冲;数字到位前
// 5 帧就从两侧 burst 出 8 色纸屑,带重力下落与自转飘散。
// 设计坐标 480×270(DesignStage 等比放大),参数表数值以此坐标系标定。
import React from 'react';
import { DesignStage, E, lerp, rand, seg, useT } from '../../_fixtures/Motion';

export const COUNTER_CONFETTI_DURATION = 138; // 4600ms @30fps

const PAL = ['#ff6b8b', '#ffb347', '#ffe86b', '#7bff9e', '#5fd8ff', '#6c8cff', '#c86cff', '#ff6cf0'];
const BURST = 0.52; // 计数到位(0.56)前"抢拍"

// 52 片纸屑的静态参数(种子与 effect.js 完全一致,确定性可复现)
const BITS = Array.from({ length: 52 }, (_, i) => {
  const isRect = rand(i * 3) > 0.4;
  const w = 5 + rand(i + 2) * 6;
  const h = isRect ? 8 + rand(i + 5) * 7 : w;
  const side = i % 2 ? 1 : -1;
  return {
    w,
    h,
    isRect,
    color: PAL[i % 8],
    x0: side * 250, // 从画面两侧出发(px,相对中心)
    y0: (rand(i + 30) - 0.5) * 60,
    vx: -side * (150 + rand(i * 5 + 1) * 300), // 向画面中间冲
    vy: -(230 + rand(i * 7 + 3) * 220),
    g: 900 + rand(i + 60) * 520,
    spin: (rand(i + 90) - 0.5) * 1500,
    d: rand(i + 120) * 0.06, // 每片略微错峰
  };
});

export const CounterConfetti: React.FC = () => {
  const t = useT();
  // 计数:easeOutQuart 冲刺
  const p = seg(t, 0.06, 0.56, E.outQuart);
  const val = Math.round(p * 1000);
  // scale 过冲 [0.2,1.3,1.3,1]
  const s1 = seg(t, 0.06, 0.3, E.outCubic);
  const s2 = seg(t, 0.56, 0.72, E.outBack);
  const sc = lerp(s1, 0.2, 1.3) + s2 * (1 - 1.3);
  // 到位冲击环
  const rp = seg(t, 0.545, 0.75, E.outQuart);
  return (
    <DesignStage bg="radial-gradient(80% 80% at 50% 45%,#161a2b,#07080e 75%)">
      {/* 中心辉光 */}
      <div
        style={{
          position: 'absolute',
          left: '50%',
          top: '47%',
          width: 340,
          height: 340,
          margin: -170,
          borderRadius: '50%',
          background: 'radial-gradient(circle,rgba(120,160,255,.28),transparent 66%)',
          opacity: 0.35 + seg(t, 0.5, 0.62, E.outCubic) * 0.65 - seg(t, 0.72, 1) * 0.5,
        }}
      />
      {/* 大数字(渐变填充文字,外层 scale 过冲) */}
      <div style={{ position: 'absolute', left: '50%', top: '47%', transformOrigin: '50% 50%', transform: `scale(${sc})` }}>
        <div
          style={{
            position: 'absolute',
            left: 0,
            top: 0,
            transform: 'translate(-50%,-50%)',
            whiteSpace: 'nowrap',
            fontWeight: 800,
            fontSize: 74,
            lineHeight: 1,
            // 原栈 -apple-system 在 Remotion 无头浏览器解析不到(落到 Arial),
            // 补 Helvetica 兜底——度量与原片的 SF Pro 逐字形吻合
            fontFamily: "-apple-system,Helvetica,'Segoe UI',sans-serif",
            letterSpacing: -2,
            background: 'linear-gradient(180deg,#ffffff,#a9bcff)',
            WebkitBackgroundClip: 'text',
            backgroundClip: 'text',
            color: 'transparent',
            textShadow: '0 0 34px rgba(130,160,255,.28)',
            opacity: Math.min(1, seg(t, 0.02, 0.12) * 1.2),
          }}
        >
          {val.toLocaleString('en-US')}
        </div>
      </div>
      {/* 底部标签:淡入 + letter-spacing 收拢 */}
      <div
        style={{
          position: 'absolute',
          left: '50%',
          top: '70%',
          transform: 'translate(-50%,0)',
          fontWeight: 700,
          fontSize: 10,
          lineHeight: 1,
          fontFamily: '-apple-system,Helvetica,sans-serif', // 同上:Helvetica 兜底对齐原片

          color: '#7d88a8',
          opacity: seg(t, 0.62, 0.78, E.outCubic),
          letterSpacing: lerp(seg(t, 0.62, 0.82, E.outCubic), 11, 5),
        }}
      >
        METRIC THIS WEEK
      </div>
      {/* 到位冲击环 */}
      <div
        style={{
          position: 'absolute',
          left: '50%',
          top: '47%',
          width: 120,
          height: 120,
          margin: -60,
          borderRadius: '50%',
          border: '2px solid rgba(160,190,255,.8)',
          // 原渲染无全局 border-box:120px 是内容宽,2px 边框外扩到 124px
          // (Remotion 注入了 * { box-sizing:border-box },显式还原才对得上原片)
          boxSizing: 'content-box',
          opacity: rp > 0 ? (1 - rp) * 0.9 : 0,
          transform: `scale(${0.35 + rp * 2.6})`,
        }}
      />
      {/* 纸屑:抢拍 burst + 重力下落 + 自转 */}
      {BITS.map((b, i) => {
        const u = seg(t, BURST + b.d, 1);
        const life = u * 1.1; // 秒级时间尺度
        const x = b.x0 + b.vx * life;
        const y = b.y0 + b.vy * life + 0.5 * b.g * life * life;
        return (
          <div
            key={i}
            style={{
              position: 'absolute',
              left: '50%',
              top: '50%',
              width: b.w,
              height: b.h,
              background: b.color,
              borderRadius: b.isRect ? 2 : '50%',
              willChange: 'transform',
              opacity: u <= 0 ? 0 : Math.min(1, u * 8) * (1 - seg(u, 0.74, 1) * 0.95),
              transform: `translate(calc(-50% + ${x}px),calc(-50% + ${y}px)) rotate(${b.spin * life}deg) scale(${0.8 + (1 - u) * 0.35})`,
            }}
          />
        );
      })}
    </DesignStage>
  );
};


  1. Read the recipe
    Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
  2. Get the code
    tool: git
    git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install
  3. Register + render
    tool: remotion
    npx remotion render <CompositionId> out.mp4 # component: demos/data/counter-confetti/CounterConfetti.tsx
  4. Or re-implement
    Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
paramtypedefaultdescription
—

Cùng danh mục