訳あって(後述)、シェル上でターミナルの256色を24bitのRGBコードへ変換する必要が出てきました。
ここではその手法をメインに、ターミナルの256色について調べたことを書いてみました。
定義区分について
これら256色は全体で統一された規則によって定義されているわけではなく、3つの異なる定義方法で分けられる領域があるようです。
- システム用パレット(0 ~ 15)
- 有彩色(16 ~ 231)
- 無彩色(232 ~ 255)
基本の16色パレットは今回置いておくとして1、問題なのは16から231までの「有彩色ゾーン」、232から255までの「無彩色ゾーン」。
こいつらの変換方法を確認しておきましょう。
有彩色ゾーンについて
まず以下の連立方程式を満たす、色インデックス( $i$ )に対応する三原色の色レベルを算出します。
\begin{eqnarray}
\left\{
\begin{array}{l}
{6^2}r + 6g + b = i - 16 \\
0 \leqq r \lt 6 \\
0 \leqq g \lt 6 \\
0 \leqq b \lt 6
\end{array}
\right.
\end{eqnarray}
これにより、0以上6未満の3値 $r, g, b$ が求められます。3値同値なら有彩色ゾーンなのに無彩色が入ってしまうのはここだけの秘密。
次に各レベルを色の強さ(以降 明度値)に変換します。
レベル0のときは(当然ながら)値は0。
そこから比例的に加算されると思いきや、レベル1で一気に**95(0x5F)が加算され、以降レベルが1段階上昇するに従い40(0x28)**が追加されます。
レベル上限は5であることを踏まえて表にするとこのようになります。
レベル | コード | 明度(%) |
---|---|---|
0 | 0x00 | 0.0 |
1 | 0x5F | 37.3 |
2 | 0x87 | 52.9 |
3 | 0xAF | 68.6 |
4 | 0xD7 | 84.3 |
5 | 0xFF | 100.0 |
あとは各明度値をくっつけておなじみ24bitのカラーコードにするだけです。
以上の流れをfishスクリプトにするとこんな感じ。
function index2levels4chroma -d "色インデックスを空白区切りにした3つのレベル値に変換"
set -l index $argv[1] # ローカル変数の設定
# 赤レベルの算出
set -l red_level (math "$index / (6 ^ 2)")
test $red_level -ge 6 # 6以上のとき
and set red_level 5
# 緑レベルの算出
set -l green_level (math "($index - (6 ^ 2) * $red_level) / 6")
test $grenn_level -ge 6
and set green_level 5
# 青レベルの算出
set -l blue_level (math "$index - (6 ^ 2) * $red_level - 6 * $green_level")
printf '%d %d %d' $red_level $green_level $blue_level
end
function level2hex4chroma -d "レベル値を16進数の明度値のコードに変換"
switch $argv[1]
case 0
printf 00
case 1 2 3 4 5
printf '%02x' (math "95 + 40 * ($argv[1] - 1)")
end
end
function index2code4chroma -d "色インデックスを24bitコードへ変換(有彩色用)"
set -l levels (index2levels4chroma $argv[1])
set -l list (string split ' ' "$levels")
printf '#%s%s%s' \
(level2hex4chroma $list[1]) \
(level2hex4chroma $list[2]) \
(level2hex4chroma $list[3])
end
無彩色ゾーンについて
無彩色ゾーンの求め方も前述のものに似ています。
- インデックスから232を減算し、明度レベル( $l$ )を得る
- $l_{dec} = 10 l + 8$ を満たす10進数の明度値( $l_{dec}$ )を求める
- 16進数に変換、3つ並べて24bitコードにする
function index2code4monochroma -d "色インデックスを24bitコードへ変換(無彩色用)"
set -l level (math "$argv[1] - 232")
set -l dec (math "10 * $level + 8")
printf '#%02x%02x%02x' $dec $dec $dec
end
ちなみに有彩色ゾーンに出現する無彩色と組み合わせた、256色ターミナルで表現できる無彩色の一覧がこちら。
インデックス | レベル(有彩) | レベル(無彩) | カラーコード | 明度(%) |
---|---|---|---|---|
16 | 0 | #000000 | 0.0 | |
232 | 0 | #080808 | 3.1 | |
233 | 1 | #121212 | 7.1 | |
234 | 2 | #1C1C1C | 11.0 | |
235 | 3 | #262626 | 14.9 | |
236 | 4 | #303030 | 18.8 | |
237 | 5 | #3A3A3A | 22.7 | |
238 | 6 | #444444 | 26.7 | |
239 | 7 | #4E4E4E | 30.6 | |
240 | 8 | #585858 | 34.5 | |
43 | 1 | #5F5F5F | 37.3 | |
241 | 9 | #626262 | 38.4 | |
242 | 10 | #6C6C6C | 42.4 | |
243 | 11 | #767676 | 46.3 | |
244 | 12 | #808080 | 50.2 | |
86 | 2 | #878787 | 52.9 | |
245 | 13 | #8A8A8A | 54.1 | |
246 | 14 | #949494 | 58.0 | |
247 | 15 | #9E9E9E | 62.0 | |
248 | 16 | #A8A8A8 | 65.9 | |
129 | 3 | #AFAFAF | 68.6 | |
249 | 17 | #B2B2B2 | 69.8 | |
250 | 18 | #BCBCBC | 73.7 | |
251 | 19 | #C6C6C6 | 77.6 | |
252 | 20 | #D0D0D0 | 81.6 | |
172 | 4 | #D7D7D7 | 84.3 | |
253 | 21 | #DADADA | 85.5 | |
254 | 22 | #E4E4E4 | 89.4 | |
255 | 23 | #EEEEEE | 93.3 | |
231 | 5 | #FFFFFF | 100.0 |
インデックス43, 86, 129, 172の中途半端感が凄まじい。
なぜこれが必要だったのか
さて、ところで皆さん、ターミナルのカラーパレットをprintfで上書きできるって知ってました?
皆さんご存知の色変更シーケンスはこのような感じだと思います。
# 文字色変更 基本の256色インデックス使用版
printf '\033[38;5;%dm%s\033[0m' $index_256 $string
# 文字色変更 RGB使用版らしい
printf '\033[38;2;%02x%02x%02xm%s\033[0m' $r $g $b $string
対してあまり語られないですが、その対話環境に限り永続的に色変更できるシーケンスも存在します。
base16-shellでは次のようなコマンドを基本形として任意の24bitカラーでパレットを上書きしています2。
# $index: パレットのインデックス(例: 暗めの緑 → 3)
printf '\033]4;%d;rgb:%02x/%02x/%02x\033\\' $index $r $g $b
見れば分かる通りこのコマンド、24bitのカラーコードが用意されていることが前提となっていますね。
現在自作しているfish用の配色設定用プラグイン(onedark-fish)ではこれをほぼそのまま用いているのですが、ここである問題が発生しました。
この拙作、設定可能な配色をonedarkに限定しながらもコマンドライン用配色とターミナルパレットをまとめて1コマンドでいい感じに統一させるというもので、開発初期はオリジナルonedarkで定義されている24bitカラーのみ用いていました。基にしているbase16-shellのコードを用いればLinuxのtty端末ですら24bitカラーが使えるという有能ぶりを見て、当初は256カラーなんて必要ないと思っていたんです。
しかし様々な端末で試験を重ねているうちに、ターミナル上で開かれているVimから内蔵ターミナルを呼び出したとき、コーディング画面との間で配色が若干崩れているのを発見したのです。
理由は簡単。ターミナルVimは配色指定に256色のインデックスを使っているから。
この問題を解決してVimでも統一性ある色味を実現する方法としては、
- インデックスがそのまま使えるエスケープシーケンスを利用
- インデックスを24bitRGBコードに変換して利用する
の2案が挙がりましたが、前述の通りターミナルパレット上書きの処理は24bitカラーのみ利用可能という制限があります。したがって案1は使えません。
結果、この問題の解決策は案2の256色インデックスを24bitカラーコードに変換すること。ただ1つしかなかったのです。
今回の調査のおかげで何とか上記問題は解決できました。
おわりに
当初は上記自作プラグインの宣伝記事を書こうと思っていたのですが、気付いたら頭に変な解説がくっついてしまいました。それだけ省略されがちだったり知ってて当たり前なこと(?)だったり、なにより必要のない情報なのだと思います。
とりあえず(少なくとも個人で調べた限りでは)数少ない情報の備忘録として、ストックでもしてくれると嬉しいです。
ついでにこの子も「カラースキーム定義」というfish界ではなかなかないジャンルのプラグインなので、fish(fisherman)使いでonedarkジャンキーの方はぜひお試しくださいませ(ノルマ達成)。
参考(にしたもの、なったもの)
- ANSIエスケープシーケンス チートシート
https://qiita.com/PruneMazui/items/8a023347772620025ad6 - base16-shell
https://github.com/chriskempson/base16-shell - 256 Colors - Cheat Sheet - Xterm, HEX, RGB, HSL
http://jonasjacek.github.io/colors/ - 256color.pl
https://gist.github.com/hSATAC/1095100 - ansi-256-colors
https://github.com/jbnicolai/ansi-256-colors - ANSI escpe code - Wikipedia
https://en.wikipedia.org/wiki/ANSI_escape_code#Colors