The Math Behind Every Slider
The exact formulas PhotoFilter applies per pixel — exposure, brightness, contrast, highlights and shadows, warmth, saturation, sharpen, vignette and grain.
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 d² 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.