nmi.jp Twitter → @tkihira
wordpressのサーバ移転 JavaScriptでglobal objectを取得する方法

HSL色空間(HLS色空間)の紹介


2011-05-28
Takuo Kihira

HTML5になって、HSL色空間(HLS色空間とも呼ばれます)が使えるようになりました。知っているととても便利なので、ここでその紹介をしましょう。

色空間といえば、一番有名なのはRGB色空間です。光の3原色の重ね合わせで全ての色が表現できる、との前提に立った色空間で、コンピュータで色を扱う際にはかなり一般的に使われます。あまりに有名なので、今回は説明を省略します。

RGB色空間はプログラムからも操作しやすいのですが、色から色に徐々に変えたい場合にはあまり適さない形式となってしまいます。そういう場合に効力を発揮するのがHSL色空間です。RGBではR(赤)、G(緑)、B(青)の3つのパラメータで色を表しましたが、HSLではH(色相)、S(彩度)、L(明度)で表します。

わかりやすくHSLを体験するために、次のプログラムを実行してみましょう。

<!DOCTYPE html>
<html><head><title>HTML5でHSL色空間を使う - その1</title>
<script>
(function() {
    window.onload = function() {
        var div = document.createElement("div");
        document.body.appendChild(div);
 
        var canvas = document.createElement("canvas");
        canvas.width = 150;
        canvas.height = 150;
        document.body.appendChild(canvas);
 
        var ctx = canvas.getContext("2d");
 
        var h = 0;
        var s = 100;
        var l = 50;
 
        (function rotateHue() {
            var hsl = "hsl(" + h + "," + s + "%," + l + "%)";
            div.innerHTML = hsl;
            ctx.beginPath();
            ctx.fillStyle = hsl;
            ctx.fillRect(0, 0, 100, 100);
            h = (h + 1) % 360;
            setTimeout(rotateHue, 20);
        })();
    };
})();</script></head>
<body></body></html>

簡単に解説します。

  • 16行目~18行目で、hslの初期値を設定しています。hは0~360で、sとlは0~100%で指定します
  • 21行目でhslの文字列を作っています。作った文字列は22行目でdivのinnerHTMLに出力しています。
  • canvasに適当な四角を描画して、setTimeoutで自分自身を呼んでループしています

上のサンプルを実行すると、次の様になります。

では、HSLそれぞれについて何を表しているか説明します。

  • H(色相):色の角度みたいなもの。0~360で指定し、0(=360)は赤色になる
  • S(彩度):色のあざやかさ。100%であれば純色になり、0%であれば灰色になる
  • L(明度):色の明るさ。100%であれば白、0%で黒、50%で純色になる

このように、HSL色空間は色がシームレスに変化する場合に効力を発揮しますが、数値を見ただけでそれが何色かを想像することが難しいのがデメリットとなります。

RGB色空間からHSL色空間への変換、HSL色空間からRGB色空間への変換、それぞれ可能です。1対1の対応ではありません(例えばLが0であればHとSが何であっても黒になる)。変換式は複雑なのですが、せっかくなのでコードだけ紹介しましょう。

<!DOCTYPE html>
<html><head><title>HTML5でHSL色空間を使う - その2</title>
<script>
(function() {
    var rgbToHSL = function(r, g, b) {
        var h;
        if(Math.max(r, g, b) == r) {
            h = ((g - b) / (Math.max(r, g, b) - Math.min(r, g, b))) * 60;
        } else if(Math.max(r, g, b) == g) {
            h = ((b - r) / (Math.max(r, g, b) - Math.min(r, g, b))) * 60 + 120;
        } else {
            h = ((r - g) / (Math.max(r, g, b) - Math.min(r, g, b))) * 60 + 240;
        }
        var l = (Math.max(r, g, b) / 255 + Math.min(r, g, b) / 255) / 2;
        var s;
        if(l <= 0.5) {
            s = (Math.max(r, g, b) - Math.min(r, g, b)) / (Math.max(r, g, b) + Math.min(r, g, b));
        } else {
            s = (Math.max(r, g, b) - Math.min(r, g, b)) / (2 * 255 - Math.max(r, g, b) - Math.min(r, g, b));
        }
        return "hsl(" + Math.floor(h) + "," + Math.floor(s * 100) + "%," + Math.floor(l * 100) + "%)";
    };
 
    var hslToRGB = function(h, s, l) {
        var max;
        l /= 100; // 0~1に正規化
        s /= 100; // 0~1に正規化
        if(l <= 0.5) {
            max = l * (1 + s);
        } else {
            max = l + s - l * s;
        }
        var min = 2 * l - max;
 
        var r = Math.floor(calc(max, min, h + 120) * 255);
        var g = Math.floor(calc(max, min, h) * 255);
        var b = Math.floor(calc(max, min, h - 120) * 255);
 
        return "rgb(" + r + "," + g + "," + b + ")";
 
        function calc(n1, n2, hue) {
            hue = (hue + 180) % 360;
            if(hue < 60) {
                return n1 + (n2 - n1) * hue / 60;
            } else if(hue < 180) {
                return n2;
            } else if(hue < 240) {
                return n1 + (n2 - n1) * (240 - hue) / 60;
            } else {
                return n1;
            }
        }
    };
 
    window.onload = function() {
        var canvas = document.createElement("canvas");
        canvas.width = 300;
        canvas.height = 300;
        document.body.appendChild(canvas);
 
        var ctx = canvas.getContext("2d");
 
        var r = Math.min(canvas.width, canvas.height) / 2;
        var cx = canvas.width / 2;
        var cy = canvas.height / 2;
        for(var x = 0; x < canvas.width; x++) {
            for(var y = 0; y < canvas.height; y++) {
                var d = (x - cx) * (x - cx) + (y - cy) * (y - cy);
                if(d < r * r) {
                    var h = (Math.floor(Math.atan2(x - cx, y - cy) / Math.PI / 2 * 360) + 360 + 270) % 360;
                    var s = 100;
                    var l = Math.floor(50 + (r - Math.sqrt(d)) / r * 50);
 
                    var color = hslToRGB(h, s, l);
//                  var color = "hsl(" + h + "," + s + "%," + l + "%)";
                    ctx.beginPath();
                    ctx.fillStyle = color;
                    ctx.fillRect(x, y, 1, 1);
                }
            }
        }
    };
})();</script></head>
<body></body></html>

変換関数に関しては、そんなものだと思っておいてください。75行目のコメントを外せば、hslToRGBを介さずに直接HSL色空間で描画しますが、実行結果は全く同じになります。上記のコードではhslToRGBしか実行していませんが、rgbToHSLも問題なく動きます。実行結果は次の通りです。