小道具：HSVによる色選択

Canvas API と CanvasRenderingContext2D の勉強として作ってみました。

「HTML Color = #RRGGBB」をクリックすると、カラーコード "#RRGGBB" をクリップボードにコピーします。(CodePen のボタンはうまくいかないようです)

<p class="codepen" data-height="868" data-default-tab="result" data-slug-hash="PodgGQM" data-user="ikiuo" style="height: 868; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/ikiuo/pen/PodgGQM">
  HSV Color Selector</a> by Ikiuo (<a href="https://codepen.io/ikiuo">@ikiuo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


```html:HSVColorSelector.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>HSV Color Selector</title>
  </head>
  <body>

    <script>

     class HSVColorSelector {

         static objectId = 1;
         static defaultOption = {
             canvas: {
                 width: 200 + 40,
                 height: 200,
             },
             td_code: {
                 style: {
                     "text-align": "center",
                     "padding": "8px",
                 },
             },
             span_desc: { innerText: "HTML Color = " },
             span_code: { innerText: "#FFFFFF" },

             UI: {
                 margin: 8,
             },
         };

         static hsvToRgb(HSV) {
             const [iH, S, V] = HSV;
             if (!S)
                 return [V, V, V];
             const L = V - S * V;
             let H = iH % 360;
             if (H < 0)
                 H += 360;
             let CH = H % 120;
             const HR = ((CH > 60) ? 1 : 0);
             const HM = ((H / 120) << 1) + HR;
             if (HR)
                 CH = 120.0 - CH;
             const M = (CH / 60) * (V - L) + L;
             switch (HM) {
                 case 0: return [V, M, L];
                 case 1: return [M, V, L];
                 case 2: return [L, V, M];
                 case 3: return [L, M, V];
                 case 4: return [M, L, V];
                 case 5: return [V, L, M];
             };
             return null;
         };

         static rgbToColorString(rgb) {
             const [r, g, b] = rgb;
             const sr = `00${Math.trunc(r * 255).toString(16)}`.toUpperCase().slice(-2);
             const sg = `00${Math.trunc(g * 255).toString(16)}`.toUpperCase().slice(-2);
             const sb = `00${Math.trunc(b * 255).toString(16)}`.toUpperCase().slice(-2);
             return `#${sr}${sg}${sb}`;
         };

         static hsvToColorString(hsv) {
             const S = HSVColorSelector;
             return S.rgbToColorString(S.hsvToRgb(hsv));
         };

         static setAttributes(element, attributes) {
             if (element && attributes) {
                 const S = HSVColorSelector;
                 for (const [name, value] of Object.entries(attributes)) {
                     if (name == 'style')
                         S.setProperties(element, value);
                     else if (name == 'innerText')
                         element.innerText = value;
                     else
                         element.setAttribute(name, value);
                 };
             };
         };

         static setProperties(element, properties) {
             if (element && properties)
                 for (const [name, value] of Object.entries(properties))
                     element.style.setProperty(name, value);
         };

         static setOption(target, option) {
             if (!option)
                 return;
             const S = HSVColorSelector;
             for (const [name, value] of Object.entries(option)) {
                 const child = target[name];
                 if (typeof(child) === 'object')
                     S.setOption(child, value);
                 else if (typeof(value) == 'object')
                     S.setOption(target[name] = {}, value);
                 else
                     target[name] = value;
             }
         };

         static setClosedPath(ctx, position) {
             ctx.beginPath();
             position.forEach((v, i) => i
                                      ? ctx.lineTo(v[0], v[1])
                                      : ctx.moveTo(v[0], v[1]));
             ctx.closePath();
             return ctx;
         };

         constructor(parent, userOption) {
             const S = HSVColorSelector;
             const name = `_HSVColorSelector_Object_No_${S.objectId++}`;
             const option = userOption ?? {};

             this.S = S;
             this.objectName = name;
             this.namePrefix = this.objectName + '_';
             this.parent = parent;
             this.option = {};
             S.setOption(this.option, S.defaultOption);
             S.setOption(this.option, option);

             this.hsv = option.hsv ?? [0, 1, 1];
             this.onupdate = option.onupdate;

             this.initSelector();
         };

         getHTMLValue() {
             return this.htmlCode.innerText;
         };

         initSelector() {
             const S = this.S;

             this.nameTable = this.namePrefix + 'Table';
             this.nameTd = this.namePrefix + 'Td';
             this.nameTr = this.namePrefix + 'Tr';
             this.nameCanvas = this.namePrefix + 'Canvas';

             this.parent.innerHTML = [
                 `<table id="${this.nameTable}">`,
                 `<tr id="${this.nameTr}_HSV">`,
                 `<td id="${this.nameTd}_HSV">`,
                 `<canvas id="${this.nameCanvas}"></canvas>`,
                 '</td></tr>',
                 '</table>',
             ].flat().join('');

             const table = document.getElementById(this.nameTable);

             const tr_hsv = document.getElementById(this.nameTr+'_HSV');
             const td_hsv = document.getElementById(this.nameTd+'_HSV');
             const canvas = document.getElementById(this.nameCanvas);

             this.htmlTable = table;
             this.canvas = canvas;
             this.canvasParameter = {};

             S.setAttributes(table, this.option.table);
             S.setAttributes(tr_hsv, this.option.tr);
             S.setAttributes(tr_hsv, this.option.tr_hsv);
             S.setAttributes(td_hsv, this.option.td);
             S.setAttributes(td_hsv, this.option.td_hsv);
             S.setAttributes(canvas, this.option.canvas);

             this.initCanvasParameter();
             this.initHSV();
             this.initButton();

             this.setHSV(this.hsv);

             canvas.onmousedown = (e => this.mouseHSV(e));
             canvas.onmousemove = (e => this.mouseHSV(e));
         };

         initCanvasParameter() {
             const S = this.S;

             const canvas = this.canvas;
             const parameter = this.canvasParameter;

             const width = canvas.width;
             const height = canvas.height;
             const h_half = height >> 1;

             const option = this.option.UI;
             const margin = option.margin;
             const saturation = option.saturation;

             const hue_min = Math.trunc(margin * 6);
             const bar_min = Math.trunc(margin * 2);

             const sv_width = width - height;
             const bar_count = saturation ? 2 : 1;
             const bar_space = Math.max(4, Math.trunc(margin >> 1));
             const bar_width = Math.max(bar_min, (saturation ? ((sv_width - bar_space) >> 1) : sv_width));

             const hue_width = Math.max(hue_min, height);
             const hue_whalf = hue_width >> 1;
             const radius = hue_whalf - margin;
             const hue_top = h_half - radius;
             const hue_bottom = h_half + radius;

             let bhs = hue_top;
             let bhe = hue_bottom;
             if (height < (margin * 4)) {
                 bhs = height * 0.25;
                 bhe = height * 0.75;
             };
             const bhh = bhe - bhs;

             let bx = 0;
             if (true) {
                 parameter.hue = {
                     area: [bx, 0, hue_width, height],
                     center: [hue_whalf, h_half],
                     radius: radius,
                 };
                 bx += hue_width;
             };

             parameter.saturation = {}
             if (saturation) {
                 parameter.saturation.area = [bx, bhs, bar_width, bhh];
                 bx += bar_width;
                 bx += bar_space;
             };

             if (true) {
                 parameter.brightness = {
                     area: [bx, bhs, bar_width, bhh],
                 };
             };
         };

         initHSV() {
             const S = this.S;
             const canvas = this.canvas;
             const parameter = this.canvasParameter;
             const hparam = parameter.hue;
             const width = canvas.width;
             const height = canvas.height;

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

             const [sx, sy] = hparam.center;
             const radius = hparam.radius;

             for (let angle = 0; angle < 360; angle++) {
                 const [px, py] = this.degToPos(angle - 1, radius);
                 const [cx, cy] = this.degToPos(angle, radius);
                 const [nx, ny] = this.degToPos(angle + 1, radius);

                 const grad = ctx.createLinearGradient(sx, sy, sx + cx, sy + cy);
                 grad.addColorStop(0, "#FFFFFF");
                 grad.addColorStop(1, S.hsvToColorString([angle + 90, 1, 1]));

                 ctx.fillStyle = grad;
                 ctx.beginPath();
                 ctx.moveTo(sx, sy);
                 ctx.lineTo(sx + px, sy + py);
                 ctx.lineTo(sx + cx, sy + cy);
                 ctx.lineTo(sx + nx, sy + ny);
                 ctx.closePath();
                 ctx.fill();
             }

             parameter.image = ctx.getImageData(0, 0, width, height);
         };

         initButton() {
             this.codeButton = this.option.UI.button;
             if (!this.codeButton)
                 return;

             const S = this.S;
             this.nameButton = this.namePrefix + 'Button';
             this.nameHTMLDesc = this.namePrefix + 'HTMLDesc';
             this.nameHTMLCode = this.namePrefix + 'HTMLCode';

             this.htmlTable.insertAdjacentHTML(
                 'beforeend',
                 [`<tr id="${this.nameTr}_CODE">`,
                  `<td id="${this.nameTd}_CODE">`,
                  `<button id="${this.nameButton}">`,
                  `<span id="${this.nameHTMLDesc}"></span>`,
                  `<code id="${this.nameHTMLCode}"></code>`,
                  '</button></td></tr>'].join(''));

             const tr_code = document.getElementById(this.nameTr+'_CODE');
             const td_code = document.getElementById(this.nameTd+'_CODE');
             const button = document.getElementById(this.nameButton);
             const desc = document.getElementById(this.nameHTMLDesc);
             const code = document.getElementById(this.nameHTMLCode);

             this.htmlCodeBase = td_code;
             this.htmlCode = code;

             S.setAttributes(tr_code, this.option.tr);
             S.setAttributes(tr_code, this.option.tr_code);
             S.setAttributes(td_code, this.option.td);
             S.setAttributes(td_code, this.option.td_code);
             S.setAttributes(button, this.option.button);
             S.setAttributes(desc, this.option.span);
             S.setAttributes(desc, this.option.span_desc);
             S.setAttributes(code, this.option.span);
             S.setAttributes(code, this.option.span_code);

             button.onclick = (e => this.toClipboard(e));
         };

         radToPos(x, r) { return [Math.cos(x) * r, Math.sin(x) * r]; };
         degToPos(x, r) { return this.radToPos(x * Math.PI / 180, r); };
         static addPos(a, b) { return [a[0] + b[0], a[1] + b[1]]; };
         static rotPos(r, p) { return [p[0] * r[0] - p[1] * r[1],
                                       p[1] * r[0] + p[0] * r[1]]; };

         drawBar(ctx, area, shsv, ehsv, chsv, pos) {
             const S = this.S;
             const [ax, ay, aw, ah] = area;
             const px = Math.trunc(ax + aw / 2);
             const py = Math.trunc(ay + (1 - pos) * ah);
             const ex = ax + aw - 1;
             const ey = ay + ah - 1;
             const msize = Math.trunc(Math.max(4, aw / 8));

             ctx.fillStyle = "#000000";
             ctx.fillRect(ax, ay, aw, ah);

             const grad = ctx.createLinearGradient(ax, ay, ax, ey);
             grad.addColorStop(0, S.hsvToColorString(ehsv));
             grad.addColorStop(1, S.hsvToColorString(shsv));
             ctx.fillStyle = grad;
             ctx.fillRect(ax + 1, ay + 1, aw - 2, ah - 2);

             const rgb = S.hsvToRgb(chsv);
             const irgb = rgb.map(v => 1 - v);
             ctx.fillStyle = S.rgbToColorString(irgb) + "C0";
             S.setClosedPath(ctx, [
                 [px - msize, py - msize],
                 [px + msize, py - msize],
                 [px - msize, py + msize],
                 [px + msize, py + msize],
             ]).fill();
             S.setClosedPath(ctx, [
                 [ax + 1, py - msize],
                 [ax + 1 + msize, py],
                 [ax + 1, py + msize],
             ]).fill();
             S.setClosedPath(ctx, [
                 [ex, py - msize],
                 [ex - msize, py],
                 [ex, py + msize],
             ]).fill();
         };

         drawHSV() {
             const S = this.S;
             const canvas = this.canvas;
             const parameter = this.canvasParameter;
             const hparam = parameter.hue;
             const sparam = parameter.saturation;
             const vparam = parameter.brightness;

             const ctx = canvas.getContext('2d');
             ctx.putImageData(parameter.image, 0, 0);

             const [h, s, v] = this.hsv;
             {
                 const deg = h - 90;
                 const radius = hparam.radius;
                 const [sx, sy] = hparam.center;
                 const [px, py] = this.degToPos(deg, radius * s);

                 ctx.lineWidth = 2;
                 ctx.strokeStyle = "#00000060";
                 ctx.beginPath();
                 ctx.arc(sx + px, sy + py, 6, 0, 2 * Math.PI);
                 ctx.stroke();

                 if (this.option.UI.huepos) {
                     const rot = this.degToPos(deg, 1);
                     S.setClosedPath(ctx, [
                         S.addPos(hparam.center, S.rotPos(rot, [-6, -6])),
                         S.addPos(hparam.center, S.rotPos(rot, [-6, +6])),
                         S.addPos(hparam.center, S.rotPos(rot, [radius + 6, +6])),
                         S.addPos(hparam.center, S.rotPos(rot, [radius + 6, -6])),
                     ]).stroke();
                 };
             };

             if (sparam.area)
                 this.drawBar(ctx, sparam.area, [h, 0, 1], [h, 1, 1], [h, s, 1], s);
             this.drawBar(ctx, vparam.area, [h, s, 0], [h, s, 1], [h, s, v], v);
         };

         setHSV(hsv) {
             const S = this.S;
             const rgb = S.hsvToRgb(hsv);
             this.hsv = hsv.slice();
             this.rgb = rgb;
             this.drawHSV();
             this.setButton();
         };

         setButton() {
             if (!this.codeButton)
                 return;

             const S = this.S;
             const rgb_code = S.rgbToColorString(this.rgb);
             this.htmlCodeBase.style.setProperty("background-color", rgb_code)
             this.htmlCode.innerHTML = rgb_code;
         }

         static checkArea(area, x, y) {
             if (!area)
                 return false;
             const ox = x - area[0];
             const oy = y - area[1];
             return ((0 <= ox && ox < area[2]) &&
                     (0 <= oy && oy < area[3]));
         };

         mouseHSV(event) {
             if (!(event.buttons & 1))
                 return;

             const S = this.S;
             const x = event.offsetX;
             const y = event.offsetY;

             const canvas = this.canvas;
             const parameter = this.canvasParameter;

             let [h, s, v] = this.hsv;

             const hparam = parameter.hue;
             const sparam = parameter.saturation;
             const vparam = parameter.brightness;

             if (S.checkArea(hparam.area, x, y)) {
                 const [sx, sy] = hparam.center;
                 const radius = hparam.radius;
                 const mx = x - sx;
                 const my = y - sy;

                 h = 90 + 180 * Math.atan2(my, mx) / Math.PI;
                 if (h <   0) h += 360;
                 if (h > 360) h -= 360;

                 s = Math.sqrt(mx * mx + my * my) / radius;
                 if (s > 1.05) return;
                 if (s > 1) s = 1;

             } else if (S.checkArea(sparam.area, x, y)) {
                 s = Math.max(0, Math.min(1, 1 - (y - sparam.area[1]) / sparam.area[3]));
             } else if (S.checkArea(vparam.area, x, y)) {
                 v = Math.max(0, Math.min(1, 1 - (y - vparam.area[1]) / vparam.area[3]));
             }
             this.setHSV([h, s, v]);

             if (this.onupdate)
                 this.onupdate();
         };

         toClipboard() {
             navigator.clipboard.writeText(this.getHTMLValue()).then(()=>{}, ()=>{});
         };

     };

     window.onload = function() {
         document.body.insertAdjacentHTML(
             'beforeend',
             ['<table border="1">',
              '<tr id="ViewerSample1"></tr>',
              '<tr id="ViewerSample2"></tr>',
              '<tr id="ViewerSample3"></tr>',
              '</table>',
             ].join(''));


         for (let v = 1; v <= 3; v++) {
             const viewer = document.getElementById(`ViewerSample${v}`);
             for (let n = 1; n <= 3; n++) {
                 const hue = (v != 1);
                 const sat = (v != 1);
                 const but = (v != 3);
                 const option = { UI: { huepos:hue, saturation:sat, button:but } };
                 viewer.insertAdjacentHTML(
                     'beforeend', [
                         `<td>HSV色選択(${n}`,
                         `:<small>${sat?'彩度あり':'彩度なし'}:${but?'ボタンあり':'ボタンなし'}`,
                         `</small>)<br><div id="HSVColorSelector_${v}_${n}"></div></td>`,
                     ].join(''));
                 const selector = new HSVColorSelector(
                     document.getElementById(`HSVColorSelector_${v}_${n}`), option);
             };
         };
     };

    </script>
  </body>
</html>
```