{
  "slug": "platform-hinge-rise",
  "title": "Platform hinge rise",
  "tagline": "A platform establishes first, two subjects flip up from adjacent hinges with a damped rebound, then a conclusion stand rises in.",
  "description": "A platform establishes first, two subjects flip up from adjacent hinges with a damped rebound, then a conclusion stand rises in.",
  "category": "ui-entrance",
  "tags": [
    "shotcraft",
    "remotion",
    "ui-entrance",
    "opening"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 3.52,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/platform-hinge-rise/poster-48c6ccc6c22f.jpg",
    "video": "https://gimgs.net/motion/media/platform-hinge-rise/video-2ea20a2fca86.mp4"
  },
  "variants": [
    {
      "key": "platform-hinge-rise",
      "label": "platform-hinge-rise",
      "description": "A support platform establishes horizontally first, then two subjects flip up from adjacent base hinges in opposing directions with a single restrained damped rebound, and finally a conclusion stand rises in from off-screen — a three-act \"stage → evidence → conclusion\" reveal.",
      "use": "",
      "video": "https://gimgs.net/motion/media/platform-hinge-rise/video-2ea20a2fca86.mp4",
      "poster": "https://gimgs.net/motion/media/platform-hinge-rise/poster-48c6ccc6c22f.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/platform-hinge-rise.html",
  "json_url": "https://gimgs.net/motion/platform-hinge-rise.json",
  "template_url": "https://gimgs.net/motion/platform-hinge-rise/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/platform-hinge-rise.md",
  "updated_at": "2026-09-24T07:40:15.379Z",
  "prompt": "## When to use\n\nSide-by-side entrance for two subjects/options/brands; buildings, equipment, or cards rising up from a base; explainer B-roll that needs to \"lay out evidence, then draw a conclusion\" in one fixed camera position\n\n## Intention\n\nLet the platform establish itself as a credible \"support relationship\" first, then let the two subjects flip up from behind the platform, and the viewer will naturally read them as evidence in the same set; the conclusion stand rising up from below at the end isn't a third peer element, it's a synthesis of the first two. The fixed camera and clear occlusion order make this suited to explanatory B-roll, and the subjects can be swapped for product cards, people, buildings, or comparison options. The core isn't \"something appears from below\" — it's **two adjacent hinges producing opposing directions of unfolding force**.\n\n## Duration & energy\n\n- Duration: About 3.5s (104f@30fps; holds still for 30f after the conclusion settles)\n- Energy: Medium (the two subjects flipping up together mid-shot is the only major action; the ending is just a steady platform rise)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n先让平台成为可信的“承托关系”，再让两个主体从平台后方翻起，观众会自然把它们\n理解为同组证据；最后从下方升起的结论台不是第三个平级元素，而是对前两者的归纳。\n固定机位和明确的遮挡顺序让这张卡适合解释性 B-roll，也能换成产品卡、人物、建筑或\n对比方案。核心不是“东西从下方出现”，而是**两个相邻铰点产生方向相反的展开力**。\n\n## 动效核心\n- f0–15：182×16 的梯形平台从中心 seed 以 `scaleX 0.012→1` 建立，缓动\n  `cubic-bezier(.16,1,.3,1)`；必须先看见承托面，主体才能随后成立\n- f12–32：背景语境圆 `scale .88→1`、`translateY 18→0`、opacity 0→1，\n  比主体早 2f 起步、晚 2f 收尾，作为低能量背景层\n- f14–30：左右主体共用 `cubic-bezier(.4,0,.2,1)`，分别从 `translateY 112/90px`\n  与 `rotate -18°/+18°` 回到 0；两块的 transform-origin 分别钉在相邻的右下/左下铰点\n- f30–46：左块 1.5°、右块 -1.2° 的三半波阻尼回摆，振幅乘 `(1-p)^2`；\n  右块延迟 1f，避免完全镜像的机械感\n- f52–74：结论台 `translateY 96→0`，opacity 在前 18% 行程先到 0.45 再收至 1；\n  f74–103 完全静止，让结论可读\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 平台建立 | f0–15，scaleX .012→1 | 少于 10f 像硬切，多于 22f 会拖慢主体登场 |\n| 语境圆 | f12–32，scale .88→1，rise 18px | 背景必须先于主体可辨认，但不能先完成太久 |\n| 双主体翻起 | f14–30；左 rise 112px/-18°，右 rise 90px/+18° | **命门**：两块必须从相邻铰点反向展开；同向旋转会失去“打开舞台”的力学关系 |\n| 阻尼回摆 | 16f；左 1.5°，右 1.2°，右延迟 1f | 只允许一次可感回摆；振幅 >2° 会像卡片玩具 |\n| 结论台 | f52–74，rise 96px | 与主体落定至少隔 6f，避免证据和结论同时抢读 |\n| 收尾 hold | 30f | 结论文字若超过 8 个汉字，继续延长总时长，不要压缩升台动作 |\n\n## 已知坑\n- 主体必须被平台上缘或独立遮罩裁住底部；没有遮挡会暴露它们从画外平移而非从平台后翻起\n- `transform-origin` 不能放在各自主体中心：中心旋转读作两张卡在晃，只有相邻底部铰点才读作展开\n- 左右块不要做成完全同尺寸、同高度、同振幅；轻微不对称是让双主体保持可辨认轮廓的关键\n- 结论台是总结层，视觉对比应低于两个主体；若给它更深颜色或更大过冲，会反客为主\n- demo 只含原创通用 DOM 形状和占位文字，不依赖复刻研究里的品牌、建筑或视频素材\n\n## 参考实现\ndemos/ui-entrance/platform-hinge-rise/\n（PlatformHingeRise.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/ui-entrance/platform-hinge-rise/PlatformHingeRise.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/platform-hinge-rise/PlatformHingeRise.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/platform-hinge-rise/PlatformHingeRise.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/platform-hinge-rise/PlatformHingeRise.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/platform-hinge-rise/PlatformHingeRise.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": 106,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/platform-hinge-rise/PlatformHingeRise.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/platform-hinge-rise/PlatformHingeRise.tsx =====\n// platform-hinge-rise — 平台建立后，两块主体从相邻底部铰点反向翻起，最后结论台升入。\n// 从独立 motion-blocking 研究中整理为纯 DOM/SVG 的通用 Remotion demo。\nimport React from 'react';\nimport { Easing, interpolate, useCurrentFrame } from 'remotion';\nimport { DesignStage } from '../../_fixtures/Motion';\n\nexport const PLATFORM_HINGE_RISE_DURATION = 104;\n\nconst CLAMP = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;\n\nconst ease = (frame: number, start: number, end: number, from = 0, to = 1) =>\n  interpolate(frame, [start, Math.max(start + 1, end)], [from, to], {\n    ...CLAMP,\n    easing: Easing.bezier(0.16, 1, 0.3, 1),\n  });\n\nconst hingeEase = (frame: number, start: number, end: number) =>\n  interpolate(frame, [start, Math.max(start + 1, end)], [0, 1], {\n    ...CLAMP,\n    easing: Easing.bezier(0.4, 0, 0.2, 1),\n  });\n\nconst dampedWobble = (frame: number, start: number, duration: number, amplitude: number) => {\n  if (frame <= start || frame >= start + duration) return 0;\n  const p = (frame - start) / duration;\n  return amplitude * Math.sin(p * Math.PI * 3) * Math.pow(1 - p, 2);\n};\n\nconst SubjectPanel: React.FC<{\n  side: 'left' | 'right';\n  progress: number;\n  wobble: number;\n}> = ({ side, progress, wobble }) => {\n  const isLeft = side === 'left';\n  const x = isLeft ? 165 : 249;\n  const w = isLeft ? 92 : 78;\n  const h = isLeft ? 72 : 59;\n  const rise = isLeft ? 112 : 90;\n  const startRotation = isLeft ? -18 : 18;\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        left: x,\n        top: 108,\n        width: w,\n        height: h,\n        clipPath: 'polygon(8% 10%,92% 0,100% 100%,0 100%)',\n        background: isLeft ? '#5f6064' : '#77787c',\n        boxShadow: '0 10px 22px rgba(0,0,0,.12)',\n        transformOrigin: isLeft ? '100% 100%' : '0% 100%',\n        transform: `translateY(${interpolate(progress, [0, 1], [rise, 0])}px) rotate(${interpolate(\n          progress,\n          [0, 1],\n          [startRotation, 0],\n        ) + wobble}deg)`,\n        display: 'grid',\n        placeItems: 'center',\n        color: '#f7f7f5',\n        fontFamily: 'Arial, sans-serif',\n        fontSize: 10,\n        fontWeight: 800,\n        letterSpacing: 1,\n      }}\n    >\n      SUBJECT\n    </div>\n  );\n};\n\nexport const PlatformHingeRise: React.FC = () => {\n  const frame = useCurrentFrame();\n  const platform = ease(frame, 0, 15, 0.012, 1);\n  const context = ease(frame, 12, 32);\n  const left = hingeEase(frame, 14, 30);\n  const right = hingeEase(frame, 14, 30);\n  const conclusion = ease(frame, 52, 74);\n  const leftWobble = dampedWobble(frame, 30, 16, 1.5);\n  const rightWobble = dampedWobble(frame, 31, 16, -1.2);\n\n  return (\n    <DesignStage bg=\"#f4f4f1\" raster=\"zoom\">\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          overflow: 'hidden',\n          fontFamily: 'Arial, sans-serif',\n        }}\n      >\n        <div\n          style={{\n            position: 'absolute',\n            left: 190,\n            top: 65,\n            width: 100,\n            height: 100,\n            borderRadius: '50%',\n            background: '#d2d2cf',\n            opacity: context,\n            transform: `translateY(${interpolate(context, [0, 1], [18, 0])}px) scale(${interpolate(\n              context,\n              [0, 1],\n              [0.88, 1],\n            )})`,\n            display: 'grid',\n            placeItems: 'start center',\n            paddingTop: 17,\n            boxSizing: 'border-box',\n            color: '#77787a',\n            fontSize: 9,\n            fontWeight: 800,\n            letterSpacing: 0.8,\n          }}\n        >\n          CONTEXT\n        </div>\n\n        <div\n          style={{\n            position: 'absolute',\n            left: 149,\n            top: 174,\n            width: 182,\n            height: 16,\n            clipPath: 'polygon(5% 0,95% 0,100% 100%,0 100%)',\n            background: '#92928f',\n            transformOrigin: '50% 50%',\n            transform: `scaleX(${platform})`,\n          }}\n        />\n\n        <div style={{ position: 'absolute', inset: 0, clipPath: 'inset(0 0 79px 0)' }}>\n          <SubjectPanel side=\"left\" progress={left} wobble={leftWobble} />\n          <SubjectPanel side=\"right\" progress={right} wobble={rightWobble} />\n        </div>\n\n        <div\n          style={{\n            position: 'absolute',\n            left: 108,\n            top: 205,\n            width: 264,\n            height: 74,\n            clipPath: 'polygon(18% 0,82% 0,100% 100%,0 100%)',\n            background: '#d9d9d6',\n            opacity: interpolate(conclusion, [0, 0.18, 1], [0, 0.45, 1], CLAMP),\n            transform: `translateY(${interpolate(conclusion, [0, 1], [96, 0])}px)`,\n            display: 'grid',\n            placeItems: 'start center',\n            paddingTop: 19,\n            boxSizing: 'border-box',\n            color: '#4f5053',\n            fontSize: 20,\n            fontWeight: 850,\n            letterSpacing: 1.8,\n          }}\n        >\n          OUTCOME\n        </div>\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}