タグ別アーカイブ: ライブラリ

vq にイベントハンドリングの機能を実装した

vq v1.1.1 をリリースしました。主な更新点は、イベントハンドリング機能の追加です。

vq は複雑なアニメーションを簡潔に書くことを目指しているライブラリで、関数型プログラミングの考え方から着想を得ています。今回の更新点以外の詳しい説明は以前書いた記事や README を参照してください。

Example

今回のアップデートで vq.click, vq.focus など、DOM 要素のイベントが発火してからアニメーションを実行するための関数を追加しました。関数の一覧は README に記載しています。例えば、クリックするたびに要素を移動させるようなコードは以下のように書けます。

const box = document.getElementById('box');

// アニメーションのプロパティ & オプション
const move1 = {/* ... */};
const move2 = {/* ... */};
const move3 = {/* ... */};

// box をクリックした時になんらかのアニメーションを実行したい
const clickBox = vq.click(box);

const moveBox = vq.sequence([
  clickBox(vq(box, move1)),
  clickBox(vq(box, move2)),
  clickBox(vq(box, move3)),
  (done) => moveBox(done) // 無限にアニメーションをさせる
]);

moveBox();

また、vq.element という、より低レベルな関数も提供しており、イベントの発火後にアニメーションを実行するかどうかをより細かくコントロールすることができます。第一引数に指定した DOM 要素が、第二引数に指定したイベントを監視します。第三引数には関数を渡すことができ、イベントが発火するたびにこの関数が実行されます。この関数が true を返した時、指定したアニメーションが実行されます。

const alert = document.getElementById('alert');

// アニメーションのプロパティ & オプション
const show = {/* ... */};
const hide = {/* ... */};

// Enter キーが押された時に何らかのアニメーションを実行したい
const pressEnter = vq.element(window, 'keypress', event => {
  return event.which === 13 // press enter
});

vq.sequence([
  vq(alert, show);            // アラートを表示させる
  pressEnter(vq(alert, hide)) // Enter が押されたらアラートを隠す
])();

イベントのハンドリングを vq で扱えるようにしたことで、ユーザーのインタラクションを伴うアニメーションもより簡単にかけるようになったのではないかと思います。それぞれのイベントハンドラはアニメーションとは独立した関数として生成できるため、小さなコードを組み上げるような実装ができ、コードの見通しが良くなることを期待しています。これからの更新では、イベントハンドラの結合や、DOM 以外のイベントを扱うための API を実装する予定です。

Vue のコンポーネントと Vuex Store を繋げるためのヘルパ vuex-connect を作った

某勉強会中にネタを思いついて、急いで作って LT してその日のうちに npm にアップしたら、翌日Vue 公式に紹介されていてだいぶビビったやつです。VueVuex のヘルパなので、この二つを理解していることが前提になります。

vuex-connect (Github)
vuex-connect (npm)

vuex-connect の機能

vuex-connect は connect 関数のみ提供しており、やっていることは react-reduxconnect と同じです。connect は第一引数に Vuex の getters、第二引数に actions を受け取ります。 また、戻り値として別の関数を返し、こちらの関数には、コンポーネント名と、コンポーネントのコンストラクタを渡します。 最終的に、渡したコンポーネントのプロパティに getter と action をつなげた上位コンポーネント (コンテナ) を返します。

// コンポーネントを定義
const HelloComponent = Vue.extend({
  props: {
    message: {
      type: String,
      required: true
    },
    updateInput: {
      type: Function,
      required: true
    }
  },
  template: `
  <div>
    <p>{{ message }}</p>
    <input type="text" :value="message" @input="updateInput">
  </div>
  `
});

// コンポーネントと Store をつなげる
const getters = {
  message: (state) => state.message
};

const actions = {
  updateInput: ({ dispatch }, event) => dispatch('UPDATE_INPUT', event.target.value)
};

const HelloContainer = connect(
  getters,
  actions
)('hello', HelloComponent);

なぜ有用なのか?

これも react-redux で言われていることと同じですが、開発の定石として、フレームワークやライブラリへの依存をなるべく少なくするというものがあります。UI コンポーネントと、状態管理を司るフレームワークを疎結合にするために、Redux では、UI の見た目の実装を行うためのコンポーネントと、Store 内の状態とやり取りするためのコンポーネントを分けるという方針をとっています。これらに関して、前者は Presentational Component、後者は Container Component と呼ばれています (Usage with React | Redux)。

Vue と Vuex で例えると、Presentational Component、Container Component のどちらも Vue のコンポーネントとして実装されます。Presentational Component は Vuex への依存関係を持っておらず、Vue のみで完結しています。Container Component は Vue と Vuex の両方に依存しており、両者をつなげる役割のみを担います。ここで、Vuex を使うのをやめて別の状態管理のフレームワークを使うケースを考えます。Vuex に依存しているコンポーネントは Container Component のみであり、また、その役割は Vue と Vuex をつなげるということのみであるため、Container Component を交換するのみで良くなります。すなわち、Vue と Vuex の依存を最小限にすることで、Presentational Component として実装した Vue コンポーネントが再利用できるようになっているということです。

Vuex の公式ドキュメントを見てみると、上記のようなことは考慮されておらず、Vue と Vuex が密結合になっているように感じたため、vuex-connect を作りました。Vue だと props でバケツリレーするのはちょっと違和感あるのですが、アプリケーションのあちこちで状態を変えられるよりは良いのかなと……。もしくは、action を props に渡すのではなく、events の方に結びつけても良いかもしれないですね。

実装

実装したての頃は react-redux とインターフェースを合わせようとして四苦八苦していたこともあり、Vue.prototype._init を上書きしたりしてました。しかし、よくよく考えてみると vuex オプションにそのまま受け取った引数を渡せばいいことに気が付き、かなり簡潔な実装になりました。

https://github.com/ktsn/vuex-connect/commit/f0a254ea5c7b330bcc2446167b970940d67a724b

コンポーネントの props に getter と action を渡したいので、template の部分をがんばって生成しています。

悩み

connect で生成したコンポーネントに props を渡せるようにするべきかを悩んでいます。react-redux だと渡せるようにしているのですが、正直あんまりそうするケースが思い浮かばないし、データの流れが二股になって複雑になることを懸念しています。また、React の場合は react-router などが props にデータを渡してくるので、それに対処するために props の値を読めるのは有用なのですが、Vue の場合はそういうケースで props を使うことがないので、やるとしても別のアプローチをするべきなのかなーとも思ってます。あと、Vue の props はちゃんとコンポーネント側で定義してあげる必要があるので実装がめんどくさそう……。

最後に

なんとなく Vuex 使ってる人はものすごく少ないような感じがするのですが、Vuex を使うことがあったら vuex-connect も一緒に使っていただけると嬉しいです。そして contribution もウェルカムです!

複雑なアニメーションとそれに伴う処理を簡潔に書くことのできるライブラリ 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

指定した行数でテキストを省略できるライブラリ Truncator を作った

N 文字目以降を省略するというライブラリはたくさんあるのですが、行数指定できるものは見かけないので作りました。N 行以上になった時は省略したいというケースは結構ある気がするんですが、なぜそういうライブラリは無いのだろう……。

Truncator – NPM
Truncator – Github

使い方

truncate(el, text, { line: 3, ellipsis: '……' });

のような感じで使います。この例だと、要素 el に文字列 text を入れて、それが 3 行に収まるように省略します。また、省略記号は ...... を指定してます。

アルゴリズム

以下のような感じのアルゴリズムで動いてます。
1. el の一行の高さ L を取得する。
2. 行数 n * L で目標の高さ H を算出する。
3. eltext を入れてその高さ h を算出する。
4. h <= H を満たす、最大の省略位置を二分探索し、省略後の文字列を el に入れる。

一行の高さを取得する

一行の高さは window.getComputedStyle(el).lineHeight で簡単に取得できる……と思いきや、normal とかが返ってくるケースがあるので、工夫する必要があります。 Truncator では、対象の要素に適当な一文字を入れた時の高さを一行の高さとして扱っています。

省略すべき位置を二分探索する

単純に、center = (left + right) / 2 して、text.substring(0, center) し、h <= H だったら left を center に、そうでなければ right を center にする二分探索です。ただ、h <= H だったときでも center が解ではないとは限らないため、次の探索でも center を探索空間に含めたままにする必要があります。

最後に

バグ報告大歓迎です!