{
  "slug": "list-stack-press",
  "title": "List stack press",
  "tagline": "List cards fly up from the bottom of the frame one by one to stack up, each landing squashing and rebounding the whole pile while a counter ticks up in sync.",
  "description": "List cards fly up from the bottom of the frame one by one to stack up, each landing squashing and rebounding the whole pile while a counter ticks up in sync.",
  "category": "ui-entrance",
  "tags": [
    "shotcraft",
    "remotion",
    "ui-entrance"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 960,
    "height": 540,
    "fps": 30,
    "duration": 3.5,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/list-stack-press/poster-9a63e948f091.jpg",
    "video": "https://gimgs.net/motion/media/list-stack-press/video-7751ec4ae8a1.mp4"
  },
  "variants": [
    {
      "key": "list-stack-press",
      "label": "list-stack-press · List stack press",
      "description": "List cards fly up from the bottom of the frame one by one to stack up, each landing squashing and rebounding the whole pile while a counter ticks up in sync.",
      "use": "",
      "video": "https://gimgs.net/motion/media/list-stack-press/video-7751ec4ae8a1.mp4",
      "poster": "https://gimgs.net/motion/media/list-stack-press/poster-9a63e948f091.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/list-stack-press.html",
  "json_url": "https://gimgs.net/motion/list-stack-press.json",
  "template_url": "https://gimgs.net/motion/list-stack-press/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/list-stack-press.md",
  "updated_at": "2026-09-24T07:40:05.657Z",
  "prompt": "## When to use\n\nFor feed/radar/inbox shots that say \"something new every day\"; emphasizes a continuously accumulating asset list.\n\n## Intention\n\n\"Stacking has weight\": each new card landing presses the settled pile down and lets it rebound — the audience reads \"this was genuinely accumulated\" from the physical feedback. The counter ticking in sync locks in the sense of quantity.\n\n## Duration & energy\n\n- Duration: About 3s (18–88f)\n- Energy: Medium\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n\"堆叠有重量\"：每张新卡落上来，已落定的整摞被压下再弹回——观众从物理反馈里读出\"这是实打实攒下来的东西\"。计数器同步跳格把数量感钉死。\n\n## 动效核心\n- **预备拍（anticipation，借鉴迪士尼法则 2）**：首张升入前给一拍段落级预备——计数器先于首卡 4–6f 亮起归零/微缩 scale(0.96)→1，把视线引到堆叠区，再让首卡入画。只做段落级一次，不逐卡加\n- 卡从画面下方升入，交替微倾斜收平，等距节拍逐张摞起\n- 每张到场压下整摞 6px、8f 弹回（stackPress 脉冲——即法则 5 的 follow-through：新卡的动能传给整摞再缓冲耗散，这一笔就是\"有重量\"的来源）\n- 落地后按拖拽层级依次收尾（法则 5）：卡体先停 → 整摞弹回 → 高亮条再长出（滞后 2–4f）——三者同帧发生读作贴图切换\n- 收尾一道 glaze 扫光掠过整摞\n- 屏幕空间 DigitRoll 计数器落一张滚一格；相机跟随堆叠向下\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 节拍 | CUES=[18,30,42,54,66]（12f 等距），每张飞 22f，bezier(0.45,0.05,0.25,1.12) 末端过冲 | 少量卡（5 张）可以等距；大批量入场才需要递加速（对照 deck-deal 的 R2） |\n| 入场 | 从下方 600px 升入，交替 ±2° tilt 收平，scale 1.06→1 | 交替倾斜打破对称；同向倾斜读作复制粘贴 |\n| 堆叠压感 | 后一张到场时整摞压下 6px、8f 弹回（脉冲 [cue,cue+4,cue+8]→[0,6,0]） | 这是\"有重量\"的关键一笔，砍掉后堆叠变成简单排版 |\n| 阴影 | 空中 `0 32px 64px`、落定 `0 2px 8px` | 阴影随高度收敛，和 deck-deal/spotlight 同一套语言 |\n| 联动高亮 | 落地后滞后 2–4f 于 72% 高度处长出 40% 宽强调色（模板片为琥珀）底色（7f 长 + 5f 淡）；最后一张离切点仅 ~12f 需压缩节拍 | 高亮是次级动作（法则 5/8）：在主动作（落地压弹）之后跟进，不与之同帧抢戏；末尾元素的节拍要按剩余帧预算压缩，不能照抄前面的 |\n| glaze 扫光 | 420px 宽、rotate 14°、mixBlendMode overlay，帧 82→96 从 −700 扫到 2600 | 扫光全镜头只此一次、扫整摞不逐卡（Q4） |\n| 计数器（digit-roll 手法） | 连续里程计位置 pos：每张卡落地让 pos 沿数字带 +1 格，单格 8f 滚定，bezier(0.25,0.8,0.25,1)；滚动量=pos·lineH 沿双份 0-9 带连续下滚 | 计数与落地不同步立刻穿帮；**单格滚动必须短于落卡间距（12f）且尾格在镜头收尾前滚定**——以 count 为 key 重挂重滚的写法每次都要从 0 滚 22f，12f 节拍下前一次永远滚不完、尾帧停在 4（PR #40 判例）；数字带双份拼接+overflow hidden+tabular-nums（不开 tabular 会横向抖） |\n| 相机 | zoom 1.35→0.9 五键跟随堆叠向下 | 正视机位（见已知坑），相机只做纵向跟随 |\n\n## 声音\n每张落地不逐张配长音；计数落定钉 click-camera（模板片钉在 f648）、段落切入钉 transition-soft（f623）。若逐张配 pop 参照 S2 三招防机枪。\n\n## 已知坑\n- 堆叠/列表信息镜头必须正视（Q6）——全片倾斜化时本镜头曾被单独回滚回正视；风格化机位逐镜头验证，不全局套用\n- 逐卡 glint 高亮扫过被否两次（Q4），高亮用静态强调色底色条即可，动感交给压弹\n- 与 history-list-stack（document-typewriter-reveal 卡尾段）是同一动作词汇——全片同一手法只当一次主角（P4），两处并用时一处必须降为配角\n- anticipation/拖拽层级参数借鉴自迪士尼 12 法则（pixel2motion skill 内置提炼版），非用户判例——是默认建议不是令；与判例冲突时判例优先\n- 预备动作幅度必须过肉眼阈值（deck-deal 判例 2026-07-09：小幅度用户完全无感，放大 12 倍才通过）——渲染后自查\"不逐帧能否看出蓄力\"\n\n## 参考实现\ndemos/ui-entrance/list-stack-press/ListStackPress.tsx（原 template/src/aifl/live/ScenePapers.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/ui-entrance/list-stack-press/ListStackPress.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/list-stack-press/ListStackPress.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/ui-entrance/list-stack-press/ListStackPress.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/ui-entrance/list-stack-press/ListStackPress.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/list-stack-press/ListStackPress.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": 105,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/list-stack-press/ListStackPress.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/ui-entrance/list-stack-press/ListStackPress.tsx =====\n// list-stack-press —— 列表卡从底部逐张飞上摞起，每张落地压弹整摞、计数器同步跳格\n// \"堆叠有重量\"：每张新卡落上来，已落定的整摞被压下再弹回——物理反馈里读出\n// \"这是实打实攒下来的东西\"。参考实现从 template ScenePapers 剥离：\n// 预备拍（计数器先于首卡 4–6f 亮起微缩，把视线引到堆叠区）；5 卡 12f 等距节拍\n// 从底部 600px 升入（交替 ±2° 倾斜收平 + scale 1.06→1）；后一张到场时整摞压下\n// 6px、8f 弹回（stackPress 脉冲——\"有重量\"的关键一笔）；落地后高亮条滞后 2–4f\n// 长出；收尾一道 glaze 扫光掠过整摞；屏幕空间 DigitRoll 计数器落一张滚一格。\n// 正视机位（堆叠/列表镜头必须正视，Q6），相机跟随堆叠向下。\nimport { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame, Easing } from 'remotion';\nimport { PageCam2D, CamKey2D } from '../../_fixtures/PageCam2D';\nimport layout from '../../_textures/live-layout.json';\n\nexport const LIST_STACK_PRESS_DURATION = 88; // 18–88f 落在 shot 内 offset 0\n\nconst cards = layout.papers.cards;\nconst PAGE_H = layout.papers.pageH;\nconst MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';\nconst AMBER = 'oklch(52% 0.115 65)';\nconst FILES = ['paper1.png', 'paper2.png', 'paper3.png', 'paper4.png', 'paper5.png'];\n\nconst CUES = [6, 18, 30, 42, 54]; // 首卡前留 6f 给计数器预备拍（anticipation）\nconst DUR = 22;\nconst FLY_EASE = Easing.bezier(0.45, 0.05, 0.25, 1.12);\nconst TILTS = [2, -2, 2, -2, 2];\n\n// 预备拍：计数器先于首卡亮起 + 微缩 (0.96→1)，把视线引到堆叠区（段落级一次）\nconst ANTICIPATE_START = 0;\nconst ANTICIPATE_END = 6;\n\nconst CAM_KEYS: CamKey2D[] = [\n  { frame: 0, cx: 960, cy: 270, zoom: 1.35 },\n  { frame: 12, cx: 960, cy: 330, zoom: 1.15 },\n  { frame: 18, cx: 960, cy: 520, zoom: 1.02 },\n  { frame: 56, cx: 960, cy: 820, zoom: 0.95 },\n  { frame: 82, cx: 960, cy: 860, zoom: 0.9 },\n];\n\n// odometer digit column —— 连续里程计，不重挂：`pos` 是数字带上的连续位置\n// （0→5），每张卡落地让 pos 多滚进一格。单格滚动必须短于 12f 落卡间距、\n// 且最后一格在镜头结束前落定——key 重挂从 0 重滚 22f 的写法两条都踩\n// （前一格永远滚不完、尾帧停在 4）。\nconst DIGITS = '0123456789';\nconst ROLL_DUR = 8; // 单格滚动帧数，< 12f 落卡间距\nconst DigitRoll: React.FC<{ pos: number; lineH: number; color: string }> = ({ pos, lineH, color }) => (\n  <span style={{ display: 'inline-flex', overflow: 'hidden', height: lineH, verticalAlign: 'bottom' }}>\n    <span style={{ display: 'inline-block', height: lineH }}>\n      <span style={{ display: 'block', transform: `translateY(${-pos * lineH}px)` }}>\n        {(DIGITS + DIGITS).split('').map((d, j) => (\n          <span key={j} style={{ display: 'block', fontSize: 96, lineHeight: `${lineH}px`, color, fontVariantNumeric: 'tabular-nums' }}>\n            {d}\n          </span>\n        ))}\n      </span>\n    </span>\n  </span>\n);\n\nexport const ListStackPress: React.FC = () => {\n  const frame = useCurrentFrame();\n  // 计数器连续位置：每张卡落地（cue+DUR）时向前滚一格，8f 滚定。\n  // 最后一张 f76 落地、f84 滚定，早于 f88 镜头结束。\n  let rollPos = 0;\n  for (const c of CUES) {\n    rollPos += interpolate(frame, [c + DUR, c + DUR + ROLL_DUR], [0, 1], {\n      extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.25, 0.8, 0.25, 1),\n    });\n  }\n\n  // 预备拍：计数器 0→6f 从 scale 0.96 微缩回 1 并亮起，视线先引到堆叠区\n  const antT = interpolate(frame, [ANTICIPATE_START, ANTICIPATE_END], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.quad),\n  });\n  const counterScale = 0.96 + 0.04 * antT;\n  const counterOpacity = 0.3 + 0.7 * antT;\n\n  // when a *later* card enters, the settled stack gets pressed down 6px then\n  // springs back over ~8 frames\n  const stackPress = (settledIndex: number) => {\n    let press = 0;\n    for (let j = settledIndex + 1; j < CUES.length; j++) {\n      const cue = CUES[j];\n      const p = interpolate(frame, [cue, cue + 4, cue + 8], [0, 6, 0], {\n        extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n      });\n      press = Math.max(press, p);\n    }\n    return press;\n  };\n\n  // glaze sweep once everything has landed (~78)\n  const glazeX = interpolate(frame, [64, 78], [-700, 2600], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.45, 0, 0.35, 1),\n  });\n  const glazeVis = interpolate(frame, [63, 68, 74, 77], [0, 1, 1, 0], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: '#faf7f2' }}>\n      <PageCam2D src=\"textures/live/papers-full.png\" pageH={PAGE_H} keys={CAM_KEYS}>\n        {/* cover the printed cards so blank slots stack in */}\n        {cards.map((c, i) => (\n          <div\n            key={`slot-${i}`}\n            style={{\n              position: 'absolute', left: c.x - 12, top: c.y - 10,\n              width: c.w + 24, height: c.h + 20, background: '#faf7f2',\n              opacity: frame >= CUES[i] + DUR - 2 ? 0 : 1,\n            }}\n          />\n        ))}\n\n        {cards.map((c, i) => {\n          const cue = CUES[i];\n          const t = interpolate(frame, [cue, cue + DUR], [0, 1], {\n            extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: FLY_EASE,\n          });\n          if (t <= 0) return null;\n          const settled = t >= 0.999;\n          const dy = 600 * (1 - t) + (settled ? stackPress(i) : 0);\n          const rot = TILTS[i] * (1 - t);\n          const scale = 1.06 - 0.06 * t;\n          const shadow = settled\n            ? '0 2px 8px rgba(60,45,30,.10)'\n            : `0 32px 64px rgba(60,45,30,${0.22 * (1 - t) + 0.06})`;\n\n          // amber highlight over the linked-project name, after landing\n          const hlStart = cue + DUR;\n          const hlGrow = interpolate(frame, [hlStart, hlStart + 7], [0, 1], {\n            extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.3, 0, 0.2, 1),\n          });\n          const hlFade = interpolate(frame, [hlStart + 7, hlStart + 12], [1, 0], {\n            extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n          });\n\n          return (\n            <div\n              key={FILES[i]}\n              style={{\n                position: 'absolute', left: c.x, top: c.y, width: c.w, height: c.h,\n                transform: `translateY(${dy}px) rotate(${rot}deg) scale(${scale})`,\n                transformOrigin: 'center center', boxShadow: shadow, borderRadius: 12,\n              }}\n            >\n              <Img src={staticFile(`textures/live/${FILES[i]}`)} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', display: 'block' }} />\n              {hlGrow > 0 && hlFade > 0 ? (\n                <div\n                  style={{\n                    position: 'absolute', left: 20, top: c.h * 0.72,\n                    width: `${hlGrow * 40}%`, height: 30,\n                    background: 'oklch(97% 0.028 85)', opacity: 0.6 * hlFade,\n                    borderRight: `2px solid ${AMBER}`, pointerEvents: 'none',\n                  }}\n                />\n              ) : null}\n            </div>\n          );\n        })}\n\n        {/* page-space glaze sweep at the end */}\n        <div\n          style={{\n            position: 'absolute', top: 40, height: PAGE_H - 80, left: glazeX, width: 420,\n            transform: 'rotate(14deg)', opacity: glazeVis * 0.5, mixBlendMode: 'overlay',\n            background: 'linear-gradient(90deg, transparent, rgba(255,240,214,0.9) 45%, rgba(255,240,214,0.9) 55%, transparent)',\n            pointerEvents: 'none',\n          }}\n        />\n      </PageCam2D>\n\n      {/* screen-space counter, top-right — lands one digit per card */}\n      <div\n        style={{\n          position: 'absolute', top: 70, right: 96, textAlign: 'right',\n          pointerEvents: 'none', opacity: counterOpacity,\n          transform: `scale(${counterScale})`, transformOrigin: 'top right',\n        }}\n      >\n        <div style={{ fontFamily: MONO, fontSize: 24, letterSpacing: '0.16em', color: 'oklch(50% 0.006 82)', textTransform: 'uppercase' }}>\n          Paper Radar\n        </div>\n        <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 6 }}>\n          <DigitRoll pos={rollPos} lineH={96 * 1.15} color={AMBER} />\n        </div>\n        <div style={{ fontFamily: MONO, fontSize: 20, letterSpacing: '0.12em', color: 'oklch(50% 0.006 82)', marginTop: 4, textTransform: 'uppercase' }}>\n          Of 31 Fetched Today\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n\n"
}