LoginSignup
1
1

More than 1 year has passed since last update.

小道具:HSVによる色選択

Last updated at Posted at 2023-03-29

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

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

See the Pen HSV Color Selector by Ikiuo (@ikiuo) on CodePen.

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>
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1