任意の色相に対して最低コントラストを満たす色を計算する


この記事は、Webデザインでユーザに任意のテーマカラーを選択させるときなどを想定しています。

一般に言う「色の種類」とは、HLS色空間HSV色空間の「色相」によって決定されます。

色相は、上の画像を見ていただけると分かる通り、0-360の数値で表すことができます。

HLS色空間では、「色相、彩度、輝度」によって色が決定されるので、彩度と輝度を固定して、色相のみをユーザに選択させればいいと考えることができます。

それを実装したものが以下のGIFになります。

しかし、実際に見てみると、赤〜青にかけては文字が見えやすい一方で、黄〜緑では文字が見えづらいことがわかります。

これは、背景色(#fff)とのコントラストが低くなることが原因です。なぜ色によってコントラストが変化するかというと、色の相対輝度にはRGBそれぞれについて重みがあり、白とコントラストが高いのは青 > 赤 > 緑です。黄色は赤と緑の中間色なので、上記の画像で黄〜緑の文字が見えづらいことにも納得できます。

相対輝度(L)の定義

{\displaystyle L=0.2126\times R+0.7152\times G+0.0722\times B}

詳しくはWIkipedia Help:配色のコントラスト比が参考になります。

では、先ほどのように彩度と輝度を固定して色相のみを変化させるのではなく、彩度のみを固定し、指定された色相に対して一定のコントラストを満たす輝度を求めてみましょう。

赤〜青は最初とほとんど変わりませんが、黄〜緑が見えやすくなったのが分かります。
JavaScriptの関数は以下の通りです。(chroma-jsという色操作が簡単にできるJavaScriptのライブラリを使用しています。)

// 背景色、色相、彩度、満たしたいコントラスト比を渡すと、それを満たす色(16進数)を返す
const getChromaFromHue = (
  baseColor: string,
  hue: number,
  saturation: number,
  minContrast: number
): chroma.Color => {
  const BC = chroma(baseColor)
  let left = 0
  let right = 1
  let count = 0
  while (right - left > 0.01) {
    const middle = Math.floor(((left + right) / 2) * 100) / 100
    const color = chroma(hue, saturation, middle, 'hsl')
    if (chroma.contrast(BC, color) > minContrast) {
      left = middle
    } else {
      right = middle
    }
    if (count > 5) break
    count += 1
  }
  return chroma(hue, saturation, left, 'hsl').hex()
}

const changeThemeColor = (hue: number) => {
  setThemeColor({
    theme: getChromaFromHue('#fff', hue, 0.7, 1.7),
    themeAA: getChromaFromHue('#fff', hue, 0.5, 4.5),
    themeAAA: getChromaFromHue('#fff', hue, 0.4, 7),
    themeLight: getChromaFromHue('#fff', hue, 0.5, 1.05),
  })
}

関数内の処理としては、指定されたコントラストを満たす最高の輝度を二分探索で求めています。
(二分探索せずに求める式があれば教えてください)

ここで、コントラスト比 > 4.5を満たすものはアクセシビリティ適合レベルAAコントラスト比 > 7.0を満たすものはアクセシビリティ適合レベルAAAです。文字色のコントラストはAA以上は満たすか、テーマをAA以上に変更するオプションなどは用意するといいでしょう。

AAを満たすボタンを設置した例