Motion · Tipografia

Pill slot cycle

A fixed sentence stem stays locked while a pill badge at the end slot-machine-rolls a new word roughly every 0.7s.

1920×108016:930 fps5.89s · 177 quadrosremotionLicença: Apache-2.0#shotcraft#remotion#typography

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

The most elegant solution for "feature listing" copy (faster than a bullet list, more semantic than scramble decoding); sections with a one-line value prop plus multiple verb phrases

Intention

Listing six features would normally need six lines, or six beats of one-by-one fade-ins that fill the whole screen. Word-slot cycling packs them into a single slot in a single sentence: the stem "One AI tool to ___" is the promise, each pill rolling through the slot is a piece of evidence, and when the final pill flies off and "do it all." lands, the evidence is complete and the conclusion is sealed. The structure carries a built-in three-act shape: promise → enumeration → close. The critical rule is that the stem never moves an inch — the viewer's eyes stay locked on the slot the whole time, and any wobble in the stem breaks the sense of enumeration.

Duration & energy

  • Duration: Entrance 12f + 21f per beat × word count + close 14f + hold; 6 words ~5.8s (demo 175f)
  • Energy: Medium (a steady metronome, no peak)

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

意图

罗列六个功能,列表要六行、逐条淡入要六拍还占满屏。词槽轮换把它们 塞进一句话的一个槽位里:句干 "One AI tool to ___" 是承诺,槽里 滚过的每个 pill 是证据,最后 pill 飞走、"do it all." 落位——证据 列举完毕,结论盖章。结构自带三幕:承诺→列举→收束。命门是句干 纹丝不动:观众的眼睛全程钉在槽位上,句干一晃列举感就散。

动效核心

  • 句干绝对定位左端锚死——不能用 flex 居中,pill 宽度变化会带着

句干重排(首轮踩过的坑,已是判例)

  • 每拍 21f(~0.7s),前 8f 完成换位、后 13f 静置——静置段是给观众

读词的,读不完的拍子等于没换

  • 入场:translateY 120→0 + blur 14→4→0,Easing.out(cubic);

离场:translateY 0→-130 + blur 0→10,Easing.in(cubic) 加速飞出 ——新来的减速落定、旧的加速逃走,方向感是"向上翻页"

  • 槽内用隐形占位 pill(visibility hidden)撑宽度,新旧两枚绝对定位

叠放——槽宽随当前词自然变化但不抖

  • 收束拍:末 pill 上飞(7f ease-in),"do it all." 从下 90px 带

Easing.back(1.4) 过冲落位——全片唯一一次过冲,留给结论

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 拍长 | 21f/词,5–7 词 | <16f 读不完短语;>28f 节拍器感散掉;8+ 词观众开始数数 | | 换位窗 | 拍内前 8f | 拉长到 12f+ 新旧同屏太久,读作两个 pill 打架 | | 飞行距离 | 入 +120 / 出 -130px | <80px 读作原地闪换;入出不对称(出略远)让离场更决绝 | | 运动模糊 | 入 14→0 / 出 0→10px blur | 去掉 blur 换位读作硬弹;>20px 中间帧糊成色块 | | 收束过冲 | Easing.back(1.4),14f | >2 结论落位像卡通;无过冲结论与列举没有档次差 | | 收尾 hold | 完整句静置 ≥45f | 这句话是全段目的,不 hold 等于白列举 |

已知坑

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

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

  • 与 odometer-digit-roll / split-flap-title 分工:那两卡是字符/数位级

机械滚动(无语义),本卡是整词级 pill 轮换且嵌在活句子里——文案是 "句干+动词短语"结构才用本卡,纯数字/纯标题词别硬套

  • 与 typewriter-moves 分工:打字机是"一个字一个字造句",本卡是

"句子造好了换零件"——同片可共存但别在同一句文案上连用

  • 与 type-rhythm-sync 分工:那是既有文字随音轨变属性(不换内容),

本卡换内容——绑音轨时本卡拍点对鼓点即可,别再叠字重脉冲

  • pill 短语长度要接近(demo 最长 "Draft an agenda")——长短差 >2 倍

槽宽跳变太猛,句子重心左右甩

  • 收束句与句干字号字重必须完全一致(demo 都是 96px/800)——

"do it all." 若比句干大,读作新标题而非句子补完

参考实现

demos/typography/pill-slot-cycle/ (PillSlotCycle.tsx) 原片出处:notion-ai 4.5–8.5s

Reference implementation (Remotion)

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

Prompt

Use the video-shotcraft shot recipe "Pill slot cycle" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/pill-slot-cycle.json
Raw TSX: https://gimgs.net/motion/pill-slot-cycle/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/typography/pill-slot-cycle/PillSlotCycle.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.89s

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,079 chars · Apache-2.0Abrir template bruto ↗
// ===== demos/typography/pill-slot-cycle/PillSlotCycle.tsx =====
// pill-slot-cycle —— 句中词槽轮换:固定句干 + 句尾 pill 老虎机式滚一格
// 源:notion-ai 4.5–8.5s。旧 pill 上飞淡出、新 pill 从下带运动模糊滑入,
// 连换 6 次后 pill 消失、句子落成 "One AI tool to do it all." 收束。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';
import { G } from '../../_fixtures/Fixtures';

const FONT = 'Helvetica, Arial, sans-serif';

const PILLS = [
  { label: 'Ask a question', icon: '?' },
  { label: 'Find in Drive', icon: '▲' },
  { label: 'Find in Slack', icon: '#' },
  { label: 'Summarize', icon: '≡' },
  { label: 'Improve writing', icon: '✎' },
  { label: 'Draft an agenda', icon: '☰' },
];

const BEAT = 21; // ~0.7s @30fps
const INTRO = 12; // 句干入场
const CYCLES = PILLS.length;

const Pill: React.FC<{ label: string; icon: string; style?: React.CSSProperties }> = ({
  label,
  icon,
  style,
}) => (
  <div
    style={{
      display: 'inline-flex',
      alignItems: 'center',
      gap: 16,
      padding: '14px 36px 14px 24px',
      borderRadius: 999,
      background: '#fff',
      border: `3px solid ${G.border}`,
      boxShadow: '0 6px 18px rgba(0,0,0,0.10)',
      fontFamily: FONT,
      fontWeight: 700,
      fontSize: 64,
      color: G.ink,
      whiteSpace: 'nowrap',
      ...style,
    }}
  >
    <span
      style={{
        width: 58,
        height: 58,
        borderRadius: 14,
        background: G.bg,
        display: 'inline-flex',
        alignItems: 'center',
        justifyContent: 'center',
        fontSize: 38,
        color: G.mid,
        flexShrink: 0,
      }}
    >
      {icon}
    </span>
    {label}
  </div>
);

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

  // 句干入场:ease-out 上浮淡入
  const stemT = interpolate(frame, [0, INTRO], [0, 1], {
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.cubic),
  });

  const cycleStart = INTRO;
  const cycleEnd = cycleStart + CYCLES * BEAT;

  // 当前处于第几个 pill 拍
  const rel = frame - cycleStart;
  const idx = Math.min(Math.floor(rel / BEAT), CYCLES - 1);
  const beatFrame = rel - idx * BEAT;

  const SWAP = 8; // 每拍前 8f 完成换位

  // 收束段:pill 上飞消失,"do it all." 从下滑入落位
  const isFinale = frame >= cycleEnd;
  const finT = interpolate(frame, [cycleEnd, cycleEnd + 14], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.back(1.4)),
  });

  // 槽内容渲染
  let slot: React.ReactNode = null;
  if (!isFinale && rel >= 0) {
    const incoming = PILLS[idx];
    const outgoing = idx > 0 ? PILLS[idx - 1] : null;

    // 新 pill:从下 +120px 带 blur 滑入,ease-out
    const inT = interpolate(beatFrame, [0, SWAP], [0, 1], {
      extrapolateRight: 'clamp',
      easing: Easing.out(Easing.cubic),
    });
    const inY = interpolate(inT, [0, 1], [120, 0]);
    const inBlur = interpolate(inT, [0, 0.7, 1], [14, 4, 0]);

    // 旧 pill:向上 -120px 加速飞出淡掉
    const outT = interpolate(beatFrame, [0, SWAP - 1], [0, 1], {
      extrapolateRight: 'clamp',
      easing: Easing.in(Easing.cubic),
    });
    const outY = interpolate(outT, [0, 1], [0, -130]);

    slot = (
      <div style={{ position: 'relative', display: 'inline-block' }}>
        {/* 撑起槽宽度的隐形占位(当前 pill) */}
        <Pill label={incoming.label} icon={incoming.icon} style={{ visibility: 'hidden' }} />
        {outgoing && outT < 1 && (
          <div
            style={{
              position: 'absolute',
              left: 0,
              top: 0,
              transform: `translateY(${outY}px)`,
              opacity: 1 - outT,
              filter: `blur(${outT * 10}px)`,
            }}
          >
            <Pill label={outgoing.label} icon={outgoing.icon} />
          </div>
        )}
        <div
          style={{
            position: 'absolute',
            left: 0,
            top: 0,
            transform: `translateY(${inY}px)`,
            opacity: idx === 0 ? inT : Math.min(1, inT * 1.6),
            filter: `blur(${inBlur}px)`,
          }}
        >
          <Pill label={incoming.label} icon={incoming.icon} />
        </div>
      </div>
    );
  } else if (isFinale) {
    // 最后一个 pill 上飞离场(前 8f),然后 "do it all." 落位
    const lastOutT = interpolate(frame, [cycleEnd, cycleEnd + 7], [0, 1], {
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
      easing: Easing.in(Easing.cubic),
    });
    slot = (
      <div style={{ position: 'relative', display: 'inline-block' }}>
        <span
          style={{
            fontFamily: FONT,
            fontWeight: 800,
            fontSize: 96,
            color: G.ink,
            letterSpacing: -1,
            display: 'inline-block',
            opacity: finT,
            transform: `translateY(${(1 - finT) * 90}px)`,
            filter: `blur(${(1 - finT) * 8}px)`,
            whiteSpace: 'nowrap',
          }}
        >
          do it all.
        </span>
        {lastOutT < 1 && (
          <div
            style={{
              position: 'absolute',
              left: 0,
              top: -8,
              transform: `translateY(${-130 * lastOutT}px)`,
              opacity: 1 - lastOutT,
              filter: `blur(${lastOutT * 10}px)`,
            }}
          >
            <Pill label={PILLS[CYCLES - 1].label} icon={PILLS[CYCLES - 1].icon} />
          </div>
        )}
      </div>
    );
  }

  return (
    <AbsoluteFill style={{ background: G.bg }}>
      {/* 句干固定不动:整行左端锚死,不随 pill 宽度居中重排 */}
      <div
        style={{
          position: 'absolute',
          left: 300,
          top: 540,
          transform: `translateY(calc(-50% + ${(1 - stemT) * 50}px))`,
          display: 'flex',
          alignItems: 'center',
          gap: 36,
          opacity: stemT,
        }}
      >
        <span
          style={{
            fontFamily: FONT,
            fontWeight: 800,
            fontSize: 96,
            color: G.ink,
            letterSpacing: -1,
            whiteSpace: 'nowrap',
          }}
        >
          One AI tool to
        </span>
        {slot}
      </div>
    </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/pill-slot-cycle/PillSlotCycle.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