1use crate::EvaluatedVisual;
2use alpha2_model::{
3 Animated, AnimatedPoint, EffectValue, GradientStop, Insets, Keyframe, LayoutLength,
4 LayoutOffset, LayoutSize, Paint, Path, PointKeyframe, Vec2, VisualProperties,
5};
6use alpha2_time::MediaTime;
7
8#[must_use]
13pub fn sample_visual(
14 properties: &VisualProperties,
15 time: MediaTime,
16 aspect: f64,
17) -> EvaluatedVisual {
18 EvaluatedVisual {
19 position: sample_point(&properties.position, time, aspect),
20 anchor: properties.anchor,
21 scale: evaluate_vec2(&properties.scale, time),
22 rotation_degrees: evaluate_scalar(&properties.rotation_degrees, time),
23 opacity: evaluate_scalar(&properties.opacity, time).clamp(0.0, 1.0),
24 }
25}
26
27fn evaluate_vec2(animation: &Animated<Vec2>, time: MediaTime) -> Vec2 {
28 match animation {
29 Animated::Constant { value } => *value,
30 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
31 Segment::Between(left, right, amount) => Vec2::new(
32 interpolate_f32(left.value.x, right.value.x, amount),
33 interpolate_f32(left.value.y, right.value.y, amount),
34 ),
35 Segment::At(keyframe) => keyframe.value,
36 Segment::Empty => Vec2::default(),
37 },
38 }
39}
40
41#[must_use]
44pub fn sample_vec2(animation: &Animated<Vec2>, time: MediaTime) -> Vec2 {
45 evaluate_vec2(animation, time)
46}
47
48#[must_use]
54pub fn sample_scalar(animation: &Animated<f32>, time: MediaTime) -> f32 {
55 evaluate_scalar(animation, time)
56}
57
58fn evaluate_scalar(animation: &Animated<f32>, time: MediaTime) -> f32 {
59 match animation {
60 Animated::Constant { value } => *value,
61 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
62 Segment::Between(left, right, amount) => {
63 interpolate_f32(left.value, right.value, amount)
64 }
65 Segment::At(keyframe) => keyframe.value,
66 Segment::Empty => 0.0,
67 },
68 }
69}
70
71#[must_use]
78pub fn sample_point(animation: &AnimatedPoint, time: MediaTime, aspect: f64) -> Vec2 {
79 match animation {
80 AnimatedPoint::Constant { value } => *value,
81 AnimatedPoint::Keyframes { keyframes } => match point_segment(keyframes, time) {
82 PointSegment::At(value) => value,
83 PointSegment::Between(left, right, amount) => {
84 if left.out_tangent.is_zero() && right.in_tangent.is_zero() {
85 return Vec2::new(
86 interpolate_f32(left.value.x, right.value.x, amount),
87 interpolate_f32(left.value.y, right.value.y, amount),
88 );
89 }
90 let aspect = if aspect.is_finite() && aspect > 0.0 {
91 aspect
92 } else {
93 1.0
94 };
95 let point = |value: Vec2| {
96 alpha2_geometry::kurbo::Point::new(
97 f64::from(value.x) * aspect,
98 f64::from(value.y),
99 )
100 };
101 let vector = |value: Vec2| {
102 alpha2_geometry::kurbo::Vec2::new(
103 f64::from(value.x) * aspect,
104 f64::from(value.y),
105 )
106 };
107 let at = alpha2_geometry::spatial::point_along(
108 point(left.value),
109 vector(left.out_tangent),
110 vector(right.in_tangent),
111 point(right.value),
112 amount,
113 );
114 Vec2::new(finite_f32(at.x / aspect), finite_f32(at.y))
115 }
116 },
117 }
118}
119
120enum PointSegment<'a> {
121 At(Vec2),
122 Between(&'a PointKeyframe, &'a PointKeyframe, f64),
123}
124
125fn point_segment(keyframes: &[PointKeyframe], time: MediaTime) -> PointSegment<'_> {
126 let next = keyframes.partition_point(|keyframe| keyframe.time <= time);
127 if next == 0 {
128 return PointSegment::At(
129 keyframes
130 .first()
131 .map_or_else(Vec2::default, |key| key.value),
132 );
133 }
134 let left = &keyframes[next - 1];
135 let Some(right) = keyframes.get(next) else {
136 return PointSegment::At(left.value);
137 };
138 if time == left.time {
139 return PointSegment::At(left.value);
140 }
141 let progress = interpolation_amount(left.time, right.time, time);
142 match left.interpolation.amount(progress) {
143 Some(amount) => PointSegment::Between(left, right, amount),
144 None => PointSegment::At(left.value),
145 }
146}
147
148#[allow(clippy::cast_possible_truncation)]
150fn finite_f32(value: f64) -> f32 {
151 if value.is_nan() {
152 0.0
153 } else {
154 value.clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32
155 }
156}
157
158#[must_use]
161pub fn sample_path(animation: &Animated<Path>, time: MediaTime) -> alpha2_geometry::EvaluatedPath {
162 use alpha2_geometry::kurbo::{Point, Vec2 as Vector};
163 use alpha2_geometry::{EvaluatedPath, EvaluatedSubpath, EvaluatedVertex};
164 match animation {
165 Animated::Constant { value } => EvaluatedPath::from(value),
166 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
167 Segment::At(keyframe) => EvaluatedPath::from(&keyframe.value),
168 Segment::Empty => EvaluatedPath::default(),
169 Segment::Between(left, right, amount) => {
170 let structure = |path: &Path| {
171 path.subpaths
172 .iter()
173 .map(|subpath| (subpath.closed, subpath.vertices.len()))
174 .collect::<Vec<_>>()
175 };
176 if structure(&left.value) != structure(&right.value) {
177 return EvaluatedPath::from(&left.value);
178 }
179 let lerp = |a: f32, b: f32| f64::from(a) + (f64::from(b) - f64::from(a)) * amount;
180 let point = |a: Vec2, b: Vec2| Point::new(lerp(a.x, b.x), lerp(a.y, b.y));
181 let vector = |a: Vec2, b: Vec2| Vector::new(lerp(a.x, b.x), lerp(a.y, b.y));
182 EvaluatedPath {
183 subpaths: left
184 .value
185 .subpaths
186 .iter()
187 .zip(&right.value.subpaths)
188 .map(|(from, to)| EvaluatedSubpath {
189 closed: from.closed,
190 vertices: from
191 .vertices
192 .iter()
193 .zip(&to.vertices)
194 .map(|(a, b)| EvaluatedVertex {
195 point: point(a.point, b.point),
196 in_tangent: vector(a.in_tangent, b.in_tangent),
197 out_tangent: vector(a.out_tangent, b.out_tangent),
198 })
199 .collect(),
200 })
201 .collect(),
202 }
203 }
204 },
205 }
206}
207
208#[must_use]
212pub fn sample_paint(animation: &Animated<Paint>, time: MediaTime) -> Paint {
213 match animation {
214 Animated::Constant { value } => value.clone(),
215 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
216 Segment::At(keyframe) => keyframe.value.clone(),
217 Segment::Empty => Paint::Solid {
218 color: alpha2_model::Rgba::new(0.0, 0.0, 0.0, 0.0),
219 },
220 Segment::Between(left, right, amount) => {
221 interpolate_paint(&left.value, &right.value, amount)
222 .unwrap_or_else(|| left.value.clone())
223 }
224 },
225 }
226}
227
228fn interpolate_paint(from: &Paint, to: &Paint, amount: f64) -> Option<Paint> {
229 let lerp = |a: f32, b: f32| interpolate_f32(a, b, amount);
230 let point = |a: Vec2, b: Vec2| Vec2::new(lerp(a.x, b.x), lerp(a.y, b.y));
231 let color = |a: alpha2_model::Rgba, b: alpha2_model::Rgba| {
232 let [red, green, blue, alpha] = interpolate_color(
233 [a.red, a.green, a.blue, a.alpha],
234 [b.red, b.green, b.blue, b.alpha],
235 amount,
236 );
237 alpha2_model::Rgba::new(red, green, blue, alpha)
238 };
239 let stops = |a: &[GradientStop], b: &[GradientStop]| -> Option<Vec<GradientStop>> {
240 if a.len() != b.len() {
241 return None;
242 }
243 let mut previous = 0.0_f32;
244 Some(
245 a.iter()
246 .zip(b)
247 .map(|(a, b)| {
248 let offset = lerp(a.offset, b.offset).clamp(previous, 1.0);
249 previous = offset;
250 GradientStop {
251 offset,
252 color: color(a.color, b.color),
253 }
254 })
255 .collect(),
256 )
257 };
258 Some(match (from, to) {
259 (Paint::Solid { color: a }, Paint::Solid { color: b }) => Paint::Solid {
260 color: color(*a, *b),
261 },
262 (
263 Paint::LinearGradient {
264 start: s0,
265 end: e0,
266 stops: a,
267 spread,
268 },
269 Paint::LinearGradient {
270 start: s1,
271 end: e1,
272 stops: b,
273 spread: other,
274 },
275 ) if spread == other => Paint::LinearGradient {
276 start: point(*s0, *s1),
277 end: point(*e0, *e1),
278 stops: stops(a, b)?,
279 spread: *spread,
280 },
281 (
282 Paint::RadialGradient {
283 center: c0,
284 radius: r0,
285 stops: a,
286 spread,
287 },
288 Paint::RadialGradient {
289 center: c1,
290 radius: r1,
291 stops: b,
292 spread: other,
293 },
294 ) if spread == other => Paint::RadialGradient {
295 center: point(*c0, *c1),
296 radius: lerp(*r0, *r1).max(f32::MIN_POSITIVE),
297 stops: stops(a, b)?,
298 spread: *spread,
299 },
300 (
301 Paint::ConicGradient {
302 center: c0,
303 angle_degrees: a0,
304 stops: a,
305 },
306 Paint::ConicGradient {
307 center: c1,
308 angle_degrees: a1,
309 stops: b,
310 },
311 ) => Paint::ConicGradient {
312 center: point(*c0, *c1),
313 angle_degrees: lerp(*a0, *a1),
314 stops: stops(a, b)?,
315 },
316 _ => return None,
317 })
318}
319
320#[derive(Copy, Clone, Debug, PartialEq)]
322pub struct EffectParamValue {
323 pub value: EffectValue,
325 pub interpolated: bool,
328}
329
330#[must_use]
336pub fn sample_effect_param(animation: &Animated<EffectValue>, time: MediaTime) -> EffectParamValue {
337 let authored = |value: EffectValue| EffectParamValue {
338 value,
339 interpolated: false,
340 };
341 match animation {
342 Animated::Constant { value } => authored(*value),
343 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
344 Segment::Between(left, right, amount) => {
345 interpolate_value(left.value, right.value, amount).map_or_else(
346 || authored(left.value),
347 |value| EffectParamValue {
348 value,
349 interpolated: true,
350 },
351 )
352 }
353 Segment::At(keyframe) => authored(keyframe.value),
354 Segment::Empty => authored(EffectValue::Float(0.0)),
355 },
356 }
357}
358
359fn interpolate_value(start: EffectValue, end: EffectValue, amount: f64) -> Option<EffectValue> {
360 let lerp = |a: f32, b: f32| interpolate_f32(a, b, amount);
361 match (start, end) {
362 (EffectValue::Float(a), EffectValue::Float(b)) => Some(EffectValue::Float(lerp(a, b))),
363 (EffectValue::Vec2(a), EffectValue::Vec2(b)) => {
364 Some(EffectValue::Vec2([lerp(a[0], b[0]), lerp(a[1], b[1])]))
365 }
366 (EffectValue::Vec4(a), EffectValue::Vec4(b)) => {
367 Some(EffectValue::Vec4(std::array::from_fn(|index| {
368 lerp(a[index], b[index])
369 })))
370 }
371 (EffectValue::Color(a), EffectValue::Color(b)) => {
372 Some(EffectValue::Color(interpolate_color(a, b, amount)))
373 }
374 _ => None,
375 }
376}
377
378#[must_use]
380pub fn sample_rgba(
381 animation: &Animated<alpha2_model::Rgba>,
382 time: MediaTime,
383) -> alpha2_model::Rgba {
384 match animation {
385 Animated::Constant { value } => *value,
386 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
387 Segment::At(keyframe) => keyframe.value,
388 Segment::Empty => alpha2_model::Rgba::new(0.0, 0.0, 0.0, 0.0),
389 Segment::Between(left, right, amount) => {
390 let (a, b) = (left.value, right.value);
391 let [red, green, blue, alpha] = interpolate_color(
392 [a.red, a.green, a.blue, a.alpha],
393 [b.red, b.green, b.blue, b.alpha],
394 amount,
395 );
396 alpha2_model::Rgba::new(red, green, blue, alpha)
397 }
398 },
399 }
400}
401
402#[allow(clippy::cast_possible_truncation)]
404fn interpolate_color(start: [f32; 4], end: [f32; 4], amount: f64) -> [f32; 4] {
405 let (start_alpha, end_alpha) = (f64::from(start[3]), f64::from(end[3]));
406 let alpha = (start_alpha + (end_alpha - start_alpha) * amount).clamp(0.0, 1.0);
407 let mut color = [0.0; 4];
408 for index in 0..3 {
409 let from = f64::from(start[index]) * start_alpha;
410 let to = f64::from(end[index]) * end_alpha;
411 let premultiplied = from + (to - from) * amount;
412 let straight = if alpha > 0.0 {
414 premultiplied / alpha
415 } else {
416 f64::from(start[index]) + (f64::from(end[index]) - f64::from(start[index])) * amount
417 };
418 color[index] = straight.clamp(0.0, 1.0) as f32;
419 }
420 color[3] = alpha as f32;
421 color
422}
423
424enum Segment<'a, T> {
425 At(&'a Keyframe<T>),
427 Between(&'a Keyframe<T>, &'a Keyframe<T>, f64),
429 Empty,
431}
432
433fn segment<T>(keyframes: &[Keyframe<T>], time: MediaTime) -> Segment<'_, T> {
434 let next = keyframes.partition_point(|keyframe| keyframe.time <= time);
435 if next == 0 {
436 return keyframes.first().map_or(Segment::Empty, Segment::At);
437 }
438 let left = &keyframes[next - 1];
439 let Some(right) = keyframes.get(next) else {
440 return Segment::At(left);
441 };
442 if time == left.time {
444 return Segment::At(left);
445 }
446 let progress = interpolation_amount(left.time, right.time, time);
447 match left.interpolation.amount(progress) {
448 Some(amount) => Segment::Between(left, right, amount),
449 None => Segment::At(left),
450 }
451}
452
453#[allow(clippy::cast_precision_loss)]
454fn interpolation_amount(start: MediaTime, end: MediaTime, time: MediaTime) -> f64 {
455 (i128::from(time.ticks()) - i128::from(start.ticks())) as f64
456 / (i128::from(end.ticks()) - i128::from(start.ticks())) as f64
457}
458
459#[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}
465
466#[must_use]
468pub fn sample_insets(animation: &Animated<Insets>, time: MediaTime) -> [f32; 4] {
469 match animation {
470 Animated::Constant { value } => value.sides().map(LayoutSize::value),
471 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
472 Segment::Between(left, right, amount) => {
473 let (from, to) = (left.value.sides(), right.value.sides());
474 std::array::from_fn(|side| {
475 interpolate_f32(from[side].value(), to[side].value(), amount)
476 })
477 }
478 Segment::At(keyframe) => keyframe.value.sides().map(LayoutSize::value),
479 Segment::Empty => [0.0; 4],
480 },
481 }
482}
483
484#[must_use]
486pub fn sample_size(animation: &Animated<LayoutSize>, time: MediaTime) -> f32 {
487 match animation {
488 Animated::Constant { value } => value.value(),
489 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
490 Segment::Between(left, right, amount) => {
491 interpolate_f32(left.value.value(), right.value.value(), amount)
492 }
493 Segment::At(keyframe) => keyframe.value.value(),
494 Segment::Empty => 0.0,
495 },
496 }
497}
498
499#[must_use]
501pub fn sample_offset(animation: &Animated<LayoutOffset>, time: MediaTime) -> Vec2 {
502 let vector = |offset: &LayoutOffset| Vec2::new(offset.x.value(), offset.y.value());
503 match animation {
504 Animated::Constant { value } => vector(value),
505 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
506 Segment::Between(left, right, amount) => Vec2::new(
507 interpolate_f32(left.value.x.value(), right.value.x.value(), amount),
508 interpolate_f32(left.value.y.value(), right.value.y.value(), amount),
509 ),
510 Segment::At(keyframe) => vector(&keyframe.value),
511 Segment::Empty => Vec2::default(),
512 },
513 }
514}
515
516#[must_use]
518pub fn sample_length(animation: &Animated<LayoutLength>, time: MediaTime) -> LayoutLength {
519 match animation {
520 Animated::Constant { value } => *value,
521 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
522 Segment::Between(left, right, amount) => left.value.with_value(interpolate_f32(
523 left.value.value(),
524 right.value.value(),
525 amount,
526 )),
527 Segment::At(keyframe) => keyframe.value,
528 Segment::Empty => LayoutLength::Fraction(0.0),
529 },
530 }
531}
532
533#[must_use]
535pub fn sample_hold<T: Clone + Default>(animation: &Animated<T>, time: MediaTime) -> T {
536 match animation {
537 Animated::Constant { value } => value.clone(),
538 Animated::Keyframes { keyframes } => {
539 let next = keyframes.partition_point(|keyframe| keyframe.time <= time);
540 keyframes
541 .get(next.saturating_sub(1))
542 .map(|keyframe| keyframe.value.clone())
543 .unwrap_or_default()
544 }
545 }
546}