Simulating 8 Types of Color Blindness in the Browser With 3×3 Matrices
Protanopia, deuteranopia, tritanopia — all three are 3×3 matrix multiplications in linear RGB space. Once you have the matrices and handle the sRGB gamma correctly, simulating color vision deficiency is a pixel-by-pixel transform. This tool runs it live on any image you drop in, plus a WCAG contrast checker on the side.
Color vision deficiency affects 8% of men and 0.5% of women. Most designs silently fail for these users — green "success" and red "error" on an interface look identical to someone with deuteranopia. A tool that shows your colors through different eyes helps you design more inclusively.
🔗 Live demo: https://sen.ltd/portfolio/colorblind-sim/
📦 GitHub: https://github.com/sen-ltd/colorblind-sim
Features:
- 8 CVD simulations (Protanopia, Protanomaly, Deuteranopia, Deuteranomaly, Tritanopia, Tritanomaly, Achromatopsia, Normal)
- Image drag-drop with side-by-side simulation grid
- WCAG contrast ratio checker (AA / AAA)
- Hex color picker for point testing
- Japanese / English UI
- Zero dependencies, 69 tests
The math: linear RGB + matrix multiply
The simulation matrices are defined in linear RGB space (not sRGB gamma-corrected). Skipping the gamma correction produces visibly wrong results:
// sRGB → linear
export function sRGBToLinear(v) {
const c = v / 255;
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}
// linear → sRGB
export function linearToSRGB(v) {
const c = v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
return Math.round(Math.max(0, Math.min(1, c)) * 255);
}
These are the official sRGB transfer functions. The curve is approximately gamma 2.2 but with a linear section near black.
Then applying a simulation matrix:
export function applyMatrix(rgb, matrix) {
const r = sRGBToLinear(rgb.r);
const g = sRGBToLinear(rgb.g);
const b = sRGBToLinear(rgb.b);
const newR = matrix[0][0]*r + matrix[0][1]*g + matrix[0][2]*b;
const newG = matrix[1][0]*r + matrix[1][1]*g + matrix[1][2]*b;
const newB = matrix[2][0]*r + matrix[2][1]*g + matrix[2][2]*b;
return {
r: linearToSRGB(newR),
g: linearToSRGB(newG),
b: linearToSRGB(newB),
};
}
Three multiplies and three adds per channel, bookended by gamma correction. Fast enough to run on every pixel of a 1000×1000 image in under a second.
The matrices
Protanopia (red-blind, missing L-cones):
0.567 0.433 0.000
0.558 0.442 0.000
0.000 0.242 0.758
Deuteranopia (green-blind, missing M-cones):
0.625 0.375 0.000
0.700 0.300 0.000
0.000 0.300 0.700
Tritanopia (blue-blind, missing S-cones):
0.950 0.050 0.000
0.000 0.433 0.567
0.000 0.475 0.525
These come from published research (Machado, Oliveira, and Fernandes 2009). The "anomaly" variants (protanomaly, deuteranomaly, tritanomaly) are weaker versions that represent shifted cone sensitivity rather than complete absence — produced by interpolating between the dichromacy matrix and the identity matrix.
WCAG contrast checker
The secondary tool: enter two colors, get the contrast ratio:
export function luminance(rgb) {
const r = sRGBToLinear(rgb.r) / 255 * 255; // skip normalization
const g = sRGBToLinear(rgb.g);
const b = sRGBToLinear(rgb.b);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
export function contrastRatio(c1, c2) {
const l1 = luminance(c1);
const l2 = luminance(c2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
The coefficients (0.2126 / 0.7152 / 0.0722) come from the human eye's peak sensitivity to green, secondarily to red, least to blue. The +0.05 offset prevents division by zero for black.
WCAG thresholds:
- AA normal text: ≥ 4.5:1
- AA large text: ≥ 3:1
- AAA normal text: ≥ 7:1
- AAA large text: ≥ 4.5:1
Black on white hits the maximum 21:1. Light gray on white might be 2:1 — fine for decoration but fails AA for body text.
Why this matters
"Red means error" is a bad UX convention because 1 in 12 men can't distinguish red from green reliably. The correct pattern is to use red + icon + text — redundancy across modalities. A simulator that shows designers exactly what their interface looks like through deuteranopia makes this concrete in a way reading specs doesn't.
Series
This is entry #68 in my 100+ public portfolio series.
- 📦 Repo: https://github.com/sen-ltd/colorblind-sim
- 🌐 Live: https://sen.ltd/portfolio/colorblind-sim/
- 🏢 Company: https://sen.ltd/

Top comments (0)