Posted at

手書き風SVGパスアニメーションを実装する

手書き風のパスアニメーションを実装するための基礎及び手順について記載します。

最終的に以下のようなものが作れるようになります。

pathanim.gif


パスアニメーションの基礎

パスアニメーションを行う上で重要なのはCSSのstroke-dasharraystroke-dashoffsetです。

stroke-dasharrayは破線を実現するためのプロパティです。

値として実線部分と隙間部分の長さを指定することができます。

path.line {

/* 線の長さ(dash):10 隙間(gap):3 */
stroke-dasharray: 10 3;
}

image.png

stroke-dashoffsetは破線のオフセットを指定するプロパティです。

少しわかりづらいですがこのプロパティを指定するとパスの開始方向に全体がスライドします。

path.line {

/* 線の長さ(dash):10 隙間(gap):3 */
stroke-dasharray: 10 3;
/* オフセット: 5 */
stroke-dashoffset: 5;
}

image.png

画像は左から右のパスなので全体が左にスライドしています。

(オフセットの部分は実際には表示されません。)

実線の長さとオフセットを同じにした場合、パスが隙間から始まります。

path.line {

/* 線の長さ(dash):10 隙間(gap):3 */
stroke-dasharray: 10 3;
/* オフセット: 10 */
stroke-dashoffset: 10;
}

image.png

実線、隙間、オフセットを全て同じ長さかつパスの長さ以上にした場合、隙間部分のみが描画領域に存在することになるのでなにも表示されなくなります。

path.line {

/* 線の長さと隙間が同じ場合は一つのみ指定する */
stroke-dasharray: 30;
/* オフセット: 30 */
stroke-dashoffset: 30;
}

image.png

この状態からオフセットを少しずつ0に近づけると少しずつ線が表示されます。

path.line {

/* 線の長さ(dash):30 隙間(gap):30 */
stroke-dasharray: 30;

animation: pathanim 3s;
animation-iteration-count: infinite;
}

@keyframes pathanim {
0% {
stroke-dashoffset: 30;
}
100% {
stroke-dashoffset: 0;
}
}

pathanim-base.gif

これがパスアニメーションの基本です。

(アニメーションの開始時に微妙に線が表示されてしまう場合はstroke-dasharrayを調整してください。)


補足:SVGファイルを作る

SVGパスアニメーションを作るためには何よりSVGファイルが必要です。

冒頭のSVGはInkscapeで作成しました。

Inkscapeでパスアニメーション用のSVGファイルを作成する際は保存時にファイルの種類を最適化SVGにします。


補足2:SVGファイルを手動で編集する

Inkscapeが出力したSVGは思ったようなパスになっていない可能性があります。

その場合はここを参考にしつつテキストエディタでパスを編集します。


手書き風パスアニメーション

今回は以下の二つを実装します


  • 1パスずつ描画する

  • 描いている最中のパスの色を変える

後者は人によってはいらないかもしれません。

この制御はjsを使わなければなりませんが、今回はvivus.jsというライブラリを使用します。

vivus.jsはよくできたライブラリで1パスずつ描画するという要望を満たすだけならほぼ何も書かずに実現することができます。


1パスずつ描画する

<!-- vivus.js読み込み -->

<script src="https://cdnjs.cloudflare.com/ajax/libs/vivus/0.4.4/vivus.js"></script>

<svg id="svg">
<!-- ...複数のパス... -->
</svg>

new Vivus('svg', {

// アニメーション再生タイプ
// 'oneByOne'を指定すると1パスずつのアニメーションになる
type: 'oneByOne',

// アニメーション全体の長さ
// 総フレーム数で指定する
// fpsで割ると秒数になる
// 60fpsで動いている場合は5秒になる
duration: 300,
});

pathanim-vivus.gif

これだけで1パスずつのアニメーションを行うことができます。

これで要件を満たしている場合は特にこれ以上やる必要はありません。


描いている最中のパスの色を変える

vivus.jsの標準ではこの機能が用意されていないので自分で実装する必要があります。

実装方法は単純で現在の再生時間から描いている最中のパスを特定し、そのパスの色を変えます。

/* 描画中のパスに適応するスタイル */

path.active {
stroke: tomato;
}

const v = new Vivus('svg', {

// アニメーション再生タイプ
// 'oneByOne'を指定すると1パスずつのアニメーションになる
type: 'oneByOne',

// アニメーション全体の長さ
// 総フレーム数で指定する
// fpsで割ると秒数になる
// 60fpsで動いている場合は5秒になる
duration: 300,

// 自動で更新されないようにする
start: 'manual',
});

// 描画の更新
const f = () => {
// vivusの内部フレームを進める
const progress = (v.currentFrame + 1) / v.duration;
v.setFrameProgress(progress);

// vivusが管理しているパスのリスト
const map = v.map;

// 完了していない場合
if (progress < 1) {
let frame = 0;
// パスのリストから現在描画しているパスを特定する
for (let i = 0; i < map.length; i++) {
const path = map[i];
// パスの終了フレーム
const next = frame + path.duration

// 描画中のパスではない
if (v.currentFrame < frame || v.currentFrame >= next) {
// クラスを削除
path.el.classList.remove('active');
}
// 描画中のパス
else {
// クラスを追加
if (!path.el.classList.contains('active')) {
path.el.classList.add('active');
}
}
frame = next;
}

requestAnimationFrame(f);
}
// 完了している場合
else {
for (let i = 0; i < map.length; i++) {
map[i].el.classList.remove('active');
}

// ループさせる場合
setTimeout(() => {
v.setFrameProgress(0);
f();
}, 3000);
}
};

requestAnimationFrame(f);

pathanim.gif


最後に

基礎部分をわかっていなくてもvivus.jsなら簡単にパスアニメーションを実装できます。

ライブラリを使用せず自分で実装する場合やvivus.jsで実現できないことを実現したい場合は基礎部分が役に立つこともあるかもしれません。