この記事の概要
SVG をモーフィングさせたいなと思うことがあります。
有名なのは GSAP の MorphSVG な気がするのですが、Plemium プラン以上でしか使えず年額 $149 です。
ちょっとしたアニメーションをつけるだけにしては高額なので、自作できないかなと思い立ちました。
非常に簡易なもの&条件はありますが、とりあえず作れたので記事にしてみました。
方針
- svg 要素のうち、変形前の
path
のd
属性を取得する - 変形後の
d
の中身を用意しておく -
d
属性の中のM
やL
を除き、数値だけの配列に変換する - 線形補完の関数を用意し、2 つの配列を渡す
-
requestAnimationFrame
を使い変形させる
HTML
アニメーションをテストする目的なので、簡素なものを用意しただけです。
<body>
<h1>Self-made SVG Morphing</h1>
<svg
width="100"
height="100"
viewBox="0 0 100 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
id="morph-path"
d="M49.6025 13L92.9038 88L49.6025 88L6.30127 88L49.6025 13Z"
fill="#D9D9D9"
/>
</svg>
<button id="morph-button">Morph</button>
</body>
現時点の見た目はこのようになっています。
JavaScript
完成形を載せます。
テスト的に作っているのでpath2
をハードコーディングしてしまっていますが、外から渡すようにすれば再利用可能になるはずです。
document.addEventListener("DOMContentLoaded", function () {
const morphButton = document.getElementById("morph-button");
const pathElement = document.getElementById("morph-path");
const path1 = pathElement.getAttribute('d');
const path2 = "M88 12L88 88L12 88L12 12L88 12Z";
function lerp(start, end, t) {
return start + (end - start) * t;
}
function parsePath(path) {
return path.match(/[-+]?[0-9]*\.?[0-9]+/g).map(Number);
}
function generatePathString(commands, values) {
let valueIndex = 0;
return commands.reduce((pathString, command) => {
let commandValues = values.slice(valueIndex, valueIndex + command.length);
valueIndex += command.length;
return pathString + command.cmd + commandValues.join(" ");
}, "");
}
function morphPaths(path1, path2, t) {
const commands = path1.match(/[a-zA-Z][^a-zA-Z]*/g).map((command) => {
const numbers = command.slice(1).match(/[-+]?[0-9]*\.?[0-9]+/g) || [];
return { cmd: command[0], length: numbers.length };
});
const values1 = parsePath(path1);
const values2 = parsePath(path2);
const morphedValues = values1.map((val, i) => lerp(val, values2[i], t));
return generatePathString(commands, morphedValues);
}
morphButton.addEventListener("click", () => {
let t = 0;
function animate() {
t += 0.05;
if (t > 1) t = 1;
const morphedPath = morphPaths(path1, path2, t);
pathElement.setAttribute("d", morphedPath);
if (t < 1) requestAnimationFrame(animate);
}
animate();
});
});
このように変化します。
もう少し複雑な形状でも問題ありません。
lerp()
線形補完の関数です。
parsePath()
d
の文字列から数値だけを抽出し、配列に変換しています。
generatePathString()
新しいパス文字列を生成しています。
morphPaths()
path1
とpath2
の間で補完されたパスを生成しています。
M
, L
などと数値を分離し、数値部分を線形補完しています。
requestAnimationFrame()
今回はボタンのクリックイベントハンドラーの中で呼び出し、実行しています。
Intersection Observer API を使えば画面に入ってきたタイミングで変形させることもできます。
現状の制約
- 1 つの
path
にしか対応できていない - 変形前後でポイントの数が同じでないといけない
- 最初の例では三角形から四角形に変形しているものの、三角形の一辺にポイントを仕込んである
- 変形前後でコマンドが同じでないといけない
- 例えば
V
はパラメーターを 1 つしかとらず、L
は 2 つとる - このとき、配列の数があわなくなり、表示がおかしくなる
- 例えば
このように、複雑なイラストを動かしたりするには機能が不足しています。
ただ、幾何形態をちょろっと動かす程度なら問題なさそうです。
「スクロール量にあわせて背景に敷き詰めた画像が少し変形する」くらいであれば使えますし、装飾として効果的なときもありそうです。
状況を見て使ってみたいです。
追記
続編のような記事も投稿しました。