3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Color Picker 対アンミカ仕様 (草案)

Last updated at Posted at 2025-12-07

八戸高専 アドカレ
7日目← →9日目

Color Picker 対アンミカ仕様 (草案)

 コンピュータ内部において、「色」というのはRGB値で表現されます。黒なら#000000、白なら#ffffff、赤は#ff0000です。しかし、このRGB値は数字を見ただけではどんな色なのかイメージし辛く、頭の中で想像した色を数値で表すのも大変です。

 そこで、色を選択する際、多くの人がカラーピッカーを使用すると思います。

よりよい色・よりよい配色を探索するために、様々なカラーピッカーが開発されています。しかし、「色の見え方」というのは人によって異なります。白が200色あるように見える人もいれば、黒が300色あるように見える人もいます。

個人差を享受しつつも、多くの人にとって扱いやすいカラーピッカーが研究されていますが、そんなものありません。使用者・使用用途・使用環境によって色の見え方は異なり、それに応じて最適なカラーピッカーも変わってきます。

 そこで、マリーアントワネット的な思考をしてみます。

最適なカラーピッカーがないなら、作ればいいじゃない!

色区間を歪める

 一般的な色空間はRGBの直行座標系を歪めることで、人の感度にあった色空間を形成しています。このとき、人によって感度の高い/低い色域が異なるため、万人にとって扱いやすいカラーピッカーの開発は困難です。
okcoloranm.gif

 個人差を埋めるためには、カラーピッカーの使用者が、自分に合うように色空間を微調整する必要があります。

ベジェ曲線

 自由度が高いながらも、操作が簡単で、非線形な変換ができるものにベジェ曲線があります。

R, G, Bのそれぞれにおいて、ベジェ曲線で非線形な変換を行えるカラーピッカーを試作してみました。

スクリーンショット (262).png

これで200色の白に挑戦できます。

スクリーンショット (263).png

プログラム

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()

むすび

 ベジェ曲線を使用して色空間を歪める話でした。デジャブ感すごいです(敗

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?