Motion · Chữ & title card

Tiêu đề giáng cấp thành nhãn mục

Tiêu đề giáng cấp thành nhãn mục, hai kiểu — A: tiêu đề lớn giữa màn hiện lên đứng một nhịp rồi thu nhỏ 0.3x trượt lên góc trên-trái thành nhãn tiểu mục, nội dung mọc lên bên dưới; B: cùng thủ pháp nhưng kèm khối tô sáng kiểu bôi đen văn bản quét vào rồi rút đi.

1920×108016:930 fps6.59s · 198 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#typography#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

Khi nào dùng

Mở chương (tiêu đề đóng vai chính rồi nhường chỗ cho nội dung); chuyển tiểu mục trong phim hướng dẫn/demo tính năng; kiểu B gợi thêm nhận diện sản phẩm loại "chữ/biên tập"

Ý đồ

Số phận thường thấy của tiêu đề chương là "xuất hiện → biến mất → nội dung vào" — tiêu đề và nội dung không hề quen biết nhau. Thẻ này để tiêu đề giáng cấp chứ không rời sân khấu: chữ lớn giữa màn đứng một nhịp tuyên bố chủ đề, sau đó một lần tween liên tục thu về 0.3x bay lên góc trên-trái, đậu thành nhãn cột của tiểu mục này — nó vẫn còn trên màn hình, chỉ là từ vai chính thành biển tên. Khán giả có được trí nhớ không gian miễn phí: từ lớn vừa rồi giờ chính là từ nhỏ ở góc trên-trái, tiểu mục này đang nói về nó. Khung nội dung bắt đầu mọc lên ngay khi việc giáng cấp còn đang diễn ra, bàn giao không có khoảng trống.

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

  • Thời lượng: A khoảng 3s (92f) / B khoảng 3.5s (104f); demo phát nối tiếp hai kiểu 196f
  • Năng lượng: Thấp trung bình (kiểu biến đổi bố cục, cảnh không khí)

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)

意图

章节标题的常规命运是"出现→消失→内容登场",标题和内容互不认识。 本卡让标题降格而不退场:居中大字站稳一拍宣告主题,然后一次连续 补间缩到 0.3x 飞到左上角,落成这一节的栏目标签——它还在屏幕上, 只是从主角变成了门牌。观众获得免费的空间记忆:刚才那个大词就是 现在左上角那个小词,本节讲的就是它。内容骨架在降格进行中就开始 生长,交接零空档。

动效核心

  • 降格是单次连续补间,Easing.inOut(cubic) 20f:scale 1→0.3 +

位置 (960,480)→(150,110) 同一条曲线——分两段(先缩后飞)读作两个动作

  • transform-origin 左中;居中修正量 translate(-50%) 随补间同步归零

(translate(${-(1-dem)*50}%, -50%))——不归零终点会偏一个半宽

  • 登场显影:12f blur 12→0 + 淡入,Easing.out(cubic);显影后必须

站稳 ≥18f 再降格——没站稳就缩,宣告拍被吃掉

  • 骨架内容错峰生长:每块宽度 0.35→1 + 上移 28px 淡入,块间 t 错峰

0.16;生长起点在降格进行到 12f 时(GROW = DEMOTE+12),标题还在 飞内容已在长——这个重叠是"交接无空档"的来源

  • B 式选中高亮:左缘/宽度两个百分比分别补间——扫入时宽度 0→100%

(盖住),撤掉时左缘 0→100%(从左让开);高亮撤净后再起降格

  • 两式串播用 4f 白闪转场

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 降格补间 | 20f inOut(cubic),scale→0.3 | <14f 读作标题被弹走;终点 >0.45 标签太大压内容 | | 站稳拍 | 显影后 ≥18f 静置 | 省掉这拍,观众没读完标题它就跑了,降格白做 | | 骨架错峰 | 块间 0.16t,宽 0.35→1 | 全部同时长出读作页面刷新而非"生长" | | 生长起点 | 降格开始后 +12f | 等标题完全落位才生长会空 8f 冷场 | | 选中高亮 | 扫入 10f / hold ~8f / 撤 8f,35% 透明蓝 | 高亮 hold <5f 读作闪烁 bug;不撤干净就降格,蓝块跟着飞很脏 | | 白闪转场 | 4f opacity 1→0 | 仅两式串播用;>6f 读作曝光事故 |

已知坑

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

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

  • 与"标题钉进 3D 空间随相机透视变形"的场景锁定拍法(已退役)分工

(空间型),本卡是 2D 版式层级的连续降格(版式型)——要"标题住进 产品空间"用那卡,要"标题变栏目名"用本卡,同片选一

  • 降格路径上不许有其他元素动——标题飞行是全屏唯一焦点,骨架生长

在它落点下方不抢(起点错开),但弹窗/光标类同帧入场必抢

  • 多节连用(每节一次降格)时左上角标签要换行不叠放:新标签落位

旧标签上移让位,否则第二节起左上角变成标签坟场

  • B 式的选中高亮是"文字可被选中"的产品暗示,非编辑器/文档类产品

用了语义空转,别为了好看加

  • 标题文案 ≤3 个词——降格到 0.3x 后还要当可读标签,长句缩完就是一条灰线

参考实现

demos/typography/title-demote-to-label/ (TitleDemoteToLabel.tsx) 原片出处:perplexity-promo 16–18.5s;B 式变体源自 framer text-selection-title

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

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

Prompt

Use the video-shotcraft shot recipe "Tiêu đề giáng cấp thành nhãn mụ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/title-demote-to-label.json
Raw TSX: https://gimgs.net/motion/title-demote-to-label/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 6.59s

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,169 chars · Apache-2.0Mở template gốc ↗
// ===== demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx =====
// title-demote-to-label —— 大标题降格为节标签
// 源:perplexity-promo 16–18.5s。大标题居中显影站稳一拍,随后缩小 ~0.3x
// 平移到左上角变小节标签,内容区(灰阶骨架块)在其下方生长。
// 附加变体(framer text-selection-title):标题登场带文本选中蓝高亮块、随后撤掉。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';
import { G } from '../../_fixtures/Fixtures';

const FONT = 'Helvetica, Arial, sans-serif';
const SEL = 'rgba(58, 128, 236, 0.35)';

// 内容骨架块:随 t 依次生长
const Skeleton: React.FC<{ t: number }> = ({ t }) => {
  const blocks = [
    { w: 1500, h: 26 },
    { w: 1280, h: 26 },
    { w: 1420, h: 26 },
    { w: 760, h: 26 },
    { w: 1500, h: 300, card: true },
  ];
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 34 }}>
      {blocks.map((b, i) => {
        const bt = interpolate(t, [i * 0.16, i * 0.16 + 0.3], [0, 1], {
          extrapolateLeft: 'clamp',
          extrapolateRight: 'clamp',
          easing: Easing.out(Easing.cubic),
        });
        return (
          <div
            key={i}
            style={{
              width: b.w * (0.35 + 0.65 * bt),
              height: b.h,
              background: b.card ? G.card : G.line,
              border: b.card ? `2px solid ${G.border}` : 'none',
              borderRadius: b.card ? 16 : 13,
              opacity: bt,
              transform: `translateY(${(1 - bt) * 28}px)`,
              boxSizing: 'border-box',
            }}
          />
        );
      })}
    </div>
  );
};

// 一个完整的"显影→(可选高亮)→降格→内容生长"小节
const DemoteScene: React.FC<{
  frame: number;
  title: string;
  withSelection: boolean;
}> = ({ frame, title, withSelection }) => {
  // 时间轴(局部帧)
  const REVEAL = 0; // 0–12 显影
  const SEL_ON = 14; // 高亮扫入 14–24
  const SEL_OFF = 32; // 高亮撤掉 32–40
  const DEMOTE = withSelection ? 44 : 32; // 降格开始
  const DEMOTE_END = DEMOTE + 20;
  const GROW = DEMOTE + 12;

  // 显影:blur + 淡入
  const rev = interpolate(frame, [REVEAL, REVEAL + 12], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.cubic),
  });

  // 降格补间:scale 1 -> 0.3,中心 -> 左上
  const dem = interpolate(frame, [DEMOTE, DEMOTE_END], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.inOut(Easing.cubic),
  });
  const scale = interpolate(dem, [0, 1], [1, 0.3]);
  // 用 left/top 补间:起点居中(由外层 flex 定位换算),终点左上角
  const x = interpolate(dem, [0, 1], [960, 150]);
  const y = interpolate(dem, [0, 1], [480, 110]);

  // 高亮块:从左扫入盖住文字,再从左撤掉
  let selLeft = 0;
  let selWidth = 0;
  if (withSelection) {
    const on = interpolate(frame, [SEL_ON, SEL_ON + 10], [0, 1], {
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
      easing: Easing.out(Easing.quad),
    });
    const off = interpolate(frame, [SEL_OFF, SEL_OFF + 8], [0, 1], {
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
      easing: Easing.in(Easing.quad),
    });
    selLeft = off * 100;
    selWidth = Math.max(0, on * 100 - selLeft);
  }

  const growT = interpolate(frame, [GROW, GROW + 40], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  return (
    <AbsoluteFill style={{ background: G.bg }}>
      {/* 内容骨架区 */}
      <div style={{ position: 'absolute', left: 150, top: 210 }}>
        <Skeleton t={growT} />
      </div>
      {/* 标题:transform-origin 左中,位置补间 */}
      <div
        style={{
          position: 'absolute',
          left: x,
          top: y,
          transform: `translate(${-(1 - dem) * 50}%, -50%) scale(${scale})`,
          transformOrigin: 'left center',
          opacity: rev,
          filter: `blur(${(1 - rev) * 12}px)`,
        }}
      >
        <div
          style={{
            position: 'relative',
            fontFamily: FONT,
            fontWeight: 800,
            fontSize: 128,
            color: G.ink,
            letterSpacing: -2,
            whiteSpace: 'nowrap',
            padding: '10px 18px',
          }}
        >
          {withSelection && selWidth > 0 && (
            <div
              style={{
                position: 'absolute',
                left: `${selLeft}%`,
                top: 8,
                width: `${selWidth}%`,
                height: 'calc(100% - 16px)',
                background: SEL,
                borderRadius: 6,
              }}
            />
          )}
          <span style={{ position: 'relative' }}>{title}</span>
        </div>
      </div>
    </AbsoluteFill>
  );
};

export const TitleDemoteToLabel: React.FC = () => {
  const frame = useCurrentFrame();
  const SPLIT = 92; // 变体 A 时长

  if (frame < SPLIT) {
    return <DemoteScene frame={frame} title="Running Subagents" withSelection={false} />;
  }
  // 变体 B:文本选中态高亮登场
  const f = frame - SPLIT;
  // 白闪转场 3f
  const flash = interpolate(f, [0, 4], [1, 0], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  return (
    <AbsoluteFill>
      <DemoteScene frame={f} title="Select the Answer" withSelection={true} />
      <AbsoluteFill style={{ background: '#fff', opacity: flash, pointerEvents: 'none' }} />
    </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/typography/title-demote-to-label/TitleDemoteToLabel.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