Oh MyUtils

색각 이상 시뮬레이터 - 색맹 접근성 테스트 온라인

적색맹, 녹색맹, 청색맹 등 색각 이상자에게 색상이 어떻게 보이는지 시뮬레이션. WCAG 명도 대비 비율 확인.

자주 묻는 질문

색각 이상 시뮬레이터란 무엇인가요?

색각 이상 시뮬레이터는 색각 결핍(CVD)을 가진 사람들에게 색상과 이미지가 어떻게 보이는지 보여주는 온라인 도구입니다. 과학적으로 검증된 알고리즘을 적용하여 색상을 변환하며, 디자이너와 개발자가 남성의 약 8%, 여성의 약 0.5%에 해당하는 색각 이상자에게 시각적 콘텐츠가 어떻게 인식되는지 이해하는 데 도움을 줍니다.

이 도구는 어떻게 사용하나요?

이 도구에는 두 가지 모드가 있습니다. 색상 모드: HEX 값, RGB 값 또는 색상 선택기를 사용하여 전경색과 배경색을 입력하면 8가지 색각 이상 유형 모두에서 이 색상이 어떻게 보이는지 WCAG 대비 비율과 함께 즉시 표시됩니다. 이미지 모드: 이미지를 업로드(드래그 앤 드롭 또는 파일 찾아보기)하고 CVD 유형을 선택하면 원본과 시뮬레이션된 버전을 나란히 볼 수 있습니다. 시뮬레이션된 이미지를 다운로드할 수도 있습니다.

내 데이터는 안전한가요? 서버로 전송되나요?

데이터는 100% 안전하며 브라우저를 벗어나지 않습니다. 모든 색상 계산, WCAG 대비 검사, 이미지 처리는 JavaScript와 Canvas API를 사용하여 전적으로 클라이언트 측에서 수행됩니다. 색상이나 이미지가 어떤 서버로도 전송되지 않으므로 독점 디자인, 기밀 브랜드 자산, 민감한 시각적 콘텐츠를 안전하게 테스트할 수 있습니다.

8가지 색각 이상 유형은 무엇인가요?

8가지 유형은 다음과 같습니다: 제1색맹(적색맹, 적색 원추세포 없음), 제2색맹(녹색맹, 가장 흔한 이색형), 제3색맹(청색맹, 청황 혼동), 제1색약(적색 감도 감소), 제2색약(녹색 감도 감소, 남성의 ~5%에 영향을 미치는 가장 흔한 CVD), 제3색약(청색 감도 감소, 매우 드묾), 전색맹(완전한 색맹, 회색조만 인식), 부분색맹(부분적 색맹, 심각하게 감소된 색상 인식).

WCAG 대비 검사기는 색각 이상 시뮬레이션과 어떻게 작동하나요?

이 도구는 전경색과 배경색 사이의 표준 WCAG 2.1 대비 비율을 계산한 다음, 각 CVD 시뮬레이션을 적용한 후 대비를 재계산합니다. 이를 통해 정상 시력에서는 WCAG 기준을 통과하지만 색각 이상자에게는 실패하는 경우를 발견할 수 있으며, 이는 진정으로 접근성 있는 디자인을 위한 핵심적인 통찰입니다.

이 도구는 어떤 시뮬레이션 알고리즘을 사용하나요?

이 도구는 동료 심사를 거친 과학적으로 검증된 알고리즘을 사용합니다: 이색형(제1색맹, 제2색맹, 제3색맹)에는 Brettel, Vienot & Mollon(1997) 알고리즘을, 이상삼색형(제1색약, 제2색약, 제3색약)에는 Machado, Oliveira & Fernandes(2009) 알고리즘을 사용합니다. 전색맹에는 ITU-R BT.709 휘도 가중치를 사용합니다. 정확성을 위해 모든 변환은 선형 RGB 색상 공간에서 수행됩니다.

이미지를 테스트할 수 있나요?

네. 이미지 탭에서 최대 4096x4096 픽셀의 이미지(JPG, PNG, WebP, GIF)를 업로드할 수 있습니다. 이 도구는 Canvas API를 사용하여 픽셀 수준의 CVD 시뮬레이션을 적용하며, 원본과 시뮬레이션된 이미지를 나란히 표시합니다. 접근성 감사나 문서화를 위해 시뮬레이션된 이미지를 다운로드할 수 있습니다.

-anopia(색맹)와 -anomaly(색약)의 차이점은 무엇인가요?

접미사 "-anopia(색맹)"는 원추세포 유형의 완전한 부재(이색형)를 의미하고, "-anomaly(색약)"는 감도 감소(이상삼색형)를 의미합니다. 예를 들어, 제1색맹(Protanopia)은 적색 원추세포가 전혀 없는 것이고, 제1색약(Protanomaly)은 적색 원추세포가 있지만 감도가 변이된 것입니다. 색약 유형은 일반적으로 색맹 유형보다 가볍고 더 흔합니다.

코드 예제

// Color Blindness Simulator - JavaScript Implementation

/**
 * sRGB to Linear RGB conversion
 */
function srgbToLinear(c) {
  const v = c / 255;
  return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
}

function linearToSrgb(c) {
  const v = Math.max(0, Math.min(1, c));
  return Math.round(
    (v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055) * 255
  );
}

/**
 * CVD simulation matrices (Brettel 1997 / Machado 2009)
 * Each is a 3x3 matrix stored as flat array [row-major]
 */
const CVD_MATRICES = {
  protanopia: [
    0.152286, 1.052583, -0.204868,
    0.114503, 0.786281, 0.099216,
    -0.003882, -0.048116, 1.051998,
  ],
  deuteranopia: [
    0.367322, 0.860646, -0.227968,
    0.280085, 0.672501, 0.047413,
    -0.011820, 0.042940, 0.968881,
  ],
  tritanopia: [
    1.255528, -0.076749, -0.178779,
    -0.078411, 0.930809, 0.147602,
    0.004733, 0.691367, 0.303900,
  ],
  achromatopsia: [
    0.2126, 0.7152, 0.0722,
    0.2126, 0.7152, 0.0722,
    0.2126, 0.7152, 0.0722,
  ],
};

/**
 * Simulate color vision deficiency for a single RGB color
 * @param {number} r - Red (0-255)
 * @param {number} g - Green (0-255)
 * @param {number} b - Blue (0-255)
 * @param {string} cvdType - CVD type key
 * @param {number} severity - 0.0 to 1.0
 * @returns {number[]} Simulated [r, g, b]
 */
function simulateColor(r, g, b, cvdType, severity = 1.0) {
  const matrix = CVD_MATRICES[cvdType];
  if (!matrix) throw new Error(`Unknown CVD type: ${cvdType}`);

  // Convert to linear
  const lr = srgbToLinear(r);
  const lg = srgbToLinear(g);
  const lb = srgbToLinear(b);

  // Apply matrix
  const sr = matrix[0] * lr + matrix[1] * lg + matrix[2] * lb;
  const sg = matrix[3] * lr + matrix[4] * lg + matrix[5] * lb;
  const sb = matrix[6] * lr + matrix[7] * lg + matrix[8] * lb;

  // Lerp with original for severity
  const fr = lr * (1 - severity) + sr * severity;
  const fg = lg * (1 - severity) + sg * severity;
  const fb = lb * (1 - severity) + sb * severity;

  return [linearToSrgb(fr), linearToSrgb(fg), linearToSrgb(fb)];
}

/**
 * Calculate WCAG 2.1 contrast ratio
 */
function getContrastRatio(fg, bg) {
  const lum = (r, g, b) =>
    0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);

  const l1 = lum(...fg);
  const l2 = lum(...bg);
  const lighter = Math.max(l1, l2);
  const darker = Math.min(l1, l2);
  return (lighter + 0.05) / (darker + 0.05);
}

/**
 * Check WCAG compliance
 */
function checkWCAG(ratio) {
  return {
    aa: { normalText: ratio >= 4.5, largeText: ratio >= 3 },
    aaa: { normalText: ratio >= 7, largeText: ratio >= 4.5 },
  };
}

// Usage
const fg = [255, 102, 0]; // Orange
const bg = [255, 255, 255]; // White

console.log("Original contrast:", getContrastRatio(fg, bg).toFixed(2) + ":1");

const cvdTypes = ["protanopia", "deuteranopia", "tritanopia", "achromatopsia"];
for (const type of cvdTypes) {
  const simFg = simulateColor(...fg, type);
  const simBg = simulateColor(...bg, type);
  const ratio = getContrastRatio(simFg, simBg);
  const wcag = checkWCAG(ratio);
  console.log(`${type}: ${ratio.toFixed(2)}:1 (AA: ${wcag.aa.normalText ? "PASS" : "FAIL"})`);
}

관련 도구