Color Picker 対アンミカ仕様 (草案)
コンピュータ内部において、「色」というのはRGB値で表現されます。黒なら#000000、白なら#ffffff、赤は#ff0000です。しかし、このRGB値は数字を見ただけではどんな色なのかイメージし辛く、頭の中で想像した色を数値で表すのも大変です。
そこで、色を選択する際、多くの人がカラーピッカーを使用すると思います。
よりよい色・よりよい配色を探索するために、様々なカラーピッカーが開発されています。しかし、「色の見え方」というのは人によって異なります。白が200色あるように見える人もいれば、黒が300色あるように見える人もいます。
個人差を享受しつつも、多くの人にとって扱いやすいカラーピッカーが研究されていますが、そんなものありません。使用者・使用用途・使用環境によって色の見え方は異なり、それに応じて最適なカラーピッカーも変わってきます。
そこで、マリーアントワネット的な思考をしてみます。
最適なカラーピッカーがないなら、作ればいいじゃない!
色区間を歪める
一般的な色空間はRGBの直行座標系を歪めることで、人の感度にあった色空間を形成しています。このとき、人によって感度の高い/低い色域が異なるため、万人にとって扱いやすいカラーピッカーの開発は困難です。

個人差を埋めるためには、カラーピッカーの使用者が、自分に合うように色空間を微調整する必要があります。
ベジェ曲線
自由度が高いながらも、操作が簡単で、非線形な変換ができるものにベジェ曲線があります。
R, G, Bのそれぞれにおいて、ベジェ曲線で非線形な変換を行えるカラーピッカーを試作してみました。
これで200色の白に挑戦できます。
プログラム
HTML
<!--
index.html
-->
<link rel="stylesheet" type="text/css" href="index.css">
<script type="module" src="index.js"></script>
<div id="domBox">
<div id="bezier">
<canvas id="bezier-r" width="296" height="296"></canvas>
<canvas id="bezier-g" width="296" height="296"></canvas>
<canvas id="bezier-b" width="296" height="296"></canvas>
</div>
<div id="pikker">
<canvas id="pikker-r" width="256" height="256"></canvas>
<canvas id="pikker-g" width="256" height="256"></canvas>
<canvas id="pikker-b" width="256" height="256"></canvas>
</div>
<div id="range">
<input id="range-r" type="range" min="0" max="255" step="1" value="0" />
<input id="range-g" type="range" min="0" max="255" step="1" value="0" />
<input id="range-b" type="range" min="0" max="255" step="1" value="0" />
</div>
<div id="color-code">
<div id="color-code-r">#000000</div>
<div id="color-code-g">#000000</div>
<div id="color-code-b">#000000</div>
</div>
</div>
CSS
/*
index.css
*/
#domBox {
flex-direction: column;
}
#bezier,
#pikker {
flex-direction: row;
}
#bezier-r,
#bezier-g,
#bezier-b,
#pikker-r,
#pikker-g,
#pikker-b {
border: solid 1px black;
touch-action: none;
}
#range-r,
#range-g,
#range-b {
width: 256px;
margin: 0px;
}
#color-code {
display: flex;
}
#color-code-r,
#color-code-g,
#color-code-b {
width: 256px;
}
JavaScript
/*
index.js
*/
import { Bezier } from "https://cdn.jsdelivr.net/npm/bezier-js@6.1.4/src/bezier.min.js"
let bezier = [
document.getElementById("bezier-r"),
document.getElementById("bezier-g"),
document.getElementById("bezier-b")]
let point = [
[0, 0, 96, 96, 160, 160, 255, 255],
[0, 0, 96, 96, 160, 160, 255, 255],
[0, 0, 96, 96, 160, 160, 255, 255]]
let point_data = [[], [], []]
let point_move_flag = [-1, -1, -1]
let pikker = [
document.getElementById("pikker-r"),
document.getElementById("pikker-g"),
document.getElementById("pikker-b")]
let range = [
document.getElementById("range-r"),
document.getElementById("range-g"),
document.getElementById("range-b")]
let rng_val = [0, 0, 0]
let color_code = [
document.getElementById("color-code-r"),
document.getElementById("color-code-g"),
document.getElementById("color-code-b")]
let clr_code_pos = [[80, 80], [80, 80], [80, 80]]
let clr_move_flag = [false, false, false]
function clamp(v, _min, _max) { return Math.min(Math.max(v, _min), _max) }
function redraw() {
let line = (ctx, x0, y0, x1, y1) => {
ctx.beginPath()
ctx.moveTo(x0, y0)
ctx.lineTo(x1, y1)
ctx.stroke()
}
let dot_lines = (ctx, c) => {
for (let j = 0; j < c.length; j++) {
ctx.beginPath()
ctx.arc(c[j].x, c[j].y, 1, 0, 2 * Math.PI)
ctx.fill()
}
}
for (let i = 0; i < 3; i++) {
const ctx = bezier[i].getContext("2d")
ctx.setTransform(1, 0, 0, -1, 20, 275)
ctx.fillStyle = "#ffffffff"
ctx.fillRect(-21, -21, 297, 296);
ctx.strokeStyle = "#333"
ctx.lineWidth = 3
line(ctx, 0, -20, 0, 275)
line(ctx, 255, -20, 255, 275)
line(ctx, -20, 0, 275, 0)
line(ctx, -20, 255, 275, 255)
ctx.lineWidth = 1
for (let x = 32; x < 255; x += 32) {
line(ctx, x, -20, x, 275)
line(ctx, -20, x, 275, x)
}
ctx.fillStyle = "#2d96f9ff"
const b = new Bezier(point[i])
for (let x = 0; x < 256; x++) {
let t = b.intersects({ p1: { x: x, y: 0 }, p2: { x: x, y: 255 } })
if (t.length <= 0) {
let p = b.get(x / 255)
let y = (p.y < 128) ? 0 : 255;
point_data[i][x] = { x: x, y: y }
}
else {
point_data[i][x] = b.get(t[0])
point_data[i][x].y = clamp(point_data[i][x].y, 0, 255)
}
}
dot_lines(ctx, point_data[i])
ctx.strokeStyle = "#2d5cf9ff"
ctx.lineWidth = 3
line(ctx, point[i][0], point[i][1], point[i][2], point[i][3])
line(ctx, point[i][4], point[i][5], point[i][6], point[i][7])
ctx.fillStyle = "#f00"
for (let j = 0; j < point[i].length; j++) {
ctx.beginPath()
ctx.arc(point[i][j * 2], point[i][j * 2 + 1], 8, 0, 2 * Math.PI)
ctx.fill()
}
}
for (let i = 0; i < 3; i++) {
let ctx = pikker[i].getContext("2d")
const imageData = ctx.getImageData(0, 0, pikker[i].width, pikker[i].height)
const data = imageData.data
for (let y = 0; y < 256; y++) {
for (let x = 0; x < 256; x++) {
let ix = (y * 256 + x) * 4
let ix_tar = ((255 - y) * 256 + x) * 4
data[ix_tar + (i + 0) % 3] = point_data[(i + 0) % 3][rng_val[i]].y
data[ix_tar + (i + 1) % 3] = point_data[(i + 1) % 3][x].y
data[ix_tar + (i + 2) % 3] = point_data[(i + 2) % 3][y].y
data[ix_tar + 3] = 255
}
}
let pos = clr_code_pos[i]
let index = (pos[1] * 256 + pos[0]) * 4
color_code[i].innerText = '#'
+ data[index + 0].toString(16).padStart(2, '0')
+ data[index + 1].toString(16).padStart(2, '0')
+ data[index + 2].toString(16).padStart(2, '0')
ctx.putImageData(imageData, 0, 0)
let d = 3
ctx.strokeStyle = "#000000"
ctx.lineWidth = 1
ctx.beginPath()
ctx.rect(pos[0] - d, pos[1] - d, 2 * d, 2 * d)
ctx.stroke()
line(ctx, pos[0], pos[1] + d, pos[0], 275)
line(ctx, pos[0], pos[1] - d, pos[0], -20)
line(ctx, pos[0] + d, pos[1], 275, pos[1])
line(ctx, pos[0] - d, pos[1], -20, pos[1])
}
}
let efunc = (event, i) => {
let x = put_ctx_x(event.offsetX)
let y = put_ctx_y(event.offsetY)
let index = point_move_flag[i]
if (index === 0) { x = Math.min(x, 0) }
else if (index === 3) { x = Math.max(x, 255) }
index *= 2
point[i][index] = x
point[i][index + 1] = y
redraw()
}
let put_ctx_x = (x) => {
if (x < 0) { return -20 }
else if (x > 295) { return 275 }
else { return x - 20 }
}
let put_ctx_y = (y) => {
if (y < 0) { return 275 }
else if (y > 295) { return -20 }
else return 275 - y
}
for (let i = 0; i < 3; i++) {
bezier[i].addEventListener("pointerdown", (event) => {
let len = (x, y) => { return Math.sqrt(x * x + y * y) }
for (let j = 0; j < point[i].length; j += 2) {
let r = len(point[i][j] - put_ctx_x(event.offsetX), point[i][j + 1] - put_ctx_y(event.offsetY))
if (r < 16) {
point_move_flag[i] = j / 2
efunc(event, i)
break
}
}
})
}
window.addEventListener("pointermove", (event) => {
for (let i = 0; i < 3; i++) {
if (bezier[i] === event.target) {
if (point_move_flag[i] !== -1) { efunc(event, i) }
}
else {
point_move_flag[i] = -1
}
}
})
window.addEventListener("pointerup", (event) => {
for (let i = 0; i < 3; i++) {
if (bezier[i] === event.target) {
if (point_move_flag[i] !== -1) {
efunc(event, i)
}
}
point_move_flag[i] = -1
}
})
let efunc_c = (event, i) => {
clr_code_pos[i][0] = Math.floor(clamp(event.offsetX, 0, 255))
clr_code_pos[i][1] = Math.floor(clamp(event.offsetY, 0, 255))
redraw()
}
for (let i = 0; i < 3; i++) {
pikker[i].addEventListener("pointerdown", (event) => {
clr_move_flag[i] = true
efunc_c(event, i)
})
}
window.addEventListener("pointermove", (event) => {
for (let i = 0; i < 3; i++) {
if (pikker[i] === event.target) {
if (clr_move_flag[i] === true) { efunc_c(event, i) }
}
else {
clr_move_flag[i] = false
}
}
})
window.addEventListener("pointerup", (event) => {
for (let i = 0; i < 3; i++) {
if (pikker[i] === event.target) {
if (clr_move_flag[i] === true) { efunc_c(event, i) }
}
clr_move_flag[i] = false
}
})
for (let i = 0; i < 3; i++) {
range[i].addEventListener("input", () => {
rng_val[i] = range[i].value
redraw()
})
}
redraw()
むすび
ベジェ曲線を使用して色空間を歪める話でした。デジャブ感すごいです(敗

