Motion · Tipografia

Pill chip slot cycle

In "Your `chip` Handled" on white, the word inside a dark pill scrolls like a slot reel while the pill's width smoothly stretches to fit.

1920×108016:930 fps5.5s · 165 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

Copy patterns like "we've got ___ handled"; a one-line close for a SaaS feature list; a hero-visual beat for light-background brand films

Intention

Word-slot cycling comes in two schools: keep the sentence out of the way of the change (a fixed-width reel), or let the sentence accept the change (the pill stretches and the surrounding text gets pushed apart). This card is the latter — the cost is you have to get the width math right, and the payoff is that the sentence reads as alive: "Your" and "Handled" get pushed along, and the viewer feels the weight of the word inside the pill. The gray ghost items peeking out above and below the pill are a second cue: they signal "this is a scrollable list," priming the viewer to expect the next word.

Duration & energy

  • Duration: About 5.0s (150f@30fps: hold → 3 swaps at 0.25/0.47/0.69 → closing hold)
  • Energy: Medium (a steady three-beat rhythm, width stretching is the only continuous motion)

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

意图

词槽轮换有两派:让句子躲开变化(宽度固定的滚轮),或者让句子接受 变化(胶囊撑宽、两侧文字被挤开)。这卡是后者,代价是必须把宽度算准, 回报是句子读起来活的——"Your" 和 "Handled" 被推着走,观众感到胶囊里 的词有分量。上下露出的灰色幽灵项是第二个记号:它明示"这是个可以滚的 列表",让观众预期还有下一个词。

动效核心

  • 宽度预量:setup 期用隐藏 <span> 逐词量 offsetWidth,各加 74px 内边距

存成 widths[](带 o.w.length·13 兜底,防 setup 时未挂载量不到)

  • 位置量 pos 由 3 段窗累加:切换点 [0.25, 0.47, 0.69],每窗 0.12

(≈18f),缓动 E.inOutCubic——两头都缓,对应"滚轮起步与停稳"

  • 内列 colIn 竖移 -pos·48px(行高 48 = 胶囊高);胶囊宽度取

lerp(frac, widths[ci], widths[ci+1]),帧内插值所以伸缩是连续的, 两侧 flex:none 的文字被 flex 布局自然挤开

  • 幽灵项在胶囊外(chipWrap 内)上 -38px / 下 58px,取 near = round(pos)

的前后邻居文本,随滚动微移 (pos-near)·48·0.5(半速视差,比胶囊内 的词慢一半,才像"外圈")

  • 幽灵项透明度 0.13 · (0.4 + settle·0.6),settle = 1 - min(1, |pos-near|·3)

——滚动中途更淡、落定时最清楚

  • 每行内是 emoji + 词的两段结构(emoji 18px、词 700 22px 白字),

胶囊 #1a1c24 底 + 0 8px 24px rgba(20,22,40,.22) 投影 + overflow:hidden

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 词表 | 4 词 + emoji(Sales/Workflow/Admin/Reports),3 次切换 | 词长差决定挤开幅度的观感:demo 最短 5 字母最长 8 字母;差 >2 倍胶囊跳变太猛,句子重心左右甩 | | 切换点 | [0.25, 0.47, 0.69],窗 0.12(≈18f) | 段间约 0.10(≈15f)静置读词;起手 0.25 前的静置让观众先认出句式 | | 缓动 | E.inOutCubic | 两头缓才像滚轮起停;outCubic(只后缓)读作被甩上来,胶囊宽度也会跟着突变 | | 胶囊 | 高 48px、圆角 99、#1a1c24、宽 = 文本宽 + 74 | +74 是左右内边距 + emoji 位;<50 词贴边,>100 胶囊胖成按钮 | | 幽灵项 | 上 -38 / 下 58px,基础透明度 0.13 | 0.13 是"看得见但不抢"的甜点;>0.25 读作三行并列文字,不是幽灵;删了列表暗示就没了 | | 幽灵视差 | (pos-near)·48·0.5(半速) | 半速是"外圈更远"的立体感来源;同速则三行像一整块滑动,幽灵失去意义 | | 句子字号 | 两侧 30px/800、胶囊内 22px/700 | 胶囊内比句子小是正确层级(徽章依附句子);等大或更大就读作两个标题打架 | | 配色 | 底 #fbfbfd、句子 #15171d、胶囊白字 | 浅底 + 单一深色块是本卡的身份证;深底版会让胶囊消失,需整套重配 |

已知坑

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

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

  • 宽度靠 setup 期 offsetWidth 实测:字体没加载完时量到的是回退字体

的宽度,胶囊会偏窄或偏宽——移植时须确保 webfont 就绪后再量,或改用 离屏 canvas measureText;length·13 那条兜底只保证不崩,不保证准

  • 词长差要控制在 2 倍内:宽度自适应是本卡的卖点也是风险,

Sales→Workflow 这一跳已经是可接受上限,再大句子两侧甩得晃眼

  • 两侧文字必须 flex:none:不然 flex 会去压缩它们而不是移动它们,

"挤开"变成"字被压扁"

  • 幽灵项文本取 round(pos) 的邻居,所以在切换的正中间(pos=x.5)会

跳一次文本——被 settle 压到最淡时发生,肉眼基本不可察;但若把 透明度调高到 0.25+,这一跳就会露出来

  • emoji 在不同平台字形宽度不一(尤其 ⚙️ 带变体选择符),会让实测宽度

漂移几像素;实战建议换成项目自己的单色图标,别依赖系统 emoji

  • 与 vertical-word-roll-blur-cycle 分工:那卡宽度固定、裸词、带滚筒 blur

(词是句子成分),本卡宽度自适应、深色胶囊、带幽灵项(词是徽章)—— 同片只留一个;声音上三次落定是三个节奏点,配轻质开关/胶囊贴合音, 连发交替双样本,配法纪律见 sound-design.md

参考实现

demos/typography/pill-chip-slot-cycle-handled/ (PillChipSlotCycleHandled.tsx)

Reference implementation (Remotion)

  • demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.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-chip-slot-cycle-handled.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.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-chip-slot-cycle-handled")

Prompt

Use the video-shotcraft shot recipe "Pill chip 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-chip-slot-cycle-handled.json
Raw TSX: https://gimgs.net/motion/pill-chip-slot-cycle-handled/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.5s

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

4,622 chars · Apache-2.0Abrir template bruto ↗
// ===== demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx =====
// pill-chip-slot-cycle-handled — Chip Slot Cycle 胶囊滚轮挤开(motion-lab 定稿转原生 Remotion)
// 白底句式 "Your [chip] Handled":深色胶囊内词垂直滚轮轮换(Sales→Workflow→Admin→Reports),
// 上下露出灰色幽灵项,胶囊宽度随词长平滑变化,两侧文字被自然挤开收拢。
// 设计坐标 480×270(DesignStage 等比放大),参数表数值以此坐标系标定。
import React, { useLayoutEffect, useRef, useState } from 'react';
import { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';

export const PILL_CHIP_SLOT_CYCLE_HANDLED_DURATION = 150; // 5000ms @30fps

const WORDS = [
  { w: 'Sales', e: '⚡' },
  { w: 'Workflow', e: '📈' },
  { w: 'Admin', e: '⚙️' },
  { w: 'Reports', e: '📄' },
];
const FONT = '700 22px -apple-system,system-ui,sans-serif';
// 兜底词宽(原 effect.js 未挂载时的估算:字符数×13,再加胶囊内留白 74);
// 首帧 useLayoutEffect 用 offsetWidth 实测替换,与原渲染逐像素一致
const FALLBACK_WIDTHS = WORDS.map((o) => o.w.length * 13 + 74);

// 两侧静态文字("Your" / "Handled")
const SIDE: React.CSSProperties = {
  color: '#15171d',
  fontWeight: 800,
  fontSize: 30,
  letterSpacing: -0.5,
  flex: 'none',
};

export const PillChipSlotCycleHandled: React.FC = () => {
  const t = useT();

  // 量宽:与原 effect.js 相同,用隐藏 span 实测每个词的 offsetWidth(+74 胶囊留白),
  // 只在首次布局量一次;量到前用兜底估算,避免首帧闪动
  const measRef = useRef<HTMLDivElement>(null);
  const [widths, setWidths] = useState<number[]>(FALLBACK_WIDTHS);
  useLayoutEffect(() => {
    const spans = measRef.current?.children;
    if (!spans) return;
    setWidths(
      WORDS.map((o, i) => ((spans[i] as HTMLElement).offsetWidth || o.w.length * 13) + 74),
    );
  }, []);

  // 三次切换:0.25 / 0.47 / 0.69,每次用 0.12 的 inOutCubic 推进一格
  let pos = 0;
  for (const s0 of [0.25, 0.47, 0.69]) pos += seg(t, s0, s0 + 0.12, E.inOutCubic);
  const ci = Math.min(WORDS.length - 1, Math.floor(pos));
  const frac = pos - ci;
  // 胶囊宽度随当前词→下一词插值,平滑挤开两侧文字
  const chipW = lerp(frac, widths[ci], widths[Math.min(ci + 1, WORDS.length - 1)]);

  // 幽灵项:当前词的前/后邻居,随滚动微移,落定时回到基准透明度
  const near = Math.round(pos);
  const roll = (pos - near) * 48 * 0.5;
  const settle = 1 - Math.min(1, Math.abs(pos - near) * 3);
  const ghost = (top: number): React.CSSProperties => ({
    position: 'absolute',
    left: '50%',
    top,
    font: FONT,
    color: '#15171d',
    whiteSpace: 'nowrap',
    transform: `translateX(-50%) translateY(${-roll}px)`,
    opacity: 0.13 * (0.4 + settle * 0.6),
  });

  return (
    <DesignStage bg="#fbfbfd">
      <div
        style={{
          position: 'absolute',
          inset: 0,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          fontFamily: '-apple-system,system-ui,sans-serif',
        }}
      >
        {/* 隐藏量宽 span(与原 setup 的 meas 等价,不参与布局显示) */}
        <div ref={measRef} style={{ position: 'absolute', visibility: 'hidden' }}>
          {WORDS.map((o) => (
            <span key={o.w} style={{ whiteSpace: 'nowrap', font: FONT }}>
              {o.w}
            </span>
          ))}
        </div>
        <div style={SIDE}>Your</div>
        <div style={{ position: 'relative', flex: 'none' }}>
          {/* 深色胶囊:overflow hidden 里放 4 行词的滚轮列 */}
          <div
            style={{
              position: 'relative',
              height: 48,
              width: chipW,
              margin: '0 14px',
              flex: 'none',
              borderRadius: 99,
              background: '#1a1c24',
              boxShadow: '0 8px 24px rgba(20,22,40,.22)',
              overflow: 'hidden',
            }}
          >
            <div
              style={{
                position: 'absolute',
                left: 0,
                right: 0,
                top: 0,
                transform: `translateY(${-pos * 48}px)`,
              }}
            >
              {WORDS.map(({ w, e }) => (
                <div
                  key={w}
                  style={{
                    height: 48,
                    display: 'flex',
                    alignItems: 'center',
                    gap: 9,
                    paddingLeft: 18,
                    whiteSpace: 'nowrap',
                  }}
                >
                  <span style={{ fontSize: 18 }}>{e}</span>
                  <span style={{ font: FONT, color: '#fff' }}>{w}</span>
                </div>
              ))}
            </div>
          </div>
          {/* 胶囊外上下的灰色幽灵项 */}
          <div style={ghost(-38)}>{near > 0 ? WORDS[near - 1].w : ''}</div>
          <div style={ghost(58)}>{near < WORDS.length - 1 ? WORDS[near + 1].w : ''}</div>
        </div>
        <div style={SIDE}>Handled</div>
      </div>
    </DesignStage>
  );
};


  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-chip-slot-cycle-handled/PillChipSlotCycleHandled.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