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}