Motion · Nhịp & montage

Ngữ pháp trailer: ba nhịp

Ba thủ pháp ngữ pháp trailer — trailer-bumper cắt nhanh móc câu mở đầu, card-footage-cadence xen thẻ chữ với cảnh đối thoại, smash-cut đập cắt vào tĩnh lặng.

1920×108016:930 fps4.72s · 142 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#rhythm#opening#transition

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

Biến thể

trailer-bumper
Ba cảnh mỗi cảnh cắt cứng dài bằng nhau 9f (0/9/18), mỗi cảnh nội bộ có cú đẩy nhẹ scale 1→1.04 giữ sức sống; 27–33f im lặng đen thuần, từ 33f tiêu đề mờ vào 16f + nâng nhẹ 44px out-cubic
Móc câu trước khi vào phim chính; cold open
card-footage-cadence
Bảy đoạn gắn có điều kiện (điểm cắt 14/22/34/42/52/62): đoạn UI mang vi động (đẩy chậm/lia cắt khung), đoạn thẻ chữ nền đen chữ trắng đứng lại 1.05→1 hơi thu nhỏ
Đối thoại hình-chữ giữa phim; chuỗi ba điểm bán hàng
smash-cut
Đoạn gầm rú chạy toàn bộ Easing.in(quad): nền đẩy 1→1.55 + xoay 1.8°, 5 thẻ bay lệch nhịp tăng tốc lao vào mặt + mờ theo cổng tốc độ; cắt cứng một khung tại 42f sang toàn cảnh tĩnh lặng không thuộc tính hoạt ảnh nào
Dấu chấm hết thu gọn cao trào lớn nhất phim

Khi nào dùng

Ba khoảnh khắc kết cấu của trailer: mở đầu móc câu thế nào (A), giữa phim đối thoại thế nào (B), cao trào kết thúc thế nào (C); ba kiểu dùng chung tạo thành bộ khung của một trailer

Ý đồ

Các thẻ nhịp điệu trong thư viện mỗi thẻ quản một tầng: beat-cut quản cách bố trí điểm cắt, montage-rhythm quản nhịp thở của đoạn, rhythm-interrupt quản việc ngắt quãng. Thẻ này quản các khoảnh khắc kết cấu của trailer — không phải một cảnh chuyển động thế nào, mà ba khớp nối của cả phim ghép ra sao: A là móc câu mở đầu — 0.9 giây trước khi vào phim chính nhét ba cảnh bắt mắt nhất cắt nhanh + một nhịp tĩnh lặng nền đen rồi mới mở đầu thật, "lời rao của lời rao"; B là đối thoại giữa phim — cảnh UI và thẻ chữ nền đen luân phiên cắt cứng, cả hình lẫn chữ đều bắt nhịp, cấu trúc hồi ba; C là dấu chấm hết cao trào — màn hình đầy chuyển động gầm rú cắt cứng ở đỉnh điểm thành một toàn cảnh tĩnh lặng chết chóc, ồn ào chuyển sang im lặng. Ba kiểu dùng chung tạo thành bộ khung của một trailer.

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

  • Thời lượng: A khoảng 4.7s / B khoảng 5s / C khoảng 4.5s
  • Năng lượng: A cao / B trung bình / C cao

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)

意图

库内节奏卡各管一段:beat-cut 管切点排布、montage-rhythm 管段落呼吸、 rhythm-interrupt 管打断。本卡管预告片的结构性时刻——不是某个 镜头怎么动,是整片的三个关节怎么接:A 是开场钩——正片前 0.9 秒塞 三个最抓眼镜头速剪 + 黑场静默一拍再开场,预告片的"预告";B 是中段 对话——UI 镜头与黑底短语卡互相接拍交替硬切,画面与文字都踩拍点, 第三幕结构;C 是高潮句号——满屏动势轰鸣最高潮一帧硬切成整齐静止 全景死寂,喧闹→死寂。三式合用即一支预告片的骨架。

三式选型

| 式 | 做法 | 适用 | |----|------|------| | A trailer-bumper | 三镜头各 9f 等长硬切(0/9/18),每镜内部 scale 1→1.04 微推保活;27–33f 纯黑静默,33f 起标题 16f 淡入 + 44px out-cubic 微升 | 正片开场前的钩子;cold open | | B card-footage-cadence | 七段条件挂载分段(14/22/34/42/52/62 切点):UI 段带微动(缓推/裁切横移),字卡段黑底白字 1.05→1 落定微缩 | 中段画面与文字对话;卖点三连 | | C smash-cut | 轰鸣段全 Easing.in(quad):背景推近 1→1.55 + rotate 1.8°、5 张飞卡错峰加速冲脸 + 速度门控模糊;42f 一帧硬切无动画属性的静止全景 | 全片最高潮的收束句号 |

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A 速剪 | 三镜等长 9f,零过渡;镜头 3 用 origin 58%/40% 放大 2.2x 怼脸特写 | 节拍感靠等长;三镜构图必须明显不同 | | A 微推 | 每镜内部 scale 1→1.04 线性 | 静帧三连读作幻灯片——命门 | | A 黑场 | 27–33f 纯黑 #000 空 div,6f | 黑场里放任何东西都毁掉"静默一拍" | | B 交替 | UI 段 12f 级 ↔ 字卡 8f 级硬切,条件挂载分段=天然零过渡 | 段长可变但要读出"接拍"感 | | B 字卡 | 黑底白字居中,落定 1.05→1 out-cubic 5f 内完成,其余全静 | 字卡的静 ↔ UI 段的动,质感对比即节奏 | | C 切点 | 42f 一帧完成全部反差;切前 3f 动势仍在加速 | 绝不减速迎接切点——命门 | | C 模糊 | 飞卡 blur = 1+4p(p∝瞬时速度),背景恒 1.5px 衬前景 | 速度门控:快才糊,不是全程糊 | | 收尾 | A 静止 91f / B 45f / C 93f(≥40f) | C 死寂段直接 return 无动画子树,帧函数级真静止 |

已知坑

  • demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,

首次实战须以真实素材回验

  • A 黑场必须纯黑无物(drop-blackout 判例同族)——黑场是喘息,

不是又一张字卡

  • B 与 cel-flash-stomp/beat-cut 字卡段的分工:那些是字在表演

(字自己是主角),这是字与画对话(互相接拍)——别混用

  • C 死寂段绝对真静止、音乐同帧骤停——声音强依赖,无声版的 smash cut

只剩一半(sound-design §4.5);A 三连切每切一声打点、黑场全消音

  • C 与 drop-blackout-slam 的区别:那是黑场蓄力再爆(静→爆),

这是爆完瞬间入定(爆→静)——方向相反,别接错位置

参考实现

demos/rhythm/trailer-grammar-moves/ (CardFootageCadence.tsx / SmashCut.tsx / TrailerBumper.tsx)

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

  • demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx
  • demos/rhythm/trailer-grammar-moves/SmashCut.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/SmashCut.tsx
  • demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/TrailerBumper.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/trailer-grammar-moves.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx, demos/rhythm/trailer-grammar-moves/SmashCut.tsx, demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("trailer-grammar-moves")

Prompt

Use the video-shotcraft shot recipe "Ngữ pháp trailer: ba nhịp" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/trailer-grammar-moves.json
Raw TSX: https://gimgs.net/motion/trailer-grammar-moves/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx, demos/rhythm/trailer-grammar-moves/SmashCut.tsx, demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 4.72s

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

10,290 chars · Apache-2.0Mở template gốc ↗
// ===== demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx =====
// card-footage-cadence|字卡穿插节奏
// UI 镜头 ↔ 黑底字卡像对话接拍:0–14f UI A 缓推 → 硬切 8f 字卡 SHIP →
// 硬切 12f UI B 1.6x 裁切缓移 → 8f 字卡 FASTER → 10f UI A 2x 裁切 →
// 10f 字卡 TODAY(下划线) → 62f 硬切收尾全景定格。
// UI 段全部带微动(推/移),字卡段除落定微缩(1.05→1, out-cubic)外全静,
// 动↔静对比即手法本体。所有切换零过渡(条件挂载,无 crossfade)。
// 收尾 62–105f 轻推收完,105–150f 真静止 45f ≥ 40f。帧确定,无随机。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';
import { FakeDashboard } from '../../_fixtures/Fixtures';

const CLAMP = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;

// 黑底白字字卡:落定微缩 1.05→1(out-cubic,段内前 5f 完成),其余全静
const TitleCard: React.FC<{ text: string; local: number; underline?: boolean }> = ({
  text,
  local,
  underline = false,
}) => {
  const scale = interpolate(local, [0, 5], [1.05, 1], {
    ...CLAMP,
    easing: Easing.out(Easing.cubic),
  });
  return (
    <AbsoluteFill
      style={{ background: '#0d0d0d', alignItems: 'center', justifyContent: 'center' }}
    >
      <div
        style={{
          transform: `scale(${scale})`,
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          gap: 26,
        }}
      >
        <div
          style={{
            fontFamily: 'Helvetica, Arial, sans-serif',
            fontWeight: 800,
            fontSize: 170,
            color: '#ffffff',
            letterSpacing: 2,
            lineHeight: 1,
          }}
        >
          {text}
        </div>
        {underline && (
          <div style={{ width: 560, height: 14, background: '#ffffff', borderRadius: 7 }} />
        )}
      </div>
    </AbsoluteFill>
  );
};

// UI 镜头:包一层 transform 做缓推/缓移,硬切由外层条件挂载保证
const UiShot: React.FC<{
  variant: 'A' | 'B';
  transform: string;
  origin?: string;
}> = ({ variant, transform, origin = '50% 50%' }) => (
  <AbsoluteFill style={{ overflow: 'hidden', background: '#ececea' }}>
    <div style={{ width: 1920, height: 1080, transform, transformOrigin: origin }}>
      <FakeDashboard variant={variant} />
    </div>
  </AbsoluteFill>
);

export const CardFootageCadence: React.FC = () => {
  const frame = useCurrentFrame();

  // ---- 分段:切点 14 / 22 / 34 / 42 / 52 / 62,总长 150 ----

  // 段1 0–14f:UI A 全景缓推 1.0→1.08
  if (frame < 14) {
    const s = interpolate(frame, [0, 14], [1, 1.08], CLAMP);
    return <UiShot variant="A" transform={`scale(${s})`} />;
  }

  // 段2 14–22f:字卡 SHIP
  if (frame < 22) {
    return <TitleCard text="SHIP" local={frame - 14} />;
  }

  // 段3 22–34f:UI B 1.6x 裁切 + 横向缓移(120px 内容位移,屏上 ~192px)。
  // origin 偏左让列表行图标入画,读得出是 UI(QA 后调:50%→35%)
  if (frame < 34) {
    const tx = interpolate(frame - 22, [0, 12], [60, -60], CLAMP);
    return (
      <UiShot variant="B" transform={`scale(1.6) translateX(${tx}px)`} origin="35% 50%" />
    );
  }

  // 段4 34–42f:字卡 FASTER
  if (frame < 42) {
    return <TitleCard text="FASTER" local={frame - 34} />;
  }

  // 段5 42–52f:UI A 另一处 2x 裁切(左上卡片区)+ 继续缓推 2.0→2.14
  if (frame < 52) {
    const s = interpolate(frame - 42, [0, 10], [2, 2.14], CLAMP);
    return <UiShot variant="A" transform={`scale(${s})`} origin="32% 30%" />;
  }

  // 段6 52–62f:字卡 TODAY + 下划线
  if (frame < 62) {
    return <TitleCard text="TODAY" local={frame - 52} underline />;
  }

  // 段7 62–150f:收尾全景 UI 定格。62–105f 极缓推收完(out-cubic),
  // 105f 后 transform 值恒为 1.02,105–150f 真静止 45f。
  const s = interpolate(frame, [62, 105], [1, 1.02], {
    ...CLAMP,
    easing: Easing.out(Easing.cubic),
  });
  return <UiShot variant="B" transform={`scale(${s})`} />;
};


// ===== demos/rhythm/trailer-grammar-moves/SmashCut.tsx =====
// smash-cut|猛切(喧闹→死寂)
// 0–41f 轰鸣段:5 张 Card 多方向高速飞掠冲镜(穿屏 translate + scale 1.5→3
// 冲脸 + 微 rotate,各卡错峰跑两轮,第二轮更快=整体仍在加速),背景
// FakeDashboard A 以 ease-in 持续加速推近 1→1.55 并滚动 rotate 1.8°——
// 切点前一刻动势最猛。42f 一帧硬切 variant B 整齐静止全景,无一物在动,
// 停满 93f(>50f)。反差即手法本体。总 135f。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, Card, FakeDashboard } from '../../_fixtures/Fixtures';

const CUT = 42; // 硬切帧:>=42 全静止

// 每张飞卡:两轮 pass,第二轮更短(更快),ease-in 让每一程都在加速。
// from/to 为屏幕外起终点(中心系坐标),飞行中 scale 1.5→3 冲脸。
type Fly = {
  from: [number, number];
  to: [number, number];
  rot: [number, number]; // 微旋转 起→终
  seed: number;
  w: number;
  h: number;
};

const FLIES: Fly[] = [
  { from: [-1400, -120], to: [1400, 60], rot: [-6, 5], seed: 1, w: 440, h: 290 },
  { from: [1400, 180], to: [-1400, -100], rot: [7, -4], seed: 2, w: 400, h: 260 },
  { from: [-300, -900], to: [200, 900], rot: [-3, 8], seed: 3, w: 460, h: 300 },
  { from: [-1300, 800], to: [1300, -750], rot: [5, -7], seed: 4, w: 420, h: 280 },
  { from: [1350, -780], to: [-1350, 820], rot: [-8, 4], seed: 5, w: 480, h: 310 },
];

// 卡 i 的第 k 轮:start = i*4 + k*20,时长 16 / 12(第二轮更快)。
// card4 第二轮 36–48f 在切点 42f 被硬切截断——正中"绝不减速迎接切点"。
const passWindow = (i: number, k: number): [number, number] => {
  const start = i * 4 + k * 20;
  const dur = k === 0 ? 16 : 12;
  return [start, start + dur];
};

const FlyCard: React.FC<{ fly: Fly; i: number; frame: number }> = ({ fly, i, frame }) => {
  // 找当前活跃的 pass(两轮)
  let active: [number, number] | null = null;
  for (let k = 0; k < 2; k++) {
    const [s, e] = passWindow(i, k);
    if (frame >= s && frame < e) { active = [s, e]; break; }
  }
  if (!active) return null;
  const [s, e] = active;
  // ease-in:全程加速,越接近终点越快
  const p = interpolate(frame, [s, e], [0, 1], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
    easing: Easing.in(Easing.quad),
  });
  const x = fly.from[0] + (fly.to[0] - fly.from[0]) * p;
  const y = fly.from[1] + (fly.to[1] - fly.from[1]) * p;
  const rot = fly.rot[0] + (fly.rot[1] - fly.rot[0]) * p;
  const scale = 1.5 + 1.5 * p; // 1.5 → 3 冲脸
  // 速度门控方向模糊感:ease-in 下瞬时速度 ∝ p,速度越快越糊
  const blur = 1 + 4 * p;
  return (
    <div style={{
      position: 'absolute', left: 960 - fly.w / 2, top: 540 - fly.h / 2,
      transform: `translate(${x}px, ${y}px) rotate(${rot}deg) scale(${scale})`,
      filter: `blur(${blur}px)`,
    }}>
      <Card w={fly.w} h={fly.h} seed={fly.seed}
        style={{ boxShadow: '0 12px 40px rgba(0,0,0,0.25)' }} />
    </div>
  );
};

export const SmashCut: React.FC = () => {
  const frame = useCurrentFrame();

  // —— 死寂段:42f 起 variant B 整齐静止全景,无任何动画属性 ——
  if (frame >= CUT) {
    return <FakeDashboard variant="B" />;
  }

  // —— 轰鸣段:背景 ease-in 加速推近 + 滚动,切点前 3f 仍在加速 ——
  const bgScale = interpolate(frame, [0, CUT], [1, 1.55], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
    easing: Easing.in(Easing.quad),
  });
  const bgRot = interpolate(frame, [0, CUT], [0, 1.8], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
    easing: Easing.in(Easing.quad),
  });

  return (
    <div style={{ width: 1920, height: 1080, background: G.bg, overflow: 'hidden', position: 'relative' }}>
      <div style={{
        width: 1920, height: 1080,
        transform: `scale(${bgScale}) rotate(${bgRot}deg)`,
        transformOrigin: '50% 50%',
        filter: 'blur(1.5px)', // 背景轻糊,衬前景飞卡
      }}>
        <FakeDashboard variant="A" />
      </div>
      {FLIES.map((fly, i) => (
        <FlyCard key={i} fly={fly} i={i} frame={frame} />
      ))}
    </div>
  );
};


// ===== demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx =====
// trailer-bumper|前置速剪预告
// 结构即全部:三连 9f 速剪(A 整页 / B 整页 / A 2.2x 怼脸特写)→ 6f 纯黑喘息
// → 正式开场大标题淡入+微升 16f 定格。每个速剪镜头内部带 1→1.04 轻推近。
// 收尾真静止:49f 后所有动画结束,静止 91f(>40f)。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, FakeDashboard } from '../../_fixtures/Fixtures';

// 时间轴(帧)
const CUT_1 = 0;   // 镜头1:variant A 整页
const CUT_2 = 9;   // 镜头2:variant B 整页
const CUT_3 = 18;  // 镜头3:variant A 2.2x 怼脸特写
const BLACK = 27;  // 纯黑静默
const TITLE = 33;  // 正式开场
const TITLE_IN = 16; // 标题淡入+微升时长

// 速剪镜头内部轻推近:镜头内 local 0→9f,scale 1→1.04(线性,活镜头感)
const push = (frame: number, start: number) =>
  interpolate(frame, [start, start + 9], [1, 1.04], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

export const TrailerBumper: React.FC = () => {
  const frame = useCurrentFrame();

  // —— 段落 1–3:三连速剪 ——
  if (frame < BLACK) {
    let inner: React.ReactNode;
    if (frame < CUT_2) {
      // 镜头1:A 整页 + 轻推近
      inner = (
        <div style={{ width: 1920, height: 1080, transform: `scale(${push(frame, CUT_1)})`, transformOrigin: '50% 50%' }}>
          <FakeDashboard variant="A" />
        </div>
      );
    } else if (frame < CUT_3) {
      // 镜头2:B 整页 + 轻推近
      inner = (
        <div style={{ width: 1920, height: 1080, transform: `scale(${push(frame, CUT_2)})`, transformOrigin: '50% 50%' }}>
          <FakeDashboard variant="B" />
        </div>
      );
    } else {
      // 镜头3:A 的 2.2x 放大裁切怼脸特写(对准中列卡片),叠加同样的轻推近
      const s = 2.2 * push(frame, CUT_3);
      inner = (
        <div style={{ width: 1920, height: 1080, transform: `scale(${s})`, transformOrigin: '58% 40%' }}>
          <FakeDashboard variant="A" />
        </div>
      );
    }
    return (
      <div style={{ width: 1920, height: 1080, overflow: 'hidden', background: G.bg }}>
        {inner}
      </div>
    );
  }

  // —— 段落 4:纯黑静默 6f,必须纯黑无物 ——
  if (frame < TITLE) {
    return <div style={{ width: 1920, height: 1080, background: '#000000' }} />;
  }

  // —— 段落 5:正式开场——浅底大标题淡入+微升 16f 后定格 ——
  const opacity = interpolate(frame, [TITLE, TITLE + TITLE_IN], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const rise = interpolate(frame, [TITLE, TITLE + TITLE_IN], [44, 0], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.cubic),
  });
  return (
    <div
      style={{
        width: 1920,
        height: 1080,
        background: G.bg,
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        overflow: 'hidden',
      }}
    >
      <div
        style={{
          opacity,
          transform: `translateY(${rise}px)`,
          fontFamily: 'Helvetica, Arial, sans-serif',
          fontWeight: 800,
          fontSize: 168,
          color: G.ink,
          letterSpacing: -2,
          textAlign: 'center',
        }}
      >
        THE LAUNCH
      </div>
    </div>
  );
};


  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/rhythm/trailer-grammar-moves/CardFootageCadence.tsx, demos/rhythm/trailer-grammar-moves/SmashCut.tsx, demos/rhythm/trailer-grammar-moves/TrailerBumper.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