背景色と、それにあわせた見やすい文字色を自動で生成

TouchDesigner（に限りませんが）でUIを設計するときに、悩むのが色です。
特に、Replicator Compで複製を前提としたUIのときに、番号ごとに色分けしたい、そしてそれを見やすくしたい、と常々思っていました。

いろいろ調べる中で、[Web Content Accessibility Guidelines (WCAG)](https://waic.jp/translations/WCAG22/)というものに出会いました。

このガイドラインの視覚（色）についての項目が大変参考になったので、今回それを実装してみました。


# まずは自動生成させる
![作成したプロジェクト](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601755/7adabc81-a3e9-f595-4dec-ca1deae6e9bb.png)
色数と範囲を決め、まずはそれに合わせたRGBテーブルを作ります。

[HSV色空間](https://ja.wikipedia.org/wiki/HSV%E8%89%B2%E7%A9%BA%E9%96%93)の色相(Hue)を変えることで、必要な色を計算します。

ここでは、`num_of_col`フィールドで、色数を指定しています（今回は7）。
`start`と`end`というスライダーで、SaturationとValueを決め、Hueの範囲を決めます。なお、`end`スライダーのSとVについては`start`と同じ数値になるよう固定しています。

それらの数値が変更されたら、`chopexec1`が実行され、`rgb`テーブルにRGB色空間の値が格納されます。

HSVからRGBへの変換はpythonのcolorsysモジュールで簡単に変換できます。
```python:chopexec1
import colorsys

def onValueChange(channel, sampleIndex, val, prev):
    # RGBを格納するtable DAT
    tableOp = op('rgb')

    # 取得したHSVの値を小数点以下2桁で丸める
    start_h = int(100 * op('merge1')['H']) / 100
    s = int(100 * op('merge1')['S']) / 100
    v = int(100 * op('merge1')['V']) / 100
    end_h = int(100 * op('merge1')['endH']) / 100

    index = op('merge1')['num']
    currentRows = tableOp.numRows
    requiredRows = int(index) + 1  # ヘッダ行分を加える

    # 現在の表の行数を必要な行数に調整
    if requiredRows > currentRows:
        for _ in range(requiredRows - currentRows):
            tableOp.appendRow()
    elif requiredRows < currentRows:
        for _ in range(currentRows - requiredRows):
            tableOp.deleteRow(requiredRows)

    # 色数分のループ
    for i in range(0, int(index)):
        # start_h から end_h までを線形補間
        if start_h <= end_h:
            temp_h = start_h + (end_h - start_h) * (i / (int(index) - 1))
        else:
            # end_h が start_h より小さい場合は環状に補間
            temp_h = (start_h + ((end_h - start_h + 1) * (i / (int(index) - 1)))) % 1
        
        col = colorsys.hsv_to_rgb(temp_h, s, v)
        tableOp[i + 1, 0] = int(100 * col[0]) / 100
        tableOp[i + 1, 1] = int(100 * col[1]) / 100
        tableOp[i + 1, 2] = int(100 * col[2]) / 100
    return

```

さて、これで指定した色の範囲から指定した数の色が取り出せました。
それでは、これらの色を背景にしたときに最適な文字色を自動で決めることはできないでしょうか。

# WCAG2.2におけるコントラスト
コントラストは以下の式で求められる
```math
\frac{L_1 + 0.05}{L_2 + 0.05}
```
ただし、L1は明るい方の色の相対輝度、L2は暗い方の色の相対輝度

# 相対輝度
相対輝度とは「最も暗い黒を 0 に、最も明るい白を 1 に正規化した色空間内の任意の点の相対的な明るさ。」とのことである。
sRGB色空間においては、色の相対輝度は、
```math
L = 0.2126 R + 0.7152 G + 0.0722 B
```
と定義されており、R、G 及び B は以下のように定義される

$R_{sRGB} \leqq 0.3928$ の場合 $\displaystyle R=\frac{R_{sRGB}}{12.92}$
そうでない場合、$\displaystyle R=\left(\frac{R_{sRGB}+0.055}{1.055}\right)^{2.4}$
（G、Bについても同様）

# 黒か白かを決める
コントラスト比を計算して、最適な文字色を決めるのがいちばん良いかと思ったのですが、白か黒のどちらかであれば絶対にコントラスト比がおおきくなるので、文字を黒か白のどちらかにするように計算を組みました。

```python:datexec1
def onTableChange(dat):
    # op('rgb') からRGB値を取得
    rgbTable = op('rgb')
    outputTable = op('textrgb')
    outputTable.clear()
    outputTable.appendRow(['r', 'g', 'b'])

    # https://waic.jp/translations/WCAG20/Overview.html#relativeluminancedef
    def relative_luminance(r, g, b):
        colors = [r, g, b]
        for i in range(len(colors)):
            if colors[i] <= 0.03928:
                colors[i] = colors[i] / 12.92
            else:
                colors[i] = ((colors[i] + 0.055) / 1.055) ** 2.4
        # 相対輝度の計算
        return 0.2126 * colors[0] + 0.7152 * colors[1] + 0.0722 * colors[2]

    for i in range(1, rgbTable.numRows):  # ヘッダー行をスキップ
        r = rgbTable[i, 'r']
        g = rgbTable[i, 'g']
        b = rgbTable[i, 'b']

        # 相対輝度の計算
        lum = relative_luminance(r, g, b)
        # 黒と白の相対輝度
        lum_black = 0
        lum_white = 1

        # コントラスト比の計算
        # https://waic.jp/translations/WCAG20/Overview.html#contrast-ratiodef
        contrast_black = (lum + 0.05) / (lum_black + 0.05)
        contrast_white = (lum_white + 0.05) / (lum + 0.05)
        # 最適な文字色の選択
        outputTable.appendRow([0, 0, 0]) if contrast_black > contrast_white else outputTable.appendRow([1, 1, 1])
    return
```
rgbテーブルに生成された背景色の相対輝度を計算し、黒と白とのコントラスト比をそれぞれ比較し、よりコントラスト比の高いほうを選択するようにしました。
![見やすい文字になった](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601755/6451b68b-8533-016b-0979-43d0b383016e.png)

あとはcomp全体をGlobal OP Shortcutに設定すれば、必要なシーンで`op.colors.op('rgb')[me.digits,0]`と指定するだけで、簡単に色を設定できます。

いうまでもなく、指定した色数よりも必要な色数が多くなりそうな場合は`me.digits`を`int(1+(me.digits-1)%(op.colors.op('merge1')['num']))`とすることで、`me.digits`にあわせて1〜numまでを繰り返します。

さいごに、toxファイルを[github](https://github.com/hiroEsaki/colors.tox/)にあげておきます。使ったら教えていただけると嬉しいです。