Color science 8 min

Thirty-eight numbers

Why this lab represents a color as a spectrum on a fixed grid instead of three coordinates, and what that choice buys you.

Almost every piece of software that handles color represents it with three numbers. A pixel is (R, G, B). A design token is a hex triple. A color picker gives you three sliders, and a color space conversion is a function from one triple to another. Three numbers is the working vocabulary of the field, and for an enormous amount of practical work, three numbers is exactly right.

This lab does not start there. Here, the primitive is a spectrum: thirty-eight numbers on a fixed wavelength grid running from 380 to 750 nanometers in ten-nanometer steps. Every instrument in the library is built on that representation. This post is about why, because the choice is not obvious and it costs something real.

Three numbers is a summary, not a description

Human color vision starts with three classes of cone cell. Each one integrates incoming light across a broad, overlapping range of wavelengths, weighted by its own sensitivity curve. What arrives at the retina is a continuous function of wavelength; what leaves it, for the purposes of color, is three numbers.

That compression is the whole reason color spaces work. If the eye reports three numbers, three numbers suffice to predict what the eye reports. The CIE standard observer, formalized in 1931, is exactly this: three sensitivity curves that, integrated against a spectrum, produce the tristimulus values X, Y, Z. Every color space you have used — sRGB, CIELAB, Oklab, HSL — is a re-parameterization of that same three-dimensional space.

The catch is what happens when you go the other way. Given a spectrum, computing the color is a well-posed problem: integrate against three curves, get three numbers, done. Given a color, recovering the spectrum is not. Infinitely many spectra integrate to the same triple. This is not a numerical inconvenience; it is a structural fact about a map from an infinite-dimensional space onto a three-dimensional one. The fibers of that map are the metamer classes, and they are the subject of a whole instrument in this lab.

So three numbers is a lossy summary of a physical object. Most of the time you do not care. You care when the question you are asking depends on the part that was thrown away.

Where the summary fails

Here is the practical version of the problem. You pick two colors that look clearly different on your monitor. You print them, or you manufacture them as paint, or you build them into a product that will sit under a shop’s lighting. Under that light, they might be indistinguishable.

Nothing went wrong with your color picker. The two colors were different as colors under your monitor’s white point. But a physical surface does not have a color; it has a reflectance function, and a color is what you get when you multiply that reflectance by a light source and integrate against the observer. Change the light, and the same two reflectance functions produce two different triples. There is no operation on the original three numbers that predicts this, because the information needed is exactly the information the three numbers discarded.

This is not exotic. It is why the textile and paint industries have cared about metamerism for a century, why “D65 versus store lighting” is a real argument in product photography, and why a certificate that a palette stays distinguishable under a family of lights has to be computed from spectra. You cannot get there from RGB. The input to the question does not contain the answer.

Why a grid, and why this one

If a spectrum is the right primitive, the next question is how to store it. A reflectance function is continuous. Software needs something finite.

The universal answer is to sample it, and the interesting choices are where to start, where to stop, and how finely to step. This lab uses 380–750 nm at 10 nm, which is thirty-eight samples. Three reasons:

The range covers where the observer has support. The CIE color matching functions are effectively zero outside roughly 380–780 nm. Sampling far into the infrared adds numbers that multiply against a zero weight. Truncating at 750 rather than 780 loses a sliver of the long-wavelength tail where the sensitivity is already tiny; that is a real approximation, and it is one worth knowing about rather than pretending away.

Ten nanometers is the interval real instruments and real datasets use. Published reflectance collections are commonly tabulated at 10 nm or 5 nm. A 10 nm grid means most source data resamples onto it without inventing detail. Going finer would mean interpolating data that was never measured that finely, which produces a spectrum that looks more precise than the measurement behind it.

Fixing it globally makes parity checkable. This is the software reason and it matters more than it sounds. Because the grid is a single constant used identically in the TypeScript reference engine and the C++ core, a spectrum computed in one can be compared sample-for-sample against the other. If the grid were a parameter, every comparison would first have to establish that both sides meant the same thing by “a spectrum,” and every bug would have a resampling explanation available to hide behind.

The cost is real: 10 nm cannot represent a genuinely narrowband source faithfully. A laser line, or the sharp phosphor peaks of some LEDs, sits between samples and gets smeared. When an instrument’s result depends on that resolution, the honest response is to say so in the output rather than to return a confident number. Several instruments here report a grid_sufficient flag for exactly this reason. The grid is a modeling decision, and modeling decisions belong in the result, not just in the documentation.

What you get back

Committing to spectra unlocks a category of question that is simply unanswerable in three dimensions:

Questions about other lights. Given reflectances, you can compute appearance under any illuminant you can write down, including ones that do not exist yet. You can search over a family of lights for the worst case. You can prove a bound over an entire continuum of lights rather than testing a handful, because for a fixed reflectance the tristimulus response is linear in the illuminant, and linearity over a convex hull is a property you can reason about exactly.

Questions about the hidden crowd. Given a color, you can characterize the whole set of spectra consistent with it — its extent, its smoothest member, the envelope it occupies at each wavelength. That set is a convex body, and its geometry says useful things about how fragile a match is.

Questions about physical possibility. Given a pixel, you can ask whether any reflectance in [0, 1] could produce it under a given light. If the answer is no, that pixel is not a diffuse surface under that light — not as a statistical anomaly, but as a fact about the object-color solid.

Each of those became an instrument in this lab. None of them is reachable from a hex code.

The honest summary

Three numbers are the right representation for the question “what will this look like on this screen right now.” They are the wrong representation for “what will this look like somewhere else,” “what else could this have been,” and “could this have been real at all.”

The lab picked the representation that makes the second set of questions answerable, and pays for it with a grid that is coarse for narrowband sources and a truncation at 750 nm. Both of those limits are stated in the outputs of the tools that they affect. That is the deal: the room does not pretend to be the world, but inside the room, the arithmetic is exact.

Instruments in this post