Motion · Interaction

Picker carousel feature cycle

A mobile-style vertical picker — the focus pill stays still while content scrolls through it, snapping to a full stop each time.

1920×108016:930 fps4.1s · 123 framesremotionLicense: Apache-2.0#shotcraft#remotion#interaction

Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player

When to use

A shot that reads off feature/scene names one by one; a "pick one" interaction demo; a picker-control showcase for mobile products

Intention

Give the physical feel of "selecting" something: the focus frame is fixed, it's the content that scrolls and gets snapped into it. Each item must actually go still for a few frames after stopping — continuous scrolling reads as a loading animation, only a pause reads as "this item is now selected." Distance-based decay (opacity + font size + grayscale all fading together with distance from center) is the only way to keep the viewer's eye locked on the middle row.

Duration & energy

  • Duration: About 3.6s (108f@30fps)
  • Energy: Medium (every snap is a beat, five beats advancing at a steady pace)

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

意图

把"选择"这件事的物理感做出来:焦点框是固定的,是内容在滚动并被吸住。 每项停下后必须真的静止几帧——连续滚动读作 loading 动画,停顿才读作 "这一项被选中了"。距离衰减(透明度 + 字号 + 灰度三通道同时衰减)是让 观众视线始终锁在中间那一行的唯一手段。

动效核心

  • 视口 300×170px(5 行 × rowH 34),焦点药丸固定在第 3 行位置

(top: rowH*2),白底 + 1px #E3E3E6 描边 + 0 2px 8px rgba(0,0,0,.06)

  • 全程 t 0.05→0.95 匀速切成 STEPS=5 步;步内位移

outQuint(min(1, local/(1−HOLD))),HOLD=5/14 的尾段完全不动 (每步≈0.65s = 走位≈0.42s + 静止≈0.23s)

  • 列表整体 translateY(rowH*2 − pos*rowH)——移动的是内容,药丸零位移
  • 距离衰减(d = |i − pos|)分三段:opacity d≤1 时 1→0.55、

1<d≤2 时 0.55→0.18;fontSize 17→14px(按 d/2); color d<0.5 用 INK、d<1.5 用 MID、更远用 #B9B9BE

  • 行内图标 opacity max(0, 1 − d*1.6)——只有焦点行看得见图标
  • 落定后药丸 scaleY 1 + sin(min(1, land/0.6)·π)*0.06 呼吸一下

(land 只在 HOLD 段内 >0,即吸附完成才呼吸)

  • 视口上下各 26% 的 PAPER→透明渐变遮罩,让进出项自然消隐

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 行高/可见行数 | rowH 34px × 5 行,焦点在第 3 行 | 可见行数必须是奇数,否则焦点行不在视觉正中 | | 步数 | STEPS=5,窗口 t 0.05→0.95 匀速 | 5 拍是 3.6s 的舒适上限;7 拍每拍就压到 0.46s 读不清 | | HOLD | 5/14 ≈ 0.357(每步尾段静止≈0.23s) | 命门:HOLD 归零就变成匀速滚动,完全丢掉"吸附"语义 | | 吸附缓动 | outQuint(作用在 local/(1−HOLD) 上) | outQuint 的急减速就是"被磁铁吸住"的手感,outCubic 太软 | | opacity 衰减 | 1 → 0.55(d≤1)→ 0.18(d≤2) | 相邻行留 0.55 是让观众感知"上下还有内容";降到 0.3 就孤立了 | | fontSize 衰减 | 17→14px,按 min(1, d/2) | 字号差是第二重景深;只做透明度会显得扁 | | 图标可见性 | max(0, 1 − d·1.6) | 只有焦点行带图标,是"当前项"的额外标记 | | 药丸呼吸 | scaleY 1→1.06→1,落定后 0.6·HOLD 内走完 | 0.06 已到上限;再大就读作药丸自己在动,抢了内容的戏 |

已知坑

  • 药丸必须是 translateY 零位移的固定层,scaleY 呼吸也不能带

位移,否则焦点框漂移,"内容穿过焦点"的语义立刻失效

  • 距离衰减写在 fontSize 上会触发逐帧重排(行高固定所以布局不跳),

行高若改成 auto 就会抖;rowH 必须写死

  • 渐变遮罩的颜色要与背景 PAPER 完全一致,差一档就露出边界带
  • 左外侧 26×22 的方形 AI 徽标用 margin:-11px 0 0 -186px 相对画面中心

定位;改视口宽度必须同改 186px

  • ITEMS 是 7 条占位功能名("Data Cleanup" 等),落地换真实功能名时

字长变化会影响居中观感——单行超过 300px 就要缩字号而不是换行

参考实现

demos/interaction/picker-carousel-feature-cycle/ (PickerCarouselFeatureCycle.tsx)

Reference implementation (Remotion)

  • demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx

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

Everything on this page is also machine-readable. Point your agent at the JSON, or add the gimgs MCP server and ask it to search motion templates.

  1. Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
  2. Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
  3. Register the component as a <Composition> in a Remotion project and render with `npx remotion render`, or re-implement the recipe in your own engine.
curl -s https://gimgs.net/motion/picker-carousel-feature-cycle.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("picker-carousel-feature-cycle")

Prompt

Use the video-shotcraft shot recipe "Picker carousel feature 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/picker-carousel-feature-cycle.json
Raw TSX: https://gimgs.net/motion/picker-carousel-feature-cycle/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 4.1s

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,261 chars · Apache-2.0Open raw template ↗
// ===== demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx =====
// picker-carousel-feature-cycle — Picker Carousel 功能名吸附轮播(motion-lab 定稿转原生 Remotion)
// 移动端风竖向选择器:焦点药丸不动、内容穿过它,每项停靠 0.45s 且带明显 outQuint
// 减速吸附 + 4–5 帧静止;按到中心距离分层控制 opacity/字号/灰度,落定时药丸做
// scaleY 1→1.06→1 极轻呼吸,左外侧固定方形 AI 徽标。
// 设计坐标 480×270(DesignStage 等比放大),参数表数值以此坐标系标定。
import React from 'react';
import { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';

export const PICKER_CAROUSEL_FEATURE_CYCLE_DURATION = 108; // 3600ms @30fps

const PAPER = '#F3F3F1';
const INK = '#111113';
const MID = '#8A8A8F';
const ROW_H = 34; // 单行高度
const ITEMS = [
  'Data Cleanup',
  'Direct Message',
  'Smart Segments',
  'Batch Actions',
  'Reward Program',
  'Automated Flows',
  'Variant Testing',
];
const ICONS = ['◎', '✉', '◧', '◈', '★', '↺', '⚑'];
const STEPS = 5; // 五次吸附推进
const HOLD = 5 / 14; // 每步末段静止占比(≈4–5 帧不动)

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

  // 主时间轴:t∈[0.05,0.95] 均分 5 步;步内前 (1-HOLD) 用 outQuint 吸附,末段静止
  const g = seg(t, 0.05, 0.95) * STEPS;
  const step = Math.min(STEPS - 1, Math.floor(g));
  const local = Math.min(1, g - step);
  const mv = E.outQuint(Math.min(1, local / (1 - HOLD)));
  const pos = step + mv;

  // 落定呼吸:吸附完成进入 HOLD 后,药丸 scaleY 1→1.06→1 极轻脉冲
  const land = Math.max(0, (local - (1 - HOLD)) / HOLD);
  const breath = land > 0 ? Math.sin(Math.min(1, land / 0.6) * Math.PI) * 0.06 : 0;

  return (
    <DesignStage bg={PAPER}>
      {/* 纸面底 —— 与原采集页 paper() 容器一致 */}
      <div
        style={{
          position: 'absolute',
          inset: 0,
          overflow: 'hidden',
          background: PAPER,
          fontFamily: "-apple-system,'Helvetica Neue',Helvetica,Arial,sans-serif",
        }}
      >
        {/* 选择器视口:300×(34×5) 居中,整体在开头 5% 内淡入 */}
        <div
          style={{
            position: 'absolute',
            left: '50%',
            top: '50%',
            width: 300,
            height: ROW_H * 5,
            transform: 'translate(-50%,-50%)',
            overflow: 'hidden',
            opacity: seg(t, 0, 0.05),
          }}
        >
          {/* 焦点药丸:位置不动,仅落定时 scaleY 呼吸。
              原采集页是 content-box,带 1px 边框需显式声明,否则高度差 2px */}
          <div
            style={{
              position: 'absolute',
              left: 0,
              right: 0,
              top: ROW_H * 2,
              height: ROW_H,
              boxSizing: 'content-box',
              borderRadius: 999,
              background: '#fff',
              border: '1px solid #E3E3E6',
              boxShadow: '0 2px 8px rgba(0,0,0,.06)',
              transform: `scaleY(${1 + breath})`,
            }}
          />
          {/* 内容列:整列 translateY 穿过焦点药丸 */}
          <div
            style={{
              position: 'absolute',
              left: 0,
              right: 0,
              top: 0,
              transform: `translateY(${ROW_H * 2 - pos * ROW_H}px)`,
            }}
          >
            {ITEMS.map((txt, i) => {
              // 按到中心距离分层:透明度两段线性、字号 17→14、颜色三档灰度
              const d = Math.abs(i - pos);
              const k2 = Math.min(2, d);
              const o = k2 <= 1 ? lerp(k2, 1, 0.55) : lerp(k2 - 1, 0.55, 0.18);
              return (
                <div
                  key={i}
                  style={{
                    height: ROW_H,
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    gap: 8,
                    fontWeight: 600,
                    fontSize: lerp(Math.min(1, d / 2), 17, 14),
                    lineHeight: 1,
                    fontFamily: "-apple-system,'Helvetica Neue',sans-serif",
                    color: d < 0.5 ? INK : d < 1.5 ? MID : '#B9B9BE',
                    opacity: o,
                  }}
                >
                  {/* 图标仅在贴近焦点时浮现(d<0.625 内可见) */}
                  <span style={{ fontSize: 14, opacity: Math.max(0, 1 - d * 1.6) }}>
                    {ICONS[i]}
                  </span>
                  <span>{txt}</span>
                </div>
              );
            })}
          </div>
          {/* 上下渐隐罩:纸色→透明→纸色,盖在列表之上 */}
          <div
            style={{
              position: 'absolute',
              inset: 0,
              pointerEvents: 'none',
              background: `linear-gradient(180deg,${PAPER} 0%,rgba(243,243,241,0) 26%,rgba(243,243,241,0) 74%,${PAPER} 100%)`,
            }}
          />
        </div>
        {/* 左外侧固定 AI 徽标:黑底白字方形圆角,开头略滞后淡入 */}
        <div
          style={{
            position: 'absolute',
            left: '50%',
            top: '50%',
            margin: '-11px 0 0 -186px',
            width: 26,
            height: 22,
            borderRadius: 6,
            background: INK,
            color: '#fff',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            fontWeight: 700,
            fontSize: 10,
            lineHeight: 1,
            fontFamily: '-apple-system,sans-serif',
            letterSpacing: 0.5,
            opacity: seg(t, 0.02, 0.09),
          }}
        >
          AI
        </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/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.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
—

More in this category