Skip to content

Vision Filters

Color Vision Deficiency (CVD) simulation. Lets you check whether a theme still reads under the four common cone-deficiency types and full grayscale, before pushing variables to Figma.

Source of truth: src/lib/vision-filters.ts (registry, matrices, SVG defs, JS bake helper).

Modes

ModePopulationWhat's missingEffect
Regular Vision~68%Normal trichromatic vision, all three cone types working
Protanopia~1.5%L-cones (red-blind)Reds appear dark; red, orange, and green become hard to distinguish
Deuteranopia~1.2%M-cones (green-blind)Greens shift to beige; reds shift to dark yellow. Red/green confusion
Tritanopia~0.03%S-cones (blue-blind)Blue/green merge; yellow/pink merge. Very rare
Achromatopsia<0.01%All conesGrayscale only — useful to verify contrast holds without colour

Where to switch modes

The vision-mode picker is a Select labelled "Vision simulation" in the bottom status bar (StatusBar.tsx), not in the preview filter bar. It was moved out of the preview's right-hand column so the filter bar stays focused on what's currently being computed (entities, layers, swatch direction) and the global colour-vision lens reads as a viewing-mode toggle rather than another filter on top of the data. The select item labels carry the population percentage inline (e.g. "Protanopia (~1.5%)"); hovering an option live-previews that mode.

Scope (Settings → Editor Display → Vision simulation)

The active CVD filter applies to one of three scopes (a Preview / Editor / All segmented control). Note the scope segmented control and the mode select both carry the label "Vision simulation" — the scope picker is in Settings, the mode picker is in the status bar:

  • Preview (default) — only preview swatches are simulated; the editor stays at true colour so you can edit raw hex values without the filter altering what you see in the picker
  • Editor — only the editor (palette + entity rows) is simulated; preview shows true colour
  • All — both the editor (active-tab content) and the preview panel are simulated; the status bar, window header, and chat panel stay at true colour

How it works

Two parallel implementations:

  1. Live preview — a hidden SVG <defs> block (injected via visionFilterDefs()) registers <filter id="vision-protanopia">…<feColorMatrix>…</filter> for each non-regular mode. The active scope applies the filter as an inline filter: url(#vision-deuteranopia) CSS style on the relevant container: the preview canvas for preview, the editor panel (via a visionFilter prop) for editor, and both the editor (active-tab content) and the preview panel for all. Browser handles the matrix multiply on the GPU; zero per-frame cost. SVG defs are cached in _visionFilterDefsCache so repeated mounts don't regenerate the string. Hovering an option in the "Vision simulation" select live-previews that mode through the same inline filter before you commit.

  2. Paste-to-canvas baking — Figma's createNodeFromSvg() ignores feColorMatrix, so the preview's SVG export is run through simulateSvg(svg, mode) first. That pass walks every fill="#hex" / stroke="#hex" attribute, applies the same 3×3 matrix in JS (simulateHex), and writes the resulting hex back. The pasted Figma frame matches what the preview showed pixel-for-pixel.

The matrices themselves are the standard Brettel/Vienot/Mollon coefficients — see MATRICES in vision-filters.ts for the exact rows.

Useful checks

  • Protanopia + Deuteranopia together — the two red-green cases cover the bulk of CVD users (~2.7%). If a theme's status colours (success / warning / error) survive both, it's safe.
  • Achromatopsia — verifies that contrast (not hue) is doing the work. If a button vanishes in grayscale, the theme is leaning on chromatic distinction alone and will fail for low-vision users too.
  • Tritanopia — rarely the bottleneck, but worth checking if the brand leans heavily on blue/green pairings.