pure JavaScriptでアニメーションGIFを作る

こんばんは、国民的スポーツのアニメGIFについてのニュースをお届けします。
今回はクライアントサイドのJavaScriptだけでアニメーションGIFを作る方法を共有させて頂きます。

ふつうアニメGIFを作ろうと思うとimagemagickなどを使ってサーバーサイドで処理させると思いますが、今回紹介するのはブラウザだけでアニメGIFを作る方法です。

jsgif

antimatter15/jsgif · GitHub
jsgifというJavaScriptでCanvasをアニメGIFに変換してくれる超絶便利ライブラリがあるのでこれを使います。
同名でアニメGIFをCanvasで制御再生できるライブラリがあって、そっちのほうが有名ですがそれとは別です。

どうやらAS3のライブラリをJSに移植したものみたいでソースコードを読もうとしても全然分からないけど、便利すぎるので全面の信頼をおいて使います。

だいたいこんな感じで使える。実際には画像をロードするのを待たないといけない。

var img1 = new Image(), img2 = new Image();
img1.src = 'sozai/1.jpg';
img2.src = 'sozai/2.jpg';

var encoder = new GIFEncoder();
encoder.setRepeat(0);
encoder.setDelay(100);
encoder.setSize(100,100);

context.drawImage(img1, 0, 0, canvas.width, canvas.height);
encoder.addFrame(context);

context.drawImage(img1, 0, 0, canvas.width, canvas.height);
encoder.addFrame(context);

encoder.finish();

気をつけること

クロスオリジン制約によるCanvasの汚染

canvasには外部ドメインから読み込んだ画像を描画したcanvasは"汚染"されて、画像として保存するなどデータを引き出すことができなくなるセキュリティ上の制約があります。
CORS Enabled Image | MDN

jsgifはcanvasを使って画像データを引き出しているので、外部ドメインから画像を読み込んでいる場合使うことができません。
外部ドメインの画像を使ってアニメGIFを作りたい場合は、CORSなプロキシを通すなどする必要があります。

file://で画像を参照する場合もクロスオリジン制約に引っかかるので、適当なHTTPサーバーを立てて動かす必要があります。

デモ

jsgifを使ったアニメGIF生成の簡単なデモを作ってみました。

GIF生成の処理は重いので、WebWorkerを使ってバックグラウンドで処理させるようにしています。

デモの様子はここから見れます。Chromeで動くと思います。素材にはパブリックドメインな人物画像を使用しています。
http://uiureo.github.com/jsgif-demo/

こんな感じの画像が生成されます。
f:id:uiureo:20121222000541g:plain

client.js
// Array{DOM Image} -> callback(dataURL)
function createGIF (args, callback) {
  var images = args.images || [];
  var option = {
    delay: args.delay || 100,
    repeat: args.repeat || 0,  // default: auto loop
    width: args.width || 400,
    height: args.height || 400
  };

  var canvas = $('#canvas')[0];
  var context = canvas.getContext('2d');

  canvas.width = option.width;
  canvas.height = option.height;

  // GIFは透明にできないから白色で塗る
  context.fillStyle = "rgb(255,255,255)";  
  context.fillRect(0, 0, canvas.width, canvas.height);

  var worker = new Worker('encoder.js');

  worker.postMessage({ cmd: 'start', data: option });

  images.forEach(function (image) {
    context.drawImage(image, 0, 0, canvas.width, canvas.height);

    // Workerにフレームのデータを送る
    worker.postMessage({ cmd: 'frame', data: context.getImageData(0, 0, canvas.width, canvas.height).data });

    context.fillRect(0, 0, canvas.width, canvas.height);
  });

  worker.postMessage({ cmd: 'finish' });

  worker.onmessage = function (e) {
    callback('data:image/gif;base64,' + encode64(e.data));
  };
}

// from https://github.com/antimatter15/jsgif/blob/master/Demos/b64.js
function encode64(input) {
	var output = "", i = 0, l = input.length,
	key = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", 
	chr1, chr2, chr3, enc1, enc2, enc3, enc4;
	while (i < l) {
		chr1 = input.charCodeAt(i++);
		chr2 = input.charCodeAt(i++);
		chr3 = input.charCodeAt(i++);
		enc1 = chr1 >> 2;
		enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
		enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
		enc4 = chr3 & 63;
		if (isNaN(chr2)) enc3 = enc4 = 64;
		else if (isNaN(chr3)) enc4 = 64;
		output = output + key.charAt(enc1) + key.charAt(enc2) + key.charAt(enc3) + key.charAt(enc4);
	}
	return output;
}

$(function () {
  var srcs = ['sozai/1.jpg', 'sozai/2.jpg'];

  var images = srcs.map(function (src) {
    var image = new Image();
    image.src = src;
    return image;
  });

  // 画像が全ロードされるまで待つ
  window.onload = function() {
    createGIF({ images: images }, function (dataURL) {
      $("#image").attr('src', dataURL);
    });
  };
}());
encoder.js

WebWorkerでアニメGIFを生成する部分です。

importScripts('LZWEncoder.js', 'NeuQuant.js', 'GIFEncoder.js');

var encoder = new GIFEncoder();
onmessage = function (e) {
  if (e.data.cmd === "start") {
    var data = e.data.data;
    encoder.setRepeat(data.repeat);
    encoder.setDelay(data.delay);
    encoder.setSize(data.width, data.height);

    encoder.start();
  } else
  if (e.data.cmd === "finish") {
    encoder.finish();
    postMessage(encoder.stream().getData());
  } else 
  if (e.data.cmd === "frame"){
    encoder.addFrame(e.data.data, true);
  }
};

問題点

データが大きすぎてURLに収まらない
小さい画像ならなんとか収まってURLで参照できるけど、アニメGIF用途ではたいていURLに収まらない。URLをコピーして参照とかできない。ちょっと不便。
サイズがでかすぎたら名前をつけて保存ができなかったりする。つらいですね。

まとめ

pure JavaScriptでアニメGIFを作る方法をご紹介しました。
サーバーサイドを書くことなく、ブラウザだけでアニメGIFを作ることができるのでとても便利ですね。

以上、スポーツニュース速報でした。