Skip to main content

alpha2_model/
effect.rs

1use std::collections::BTreeMap;
2
3use crate::{Animated, EffectResourceId, Interpolation};
4
5use serde::{Deserialize, Serialize};
6
7/// Artistic blend operation stored independently from a renderer implementation.
8///
9/// The modes are those of W3C Compositing and Blending Level 1 (the CSS
10/// `mix-blend-mode` values), with the specification's formulas.
11#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13#[serde(rename_all = "snake_case")]
14pub enum ClipBlendMode {
15    /// Preserve source color and apply source-over coverage.
16    #[default]
17    Normal,
18    /// Multiply source and backdrop components.
19    Multiply,
20    /// Inverse-multiply source and backdrop components.
21    Screen,
22    /// Multiply dark backdrop regions and screen light regions.
23    Overlay,
24    /// The darker of source and backdrop, per channel.
25    Darken,
26    /// The lighter of source and backdrop, per channel.
27    Lighten,
28    /// Brighten the backdrop to reflect the source.
29    ColorDodge,
30    /// Darken the backdrop to reflect the source.
31    ColorBurn,
32    /// Multiply or screen, depending on the source.
33    HardLight,
34    /// Darken or lighten, depending on the source: a diffused hard light.
35    SoftLight,
36    /// The absolute difference of source and backdrop.
37    Difference,
38    /// Like difference, with lower contrast.
39    Exclusion,
40    /// The source's hue with the backdrop's saturation and luminosity.
41    Hue,
42    /// The source's saturation with the backdrop's hue and luminosity.
43    Saturation,
44    /// The source's hue and saturation with the backdrop's luminosity.
45    Color,
46    /// The source's luminosity with the backdrop's hue and saturation.
47    Luminosity,
48}
49
50/// Serializable value supplied to one runtime effect parameter.
51#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
52#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
53#[serde(deny_unknown_fields)]
54#[serde(tag = "type", content = "value", rename_all = "snake_case")]
55pub enum EffectValue {
56    /// One finite scalar.
57    Float(f32),
58    /// Two finite components, normally in normalized coordinates.
59    Vec2([f32; 2]),
60    /// Four finite components that are not a color, such as per-channel
61    /// thresholds; they keyframe component by component.
62    Vec4([f32; 4]),
63    /// Straight (not premultiplied), sRGB-encoded RGBA components in the
64    /// inclusive unit interval, as a color picker shows them.
65    Color([f32; 4]),
66    /// Index into an effect definition's enum choices.
67    Enum(u32),
68    /// Boolean toggle.
69    Bool(bool),
70}
71
72impl EffectValue {
73    /// Whether keyframes of this type interpolate. `Enum` and `Bool` values
74    /// have nothing between them, so their keyframes only hold.
75    #[must_use]
76    pub const fn interpolates(&self) -> bool {
77        matches!(
78            self,
79            Self::Float(_) | Self::Vec2(_) | Self::Vec4(_) | Self::Color(_)
80        )
81    }
82
83    /// Whether `other` is the same type of value.
84    #[must_use]
85    pub fn same_type(&self, other: &Self) -> bool {
86        core::mem::discriminant(self) == core::mem::discriminant(other)
87    }
88
89    pub(crate) fn is_valid(&self) -> bool {
90        match self {
91            Self::Float(value) => value.is_finite(),
92            Self::Vec2(values) => values.iter().all(|value| value.is_finite()),
93            Self::Vec4(values) => values.iter().all(|value| value.is_finite()),
94            Self::Color(values) => values
95                .iter()
96                .all(|value| value.is_finite() && (0.0..=1.0).contains(value)),
97            Self::Enum(_) | Self::Bool(_) => true,
98        }
99    }
100}
101
102/// One versioned runtime effect invocation: a composition's effect or
103/// transition node, or a timeline transition.
104#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106#[serde(deny_unknown_fields)]
107pub struct ClipEffect {
108    /// Stable effect definition identifier.
109    pub id: String,
110    /// Exact effect definition version required by the project.
111    pub version: u32,
112    /// Named parameter values, constant or keyframed. Omitted entries use
113    /// definition defaults.
114    ///
115    /// Keyframe times are composition times in a composition node and
116    /// timeline times in a timeline transition. A parameter's keyframes all
117    /// have one value type, and `enum` and `bool` keyframes only hold. An
118    /// interpolated value that a timing curve carries past the parameter's
119    /// range is clamped to it; an authored value outside it is an error.
120    #[serde(default)]
121    pub params: BTreeMap<String, Animated<EffectValue>>,
122    /// Named external resources matched against the effect definition.
123    #[serde(default)]
124    pub resources: BTreeMap<String, EffectResourceId>,
125}
126
127impl ClipEffect {
128    pub(crate) fn is_valid(&self) -> bool {
129        !self.id.is_empty()
130            && self.version > 0
131            && self.params.iter().all(|(name, value)| {
132                let mut bytes = name.bytes();
133                bytes
134                    .next()
135                    .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
136                    && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
137                    && valid_param(value)
138            })
139            && self.resources.iter().all(|(name, id)| {
140                let mut bytes = name.bytes();
141                bytes
142                    .next()
143                    .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
144                    && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
145                    && !id.as_str().is_empty()
146            })
147    }
148}
149
150pub(crate) fn valid_param(animation: &Animated<EffectValue>) -> bool {
151    match animation {
152        Animated::Constant { value } => value.is_valid(),
153        Animated::Keyframes { keyframes } => {
154            let Some(first) = keyframes.first() else {
155                return false;
156            };
157            crate::animation::valid_keyframes(keyframes)
158                && keyframes.iter().all(|keyframe| {
159                    keyframe.value.is_valid()
160                        && keyframe.value.same_type(&first.value)
161                        && (first.value.interpolates()
162                            || keyframe.interpolation == Interpolation::Hold)
163                })
164        }
165    }
166}