カラーピッカー

だいたいChatGPT4くんの手による。


デフォルトのピッカー

input type="color"の場合

`<input type="color">` 要素を使用すると、ブラウザが内蔵しているカラーピッカーが表示されます。この要素を使うと、ウェブページにカラーピッカーインターフェースを簡単に組み込むことができます。内部的な流れは以下のようになります:

  1. HTMLレンダリング:

    • ブラウザはHTMLページを読み込み、DOM(Document Object Model)を構築します。

    • `<input type="color">` 要素が見つかると、ブラウザはこの要素を特定の形式のウィジェット(この場合はカラーピッカー)としてレンダリングします。

  2. ユーザーインタラクション:

    • ユーザーがこのカラーピッカー要素をクリックすると、ブラウザは内蔵のカラーピッカーインターフェースを表示します。

    • ユーザーは色を選択でき、その色は要素の `value` プロパティに16進数のカラーコードとして保存されます。

  3. JavaScriptとの連携:

    • カラーピッカーの値が変更されると、`input` イベントが発生します。

    • JavaScriptを使用して、このイベントをリッスンし、色の変更に対応する処理を行うことができます。例えば、選択された色をページの別の部分に表示したり、他の処理をトリガーすることができます。

この機能はブラウザに組み込まれているため、開発者は独自のカラーピッカーインターフェースを作成する必要がなく、標準的なHTML入力要素を使って直感的なカラーピッカー機能を提供できます。ただし、各ブラウザのカラーピッカーの見た目や動作は多少異なる場合があります。

inputとchange

`input` イベントと `change` イベントは、ユーザーが入力要素(例えば、`<input>`, `<textarea>`, `<select>` など)を操作したときに発生するイベントですが、それぞれ異なるタイミングと状況でトリガーされます。これらの違いを詳しく見ていきましょう:

`input` イベント

  • 発生タイミング: `input` イベントは、要素の値が変更されるたびにリアルタイムで発生します。これには、ユーザーがテキストをタイプする、スライダーを動かす、カラーピッカーで色を選ぶなどのアクションが含まれます。

  • 用途: このイベントは、入力が行われるたびに何らかの処理を行いたい場合(例: ライブ検証、リアルタイムのフィルタリング、動的なインターフェースの更新など)に使用されます。

  • 特徴: `input` イベントは、ユーザーが入力を行っている間、つまり値が変化している間に繰り返し発生します。

`change` イベント

  • 発生タイミング: `change` イベントは、要素の値が変更され、その要素がフォーカスを失ったとき(例えば、入力フィールドの外をクリックしたとき)に発生します。

  • 用途: このイベントは、ユーザーが入力を完了し、その結果に基づいて何かしらのアクションを起こしたい場合に使用されます(例: フォームの送信、入力値に基づいた計算や検証の実行など)。

  • 特徴: `change` イベントは、値の変更が「確定」したときに一度だけ発生します。

  • `<input type="text">` にテキストを入力している場合、各文字が入力されるたびに `input` イベントが発生します。入力フィールドからフォーカスが外れたとき(例えば、別の場所をクリックしたとき)、`change` イベントが発生します。

  • `<input type="color">`(カラーピッカー)の場合、色を選択している最中に `input` イベントが発生し、色の選択が完了してカラーピッカーが閉じられたときに `change` イベントが発生します。

<!DOCTYPE html>
<html>
<body>

<h2>JavaScript カラーピッカー</h2>

<input type="color" id="myColor" value="#ff0000">

<p id="colorValue"></p>

<script>
var colorPicker = document.getElementById("myColor");
var colorValue = document.getElementById("colorValue");

// 初期値の設定
colorValue.innerHTML = "選択された色: " + colorPicker.value; 

colorPicker.addEventListener("input", function() {
  colorValue.innerHTML = "選択された色: " + colorPicker.value;
}, false);
</script>

</body>
</html> 

右クリックするとhexをクリップボードに取得できるようにする

%%html
<!DOCTYPE html>
<html>
<body>

<h2>JavaScript カラーピッカー</h2>

<input type="color" id="myColor" value="#ff0000">

<p id="colorValue"></p>

<script>
var colorPicker = document.getElementById("myColor");
var colorValue = document.getElementById("colorValue");

// 初期値の設定
colorValue.innerHTML = "選択された色: " + colorPicker.value; 

colorPicker.addEventListener("input", function() {
  colorValue.innerHTML = "選択された色: " + colorPicker.value;
}, false);

colorPicker.addEventListener("contextmenu", function(event) {
  event.preventDefault();
  var colorHex = colorPicker.value;
  navigator.clipboard.writeText(colorHex);
});
</script>

</body>
</html>

コピペテスト

このコードはローカルでは一応動く(それでもブラウザが確認してきたりする)。Google Colabとかでは動かない。
多分ClipBoardAPIとのかねあい問題。テキストフィールドを間に挟むのが妥当。

<!DOCTYPE html>
<html>
<body>

<h2>JavaScript カラーピッカー</h2>

<input type="color" id="myColor" value="#ff0000">

<p id="colorValue"></p>

<script>
var colorPicker = document.getElementById("myColor");
var colorValue = document.getElementById("colorValue");

// 初期値の設定
colorValue.innerHTML = "選択された色: " + colorPicker.value; 

colorPicker.addEventListener("input", function() {
  colorValue.innerHTML = "選択された色: " + colorPicker.value;
}, false);

colorPicker.addEventListener("contextmenu", function(event) {
  event.preventDefault();
  showContextMenu(event.pageX, event.pageY);
});

function showContextMenu(x, y) {
  var contextMenu = document.createElement("div");
  contextMenu.style.position = "absolute";
  contextMenu.style.top = y + "px";
  contextMenu.style.left = x + "px";
  contextMenu.style.backgroundColor = "#ffffff";
  contextMenu.style.border = "1px solid #cccccc";
  contextMenu.style.zIndex = 1000;
  contextMenu.id = "contextMenu";

  var copyOption = document.createElement("div");
  copyOption.innerText = "コピー";
  copyOption.style.padding = "8px";
  copyOption.style.cursor = "pointer";
  copyOption.onclick = function() {
    var hexValue = colorPicker.value;
    navigator.clipboard.writeText(hexValue);
    document.body.removeChild(contextMenu);
  };

  var pasteOption = document.createElement("div");
  pasteOption.innerText = "ペースト";
  pasteOption.style.padding = "8px";
  pasteOption.style.cursor = "pointer";
  pasteOption.onclick = function() {
    navigator.clipboard.readText().then(function(text) {
      if (/^#[0-9A-F]{6}$/i.test(text)) {
        colorPicker.value = text;
        colorValue.innerHTML = "選択された色: " + text;
      }
      document.body.removeChild(contextMenu);
    });
  };

  contextMenu.appendChild(copyOption);
  contextMenu.appendChild(pasteOption);
  document.body.appendChild(contextMenu);

  // メニュー外をクリックしたときにメニューを閉じる
  document.addEventListener("click", function onClickOutside(event) {
    if (!contextMenu.contains(event.target)) {
      document.body.removeChild(contextMenu);
      document.removeEventListener("click", onClickOutside);
    }
  });
}

</script>

</body>
</html>


jscolorの場合

`jscolor` は、Webページに色の選択機能を提供するためのJavaScriptライブラリです。このライブラリの主な特徴と使い方について以下のようにまとめられます:

特徴と機能
自己完結型
: `jscolor.js` は自己完結型のJavaScriptライブラリで、他のフレームワーク(jQueryなど)を必要としませんが、これらとも互換性があります。
カラーピッカーの機能: ウェブカラーピッカーで透明度(アルファチャネル)をサポートし、カスタマイズ可能なパレットを提供します。CSSカラー(`rgba()` やヘックス形式、`#rrggbbaa`形式)をサポートします。

インストールと設定
インストール
: `jscolor.js` を直接ダウンロードするか、Node.jsのnpmパッケージからインストール(`npm install @eastdesire/jscolor`)、またはCDNJSから利用することができます。
HTMLへの組み込み: `<script src="jscolor.js"></script>` を使ってHTMLにライブラリを含めます。
データ属性の追加: 色を選択したいHTML要素(`<input>`、`<button>`、`<span>` など)に `data-jscolor` 属性を追加します【27†source】。

設定とカスタマイズ
全体のデフォルト設定
: `jscolor.presets.default = {<default_options>}` を使って、すべてのカラーピッカーのデフォルト設定を変更できます。
個別の設定: 各カラーピッカーを `data-jscolor` 属性を使って個別に設定でき、これらの設定は継承された設定を上書きします。
プリセット: ビルトインのプリセット(`light`, `dark`, `small`, `medium`, `large` など)を使用してカラーピッカーを簡単に設定できます。

スクリプティングとAPI
カラーピッカーインスタンスへのアクセス
: `data-jscolor` 属性が付けられた要素は、DOMがロードされた後にピッカーインスタンスをそのプロパティとして持ちます(例: `document.querySelector('#colorInput1').jscolor`)。
プログラムによるピッカーの作成: 新しいキーワードを使用してプログラムにより新しいカラーピッカーを作成できます(例: `var myPicker = new JSColor(<targetElement>, <options>)`)。
ピッカーインスタンスの設定: 作成時に既知のオプションをパラメータとして設定することが推奨されていますが、後から `option()` メソッドを使用して変更することもできます。

APIオプション

`jscolor` は多数の設定オプションを提供しており、これらを使用してカラーピッカーの挙動や外観を細かくカスタマイズできます。例えば、アルファチャネルの有効化、背景色や境界色の設定、カラーピ

ッカーのサイズや形状のカスタマイズなどが可能です【34†source】。

静的プロパティ

  • `activeClassName`: ピッカーウィンドウが開かれたときにターゲット要素に設定されるクラス名。

  • `className`: カラーピッカーが作成されたときにターゲット要素に設定されるクラス名。

  • `looseJSON`: `data-jscolor` 属性の設定が緩いJSON形式(引用符なしのオプション名など)であってもよいかどうかを指定します【35†source】。

`jscolor` は、その単純さとカスタマイズの容易さから、ウェブ開発において非常に人気のある色選択ツールです。

<!DOCTYPE html>
<html>
<head>
    <title>Custom Color Picker</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jscolor/2.4.5/jscolor.js"></script>
</head>
<body>

<p>常に表示されるカラーピッカー:</p>
<button class="jscolor {valueElement:null,value:'ff6699'}">選択した色</button>

</body>
</html>

3つのスライダー形式の場合

<!DOCTYPE html>
<html>
<body>

<h2>カスタムカラーピッカー</h2>

<div id="picker">
  <label>R: <input type="range" id="red" min="0" max="255"></label>
  <label>G: <input type="range" id="green" min="0" max="255"></label>
  <label>B: <input type="range" id="blue" min="0" max="255"></label>
</div>

<div id="colorBox"></div>
<p id="rgbValue"></p>

<style>
  #colorBox {
    width: 200px;
    height: 200px;
    border: 1px solid black;
  }
</style>

<script>
  const redSlider = document.getElementById('red');
  const greenSlider = document.getElementById('green');
  const blueSlider = document.getElementById('blue');
  const colorBox = document.getElementById('colorBox');
  const rgbValue = document.getElementById('rgbValue');

  function setColor() {
    const r = redSlider.value;
    const g = greenSlider.value;
    const b = blueSlider.value;

    colorBox.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
    rgbValue.textContent = `選択された色: rgb(${r}, ${g}, ${b})`;
  }

  redSlider.addEventListener('input', setColor);
  greenSlider.addEventListener('input', setColor);
  blueSlider.addEventListener('input', setColor);

  setColor();
</script>

</body>
</html>


HSB

<!DOCTYPE html>
<html>
<body>

<h2>HSBピッカー</h2>

<label>Hue: <input type="range" id="hue" min="0" max="360" value="0"></label>
<canvas id="sbPicker" width="200" height="200"></canvas>

<div id="colorBox"></div>
<p id="hsbValue"></p>

<style>
  #colorBox {
    width: 200px;
    height: 200px;
    border: 1px solid black;
  }
</style>

<script>
  const hueSlider = document.getElementById('hue');
  const canvas = document.getElementById('sbPicker');
  const ctx = canvas.getContext('2d');
  const colorBox = document.getElementById('colorBox');
  const hsbValue = document.getElementById('hsbValue');

  function drawSBPicker(hue) {
    const gradientSaturation = ctx.createLinearGradient(0, 0, canvas.width, 0);
    gradientSaturation.addColorStop(0, 'hsl(' + hue + ', 0%, 50%)');
    gradientSaturation.addColorStop(1, 'hsl(' + hue + ', 100%, 50%)');
    
    const gradientBrightness = ctx.createLinearGradient(0, 0, 0, canvas.height);
    gradientBrightness.addColorStop(0, 'hsl(' + hue + ', 100%, 100%)');
    gradientBrightness.addColorStop(1, 'hsl(' + hue + ', 100%, 0%)');

    ctx.fillStyle = gradientSaturation;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = gradientBrightness;
    ctx.globalCompositeOperation = 'multiply';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.globalCompositeOperation = 'source-over';
  }

  function setColor(e) {
    const hue = hueSlider.value;
    const rect = e.target.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    const saturation = Math.round(x / rect.width * 100);
    const brightness = Math.round(100 - y / rect.height * 100);

    colorBox.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${brightness}%)`;
    hsbValue.textContent = `選択された色: hsl(${hue}, ${saturation}%, ${brightness}%)`;
  }

  hueSlider.addEventListener('input', () => {
    drawSBPicker(hueSlider.value);
  });
  canvas.addEventListener('click', setColor);
  drawSBPicker(hueSlider.value);
</script>

</body>
</html>

RGB

<!DOCTYPE html>
<html>
<body>

<h2>RGBピッカー</h2>

<label>Red: <input type="range" id="red" min="0" max="255" value="0"></label>
<canvas id="gbPicker" width="200" height="200"></canvas>

<div id="colorBox"></div>
<p id="rgbValue"></p>

<style>
  #colorBox {
    width: 200px;
    height: 200px;
    border: 1px solid black;
  }
</style>

<script>
  const redSlider = document.getElementById('red');
  const canvas = document.getElementById('gbPicker');
  const ctx = canvas.getContext('2d');
  const colorBox = document.getElementById('colorBox');
  const rgbValue = document.getElementById('rgbValue');

  function drawGBPicker(red) {
    const gradientGreen = ctx.createLinearGradient(0, 0, canvas.width, 0);
    gradientGreen.addColorStop(0, 'rgb(' + red + ', 0, 128)');
    gradientGreen.addColorStop(1, 'rgb(' + red + ', 255, 128)');
    
    const gradientBlue = ctx.createLinearGradient(0, 0, 0, canvas.height);
    gradientBlue.addColorStop(0, 'rgb(' + red + ', 128, 255)');
    gradientBlue.addColorStop(1, 'rgb(' + red + ', 128, 0)');
    
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = gradientGreen;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = gradientBlue;
    ctx.globalCompositeOperation = 'source-over';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }

  function setColor(e) {
    const red = redSlider.value;
    const rect = e.target.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    const green = Math.round(x / rect.width * 255);
    const blue = Math.round(255 - y / rect.height * 255);

    colorBox.style.backgroundColor = `rgb(${red}, ${green}, ${blue})`;
    rgbValue.textContent = `選択された色: rgb(${red}, ${green}, ${blue})`;
  }

  redSlider.addEventListener('input', () => {
    drawGBPicker(redSlider.value);
  });
  canvas.addEventListener('click', setColor);
  drawGBPicker(redSlider.value);
</script>

</body>
</html>


デフォルトピッカーの自作

自作とはいえ、作ったのはChatGPT4

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Color Picker with Hue Slider and Saturation/Brightness Gradient</title>
<style>
  #colorCanvas {
    width: 300px;
    height: 150px;
    border: 1px solid #000;
    cursor: crosshair;
  }
  #hueSlider {
    width: 300px;
    height: 20px;
    background: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red);
    margin-top: 10px;
    cursor: pointer;
  }
  #colorPreview {
    width: 50px;
    height: 50px;
    border: 1px solid #000;
    margin-top: 10px;
  }
</style>
</head>
<body>
<canvas id="colorCanvas"></canvas>
<div id="hueSlider"></div>
<div id="colorPreview"></div>

<script>
  const canvas = document.getElementById('colorCanvas');
  canvas.width = 300;
  canvas.height = 150;
  const ctx = canvas.getContext('2d');
  const hueSlider = document.getElementById('hueSlider');
  const colorPreview = document.getElementById('colorPreview');
  let hue = 0;

  function drawGradient(hue) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    const satGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
    satGradient.addColorStop(0, `hsl(${hue}, 0%, 50%)`);
    satGradient.addColorStop(1, `hsl(${hue}, 100%, 50%)`);
    ctx.fillStyle = satGradient;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    const brightGradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
    brightGradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
    brightGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0)');
    brightGradient.addColorStop(0.5, 'rgba(0, 0, 0, 0)');
    brightGradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
    ctx.fillStyle = brightGradient;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }

  function pickColor(event) {
    const rect = canvas.getBoundingClientRect();
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;
    const imageData = ctx.getImageData(x, y, 1, 1).data;
    colorPreview.style.backgroundColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`;
  }

  hueSlider.addEventListener('click', function(event) {
    const rect = hueSlider.getBoundingClientRect();
    const x = event.clientX - rect.left;
    hue = (x / hueSlider.offsetWidth) * 360;
    drawGradient(hue);
  });

  canvas.addEventListener('click', pickColor);

  drawGradient(hue); // Initial gradient draw
</script>
</body>
</html>


ここから手直ししたもの
ChatGPT君が更新手順をぐちゃぐちゃにするため、直した。

基本
GUI(Knob)->h,s,v値更新->Canvas, Preview更新
他のケース未対応


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Complete Color Picker</title>
    <style>
        body {
            user-select: none;
        }

        .slider-container {
            position: relative;
            width: 300px;
            margin-bottom: 5px;
        }

        .slider {
            width: 100%;
            height: 20px;
            background: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red);
            position: relative;
        }

        .sliderKnob {
            position: absolute;
            width: 10px;
            height: 20px;
            background: white;
            border-radius: 5px;
            cursor: pointer;
            left: 0;
            /* 初期位置 */
            z-index: 2;
            /* 他の要素の上に表示 */
        }

        .canvas-container {
            position: relative;
            width: 300px;
            height: 150px;
            margin-bottom: 5px;
        }

        #colorCanvas {
            width: 300px;
            height: 150px;
            border: 1px solid #000;
            position: relative;
            cursor: crosshair;
        }

        .canvasKnob {
            position: absolute;
            width: 10px;
            height: 10px;
            border: 1px solid #000;
            border-radius: 50%;
            cursor: pointer;
        }

        #colorPreview {
            width: 50px;
            height: 50px;
            border: 1px solid #000;
            margin-top: 5px;
        }

        .value-input {
            width: 40px;
        }
    </style>
</head>

<body>
    <div id="hueSliderContainer" class="slider-container">
        <div id="hueSlider" class="slider"></div>
        <div id="hueSliderKnob" class="sliderKnob"></div>
    </div>

    <div id="SVCanvasContainer" class="canvas-container">
        <canvas id="SVCanvas" width="300" height="150"></canvas>
        <div id="svCanvasKnob" class="canvasKnob"></div> <!-- IDからクラスに変更 -->
    </div>


    <div id="colorPreview"></div>
    <div>
        <label>H <input type="number" min="0" max="360" value="0" id="hValue" class="value-input"></label>
        <label>S <input type="number" min="0" max="100" value="100" id="sValue" class="value-input"></label>
        <label>V <input type="number" min="0" max="100" value="100" id="vValue" class="value-input"></label>
    </div>
    <div>
        <label>R <input type="number" min="0" max="255" value="255" id="rValue" class="value-input"></label>
        <label>G <input type="number" min="0" max="255" value="0" id="gValue" class="value-input"></label>
        <label>B <input type="number" min="0" max="255" value="0" id="bValue" class="value-input"></label>
    </div>

    <script>

        // Initial values
        //->knob position

        let hue = 0,
            saturation = 100,
            value = 100;

        // Select elements
        const hueSlider = document.getElementById('hueSlider');
        const hueKnob = document.getElementById('hueSliderKnob');
        const colorCanvas = document.getElementById('SVCanvas');
        const canvasKnob = document.getElementById('svCanvasKnob');
        const colorPreview = document.getElementById('colorPreview');

        const hValue = document.getElementById('hValue');
        const sValue = document.getElementById('sValue');
        const vValue = document.getElementById('vValue');
        const rValue = document.getElementById('rValue');
        const gValue = document.getElementById('gValue');
        const bValue = document.getElementById('bValue');


        // Event listeners
        let isDraggingSlider = false;
        let isDraggingCanvas = false;

        // Event listeners for hueKnob
        hueKnob.addEventListener('mousedown', function (event) {
            isDraggingSlider = true;
        });
        document.addEventListener('mousemove', function (event) {
            if (isDraggingSlider) {
                updateHueFromMousepos(event);
                updateFromMouseposSliderKnobPosition(event);
                updateColorCanvas();
                updateColorPreview();
            }
        });
        document.addEventListener('mouseup', function () {
            isDraggingSlider = false;
        });




        canvasKnob.addEventListener('mousedown', function (event) {
            isDraggingCanvas = true;            
        });
        document.addEventListener('mousemove', function (event) {
            if (isDraggingCanvas) {
                updateSaturationAndValueFromMousepos(event); // s,vの値を更新
                updateFromMouseposCanvasKnobPosition(event); // ドラッグ中にノブの位置を更新
                updateColorCanvas();
                updateColorPreview();                
            }
        });
        document.addEventListener('mouseup', function () {
            isDraggingCanvas = false;
        });



        // From value
        function updateColorPreview() {
            const rgb = HSVtoRGB(hue / 360, saturation / 100, value / 100);
            colorPreview.style.backgroundColor = `rgb(${rgb.r},${rgb.g},${rgb.b})`;
            rValue.value = rgb.r;
            gValue.value = rgb.g;
            bValue.value = rgb.b;
            hValue.value = Math.round(hue);
            sValue.value = Math.round(saturation);
            vValue.value = Math.round(value);
        }

        // From value
        function updateColorCanvas() {
            const ctx = colorCanvas.getContext('2d');
            const width = colorCanvas.width;
            const height = colorCanvas.height;
            ctx.clearRect(0, 0, width, height);

            // Draw saturation gradient
            const satGradient = ctx.createLinearGradient(0, 0, width, 0);
            satGradient.addColorStop(0, 'white');
            satGradient.addColorStop(1, `hsl(${hue}, 100%, 50%)`);
            ctx.fillStyle = satGradient;
            ctx.fillRect(0, 0, width, height);

            // Draw value gradient
            const valGradient = ctx.createLinearGradient(0, 0, 0, height);
            valGradient.addColorStop(0, 'rgba(0,0,0,0)');
            valGradient.addColorStop(1, 'black');
            ctx.fillStyle = valGradient;
            ctx.fillRect(0, 0, width, height);
        }



        // Update knob position
        // function updateFromValueSliderKnobPosition() {
        //     const knobPosition = (hue / 360) * hueSlider.offsetWidth;
        //     hueKnob.style.left = `${knobPosition - hueKnob.offsetWidth / 2}px`;
        // }

        //mouse pos
        //->knob pos
        //->hue, saturation, value
        function updateFromMouseposSliderKnobPosition(event) {
            const rect = hueSlider.getBoundingClientRect();
            let x = event.clientX - rect.left;
            x = Math.max(0, Math.min(x, colorCanvas.width));
            hueKnob.style.left = `${x}px`;
        }
        function updateFromMouseposCanvasKnobPosition(event) {
            const rect = colorCanvas.getBoundingClientRect();
            let x = event.clientX - rect.left;
            let y = event.clientY - rect.top;

            // x, yの値をキャンバスの範囲内に制限
            x = Math.max(0, Math.min(x, colorCanvas.width));
            y = Math.max(0, Math.min(y, colorCanvas.height));

            // ノブの位置を更新
            canvasKnob.style.left = `${x}px`;
            canvasKnob.style.top = `${y}px`;
        }

        //mouse pos
        //->hue, saturation, value
        //->knob pos
        function updateHueFromMousepos(event) {
            const rect = hueSlider.getBoundingClientRect();
            const x = Math.max(0, Math.min(event.clientX - rect.left, hueSlider.clientWidth));
            hue = (x / hueSlider.clientWidth) * 360;
        }
        function updateSaturationAndValueFromMousepos(event) {
            const rect = colorCanvas.getBoundingClientRect();
            let x = event.clientX - rect.left;
            let y = event.clientY - rect.top;

            // x, yの値をキャンバスの範囲内に制限
            x = Math.max(0, Math.min(x, colorCanvas.width));
            y = Math.max(0, Math.min(y, colorCanvas.height));

            // キャンバスの実際の描画サイズを使用して彩度と明度を計算
            saturation = (x / colorCanvas.width) * 100;
            value = 100 - (y / colorCanvas.height) * 100;
        }


        //->hue, saturation, value
        //->knob pos
        function initKnobPositionFromValue() {
            const knobPosition = (hue / 360) * hueSlider.offsetWidth;
            // スライダーノブの`top`位置を調整して、はみ出さないようにします。
            hueKnob.style.left = `${knobPosition - hueKnob.offsetWidth / 2}px`;
            hueKnob.style.top = `0px`; // トラックの上部に合わせる
        }
        function initCanvasKnobPositionFromValue() {
            // 彩度と明度が100%の場合、ノブは右下隅に配置されます。
            const xPosition = colorCanvas.clientLeft + (saturation / 100) * colorCanvas.width;
            const yPosition = colorCanvas.clientTop + (1-(value / 100)) * colorCanvas.height;
            canvasKnob.style.left = `${xPosition}px`;
            canvasKnob.style.top = `${yPosition}px`;
        }
        // Other event listeners and functions for RGB inputs and canvas interaction
        // Utility functions
        function HSVtoRGB(h, s, v) {
            let r, g, b, i, f, p, q, t;
            if (h && s === undefined && v === undefined) {
                s = h.s, v = h.v, h = h.h;
            }
            i = Math.floor(h * 6);
            f = h * 6 - i;
            p = v * (1 - s);
            q = v * (1 - f * s);
            t = v * (1 - (1 - f) * s);
            switch (i % 6) {
                case 0: r = v, g = t, b = p; break;
                case 1: r = q, g = v, b = p; break;
                case 2: r = p, g = v, b = t; break;
                case 3: r = p, g = q, b = v; break;
                case 4: r = t, g = p, b = v; break;
                case 5: r = v, g = p, b = q; break;
            }
            return {
                r: Math.round(r * 255),
                g: Math.round(g * 255),
                b: Math.round(b * 255)
            };
        }

        function RGBtoHSV(r, g, b) {
            r /= 255, g /= 255, b /= 255;
            let max = Math.max(r, g, b), min = Math.min(r, g, b);
            let h, s, v = max;

            let d = max - min;
            s = max == 0 ? 0 : d / max;

            if (max == min) {
                h = 0; // achromatic
            } else {
                switch (max) {
                    case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                    case g: h = (b - r) / d + 2; break;
                    case b: h = (r - g) / d + 4; break;
                }
                h /= 6;
            }
            return { h: h * 360, s: s * 100, v: v * 100 };
        }

        document.addEventListener('DOMContentLoaded', function () {
            updateColorCanvas();
            updateColorPreview();
            initKnobPositionFromValue(); // hueKnobの初期位置を設定
            initCanvasKnobPositionFromValue(); // canvasKnobの初期位置を設定
        });



    </script>
</body>

</html>



ChatGPTくんの作ったグラデは
x方向のグラデとy方向のグラデを重ねる。

彩度:右が100

            // Draw saturation gradient
            const satGradient = ctx.createLinearGradient(0, 0, width, 0);
            satGradient.addColorStop(0, 'white');
            satGradient.addColorStop(1, `hsl(${hue}, 100%, 50%)`);
            ctx.fillStyle = satGradient;
            ctx.fillRect(0, 0, width, height);

明度:上が100

            // Draw value gradient
            const valGradient = ctx.createLinearGradient(0, 0, 0, height);
            valGradient.addColorStop(0, 'rgba(0,0,0,0)');
            valGradient.addColorStop(1, 'black');
            ctx.fillStyle = valGradient;
            ctx.fillRect(0, 0, width, height);


Knobのずれが気になるなら以下のように適当に変更を加える。

        .sliderKnob {
            position: absolute;
            width: 10px;
            height: 20px;
            background: white;
            border: 1px solid #000;
            cursor: pointer;
            left: 0;
            z-index: 2;
        }

        .canvasKnob {
            position: absolute;
            width: 10px;
            height: 10px;
            background: white;
            border: 1px solid #000;
            border-radius: 50%;
            cursor: pointer;
            transform: translate(-50%, -50%);
        }

負の遺産

色をクラス化したもの。

ミスったところ
ColorクラスというかColorオブジェクトはrgbベースだが
カラーピッカーは内部的にhsvを用いている。
そのためイベント発火毎にRGBtoHSVを再計算する必要があり、
あまりにうす汚い。

やれること
HSVベースのカラークラス、あるいは
RGBおよびHSVを常に内部的に更新するクラスを新設する。
あるいはcolor.hなどのプロパティ起動時に遅延で計算するクラスを仕立ててしまうやり方。
→しかし
やはりColorクラスはrgb+各種変換メソッドの方が軽めでしっくりくる。

よってColorクラスはrgbベースのままで
ピッカーに組み込まれるでなく、
ピッカーは自身の内部データhsvを入出力時にColorクラスないしオブジェクトで変換するのが良い。

以下、組み込んで失敗したもの。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <title>Complete Color Picker</title>
    <style>
        body {
            user-select: none;
        }

        .slider-container {
            position: relative;
            width: 300px;
            margin-bottom: 5px;
        }

        .hSlider {
            width: 100%;
            height: 20px;
            background: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red);
            position: relative;
        }

        .sliderKnob {
            position: absolute;
            width: 10px;
            height: 20px;
            background: white;
            border: 1px solid #000;
            cursor: pointer;
            left: 0;
            z-index: 2;
        }

        .canvas-container {
            position: relative;
            width: 300px;
            height: 150px;
            margin-bottom: 5px;
        }

        #colorCanvas {
            width: 300px;
            height: 150px;
            border: 1px solid #000;
            position: relative;
            cursor: crosshair;
        }

        .canvasKnob {
            position: absolute;
            width: 10px;
            height: 10px;
            background: white;
            border: 1px solid #000;
            border-radius: 50%;
            cursor: pointer;
            transform: translate(-50%, -50%);
        }

        #colorPreview {
            width: 50px;
            height: 50px;
            border: 1px solid #000;
            margin-top: 5px;
        }

        .value-input {
            width: 40px;
        }

        .text-output {
            width: 120px;
        }
    </style>
</head>

<body>
    <div id="hueSliderContainer" class="slider-container">
        <div id="hueSlider" class="hSlider"></div>
        <div id="hueSliderKnob" class="sliderKnob"></div>
    </div>

    <div id="SVCanvasContainer" class="canvas-container">
        <canvas id="SVCanvas" width="300" height="150"></canvas>
        <div id="svCanvasKnob" class="canvasKnob"></div>
    </div>


    <div id="colorPreview"></div>
    <div>
        <label>H <input type="number" min="0" max="360" value="0" id="hValue" class="value-input"></label>
        <label>S <input type="number" min="0" max="100" value="100" id="sValue" class="value-input"></label>
        <label>V <input type="number" min="0" max="100" value="100" id="vValue" class="value-input"></label>
    </div>
    <div>
        <label>R <input type="number" min="0" max="255" value="255" id="rValue" class="value-input"></label>
        <label>G <input type="number" min="0" max="255" value="0" id="gValue" class="value-input"></label>
        <label>B <input type="number" min="0" max="255" value="0" id="bValue" class="value-input"></label>
    </div>

    <textarea class="text-output"></textarea>

    <script>

        class Color {
            constructor(r, g, b, a = 255) {
                this.r = r;
                this.g = g;
                this.b = b;
                this.a = a;

                this.clip();
            }

            clip() {
                if (this.r < 0) { this.r = 0; }
                if (this.r > 255) { this.r = 255; }
                if (this.g < 0) { this.g = 0; }
                if (this.g > 255) { this.g = 255; }
                if (this.b < 0) { this.b = 0; }
                if (this.b > 255) { this.b = 255; }
                if (this.a < 0) { this.a = 0; }
                if (this.a > 255) { this.a = 255; }
            }

            static fromHSV(h,s,v){
                return Color.fromColor(Color.HSVtoRGB(h,s,v));
            }
            
            static fromColor(c) {
                if(!c.a){
                    return new Color(c.r, c.g, c.b, c.a);
                }
                else{
                    return new Color(c.r, c.g, c.b);
                }
            }

            toHexString() {
                return `#${this.r.toString(16).padStart(2, '0')}${this.g.toString(16).padStart(2, '0')}${this.b.toString(16).padStart(2, '0')}`;
            }

            toHSV() {
                return Color.RGBtoHSV(this.r, this.g, this.b);
            }

            //aは0-255から0-100に変換
            toHSVA() {
                const hsv = this.toHSV();
                let aa = this.a / 255 * 100;
                if (aa < 0) { aa = 0; }
                if (aa > 100) { aa = 100; }
                return { ...hsv, a: aa };
            }

            //入力0-255を0-1に正規化, 出力は0-360, 0-100, 0-100
            static RGBtoHSV(r, g, b) {
                r /= 255; g /= 255; b /= 255;
                const max = Math.max(r, g, b);
                const min = Math.min(r, g, b);
                let h, s, v = max;

                const diff = max - min;
                s = max === 0 ? 0 : diff / max;

                if (max === min) {
                    h = 0; // achromatic
                } else {
                    switch (max) {
                        case r:
                            h = (g - b) / diff + (g < b ? 6 : 0);
                            break;
                        case g:
                            h = (b - r) / diff + 2;
                            break;
                        case b:
                            h = (r - g) / diff + 4;
                            break;
                    }
                    h /= 6;
                }

                // hの値が0から1の間で返ってくるので、0から360の間で取得するために360を掛けます
                h = Math.round(h * 360);
                // s, vは0から1の間で返ってくるので、パーセンテージとして扱いたい場合は100を掛けます
                s = Math.round(s * 100);
                v = Math.round(v * 100);

                return { h, s, v };
            }

            //aは0-255から0-100に変換
            static RGBAtoHSVA(r, g, b, a) {
                const hsv = Color.RGBtoHSV(r, g, b);
                let aa = a / 255 * 100;
                if (aa < 0) { aa = 0; }
                if (aa > 100) { aa = 100; }
                return { ...hsv, a: aa };
            }

            static HSVtoRGB(h, s, v) {
                h /= 360;
                s /= 100;
                v /= 100;

                let r, g, b;

                let i = Math.floor(h * 6);
                let f = h * 6 - i;
                let p = v * (1 - s);
                let q = v * (1 - f * s);
                let t = v * (1 - (1 - f) * s);

                switch (i % 6) {
                    case 0: r = v, g = t, b = p; break;
                    case 1: r = q, g = v, b = p; break;
                    case 2: r = p, g = v, b = t; break;
                    case 3: r = p, g = q, b = v; break;
                    case 4: r = t, g = p, b = v; break;
                    case 5: r = v, g = p, b = q; break;
                }

                r = Math.round(r * 255);
                g = Math.round(g * 255);
                b = Math.round(b * 255);

                return { r, g, b };
            }

            //aは0-100から0-255に変換
            static HSVAtoRGBA(h, s, v, a) {
                const rgb = Color.HSVtoRGB(h, s, v);
                let aa = 255 * a / 100;
                if (aa < 0) { aa = 0; }
                if (aa > 255) { aa = 255; }
                return { ...rgb, a: aa };
            }
        }//Color

        // Initial values
        //->knob position

        let hue = 0,
            saturation = 100,
            value = 100;

        let currentColor = Color.fromHSV(0,100,100);

        // Select elements
        const hueSlider = document.getElementById('hueSlider');
        const hueKnob = document.getElementById('hueSliderKnob');
        const colorCanvas = document.getElementById('SVCanvas');
        const canvasKnob = document.getElementById('svCanvasKnob');
        const colorPreview = document.getElementById('colorPreview');

        const hValue = document.getElementById('hValue');
        const sValue = document.getElementById('sValue');
        const vValue = document.getElementById('vValue');
        const rValue = document.getElementById('rValue');
        const gValue = document.getElementById('gValue');
        const bValue = document.getElementById('bValue');


        // Event listeners
        let isDraggingSlider = false;
        let isDraggingCanvas = false;

        // Event listeners for hueKnob
        hueKnob.addEventListener('mousedown', function (event) {
            isDraggingSlider = true;
        });
        document.addEventListener('mousemove', function (event) {
            if (isDraggingSlider) {
                updateHueFromMousepos(event);
                updateSliderKnobPositionFromMousepos(event);
                updateColorCanvas();
                updateColorPreview();
            }
        });
        document.addEventListener('mouseup', function () {
            isDraggingSlider = false;
        });

        canvasKnob.addEventListener('mousedown', function (event) {
            isDraggingCanvas = true;
        });
        document.addEventListener('mousemove', function (event) {
            if (isDraggingCanvas) {
                updateSaturationAndValueFromMousepos(event);
                updateCanvasKnobPositionFromMousepos(event);
                updateColorCanvas();
                updateColorPreview();
            }
        });
        document.addEventListener('mouseup', function () {
            isDraggingCanvas = false;
        });

        // From value
        function updateColorPreview() {
            //const rgb = HSVtoRGB(hue / 360, saturation / 100, value / 100);
            const rgb = currentColor;       
            const hsv = currentColor.toHSV();
            colorPreview.style.backgroundColor = `rgb(${rgb.r},${rgb.g},${rgb.b})`;
            rValue.value = rgb.r;
            gValue.value = rgb.g;
            bValue.value = rgb.b;
            // hValue.value = Math.round(hue);
            // sValue.value = Math.round(saturation);
            // vValue.value = Math.round(value);
            hValue.value = Math.round(hsv.h);
            sValue.value = Math.round(hsv.s);
            vValue.value = Math.round(hsv.v);
            
        }

        // From value
        function updateColorCanvas() {
            const hsv = currentColor.toHSV();

            //console.log(currentColor);

            const ctx = colorCanvas.getContext('2d');
            const width = colorCanvas.width;
            const height = colorCanvas.height;
            ctx.clearRect(0, 0, width, height);

            // // Draw saturation gradient
            const satGradient = ctx.createLinearGradient(0, 0, width, 0);
            satGradient.addColorStop(0, 'white');
            satGradient.addColorStop(1, `hsl(${hsv.h}, 100%, 50%)`);
            ctx.fillStyle = satGradient;
            ctx.fillRect(0, 0, width, height);

            // Draw value gradient
            const valGradient = ctx.createLinearGradient(0, 0, 0, height);
            valGradient.addColorStop(0, 'rgba(0,0,0,0)');
            valGradient.addColorStop(1, 'black');
            ctx.fillStyle = valGradient;
            ctx.fillRect(0, 0, width, height);
        }



        // Update knob position
        // function updateFromValueSliderKnobPosition() {
        //     const knobPosition = (hue / 360) * hueSlider.offsetWidth;
        //     hueKnob.style.left = `${knobPosition - hueKnob.offsetWidth / 2}px`;
        // }

        //mouse pos
        //->knob pos
        //->hue, saturation, value
        function updateSliderKnobPositionFromMousepos(event) {
            const rect = hueSlider.getBoundingClientRect();
            let x = event.clientX - rect.left;
            x = Math.max(0, Math.min(x, colorCanvas.width));
            hueKnob.style.left = `${x}px`;
        }
        function updateCanvasKnobPositionFromMousepos(event) {
            const rect = colorCanvas.getBoundingClientRect();
            let x = event.clientX - rect.left;
            let y = event.clientY - rect.top;
            x = Math.max(0, Math.min(x, colorCanvas.width));
            y = Math.max(0, Math.min(y, colorCanvas.height));
            canvasKnob.style.left = `${x}px`;
            canvasKnob.style.top = `${y}px`;
        }

        //mouse pos
        //->hue, saturation, value
        //->knob pos
        function updateHueFromMousepos(event) {
            const hsv = currentColor.toHSV();
            const rect = hueSlider.getBoundingClientRect();
            const x = Math.max(0, Math.min(event.clientX - rect.left, hueSlider.clientWidth));
            const h = (x / hueSlider.clientWidth) * 360;
            currentColor = Color.fromHSV(h, hsv.s, hsv.v);
        }
        function updateSaturationAndValueFromMousepos(event) {
            const hsv = currentColor.toHSV();
            const rect = colorCanvas.getBoundingClientRect();
            let x = event.clientX - rect.left;
            let y = event.clientY - rect.top;

            x = Math.max(0, Math.min(x, colorCanvas.width));
            y = Math.max(0, Math.min(y, colorCanvas.height));

            const s = (x / colorCanvas.width) * 100;
            const v = 100 - (y / colorCanvas.height) * 100;

            currentColor = Color.fromHSV(hsv.h, s, v);
        }


        //->hue, saturation, value
        //->knob pos
        function initKnobPositionFromValue() {
            const hsv = currentColor.toHSV();
            const knobPosition = (hsv.h / 360) * hueSlider.offsetWidth;
            hueKnob.style.left = `${knobPosition - hueKnob.offsetWidth / 2}px`;
            hueKnob.style.top = `0px`;
        }
        function initCanvasKnobPositionFromValue() {
            const hsv = currentColor.toHSV();
            const xPosition = colorCanvas.clientLeft + (hsv.s / 100) * colorCanvas.width;
            const yPosition = colorCanvas.clientTop + (1 - (hsv.v / 100)) * colorCanvas.height;
            canvasKnob.style.left = `${xPosition}px`;
            canvasKnob.style.top = `${yPosition}px`;
        }
        // Other event listeners and functions for RGB inputs and canvas interaction
        // Utility functions
        // function HSVtoRGB(h, s, v) {
        //     let r, g, b, i, f, p, q, t;
        //     if (h && s === undefined && v === undefined) {
        //         s = h.s, v = h.v, h = h.h;
        //     }
        //     i = Math.floor(h * 6);
        //     f = h * 6 - i;
        //     p = v * (1 - s);
        //     q = v * (1 - f * s);
        //     t = v * (1 - (1 - f) * s);
        //     switch (i % 6) {
        //         case 0: r = v, g = t, b = p; break;
        //         case 1: r = q, g = v, b = p; break;
        //         case 2: r = p, g = v, b = t; break;
        //         case 3: r = p, g = q, b = v; break;
        //         case 4: r = t, g = p, b = v; break;
        //         case 5: r = v, g = p, b = q; break;
        //     }
        //     return {
        //         r: Math.round(r * 255),
        //         g: Math.round(g * 255),
        //         b: Math.round(b * 255)
        //     };
        // }

        // function RGBtoHSV(r, g, b) {
        //     r /= 255, g /= 255, b /= 255;
        //     let max = Math.max(r, g, b), min = Math.min(r, g, b);
        //     let h, s, v = max;

        //     let d = max - min;
        //     s = max == 0 ? 0 : d / max;

        //     if (max == min) {
        //         h = 0; // achromatic
        //     } else {
        //         switch (max) {
        //             case r: h = (g - b) / d + (g < b ? 6 : 0); break;
        //             case g: h = (b - r) / d + 2; break;
        //             case b: h = (r - g) / d + 4; break;
        //         }
        //         h /= 6;
        //     }
        //     return { h: h * 360, s: s * 100, v: v * 100 };
        // }

        document.addEventListener('DOMContentLoaded', function () {
            updateColorCanvas();
            updateColorPreview();
            initKnobPositionFromValue();
            initCanvasKnobPositionFromValue();
        });



    </script>
</body>

</html>

全部JavaScript

ただしChatGPTくんが横着したため、rgb,hsvの値を
表示するボックスがなくなった。

InputFIeldなし

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Complete Color Picker</title>
    <style>
        body {
            user-select: none;
        }

        .slider-container {
            position: relative;
            width: 300px;
            margin-bottom: 5px;
        }

        .slider {
            width: 100%;
            height: 20px;
            background: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red);
            position: relative;
        }

        .sliderKnob {
            position: absolute;
            width: 10px;
            height: 20px;
            background: white;
            border: 1px solid #000;
            cursor: pointer;
            left: 0;
            z-index: 2;
        }

        .canvas-container {
            position: relative;
            width: 300px;
            height: 150px;
            margin-bottom: 5px;
        }

        #colorCanvas {
            width: 300px;
            height: 150px;
            border: 1px solid #000;
            position: relative;
            cursor: crosshair;
        }

        .canvasKnob {
            position: absolute;
            width: 10px;
            height: 10px;
            background: white;
            border: 1px solid #000;
            border-radius: 50%;
            cursor: pointer;
            transform: translate(-50%, -50%);
        }

        #colorPreview {
            width: 50px;
            height: 50px;
            border: 1px solid #000;
            margin-top: 5px;
        }

        .value-input {
            width: 40px;
        }
    </style>
</head>

<body>
    <div id="hueSliderContainer" class="slider-container">
        <div id="hueSlider" class="slider"></div>
        <div id="hueSliderKnob" class="sliderKnob"></div>
    </div>

    <div id="SVCanvasContainer" class="canvas-container">
        <canvas id="SVCanvas" width="300" height="150"></canvas>
        <div id="svCanvasKnob" class="canvasKnob"></div> <!-- IDからクラスに変更 -->
    </div>


    <div id="colorPreview"></div>
    <div>
        <label>H <input type="number" min="0" max="360" value="0" id="hValue" class="value-input"></label>
        <label>S <input type="number" min="0" max="100" value="100" id="sValue" class="value-input"></label>
        <label>V <input type="number" min="0" max="100" value="100" id="vValue" class="value-input"></label>
    </div>
    <div>
        <label>R <input type="number" min="0" max="255" value="255" id="rValue" class="value-input"></label>
        <label>G <input type="number" min="0" max="255" value="0" id="gValue" class="value-input"></label>
        <label>B <input type="number" min="0" max="255" value="0" id="bValue" class="value-input"></label>
    </div>

    <script>

        // Initial values
        //->knob position

        let hue = 0,
            saturation = 100,
            value = 100;

        // Select elements
        const hueSlider = document.getElementById('hueSlider');
        const hueKnob = document.getElementById('hueSliderKnob');
        const colorCanvas = document.getElementById('SVCanvas');
        const canvasKnob = document.getElementById('svCanvasKnob');
        const colorPreview = document.getElementById('colorPreview');

        const hValue = document.getElementById('hValue');
        const sValue = document.getElementById('sValue');
        const vValue = document.getElementById('vValue');
        const rValue = document.getElementById('rValue');
        const gValue = document.getElementById('gValue');
        const bValue = document.getElementById('bValue');


        // Event listeners
        let isDraggingSlider = false;
        let isDraggingCanvas = false;

        // Event listeners for hueKnob
        hueKnob.addEventListener('mousedown', function (event) {
            isDraggingSlider = true;
        });
        document.addEventListener('mousemove', function (event) {
            if (isDraggingSlider) {
                updateHueFromMousepos(event);
                updateFromMouseposSliderKnobPosition(event);
                updateColorCanvas();
                updateColorPreview();
            }
        });
        document.addEventListener('mouseup', function () {
            isDraggingSlider = false;
        });




        canvasKnob.addEventListener('mousedown', function (event) {
            isDraggingCanvas = true;            
        });
        document.addEventListener('mousemove', function (event) {
            if (isDraggingCanvas) {
                updateSaturationAndValueFromMousepos(event); // s,vの値を更新
                updateFromMouseposCanvasKnobPosition(event); // ドラッグ中にノブの位置を更新
                updateColorCanvas();
                updateColorPreview();                
            }
        });
        document.addEventListener('mouseup', function () {
            isDraggingCanvas = false;
        });



        // From value
        function updateColorPreview() {
            const rgb = HSVtoRGB(hue / 360, saturation / 100, value / 100);
            colorPreview.style.backgroundColor = `rgb(${rgb.r},${rgb.g},${rgb.b})`;
            rValue.value = rgb.r;
            gValue.value = rgb.g;
            bValue.value = rgb.b;
            hValue.value = Math.round(hue);
            sValue.value = Math.round(saturation);
            vValue.value = Math.round(value);
        }

        // From value
        function updateColorCanvas() {
            const ctx = colorCanvas.getContext('2d');
            const width = colorCanvas.width;
            const height = colorCanvas.height;
            ctx.clearRect(0, 0, width, height);

            // Draw saturation gradient
            const satGradient = ctx.createLinearGradient(0, 0, width, 0);
            satGradient.addColorStop(0, 'white');
            satGradient.addColorStop(1, `hsl(${hue}, 100%, 50%)`);
            ctx.fillStyle = satGradient;
            ctx.fillRect(0, 0, width, height);

            // Draw value gradient
            const valGradient = ctx.createLinearGradient(0, 0, 0, height);
            valGradient.addColorStop(0, 'rgba(0,0,0,0)');
            valGradient.addColorStop(1, 'black');
            ctx.fillStyle = valGradient;
            ctx.fillRect(0, 0, width, height);
        }



        // Update knob position
        // function updateFromValueSliderKnobPosition() {
        //     const knobPosition = (hue / 360) * hueSlider.offsetWidth;
        //     hueKnob.style.left = `${knobPosition - hueKnob.offsetWidth / 2}px`;
        // }

        //mouse pos
        //->knob pos
        //->hue, saturation, value
        function updateFromMouseposSliderKnobPosition(event) {
            const rect = hueSlider.getBoundingClientRect();
            let x = event.clientX - rect.left;
            x = Math.max(0, Math.min(x, colorCanvas.width));
            hueKnob.style.left = `${x}px`;
        }
        function updateFromMouseposCanvasKnobPosition(event) {
            const rect = colorCanvas.getBoundingClientRect();
            let x = event.clientX - rect.left;
            let y = event.clientY - rect.top;

            // x, yの値をキャンバスの範囲内に制限
            x = Math.max(0, Math.min(x, colorCanvas.width));
            y = Math.max(0, Math.min(y, colorCanvas.height));

            // ノブの位置を更新
            canvasKnob.style.left = `${x}px`;
            canvasKnob.style.top = `${y}px`;
        }

        //mouse pos
        //->hue, saturation, value
        //->knob pos
        function updateHueFromMousepos(event) {
            const rect = hueSlider.getBoundingClientRect();
            const x = Math.max(0, Math.min(event.clientX - rect.left, hueSlider.clientWidth));
            hue = (x / hueSlider.clientWidth) * 360;
        }
        function updateSaturationAndValueFromMousepos(event) {
            const rect = colorCanvas.getBoundingClientRect();
            let x = event.clientX - rect.left;
            let y = event.clientY - rect.top;

            // x, yの値をキャンバスの範囲内に制限
            x = Math.max(0, Math.min(x, colorCanvas.width));
            y = Math.max(0, Math.min(y, colorCanvas.height));

            // キャンバスの実際の描画サイズを使用して彩度と明度を計算
            saturation = (x / colorCanvas.width) * 100;
            value = 100 - (y / colorCanvas.height) * 100;
        }


        //->hue, saturation, value
        //->knob pos
        function initKnobPositionFromValue() {
            const knobPosition = (hue / 360) * hueSlider.offsetWidth;
            // スライダーノブの`top`位置を調整して、はみ出さないようにします。
            hueKnob.style.left = `${knobPosition - hueKnob.offsetWidth / 2}px`;
            hueKnob.style.top = `0px`; // トラックの上部に合わせる
        }
        function initCanvasKnobPositionFromValue() {
            // 彩度と明度が100%の場合、ノブは右下隅に配置されます。
            const xPosition = colorCanvas.clientLeft + (saturation / 100) * colorCanvas.width;
            const yPosition = colorCanvas.clientTop + (1-(value / 100)) * colorCanvas.height;
            canvasKnob.style.left = `${xPosition}px`;
            canvasKnob.style.top = `${yPosition}px`;
        }
        // Other event listeners and functions for RGB inputs and canvas interaction
        // Utility functions
        function HSVtoRGB(h, s, v) {
            let r, g, b, i, f, p, q, t;
            if (h && s === undefined && v === undefined) {
                s = h.s, v = h.v, h = h.h;
            }
            i = Math.floor(h * 6);
            f = h * 6 - i;
            p = v * (1 - s);
            q = v * (1 - f * s);
            t = v * (1 - (1 - f) * s);
            switch (i % 6) {
                case 0: r = v, g = t, b = p; break;
                case 1: r = q, g = v, b = p; break;
                case 2: r = p, g = v, b = t; break;
                case 3: r = p, g = q, b = v; break;
                case 4: r = t, g = p, b = v; break;
                case 5: r = v, g = p, b = q; break;
            }
            return {
                r: Math.round(r * 255),
                g: Math.round(g * 255),
                b: Math.round(b * 255)
            };
        }

        function RGBtoHSV(r, g, b) {
            r /= 255, g /= 255, b /= 255;
            let max = Math.max(r, g, b), min = Math.min(r, g, b);
            let h, s, v = max;

            let d = max - min;
            s = max == 0 ? 0 : d / max;

            if (max == min) {
                h = 0; // achromatic
            } else {
                switch (max) {
                    case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                    case g: h = (b - r) / d + 2; break;
                    case b: h = (r - g) / d + 4; break;
                }
                h /= 6;
            }
            return { h: h * 360, s: s * 100, v: v * 100 };
        }

        document.addEventListener('DOMContentLoaded', function () {
            updateColorCanvas();
            updateColorPreview();
            initKnobPositionFromValue(); // hueKnobの初期位置を設定
            initCanvasKnobPositionFromValue(); // canvasKnobの初期位置を設定
        });



    </script>
</body>

</html>

InputField追加

ほぼ完成

追加してもよいのは
Input要素からの入力を処理したり、
ノブを経由せずにスライダーやキャンパスをクリックで直接その色を取得するような機能。

個人的にやるべきはカラークラスの入出力およびJSON形式の入出力とコピペである。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Complete Color Picker</title>
</head>

<body>
    <script>
        class ColorPicker {
            constructor(container) {
                this.container = container;
                this.createColorPicker();
                this.initEventListeners();

                this.updateColorCanvas();
                this.updateColorPreview();
                this.initCanvasKnobPositionFromValue();
            }

            createColorPicker() {
                // Create HTML elements
                this.hueSliderContainer = this.createElement('div', 'slider-container');
                this.hueSlider = this.createElement('div', 'slider');
                this.hueKnob = this.createElement('div', 'sliderKnob');

                this.SVCanvasContainer = this.createElement('div', 'canvas-container');
                this.colorCanvas = this.createElement('canvas', null, { id: 'SVCanvas', width: 300, height: 150 });
                this.canvasKnob = this.createElement('div', 'canvasKnob');

                this.colorPreview = this.createElement('div', null, { id: 'colorPreview' });

                // Append elements
                this.hueSliderContainer.appendChild(this.hueSlider);
                this.hueSliderContainer.appendChild(this.hueKnob);
                this.SVCanvasContainer.appendChild(this.colorCanvas);
                this.SVCanvasContainer.appendChild(this.canvasKnob);
                this.container.appendChild(this.hueSliderContainer);
                this.container.appendChild(this.SVCanvasContainer);
                this.container.appendChild(this.colorPreview);

                // Create HSV input fields
                const hsvContainer = this.createElement('div');
                this.hValue = this.createInputField('H', 'hValue', 0, 360);
                this.sValue = this.createInputField('S', 'sValue', 0, 100, 100);
                this.vValue = this.createInputField('V', 'vValue', 0, 100, 100);
                hsvContainer.appendChild(this.hValue.label);
                hsvContainer.appendChild(this.sValue.label);
                hsvContainer.appendChild(this.vValue.label);

                // Create RGB input fields
                const rgbContainer = this.createElement('div');
                this.rValue = this.createInputField('R', 'rValue', 0, 255, 255);
                this.gValue = this.createInputField('G', 'gValue', 0, 255);
                this.bValue = this.createInputField('B', 'bValue', 0, 255);
                rgbContainer.appendChild(this.rValue.label);
                rgbContainer.appendChild(this.gValue.label);
                rgbContainer.appendChild(this.bValue.label);

                // Append containers
                this.container.appendChild(hsvContainer);
                this.container.appendChild(rgbContainer);

                // Create CSS Styles
                this.createStyles();

                // Initial values
                this.hue = 0;
                this.saturation = 100;
                this.value = 100;
            }

            createElement(tag, className = null, attributes = {}) {
                const element = document.createElement(tag);
                if (className) element.className = className;
                Object.keys(attributes).forEach(attr => element.setAttribute(attr, attributes[attr]));
                return element;
            }

            createInputField(name, id, min, max, value = 0) {
                const label = this.createElement('label');
                label.textContent = `${name} `;
                const input = this.createElement('input', 'value-input', { type: 'number', min: min, max: max, value: value, id: id });
                label.appendChild(input);
                return { label, input };
            }

            createStyles() {
                const style = document.createElement('style');
                style.textContent = `
                    body { user-select: none; }
                    .slider-container { position: relative; width: 300px; margin-bottom: 5px; }
                    .slider { width: 100%; height: 20px; background: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red); position: relative; }
                    .sliderKnob, .canvasKnob { position: absolute; width: 10px; height: 20px; background: white; border: 1px solid #000; cursor: pointer; top: 0; left: 0; z-index: 2; }
                    .canvas-container { position: relative; width: 300px; height: 150px; margin-bottom: 5px; }
                    #SVCanvas { width: 300px; height: 150px; border: 1px solid #000; position: relative; cursor: crosshair; }
                    .canvasKnob { height: 10px; border-radius: 50%; transform: translate(-50%, -50%); }
                    #colorPreview { width: 50px; height: 50px; border: 1px solid #000; margin-top: 5px; }
                `;
                document.head.appendChild(style);
            }

            initEventListeners() {
                this.hueKnob.addEventListener('mousedown', () => this.isDraggingSlider = true);
                this.canvasKnob.addEventListener('mousedown', () => this.isDraggingCanvas = true);

                document.addEventListener('mousemove', (event) => {
                    if (this.isDraggingSlider) {
                        this.updateHueFromMousepos(event);
                        this.updateSliderKnobPositionFromMousepos(event);
                        this.updateColorCanvas();
                        this.updateColorPreview();
                        this.updateInputField();
                    }

                    if (this.isDraggingCanvas) {
                        this.updateSaturationAndValueFromMousepos(event);
                        this.updateCanvasKnobPositionFromMousepos(event);
                        this.updateColorCanvas();
                        this.updateColorPreview();
                        this.updateInputField();
                    }
                });

                document.addEventListener('mouseup', () => {
                    this.isDraggingSlider = false;
                    this.isDraggingCanvas = false;
                });
            }

            updateColorPreview() {
                const rgb = this.HSVtoRGB(this.hue / 360, this.saturation / 100, this.value / 100);
                this.colorPreview.style.backgroundColor = `rgb(${rgb.r},${rgb.g},${rgb.b})`;
            }

            updateColorCanvas() {
                const ctx = this.colorCanvas.getContext('2d');
                const width = this.colorCanvas.width;
                const height = this.colorCanvas.height;
                ctx.clearRect(0, 0, width, height);

                const satGradient = ctx.createLinearGradient(0, 0, width, 0);
                satGradient.addColorStop(0, 'white');
                satGradient.addColorStop(1, `hsl(${this.hue}, 100%, 50%)`);
                ctx.fillStyle = satGradient;
                ctx.fillRect(0, 0, width, height);

                const valGradient = ctx.createLinearGradient(0, 0, 0, height);
                valGradient.addColorStop(0, 'rgba(0,0,0,0)');
                valGradient.addColorStop(1, 'black');
                ctx.fillStyle = valGradient;
                ctx.fillRect(0, 0, width, height);
            }

            updateInputField() {
                const rgb = this.HSVtoRGB(this.hue / 360, this.saturation / 100, this.value / 100);
                this.rValue.input.value = rgb.r;
                this.gValue.input.value = rgb.g;
                this.bValue.input.value = rgb.b;
                this.hValue.input.value = Math.round(this.hue);
                this.sValue.input.value = Math.round(this.saturation);
                this.vValue.input.value = Math.round(this.value);
            }

            updateSliderKnobPositionFromMousepos(event) {
                const rect = this.hueSlider.getBoundingClientRect();
                let x = event.clientX - rect.left;
                x = Math.max(0, Math.min(x, this.colorCanvas.width));
                this.hueKnob.style.left = `${x}px`;
            }

            updateCanvasKnobPositionFromMousepos(event) {
                const rect = this.colorCanvas.getBoundingClientRect();
                let x = event.clientX - rect.left;
                let y = event.clientY - rect.top;
                x = Math.max(0, Math.min(x, this.colorCanvas.width));
                y = Math.max(0, Math.min(y, this.colorCanvas.height));
                this.canvasKnob.style.left = `${x}px`;
                this.canvasKnob.style.top = `${y}px`;
            }

            updateHueFromMousepos(event) {
                const rect = this.hueSlider.getBoundingClientRect();
                const x = Math.max(0, Math.min(event.clientX - rect.left, this.hueSlider.clientWidth));
                this.hue = (x / this.hueSlider.clientWidth) * 360;
            }

            updateSaturationAndValueFromMousepos(event) {
                const rect = this.colorCanvas.getBoundingClientRect();
                let x = event.clientX - rect.left;
                let y = event.clientY - rect.top;
                x = Math.max(0, Math.min(x, this.colorCanvas.width));
                y = Math.max(0, Math.min(y, this.colorCanvas.height));
                this.saturation = (x / this.colorCanvas.width) * 100;
                this.value = 100 - (y / this.colorCanvas.height) * 100;
            }

            //->hue, saturation, value
            //->knob pos
            initKnobPositionFromValue() {
                const knobPosition = (hue / 360) * hueSlider.offsetWidth;
                hueKnob.style.left = `${knobPosition - hueKnob.offsetWidth / 2}px`;
            }

            initCanvasKnobPositionFromValue() {
                const xPosition = this.colorCanvas.clientLeft + (this.saturation / 100) * this.colorCanvas.width;
                const yPosition = this.colorCanvas.clientTop + (1 - (this.value / 100)) * this.colorCanvas.height;
                this.canvasKnob.style.left = `${xPosition}px`;
                this.canvasKnob.style.top = `${yPosition}px`;
            }

            HSVtoRGB(h, s, v) {
                let r, g, b, i, f, p, q, t;
                i = Math.floor(h * 6);
                f = h * 6 - i;
                p = v * (1 - s);
                q = v * (1 - f * s);
                t = v * (1 - (1 - f) * s);
                switch (i % 6) {
                    case 0: r = v, g = t, b = p; break;
                    case 1: r = q, g = v, b = p; break;
                    case 2: r = p, g = v, b = t; break;
                    case 3: r = p, g = q, b = v; break;
                    case 4: r = t, g = p, b = v; break;
                    case 5: r = v, g = p, b = q; break;
                }
                return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
            }
        }

        document.addEventListener('DOMContentLoaded', function () {
            const container = document.body;
            new ColorPicker(container);
        });
    </script>
</body>

</html>

この記事が気に入ったらサポートをしてみませんか?