{
  "slug": "card-flock-tumble",
  "title": "Card Flock Tumble Climax",
  "tagline": "Three UI page cards tumble in 3D from their thin edges into a staggered lineup (fully sharp, seamless spline motion throughout), keep slowly rotating once settled, then snap-collapse into the center, bursting into a single turbulent smoke ring as a giant wordmark cuts across.",
  "description": "Three UI page cards tumble in 3D from their thin edges into a staggered lineup (fully sharp, seamless spline motion throughout), keep slowly rotating once set",
  "category": "transition",
  "tags": [
    "shotcraft",
    "remotion",
    "transition"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.39,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/card-flock-tumble/poster-2d46403cc99b.jpg",
    "video": "https://gimgs.net/motion/media/card-flock-tumble/video-a358216b4ad5.mp4"
  },
  "variants": [
    {
      "key": "card-flock-tumble",
      "label": "card-flock-tumble",
      "description": "Three UI page cards tumble in 3D from their thin edges into a staggered lineup (fully sharp, seamless spline motion throughout), keep slowly rotating once settled, then snap-collapse into the center, bursting into a single turbulent smoke ring as a giant wordmark cuts across.",
      "use": "",
      "video": "https://gimgs.net/motion/media/card-flock-tumble/video-a358216b4ad5.mp4",
      "poster": "https://gimgs.net/motion/media/card-flock-tumble/poster-2d46403cc99b.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/card-flock-tumble.html",
  "json_url": "https://gimgs.net/motion/card-flock-tumble.json",
  "template_url": "https://gimgs.net/motion/card-flock-tumble/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/card-flock-tumble.md",
  "updated_at": "2026-09-24T07:39:47.488Z",
  "prompt": "## When to use\n\nA high-energy climax segment (feature-page cluster → brand slogan burst transition); a neon dark-mode tone; a segment where \"multi-page capability\" collapses into one line.\n\n## Intention\n\nThree page cards tumble through 3D space like a fanned-out deck of cards being thrown, lining up in a staggered stance, then get instantly sucked into the center and burst into a smoke ring, with a giant brand wordmark punching through it — \"capability showcase → energy collapse → slogan burst\" in one continuous motion. Three things are critical: seamlessness (from thin-edge to tumble to settling, one continuous Catmull-Rom spline with continuous derivatives — any segmented-interpolation jerkiness will be noticed); never stop (after settling, keep a low angular spin going right up to the collapse — the stillness segment was cut deliberately, \"don't stop, keep spinning\" — the climax segment has no freeze frames); and full sharpness throughout (all motion blur/depth-of-field blur removed — \"do not add blur effects\" was a deliberate note).\n\n## Duration & energy\n\n- Duration: Tumble ~1.5s + slow-spin showcase ~0.3s + collapse 0.3s + smoke ring + giant text ~2s; full segment 4.5s.\n- Energy: Extremely high (used at the film's absolute energy peak).\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n三张页卡像被甩出的扑克在 3D 空间翻飞列队，站定亮相后被瞬间\n吸入中心，炸出一圈烟雾环，品牌巨字穿环而出——\"能力展示→能量\n坍缩→口号爆发\"一气呵成。命门有三：**丝滑**（侧棱→翻飞→站定\n一条 Catmull-Rom 样条导数连续，分段插值的顿挫会被读出来）、\n**不许停**（站定后保持低角速度慢转到收束，静止段被裁\"不要停住，\n保持旋转\"——高潮段没有静帧）、**全程清晰**（motion blur/景深糊\n全删，被裁\"不要加模糊效果\"）。\n\n## 动效核心\n- 翻飞：三卡各自 k0（近 90° 侧棱薄边）→k1（翻飞中段）→k2（阶梯\n  站定）Catmull-Rom 样条，Easing.out(cubic) 驱动，~44f\n- 站定慢转：idle 漂移 ry +0.34°/f、rx -0.1°/f、rz +0.05°/f（14f\n  平方缓入避免与样条衔接顿挫），到收束时刻不停\n- 收束：10f Easing.in(quad) 从慢转姿态直接加速吸入中心缩小，\n  透明度前 60% 满格（\"吸入\"运动要清晰可读）\n- 烟雾环：**单个**（多圈被裁），feTurbulence 三组位移（絮边/亮斑/\n  暗斑）+上桃下紫渐变——破碎絮边、明暗斑块、减速外扩缓慢长大、\n  衰减极慢驻留，中心无水面光；粒子效果零残留（被裁）\n- 巨字：横贯全屏渐显+微放大，霓虹渐变描边\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 三卡间距/大小 | 大幅重叠成一叠阶梯、scale 1.6–1.74 | 两轮加码判例：\"再靠近一些，再大些\"——阶梯要读作一组不是三张 |\n| 站定慢转 | ry ~0.34°/f 连续 | **不许静止**（判例）；>1°/f 读作还没站定 |\n| 收束时长 | 10f ease-in | 22f 版被裁\"要快\"；透明度早退=吸入不可读（返工判例） |\n| 烟雾环 | 单圈、湍流絮边、衰减极慢 | 多圈被裁；平滑霓虹圈/粒子版被裁——对照原片密帧做湍流质感 |\n| 模糊 | 全程零 blur | 翻飞/收束加 blur 均被裁 |\n| 样条 | 一条 Catmull-Rom 贯穿 | 分段 lerp 有速度台阶，\"丝滑流畅\"是裁决原话 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 烟雾环质感判例：干净图形环（描边圆/同心波）三轮被裁，最终版\n  feTurbulence 湍流絮边对照原片密帧逐格校——\"要一样\"；残余差距\n  是噪声场静态（整体旋转+长大），无逐帧湍流滚动，极近看流动感弱\n- 与 particle-celebrate-hits 分工：那张是粒子礼花打击点；本卡\n  烟雾环是能量坍缩的余波，禁加粒子\n- 与 bubble-swarm-takeover 分工：那张是群体涌入接管；本卡是\n  三卡精确列队+坍缩，数量意义相反\n\n## 参考实现\ndemos/transition/card-flock-tumble/\n（CardFlockTumble.tsx）\n原片出处：clickup-30.mp4\n\n## Reference implementation (Remotion)\n\n- demos/transition/card-flock-tumble/CardFlockTumble.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/card-flock-tumble/CardFlockTumble.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/transition/card-flock-tumble/CardFlockTumble.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/transition/card-flock-tumble/CardFlockTumble.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/card-flock-tumble/CardFlockTumble.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": 132,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/card-flock-tumble/CardFlockTumble.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/transition/card-flock-tumble/CardFlockTumble.tsx =====\n// card-flock-tumble v5 —— 批次 14 按用户意见再改（承 v4）：\n// 用户意见（逐字）：\"首先三个页面不要加模糊效果；然后那个水波扩散一个\n// 圆环就够，不是多个；但是效果你截图看看原片的样式，要一样\"\n// 落实：①三卡全程清晰——翻飞中段 motion blur、收束段 blur 全删；\n// ②多圈同心水波删掉，只留一个环；③环样式对照原片密帧（12fps）重做：\n//    原片是单个湍流烟雾环——边缘破碎起絮、环身明暗斑块交错、粉紫为主\n//    顶部偏奶桃色、出现后减速外扩且全程缓慢长大、衰减极慢（片尾仍在，\n//    弥散变淡而非熄灭）、中心无水面光。用 feTurbulence+位移贴图模拟。\nimport React, { useId } from 'react';\nimport { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';\n\nconst mulberry32 = (a: number) => () => {\n  let t = (a += 0x6d2b79f5);\n  t = Math.imul(t ^ (t >>> 15), t | 1);\n  t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n};\n\nconst FONT = '\"Avenir Next\", Futura, \"Helvetica Neue\", sans-serif';\n\n// ---------- 时间轴（30fps / 130f） ----------\nconst WALL_UP = [6, 22] as const; // 墙亮起\nconst FLIGHT = [10, 54] as const; // 侧棱→翻飞→站定：一条连续样条\nconst CARD_OUT = [62, 72] as const; // 收束（快！10 帧向中心聚拢）\nconst RING_T0 = 70; // 波纹环自收束点扩散\nconst TEXT_T0 = 84; // STRONGER 出现\n\n// ---------- 霓虹描边文字墙（S2：黄左→品红→紫→蓝右） ----------\nconst ROW_GRADS: [string, string][] = [\n  ['#ffe14d', '#5ad0ff'],\n  ['#ff5ad0', '#7d8bff'],\n  ['#ffb84d', '#b46bff'],\n];\nconst NeonWall: React.FC<{ frame: number }> = ({ frame }) => {\n  const up = interpolate(frame, [WALL_UP[0], WALL_UP[1]], [0.12, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const out = interpolate(frame, [CARD_OUT[0], CARD_OUT[1] + 4], [1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const blurOut = interpolate(frame, [CARD_OUT[0], CARD_OUT[1] + 4], [0, 26], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  if (out <= 0.01) return null;\n  const drift = frame * 2.0;\n  return (\n    <AbsoluteFill style={{ overflow: 'hidden', opacity: up * out, filter: `blur(${blurOut}px)` }}>\n      {Array.from({ length: 3 }).map((_, row) => {\n        const [cL, cR] = ROW_GRADS[row];\n        return (\n          <div\n            key={row}\n            style={{\n              position: 'absolute',\n              top: -140 + row * 380,\n              left: 0,\n              whiteSpace: 'nowrap',\n              fontFamily: FONT,\n              fontWeight: 800,\n              fontStyle: 'italic',\n              fontSize: 330,\n              letterSpacing: 6,\n              transform: `translateX(${(row % 2 === 0 ? -1 : 1) * drift - 600}px)`,\n            }}\n          >\n            {Array.from({ length: 4 }).map((_, i) => (\n              <span\n                key={i}\n                style={{\n                  marginRight: 70,\n                  color: 'transparent',\n                  WebkitTextStroke: `4px ${i % 2 === 0 ? cL : cR}`,\n                  filter: `drop-shadow(0 0 18px ${i % 2 === 0 ? cL : cR})`,\n                  opacity: 0.6,\n                }}\n              >\n                FASTER\n              </span>\n            ))}\n          </div>\n        );\n      })}\n      <AbsoluteFill style={{ background: 'radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,0.72) 85%)' }} />\n    </AbsoluteFill>\n  );\n};\n\n// ---------- ClickUp 风格 UI 卡 ----------\nconst UiCard: React.FC<{ seed: number; title: string }> = ({ seed, title }) => (\n  <div\n    style={{\n      width: 560,\n      height: 400,\n      background: '#fbfbfc',\n      borderRadius: 14,\n      padding: 0,\n      boxSizing: 'border-box',\n      boxShadow: '0 0 60px rgba(190,140,255,0.3), 0 22px 60px rgba(0,0,0,0.55)',\n      display: 'flex',\n      overflow: 'hidden',\n    }}\n  >\n    <div style={{ width: 128, background: '#f3f3f6', padding: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>\n      <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>\n        <div style={{ width: 14, height: 14, borderRadius: 4, background: 'linear-gradient(135deg,#7b68ee,#ff5ad0)' }} />\n        <div style={{ height: 8, width: 52, background: '#c9c9d2', borderRadius: 4 }} />\n      </div>\n      {Array.from({ length: 8 }).map((_, i) => (\n        <div key={i} style={{ height: 7, width: `${52 + ((i * 31 + seed * 17) % 42)}%`, background: '#d9d9df', borderRadius: 4 }} />\n      ))}\n    </div>\n    <div style={{ flex: 1, padding: 18, display: 'flex', flexDirection: 'column', gap: 10 }}>\n      <div style={{ fontFamily: FONT, fontWeight: 700, fontSize: 26, color: '#3a3a44' }}>{title}</div>\n      <div style={{ height: 10, width: '58%', background: '#ececf1', borderRadius: 5 }} />\n      {Array.from({ length: 6 }).map((_, i) => (\n        <div key={i} style={{ display: 'flex', gap: 8, alignItems: 'center' }}>\n          <div style={{ width: 11, height: 11, borderRadius: '50%', background: ['#7b68ee', '#ff5ad0', '#5ad0ff'][(i + seed) % 3], opacity: 0.7 }} />\n          <div style={{ height: 8, width: `${78 - ((i * 23 + seed * 29) % 40)}%`, background: '#e8e8ee', borderRadius: 4 }} />\n        </div>\n      ))}\n      <div style={{ marginTop: 'auto', display: 'flex', gap: 8 }}>\n        <div style={{ width: 74, height: 22, background: '#7b68ee', opacity: 0.75, borderRadius: 6 }} />\n        <div style={{ width: 46, height: 22, background: '#e4e4ea', borderRadius: 6 }} />\n      </div>\n    </div>\n  </div>\n);\n\n// ---------- 单个湍流烟雾环（对照原片密帧重做） ----------\n// 原片形态：只有一个环；边缘破碎起絮（湍流位移）、环身明暗斑块交错；\n// 粉紫为主、顶部奶桃色；出现后减速外扩且全程缓慢长大；衰减极慢——\n// 片尾仍清晰可见（弥散变淡而非熄灭）；中心无水面光、背景纯黑。\nconst SmokeRing: React.FC<{ frame: number }> = ({ frame }) => {\n  // 滤镜/渐变 ID 按实例生成，多实例同场不串引（useId 的 «:» 在 url() 里非法，需清洗）\n  // hooks 必须在条件 return 之前调用\n  const uid = useId().replace(/[^a-zA-Z0-9]/g, '');\n  const t = frame - RING_T0;\n  if (t < 0) return null;\n  // 半径：小出现→减速外扩，之后仍缓慢长大（原片全程未停）\n  const grow = Easing.out(Easing.cubic)(Math.min(1, t / 52));\n  const R = 46 + 330 * grow + Math.max(0, t - 52) * 1.6;\n  // 衰减极慢：快速浮现后长期驻留，只轻微变淡弥散\n  const op = interpolate(t, [0, 5, 30, 60], [0, 1, 0.92, 0.72], { extrapolateRight: 'clamp' });\n  // 环身宽度：随半径同步长大，相对占比略微摊薄（早期 ~R/3）\n  const w = R * interpolate(t, [0, 52], [0.36, 0.27], { extrapolateRight: 'clamp' });\n  // 湍流位移幅度随环长大而放大（絮块尺寸跟环成比例）\n  const disp = 60 + grow * 90;\n  const rot = t * 0.35; // 纹理缓慢流动感\n  return (\n    <AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center' }}>\n      <svg width={1920} height={1080} viewBox=\"0 0 1920 1080\" style={{ position: 'absolute', inset: 0, overflow: 'visible' }}>\n        <defs>\n          {/* 破碎絮状边缘：分形噪声位移主体（octaves=4 → 更细的絮丝） */}\n          <filter id={`smokeA-${uid}`} x=\"-60%\" y=\"-60%\" width=\"220%\" height=\"220%\">\n            <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.013 0.016\" numOctaves={4} seed={11} result=\"n\" />\n            <feDisplacementMap in=\"SourceGraphic\" in2=\"n\" scale={disp} xChannelSelector=\"R\" yChannelSelector=\"G\" />\n          </filter>\n          {/* 第二组噪声（不同种子）：亮斑错位层 */}\n          <filter id={`smokeB-${uid}`} x=\"-60%\" y=\"-60%\" width=\"220%\" height=\"220%\">\n            <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.021 0.018\" numOctaves={4} seed={37} result=\"n\" />\n            <feDisplacementMap in=\"SourceGraphic\" in2=\"n\" scale={disp * 0.85} xChannelSelector=\"R\" yChannelSelector=\"G\" />\n          </filter>\n          {/* 第三组噪声：暗斑洞隙层（压出环身明暗交错的黑斑） */}\n          <filter id={`smokeC-${uid}`} x=\"-60%\" y=\"-60%\" width=\"220%\" height=\"220%\">\n            <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.019 0.023\" numOctaves={3} seed={73} result=\"n\" />\n            <feDisplacementMap in=\"SourceGraphic\" in2=\"n\" scale={disp * 1.1} xChannelSelector=\"R\" yChannelSelector=\"G\" />\n          </filter>\n          {/* 顶部奶桃色、下部粉紫的环身渐变（降饱和，对照原片灰粉调） */}\n          <linearGradient id={`ringGrad-${uid}`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n            <stop offset=\"0%\" stopColor=\"hsla(28 45% 78% / 0.85)\" />\n            <stop offset=\"35%\" stopColor=\"hsla(315 45% 74% / 0.85)\" />\n            <stop offset=\"100%\" stopColor=\"hsla(276 42% 66% / 0.85)\" />\n          </linearGradient>\n        </defs>\n        <g transform={`rotate(${rot} 960 540)`} opacity={op}>\n          {/* 外圈弥散柔光 */}\n          <g style={{ filter: `url(#smokeA-${uid})` }}>\n            <circle cx={960} cy={540} r={R} fill=\"none\" stroke=\"hsla(295 40% 68% / 0.26)\" strokeWidth={w * 1.9} style={{ filter: 'blur(22px)' }} />\n          </g>\n          {/* 环身主体（湍流位移 → 破碎絮状边缘；blur 收小保留湍流纹理） */}\n          <g style={{ filter: `url(#smokeA-${uid})` }}>\n            <circle cx={960} cy={540} r={R} fill=\"none\" stroke={`url(#ringGrad-${uid})`} strokeWidth={w} style={{ filter: 'blur(9px)' }} opacity={0.85} />\n          </g>\n          {/* 亮斑层：另一组噪声错位叠加 */}\n          <g style={{ filter: `url(#smokeB-${uid})` }}>\n            <circle cx={960} cy={540} r={R * 0.99} fill=\"none\" stroke=\"hsla(310 60% 86% / 0.6)\" strokeWidth={w * 0.45} style={{ filter: 'blur(6px)' }} />\n          </g>\n          {/* 暗斑洞隙：第三组噪声轻压环身 → 明暗斑块交错（轻微，勿成迷彩） */}\n          <g style={{ filter: `url(#smokeC-${uid})` }}>\n            <circle cx={960} cy={540} r={R * 1.005} fill=\"none\" stroke=\"hsla(262 40% 10% / 0.32)\" strokeWidth={w * 0.4} style={{ filter: 'blur(7px)' }} />\n          </g>\n        </g>\n      </svg>\n    </AbsoluteFill>\n  );\n};\n\n// ---------- 卡片位姿：Catmull-Rom 样条连续插值（真 3D 丝滑转动） ----------\ntype Pose = { x: number; y: number; rx: number; ry: number; rz: number; s: number };\nconst POSE_KEYS: (keyof Pose)[] = ['x', 'y', 'rx', 'ry', 'rz', 's'];\n\n// Catmull-Rom（端点重复），保证经过所有关键姿态且导数连续 → 无分段突变\nconst catmull = (p0: number, p1: number, p2: number, p3: number, t: number): number => {\n  const t2 = t * t;\n  const t3 = t2 * t;\n  return (\n    0.5 *\n    (2 * p1 +\n      (-p0 + p2) * t +\n      (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 +\n      (-p0 + 3 * p1 - 3 * p2 + p3) * t3)\n  );\n};\nconst splinePose = (keys: Pose[], u: number): Pose => {\n  // keys.length === 3：两段样条，0.55 处过中间关键帧\n  const seg = u < 0.55 ? 0 : 1;\n  const lt = seg === 0 ? u / 0.55 : (u - 0.55) / 0.45;\n  const out = {} as Pose;\n  for (const k of POSE_KEYS) {\n    const v0 = keys[Math.max(0, seg - 1)][k];\n    const v1 = keys[seg][k];\n    const v2 = keys[seg + 1][k];\n    const v3 = keys[Math.min(2, seg + 2)][k];\n    out[k] = catmull(v0, v1, v2, v3, lt);\n  }\n  return out;\n};\n\n// 三卡（v4 再加码）：间距再收紧（±125/±85 级，三卡大幅重叠成一叠阶梯）\n// + 站定再放大（s≈1.5-1.6，对齐截图 3/4 里卡片群占满画面中部）\n// k0 侧棱（近 90° 薄边）→ k1 翻飞中段 → k2 阶梯站定\nconst CARDS: { title: string; k: [Pose, Pose, Pose]; conv: Pose }[] = [\n  {\n    title: 'Inbox',\n    k: [\n      { x: -8, y: -16, rx: 9, ry: 88, rz: 12, s: 1.05 },\n      { x: -135, y: -92, rx: 16, ry: 44, rz: -8, s: 1.38 },\n      { x: -108, y: -76, rx: 4, ry: 13, rz: -2, s: 1.62 },\n    ],\n    conv: { x: -20, y: -12, rx: 0, ry: 55, rz: 4, s: 0.12 },\n  },\n  {\n    title: 'List view',\n    k: [\n      { x: 0, y: 0, rx: 8, ry: 89, rz: 12, s: 1.0 },\n      { x: -16, y: -7, rx: 13, ry: 38, rz: -7, s: 1.42 },\n      { x: -5, y: -2, rx: 3, ry: 11, rz: -2, s: 1.68 },\n    ],\n    conv: { x: 0, y: 0, rx: 0, ry: 60, rz: 4, s: 0.12 },\n  },\n  {\n    title: 'Home',\n    k: [\n      { x: 8, y: 16, rx: 7, ry: 90, rz: 12, s: 0.95 },\n      { x: 112, y: 78, rx: 11, ry: 34, rz: -6, s: 1.46 },\n      { x: 90, y: 70, rx: 2, ry: 9, rz: -1, s: 1.74 },\n    ],\n    conv: { x: 15, y: 10, rx: 0, ry: 65, rz: 4, s: 0.12 },\n  },\n];\nconst lerpPose = (a: Pose, b: Pose, t: number): Pose => {\n  const out = {} as Pose;\n  for (const k of POSE_KEYS) out[k] = a[k] + (b[k] - a[k]) * t;\n  return out;\n};\n\nexport const CardFlockTumble: React.FC = () => {\n  const frame = useCurrentFrame();\n  // 渐变 ID 按实例生成，多实例同场不串引\n  const gradId = `strokeGrad-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;\n\n  // STRONGER 巨字\n  const st = frame - TEXT_T0;\n  const textScale = interpolate(st, [0, 34], [0.6, 1.0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n  const textOpacity = interpolate(st, [0, 12, 34], [0, 0.8, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <AbsoluteFill style={{ background: '#060509' }}>\n      <NeonWall frame={frame} />\n\n      {/* 卡片群 */}\n      {frame < CARD_OUT[1] + 2 && (\n        <AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center', perspective: 1400 }}>\n          {CARDS.map((c, i) => {\n            let pose: Pose;\n            let op = 1;\n            // v6（批次 15）：用户意见\"三个页面收缩之前不要停住，保持旋转\n            // （只不过旋转放慢\"——站定后到收缩前 idle 慢转（低角速度连续\n            // 漂移，平方缓入避免与样条衔接处顿挫），收缩从漂移后姿态出发\n            const idleAt = (f: number) => {\n              const t = Math.min(f, CARD_OUT[0]) - FLIGHT[1] * 0.86;\n              if (t <= 0) return { ry: 0, rx: 0, rz: 0 };\n              const ramp = Math.min(1, t / 14) ** 2;\n              return { ry: t * 0.34 * ramp, rx: t * -0.1 * ramp, rz: t * 0.05 * ramp };\n            };\n            const drift = idleAt(frame);\n            if (frame < FLIGHT[0]) {\n              pose = c.k[0];\n            } else if (frame < CARD_OUT[0]) {\n              // 全程一条样条：侧棱→翻飞→站定，导数连续，丝滑 3D 转动\n              // （用户意见①：页面全程清晰，翻飞段 motion blur 已删）\n              const raw = Math.min(1, (frame - FLIGHT[0]) / (FLIGHT[1] - FLIGHT[0]));\n              const u = Easing.out(Easing.cubic)(raw);\n              pose = splinePose(c.k, u);\n              pose = { ...pose, ry: pose.ry + drift.ry, rx: pose.rx + drift.rx, rz: pose.rz + drift.rz };\n            } else {\n              // 收束：快！10 帧加速向中心聚拢缩小（收束段 blur 也删，全程清晰）；\n              // 透明度前 60% 保持满格，让\"吸入中心\"的运动清晰可读\n              const r = Math.min(1, (frame - CARD_OUT[0]) / (CARD_OUT[1] - CARD_OUT[0]));\n              const e = Easing.in(Easing.quad)(r);\n              const from = { ...c.k[2], ry: c.k[2].ry + drift.ry, rx: c.k[2].rx + drift.rx, rz: c.k[2].rz + drift.rz };\n              pose = lerpPose(from, c.conv, e);\n              op = 1 - Easing.in(Easing.cubic)(Math.max(0, (r - 0.55) / 0.45));\n            }\n            return (\n              <div\n                key={i}\n                style={{\n                  position: 'absolute',\n                  transform: `translate3d(${pose.x}px, ${pose.y}px, 0) rotateX(${pose.rx}deg) rotateY(${pose.ry}deg) rotateZ(${pose.rz}deg) scale(${pose.s})`,\n                  opacity: op,\n                  zIndex: 10 + i,\n                }}\n              >\n                <UiCard seed={i} title={c.title} />\n              </div>\n            );\n          })}\n        </AbsoluteFill>\n      )}\n\n      {/* 单个湍流烟雾环（对照原片密帧样式） */}\n      <SmokeRing frame={frame} />\n\n      {/* STRONGER 巨字横贯全屏（S6） */}\n      {st >= 0 && (\n        <AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center' }}>\n          <svg\n            width={1920}\n            height={560}\n            viewBox=\"0 0 1920 560\"\n            style={{\n              transform: `scale(${textScale})`,\n              opacity: textOpacity,\n              filter: 'drop-shadow(0 0 26px rgba(190,110,255,0.55))',\n              overflow: 'visible',\n            }}\n          >\n            <defs>\n              <linearGradient id={gradId} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n                <stop offset=\"0%\" stopColor=\"#ffe14d\" />\n                <stop offset=\"30%\" stopColor=\"#ff5ad0\" />\n                <stop offset=\"60%\" stopColor=\"#b46bff\" />\n                <stop offset=\"100%\" stopColor=\"#5ad0ff\" />\n              </linearGradient>\n            </defs>\n            <text\n              x=\"960\"\n              y=\"360\"\n              textAnchor=\"middle\"\n              fontFamily={FONT}\n              fontWeight={800}\n              fontStyle=\"italic\"\n              fontSize={352}\n              letterSpacing={2}\n              fill=\"none\"\n              stroke={`url(#${gradId})`}\n              strokeWidth={4.5}\n            >\n              STRONGER\n            </text>\n          </svg>\n        </AbsoluteFill>\n      )}\n    </AbsoluteFill>\n  );\n};\n\n\n"
}