はじめに

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 に修正しました。