LOG

HSLで色を変える

よくCSSなどでは色の指定を16進数で「#FF11AA」という具合に指定するが、色をリアルタイムに変換するにはどうしたらいいんだろうと思い色々調べてみた。
とりあえず作ったもの

デモ

色指定の種類について

RGBは16進数で色を指定したもので、「#FF11AA」や「0xFF11AA」という形を取る。先頭の#や0xは続く値が16進数であるという宣言部分(コンパイラによって表記が違う)
これは1色を1byteで表し、RGBの順に並べている。
1byteは8bitからなり1bitは0か1の2進数なので8bitで表せる数の最大は11111111、10進数で11111111は255なので1色は0〜255(16進数で0〜FF)で表す。

RGBだと色、明るさ、鮮やかさで分類し難しいが、
HSLという形式だと色相、彩度、輝度で値を指定するので、色味を自由に変更し易い。

参考にさせてもらったのは以下のページ

HSLの計算方法が下記URLがとてもわかり易かった

デモのコード

const SHAPE_NUM = 24;
const SIZE      = { X : 500, Y : 500 };
const CENTER    = { X : SIZE.X/2, Y : SIZE.Y/2 };
let $canvas,shapes,stats,stage,renderer;

// CLASS Helper ================================================================
//http://d.hatena.ne.jp/ja9/20100903/1283504341
class Helper{
    static RGB2HSL(r,g,b){
        // 参考
        // wikipedia      :https://ja.wikipedia.org/wiki/HLS%E8%89%B2%E7%A9%BA%E9%96%93
        // stack overflow :http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
        // 色相、彩度、明度の計算方法:http://imagingsolution.blog107.fc2.com/blog-entry-247.html
        //
        // HSLは双六角錐モデル
        //
        // 上から見た図(色相) 反時計回り
        //    G(120)
        //   / ̄ ̄ ̄\
        // /  / ̄\  \R(0)
        // \  \_/  /
        //   \___/
        //    B(240)
        //
        // 横から見た図(右半分) 輝度と彩度
        //       L(1)
        //       |\
        //       |  \
        // L(0.5)|__\R(255=1)
        //     Lx|__/
        //       |  /
        //       |/
        //       L(0)
        //
        r /= 255; // 0~255を0~1に変える
        g /= 255; // 0~255を0~1に変える
        b /= 255; // 0~255を0~1に変える
        let max = Math.max(r,g,b);
        let min = Math.min(r,g,b);
        let h; // 色相(Hue) | 円
        let s; // 彩度(Saturation) | 半径
        let l; // 輝度(Lightness/Luminance) | z座標 | 0で黒、1で白
        l = (max + min) / 2; // 輝度は0〜1になる。rgbの最小値と最大値で中間値を出しこれが輝度になる。
        if(max === min) h = s = 0;
        else{
            let d = max - min;
            s     = l > 0.5 ? d / (2 - max - min) : d / (max + min); // 彩度は0~1のため、輝度が0.5以上か以下かで処理を変える
            switch(min){
                case r :
                    h = 60 * ( (b-g) / d ) + 180; // 約分して h = ( (b-g) / d ) + 3 でも良い
                    break;
                case g :
                    h = 60 * ( (r-b) / d ) + 300; // 約分して h = ( (r-b) / d ) + 5 でも良い
                    break;
                case b :
                    h = 60 * ( (g-r) / d ) + 60; // 約分して h = (g-r) / d でも良い
                    break;
            }
            h /= 360; // 角度(0~360)を0~1にする
            return [h,s,l];
        }
    }
    static HSL2RGB(h,s,l){
        // HSLからRGBへの変換公式は見つけれなかった・・・
        let r, g, b;
        if(s == 0) r = g = b = l; // achromatic
        else{
            let hue2rgb = function hue2rgb(p, q, t){
                if(t < 0) t += 1;
                if(t > 1) t -= 1;
                if(t < 1/6) return p + (q - p) * 6 * t;
                if(t < 1/2) return q;
                if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
                return p;
            }
            let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
            let p = 2 * l - q;
            r = hue2rgb(p, q, h + 1/3);
            g = hue2rgb(p, q, h);
            b = hue2rgb(p, q, h - 1/3);
        }
        return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
    }
}

// CLASS Shape =================================================================
class Shape{
    get size(){ return 5; }
    get speed(){ return 0.1; }
    get diameter(){ return 200; }
    // コンストラクタ ------------------------------------------------------------
    constructor(angle,num){
        this.angle  = angle;
        this.radian = this.angle2radian(this.angle);
        this.g      = new PIXI.Graphics();
        this.h      = num != 0 ? 1 / SHAPE_NUM * num : 0;
        // this.h = Math.random() * 1;
        this.s = 0.8;
        this.l = 0.6;
        let x  = Math.cos(this.radian) * this.diameter / 2;
        let y  = Math.sin(this.radian) * this.diameter / 2;
        this.changeColor();
        this.g.x = CENTER.X + x;
        this.g.y = CENTER.Y + y;
    }
    // 角度をアングルに変更 ------------------------------------------------------
    angle2radian(angle){
        return angle * Math.PI / 180;
    }
    // 色変更 --------------------------------------------------------------------
    changeColor(){
        this.h     = (this.h + 0.01) % 1;
        let color  = Helper.HSL2RGB(this.h,this.s,this.l);
        let r      = ('0'+(color[0]).toString(16)).slice(-2);
        let g      = ('0'+(color[1]).toString(16)).slice(-2);
        let b      = ('0'+(color[2]).toString(16)).slice(-2);
        this.color = '0x'+r+g+b;
        this.g.clear();
        this.g.beginFill(this.color);
        this.g.drawCircle(0,0,this.size);
    }
    // 移動 ----------------------------------------------------------------------
    move(){
        this.angle += this.speed;
        this.radian = this.angle2radian(this.angle);
        let x       = Math.cos(this.radian) * this.diameter / 2;
        let y       = Math.sin(this.radian) * this.diameter / 2;
        this.g.x    = CENTER.X + x;
        this.g.y    = CENTER.Y + y;
    }
}
// Animation ===================================================================
let animation = ()=>{
    stats.begin();
    let i = 0;
    for(; i < SHAPE_NUM; i=(i+1)|0){
        shapes[i].changeColor();
        shapes[i].move();
    }
    renderer.render(stage);
    stats.end();
    requestAnimationFrame(animation);
}

// Init ========================================================================
let init = ()=>{
    shapes   = [];
    $canvas  = $('#my-canvas');
    stage    = new PIXI.Container();
    renderer = PIXI.autoDetectRenderer(SIZE.X,SIZE.Y,{
        'view'      : $canvas[0],
        'antialias' : true
    });
    // Stats ---------------------------------------------------------------------
    stats = new Stats();
    stats.setMode(0); // 0: fps, 1: ms, 2: mb
    stats.domElement.style.position   = 'absolute';
    stats.domElement.style.left       = '0px';
    stats.domElement.style.top        = '0px';
    stats.domElement.style.marginLeft = '0px';
    stats.domElement.style.marginTop  = '0px';
    stats.domElement.style.zIndex     = '9999';
    document.body.appendChild( stats.domElement );
    // Shapeの作成 ---------------------------------------------------------------
    for(let i = 0,l = SHAPE_NUM; i < l; i++){
        let shape = new Shape( 360 / SHAPE_NUM * i, i );
        stage.addChild(shape.g);
        shapes.push(shape);
    }
    animation();
}

// Go =========================================================================
init();