🎨

誰もが使いやすいデザインへ - 色覚シミュレーターで学ぶアクセシビリティの実践

に公開

はじめに - なぜ色覚シミュレーションが必要なのか

「このボタン、緑色で目立つから大丈夫だよね」
「エラーは赤、成功は緑で統一しよう」

こんな会話、開発現場でよく耳にしませんか。しかし、実は色覚特性を持つ方にとって、赤と緑の区別は難しいことがあるんですね。

日本では約20人に1人が色覚特性を持つと言われています。つまり、50人規模の会社であれば2〜3人、学校の1クラスなら1〜2人はいる計算です。決して珍しいことではありません。

今回は、誰でも簡単に色覚シミュレーションができるWebツールを開発した経緯と、その技術的な実装について詳しく解説します。

色覚シミュレーターとは

https://tools.easegis.jp/ja/tools/image/color-blindness-simulator

開発した「色覚シミュレーター」は、アップロードした画像が色覚特性を持つ方にどう見えるかを視覚的に確認できるツールです。

5種類の色覚タイプ(正常色覚、1型色覚、2型色覚、3型色覚、全色盲)のシミュレーションを一度に表示できるため、デザインレビューの際に複数の視点から確認できるのが特徴です。

なぜこのツールを作ったのか

きっかけは実際の現場での悩み

あるプロジェクトで、色によるステータス表示を実装した際のことです。

レビュー担当者から「緑と赤の組み合わせだと見分けにくい人がいるかもしれない」という指摘を受けました。そのとき初めて、色覚特性について深く考えるきっかけになったんですね。

既存のツールも調べましたが、以下のような課題がありました。

  • インストール型のソフトウェアが多く、手軽に使えない
  • 有料ツールが多く、個人開発には敷居が高い
  • シミュレーション結果を一覧表示できるツールが少ない

「それなら、自分で作ってみよう」と思い立ったのが開発のスタートでした。

色覚の種類と特徴

実装を始める前に、色覚のメカニズムを理解する必要がありました。

人間の目には、赤(L錐体)、緑(M錐体)、青(S錐体)の3種類の錐体細胞があります。これらの組み合わせで色を認識するのですが、色覚特性を持つ方はいずれかの錐体細胞の機能が弱かったり欠けていたりするんですね。

1型色覚(P型・Protanopia)

赤色を感じるL錐体の機能が弱い、または欠けている状態です。
赤と緑、赤と茶色の区別が難しくなります。

2型色覚(D型・Deuteranopia)

緑色を感じるM錐体の機能が弱い、または欠けている状態です。
赤と緑の区別が難しく、1型色覚と似た見え方になります。

3型色覚(T型・Tritanopia)

青色を感じるS錐体の機能が弱い、または欠けている状態です。
青と黄色、紫と赤の区別が難しくなります。比較的まれなタイプです。

全色盲(Achromatopsia)

すべての錐体細胞が機能せず、色をまったく認識できない状態です。
モノクロでしか世界が見えません。非常にまれですね。

技術的な実装

ここからは、実際の実装について解説します。

色変換アルゴリムの選定

色覚シミュレーションには、科学的に検証された変換行列を使用する必要があります。

いくつかの論文を調べた結果、Brettel, Viénot and Mollon (1997) の研究に基づく変換行列を採用しました。この研究は色覚シミュレーションの分野で広く参照されている信頼性の高いものです。

RGB変換行列の実装

各色覚タイプに対応した変換行列を定義します。

const transformColor = (r: number, g: number, b: number, type: ColorBlindnessType) => {
  // RGB値を0-1に正規化
  let red = r / 255;
  let green = g / 255;
  let blue = b / 255;

  let newR: number, newG: number, newB: number;

  switch (type) {
    case 'protanopia': // 1型色覚(赤色弱)
      newR = 0.567 * red + 0.433 * green + 0.0 * blue;
      newG = 0.558 * red + 0.442 * green + 0.0 * blue;
      newB = 0.0 * red + 0.242 * green + 0.758 * blue;
      break;

    case 'deuteranopia': // 2型色覚(緑色弱)
      newR = 0.625 * red + 0.375 * green + 0.0 * blue;
      newG = 0.7 * red + 0.3 * green + 0.0 * blue;
      newB = 0.0 * red + 0.3 * green + 0.7 * blue;
      break;

    case 'tritanopia': // 3型色覚(青色弱)
      newR = 0.95 * red + 0.05 * green + 0.0 * blue;
      newG = 0.0 * red + 0.433 * green + 0.567 * blue;
      newB = 0.0 * red + 0.475 * green + 0.525 * blue;
      break;

    case 'achromatopsia': // 全色盲
      const gray = 0.299 * red + 0.587 * green + 0.114 * blue;
      newR = gray;
      newG = gray;
      newB = gray;
      break;

    default:
      return { r, g, b };
  }

  // 0-255の範囲に戻す
  return {
    r: Math.max(0, Math.min(255, Math.round(newR * 255))),
    g: Math.max(0, Math.min(255, Math.round(newG * 255))),
    b: Math.max(0, Math.min(255, Math.round(newB * 255))),
  };
};

変換行列の係数は、人間の錐体細胞の分光感度特性に基づいて算出されています。正規化と逆正規化を行うことで、計算精度を保ちつつ実装しています。

Canvas APIによる画像処理

ブラウザ上で完結する画像処理には、HTML5のCanvas APIを使用しました。

useEffect(() => {
  if (!image) return;

  const img = new Image();
  img.onload = () => {
    // 画像サイズの調整
    const maxWidth = 400;
    const maxHeight = 400;
    let width = img.width;
    let height = img.height;

    if (width > maxWidth || height > maxHeight) {
      const ratio = Math.min(maxWidth / width, maxHeight / height);
      width = width * ratio;
      height = height * ratio;
    }

    // 各色覚タイプごとに処理
    simulationTypes.forEach((simType) => {
      const canvas = canvasRefs.current[simType.id];
      if (!canvas) return;

      const ctx = canvas.getContext('2d');
      if (!ctx) return;

      canvas.width = width;
      canvas.height = height;

      // 元画像を描画
      ctx.drawImage(img, 0, 0, width, height);

      // 正常色覚以外は色変換を適用
      if (simType.id !== 'normal') {
        const imageData = ctx.getImageData(0, 0, width, height);
        const data = imageData.data;

        // ピクセル単位で変換
        for (let i = 0; i < data.length; i += 4) {
          const r = data[i];
          const g = data[i + 1];
          const b = data[i + 2];

          const transformed = transformColor(r, g, b, simType.id);

          data[i] = transformed.r;
          data[i + 1] = transformed.g;
          data[i + 2] = transformed.b;
        }

        ctx.putImageData(imageData, 0, 0);
      }
    });

    setIsProcessing(false);
  };
  img.src = image;
}, [image]);

getImageDataでピクセルデータを取得し、RGBA値の配列として処理します。4つの要素(R, G, B, A)が1ピクセルを表すため、4つずつループで処理していますね。

クリップボード対応の実装

デザイナーがスクリーンショットをすぐに確認できるよう、クリップボードからの貼り付けに対応しました。

useEffect(() => {
  const handlePaste = async (e: ClipboardEvent) => {
    const target = e.target as HTMLElement;
    // 入力フォームでのペーストは無視
    if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
      return;
    }

    const items = e.clipboardData?.items;
    if (!items) return;

    for (let i = 0; i < items.length; i++) {
      const item = items[i];
      if (item.type.startsWith('image/')) {
        e.preventDefault();
        const blob = item.getAsFile();
        if (!blob) continue;

        const reader = new FileReader();
        reader.onload = (event) => {
          const imageUrl = (event.target?.result as string) + '#' + Date.now();
          setImage(imageUrl);
        };
        reader.readAsDataURL(blob);
        break;
      }
    }
  };

  document.addEventListener('paste', handlePaste);
  return () => {
    document.removeEventListener('paste', handlePaste);
  };
}, []);

Clipboard APIを使うことで、クリップボードにコピーされた画像を直接読み込めるようになりました。URL末尾にタイムスタンプを付加することで、同じ画像を貼り付けた際のキャッシュ問題も回避していますね。

レイアウトの工夫

UIデザインでは、比較しやすさを重視しました。

左側に正常色覚、右側に4種類の色覚特性を並べることで、一目で違いがわかるレイアウトにしています。

<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
  {/* 左: 正常色覚 (1カラム) */}
  <div className="lg:col-span-1">
    {/* 正常色覚の表示 */}
  </div>

  {/* 右: 色覚特性4種類 (2カラム) */}
  <div className="lg:col-span-2">
    <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
      {/* 4種類の色覚シミュレーション */}
    </div>
  </div>
</div>

レスポンシブ対応により、スマートフォンでも快適に利用できます。

開発中に学んだこと

パフォーマンスの最適化

当初、画像をアップロードするたびに5つのシミュレーションを順次処理していたため、大きな画像では処理に時間がかかりました。

そこで、以下の最適化を行いました。

画像を最大400x400pxにリサイズすることで、処理時間を大幅に短縮
全色覚タイプを並列処理することで、ユーザー体験を向上

リサイズによって若干の品質低下はあるものの、色覚シミュレーションの目的には十分な精度を保てることを確認しました。

ブラウザ互換性への配慮

Clipboard APIは比較的新しいAPIのため、古いブラウザでは動作しないことがあります。

そのため、ファイル選択による従来のアップロード方法も併用し、どの環境でも基本機能が使えるようにしました。

const pasteFromClipboard = async () => {
  try {
    const clipboardItems = await navigator.clipboard.read();
    // クリップボード処理
  } catch (error) {
    console.error('Failed to read from clipboard:', error);
    alert('クリップボードからの貼り付けに失敗しました');
  }
};

エラーハンドリングを適切に行うことで、ユーザーに何が起きたかを伝えられるようにしています。

アクセシビリティ配慮の実践例

このツールを使うことで、具体的にどんな配慮ができるのか、いくつか例を挙げます。

エラーメッセージの改善

悪い例:「エラー」という文字を赤色で表示
良い例:赤色 + × アイコン + 「エラー」というテキスト

色だけに頼らず、アイコンとテキストを併用することで、色覚に依存しない情報伝達が可能になりますね。

グラフや図表の配慮

悪い例:赤・緑・青のみで折れ線グラフを描画
良い例:色 + 線のスタイル(実線・破線・点線)+ データラベル

色以外の視覚的差異を持たせることで、誰でも理解しやすいグラフになります。

リンクの視認性

悪い例:青色の文字のみでリンクを表示
良い例:青色 + 下線 + ホバー時のスタイル変化

Webアクセシビリティガイドライン(WCAG)でも、リンクは色だけでなく下線などの視覚的手がかりを持つことが推奨されています。

今後の展望

現在のバージョンでは基本的な色覚シミュレーションを実装していますが、今後は以下の機能追加を検討しています。

色覚タイプごとの推奨配色の提案機能
Webページ全体をシミュレーションできるブラウザ拡張機能
デザインツール(Figma、Adobe XD)との連携

特にブラウザ拡張機能は需要がありそうだな、と感じています。既存のWebサイトが色覚特性を持つ方にどう見えるかを、その場で確認できるようになると便利ですよね。

まとめ

色覚シミュレーターを開発したことで、アクセシビリティの重要性を身をもって理解できました。

技術的には、Canvas APIによる画像処理と科学的な変換行列の実装により、ブラウザ上で高精度なシミュレーションを実現しています。

デザインやWebサイト制作の際に、ぜひこのツールを活用してみてください。色覚特性を持つ方にも配慮したデザインは、結果的にすべてのユーザーにとって使いやすいデザインになるはずです。

ツールはこちらから無料で利用できます。
https://tools.easegis.jp/ja/tools/image/color-blindness-simulator

最後まで読んでいただき、ありがとうございました!

Discussion