TouchDesigner(に限りませんが)でUIを設計するときに、悩むのが色です。
特に、Replicator Compで複製を前提としたUIのときに、番号ごとに色分けしたい、そしてそれを見やすくしたい、と常々思っていました。
いろいろ調べる中で、Web Content Accessibility Guidelines (WCAG)というものに出会いました。
このガイドラインの視覚(色)についての項目が大変参考になったので、今回それを実装してみました。
まずは自動生成させる
色数と範囲を決め、まずはそれに合わせたRGBテーブルを作ります。
HSV色空間の色相(Hue)を変えることで、必要な色を計算します。
ここでは、num_of_col
フィールドで、色数を指定しています(今回は7)。
start
とend
というスライダーで、SaturationとValueを決め、Hueの範囲を決めます。なお、end
スライダーのSとVについてはstart
と同じ数値になるよう固定しています。
それらの数値が変更されたら、chopexec1
が実行され、rgb
テーブルにRGB色空間の値が格納されます。
HSVからRGBへの変換はpythonのcolorsysモジュールで簡単に変換できます。
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におけるコントラスト
コントラストは以下の式で求められる
\frac{L_1 + 0.05}{L_2 + 0.05}
ただし、L1は明るい方の色の相対輝度、L2は暗い方の色の相対輝度
相対輝度
相対輝度とは「最も暗い黒を 0 に、最も明るい白を 1 に正規化した色空間内の任意の点の相対的な明るさ。」とのことである。
sRGB色空間においては、色の相対輝度は、
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についても同様)
黒か白かを決める
コントラスト比を計算して、最適な文字色を決めるのがいちばん良いかと思ったのですが、白か黒のどちらかであれば絶対にコントラスト比がおおきくなるので、文字を黒か白のどちらかにするように計算を組みました。
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テーブルに生成された背景色の相対輝度を計算し、黒と白とのコントラスト比をそれぞれ比較し、よりコントラスト比の高いほうを選択するようにしました。
あとは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にあげておきます。使ったら教えていただけると嬉しいです。