use super::{
BlendEffectMode, CanvasComposite, CanvasEdgeBehavior, CompositionEffectFactoryError,
CompositionEffectFactoryLoadStatus, EffectBorderMode, GraphicsEffect, GraphicsEffectSource,
};
use crate::media::imaging::BitmapBlendingMode;
use crate::media::{Color, ColorMatrix, ColorMatrixAlphaMode};
pub(crate) const MAX_EFFECT_NODES: usize = 256;
pub(crate) const MAX_EFFECT_DEPTH: usize = 32;
pub fn blur_amount_to_radius(blur_amount: f64) -> f64 {
if blur_amount.is_nan() || blur_amount <= 0.5 {
return 0.0;
}
(blur_amount - 0.5) / 0.288675
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct EffectScalar {
pub(crate) key: Option<String>,
pub(crate) value: f32,
}
impl EffectScalar {
fn new(value: f32) -> Self {
Self { key: None, value }
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct EffectColor {
pub(crate) key: Option<String>,
pub(crate) value: Color,
}
impl EffectColor {
fn new(value: Color) -> Self {
Self { key: None, value }
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum EffectMatrix {
Fixed(ColorMatrix),
Saturation(EffectScalar),
Tint(EffectColor),
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum EffectNodeKind {
Parameter(String),
GaussianBlur {
amount: EffectScalar,
hard: bool,
source: Box<EffectNode>,
},
Opacity { opacity: EffectScalar, source: Box<EffectNode> },
CrossFade { weight: EffectScalar, source1: Box<EffectNode>, source2: Box<EffectNode> },
Composite { mode: BitmapBlendingMode, sources: Vec<EffectNode> },
ColorSource(EffectColor),
Blend { mode: BitmapBlendingMode, background: Box<EffectNode>, foreground: Box<EffectNode> },
ColorMatrix { matrix: EffectMatrix, alpha_mode: ColorMatrixAlphaMode, source: Box<EffectNode> },
AlphaMask { source: Box<EffectNode>, mask: Box<EffectNode> },
Border { tile: bool, source: Box<EffectNode> },
ArithmeticWithColor {
multiply: EffectScalar,
source1: EffectScalar,
source2: EffectScalar,
offset: EffectScalar,
image_is_source1: bool,
image: Box<EffectNode>,
color: Box<EffectNode>,
},
SceneLighting { ambient: EffectScalar, diffuse: EffectScalar, specular: EffectScalar, shine: EffectScalar },
Shadow { amount: EffectScalar, color: EffectColor, source: Box<EffectNode> },
Pixels { op: PixelOp, sources: Vec<EffectNode> },
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum PixelOp {
Blend(BlendEffectMode),
Arithmetic { multiply: EffectScalar, source1: EffectScalar, source2: EffectScalar, offset: EffectScalar },
MaskInvert,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct EffectNode {
pub(crate) name: String,
pub(crate) kind: EffectNodeKind,
}
impl EffectNode {
pub(crate) fn sources(&self) -> Vec<&EffectNode> {
match &self.kind {
EffectNodeKind::Parameter(_) | EffectNodeKind::ColorSource(_) | EffectNodeKind::SceneLighting { .. } => {
Vec::new()
}
EffectNodeKind::GaussianBlur { source, .. }
| EffectNodeKind::Opacity { source, .. }
| EffectNodeKind::ColorMatrix { source, .. }
| EffectNodeKind::Shadow { source, .. }
| EffectNodeKind::Border { source, .. } => vec![&**source],
EffectNodeKind::Pixels { sources, .. } => sources.iter().collect(),
EffectNodeKind::CrossFade { source1, source2, .. } => vec![&**source1, &**source2],
EffectNodeKind::Composite { sources, .. } => sources.iter().collect(),
EffectNodeKind::Blend { background, foreground, .. } => vec![&**background, &**foreground],
EffectNodeKind::AlphaMask { source, mask } => vec![&**source, &**mask],
EffectNodeKind::ArithmeticWithColor { image, color, image_is_source1, .. } => {
if *image_is_source1 {
vec![&**image, &**color]
} else {
vec![&**color, &**image]
}
}
}
}
fn sources_mut(&mut self) -> Vec<&mut EffectNode> {
match &mut self.kind {
EffectNodeKind::Parameter(_) | EffectNodeKind::ColorSource(_) | EffectNodeKind::SceneLighting { .. } => {
Vec::new()
}
EffectNodeKind::GaussianBlur { source, .. }
| EffectNodeKind::Opacity { source, .. }
| EffectNodeKind::ColorMatrix { source, .. }
| EffectNodeKind::Shadow { source, .. }
| EffectNodeKind::Border { source, .. } => vec![&mut **source],
EffectNodeKind::Pixels { sources, .. } => sources.iter_mut().collect(),
EffectNodeKind::CrossFade { source1, source2, .. } => vec![&mut **source1, &mut **source2],
EffectNodeKind::Composite { sources, .. } => sources.iter_mut().collect(),
EffectNodeKind::Blend { background, foreground, .. } => vec![&mut **background, &mut **foreground],
EffectNodeKind::AlphaMask { source, mask } => vec![&mut **source, &mut **mask],
EffectNodeKind::ArithmeticWithColor { image, color, .. } => vec![&mut **image, &mut **color],
}
}
fn bind_animatable(&mut self, effect: &str, property: &str, key: &str) -> Result<bool, ()> {
let mut found = false;
if self.name == effect && !self.name.is_empty() {
let is = |name: &str| property.eq_ignore_ascii_case(name);
let key = Some(key.to_owned());
match &mut self.kind {
EffectNodeKind::GaussianBlur { amount, .. } if is("BlurAmount") => amount.key = key,
EffectNodeKind::Opacity { opacity, .. } if is("Opacity") => opacity.key = key,
EffectNodeKind::CrossFade { weight, .. } if is("Weight") || is("CrossFade") => weight.key = key,
EffectNodeKind::ColorSource(color) if is("Color") => color.key = key,
EffectNodeKind::ColorMatrix { matrix: EffectMatrix::Saturation(saturation), .. }
if is("Saturation") =>
{
saturation.key = key
}
EffectNodeKind::ColorMatrix { matrix: EffectMatrix::Tint(color), .. } if is("Color") => {
color.key = key
}
EffectNodeKind::ArithmeticWithColor { multiply, .. } if is("MultiplyAmount") => multiply.key = key,
EffectNodeKind::ArithmeticWithColor { source1, .. } if is("Source1Amount") => source1.key = key,
EffectNodeKind::ArithmeticWithColor { source2, .. } if is("Source2Amount") => source2.key = key,
EffectNodeKind::ArithmeticWithColor { offset, .. } if is("Offset") => offset.key = key,
EffectNodeKind::SceneLighting { ambient, .. } if is("AmbientAmount") => ambient.key = key,
EffectNodeKind::SceneLighting { diffuse, .. } if is("DiffuseAmount") => diffuse.key = key,
EffectNodeKind::SceneLighting { specular, .. } if is("SpecularAmount") => specular.key = key,
EffectNodeKind::SceneLighting { shine, .. } if is("SpecularShine") => shine.key = key,
EffectNodeKind::Shadow { amount, .. } if is("BlurAmount") => amount.key = key,
EffectNodeKind::Shadow { color, .. } if is("ShadowColor") => color.key = key,
EffectNodeKind::Pixels { op: PixelOp::Arithmetic { multiply, .. }, .. } if is("MultiplyAmount") => {
multiply.key = key
}
EffectNodeKind::Pixels { op: PixelOp::Arithmetic { source1, .. }, .. } if is("Source1Amount") => {
source1.key = key
}
EffectNodeKind::Pixels { op: PixelOp::Arithmetic { source2, .. }, .. } if is("Source2Amount") => {
source2.key = key
}
EffectNodeKind::Pixels { op: PixelOp::Arithmetic { offset, .. }, .. } if is("Offset") => offset.key = key,
_ => return Err(()),
}
found = true;
}
for source in self.sources_mut() {
found |= source.bind_animatable(effect, property, key)?;
}
Ok(found)
}
fn animatable_value(&self, key: &str) -> Option<EffectPropertyValue> {
let scalar = |value: &EffectScalar| {
(value.key.as_deref() == Some(key)).then_some(EffectPropertyValue::Scalar(value.value))
};
let color =
|value: &EffectColor| (value.key.as_deref() == Some(key)).then_some(EffectPropertyValue::Color(value.value));
let own = match &self.kind {
EffectNodeKind::GaussianBlur { amount: value, .. }
| EffectNodeKind::Opacity { opacity: value, .. }
| EffectNodeKind::CrossFade { weight: value, .. } => scalar(value),
EffectNodeKind::ColorSource(value) => color(value),
EffectNodeKind::ColorMatrix { matrix: EffectMatrix::Saturation(value), .. } => scalar(value),
EffectNodeKind::ColorMatrix { matrix: EffectMatrix::Tint(value), .. } => color(value),
EffectNodeKind::ArithmeticWithColor { multiply, source1, source2, offset, .. } => {
scalar(multiply).or_else(|| scalar(source1)).or_else(|| scalar(source2)).or_else(|| scalar(offset))
}
EffectNodeKind::SceneLighting { ambient, diffuse, specular, shine } => {
scalar(ambient).or_else(|| scalar(diffuse)).or_else(|| scalar(specular)).or_else(|| scalar(shine))
}
EffectNodeKind::Shadow { amount, color: shadow_color, .. } => scalar(amount).or_else(|| color(shadow_color)),
EffectNodeKind::Pixels { op: PixelOp::Arithmetic { multiply, source1, source2, offset }, .. } => {
scalar(multiply).or_else(|| scalar(source1)).or_else(|| scalar(source2)).or_else(|| scalar(offset))
}
EffectNodeKind::Pixels { .. } => None,
EffectNodeKind::Parameter(_)
| EffectNodeKind::Composite { .. }
| EffectNodeKind::Blend { .. }
| EffectNodeKind::ColorMatrix { matrix: EffectMatrix::Fixed(_), .. }
| EffectNodeKind::AlphaMask { .. }
| EffectNodeKind::Border { .. } => None,
};
own.or_else(|| self.sources().into_iter().find_map(|source| source.animatable_value(key)))
}
fn collect_parameters(&self, parameters: &mut Vec<String>) {
if let EffectNodeKind::Parameter(name) = &self.kind {
if !parameters.iter().any(|existing| existing == name) {
parameters.push(name.clone());
}
}
for source in self.sources() {
source.collect_parameters(parameters);
}
}
fn first_parameter_path(&self, path: &mut Vec<usize>) -> bool {
if matches!(self.kind, EffectNodeKind::Parameter(_)) {
return true;
}
for (index, source) in self.sources().into_iter().enumerate() {
path.push(index);
if source.first_parameter_path(path) {
return true;
}
path.pop();
}
false
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum EffectPropertyValue {
Scalar(f32),
Color(Color),
}
#[derive(Debug, Default)]
pub(crate) struct EffectGraph {
pub(crate) root: Option<EffectNode>,
pub(crate) parameters: Vec<String>,
pub(crate) animatable: Vec<(String, EffectPropertyValue)>,
pub(crate) layer_path: Option<Vec<usize>>,
}
impl EffectGraph {
pub(crate) fn failed() -> EffectGraph {
EffectGraph::default()
}
pub(crate) fn load(
effect: &GraphicsEffect,
animatable: &[&str],
) -> Result<EffectGraph, (CompositionEffectFactoryLoadStatus, CompositionEffectFactoryError)> {
let mut count = 0;
let mut root = build_node(effect, 1, &mut count)?;
let mut properties: Vec<(String, EffectPropertyValue)> = Vec::with_capacity(animatable.len());
for &key in animatable {
let invalid = || {
(CompositionEffectFactoryLoadStatus::Other, CompositionEffectFactoryError::InvalidAnimatableProperty(key.to_owned()))
};
let (effect_name, property) = key.split_once('.').ok_or_else(invalid)?;
if !root.bind_animatable(effect_name, property, key).map_err(|_| invalid())? {
return Err(invalid());
}
if !properties.iter().any(|(existing, _)| existing == key) {
let value = root.animatable_value(key).ok_or_else(invalid)?;
properties.push((key.to_owned(), value));
}
}
let mut parameters = Vec::new();
root.collect_parameters(&mut parameters);
let mut path = Vec::new();
let layer_path = root.first_parameter_path(&mut path).then_some(path);
Ok(EffectGraph { root: Some(root), parameters, animatable: properties, layer_path })
}
pub(crate) fn is_loaded(&self) -> bool {
self.root.is_some()
}
pub(crate) fn is_animatable(&self, key: &str) -> bool {
self.animatable.iter().any(|(existing, _)| existing == key)
}
pub(crate) fn has_parameter(&self, name: &str) -> bool {
self.parameters.iter().any(|existing| existing == name)
}
}
type LoadError = (CompositionEffectFactoryLoadStatus, CompositionEffectFactoryError);
fn too_complex() -> LoadError {
(CompositionEffectFactoryLoadStatus::EffectTooComplex, CompositionEffectFactoryError::EffectTooComplex)
}
fn not_supported(effect: &GraphicsEffect) -> LoadError {
(CompositionEffectFactoryLoadStatus::Other, CompositionEffectFactoryError::EffectTypeNotSupported(effect.type_name()))
}
fn build_source(source: &Option<GraphicsEffectSource>, owner: &GraphicsEffect, depth: usize, count: &mut usize) -> Result<EffectNode, LoadError> {
match source {
None => Err((
CompositionEffectFactoryLoadStatus::Other,
CompositionEffectFactoryError::MissingSource { effect: owner.type_name(), name: owner.name().to_owned() },
)),
Some(GraphicsEffectSource::Parameter(parameter)) => {
*count += 1;
if *count > MAX_EFFECT_NODES {
return Err(too_complex());
}
Ok(EffectNode { name: String::new(), kind: EffectNodeKind::Parameter(parameter.name().to_owned()) })
}
Some(GraphicsEffectSource::Effect(effect)) => build_node(effect, depth + 1, count),
}
}
pub(crate) fn composite_blending_mode(mode: CanvasComposite) -> Option<BitmapBlendingMode> {
Some(match mode {
CanvasComposite::SourceOver => BitmapBlendingMode::SourceOver,
CanvasComposite::DestinationOver => BitmapBlendingMode::DestinationOver,
CanvasComposite::SourceIn => BitmapBlendingMode::SourceIn,
CanvasComposite::DestinationIn => BitmapBlendingMode::DestinationIn,
CanvasComposite::SourceOut => BitmapBlendingMode::SourceOut,
CanvasComposite::DestinationOut => BitmapBlendingMode::DestinationOut,
CanvasComposite::SourceAtop => BitmapBlendingMode::SourceAtop,
CanvasComposite::DestinationAtop => BitmapBlendingMode::DestinationAtop,
CanvasComposite::Xor => BitmapBlendingMode::Xor,
CanvasComposite::Add => BitmapBlendingMode::Plus,
CanvasComposite::Copy | CanvasComposite::BoundedCopy => BitmapBlendingMode::Source,
CanvasComposite::MaskInvert => return None,
})
}
pub(crate) fn blend_blending_mode(mode: BlendEffectMode) -> Option<BitmapBlendingMode> {
Some(match mode {
BlendEffectMode::Multiply => BitmapBlendingMode::Multiply,
BlendEffectMode::Screen => BitmapBlendingMode::Screen,
BlendEffectMode::Darken => BitmapBlendingMode::Darken,
BlendEffectMode::Lighten => BitmapBlendingMode::Lighten,
BlendEffectMode::ColorBurn => BitmapBlendingMode::ColorBurn,
BlendEffectMode::ColorDodge => BitmapBlendingMode::ColorDodge,
BlendEffectMode::LinearDodge => BitmapBlendingMode::Plus,
BlendEffectMode::Overlay => BitmapBlendingMode::Overlay,
BlendEffectMode::SoftLight => BitmapBlendingMode::SoftLight,
BlendEffectMode::HardLight => BitmapBlendingMode::HardLight,
BlendEffectMode::Difference => BitmapBlendingMode::Difference,
BlendEffectMode::Exclusion => BitmapBlendingMode::Exclusion,
BlendEffectMode::Hue => BitmapBlendingMode::Hue,
BlendEffectMode::Saturation => BitmapBlendingMode::Saturation,
BlendEffectMode::Color => BitmapBlendingMode::Color,
BlendEffectMode::Luminosity => BitmapBlendingMode::Luminosity,
BlendEffectMode::Dissolve
| BlendEffectMode::LinearBurn
| BlendEffectMode::DarkerColor
| BlendEffectMode::LighterColor
| BlendEffectMode::VividLight
| BlendEffectMode::LinearLight
| BlendEffectMode::PinLight
| BlendEffectMode::HardMix
| BlendEffectMode::Subtract
| BlendEffectMode::Division => return None,
})
}
fn color_source_of(source: &Option<GraphicsEffectSource>) -> Option<&GraphicsEffect> {
match source {
Some(GraphicsEffectSource::Effect(effect)) if matches!(**effect, GraphicsEffect::ColorSource(_)) => Some(effect),
_ => None,
}
}
fn build_node(effect: &GraphicsEffect, depth: usize, count: &mut usize) -> Result<EffectNode, LoadError> {
*count += 1;
if depth > MAX_EFFECT_DEPTH || *count > MAX_EFFECT_NODES {
return Err(too_complex());
}
let boxed = |source: &Option<GraphicsEffectSource>, count: &mut usize| -> Result<Box<EffectNode>, LoadError> {
Ok(Box::new(build_source(source, effect, depth, count)?))
};
let matrix_of = |matrix: EffectMatrix, alpha_mode, source: &Option<GraphicsEffectSource>, count: &mut usize| {
Ok::<_, LoadError>(EffectNodeKind::ColorMatrix { matrix, alpha_mode, source: boxed(source, count)? })
};
let kind = match effect {
GraphicsEffect::GaussianBlur(blur) => EffectNodeKind::GaussianBlur {
amount: EffectScalar::new(blur.blur_amount),
hard: blur.border_mode == EffectBorderMode::Hard,
source: boxed(&blur.source, count)?,
},
GraphicsEffect::Opacity(opacity) => {
EffectNodeKind::Opacity { opacity: EffectScalar::new(opacity.opacity), source: boxed(&opacity.source, count)? }
}
GraphicsEffect::CrossFade(cross_fade) => EffectNodeKind::CrossFade {
weight: EffectScalar::new(cross_fade.weight),
source1: boxed(&cross_fade.source1, count)?,
source2: boxed(&cross_fade.source2, count)?,
},
GraphicsEffect::Composite(composite) => {
let mut sources = Vec::with_capacity(composite.sources.len());
for source in &composite.sources {
sources.push(build_source(&Some(source.clone()), effect, depth, count)?);
}
match composite_blending_mode(composite.mode) {
Some(mode) => EffectNodeKind::Composite { mode, sources },
None => EffectNodeKind::Pixels { op: PixelOp::MaskInvert, sources },
}
}
GraphicsEffect::ColorSource(color) => EffectNodeKind::ColorSource(EffectColor::new(color.color)),
GraphicsEffect::Saturation(saturation) => matrix_of(
EffectMatrix::Saturation(EffectScalar::new(saturation.saturation)),
ColorMatrixAlphaMode::Premultiplied,
&saturation.source,
count,
)?,
GraphicsEffect::Blend(blend) => match blend_blending_mode(blend.mode) {
Some(mode) => EffectNodeKind::Blend {
mode,
background: boxed(&blend.background, count)?,
foreground: boxed(&blend.foreground, count)?,
},
None => EffectNodeKind::Pixels {
op: PixelOp::Blend(blend.mode),
sources: vec![*boxed(&blend.background, count)?, *boxed(&blend.foreground, count)?],
},
},
GraphicsEffect::ColorMatrix(matrix) => {
matrix_of(EffectMatrix::Fixed(matrix.color_matrix), matrix.alpha_mode, &matrix.source, count)?
}
GraphicsEffect::Tint(tint) => matrix_of(
EffectMatrix::Tint(EffectColor::new(tint.color)),
ColorMatrixAlphaMode::Premultiplied,
&tint.source,
count,
)?,
GraphicsEffect::Invert(invert) => {
matrix_of(EffectMatrix::Fixed(ColorMatrix::invert()), ColorMatrixAlphaMode::Premultiplied, &invert.source, count)?
}
GraphicsEffect::Grayscale(grayscale) => matrix_of(
EffectMatrix::Fixed(ColorMatrix::grayscale()),
ColorMatrixAlphaMode::Premultiplied,
&grayscale.source,
count,
)?,
GraphicsEffect::LuminanceToAlpha(luminance) => matrix_of(
EffectMatrix::Fixed(ColorMatrix::luminance_to_alpha()),
ColorMatrixAlphaMode::Premultiplied,
&luminance.source,
count,
)?,
GraphicsEffect::ArithmeticComposite(arithmetic) => {
let (image_is_source1, image, color) = match (color_source_of(&arithmetic.source1), color_source_of(&arithmetic.source2)) {
(_, Some(_)) => (true, &arithmetic.source1, &arithmetic.source2),
(Some(_), None) => (false, &arithmetic.source2, &arithmetic.source1),
(None, None) => {
return Ok(EffectNode {
name: effect.name().to_owned(),
kind: EffectNodeKind::Pixels {
op: PixelOp::Arithmetic {
multiply: EffectScalar::new(arithmetic.multiply_amount),
source1: EffectScalar::new(arithmetic.source1_amount),
source2: EffectScalar::new(arithmetic.source2_amount),
offset: EffectScalar::new(arithmetic.offset),
},
sources: vec![
*boxed(&arithmetic.source1, count)?,
*boxed(&arithmetic.source2, count)?,
],
},
})
}
};
EffectNodeKind::ArithmeticWithColor {
multiply: EffectScalar::new(arithmetic.multiply_amount),
source1: EffectScalar::new(arithmetic.source1_amount),
source2: EffectScalar::new(arithmetic.source2_amount),
offset: EffectScalar::new(arithmetic.offset),
image_is_source1,
image: boxed(image, count)?,
color: boxed(color, count)?,
}
}
GraphicsEffect::AlphaMask(mask) => {
EffectNodeKind::AlphaMask { source: boxed(&mask.source, count)?, mask: boxed(&mask.alpha_mask, count)? }
}
GraphicsEffect::Border(border) => EffectNodeKind::Border {
tile: border.extend_x != CanvasEdgeBehavior::Clamp || border.extend_y != CanvasEdgeBehavior::Clamp,
source: boxed(&border.source, count)?,
},
GraphicsEffect::Shadow(shadow) => EffectNodeKind::Shadow {
amount: EffectScalar::new(shadow.blur_amount),
color: EffectColor::new(shadow.shadow_color),
source: boxed(&shadow.source, count)?,
},
GraphicsEffect::SceneLighting(lighting) => EffectNodeKind::SceneLighting {
ambient: EffectScalar::new(lighting.ambient_amount),
diffuse: EffectScalar::new(lighting.diffuse_amount),
specular: EffectScalar::new(lighting.specular_amount),
shine: EffectScalar::new(lighting.specular_shine),
},
};
Ok(EffectNode { name: effect.name().to_owned(), kind })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::media::Colors;
use crate::rendering::composition::effects::{
ArithmeticCompositeEffect, BlendEffect, ColorSourceEffect, CompositionEffectSourceParameter, SaturationEffect,
SceneLightingEffect,
};
fn parameter(name: &str) -> Option<GraphicsEffectSource> {
Some(CompositionEffectSourceParameter::new(name).into())
}
#[test]
fn saturation_loads_as_an_animatable_colour_matrix() {
let effect: GraphicsEffect =
SaturationEffect { name: "Saturation".into(), saturation: 0.25, source: parameter("source") }.into();
let graph = EffectGraph::load(&effect, &["Saturation.Saturation"]).expect("loads");
assert_eq!(graph.animatable, vec![("Saturation.Saturation".to_owned(), EffectPropertyValue::Scalar(0.25))]);
assert!(matches!(graph.root.unwrap().kind, EffectNodeKind::ColorMatrix { matrix: EffectMatrix::Saturation(_), .. }));
}
#[test]
fn blend_modes_without_a_blend_layer_are_drawn_from_the_pixels() {
let effect: GraphicsEffect = BlendEffect {
mode: BlendEffectMode::Dissolve,
background: parameter("a"),
foreground: parameter("b"),
..Default::default()
}
.into();
let graph = EffectGraph::load(&effect, &[]).expect("loads");
assert!(matches!(
graph.root.unwrap().kind,
EffectNodeKind::Pixels { op: PixelOp::Blend(BlendEffectMode::Dissolve), .. }
));
let effect: GraphicsEffect = BlendEffect {
mode: BlendEffectMode::Luminosity,
background: parameter("a"),
foreground: parameter("b"),
..Default::default()
}
.into();
let graph = EffectGraph::load(&effect, &[]).expect("loads");
assert!(matches!(
graph.root.unwrap().kind,
EffectNodeKind::Blend { mode: BitmapBlendingMode::Luminosity, .. }
));
}
#[test]
fn arithmetic_composite_of_two_images_is_drawn_from_the_pixels() {
let both_images: GraphicsEffect = ArithmeticCompositeEffect {
name: "Arithmetic".into(),
source1: parameter("a"),
source2: parameter("b"),
..Default::default()
}
.into();
let graph = EffectGraph::load(&both_images, &["Arithmetic.Offset"]).expect("loads");
assert!(matches!(graph.root.unwrap().kind, EffectNodeKind::Pixels { op: PixelOp::Arithmetic { .. }, .. }));
let with_color: GraphicsEffect = ArithmeticCompositeEffect {
name: "Arithmetic".into(),
source1: parameter("a"),
source2: Some(ColorSourceEffect { name: "Base".into(), color: Colors::RED }.into()),
..Default::default()
}
.into();
let graph = EffectGraph::load(&with_color, &["Base.Color", "Arithmetic.Offset"]).expect("loads");
assert_eq!(graph.parameters, vec!["a".to_owned()]);
assert_eq!(graph.animatable.len(), 2);
}
#[test]
fn scene_lighting_amounts_are_animatable() {
let effect: GraphicsEffect = SceneLightingEffect { name: "Light".into(), ..Default::default() }.into();
let graph = EffectGraph::load(&effect, &["Light.DiffuseAmount"]).expect("loads");
assert_eq!(graph.animatable, vec![("Light.DiffuseAmount".to_owned(), EffectPropertyValue::Scalar(1.0))]);
assert!(graph.layer_path.is_none());
}
}