use std::fmt::Display;
use std::rc::Rc;
use valo::DisplayList;
use valo::DisplayListBuilder;
pub type Canvas = DisplayListBuilder;
pub type Picture = DisplayList;
pub use valo::{
Backdrop, BlendMode, BlurStyle, ClipOp, FillRule, Image, MaskBlur, Paint, PaintStyle, Path,
PathBuilder, Stroke,
};
pub use valo;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ColorFilter {
Mode {
color: crate::Color,
blend_mode: BlendMode,
},
Matrix([f64; 20]),
LinearToSrgbGamma,
SrgbToLinearGamma,
}
impl ColorFilter {
pub const fn mode(color: crate::Color, blend_mode: BlendMode) -> ColorFilter {
ColorFilter::Mode { color, blend_mode }
}
pub const fn matrix(matrix: [f64; 20]) -> ColorFilter {
ColorFilter::Matrix(matrix)
}
pub const fn linear_to_srgb_gamma() -> ColorFilter {
ColorFilter::LinearToSrgbGamma
}
pub const fn srgb_to_linear_gamma() -> ColorFilter {
ColorFilter::SrgbToLinearGamma
}
pub fn saturation(saturation: f64) -> ColorFilter {
const R_LUMINANCE: f64 = 0.2126;
const G_LUMINANCE: f64 = 0.7152;
const B_LUMINANCE: f64 = 0.0722;
let inv_sat = 1.0 - saturation;
ColorFilter::matrix([
inv_sat * R_LUMINANCE + saturation,
inv_sat * G_LUMINANCE,
inv_sat * B_LUMINANCE,
0.0,
0.0,
inv_sat * R_LUMINANCE,
inv_sat * G_LUMINANCE + saturation,
inv_sat * B_LUMINANCE,
0.0,
0.0,
inv_sat * R_LUMINANCE,
inv_sat * G_LUMINANCE,
inv_sat * B_LUMINANCE + saturation,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
])
}
pub fn debug_short_description(&self) -> String {
match self {
ColorFilter::Mode { color, blend_mode } => {
format!("ColorFilter.mode({color:?}, {blend_mode:?})")
}
ColorFilter::Matrix(matrix) => format!("ColorFilter.matrix({matrix:?})"),
ColorFilter::LinearToSrgbGamma => String::from("ColorFilter.linearToSrgbGamma()"),
ColorFilter::SrgbToLinearGamma => String::from("ColorFilter.srgbToLinearGamma()"),
}
}
}
impl std::fmt::Display for ColorFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.debug_short_description())
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ImageFilter {
Blur {
sigma_x: f64,
sigma_y: f64,
bounds: Option<crate::Rect>,
},
Color(ColorFilter),
Compose {
outer: Rc<ImageFilter>,
inner: Rc<ImageFilter>,
},
}
impl ImageFilter {
pub const fn blur(sigma_x: f64, sigma_y: f64) -> ImageFilter {
ImageFilter::Blur {
sigma_x,
sigma_y,
bounds: None,
}
}
pub fn bounds(self, bounds: crate::Rect) -> ImageFilter {
match self {
ImageFilter::Blur {
sigma_x, sigma_y, ..
} => ImageFilter::Blur {
sigma_x,
sigma_y,
bounds: Some(bounds),
},
other => other,
}
}
pub fn compose(outer: ImageFilter, inner: ImageFilter) -> ImageFilter {
ImageFilter::Compose {
outer: Rc::new(outer),
inner: Rc::new(inner),
}
}
pub fn debug_short_description(&self) -> String {
match self {
ImageFilter::Blur {
sigma_x,
sigma_y,
bounds,
} => {
let bounds = match bounds {
Some(bounds) => format!(", bounds: {bounds:?}"),
None => String::new(),
};
format!("blur({sigma_x}, {sigma_y}, unspecified{bounds})")
}
ImageFilter::Color(filter) => filter.debug_short_description(),
ImageFilter::Compose { outer, inner } => format!(
"{} -> {}",
inner.debug_short_description(),
outer.debug_short_description()
),
}
}
}
impl std::fmt::Display for ImageFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImageFilter::Blur { .. } => write!(f, "ImageFilter.{}", self.debug_short_description()),
ImageFilter::Color(filter) => Display::fmt(filter, f),
ImageFilter::Compose { .. } => write!(
f,
"ImageFilter.compose(source -> {} -> result)",
self.debug_short_description()
),
}
}
}
#[cfg(test)]
mod tests {
use super::{ColorFilter, ImageFilter};
use crate::{BlendMode, Color, Rect};
#[test]
fn a_saturation_of_one_is_the_identity_matrix() {
let ColorFilter::Matrix(matrix) = ColorFilter::saturation(1.0) else {
panic!("saturation is a matrix filter");
};
let identity = [
1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, ];
for (value, expected) in matrix.iter().zip(identity) {
assert!((value - expected).abs() < 1e-12, "{matrix:?}");
}
}
#[test]
fn a_blur_reports_its_bounds_and_a_composition_reads_inner_then_outer() {
let blur = ImageFilter::blur(2.0, 3.0).bounds(Rect::from_ltwh(0.0, 0.0, 4.0, 5.0));
assert_eq!(
blur.debug_short_description(),
"blur(2, 3, unspecified, bounds: Rect.fromLTRB(0.0, 0.0, 4.0, 5.0))"
);
let tint = ImageFilter::Color(ColorFilter::mode(
Color::new(0xFF000000),
BlendMode::SrcOver,
));
let composed = ImageFilter::compose(tint, ImageFilter::blur(2.0, 3.0));
assert_eq!(
composed.to_string(),
"ImageFilter.compose(source -> blur(2, 3, unspecified) -> \
ColorFilter.mode(Color(alpha: 1.0000, red: 0.0000, green: 0.0000, blue: 0.0000, \
colorSpace: ColorSpace.sRGB), SrcOver) -> result)"
);
}
}