document

HTML5 & JavaScript side
twitter: @y_imaya

2013年08月

Canvas でカラーハーフトーン

はじめに

ネット上で Photoshop にはカラーハーフトーンというフィルタがあることを知って、面白そうなので実装してみました。
適当にググってカラーハーフトーンフィルタを使った画像を見ながら「多分こういうものだろう」と考えて実装したものなので Photoshop のものとは大きく異なる可能性があります。

カラーハーフトーンってなに

元々は網点という印刷まわりの技法のようですが、Photoshop のものは完全に画像の効果としてあつかった方が良いみたいです。
ネットで使用例を見た感じ、各色ごとに指定された角度の正方形に画像を分割して、色の多いところは大きく、少ないところは小さく円を描いて水玉模様みたいにする効果だと思います。

実装1: 独自実装(わりとゴリ押し)

(0, 0) から角度と正方形のサイズから dx, dy を計算してそれを加算していきます。
走査の改行は 90 度回転させた角度で同じように計算します。
この場合、斜めに進んで行くので Y 方向に加算していくだけでは走査に漏れが発生するため、X 方向に進ませた時 Y が 0 以下のときは Y を負の方向に進ませてみて描画できそうなら描画してさらに負の方向にすすませるということをしています。

scanline

描画すべきかどうかは、四隅の点のどれかが描画領域にはいっているかどうかで判定しています。

実装2: 行列を使った変換

回転行列を使って、全てのピクセルを矩形サイズで割った大きさの画像に縮小&回転させた画像に変換し、そこから元の大きさ・角度に逆変換することで上記のような走査ではなく、適切に処理できると考えました。

実際にやってみたところ、変換時に角度によっては微妙にピクセルがずれ、それを戻すと割と目立つ違いになってしまうので微妙な感じになってしまいました。
うまく変換する方法を思いつくまでこちらの方法はひとまず置いておこうと思います。

half

各色毎に回転・縮小して…

inverse

逆行列で元にもどします。

行列の計算は goog.math.Matrix という Closure Library のコードを使っています。

実装例

jsdo.it に置いておきました。
事前にグレイスケール化や二値化するフィルタをかけたり、カラーハーフトーンが終わった後に特定の色で塗りつぶしたりできるようになってます。

まとめ

Canvas + JavaScript は画像加工ツール

Canvasできれいな色相環を描画する

はじめに

JavaScript で Canvas を使っていると、HSV の Color Picker とか作りたくなって色相環を描画したくなることがよくあるとおもいます。
ここでは、自分の行っている色相環の描画方法を説明します。

準備

色相を扱うのために HSV 色空間を使います。HSV から RGB への変換は以下の function を用います。

function hsvToRGB(hue, saturation, value) {
    var hi;
    var f;
    var p;
    var q;
    var t;

    while (hue < 0) {
        hue += 360;
    }
    hue = hue % 360;

    saturation = saturation < 0 ? 0
        : saturation > 1 ? 1
        : saturation;

    value = value < 0 ? 0
        : value > 1 ? 1
        : value;

    value *= 255;
        hi = (hue / 60 | 0) % 6;
        f = hue / 60 - hi;
        p = value * (1 -           saturation) | 0;
        q = value * (1 -      f  * saturation) | 0;
        t = value * (1 - (1 - f) * saturation) | 0;
    value |= 0;

    switch (hi) {
        case 0:
            return [value, t, p];
        case 1:
            return [q, value, p];
        case 2:
            return [p, value, t];
        case 3:
            return [p, q, value];
        case 4:
            return [t, p, value];
        case 5:
            return [value, p, q];
    }

    throw new Error('invalid hue');
}

距離で判別する

まず最初に思いつくのが、以下のような方法でしょう。

  1. x, y から中心となる点までの距離を計算する (三平方の定理)
  2. 距離が円の範囲内ならば、中心までの角度を計算する (Math.atan2)
  3. 角度から色相を計算し、RGBに変換して描画する

1の距離の計算ですが、距離の比較にしか使わないため、平方根を取らずに半径を二乗する事で高速化してもよいですね。
ただし、この場合アンチエイリアスなどがかからず、端の方がギザギザになってしまいます。

これを JavaScript で書くと以下のようなコードになります。

var width = 300;
var height = 300;
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");

canvas.width = width;
canvas.height = height;
document.body.appendChild(canvas);

// 円の外側の距離
var r1 = Math.min(canvas.width, canvas.height) / 2;
// 円の内側の距離
var r2 = r1 - 30;
// 中心
var cx = canvas.width / 2;
var cy = canvas.height / 2;

var imageData = ctx.getImageData(0, 0, width, height);
var pixelArray = imageData.data;

for(var x = 0; x < width; x++) {
    for(var y = 0; y < height; y++) {
        var d = (- cx) * (- cx) + (- cy) * (-cy);

        if(< r1 * r1 && d > r2 * r2) {
            var baseIndex = (* width + x) * 4;
            var hue = Math.atan2(- cx, y - cy) / Math.PI / 2 * 360;
            var color = hsvToRGB(hue, 1, 1);

            pixelArray[baseIndex  ] = color[0];
            pixelArray[baseIndex+1] = color[1];
            pixelArray[baseIndex+2] = color[2];
            pixelArray[baseIndex+3] = 255;
        }
    }
}

ctx.putImageData(imageData, 0, 0);

一度円を描画してアルファ値で描画対象か判別する

自分はこのギザギザが許せなかったので何か良い方法がないか考えたところ、一度適当な色の円を書いて、その場所に色を上書きしていく方法を思いつきました。

var width = 300;
var height = 300;
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");

canvas.width = width;
canvas.height = height;
document.body.appendChild(canvas);

// 円の外側の距離
var r1 = Math.min(canvas.width, canvas.height) / 2;
// 円の内側の距離
var r2 = r1 - 30;
// 中心
var cx = canvas.width / 2;
var cy = canvas.height / 2;

// 円を描く
ctx.arc(cx, cy, r1, 0, Math.PI * 2, true);
ctx.arc(cx, cy, r2, 0, Math.PI * 2, false);
ctx.fill();

そして、その描画された部分のアルファ値を見て色を上書きします。

var imageData = ctx.getImageData(0, 0, width, height);
var pixelArray = imageData.data;

for(var x = 0; x < canvas.width; x++) {
    for(var y = 0; y < canvas.height; y++) {
        var baseIndex = (* width + x) * 4;

        // 透明でなければ上書きする
        if(pixelArray[baseIndex + 3] > 0) {
            var hue = Math.atan2(- cx, y - cy) / Math.PI / 2 * 360;
            var color = hsvToRGB(hue, 1, 1);

            pixelArray[baseIndex  ] = color[0];
            pixelArray[baseIndex+1] = color[1];
            pixelArray[baseIndex+2] = color[2];
        }
    }
}

ctx.putImageData(imageData, 0, 0);

パフォーマンス

どちらの方法も手元の Chrome 28, Firefox 23 だと 9-14ms 程度だったので性能的な違いはあまりないと思います。

追記: 2013/08/16

CanvasRenderingContext2D#arc の endAngle を Math.PI * 2 より大きい整数である 7 としていましたが、Math.PI * 2 を超える値の扱いが safari でおかしくなるため、Math.PI * 2 に修正しました。

記事検索
最新コメント
最新トラックバック
カテゴリ別アーカイブ
タグクラウド
QRコード
QRコード
  • ライブドアブログ