{
  "slug": "page-turn-transitions",
  "title": "Page-turn transitions",
  "tagline": "Two full-page transitions: cube-rotate turns two pages like adjacent cube faces, and barn-door-split splits the old page open like doors.",
  "description": "Two full-page transitions: cube-rotate turns two pages like adjacent cube faces, and barn-door-split splits the old page open like doors.",
  "category": "transition",
  "tags": [
    "shotcraft",
    "remotion",
    "transition"
  ],
  "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/page-turn-transitions/poster-07b6c8043dd6.jpg",
    "video": "https://gimgs.net/motion/media/page-turn-transitions/video-6c33f7836d24.mp4"
  },
  "variants": [
    {
      "key": "cube-rotate",
      "label": "cube-rotate",
      "description": "The two pages sit on adjacent faces of a cube (rotateY 0/90° + translateZ W/2) while the scene layer rotates -90°; the old face darkens as it turns out and the new face brightens as it turns in, with a dark edge pinched between the two faces at 45°.",
      "use": "Parallel chapter turns; switching between two major product feature areas",
      "video": "https://gimgs.net/motion/media/page-turn-transitions/video-6c33f7836d24.mp4",
      "poster": "https://gimgs.net/motion/media/page-turn-transitions/poster-07b6c8043dd6.jpg"
    },
    {
      "key": "barn-door-split",
      "label": "barn-door-split",
      "description": "The old page is split into two aligned 960px overflow containers that slide outward off-screen together with an Easing.in(cubic); the inner edges of the crack get a bright line and shadow, while the new page underneath scales 1.06→1 as it rises up to meet it.",
      "use": "Old-to-new swaps: before/after a redesign, option A → option B",
      "video": "https://gimgs.net/motion/media/page-turn-transitions/video-357b518210b0.mp4",
      "poster": "https://gimgs.net/motion/media/page-turn-transitions/poster-barn-door-split-d49a21b6272b.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/page-turn-transitions.html",
  "json_url": "https://gimgs.net/motion/page-turn-transitions.json",
  "template_url": "https://gimgs.net/motion/page-turn-transitions/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/page-turn-transitions.md",
  "updated_at": "2026-09-24T07:40:11.151Z",
  "prompt": "## When to use\n\nChapter-level page changes: a \"turning the page\" ritual between two parallel major sections; division of labor with the shot-transitions family (camera hand-off) — that's \"the camera moves across,\" this is \"the page itself is a physical object\"\n\n## Intention\n\nThe shot-transitions six and transition-travel/hidden-cut all treat the page as a \"scene\" — the camera moves between scenes. This card treats the page as a **physical object**: it has thickness, weight, it can turn over and split apart. The physical-object metaphor carries more ceremony, suited to major chapter-level page changes (feature A wraps up → feature B begins) rather than small shot-to-shot hand-offs. A, the cube, is \"spatially parallel\" — the two pages are two faces of a box, implying a peer relationship; B, the barn door, is \"the old one steps aside\" — the old page tears open from the center and slides away while the new page rises up from below, implying replacement. Choose by semantics: use A for parallel page turns, B for old-to-new swaps.\n\n## Duration & energy\n\n- Duration: Per style: establish 30f + transition 20–38f + settle ≥40f, about 4.4–4.7s\n- Energy: Medium-high\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\nshot-transitions 六式和 transition-travel/hidden-cut 都把页面当\"场景\"\n——相机在场景间移动。本卡把页面当**实体**：它有厚度、有重量、会翻面\n会裂开。体块隐喻的仪式感更强，适合章节级大换页（功能 A 讲完→功能 B）\n而非镜头级小交棒。A 立方体是\"空间上并列\"——两页是盒子的两面，\n暗示同级关系；B 对开门是\"旧的让位\"——旧页从正中撕开滑走、新页\n从底下迎上来，暗示取代关系。按语义选：并列翻篇用 A，新旧交替用 B。\n\n## 两式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A cube-rotate | 两页贴立方体相邻面（rotateY 0/90° + translateZ W/2），场景层转 -90°；旧面转出压暗、新面转进变亮，45° 时两面夹一条暗棱 | 并列章节翻篇；产品两大功能区切换 |\n| B barn-door-split | 旧页两个 960px overflow 容器对位拼合，同时向外 Easing.in(cubic) 滑出画外；裂缝内边缘亮线+投影，新页底层 scale 1.06→1 迎上 | 新旧交替：改版前后、方案 A→方案 B |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 结构命门 | 场景层 `translateZ(−W/2) rotateY(θ)` 把正面拉回 z=0 | 少了这层 hold 段页面尺度不对（被 perspective 推远） |\n| A 转速 | θ 0→−90° / 38f inOut cubic；中段叠 blur 1.5px 落定摘 | <28f 读不出体块感；>50f 读作慢慢挪 |\n| A 亮度对切 | 旧面 1→0.55、新面 0.55→1 + 接缝竖向渐变阴影 sin(pπ) | 无明暗对切两面读作同一张图在转 |\n| B 预告 | 裂前中缝 2px 细线闪现两次（18–22/25–29f） | 直接裂开观众没看到裂点在哪 |\n| B 滑出 | 各 translateX ∓980px / 20f Easing.in(cubic) 加速 | ease-out 滑出是\"被推走\"，ease-in 才是\"让位\" |\n| B 撕裂边 | 内边缘 3px G.ink 亮线 + 8px 投影 | 无边线两半读作普通左右擦除 |\n| 收尾 | 新页落定真静止 ≥40f | R1 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 一缝一式规矩同 shot-transitions；体块转场比镜头交棒更重，\n  **全片 ≤2 次**，连续两缝都用体块读作 PPT 翻页\n- A 式官方 `@remotion/transitions` cube() 是付费件——本卡是 CSS 自实现，\n  backlog 已有\"评估官方转场原语\"待选项，若引入官方件可对照\n- B 式两半必须**对位拼合无缝**（右半内层 translateX(−960px)）——\n  静止时看出接缝就穿帮；真实截图注意奇数宽度取整\n- 转场前后两页都要有 ≥30f 静止建立/收尾——两端都在动，体块感被\n  运动淹没\n\n## 参考实现\ndemos/transition/page-turn-transitions/\n（BarnDoorSplit.tsx / CubeRotate.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/transition/page-turn-transitions/BarnDoorSplit.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/page-turn-transitions/BarnDoorSplit.tsx\n- demos/transition/page-turn-transitions/CubeRotate.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/page-turn-transitions/CubeRotate.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/page-turn-transitions/BarnDoorSplit.tsx, demos/transition/page-turn-transitions/CubeRotate.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/page-turn-transitions/BarnDoorSplit.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/page-turn-transitions/BarnDoorSplit.tsx"
    },
    {
      "path": "demos/transition/page-turn-transitions/CubeRotate.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/page-turn-transitions/CubeRotate.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/transition/page-turn-transitions/BarnDoorSplit.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/page-turn-transitions/CubeRotate.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/page-turn-transitions/BarnDoorSplit.tsx =====\n// 对开门裂幕（barn-door-split-reveal）——剪映\"开门式\"转场。\n// FakeDashboard A 从画面正中垂直裂成左右两半：两个 960×1080 overflow:hidden\n// 容器各装一份完整 A（右半内层 translateX(-960) 对位拼合），同时向外加速滑\n// 出画外，露出底层 FakeDashboard B 从 scale 1.06 轻推到 1.0 迎上来。\n// 裂缝各自内边缘 2px G.ink 亮线 + 8px 投影强调\"撕开\"。\n// 关键帧：0–30 静止展示 A（18–22 / 25–29 中缝细线两次闪现预告裂点）→\n// 30–50 两半各 translateX ∓980（Easing.in cubic 加速滑出）→\n// 30–55 底层 B scale 1.06→1.0（out cubic）→ 55–130 全静止（75f）。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, FakeDashboard, TitleBlock } from '../../_fixtures/Fixtures';\n\nexport const BarnDoorSplit: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 两半外滑位移：30–50f，0 → 980px，加速离场\n  const slide = interpolate(frame, [30, 50], [0, 980], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.cubic),\n  });\n\n  // 底层 B：30–55f 从 1.06 轻推到 1.0 迎上来\n  const bScale = interpolate(frame, [30, 55], [1.06, 1.0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n\n  // 裂前预告：中缝细线两次闪现（帧确定的开关，无随机）\n  const crackFlash =\n    (frame >= 18 && frame < 22) || (frame >= 25 && frame < 29);\n  // 裂开后内边缘亮线 + 阴影常驻（随门一起滑出画外）\n  const tornEdge = frame >= 30;\n\n  const edgeLine = (side: 'left' | 'right'): React.CSSProperties => ({\n    position: 'absolute',\n    top: 0,\n    [side]: 0,\n    width: 3,\n    height: 1080,\n    background: G.ink,\n    boxShadow:\n      side === 'right'\n        ? '-8px 0 14px rgba(0,0,0,0.4)'\n        : '8px 0 14px rgba(0,0,0,0.4)',\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      {/* 底层 FakeDashboard B：scale 1.06 → 1.0 迎上来 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 0,\n          top: 0,\n          width: 1920,\n          height: 1080,\n          transform: `scale(${bScale})`,\n          transformOrigin: '50% 50%',\n        }}\n      >\n        <FakeDashboard variant=\"B\" />\n      </div>\n\n      {/* 左门：960×1080 视口，装完整 A 的左半 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 0,\n          top: 0,\n          width: 960,\n          height: 1080,\n          overflow: 'hidden',\n          transform: `translateX(${-slide}px)`,\n        }}\n      >\n        <div style={{ position: 'absolute', left: 0, top: 0, width: 1920, height: 1080 }}>\n          <FakeDashboard variant=\"A\" />\n        </div>\n        {tornEdge && <div style={edgeLine('right')} />}\n      </div>\n\n      {/* 右门：960×1080 视口，内层 translateX(-960) 对位拼合 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 960,\n          top: 0,\n          width: 960,\n          height: 1080,\n          overflow: 'hidden',\n          transform: `translateX(${slide}px)`,\n        }}\n      >\n        <div\n          style={{\n            position: 'absolute',\n            left: 0,\n            top: 0,\n            width: 1920,\n            height: 1080,\n            transform: 'translateX(-960px)',\n          }}\n        >\n          <FakeDashboard variant=\"A\" />\n        </div>\n        {tornEdge && <div style={edgeLine('left')} />}\n      </div>\n\n      {/* 裂点预告：中缝 2px 细线闪现两次 */}\n      {crackFlash && (\n        <div\n          style={{\n            position: 'absolute',\n            left: 959,\n            top: 0,\n            width: 2,\n            height: 1080,\n            background: G.ink,\n          }}\n        />\n      )}\n\n      <div style={{ position: 'absolute', left: 120, top: 96 }}>\n        <TitleBlock text=\"BARN DOOR SPLIT\" size={54} />\n      </div>\n    </div>\n  );\n};\n\n\n// ===== demos/transition/page-turn-transitions/CubeRotate.tsx =====\n// 立方体空间翻转（cube-rotate）——cube transition 转场自实现。\n// FakeDashboard A / B 各缩放 0.82 居中，当立方体相邻两面：perspective 1400px，\n// 面宽 W=1920*0.82，两面各 rotateY(面角) translateZ(W/2)，场景整体先\n// translateZ(-W/2) 再 rotateY(θ)（把正面拉回 z=0，保证 hold 时精确 0.82 尺度）。\n// 旧面 A 转出画面 brightness 1→0.55 压暗，新面 B 从 +90° 侧转入 0.55→1 亮起；\n// 接缝两侧各一条竖向渐变阴影，随角度 sin(pπ) 加深再消失；转动中段叠 blur(1.5px)，\n// 落定摘掉（filter 条件挂载，保证收尾逐帧完全相同）。\n// 关键帧：0–30 静止展示 A → 30–68 θ 0→-90°（inOut cubic）翻转 → 68–140 B 真静止 72f。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, FakeDashboard, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst S = 0.82;\nconst W = 1920 * S; // 面宽 = 立方体棱长\nconst H = 1080 * S;\n\n// 单个立方体面：W×H 视口内放缩放 0.82 的整幅 dashboard\nconst Face: React.FC<{\n  variant: 'A' | 'B';\n  rot: number; // 面自身的 rotateY（A=0，B=90）\n  brightness: number;\n  seam: number; // 接缝阴影不透明度 0–1\n  seamSide: 'right' | 'left'; // 阴影贴在哪条竖边（即共享棱一侧）\n}> = ({ variant, rot, brightness, seam, seamSide }) => (\n  <div\n    style={{\n      position: 'absolute',\n      width: W,\n      height: H,\n      overflow: 'hidden',\n      backfaceVisibility: 'hidden',\n      transform: `rotateY(${rot}deg) translateZ(${W / 2}px)`,\n      filter: `brightness(${brightness})`,\n      background: G.bg,\n    }}\n  >\n    <div style={{ transform: `scale(${S})`, transformOrigin: '0 0' }}>\n      <FakeDashboard variant={variant} />\n    </div>\n    {seam > 0.01 && (\n      <div\n        style={{\n          position: 'absolute',\n          top: 0,\n          [seamSide]: 0,\n          width: 150,\n          height: H,\n          opacity: seam,\n          background:\n            seamSide === 'right'\n              ? 'linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.55))'\n              : 'linear-gradient(to left, rgba(0,0,0,0), rgba(0,0,0,0.55))',\n        }}\n      />\n    )}\n  </div>\n);\n\nexport const CubeRotate: React.FC = () => {\n  const frame = useCurrentFrame();\n  // 翻转进度：30–68f，inOut cubic；两端 clamp 保证 hold 段逐帧恒定\n  const p = interpolate(frame, [30, 68], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.inOut(Easing.cubic),\n  });\n  const theta = -90 * p;\n  const brightA = 1 - 0.45 * p; // 旧面转出压暗 1→0.55\n  const brightB = 0.55 + 0.45 * p; // 新面转入亮起 0.55→1\n  const mid = Math.sin(p * Math.PI); // 0→1→0，45° 时最深\n  const seam = mid * 0.9;\n  const blur = mid * 1.5;\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      {/* blur 外包一层：>0.02 才挂 filter，落定后收尾帧完全相同 */}\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          filter: blur > 0.02 ? `blur(${blur}px)` : undefined,\n        }}\n      >\n        <div\n          style={{\n            position: 'absolute',\n            left: (1920 - W) / 2,\n            top: (1080 - H) / 2,\n            width: W,\n            height: H,\n            perspective: 1400,\n          }}\n        >\n          <div\n            style={{\n              position: 'absolute',\n              inset: 0,\n              transformStyle: 'preserve-3d',\n              transform: `translateZ(${-W / 2}px) rotateY(${theta}deg)`,\n            }}\n          >\n            {/* 面 A：正面出发，绕左而去；接缝棱在其右边 */}\n            <Face variant=\"A\" rot={0} brightness={brightA} seam={seam} seamSide=\"right\" />\n            {/* 面 B：从 +90° 侧面转进来；接缝棱在其左边 */}\n            <Face variant=\"B\" rot={90} brightness={brightB} seam={seam} seamSide=\"left\" />\n          </div>\n        </div>\n      </div>\n      <div style={{ position: 'absolute', left: 120, top: 60 }}>\n        <TitleBlock text=\"CUBE ROTATE\" size={54} />\n      </div>\n    </div>\n  );\n};\n\n\n"
}