{
  "slug": "svg-shape-morph",
  "title": "SVG shape morph loop",
  "tagline": "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.",
  "description": "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 i",
  "category": "ui-entrance",
  "tags": [
    "shotcraft",
    "remotion",
    "ui-entrance"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.7,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/svg-shape-morph/poster-33fc6277f8b5.jpg",
    "video": "https://gimgs.net/motion/media/svg-shape-morph/video-90b9cbc7be3d.mp4"
  },
  "variants": [
    {
      "key": "svg-shape-morph",
      "label": "svg-shape-morph",
      "description": "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",
      "use": "",
      "video": "https://gimgs.net/motion/media/svg-shape-morph/video-90b9cbc7be3d.mp4",
      "poster": "https://gimgs.net/motion/media/svg-shape-morph/poster-33fc6277f8b5.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/svg-shape-morph.html",
  "json_url": "https://gimgs.net/motion/svg-shape-morph.json",
  "template_url": "https://gimgs.net/motion/svg-shape-morph/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/svg-shape-morph.md",
  "updated_at": "2026-09-24T07:40:25.983Z",
  "prompt": "## When to use\n\nExpressing abstract \"shape-shifting/adaptive/organic growth\" concepts; an atmosphere beat before an opening logo, or a transition shape between chapters\n\n## Intention\n\nYou 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.\n\n## Duration & energy\n\n- Duration: ~5.2s (156f@30fps)\n- Energy: Low (a continuous flow with no peak, good as a voiceover backdrop or breathing beat)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n\"形态会变\"这件事，用两张图交叉淡入是说不清的——观众看到的是两个东西重叠。真正的\nmorph 要让观众相信这一直是**同一个东西**在改变形状。等点数重采样加逐点插值就是这个\n\"同一性\"的技术保证：没有点的生成或消失，只有半径在变。变回原形是为了闭环——观众\n看到往返，才知道这是可逆的能力而不是一次性的变身。\n\n## 动效核心\n- 两形状用极坐标函数定义，不是路径字符串：\n  `rA(θ) = 76*(1 + 0.30*cos(3θ) + 0.05*sin(7θ+0.8))`、\n  `rB(θ) = 76*(1 + 0.26*sin(5θ+1.2) + 0.06*cos(2θ))`——三瓣形与五瓣形，各带一层高频\n  扰动做\"有机\"细节\n- 重采样：两形状都在 `i/140 * 2π` 上取样存进 `radA/radB` 数组，天生等点数且一一对应\n  ——这就是 morphTo 需要的点数对齐，只不过在极坐标下免费获得\n- 插值只发生在半径上：`r = lerp(m, radA[i], radB[i])`，角度恒定。这保证插值中间态一定\n  是一条不自交的闭合曲线，不会出现路径插值常见的打结\n- 往返用两条窗口相减：`m1 = seg(t, 0.08, 0.42, E.inOutCubic)`（A→B）、\n  `m2 = seg(t, 0.58, 0.92, E.inOutCubic)`（B→A），`m = m1 - m2`——0 是 A、1 是 B，\n  中间 0.42→0.58 是 B 形态的静置段（约 25f）\n- 呼吸与自转是叠加的第二层：`breath = 1 + 0.045*(sin(m1*π) + sin(m2*π))` 让缩放在两次\n  变形的**中途**各鼓一次（形状最不稳定时鼓得最多）；`rot = sin(t*2π)*4°` 走一个整数\n  周期，末帧回零\n- 色相跟形态绑定：`hue = lerp(m, 185, 305)`，同时驱动描边 `hsl(hue,90%,66%)`、\n  填充 `hsla(hue,80%,58%,.14)` 与 `drop-shadow` 光晕——颜色是形态的读数，不是独立装饰\n- 字幕在 `m > 0.5` 时切换 `morphTo(shapeB)` / `morphTo(shapeA)`，透明度\n  `0.4 + 0.6*|m-0.5|*2` 在形态最\"纯\"时最亮、变形中途最淡\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 采样点数 | N=140 | 140 点在 480×270 上足够平滑（`L` 直线段肉眼看不出）；<60 点能看到多边形棱角，>300 点是纯浪费 |\n| 基半径 | BASE=76，中心 (240,138) | 76 相对 270 高度约占 56%——留边给光晕；调大要检查 `0.30` 的凸起是否顶出 viewBox |\n| 形状 A | `1 + 0.30cos(3θ) + 0.05sin(7θ+0.8)` | 3θ 定瓣数、0.30 定凸起深度；深度 >0.5 时凹处半径趋近 0，形状会掐出尖角 |\n| 形状 B | `1 + 0.26sin(5θ+1.2) + 0.06cos(2θ)` | 与 A 的瓣数不同（5 vs 3）才有变形张力；瓣数相同只会看到形状\"转了转\" |\n| 变形窗 | 0.08→0.42 / 0.58→0.92，均 inOutCubic | 行程 0.34（≈53f）；inOut 的缓起缓收是\"有机\"的关键，换 linear 立刻读作机械插值 |\n| 中段静置 | 0.42→0.58（≈25f） | 这是让观众认清 B 形态的时间；砍掉会读作一次来回摆动而不是 A→B→A |\n| 呼吸幅度 | 0.045 × `sin(m*π)` | 4.5% 的呼吸在变形中途达峰；>10% 会读作缩放动画而不是形变 |\n| 自转 | `sin(t*2π)*4°`（1 个整数周期） | 4° 是暗示级别的漂移，末帧回零可循环；>15° 会看出是整体旋转，与形变抢注意力 |\n| 色相区间 | 185°→305°（青→紫） | 跨度 120° 让两个形态有明确的身份差；跨度 <40° 时颜色变化读不出，色相就白给了 |\n\n## 已知坑\n- 极坐标定义天生只能表达\"从中心看每个角度一个半径\"的形状——星形、花瓣、水滴都行，\n  但**任何凹到自遮挡的形状（C 形、环形、有孔洞）都表达不出来**。要 morph 真实 logo\n  轮廓必须换成路径重采样，这张卡的技术路线不适用\n- 路径用 `L` 直线段拼接而非曲线段，靠点数密度掩盖。如果做大幅放大（>2 倍）或极粗\n  描边，棱角会显形——放大场景要提高 N 或改用 `C` 段\n- 每帧重建整条 `d` 字符串（140 点字符串拼接 × 2 个 path）。480×270 无压力，但如果同屏\n  放多个实例或点数上到 500，要考虑缓存插值结果\n- 两条形状函数写死在 setup 里，没有做成参数。要换形状是改函数体而不是传参——移植时\n  建议把 `rA/rB` 提成入参，否则每个变体都要复制一份 setup\n- 字幕 `morphTo(shapeB)` 是 anime.js API 的自我说明式占位文案，成片里应换成有语义的\n  文字或直接去掉\n- 背景写死 `#0a0b10`，整套颜色（90% 饱和描边 + 8px 光晕）依赖深底。白底上光晕失效，\n  填充的 0.14 透明度也几乎不可见，浅色主题需要重配饱和度与描边宽度\n\n## 参考实现\ndemos/ui-entrance/svg-shape-morph/\n（SvgShapeMorph.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/ui-entrance/svg-shape-morph/SvgShapeMorph.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/svg-shape-morph/SvgShapeMorph.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/svg-shape-morph/SvgShapeMorph.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/svg-shape-morph/SvgShapeMorph.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/svg-shape-morph/SvgShapeMorph.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": 171,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/svg-shape-morph/SvgShapeMorph.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/svg-shape-morph/SvgShapeMorph.tsx =====\n// svg-shape-morph — Shape Morph 轮廓变形（motion-lab 定稿转原生 Remotion）\n// 一个 SVG 轮廓平滑变形为另一个再变回：两条闭合轮廓先重采样到相同点数\n// （极坐标 140 点），逐点插值 + inOutCubic，变形中段加轻微 scale 呼吸与\n// 色相漂移，得到有机的流动感。\n// 设计坐标 480×270（DesignStage 等比放大），参数表数值以此坐标系标定。\nimport React from 'react';\nimport { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';\n\nexport const SVG_SHAPE_MORPH_DURATION = 156; // 5200ms @30fps\n\nconst CX = 240;\nconst CY = 138;\nconst N = 140;\nconst BASE = 76;\n\n// 两个形状：同一采样点数（等价 morphTo 的点数对齐）\nconst rA = (th: number) => BASE * (1 + 0.3 * Math.cos(th * 3) + 0.05 * Math.sin(th * 7 + 0.8));\nconst rB = (th: number) => BASE * (1 + 0.26 * Math.sin(th * 5 + 1.2) + 0.06 * Math.cos(th * 2));\nconst RAD_A: number[] = [];\nconst RAD_B: number[] = [];\nfor (let i = 0; i < N; i++) {\n  const th = (i / N) * Math.PI * 2;\n  RAD_A.push(rA(th));\n  RAD_B.push(rB(th));\n}\n\n// 按混合系数 m 逐点插值两条轮廓，拼 path d\nconst build = (m: number) => {\n  let d = '';\n  for (let i = 0; i < N; i++) {\n    const th = (i / N) * Math.PI * 2;\n    const r = lerp(m, RAD_A[i], RAD_B[i]);\n    const x = (CX + Math.cos(th) * r).toFixed(1);\n    const y = (CY + Math.sin(th) * r).toFixed(1);\n    d += (i ? 'L' : 'M') + x + ',' + y;\n  }\n  return d + 'Z';\n};\n\nexport const SvgShapeMorph: React.FC = () => {\n  const t = useT();\n  const m1 = seg(t, 0.08, 0.42, E.inOutCubic); // A → B\n  const m2 = seg(t, 0.58, 0.92, E.inOutCubic); // B → A\n  const m = m1 - m2; // 0=A 1=B\n  const d = build(m);\n  const hue = lerp(m, 185, 305);\n  // 变形中段的 scale 呼吸 + 缓慢自转\n  const breath = 1 + 0.045 * (Math.sin(m1 * Math.PI) + Math.sin(m2 * Math.PI));\n  const rot = Math.sin(t * Math.PI * 2) * 4;\n  return (\n    <DesignStage bg=\"#0a0b10\">\n      <svg\n        viewBox=\"0 0 480 270\"\n        style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', background: '#0a0b10' }}\n      >\n        <g transform={`translate(${CX},${CY}) scale(${breath}) rotate(${rot}) translate(${-CX},${-CY})`}>\n          <path d={d} fill={`hsla(${hue},80%,58%,.14)`} />\n          <path\n            d={d}\n            fill=\"none\"\n            strokeWidth={2}\n            strokeLinejoin=\"round\"\n            stroke={`hsl(${hue},90%,66%)`}\n            style={{ filter: `drop-shadow(0 0 8px hsla(${hue},90%,60%,.4))` }}\n          />\n        </g>\n        <text\n          x={CX}\n          y={252}\n          textAnchor=\"middle\"\n          fill=\"#5b6480\"\n          opacity={0.4 + 0.6 * Math.abs(m - 0.5) * 2}\n          style={{ font: '10px \"SF Mono\",Menlo,monospace', letterSpacing: '2px' }}\n        >\n          {m > 0.5 ? 'morphTo(shapeB)' : 'morphTo(shapeA)'}\n        </text>\n      </svg>\n    </DesignStage>\n  );\n};\n\n\n"
}