Motion · UI xuất hiện

Bệ đỡ mở bản lề dựng lên

Bệ đỡ thiết lập ngang trước, hai chủ thể lật lên từ hai bản lề đáy kề nhau kèm dao động tắt dần, rồi bệ kết luận trồi lên.

1920×108016:930 fps3.52s · 106 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#ui-entrance#opening

Nguồn: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composition Remotion (React/TSX) — có video preview, không có player trong trình duyệt

Khi nào dùng

Màn ra mắt song song cho hai chủ thể/phương án/thương hiệu; công trình, thiết bị, thẻ dựng lên từ bệ; cảnh giải thích cần "bày chứng cứ trước, kết luận sau" trong một góc máy cố định

Ý đồ

Để bệ đỡ tự thiết lập thành một "quan hệ nâng đỡ" đáng tin trước, rồi để hai chủ thể lật lên từ phía sau bệ, người xem sẽ tự nhiên hiểu chúng là cùng một bộ chứng cứ; bệ kết luận trồi lên từ dưới ở cuối không phải yếu tố ngang hàng thứ ba, mà là sự tổng hợp của hai cái trước. Góc máy cố định và thứ tự che khuất rõ ràng khiến cảnh này phù hợp cho B-roll giải thích, cũng có thể đổi thành thẻ sản phẩm, con người, công trình hay phương án so sánh. Cốt lõi không phải "vật xuất hiện từ dưới lên", mà là hai bản lề kề nhau tạo ra lực mở theo hai hướng ngược nhau.

Thời lượng & năng lượng

  • Thời lượng: Khoảng 3.5s (104f@30fps; đứng yên 30f sau khi kết luận an vị)
  • Năng lượng: Trung bình (hai chủ thể cùng lật lên giữa đoạn là động tác chính duy nhất, cuối đoạn chỉ là bệ trồi lên chắc chắn)

Công thức cảnh quay (bản gốc tiếng Trung — timing, easing, bảng tham số, lỗi hay gặp)

意图

先让平台成为可信的“承托关系”,再让两个主体从平台后方翻起,观众会自然把它们 理解为同组证据;最后从下方升起的结论台不是第三个平级元素,而是对前两者的归纳。 固定机位和明确的遮挡顺序让这张卡适合解释性 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)

Mã nguồn tham chiếu (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

Component import fixture chung từ demos/_fixtures và helper từ assets/lib của repo; xem SKILL.md trong repo để biết quy trình render.

Mọi thứ trên trang này đều có bản máy đọc. Đưa agent link JSON, hoặc thêm MCP server gimgs rồi bảo nó tìm motion template.

  1. Tải JSON — trong đó có công thức cảnh quay (timing, easing, bảng tham số, lỗi hay gặp) và toàn bộ source TSX.
  2. Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
  3. Đăng ký component thành <Composition> trong project Remotion rồi render bằng `npx remotion render`, hoặc dựng lại công thức bằng engine của bạn.
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 "Bệ đỡ mở bản lề dựng lên" (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.0Mở template gốc ↗
// ===== 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
—

Cùng danh mục