use std::fmt::Display;
use std::sync::Arc;
use valo::DisplayList;
use valo::DisplayListBuilder;
use valo::Op;
pub type Canvas = DisplayListBuilder;
pub type Picture = DisplayList;
pub fn flattened_ops(picture: &Picture) -> Vec<Op> {
let mut ops = Vec::new();
flatten_into(picture, &mut ops);
ops
}
fn flatten_into(picture: &Picture, ops: &mut Vec<Op>) {
for op in picture.ops() {
ops.push(op.clone());
if let Op::DrawDisplayList { list, .. } = op {
flatten_into(list, ops);
}
}
}
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())
}
}
impl ColorFilter {
pub fn to_valo(&self) -> valo::ColorFilter {
match *self {
ColorFilter::Mode { color, blend_mode } => {
valo::ColorFilter::Blend(color.into(), blend_mode)
}
ColorFilter::Matrix(matrix) => valo::ColorFilter::Matrix(valo_matrix(matrix)),
ColorFilter::LinearToSrgbGamma => valo::ColorFilter::LinearToSrgbGamma,
ColorFilter::SrgbToLinearGamma => valo::ColorFilter::SrgbToLinearGamma,
}
}
}
fn valo_matrix(matrix: [f64; 20]) -> [f32; 20] {
const ROW_LENGTH: usize = 5;
const TRANSLATION_COLUMN: usize = 4;
std::array::from_fn(|index| {
let entry = matrix[index];
let entry = if index % ROW_LENGTH == TRANSLATION_COLUMN {
entry / 255.0
} else {
entry
};
entry as f32
})
}
#[derive(Clone, Debug, PartialEq)]
pub enum ImageFilter {
Blur {
sigma_x: f64,
sigma_y: f64,
bounds: Option<crate::Rect>,
},
Color(ColorFilter),
Compose {
outer: Arc<ImageFilter>,
inner: Arc<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: Arc::new(outer),
inner: Arc::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()
),
}
}
}
impl ImageFilter {
pub fn to_valo(&self) -> valo::ImageFilter {
match self {
ImageFilter::Blur {
sigma_x,
sigma_y,
bounds,
} => {
let (sigma_x, sigma_y) = (*sigma_x as f32, *sigma_y as f32);
match bounds {
Some(bounds) => {
valo::ImageFilter::bounded_blur(sigma_x, sigma_y, (*bounds).into())
}
None => valo::ImageFilter::blur(sigma_x, sigma_y),
}
}
ImageFilter::Color(filter) => valo::ImageFilter::Color(filter.to_valo()),
ImageFilter::Compose { outer, inner } => {
valo::ImageFilter::compose(outer.to_valo(), inner.to_valo())
}
}
}
}
#[cfg(test)]
mod tests {
use super::{ColorFilter, ImageFilter, valo};
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)"
);
}
#[test]
fn a_colour_matrix_reaches_valo_with_its_translation_in_the_channels_range() {
let mut matrix = [0.0; 20];
matrix[0] = 2.0;
matrix[4] = 51.0;
matrix[18] = 1.0;
matrix[19] = 255.0;
let valo::ColorFilter::Matrix(replayed) = ColorFilter::matrix(matrix).to_valo() else {
panic!("a matrix reaches valo as a matrix");
};
assert_eq!(
[replayed[0], replayed[4], replayed[18], replayed[19]],
[2.0, 0.2, 1.0, 1.0]
);
}
#[test]
fn a_mode_filter_reaches_valo_as_a_blend_and_a_gamma_curve_as_the_same_curve() {
let green = Color::new(0x8C00FF00);
assert_eq!(
ColorFilter::mode(green, BlendMode::Darken).to_valo(),
valo::ColorFilter::Blend(green.into(), BlendMode::Darken)
);
assert_eq!(
ColorFilter::linear_to_srgb_gamma().to_valo(),
valo::ColorFilter::LinearToSrgbGamma
);
assert_eq!(
ColorFilter::srgb_to_linear_gamma().to_valo(),
valo::ColorFilter::SrgbToLinearGamma
);
}
#[test]
fn a_composition_reaches_valo_in_its_order_with_both_sigmas_and_the_bounds() {
let tint = ColorFilter::mode(Color::new(0xFF0000FF), BlendMode::Multiply);
let bounds = Rect::from_ltwh(0.0, 0.0, 4.0, 4.0);
let filter = ImageFilter::compose(
ImageFilter::Color(tint),
ImageFilter::blur(2.0, 5.0).bounds(bounds),
);
assert_eq!(
filter.to_valo(),
valo::ImageFilter::compose(
valo::ImageFilter::Color(valo::ColorFilter::Blend(
Color::new(0xFF0000FF).into(),
BlendMode::Multiply
)),
valo::ImageFilter::bounded_blur(2.0, 5.0, bounds.into()),
)
);
}
#[test]
fn a_gamma_curve_reaches_valo_in_its_place_in_a_composition() {
let filter = ImageFilter::compose(
ImageFilter::Color(ColorFilter::linear_to_srgb_gamma()),
ImageFilter::blur(3.0, 3.0),
);
assert_eq!(
filter.to_valo(),
valo::ImageFilter::compose(
valo::ImageFilter::Color(valo::ColorFilter::LinearToSrgbGamma),
valo::ImageFilter::blur(3.0, 3.0),
)
);
}
}