// Motion Prompt #012 · MIT License (c) 2026 Shohei Ohtaki · https://showheyohtaki.github.io/motion-prompt/#012 // // Shape morph / モーフィング // Pack: motion (Motion graphics moves) · element 12 · Category: Motion polish // What happens: A shape changes smoothly from circle to square to star to heart, with its colour. // Motion spec: // - Four shapes, 1.175 s each: circle → square with rounded-off corners → star → heart → back to circle. Base radius 64. // - Each segment holds first, then morphs to the next shape between 0.62 s and 0.04 s before its end (0.58 s). // - Every outline is stored as 240 points; points with the same index are linearly interpolated. Progress uses backOut (s = 1.6), so it overshoots slightly. // - Colour: blue → coral → yellow → mint, blended with quadratic ease-in-out. // - During the morph the scale swells by 1 + 0.07 × sin(π × progress). // - Four dots below; the dot for the current shape is larger and coloured. // When to use it: Use it to turn one icon or logo into another without a cut. // Tags: morph, morphing, shape, path, interpolate, transform, icon // Size: 539x208 px · Cycle: 4.7 s // Needs: kit motion -> https://showheyohtaki.github.io/motion-prompt/api/v1/kit/motion.js // Live demo: https://showheyohtaki.github.io/motion-prompt/#012 // // cells[i](k, w, h, u): k = CanvasRenderingContext2D (already cleared and wrapped in save/restore by the caller), w,h = size, u = seconds in [0, cycle). The picture depends only on u. // Element code starts here. CELLS, constants and helpers live in the kit. // @element 12 // 12 モーフィング(● → ■ → ★ → ♥ と、形と色がなめらかに変わる) const MN = 240; const SHAPES = (() => { const R = 64, mk = fn => { const a = []; for (let i = 0; i < MN; i++) a.push(fn(i / MN)); return a; }; const circle = mk(t => { const a = t * Math.PI * 2 - Math.PI / 2; return [Math.cos(a) * R, Math.sin(a) * R]; }); const sq = mk(t => { const a = t * Math.PI * 2 - Math.PI / 2; let r = R * 0.88 / Math.max(Math.abs(Math.cos(a)), Math.abs(Math.sin(a))); r = Math.min(r, R * 1.13); return [Math.cos(a) * r, Math.sin(a) * r]; }); const st = mk(t => { const seg = 1 / 10, j = Math.floor(t / seg), f = (t - j * seg) / seg, ro = R * 1.2, ri = R * 0.56; const a0 = j * Math.PI / 5 - Math.PI / 2, a1 = (j + 1) * Math.PI / 5 - Math.PI / 2, r0 = j % 2 ? ri : ro, r1 = j % 2 ? ro : ri; return [lerp(Math.cos(a0) * r0, Math.cos(a1) * r1, f), lerp(Math.sin(a0) * r0, Math.sin(a1) * r1, f)]; }); const hp = mk(t => { const a = t * Math.PI * 2, s = R / 15.5; return [16 * Math.pow(Math.sin(a), 3) * s, -(13 * Math.cos(a) - 5 * Math.cos(2 * a) - 2 * Math.cos(3 * a) - Math.cos(4 * a)) * s - 4]; }); return [circle, sq, st, hp]; })(); const MCOL = [C.blue, C.coral, C.yel, C.mint]; CELLS[11] = (k, w, h, u) => { stage(k); const T = CYC / 4, i = Math.min(3, Math.floor(u / T)), lu = u - i * T; const pr = P(lu, T - 0.62, T - 0.04), p = backOut(pr, 1.6), pc = eIOs(pr); const A = SHAPES[i], B = SHAPES[(i + 1) % 4], X = w / 2, Y = h / 2 + 3; // 次の形の予告(左右の小さな点) for (let j = 0; j < 4; j++) { const on = j === i ? 1 - pc : j === (i + 1) % 4 ? pc : 0; k.fillStyle = mix(C.navy3, MCOL[j], on); k.beginPath(); k.arc(X - 39 + j * 26, h - 16, 5 + 2 * on, 0, 7); k.fill(); } const pulse = 1 + 0.07 * Math.sin(Math.PI * pr); k.save(); k.translate(X, Y - 8); k.scale(pulse, pulse); k.fillStyle = mix(MCOL[i], MCOL[(i + 1) % 4], pc); k.beginPath(); for (let j = 0; j < MN; j++) { const x = lerp(A[j][0], B[j][0], p), y = lerp(A[j][1], B[j][1], p); j ? k.lineTo(x, y) : k.moveTo(x, y); } k.closePath(); k.lineJoin = 'round'; k.strokeStyle = k.fillStyle; k.lineWidth = 8; k.stroke(); k.fill(); k.restore(); };