Motion · Chuyển cảnh

Chuyển cảnh quét hình học

Hai kiểu chuyển cảnh quét hình học — clock-wipe kim đồng hồ radar quét đổi trang và blinds-slice 12 dải dọc lật lệch nhịp thành sóng.

1920×108016:930 fps5.06s · 152 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#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ể

clock-wipe
Trang B nằm dưới lớp clip-path polygon hình quạt, kim đồng hồ quét từ tâm màn hình theo chiều kim đồng hồ đều 360°, quét tới đâu lộ B tới đó; dải quét mang nhiều lớp vệt sáng
Ngữ nghĩa làm mới dữ liệu/trạng thái; trang kiểu bảng điều khiển
blinds-slice
12 dải dọc rộng 160px overflow hidden + lớp trong cả trang lệch margin âm để căn khớp; trong mỗi dải A co lại mép trái bằng scaleX(1-p), B mở rộng mép phải bằng scaleX(p), lệch nhịp delay thành sóng, vệt sáng chạy theo đường nối theo sóng
Ngữ nghĩa lật trang/tiến tới; trang có luồng đọc theo chiều ngang

Khi nào dùng

Chuyển cảnh phổ dụng nơi cả trang cũ lẫn trang mới đều không động, một biên giới hình học quét qua hoàn tất bàn giao; không phụ thuộc có phần tử phù hợp trong bố cục, chỗ nào cũng dùng được

Ý đồ

Thư viện chuyển cảnh đã có ba dòng: dòng xuyên qua (khoan vào), dòng giấu điểm cắt (che đi), dòng khối thực thể (trang là vật thể). Thẻ này là dòng thứ tư quét hình học — cả trang cũ lẫn trang mới đều không động, một biên giới hình học quét qua hoàn tất bàn giao, hình dạng của cú quét chính là ngữ nghĩa: A quét tròn là "bảng điều khiển làm mới một màn dữ liệu", B quét dải là "rèm lật từng lá đổi trang". Khác với chuyển cảnh quét mặt nạ phần tử ở kiểu F của shot-transitions: F dùng phần tử thực trong trang làm mặt nạ (phụ thuộc bố cục), thẻ này là hình học thuần túy — tính phổ dụng chính là định vị của nó. Chọn theo ngữ nghĩa: làm mới dữ liệu dùng A, lật trang tiến tới theo chiều ngang dùng B.

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

  • Thời lượng: Mỗi kiểu trạng thái trước ≥20f + quét 32–60f + đuôi ≥40f, khoảng 5s (150f)
  • Năng lượng: Trung bì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)

意图

转场库已有三族:穿越系(钻进去)、藏切系(遮住切)、体块系(页面是 实体)。本卡是第四族几何擦除系——新旧页都不动,一条几何边界扫过 完成交接,擦除的形状即语义:A 圆扫是"仪表盘刷新了一屏数据",B 条扫 是"百叶窗逐叶翻面换页"。与 shot-transitions F 元素遮罩擦除的区别: F 用页面内真实元素当遮罩(依赖构图),本卡是纯几何形——通用性即定位。 按语义选:数据刷新用 A,横向推进翻页用 B。

两式选型

| 式 | 做法 | 适用 | |----|------|------| | A clock-wipe | B 页上层套扇形 clip-path polygon,指针从屏心 12 点顺时针匀速扫 360°,扫过处露 B;扫描沿带多层亮线 | 数据/状态刷新语义;仪表盘类页面 | | B blinds-slice | 12 根 160px 竖条 overflow hidden + 内层整页负 margin 对位;条内 A scaleX(1-p) 左缘收缩、B scaleX(p) 右缘展开,错峰 delay 成波,缝上亮线随波扫 | 翻页/推进语义;横向阅读动线页面 |

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A 扇形 | 中心屏心(960,540)、半径 1400 盖满四角,固定 73 顶点(SEGS=72) | 顶点数恒定且够密(40+)防锯齿跳变——命门 | | A 扫速 | 30–90f 指针 0→360° 纯 linear,双向 clamp | 雷达要匀速;加缓动就不是雷达是钟摆 | | A 亮线 | 四层 SVG line:26px 白 0.35 柔光 + 13px 白 0.60 + 9px 黑 0.55 暗描边 + 4px 白核 | 白底判例:纯提亮不可见,必须加暗描边;首渲 3px 白线太弱,加码 1.5x 才过 | | B 波 | 20–52f:delay=列号×2f,每条 10f Easing.in(cubic) | 交接点恒为 x+160(1-p),数学上无露底 | | B 缝亮线 | 三层 SVG:16px 白 0.45 柔光 + 6px 黑 0.55 暗描边 + 3px 白核,进出各 2f 淡入淡出 | 同 A 白底判例 | | 摘罩 | A 96f / B 52f 起条件卸载全部擦除结构,B 页直出 | opacity 0 不算摘,残留 clip-path 毁真静止 | | 收尾 | A 54f / B 98f 真静止(≥40f) | R1 |

已知坑

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

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

  • 擦除边界必须带亮线/高光——无亮线的 wipe 读作 PPT 转场,命门;

亮线在浅色区靠黑描边、深色区靠白核,两侧都要可读

  • A 与 circle-match-iris 的区别:iris 是从锚点炸开的圆(半径在长),

clock 是角度在扫(半径恒定)——别因为都是圆混用

  • 几何擦除全片 ≤2 次,且 A/B 别同片连用(两次"边界扫过"读作模板感)
  • 亮线淡出与摘罩必须同帧衔接(A 90–96f 淡出、96f 卸载),

差一帧就是残线穿帮

参考实现

demos/transition/wipe-transitions/ (BlindsSlice.tsx / ClockWipe.tsx)

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

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

Prompt

Use the video-shotcraft shot recipe "Chuyển cảnh quét hình học" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/wipe-transitions.json
Raw TSX: https://gimgs.net/motion/wipe-transitions/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/transition/wipe-transitions/BlindsSlice.tsx, demos/transition/wipe-transitions/ClockWipe.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.06s

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

6,733 chars · Apache-2.0Mở template gốc ↗
// ===== demos/transition/wipe-transitions/BlindsSlice.tsx =====
// blinds-slice|百叶窗切条错峰擦除
// FakeDashboard A → B。12 根 160px 竖条,从左到右 delay=列号×2f,
// 每条 10f 内完成翻换:条内 A scaleX 1→0(origin 左缘)与 B scaleX 0→1
// (origin 右缘)共用同一进度 p(Easing.in(cubic)),交接缝恒等于
// x+160(1-p),数学上无露底。缝上亮线(柔光+暗描边+白核)随波扫过。
// 波 20–52f;52f 起摘罩(整页 B 直出、条结构与亮线全部卸载),
// 52–150f 真静止 98f ≥ 40f。帧确定,无随机源。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';
import { FakeDashboard } from '../../_fixtures/Fixtures';

const STRIPS = 12;
const W = 160; // 每条宽 12×160 = 1920
const WAVE_START = 20;
const STAGGER = 2; // 列号 × 2f
const FLIP = 10; // 每条 10f 完成翻换
const WAVE_END = WAVE_START + (STRIPS - 1) * STAGGER + FLIP; // 52

// 条内某页的切片:外层 160 宽裁剪,内层整页 1920 负 margin 对位
const Slice: React.FC<{ x: number; variant: 'A' | 'B' }> = ({ x, variant }) => (
  <div style={{ width: 1920, height: 1080, marginLeft: -x }}>
    <FakeDashboard variant={variant} />
  </div>
);

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

  // 摘罩:波完成后条结构全部卸载,B 整页直出
  if (frame >= WAVE_END) {
    return (
      <AbsoluteFill style={{ background: '#ececea' }}>
        <FakeDashboard variant="B" />
      </AbsoluteFill>
    );
  }

  const seams: { x: number; opacity: number }[] = [];

  const strips = Array.from({ length: STRIPS }).map((_, i) => {
    const x = i * W;
    const start = WAVE_START + i * STAGGER;
    const end = start + FLIP;

    // 未开始:纯 A 切片;已完成:纯 B 切片
    if (frame < start) {
      return (
        <div key={i} style={{ position: 'absolute', left: x, top: 0, width: W, height: 1080, overflow: 'hidden' }}>
          <Slice x={x} variant="A" />
        </div>
      );
    }
    if (frame >= end) {
      return (
        <div key={i} style={{ position: 'absolute', left: x, top: 0, width: W, height: 1080, overflow: 'hidden' }}>
          <Slice x={x} variant="B" />
        </div>
      );
    }

    // 翻换中:A、B 共用同一进度 p——A 宽 160(1-p) 靠左,B 宽 160p 靠右,
    // 交接点恒为 x+160(1-p),无露底
    const p = interpolate(frame, [start, end], [0, 1], {
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
      easing: Easing.in(Easing.cubic),
    });

    // 缝亮线:进出各 2f 线性淡入淡出
    const seamOpacity = Math.min(
      interpolate(frame, [start, start + 2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
      interpolate(frame, [end - 2, end], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
    );
    seams.push({ x: x + W * (1 - p), opacity: seamOpacity });

    return (
      <div key={i} style={{ position: 'absolute', left: x, top: 0, width: W, height: 1080, overflow: 'hidden' }}>
        {/* A:向左缘收缩 */}
        <div style={{ position: 'absolute', inset: 0, overflow: 'hidden', transform: `scaleX(${1 - p})`, transformOrigin: '0% 50%' }}>
          <Slice x={x} variant="A" />
        </div>
        {/* B:从右缘展开 */}
        <div style={{ position: 'absolute', inset: 0, overflow: 'hidden', transform: `scaleX(${p})`, transformOrigin: '100% 50%' }}>
          <Slice x={x} variant="B" />
        </div>
      </div>
    );
  });

  return (
    <AbsoluteFill style={{ background: '#ececea' }}>
      {strips}
      {/* 缝亮线:白底判例——纯提亮不可见,柔光 + 暗描边 + 白核三层 */}
      {seams.length > 0 && (
        <svg width={1920} height={1080} style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
          {seams.map((s, i) => (
            <g key={i} opacity={s.opacity}>
              <line x1={s.x} y1={0} x2={s.x} y2={1080} stroke="rgba(255,255,255,0.45)" strokeWidth={16} />
              <line x1={s.x} y1={0} x2={s.x} y2={1080} stroke="rgba(0,0,0,0.55)" strokeWidth={6} />
              <line x1={s.x} y1={0} x2={s.x} y2={1080} stroke="rgba(255,255,255,0.95)" strokeWidth={3} />
            </g>
          ))}
        </svg>
      )}
    </AbsoluteFill>
  );
};


// ===== demos/transition/wipe-transitions/ClockWipe.tsx =====
// clock-wipe|时钟扫描擦除
// FakeDashboard A → B。30–90f 一根隐形雷达指针从 12 点方向顺时针扫一圈,
// B 页在上层用大扇形 clip-path polygon 逐帧张开;扫描沿带亮线(白核+暗描边+柔光)。
// 90–96f 亮线淡出,96f 起摘罩(B 直接满屏、无 clip-path、亮线卸载),
// 96–150f 真静止 54f ≥ 40f。帧确定,无随机。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';
import { FakeDashboard } from '../../_fixtures/Fixtures';

const CX = 960;
const CY = 540;
const R = 1400; // 大于中心到角的距离 ~1101,扇形完全盖角
const SEGS = 72; // 顶点数固定且够密,避免锯齿跳变

// 12 点方向为 0°,顺时针(屏幕坐标 y 向下)
const polar = (deg: number, r: number): [number, number] => {
  const a = (deg * Math.PI) / 180;
  return [CX + r * Math.sin(a), CY - r * Math.cos(a)];
};

const fanClip = (theta: number): string => {
  const pts: string[] = [`${CX}px ${CY}px`];
  for (let i = 0; i <= SEGS; i++) {
    const [x, y] = polar((theta * i) / SEGS, R);
    pts.push(`${x.toFixed(1)}px ${y.toFixed(1)}px`);
  }
  return `polygon(${pts.join(', ')})`;
};

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

  // 30–90f 指针 0→360°,linear(时钟扫描要匀速才像雷达)
  const theta = interpolate(frame, [30, 90], [0, 360], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const wipeDone = frame >= 90;

  // 亮线:扫描期间常亮,90–96f 线性淡出,96f 起条件卸载(摘罩判例)
  const lineOpacity = interpolate(frame, [90, 96], [1, 0], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const lineMounted = frame >= 30 && frame < 96;

  const [x2, y2] = polar(theta, R);

  return (
    <AbsoluteFill style={{ background: '#ececea' }}>
      {/* 底层:A 页。擦完后卸载(上层 B 已满屏) */}
      {!wipeDone && (
        <AbsoluteFill>
          <FakeDashboard variant="A" />
        </AbsoluteFill>
      )}

      {/* 上层:B 页。扫描期挂扇形 clip-path,擦完后摘罩直出 */}
      {frame >= 30 && (
        <AbsoluteFill style={wipeDone ? undefined : { clipPath: fanClip(theta) }}>
          <FakeDashboard variant="B" />
        </AbsoluteFill>
      )}

      {/* 扫描亮线:柔光 + 暗描边 + 白核,从屏心指向当前角度 */}
      {lineMounted && (
        <svg
          width={1920}
          height={1080}
          style={{ position: 'absolute', inset: 0, opacity: lineOpacity, pointerEvents: 'none' }}
        >
          {/* 柔光带(QA 后加码 1.5x:白底看不清就加深+加宽) */}
          <line x1={CX} y1={CY} x2={x2} y2={y2} stroke="rgba(255,255,255,0.35)" strokeWidth={26} strokeLinecap="round" />
          <line x1={CX} y1={CY} x2={x2} y2={y2} stroke="rgba(255,255,255,0.60)" strokeWidth={13} strokeLinecap="round" />
          {/* 暗描边:白底上"提亮"不可见,加深保证浅色区也读得出指针 */}
          <line x1={CX} y1={CY} x2={x2} y2={y2} stroke="rgba(0,0,0,0.55)" strokeWidth={9} strokeLinecap="round" />
          {/* 白核 */}
          <line x1={CX} y1={CY} x2={x2} y2={y2} stroke="rgba(255,255,255,0.95)" strokeWidth={4} strokeLinecap="round" />
        </svg>
      )}
    </AbsoluteFill>
  );
};


  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/transition/wipe-transitions/BlindsSlice.tsx, demos/transition/wipe-transitions/ClockWipe.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