Curves Editor

Freehand transfer curves on a monotone spline, per channel — RGB, CIELAB (L*, a*, b*, Chroma) and Saturation — with a live before/after histogram, presets, and a mask-aware, instant-apply pipeline.

1. Using the tool

The grid is your transfer curve: the horizontal axis is input tone, the vertical axis is output. Each channel keeps its own curve, and the channels you aren't editing are drawn as dim overlays so you always see the whole transform.

Shaping the curve

Working from the image

Channels, presets & preview

ControlWhat it does
ChannelK (brightness / RGB linked), R/G/B, L*, a*, b*, Chroma, or Saturation — each with an independent curve; the inactive ones draw as dim overlays.
PresetsBuilt-in shapes (S-curves, lift/crush shadows, highlight roll-off, …) plus your own saved multi-channel presets.
Pop OutDetach the editor into its own resizable window (position/size remembered).
HistogramBefore (dim) vs after (bright) for R/G/B and luminance, with log scaling and x-axis zoom/pan.
Toggle Preview / Apply / ResetShow the result on the canvas, commit it to the document, or return every channel to the identity line.
Grip (bottom-left)Drag to the canvas to mint a Curves shortcut with all channel curves baked in, or drop on an image to apply headlessly.

2. Working with the colour channels

L*, a*, b* and Chroma behave differently from the RGB curves in one important way, and once that clicks they become the most precise colour tools in the program. This section is the practical version; the maths is in §3.

2.1 Which way is which

CIELAB splits colour into two opponent axes. a* is the red–green axis, b* is the yellow–blue axis. On both of them, raising the curve moves toward the warm end and lowering it moves toward the cool end:

ChannelRaise the curve (output above the diagonal)Lower the curve
a*toward red / magentatoward green
b*toward yellowtoward blue
L*lighterdarker
Chromamore saturated, hue unchangedtoward neutral grey
Saturationmore vivid (HSV)toward grey
Mnemonic: up is warm, down is cool — on both colour axes. a* up is red, b* up is yellow; a* down is green, b* down is blue.

2.2 The x-axis is colour, not brightness

This is the part that catches people. In the K and R/G/B curves, the horizontal axis is tone — left is shadows, right is highlights. In a* and b* it is not. The horizontal axis is the pixel's existing position on that colour axis:

ChannelLeft of the gridCentreRight of the grid
a*already greenneutral / greyalready red
b*already blueneutral / greyalready yellow

So a handle placed on the right-hand side of the a* curve acts on the red pixels and barely touches anything else — no mask required. That makes a*/b* curves selective by colour in the same way an RGB curve is selective by brightness. If you want to act on a region that happens to be a particular colour, this is usually the fastest route to it.

The readouts follow the axis. On a colour channel the grid's hover line and the double-click-to-place point both use the pixel's actual L*/a*/b*/Chroma/Saturation value, not its brightness, so a feature you pick off the image lands exactly where the curve will act on it. The status line shows the R/G/B levels and that axis value side by side. The histogram stays in R/G/B — it shows the tonal distribution, not the a*/b* distribution — so ignore it when judging where to put a colour handle.

2.3 Neutral, and the part of the grid that's actually reachable

a* and b* are signed — zero means neutral, not black. They are mapped into the grid as (a*+128)/255 and (b*+128)/255, so a* = 0 and b* = 0 land at 0.502, essentially dead centre. The identity diagonal passes through it, which is why an untouched curve leaves greys grey.

If you want a contrast-style pivot that keeps neutrals neutral, put the pivot point at 0.502 rather than 0.500 — type it into the Point X box. Half a thousandth is invisible on most images, but on a carefully neutralised background it is the difference between no cast and a faint one.

The grids also run wider than any real colour can reach. a* and b* are mapped over ±128 and Chroma over 0–200, but no displayable colour gets that far — the most extreme primaries top out well short of the edges:

ChannelReachable input rangeDead zone
a*about 0.16 – 0.89both ends
b*about 0.08 – 0.87both ends
Chroma0.00 – about 0.67right third

A handle parked outside those ranges does nothing at all, because no pixel ever has that input value. On a real image the occupied range is narrower still — most pixels cluster near the centre — so the useful work happens in the middle of the grid and a handle dragged to the far edge is usually a handle doing nothing. Keep moves close to the centre and they'll bite harder than they look.

2.4 Four shapes worth knowing

2.5 Recipes

2.6 a*/b* vs Chroma vs Saturation

Large a*/b* excursions can push colours out of gamut on the way back from Lab, where each channel is hard-clamped to [0,1] — there is no soft roll-off. Clamping one channel and not the others shifts the hue, so a bright core can go flat or drift colour after a big move. Back the curve off and finish with Chroma instead.

3. Technical details

A curve is a monotone spline through your control points, baked to a lookup table and applied either per RGB channel or per colour-space component. Everything runs on float data in [0,1].

3.1 The curve: a monotone PCHIP spline

The control points are interpolated with a PCHIP (Piecewise Cubic Hermite) spline — a shape-preserving monotone cubic. Unlike a natural cubic it never overshoots or rings between points, so the curve does exactly what the handles say and the transfer stays monotone (no tonal inversions).

3.2 From curve to lookup table

The spline is sampled into a lookup table — 65 536 entries for the final apply (256 for the live preview) — and every pixel is mapped through it:

\mathrm{out}(x)=\mathrm{LUT}\left[\,\lfloor x\,(N-1)\rfloor\,\right],\qquad N=65536

Because the LUT is monotone, the before→after histogram can be remapped exactly by resampling the input CDF onto the output axis — mass-preserving and gap-free, so the displayed “after” histogram can't show a false comb where the curve expands tones.

3.3 Channel modes

RGB. K applies one LUT to all three channels (linked brightness); R/G/B curve a single channel. When both are set, the per-channel curve is composed after K:

\mathrm{LUT}_{R}'=\mathrm{LUT}_{R}\circ\mathrm{LUT}_{K}

CIELAB (L*, a*, b*, Chroma). The image is converted to XYZ with the D65 primaries matrix, applied directly to the working data — no sRGB transfer function is decoded first, so the channel values are treated as linear. The inverse omits it symmetrically, so the round-trip is exact:

[ X ] [ 0.412456 0.357576 0.180438 ] [ R ] [ Y ] = [ 0.212673 0.715152 0.072175 ] [ G ] [ Z ] [ 0.019334 0.119192 0.950304 ] [ B ]

then XYZ→Lab about the D65 white point (Xn, Yn, Zn) = (0.95047, 1, 1.08883):

L^{*}=116\,f\!\left(\frac{Y}{Y_n}\right)-16
a^{*}=500\left(f\!\left(\frac{X}{X_n}\right)-f\!\left(\frac{Y}{Y_n}\right)\right),\qquad b^{*}=200\left(f\!\left(\frac{Y}{Y_n}\right)-f\!\left(\frac{Z}{Z_n}\right)\right)
f(t)=\sqrt[3]{t}\ \ (t>\delta^{3}),\qquad f(t)=\frac{t}{3\delta^{2}}+\frac{4}{29}\ \ (t\leq\delta^{3}),\qquad \delta=\frac{6}{29}

The selected component is normalised to [0,1], curved, and de-normalised (L*/100, (a*+128)/255, (b*+128)/255) — which is why neutral falls at 0.502 on the a*/b* grids (§2.3). For Chroma the curve scales the colour's radius in the a*–b* plane while keeping its hue:

C=\sqrt{a^{*2}+b^{*2}}\;,\qquad (a^{*},b^{*})\;\to\;(a^{*},b^{*})\cdot\frac{C_{\mathrm{new}}}{C}

Saturation curves the S channel of HSV and converts back.

3.4 Order of operations

When several channels are active they are applied in a fixed order so the result is deterministic: first the RGB stage (K composed into R/G/B), then the Lab stage (L*, a*, b*, Chroma via one round-trip), then Saturation (via HSV). Mono images support K (applied directly) and L* (through the Lab lightness nonlinearity); the colour-only channels are skipped.

3.5 Inflection pivot

Ctrl+clicking the grid (or double-clicking the image) sets a pivot u and redistributes the interior handles evenly on either side of it along the identity line — a quick way to seed a symmetric contrast curve around a chosen tone.

3.6 Masking & apply

With an active mask the result is blended back against the source over the mask:

\mathrm{out}=\mathrm{mask}\cdot\mathrm{out}+(1-\mathrm{mask})\cdot\mathrm{src}