HTML canvas のビットマップ操作のメモ

未だに Flash 脳の私は canvas の操作をする時に「BitmapData のあのメソッドと同じことをしたい」と考えるのですが, canvasAPI はパッと見では必要最低限以下なんじゃないかというくらいの機能しか提供していないので, どうしたら実現できるのかすぐにはわからないところがあります.

というわけで, とりあえず私が必要としていた,

  • .getPixel32()
  • .setPixel32()
  • .copyPixels()

の 3 つの実装方法をメモしておきます.

.getPixel32()

context.getImageData() を使用します.

BitmapData でいうところの .getPixels().getVector() のようなものですが, canvas には他に画像のピクセルデータを取得する方法が (たぶん) ないので仕方がありません. 1px * 1px の領域についての ImageData を取得して, そのデータを読み取ることで .getPixel32() と同じ動作を実現します.

function getPixel32(canvas, x, y) {
    let context = canvas.getContext("2d");
    // 1px * 1px の ImageData を取得
    let pixel = context.getImageData(x, y, 1, 1);
    // 適当に 32bit int に変換
    return (pixel.data[3] << 24) | (pixel.data[0] << 16)
        | (pixel.data[1] << 8) | pixel.data[2];
}

.setPixel32()

context.createImageData() で 1px * 1px の ImageData を作成して context.putImageData() で描画するか, context.fillStyle で色を指定してから context.fillRect() で 1px * 1px の領域を塗りつぶします.

context.createImageData(), context.putImageData() を用いる場合は以下のとおり.

function setPixel32(canvas, x, y, color) {
    let context = canvas.getContext("2d");
    // 1px * 1px の ImageData を作成
    let pixel = context.createImageData(1, 1);
    // data に書き込み
    pixel.data[0] = (color >>> 16) & 0xFF;
    pixel.data[1] = (color >>> 8)  & 0xFF;
    pixel.data[2] = color          & 0xFF;
    pixel.data[3] = (color >>> 24) & 0xFF;
    // canvas に描画
    context.putImageData(pixel, x, y);
}

context.fillStylecontext.fillRect() を用いる方法は次の .copyPixels() の場合とほぼ同じで, かなり面倒なので省略します.

.copyPixels()

アルファのソースを別にした場合は話がややこしくなるので, 今回は第 4 引数以降に何も与えなかった場合, つまりソースのビットマップで元のビットマップを (アルファを含めて) 上書きする場合を考えます (第 4 引数以降を null, null, true とかにしてアルファ合成する場合は context.drawImage() を使えば簡単なはず).

これは言葉で説明してもわかりづらいので以下のコードとコメントを読んでください.

function copyPixels(canvas, src, sx, sy, sw, sh, dx, dy) {
    let context = canvas.getContext("2d");
    // globalCompositeOperation などを変更するので,
    // 後で戻せるように save
    context.save();
    // ソースで上書きするようにする
    context.globalCompositeOperation = "copy";
    // クリッピング領域を作成
    // これをしないと元の画像全体が上書きされる
    context.beginPath();
    context.rect(dx, dy, sw, sh);
    context.clip();
    // 描画
    context.drawImage(
        src,
        sx, sy, sw, sh,
        dx, dy, sw, sh
    );
    // context を保存した状態に戻す
    context.restore();
}

ちなみに他の方法として context.getImageData() の後 context.putImageData() という方法が考えられますが, どうやら処理が遅いようです (ぐぐったらそういう記述を見つけただけで検証していない, けど余計に ImageData を作っているので少なくともその分は遅そう).