mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-26 08:58:11 +08:00
Fix the blend mode formulas for the Overlay, whole-color, and alpha-only modes (#4449)
This commit is contained in:
@@ -190,11 +190,14 @@ impl Display for BlendMode {
|
|||||||
/// Composites `foreground` over `background` with the given blend mode, fading the result by `opacity`.
|
/// Composites `foreground` over `background` with the given blend mode, fading the result by `opacity`.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f32) -> Color {
|
pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f32) -> Color {
|
||||||
|
// The alpha-only utility modes composite no color, so opacity interpolates their alpha toward the backdrop's instead
|
||||||
|
let faded_alpha = |applied: Color| background.with_alpha(background.a() + (applied.a() - background.a()) * opacity);
|
||||||
|
|
||||||
let target_color = match blend_mode {
|
let target_color = match blend_mode {
|
||||||
// The alpha-only utility modes composite alpha directly, with no color mixing to fade
|
// The alpha-only utility modes composite alpha directly, with no color mixing to fade
|
||||||
BlendMode::Erase => return background.alpha_subtract(foreground),
|
BlendMode::Erase => return faded_alpha(background.alpha_subtract(foreground)),
|
||||||
BlendMode::Restore => return background.alpha_add(foreground),
|
BlendMode::Restore => return faded_alpha(background.alpha_add(foreground)),
|
||||||
BlendMode::MultiplyAlpha => return background.alpha_multiply(foreground),
|
BlendMode::MultiplyAlpha => return faded_alpha(background.alpha_multiply(foreground)),
|
||||||
blend_mode => apply_blend_mode(foreground, background, blend_mode),
|
blend_mode => apply_blend_mode(foreground, background, blend_mode),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -254,4 +257,51 @@ mod tests {
|
|||||||
let swapped_hard_light = apply_blend_mode(b, a, BlendMode::HardLight);
|
let swapped_hard_light = apply_blend_mode(b, a, BlendMode::HardLight);
|
||||||
assert_eq!(overlay, swapped_hard_light);
|
assert_eq!(overlay, swapped_hard_light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blended_colors_keep_the_foreground_alpha() {
|
||||||
|
let foreground = Color::from_rgbaf32_unchecked(0.8, 0.3, 0.6, 0.25);
|
||||||
|
let background = Color::from_rgbaf32_unchecked(0.2, 0.7, 0.4, 1.);
|
||||||
|
|
||||||
|
let modes = [
|
||||||
|
BlendMode::Multiply,
|
||||||
|
BlendMode::Overlay,
|
||||||
|
BlendMode::DarkerColor,
|
||||||
|
BlendMode::LighterColor,
|
||||||
|
BlendMode::Hue,
|
||||||
|
BlendMode::Saturation,
|
||||||
|
BlendMode::Color,
|
||||||
|
BlendMode::Luminosity,
|
||||||
|
];
|
||||||
|
for mode in modes {
|
||||||
|
let blended = apply_blend_mode(foreground, background, mode);
|
||||||
|
assert!((blended.a() - 0.25).abs() < 1e-5, "{mode} alpha was {}", blended.a());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn darker_color_compares_unassociated_channels() {
|
||||||
|
// The premultiplied backdrop reads as 0.1 gray but is really 0.5 gray, so the 0.4 gray foreground is the darker color
|
||||||
|
let foreground = Color::from_rgbaf32_unchecked(0.4, 0.4, 0.4, 1.);
|
||||||
|
let background = Color::from_rgbaf32_unchecked(0.1, 0.1, 0.1, 0.2);
|
||||||
|
|
||||||
|
let blended = apply_blend_mode(foreground, background, BlendMode::DarkerColor);
|
||||||
|
|
||||||
|
assert!((blended.r() - 0.4).abs() < 1e-5, "red was {}", blended.r());
|
||||||
|
assert!((blended.a() - 1.).abs() < 1e-5, "alpha was {}", blended.a());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alpha_only_modes_fade_with_opacity() {
|
||||||
|
let foreground = Color::from_rgbaf32_unchecked(0.9, 0.9, 0.9, 1.);
|
||||||
|
let background = Color::from_rgbaf32_unchecked(0.3, 0.5, 0.7, 1.);
|
||||||
|
|
||||||
|
// A full-opacity erase removes all coverage, and half opacity fades that effect halfway back toward the backdrop
|
||||||
|
let full = blend_colors(foreground, background, BlendMode::Erase, 1.);
|
||||||
|
let half = blend_colors(foreground, background, BlendMode::Erase, 0.5);
|
||||||
|
|
||||||
|
assert!((full.a() - 0.).abs() < 1e-5, "alpha was {}", full.a());
|
||||||
|
assert!((half.a() - 0.5).abs() < 1e-5, "alpha was {}", half.a());
|
||||||
|
assert!((half.r() - 0.3).abs() < 1e-5, "red was {}", half.r());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -704,10 +704,13 @@ impl Color {
|
|||||||
c_b + c_s - 1.
|
c_b + c_s - 1.
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whole-color "Darker Color" blend: keeps whichever color has the lower mean RGB.
|
/// Whole-color "Darker Color" blend: keeps whichever color has the lower mean RGB, with `other`'s alpha.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_darker_color(&self, other: Color) -> Color {
|
pub fn blend_darker_color(&self, other: Color) -> Color {
|
||||||
if self.average_rgb_channels() <= other.average_rgb_channels() { *self } else { other }
|
let background = self.to_unassociated_alpha();
|
||||||
|
let darker = if background.average_rgb_channels() <= other.average_rgb_channels() { background } else { other };
|
||||||
|
|
||||||
|
darker.with_alpha(other.alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-channel "Screen" blend.
|
/// Per-channel "Screen" blend.
|
||||||
@@ -734,10 +737,13 @@ impl Color {
|
|||||||
c_b + c_s
|
c_b + c_s
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whole-color "Lighter Color" blend: keeps whichever color has the higher mean RGB.
|
/// Whole-color "Lighter Color" blend: keeps whichever color has the higher mean RGB, with `other`'s alpha.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_lighter_color(&self, other: Color) -> Color {
|
pub fn blend_lighter_color(&self, other: Color) -> Color {
|
||||||
if self.average_rgb_channels() >= other.average_rgb_channels() { *self } else { other }
|
let background = self.to_unassociated_alpha();
|
||||||
|
let lighter = if background.average_rgb_channels() >= other.average_rgb_channels() { background } else { other };
|
||||||
|
|
||||||
|
lighter.with_alpha(other.alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-channel "Soft Light" blend.
|
/// Per-channel "Soft Light" blend.
|
||||||
@@ -816,33 +822,36 @@ impl Color {
|
|||||||
if c_b == 0. { 1. } else { c_b / c_s }
|
if c_b == 0. { 1. } else { c_b / c_s }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whole-color "Hue" blend: source hue with this color's saturation and Rec.601 luma.
|
/// Whole-color "Hue" blend: source hue with this color's saturation and Rec.601 luma, with `c_s`'s alpha.
|
||||||
pub fn blend_hue(&self, c_s: Color) -> Color {
|
pub fn blend_hue(&self, c_s: Color) -> Color {
|
||||||
let sat_b = self.chroma_range();
|
let background = self.to_unassociated_alpha();
|
||||||
let lum_b = self.luminance_rec_601();
|
let sat_b = background.chroma_range();
|
||||||
c_s.with_saturation(sat_b).with_luminance(lum_b)
|
let lum_b = background.luminance_rec_601();
|
||||||
|
|
||||||
|
c_s.with_saturation(sat_b).with_luminance(lum_b).with_alpha(c_s.alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whole-color "Saturation" blend: this color's hue/luma with source saturation.
|
/// Whole-color "Saturation" blend: this color's hue/luma with source saturation, with `c_s`'s alpha.
|
||||||
pub fn blend_saturation(&self, c_s: Color) -> Color {
|
pub fn blend_saturation(&self, c_s: Color) -> Color {
|
||||||
|
let background = self.to_unassociated_alpha();
|
||||||
let sat_s = c_s.chroma_range();
|
let sat_s = c_s.chroma_range();
|
||||||
let lum_b = self.luminance_rec_601();
|
let lum_b = background.luminance_rec_601();
|
||||||
|
|
||||||
self.with_saturation(sat_s).with_luminance(lum_b)
|
background.with_saturation(sat_s).with_luminance(lum_b).with_alpha(c_s.alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whole-color "Color" blend: source hue/saturation with this color's luma.
|
/// Whole-color "Color" blend: source hue/saturation with this color's luma, with `c_s`'s alpha.
|
||||||
pub fn blend_color(&self, c_s: Color) -> Color {
|
pub fn blend_color(&self, c_s: Color) -> Color {
|
||||||
let lum_b = self.luminance_rec_601();
|
let lum_b = self.to_unassociated_alpha().luminance_rec_601();
|
||||||
|
|
||||||
c_s.with_luminance(lum_b)
|
c_s.with_luminance(lum_b).with_alpha(c_s.alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whole-color "Luminosity" blend: this color's hue/saturation with source luma.
|
/// Whole-color "Luminosity" blend: this color's hue/saturation with source luma, with `c_s`'s alpha.
|
||||||
pub fn blend_luminosity(&self, c_s: Color) -> Color {
|
pub fn blend_luminosity(&self, c_s: Color) -> Color {
|
||||||
let lum_s = c_s.luminance_rec_601();
|
let lum_s = c_s.luminance_rec_601();
|
||||||
|
|
||||||
self.with_luminance(lum_s)
|
self.to_unassociated_alpha().with_luminance(lum_s).with_alpha(c_s.alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All four channels as `(red, green, blue, alpha)`.
|
/// All four channels as `(red, green, blue, alpha)`.
|
||||||
|
|||||||
Reference in New Issue
Block a user