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. 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].

2.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).

2.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[\,\mathrm{round}(x\,(N-1))\,\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.

2.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 sRGB→XYZ (D65) with

[ 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). 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.

2.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.

2.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.

2.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}