Motion · UI entrance

SVG shape morph loop

A 140-point closed outline morphs smoothly into another and back; both shapes are resampled to equal point counts in polar coordinates with per-point radius interpolation and inOutCubic, plus a light scale breathing, slow rotation, and hue drifting 185°→305° mid-morph.

1920×108016:930 fps5.7s · 171 framesremotionLicense: Apache-2.0#shotcraft#remotion#ui-entrance

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

When to use

Expressing abstract "shape-shifting/adaptive/organic growth" concepts; an atmosphere beat before an opening logo, or a transition shape between chapters

Intention

You can't make "shape changes" convincing with a cross-fade between two images — the viewer just sees two things overlapping. A true morph needs the viewer to believe this is the same thing all along, changing shape. Equal-count resampling plus per-point interpolation is the technical guarantee of that "sameness": no points are born or die, only radius changes. Morphing back closes the loop — seeing the round trip is what tells the viewer this is a reversible capability, not a one-time transformation.

Duration & energy

  • Duration: ~5.2s (156f@30fps)
  • Energy: Low (a continuous flow with no peak, good as a voiceover backdrop or breathing beat)

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

意图

"形态会变"这件事,用两张图交叉淡入是说不清的——观众看到的是两个东西重叠。真正的 morph 要让观众相信这一直是同一个东西在改变形状。等点数重采样加逐点插值就是这个 "同一性"的技术保证:没有点的生成或消失,只有半径在变。变回原形是为了闭环——观众 看到往返,才知道这是可逆的能力而不是一次性的变身。

动效核心

  • 两形状用极坐标函数定义,不是路径字符串:

rA(θ) = 76*(1 + 0.30*cos(3θ) + 0.05*sin(7θ+0.8))、 rB(θ) = 76*(1 + 0.26*sin(5θ+1.2) + 0.06*cos(2θ))——三瓣形与五瓣形,各带一层高频 扰动做"有机"细节

  • 重采样:两形状都在 i/140 * 2π 上取样存进 radA/radB 数组,天生等点数且一一对应

——这就是 morphTo 需要的点数对齐,只不过在极坐标下免费获得

  • 插值只发生在半径上:r = lerp(m, radA[i], radB[i]),角度恒定。这保证插值中间态一定

是一条不自交的闭合曲线,不会出现路径插值常见的打结

  • 往返用两条窗口相减:m1 = seg(t, 0.08, 0.42, E.inOutCubic)(A→B)、

m2 = seg(t, 0.58, 0.92, E.inOutCubic)(B→A),m = m1 - m2——0 是 A、1 是 B, 中间 0.42→0.58 是 B 形态的静置段(约 25f)

  • 呼吸与自转是叠加的第二层:breath = 1 + 0.045*(sin(m1*π) + sin(m2*π)) 让缩放在两次

变形的中途各鼓一次(形状最不稳定时鼓得最多);rot = sin(t*2π)*4° 走一个整数 周期,末帧回零

  • 色相跟形态绑定:hue = lerp(m, 185, 305),同时驱动描边 hsl(hue,90%,66%)、

填充 hsla(hue,80%,58%,.14) 与 drop-shadow 光晕——颜色是形态的读数,不是独立装饰

  • 字幕在 m > 0.5 时切换 morphTo(shapeB) / morphTo(shapeA),透明度

0.4 + 0.6*|m-0.5|*2 在形态最"纯"时最亮、变形中途最淡

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 采样点数 | N=140 | 140 点在 480×270 上足够平滑(L 直线段肉眼看不出);<60 点能看到多边形棱角,>300 点是纯浪费 | | 基半径 | BASE=76,中心 (240,138) | 76 相对 270 高度约占 56%——留边给光晕;调大要检查 0.30 的凸起是否顶出 viewBox | | 形状 A | 1 + 0.30cos(3θ) + 0.05sin(7θ+0.8) | 3θ 定瓣数、0.30 定凸起深度;深度 >0.5 时凹处半径趋近 0,形状会掐出尖角 | | 形状 B | 1 + 0.26sin(5θ+1.2) + 0.06cos(2θ) | 与 A 的瓣数不同(5 vs 3)才有变形张力;瓣数相同只会看到形状"转了转" | | 变形窗 | 0.08→0.42 / 0.58→0.92,均 inOutCubic | 行程 0.34(≈53f);inOut 的缓起缓收是"有机"的关键,换 linear 立刻读作机械插值 | | 中段静置 | 0.42→0.58(≈25f) | 这是让观众认清 B 形态的时间;砍掉会读作一次来回摆动而不是 A→B→A | | 呼吸幅度 | 0.045 × sin(m*π) | 4.5% 的呼吸在变形中途达峰;>10% 会读作缩放动画而不是形变 | | 自转 | sin(t*2π)*4°(1 个整数周期) | 4° 是暗示级别的漂移,末帧回零可循环;>15° 会看出是整体旋转,与形变抢注意力 | | 色相区间 | 185°→305°(青→紫) | 跨度 120° 让两个形态有明确的身份差;跨度 <40° 时颜色变化读不出,色相就白给了 |

已知坑

  • 极坐标定义天生只能表达"从中心看每个角度一个半径"的形状——星形、花瓣、水滴都行,

但任何凹到自遮挡的形状(C 形、环形、有孔洞)都表达不出来。要 morph 真实 logo 轮廓必须换成路径重采样,这张卡的技术路线不适用

  • 路径用 L 直线段拼接而非曲线段,靠点数密度掩盖。如果做大幅放大(>2 倍)或极粗

描边,棱角会显形——放大场景要提高 N 或改用 C 段

  • 每帧重建整条 d 字符串(140 点字符串拼接 × 2 个 path)。480×270 无压力,但如果同屏

放多个实例或点数上到 500,要考虑缓存插值结果

  • 两条形状函数写死在 setup 里,没有做成参数。要换形状是改函数体而不是传参——移植时

建议把 rA/rB 提成入参,否则每个变体都要复制一份 setup

  • 字幕 morphTo(shapeB) 是 anime.js API 的自我说明式占位文案,成片里应换成有语义的

文字或直接去掉

  • 背景写死 #0a0b10,整套颜色(90% 饱和描边 + 8px 光晕)依赖深底。白底上光晕失效,

填充的 0.14 透明度也几乎不可见,浅色主题需要重配饱和度与描边宽度

参考实现

demos/ui-entrance/svg-shape-morph/ (SvgShapeMorph.tsx)

Reference implementation (Remotion)

  • demos/ui-entrance/svg-shape-morph/SvgShapeMorph.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/svg-shape-morph/SvgShapeMorph.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/svg-shape-morph.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/ui-entrance/svg-shape-morph/SvgShapeMorph.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("svg-shape-morph")

Prompt

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

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

2,684 chars · Apache-2.0Open raw template ↗
// ===== demos/ui-entrance/svg-shape-morph/SvgShapeMorph.tsx =====
// svg-shape-morph — Shape Morph 轮廓变形(motion-lab 定稿转原生 Remotion)
// 一个 SVG 轮廓平滑变形为另一个再变回:两条闭合轮廓先重采样到相同点数
// (极坐标 140 点),逐点插值 + inOutCubic,变形中段加轻微 scale 呼吸与
// 色相漂移,得到有机的流动感。
// 设计坐标 480×270(DesignStage 等比放大),参数表数值以此坐标系标定。
import React from 'react';
import { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';

export const SVG_SHAPE_MORPH_DURATION = 156; // 5200ms @30fps

const CX = 240;
const CY = 138;
const N = 140;
const BASE = 76;

// 两个形状:同一采样点数(等价 morphTo 的点数对齐)
const rA = (th: number) => BASE * (1 + 0.3 * Math.cos(th * 3) + 0.05 * Math.sin(th * 7 + 0.8));
const rB = (th: number) => BASE * (1 + 0.26 * Math.sin(th * 5 + 1.2) + 0.06 * Math.cos(th * 2));
const RAD_A: number[] = [];
const RAD_B: number[] = [];
for (let i = 0; i < N; i++) {
  const th = (i / N) * Math.PI * 2;
  RAD_A.push(rA(th));
  RAD_B.push(rB(th));
}

// 按混合系数 m 逐点插值两条轮廓,拼 path d
const build = (m: number) => {
  let d = '';
  for (let i = 0; i < N; i++) {
    const th = (i / N) * Math.PI * 2;
    const r = lerp(m, RAD_A[i], RAD_B[i]);
    const x = (CX + Math.cos(th) * r).toFixed(1);
    const y = (CY + Math.sin(th) * r).toFixed(1);
    d += (i ? 'L' : 'M') + x + ',' + y;
  }
  return d + 'Z';
};

export const SvgShapeMorph: React.FC = () => {
  const t = useT();
  const m1 = seg(t, 0.08, 0.42, E.inOutCubic); // A → B
  const m2 = seg(t, 0.58, 0.92, E.inOutCubic); // B → A
  const m = m1 - m2; // 0=A 1=B
  const d = build(m);
  const hue = lerp(m, 185, 305);
  // 变形中段的 scale 呼吸 + 缓慢自转
  const breath = 1 + 0.045 * (Math.sin(m1 * Math.PI) + Math.sin(m2 * Math.PI));
  const rot = Math.sin(t * Math.PI * 2) * 4;
  return (
    <DesignStage bg="#0a0b10">
      <svg
        viewBox="0 0 480 270"
        style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', background: '#0a0b10' }}
      >
        <g transform={`translate(${CX},${CY}) scale(${breath}) rotate(${rot}) translate(${-CX},${-CY})`}>
          <path d={d} fill={`hsla(${hue},80%,58%,.14)`} />
          <path
            d={d}
            fill="none"
            strokeWidth={2}
            strokeLinejoin="round"
            stroke={`hsl(${hue},90%,66%)`}
            style={{ filter: `drop-shadow(0 0 8px hsla(${hue},90%,60%,.4))` }}
          />
        </g>
        <text
          x={CX}
          y={252}
          textAnchor="middle"
          fill="#5b6480"
          opacity={0.4 + 0.6 * Math.abs(m - 0.5) * 2}
          style={{ font: '10px "SF Mono",Menlo,monospace', letterSpacing: '2px' }}
        >
          {m > 0.5 ? 'morphTo(shapeB)' : 'morphTo(shapeA)'}
        </text>
      </svg>
    </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/svg-shape-morph/SvgShapeMorph.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