Motion · UI entrance

Research card stack scroll

Dark paper cards fly in every 12 frames along a bottom-right axis to stack up, only the topmost fully rendered while the rest blur and dim.

1920×108016:930 fps5.3s · 159 framesremotionLicense: Apache-2.0#shotcraft#remotion#ui-entrance

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

When to use

Conveying the sheer scale of "reading a huge volume of material/processing a mass of documents"; a capability shot for research, retrieval, or batch-processing products

Intention

The point being made is "quantity," but dumping 13 cards on screen at once reads as a wall. Turn it into a pipeline instead: cards come in one at a time, each one genuinely gets read (the top card is always the one rendered in full), then sinks into the stack and blurs as the next one arrives. What the viewer feels is continuous, ongoing throughput, not a static pile. The background grid moving down at the same speed is the speed reference — without it, the pile would just read as "stuck in place and shrinking."

Duration & energy

  • Duration: About 4.8s (144f@30fps)
  • Energy: Medium-high (constant high frequency, no burst but sustained pressure)

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

意图

要说的是"数量",但一次给 13 张卡会读成一堵墙。改成流水线:卡一张一张进,每张都被 读过(最上一张永远是全文渲染的那张),读完就沉进堆里变模糊。观众感受到的是持续不断 的吞吐量,而不是一个静态的堆。背景 grid 同速下移是速度参照——没有它,堆积会读作 "卡在原地缩小"。

动效核心

  • 时间轴用真实帧号而非归一化 t:内部 F=144,每张卡的本地时间 e = t*144 - i*12,

一切编排(入场、堆积、深度)都以 PER=12 帧为节拍单位写死

  • 入场是 6 帧短促动作:seg(clamp((e+6)/6), 0, 1, E.outCubic)——注意起点是 e=-6,

即卡在自己的节拍到达前 6 帧就开始飞,translateY(-40→0) + scale(0.94→1)

  • 落位 squash:e ∈ [0, 1.6) 时 y 轴 scale 额外乘 0.97,只压 1.6 帧——短到读不出是

变形,只读作"砸实了"

  • 堆积漂移:落位后 drift = e/12*30,每个节拍向下 30px、向右 XOFF=9px,形成微微

向右下的轴线;轴线的存在让 13 张卡不是同心叠放而是有厚度的一叠

  • 深度衰减:depth = min(1, drift/(30*3))——三个节拍内衰减到底,blur(depth*4) +

brightness(1-depth*0.25);再往下 (drift-30*3.2)/(30*1.6) 把 opacity 拉到 0 回收

  • 焦点唯一:focus = floor(f/12),只有 i === focus 的卡显示 body(作者行 + 4 条摘要

骨架),其余卡的 body 直接 opacity:0 只留标题——"正在读的那一张"永远只有一张

  • 背景 grid:translateY((f/12*30*S) % 24),位移量与卡的 drift 用同一个 GAP=30

推导,所以 grid 与卡堆严格同速;对 24px 取模让它无缝循环

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 节拍 | PER=12 帧/张,13 张 | 12f 是"紧但看得清"的下限;<8f 标题来不及读,>20f 量级感垮掉变成逐张展示 | | 入场行程 | 6 帧(outCubic),提前量 -6 帧 | 行程只有节拍的一半 → 每张在下一张来之前已经落定;行程 ≥ PER 时会有两张同时在飞 | | 入场位移/缩放 | translateY(-40→0)、scale 0.94→1 | 40px 相对 96px 卡高是小幅——卡是"落进去"不是"飞进来";缩放差只有 0.06,再大就读作弹窗 | | 落位压缩 | 1.6 帧,y 轴 ×0.97 | 3% 是刚好能感知的量;>6% 或时长 >3f 会看出橡皮变形 | | 堆积轴线 | 每节拍 y+30px、x+9px | 9/30 的比值决定轴线斜度;x 归零会变成正下方同心叠压,厚度感消失 | | 深度衰减 | depth = drift/90,blur 0→4px、brightness 1→0.75 | 90px(3 个节拍)内衰减完 → 画面里始终只有约 3 张是"半清晰"的;衰减距离拉长会让下方糊卡堆太厚,画面变脏 | | 回收窗 | drift > 96px 起淡出,48px 内到 0 | 回收太早会看到卡在画面中央消失;太晚 13 张全在场,DOM 与 blur 开销都上去 | | 内容坐标系 | 逻辑 420×250,整体 scale(min(W/420, H/250)) | 所有像素值写在逻辑坐标系里,改画布尺寸不用重排;卡 296×96、字号 9.5px 都是逻辑值 |

已知坑

  • 卡数与节拍是绑定的:13 张 × 12 帧 = 156 帧 > F=144,末两张在 t=1 时仍在入场途中,

末帧不是干净的静态。要做定格结尾,把卡数压到 F/PER - 1 以内(12 帧节拍下 ≤11 张)

  • focus = floor(f/12) 会超出卡数上界(f=144 时 focus=12,正好是最后一张)——加卡数

或改节拍后要确认 focus 不会长期指向不存在的索引,否则出现"没有任何一张是清晰的"

  • 全部时间常量都写死在 144 帧的假设上(F=144, fr=f/F)。改时长必须同时改 F,

否则节拍会随时长伸缩,12 帧的紧凑感丢失

  • 标题是 13 条真实感的论文标题占位、preprint:24xx 编号由索引生成,落地要换成项目

真实文档标题;rand(i*5+k) 生成的摘要骨架条宽度是确定性伪随机,不要换成 Math.random

  • 深色卡(INK 底 + #2A2A2A 描边)压在 #FAFAFA 浅底上是这套对比度的前提;卡色

改浅后 brightness(0.75) 的深度衰减几乎看不出来,需要改用 opacity 或灰度衰减

参考实现

demos/ui-entrance/research-card-stack-scroll/ (ResearchCardStackScroll.tsx)

Reference implementation (Remotion)

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

Prompt

Use the video-shotcraft shot recipe "Research card stack scroll" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/research-card-stack-scroll.json
Raw TSX: https://gimgs.net/motion/research-card-stack-scroll/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/ui-entrance/research-card-stack-scroll/ResearchCardStackScroll.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.3s

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,578 chars · Apache-2.0Open raw template ↗
// ===== demos/ui-entrance/research-card-stack-scroll/ResearchCardStackScroll.tsx =====
// research-card-stack-scroll — Research Stack 论文卡叠压滚流(motion-lab 定稿转原生 Remotion)
// 深色论文卡沿微微向右下的轴线连续飞入并叠压在中心:入场是 translateY(-40)+scale 0.94→1
// 的 6 帧短促 spring,落位带 1 帧压缩;只有最上一张全清晰渲染标题+作者+摘要,
// 下方卡按 depth 递增 blur/变暗只露顶部标题条,背景浅灰横向 grid 同步移动做速度参照。
// 设计坐标 480×270(DesignStage 等比放大),参数表数值以此坐标系标定。
import React from 'react';
import { DesignStage, E, lerp, rand, seg, useT } from '../../_fixtures/Motion';

export const RESEARCH_CARD_STACK_SCROLL_DURATION = 144; // 4800ms @30fps

const ORANGE = '#FF6A1F';
const ORANGE_SOFT = '#FF8A44';
const INK = '#1A1A1A';
const FONT = '-apple-system,system-ui,"SF Pro Text","PingFang SC",sans-serif';

// 原 setup 里的 stage.clientWidth/clientHeight 分支——原渲染 stage 恒为 480×270
const W = 480;
const H = 270;
const F = 144; // recipe 帧总数(f = t·F)
const PER = 12; // 每 12 帧进一张卡
const GAP = 30; // 叠压后每张卡的下移间距
const XOFF = 9; // 滚流时的右移量

// 逻辑画布 420×250 → 实际 stage 的等比缩放
const LW = 420;
const LH = 250;
const S = Math.min(W / LW, H / LH);

const TITLES = [
  'Sparse Attention Mechanisms for Long-Context Reasoning',
  'Retrieval Drift in Multi-Hop Agent Pipelines',
  'Latent Caching Reduces Tool-Call Latency by 41%',
  'On the Calibration of Preference Reward Models',
  'Grid-Aligned Motion Priors for UI Animation',
  'Cheap Verifiers Beat Expensive Samplers',
  'Structured Decoding Without Grammar Loss',
  'Depth-Ordered Compositing for Live Interfaces',
  'Token-Budget Routing in Agent Fleets',
  'Contrastive Layouts for Document Understanding',
  'Fast Approximate Re-Ranking at Query Time',
  'Signal Propagation in Deep Residual Agents',
  'A Note on Deterministic Replay of Motion',
];
const CW = 296;
const CH = 96;

export const ResearchCardStackScroll: React.FC = () => {
  const t = useT();
  const f = t * F;
  const focus = Math.floor(f / PER); // 只有最上一张卡渲染正文
  return (
    <DesignStage bg="#FAFAFA">
      {/* 背景横向 grid:随滚流同步下移做速度参照 */}
      <div
        style={{
          position: 'absolute',
          inset: -40,
          backgroundImage: 'linear-gradient(rgba(0,0,0,.055) 1px,transparent 1px)',
          backgroundSize: '100% 24px',
          transform: `translateY(${((f / PER) * GAP * S) % 24}px)`,
        }}
      />
      {/* 逻辑画布 track:中心锚点 + 等比缩放 */}
      <div
        style={{
          position: 'absolute',
          left: '50%',
          top: '50%',
          width: 0,
          height: 0,
          transformOrigin: '0 0',
          transform: `scale(${S})`,
        }}
      >
        {TITLES.map((tt, i) => {
          const e = f - i * PER; // 该卡的本地帧
          const p = seg(Math.max(0, Math.min(1, (e + 6) / 6)), 0, 1, E.outCubic); // 6 帧入场
          const drift = (Math.max(0, e) / PER) * GAP; // 叠压后随流下移
          const squash = e >= 0 && e < 1.6 ? 0.97 : 1; // 落位 1 帧压缩
          const y = lerp(p, -40, 0) + drift;
          const x = (Math.max(0, e) / PER) * XOFF;
          const depth = Math.min(1, drift / (GAP * 3)); // depth 递增 blur + 变暗
          const opacity =
            e < -6 ? 0 : Math.min(1, (e + 6) / 2) * (1 - Math.max(0, (drift - GAP * 3.2) / (GAP * 1.6)));
          return (
            <div
              key={i}
              style={{
                position: 'absolute',
                left: -CW / 2,
                top: -CH / 2 - 14,
                width: CW,
                height: CH,
                background: INK,
                borderRadius: 11,
                overflow: 'hidden',
                fontFamily: FONT,
                zIndex: i,
                opacity,
                boxShadow: '0 14px 34px rgba(0,0,0,.22)',
                border: '1px solid #2A2A2A',
                boxSizing: 'border-box',
                transform: `translate(${x}px,${y}px) scale(${lerp(p, 0.94, 1)},${lerp(p, 0.94, 1) * squash})`,
                filter: `blur(${(depth * 4).toFixed(2)}px) brightness(${(1 - depth * 0.25).toFixed(3)})`,
              }}
            >
              {/* 左侧色条:每 4 张一根橙色 */}
              <div
                style={{
                  position: 'absolute',
                  left: 0,
                  top: 0,
                  width: 3,
                  height: '100%',
                  background: i % 4 === 1 ? ORANGE : '#2E2E2E',
                }}
              />
              {/* 标题条:所有卡都渲染 */}
              <div
                style={{
                  position: 'absolute',
                  left: 14,
                  top: 11,
                  width: 264,
                  font: `650 9.5px/1.35 ${FONT}`,
                  color: '#F0F0F0',
                }}
              >
                {tt}
              </div>
              {/* 正文(作者 + 摘要骨架线):只有 focus 卡可见 */}
              <div style={{ position: 'absolute', left: 14, top: 40, width: 266, opacity: i === focus ? 1 : 0 }}>
                <div style={{ font: `500 8px/1 ${FONT}`, color: ORANGE_SOFT, letterSpacing: '.3px' }}>
                  {`A. Author, B. Writer, C. Reader · preprint:24${10 + i}.0${i % 9}${i % 7}`}
                </div>
                {[0, 1, 2, 3].map((k) => (
                  <div
                    key={k}
                    style={{
                      marginTop: k ? 5 : 9,
                      width: `${100 - k * 11 - rand(i * 5 + k) * 12}%`,
                      height: 4,
                      borderRadius: 2,
                      background: '#3A3A3A',
                    }}
                  />
                ))}
              </div>
            </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/ui-entrance/research-card-stack-scroll/ResearchCardStackScroll.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