Oteto Blogのロゴ

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();

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

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