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