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>