{
  "slug": "title-demote-to-label",
  "title": "Title demoted to section label",
  "tagline": "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.",
  "description": "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 gro",
  "category": "typography",
  "tags": [
    "shotcraft",
    "remotion",
    "typography",
    "transition"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 6.59,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/title-demote-to-label/poster-43ea748197d4.jpg",
    "video": "https://gimgs.net/motion/media/title-demote-to-label/video-e7dc7173ab84.mp4"
  },
  "variants": [
    {
      "key": "title-demote-to-label",
      "label": "title-demote-to-label",
      "description": "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",
      "use": "",
      "video": "https://gimgs.net/motion/media/title-demote-to-label/video-e7dc7173ab84.mp4",
      "poster": "https://gimgs.net/motion/media/title-demote-to-label/poster-43ea748197d4.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/title-demote-to-label.html",
  "json_url": "https://gimgs.net/motion/title-demote-to-label.json",
  "template_url": "https://gimgs.net/motion/title-demote-to-label/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/title-demote-to-label.md",
  "updated_at": "2026-09-24T07:40:28.842Z",
  "prompt": "## When to use\n\nChapter 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\n\n## Intention\n\nA 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.\n\n## Duration & energy\n\n- Duration: A: ~3s (92f) / B: ~3.5s (104f); the two styles played back-to-back in a demo run 196f\n- Energy: Low-medium (a layout-change type, an atmosphere shot)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n章节标题的常规命运是\"出现→消失→内容登场\"，标题和内容互不认识。\n本卡让标题**降格而不退场**：居中大字站稳一拍宣告主题，然后一次连续\n补间缩到 0.3x 飞到左上角，落成这一节的栏目标签——它还在屏幕上，\n只是从主角变成了门牌。观众获得免费的空间记忆：刚才那个大词就是\n现在左上角那个小词，本节讲的就是它。内容骨架在降格进行中就开始\n生长，交接零空档。\n\n## 动效核心\n- 降格是**单次连续补间**，Easing.inOut(cubic) 20f：scale 1→0.3 +\n  位置 (960,480)→(150,110) 同一条曲线——分两段（先缩后飞）读作两个动作\n- transform-origin 左中；居中修正量 translate(-50%) 随补间同步归零\n  （`translate(${-(1-dem)*50}%, -50%)`）——不归零终点会偏一个半宽\n- 登场显影：12f blur 12→0 + 淡入，Easing.out(cubic)；显影后必须\n  站稳 ≥18f 再降格——没站稳就缩，宣告拍被吃掉\n- 骨架内容错峰生长：每块宽度 0.35→1 + 上移 28px 淡入，块间 t 错峰\n  0.16；生长起点在降格进行到 12f 时（GROW = DEMOTE+12），标题还在\n  飞内容已在长——这个重叠是\"交接无空档\"的来源\n- B 式选中高亮：左缘/宽度两个百分比分别补间——扫入时宽度 0→100%\n  （盖住），撤掉时左缘 0→100%（从左让开）；高亮撤净后再起降格\n- 两式串播用 4f 白闪转场\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 降格补间 | 20f inOut(cubic)，scale→0.3 | <14f 读作标题被弹走；终点 >0.45 标签太大压内容 |\n| 站稳拍 | 显影后 ≥18f 静置 | 省掉这拍，观众没读完标题它就跑了，降格白做 |\n| 骨架错峰 | 块间 0.16t，宽 0.35→1 | 全部同时长出读作页面刷新而非\"生长\" |\n| 生长起点 | 降格开始后 +12f | 等标题完全落位才生长会空 8f 冷场 |\n| 选中高亮 | 扫入 10f / hold ~8f / 撤 8f，35% 透明蓝 | 高亮 hold <5f 读作闪烁 bug；不撤干净就降格，蓝块跟着飞很脏 |\n| 白闪转场 | 4f opacity 1→0 | 仅两式串播用；>6f 读作曝光事故 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 与\"标题钉进 3D 空间随相机透视变形\"的场景锁定拍法（已退役）分工\n  （空间型），本卡是 2D 版式层级的连续降格（版式型）——要\"标题住进\n  产品空间\"用那卡，要\"标题变栏目名\"用本卡，同片选一\n- 降格路径上不许有其他元素动——标题飞行是全屏唯一焦点，骨架生长\n  在它落点下方不抢（起点错开），但弹窗/光标类同帧入场必抢\n- 多节连用（每节一次降格）时左上角标签要**换行不叠放**：新标签落位\n  旧标签上移让位，否则第二节起左上角变成标签坟场\n- B 式的选中高亮是\"文字可被选中\"的产品暗示，非编辑器/文档类产品\n  用了语义空转，别为了好看加\n- 标题文案 ≤3 个词——降格到 0.3x 后还要当可读标签，长句缩完就是一条灰线\n\n## 参考实现\ndemos/typography/title-demote-to-label/\n（TitleDemoteToLabel.tsx）\n原片出处：perplexity-promo 16–18.5s；B 式变体源自 framer text-selection-title\n\n## Reference implementation (Remotion)\n\n- demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "npx remotion render <CompositionId> out.mp4   # component: demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 198,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx =====\n// title-demote-to-label —— 大标题降格为节标签\n// 源：perplexity-promo 16–18.5s。大标题居中显影站稳一拍，随后缩小 ~0.3x\n// 平移到左上角变小节标签，内容区（灰阶骨架块）在其下方生长。\n// 附加变体（framer text-selection-title）：标题登场带文本选中蓝高亮块、随后撤掉。\nimport React from 'react';\nimport { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G } from '../../_fixtures/Fixtures';\n\nconst FONT = 'Helvetica, Arial, sans-serif';\nconst SEL = 'rgba(58, 128, 236, 0.35)';\n\n// 内容骨架块：随 t 依次生长\nconst Skeleton: React.FC<{ t: number }> = ({ t }) => {\n  const blocks = [\n    { w: 1500, h: 26 },\n    { w: 1280, h: 26 },\n    { w: 1420, h: 26 },\n    { w: 760, h: 26 },\n    { w: 1500, h: 300, card: true },\n  ];\n  return (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: 34 }}>\n      {blocks.map((b, i) => {\n        const bt = interpolate(t, [i * 0.16, i * 0.16 + 0.3], [0, 1], {\n          extrapolateLeft: 'clamp',\n          extrapolateRight: 'clamp',\n          easing: Easing.out(Easing.cubic),\n        });\n        return (\n          <div\n            key={i}\n            style={{\n              width: b.w * (0.35 + 0.65 * bt),\n              height: b.h,\n              background: b.card ? G.card : G.line,\n              border: b.card ? `2px solid ${G.border}` : 'none',\n              borderRadius: b.card ? 16 : 13,\n              opacity: bt,\n              transform: `translateY(${(1 - bt) * 28}px)`,\n              boxSizing: 'border-box',\n            }}\n          />\n        );\n      })}\n    </div>\n  );\n};\n\n// 一个完整的\"显影→(可选高亮)→降格→内容生长\"小节\nconst DemoteScene: React.FC<{\n  frame: number;\n  title: string;\n  withSelection: boolean;\n}> = ({ frame, title, withSelection }) => {\n  // 时间轴（局部帧）\n  const REVEAL = 0; // 0–12 显影\n  const SEL_ON = 14; // 高亮扫入 14–24\n  const SEL_OFF = 32; // 高亮撤掉 32–40\n  const DEMOTE = withSelection ? 44 : 32; // 降格开始\n  const DEMOTE_END = DEMOTE + 20;\n  const GROW = DEMOTE + 12;\n\n  // 显影：blur + 淡入\n  const rev = interpolate(frame, [REVEAL, REVEAL + 12], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n\n  // 降格补间：scale 1 -> 0.3，中心 -> 左上\n  const dem = interpolate(frame, [DEMOTE, DEMOTE_END], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.inOut(Easing.cubic),\n  });\n  const scale = interpolate(dem, [0, 1], [1, 0.3]);\n  // 用 left/top 补间：起点居中（由外层 flex 定位换算），终点左上角\n  const x = interpolate(dem, [0, 1], [960, 150]);\n  const y = interpolate(dem, [0, 1], [480, 110]);\n\n  // 高亮块：从左扫入盖住文字，再从左撤掉\n  let selLeft = 0;\n  let selWidth = 0;\n  if (withSelection) {\n    const on = interpolate(frame, [SEL_ON, SEL_ON + 10], [0, 1], {\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n      easing: Easing.out(Easing.quad),\n    });\n    const off = interpolate(frame, [SEL_OFF, SEL_OFF + 8], [0, 1], {\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n      easing: Easing.in(Easing.quad),\n    });\n    selLeft = off * 100;\n    selWidth = Math.max(0, on * 100 - selLeft);\n  }\n\n  const growT = interpolate(frame, [GROW, GROW + 40], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <AbsoluteFill style={{ background: G.bg }}>\n      {/* 内容骨架区 */}\n      <div style={{ position: 'absolute', left: 150, top: 210 }}>\n        <Skeleton t={growT} />\n      </div>\n      {/* 标题：transform-origin 左中，位置补间 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: x,\n          top: y,\n          transform: `translate(${-(1 - dem) * 50}%, -50%) scale(${scale})`,\n          transformOrigin: 'left center',\n          opacity: rev,\n          filter: `blur(${(1 - rev) * 12}px)`,\n        }}\n      >\n        <div\n          style={{\n            position: 'relative',\n            fontFamily: FONT,\n            fontWeight: 800,\n            fontSize: 128,\n            color: G.ink,\n            letterSpacing: -2,\n            whiteSpace: 'nowrap',\n            padding: '10px 18px',\n          }}\n        >\n          {withSelection && selWidth > 0 && (\n            <div\n              style={{\n                position: 'absolute',\n                left: `${selLeft}%`,\n                top: 8,\n                width: `${selWidth}%`,\n                height: 'calc(100% - 16px)',\n                background: SEL,\n                borderRadius: 6,\n              }}\n            />\n          )}\n          <span style={{ position: 'relative' }}>{title}</span>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nexport const TitleDemoteToLabel: React.FC = () => {\n  const frame = useCurrentFrame();\n  const SPLIT = 92; // 变体 A 时长\n\n  if (frame < SPLIT) {\n    return <DemoteScene frame={frame} title=\"Running Subagents\" withSelection={false} />;\n  }\n  // 变体 B：文本选中态高亮登场\n  const f = frame - SPLIT;\n  // 白闪转场 3f\n  const flash = interpolate(f, [0, 4], [1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  return (\n    <AbsoluteFill>\n      <DemoteScene frame={f} title=\"Select the Answer\" withSelection={true} />\n      <AbsoluteFill style={{ background: '#fff', opacity: flash, pointerEvents: 'none' }} />\n    </AbsoluteFill>\n  );\n};\n\n\n"
}