fix(adjust): absolute Selective Color scales by the room the ink has (#3793)
Photoshop's absolute mode scales each range's ink change by how much the ink can still move: adding ink by min(1, 2 x (1 - ink)), removing it by min(1, 2 x ink). PhotoCraft added the full percentage and clipped, so Neutrals Cyan +50 darkened a grey 64 to 0 instead of 32. Measured in Photoshop 27.11 over every range x ink at +-50 (72 cases): all within 1 level now (8 of 72 within 3 before). CPU and GPU paths. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dcc9f86aa5
commit
7890b277a0
|
|
@ -606,10 +606,12 @@ pub fn selective_color_weights(c: [f32; 3]) -> [f32; 9] {
|
|||
]
|
||||
}
|
||||
|
||||
/// Photoshop Selective Color (approximation): each range shifts the cyan, magenta and yellow ink
|
||||
/// (1 − R, G, B) by its percentage, plus its black percentage on every ink, weighted by how much
|
||||
/// the colour belongs to the range. Relative mode scales the shift by the ink already present
|
||||
/// (so it can't tint pure white); absolute adds it outright.
|
||||
/// Photoshop Selective Color: each range shifts the cyan, magenta and yellow ink (1 − R, G, B) by
|
||||
/// its percentage, plus its black percentage on every ink, weighted by how much the colour belongs
|
||||
/// to the range. Relative mode scales the shift by the ink already present (so it can't tint pure
|
||||
/// white). Absolute mode scales it by the room the ink has: adding ink by min(1, 2 × (1 − ink)),
|
||||
/// removing it by min(1, 2 × ink), so Neutrals Cyan +50 darkens red 64 by 32, not 64 (all 72
|
||||
/// absolute range × ink cases within 1 level of Photoshop 27.11).
|
||||
pub fn selective_color(c: [f32; 3], relative: bool, adj: &[[f32; 4]; 9]) -> [f32; 3] {
|
||||
let w = selective_color_weights(c);
|
||||
let mut delta = [0.0f32; 3];
|
||||
|
|
@ -620,7 +622,14 @@ pub fn selective_color(c: [f32; 3], relative: bool, adj: &[[f32; 4]; 9]) -> [f32
|
|||
let a = adj[r].map(|v| v / 100.0);
|
||||
for i in 0..3 {
|
||||
let ink = 1.0 - c[i];
|
||||
let d = if relative { (a[i] + a[3]) * ink } else { a[i] + a[3] };
|
||||
let d = a[i] + a[3];
|
||||
let d = if relative {
|
||||
d * ink
|
||||
} else if d > 0.0 {
|
||||
d * (2.0 * (1.0 - ink)).min(1.0)
|
||||
} else {
|
||||
d * (2.0 * ink).min(1.0)
|
||||
};
|
||||
delta[i] += d * wr;
|
||||
}
|
||||
}
|
||||
|
|
@ -1042,6 +1051,30 @@ mod lookup_tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Photoshop 27.11, measured: absolute Selective Color on greys (red channel after one range's
|
||||
/// cyan change).
|
||||
#[test]
|
||||
fn absolute_selective_color_matches_photoshop() {
|
||||
// (range index, cyan %, grey, Photoshop's red)
|
||||
let measured = [
|
||||
(7, 50.0, 64u8, 32u8),
|
||||
(7, 50.0, 192, 129),
|
||||
(7, -50.0, 192, 223),
|
||||
(7, -50.0, 64, 128),
|
||||
(6, -50.0, 192, 224),
|
||||
(6, -50.0, 240, 253),
|
||||
(8, 50.0, 64, 32),
|
||||
(8, 50.0, 16, 2),
|
||||
];
|
||||
for (range, cyan, grey, want) in measured {
|
||||
let mut adj = [[0.0f32; 4]; 9];
|
||||
adj[range][0] = cyan;
|
||||
let g = grey as f32 / 255.0;
|
||||
let got = selective_color([g; 3], false, &adj)[0] * 255.0;
|
||||
assert!((got - want as f32).abs() <= 1.5, "range {range} cyan {cyan} grey {grey}: {got} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selective_color_zero_is_identity_and_ranges_are_local() {
|
||||
let zero = [[0.0f32; 4]; 9];
|
||||
|
|
|
|||
|
|
@ -527,7 +527,12 @@ fn color_lookup(c: vec3<f32>, n: i32, tetra: bool) -> vec3<f32> {
|
|||
fn selective_row(r: i32, w: f32, c: vec3<f32>, relative: bool) -> vec3<f32> {
|
||||
let k = lut_at(r * 4 + 3) / 100.0;
|
||||
var d = vec3(lut_at(r * 4), lut_at(r * 4 + 1), lut_at(r * 4 + 2)) / 100.0 + k;
|
||||
if (relative) { d = d * (1.0 - c); }
|
||||
if (relative) {
|
||||
d = d * (1.0 - c);
|
||||
} else {
|
||||
// Absolute: scaled by the room the ink has (adding: 2 × (1 − ink), removing: 2 × ink, capped at 1).
|
||||
d = d * select(min(2.0 * (1.0 - c), vec3(1.0)), min(2.0 * c, vec3(1.0)), d > vec3(0.0));
|
||||
}
|
||||
return select(vec3(0.0), d * w, w > 0.0);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue