The guarantee
Here it is in plain English. This is the whole promise, and it is the sentence every number on this page is measured against.
Every two colours in the set stay clearly apart for someone with typical vision, for each of the three kinds of colour blindness, and on a black-and-white screen — all at the same time, not one at a time.
checking that quote against the engine…
The words that carry the weight are all at the same time. It is easy to find colours that survive one condition. It is easy to find colours that survive another. Finding colours that survive all five simultaneously is a different, and much harder, promise — and it is the promise that runs out.
The same thing, formally
score(a, b) = min over c in {normal, protan, deutan, tritan, grey} of ΔE′CAM16-UCS( simc(a), simc(b) )
conforming(P, Δ) ⇔ for all a ≠ b in P: score(a, b) ≥ Δ
What “all five” does not cover
The word carrying the weight in that promise is all, and a word like that is only honest with its edge drawn. It means these five conditions and nothing else. The three colour-blindness rows are the complete dichromacies — a whole cone class missing. Anomalous trichromacy, where the cone is present but its sensitivity is shifted, is a different thing and it is not modelled here at all.
What the threshold means, in plain words
Distance is measured in CAM16-UCS, a space built so that equal numeric steps are meant to be equally visible steps [1]. The scale has a natural top and bottom: black to white.
Δ is not a tuning knob. The temptation in a project like this is to hit a
nice round number of colours by quietly lowering the threshold. So the deliverable here is not
one N: it is the whole curve N*(Δ), published in section 3, with the game picking
one point on it. If Δ ever moves, every number here moves with it, and this page says so out
loud.
The choice inside the number: which CAM16-UCS
There is a second thing a threshold in this metric depends on, and stating it is the
difference between a result and a claim. Li et al. 2017 publish three scalings
of the same space in the same paper [1], differing in one constant —
KL, which stretches or squashes the lightness axis before the distance is
taken. LCD is fitted to large colour differences, SCD to small
ones, and UCS across the whole range.
They are not interchangeable here, because the ceiling this page publishes is essentially a
length divided by Δ — and KL is exactly the constant that sets that
length. So the same one-line argument gives a different answer under each. All three are computed
below, live, by machinery this page first checks against the shipped engine:
Colour blindness is not a filter you bolt on
The common mental model is that colour blindness dims colours a bit, so a palette that looks bold enough will be fine. That model is wrong in a specific and destructive way: dichromacy projects colour onto a lower-dimensional space. Whole families of very different colours land on the same point. Not similar. The same.
Pick any two colours and watch what each observer condition does to the gap between them.
The number under each pair is the real ΔE′ the engine computes, and the score for
the pair is the smallest of the five — because the promise has to hold under
all of them at once.
Read this before you trust a swatch
The little colour squares are not the simulated colours. The numbers are.
A dichromat’s version of a colour is very often outside anything an sRGB screen
can physically show — about a quarter of all colours under protanopia. There is no pixel for
it. So every simulated square on this page is the closest colour the screen can show,
a stand-in, while every ΔE′ is computed from the real thing.
That is not a small caveat, so the page measures it for you instead of asking you to take it on faith. The table below has a column called picture off by: it is the distance, in the same units as everything else, between the square you are looking at and the value the number came from. Small means the picture is a good likeness. Large means look at the number, not the square.
measuring…
Why this kills the naive approach
A designer checking their work in a colour-blindness simulator is checking one condition, usually deuteranopia, usually by eye. The pairs that kill a palette are rarely the ones that look risky. In section 5, across every published palette measured, the pair that sets the score is chosen by the same condition every single time — and it is the one almost nobody designs for: plain greyscale.
The curve: N*(Δ)
The deliverable is not one number. It is the largest conforming palette size at every threshold on the published range. Two things are plotted, and they are different objects: an upper bound that no palette anywhere can beat, and a construction that the shipped search actually built.
Read the columns carefully
- Upper bound (exact). Proved from the greyscale condition alone, with no search of any kind. No palette of that size plus one exists at that threshold — not one we failed to find, one that cannot be written down. Section 4 is the argument.
- Upper bound (polyline). The looser bound you get by dividing the length of the grey curve by Δ. It is valid, it is cheaper, and it is wrong to publish as the ceiling: it overstates what has been proved. It is shown here only so you can see the difference, and the thresholds where the two disagree are marked.
- Construction (achromatic). An explicit conforming palette of greys, built by greedy packing, that meets the exact upper bound at every threshold. Upper bound met by construction means N* is pinned exactly, not bracketed.
- Construction (chromatic, by search). A grey ramp is a lousy categorical
palette for a human. This column is what
searchPalette(N, {seed, threshold})— the shipped function, called with nothing but a seed and a threshold — actually returns. It runs live in this page, in the background, while you read.
…
Why the ceiling is a proof and not a failed search
Most “we could not find more than N” results are search results. This one is not. The upper bound needs no search at all, and here is the whole argument in four steps a non-mathematician can follow.
Step 1 — greyscale collapses everything onto one line
The grey condition takes a colour to its relative luminance and paints that back
as a neutral. There are only 256 neutral 8-bit colours,
#000000 to #ffffff. So the whole 16.7-million-colour sRGB cube lands on
256 points. Millions of colours in; 256 out. Here are colours that are wildly different, and the
single grey they all become:
Step 2 — two colours on the same grey are the same colour, to the checker
If two palette colours land on the same grey, their distance under the grey
condition is exactly 0. The score is a minimum over the five conditions, so
their score is 0 too, however brilliant they look in colour. A conforming palette of N colours
therefore needs N different greys, every pair at least Δ apart.
Step 3 — that is a one-dimensional packing problem, and it is exact
Points on a line, pick as many as you can with a minimum gap. Greedy from the dark end is optimal, and it is exact — not an approximation, not a sample — because those 256 greys are the entire set of reachable values, not a grid drawn through a continuum. The engine checks the one property that makes greedy provably optimal here (distance strictly increasing as you move away along the axis) at load time, over every pair on the axis, rather than assuming it.
…
Step 4 — the ruler
Here is the whole argument as one picture, and it is the only picture on this page you really
need. The strip is all 256 greys — the entire image of the sRGB cube under the
grey condition. The marks are where the packing puts the colours. The brackets carry
the real distance between neighbours. The number left over at the top is why there is no room for
one more.
The grey ruler
Every position, every bracket and the verdict above are recomputed from the engine on each step of this slider. The picture is not an illustration of the result; it is the result, drawn.
What the engine actually does when the game asks for one more
The game walks down this curve and asks the engine for one more colour until it says no. There
are two ways to say no and they are not the same claim, so the engine names which one it
is making rather than leaving a reader to infer it: refusalKind: "search" means a
real search ran and came back empty, and refusalKind: "bound" means nothing was
tried, because nothing could work. Here is what the shipped function returns at the wall, asked
exactly the way the game asks it:
The obvious attack, closed
CAM16 needs a stated viewing condition, and picking a flattering one would be exactly the quiet tuning this project exists not to do. So the packing is recomputed here across a 64-fold range of adapting luminance, from a very dim room to a bright one. If the ceiling moved with the room, it would be an artefact of our choices. It does not move.
Computed live, in this page, by rebuilding the CAM16 viewing conditions at
each LA and re-running the packing from scratch.
What this does and does not establish
- Proved: no conforming palette larger than the exact bound exists, at any
threshold, for any observer set containing
grey. This is an impossibility result, not a search outcome. - Proved: a conforming palette of exactly that size exists, because the packing itself is one — all 256 neutrals are exact fixed points of all five conditions, so for two greys the five conditions return the same number and the guarantee reduces to the packing.
- Therefore N* is exact, not bracketed, at every threshold on the published curve. The two bounds meet.
- Not proved by the above: that a colourful palette of that size exists. That is the search column, and it is a search result. It is reported as one.
The published palettes, measured
These are the good ones — the sets a careful person reaches for. Measured against this guarantee, here is how they do. Read the framing paragraph under the table before you draw a conclusion from it.
This is a finding, not a takedown
These palettes were built to keep a different promise, and they keep it. Okabe–Ito set out to make figures readable for colourblind readers in print and on screen. Paul Tol’s sets are built for scientific plots. ColorBrewer is built for maps. Not one of them ever claimed that every pair would survive protanopia and deuteranopia and tritanopia and greyscale simultaneously at a stated numeric threshold. That is a strictly harder promise, and it is the one measured here.
The right way to read the table: under normal vision every one of these palettes is comfortably separated — that column is what they were designed for and they deliver it. The all-five column is a different question. And the answer to that question is remarkably consistent about which condition does the killing.
There is one more honest point in the table, and it matters: a published palette does pass. Look at the small ones. When a designer keeps the count low enough, the harder promise is kept without anyone having set out to keep it. The wall this project measures is not a claim that published work is careless. It is a claim about how many.
The ordering trap
The guarantee preserves distance. It does not preserve direction. Two colours can sit a comfortable distance apart under all five conditions while their hue order, their warmth, and their similarity ranking are completely different for a dichromat. This has a consequence that no colourblind mode can fix.
6.1 A gradient, and what a dichromat gets
Colour-ordering puzzle games — the genre where you drag tiles into their correct place in a gradient — are built on ranking, not identity. Here is a gradient of exactly that kind. Row one is what a trichromat sees. Row two is the same row simulated under deuteranopia. Row three is the same as row two, with two tiles swapped into the wrong places.
The number that matters is the last one: how much this observer’s percept changes when the puzzle is wrong. If swapping two tiles moves what they receive by less than the smallest visible step, then the solved board and the broken board are the same thing to them. The player is not being asked to see something. They are being asked to guess.
Measured on the exact projection, not on the squares above — for the same reason section 2 gives. On “blue → purple” under deuteranopia the two answers are far enough apart to move this demo’s own verdict from one band to the next, so it is not a distinction without a difference.
This is not fixable by choosing better colours, because the defect is in the verb. A game built on “put these in order” is closed to a dichromat however the palette is chosen. A game built on “is this the same as that” is not. That is why the game this page belongs to has five verbs and every one of them is an identity judgement.
The two games this is really about, and exactly how far we are entitled to go
The genre described above is I Love Hue and Blendoku, and honesty requires being precise about which parts of that are measured and which are quoted.
- Measured here, by us: the demonstration above. A gradient, simulated with the shipped CVD model, with the ordering collapsing. That is a claim about the verb — arrived at from the model, not from playing anything.
- Not measured here: either shipping app. There is no official web build of either, and neither is installable on the machine this was built on. Where this project needed a playable comparison it built documented reconstructions of the two loops and compared against those, and every published comparison says so. This page makes no claim about the difficulty or quality of either product.
- Quoted, with attribution: Blendoku’s own developer, Rod Green of Lonely Few, writing about the colourblind mode he shipped: “there could still be cases where you’re unable to distinguish all the colors in a level regardless of how much you color shift.” (Guest post via Evolve PR, 27 June 2013.) That is a developer describing the exact hole this page measures, in his own product, in writing, before anybody outside had to.
- Also quoted: this project’s research file on I Love Hue records that it found no colourblind mode in either I Love Hue title, and that the App Store accessibility panel for both reads “The developer has not yet indicated which accessibility features this app supports.”
None of that makes either game bad. Both are well made and enormously popular. It makes them structurally closed to a set of players — and that is a different, more specific, and more useful thing to say.
6.2 Category collapse — the sharper version of the same problem
It goes further than order. A conforming palette keeps every pair far apart, and the same palette can still lose its categorical structure entirely. Below, the hue angle of each palette colour is plotted under each condition. Under typical vision they spread out. Under each dichromacy they fall into clusters — and colour working memory leans on exactly that categorical structure [9].
Clustering rule, stated so you can check it: a colour counts as
chromatic if its CAM16 colourfulness M exceeds 5 (below that a colour has no
meaningful hue), and two chromatic colours are in the same hue cluster if their CAM16 hue angles
are within 30°, wrapping around the circle. Change the rule and the counts change; the collapse
does not.
What survives: telling them apart. What does not: which ones feel like they belong together. A design that leans on “the warm ones mean X” is leaning on something the guarantee never promised.
The negative control
A checker that passes everything is worthless, however green its output. So the engine ships a family of deliberately broken twins: same size, same lightness spread, same “looks like a real palette”, one defect each. The same checker that clears the real palette must reject them, and name the pair and the condition that did it.
A real palette and its twin, side by side
Look at the two rows before you look at the verdicts. They are equally plausible. Both have a sensible lightness spread, both would pass a glance, and one of them would ship. The difference is not visible in the swatches — it is visible in the measurement, which is the entire point of having one.
Citations
One invented citation destroys the credibility of every correct one beside it. Everything below was read first-hand during this build. Where a source did not say what was hoped, that is recorded rather than papered over.
- Li, C., Li, Z., Wang, Z., Xu, Y., Luo, M. R., Cui, G., Melgosa, M., Brill, M. H. & Pointer, M. (2017). “Comprehensive color solutions: CAM16, CAT16, and CAM16-UCS.” Color Research & Application 42(6), 703–718. doi:10.1002/col.22131 The distance of record. Every ΔE′ on this page is Euclidean distance in CAM16-UCS, with KL = 1.
- Brettel, H., Viénot, F. & Mollon, J. D. (1997). “Computerized simulation of color appearance for dichromats.” Journal of the Optical Society of America A 14(10), 2647–2655. doi:10.1364/JOSAA.14.002647 The dichromacy model implemented here — the full two-half-plane projection, not the single-matrix shortcut.
- Viénot, F., Brettel, H. & Mollon, J. D. (1999). “Digital video colourmaps for checking the legibility of displays by dichromats.” Color Research & Application 24(4), 243–252. Implemented as a cross-check only. It is a single-plane approximation valid for protan and deutan; the engine refuses to use it for tritan, because doing so is a known error.
- Machado, G. M., Oliveira, M. M. & Fernandes, L. A. F. (2009). “A physiologically-based model for simulation of color vision deficiency.” IEEE Transactions on Visualization and Computer Graphics 15(6), 1291–1298. Second cross-check model. Matrices transcribed from the authors’ own published tables at inf.ufrgs.br/~oliveira.
- Smith, V. C. & Pokorny, J. (1975). “Spectral sensitivity of the foveal cone photopigments between 400 and 500 nm.” Vision Research 15(2), 161–171. The cone fundamentals the Brettel implementation uses, with the Judd–Vos modified colour matching functions tabulated by the Colour & Vision Research Laboratory (cvrl.org).
- IEC 61966-2-1:1999. Multimedia systems and equipment — Colour measurement and management — Part 2-1: Default RGB colour space — sRGB. Transfer function, primaries, and the 64-lux reference viewing environment that fixes the CAM16 adapting luminance.
-
W3C. Web Content Accessibility Guidelines (WCAG) 2.2, W3C Recommendation,
12 December 2024. w3.org/TR/WCAG22
Definition of relative luminance — the 0.2126 / 0.7152 / 0.0722
coefficients the
greycondition uses. -
Somers, A. (Myndex Technologies). APCA — Advanced Perceptual Contrast Algorithm,
algorithm revision SA98G (“0.98G-4g”), revision date 3 July 2022. Reference
implementation
apca-w30.1.9 — github.com/Myndex/apca-w3, read here from two independent copies (the git tree and the pinned npm tarball) which agree character for character. Provenance, corrected. This is not a W3C publication and there is no such thing as a “W3C Beta 0.1.9” —apca-w3is a package name and0.1.9is that package’s version. APCA is independent work by its author, who is a W3C Invited Expert; it appeared in WCAG 3 working drafts as exploratory content and was removed from the July 2023 draft, and the current WCAG 3 editor’s draft says the contrast algorithm for WCAG 3 is still undetermined. So it is cited here as a stable, versioned, public algorithm that reproduces its own published reference values to the bit, and as nothing more. Chosen over WCAG 2.x ratios for text on coloured tiles because WCAG 2’s ratio is a known-poor predictor at mid lightness, which is exactly where a chromatic palette lives — and WCAG 2.2 ratios are computed and reported alongside so nobody has to take the choice on faith. And one thing it is not: a second safeguard. APCA is a luminance-only model, and thegreyobserver condition is luminance too — so the contrast rule and the greyscale clause are the same physical quantity asked two different questions (can this text be read on that tile versus are these two tiles apart), not two independent lines of defence. Passing both is one safeguard measured twice. - Bae, G.-Y., Olkkonen, M., Allred, S. R. & Flombaum, J. I. (2015). “Why some colors appear more memorable than others: A model combining categories and particulars in color working memory.” Journal of Experimental Psychology: General 144(4), 744–763. Quoted for one thing only, verbatim from the abstract: “responses drawn from working memory are significantly biased away from category boundaries and toward category centers.” This is why section 6.2’s category collapse is a real cost and not a curiosity.
- Luck, S. J. & Vogel, E. K. (1997). “The capacity of visual working memory for features and conjunctions.” Nature 390, 279–281. About four colours held at once. Cited for scale: the wall this page measures is above what a person comfortably holds anyway.
-
Okabe, M. & Ito, K. (2008). Color Universal Design (CUD): How to make figures and
presentations that are friendly to colorblind people. J*FLY,
jfly.uni-koeln.de/color
Provenance note, because it matters. That page presents
the palette as an image, not as numbers — we fetched it and confirmed the hex values are not
in its text. The eight sRGB values measured in section 5 are the ones distributed as the
"Okabe-Ito"palette in R’sgrDevices(src/library/grDevices/R/colorstuff.R), where they are attributed to “Masataka Okabe & Kei Ito” with a link to the J*FLY page. R’s table carries a ninth entry,gray = #999999, which is R’s own addition and is not measured here. -
Brewer, C. A. & Harrower, M. ColorBrewer 2.0,
colorbrewer2.org; scheme definitions read from
colorbrewer2.org/export/colorbrewer.json. See also Harrower, M. & Brewer, C. A. (2003). “ColorBrewer.org: An online tool for selecting colour schemes for maps.” The Cartographic Journal 40(1), 27–37. Dark2 and Set2 are read from that JSON export as RGB triples and converted to hex. The export does not carry a colourblind-safe flag, so no claim about ColorBrewer’s own safety marking is made from it here. - Tol, P. Colour Schemes (Paul Tol’s Notes), SRON personal pages, sronpersonalpages.nl/~pault The bright, high-contrast, vibrant, muted, medium-contrast and light qualitative schemes, read from that page during this build. The page as fetched carries update dates but no technical-note number, so none is cited.
- Ottosson, B. (2020). A perceptual color space for image processing (OKLab). bottosson.github.io/posts/oklab Implemented as a cross-check only. Agreement between the two rankings is evidence the implementation is sane; CAM16-UCS is the number of record.
-
CIE 159:2004, A colour appearance model for colour management systems: CIECAM02.
Source of the convention
LA = LW × Yb/100, carried into CAM16 unchanged. Section 9 records how getting this wrong once moved our numbers. - Mahy, M., Van Eycken, L. & Oosterlinck, A. (1994). “Evaluation of uniform color spaces developed after the adoption of CIELAB and CIELUV.” Color Research & Application 19(2), 105–121. doi:10.1111/j.1520-6378.1994.tb00070.x Cited for exactly one figure and no more: the widely-used ≈ 2.3 ΔE per just-noticeable difference, which in that paper is a mean discrimination-ellipse radius in CIELAB, reported with a coefficient of variation of roughly 57 % — an average of something that varies by about half its own size across the space. Section 1 uses it once, hedged, and only as a divisor: Δ over 2.3, printed live, rather than the folk version that multiplies Δ by one. Two caveats travel with it and are printed there: it is a CIELAB figure being borrowed for a CAM16-UCS scale, and it is an average, not a constant.
- Birch, J. (2012). “Worldwide prevalence of red-green color deficiency.” Journal of the Optical Society of America A 29(3), 313–320. doi:10.1364/JOSAA.29.000313 Cited in section 1 for the size of the population this page draws a boundary through, and for nothing else. Its abstract, read first-hand, reports inherited red-green deficiency at about 8 % of European Caucasian men and about 0.4 % of women, and between 4 % and 6.5 % of men of Chinese and Japanese ethnicity. What we wanted from it and did not take: the abstract does not split anomalous trichromacy from dichromacy — that split is in the paper’s Table 4, which we did not read — so no such figure is published here, and section 1 says so instead of rounding one up from memory.
What is proved, what is not, and what moved
Proved
Searched hard and not found — which is a different thing
Simulated, and therefore not a claim about any real person
This is the one that cannot be closed in software, so it is declared rather than solved. Every dichromatic figure on this page is produced by Brettel, Viénot & Mollon 1997 [2], a published model of dichromatic appearance. It is a model.
- We say “simulated under Brettel 1997”. We never say “a protanope will see”. Nobody on this build is a dichromat and no dichromat has been asked.
- Dichromacy is not one thing, and the guarantee’s word all has an edge. Anomalous trichromacy is not modelled here at all; the three conditions are the complete dichromatic cases. Section 1 draws that boundary, measures which end of the severity dial is the hard one instead of assuming it, and prints the part of the answer that does not go our way.
- The
greycondition is the exception, and it is not a simulation of a person at all: it is what a monochrome display physically emits, defined by the WCAG 2.2 relative luminance formula [7]. That is why it is the condition whose argument in section 4 is a proof rather than a model. - In the game, simulation is applied to pixels read back from a real browser, not to the engine’s intended values, so at minimum the thing being simulated is the thing actually displayed.
Things this page says that we could not reproduce
What moved during the build
How to check any of it yourself
Open the console on this page. The whole computed dataset is on
window.__explainer — every table, every measurement, with the engine’s own
objects attached. Or run the engine directly: it is ESM with zero dependencies, and the same
files run in Node and in the browser with no build step.