1use alloc::{collections::BTreeMap, vec::Vec};
24
25use azul_css::props::basic::animation::AnimationInterpolationFunction;
26
27use crate::{
28 diff::{calculate_reconciliation_key, NodeMove},
29 dom::NodeData,
30 geom::LogicalRect,
31 id::NodeId,
32 styled_dom::NodeHierarchyItem,
33};
34
35pub use azul_css::props::basic::animation::SpringCurve as Spring;
37
38#[derive(Debug, Clone, Copy, PartialEq)]
43pub struct AnimChannel {
44 pub from: f32,
46 pub to: f32,
48 pub current: f32,
50 pub velocity: f32,
52 pub elapsed_secs: f32,
54 pub mode: InterpolationMode,
56 finished: bool,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq)]
65pub enum InterpolationMode {
66 Curve {
68 function: AnimationInterpolationFunction,
70 duration_secs: f32,
72 },
73 Spring(Spring),
75}
76
77impl Default for InterpolationMode {
78 fn default() -> Self {
79 Self::Spring(Spring::SMOOTH)
80 }
81}
82
83impl AnimChannel {
84 #[must_use]
86 pub const fn curve(
87 from: f32,
88 to: f32,
89 function: AnimationInterpolationFunction,
90 duration_secs: f32,
91 ) -> Self {
92 Self {
93 from,
94 to,
95 current: from,
96 velocity: 0.0,
97 elapsed_secs: 0.0,
98 mode: InterpolationMode::Curve {
99 function,
100 duration_secs,
101 },
102 finished: false,
103 }
104 }
105
106 #[must_use]
108 pub const fn spring(from: f32, to: f32, spring: Spring) -> Self {
109 Self {
110 from,
111 to,
112 current: from,
113 velocity: 0.0,
114 elapsed_secs: 0.0,
115 mode: InterpolationMode::Spring(spring),
116 finished: false,
117 }
118 }
119
120 pub fn tick(&mut self, dt: f32) -> f32 {
122 if self.finished {
123 return self.current;
124 }
125 match self.mode {
126 InterpolationMode::Curve {
127 function,
128 duration_secs,
129 } => {
130 if duration_secs <= 0.0 {
131 self.current = self.to;
132 self.velocity = 0.0;
133 self.finished = true;
134 return self.current;
135 }
136 self.elapsed_secs += dt.max(0.0);
137 let linear_t = (self.elapsed_secs / duration_secs).clamp(0.0, 1.0);
138 let eased = ease(function, linear_t);
139 let previous = self.current;
140 #[allow(clippy::suboptimal_flops)]
142 {
143 self.current = self.from + (self.to - self.from) * eased;
144 }
145 self.velocity = if dt > 0.0 {
147 (self.current - previous) / dt
148 } else {
149 0.0
150 };
151 if linear_t >= 1.0 {
152 self.current = self.to;
153 self.finished = true;
154 }
155 }
156 InterpolationMode::Spring(spring) => {
157 let (value, velocity) = spring.step(self.current, self.to, self.velocity, dt);
158 self.current = value;
159 self.velocity = velocity;
160 if spring.is_settled(value, self.to, velocity) {
161 self.current = self.to;
162 self.velocity = 0.0;
163 self.finished = true;
164 }
165 }
166 }
167 self.current
168 }
169
170 #[must_use]
172 pub const fn is_finished(&self) -> bool {
173 self.finished
174 }
175
176 pub fn retarget(&mut self, new_to: f32) {
180 if (self.to - new_to).abs() < f32::EPSILON && !self.finished {
181 return; }
183 self.from = self.current;
184 self.to = new_to;
185 self.elapsed_secs = 0.0;
186 self.finished = false;
187 }
189}
190
191#[must_use]
196pub fn ease(function: AnimationInterpolationFunction, t: f32) -> f32 {
197 let t = t.clamp(0.0, 1.0);
198 match function {
199 AnimationInterpolationFunction::Linear => t,
200 AnimationInterpolationFunction::Ease => cubic_bezier_y(0.25, 0.1, 0.25, 1.0, t),
202 AnimationInterpolationFunction::EaseIn => cubic_bezier_y(0.42, 0.0, 1.0, 1.0, t),
203 AnimationInterpolationFunction::EaseOut => cubic_bezier_y(0.0, 0.0, 0.58, 1.0, t),
204 AnimationInterpolationFunction::EaseInOut => cubic_bezier_y(0.42, 0.0, 0.58, 1.0, t),
205 AnimationInterpolationFunction::Spring(_) => {
206 crate::diagnostics::emit(String::from(
207 "Warning: Spring evaluated as an easing curve. This is a misusage.",
208 ));
209 cubic_bezier_y(0.42, 0.0, 0.58, 1.0, t)
211 }
212 AnimationInterpolationFunction::CubicBezier(curve) => cubic_bezier_y(
215 curve.ctrl_1.x,
216 curve.ctrl_1.y,
217 curve.ctrl_2.x,
218 curve.ctrl_2.y,
219 t,
220 ),
221 }
222}
223
224fn cubic_bezier_y(x1: f32, y1: f32, x2: f32, y2: f32, x: f32) -> f32 {
233 const NEWTON_ITERATIONS: usize = 4;
234 const BISECTION_ITERATIONS: usize = 12;
235 const EPSILON: f32 = 1e-5;
236
237 #[allow(clippy::suboptimal_flops)]
239 let bezier = |a: f32, b: f32, t: f32| {
240 let inv = 1.0 - t;
241 3.0 * inv * inv * t * a + 3.0 * inv * t * t * b + t * t * t
242 };
243 #[allow(clippy::suboptimal_flops)]
244 let bezier_slope = |a: f32, b: f32, t: f32| {
245 let inv = 1.0 - t;
246 3.0 * inv * inv * a + 6.0 * inv * t * (b - a) + 3.0 * t * t * (1.0 - b)
247 };
248
249 if x <= 0.0 {
250 return 0.0;
251 }
252 if x >= 1.0 {
253 return 1.0;
254 }
255
256 let mut t = x;
257 for _ in 0..NEWTON_ITERATIONS {
258 let error = bezier(x1, x2, t) - x;
259 if error.abs() < EPSILON {
260 return bezier(y1, y2, t);
261 }
262 let slope = bezier_slope(x1, x2, t);
263 if slope.abs() < EPSILON {
264 break;
265 }
266 t -= error / slope;
267 }
268
269 let (mut low, mut high) = (0.0_f32, 1.0_f32);
270 let mut t = x;
271 for _ in 0..BISECTION_ITERATIONS {
272 let value = bezier(x1, x2, t);
273 if (value - x).abs() < EPSILON {
274 break;
275 }
276 if value < x {
277 low = t;
278 } else {
279 high = t;
280 }
281 t = (low + high) * 0.5;
282 }
283 bezier(y1, y2, t)
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Default)]
294#[repr(C)]
295pub struct FlipTransform {
296 pub translate_x: f32,
298 pub translate_y: f32,
300 pub scale_x: f32,
302 pub scale_y: f32,
304}
305
306impl FlipTransform {
307 pub const IDENTITY: Self = Self {
309 translate_x: 0.0,
310 translate_y: 0.0,
311 scale_x: 1.0,
312 scale_y: 1.0,
313 };
314
315 #[must_use]
317 pub fn is_identity(&self) -> bool {
318 self.translate_x.abs() < 0.01
319 && self.translate_y.abs() < 0.01
320 && (self.scale_x - 1.0).abs() < 0.001
321 && (self.scale_y - 1.0).abs() < 0.001
322 }
323}
324
325#[must_use]
330pub fn flip(first: LogicalRect, last: LogicalRect) -> FlipTransform {
331 let _ = (first.size, last.size); FlipTransform {
333 translate_x: first.origin.x - last.origin.x,
334 translate_y: first.origin.y - last.origin.y,
335 scale_x: 1.0,
336 scale_y: 1.0,
337 }
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342pub enum AnimClass {
343 Enter,
345 Exit,
347 Move,
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
356#[repr(C)]
357pub struct AnimKey(pub u64);
358
359#[derive(Debug, Clone, Copy, PartialEq)]
361pub struct ActiveAnim {
362 pub class: AnimClass,
364 pub translate_x: AnimChannel,
366 pub translate_y: AnimChannel,
368 pub scale_x: AnimChannel,
370 pub scale_y: AnimChannel,
372 pub opacity: AnimChannel,
374}
375
376impl ActiveAnim {
377 #[must_use]
379 pub const fn move_from_flip(flip: FlipTransform, mode: InterpolationMode) -> Self {
380 Self {
381 class: AnimClass::Move,
382 translate_x: channel(flip.translate_x, 0.0, mode),
383 translate_y: channel(flip.translate_y, 0.0, mode),
384 scale_x: channel(flip.scale_x, 1.0, mode),
385 scale_y: channel(flip.scale_y, 1.0, mode),
386 opacity: channel(1.0, 1.0, mode),
387 }
388 }
389
390 #[must_use]
392 pub const fn enter_slide(from_x: f32, from_y: f32, mode: InterpolationMode) -> Self {
393 Self {
394 class: AnimClass::Enter,
395 translate_x: channel(from_x, 0.0, mode),
396 translate_y: channel(from_y, 0.0, mode),
397 scale_x: channel(1.0, 1.0, mode),
398 scale_y: channel(1.0, 1.0, mode),
399 opacity: channel(1.0, 1.0, mode),
400 }
401 }
402
403 pub fn retarget_presence(&mut self, class: AnimClass, to_x: f32, to_y: f32) {
409 self.class = class;
410 self.translate_x.retarget(to_x);
411 self.translate_y.retarget(to_y);
412 self.scale_x.retarget(1.0);
413 self.scale_y.retarget(1.0);
414 self.opacity.retarget(1.0);
415 }
416
417 #[must_use]
418 pub const fn exit_slide(to_x: f32, to_y: f32, mode: InterpolationMode) -> Self {
419 Self {
420 class: AnimClass::Exit,
421 translate_x: channel(0.0, to_x, mode),
422 translate_y: channel(0.0, to_y, mode),
423 scale_x: channel(1.0, 1.0, mode),
424 scale_y: channel(1.0, 1.0, mode),
425 opacity: channel(1.0, 1.0, mode),
426 }
427 }
428
429 pub fn tick(&mut self, dt: f32) {
431 self.translate_x.tick(dt);
432 self.translate_y.tick(dt);
433 self.scale_x.tick(dt);
434 self.scale_y.tick(dt);
435 self.opacity.tick(dt);
436 }
437
438 #[must_use]
440 pub const fn is_finished(&self) -> bool {
441 self.translate_x.is_finished()
442 && self.translate_y.is_finished()
443 && self.scale_x.is_finished()
444 && self.scale_y.is_finished()
445 && self.opacity.is_finished()
446 }
447
448 #[must_use]
450 pub const fn current_transform(&self) -> FlipTransform {
451 FlipTransform {
452 translate_x: self.translate_x.current,
453 translate_y: self.translate_y.current,
454 scale_x: self.scale_x.current,
455 scale_y: self.scale_y.current,
456 }
457 }
458
459 #[must_use]
461 pub const fn current_opacity(&self) -> f32 {
462 self.opacity.current
463 }
464
465 pub fn retarget_move(&mut self, flip: FlipTransform) {
467 self.translate_x.retarget(0.0);
469 self.translate_y.retarget(0.0);
470 self.scale_x.retarget(1.0);
471 self.scale_y.retarget(1.0);
472 self.translate_x.current += flip.translate_x;
474 self.translate_y.current += flip.translate_y;
475 }
476}
477
478const fn channel(from: f32, to: f32, mode: InterpolationMode) -> AnimChannel {
479 match mode {
480 InterpolationMode::Curve {
481 function,
482 duration_secs,
483 } => AnimChannel::curve(from, to, function, duration_secs),
484 InterpolationMode::Spring(spring) => AnimChannel::spring(from, to, spring),
485 }
486}
487
488#[derive(Debug, Clone, Default)]
492pub struct AnimationManager {
493 active: BTreeMap<AnimKey, ActiveAnim>,
494}
495
496impl AnimationManager {
497 #[must_use]
499 pub const fn new() -> Self {
500 Self {
501 active: BTreeMap::new(),
502 }
503 }
504
505 #[must_use]
507 pub fn len(&self) -> usize {
508 self.active.len()
509 }
510
511 #[must_use]
513 pub fn is_empty(&self) -> bool {
514 self.active.is_empty()
515 }
516
517 pub fn start_or_retarget_move(
519 &mut self,
520 key: AnimKey,
521 flip: FlipTransform,
522 mode: InterpolationMode,
523 ) {
524 if let Some(existing) = self.active.get_mut(&key) {
525 existing.retarget_move(flip);
526 } else {
527 self.active
528 .insert(key, ActiveAnim::move_from_flip(flip, mode));
529 }
530 }
531
532 pub fn start_enter(&mut self, key: AnimKey, from: (f32, f32), mode: InterpolationMode) {
534 self.active
535 .entry(key)
536 .or_insert_with(|| ActiveAnim::enter_slide(from.0, from.1, mode));
537 }
538
539 pub fn start_exit(&mut self, key: AnimKey, to: (f32, f32), mode: InterpolationMode) {
544 match self.active.get_mut(&key) {
545 Some(anim) => anim.retarget_presence(AnimClass::Exit, to.0, to.1),
546 None => {
547 self.active
548 .insert(key, ActiveAnim::exit_slide(to.0, to.1, mode));
549 }
550 }
551 }
552
553 pub fn get_mut(&mut self, key: AnimKey) -> Option<&mut ActiveAnim> {
555 self.active.get_mut(&key)
556 }
557
558 #[must_use]
560 pub fn get(&self, key: AnimKey) -> Option<&ActiveAnim> {
561 self.active.get(&key)
562 }
563
564 pub fn iter(&self) -> impl Iterator<Item = (AnimKey, &ActiveAnim)> {
566 self.active.iter().map(|(k, v)| (*k, v))
567 }
568
569 pub fn tick(&mut self, dt: f32) -> Vec<AnimKey> {
573 let mut finished = Vec::new();
574 for (key, anim) in &mut self.active {
575 anim.tick(dt);
576 if anim.is_finished() {
577 finished.push(*key);
578 }
579 }
580 for key in &finished {
581 self.active.remove(key);
582 }
583 finished
584 }
585
586 pub fn cancel(&mut self, key: AnimKey) -> Option<ActiveAnim> {
588 self.active.remove(&key)
589 }
590}
591
592pub fn correspondences_from_moves<F, L>(
596 node_moves: &[NodeMove],
597 new_node_data: &[NodeData],
598 new_hierarchy: &[NodeHierarchyItem],
599 first_rect: F,
600 last_rect: L,
601) -> Vec<(AnimKey, LogicalRect, LogicalRect)>
602where
603 F: Fn(NodeId) -> Option<LogicalRect>,
604 L: Fn(NodeId) -> Option<LogicalRect>,
605{
606 let mut out = Vec::new();
607 for m in node_moves {
608 let (Some(first), Some(last)) = (first_rect(m.old_node_id), last_rect(m.new_node_id))
609 else {
610 continue;
611 };
612 if m.new_node_id.index() >= new_node_data.len() {
613 continue; }
615 let key = AnimKey(calculate_reconciliation_key(
616 new_node_data,
617 new_hierarchy,
618 m.new_node_id,
619 ));
620 out.push((key, first, last));
621 }
622 out
623}
624
625#[must_use]
629pub fn anim_keys_for_moves(
630 node_moves: &[NodeMove],
631 new_node_data: &[NodeData],
632 new_hierarchy: &[NodeHierarchyItem],
633) -> Vec<(AnimKey, NodeId)> {
634 node_moves
635 .iter()
636 .filter(|m| m.new_node_id.index() < new_node_data.len())
637 .map(|m| {
638 (
639 AnimKey(calculate_reconciliation_key(
640 new_node_data,
641 new_hierarchy,
642 m.new_node_id,
643 )),
644 m.new_node_id,
645 )
646 })
647 .collect()
648}
649
650pub fn seed_moves<I>(
654 manager: &mut AnimationManager,
655 correspondences: I,
656 mode: InterpolationMode,
657) -> usize
658where
659 I: IntoIterator<Item = (AnimKey, LogicalRect, LogicalRect)>,
660{
661 let mut seeded = 0;
662 for (key, first, last) in correspondences {
663 let transform = flip(first, last);
664 if transform.is_identity() {
665 continue;
666 }
667 manager.start_or_retarget_move(key, transform, mode);
668 seeded += 1;
669 }
670 seeded
671}
672
673#[cfg(test)]
674#[path = "animation_test.rs"]
675mod animation_test;