// svg-shape-morph — Shape Morph outline morphing (motion-lab final cut ported to native Remotion) // one SVG outline smoothly morphs into another and back: both closed outlines are first resampled // to the same point count (140 polar points), then interpolated point-by-point with inOutCubic, // plus a slight scale breathing and hue drift in the middle of the morph for an organic, fluid feel. // Design coordinates 480×270 (DesignStage scales proportionally), parameter table values calibrated to this coordinate system. 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; // two shapes: same sampling point count (equivalent to morphTo's point alignment) 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)); } // interpolate the two outlines point-by-point by blend factor m, building the 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 breathing + slow rotation during the morph 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 ( {m > 0.5 ? 'morphTo(shapeB)' : 'morphTo(shapeA)'} ); };