🎨

chroma.jsでJavaScriptから色を操作してみる

2024/12/22に公開

この記事は CA Tech Lounge Advent Calendar 2024 22 日目の記事です。昨日は HASURO さんの「Vim を使おう!初めに覚えた方が良い Vim コマンド集」でした。

主要な機能の説明を網羅しようとした結果、ライブラリの紹介というより、もはや色空間とカラースケールそのものについて解説する記事になってしまいました。

はじめに

先日 React の UI コンポーネントライブラリである Mantine のドキュメントを読んでいたところ、 Colors generator というページを発見しました。

https://mantine.dev/colors-generator/

このページを見ていて、どのように単一色からカラーマップを生成しているのか気になったため、ドキュメントのリポジトリを確認したところ chroma.js というライブラリが使われていることが分かりました。軽く使ってみたところ非常に多機能で面白いと感じたため、主要な機能をまとめて記事にしました。

chroma.js とは

あらゆる種類の色変換とカラースケール生成ができる JavaScript のライブラリです。

https://gka.github.io/chroma.js/

依存性ゼロかつ軽量であり、バンドルサイズは圧縮された状態で 16.3KB らしいです。

後述するようにデータ可視化に使えそうな機能が多くあり、オーナーのプロフィールに Visual Data Journalist とあることからも、地図やグラフの色を考える際に使うことができそうです。

導入

使い方も非常に簡単です。まずはパッケージと型定義をインストール。

npm install chroma-js
npm install -D @types/chroma-js

例えば

import chroma from "chroma-js";

const color1 = chroma("pink").darken().saturate(2);
console.log(color1.hex()); // #ff6d93

とすれば、ピンクを暗くし、輝度を上げた色を、HEX 形式で求めることができます。

基本的な使い方

CSS で有効である色名による入力(W3CX11[1])や複数の色空間による入力、16 進数による入力など、様々な形式に対応しています。

以降サンプルコードを示しますが、公式ドキュメントのほうがプレビュー結果もついていて読みやすいので、各セクションのリンクからドキュメントを確認していただけるとわかりやすいと思います。

chroma("hotpink"); // #ff69b4

// #ff3399
chroma("#ff3399");
chroma("F39");
chroma(0xff3399);
chroma(0xff, 0x33, 0x99);
chroma(255, 51, 153);
chroma([255, 51, 153]);
chroma(330, 1, 0.6, "hsl");

chroma({ h: 120, s: 1, l: 0.75 }); // #80ff80
chroma({ l: 80, c: 25, h: 200 }); // #85d4d5
chroma({ c: 1, m: 0.5, y: 0, k: 0.2 }); // #0066cc

値が有効であるかどうかは chroma.valid から確認できます。

chroma.valid("red"); // true
chroma.valid("bread"); // false

私が最初にライブラリを触ったとき、知らない色空間が多くてナニモワカラン状態だったため、chroma.js でサポートされている色空間についてアコーディオン内で簡単に説明します。

色空間について

RGB (Red, Green, Blue)

光の三原色にあたる「赤」「緑」「青」の三色で色を表します。

デジタルディスプレイやカメラは、この色空間を使って色を描画します。それぞれの値は 0 から 255 の範囲で表され、光の強さを指します。

0, 0, 0 が黒、255, 255, 255 が白です。

RGBA (Red, Green, Blue, Alpha)

RGB に「透明度」を表すアルファ値を追加したものです。

この値は 0.0 から 1.0 の範囲で表され、0.0 は完全透明、1.0 は完全不透明を意味します。

CMYK (Cyan, Magenta, Yellow, Key/Black)

シアン、マゼンタ、イエロー、黒の 4 色を使用した減法混色のモデルです。主に印刷業界で使用され、紙にインクを重ねることで色を再現します。

CMYK(0, 100, 100, 0) は純粋な赤を表します。

HSL (Hue, Saturation, Lightness)

色相、彩度、明度で色を表現する色空間です。特に、色を人間の感覚に近い方法で分類したものとして、色の挙動を直感的に控えられるのが特徴です。色相は 0° から 360° の角度で表され、色の沿光を表します。たとえば、HSL の「明度」の値を変えることで、同じ色でも、より明るくしたり暗くしたりできます。

HSV (Hue, Saturation, Value)

HSL に似ていますが、こちらは明度を「Value」として表します。この値は色の最大光度に基づいており、その強さを指します。たとえば、全ての明度を 100%にすると、最も明るい色が形成されます。グラフィックツールや色を選択するインターフェイスによく使用されます。

HSI (Hue, Saturation, Intensity)

HSV と似た構造ですが、この色空間では「強度」を「Intensity」として表し、色の平均明るさを重要視します。例えば、この値を用いることで、色の主要要素の平均値を基準に考慮することができます。この色空間は、特に画像処理においてよく使用されます。

Lab (CIELab)

色を人間の視覚に基づいて定義した方式です。この空間は、「明度 (Lightness)」、「緑-赤軸 (a)」、「青-黄軸 (b)」の三要素で表現されます。

  • L*: 明度(Lightness)明るさを示す値で、0 が完全な黒、100 が完全な白
  • a*: 緑-赤軸(Green-Red axis)プラス方向が赤、マイナス方向が緑
  • b*: 青-黄軸(Blue-Yellow axis)プラス方向が黄、マイナス方向が青

この空間の特徴は、色の比較や表現を楽に行える点です。たとえば、明度の値のみを変更することで、色を明るくしたり暗くしたりできます。印刷や色相計算で使用されます。

LCH (Lightness, Chroma, Hue)

Lab 色空間を極座標に変換したものです。「明度」「彩度」「色相」の要素で表され、Lab よりも色に関する日常的な記述により近く、色補正を理解しやすいというメリットがあります。

OKLab

新しい色空間で、Lab を改良したものです。この色空間は、人間の視覚により近い色を描画することを目的としています。特に、グラデーションや色の渡り変わりが自然な表現を作り出せるのが強みです。

OKLCH

OKLCH は、OKLab を極座標変換したもので、「明度」「彩度」「色相」の要素で構成されています。OKLab の強みを生かしつつ、より直感的な色の操作を可能にします。

記事を書くにあたって OKLab と OKLCH の「OK」が何を意味するのか調べたのですが、どちらも Björn Ottosson というデザイナーが開発したということしか分かりませんでした。[2]

色を操作する

色を暗く/明るくする

color.darkencolor.brighten を使います。color.brightencolor.darken の逆で、引数に負の値を与えることで同じ結果が得られます。

chroma("hotpink").darken(); // #c93384
chroma("hotpink").brighten(); // #ff9ce6

実装はsrc/ops/darken.jsで、色を HSL に変換し、Lightness の値を操作することで明るさを変えています。このライブラリでは引数として与えた数に 18 という定数をかけた数を減算しているのですが、18 という数字にどのような根拠があるのかは分かりませんでした。

色の彩度を変える

color.saturate を使います。color.desaturate で逆のことができます。

chroma("hotpink").saturate(); // #e77dae

実装はsrc/ops/saturate.jsで、色を LCH に変換し、Chroma の値を操作することで彩度を変えています。ここでも 18 という定数を使い、元の値に加算しています。color.desaturate で逆のことをできます。

色を混ぜる

chroma.mixcolor.mix が使えます。2 色以上の色を混ぜたいときは chroma.average も使えます。色の比率を指定することもできて、0.5 の場合は chrome.average に 2 つの色を与えたときと同じ結果になります。

chroma.mix("hotpink", "blue"); // #b44add
chroma("hotpink").mix("blue"); // #b44add

ランダムな色を生成する

chroma.randomから利用できます。

chroma.random();

src/generator/ramdom.jsに実装がありますが、HEX 形式で乱数を使って文字列を組み立てているようです。この機能は使い所が広そうです。

CSS で使える形式に変換する

color.cssから利用できます。デフォルトが rgb で、このほか hsl, lab, lch, oklab, oklch を選択できます。

chroma("teal").css("hsl"); // "hsl(180deg 100% 25.1%)"
chroma("teal").css("lab"); // "lab(47.99% -30.39 -8.98)"
chroma("teal").css("oklch"); // "oklch(54.31% 0.09 194.76deg)"

変換時色が存在するかどうか確認する

CIELab 色空間から RGB に色を変換するとき、色チャンネルは[0..255]の範囲にクリップされます。その範囲外の色は自然界に存在するかもしれませんが、RGB モニタでは表示できません。

color.clipped を使用すると、色がクリッピングされているかどうかをテストできます。

[(c = chroma.hcl(50, 40, 20)), c.clipped()]; // [#581d00,true]
[(c = chroma.hcl(50, 40, 40)), c.clipped()]; // [#904c2d,false]
[(c = chroma.hcl(50, 40, 60)), c.clipped()]; // [#c97e5c,false]
[(c = chroma.hcl(50, 40, 80)), c.clipped()]; // [#ffb38f,true]
[(c = chroma.hcl(50, 40, 100)), c.clipped()]; // [#ffebc5,true]

カラースケールを作る

グラデーションを作る

chroma.scale を使います。引数に経由させたい色を指定し、0 から 1 の間で引数を与えることで中間色を生成させることができます。

chroma.scale(["yellow", "008ae5"]);
chroma.scale(["yellow", "red", "black"]);

scale.mode で色空間を指定することで、RGB 以外の方法でグラデーションさせることができます。

また 2 つ以上の引数を与えた場合、基本的には等幅ですが、color.domain を指定することで breakpoint となる位置を変更することもできます。

n 個の色を生成する

scale.colors で、グラデーションの中から n 個の色を取り出すことができます。明度の異なるカラーパレットを作りたいときに便利です。

chroma.scale("OrRd").colors(5);
chroma.scale(["white", "black"]).colors(12);

地図のためのカラースキーマ

ColorBrewer は Cynthia Brewer によって作成された地図のためのカラースキーマをベースに開発された、プレビューを見ながらカラースキーマを選択できるオンラインツールです。データの種類(連続、発散、または定性的)に基づいてカラースキームを選ぶことができます。

chroma.brewer を使います。Object.keys(chroma.brewer)から利用可能なカラースキーマの一覧を確認できます。

Object.keys(chroma.brewer);
// ['OrRd', 'PuBu', 'BuPu', 'Oranges', 'BuGn', 'YlOrBr', 'YlGn', 'Reds', 'RdPu', 'Greens', 'YlGnBu', 'Purples', 'GnBu', 'Greys', 'YlOrRd', 'PuRd', 'Blues', 'PuBuGn', 'Viridis', 'Spectral', 'RdYlGn', 'RdBu', 'PiYG', 'PRGn', 'RdYlBu', 'BrBG', 'RdGy', 'PuOr', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1']

連続データ用のスキーマは 9 色、発散データ用のスキーマは 11 色、定性的なデータ用のスキーマは 8〜12 色で構成されていますが、以下のように色数を減らすこともできます。

chroma.scale("RdBu").colors(5);
// offical 5-color RdBu:
["#ca0020", "#f4a582", "#f7f7f7", "#92c5de", "#0571b0"];

なお chroma.limits でデータのクラスタリングを行うことができ、数値の入った配列を渡してあげることで、データの数を指定した個数に減らすことができます。データの区切り方は等距離(e)、分位(q)、対数(l)、k-means(k)の 4 種類から選べます。

const data = [
  2.0, 3.5, 3.6, 3.8, 3.8, 4.1, 4.3, 4.4, 4.6, 4.9, 5.2, 5.3, 5.4, 5.7, 5.8,
  5.9, 6.2, 6.5, 6.8, 7.2, 8,
];
chroma.limits(data, "e", 4); // [2,3.5,5,6.5,8]
chroma.limits(data, "q", 4); // [2,4.1,5.2,5.9,8]
chroma.limits(data, "l", 4); // [2,2.83,4,5.66,8]
chroma.limits(data, "k", 4); // [2,8]

Cubehelix

Cubehelix は天文学における強度画像の表示に適したカラースキーマで、2011 年に D.A.Green によって提案されました。

天文学で使われる多くのカラースキーマには「黄色や緑が赤よりも明るく見える」など、色が視覚的な明るさに応じて単調に増加しないことが多いという問題がありました。Cubehelix は強度画像を正確かつ視覚的にわかりやすく表示するために、明るさが単調に増加するカラースキーマを提供しているらしいです。

ラジオ天文学用解析ソフトウェアに組み込まれているほか、地理データ(標高)やフラクタル画像、微粒子汚染の分布図における視覚化にも使われています。特にモノクロで印刷した際にも情報を伝えられる特性を持っています。

ケンブリッジ大学のDave Green's cubehelix colour schemeというページに詳細な解説があります。

スキーマは開始色、終了色、回転数、明度、彩度の 4 つのパラメータで調整することができます。

chroma.js では chroma.cubehelix から利用できます。

chroma.cubehelix(); // #000000
chroma.cubehelix(300, 0.5, 1, 0.8, "rgb"); // #ff0000

Cubehelix からカラースケールを生成することもできます。

chroma
  .cubehelix()
  .start(200)
  .rotations(-0.35)
  .gamma(0.7)
  .lightness([0.3, 0.8])
  .scale() // convert to chroma.scale
  .correctLightness()
  .colors(5);

計算する

コントラスト比を計算する

chroma.contrast から、ウェブコンテンツアクセシビリティガイドライン(WCAG)が定める 2 色間のコントラスト比を計算することができます。

// contrast smaller than 4.5 = too low
chroma.contrast("pink", "hotpink"); // 1.721
// contrast greater than 4.5 = high enough
chroma.contrast("pink", "purple"); // 6.124

WCAG は最低でも、サイズの大きなテキストの場合 3:1 以上、その他の文字は 4.5:1 以上のコントラスト比を要求しています。

現在 WCAG は 2023 年 10 月に勧告された WCAG 2.2 が最新版ですが、次のバージョンである WCAG 3.0 が Working Draft にあり、コントラストの計算方法が変更される予定です。chroma.js では chroma.contrastAPCA からこの計算方法によるコントラスト比を求めることができます。

chroma.contrastAPCA("hotpink", "pink"); // 23.746
chroma.contrastAPCA("purple", "pink"); // 62.534

APCA (Advanced Perceptual Contrast Algorithm) の方は計算方法が複雑なため、ここでは深く解説しませんが、文字色と背景色でコントラスト値 Lightness contrast (Lc)を計算するアルゴリズムのことを指しています。

この Lc の値に対して、空間周波数の影響を考慮したうえで、WCAG でフォントサイズとウェイトの推奨値を決めている、という感じです。

下の記事がわかりやすいのでぜひ参考にしてください。

https://qiita.com/shunito/items/4aecd32fcb3f22064e4a

色の輝度を計算する

chroma.luminance で色の輝度を計算することができます。WCAG の定義に従った計算方法で、輝度は 0 の黒から 1 の白までの間で表されます。

chroma("white").luminance(); // 1
chroma("hotpink").luminance(); // 0.347
chroma("black").luminance(); // 0

色温度を計算する

chroma.temperature で色温度を計算することができます。色温度は 1000K から 40000K の間で表され、1000K は赤、40000K は青を表します。

chroma("hotpink").temperature(); // 4370
chroma("white").temperature(); // 6500
chroma("skyblue").temperature(); // 40000

色の距離を計算する

chroma.distanceで 2 色間のユークリッド距離を計算することができます。デフォルトは Lab 色空間で計算されますが、第 3 引数で色空間を指定することもできます。

このほかchroma.deltaEから、国際照明委員会(CIE)が指定している、Bruce Lindbloom による色差計算式も利用することができます。こちらは 0 から 100 の範囲で表され、0 に近いほど色が近いことを示します。

chroma.distance("red", "blue"); // 131.153
chroma.deltaE("red", "blue"); // 22.25

Bruce Lindbloom による実装はユークリッド距離による実装と比較すると計算コストが高いため、chroma.distanceは精密な色差を必要としないシーンで、chroma.deltaEは色の違いがわずかな場合で使うと良い、らしいです。

むすびにかえて

いろってむずかしい。

明日 21 日目は SuperHotDogCat さんで「SQLAlchemy の中身を深堀りする」です。お楽しみに!

脚注
  1. 154 色の色名が定義されていて、一覧はCSS3 module: Colorから確認できる。X11 とは Gray や Green、Maroon、Purple などの色が微妙に異なっている ↩︎

  2. Gemini は「頭字語のような特定の何かを意味するものではない」と言ってくれたが、ChatGPT と Claude は平気でハルシネーションしてきた ↩︎

Discussion