🧮

アルファチャンネルを持つ色のコントラスト比の計算

こんにちは。Ubieでデザイン、フロントエンド関連のお手伝いをしています、腹筋ローラーの力を信じろです。腹筋しろよ(私は最近してません)。

Qiita/アクセシビリティ Advent Calendar 2023の25日目を滑り込み(2日遅れ)で書いていきます。

サイトのグレーを定義する場合に、アルファチャンネル(透過度)を設定することがあります。グレーを colorborder-color に指定することで、背景色など、他要素との馴染みがよくなります。

UbieのデザインシステムであるUbie Vitalsでも、カラーパレットに採用したグレー(ブラック)にはアルファチャンネルを含めています(Primitive Colors | Ubie Vitals)。
※Ubie Vitalsについては、年明け特集記事が公開されますのでお楽しみに!

ただし、半透明な色のコントラストを求めようとした場合、対応するツールやスクリプトは多くありません。

今回は、自前のスクリプト(JavaScript)でコントラストを計算する方法を検討します。

アイデア

WCAGが定義するコントラストの計算方法では、不透明な色同士のコントラスト方法に対応しています。
不透明な色の上に透過度を含む色を重ねれば不透明な色が得られます。不透明な白の上に比較したい色を重ねていくことで、コントラスト比が計算可能である色が取り出せます。

  1. ベースとして #ffffff の上に後景色を重ねる・・・a1
  2. a1の上に前景色を重ねる・・・a2
  3. a1a2のコントラスト比を計算

実際には後景色の更に後ろには常に白が存在するとは限りませんので、あくまで目安になります。

正直明確に正しい方針かどうか不安があります。もっと良いやり方があれば、情報お待ちしております!

コード

全体のコードは https://github.com/8845musign/calc-contrast を見てください。計算に小数点が絡むため、Decimal.js を導入しています。

記事中ではわかりやすさのために Decimal.js を排除しています。

全体の流れ

処理の流れです。contrast.tscontrast() を抜粋します。

contrast.ts
export const contrast = (backgroundColor: string, forgroundColor: string) => {
  const rgbBg = hexToRgb(backgroundColor);
  const rgbFor = hexToRgb(forgroundColor);

  const blendedBackground = blend(white, rgbBg);
  const blendedForground = blend(blendedBackground, rgbFor);

  return _contrast(blendedBackground, blendedForground);
};
  1. HEX形式で受け取った色を、RGB(もしくはRGBA)のオブジェクトに変換
  2. 不透明な白と後景色を合成
  3. 合成された後景色と前景色を合成
  4. コントラスト比を計算

というシンプルな流れです。

ちなみに、HEXでのアルファチャンネルを含む色の表現は、# + 8桁の文字列で表現されます。例えば、48%の#16191fであれば 16191f7a と表記することができます。
変換自体は hexToRgba.ts で行っていますので、ご興味があればのぞいてみてください。

色の合成

色をRGBAに変換してそれぞれの原色ごとに色を合成していきます。合成自体は簡単です。

// わかりやすさのためにJavaScriptで表記しています
const blendColor = (backgroundColor, forgroundColor, forgroundAlpha) => {
  return backgroundColor * (1 - forgroundAlpha) + forgroundColor * forgroundAlpha;
};  

※実際のコードは blend.ts にあります。

これは複数のサイトを見たり、ChatGPTに質問したりした結果得られた計算式です。根拠となる定義がどこに存在するかはわかりません……。
ただし、テストを書いて計算が正しいことを確認しています。

ChatGPTに聞いてみた。アルファチャンネルを含む色の合成計算式の根拠は?

https://chat.openai.com/share/5b853297-738f-49e6-866b-ae517d8b7934
※情報の正確性はわかりません


腹筋: この計算式の原典は何ですか?

ChatGPT:
RGBAの合成に関する計算式の原典を特定するのは難しいですが、この式はコンピュータグラフィックスとデジタルイメージングの分野で広く使われています。特に、PorterとDuffによる1984年の論文「Compositing Digital Images」が、デジタルイメージングにおけるアルファブレンディングの概念を広めるのに重要な役割を果たしました。

この論文では、異なる透明度を持つイメージを合成する際の数学的な基礎が紹介されており、現代の多くのイメージ処理アルゴリズムやソフトウェアの基礎となっています。RGBAの合成式は、この論文で紹介された概念に基づいており、後に様々な形で拡張されてきました。

ただし、RGBA合成の具体的な式は様々なソースや実装で見ることができ、特定の原典に帰属するものではなく、むしろコンピュータグラフィックスの分野で発展してきた一連の技術と概念の集大成と言えます。

この計算をR・G・Bごとに繰り返していきます。本来は、アルファチャンネルの合成も行わねばなりません。しかし、今回は不透明な色を起点に合成を行うためスキップしています。

コントラスト比の計算

本記事でコントラスト比の計算に触れると、記事が長大になるため割愛いたします🙏
興味がある方はcontrast.tsをご参照ください。

また、比較的参考となる記事もネットには存在しますし、ライブラリも豊富です。

いくつかのWCAGのドキュメントの中でも計算については触れられています。WCAG2.1関連ドキュメントの日本語訳を掲載しておきます。

計算のチェック方法

既存のツールを使って計算の確認を行います。

半透明のコントラストをチェックするツールが少ない言っても、ゼロではありません。Colour Contrast Analyzer (CCA)は良い選択肢のひとつです。Windows/Mac両対応かつ無料で利用することができます。

Colour Contrast Analyzerのスクリーンショット。白背景に半透明なグレーを重ねた場合のコントラスト比が計算できている

また、各種デザインツールで色の合成を行い、得られた不透明色を既存ツールに入れるのも良いでしょう。私の場合はFigmaで実際の色を重ねながら色を取り出しました(色を重ねた様子をスクショに貼り、Figmaにスクショをインポートして色を拾う人力作業)。

Figmaのスクリーンショット。乱雑に矩形が並べられている……
ぐちゃぐちゃ……

なお、チェック自体はVitestのin-source testingを使って行いました。その場でテストがかけるので楽ちんです。

contrast.ts
export const contrast = (backgroundColor: string, forgroundColor: string) => {
...
};

...
if (import.meta.vitest) {
  const { it, expect } = import.meta.vitest;

  it("should return the correct contrast ratio", () => {
    expect(contrast("#BAF7CD", "#3636A1")).toBe(7.854663932468741);
  });

  it("Alpha Black and alpha Pink have low contrast.", () => {
    expect(contrast("#16191f0a", "#da317080")).toBe(2.098695931631442);
  });

  it("Alpha Black and white have high contrast.", () => {
    expect(contrast("#16191fa3", "#FFFFFF")).toBe(5.26190355827513);
  });
}

終わりに

Webにおけるアルファチャンネルの利用は浸透してきたにも関わらず、コントラスト比をチェックするツールが限定的なのは少し驚きました。
特定のケースで有用な場合を想定すれば、計算自体は可能です。今後便利なツールがどんどん開発されることを願います!

Ubie テックブログ

Discussion