「HTML5のcanvasで作る画像フィルター」は自分ならこう書く

こちらの記事をたまたま拝見して、コメント欄もなければトラックバックもできないようなので、どうやって反応を返せばいいのかわからないけれど、もし古籏一浩さんご本人が見てくれたらいいなあ、ということで書きます。


全体的にはとても素晴らしい記事なのですが、いくつか言いたいことがあります。

ImageData

まず、CanvasRenderingContext2D#createImageData というメソッド。これは知りませんでした。

Opera には window.ImageData というグローバルオブジェクトとして似たものが定義されています。(createImageData と違い、第三引数に配列を渡せるのが特徴ですが)

if (window.CanvasRenderingContext2D && !CanvasRenderingContext2D.prototype.createImageData && window.ImageData) {
  CanvasRenderingContext2D.prototype.createImageData = function(w,h) {return new ImageData(w,h) };
}

としておけば Opera でも createImageData が使えます。

putImageData メソッドは、ちょっと前の仕様では width height data を持ったオブジェクトなら何でも渡せたのですが、最近は改訂されて、ImageData オブジェクトしか渡せなくなったようです。OperaFirefox は今でもそのようなオブジェクト全般を受け付けてくれます。(そんなことは今から習う人は知る必要のないことですが)

getContext("opera-2dgame")

Opera の独自仕様で、標準化に向けて動いているわけではないようなので、あのように取り上げるのは不適切かと思います。

Canvas で1ピクセルの点を置く場合、"opera-2dgame" コンテキストの setPixel を使う方法は確かに context.fillRect(x,y,1,1) で1ピクセルの四角形を描くという方法より高速でしたが、putImageData のある現在ではそちら使ったほうが速いので、既に "opera-2dgame" は役目を終えたと思います。

ピクセル単位のputImageData

どうしてもピクセル単位で処理しなければいけない場合は別でしょうが、画像全体を描画するのに putImageData を何度も使う理由が無いように思います。

サンプル 3 とサンプル 5 も、サンプル 4 のように一括で putImageData してしまったほうがいいと思います。

画像を絶対パスで指定

後述するFirefoxの場合は、画像を同一ドメインに置き、http://〜から始まるURLを絶対パスで指定する必要がありますが

http://ascii.jp/elem/000/000/465/465675/index-2.html

この部分は実験しても再現できません。もしかしたら間違いではないかと思います。(僕の間違いなら失礼しました)

ちなみに、Firefox だと security.fileuri.strict_origin_policy を false にすれば file: URL でも試せます。

ロード中の画像

仕様では、画像のロード中に drawImage を実行しても、何も描写されないことになっています。

If the image argument is an HTMLImageElement object whose complete attribute is false, or if the image argument is an HTMLVideoElement object whose readyState attribute is either HAVE_NOTHING or HAVE_METADATA, then the implementation must return null.

http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html

本来ならサンプル 3 とサンプル 5 以外のものも、OperaSafari/Chrome で動かなくなってもおかしくないと思います。(さらに Opera は display:none かつ onload 属性のない画像はリクエストを出さないので…と、これは Opera に言うべきですね)

HTML 中にない画像をロードするときなどは

if (imgObj.complete) {
  //
} else {
  imgObj.onload = //
  imgObj.src = imgObj.src; // Opera では onload を付けた後に src を付ける必要があるのでこうする
}

のように分けるのがいいと思います。

というわけで、

IE 以外なら、ブラウザ判定しなくてもこれで動くと思います。(IEcanvas を実現するライブラリで getImageData や putImageData が使えるものを僕は知らないので)

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="ja">
  <head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
    <title>Sample</title>
    <link rel="stylesheet" href="css/main.css" type="text/css" media="all">

<script type="text/javascript">
window.onload = function(){
  if (window.CanvasRenderingContext2D && !CanvasRenderingContext2D.prototype.createImageData && window.ImageData) {
    CanvasRenderingContext2D.prototype.createImageData = function(w,h) {return new ImageData(w,h) };
  }
  var _canvasW = 300; // 横幅300ピクセル
  var _canvasH = 169; // 縦幅169ピクセル
  var canvas = document.getElementById("myCanvas");
  var context = canvas.getContext("2d");
  var imgObj = new Image(_canvasW, _canvasH);
  imgObj.src = "asama.jpg";

  var start = function(){
    context.drawImage(imgObj, 0, 0);
    var imageData = context.getImageData(0, 0, _canvasW, _canvasH); // ピクセル値を取得する
    var w = imageData.width;
    var h = imageData.height;
    var pixelImage = context.createImageData(w, h);
    // フィルタ処理
    for(var y=0; y<h; y++){
      for(var x=0; x<w; x++){
        var ptr = (y * w + x ) * 4; // ピクセル処理する配列の要素位置を計算
        var R = imageData.data[ptr + 0];
        var G = imageData.data[ptr + 1];
        var B = imageData.data[ptr + 2];
        R = R  * 2;
        if (R > 255) R = 255;
        G = Math.floor(G / 2);
        B = Math.floor(B / 2);
        pixelImage.data[ptr + 0] = R;
        pixelImage.data[ptr + 1] = G;
        pixelImage.data[ptr + 2] = B;
        pixelImage.data[ptr + 3] = 255;
      }
    }
    context.putImageData(pixelImage, 0, 0);
  };

  if (imgObj.complete) {
    start();
  } else {
    imgObj.onload = start;
    imgObj.src = imgObj.src; // Opera では onload を付けた後に src を付ける必要があるのでこうする
  }
}
</script>

  </head>
  <body>
    <h1>ブラウザでフィルタ処理</h1>
    <canvas id="myCanvas" width="300" height="169">CANVASに対応したブラウザで実行してください</canvas>
  </body>
</html>