1use crate::{
2 composition::model::EffectInstance,
3 foundation::core::Affine,
4 foundation::error::{WavyteError, WavyteResult},
5};
6
7#[derive(Clone, Debug, PartialEq)]
8pub enum Effect {
10 OpacityMul {
12 value: f32,
14 },
15 TransformPost {
17 value: Affine,
19 },
20 Blur {
22 radius_px: u32,
24 sigma: f32,
26 },
27}
28
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub struct InlineFx {
32 pub opacity_mul: f32,
34 pub transform_post: Affine,
36}
37
38impl Default for InlineFx {
39 fn default() -> Self {
40 Self {
41 opacity_mul: 1.0,
42 transform_post: Affine::IDENTITY,
43 }
44 }
45}
46
47#[derive(Clone, Debug, PartialEq)]
48pub enum PassFx {
50 Blur {
52 radius_px: u32,
54 sigma: f32,
56 },
57}
58
59#[derive(Clone, Debug, Default, PartialEq)]
60pub struct FxPipeline {
62 pub inline: InlineFx,
64 pub passes: Vec<PassFx>,
66}
67
68pub fn parse_effect(inst: &EffectInstance) -> WavyteResult<Effect> {
70 let kind = inst.kind.trim().to_ascii_lowercase();
71 if kind.is_empty() {
72 return Err(WavyteError::validation("effect kind must be non-empty"));
73 }
74
75 match kind.as_str() {
76 "opacitymul" | "opacity_mul" | "opacity-mul" => {
77 let value = get_f32(&inst.params, "value")?;
78 if !value.is_finite() || value < 0.0 {
79 return Err(WavyteError::validation(
80 "OpacityMul.value must be finite and >= 0",
81 ));
82 }
83 Ok(Effect::OpacityMul { value })
84 }
85 "transformpost" | "transform_post" | "transform-post" => {
86 let value = parse_affine(&inst.params)?;
87 Ok(Effect::TransformPost { value })
88 }
89 "blur" => {
90 let radius_px = get_u32(&inst.params, "radius_px")?;
91 if radius_px > 256 {
92 return Err(WavyteError::validation(
93 "Blur.radius_px must be <= 256 in v0.2.1",
94 ));
95 }
96 let sigma = match inst.params.get("sigma") {
97 Some(v) => {
98 let s = v
99 .as_f64()
100 .ok_or_else(|| WavyteError::validation("Blur.sigma must be a number"))?
101 as f32;
102 if !s.is_finite() || s <= 0.0 {
103 return Err(WavyteError::validation("Blur.sigma must be finite and > 0"));
104 }
105 s
106 }
107 None => (radius_px as f32) / 2.0,
108 };
109 Ok(Effect::Blur { radius_px, sigma })
110 }
111 _ => Err(WavyteError::validation(format!(
112 "unknown effect kind '{kind}'"
113 ))),
114 }
115}
116
117pub fn normalize_effects(effects: &[Effect]) -> FxPipeline {
119 let mut inline = InlineFx::default();
120 let mut passes = Vec::<PassFx>::new();
121
122 for e in effects {
123 match *e {
124 Effect::OpacityMul { value } => inline.opacity_mul *= value,
125 Effect::TransformPost { value } => inline.transform_post *= value,
126 Effect::Blur { radius_px, sigma } => {
127 if radius_px == 0 {
128 continue;
129 }
130 passes.push(PassFx::Blur { radius_px, sigma });
131 }
132 }
133 }
134
135 if !inline.opacity_mul.is_finite() || inline.opacity_mul < 0.0 {
136 inline.opacity_mul = 0.0;
137 }
138
139 if inline.opacity_mul == 1.0 && inline.transform_post == Affine::IDENTITY && passes.is_empty() {
140 FxPipeline::default()
141 } else {
142 FxPipeline { inline, passes }
143 }
144}
145
146fn get_u32(obj: &serde_json::Value, key: &str) -> WavyteResult<u32> {
147 let Some(v) = obj.get(key) else {
148 return Err(WavyteError::validation(format!(
149 "missing effect param '{key}'"
150 )));
151 };
152 let Some(n) = v.as_u64() else {
153 return Err(WavyteError::validation(format!(
154 "effect param '{key}' must be an integer"
155 )));
156 };
157 u32::try_from(n)
158 .map_err(|_| WavyteError::validation(format!("effect param '{key}' is out of range")))
159}
160
161fn get_f32(obj: &serde_json::Value, key: &str) -> WavyteResult<f32> {
162 let Some(v) = obj.get(key) else {
163 return Err(WavyteError::validation(format!(
164 "missing effect param '{key}'"
165 )));
166 };
167 let Some(n) = v.as_f64() else {
168 return Err(WavyteError::validation(format!(
169 "effect param '{key}' must be a number"
170 )));
171 };
172 let n = n as f32;
173 if !n.is_finite() {
174 return Err(WavyteError::validation(format!(
175 "effect param '{key}' must be finite"
176 )));
177 }
178 Ok(n)
179}
180
181fn parse_affine(params: &serde_json::Value) -> WavyteResult<Affine> {
182 if let Some(a) = params.get("affine") {
183 let Some(arr) = a.as_array() else {
184 return Err(WavyteError::validation(
185 "transform_post.affine must be an array",
186 ));
187 };
188 if arr.len() != 6 {
189 return Err(WavyteError::validation(
190 "transform_post.affine must have length 6",
191 ));
192 }
193 let mut coeffs = [0.0f64; 6];
194 for (i, v) in arr.iter().enumerate() {
195 coeffs[i] = v.as_f64().ok_or_else(|| {
196 WavyteError::validation("transform_post.affine entries must be numbers")
197 })?;
198 }
199 return Ok(Affine::new(coeffs));
200 }
201
202 let t = match params.get("translate") {
204 Some(v) => {
205 let a = v
206 .as_array()
207 .ok_or_else(|| WavyteError::validation("transform_post.translate must be [x,y]"))?;
208 if a.len() != 2 {
209 return Err(WavyteError::validation(
210 "transform_post.translate must be [x,y]",
211 ));
212 }
213 let x = a[0].as_f64().ok_or_else(|| {
214 WavyteError::validation("transform_post.translate x must be number")
215 })?;
216 let y = a[1].as_f64().ok_or_else(|| {
217 WavyteError::validation("transform_post.translate y must be number")
218 })?;
219 Affine::translate((x, y))
220 }
221 None => Affine::IDENTITY,
222 };
223
224 let rot = match (params.get("rotation_rad"), params.get("rotate_deg")) {
225 (Some(v), _) => {
226 let r = v.as_f64().ok_or_else(|| {
227 WavyteError::validation("transform_post.rotation_rad must be number")
228 })?;
229 Affine::rotate(r)
230 }
231 (None, Some(v)) => {
232 let deg = v.as_f64().ok_or_else(|| {
233 WavyteError::validation("transform_post.rotate_deg must be number")
234 })?;
235 Affine::rotate(deg.to_radians())
236 }
237 (None, None) => Affine::IDENTITY,
238 };
239
240 let scale = match params.get("scale") {
241 Some(v) => {
242 let a = v
243 .as_array()
244 .ok_or_else(|| WavyteError::validation("transform_post.scale must be [sx,sy]"))?;
245 if a.len() != 2 {
246 return Err(WavyteError::validation(
247 "transform_post.scale must be [sx,sy]",
248 ));
249 }
250 let sx = a[0]
251 .as_f64()
252 .ok_or_else(|| WavyteError::validation("transform_post.scale sx must be number"))?;
253 let sy = a[1]
254 .as_f64()
255 .ok_or_else(|| WavyteError::validation("transform_post.scale sy must be number"))?;
256 Affine::scale_non_uniform(sx, sy)
257 }
258 None => Affine::IDENTITY,
259 };
260
261 Ok(t * rot * scale)
262}
263
264#[cfg(test)]
265#[path = "../../tests/unit/effects/fx.rs"]
266mod tests;