#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlendFactor {
Zero,
One,
SrcAlpha,
OneMinusSrcAlpha,
DstAlpha,
OneMinusDstAlpha,
DstColor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlendFactors {
pub src: BlendFactor,
pub dst: BlendFactor,
}
impl BlendFactors {
const fn new(src: BlendFactor, dst: BlendFactor) -> Self {
Self { src, dst }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum BlendMode {
Clear,
Src,
Dst,
#[default]
SrcOver,
DstOver,
SrcIn,
DstIn,
SrcOut,
DstOut,
SrcATop,
DstATop,
Xor,
Plus,
Modulate,
Multiply,
Screen,
Overlay,
Darken,
Lighten,
ColorDodge,
ColorBurn,
HardLight,
SoftLight,
Difference,
Exclusion,
Hue,
Saturation,
Color,
Luminosity,
}
impl BlendMode {
pub fn is_destructive(self) -> bool {
matches!(
self,
Self::Clear
| Self::Src
| Self::SrcIn
| Self::DstIn
| Self::SrcOut
| Self::DstOut
| Self::DstATop
| Self::Xor
| Self::Modulate
)
}
pub const PORTER_DUFF: &'static [Self] = &[
Self::Clear,
Self::Src,
Self::Dst,
Self::SrcOver,
Self::DstOver,
Self::SrcIn,
Self::DstIn,
Self::SrcOut,
Self::DstOut,
Self::SrcATop,
Self::DstATop,
Self::Xor,
Self::Plus,
Self::Modulate,
];
pub const ADVANCED: &'static [Self] = &[
Self::Multiply,
Self::Screen,
Self::Overlay,
Self::Darken,
Self::Lighten,
Self::ColorDodge,
Self::ColorBurn,
Self::HardLight,
Self::SoftLight,
Self::Difference,
Self::Exclusion,
Self::Hue,
Self::Saturation,
Self::Color,
Self::Luminosity,
];
pub const ALL: &'static [Self] = &[
Self::Clear,
Self::Src,
Self::Dst,
Self::SrcOver,
Self::DstOver,
Self::SrcIn,
Self::DstIn,
Self::SrcOut,
Self::DstOut,
Self::SrcATop,
Self::DstATop,
Self::Xor,
Self::Plus,
Self::Modulate,
Self::Multiply,
Self::Screen,
Self::Overlay,
Self::Darken,
Self::Lighten,
Self::ColorDodge,
Self::ColorBurn,
Self::HardLight,
Self::SoftLight,
Self::Difference,
Self::Exclusion,
Self::Hue,
Self::Saturation,
Self::Color,
Self::Luminosity,
];
pub const fn code(self) -> f32 {
match self {
Self::Clear => 0.0,
Self::Src => 1.0,
Self::Dst => 2.0,
Self::SrcOver => 3.0,
Self::DstOver => 4.0,
Self::SrcIn => 5.0,
Self::DstIn => 6.0,
Self::SrcOut => 7.0,
Self::DstOut => 8.0,
Self::SrcATop => 9.0,
Self::DstATop => 10.0,
Self::Xor => 11.0,
Self::Plus => 12.0,
Self::Modulate => 13.0,
Self::Multiply => 14.0,
Self::Screen => 15.0,
Self::Overlay => 16.0,
Self::Darken => 17.0,
Self::Lighten => 18.0,
Self::ColorDodge => 19.0,
Self::ColorBurn => 20.0,
Self::HardLight => 21.0,
Self::SoftLight => 22.0,
Self::Difference => 23.0,
Self::Exclusion => 24.0,
Self::Hue => 25.0,
Self::Saturation => 26.0,
Self::Color => 27.0,
Self::Luminosity => 28.0,
}
}
pub const fn is_advanced(self) -> bool {
!matches!(
self,
Self::Clear
| Self::Src
| Self::Dst
| Self::SrcOver
| Self::DstOver
| Self::SrcIn
| Self::DstIn
| Self::SrcOut
| Self::DstOut
| Self::SrcATop
| Self::DstATop
| Self::Xor
| Self::Plus
| Self::Modulate
)
}
pub const fn factors(self) -> Option<BlendFactors> {
use BlendFactor::*;
Some(match self {
Self::Clear => BlendFactors::new(Zero, Zero),
Self::Src => BlendFactors::new(One, Zero),
Self::Dst => BlendFactors::new(Zero, One),
Self::SrcOver => BlendFactors::new(One, OneMinusSrcAlpha),
Self::DstOver => BlendFactors::new(OneMinusDstAlpha, One),
Self::SrcIn => BlendFactors::new(DstAlpha, Zero),
Self::DstIn => BlendFactors::new(Zero, SrcAlpha),
Self::SrcOut => BlendFactors::new(OneMinusDstAlpha, Zero),
Self::DstOut => BlendFactors::new(Zero, OneMinusSrcAlpha),
Self::SrcATop => BlendFactors::new(DstAlpha, OneMinusSrcAlpha),
Self::DstATop => BlendFactors::new(OneMinusDstAlpha, SrcAlpha),
Self::Xor => BlendFactors::new(OneMinusDstAlpha, OneMinusSrcAlpha),
Self::Plus => BlendFactors::new(One, One),
Self::Modulate => BlendFactors::new(DstColor, Zero),
_ => return None,
})
}
pub const fn reads_destination(self) -> bool {
match self.factors() {
Some(factors) => {
!matches!(factors.dst, BlendFactor::Zero)
|| matches!(
factors.src,
BlendFactor::DstAlpha
| BlendFactor::OneMinusDstAlpha
| BlendFactor::DstColor
)
}
None => true,
}
}
pub const fn respects_coverage(self) -> bool {
match self.factors() {
Some(factors) => matches!(
factors.dst,
BlendFactor::One | BlendFactor::OneMinusSrcAlpha
),
None => true,
}
}
pub const fn is_plain_write(self) -> bool {
matches!(self, Self::Src)
}
pub const fn name(self) -> &'static str {
match self {
Self::Clear => "clear",
Self::Src => "src",
Self::Dst => "dst",
Self::SrcOver => "src-over",
Self::DstOver => "dst-over",
Self::SrcIn => "src-in",
Self::DstIn => "dst-in",
Self::SrcOut => "src-out",
Self::DstOut => "dst-out",
Self::SrcATop => "src-atop",
Self::DstATop => "dst-atop",
Self::Xor => "xor",
Self::Plus => "plus",
Self::Modulate => "modulate",
Self::Multiply => "multiply",
Self::Screen => "screen",
Self::Overlay => "overlay",
Self::Darken => "darken",
Self::Lighten => "lighten",
Self::ColorDodge => "color-dodge",
Self::ColorBurn => "color-burn",
Self::HardLight => "hard-light",
Self::SoftLight => "soft-light",
Self::Difference => "difference",
Self::Exclusion => "exclusion",
Self::Hue => "hue",
Self::Saturation => "saturation",
Self::Color => "color",
Self::Luminosity => "luminosity",
}
}
}
impl std::fmt::Display for BlendMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
pub fn separable_blend(mode: BlendMode, backdrop: f32, source: f32) -> Option<f32> {
let (cb, cs) = (backdrop, source);
let multiply = |a: f32, b: f32| a * b;
let screen = |a: f32, b: f32| a + b - a * b;
let hard_light = |cb: f32, cs: f32| {
if cs <= 0.5 {
multiply(cb, 2.0 * cs)
} else {
screen(cb, 2.0 * cs - 1.0)
}
};
Some(match mode {
BlendMode::Multiply => multiply(cb, cs),
BlendMode::Screen => screen(cb, cs),
BlendMode::Overlay => hard_light(cs, cb),
BlendMode::Darken => cb.min(cs),
BlendMode::Lighten => cb.max(cs),
BlendMode::ColorDodge => {
if cb <= 0.0 {
0.0
} else if cs >= 1.0 {
1.0
} else {
(cb / (1.0 - cs)).min(1.0)
}
}
BlendMode::ColorBurn => {
if cb >= 1.0 {
1.0
} else if cs <= 0.0 {
0.0
} else {
1.0 - ((1.0 - cb) / cs).min(1.0)
}
}
BlendMode::HardLight => hard_light(cb, cs),
BlendMode::SoftLight => {
if cs <= 0.5 {
cb - (1.0 - 2.0 * cs) * cb * (1.0 - cb)
} else {
let d = if cb <= 0.25 {
((16.0 * cb - 12.0) * cb + 4.0) * cb
} else {
cb.sqrt()
};
cb + (2.0 * cs - 1.0) * (d - cb)
}
}
BlendMode::Difference => (cs - cb).abs(),
BlendMode::Exclusion => cs + cb - 2.0 * cs * cb,
_ => return None,
})
}
pub fn nonseparable_blend(
mode: BlendMode,
backdrop: [f32; 3],
source: [f32; 3],
) -> Option<[f32; 3]> {
fn lum(c: [f32; 3]) -> f32 {
0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
}
fn clip_color(mut c: [f32; 3]) -> [f32; 3] {
let l = lum(c);
let n = c[0].min(c[1]).min(c[2]);
let x = c[0].max(c[1]).max(c[2]);
if n < 0.0 {
let span = l - n;
if span > 0.0 {
for channel in &mut c {
*channel = l + (*channel - l) * l / span;
}
}
}
if x > 1.0 {
let span = x - l;
if span > 0.0 {
for channel in &mut c {
*channel = l + (*channel - l) * (1.0 - l) / span;
}
}
}
c
}
fn set_lum(mut c: [f32; 3], l: f32) -> [f32; 3] {
let d = l - lum(c);
for channel in &mut c {
*channel += d;
}
clip_color(c)
}
fn sat(c: [f32; 3]) -> f32 {
c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
}
fn set_sat(c: [f32; 3], s: f32) -> [f32; 3] {
let mut order = [0usize, 1, 2];
order.sort_by(|a, b| {
c[*a]
.partial_cmp(&c[*b])
.unwrap_or(std::cmp::Ordering::Equal)
});
let (low, mid, high) = (order[0], order[1], order[2]);
let mut out = [0.0f32; 3];
if c[high] > c[low] {
out[mid] = (c[mid] - c[low]) * s / (c[high] - c[low]);
out[high] = s;
}
out[low] = 0.0;
out
}
let (cb, cs) = (backdrop, source);
Some(match mode {
BlendMode::Hue => set_lum(set_sat(cs, sat(cb)), lum(cb)),
BlendMode::Saturation => set_lum(set_sat(cb, sat(cs)), lum(cb)),
BlendMode::Color => set_lum(cs, lum(cb)),
BlendMode::Luminosity => set_lum(cb, lum(cs)),
_ => return None,
})
}
pub fn blend_advanced(mode: BlendMode, source: [f32; 4], backdrop: [f32; 4]) -> Option<[f32; 4]> {
let (a_s, a_b) = (source[3], backdrop[3]);
let mut out = [0.0f32; 4];
out[3] = a_s + a_b - a_s * a_b;
let straight = |c: [f32; 4], a: f32| {
if a > 0.0 {
[
(c[0] / a).clamp(0.0, 1.0),
(c[1] / a).clamp(0.0, 1.0),
(c[2] / a).clamp(0.0, 1.0),
]
} else {
[0.0; 3]
}
};
let cs = straight(source, a_s);
let cb = straight(backdrop, a_b);
let blended = match nonseparable_blend(mode, cb, cs) {
Some(blended) => blended,
None => {
let mut per_channel = [0.0f32; 3];
for (channel, slot) in per_channel.iter_mut().enumerate() {
*slot = separable_blend(mode, cb[channel], cs[channel])?;
}
per_channel
}
};
for channel in 0..3 {
out[channel] = a_s * (1.0 - a_b) * cs[channel]
+ a_s * a_b * blended[channel]
+ (1.0 - a_s) * a_b * cb[channel];
}
Some(out)
}
#[cfg(test)]
mod coverage_tests {
use super::*;
#[test]
fn a_mode_respects_coverage_when_a_transparent_source_changes_nothing() {
for mode in [
BlendMode::SrcOver,
BlendMode::DstOver,
BlendMode::Dst,
BlendMode::Plus,
BlendMode::Xor,
BlendMode::SrcATop,
BlendMode::DstOut,
] {
assert!(mode.respects_coverage(), "{} should", mode.name());
}
for mode in [
BlendMode::Clear,
BlendMode::Src,
BlendMode::SrcIn,
BlendMode::DstIn,
BlendMode::SrcOut,
BlendMode::DstATop,
BlendMode::Modulate,
] {
assert!(!mode.respects_coverage(), "{} should not", mode.name());
}
}
#[test]
fn every_advanced_mode_respects_coverage() {
for mode in BlendMode::ALL {
if mode.is_advanced() {
assert!(mode.respects_coverage(), "{} should", mode.name());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn value(factor: BlendFactor, src: [f32; 4], dst: [f32; 4], channel: usize) -> f32 {
match factor {
BlendFactor::Zero => 0.0,
BlendFactor::One => 1.0,
BlendFactor::SrcAlpha => src[3],
BlendFactor::OneMinusSrcAlpha => 1.0 - src[3],
BlendFactor::DstAlpha => dst[3],
BlendFactor::OneMinusDstAlpha => 1.0 - dst[3],
BlendFactor::DstColor => dst[channel],
}
}
fn blend(mode: BlendMode, src: [f32; 4], dst: [f32; 4]) -> [f32; 4] {
let f = mode.factors().expect("Porter-Duff mode");
let mut out = [0.0f32; 4];
for channel in 0..4 {
out[channel] = (src[channel] * value(f.src, src, dst, channel)
+ dst[channel] * value(f.dst, src, dst, channel))
.clamp(0.0, 1.0);
}
out
}
fn close(a: [f32; 4], b: [f32; 4]) -> bool {
a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-5)
}
const SRC: [f32; 4] = [0.5, 0.0, 0.0, 0.5];
const DST: [f32; 4] = [0.0, 0.0, 1.0, 1.0];
#[test]
fn clear_leaves_nothing() {
assert!(close(blend(BlendMode::Clear, SRC, DST), [0.0; 4]));
}
#[test]
fn src_and_dst_each_keep_one_side_entirely() {
assert!(close(blend(BlendMode::Src, SRC, DST), SRC));
assert!(close(blend(BlendMode::Dst, SRC, DST), DST));
}
#[test]
fn source_over_and_destination_over_are_mirror_images() {
assert!(close(
blend(BlendMode::SrcOver, SRC, DST),
[0.5, 0.0, 0.5, 1.0]
));
assert!(close(blend(BlendMode::DstOver, SRC, DST), DST));
}
#[test]
fn the_in_modes_clip_one_side_to_the_other() {
assert!(close(blend(BlendMode::SrcIn, SRC, DST), SRC));
assert!(close(
blend(BlendMode::DstIn, SRC, DST),
[0.0, 0.0, 0.5, 0.5]
));
}
#[test]
fn the_out_modes_are_the_complement_of_the_in_modes() {
assert!(close(blend(BlendMode::SrcOut, SRC, DST), [0.0; 4]));
assert!(close(
blend(BlendMode::DstOut, SRC, DST),
[0.0, 0.0, 0.5, 0.5]
));
}
#[test]
fn atop_keeps_the_shape_of_the_side_it_is_named_for() {
let result = blend(BlendMode::SrcATop, SRC, DST);
assert!((result[3] - DST[3]).abs() < 1e-5, "alpha should follow dst");
assert!(close(result, [0.5, 0.0, 0.5, 1.0]));
}
#[test]
fn xor_keeps_only_what_the_other_side_does_not_cover() {
assert!(close(blend(BlendMode::Xor, SRC, DST), [0.0, 0.0, 0.5, 0.5]));
}
#[test]
fn plus_accumulates_and_saturates() {
let sum = blend(BlendMode::Plus, SRC, DST);
assert!(close(sum, [0.5, 0.0, 1.0, 1.0]));
let bright = blend(BlendMode::Plus, [0.8, 0.8, 0.8, 1.0], [0.8, 0.8, 0.8, 1.0]);
assert!(close(bright, [1.0, 1.0, 1.0, 1.0]));
}
#[test]
fn modulate_multiplies_the_two() {
let result = blend(
BlendMode::Modulate,
[0.5, 1.0, 0.5, 1.0],
[0.5, 0.5, 1.0, 1.0],
);
assert!(close(result, [0.25, 0.5, 0.5, 1.0]));
}
#[test]
fn only_src_can_skip_blending_entirely() {
for mode in BlendMode::PORTER_DUFF {
assert_eq!(
mode.is_plain_write(),
*mode == BlendMode::Src,
"{mode} misreported whether it is a plain write"
);
}
}
#[test]
fn modes_that_ignore_the_destination_are_identified() {
for mode in [BlendMode::Clear, BlendMode::Src] {
assert!(!mode.reads_destination(), "{mode}");
}
for mode in [
BlendMode::SrcOver,
BlendMode::DstOver,
BlendMode::SrcIn,
BlendMode::Modulate,
BlendMode::Xor,
] {
assert!(mode.reads_destination(), "{mode}");
}
}
#[test]
fn the_separable_modes_hit_their_defining_fixed_points() {
let cases: &[(BlendMode, f32, f32, f32)] = &[
(BlendMode::Multiply, 0.5, 0.5, 0.25),
(BlendMode::Screen, 0.5, 0.5, 0.75),
(BlendMode::Darken, 0.2, 0.8, 0.2),
(BlendMode::Lighten, 0.2, 0.8, 0.8),
(BlendMode::Difference, 0.2, 0.8, 0.6),
(BlendMode::Exclusion, 0.5, 0.5, 0.5),
(BlendMode::HardLight, 0.3, 0.5, 0.3),
(BlendMode::Overlay, 0.5, 0.3, 0.3),
(BlendMode::SoftLight, 0.3, 0.5, 0.3),
(BlendMode::ColorDodge, 0.0, 1.0, 0.0),
(BlendMode::ColorDodge, 0.25, 0.5, 0.5),
(BlendMode::ColorBurn, 1.0, 0.0, 1.0),
(BlendMode::ColorBurn, 0.5, 0.5, 0.0),
];
for &(mode, cb, cs, want) in cases {
let got = separable_blend(mode, cb, cs).expect("separable");
assert!(
(got - want).abs() < 1e-6,
"{mode}(backdrop {cb}, source {cs}) gave {got}, want {want}"
);
}
}
#[test]
fn soft_light_is_continuous_where_its_two_branches_meet() {
for &(cb, cs) in &[(0.25, 0.5), (0.2499, 0.75), (0.5, 0.4999)] {
let here = separable_blend(BlendMode::SoftLight, cb, cs).unwrap();
let there = separable_blend(BlendMode::SoftLight, cb + 2e-4, cs + 2e-4).unwrap();
assert!((here - there).abs() < 1e-3, "step at ({cb}, {cs})");
}
}
#[test]
fn an_advanced_mode_over_nothing_is_just_the_source() {
let src = [0.3, 0.0, 0.15, 0.6];
for mode in BlendMode::ADVANCED {
let out = blend_advanced(*mode, src, [0.0; 4]).expect("advanced");
assert!(close(out, src), "{mode} over nothing gave {out:?}");
}
}
#[test]
fn the_non_separable_modes_take_what_they_are_named_for() {
fn lum(c: [f32; 3]) -> f32 {
0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
}
fn sat(c: [f32; 3]) -> f32 {
c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
}
let cb = [0.15, 0.55, 0.35];
let cs = [0.75, 0.5, 0.45];
assert!((lum(cb) - lum(cs)).abs() > 0.05);
assert!((sat(cb) - sat(cs)).abs() > 0.05);
let close = |a: f32, b: f32| (a - b).abs() < 1e-4;
let hue = nonseparable_blend(BlendMode::Hue, cb, cs).unwrap();
assert!(close(lum(hue), lum(cb)), "hue kept the wrong luminosity");
assert!(close(sat(hue), sat(cb)), "hue kept the wrong saturation");
let saturation = nonseparable_blend(BlendMode::Saturation, cb, cs).unwrap();
assert!(
close(lum(saturation), lum(cb)),
"saturation moved luminosity"
);
assert!(
close(sat(saturation), sat(cs)),
"saturation did not take the source's"
);
let color = nonseparable_blend(BlendMode::Color, cb, cs).unwrap();
assert!(close(lum(color), lum(cb)), "color moved luminosity");
let luminosity = nonseparable_blend(BlendMode::Luminosity, cb, cs).unwrap();
assert!(
close(lum(luminosity), lum(cs)),
"luminosity did not take the source's"
);
assert!(
close(sat(luminosity), sat(cb)),
"luminosity moved saturation"
);
}
#[test]
fn setting_a_luminosity_keeps_every_channel_in_range() {
for (cb, cs) in [
([0.02, 0.02, 0.9], [0.99, 0.99, 0.99]),
([0.99, 0.5, 0.02], [0.01, 0.01, 0.01]),
([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]),
([1.0, 1.0, 1.0], [0.0, 0.0, 0.0]),
] {
for mode in [BlendMode::Color, BlendMode::Luminosity, BlendMode::Hue] {
let out = nonseparable_blend(mode, cb, cs).unwrap();
for channel in out {
assert!(
(-1e-5..=1.0 + 1e-5).contains(&channel),
"{mode} on {cb:?} and {cs:?} gave {out:?}"
);
assert!(channel.is_finite(), "{mode} produced {out:?}");
}
}
}
}
#[test]
fn a_flat_backdrop_has_no_saturation_to_take() {
let gray = [0.4, 0.4, 0.4];
let colorful = [0.9, 0.2, 0.5];
let hue = nonseparable_blend(BlendMode::Hue, gray, colorful).unwrap();
assert!(
hue.iter().all(|c| (c - hue[0]).abs() < 1e-5),
"hue from a gray backdrop came back colored: {hue:?}"
);
let saturation = nonseparable_blend(BlendMode::Saturation, gray, colorful).unwrap();
assert!(
saturation.iter().all(|c| (c - saturation[0]).abs() < 1e-5),
"saturating a gray produced color from nothing: {saturation:?}"
);
}
#[test]
fn the_two_families_do_not_overlap() {
assert_eq!(
BlendMode::PORTER_DUFF.len() + BlendMode::ADVANCED.len(),
BlendMode::ALL.len()
);
for mode in BlendMode::ALL {
assert_eq!(
mode.is_advanced(),
mode.factors().is_none(),
"{mode} disagrees with itself about which family it is in"
);
let has_formula = separable_blend(*mode, 0.5, 0.5).is_some()
|| nonseparable_blend(*mode, [0.5; 3], [0.5; 3]).is_some();
assert_eq!(
mode.is_advanced(),
has_formula,
"{mode} has no advanced formula but claims to be advanced"
);
assert!(
!(separable_blend(*mode, 0.5, 0.5).is_some()
&& nonseparable_blend(*mode, [0.5; 3], [0.5; 3]).is_some()),
"{mode} has a formula in both families"
);
}
}
#[test]
fn every_mode_has_a_distinct_name() {
let mut names: Vec<&str> = BlendMode::ALL.iter().map(|m| m.name()).collect();
names.sort_unstable();
let count = names.len();
names.dedup();
assert_eq!(names.len(), count, "duplicate blend mode name");
}
}