Gamma beat saturation: why screen colours lie on cheap LEDs
I built a small Mac app that makes a A$49 Bluetooth LED bar follow whatever is on my screen. Getting the bar to change colour took an afternoon. Getting it to show the right colour took much longer, and taught me something I didn't expect.
The problem: everything looked pastel
The first version simply sent the screen's colour straight to the light. A teal on screen, say red 40, green 180, blue 180, went to the bar as exactly those numbers. On the bar it looked minty white. So did most other colours: deep blue became pale blue, orange became peach. Everything drifted towards white.
The bar wasn't broken. A screen and an LED don't mix light the same way.
The colour maths is Lantern's own code. The bar is a simple model: it drives each channel in proportion to the value sent, with green and blue brighter than red.
Why it happens
A screen is built to reproduce colours accurately: its red, green and blue are balanced, and it applies a gamma curve so that a value of 40 out of 255 really is dim. A cheap LED controller does neither. It roughly drives each channel in proportion to the number you send, and its green and blue are much stronger than its red.
So that small red 40 in a teal, nearly invisible on a screen, becomes a noticeable glow of red on the bar, and the weak channels together add up to white.
On a screen, the minor channels of a colour are a nuance. On an LED, they are contamination.
What I tried first: more saturation
The obvious fix was to boost saturation and push every colour away from grey. It helped a little and broke something else: everything collapsed towards the nearest primary. A dusty blue and a navy both became the same full blue. The bar was no longer pastel, but it had lost every in-between hue.
That was the giveaway. Saturation acts on the whole colour at once, while the actual problem was the small channels specifically.
What worked: gamma
Gamma correction raises each channel to a power. With a gamma of 3.5, a channel at full strength stays at full strength, but a channel at 20% drops to under 1%. That is the right shape for this problem: it suppresses the weak channels hard and barely touches the strong ones. The red in the teal all but disappears, so the bar shows teal, not mint, while a dusty blue keeps its slight green and stays distinct from navy.
One more adjustment was needed. Gamma also dims the colour overall, so after applying it I rescale the result back to its original brightness. Only the purity changes, not the brightness.
How I found the numbers
Not by theory. I built a calibration window: pick a colour from a wheel, or grab any pixel from the screen, and the bar shows it next to the on-screen swatch while sliders adjust saturation, gamma and each channel's strength. Then I tuned by eye until they matched.
The settings I landed on for this bar: gamma 3.5, saturation left unchanged, and blue turned down to 0.45, because this bar runs blue. Another bar would need different numbers, which is why the calibration tool ships with the app instead of the values being hard-coded.
The general lesson
When an output disagrees with your input, it is tempting to reach for the control that sounds right: the colours look washed out, so add saturation. The better question is where the error comes from. Here it came from the smallest components of each colour, and the fix was the one that targets exactly those. It is the same instinct as debugging a model: find the part of the input the error actually lives in, rather than turning the global knob.