1use crate::EvaluatedVisual;
2use alpha2_model::{
3 Animated, AnimatedPoint, EffectValue, GradientStop, Keyframe, Paint, Path, PointKeyframe, Vec2,
4 VisualProperties,
5};
6use alpha2_time::MediaTime;
7
8#[must_use]
17pub fn sample_visual(
18 properties: &VisualProperties,
19 time: MediaTime,
20 aspect: f64,
21) -> EvaluatedVisual {
22 EvaluatedVisual {
23 position: sample_point(&properties.position, time, aspect),
24 anchor: properties.anchor,
25 scale: evaluate_vec2(&properties.scale, time),
26 rotation_degrees: evaluate_scalar(&properties.rotation_degrees, time),
27 opacity: evaluate_scalar(&properties.opacity, time).clamp(0.0, 1.0),
28 }
29}
30
31fn evaluate_vec2(animation: &Animated<Vec2>, time: MediaTime) -> Vec2 {
32 match animation {
33 Animated::Constant { value } => *value,
34 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
35 Segment::Between(left, right, amount) => Vec2::new(
36 interpolate_f32(left.value.x, right.value.x, amount),
37 interpolate_f32(left.value.y, right.value.y, amount),
38 ),
39 Segment::At(keyframe) => keyframe.value,
40 Segment::Empty => Vec2::default(),
41 },
42 }
43}
44
45#[must_use]
49pub fn sample_vec2(animation: &Animated<Vec2>, time: MediaTime) -> Vec2 {
50 evaluate_vec2(animation, time)
51}
52
53#[must_use]
62pub fn sample_scalar(animation: &Animated<f32>, time: MediaTime) -> f32 {
63 evaluate_scalar(animation, time)
64}
65
66fn evaluate_scalar(animation: &Animated<f32>, time: MediaTime) -> f32 {
67 match animation {
68 Animated::Constant { value } => *value,
69 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
70 Segment::Between(left, right, amount) => {
71 interpolate_f32(left.value, right.value, amount)
72 }
73 Segment::At(keyframe) => keyframe.value,
74 Segment::Empty => 0.0,
75 },
76 }
77}
78
79#[must_use]
89pub fn sample_point(animation: &AnimatedPoint, time: MediaTime, aspect: f64) -> Vec2 {
90 match animation {
91 AnimatedPoint::Constant { value } => *value,
92 AnimatedPoint::Keyframes { keyframes } => match point_segment(keyframes, time) {
93 PointSegment::At(value) => value,
94 PointSegment::Between(left, right, amount) => {
95 if left.out_tangent.is_zero() && right.in_tangent.is_zero() {
96 return Vec2::new(
97 interpolate_f32(left.value.x, right.value.x, amount),
98 interpolate_f32(left.value.y, right.value.y, amount),
99 );
100 }
101 let aspect = if aspect.is_finite() && aspect > 0.0 {
102 aspect
103 } else {
104 1.0
105 };
106 let point = |value: Vec2| {
107 alpha2_geometry::kurbo::Point::new(
108 f64::from(value.x) * aspect,
109 f64::from(value.y),
110 )
111 };
112 let vector = |value: Vec2| {
113 alpha2_geometry::kurbo::Vec2::new(
114 f64::from(value.x) * aspect,
115 f64::from(value.y),
116 )
117 };
118 let at = alpha2_geometry::spatial::point_along(
119 point(left.value),
120 vector(left.out_tangent),
121 vector(right.in_tangent),
122 point(right.value),
123 amount,
124 );
125 Vec2::new(finite_f32(at.x / aspect), finite_f32(at.y))
126 }
127 },
128 }
129}
130
131enum PointSegment<'a> {
133 At(Vec2),
134 Between(&'a PointKeyframe, &'a PointKeyframe, f64),
135}
136
137fn point_segment(keyframes: &[PointKeyframe], time: MediaTime) -> PointSegment<'_> {
138 let next = keyframes.partition_point(|keyframe| keyframe.time <= time);
139 if next == 0 {
140 return PointSegment::At(
141 keyframes
142 .first()
143 .map_or_else(Vec2::default, |key| key.value),
144 );
145 }
146 let left = &keyframes[next - 1];
147 let Some(right) = keyframes.get(next) else {
148 return PointSegment::At(left.value);
149 };
150 if time == left.time {
151 return PointSegment::At(left.value);
152 }
153 let progress = interpolation_amount(left.time, right.time, time);
154 match left.interpolation.amount(progress) {
155 Some(amount) => PointSegment::Between(left, right, amount),
156 None => PointSegment::At(left.value),
157 }
158}
159
160#[allow(clippy::cast_possible_truncation)]
162fn finite_f32(value: f64) -> f32 {
163 if value.is_nan() {
164 0.0
165 } else {
166 value.clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32
167 }
168}
169
170#[must_use]
174pub fn sample_path(animation: &Animated<Path>, time: MediaTime) -> alpha2_geometry::EvaluatedPath {
175 use alpha2_geometry::kurbo::{Point, Vec2 as Vector};
176 use alpha2_geometry::{EvaluatedPath, EvaluatedSubpath, EvaluatedVertex};
177 match animation {
178 Animated::Constant { value } => EvaluatedPath::from(value),
179 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
180 Segment::At(keyframe) => EvaluatedPath::from(&keyframe.value),
181 Segment::Empty => EvaluatedPath::default(),
182 Segment::Between(left, right, amount) => {
183 let structure = |path: &Path| {
184 path.subpaths
185 .iter()
186 .map(|subpath| (subpath.closed, subpath.vertices.len()))
187 .collect::<Vec<_>>()
188 };
189 if structure(&left.value) != structure(&right.value) {
190 return EvaluatedPath::from(&left.value);
191 }
192 let lerp = |a: f32, b: f32| f64::from(a) + (f64::from(b) - f64::from(a)) * amount;
193 let point = |a: Vec2, b: Vec2| Point::new(lerp(a.x, b.x), lerp(a.y, b.y));
194 let vector = |a: Vec2, b: Vec2| Vector::new(lerp(a.x, b.x), lerp(a.y, b.y));
195 EvaluatedPath {
196 subpaths: left
197 .value
198 .subpaths
199 .iter()
200 .zip(&right.value.subpaths)
201 .map(|(from, to)| EvaluatedSubpath {
202 closed: from.closed,
203 vertices: from
204 .vertices
205 .iter()
206 .zip(&to.vertices)
207 .map(|(a, b)| EvaluatedVertex {
208 point: point(a.point, b.point),
209 in_tangent: vector(a.in_tangent, b.in_tangent),
210 out_tangent: vector(a.out_tangent, b.out_tangent),
211 })
212 .collect(),
213 })
214 .collect(),
215 }
216 }
217 },
218 }
219}
220
221#[must_use]
228pub fn sample_paint(animation: &Animated<Paint>, time: MediaTime) -> Paint {
229 match animation {
230 Animated::Constant { value } => value.clone(),
231 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
232 Segment::At(keyframe) => keyframe.value.clone(),
233 Segment::Empty => Paint::Solid {
234 color: alpha2_model::Rgba::new(0.0, 0.0, 0.0, 0.0),
235 },
236 Segment::Between(left, right, amount) => {
237 interpolate_paint(&left.value, &right.value, amount)
238 .unwrap_or_else(|| left.value.clone())
239 }
240 },
241 }
242}
243
244fn interpolate_paint(from: &Paint, to: &Paint, amount: f64) -> Option<Paint> {
245 let lerp = |a: f32, b: f32| interpolate_f32(a, b, amount);
246 let point = |a: Vec2, b: Vec2| Vec2::new(lerp(a.x, b.x), lerp(a.y, b.y));
247 let color = |a: alpha2_model::Rgba, b: alpha2_model::Rgba| {
248 let [red, green, blue, alpha] = interpolate_color(
249 [a.red, a.green, a.blue, a.alpha],
250 [b.red, b.green, b.blue, b.alpha],
251 amount,
252 );
253 alpha2_model::Rgba::new(red, green, blue, alpha)
254 };
255 let stops = |a: &[GradientStop], b: &[GradientStop]| -> Option<Vec<GradientStop>> {
256 if a.len() != b.len() {
257 return None;
258 }
259 let mut previous = 0.0_f32;
260 Some(
261 a.iter()
262 .zip(b)
263 .map(|(a, b)| {
264 let offset = lerp(a.offset, b.offset).clamp(previous, 1.0);
265 previous = offset;
266 GradientStop {
267 offset,
268 color: color(a.color, b.color),
269 }
270 })
271 .collect(),
272 )
273 };
274 Some(match (from, to) {
275 (Paint::Solid { color: a }, Paint::Solid { color: b }) => Paint::Solid {
276 color: color(*a, *b),
277 },
278 (
279 Paint::LinearGradient {
280 start: s0,
281 end: e0,
282 stops: a,
283 spread,
284 },
285 Paint::LinearGradient {
286 start: s1,
287 end: e1,
288 stops: b,
289 spread: other,
290 },
291 ) if spread == other => Paint::LinearGradient {
292 start: point(*s0, *s1),
293 end: point(*e0, *e1),
294 stops: stops(a, b)?,
295 spread: *spread,
296 },
297 (
298 Paint::RadialGradient {
299 center: c0,
300 radius: r0,
301 stops: a,
302 spread,
303 },
304 Paint::RadialGradient {
305 center: c1,
306 radius: r1,
307 stops: b,
308 spread: other,
309 },
310 ) if spread == other => Paint::RadialGradient {
311 center: point(*c0, *c1),
312 radius: lerp(*r0, *r1).max(f32::MIN_POSITIVE),
313 stops: stops(a, b)?,
314 spread: *spread,
315 },
316 (
317 Paint::ConicGradient {
318 center: c0,
319 angle_degrees: a0,
320 stops: a,
321 },
322 Paint::ConicGradient {
323 center: c1,
324 angle_degrees: a1,
325 stops: b,
326 },
327 ) => Paint::ConicGradient {
328 center: point(*c0, *c1),
329 angle_degrees: lerp(*a0, *a1),
330 stops: stops(a, b)?,
331 },
332 _ => return None,
333 })
334}
335
336#[derive(Copy, Clone, Debug, PartialEq)]
338pub struct EffectParamValue {
339 pub value: EffectValue,
341 pub interpolated: bool,
346}
347
348#[must_use]
356pub fn sample_effect_param(animation: &Animated<EffectValue>, time: MediaTime) -> EffectParamValue {
357 let authored = |value: EffectValue| EffectParamValue {
358 value,
359 interpolated: false,
360 };
361 match animation {
362 Animated::Constant { value } => authored(*value),
363 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
364 Segment::Between(left, right, amount) => {
365 interpolate_value(left.value, right.value, amount).map_or_else(
366 || authored(left.value),
367 |value| EffectParamValue {
368 value,
369 interpolated: true,
370 },
371 )
372 }
373 Segment::At(keyframe) => authored(keyframe.value),
374 Segment::Empty => authored(EffectValue::Float(0.0)),
375 },
376 }
377}
378
379fn interpolate_value(start: EffectValue, end: EffectValue, amount: f64) -> Option<EffectValue> {
380 let lerp = |a: f32, b: f32| interpolate_f32(a, b, amount);
381 match (start, end) {
382 (EffectValue::Float(a), EffectValue::Float(b)) => Some(EffectValue::Float(lerp(a, b))),
383 (EffectValue::Vec2(a), EffectValue::Vec2(b)) => {
384 Some(EffectValue::Vec2([lerp(a[0], b[0]), lerp(a[1], b[1])]))
385 }
386 (EffectValue::Vec4(a), EffectValue::Vec4(b)) => {
387 Some(EffectValue::Vec4(std::array::from_fn(|index| {
388 lerp(a[index], b[index])
389 })))
390 }
391 (EffectValue::Color(a), EffectValue::Color(b)) => {
392 Some(EffectValue::Color(interpolate_color(a, b, amount)))
393 }
394 _ => None,
395 }
396}
397
398#[allow(clippy::cast_possible_truncation)]
401fn interpolate_color(start: [f32; 4], end: [f32; 4], amount: f64) -> [f32; 4] {
402 let (start_alpha, end_alpha) = (f64::from(start[3]), f64::from(end[3]));
403 let alpha = (start_alpha + (end_alpha - start_alpha) * amount).clamp(0.0, 1.0);
404 let mut color = [0.0; 4];
405 for index in 0..3 {
406 let from = f64::from(start[index]) * start_alpha;
407 let to = f64::from(end[index]) * end_alpha;
408 let premultiplied = from + (to - from) * amount;
409 let straight = if alpha > 0.0 {
412 premultiplied / alpha
413 } else {
414 f64::from(start[index]) + (f64::from(end[index]) - f64::from(start[index])) * amount
415 };
416 color[index] = straight.clamp(0.0, 1.0) as f32;
417 }
418 color[3] = alpha as f32;
419 color
420}
421
422enum Segment<'a, T> {
424 At(&'a Keyframe<T>),
426 Between(&'a Keyframe<T>, &'a Keyframe<T>, f64),
428 Empty,
430}
431
432fn segment<T>(keyframes: &[Keyframe<T>], time: MediaTime) -> Segment<'_, T> {
433 let next = keyframes.partition_point(|keyframe| keyframe.time <= time);
434 if next == 0 {
435 return keyframes.first().map_or(Segment::Empty, Segment::At);
436 }
437 let left = &keyframes[next - 1];
438 let Some(right) = keyframes.get(next) else {
439 return Segment::At(left);
440 };
441 if time == left.time {
443 return Segment::At(left);
444 }
445 let progress = interpolation_amount(left.time, right.time, time);
446 match left.interpolation.amount(progress) {
447 Some(amount) => Segment::Between(left, right, amount),
448 None => Segment::At(left),
449 }
450}
451
452#[allow(clippy::cast_precision_loss)]
453fn interpolation_amount(start: MediaTime, end: MediaTime, time: MediaTime) -> f64 {
454 (i128::from(time.ticks()) - i128::from(start.ticks())) as f64
455 / (i128::from(end.ticks()) - i128::from(start.ticks())) as f64
456}
457
458#[allow(clippy::cast_possible_truncation)]
461fn interpolate_f32(start: f32, end: f32, amount: f64) -> f32 {
462 (f64::from(start) + (f64::from(end) - f64::from(start)) * amount)
463 .clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32
464}