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}