Motion · Entrada de UI

Morph from primitive shape

A stroked circle breathes once (anticipation), then its SVG path interpolates over 24f into a rounded card outline, and the content fades in.

1920×108016:930 fps4.72s · 142 quadrosremotionLicença: Apache-2.0#shotcraft#remotion#ui-entrance#opening

Fonte: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composição Remotion (React/TSX) — vídeo de prévia, sem player no navegador

When to use

Entrances for graphic/icon/card-outline subjects; the classic primitive for a logo turning into a UI container.

Intention

The entrance library is all "coming in from outside" — flying in, slamming down, flipping — the subject already exists off-screen beforehand. This card is growing in place: a stroked circle at the frame's center takes one breath first, then its outline continuously morphs into a rounded rectangle card, and the content fades in after. The primitive's pause-and-breathe is the anticipation beat — the graphic-design translation of Disney's anticipation principle: the circle "inhales" first, so the audience already knows it's about to change, and the transformation itself needs no explanation. Only applies to graphic/icon/card-outline subjects — a bitmap screenshot can't morph.

Duration & energy

  • Duration: ~4.7s (breathing 20f + morph 24f + content fade-in 12f)
  • Energy: Medium-low

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

意图

入场库全是"从外面进来"——飞入、砸落、翻面,主体都预先存在于画外; 本卡是就地长出来:画面中心一个描边正圆先鼓一口气(scale 1→1.12→1),随即轮廓连续变形为圆角矩形卡片,内容再淡入。原型 停留加呼吸这一拍就是预备动作——迪士尼 anticipation 的图形学翻译: 圆先"吸气",观众就知道它要变,变形本身因此不需要任何解释。 仅适用图形/图标/卡轮廓类主体——位图截图不能 morph。

动效核心

  • 圆与圆角矩形写成同构 path:M + 8 段 cubic(4 直边段三等分点

做退化 cubic + 4 圆角段 kappa 公式 k=4/3·tan(Δθ/4)),逐命令 数值 lerp 生成 d 字符串,帧驱动全确定性

  • 圆锚点取矩形锚点同方位角,保证 t=0 是标准正圆、中途无自交
  • 呼吸拍 scale 1→1.12→1(spec 1.06 看不清,加码后可感——可感性

判例同族);呼吸与变形都用 Easing.inOut(cubic),interpolate 全 clamp

  • 内容是 HTML 层叠在 SVG 上,变形完成后 12f 淡入,不做 path 内嵌
  • 时间轴 140f:0–10 静置|10–30 呼吸|30–54 变形|56–68 内容

淡入|68–140 真静止 72f

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 呼吸幅度 | 1→1.12→1,20f inOut cubic | <1.1 读不出"吸气";>1.2 像要爆炸不是要变形 | | 原型停留 | 静置 10f + 呼吸 20f,合计 30f 才起变形 | 停留 <15f 读不出"预备",变形显得突兀 | | 变形时长 | 24f | 太快像切换、太慢像融化;24f 是"生长"的速度 | | 目标形态 | 520×300 圆角矩形卡(demo 值) | 圆角越大与圆同构性越好、中段越顺 | | 内容淡入 | 变形落定后 2f 起,12f 淡入 | 变形没完就进内容 = 内容跟着轮廓扭 |

已知坑

  • demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,

首次实战须以真实素材回验

  • 变形中途别叠其他动画——morph 本身就是全部信息,同时飞入/

变色/晃动会把"长出来"稀释成"乱动"

  • 位图截图不能 morph:本卡只对矢量轮廓成立,页面截图请回

入场库常规手法

  • path 插值两端命令数必须严格同构(同为 M+8C),命令数不齐

中途会闪跳;改目标形状时先对齐锚点数再调参

  • 变形中段呈"桶形"过渡是正常形态(QA 判例),不是穿帮;

出现自交/尖刺才是锚点方位角没对齐

参考实现

demos/ui-entrance/morph-from-primitive/ (MorphFromPrimitive.tsx)

Reference implementation (Remotion)

  • demos/ui-entrance/morph-from-primitive/MorphFromPrimitive.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/morph-from-primitive/MorphFromPrimitive.tsx

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

Tudo nesta página também é legível por máquina. Aponte seu agente para o JSON ou adicione o servidor MCP do gimgs e peça para buscar templates de motion.

  1. Baixe o JSON — contém a receita do plano (timing, easing, tabela de parâmetros, armadilhas) e o código TSX completo.
  2. Clone o repositório video-shotcraft; o componente importa fixtures de demos/_fixtures e helpers de assets/lib.
  3. Registre o componente como <Composition> em um projeto Remotion e renderize com `npx remotion render`, ou reimplemente a receita no seu próprio engine.
curl -s https://gimgs.net/motion/morph-from-primitive.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/ui-entrance/morph-from-primitive/MorphFromPrimitive.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("morph-from-primitive")

Prompt

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

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,907 chars · Apache-2.0Abrir template bruto ↗
// ===== demos/ui-entrance/morph-from-primitive/MorphFromPrimitive.tsx =====
// morph-from-primitive|原型变形
// 正圆呼吸一拍(anticipation) → path d 逐数值插值变形成 520×300 圆角矩形卡片轮廓
// → 卡片内部灰阶内容条淡入。全部帧驱动、确定性。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G } from '../../_fixtures/Fixtures';

const CX = 960;
const CY = 540;
const R = 130;
const RECT_W = 520;
const RECT_H = 300;
const RECT_R = 20;

// ---- 同构 path:M + 8 段 cubic(4 直边段 + 4 圆角段),圆与圆角矩形共用锚点拓扑 ----
// 圆角矩形锚点(中心坐标系,顺时针,从右边上端点起):
// A0(hw,-ihh) 右边上 → A1(hw,ihh) 右边下 → A2(iw,hh) 底边右 → A3(-iw,hh) 底边左
// → A4(-hw,ihh) 左边下 → A5(-hw,-ihh) 左边上 → A6(-iw,-hh) 顶边左 → A7(iw,-hh) 顶边右 → A0
const hw = RECT_W / 2; // 260
const hh = RECT_H / 2; // 150
const iw = hw - RECT_R; // 240
const ihh = hh - RECT_R; // 130
const KAPPA = 0.5522847498;

type Seg = [number, number, number, number, number, number]; // c1x c1y c2x c2y x y
type Shape = { start: [number, number]; segs: Seg[] };

const line = (from: [number, number], to: [number, number]): Seg => [
  from[0] + (to[0] - from[0]) / 3,
  from[1] + (to[1] - from[1]) / 3,
  from[0] + ((to[0] - from[0]) * 2) / 3,
  from[1] + ((to[1] - from[1]) * 2) / 3,
  to[0],
  to[1],
];
// 90° 圆角弧 from → to(弧心 c):c1 = from + k·(to−c),c2 = to + k·(from−c)
const corner = (
  from: [number, number],
  to: [number, number],
  c: [number, number]
): Seg => [
  from[0] + KAPPA * (to[0] - c[0]),
  from[1] + KAPPA * (to[1] - c[1]),
  to[0] + KAPPA * (from[0] - c[0]),
  to[1] + KAPPA * (from[1] - c[1]),
  to[0],
  to[1],
];

const rectAnchors: [number, number][] = [
  [hw, -ihh],
  [hw, ihh],
  [iw, hh],
  [-iw, hh],
  [-hw, ihh],
  [-hw, -ihh],
  [-iw, -hh],
  [iw, -hh],
];
const cornerCenters: [number, number][] = [
  [iw, ihh], // A1→A2 右下
  [-iw, ihh], // A3→A4 左下
  [-iw, -ihh], // A5→A6 左上
  [iw, -ihh], // A7→A0 右上
];

const rectShape: Shape = {
  start: rectAnchors[0],
  segs: [
    line(rectAnchors[0], rectAnchors[1]),
    corner(rectAnchors[1], rectAnchors[2], cornerCenters[0]),
    line(rectAnchors[2], rectAnchors[3]),
    corner(rectAnchors[3], rectAnchors[4], cornerCenters[1]),
    line(rectAnchors[4], rectAnchors[5]),
    corner(rectAnchors[5], rectAnchors[6], cornerCenters[2]),
    line(rectAnchors[6], rectAnchors[7]),
    corner(rectAnchors[7], rectAnchors[0], cornerCenters[3]),
  ],
};

// 圆:锚点取矩形锚点的同方位角,段间用精确弧公式 k = 4/3·tan(Δθ/4)
const angles = rectAnchors.map(([x, y]) => Math.atan2(y, x));
const circAnchors: [number, number][] = angles.map((a) => [
  R * Math.cos(a),
  R * Math.sin(a),
]);
const arcSeg = (i: number): Seg => {
  const a1 = angles[i];
  let a2 = angles[(i + 1) % 8];
  if (a2 <= a1) a2 += Math.PI * 2;
  const k = (4 / 3) * Math.tan((a2 - a1) / 4);
  const p1 = circAnchors[i];
  const p2 = circAnchors[(i + 1) % 8];
  return [
    p1[0] - k * R * Math.sin(a1),
    p1[1] + k * R * Math.cos(a1),
    p2[0] + k * R * Math.sin(a2 % (Math.PI * 2)),
    p2[1] - k * R * Math.cos(a2 % (Math.PI * 2)),
    p2[0],
    p2[1],
  ];
};
const circShape: Shape = {
  start: circAnchors[0],
  segs: [0, 1, 2, 3, 4, 5, 6, 7].map(arcSeg),
};

const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
const f2 = (n: number) => n.toFixed(2);

const morphPath = (t: number): string => {
  const sx = lerp(circShape.start[0], rectShape.start[0], t);
  const sy = lerp(circShape.start[1], rectShape.start[1], t);
  let d = `M ${f2(CX + sx)} ${f2(CY + sy)}`;
  for (let i = 0; i < 8; i++) {
    const a = circShape.segs[i];
    const b = rectShape.segs[i];
    const v = a.map((n, j) => lerp(n, b[j], t));
    d += ` C ${f2(CX + v[0])} ${f2(CY + v[1])} ${f2(CX + v[2])} ${f2(
      CY + v[3]
    )} ${f2(CX + v[4])} ${f2(CY + v[5])}`;
  }
  return d + ' Z';
};

// ---- 时间轴(30fps,总 140f)----
// 0–10 静置圆 | 10–30 呼吸一拍 scale 1→1.06→1 | 30–54 变形 24f
// | 56–68 内容条淡入 12f | 68–140 真静止 72f
export const MorphFromPrimitive: React.FC = () => {
  const frame = useCurrentFrame();

  const breath = interpolate(frame, [10, 20, 30], [1, 1.12, 1], {
    easing: Easing.inOut(Easing.cubic),
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  const t = interpolate(frame, [30, 54], [0, 1], {
    easing: Easing.inOut(Easing.cubic),
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  const contentOpacity = interpolate(frame, [56, 68], [0, 1], {
    easing: Easing.out(Easing.cubic),
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  const d = morphPath(t);

  return (
    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative' }}>
      <svg
        width={1920}
        height={1080}
        viewBox="0 0 1920 1080"
        style={{ position: 'absolute', inset: 0 }}
      >
        <g transform={`translate(${CX} ${CY}) scale(${breath}) translate(${-CX} ${-CY})`}>
          <path d={d} fill="none" stroke={G.ink} strokeWidth={3} />
        </g>
      </svg>
      {/* 卡片内容条(参考 line-boil 描边卡内容),变形完成后淡入 */}
      <div
        style={{
          position: 'absolute',
          left: CX - RECT_W / 2,
          top: CY - RECT_H / 2,
          width: RECT_W,
          height: RECT_H,
          boxSizing: 'border-box',
          padding: 36,
          display: 'flex',
          flexDirection: 'column',
          gap: 18,
          opacity: contentOpacity,
        }}
      >
        <div style={{ height: 26, width: '58%', background: G.bar, borderRadius: 13 }} />
        <div style={{ height: 16, width: '86%', background: G.line, borderRadius: 8 }} />
        <div style={{ height: 16, width: '72%', background: G.line, borderRadius: 8 }} />
        <div style={{ marginTop: 'auto', display: 'flex', gap: 14, alignItems: 'center' }}>
          <div style={{ width: 40, height: 40, borderRadius: 20, background: G.mid }} />
          <div style={{ height: 16, width: 120, background: G.line, borderRadius: 8 }} />
        </div>
      </div>
    </div>
  );
};


  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/morph-from-primitive/MorphFromPrimitive.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
—

Mais nesta categoria