Blink レンダラで色覚異常をシミュレートする

この記事では、DevTools と Blink レンダラで色覚異常シミュレーションを実装した理由とその方法について説明します。

背景: 色のコントラストが不適切

低コントラストのテキストは、ウェブで自動検出されるユーザー補助の問題として最も多いものです。

ウェブでのユーザー補助に関する一般的な問題のリスト。よくある問題は、低コントラストのテキストです。

WebAIM による、上位 100 万ウェブサイトのアクセシビリティ分析によると、ホームページの 86% 以上でコントラストが低いことがわかっています。平均すると、各ホームページに低コントラスト テキストが 36 個ずつあります。

DevTools を使用したコントラストの問題の検出、把握、修正

デベロッパーやデザイナーは Chrome DevTools を使用して、ウェブ アプリのコントラストを調整したり、より利用しやすいカラーパターンを選択したりできます。

最近このリストに新しいツールが追加されました。これは他とは少し異なるツールです。上記のツールは主に、コントラスト比情報の表示と、修正のためのオプションの提供に重点を置いています。しかし、DevTools にはデベロッパーがこの問題空間をより深くunderstandingするための手段がまだ欠けていることが判明しました。これに対処するために、DevTools の [Rendering] タブに視覚障がいシミュレーションを実装しました。

Puppeteer では、新しい page.emulateVisionDeficiency(type) API を使用して、これらのシミュレーションをプログラムで有効にできます。

色覚異常

20 人に 1 人程度は色覚障がい(より正確には「色覚異常」という用語でも知られています)を患っています。このような不鮮明な部分は色の区別が困難になり、コントラストの問題が増幅される可能性があります

色覚異常のない、溶けたクレヨンのカラフルな写真
色覚異常のない、溶けたクレヨンのカラフルな写真
ALT_TEXT_HERE
溶けたクレヨンのカラフルな絵で色覚異常(色覚異常)をシミュレートした影響。
溶けたクレヨンのカラフルな画像で 2 型 2 色覚のシミュレーションを実施した影響
溶けたクレヨンのカラフルな画像に第 2 色覚(2 型 2 色覚)をシミュレーションした結果の影響。
溶けたクレヨンのカラフルな画像で 1 型 2 色覚をシミュレーションした結果の影響
溶けたクレヨンのカラフルな画像で 1 型 2 色覚をシミュレートした影響。
溶けたクレヨンのカラフルな画像で 3 型 2 色覚をシミュレートした影響
溶けたクレヨンのカラフルな画像で 3 型 2 色覚(3 型 2 色覚)をシミュレーションした結果の影響。

通常版のデベロッパーであれば、見た目に問題がない色ペアに対して、DevTools でコントラスト比が不適切に表示される場合があります。これは、コントラスト比の式でこうした色覚の欠陥が考慮されるためです。低コントラストのテキストでも読むことができる場合もありますが、視覚障がいのあるユーザーにはその権限はありません。

Google は、デザイナーやデベロッパーが自分のウェブアプリでこうした視覚障がいの影響をシミュレートできるようにすることで、見逃されているものを提供することを目指しています。DevTools は、コントラストの問題を検出修正できるだけでなく、問題を理解することもできるようになります。

HTML、CSS、SVG、C++ で色覚異常をシミュレートする

Blink Renderer の機能の実装について説明する前に、ウェブ テクノロジーを使用して同等の機能を実装する方法を理解しておきましょう。

このような色覚異常のシミュレーションは、それぞれページ全体を覆うオーバーレイと考えることができます。ウェブ プラットフォームには、そのための機能が用意されています。それは、CSS フィルタです。CSS の filter プロパティを使用すると、blurcontrastgrayscalehue-rotate などの事前定義されたフィルタ関数を使用できます。さらに細かく制御するために、filter プロパティは、カスタム SVG フィルタ定義を指す URL も受け入れます。

<style>
  :root {
    filter: url(#deuteranopia);
  }
</style>
<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

上記の例では、カラー マトリックスに基づくカスタム フィルタ定義を使用しています。概念的には、すべてのピクセルの [Red, Green, Blue, Alpha] カラー値が行列乗算されて新しいカラー [R′, G′, B′, A′] が作成されます。

行列の各行には 5 つの値が含まれています。(左から右に)R、G、B、A の乗数と、一定のシフト値を表す 5 つ目の値です。4 行があります。行列の 1 行目は新しい赤値を計算するために使用され、2 行目は緑、3 行目は青、最後の行は Alpha です。

この例の正確な数値はどこから得られるのか、疑問に思われるかもしれません。このカラー マトリックスが第 2 色覚に優れているのは、なぜでしょうか?答えは科学です。値は、Machado、Oliveira、Fernandes 氏による生理学的に正確な色覚不全シミュレーション モデルに基づいています。

とりあえず、この SVG フィルタは、CSS を使用してページ上の任意の要素に適用できるようになりました。他の視力の問題についても、同じパターンを繰り返すことができます。どのように表示されるかは、こちらのデモでご確認ください。

必要に応じて、次のように DevTools の機能を構築できます。ユーザーが DevTools UI で視力不全をエミュレートするときに、検査対象のドキュメントに SVG フィルタを挿入してから、ルート要素にフィルタ スタイルを適用します。ただし、このアプローチにはいくつかの問題があります。

  • ページのルート要素にすでにフィルタが設定されている場合は、コードがオーバーライドします。
  • ページにすでに id="deuteranopia" の要素があり、フィルタ定義と競合している可能性があります。
  • ページは特定の DOM 構造に依存している可能性があるため、<svg> を DOM に挿入すると、これらの想定に違反する可能性があります。

エッジケースはさておき、このアプローチの主な問題は、プログラムによってページに加えられる変更が行われることです。DevTools ユーザーが DOM を検査すると、追加していない <svg> 要素や、記述していない CSS filter が突然表示されることがあります。これは混乱しそうです。この機能を DevTools に実装するには、このような欠点のないソリューションが必要です。

煩わしさを軽減する方法を見ていきましょう。このソリューションには、非表示にする必要がある部分が 2 つあります。1)filter プロパティを使用した CSS スタイルと、2)現在 DOM の一部である SVG フィルタ定義です。

<!-- Part 1: the CSS style with the filter property -->
<style>
  :root {
    filter: url(#deuteranopia);
  }
</style>
<!-- Part 2: the SVG filter definition -->
<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

ドキュメント内の SVG への依存関係を回避する

まずはパート 2 です。SVG が DOM に追加されないようにする方法です。1 つの方法として、画像を別の SVG ファイルに移すことが考えられます。上記の HTML から <svg>…</svg> をコピーして、filter.svg として保存することもできますが、まずはいくつかの変更を行う必要があります。HTML 内のインライン SVG は、HTML 解析ルールに従います。そのため、属性値を囲む引用符を省略するなど、問題を回避できます。ただし、別のファイルで SVG を使用することは有効な XML であり、XML 解析は HTML よりもはるかに厳格です。再び SVG-in-HTML スニペットは次のようになります。

<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

この有効なスタンドアロン SVG(つまり XML)を作成するには、いくつかの変更を行う必要があります。答えはわかりますか?

<svg xmlns="http://www.w3.org/2000/svg">
 
<filter id="deuteranopia">
   
<feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000"
/>
 
</filter>
</svg>

1 つ目の変更点は、最上位の XML 名前空間宣言です。2 つ目は、いわゆる「ソリダス」です。このスラッシュは、<feColorMatrix> タグで要素の開始と終了の両方を示すものです。この最後の変更は実際には必須ではありませんが(代わりに明示的な </feColorMatrix> 終了タグを使うことができます)、XML と SVG-in-HTML の両方がこの /> 省略形をサポートしているため、この省略形を利用することもできます。

いずれにせよ、これらの変更により、最終的にこれを有効な SVG ファイルとして保存し、HTML ドキュメントの CSS filter プロパティ値から参照できます。

<style>
  :root {
    filter: url(filters.svg#deuteranopia);
  }
</style>

これで、ドキュメントに SVG を挿入する必要がなくなります。その効果は格段に向上しています。しかし、現在は別のファイルに依存しています。それはやはり依存関係です。どうにかしてそれを取り除くことができるでしょうか?

結局のところ、ファイルは必要ありません。データ URL を使用すると、URL 内のファイル全体をエンコードできます。これを実現するため、先ほどの SVG ファイルのコンテンツを文字どおり取得して、data: プレフィックスを追加し、適切な MIME タイプを構成します。これにより、まったく同じ SVG ファイルを表す有効なデータ URL が得られます。

data:image/svg+xml,
  <svg xmlns="http://www.w3.org/2000/svg">
    <filter id="deuteranopia">
      <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                             0.280  0.673  0.047  0.000  0.000
                            -0.012  0.043  0.969  0.000  0.000
                             0.000  0.000  0.000  1.000  0.000" />
    </filter>
  </svg>

メリットとしては、HTML ドキュメントで使用するためだけに、ファイルをどこにでも保存したり、ディスクやネットワーク経由で読み込んだりする必要がなくなります。つまり、先ほどのようにファイル名を参照するのではなく、データ URL を指定できるようになりました。

<style>
  :root {
    filter: url('data:image/svg+xml,\
      <svg xmlns="http://www.w3.org/2000/svg">\
        <filter id="deuteranopia">\
          <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000\
                                 0.280  0.673  0.047  0.000  0.000\
                                -0.012  0.043  0.969  0.000  0.000\
                                 0.000  0.000  0.000  1.000  0.000" />\
        </filter>\
      </svg>#deuteranopia');
  }
</style>

前と同様に、URL の最後に使用するフィルタの ID を指定します。なお、URL 内の SVG ドキュメントを Base64 でエンコードする必要はありません。その場合、読みやすさが損なわれ、ファイルサイズが大きくなるだけです。データの URL の改行文字によって CSS 文字列リテラルが終わらないよう、各行の最後にバックスラッシュを追加しました。

ここまでは、ウェブ テクノロジーを使って視覚障がいをシミュレートする方法だけを説明してきました。興味深いことに、Blink レンダラの最終実装は、実際にはよく似ています。以下は、同じ手法で特定のフィルタ定義を持つデータ URL を作成するための C++ ヘルパー ユーティリティです。

AtomicString CreateFilterDataUrl(const char* piece) {
  AtomicString url =
      "data:image/svg+xml,"
        "<svg xmlns=\"http://www.w3.org/2000/svg\">"
          "<filter id=\"f\">" +
            StringView(piece) +
          "</filter>"
        "</svg>"
      "#f";
  return url;
}

ここでは、これを使用して必要なすべてのフィルタを作成する方法を示します。

AtomicString CreateVisionDeficiencyFilterUrl(VisionDeficiency vision_deficiency) {
  switch (vision_deficiency) {
    case VisionDeficiency::kAchromatopsia:
      return CreateFilterDataUrl("…");
    case VisionDeficiency::kBlurredVision:
      return CreateFilterDataUrl("<feGaussianBlur stdDeviation=\"2\"/>");
    case VisionDeficiency::kDeuteranopia:
      return CreateFilterDataUrl(
          "<feColorMatrix values=\""
          " 0.367  0.861 -0.228  0.000  0.000 "
          " 0.280  0.673  0.047  0.000  0.000 "
          "-0.012  0.043  0.969  0.000  0.000 "
          " 0.000  0.000  0.000  1.000  0.000 "
          "\"/>");
    case VisionDeficiency::kProtanopia:
      return CreateFilterDataUrl("…");
    case VisionDeficiency::kTritanopia:
      return CreateFilterDataUrl("…");
    case VisionDeficiency::kNoVisionDeficiency:
      NOTREACHED();
      return "";
  }
}

なお、この手法では、なんらかの要素を再実装したり、車輪を作り直したりすることなく、SVG フィルタの機能をフル活用することができます。Blink Renderer 機能も実装していますが、これは Web Platform を利用して実現しています。

ここまで、SVG フィルタを作成し、CSS の filter プロパティ値内で使用できるデータ URL を作成する方法を確認しました。この手法にはどのような問題がありますか?ターゲット ページにデータ URL をブロックする Content-Security-Policy が含まれている可能性があるため、すべての場合に読み込まれるデータ URL に頼ることはできません。Blink レベルの最終的な実装では、読み込み中にこうした「内部」データ URL について CSP をバイパスするよう特別な注意が必要です。

特殊なケースはさておき、Google は順調に進歩しています。同じドキュメント内にインライン <svg> が存在することに依存しなくなったため、解決策は事実上、自己完結型の CSS filter プロパティ定義を 1 つだけに縮小しました。これで、これもやめましょう。

ドキュメント内の CSS 依存関係を回避する

ここまでの内容をまとめます。

<style>
  :root {
    filter: url('data:…');
  }
</style>

今でも CSS の filter プロパティに依存していますが、実際のドキュメントでは filter がオーバーライドされて破損する可能性があります。また、DevTools で計算済みのスタイルを調べるときにもこのメッセージが表示されます。これは混乱を招く可能性があります。このような問題を回避するにはどうすればよいでしょうか。デベロッパーがプログラムで監視できるようにせずに、ドキュメントにフィルタを追加する方法を見つける必要があります。

そこで、filter のように動作するが、名前が異なる(--internal-devtools-filter など)Chrome 内部の CSS プロパティを新しく作成することにしました。その後、特別なロジックを追加して、このプロパティが DevTools や DOM での計算済みスタイルに表示されないようにすることもできます。必要な 1 つの要素、つまりルート要素でのみ動作するようにすることもできます。しかし、このソリューションは理想的とは言えません。filter で既存の機能を複製することになり、この非標準のプロパティを隠そうとしても、ウェブ デベロッパーがそれを認識して使い始める可能性はあるため、ウェブ プラットフォームには悪影響を及ぼします。CSS スタイルを DOM で監視せずに適用するための別の方法が必要です。どうすればよいですか?

CSS の仕様には、使用するビジュアル フォーマット モデルを紹介するセクションがあります。このモデルの主なコンセプトの一つはビューポートです。ユーザーがウェブページを参照する際に使用するビジュアル ビューです。密接に関連する概念として、「最初の要素を含むブロック」があります。これは、仕様レベルでのみ存在する、スタイル設定可能なビューポート <div> のようなものです。この「ビューポート」というコンセプトを、あらゆるところに表している仕様です。たとえば、コンテンツが収まらないときにブラウザがスクロールバーをどのように表示するかご存知ですか?これはすべて、この「ビューポート」に基づいて CSS 仕様で定義されています。

この viewport は、実装の詳細として Blink Renderer 内にも存在します。仕様に従ってデフォルトのビューポート スタイルを適用するコードを次に示します

scoped_refptr<ComputedStyle> StyleResolver::StyleForViewport() {
  scoped_refptr<ComputedStyle> viewport_style =
      InitialStyleForElement(GetDocument());
  viewport_style->SetZIndex(0);
  viewport_style->SetIsStackingContextWithoutContainment(true);
  viewport_style->SetDisplay(EDisplay::kBlock);
  viewport_style->SetPosition(EPosition::kAbsolute);
  viewport_style->SetOverflowX(EOverflow::kAuto);
  viewport_style->SetOverflowY(EOverflow::kAuto);
  // …
  return viewport_style;
}

C++ や Blink のスタイル エンジンの複雑さを理解しなくても、このコードがビューポート(より正確には、含まれる初期ブロック)の z-indexdisplaypositionoverflow を処理することを理解できます。これらはすべて、CSS でおなじみの概念です。スタック コンテキストに関連するその他の魔法もありますが、CSS プロパティには直接変換できませんが、全体的には、この viewport オブジェクトは DOM 要素と同様に、Blink 内から CSS を使用してスタイル設定できるものと考えることができます。ただし、DOM の一部ではありません。

これで、まさに求めているものが得られました。filter スタイルを viewport オブジェクトに適用できます。これにより、表示可能なページスタイルや DOM に干渉することなく、レンダリングに視覚的に影響を及ぼします。

まとめ

ここでの小さな取り組みをまとめると、まず、C++ ではなくウェブ テクノロジーを使用してプロトタイプを作成し、その一部を Blink Renderer に移行する作業を開始しました。

  • まず、データ URL をインライン化することで、プロトタイプをより自己完結型にしました。
  • 次に、それらの内部データ URL の読み込みを特殊なケースにして、CSP に適したものにしました。
  • スタイルを Blink 内部の viewport に移動することで、実装を DOM に依存せず、プログラムで監視不能にしました。

この実装の特徴は、最終的に、HTML/CSS/SVG のプロトタイプが最終的な技術設計に影響を与えたことです。Blink レンダラ内でも Web Platform を活用する方法が見つかりました。

詳しい背景情報については、Google の設計案または Chromium トラッキング バグをご覧ください。関連するすべてのパッチが記載されています。

プレビュー チャネルをダウンロードする

デフォルトの開発ブラウザとして、Chrome CanaryDevBeta の使用を検討してください。これらのプレビュー チャネルでは、最新の DevTools 機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたり、ユーザーが事前にサイトに関する問題を発見したりすることができます。

Chrome DevTools チームへのお問い合わせ

投稿内の新機能や変更点、または DevTools に関するその他の事項について議論するには、以下のオプションを使用します。

  • crbug.com からご提案やフィードバックをお寄せください。
  • DevTools の [その他のオプション] その他   > [ヘルプ] > [DevTools の問題を報告する] を使用して、DevTools の問題を報告する。
  • @ChromeDevTools でツイートします。
  • DevTools の新機能の YouTube 動画または DevTools のヒントの YouTube 動画で、コメントを記入してください。