Motion · Dữ liệu

Lưới avatar mọc vòng tô màu

Lưới thẻ 8×7 mọc từng vòng từ tâm ra đến khi kín (nội dung trộn chữ cái đầu/icon/ảnh placeholder), rồi khoảng 15% thẻ đổi đỏ ngẫu nhiên đánh dấu bất thường, chú thích tiêu đề nằm cố định giữa khung.

1920×108016:930 fps6.1s · 183 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

Tường thuật dữ liệu kiểu "bất thường/điểm nổi bật xuất hiện giữa đám đông": sức khỏe tệp người dùng, dashboard giám sát, tổng quan trạng thái hàng loạt.

Ý đồ

Cấu trúc hai màn: Màn 1 "nhóm hình thành" — thẻ mọc từng vòng từ tâm ra, như khuẩn lạc lớn dần dưới kính hiển vi; Màn 2 "bất thường xuất hiện" — sau khi lấp đầy, từng thẻ lần lượt chuyển đỏ, buộc mắt người xem phải quét khắp lưới tìm chấm đỏ. Nó biến câu "chúng tôi theo dõi mọi mục cho bạn" thành một trải nghiệm.

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

  • Thời lượng: Khoảng 5.6s (168f@30fps; lấp đầy 0.5–1.7s · tô màu 1.7–3.4s, so le).
  • Năng lượng: Trung bình (giai đoạn mọc lưới có nhịp điệu, giai đoạn tô màu là khoảnh khắc "phát hiện" yên tĩnh).

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)

意图

两幕结构:第一幕"群体成形"——卡片从中心一环环长出,像镜头下的菌落生长;第二幕"异常浮现"——铺满后个别卡片陆续变红,观众的眼睛被迫扫描全场找红点。它把"我们帮你盯着所有项"这句话拍成了体验。

动效核心

  • 分环 stagger:每格按 ring = round(hypot((c-3.5), (r-3)/0.85)) 算环号,延迟 (ring*4 + rand*3)/TOTAL ——同环内再加 3 帧随机抖动,生长波有机不机械
  • 卡片入场只做 opacity(0.018 窗口)+ scale 0.8→1(0.03,outQuad),无位移——"长出来"而不是"飞进来"
  • 卡片内容三种占位混合(rand(i*9.1) 决定):首字母对(logo 位)、几何图标符(◆▲●✦)、渐变色块(图片位)——实际使用换项目素材(头像/logo/缩略图均可)
  • 中央 3×6 区域 visibility:hidden 占位不销毁——网格完整性保留,给标题和图例腾出舞台
  • 染色幕:15% 的卡片(rand<0.15)在 t=0.30–0.60 随机时刻各自 0.036 窗口染色——白底→浅红 #FDECEC、描边→#F6CFCF、状态点绿→红,三通道同拍 mix
  • 标题 0.02–0.10 先入(scale 0.98→1),图例(Active/Pending/Inactive 三色点)0.26 跟进

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 生长速度 | 每 4 帧一环 + 3 帧抖动,1.2s 铺满 | 快于 3 帧/环读作"闪现";抖动砍掉后波前整齐得像机器 | | 异常比例 | 15% | <8% 观众可能漏看;>25% "异常"变"普遍",叙事反转 | | 染色窗口 | 每张 0.036,散布在 0.30–0.60 | 全部同拍染色是警报不是浮现;散布区间越长"陆续发现"感越强 | | 状态色 | 绿 #37C46B → 红 #F0453A(点),底 #FDECEC | 功能色不随品牌换;底色染红别深过 #F8D8D8,卡片文字要仍可读 | | 网格规模 | 8×7 − 中央 18 格 = 38 张可见 | 更大网格需同步缩 gap 和字号;中央留空区随标题长度调 | | 内容混合比 | 首字母/图标/图片 各 1/3(rand 均分) | 全换成同类(如全头像)也成立;混合版本更像"多类型资产"场景 |

已知坑

  • 中央留空用 visibility:hidden 而不是不渲染——删格子会让分环计算的环心偏移,生长波变形
  • 染色的三个通道(底/描边/点)必须同一条 cT 曲线驱动,分开会出现"点红了底还白"的中间态
  • 图片占位是 CSS 渐变色块,换真实图片时保留 overflow:hidden 和 border-radius 继承,注意深色图上状态点要加白描边
  • 标题/图例 z-index 在卡片之上但无底板,若换更密的网格需给中央区加半透明底垫保证可读

参考实现

demos/data/avatar-grid-radial-build-colorize/ (AvatarGridRadialBuildColorize.tsx)

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

  • demos/data/avatar-grid-radial-build-colorize/AvatarGridRadialBuildColorize.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/avatar-grid-radial-build-colorize/AvatarGridRadialBuildColorize.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/avatar-grid-radial-build-colorize.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/data/avatar-grid-radial-build-colorize/AvatarGridRadialBuildColorize.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("avatar-grid-radial-build-colorize")

Prompt

Use the video-shotcraft shot recipe "Lưới avatar mọc vòng tô màu" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/avatar-grid-radial-build-colorize.json
Raw TSX: https://gimgs.net/motion/avatar-grid-radial-build-colorize/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/data/avatar-grid-radial-build-colorize/AvatarGridRadialBuildColorize.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 6.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

7,614 chars · Apache-2.0Mở template gốc ↗
// ===== demos/data/avatar-grid-radial-build-colorize/AvatarGridRadialBuildColorize.tsx =====
// avatar-grid-radial-build-colorize — Avatar Grid 分环生长随机染色(motion-lab 定稿转原生 Remotion)
// 8×7 小卡片网格由中心向四周分环生长(每 4 帧扩一环,1.2s 铺满),卡片内容混合三种占位:
// 首字母 / 图标符 / 色块缩略图,只做 opacity + scale 0.8→1 不位移;铺满后约 15% 的卡片
// 在 1s 内随机时刻把底色染成浅红、状态点转红,形成"异常项逐渐浮现"的呼吸感。
// 中央 3×6 区域 visibility:hidden 占位留给标题与图例,标题层始终 100% 不透明。
// 设计坐标 480×270(DesignStage 等比放大),参数表数值以此坐标系标定。
import React from 'react';
import { DesignStage, E, lerp, rand, seg, useT } from '../../_fixtures/Motion';

export const AVATAR_GRID_RADIAL_BUILD_COLORIZE_DURATION = 168; // 5600ms @30fps

const PAPER = '#F3F3F1';
const INK = '#111113';
const MID = '#8A8A8F';
const CARD_WHITE = '#ffffff';
const FLAG_BG = '#FDECEC';

// 颜色插值(effect.js 的 mix;hex2rgb 原来自 lab 页面全局,这里内联实现)
const hex2rgb = (hex: string): [number, number, number] => [
  parseInt(hex.slice(1, 3), 16),
  parseInt(hex.slice(3, 5), 16),
  parseInt(hex.slice(5, 7), 16),
];
const mix = (a: string, b: string, t: number) => {
  const A = hex2rgb(a);
  const B = hex2rgb(b);
  return `rgb(${Math.round(A[0] + (B[0] - A[0]) * t)},${Math.round(A[1] + (B[1] - A[1]) * t)},${Math.round(
    A[2] + (B[2] - A[2]) * t,
  )})`;
};

const COLS = 8;
const ROWS = 7;
const TOTAL = 5.6 * 30; // 总帧数(delay 换算用)

// 网格几何:等价 effect.js 的 inset:16px + gap:10px CSS grid,但改为绝对定位手排。
// CSS grid 在 480×270 设计坐标下会把轨道取整到整像素(放大 4 倍后节距 140/144 交替),
// 与原片的全分辨率均匀分布不符;小数设计坐标 + 绝对定位可保住亚像素节距。
const GAP = 10;
const CELL_W = (480 - 32 - (COLS - 1) * GAP) / COLS; // 47.25
const CELL_H = (270 - 32 - (ROWS - 1) * GAP) / ROWS; // ≈25.43

// 卡片内容三种占位:首字母(logo 位) / 图标符 / 色块缩略图(图片位),实际使用换项目素材
const INI = ['VS', 'KJ', 'EM', 'AL', 'TR', 'MN', 'BQ', 'DW', 'RC', 'SF', 'PL', 'GH'];
const ICONS = ['◆', '▲', '●', '■', '✦', '◐', '❖', '▣'];

// 每格的静态参数(种子与 effect.js 逐一对应,跨帧确定)
const CELLS = Array.from({ length: ROWS * COLS }, (_, i) => {
  const r = Math.floor(i / COLS);
  const c = i % COLS;
  const hidden = r >= 2 && r <= 4 && c >= 1 && c <= 6; // 中央留空给标题 + 图例(占位不破坏网格)
  const kind = Math.floor(rand(i * 9.1) * 3); // 0=首字母 1=图标 2=图片缩略
  const ini = INI[Math.floor(rand(i * 3.7) * INI.length)];
  const icon = ICONS[Math.floor(rand(i * 5.3) * ICONS.length)];
  const h = Math.floor(rand(i * 7.7) * 360); // 缩略图渐变色相
  const ring = Math.round(Math.hypot((c - 3.5) / 1.0, (r - 3) / 0.85)); // 到中心的"环号"
  const delay = (ring * 4 + rand(i + 40) * 3) / TOTAL; // 每 4 帧扩一环 + 帧级抖动
  const flagged = rand(i + 900) < 0.15; // ~15% 异常卡
  const at = 0.3 + rand(i + 1600) * 0.3; // 染色随机时刻
  return { hidden, kind, ini, icon, h, delay, flagged, at };
});

const LEGEND: Array<[string, string]> = [
  ['Active', '#37C46B'],
  ['Pending', '#F5A524'],
  ['Inactive', '#F0453A'],
];

export const AvatarGridRadialBuildColorize: React.FC = () => {
  const t = useT();
  // 标题淡入 + 轻微 scale;图例随后浮现
  const tIn = seg(t, 0.02, 0.1, E.outQuad);
  const legendIn = seg(t, 0.26, 0.36, E.outQuad);
  return (
    <DesignStage bg={PAPER} raster="zoom">
      <div
        style={{
          position: 'absolute',
          inset: 0,
          overflow: 'hidden',
          fontFamily: "-apple-system,'Helvetica Neue',Helvetica,Arial,sans-serif",
        }}
      >
        <div style={{ position: 'absolute', inset: 16 }}>
          {CELLS.map((cl, i) => {
            const r = Math.floor(i / COLS);
            const c = i % COLS;
            const f = 0.09 + cl.delay;
            const o = seg(t, f, f + 0.018, E.linear);
            const sc = seg(t, f, f + 0.03, E.outQuad);
            // 异常卡:底色染浅红、边框染粉、状态点转红
            const cT = cl.flagged ? seg(t, cl.at, cl.at + 0.036, E.outQuad) : 0;
            return (
              <div
                key={i}
                style={{
                  position: 'absolute',
                  left: c * (CELL_W + GAP),
                  top: r * (CELL_H + GAP),
                  width: CELL_W,
                  height: CELL_H,
                  boxSizing: 'border-box', // 等价 grid stretch:边框计入轨道尺寸
                  borderRadius: 8,
                  background: cl.flagged ? mix(CARD_WHITE, FLAG_BG, cT) : CARD_WHITE,
                  border: `1px solid ${cl.flagged ? mix('#E7E7E4', '#F6CFCF', cT) : '#E7E7E4'}`,
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  overflow: 'hidden',
                  fontWeight: 700,
                  fontSize: cl.kind === 1 ? 13 : 11,
                  lineHeight: 1,
                  fontFamily: '-apple-system,Helvetica,sans-serif', // Remotion 无头浏览器缺 -apple-system,补 Helvetica 对齐原片 SF 字形
                  color: cl.kind === 1 ? '#6a6f7c' : '#3A3A3E',
                  opacity: o,
                  boxShadow: '0 1px 2px rgba(0,0,0,.04)',
                  visibility: cl.hidden ? 'hidden' : 'visible',
                  transform: `scale(${lerp(sc, 0.8, 1)})`,
                }}
              >
                {cl.kind === 0 ? cl.ini : cl.kind === 1 ? cl.icon : null}
                {cl.kind === 2 && (
                  <div
                    style={{
                      position: 'absolute',
                      inset: 0,
                      borderRadius: 7,
                      background: `linear-gradient(${45 + (cl.h % 90)}deg, hsl(${cl.h},18%,78%), hsl(${
                        (cl.h + 40) % 360
                      },22%,62%))`,
                    }}
                  />
                )}
                <div
                  style={{
                    position: 'absolute',
                    right: 4,
                    top: 4,
                    width: 5,
                    height: 5,
                    borderRadius: '50%',
                    background: cl.flagged ? mix('#37C46B', '#F0453A', cT) : '#37C46B',
                  }}
                />
              </div>
            );
          })}
        </div>
        <div
          style={{
            position: 'absolute',
            left: '50%',
            top: '45%',
            transform: `translate(-50%,-50%) scale(${lerp(tIn, 0.98, 1)})`,
            fontWeight: 800,
            fontSize: 30,
            lineHeight: 1.2,
            fontFamily: "-apple-system,'Helvetica Neue',sans-serif",
            letterSpacing: -1.2,
            color: INK,
            textAlign: 'center',
            zIndex: 5,
            whiteSpace: 'nowrap',
            opacity: tIn,
          }}
        >
          Let's bring them back in
        </div>
        <div
          style={{
            position: 'absolute',
            left: '50%',
            top: '56%',
            transform: 'translateX(-50%)',
            display: 'flex',
            gap: 14,
            alignItems: 'center',
            zIndex: 5,
            padding: '4px 10px',
            opacity: legendIn,
            whiteSpace: 'nowrap',
          }}
        >
          {LEGEND.map(([txt, col]) => (
            <div
              key={txt}
              style={{
                display: 'flex',
                alignItems: 'center',
                gap: 5,
                fontWeight: 600,
                fontSize: 11,
                lineHeight: 1,
                fontFamily: '-apple-system,Helvetica,sans-serif', // Remotion 无头浏览器缺 -apple-system,补 Helvetica 对齐原片 SF 字形
                color: MID,
              }}
            >
              <span
                style={{ width: 6, height: 6, borderRadius: '50%', background: col, display: 'inline-block' }}
              />
              <span>{txt}</span>
            </div>
          ))}
        </div>
      </div>
    </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/avatar-grid-radial-build-colorize/AvatarGridRadialBuildColorize.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