A photo can be too dark, too flat, or too gray, and those are three different complaints. Brightness, contrast, and saturation each answer one of them. Turning the wrong one up is why a "brighter" edit sometimes looks milky, and why a "more colorful" edit sometimes just looks harsher.

The three controls below are the ordinary per-pixel versions used in a browser canvas: an amount in percent, factor = amount / 100, and each red, green, and blue value kept inside 0–255. At 100% every formula is the identity. Nothing else in the file is consulted. There is no blur, no tone curve, and no separate luminance plane unless the formula builds one.

What each word means

Brightness is how much light the picture is carrying. A dark doorway and a sunlit wall can have the same colors. Brightness is the difference between those two.

Contrast is how far the light parts and the dark parts sit from each other. A foggy afternoon can be bright enough and still look flat, because the brightest cloud and the darkest tree are close together. Contrast is that gap.

Saturation is how far a color sits from gray. A navy shirt and a pale blue shirt can be the same lightness. Saturation is what separates them. A pixel with equal red, green, and blue is gray at every saturation setting.

The three are independent questions. A dim photo can be highly saturated. A bright photo can be gray. A high-contrast photo can still have weak color. The formulas make that concrete, because each one refuses to touch the quantity the others are about, until a channel hits the 0 or 255 rail and the refusal breaks.

Brightness multiplies every channel

const factor = amount / 100; const out = clamp(Math.round(channel * factor), 0, 255); 

The same gain is applied to red, green, and blue. At 0% the pixel is black. At 200% every channel is doubled. At 50% every channel is halved, which is darker, not a fade-to-white.

Channel ratios stay put for as long as no channel clips. A pixel at (120, 80, 40) and the same pixel at 150%, (180, 120, 60), are the same hue. The distance of each channel from the pixel's gray also scales with the gain, so chroma does rise, but only because the whole pixel got larger, not because color was pulled away from gray on purpose.

Clipping is the part people notice. Once a channel reaches 255 it stops, and the other two may still be climbing. That pixel does change hue. On the coastal photo used later, 150% brightness pushed about half of the pixels onto the 0 or 255 rail. Mean luminance moved from 127 to 174. The clouds lost their gray shading and became flat white. The footpath and the shadows under the cars lifted, which is the actual brightness change. The grass looking a little stronger is a side effect of the multiply, plus clipping in the sky.

This is not "add white." Adding white would move every channel toward 255 and pull the pixel toward gray. A gain keeps the ratios until the ceiling.

Contrast stretches distance from middle gray

const factor = amount / 100; const out = clamp(Math.round((channel - 128) * factor + 128), 0, 255); 

128 is the midpoint of an 8-bit channel, not the average of the photo. Each channel moves away from that midpoint when the factor is above 1, and toward it when the factor is below 1. At 0% every channel becomes 128, so the picture is a flat mid-gray. At 200% the distance from 128 is doubled.

A pixel lighter than middle gray gets lighter. A pixel darker than middle gray gets darker. On this photo the mean luminance barely moved, from 127.1 to 125.6, because the pivot sits near the middle of the encoding and the scene was already centered there. The hedge, the pine, and the cars went darker. The clouds went whiter. That is the contrast change: the same scene, a wider gap between its light and dark parts.

Gaps between channels stretch too, because each channel is scaled around 128 on its own. A colored pixel therefore looks more saturated even though this formula never computes a gray value. Measured chroma rose from 64.8 to 87.9. About 43% of pixels landed on a rail. A gray pixel, equal channels, stays gray, because all three channels move together. Contrast can imitate a saturation boost on colored pixels, and it still will not colorize a gray one.

Saturation scales distance from gray

const factor = amount / 100; const gray = r * 0.2126 + g * 0.7152 + b * 0.0722; const out = clamp(Math.round(gray + (channel - gray) * factor), 0, 255); 

gray is Rec. 709 luminance: green counts for most of it, then red, then blue. That matches the contribution of those channels to perceived lightness. The gray part of the pixel is written back unchanged. Only the remainder, channel - gray, is multiplied.

At 0% the remainder is discarded and the pixel is (gray, gray, gray). Lightness is the same, color is gone. At 200% the remainder is doubled, so the pixel moves twice as far from gray in the same direction. Hue does not rotate. A blue pixel stays on the blue side of gray.

On this photo the mean luminance stayed 127.1, the same number as the original, because the gray axis is held still and clipping was rare. Chroma rose from 64.8 to 97.0. About 4% of pixels hit a rail, mostly where a channel was already near the edge and the extra chroma pushed it over. The ocean and the lawn get stronger. The brightness of the road and the clouds stays close to the source. That is the signature of a saturation edit: color moves, lightness does not, until clipping forces a channel to stop.

One photo, three edits at 150%

The source is one coastal street, 760×428. Each edit is the full frame at 150% (factor = 1.5), then a JPEG. The files were made with ROCKIMG's Adjust Image Brightness, Adjust Image Contrast, and Adjust Image Saturation. The arithmetic is the same one shown above.

Original:

Coastal street before any adjustment

Brightness at 150%. The path, the walls, and the shaded grass are lighter. Cloud detail is gone where the channels hit 255.

Same coastal street with brightness at 150 percent

Contrast at 150%. Overall lightness is close to the original. Shadows under the trees and cars are deeper, and the clouds are harder white. The color looks stronger because channel gaps grew, not because saturation was the control.

Same coastal street with contrast at 150 percent

Saturation at 150%. The sea and the grass carry more color. The road and the clouds keep nearly the same lightness as the original, which is what holding luminance still looks like.

Same coastal street with saturation at 150 percent

Edit at 150% Mean luminance Mean chroma Pixels pushed onto 0 or 255
Original 127.1 64.8 —
Brightness 174.3 73.9 about 50%
Contrast 125.6 87.9 about 43%
Saturation 127.1 97.0 about 4%

Chroma here is the average of |R − gray| + |G − gray| + |B − gray| with the same Rec. 709 gray as the saturation formula. It is a distance from gray, not a score of whether the picture looks good.

What the three knobs will not do

Raising brightness will not recover a flat picture. It lifts darks and lights together, and the lights run out of room first.

Raising contrast will not reliably add color. It widens the gap around middle gray. Colored pixels often look richer afterward, and gray pixels stay gray.

Raising saturation will not fix a dark exposure. Lightness is the term the formula is built to leave alone.

Past the clip, none of the three is reversible. A channel that became 0 or 255 has lost the amount it was supposed to move, and the hue of that pixel is no longer the hue of the source. 150% on this frame is already enough to show that on brightness and contrast. Saturation still has headroom, which is why its luminance number matches the original and the other two do not.