The Math Behind Every Slider

The exact formulas PhotoFilter applies per pixel — exposure, brightness, contrast, highlights and shadows, warmth, saturation, sharpen, vignette and grain.

Published 2026-09-26

Photo editors love to present sliders as mysterious dials labeled with vibes. Underneath, every adjustment on this site is a short formula applied to every pixel — and since the same function renders the preview and the download, these are the real equations, not a paraphrase.

The pipeline runs in a fixed order: exposure → brightness → contrast → highlights/shadows → warmth → saturation → sharpen → vignette → grain. Order matters, and we’ll see why at the end.

Exposure: multiplication in stops

out = in × 2^(slider / 100)

The slider maps −100…+100 onto −1…+1 photographic stops. Each stop doubles or halves the light, which is how a camera’s exposure compensation works. +30 is about a third of a stop; −50 is half a stop down. Because it multiplies, black stays black — only recorded light gets scaled.

Brightness: a flat multiplier

out = in × (1 + slider / 100)

+20 multiplies every channel by 1.2; −100 lands on exactly 0. It’s the same definition as CSS brightness(). In practice exposure and brightness look similar here — the difference is convention: exposure is framed in stops for photographers, brightness in percent for everyone else. When in doubt, reach for exposure first (the which-slider guide explains why).

Contrast: the classic 259 curve

C = slider × 2.55 → factor = 259(C + 255) / (255(259 − C)) → out = (in − 128) × factor + 128

This is the formula photo editors have used for decades. It pivots values around mid-gray (128): darks get darker, brights get brighter, and midtones barely move — which is why contrast adds “punch” without shifting overall brightness much. At slider +20 (C = 51) the factor is ≈1.49: a pixel at 100 becomes (100−128)×1.49+128 ≈ 86. The 259 form soft-clips near the rails; we also cap C at ±254 so the curve never divides by zero.

Highlights and shadows: luminance-masked shifts

L = clamp(luma / 255, 0, 1)

delta = shadows × 0.7 × (1 − L)² + highlights × 0.5 × L²

The squared masks make the effect strongest where it belongs: the shadows slider mostly moves the darkest quarter of the tone range, the highlights slider the brightest. +shadows lifts dark areas (recovering faces in backlight); −highlights pulls bright skies back toward detail. Because both keys off the current pixel’s luminance, they adapt per-pixel instead of applying a flat shift.

Warmth: a red–blue see-saw

R += slider × 0.4; B −= slider × 0.4

+50 adds 20 levels to red and removes 20 from blue — a straightforward white-balance style shift, symmetric so −50 cools by the same amount. Green is left alone: warm/cool is fundamentally a red-vs-blue trade.

Saturation: interpolation against luma

L = 0.2126R + 0.7152G + 0.0722B → out = L + (in − L) × (1 + slider / 100)

At −100 every channel becomes L — true grayscale, weighted by human-eye sensitivity (Rec.709). At +100 the distance from gray doubles. The luma weights matter: they’re why our B&W presets still look like the scene instead of a flat murk.

Sharpen: a 3×3 convolution

a = slider / 100 × 0.55 applied to the kernel [0, −a, 0, −a, 1+4a, −a, 0, −a, 0]

Each pixel gains the difference between itself and its four neighbors — edges get exaggerated, flat regions stay untouched (neighbors equal the center, so the kernel sums to the original value). Negative values blend toward a small gaussian blur instead, for softening noise or skin.

Vignette: radial falloff

factor = 1 − (slider / 100) × 0.7 × d²

where is the pixel’s squared distance from the image center, normalized so a corner is 1.0. At +100 the very corner is multiplied by 0.30 — a strong frame; +15–25 is the subtle darkening that nudges attention inward. Negative values lighten the edges instead.

Grain: deterministic per-pixel noise

delta = (hash(x, y, seed) − 0.5) × 55 × slider / 100

The hash is integer math over pixel coordinates plus a seed derived from the file’s name and size — so the pattern is stable between preview and export, and differs between files. It’s added after everything else, the way film grain sits on top of a developed image.

Why this order

Tone first (exposure, brightness, contrast), then targeted tone (highlights/shadows), then color (warmth, saturation), then detail (sharpen), then the finishing layers (vignette, grain). Put warmth after saturation and the boosted channels amplify differently; sharpen after grain and you’d sharpen the noise into speckles. The order above is the one that behaves — and it’s the same order the editor applies when you download.

Try it: drop a photo in, set saturation to −100, and every pixel becomes 0.2126R + 0.7152G + 0.0722B of its former self — you can verify it against your own image with a color picker.

Frequently asked questions

Why does contrast use that odd 259 formula?

It's the classic photo-editor curve (used by GIMP and documented since the early 2000s): factor = 259(C+255) / (255(259−C)). Unlike a naive multiplier it eases off near pure black and white, so extreme settings clip gracefully instead of slamming to a hard wall. At slider ±100 we cap C at ±254 to keep the curve defined.

Why Rec.709 luma and not a simple average?

Because your eye doesn't weigh channels equally — green contributes ~72% of perceived brightness, blue only ~7%. Averaging (R+G+B)/3 makes saturated reds and blues look artificially dark when desaturated. The 0.2126/0.7152/0.0722 weights keep perceived brightness stable when you pull saturation down, which is exactly what a grayscale conversion should do.

Is the grain random on every render?

No — it's a deterministic hash of each pixel's coordinates plus a seed derived from your file's name and size. Same photo + same slider value + same render size = identical grain, every time. One honest caveat: the hash is keyed to pixel coordinates, so when your source exceeds the 1400px preview cap, the export's grain pattern sits at different coordinates than the preview's — same strength and character, not the same speckle positions. Random-per-render grain would be untestable and, honestly, annoying.

Does slider order actually matter?

Enormously. Warmth before saturation boosts the warm channels then amplifies them; warmth after would tint a finished grade. Grain before sharpen would get amplified by the sharpening kernel into harsh speckles. The pipeline order — tone, color, sharpen, then vignette+grain — is chosen so each stage sees the pixels the previous stage intended.