Motion · Tipografia

Title demoted to section label

Title demoted to section label, two versions — A: a big centered title holds a beat then shrinks 0.3x and slides to the top-left as a small label, content growing beneath it; B: the same move with a text-selection highlight sweeping in and out on entry.

1920×108016:930 fps6.59s · 198 quadrosremotionLicença: Apache-2.0#shotcraft#remotion#typography#transition

Fonte: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composição Remotion (React/TSX) — vídeo de prévia, sem player no navegador

When to use

Chapter openers (the title plays lead, then hands off to the content); section handoffs in tutorial/demo films; style B adds an "editing/writing" product identity hint

Intention

A chapter title's usual fate is "appear → disappear → content enters" — the title and content never meet. This card lets the title demote rather than exit: a big centered word holds a beat to announce the topic, then a single continuous tween shrinks it to 0.3x and flies it to the top-left corner, settling as this section's column label — it's still on screen, just changed from protagonist to nameplate. The viewer gets free spatial memory: that big word from a moment ago is now that small word in the top-left corner, and this section is about it. The content skeleton starts growing while the demotion is still in progress, with zero handoff gap.

Duration & energy

  • Duration: A: ~3s (92f) / B: ~3.5s (104f); the two styles played back-to-back in a demo run 196f
  • Energy: Low-medium (a layout-change type, an atmosphere shot)

Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)

意图

章节标题的常规命运是"出现→消失→内容登场",标题和内容互不认识。 本卡让标题降格而不退场:居中大字站稳一拍宣告主题,然后一次连续 补间缩到 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

Reference implementation (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

The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.

Tudo nesta página também é legível por máquina. Aponte seu agente para o JSON ou adicione o servidor MCP do gimgs e peça para buscar templates de motion.

  1. Baixe o JSON — contém a receita do plano (timing, easing, tabela de parâmetros, armadilhas) e o código TSX completo.
  2. Clone o repositório video-shotcraft; o componente importa fixtures de demos/_fixtures e helpers de assets/lib.
  3. Registre o componente como <Composition> em um projeto Remotion e renderize com `npx remotion render`, ou reimplemente a receita no seu próprio engine.
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 "Title demoted to section label" (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.0Abrir template bruto ↗
// ===== 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
—

Mais nesta categoria