// ============================================================================
// Blend Modes (mix-blend-mode / isolation)
// ============================================================================
///|
/// CSS mix-blend-mode values
pub(all) enum BlendMode {
Normal
Multiply
Screen
Overlay
Darken
Lighten
ColorDodge
ColorBurn
HardLight
SoftLight
Difference
Exclusion
Hue
Saturation
ColorMode // 'color' in CSS
Luminosity
} derive(Debug, Eq)
///|
/// CSS isolation values
pub(all) enum Isolation {
Auto
Isolate
} derive(Debug, Eq)
///|
/// Blend two colors using the specified blend mode
fn blend_with_mode(backdrop : Color, source : Color, mode : BlendMode) -> Color {
let bd_r = backdrop.r.to_double() / 255.0
let bd_g = backdrop.g.to_double() / 255.0
let bd_b = backdrop.b.to_double() / 255.0
let bd_a = backdrop.a.to_double() / 255.0
let src_r = source.r.to_double() / 255.0
let src_g = source.g.to_double() / 255.0
let src_b = source.b.to_double() / 255.0
let src_a = source.a.to_double() / 255.0
// Apply blend mode to RGB
let (blended_r, blended_g, blended_b) = match mode {
Normal => (src_r, src_g, src_b)
Multiply => (bd_r * src_r, bd_g * src_g, bd_b * src_b)
Screen =>
(
1.0 - (1.0 - bd_r) * (1.0 - src_r),
1.0 - (1.0 - bd_g) * (1.0 - src_g),
1.0 - (1.0 - bd_b) * (1.0 - src_b),
)
Overlay =>
(
blend_overlay_channel(bd_r, src_r),
blend_overlay_channel(bd_g, src_g),
blend_overlay_channel(bd_b, src_b),
)
Darken =>
(
if bd_r < src_r {
bd_r
} else {
src_r
},
if bd_g < src_g {
bd_g
} else {
src_g
},
if bd_b < src_b {
bd_b
} else {
src_b
},
)
Lighten =>
(
if bd_r > src_r {
bd_r
} else {
src_r
},
if bd_g > src_g {
bd_g
} else {
src_g
},
if bd_b > src_b {
bd_b
} else {
src_b
},
)
ColorDodge =>
(
blend_color_dodge_channel(bd_r, src_r),
blend_color_dodge_channel(bd_g, src_g),
blend_color_dodge_channel(bd_b, src_b),
)
ColorBurn =>
(
blend_color_burn_channel(bd_r, src_r),
blend_color_burn_channel(bd_g, src_g),
blend_color_burn_channel(bd_b, src_b),
)
HardLight =>
(
blend_overlay_channel(src_r, bd_r), // HardLight is Overlay with args swapped
blend_overlay_channel(src_g, bd_g),
blend_overlay_channel(src_b, bd_b),
)
SoftLight =>
(
blend_soft_light_channel(bd_r, src_r),
blend_soft_light_channel(bd_g, src_g),
blend_soft_light_channel(bd_b, src_b),
)
Difference =>
((bd_r - src_r).abs(), (bd_g - src_g).abs(), (bd_b - src_b).abs())
Exclusion =>
(
bd_r + src_r - 2.0 * bd_r * src_r,
bd_g + src_g - 2.0 * bd_g * src_g,
bd_b + src_b - 2.0 * bd_b * src_b,
)
Hue => blend_hue(bd_r, bd_g, bd_b, src_r, src_g, src_b)
Saturation => blend_saturation(bd_r, bd_g, bd_b, src_r, src_g, src_b)
ColorMode => blend_color_mode(bd_r, bd_g, bd_b, src_r, src_g, src_b)
Luminosity => blend_luminosity(bd_r, bd_g, bd_b, src_r, src_g, src_b)
}
// W3C compositing and blending formula in premultiplied form.
let out_a = src_a + bd_a * (1.0 - src_a)
if out_a < 0.001 {
return Color::transparent()
}
let out_r = (
src_a * (1.0 - bd_a) * src_r +
src_a * bd_a * blended_r +
(1.0 - src_a) * bd_a * bd_r
) /
out_a
let out_g = (
src_a * (1.0 - bd_a) * src_g +
src_a * bd_a * blended_g +
(1.0 - src_a) * bd_a * bd_g
) /
out_a
let out_b = (
src_a * (1.0 - bd_a) * src_b +
src_a * bd_a * blended_b +
(1.0 - src_a) * bd_a * bd_b
) /
out_a
Color::rgba(
clamp_int((out_r * 255.0).round().to_int(), 0, 255),
clamp_int((out_g * 255.0).round().to_int(), 0, 255),
clamp_int((out_b * 255.0).round().to_int(), 0, 255),
clamp_int((out_a * 255.0).round().to_int(), 0, 255),
)
}
///|
/// Overlay blend for a single channel
fn blend_overlay_channel(backdrop : Double, source : Double) -> Double {
if backdrop < 0.5 {
2.0 * backdrop * source
} else {
1.0 - 2.0 * (1.0 - backdrop) * (1.0 - source)
}
}
///|
/// Color dodge blend for a single channel
fn blend_color_dodge_channel(backdrop : Double, source : Double) -> Double {
if backdrop < 0.001 {
0.0
} else if source >= 0.999 {
1.0
} else {
let result = backdrop / (1.0 - source)
if result > 1.0 {
1.0
} else {
result
}
}
}
///|
/// Color burn blend for a single channel
fn blend_color_burn_channel(backdrop : Double, source : Double) -> Double {
if backdrop >= 0.999 {
1.0
} else if source < 0.001 {
0.0
} else {
let result = 1.0 - (1.0 - backdrop) / source
if result < 0.0 {
0.0
} else {
result
}
}
}
///|
/// Soft light blend for a single channel
fn blend_soft_light_channel(backdrop : Double, source : Double) -> Double {
if source <= 0.5 {
backdrop - (1.0 - 2.0 * source) * backdrop * (1.0 - backdrop)
} else {
let d = if backdrop <= 0.25 {
((16.0 * backdrop - 12.0) * backdrop + 4.0) * backdrop
} else {
sqrt_approx(backdrop)
}
backdrop + (2.0 * source - 1.0) * (d - backdrop)
}
}
///|
/// Approximate square root
fn sqrt_approx(x : Double) -> Double {
if x < 0.0 {
return 0.0
}
// Newton-Raphson iteration
let mut guess = x / 2.0
if guess < 0.001 {
guess = 0.001
}
for _ in 0..<10 {
guess = (guess + x / guess) / 2.0
}
guess
}
///|
/// Get luminosity of RGB
fn get_luminosity(r : Double, g : Double, b : Double) -> Double {
0.3 * r + 0.59 * g + 0.11 * b
}
///|
/// Get saturation as defined for CSS non-separable blend modes
fn get_saturation(r : Double, g : Double, b : Double) -> Double {
r.max(g).max(b) - r.min(g).min(b)
}
///|
/// Set saturation as defined for CSS non-separable blend modes
fn set_saturation(
r : Double,
g : Double,
b : Double,
target_saturation : Double,
) -> (Double, Double, Double) {
let min_val = r.min(g).min(b)
let max_val = r.max(g).max(b)
if max_val == min_val {
return (0.0, 0.0, 0.0)
}
let scale = target_saturation / (max_val - min_val)
((r - min_val) * scale, (g - min_val) * scale, (b - min_val) * scale)
}
///|
/// Set luminosity of RGB
fn set_luminosity(
r : Double,
g : Double,
b : Double,
target_lum : Double,
) -> (Double, Double, Double) {
let current_lum = get_luminosity(r, g, b)
let d = target_lum - current_lum
clip_color(r + d, g + d, b + d)
}
///|
/// Clip color to valid range
fn clip_color(r : Double, g : Double, b : Double) -> (Double, Double, Double) {
let lum = get_luminosity(r, g, b)
let min_val = if r < g {
if r < b {
r
} else {
b
}
} else if g < b {
g
} else {
b
}
let max_val = if r > g {
if r > b {
r
} else {
b
}
} else if g > b {
g
} else {
b
}
let mut out_r = r
let mut out_g = g
let mut out_b = b
if min_val < 0.0 && lum - min_val > 0.001 {
let factor = lum / (lum - min_val)
out_r = lum + (out_r - lum) * factor
out_g = lum + (out_g - lum) * factor
out_b = lum + (out_b - lum) * factor
}
if max_val > 1.0 && max_val - lum > 0.001 {
let factor = (1.0 - lum) / (max_val - lum)
out_r = lum + (out_r - lum) * factor
out_g = lum + (out_g - lum) * factor
out_b = lum + (out_b - lum) * factor
}
(out_r, out_g, out_b)
}
///|
/// Hue blend mode
fn blend_hue(
bd_r : Double,
bd_g : Double,
bd_b : Double,
src_r : Double,
src_g : Double,
src_b : Double,
) -> (Double, Double, Double) {
let (r, g, b) = set_saturation(
src_r,
src_g,
src_b,
get_saturation(bd_r, bd_g, bd_b),
)
set_luminosity(r, g, b, get_luminosity(bd_r, bd_g, bd_b))
}
///|
/// Saturation blend mode
fn blend_saturation(
bd_r : Double,
bd_g : Double,
bd_b : Double,
src_r : Double,
src_g : Double,
src_b : Double,
) -> (Double, Double, Double) {
let (r, g, b) = set_saturation(
bd_r,
bd_g,
bd_b,
get_saturation(src_r, src_g, src_b),
)
set_luminosity(r, g, b, get_luminosity(bd_r, bd_g, bd_b))
}
///|
/// Color blend mode
fn blend_color_mode(
bd_r : Double,
bd_g : Double,
bd_b : Double,
src_r : Double,
src_g : Double,
src_b : Double,
) -> (Double, Double, Double) {
set_luminosity(src_r, src_g, src_b, get_luminosity(bd_r, bd_g, bd_b))
}
///|
/// Luminosity blend mode
fn blend_luminosity(
bd_r : Double,
bd_g : Double,
bd_b : Double,
src_r : Double,
src_g : Double,
src_b : Double,
) -> (Double, Double, Double) {
set_luminosity(bd_r, bd_g, bd_b, get_luminosity(src_r, src_g, src_b))
}
///|
/// Blend two images using the specified blend mode
fn blend_images(backdrop : Image, source : Image, mode : BlendMode) -> Image {
let width = if backdrop.width < source.width {
backdrop.width
} else {
source.width
}
let height = if backdrop.height < source.height {
backdrop.height
} else {
source.height
}
let result = Image::new(width, height)
for y in 0..