1#![forbid(unsafe_code)]
2
3use std::ops::Range;
4use std::time::Duration;
5
6use sequoia_core::Vec2;
7
8use crate::{Animation, Easing, Keyframe, Lerp, Spring, Track};
9
10fn dur_secs(d: Duration) -> f32 {
11 d.as_secs_f32().max(0.0)
12}
13
14#[derive(Clone, Debug)]
15pub struct Animate {
16 pub opacity: Option<(f32, f32)>,
17 pub offset: Option<(Vec2, Vec2)>,
18 pub duration_secs: f32,
19 pub delay_secs: f32,
20 pub easing: Easing,
21}
22
23impl Animate {
24 fn sample_one(from: f32, to: f32, delay: f32, dur: f32, ease: Easing, t: f32) -> f32 {
25 let t = t.max(0.0);
26 if t < delay {
27 return from;
28 }
29 if dur <= 0.0 {
30 return to;
31 }
32 let k = ((t - delay) / dur).clamp(0.0, 1.0);
33 from + (to - from) * ease.apply(k)
34 }
35
36 pub fn sample_at(&self, elapsed_secs: f32) -> AnimateFrame {
37 let op = self.opacity.map(|(a, b)| {
38 Self::sample_one(
39 a,
40 b,
41 self.delay_secs,
42 self.duration_secs,
43 self.easing,
44 elapsed_secs,
45 )
46 });
47 let off = self.offset.map(|(a, b)| {
48 let x = Self::sample_one(
49 a.x,
50 b.x,
51 self.delay_secs,
52 self.duration_secs,
53 self.easing,
54 elapsed_secs,
55 );
56 let y = Self::sample_one(
57 a.y,
58 b.y,
59 self.delay_secs,
60 self.duration_secs,
61 self.easing,
62 elapsed_secs,
63 );
64 Vec2::new(x, y)
65 });
66 AnimateFrame {
67 opacity: op,
68 offset: off,
69 }
70 }
71
72 pub fn is_running(&self, elapsed_secs: f32) -> bool {
73 elapsed_secs < self.delay_secs + self.duration_secs
74 }
75
76 pub fn into_animation(&self, name: impl Into<String>) -> Option<Animation> {
77 let name = name.into();
78 let mut anim = Animation::new(name, self.duration_secs.max(0.0) as f64);
79 let mut any = false;
80 if let Some((a, b)) = self.opacity {
81 let track = Track::try_new(vec![
82 Keyframe::new(0.0, crate::Value::f32(a)),
83 Keyframe::new(self.duration_secs.max(0.0) as f64, crate::Value::f32(b)),
84 ])?;
85 anim = anim.with_track("opacity", track);
86 any = true;
87 }
88 if let Some((a, b)) = self.offset {
89 let dx = Track::try_new(vec![
90 Keyframe::new(0.0, crate::Value::px(a.x)),
91 Keyframe::new(self.duration_secs.max(0.0) as f64, crate::Value::px(b.x)),
92 ])?;
93 anim = anim.with_track("x", dx);
94 let dy = Track::try_new(vec![
95 Keyframe::new(0.0, crate::Value::px(a.y)),
96 Keyframe::new(self.duration_secs.max(0.0) as f64, crate::Value::px(b.y)),
97 ])?;
98 anim = anim.with_track("y", dy);
99 any = true;
100 }
101 if any { Some(anim) } else { None }
102 }
103}
104
105#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct AnimateFrame {
107 pub opacity: Option<f32>,
108 pub offset: Option<Vec2>,
109}
110
111impl From<AnimateBuilder> for Animate {
112 fn from(b: AnimateBuilder) -> Self {
113 b.build()
114 }
115}
116
117pub fn animate() -> AnimateBuilder {
118 AnimateBuilder {
119 opacity: None,
120 offset: None,
121 duration_secs: 0.25,
122 delay_secs: 0.0,
123 easing: Easing::EaseOutCubic,
124 }
125}
126
127#[derive(Clone, Debug)]
128pub struct AnimateBuilder {
129 opacity: Option<(f32, f32)>,
130 offset: Option<(Vec2, Vec2)>,
131 duration_secs: f32,
132 delay_secs: f32,
133 easing: Easing,
134}
135
136impl AnimateBuilder {
137 pub fn opacity(mut self, range: Range<f32>) -> Self {
138 self.opacity = Some((range.start.clamp(0.0, 1.0), range.end.clamp(0.0, 1.0)));
139 self
140 }
141
142 pub fn translate(mut self, from: Vec2, to: Vec2) -> Self {
143 self.offset = Some((from, to));
144 self
145 }
146
147 pub fn translate_xy(mut self, from: (f32, f32), to: (f32, f32)) -> Self {
148 self.offset = Some((Vec2::new(from.0, from.1), Vec2::new(to.0, to.1)));
149 self
150 }
151
152 pub fn translate_y(mut self, range: Range<f32>) -> Self {
153 let (from, to) = (Vec2::new(0.0, range.start), Vec2::new(0.0, range.end));
154 self.offset = Some((from, to));
155 self
156 }
157
158 pub fn translate_x(mut self, range: Range<f32>) -> Self {
159 let (from, to) = (Vec2::new(range.start, 0.0), Vec2::new(range.end, 0.0));
160 self.offset = Some((from, to));
161 self
162 }
163
164 pub fn duration(mut self, d: Duration) -> Self {
165 self.duration_secs = dur_secs(d);
166 self
167 }
168
169 pub fn delay(mut self, d: Duration) -> Self {
170 self.delay_secs = dur_secs(d);
171 self
172 }
173
174 pub fn ease(mut self, easing: Easing) -> Self {
175 self.easing = easing;
176 self
177 }
178
179 pub fn ease_out(mut self) -> Self {
180 self.easing = Easing::EaseOutCubic;
181 self
182 }
183
184 pub fn ease_in_out(mut self) -> Self {
185 self.easing = Easing::EaseInOutCubic;
186 self
187 }
188
189 pub fn linear(mut self) -> Self {
190 self.easing = Easing::Linear;
191 self
192 }
193
194 pub fn build(self) -> Animate {
195 Animate {
196 opacity: self.opacity,
197 offset: self.offset,
198 duration_secs: self.duration_secs,
199 delay_secs: self.delay_secs,
200 easing: self.easing,
201 }
202 }
203
204 pub fn sample_at(&self, elapsed_secs: f32) -> AnimateFrame {
205 self.clone().build().sample_at(elapsed_secs)
206 }
207}
208
209pub fn animation(from: f32, to: f32) -> TweenBuilder {
210 TweenBuilder {
211 from,
212 to,
213 duration_secs: 0.25,
214 delay_secs: 0.0,
215 easing: Easing::EaseOutCubic,
216 }
217}
218
219#[derive(Clone, Copy, Debug)]
220pub struct TweenBuilder {
221 from: f32,
222 to: f32,
223 duration_secs: f32,
224 delay_secs: f32,
225 easing: Easing,
226}
227
228impl TweenBuilder {
229 pub fn duration(mut self, d: Duration) -> Self {
230 self.duration_secs = dur_secs(d);
231 self
232 }
233
234 pub fn delay(mut self, d: Duration) -> Self {
235 self.delay_secs = dur_secs(d);
236 self
237 }
238
239 pub fn ease(mut self, easing: Easing) -> Self {
240 self.easing = easing;
241 self
242 }
243
244 pub fn ease_out(self) -> Self {
245 self.ease(Easing::EaseOutCubic)
246 }
247
248 pub fn sample_at(&self, elapsed_secs: f32) -> f32 {
249 Animate::sample_one(
250 self.from,
251 self.to,
252 self.delay_secs,
253 self.duration_secs,
254 self.easing,
255 elapsed_secs,
256 )
257 }
258
259 pub fn is_running(&self, elapsed_secs: f32) -> bool {
260 elapsed_secs < self.delay_secs + self.duration_secs
261 }
262}
263
264pub fn spring(target: f32) -> SpringBuilder {
265 SpringBuilder {
266 from: 0.0,
267 target,
268 stiffness: 180.0,
269 damping: 20.0,
270 mass: 1.0,
271 }
272}
273
274#[derive(Clone, Copy, Debug)]
275pub struct SpringBuilder {
276 from: f32,
277 target: f32,
278 stiffness: f32,
279 damping: f32,
280 mass: f32,
281}
282
283impl SpringBuilder {
284 pub fn from(mut self, value: f32) -> Self {
285 self.from = value;
286 self
287 }
288
289 pub fn stiffness(mut self, k: f32) -> Self {
290 self.stiffness = k.max(0.0);
291 self
292 }
293
294 pub fn damping(mut self, c: f32) -> Self {
295 self.damping = c.max(0.0);
296 self
297 }
298
299 pub fn mass(mut self, m: f32) -> Self {
300 self.mass = m.max(0.0001);
301 self
302 }
303
304 pub fn spring(&self) -> Spring {
305 Spring::from_stiffness_damping(self.stiffness, self.damping, self.mass)
306 }
307
308 pub fn sample_at(&self, t_secs: f32) -> f32 {
309 let s = self.spring().value(t_secs.max(0.0));
310 self.from + (self.target - self.from) * s
311 }
312
313 pub fn settle_time(&self, eps: f32) -> f32 {
314 self.spring().settle_time(eps)
315 }
316}
317
318pub fn lerp_sample<T: Lerp>(from: T, to: T, easing: Easing, progress: f32) -> T {
319 from.lerp(to, easing.apply(progress.clamp(0.0, 1.0)))
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::DurationExt;
326
327 #[test]
328 fn animate_opacity_walks_zero_to_one() {
329 let a = animate()
330 .opacity(0.0..1.0)
331 .duration(200.ms())
332 .ease_out()
333 .build();
334 assert_eq!(a.sample_at(0.0).opacity, Some(0.0));
335 let mid = a.sample_at(0.1).opacity.unwrap();
336 assert!(mid > 0.0 && mid < 1.0, "mid={mid}");
337 assert_eq!(a.sample_at(10.0).opacity, Some(1.0));
338 assert!(a.is_running(0.1));
339 assert!(!a.is_running(10.0));
340 }
341
342 #[test]
343 fn animate_translate_y_and_delay() {
344 use std::time::Duration;
345 let a = animate()
346 .translate_y(-8.0..0.0)
347 .duration(Duration::from_millis(200))
348 .delay(Duration::from_millis(100))
349 .linear()
350 .build();
351 assert_eq!(a.sample_at(0.0).offset, Some(Vec2::new(0.0, -8.0)));
352 assert_eq!(a.sample_at(0.05).offset, Some(Vec2::new(0.0, -8.0)));
353 let mid = a.sample_at(0.2).offset.unwrap();
354 assert!(mid.y > -8.0 && mid.y < 0.0, "mid={mid:?}");
355 assert_eq!(a.sample_at(10.0).offset, Some(Vec2::ZERO));
356 }
357
358 #[test]
359 fn tween_builder_samples() {
360 let t = animation(0.0, 1.0).duration(200.ms()).ease_out();
361 assert_eq!(t.sample_at(0.0), 0.0);
362 assert_eq!(t.sample_at(10.0), 1.0);
363 }
364
365 #[test]
366 fn spring_builder_settles_and_maps_params() {
367 let s = spring(100.0).from(0.0).stiffness(180.0).damping(20.0);
368 assert_eq!(s.sample_at(0.0), 0.0);
369 let settled = s.sample_at(s.settle_time(0.001) + 1.0);
370 assert!((settled - 100.0).abs() < 0.5, "settled={settled}");
371
372 let stiff = spring(1.0).from(0.0).stiffness(200.0).damping(40.0);
373 let mut peak = 0.0f32;
374 let mut t = 0.0;
375 while t < 2.0 {
376 peak = peak.max(stiff.sample_at(t));
377 t += 0.005;
378 }
379 assert!(peak <= 1.001, "peak={peak}");
380 }
381
382 #[test]
383 fn animate_lowers_to_animation_clip() {
384 let a = animate().opacity(0.0..1.0).duration(200.ms()).build();
385 let clip = a.into_animation("fade").expect("ma track");
386 let f = clip.sample(0.0);
387 assert!(f.get("opacity").is_some());
388 }
389}