mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
fix(color): match Photoshop blend corner values (#699)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
This commit is contained in:
@@ -164,6 +164,8 @@ pub fn blend_channel(mode: BlendMode, cb: f32, cs: f32) -> f32 {
|
||||
BlendMode::Overlay => hard_light(cs, cb),
|
||||
BlendMode::SoftLight => soft_light_ps(cb, cs),
|
||||
BlendMode::HardLight => hard_light(cb, cs),
|
||||
BlendMode::VividLight if cs <= 0.0 => 0.0,
|
||||
BlendMode::VividLight if cs >= 1.0 => 1.0,
|
||||
BlendMode::VividLight => {
|
||||
if cs <= 0.5 {
|
||||
color_burn(cb, 2.0 * cs)
|
||||
@@ -180,7 +182,7 @@ pub fn blend_channel(mode: BlendMode, cb: f32, cs: f32) -> f32 {
|
||||
}
|
||||
}
|
||||
BlendMode::HardMix => {
|
||||
if cb + cs >= 1.0 - 1e-6 {
|
||||
if vivid_light_hard_mix(cb, cs) >= 0.5 - 1e-6 {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
@@ -201,6 +203,26 @@ pub fn blend_channel(mode: BlendMode, cb: f32, cs: f32) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn vivid_light_hard_mix(cb: f32, cs: f32) -> f32 {
|
||||
const EDGE: f32 = 1e-4;
|
||||
if cs <= 0.5 {
|
||||
if cb >= 1.0 - EDGE {
|
||||
1.0
|
||||
} else if cs <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
1.0 - ((1.0 - cb) / (2.0 * cs)).min(1.0)
|
||||
}
|
||||
} else if cb <= EDGE {
|
||||
0.0
|
||||
} else if cs >= 1.0 {
|
||||
1.0
|
||||
} else {
|
||||
(cb / (2.0 * (1.0 - cs))).min(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn color_burn(cb: f32, cs: f32) -> f32 {
|
||||
if cb >= 1.0 {
|
||||
@@ -409,6 +431,34 @@ mod tests {
|
||||
assert!(close(blend_channel(BlendMode::HardMix, 0.6, 0.3), 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vivid_light_source_extremes_and_interior_values() {
|
||||
for cb in [0.0, 0.25, 0.5, 0.75, 1.0] {
|
||||
assert_eq!(blend_channel(BlendMode::VividLight, cb, 0.0), 0.0);
|
||||
assert_eq!(blend_channel(BlendMode::VividLight, cb, 1.0), 1.0);
|
||||
}
|
||||
assert!(close(blend_channel(BlendMode::VividLight, 0.75, 0.25), 0.5));
|
||||
assert!(close(blend_channel(BlendMode::VividLight, 0.25, 0.75), 0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hard_mix_matches_photoshop_extremes_and_interior_values() {
|
||||
assert_eq!(blend_channel(BlendMode::HardMix, 1.0, 0.0), 1.0);
|
||||
assert_eq!(blend_channel(BlendMode::HardMix, 0.0, 1.0), 0.0);
|
||||
assert_eq!(blend_channel(BlendMode::HardMix, 1.0 - 1e-5, 0.0), 1.0);
|
||||
assert_eq!(blend_channel(BlendMode::HardMix, 1e-5, 1.0), 0.0);
|
||||
assert_eq!(blend_channel(BlendMode::HardMix, 0.6, 0.4), 1.0);
|
||||
assert_eq!(blend_channel(BlendMode::HardMix, 0.6, 0.3), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vivid_light_and_hard_mix_composite_extremes() {
|
||||
assert_eq!(composite(BlendMode::VividLight, [1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 1.0], 1.0), [0.0, 0.0, 0.0, 1.0]);
|
||||
assert_eq!(composite(BlendMode::VividLight, [0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0, 1.0], 1.0), [1.0, 1.0, 1.0, 1.0]);
|
||||
assert_eq!(composite(BlendMode::HardMix, [0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0, 1.0], 1.0), [0.0, 0.0, 0.0, 1.0]);
|
||||
assert_eq!(composite(BlendMode::HardMix, [1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 1.0], 1.0), [1.0, 1.0, 1.0, 1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn separable_outputs_stay_in_unit_range() {
|
||||
let steps: Vec<f32> = (0..=20).map(|i| i as f32 / 20.0).collect();
|
||||
|
||||
@@ -258,4 +258,28 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_vivid_light_and_hard_mix_composites_match_photoshop() {
|
||||
let cases = [
|
||||
(BlendMode::HardMix, 0.0, 1.0),
|
||||
(BlendMode::HardMix, 1.0, 0.0),
|
||||
(BlendMode::HardMix, 0.6, 0.4),
|
||||
(BlendMode::HardMix, 0.6, 0.3),
|
||||
(BlendMode::VividLight, 1.0, 0.0),
|
||||
(BlendMode::VividLight, 0.0, 1.0),
|
||||
(BlendMode::VividLight, 0.75, 0.25),
|
||||
(BlendMode::VividLight, 0.25, 0.75),
|
||||
];
|
||||
|
||||
for (mode, cb, cs) in cases {
|
||||
let backdrop = [cb, cb, cb, 1.0];
|
||||
let source = [cs, cs, cs, 1.0];
|
||||
let generic = generic::composite(mode, backdrop, source, 1.0);
|
||||
let photoshop = composite(mode, backdrop, source, 1.0);
|
||||
for channel in 0..4 {
|
||||
assert!((generic[channel] - photoshop[channel]).abs() < 1e-6, "{mode:?} Cb={cb} Cs={cs}: {generic:?} != {photoshop:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user