{
  "slug": "panel-grid-moves",
  "title": "Panel grid rhythm moves",
  "tagline": "Three grid-rhythm techniques: flash-fill a grid, reshuffle it, or line up angled panels — all riding the beat.",
  "description": "Three grid-rhythm techniques: flash-fill a grid, reshuffle it, or line up angled panels — all riding the beat.",
  "category": "rhythm",
  "tags": [
    "shotcraft",
    "remotion",
    "rhythm",
    "ui-entrance"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.72,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/panel-grid-moves/poster-e18c57dd1677.jpg",
    "video": "https://gimgs.net/motion/media/panel-grid-moves/video-e722cc4891b8.mp4"
  },
  "variants": [
    {
      "key": "grid-flash-mosaic",
      "label": "grid-flash-mosaic",
      "description": "The 3×3 grid mounts one cell every 2f in an h(i)-scrambled order with a hard cut-in (each cell scales 1.18→1 over 3f plus a 2f deepening pulse); once full the grid holds a breathing beat, then the center cell scales up 3.28x over 14f with Easing.in(cubic) to swallow the screen.",
      "use": "Full feature-matrix reveal in one second; build-up before a climax",
      "video": "https://gimgs.net/motion/media/panel-grid-moves/video-e722cc4891b8.mp4",
      "poster": "https://gimgs.net/motion/media/panel-grid-moves/poster-e18c57dd1677.jpg"
    },
    {
      "key": "flip-grid-reflow",
      "label": "flip-grid-reflow",
      "description": "Two coordinate tables are pre-written (row layout / 3×2 grid); each card flies in a straight line with delay=i×1.5f over 16f inOut cubic plus a scale 1→1.28 with a 1.02 overshoot; once settled a 6f brightness-0.78 pulse hits the whole frame.",
      "use": "Layout narrative: a semantic shift from side-by-side to grouped",
      "video": "https://gimgs.net/motion/media/panel-grid-moves/video-e86616943a9c.mp4",
      "poster": "https://gimgs.net/motion/media/panel-grid-moves/poster-flip-grid-reflow-47497f5e025c.jpg"
    },
    {
      "key": "comic-panel-split",
      "label": "comic-panel-split",
      "description": "Each of the three panels holds a full-page clip-path cut at a 12° diagonal edge plus a translate/scale camera position (1x/1.9x/2.6x), popping in one panel every 2f; they freeze for 18f with a slow push on each panel to keep it alive, then the last panel's diagonal edge expands over 12f with out-cubic to swallow the screen.",
      "use": "Same subject, multiple angles side by side; comic-book-feel sections",
      "video": "https://gimgs.net/motion/media/panel-grid-moves/video-f85c2fe26c28.mp4",
      "poster": "https://gimgs.net/motion/media/panel-grid-moves/poster-comic-panel-split-e8a0248bf9fc.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/panel-grid-moves.html",
  "json_url": "https://gimgs.net/motion/panel-grid-moves.json",
  "template_url": "https://gimgs.net/motion/panel-grid-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/panel-grid-moves.md",
  "updated_at": "2026-09-24T07:40:12.301Z",
  "prompt": "## When to use\n\nUsing the \"grid\" itself as a rhythm device: cell-by-cell reveal on the beat (A), collective repositioning on the beat (B), same-subject multi-angle freeze layout (C); all three ride the beat\n\n## Intention\n\nThe difference from wall-reveal-moves is the anchor point: that card is about \"how the whole wall enters\" (a one-time reveal), this one is about \"how the grid becomes a rhythm device\" — splitting, reshuffling, and lining up panels all happen on the beat, each grid change is a drum hit. A fills: a 3×3 grid hard-mounts cell by cell in sixteenth-note scrambled order, holds a beat once full, then the center cell scales up 3.28x over 14f with Easing.in(cubic) to swallow the screen — a full feature matrix reveal in one second. B moves: six cards collectively swap straight-line positions on the beat (row → 3×2 grid), settling in half a second plus a deepening pulse to close it out — the layout reshuffle itself is a drum hit. C aligns: the frame cuts hard into 3 diagonal panels, the same product freezes in three different camera angles at once, holds half a beat, then one panel's diagonal edge expands to swallow the screen — Scott Pilgrim's split-screen language. Choose by need: multi-feature reveal uses A, layout narrative uses B, single-product multi-angle uses C.\n\n## Duration & energy\n\n- Duration: A ~4.7s / B ~4.8s / C ~5s\n- Energy: A High / B Medium / C Medium-high\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n与 wall-reveal-moves 的区别是本卡的定位锚：那是\"整墙怎么入场\"\n（一次性揭示），这是\"格子怎么当节奏器\"——分格、重排、并列都发生\n在节拍上，格变即鼓点。A 是填：3×3 格按十六分音符乱序逐格啪啪硬入，\n填满停一拍，中心格放大吞掉全屏——功能矩阵一秒全亮相；B 是移：6 张卡\n在节拍点集体直线换位（横排→3×2 网格），半秒落定 + 加深脉冲收束——\n布局重排本身当一个鼓点；C 是并：画面咔咔切成 3 个斜线分格，同一产品\n三种机位同时定格，停半拍后一格斜边扩张吃掉全屏——Scott Pilgrim 的\n分屏语言。按需选：多功能亮相用 A，布局叙事用 B，单品多视角用 C。\n\n## 三式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A grid-flash-mosaic | 3×3 格每 2f 一格按 h(i) 乱序条件挂载硬入（入格 3f scale 1.18→1 + 2f 加深脉冲），满墙呼吸一拍，中心格 14f Easing.in(cubic) 放大 3.28x 吞屏 | 功能矩阵一秒全亮相；高潮前铺排 |\n| B flip-grid-reflow | 预写两套坐标表（横排/3×2 网格），每卡 delay=i×1.5f、16f inOut cubic 直线飞行 + scale 1→1.28 带 1.02 过冲；落定后 6f brightness 0.78 全画面脉冲 | 布局叙事：并列→分组的语义换挡 |\n| C comic-panel-split | 三格各一份整页 clip-path 12° 斜边裁切 + translate/scale 摆机位（1x/1.9x/2.6x），逐格 2f 间隔弹入；定格 18f 各格缓推保活，末格斜边 12f out-cubic 扩张吃屏 | 同一主体多视角并列；漫画感段落 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 硬入 | 未到拍点直接不渲染（条件挂载），零淡化 | 逐格硬入无淡化——命门；淡入就不是\"啪\" |\n| A 描边 | 每格 3px 深色描边（border-box） | 白底提亮不可见判例的反向应用：白卡靠描边分格 |\n| A 吞屏 | 中心格塞 0.3125x 整页，放大 3.28（>3.2）让描边滑出画外 | 描边残在收尾满屏上就穿帮 |\n| B 坐标 | 两套表预写（行 y=330 / 网格列 571.6/960/1348.4、行 416.2/663.8），纯 transform | 免真 FLIP 量测；帧确定无随机 |\n| B 交错 | 偶数索引上排、奇数下排，轨迹互不对穿 | 乱排轨迹会恶性穿帮 |\n| B 脉冲 | f58–64 brightness 0.78，filter 仅窗口内条件挂载 | 摘罩判例；窗口外挂 filter 毁真静止 |\n| C 斜缝 | SVG 双线：16px 墨 #2f2f2f 底 + 10px 白核，12° 斜角 | 斜缝白线是漫画感命门；直缝读作分屏工具 |\n| C 保活 | 定格 18f 各格 scale 缓推 +3%/+5.5%/+8% | 三格全静读作截图拼贴 |\n| 收尾 | A 68f / B 81f / C 93f 真静止（≥40f） | R1；C 摘罩帧与扩张终值对齐防跳变 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- C 三格必须是**同一主体的不同机位**——三格塞不同内容读作 bento\n  排版不是漫画分格，语义就散了\n- 三式都吃拍点、声音强依赖：A 逐格一声啪、B 起飞与脉冲各一声、\n  C 逐格咔咔三声（sound-design §4.5）；不踩拍的分格只是布局动画\n- 同片分格系 ≤1 段——格子语言辨识度太强，两段就撞\n- A 与 wall-reveal-moves 别同片连用（都是\"墙\"，观众分不出\n  入场还是节奏器）\n\n## 参考实现\ndemos/rhythm/panel-grid-moves/\n（ComicPanelSplit.tsx / FlipGridReflow.tsx / GridFlashMosaic.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx\n- demos/rhythm/panel-grid-moves/FlipGridReflow.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/panel-grid-moves/FlipGridReflow.tsx\n- demos/rhythm/panel-grid-moves/GridFlashMosaic.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/panel-grid-moves/GridFlashMosaic.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/rhythm/panel-grid-moves/ComicPanelSplit.tsx, demos/rhythm/panel-grid-moves/FlipGridReflow.tsx, demos/rhythm/panel-grid-moves/GridFlashMosaic.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/rhythm/panel-grid-moves/ComicPanelSplit.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx"
    },
    {
      "path": "demos/rhythm/panel-grid-moves/FlipGridReflow.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/panel-grid-moves/FlipGridReflow.tsx"
    },
    {
      "path": "demos/rhythm/panel-grid-moves/GridFlashMosaic.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/panel-grid-moves/GridFlashMosaic.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": 142,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/panel-grid-moves/FlipGridReflow.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/panel-grid-moves/GridFlashMosaic.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/rhythm/panel-grid-moves/ComicPanelSplit.tsx =====\n// comic-panel-split｜漫画分格并列\n// FakeDashboard A（加 KPI 数字块）全屏 20f → 咔咔咔切成 3 个斜线分格\n// （12° 斜边，10px 白缝 + 墨线描边），逐格 2f 间隔弹入（3f scale 1.06→1\n// + 加深脉冲）。三格 = 同页面三机位：全景 1x / 卡片特写 1.9x / 数字区\n// 特写 2.6x。定格 18f 各格缓慢微推近保持活 → 第三格斜边框 12f out-cubic\n// 扩张吃掉全屏成为下一镜特写。57f 起摘罩（特写直出、分格结构与缝线\n// 全部卸载），57–150f 真静止 93f ≥ 40f。帧确定，无随机源。\nimport React from 'react';\nimport { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { FakeDashboard, G } from '../../_fixtures/Fixtures';\n\nconst SPLIT = 20;              // 全屏 A 结束、开始分格\nconst POP = 3;                 // 每格弹入 3f\nconst STAGGER = 2;             // 逐格 2f 间隔\nconst HOLD_END = 45;           // 27f 弹完 + 定格 18f\nconst EXPAND_END = 57;         // 第三格 12f 扩张结束\n// 57–150 真静止 93f\n\n// 斜边 12°：tan(12°)×1080 ≈ 230，顶边比底边右移 230\n// 缝 10px（水平半宽 5px）\n// 边界1：顶 750 / 底 520；边界2：顶 1405 / 底 1175\nconst outCubic = Easing.out(Easing.cubic);\n\n// 页面 = FakeDashboard A + 左上卡片内 KPI 数字块（给\"数字区特写\"一个锚点）\nconst PageA: React.FC = () => (\n  <div style={{ width: 1920, height: 1080, position: 'relative' }}>\n    <FakeDashboard variant=\"A\" />\n    <div style={{\n      position: 'absolute', left: 328, top: 320, width: 380, height: 160,\n      background: G.card, borderRadius: 12, display: 'flex', flexDirection: 'column',\n      alignItems: 'center', justifyContent: 'center', gap: 8,\n    }}>\n      <div style={{ fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 96, color: G.ink, letterSpacing: -2, lineHeight: 1 }}>\n        1,284\n      </div>\n      <div style={{ height: 10, width: 150, background: G.mid, borderRadius: 5 }} />\n    </div>\n  </div>\n);\n\ntype PanelSpec = {\n  clip: (f: number) => string; // clip-path polygon\n  centroidX: number;           // 弹入缩放原点\n  originX: number; originY: number; // 内容变换原点（焦点）\n  baseScale: number;           // 机位倍率\n  tx: number; ty: number;      // 焦点搬到格中心的位移\n  z: number;\n};\n\nexport const ComicPanelSplit: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // ===== 摘罩：扩张完成后特写直出，分格结构 / 缝线全部卸载 =====\n  if (frame >= EXPAND_END) {\n    return (\n      <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>\n        <div style={{\n          width: 1920, height: 1080,\n          transform: 'translate(442px, 140px) scale(2.6)',\n          transformOrigin: '518px 400px',\n        }}>\n          <PageA />\n        </div>\n      </AbsoluteFill>\n    );\n  }\n\n  // ===== 阶段 1：全屏 A =====\n  if (frame < SPLIT) {\n    return (\n      <AbsoluteFill style={{ background: G.bg }}>\n        <PageA />\n      </AbsoluteFill>\n    );\n  }\n\n  // ===== 阶段 2/3：分格 + 第三格扩张 =====\n  // 定格期微推近（27–45f 线性，很缓）\n  const push = interpolate(frame, [SPLIT + 2 * STAGGER + POP, HOLD_END], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  // 第三格扩张进度（扩散用 out-cubic）\n  const ex = interpolate(frame, [HOLD_END, EXPAND_END], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: outCubic,\n  });\n\n  // 第三格左斜边（= 边界2 吃屏轨迹）\n  const e3Top = 1410 + ex * (-60 - 1410);    // 1410 → -60\n  const e3Bot = 1180 + ex * (-290 - 1180);   // 1180 → -290\n\n  const panels: PanelSpec[] = [\n    { // 全景 1x\n      clip: () => 'polygon(0px 0px, 745px 0px, 515px 1080px, 0px 1080px)',\n      centroidX: 315, originX: 960, originY: 540,\n      baseScale: 1 + push * 0.03, tx: 0, ty: 0, z: 1,\n    },\n    { // 卡片特写 1.9x（中上卡片）\n      clip: () => 'polygon(755px 0px, 1400px 0px, 1170px 1080px, 525px 1080px)',\n      centroidX: 962, originX: 1070, originY: 371,\n      baseScale: 1.9 + push * 0.055, tx: -108, ty: 169, z: 1,\n    },\n    { // 数字区特写 2.6x（KPI 块），扩张时焦点从格中心搬到屏中心\n      clip: () => `polygon(${e3Top}px 0px, 1920px 0px, 1920px 1080px, ${e3Bot}px 1080px)`,\n      centroidX: 1607, originX: 518, originY: 400,\n      // 扩张时 push 增量退掉，scale 收敛回 2.6（与摘罩帧完全一致）\n      baseScale: 2.6 + push * 0.08 * (1 - ex), tx: 1089 + ex * (442 - 1089), ty: 140, z: 3,\n    },\n  ];\n\n  // 缝线透明度：边界1 随第 2 格弹入出现、扩张期被吃前线性淡出；\n  // 边界2 随第 3 格弹入出现、扩张末 4f 线性淡出（消散用线性）\n  const seam1O = Math.min(\n    interpolate(frame, [SPLIT + STAGGER, SPLIT + STAGGER + 2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),\n    interpolate(frame, [HOLD_END, HOLD_END + 3], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),\n  );\n  const seam2O = Math.min(\n    interpolate(frame, [SPLIT + 2 * STAGGER, SPLIT + 2 * STAGGER + 2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),\n    interpolate(frame, [EXPAND_END - 4, EXPAND_END], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),\n  );\n\n  return (\n    <AbsoluteFill style={{ background: '#ffffff' }}>\n      {panels.map((p, i) => {\n        const start = SPLIT + i * STAGGER;\n        if (frame < start) return null; // 未弹入不渲染\n        // 弹入：3f scale 1.06→1 + 加深脉冲\n        const pop = interpolate(frame, [start, start + POP], [0, 1], {\n          extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: outCubic,\n        });\n        const popScale = 1.06 - 0.06 * pop;\n        const pulse = 0.3 * (1 - pop);\n        return (\n          <div key={i} style={{\n            position: 'absolute', inset: 0, zIndex: p.z,\n            clipPath: p.clip(frame),\n            transform: `scale(${popScale})`,\n            transformOrigin: `${p.centroidX}px 540px`,\n          }}>\n            <div style={{\n              width: 1920, height: 1080,\n              transform: `translate(${p.tx}px, ${p.ty}px) scale(${p.baseScale})`,\n              transformOrigin: `${p.originX}px ${p.originY}px`,\n            }}>\n              <PageA />\n            </div>\n            {pulse > 0.005 && (\n              <div style={{ position: 'absolute', inset: 0, background: `rgba(0,0,0,${pulse})` }} />\n            )}\n          </div>\n        );\n      })}\n      {/* 斜缝：白缝命门 + 墨线描边（暗 16 下、白 10 上 → 两侧各 3px 墨边） */}\n      {(seam1O > 0.005 || seam2O > 0.005) && (\n        <svg width={1920} height={1080} style={{ position: 'absolute', inset: 0, zIndex: 5, pointerEvents: 'none' }}>\n          {seam1O > 0.005 && (\n            <g opacity={seam1O}>\n              <line x1={750} y1={-10} x2={520} y2={1090} stroke=\"#2f2f2f\" strokeWidth={16} />\n              <line x1={750} y1={-10} x2={520} y2={1090} stroke=\"#ffffff\" strokeWidth={10} />\n            </g>\n          )}\n          {seam2O > 0.005 && (\n            <g opacity={seam2O}>\n              <line x1={e3Top - 5} y1={-10} x2={e3Bot - 5} y2={1090} stroke=\"#2f2f2f\" strokeWidth={16} />\n              <line x1={e3Top - 5} y1={-10} x2={e3Bot - 5} y2={1090} stroke=\"#ffffff\" strokeWidth={10} />\n            </g>\n          )}\n        </svg>\n      )}\n    </AbsoluteFill>\n  );\n};\n\n\n// ===== demos/rhythm/panel-grid-moves/FlipGridReflow.tsx =====\n// flip-grid-reflow —— 网格 FLIP 重排\n// 6 张 Card 初始横排一行（marquee 式，屏心偏上）静止 30f，节拍点集体换位：\n// 每卡沿直线飞向 3×2 网格目标位（预写两套坐标表），scale 1→1.28，\n// delay = 卡索引×1.5f 微错峰，16f inOut(cubic) + 落定 3f 过冲 1.02。\n// 全部落定后整体加深脉冲收束。收尾真静止 ≥40f。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, Card, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst CARD_W = 280;\nconst CARD_H = 170;\nconst N = 6;\n\n// ---- 坐标表 A：横排一行（卡间 24px，屏心偏上）----\n// 总宽 6*280 + 5*24 = 1800，x0 = 60；中心 y = 330\nconst ROW_Y = 330;\nconst ROW_CENTERS: Array<[number, number]> = Array.from({ length: N }, (_, i) => [\n  60 + CARD_W / 2 + i * (CARD_W + 24),\n  ROW_Y,\n]); // x: 200, 504, 808, 1112, 1416, 1720\n\n// ---- 坐标表 B：3×2 网格（卡放大 1.28 → 358.4×217.6，网格间距 30px）----\n// 网格总宽 3*358.4 + 2*30 = 1135.2 → x0 = 392.4；总高 2*217.6 + 30 = 465.2 → y0 = 307.4\nconst GC = [571.6, 960, 1348.4]; // 列中心\nconst GR = [416.2, 663.8]; // 行中心\n// 交错映射：偶数索引去上排、奇数去下排（每卡直线飞行、互不对穿）\nconst GRID_CENTERS: Array<[number, number]> = [\n  [GC[0], GR[0]], // card0 → 上左\n  [GC[0], GR[1]], // card1 → 下左\n  [GC[1], GR[0]], // card2 → 上中\n  [GC[1], GR[1]], // card3 → 下中\n  [GC[2], GR[0]], // card4 → 上右\n  [GC[2], GR[1]], // card5 → 下右\n];\n\nconst BEAT = 30; // 节拍点\nconst STAGGER = 1.5; // 每卡延迟\nconst MOVE = 16; // 飞行帧数\nconst SETTLE = 3; // 落定过冲帧数\nconst SCALE_END = 1.28;\nconst OVERSHOOT = 1.02;\n// 末卡完全落定：30 + 5*1.5 + 16 + 3 = 56.5\nconst ALL_SETTLED = BEAT + (N - 1) * STAGGER + MOVE + SETTLE; // 56.5\n\n// 加深脉冲：f58 → f61 谷值 → f64 回正（谷值 brightness 0.78，spec 4f 放宽到 6f 保可感）\nconst PULSE_IN = 58;\nconst PULSE_MID = 61;\nconst PULSE_OUT = 64;\n\nconst moveEase = Easing.inOut(Easing.cubic);\n\nconst FlipCard: React.FC<{ i: number; frame: number }> = ({ i, frame }) => {\n  const t0 = BEAT + i * STAGGER;\n  const [x0, y0] = ROW_CENTERS[i];\n  const [x1, y1] = GRID_CENTERS[i];\n\n  const x = interpolate(frame, [t0, t0 + MOVE], [x0, x1], {\n    easing: moveEase, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  const y = interpolate(frame, [t0, t0 + MOVE], [y0, y1], {\n    easing: moveEase, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  // scale：16f 冲到 1.28*1.02，随后 3f 回落到 1.28\n  const sUp = interpolate(frame, [t0, t0 + MOVE], [1, SCALE_END * OVERSHOOT], {\n    easing: moveEase, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  const sBack = interpolate(frame, [t0 + MOVE, t0 + MOVE + SETTLE], [SCALE_END * OVERSHOOT, SCALE_END], {\n    easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  const s = frame < t0 + MOVE ? sUp : sBack;\n\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        left: x - CARD_W / 2,\n        top: y - CARD_H / 2,\n        width: CARD_W,\n        height: CARD_H,\n        transform: `scale(${s})`,\n        transformOrigin: '50% 50%',\n        zIndex: i,\n      }}\n    >\n      <Card w={CARD_W} h={CARD_H} seed={i + 1} />\n    </div>\n  );\n};\n\nexport const FlipGridReflow: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 加深脉冲：仅脉冲窗口内挂载 filter，窗口外完全不挂（摘罩）\n  const pulsing = frame >= PULSE_IN && frame <= PULSE_OUT;\n  const bright = pulsing\n    ? interpolate(frame, [PULSE_IN, PULSE_MID, PULSE_OUT], [1, 0.78, 1], {\n        extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n      })\n    : 1;\n\n  return (\n    <div\n      style={{\n        width: 1920,\n        height: 1080,\n        background: G.bg,\n        position: 'relative',\n        overflow: 'hidden',\n        ...(pulsing ? { filter: `brightness(${bright})` } : {}),\n      }}\n    >\n      <div style={{ position: 'absolute', left: 70, top: 56 }}>\n        <TitleBlock text=\"FLIP GRID REFLOW\" size={44} />\n      </div>\n      {Array.from({ length: N }).map((_, i) => (\n        <FlipCard key={i} i={i} frame={frame} />\n      ))}\n    </div>\n  );\n};\n\n\n// ===== demos/rhythm/panel-grid-moves/GridFlashMosaic.tsx =====\n// grid-flash-mosaic —— 九宫格闪切\n// 浅底 → f25 起 3×3 网格按十六分音符(每 2f)逐格啪啪硬入(顺序 h(i) 打乱)，\n// 每格 = FakeDashboard 不同区域裁切片或灰卡；入格 3f scale 1.18→1 + 2f 加深脉冲。\n// 填满后停 14f(整墙微呼吸 1.008) → 中心格 14f Easing.in(cubic) 放大吞掉全屏\n// 成为满屏页面。收尾真静止 ≥40f。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, Card, FakeDashboard } from '../../_fixtures/Fixtures';\n\nconst h = (n: number) => {\n  const s = Math.sin(n * 127.3) * 43758.5453;\n  return s - Math.floor(s);\n};\n\nconst CELL_W = 600;\nconst CELL_H = 340;\nconst GAP = 12;\nconst GRID_X = (1920 - (CELL_W * 3 + GAP * 2)) / 2; // 48\nconst GRID_Y = (1080 - (CELL_H * 3 + GAP * 2)) / 2; // 18\n\nconst FILL_START = 25; // 第一格落下\nconst STEP = 2; // 十六分音符：每 2f 一格\n// 打乱顺序：索引按 h(i+1) 排序\nconst ORDER = Array.from({ length: 9 }, (_, i) => i).sort(\n  (a, b) => h(a + 1) - h(b + 1)\n);\nconst RANK: number[] = [];\nORDER.forEach((cell, k) => (RANK[cell] = k));\n\nconst LAST_IN = FILL_START + 8 * STEP + 3; // 最后一格入格动画结束 f44\nconst HOLD_END = LAST_IN + 14; // 整墙呼吸段结束 f58\nconst ZOOM_DUR = 14;\nconst ZOOM_END = HOLD_END + ZOOM_DUR; // f72，之后真静止\n\n// 中心格内是整页 FakeDashboard 缩小版(0.3125)，放大后正好成为满屏页面\nconst MINI_SCALE = CELL_W / 1920; // 0.3125\n// 3.2 恰好铺满；加到 3.28 让格子的深色描边完全滑出画外，收尾满屏页面干净\nconst ZOOM_SCALE = 3.28;\n\n// 非中心格：FakeDashboard 裁切偏移 (x, y, variant) 或灰卡(null)\nconst CROPS: Array<{ x: number; y: number; v: 'A' | 'B' } | null> = [\n  { x: -120, y: -60, v: 'A' },\n  null, // 灰卡\n  { x: -1260, y: -120, v: 'B' },\n  { x: -60, y: -520, v: 'B' },\n  null, // 中心格(单独处理，占位)\n  { x: -1300, y: -640, v: 'A' },\n  { x: -420, y: -300, v: 'B' },\n  null, // 灰卡\n  { x: -900, y: -680, v: 'A' },\n];\n\nconst CellContent: React.FC<{ i: number }> = ({ i }) => {\n  if (i === 4) {\n    // 中心格：整页 dashboard 缩到格内\n    return (\n      <div\n        style={{\n          position: 'absolute',\n          left: 0,\n          top: (CELL_H - 1080 * MINI_SCALE) / 2,\n          transform: `scale(${MINI_SCALE})`,\n          transformOrigin: 'top left',\n        }}\n      >\n        <FakeDashboard variant=\"A\" />\n      </div>\n    );\n  }\n  const crop = CROPS[i];\n  if (crop === null) {\n    // 灰卡格\n    return (\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          background: G.panel,\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n        }}\n      >\n        <Card w={430} h={240} seed={i * 3 + 2} />\n      </div>\n    );\n  }\n  // 裁切片：整页 dashboard 以不同偏移塞进格子(相当于 backgroundPosition 各异)\n  return (\n    <div style={{ position: 'absolute', left: crop.x, top: crop.y }}>\n      <FakeDashboard variant={crop.v} />\n    </div>\n  );\n};\n\nexport const GridFlashMosaic: React.FC = () => {\n  const f = useCurrentFrame();\n\n  // 整墙微呼吸：仅在填满后的 14f 停顿段，一个正弦来回 1→1.008→1\n  const breath =\n    f >= LAST_IN && f < HOLD_END\n      ? 1 + 0.008 * Math.sin((Math.PI * (f - LAST_IN)) / 14)\n      : 1;\n\n  // 中心格放大：14f Easing.in(cubic)，吞掉全屏\n  const zoom = interpolate(f, [HOLD_END, ZOOM_END], [1, ZOOM_SCALE], {\n    easing: Easing.in(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <div\n      style={{\n        width: 1920,\n        height: 1080,\n        background: G.bg,\n        position: 'relative',\n        overflow: 'hidden',\n      }}\n    >\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          transform: `scale(${breath})`,\n          transformOrigin: '960px 540px',\n        }}\n      >\n        {Array.from({ length: 9 }).map((_, i) => {\n          const start = FILL_START + RANK[i] * STEP;\n          if (f < start) return null; // 硬入：未到拍点不渲染，无淡化\n          const row = Math.floor(i / 3);\n          const col = i % 3;\n          // 入格 3f scale 1.18→1\n          const popScale = interpolate(f, [start, start + 3], [1.18, 1], {\n            extrapolateLeft: 'clamp',\n            extrapolateRight: 'clamp',\n          });\n          // 2f 加深脉冲\n          const darken = interpolate(f, [start, start + 2], [0.45, 0], {\n            extrapolateLeft: 'clamp',\n            extrapolateRight: 'clamp',\n          });\n          const isCenter = i === 4;\n          const cellScale = isCenter ? popScale * zoom : popScale;\n          return (\n            <div\n              key={i}\n              style={{\n                position: 'absolute',\n                left: GRID_X + col * (CELL_W + GAP),\n                top: GRID_Y + row * (CELL_H + GAP),\n                width: CELL_W,\n                height: CELL_H,\n                overflow: 'hidden',\n                background: G.card,\n                border: `3px solid ${G.ink}`,\n                boxSizing: 'border-box',\n                transform: `scale(${cellScale})`,\n                transformOrigin: 'center',\n                zIndex: isCenter ? 10 : 1,\n              }}\n            >\n              <CellContent i={i} />\n              {darken > 0.001 && (\n                <div\n                  style={{\n                    position: 'absolute',\n                    inset: 0,\n                    background: '#000',\n                    opacity: darken,\n                  }}\n                />\n              )}\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n\n\n"
}