SVGアニメーションを簡単に実装できるSnap.svgの使い方

そもそもSnap.svgとは?

Snap.svgの公式サイト

Snap.svgはSVGにアニメーションを付けることができるJavaScriptのライブラリです。

ベクター画像のパスを指定し動きを付けることで、CSSのみだと難しい複雑なアニメーションを実現することができます。

Snap.svgでアニメーションを実装する方法

完成イメージ

今回は上のような、振り回されてるハンマーを作ってみます。

※ gif画像のため、多少カクついて見えるかもしれません。

1. jsファイルを読み込む

まず公式サイトからjsファイルをダウンロードします。

zipを解凍後、Snap.svg > dist > snap.svg-min.jsを任意のディレクトリに設置します。

<script type="text/javascript" src="snap.svg-min.js"></script>

あとはheadタグ内でそれを読み込みます。

2. SVG画像を用意する

<svg xmlns="http://www.w3.org/2000/svg" viewBox="-70 -50 400 300">
<rect
x="60.63957"
y="155.39365"
width="189.22245"
height="5"
rx="2.5"
transform="translate(-105.38649 -14.71627) rotate(-21.37376)"
fill="#c69c6d"
/>
<path
d="M105.05159,208.94745A28.47838,28.47838,0,0,1,80.09194,218.716Q69.30588,191.15637,58.5198,163.59674a22.33592,22.33592,0,0,1,24.95965-9.76849Q94.26553,181.38786,105.05159,208.94745Z"
transform="translate(-58.5198 -82.15695)"
fill="#3f4a57"
/>
<ellipse
cx="71.20314"
cy="159.23249"
rx="13.40157"
ry="5.02559"
transform="translate(-111.6555 -45.25486) rotate(-21.374)"
fill="#303a45"
/>
</svg>

ハンマーのSVG画像

まず動かしたいSVG画像を用意します。

3. 動かしたいパスにクラスを指定

<svg id="js-svg" xmlns="http://www.w3.org/2000/svg" viewBox="-70 -50 400 300">
<rect
id="svg-move-parts1"
x="60.63957"
y="155.39365"
width="189.22245"
height="5"
rx="2.5"
transform="translate(-105.38649 -14.71627) rotate(-21.37376)"
fill="#c69c6d"
/>
<path
id="svg-move-parts2"
d="M105.05159,208.94745A28.47838,28.47838,0,0,1,80.09194,218.716Q69.30588,191.15637,58.5198,163.59674a22.33592,22.33592,0,0,1,24.95965-9.76849Q94.26553,181.38786,105.05159,208.94745Z"
transform="translate(-58.5198 -82.15695)"
fill="#3f4a57"
/>
<ellipse
id="svg-move-parts3"
cx="71.20314"
cy="159.23249"
rx="13.40157"
ry="5.02559"
transform="translate(-111.6555 -45.25486) rotate(-21.374)"
fill="#303a45"
/>
</svg>

今回の場合は全てのパスを動かしたいのでそれぞれにidを割り当てます。

4. 変形後のパスを用意する

<rect
id="svg-move-parts1"
x="60.63957"
y="155.39365"
width="189.22245"
height="5"
rx="2.5"
transform="translate(-105.38649 -14.71627) rotate(-21.37376)"
fill="#c69c6d"
/>
<path
id="svg-move-parts2"
d="M105.05159,208.94745A28.47838,28.47838,0,0,1,80.09194,218.716Q69.30588,191.15637,58.5198,163.59674a22.33592,22.33592,0,0,1,24.95965-9.76849Q94.26553,181.38786,105.05159,208.94745Z"
transform="translate(-58.5198 -82.15695)"
fill="#3f4a57"
/>
<ellipse
id="svg-move-parts3"
cx="71.20314"
cy="159.23249"
rx="13.40157"
ry="5.02559"
transform="translate(-111.6555 -45.25486) rotate(-21.374)"
fill="#303a45"
/>

変形後(アニメーション終了時)のタグを用意します。

x="60.63957" y="155.39365" width="189.22245" height="5" rx="2.5" transform="translate(-105.38649 -14.71627) rotate(-21.37376)" fill="#c69c6d"

そのタグから座標を抜き取ります。例だとこの部分ですね。

5. animateメソッドで動かす

const SPEED = 200; // アニメーションの速度
const $svgParts1 = Snap("#svg-move-parts1");
const $svgParts2 = Snap("#svg-move-parts2");
const $svgParts3 = Snap("#svg-move-parts3");
$svgParts1.animate(
{
x: "75",
y: "34",
width: "189.22245",
height: "5",
rx: "2.5",
transform: "translate(137.27166 -126.41038) rotate(77.23189)",
},
SPEED,
mina.easein
);
$svgParts2.animate(
{
d: "M156.4867,71.15524a28.47835,28.47835,0,0,1-5.92376-26.14034l57.72658-13.08162a22.33593,22.33593,0,0,1,5.92373,26.14033Z",
},
SPEED,
mina.easein
);
$svgParts3.animate(
{
cx: "246",
cy: "15",
rx: "5.02559",
ry: "13.40157",
transform: "translate(-148.11801 15.75089) rotate(-12.76835)",
},
SPEED,
mina.easein
);

まずSnapクラスのインスタンスを作成し、動かしたいタグそれぞれのidを指定します。

第2引数ではアニメーションの速度(ミリ秒)を、第3引数ではイージングを指定。

そしてそのインスタンスそれぞれで、先ほど抜き取ったパスを引数としてanimateメソッドを呼びます。

2点間のアニメーション

色々ツッコミどころのある動きですが、これで簡単な2点間のアニメーションは実装できました。

今回のアニメーションは3点間を行き来することによってハンマーを振り回してるように見せるものなので、3つのパスを行ったり来たり&繰り返す必要があります。

6. アニメーションを繰り返す

$svgParts3.animate(
{
cx: "71.20314",
cy: "159.23249",
rx: "13.40157",
ry: "5.02559",
transform: "translate(-111.6555 -45.25486) rotate(-21.374)",
},
SPEED,
EASEIN,
humerAnimation1
);

上記ようにアニメーションが終了後に実行されるコールバック関数を引数で指定できるので、これを利用して3点間のアニメーションを繰り返します。

const EASEIN = mina.easein;
let direction = true;
function humerAnimation0() {
direction = true;
$svgParts1.animate(
{
x: "60.63957",
y: "155.39365",
width: "189.22245",
height: "5",
rx: "2.5",
transform: "translate(-105.38649 -14.71627) rotate(-21.37376)",
},
SPEED,
EASEIN
);
$svgParts2.animate(
{
d: "M105.05159,208.94745A28.47838,28.47838,0,0,1,80.09194,218.716Q69.30588,191.15637,58.5198,163.59674a22.33592,22.33592,0,0,1,24.95965-9.76849Q94.26553,181.38786,105.05159,208.94745Z",
},
SPEED,
EASEIN
);
$svgParts3.animate(
{
cx: "71.20314",
cy: "159.23249",
rx: "13.40157",
ry: "5.02559",
transform: "translate(-111.6555 -45.25486) rotate(-21.374)",
},
SPEED,
EASEIN,
humerAnimation1
);
}
function humerAnimation1() {
if (direction) {
$svgParts1.animate(
{
x: "75",
y: "34",
width: "189.22245",
height: "5",
rx: "2.5",
transform: "translate(137.27166 -126.41038) rotate(77.23189)",
},
SPEED,
EASEIN
);
$svgParts2.animate(
{
d: "M156.4867,71.15524a28.47835,28.47835,0,0,1-5.92376-26.14034l57.72658-13.08162a22.33593,22.33593,0,0,1,5.92373,26.14033Z",
},
SPEED,
EASEIN
);
$svgParts3.animate(
{
cx: "246",
cy: "15",
rx: "5.02559",
ry: "13.40157",
transform: "translate(-148.11801 15.75089) rotate(-12.76835)",
},
SPEED,
EASEIN,
humerAnimation2
);
} else {
$svgParts1.animate(
{
x: "75",
y: "34",
width: "189.22245",
height: "5",
rx: "2.5",
transform: "translate(137.27166 -126.41038) rotate(77.23189)",
},
SPEED,
EASEIN
);
$svgParts3.animate(
{
cx: "246",
cy: "15",
rx: "5.02559",
ry: "13.40157",
transform: "translate(-148.11801 15.75089) rotate(-12.76835)",
},
SPEED,
EASEIN
);
$svgParts2.animate(
{
d: "M156.4867,71.15524a28.47835,28.47835,0,0,1-5.92376-26.14034l57.72658-13.08162a22.33593,22.33593,0,0,1,5.92373,26.14033Z",
},
SPEED,
EASEIN,
humerAnimation0
);
}
}
function humerAnimation2() {
direction = false;
$svgParts1.animate(
{
width: "189.22245",
height: "5",
rx: "2.5",
transform: "translate(385 145) rotate(174.6146)",
},
SPEED,
EASEIN
);
$svgParts2.animate(
{
d: "M329.75652,173.71985a28.47841,28.47841,0,0,1,26.68481-2.51572q2.77778,29.46448,5.55553,58.929a22.336,22.336,0,0,1-26.68481,2.51569Q332.53429,203.18431,329.75652,173.71985Z",
},
SPEED,
EASEIN
);
$svgParts3.animate(
{
cx: "433",
cy: "240",
rx: "13.40157",
ry: "5.02559",
transform: "translate(-163.48199 -48.41855) rotate(-5.38564)",
},
SPEED,
EASEIN,
humerAnimation1
);
}
humerAnimation1();

上記のようにdirectionでアニメーションの方向を保存し、アニメーションを繰り返します。

7. 速度などを調整

ハンマーが振り回されているように見せるため、2つ目のパスの透明度を下げて残像のようにします。

const $svgParts1 = Snap("#svg-move-parts1");
const $svgParts2 = Snap("#svg-move-parts2");
const $svgParts3 = Snap("#svg-move-parts3");
const svgBody = $("#js-svg");
const svgParts3 = $("#svg-move-parts3");
const SPEED = 320;
const EASEIN = mina.easein;
let direction = true;
let animationCount = 0;
function humerAnimation0() {
direction = true;
svgBody.css("opacity", "1");
$svgParts1.animate(
{
x: "60.63957",
y: "155.39365",
width: "189.22245",
height: "5",
rx: "2.5",
transform: "translate(-105.38649 -14.71627) rotate(-21.37376)",
},
SPEED,
EASEIN
);
$svgParts2.animate(
{
d: "M105.05159,208.94745A28.47838,28.47838,0,0,1,80.09194,218.716Q69.30588,191.15637,58.5198,163.59674a22.33592,22.33592,0,0,1,24.95965-9.76849Q94.26553,181.38786,105.05159,208.94745Z",
},
SPEED,
EASEIN
);
$svgParts3.animate(
{
cx: "71.20314",
cy: "159.23249",
rx: "13.40157",
ry: "5.02559",
transform: "translate(-111.6555 -45.25486) rotate(-21.374)",
},
SPEED,
EASEIN,
humerAnimation1
);
}
function humerAnimation1() {
let waitSecond = 200; // 最初の待つ時間
if (animationCount == 0) {
waitSecond = 0;
}
if (direction) {
setTimeout(function () {
svgBody.css("opacity", ".1");
svgParts3.css("display", "none");
$svgParts1.animate(
{
x: "75",
y: "34",
width: "189.22245",
height: "5",
rx: "2.5",
transform: "translate(137.27166 -126.41038) rotate(77.23189)",
},
SPEED - 200,
EASEIN
);
$svgParts2.animate(
{
d: "M156.4867,71.15524a28.47835,28.47835,0,0,1-5.92376-26.14034l57.72658-13.08162a22.33593,22.33593,0,0,1,5.92373,26.14033Z",
},
SPEED - 200,
EASEIN
);
$svgParts3.animate(
{
cx: "246",
cy: "15",
rx: "5.02559",
ry: "13.40157",
transform: "translate(-148.11801 15.75089) rotate(-12.76835)",
},
SPEED - 200,
EASEIN,
humerAnimation2
);
}, waitSecond);
} else {
setTimeout(function () {
svgBody.css("opacity", ".1");
svgParts3.css("display", "none");
$svgParts1.animate(
{
x: "75",
y: "34",
width: "189.22245",
height: "5",
rx: "2.5",
transform: "translate(137.27166 -126.41038) rotate(77.23189)",
},
SPEED - 200,
EASEIN
);
$svgParts2.animate(
{
d: "M156.4867,71.15524a28.47835,28.47835,0,0,1-5.92376-26.14034l57.72658-13.08162a22.33593,22.33593,0,0,1,5.92373,26.14033Z",
},
SPEED - 200,
EASEIN
);
$svgParts3.animate(
{
cx: "246",
cy: "15",
rx: "5.02559",
ry: "13.40157",
transform: "translate(-148.11801 15.75089) rotate(-12.76835)",
},
SPEED - 200,
EASEIN,
humerAnimation0
);
}, 100);
}
}
function humerAnimation2() {
direction = false;
svgBody.css("opacity", "1");
$svgParts1.animate(
{
width: "189.22245",
height: "5",
rx: "2.5",
transform: "translate(385 145) rotate(174.6146)",
},
SPEED,
EASEIN
);
$svgParts2.animate(
{
d: "M329.75652,173.71985a28.47841,28.47841,0,0,1,26.68481-2.51572q2.77778,29.46448,5.55553,58.929a22.336,22.336,0,0,1-26.68481,2.51569Q332.53429,203.18431,329.75652,173.71985Z",
},
SPEED,
EASEIN,
humerAnimation1
);
$svgParts3.animate(
{
cx: "433",
cy: "240",
rx: "13.40157",
ry: "5.02559",
transform: "translate(-163.48199 -48.41855) rotate(-5.38564)",
},
SPEED,
EASEIN,
svgParts3UnVanish
);
animationCount++;
}
function svgParts3UnVanish() {
svgParts3.css("display", "initial");
}
//アニメーションスタート
humerAnimation1();

アニメーションの実装完了

その他アニメーションのタイミングや速度などを変更すれば完成です 🎉