Classic: how hard can it be? — Ilia MalaninRU
Читать на русском
It doesn’t get simpler
Of Photoshop’s four interpolation methods, Classic looks like the harmless one.
Classic is the oldest of the four. Up until Photoshop 2022 it was the<br>default, which is why an enormous number of PSD files hold gradients<br>computed by its rules.
What could possibly be hard about it? Take one RGB triplet and another, split<br>each into channels and interpolate them independently. No conversions into<br>linear RGB<br>or Oklab,<br>both of which we covered last time. An evening’s work, two at most.
Think we’re done?
This is the tip of the iceberg, and that’s where we’ll start. This article is<br>about the curve Photoshop draws from one color to another.
Every channel on its own
Classic doesn’t work with color as a whole. It takes it apart into three<br>numbers and processes each channel independently, knowing nothing about the<br>other two.
For a transition from red (255, 0, 0) to blue (0, 0, 255) you get three<br>independent curves: 255 → 0 for red, a constant 0 for green, and 0 → 255<br>for blue. Then the three results are reassembled into a color.
Last time we called this interpolation in sRGB, for brevity. More precisely:<br>Classic interpolates the document’s encoded RGB values directly, without<br>converting them into any other color space. In an ordinary sRGB document those<br>really are sRGB numbers, but the principle isn’t tied to sRGB specifically.<br>The numbers enter the algorithm exactly as they left the color picker.
That decision has a price, and we’ve already seen it: last time the<br>black-to-white transition in Classic<br>came out darker than in Linear. The reason is the same puzzle involving 128,<br>which sits in the middle of the numeric scale but carries only about 21% of the<br>light. Classic falls into that trap precisely because it never leaves the<br>document’s numbers.
From here on we’ll normalize the channel values for convenience: a byte<br>0..255 divided by 255, so we work with numbers from 0 to 1. The<br>formulas read easier that way, and the shape of the curve doesn’t change<br>with the scale.
Stops and segments
A quick reminder of what a gradient is made of. There are stops : points<br>where the color is set by hand. In a black-to-white gradient, for instance, the<br>black and white endpoints are the stops.
Every stop has:
a position within the gradient,
an RGB color.
And between each pair of adjacent stops there’s a midpoint, the center of the<br>transition, which we covered in<br>“Midpoint as center of gravity”<br>last time. Position and color describe the stop itself, while the midpoint<br>describes the transition between two stops. So n stops come with n−1<br>midpoints. Those gaps between adjacent stops are what we’ll call segments .<br>There are n−1 of them too:
stop 0segment 0stop 1segment 1stop 2
Each segment is computed separately from the rest, and the first step is to<br>convert the pixel’s position from the coordinates of the whole gradient to the<br>coordinates of the segment:
u=x−pipi+1−piu=\frac{x-p_i}{p_{i+1}-p_i}u=pi+1−pix−pi<br>Where:
x: the pixel’s position in the whole gradient
pᵢ: the position of the segment’s left stop
pᵢ₊₁: the position of the right stop
u ∈ [0,1]: the normalized position of the pixel inside the segment
If x falls outside the outermost stops, there’s no segment to look for. To the<br>left of the first stop its color holds. To the right of the last one, the color<br>of the last stop holds.
Let’s do it with numbers. Say we’re drawing a horizontal gradient in a rectangle<br>measuring 200 × 50 pixels, with three stops: black at the very start, white at<br>60% of the length, and gray at the end. In pixels that’s 0, 120 and 200.
0 px120 px200 px150 px
Take the position at pixel 150. In the whole gradient that’s three quarters<br>of the way: x = 150/200 = 0.75. But that’s not what interpolation cares about.<br>It cares where the point sits inside its own segment, and it lands in the<br>second one, running from the white stop to the gray. That segment’s bounds are<br>p₁ = 0.6 and p₂ = 1.
u=0.75−0.61−0.6=0.375u=\frac{0.75-0.6}{1-0.6}=0.375u=1−0.60.75−0.6=0.375<br>So: across the whole gradient the pixel has covered 75% of the distance, but<br>within its segment only 37.5%. From here on the algorithm works with the second<br>number.
This means that as far as a segment is concerned, it’s always a transition<br>from 0 to 1. How many pixels it actually occupies, and where in the gradient<br>it starts, no longer matters for interpolation.
Remapping the position through the midpoint
We covered the midpoint in detail<br>last time,<br>so here’s just a reminder of the formula:
φ(u,m)={u2m,u≤m12+u−m2(1−m),u>m\varphi(u,m)=<br>\begin{cases}<br>\dfrac{u}{2m}, & u \leq m \\[8pt]<br>\dfrac{1}{2}+\dfrac{u-m}{2(1-m)}, & u > m<br>\end{cases}φ(u,m)=⎩⎨⎧2mu,21+2(1−m)u−m,u≤mu>m<br>It’s built so that φ(m, m) = 0.5: wherever the diamond slides off to, as far<br>as interpolation is concerned it always ends up exactly in the middle. We’ll<br>call the remapped position t. It’s t, not the original u, that goes...