Katashin .info

複雑なアニメーションとそれに伴う処理を簡潔に書くことのできるライブラリ vq を作った

最近 JavaScript のアニメーションの実装につらみを感じていたので、それを解消するためにライブラリを作りました。 vq というライブラリで、Velocity.js というライブラリのヘルパーという位置づけです。 内部のアニメーションは Velocity.js にまかせていて、vq は記述を簡潔に書けるようにしています。

vq - GitHub vq - NPM

Velocity.js のつらみ #

Velocity.js は、jQuery.animate と同じような文法で DOM 要素のアニメーションを記述することのできる JavaScript ライブラリです。 Velocity.js のインタフェースは jQuery と似たような感じですが、実装では requestAnimationFrame でアニメーションさせてたり、jQuery.animate にはない機能を備えていたりと、jQuery.animate よりも優れています。DOM のアニメーションを実装するときには無くてはならない存在です(と思ってます)。

しかし、アニメーションの規模や複雑さが増加してくると、Velocity.js 単体ではかなりつらくなってきます。以下に具体例を挙げます。

1. 複数の要素のアニメーション #

Velocity.js は1つの要素を対象としたアニメーションは、メソッドチェーンで簡潔に書くことができます。

$el.velocity(...).velocity(...);

しかし、アニメーションさせたい要素が二つ以上あり、それぞれ、他の要素のアニメーションの進捗に依存している時、うまく書くことができなくなります。例えば、complete コールバックを使用して、あるアニメーションの終了したことを確認して、次のアニメーションを行うという処理を書くとき、ネストが発生してしまいます。

$el1
  .velocity({
    height: 200,
    width: 300
  }, {
    duration: 500,
    complete: function() {
      $el2.velocity({ // <--- つらい
        ...
      });
  }
});

Velocity UI Pack には複数の要素のアニメーションを書くための RunSequence という関数も用意されていますが、こちらも少々力不足なように感じます。例えば、アニメーションの合間に、アニメーション以外の処理を挟む際に、ネストが発生してしまいます。これは 2. 3. で詳しく述べます。

2. アニメーションの間に特定の処理を入れる #

Velocity.js はアニメーション以外の処理をハンドリングすることが苦手です。 例えば、以下のように、RunSequence で複数の要素をアニメーションさせる例を考えます。

$.Velocity.RunSequence([
  {
    e: el1,
    p: props1,
    o: opts1,
  },
  {
    e: el2,
    p: props2,
    o: opts2,
  },
  {
    e: el3,
    p: props3,
    o: opts3,
  },
])

ここで、el2 をアニメーションさせた後に、特定の処理 (たとえば、テキストを変えるとか) を行う時を考えます。RunSequence はアニメーションのみをサポートしているため、アニメーション以外の処理は complete コールバック内で行う必要があります。また、上記のように、アニメーションに関する設定があらかじめ変数として別の部分で定義されている時、complete コールバックを生やすことが必要であり、そのコードを書くと一気に汚くなります。

$.Velocity.RunSequence([
  {
    e: el1,
    p: props1,
    o: opts1
  }, {
    e: el2,
    p: props2,
    o: $.extend({
      complete: function() { // <-- とてもつらい
        ...
      }
    }, opts2)
  }, {
    e: el3,
    p: props3,
    o: opts3
  }
]);

上記の例だけでもかなりつらいですが、これに加えて、complete 内の処理が非同期で、その非同期処理の後にアニメーションを続けるといった処理を書くとなると、絶望的な状況になります。

3. 共通処理を少し変更するというのがやりづらい #

Velocity.js は jQuery.animate のように、第一引数にアニメーションさせるプロパティ、第二引数にアニメーションのオプションを指定することができますが、これらを一つのオブジェクトにまとめて渡すこともできます。これを利用して、共通のアニメーションを別の場所に定義して使い回すという使い方もできます。具体的には以下の様なコードになります。

// プロパティを p, オプションを o で指定
var fadeIn = {
  p: { opacity: [1, 0] },
  o: { duration: 500, easing: 'easeOutQuad' },
}

$el.velocity(fadeIn)

この書き方はコードの見通しが良くなって便利なのですが、ある状況特有の設定をしたい時につらくなります。 例えば、ある部分だけアニメーションにディレイを書けたい場合、odelay を追加する必要があるのですが、2. で挙げたように、変数に格納したオプションに新たな値を追加するのはコードをかなり汚くしてしまいます。

また、Velocity.js には RegisterEffect という、あるアニメーションを登録して、使い回すという機能があります。RegisterEffect を使えばある程度共通処理はきれいになりますが、これを使うと RunSequence が使えなかったりします。

vq の特徴 #

vq は上記のようなつらさを解消できる設計となっています。上記のようなケースでもネストを発生させることなく、オプションに値を追加することも簡潔な記法で行うことができます。 vq の基本的な書き方は以下のとおりです。vq(el, animation) は Velocity.js の書き方を真似ていて、el がアニメーションさせる要素、animation がアニメーションのプロパティとオプションです。引数が二つの場合は、二番目の引数にプロパティとオプションの両方が記載されているとみなします。プロパティとオプションを分けて、それぞれ第二引数、第三引数として渡すこともできます。

以下の例では、el1 に animation1、el2 に animation2、el3 に animation3 が順番に実行され、その後、ログに "animated" と出力されます。

vq.sequence([
  vq(el1, animation1),
  vq(el2, animation2),
  vq(el3, animation3),
  function () {
    console.log('animated')
  },
])

HTML 要素とアニメーションの設定を分けている #

前に述べたとおり、アニメーションのプロパティとオプションをあらかじめ変数に入れておくことはコードの見通しが良くなるため、複雑なアニメーションを書くときには有効だと思います。しかし、Velocity.js の RunSequence は、HTML 要素、プロパティ、オプションをまとめた一つのオブジェクトしか受け付けないため、あらかじめ変数に入れて使い回す書き方がしづらいと感じます。なぜなら、アニメーションを使い回すのに、プロパティとオプションを切り出すのは良いのですが、HTML 要素は切り出すべきものではないためです。むしろ、HTML 要素は場面によっていろいろと異なるものになると思います。

vq では、HTML 要素と、プロパティ、オプションを別の引数として受け取るため、これらを分離することが容易にできます。

アニメーションとそれ以外の処理を同じものとして扱う #

vq.sequence は Velocity.js の RunSequence と同じように、アニメーションを順番に実行する関数です。この関数はアニメーションの実行だけでなく、任意のタイミングで関数を実行することができます。以下の例では、animation1 と animation2 の間に、"log" と出力する処理を書いています。

vq.sequence([
  vq(el1, animation1),
  function () {
    console.log('log')
  },
  vq(el2, animation2),
])

固有の処理を後で付け加えることが容易 #

ある特定の状況だけ、共通処理とは少し異なる挙動のアニメーションをしたい場合、メソッドチェーンによってそれを行うことができます。以下の例では、animation1 に 1000ms のディレイをつけ、duration を 700ms に変更しています。

vq.sequence([
  vq(el1, animation1).delay(1000).duration(700),
  vq(el2, animation2),
])

vq の仕組み #

以下、vq の具体的な実装について説明します。

アニメーションを実行する関数を生成する #

vq(el, animation) という関数は実はアニメーションを実行させているのではなく、アニメーションを実行する関数を返しています。具体的に、以下のようなコードを考えます。

vq.sequence([
  vq(el1, animation1),
  function () {
    console.log('test')
  },
])

このコードは以下のコードと同じと考えて良いです。(厳密には違いますが)

vq.sequence([
  function (done) {
    animation1.o.complete = done
    el1.velocity(animation1)
  },
  function () {
    console.log('test')
  },
])

すなわち、vq.sequence は関数の配列を受け取り、それを端から順番に実行していくだけの関数です。 また、引数に与えられた関数が引数を受け取る形になっている場合、第一引数をコールバック関数とみなし、そのコールバックが呼ばれないかぎり、次の処理には移りません。上記の例では、vq によって生成された関数が、第一引数に done というコールバックを持っています。これをオプションの complete に代入しているため、vq.sequence はアニメーション終了まで処理を待機させます。

関数オブジェクトにさらに関数を生やす #

vq で生成した関数には、オプションを変更するためのメソッドが生えています。これは単純に、関数を生成する際に、そのメンバとしてメソッドを代入しているだけです。 JavaScript は関数もオブジェクトとして扱われるため、通常のオブジェクトと同様に、メンバを追加することができます。

生成した関数に、アニメーションのオプションを記録しておき、メソッドが呼ばれた時、オプションの値を変更させています。 また、メソッドチェーンができるように戻り値で元の関数を返すようにしています。

まとめ #

JavaScript のアニメーションライブラリには Velocity.js という便利なライブラリがありますが、規模が大きく、複雑なアニメーションを実装するときはかなりつらくなってしまうケースがありました。今回、そのつらみを取り除くために、vq というライブラリを作りました。vq を使うと、ネストをすることなく複雑なアニメーションを書くことができます。また、あるケースの時だけ、共通部品として定義したアニメーションとは異なる挙動をさせたいということも簡潔に書くことができます。

現在は並列で実行するようなアニメーションには対応していないため、次はそれに対応したいと考えています。ぜひ、試してみて、フィードバックをいただけたら嬉しいです。

vq - GitHub vq - NPM