1use crate::EvaluatedVisual;
2use alpha2_model::{Animated, EffectValue, Keyframe, Vec2, VisualProperties};
3use alpha2_time::MediaTime;
4
5#[must_use]
12pub fn sample_visual(properties: &VisualProperties, time: MediaTime) -> EvaluatedVisual {
13 EvaluatedVisual {
14 position: evaluate_vec2(&properties.position, time),
15 anchor: properties.anchor,
16 scale: evaluate_vec2(&properties.scale, time),
17 rotation_degrees: evaluate_scalar(&properties.rotation_degrees, time),
18 opacity: evaluate_scalar(&properties.opacity, time).clamp(0.0, 1.0),
19 }
20}
21
22fn evaluate_vec2(animation: &Animated<Vec2>, time: MediaTime) -> Vec2 {
23 match animation {
24 Animated::Constant { value } => *value,
25 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
26 Segment::Between(left, right, amount) => Vec2::new(
27 interpolate_f32(left.value.x, right.value.x, amount),
28 interpolate_f32(left.value.y, right.value.y, amount),
29 ),
30 Segment::At(keyframe) => keyframe.value,
31 Segment::Empty => Vec2::default(),
32 },
33 }
34}
35
36#[must_use]
39pub fn sample_vec2(animation: &Animated<Vec2>, time: MediaTime) -> Vec2 {
40 evaluate_vec2(animation, time)
41}
42
43#[must_use]
52pub fn sample_scalar(animation: &Animated<f32>, time: MediaTime) -> f32 {
53 evaluate_scalar(animation, time)
54}
55
56fn evaluate_scalar(animation: &Animated<f32>, time: MediaTime) -> f32 {
57 match animation {
58 Animated::Constant { value } => *value,
59 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
60 Segment::Between(left, right, amount) => {
61 interpolate_f32(left.value, right.value, amount)
62 }
63 Segment::At(keyframe) => keyframe.value,
64 Segment::Empty => 0.0,
65 },
66 }
67}
68
69#[derive(Copy, Clone, Debug, PartialEq)]
71pub struct EffectParamValue {
72 pub value: EffectValue,
74 pub interpolated: bool,
79}
80
81#[must_use]
89pub fn sample_effect_param(animation: &Animated<EffectValue>, time: MediaTime) -> EffectParamValue {
90 let authored = |value: EffectValue| EffectParamValue {
91 value,
92 interpolated: false,
93 };
94 match animation {
95 Animated::Constant { value } => authored(*value),
96 Animated::Keyframes { keyframes } => match segment(keyframes, time) {
97 Segment::Between(left, right, amount) => {
98 interpolate_value(left.value, right.value, amount).map_or_else(
99 || authored(left.value),
100 |value| EffectParamValue {
101 value,
102 interpolated: true,
103 },
104 )
105 }
106 Segment::At(keyframe) => authored(keyframe.value),
107 Segment::Empty => authored(EffectValue::Float(0.0)),
108 },
109 }
110}
111
112fn interpolate_value(start: EffectValue, end: EffectValue, amount: f64) -> Option<EffectValue> {
113 let lerp = |a: f32, b: f32| interpolate_f32(a, b, amount);
114 match (start, end) {
115 (EffectValue::Float(a), EffectValue::Float(b)) => Some(EffectValue::Float(lerp(a, b))),
116 (EffectValue::Vec2(a), EffectValue::Vec2(b)) => {
117 Some(EffectValue::Vec2([lerp(a[0], b[0]), lerp(a[1], b[1])]))
118 }
119 (EffectValue::Vec4(a), EffectValue::Vec4(b)) => {
120 Some(EffectValue::Vec4(std::array::from_fn(|index| {
121 lerp(a[index], b[index])
122 })))
123 }
124 (EffectValue::Color(a), EffectValue::Color(b)) => {
125 Some(EffectValue::Color(interpolate_color(a, b, amount)))
126 }
127 _ => None,
128 }
129}
130
131#[allow(clippy::cast_possible_truncation)]
134fn interpolate_color(start: [f32; 4], end: [f32; 4], amount: f64) -> [f32; 4] {
135 let (start_alpha, end_alpha) = (f64::from(start[3]), f64::from(end[3]));
136 let alpha = (start_alpha + (end_alpha - start_alpha) * amount).clamp(0.0, 1.0);
137 let mut color = [0.0; 4];
138 for index in 0..3 {
139 let from = f64::from(start[index]) * start_alpha;
140 let to = f64::from(end[index]) * end_alpha;
141 let premultiplied = from + (to - from) * amount;
142 let straight = if alpha > 0.0 {
145 premultiplied / alpha
146 } else {
147 f64::from(start[index]) + (f64::from(end[index]) - f64::from(start[index])) * amount
148 };
149 color[index] = straight.clamp(0.0, 1.0) as f32;
150 }
151 color[3] = alpha as f32;
152 color
153}
154
155enum Segment<'a, T> {
157 At(&'a Keyframe<T>),
159 Between(&'a Keyframe<T>, &'a Keyframe<T>, f64),
161 Empty,
163}
164
165fn segment<T>(keyframes: &[Keyframe<T>], time: MediaTime) -> Segment<'_, T> {
166 let next = keyframes.partition_point(|keyframe| keyframe.time <= time);
167 if next == 0 {
168 return keyframes.first().map_or(Segment::Empty, Segment::At);
169 }
170 let left = &keyframes[next - 1];
171 let Some(right) = keyframes.get(next) else {
172 return Segment::At(left);
173 };
174 if time == left.time {
176 return Segment::At(left);
177 }
178 let progress = interpolation_amount(left.time, right.time, time);
179 match left.interpolation.amount(progress) {
180 Some(amount) => Segment::Between(left, right, amount),
181 None => Segment::At(left),
182 }
183}
184
185#[allow(clippy::cast_precision_loss)]
186fn interpolation_amount(start: MediaTime, end: MediaTime, time: MediaTime) -> f64 {
187 (i128::from(time.ticks()) - i128::from(start.ticks())) as f64
188 / (i128::from(end.ticks()) - i128::from(start.ticks())) as f64
189}
190
191#[allow(clippy::cast_possible_truncation)]
194fn interpolate_f32(start: f32, end: f32, amount: f64) -> f32 {
195 (f64::from(start) + (f64::from(end) - f64::from(start)) * amount)
196 .clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32
197}