DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

How a Colour-Blindness Simulator Actually Works: sRGB to LMS Cones and a Projection

Roughly 1 in 12 men can't reliably tell your red from your green. If you design interfaces, you want to see what they see — and it turns out you can render it yourself with nothing but a canvas and a few matrix multiplications. No library, no filter hacks. Here's the exact pipeline.

Vision runs on three cones

Human colour vision is trichromatic: three cone types — L (long / reddish), M (medium / greenish), and S (short / bluish). Every colour you perceive is just how hard those three fire. In the common colour-vision deficiencies, one cone type is missing, so a whole dimension of colour collapses: reds and greens fold onto the same murky yellows.

Step 1 — undo the gamma

The 0–255 bytes in an image are gamma-encoded sRGB, spaced for perception rather than proportional to physical light. Cone responses are linear in light, so you must convert to linear before any matrix touches the colour, and convert back afterward. Skip this and the simulated colours come out visibly wrong.

function srgbToLinear(c){           // 0..255 -> 0..1 linear
  c /= 255;
  return c <= 0.04045 ? c / 12.92
                      : Math.pow((c + 0.055) / 1.055, 2.4);
}
Enter fullscreen mode Exit fullscreen mode

Step 2 — into cone space, then project

A fixed 3×3 matrix maps linear RGB to the LMS cone responses. This is the only space where "the L cone is missing" is a clean, removable coordinate rather than a tangled mix of channels. Each deficiency is then a small matrix that rebuilds the missing cone's signal from the two that remain — projecting 3-D colour onto the 2-D plane a dichromat can still separate:

const RGB_TO_LMS = [
  [0.31399022, 0.63951294, 0.04649755],   // L (long / red)
  [0.15537241, 0.75789446, 0.08670142],   // M (medium / green)
  [0.01775239, 0.10944209, 0.87256922],   // S (short / blue)
];
const SIM = {
  normal:       [[1,0,0], [0,1,0], [0,0,1]],
  protanopia:   [[0, 1.05118294, -0.05116099], [0,1,0], [0,0,1]],
  deuteranopia: [[1,0,0], [0.9513092, 0, 0.04866992], [0,0,1]],
  tritanopia:   [[1,0,0], [0,1,0], [-0.86744736, 1.86727089, 0]],
};
Enter fullscreen mode Exit fullscreen mode

One pixel, end to end

Chain it together: linearise, go to LMS, apply the deficiency matrix, invert back to linear RGB, re-apply gamma. Normal vision short-circuits to an exact copy:

function simRGB(r, g, b, type){
  if (type === "normal") return [r, g, b];                 // identity
  const lr = S2L[r], lg = S2L[g], lb = S2L[b];             // 1. linear
  const M = RGB_TO_LMS;
  const L = dot(M[0], lr, lg, lb),
        Q = dot(M[1], lr, lg, lb),
        S = dot(M[2], lr, lg, lb);                          // 2. LMS
  const P = SIM[type];
  const L2 = dot(P[0], L, Q, S),
        Q2 = dot(P[1], L, Q, S),
        S2 = dot(P[2], L, Q, S);                            // 3. project
  const N = LMS_TO_RGB;
  return [                                                  // 4. + 5.
    linearToSrgb(dot(N[0], L2, Q2, S2)),
    linearToSrgb(dot(N[1], L2, Q2, S2)),
    linearToSrgb(dot(N[2], L2, Q2, S2)),
  ];
}
Enter fullscreen mode Exit fullscreen mode

Total colour blindness (achromatopsia) is the degenerate case — no cone matrix, because there's no colour left. Just take the Rec.709 luminance in linear light and write it into all three channels.

Why red/green really does merge

Because a deficiency projects colour onto a plane, two very different originals can land on the same spot. You can turn that from a vibe into a number: simulate both colours, convert each to CIE-Lab, and take the ΔE distance between them. A large ΔE means they stay distinct; a tiny one means the deficiency merged them. Pure red run through deuteranopia lands near [156, 156, 0] — a dark yellow — which is the signature of a green-blind eye.

This is not a WCAG contrast checker. That measures luminance ratios; this simulates colour vision itself. The practical takeaway: never encode meaning in hue alone — add an icon, a label, or a shape.

Drop in your own image (read locally, never uploaded), flip between the four deficiencies, and run the contrast-collapse checker live here: https://dev48v.infy.uk/solve/day55-color-blindness-simulator.html

Top comments (0)