色相環とjQuery UIの勉強

色に関する勉強

色の属性(色相・明度・彩度)とか色相環について、
特にRGBとの関連についてよくわからなかったので勉強してみました。


「読んだのはWeb配色デザインのセオリー」っていう本です。
そういえば色ってものについてちゃんと考えたことがなかったので、
原色の種類と明るさと鮮やかさという軸で考えると人間の感覚的に
理解・説明しやすいということを体系的に理解できたのはよかったです。


RGBの考え方に最初に触れた時にすごくわかりやすいやん、
と思っていたのは、あくまで原色だけで、中間色を使う必要がほとんどなかったため
あまり深く考えていなかったということでした。
あと、光の3原色っていうのは学校でならったからかなー。
色相とかは習ったのかな?よく覚えてない。

じゃ、色相環でも作るか

でまあ、だいたい分かったところで、せっかくなのでプログラマとしては
色相環を作るプログラムでも作ってみようかというわけです。
んで、最近jQueryを勉強しているので、JavaScriptでかいてみようかなと。


とりあえずは座標変換ですな、HSV色空間からRGB色空間(またその逆)の。
ちょちょっとググってみれば、ありますがな、変換方法が。wikipedia:HSV色空間


こいつを参考にColorパッケージを実装して、jQuery UIを使って、なんかいけてる
感じのUIをのっけたりして楽しくjQueryも勉強してしまおう的なノリです。

まとめ

最終的には、プログラムで色相環を生成して、明度と彩度はスライダを使って
指定できるようにして、特定の色を選ぶとそれをメインカラーとして、サブカラー
とアクセントカラーを並べるダイアログが開くようにしてみました。
jQuery UIすっげー楽しーやんってことで意味もなく色相環をDraggableにしてます。


結果的には、作ることが楽しかったのでいいんですが、メインカラーの選択から
他の色を選定するアルゴリズムが微妙で、この辺が自動化できたらいいなと
思います。(できるのか?)むしろ、絵師の意見を取り入れたいところです。
一応、本のスプリットコンプリメンタリー配色、ドミナントカラー配色っぽいことを
やってはみましたが。。。

プログラムソースについて

さて、肝心のソースですが、舞台となるHTMLはこんな感じ。

x.html
----
<html>
<head>
<title>color</test>
<link rel="stylesheet" href="../jq-css/jquery-ui-1.7.2.custom.css" />
<script type="text/javascript" src="../jquery-1.3.2.js"></script>
<script type="text/javascript" src="../jq-ui/ui/ui.core.js"></script>
<script type="text/javascript" src="../jq-ui/ui/ui.slider.js"></script>
<script type="text/javascript" src="../jq-ui/ui/ui.draggable.js"></script>
<script type="text/javascript" src="../jq-ui/ui/ui.resizable.js"></script>
<script type="text/javascript" src="../jq-ui/ui/ui.dialog.js"></script>
<script type="text/javascript" src="./Color.js"></script>
<script type="text/javascript" src="./main.js"></script>
</head>
<body>
<h1>Color</h1>
<div id="ctrl">
<div id="val">Value:<span class="text"></span><div class="slider"></div></div>
<div id="sat">Saturation:<span class="text"></span><div class="slider"></div></div>
</div>
<div id="colors">
</div>
<div id="dialog" title="Color Scheme Example">
</div>
</body>
</html>

jQuery関係のスクリプトは適当にもってきてください。
控えめなJavaScriptを目指すとHTMLはシンプルですね。要素はほとんど動的に作ってます。
Color.jsは上述のColorパッケージで、HSVクラス?とRGBクラス?を収めています。
main.jsはアプリケーション本体です。


では、Colorパッケージがこんな感じ。

Color.js
----
var Color = {};
(function () {
    function HSV(h,s,v) {
        if ( s === undefined ) { s = 1; }
        if ( v === undefined ) { v = 1; }
        this.hue = this.normHue(h);
        this.saturation = this.norm(s);
        this.value = this.norm(v);
    }
    function RGB(r,g,b) {
        if ( typeof r == "number" ) {
            if ( g === undefined ) { g = r; }
            if ( b === undefined ) { b = r; }
        }
        else if ( typeof r == "string" ) {
            var res = r.match("^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$");
            r = parseInt(res[1],16)/255;
            g = parseInt(res[2],16)/255;
            b = parseInt(res[3],16)/255;
        }
        this.red = this.norm(r);
        this.green = this.norm(g);
        this.blue = this.norm(b);
    }
    function _norm(v) {
        if ( v > 1 ) { return 1; }
        else if ( v < 0 ) { return 0; }
        return v;
    }
    function _dec2hexstr(v) {
        var ret = Math.floor(v * 255).toString(16);
        if ( ret.length == 1 ) {
            ret = '0' + ret;
        }
        return ret;
    }

    var ns = Color;
    ns.HSV = HSV;
    ns.RGB = RGB;

    ns.HSV.prototype.normHue = function(h) {
        while( h < 0 ) {
            h += 360;
        }
        h = h - Math.floor(h/360)*360;
        return h;
    };
    ns.HSV.prototype.norm = _norm;
    ns.HSV.prototype.toRGB = function() {
        var ret;
        if ( this.saturation == 0 ) {
            ret = new Color.RGB(this.value);
            return ret;
        }
        var H_60,Hi,f,p,q,t;
        H_60 = Math.floor(this.hue/60);
        Hi = (H_60)%6;
        f = this.hue/60 - H_60;
        p = this.value * ( 1 - this.saturation );
        q = this.value * ( 1 - f * this.saturation );
        t = this.value * ( 1 - (1-f) * this.saturation );
        switch(Hi) {
            case 0:
                ret = new Color.RGB(this.value,t,p);
                break;
            case 1:
                ret = new Color.RGB(q,this.value,p);
                break;
            case 2:
                ret = new Color.RGB(p,this.value,t);
                break;
            case 3:
                ret = new Color.RGB(p,q,this.value);
                break;
            case 4:
                ret = new Color.RGB(t,p,this.value);
                break;
            case 5:
                ret = new Color.RGB(this.value,p,q);
                break;
        }
        return ret;
    };
    ns.HSV.prototype.setValue = function(v) {
        this.value = this.norm(v);
    };
    ns.HSV.prototype.addValue = function(v) {
        this.value = this.norm(this.value + v);
    };
    ns.HSV.prototype.setSaturation = function(v) {
        this.saturation = this.norm(v);
    };
    ns.HSV.prototype.addSaturation = function(v) {
        this.saturation = this.norm(this.saturation + v);
    };
    ns.HSV.prototype.addHue = function(v) {
        this.hue = this.normHue(this.hue + v);
    };
    ns.HSV.prototype.clone = function() {
        return new Color.HSV(this.hue,this.saturation,this.value);
    };

    ns.RGB.prototype.norm = _norm;
    ns.RGB.prototype.toString = function() {
        var r = _dec2hexstr(this.red);
        var g = _dec2hexstr(this.green);
        var b = _dec2hexstr(this.blue);
        return '#' + r + g + b;
    };
    ns.RGB.prototype.toHSV = function() {
        var ret,max,min,proc;
        if ( this.red > this.green ) {
            if ( this.green > this.blue ) {
                max = this.red;
                min = this.blue;
                proc = 0;
            }
            else if ( this.red > this.blue ) {
                max = this.red;
                min = this.green;
                proc = 0;
            }
            else {
                max = this.blue;
                min = this.green;
                proc = 2;
            }
        }
        else { // this.green >= this.red
            if ( this.red > this.blue ) {
                max = this.green;
                min = this.blue;
                proc = 1;
            }
            else if ( this.green > this.blue ) {
                max = this.green;
                min = this.red;
                proc = 1;
            }
            else {
                max = this.blue;
                min = this.red;
                proc = 2;
            }
        }
        var v = max,sat,hue;
        if ( v == 0 ) {
            ret = new Color.HSV(0,0,0);
            return ret;
        }
        sat = (max - min)/max;
        if ( sat == 0 ) {
            ret = new Color.HSV(0,0,v);
            return ret;
        }
        switch(proc) {
            case 0:
                hue = 60 * (this.green - this.blue)/(max-min);
                break;
            case 1:
                hue = 60 * (this.blue - this.red)/(max-min) + 120;
                break;
            case 2:
                hue = 60 * (this.red - this.green)/(max-min) + 240;
                break;
        }
        ret = new Color.HSV(hue,sat,v);
        return ret;
    };
})();


そんで、メインになるアプリケーション部分はこんな感じです。

main.js
----
jQuery.noConflict();
jQuery(document).ready(function(){
    var colors = 24;
    var r = 100;
    var dr = 2*Math.PI/colors;
    var l = 2*r*Math.sin(dr/2);
    var p_2 = Math.PI/2;
    var cont = jQuery('#colors')
            .css('width', (2*r)+'px')
            .css('height', (2*r)+'px')
            .css('position','relative')
            .css('top', (r/2)+'px')
            .css('left', (r/2)+'px')
            .draggable();
    for(var i=0;i<colors;i++) {
        var t = dr*i;
        var d = 180*t/Math.PI;
        var x = Math.cos(t - p_2)*(r+l) + r - l/2;
        var y = Math.sin(t - p_2)*(r+l) + r - l/2;
        var hsv = new Color.HSV(d);
        var rgb = hsv.toRGB().toString();
        jQuery.data(
            jQuery('<div></div>')
                .addClass('color')
                .css('background-color',rgb)
                .css('width', l+'px')
                .css('height', l+'px')
                .css('position','absolute')
                .css('top', y+'px')
                .css('left', x+'px')
                .attr('title',rgb)
                .click(function(){
                    var obj = jQuery.data(this,"hsv_obj");
                    jQuery("#dialog")
                        .text( '' )
                        .append( color_scheme1(obj) )
                        .append( color_scheme2(obj) )
                        .append( color_scheme3(obj) )
                        .append( color_scheme4(obj) )
                        .append( color_scheme5(obj) )
                        .dialog('open');
                })
                .appendTo(cont).get(0),
            "hsv_obj", hsv
        );
    }
    jQuery('#val > .text').text( 1.0 );
    jQuery('#sat > .text').text( 1.0 );
    jQuery('#ctrl .slider')
        .css('width','200px')
        .slider({
            max : 1.0 ,
            step : 0.1 ,
            value : 1.0 ,
            change : function(event,ui) {
                jQuery(jQuery(ui.handle).parents().get(1))
                    .find('.text').text( ui.value );
                change_colors();
            } ,
        });
    jQuery("#dialog").dialog({
        autoOpen: false
    });
});

function change_colors() {
    var v = parseFloat(jQuery('#val > .text').text());
    var s = parseFloat(jQuery('#sat > .text').text());
    jQuery('.color').each(function(i){
        var hsv = jQuery.data(this,"hsv_obj");
        hsv.setValue(v);
        hsv.setSaturation(s);
        var rgb = hsv.toRGB().toString();
        jQuery(this).css('background-color',rgb).attr('title',rgb);
    });
}

function _mk_color_bar(base,sub,accent) {
    return jQuery('<div></div>')
        .css('position', 'relative')
        .css('width', '100px')
        .css('height', '15px')
        .css('padding', '5px')
        .append(
            jQuery('<div></div>')
                .css('float', 'left')
                .css('background-color',base.toString())
                .css('width', '70px')
                .css('height', '15px')
        ).append(
            jQuery('<div></div>')
                .css('float', 'left')
                .css('background-color',accent.toString())
                .css('width', '5px')
                .css('height', '15px')
        ).append(
            jQuery('<div></div>')
                .css('float', 'left')
                .css('background-color',sub.toString())
                .css('width', '25px')
                .css('height', '15px')
        )
}

function _mk_colors1(org,delta_hue_sub,delta_hue_acc) {
    var obj1 = org.clone();
    var obj2 = org.clone();
    obj1.addHue(delta_hue_sub);
    obj2.addHue(delta_hue_acc);
    return { "sub" : obj1 , "acc" : obj2 };
}

function _mk_colors2(org,opt) {
    var val_threshold = (opt && opt.val_threshold !== "undefined" ) ? opt.val_threshold : 0.5;
    var sat_threshold = (opt && opt.sat_threshold === "undefined" ) ? opt.sat_threshold : 0.5;
    var delta_acc_val = (opt && opt.delta_acc_val === "undefined" ) ? opt.delta_acc_val : 0.3;
    var delta_acc_sat = (opt && opt.delta_acc_sat === "undefined" ) ? opt.delta_acc_sat : 0.2;
    var delta_sub_val = (opt && opt.delta_sub_val === "undefined" ) ? opt.delta_sub_val : 0.0;
    var delta_sub_sat = (opt && opt.delta_sub_sat === "undefined" ) ? opt.delta_sub_sat : 0.3;
    var sub = org.clone();
    var acc = org.clone();
    if ( org.value >= val_threshold ) {
        acc.addValue(-delta_acc_val);
        sub.addValue(-delta_sub_val);
    }
    else {
        acc.addValue(delta_acc_val);
        sub.addValue(delta_sub_val);
    }
    if ( org.saturation >= sat_threshold ) {
        acc.addSaturation(-delta_acc_sat);
        sub.addSaturation(-delta_sub_sat);
    }
    else {
        acc.addSaturation(delta_acc_sat);
        sub.addSaturation(delta_sub_sat);
    }
    return { "acc" : acc, "sub" : sub };
}

function color_scheme1(org) {
    var pair = _mk_colors1(org,30,180);
    return _mk_color_bar(org.toRGB(),pair.sub.toRGB(),pair.acc.toRGB())
}

function color_scheme2(org) {
    var pair = _mk_colors1(org,-30,180);
    return _mk_color_bar(org.toRGB(),pair.sub.toRGB(),pair.acc.toRGB())
}

function color_scheme3(org) {
    var pair = _mk_colors2(org);
    return _mk_color_bar(org.toRGB(),pair.sub.toRGB(),pair.acc.toRGB());
}

function color_scheme4(org) {
    var pair = _mk_colors2(org);
    return _mk_color_bar(org.toRGB(),pair.acc.toRGB(),pair.sub.toRGB());
}

function color_scheme5(org) {
    var pair = _mk_colors2(org,{"delta_acc_val":0.2,"delta_acc_sat":0.3,"delta_sub_val":0.3,"delta_sub_sat":0.0});
    return _mk_color_bar(org.toRGB(),pair.sub.toRGB(),pair.acc.toRGB());
}

まあね、main.jsは正直もうちょっと何とかしたい。でも息抜きってことでこれでいいっしょ。