mod modes;
mod non_separable;
use crate::convert::convert;
use crate::spaces::{Hsv, Rgb, Xyz65};
use crate::traits::ColorSpace;
use crate::Color;
use non_separable::Triple;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlendMode {
Normal,
Multiply,
Screen,
HardLight,
Overlay,
Darken,
Lighten,
ColorDodge,
ColorBurn,
SoftLight,
Difference,
Exclusion,
Hue,
Saturation,
Color,
Luminosity,
}
impl BlendMode {
fn is_non_separable(self) -> bool {
matches!(
self,
BlendMode::Hue | BlendMode::Saturation | BlendMode::Color | BlendMode::Luminosity
)
}
fn apply(self, b: f64, s: f64) -> f64 {
match self {
BlendMode::Normal => modes::normal(b, s),
BlendMode::Multiply => modes::multiply(b, s),
BlendMode::Screen => modes::screen(b, s),
BlendMode::HardLight => modes::hard_light(b, s),
BlendMode::Overlay => modes::overlay(b, s),
BlendMode::Darken => modes::darken(b, s),
BlendMode::Lighten => modes::lighten(b, s),
BlendMode::ColorDodge => modes::color_dodge(b, s),
BlendMode::ColorBurn => modes::color_burn(b, s),
BlendMode::SoftLight => modes::soft_light(b, s),
BlendMode::Difference => modes::difference(b, s),
BlendMode::Exclusion => modes::exclusion(b, s),
BlendMode::Hue | BlendMode::Saturation | BlendMode::Color | BlendMode::Luminosity => {
unreachable!("non-separable mode dispatched to scalar apply")
}
}
}
fn apply_triple(self, b: Triple, s: Triple) -> Triple {
match self {
BlendMode::Hue => non_separable::hue(b, s),
BlendMode::Saturation => non_separable::saturation(b, s),
BlendMode::Color => non_separable::color(b, s),
BlendMode::Luminosity => non_separable::luminosity(b, s),
_ => unreachable!("separable mode dispatched to triple apply"),
}
}
pub fn from_css_name(name: &str) -> Option<Self> {
Some(match name {
"normal" => BlendMode::Normal,
"multiply" => BlendMode::Multiply,
"screen" => BlendMode::Screen,
"hard-light" => BlendMode::HardLight,
"overlay" => BlendMode::Overlay,
"darken" => BlendMode::Darken,
"lighten" => BlendMode::Lighten,
"color-dodge" => BlendMode::ColorDodge,
"color-burn" => BlendMode::ColorBurn,
"soft-light" => BlendMode::SoftLight,
"difference" => BlendMode::Difference,
"exclusion" => BlendMode::Exclusion,
"hue" => BlendMode::Hue,
"saturation" => BlendMode::Saturation,
"color" => BlendMode::Color,
"luminosity" => BlendMode::Luminosity,
_ => return None,
})
}
}
pub fn blend(colors: &[Color], mode: BlendMode) -> Color {
assert!(!colors.is_empty(), "blend: at least one color is required");
let mut iter = colors.iter().map(|c| to_rgb_with_alpha(*c));
let mut acc = iter.next().expect("non-empty checked above");
for src in iter {
acc = porter_duff(acc, src, mode);
}
Color::Rgb(Rgb {
r: acc.r,
g: acc.g,
b: acc.b,
alpha: Some(acc.a),
})
}
pub fn blend_str(colors: &[Color], mode: &str) -> Option<Color> {
BlendMode::from_css_name(mode).map(|m| blend(colors, m))
}
#[derive(Clone, Copy)]
struct Rgba {
r: f64,
g: f64,
b: f64,
a: f64,
}
fn to_rgb_with_alpha(c: Color) -> Rgba {
let rgb: Rgb = match c {
Color::Rgb(x) => x,
Color::LinearRgb(x) => x.into(),
Color::Hsl(x) => x.into(),
Color::Hsv(x) => x.into(),
Color::Hwb(x) => Hsv::from(x).into(),
other => convert::<Xyz65, Rgb>(color_to_xyz65(other)),
};
Rgba {
r: rgb.r,
g: rgb.g,
b: rgb.b,
a: rgb.alpha.unwrap_or(1.0),
}
}
fn color_to_xyz65(c: Color) -> Xyz65 {
match c {
Color::Rgb(x) => x.to_xyz65(),
Color::LinearRgb(x) => x.to_xyz65(),
Color::Hsl(x) => x.to_xyz65(),
Color::Hsv(x) => x.to_xyz65(),
Color::Hwb(x) => x.to_xyz65(),
Color::Lab(x) => x.to_xyz65(),
Color::Lab65(x) => x.to_xyz65(),
Color::Lch(x) => x.to_xyz65(),
Color::Lch65(x) => x.to_xyz65(),
Color::Oklab(x) => x.to_xyz65(),
Color::Oklch(x) => x.to_xyz65(),
Color::Xyz50(x) => x.to_xyz65(),
Color::Xyz65(x) => x,
Color::P3(x) => x.to_xyz65(),
Color::Rec2020(x) => x.to_xyz65(),
Color::A98(x) => x.to_xyz65(),
Color::ProphotoRgb(x) => x.to_xyz65(),
Color::Cubehelix(x) => x.to_xyz65(),
Color::Dlab(x) => x.to_xyz65(),
Color::Dlch(x) => x.to_xyz65(),
Color::Jab(x) => x.to_xyz65(),
Color::Jch(x) => x.to_xyz65(),
Color::Yiq(x) => x.to_xyz65(),
Color::Hsi(x) => x.to_xyz65(),
Color::Hsluv(x) => x.to_xyz65(),
Color::Hpluv(x) => x.to_xyz65(),
Color::Okhsl(x) => x.to_xyz65(),
Color::Okhsv(x) => x.to_xyz65(),
Color::Itp(x) => x.to_xyz65(),
Color::Xyb(x) => x.to_xyz65(),
Color::Luv(x) => x.to_xyz65(),
Color::Lchuv(x) => x.to_xyz65(),
Color::Prismatic(x) => x.to_xyz65(),
}
}
fn porter_duff(b: Rgba, s: Rgba, mode: BlendMode) -> Rgba {
let alpha = s.a + b.a * (1.0 - s.a);
if alpha == 0.0 {
return Rgba {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
};
}
let (fr, fg, fb) = if mode.is_non_separable() {
let triple = mode.apply_triple(
Triple {
r: b.r,
g: b.g,
b: b.b,
},
Triple {
r: s.r,
g: s.g,
b: s.b,
},
);
(triple.r, triple.g, triple.b)
} else {
(
mode.apply(b.r, s.r),
mode.apply(b.g, s.g),
mode.apply(b.b, s.b),
)
};
let combine = |bc: f64, sc: f64, f: f64| -> f64 {
let v = s.a * (1.0 - b.a) * sc + s.a * b.a * f + (1.0 - s.a) * b.a * bc;
(v / alpha).clamp(0.0, 1.0)
};
Rgba {
r: combine(b.r, s.r, fr),
g: combine(b.g, s.g, fg),
b: combine(b.b, s.b, fb),
a: alpha,
}
}