カラーサンプル (HTML) 作ってみた

# はじめに
PythonやRで作図をするときに好きな色を16進数表記で指定したい時があります。
これまで、パワポやエクセルを開いてカラーパレット(って言うんですかね?)で色を選んで、その16進数表記をコピペ・・
とかやっていましたが、いちいち面倒だなぁと思ったのでパレットから色を選んで16進数をコピーできるHTMLを作ってみました。
![color_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/485256/0ba31feb-bd74-9876-65d1-59f9309eb230.png)
真ん中のパレットは横が「色相」縦が「明度」、右の細長い部分は「彩度」になっています。
彩度はパレット上からマウスホイールでも調整可能です。パレット上でクリックした色が「選択した色」になり、16進数が表示され「Copy」ボタンでコピーできます。

# 使い方
1. メモ帳に以下の`color_sample.html`をコピペ
1. `color_sample.html`等の任意の名前で保存
1. ウェブブラウザで開く→上記画像のような画面が開く<br>※`color_sample.html`をブックマークに登録するか、デスクトップに置いておくと便利かも知れません

```html:color_sample.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Color Sample</title>
    </head>
    <body>
        <div>
            <!-- 選択した色を表示するためのブロック -->
            <div id="selected_color_wrapper_w" style="display:flex;">
                <div id="selected_color_wrapper" style="display:flex;flex-direction:column;align-items:center;padding:0px 3px 0px 0px;">
                    <canvas id="selected_color" style="border:1px solid gray"></canvas>
                    <div>
                        <p style="display:inline;">選択した色：</p>
                        <p id="selected_color_character" style="display:inline;"></p>
                        <button id="selected_color_copy">Copy</button>
                    </div>
                </div>
                <div id="current_color_wrapper" style="display:flex;flex-direction:column;align-items:center;">
                    <canvas id="current_color" style="border:1px solid gray"></canvas>
                    <div>
                        <p style="display:inline;">現在の色：</p>
                        <p id="current_color_character" style="display:inline;"></p>
                    </div>
                </div>
            </div>
        </div>
        <!-- カラーパレット部分 -->
        <!-- クリック判定用の透明なやつ -->
        <div id="pallet_wrapper3" style="display:inline-block;">
            <div id="pallet_wrapper2" style="display:flex;flex-direction:column;align-items:center;">
                <div id="pallet_wrapper1">
                    <canvas id="color_pallet_dummy" style="position:absolute;cursor:crosshair;z-index:5;"></canvas>
                    <canvas id="color_pallet" style="display:inline-block;"></canvas>
                    <canvas id="pallet_marker" width=10 height=10 style="position:absolute;z-index:1;"></canvas>
                </div>
                <p>色相 / 明度</p>
            </div>
        </div>
        <div id="satu_bar_wrapper3" style="display:inline-block;">
            <div id="satu_bar_wrapper2" style="display:flex;flex-direction:column;align-items:center;">
                <div id="satu_bar_wrapper1">
                    <canvas id="saturation_bar_dummy" style="position:absolute;display:inline-block;cursor:crosshair;z-index:5;"></canvas>
                    <canvas id="saturation_bar" style="display:inline-block;"></canvas>
                    <canvas id="satu_marker" style="position:absolute;z-index:1;"></canvas>
                </div>
                <p>彩度</p>
            </div>
        </div>
    </body>
</html>
<script type="text/javascript">
// -------------------------------------------------- //
// html要素の変数への格納
var color_pallet=document.getElementById("color_pallet");
var color_pallet_dummy=document.getElementById("color_pallet_dummy");
var pallet_marker=document.getElementById("pallet_marker");
var saturation_bar=document.getElementById("saturation_bar");
var saturation_bar_dummy=document.getElementById("saturation_bar_dummy");
var satu_marker=document.getElementById("satu_marker");
var selected_color=document.getElementById("selected_color");
var current_color=document.getElementById("current_color");
var selected_color_character=document.getElementById("selected_color_character");
var current_color_character=document.getElementById("current_color_character");
var selected_color_copy=document.getElementById("selected_color_copy");

// 要素の調整（値取得順序の関係で先にやる必要があるものがある）
selected_color.widht=window.innerWidth*0.1;
selected_color.height=window.innerHeight*0.1;
current_color.widht=window.innerWidth*0.1;
current_color.height=window.innerHeight*0.1;

// -------------------------------------------------- //
// 必要変数の定義＋オブジェクトの調整
// パレットの縦横の分割数
var pallet_nrow=100;
var pallet_ncol=300;
// パレットと彩度バーの左上の座標
var pallet_left=color_pallet.getBoundingClientRect().left;
var pallet_top=color_pallet.getBoundingClientRect().top;
// パレットと彩度バーのマーカーの座標
var pallet_x_cord=0;
var pallet_y_cord=0;
var pml=pallet_marker_length=5;
var bar_y_cord=0;
// 現在選択している色の値
var current_selected_color="";
var current_selected_hue=1;
var current_selected_value=1;
var current_selected_satu=1;
var current_selected_alpha=0;
var current_hovering_color="";
var current_hovering_hue=1;
var current_hovering_value=1;
var current_hovering_satu=1;
var current_hovering_alpha=0;
// マウスの現在座標
var mouse_x=0;
var mouse_y=0;

//----- カラーパレット描画用の変数＋調整 -----//
var pallet_width=Math.round(window.innerWidth*0.9);
var pallet_height=Math.round(window.innerHeight*0.7);
pallet_width=pallet_width-(pallet_width % pallet_ncol);
pallet_height=pallet_height-(pallet_height % pallet_nrow);
color_pallet.width=pallet_width+1;
color_pallet.height=pallet_height+1;
// ダミーパレットの位置、サイズ調整
color_pallet_dummy.style.top=pallet_top+'px';
color_pallet_dummy.style.left=pallet_left+'px';
color_pallet_dummy.width=pallet_width+1;
color_pallet_dummy.height=pallet_height+1;
var unit_width=pallet_width/pallet_ncol;
var unit_height=pallet_height/pallet_nrow;
var unit_hue=360/pallet_ncol;
var unit_value=1/pallet_nrow;
var current_left=0;
var current_top=0;
var current_hue=0;
var current_value=1;
var convert_xToHue=360/(unit_width*pallet_ncol);
var convert_yToValue=convert_yToSatu=1/(unit_height*pallet_nrow);
// タイマー付きでカラーパレットを描画するためのオブジェクト
var pallet_timer;

//----- 彩度バー描画用の変数＋調整 -----//
var bar_width=Math.round(window.innerWidth*0.05);
var bar_height=pallet_height;
saturation_bar.width=bar_width;
saturation_bar.height=bar_height;
saturation_bar_dummy.width=bar_width;
saturation_bar_dummy.height=bar_height;
var unit_satu=1/pallet_nrow;
// ホイールで彩度マーカーを動かす際の刻み幅
var unit_satu_maker_move=Math.round(pallet_nrow/20);
// マーカーが取れる上限下限。処理が進まないと決められないため下で値は代入
var satu_maker_upper_limit=0;
var satu_maker_lower_limit=0;

// -------------------------------------------------- //
// 関数定義
// hsvをrgbに変換するための関数の定義
function hsvToRgb(arg_h,arg_s,arg_v){
    // hue｜色相
    // saturation｜彩度
    // value｜明度
    // h,s,vの値を引数にとって、rgbの16進数表記を返り値とする
    var c=arg_v*arg_s;
    var h_dash=arg_h/60;
    var x=c*(1-Math.abs(h_dash%2-1));
    var v_c=arg_v-c
    var red=v_c;
    var green=v_c;
    var blue=v_c;
    switch(Math.floor(h_dash)){
        case 0:
            red=red+c;
            green=green+x;
            break;
        case 1:
            red=red+x;
            green=green+c;
            break;
        case 2:
            green=green+c;
            blue=blue+x;
            break;
        case 3:
            green=green+x;
            blue=blue+c;
            break;
        case 4:
            red=red+x;
            blue=blue+c;
            break;
        case 5:
            red=red+c;
            blue=blue+x;
            break;
        case 6:
            red=red+c;
            green=green+x;
            break;
        default:
    }
    red=Math.round(255*red);
    green=Math.round(255*green);
    blue=Math.round(255*blue);
    rgb="#"+("0"+red.toString(16)).slice(-2)+("0"+green.toString(16)).slice(-2)+("0"+blue.toString(16)).slice(-2);
    return rgb
}

// カラーパレットを描画する関数（彩度(s)と透明度(alpha)が引数）
function drawPallet(arg_s,arg_alpha){
    console.log(current_selected_satu);
    var context=color_pallet.getContext("2d");
    var current_left=0;
    var current_top=0;
    var current_hue=0;
    var current_value=1;
    for(i_row=0;i_row<pallet_nrow;i_row=(i_row+1)|0){
        current_left=0;
        current_hue=0;
        for(i_col=0;i_col<pallet_ncol;i_col=(i_col+1)|0){
            // パスの初期化
            context.beginPath();
            // 四角のの座標(x,y)とサイズ(幅,高さ)を指定
            context.rect(current_left,current_top,unit_width,unit_height);
            // 色の指定
            context.fillStyle=hsvToRgb(current_hue,arg_s,current_value);
            // 塗り潰しの実行
            context.fill();
            current_left=current_left+unit_width;   
            current_hue=current_hue+unit_hue; 
        }
        current_top=current_top+unit_height;
        current_value=current_value-unit_value;
    }
    // 外側の枠線を描画
    context.beginPath();
    context.rect(0,0,pallet_width,pallet_height);
    context.strokeStyle="gray";
    context.lineWidth=1;
    context.stroke();
}

// 彩度バーを描画する関数（色相(h)、明度(v)、透明度(alpha)が引数）
function drawSatuBar(arg_h,arg_v,arg_alpha){
    context=saturation_bar.getContext("2d");
    current_left=0;
    current_top=0;
    var current_satu=1;
    for(i_row=0;i_row<pallet_nrow;i_row=(i_row+1)|0){
        context.beginPath();
        context.rect(current_left,current_top,bar_width,unit_height);
        context.fillStyle=hsvToRgb(arg_h,current_satu,arg_v);
        context.fill();
        current_top=current_top+unit_height;
        current_satu=current_satu-unit_satu;
    }
    // 外側の枠線を描画
    context.beginPath();
    context.rect(0,0,bar_width,bar_height);
    context.strokeStyle="gray";
    context.lineWidth=1;
    context.stroke();
}

// パレット内にマウスがある時座標の取得＋色の再計算の関数
function hover_color_pallet(e){
    //座標を取得する
    var mX=e.pageX;
    var mY=e.pageY;
    //座標を表示する
    mouse_x=mX;
    mouse_y=mY;
    current_hovering_hue=(mouse_x-pallet_left)*convert_xToHue;
    current_hovering_value=1-(mouse_y-pallet_top)*convert_yToValue;
    current_hovering_color=hsvToRgb(current_hovering_hue,current_hovering_satu,current_hovering_value);
    current_color.style.backgroundColor=current_hovering_color;
    // 彩度バーの再描画
    drawSatuBar(current_hovering_hue,current_hovering_value,current_selected_alpha);
}

// パレット内をクリックした際の挙動の関数
function click_color_pallet(){
    // パレット内のマーカーの位置を変える
    pallet_marker.style.top=mouse_y-pml-1+'px';
    pallet_marker.style.left=mouse_x-pml-1+'px';
    // クリックした個所の色を取得する
    // クリックした色に変更する
    current_selected_hue=current_hovering_hue;
    current_selected_value=current_hovering_value;
    current_selected_color=hsvToRgb(current_selected_hue,current_selected_satu,current_selected_value);
    selected_color.style.backgroundColor=current_selected_color;
    selected_color_character.innerText=current_selected_color;
    // 彩度バーの再描画
    drawSatuBar(current_selected_hue,current_selected_value,current_selected_alpha);
}

// 彩度バー内にマウスがある時の座標の取得＋色の再計算の関数
function hover_satu_bar(e){
    //座標を取得する
    var mX=e.pageX;
    var mY=e.pageY;
    //座標を表示する
    mouse_x=mX;
    mouse_y=mY;
    current_hovering_satu=1-(mouse_y-satu_top)*convert_yToSatu;
    current_hovering_color=hsvToRgb(current_selected_hue,current_hovering_satu,current_selected_value);
    current_color.style.backgroundColor=current_hovering_color;
}

// 彩度バー内をクリックした際の挙動の関数
function click_satu_bar(){
    // 彩度バー内のマーカーの位置を変える
    satu_marker.style.top=mouse_y+'px';
    // satu_marker.style.left=satu_left+'px';
    // クリックした色に変更する
    current_selected_satu=current_hovering_satu;
    current_selected_color=hsvToRgb(current_selected_hue,current_selected_satu,current_selected_value);
    selected_color.style.backgroundColor=current_selected_color;
    selected_color_character.innerText=current_selected_color;
    // カラーパレットの再描画
    drawPallet(current_selected_satu,current_selected_alpha);
}


// カラーパレット内でマウスホイールでサイドバーを動かすための関数
function wheel_satu_bar(e){
    var delta=e.deltaY
    // 現在のマーカーのy座標
    var current_satu_maker_y=Number(satu_marker.style.top.split("px")[0]);
    // 更新後のマーカーのy座標
    var new_satu_maker_y=0;
    if(delta>0){//delta>0 → ホイールは下向き
        new_satu_maker_y=Math.min(current_satu_maker_y+unit_satu_maker_move,satu_maker_upper_limit);
    }else{
        new_satu_maker_y=Math.max(current_satu_maker_y-unit_satu_maker_move,satu_maker_lower_limit);
    }
    // 彩度マーカーの位置の更新
    satu_marker.style.top=new_satu_maker_y+'px';
    // 現在の彩度を設定
    current_selected_satu=current_hovering_satu=1-(new_satu_maker_y-satu_top)*convert_yToSatu;
    // 選択した色と現在の色を更新する
    current_hovering_color=hsvToRgb(current_hovering_hue,current_hovering_satu,current_hovering_value);
    current_selected_color=hsvToRgb(current_selected_hue,current_hovering_satu,current_selected_value);
    current_color.style.backgroundColor=current_hovering_color;
    selected_color.style.backgroundColor=current_selected_color;
    selected_color_character.innerText=current_selected_color;
    // 連続で描画すると非常にもっさりと重くなるため、ホイールが止まったら描画する様にする
    clearTimeout(pallet_timer);
    pallet_timer=setTimeout(function(){drawPallet(current_selected_satu,current_selected_alpha)},50);
}


// マウスの位置を追尾する
// ホバー中カラーパレット、彩度バーの再描画をする
window.onload=function(){
    color_pallet_dummy.addEventListener("mousemove",hover_color_pallet);
    saturation_bar_dummy.addEventListener("mousemove",hover_satu_bar);
}
// カラーパレットからマウスが出たタイミングで、彩度バーの再描画をする
color_pallet_dummy.addEventListener("mouseout",function(){
    drawSatuBar(current_selected_hue,current_selected_value,current_selected_alpha);
});

// ホイールで彩度の変更を可能にする
color_pallet_dummy.onwheel=wheel_satu_bar;


// -------------------------------------------------- //
//----- カラーパレットの描画 -----//
// キャンバスサイズの調整（描画するタイル数で割り切れる様に調整）
drawPallet(current_selected_satu,current_selected_alpha);

//----- マーカーの描画（初期位置は左上） -----//
context=pallet_marker.getContext("2d");
// moveTo｜始点。lineTo｜終点
// 白線
context.lineWidth=2;
context.strokeStyle="white";
// 横線
context.beginPath();
context.moveTo(0,pml);
context.lineTo(pml*2,pml);
context.stroke();
// 縦線
context.beginPath();
context.moveTo(pml,0);
context.lineTo(pml,pml*2);
context.stroke();
// パレットのマーカーを初期位置（左上）にセット
pallet_marker.style.top=pallet_top-pml+1+'px';
pallet_marker.style.left=pallet_left-pml+1+'px';

// パレット内をクリックした際の挙動を設定
color_pallet_dummy.onclick=click_color_pallet;


// -------------------------------------------------- //
//----- 彩度バーの描画 -----//
drawSatuBar(current_selected_hue,current_selected_value,current_selected_alpha);


//----- 彩度マーカーの描画 -----//
// 彩度バーの左上
var satu_left=saturation_bar.getBoundingClientRect().left+window.pageXOffset;
var satu_top=saturation_bar.getBoundingClientRect().top+window.pageYOffset;
// 彩度マーカーの上限下限の設定
satu_maker_upper_limit=satu_top+bar_height;
satu_maker_lower_limit=satu_top;


// 彩度バーのサイズ調整
satu_marker.width=bar_width;
satu_marker.height=2;
satu_marker
context=satu_marker.getContext("2d");
// moveTo｜始点。lineTo｜終点
// 白線
context.lineWidth=2;
context.strokeStyle="white";
// 横線
context.beginPath();
context.moveTo(0,1);
context.lineTo(bar_width,1);
context.stroke();
// 彩度バーのマーカーを初期位置（左上）にセット
satu_marker.style.top=satu_top+'px';
satu_marker.style.left=satu_left+'px';

// 彩度バー内をクリックした際の挙動を設定
saturation_bar_dummy.onclick=click_satu_bar;

// -------------------------------------------------- //
// 現在の選択色の見本とRGBの表記の表示
selected_color.style.backgroundColor=hsvToRgb(1,1,1);
selected_color_character.innerText=hsvToRgb(1,1,1);
current_color.style.backgroundColor=hsvToRgb(1,0,1);

// コピーボタンをクリックしたらコピー
selected_color_copy.onclick=function(){
    navigator.clipboard.writeText(selected_color_character.innerText);
}
</script>

```

#おわりに
今回作った様なものは既にありそう感じがするのですが、探しても見つからなかったためGWの暇つぶしを兼ねて作ってみました。
またライブラリ等は使用せずフルスクラッチで作ったため、想像以上にコードが長くなり予期せず400行越えのコードとなってしまいました。（ライブラリを使うとライブラリの仕様変更に振り回されることもあるかと思い使用しませんでした。）
処理速度等も気にして多少は工夫してコーディングしていますが、もしアドバイス等あれば頂けると幸いです。

お読みいただきありがとうございました。