Motion · UI entrance

Platform hinge rise

A platform establishes first, two subjects flip up from adjacent hinges with a damped rebound, then a conclusion stand rises in.

1920×108016:930 fps3.52s · 106 framesremotionLicense: Apache-2.0#shotcraft#remotion#ui-entrance#opening

Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player

When to use

Side-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

Intention

Let 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.

Duration & energy

  • Duration: About 3.5s (104f@30fps; holds still for 30f after the conclusion settles)
  • Energy: Medium (the two subjects flipping up together mid-shot is the only major action; the ending is just a steady platform rise)

Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)

意图

先让平台成为可信的“承托关系”,再让两个主体从平台后方翻起,观众会自然把它们 理解为同组证据;最后从下方升起的结论台不是第三个平级元素,而是对前两者的归纳。 固定机位和明确的遮挡顺序让这张卡适合解释性 B-roll,也能换成产品卡、人物、建筑或 对比方案。核心不是“东西从下方出现”,而是两个相邻铰点产生方向相反的展开力。

动效核心

  • f0–15:182×16 的梯形平台从中心 seed 以 scaleX 0.012→1 建立,缓动

cubic-bezier(.16,1,.3,1);必须先看见承托面,主体才能随后成立

  • f12–32:背景语境圆 scale .88→1、translateY 18→0、opacity 0→1,

比主体早 2f 起步、晚 2f 收尾,作为低能量背景层

  • f14–30:左右主体共用 cubic-bezier(.4,0,.2,1),分别从 translateY 112/90px

与 rotate -18°/+18° 回到 0;两块的 transform-origin 分别钉在相邻的右下/左下铰点

  • f30–46:左块 1.5°、右块 -1.2° 的三半波阻尼回摆,振幅乘 (1-p)^2;

右块延迟 1f,避免完全镜像的机械感

  • f52–74:结论台 translateY 96→0,opacity 在前 18% 行程先到 0.45 再收至 1;

f74–103 完全静止,让结论可读

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 平台建立 | f0–15,scaleX .012→1 | 少于 10f 像硬切,多于 22f 会拖慢主体登场 | | 语境圆 | f12–32,scale .88→1,rise 18px | 背景必须先于主体可辨认,但不能先完成太久 | | 双主体翻起 | f14–30;左 rise 112px/-18°,右 rise 90px/+18° | 命门:两块必须从相邻铰点反向展开;同向旋转会失去“打开舞台”的力学关系 | | 阻尼回摆 | 16f;左 1.5°,右 1.2°,右延迟 1f | 只允许一次可感回摆;振幅 >2° 会像卡片玩具 | | 结论台 | f52–74,rise 96px | 与主体落定至少隔 6f,避免证据和结论同时抢读 | | 收尾 hold | 30f | 结论文字若超过 8 个汉字,继续延长总时长,不要压缩升台动作 |

已知坑

  • 主体必须被平台上缘或独立遮罩裁住底部;没有遮挡会暴露它们从画外平移而非从平台后翻起
  • transform-origin 不能放在各自主体中心:中心旋转读作两张卡在晃,只有相邻底部铰点才读作展开
  • 左右块不要做成完全同尺寸、同高度、同振幅;轻微不对称是让双主体保持可辨认轮廓的关键
  • 结论台是总结层,视觉对比应低于两个主体;若给它更深颜色或更大过冲,会反客为主
  • demo 只含原创通用 DOM 形状和占位文字,不依赖复刻研究里的品牌、建筑或视频素材

参考实现

demos/ui-entrance/platform-hinge-rise/ (PlatformHingeRise.tsx)

Reference implementation (Remotion)

  • demos/ui-entrance/platform-hinge-rise/PlatformHingeRise.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/platform-hinge-rise/PlatformHingeRise.tsx

The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.

Everything on this page is also machine-readable. Point your agent at the JSON, or add the gimgs MCP server and ask it to search motion templates.

  1. Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
  2. Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
  3. Register the component as a <Composition> in a Remotion project and render with `npx remotion render`, or re-implement the recipe in your own engine.
curl -s https://gimgs.net/motion/platform-hinge-rise.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/ui-entrance/platform-hinge-rise/PlatformHingeRise.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("platform-hinge-rise")

Prompt

Use the video-shotcraft shot recipe "Platform hinge rise" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/platform-hinge-rise.json
Raw TSX: https://gimgs.net/motion/platform-hinge-rise/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/ui-entrance/platform-hinge-rise/PlatformHingeRise.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 3.52s

1. Read the JSON and follow its "prompt" section (when to use, intention, parameter table, known pitfalls).
2. Adapt it for: <describe what you want>.
3. Either clone the repo, register the component as a <Composition> in a Remotion project and run npx remotion render <CompositionId> out.mp4 — or re-implement the recipe in your own engine (HTML/WAAPI works: keep the same timing and easing).

JSON · Markdown · llms.txt

5,063 chars · Apache-2.0Open raw template ↗
// ===== demos/ui-entrance/platform-hinge-rise/PlatformHingeRise.tsx =====
// platform-hinge-rise — 平台建立后,两块主体从相邻底部铰点反向翻起,最后结论台升入。
// 从独立 motion-blocking 研究中整理为纯 DOM/SVG 的通用 Remotion demo。
import React from 'react';
import { Easing, interpolate, useCurrentFrame } from 'remotion';
import { DesignStage } from '../../_fixtures/Motion';

export const PLATFORM_HINGE_RISE_DURATION = 104;

const CLAMP = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;

const ease = (frame: number, start: number, end: number, from = 0, to = 1) =>
  interpolate(frame, [start, Math.max(start + 1, end)], [from, to], {
    ...CLAMP,
    easing: Easing.bezier(0.16, 1, 0.3, 1),
  });

const hingeEase = (frame: number, start: number, end: number) =>
  interpolate(frame, [start, Math.max(start + 1, end)], [0, 1], {
    ...CLAMP,
    easing: Easing.bezier(0.4, 0, 0.2, 1),
  });

const dampedWobble = (frame: number, start: number, duration: number, amplitude: number) => {
  if (frame <= start || frame >= start + duration) return 0;
  const p = (frame - start) / duration;
  return amplitude * Math.sin(p * Math.PI * 3) * Math.pow(1 - p, 2);
};

const SubjectPanel: React.FC<{
  side: 'left' | 'right';
  progress: number;
  wobble: number;
}> = ({ side, progress, wobble }) => {
  const isLeft = side === 'left';
  const x = isLeft ? 165 : 249;
  const w = isLeft ? 92 : 78;
  const h = isLeft ? 72 : 59;
  const rise = isLeft ? 112 : 90;
  const startRotation = isLeft ? -18 : 18;
  return (
    <div
      style={{
        position: 'absolute',
        left: x,
        top: 108,
        width: w,
        height: h,
        clipPath: 'polygon(8% 10%,92% 0,100% 100%,0 100%)',
        background: isLeft ? '#5f6064' : '#77787c',
        boxShadow: '0 10px 22px rgba(0,0,0,.12)',
        transformOrigin: isLeft ? '100% 100%' : '0% 100%',
        transform: `translateY(${interpolate(progress, [0, 1], [rise, 0])}px) rotate(${interpolate(
          progress,
          [0, 1],
          [startRotation, 0],
        ) + wobble}deg)`,
        display: 'grid',
        placeItems: 'center',
        color: '#f7f7f5',
        fontFamily: 'Arial, sans-serif',
        fontSize: 10,
        fontWeight: 800,
        letterSpacing: 1,
      }}
    >
      SUBJECT
    </div>
  );
};

export const PlatformHingeRise: React.FC = () => {
  const frame = useCurrentFrame();
  const platform = ease(frame, 0, 15, 0.012, 1);
  const context = ease(frame, 12, 32);
  const left = hingeEase(frame, 14, 30);
  const right = hingeEase(frame, 14, 30);
  const conclusion = ease(frame, 52, 74);
  const leftWobble = dampedWobble(frame, 30, 16, 1.5);
  const rightWobble = dampedWobble(frame, 31, 16, -1.2);

  return (
    <DesignStage bg="#f4f4f1" raster="zoom">
      <div
        style={{
          position: 'absolute',
          inset: 0,
          overflow: 'hidden',
          fontFamily: 'Arial, sans-serif',
        }}
      >
        <div
          style={{
            position: 'absolute',
            left: 190,
            top: 65,
            width: 100,
            height: 100,
            borderRadius: '50%',
            background: '#d2d2cf',
            opacity: context,
            transform: `translateY(${interpolate(context, [0, 1], [18, 0])}px) scale(${interpolate(
              context,
              [0, 1],
              [0.88, 1],
            )})`,
            display: 'grid',
            placeItems: 'start center',
            paddingTop: 17,
            boxSizing: 'border-box',
            color: '#77787a',
            fontSize: 9,
            fontWeight: 800,
            letterSpacing: 0.8,
          }}
        >
          CONTEXT
        </div>

        <div
          style={{
            position: 'absolute',
            left: 149,
            top: 174,
            width: 182,
            height: 16,
            clipPath: 'polygon(5% 0,95% 0,100% 100%,0 100%)',
            background: '#92928f',
            transformOrigin: '50% 50%',
            transform: `scaleX(${platform})`,
          }}
        />

        <div style={{ position: 'absolute', inset: 0, clipPath: 'inset(0 0 79px 0)' }}>
          <SubjectPanel side="left" progress={left} wobble={leftWobble} />
          <SubjectPanel side="right" progress={right} wobble={rightWobble} />
        </div>

        <div
          style={{
            position: 'absolute',
            left: 108,
            top: 205,
            width: 264,
            height: 74,
            clipPath: 'polygon(18% 0,82% 0,100% 100%,0 100%)',
            background: '#d9d9d6',
            opacity: interpolate(conclusion, [0, 0.18, 1], [0, 0.45, 1], CLAMP),
            transform: `translateY(${interpolate(conclusion, [0, 1], [96, 0])}px)`,
            display: 'grid',
            placeItems: 'start center',
            paddingTop: 19,
            boxSizing: 'border-box',
            color: '#4f5053',
            fontSize: 20,
            fontWeight: 850,
            letterSpacing: 1.8,
          }}
        >
          OUTCOME
        </div>
      </div>
    </DesignStage>
  );
};


  1. Read the recipe
    Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
  2. Get the code
    tool: git
    git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install
  3. Register + render
    tool: remotion
    npx remotion render <CompositionId> out.mp4 # component: demos/ui-entrance/platform-hinge-rise/PlatformHingeRise.tsx
  4. Or re-implement
    Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
paramtypedefaultdescription
—

More in this category