12周年記念パーティ開催! 2024/5/10(金) 19:00

ピクセルグリッドのケーススタディ 第1回 Node学園祭サイト 装飾編 1

ピクセルグリッドが手がけたイベントサイトをケースとし、2D Canvasを使った効果的なアニメーション、カラーリングの実装を解説します。

発行

著者 小山田 晃浩 フロントエンド・エンジニア
ピクセルグリッドのケーススタディ シリーズの記事一覧

はじめに

Node学園祭のWebサイトをピクセルグリッドで制作しました。普段のお堅いお仕事、クライアントさんとは異なる内容だったので、今回は、この実装を題材に、使用された技術紹介をしていきます。

まずページを開いてすぐに目につくのは、背景の動く装飾でしょう。マウスポイントに収縮する円が描かれ、マウスカーソルを動かすと、円と円が線でつながれます。

実は去年(2012年)のNode学園祭のWebサイトもピクセルグリッドで作成を行い、当時も似た装飾を施していました。Webページを同時に閲覧している人同士をつなげるという去年のコンセプトを流用し、出てきた表現案を形にしています。

この表現には2D Canvasを利用しています。構成は、円が収縮するアニメーションと、線が伸びるアニメーションの2つに分けることができます。これらがどのように動いているのか、少し噛み砕いて見てみましょう。

円を描画する

この表現をするにあたり、当初、CreateJSのようなライブラリの利用を考えましたが、次のような理由から、特にライブラリを利用せずに作成しています。

  • 描画内容がプリミティブな図形だけ
  • 色の変更を柔軟にコントロールしたい

2D Canvasでは次のコードで円を描くことができます。

<canvas width="400" height="300" id="myCanvas"></canvas>

<script>
// canvas要素から、2D Canvas用のAPIを引き出します。
var canvas = document.getElementById( 'myCanvas' );
var ctx = canvas.getContext( '2d' );
// 円の中心x座標、中心y座標、半径を決めます
var cx = 200;
var cy = 150;
var radius = 30;

// コンテキストの状態を変更する前の
// 初期状態を保存しておきます。
ctx.save();
ctx.beginPath();
// arcは円弧を描くことができます。
// 0度から360度まで円弧を描けば円になります。
// 角度に対応する引数はラジアンで指定しなくてはいけません。
// 0度   = 0 ラジアン
// 360度 = Math.PI * 2 ラジアン
// 以下は、`cx`, `cy`を中心に半径`radius`の円弧を
// 0度から360度まで引いています
ctx.arc( cx, cy, radius, 0, Math.PI * 2, false );
// 塗りつぶし色を指定します
// 2D CanvasではCSSの色形式が有効です
ctx.fillStyle = 'rgba( 255, 0, 0, 1 )';
ctx.fill();
// 上記でcanvasの状態を変更してしまったため
// 次回の操作を見越し、初期状態(ctx.saveの前)に戻しておきます。
// さもなくば、上記の状態がずっと引き継がれてしまいます。
ctx.restore();
</script>

収縮するアニメーション

先ほどのコードを見てわかる通り、円の半径(radius)を徐々に変化させれば、円の収縮アニメーションが実現できます。

2D Canvasにおいて、アニメーションを実現する場合には、基本的に次の手順を踏みます。

  1. 半径10.0の円描画
  2. 次のフレームで、前フレームの内容を消去して、半径9.9の円描画
  3. 次のフレームで、前フレームの内容を消去して、半径9.8の円描画
  4. 次のフレームで、前フレームの内容を消去して、半径9.7の円描画
  5. 次のフレームで、前フレームの内容を消去して、半径9.6の円描画
  6. ...

上記のようなケースで、「次の描画タイミングに何か(関数)を実行する」場合、JavaScriptでは、setTimeoutではなく、requestAnimationFrameを利用します。

requestAnimationFrameは別の機会に改めて詳しく解説しようと思いますが、ここではひとまず、「次の描画タイミングに何か(関数)を実行する」ための関数として覚えておいてください。

なお、requestAnimationFrame実装は最新版のブラウザは対応しており、足並みは、すでに揃っています。しかしながら、旧ブラウザ*を気にかけるなら、次のようなsetTimeoutで代用するポリフィルを噛ませておくといいでしょう。

*注:旧ブラウザ

Internet Explorer9以前、Safari 5.1以前、iOS Safari 5.1以前、Android 4.3以前は対応していません。

var RAF = ( function(){
  return window.requestAnimationFrame|
         window.webkitRequestAnimationFrame|
         function( callback ){
             window.setTimeout( callback, 1000.0 / 60.0 );
         };
} )();

また、2D Canvasでは、描画した内容は、明示的に消すまで残り続けます。例えば、以下のようになります。

ですので、2D Canvasでアニメーションを行う際には「前フレームの内容を消去」という手順が必要になります。前フレームの内容を消去するには、次の2つの方法があります。

  • ctx.clearRect( x, y, width, height )で、指定の領域をクリアする
  • canvas要素の大きさを変更して強制的に全領域をクリアする

いずれの方法でも消去が可能ですが、単に全領域をクリアしたいのであれば、後者の方法が手軽です。なお、ctx.clearRectは特定の場合、一部のAndroid端末で不具合の原因になってしまいます。

ですので、ここでは、全クリア用のコードとして以下を採用します。

function clearCanvas () {
  canvas.width = canvas.width;
}

さて、ここまで紹介した円収縮のアニメーションをコードにするなら以下のようになります。下記のコードでは進行度(経過時間)に応じて、円の半径を変更するという処理を加えました。

<canvas width="400" height="300" id="myCanvas"></canvas>

<script>
var RAF = ( function () {
  return window.requestAnimationFrame|
         window.webkitRequestAnimationFrame|
         function ( callback ) {
             window.setTimeout( callback, 1000.0 / 60.0 );
         };
} )();

var canvas = document.getElementById( 'myCanvas' );
var ctx = canvas.getContext( '2d' );
var cx = 200;
var cy = 150;
var radius = 30;
var progress = 0; //「アニメーション進行度」初期値

function drawCircle ( raduis ) {
  ctx.save();

  ctx.beginPath();
  ctx.arc( cx, cy, radius, 0, Math.PI * 2, false );
  ctx.fillStyle = 'rgba( 255, 0, 0, 1 )';
  ctx.fill();

  ctx.beginPath();
  // 「アニメーション進行度」に応じて半径を決める
  ctx.arc( cx, cy, radius - ( progress / 2 - 20 ), 0, Math.PI * 2, false );
  ctx.fillStyle = 'rgba( 255, 0, 0, 0.5 )';
  ctx.fill();

  ctx.restore();
}

function updateProgress () {
  // 経過時間に応じて
  // 「アニメーション進行度」を0から100で変化させる
  if ( progress < 100 ) {
    progress ++;
  } else {
    progress = 0;
  }
}

function clearCanvas () {
  canvas.width = canvas.width;
}

( function anim () {
  RAF( anim ); // requestAnimationFrameで再帰的に実行し続ける
  updateProgress();
  clearCanvas();
  drawCircle();
} )();
</script>

色を計算する

さて、ここからは色についてを見ていきましょう。CSSでの色のアニメーションは、transitionプロパティを使えば、自動で中間色を計算してくれます。しかし、2D CanvasやWebGLにおいては、アニメーションをする際、中間色は自分で計算しなくてはいけません。それでは、中間色はどのように決定すればいいのでしょうか。

2D Canvasでの塗色は、CSSの色指定を許容しており、例えば、次のようなCSS同様の指定が可能です。

説明
キーワード指定 red
16進数指定 #ff0000
10進数指定 rgba(255, 0, 0, 1)

この中でも、10進数指定は、数値の計算に便利でアニメーションがしやすいといえます。数値で色を表すことができるのであれば、その数値を変化させれば、色のアニメーションができるわけですね。

補足

なお、16進数指定であっても文字列を分解し、16進数から10進数に変換すれば、計算はできます。また、CSSでのキーワード指定であっても、キーワードにバインドされる色値はSVGの仕様で決まっています。例えば、Aquargb( 0, 255, 255)BlueVioletrgb(138, 43, 226)といった具合です。ですから、キーワードのインデックスを内部で持たせれば、変換は可能ではあります。

// キーワードインデックスの例
{
  "aliceblue"   : [ 240, 248, 255 ],
  "antiquewhite": [ 250, 235, 215 ],
  "aqua"        : [ 0, 255, 255 ],
  "aquamarine"  : [ 127, 255, 212 ],
  ...
};

とはいえ、これらの処理は一手間かかりますので、可能なら、10進数指定を採用するといいでしょう。

さて、話が少しそれましたが、色は数値なので、これを操作すればアニメーションができるわけです。ある色から、別の色にアニメーション遷移させたいのなら、少しずつ色を近づけていけばいいわけですね。

例えば、赤から青に遷移させるには、次のようなフローとなるでしょう。

1. rgba( 255, 0, 0 )
2. rgba( 250, 0, 5 )
3. rgba( 245, 0, 10 )
4. rgba( 240, 0, 15 )
5. rgba( 235, 0, 20 )
...
n. rgba( 0, 0, 255 )

これはコードに置き換えるなら、まず次のようにアニメーションの開始時と終了時の色の値を変数として用意します。

var COLOR1 = [ 255, 0, 0 ];
var COLOR2 = [ 0, 0, 255 ];

そして、3つの色成分(RGB)に対して、次の操作をすると、アニメーション進行度に対する中間色を求めることができます。

//`progress`は`0.0`から`100.0`とする
for ( var i = 0; i < 3; i ++ ) {
  color[ i ] = ( COLOR1[ i ] + ( COLOR2[ i ] - COLOR1[ i ] ) * progress / 100 )|0;
}

進行度に対して、透明度alphaも変化させれば、Node学園祭2013サイトで使われていた円のアニメーションが完成するわけです。

まとめ

今回紹介したように、プリミティブな図形だけでも、アニメーションをつけたり、色を変化させるなど、工夫次第で、閲覧者の目を引く表現の材料となるわけですね。さらに、色は数値であり、それを計算するためには、10進数が便利であることも解説しました。次回は、線のアニメーションをご紹介します。