1use crate::engine::animation::types::{Easing, StepPosition};
4use crate::engine::css::values::{CssValue, Unit};
5use crate::engine::layouter::resolve_css_color;
6use crate::engine::layouter::types::ColorScheme;
7
8pub fn apply_easing(t: f32, easing: Easing) -> f32 {
10 let t = t.clamp(0.0, 1.0);
11 match easing {
12 Easing::Linear => t,
13 Easing::Ease => cubic_bezier(0.25, 0.1, 0.25, 1.0, t),
14 Easing::EaseIn => cubic_bezier(0.42, 0.0, 1.0, 1.0, t),
15 Easing::EaseOut => cubic_bezier(0.0, 0.0, 0.58, 1.0, t),
16 Easing::EaseInOut => cubic_bezier(0.42, 0.0, 0.58, 1.0, t),
17 Easing::CubicBezier(x1, y1, x2, y2) => cubic_bezier(x1, y1, x2, y2, t),
18 Easing::Steps(steps, pos) => steps_easing(steps.max(1), pos, t),
19 }
20}
21
22fn steps_easing(steps: u32, pos: StepPosition, t: f32) -> f32 {
23 let n = steps as f32;
24 if n <= 0.0 {
25 return t;
26 }
27 let (jumps, offset): (f32, f32) = match pos {
28 StepPosition::JumpEnd => (n, 0.0),
29 StepPosition::JumpStart => (n, 1.0),
30 StepPosition::JumpNone => ((n - 1.0).max(1.0), 0.0),
31 StepPosition::JumpBoth => (n + 1.0, 1.0),
32 };
33 let step = ((t * jumps) + offset).floor().min(n);
34 let denom = match pos {
35 StepPosition::JumpNone => (n - 1.0).max(1.0),
36 StepPosition::JumpEnd => n,
37 StepPosition::JumpStart => n,
38 StepPosition::JumpBoth => n,
39 };
40 (step / denom).clamp(0.0, 1.0)
41}
42
43fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32, t: f32) -> f32 {
44 if t <= 0.0 {
45 return 0.0;
46 }
47 if t >= 1.0 {
48 return 1.0;
49 }
50 let mut u = t;
52 for _ in 0..8 {
53 let x = sample_curve(x1, x2, u) - t;
54 if x.abs() < 1e-5 {
55 break;
56 }
57 let d = sample_derivative(x1, x2, u);
58 if d.abs() < 1e-6 {
59 break;
60 }
61 u = (u - x / d).clamp(0.0, 1.0);
62 }
63 sample_curve(y1, y2, u).clamp(0.0, 1.0)
64}
65
66fn sample_curve(a: f32, b: f32, t: f32) -> f32 {
67 let mt = 1.0 - t;
69 3.0 * mt * mt * t * a + 3.0 * mt * t * t * b + t * t * t
70}
71
72fn sample_derivative(a: f32, b: f32, t: f32) -> f32 {
73 let mt = 1.0 - t;
74 3.0 * mt * mt * a + 6.0 * mt * t * (b - a) + 3.0 * t * t * (1.0 - b)
75}
76
77fn transform_function_angle(args: &[Vec<CssValue>]) -> Option<f32> {
80 let v = args.first()?.first()?;
81 match *v {
82 CssValue::Length(val, Unit::Deg) => Some(val.to_radians()),
83 CssValue::Length(val, Unit::Rad) => Some(val),
84 CssValue::Length(val, Unit::Grad) => Some(val * std::f32::consts::PI / 200.0),
85 CssValue::Length(val, Unit::Turn) => Some(val * 2.0 * std::f32::consts::PI),
86 CssValue::Number(n) => Some(n.to_radians()),
87 _ => None,
88 }
89}
90
91fn transform_function_scale(args: &[Vec<CssValue>]) -> Option<(f32, f32)> {
94 let flat: Vec<&CssValue> = args.iter().flatten().collect();
95 let sx = match flat.first()? {
96 CssValue::Number(n) => *n,
97 _ => return None,
98 };
99 let sy = match flat.get(1).copied() {
100 Some(CssValue::Number(n)) => *n,
101 _ => sx,
102 };
103 Some((sx, sy))
104}
105
106fn transform_function_translate(fn_name: &str, args: &[Vec<CssValue>]) -> Option<(f32, f32)> {
115 let flat: Vec<&CssValue> = args.iter().flatten().collect();
116 let to_px = |v: &CssValue| -> Option<f32> {
117 match v {
118 CssValue::Number(n) => Some(*n),
119 CssValue::Length(val, Unit::Px) => Some(*val),
120 _ => None,
121 }
122 };
123 let first = flat.first()?;
124 let tx_val = to_px(first)?;
125 let ty_val = match flat.get(1).copied() {
126 Some(CssValue::Number(n)) => *n,
127 Some(CssValue::Length(val, Unit::Px)) => *val,
128 _ => 0.0,
129 };
130 match fn_name {
131 "translatex" => Some((tx_val, 0.0)),
132 "translatey" => Some((0.0, tx_val)),
133 "translate" => Some((tx_val, ty_val)),
134 _ => None,
135 }
136}
137
138fn could_be_css_color(value: &CssValue) -> bool {
142 match value {
143 CssValue::Color(_) => true,
144 CssValue::Keyword(_) => true,
145 CssValue::Function(name, _) => matches!(
146 name.to_ascii_lowercase().as_str(),
147 "rgb"
148 | "rgba"
149 | "hsl"
150 | "hsla"
151 | "lab"
152 | "lch"
153 | "oklab"
154 | "oklch"
155 | "color-mix"
156 | "light-dark"
157 | "hwb"
158 | "color"
159 ),
160 _ => false,
161 }
162}
163
164fn transform_identity_like(value: &CssValue) -> Option<CssValue> {
167 match value {
168 CssValue::Function(name, _) => {
169 let identity = match name.to_ascii_lowercase().as_str() {
170 "translate" | "translatex" | "translatey" => CssValue::Function(
171 "translate".into(),
172 vec![vec![
173 CssValue::Length(0.0, Unit::Px),
174 CssValue::Length(0.0, Unit::Px),
175 ]],
176 ),
177 "scale" => CssValue::Function("scale".into(), vec![vec![CssValue::Number(1.0)]]),
178 "rotate" => CssValue::Function(
179 "rotate".into(),
180 vec![vec![CssValue::Length(0.0, Unit::Deg)]],
181 ),
182 _ => return None,
183 };
184 Some(identity)
185 }
186 CssValue::List(items) => {
187 let identities = items
188 .iter()
189 .map(transform_identity_like)
190 .collect::<Option<Vec<_>>>()?;
191 Some(CssValue::List(identities))
192 }
193 _ => None,
194 }
195}
196
197pub fn interpolate_value(
200 from: &CssValue,
201 to: &CssValue,
202 t: f32,
203 color_scheme: ColorScheme,
204) -> Option<CssValue> {
205 let t = t.clamp(0.0, 1.0);
206 if matches!(from, CssValue::Keyword(value) if value.eq_ignore_ascii_case("none"))
207 && let Some(identity) = transform_identity_like(to)
208 {
209 return interpolate_value(&identity, to, t, color_scheme);
210 }
211 if matches!(to, CssValue::Keyword(value) if value.eq_ignore_ascii_case("none"))
212 && let Some(identity) = transform_identity_like(from)
213 {
214 return interpolate_value(from, &identity, t, color_scheme);
215 }
216 match (from, to) {
217 (CssValue::Number(a), CssValue::Number(b)) => Some(CssValue::Number(a + (b - a) * t)),
218 (CssValue::Length(a, ua), CssValue::Length(b, ub)) if ua == ub => {
219 Some(CssValue::Length(a + (b - a) * t, *ua))
220 }
221 (CssValue::Length(_, Unit::Px), CssValue::Number(_))
222 | (CssValue::Number(_), CssValue::Length(_, Unit::Px)) => {
223 let av = match from {
224 CssValue::Length(v, _) => *v,
225 CssValue::Number(v) => *v,
226 _ => 0.0,
227 };
228 let bv = match to {
229 CssValue::Length(v, _) => *v,
230 CssValue::Number(v) => *v,
231 _ => 0.0,
232 };
233 Some(CssValue::Length(av + (bv - av) * t, Unit::Px))
234 }
235 (CssValue::Function(from_name, from_args), CssValue::Function(to_name, to_args)) => {
239 let f = from_name.to_ascii_lowercase();
240 let g = to_name.to_ascii_lowercase();
241 let same_group = match f.as_str() {
242 "rotate" => g == "rotate",
243 "scale" => g == "scale",
244 "translate" | "translatex" | "translatey" => {
245 matches!(g.as_str(), "translate" | "translatex" | "translatey")
246 }
247 _ => false,
248 };
249 if !same_group {
250 return None;
251 }
252 match f.as_str() {
253 "rotate" => {
254 let a0 = transform_function_angle(from_args)?;
255 let a1 = transform_function_angle(to_args)?;
256 let interp = a0 + (a1 - a0) * t;
257 Some(CssValue::Function(
258 "rotate".into(),
259 vec![vec![CssValue::Length(interp.to_degrees(), Unit::Deg)]],
260 ))
261 }
262 "scale" => {
263 let (sx0, sy0) = transform_function_scale(from_args)?;
264 let (sx1, sy1) = transform_function_scale(to_args)?;
265 let sx = sx0 + (sx1 - sx0) * t;
266 let sy = sy0 + (sy1 - sy0) * t;
267 if (sx - sy).abs() < f32::EPSILON {
268 Some(CssValue::Function(
269 "scale".into(),
270 vec![vec![CssValue::Number(sx)]],
271 ))
272 } else {
273 Some(CssValue::Function(
274 "scale".into(),
275 vec![vec![CssValue::Number(sx)], vec![CssValue::Number(sy)]],
276 ))
277 }
278 }
279 "translate" | "translatex" | "translatey" => {
280 let (tx0, ty0) = transform_function_translate(&f, from_args)?;
281 let (tx1, ty1) = transform_function_translate(&g, to_args)?;
282 let tx = tx0 + (tx1 - tx0) * t;
283 let ty = ty0 + (ty1 - ty0) * t;
284 Some(CssValue::Function(
285 "translate".into(),
286 vec![vec![
287 CssValue::Length(tx, Unit::Px),
288 CssValue::Length(ty, Unit::Px),
289 ]],
290 ))
291 }
292 _ => None,
293 }
294 }
295 (CssValue::List(from_list), CssValue::List(to_list)) => {
298 if from_list.is_empty() || from_list.len() != to_list.len() {
299 return None;
300 }
301 let mut result = Vec::with_capacity(from_list.len());
302 for (from_fn, to_fn) in from_list.iter().zip(to_list.iter()) {
303 {
304 let v = interpolate_value(from_fn, to_fn, t, color_scheme)?;
305 result.push(v)
306 }
307 }
308 Some(CssValue::List(result))
309 }
310 _ => {
311 if !could_be_css_color(from) || !could_be_css_color(to) {
316 return None;
317 }
318 if let (Some(ca), Some(cb)) = (
320 resolve_css_color("animation", from, color_scheme),
321 resolve_css_color("animation", to, color_scheme),
322 ) {
323 let a = ca.to_linear_f32_array();
324 let b = cb.to_linear_f32_array();
325 let mixed = [
326 a[0] + (b[0] - a[0]) * t,
327 a[1] + (b[1] - a[1]) * t,
328 a[2] + (b[2] - a[2]) * t,
329 a[3] + (b[3] - a[3]) * t,
330 ];
331 let c = crate::engine::layouter::types::Color::from_linear_f32_array(mixed);
332 Some(CssValue::Color(format!(
334 "{:02x}{:02x}{:02x}{:02x}",
335 c.0, c.1, c.2, c.3
336 )))
337 } else {
338 None
339 }
340 }
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn easing_linear_identity() {
350 assert!((apply_easing(0.0, Easing::Linear) - 0.0).abs() < 1e-6);
351 assert!((apply_easing(0.5, Easing::Linear) - 0.5).abs() < 1e-6);
352 assert!((apply_easing(1.0, Easing::Linear) - 1.0).abs() < 1e-6);
353 }
354
355 #[test]
356 fn easing_ease_endpoints() {
357 for e in [
358 Easing::Ease,
359 Easing::EaseIn,
360 Easing::EaseOut,
361 Easing::EaseInOut,
362 Easing::CubicBezier(0.4, 0.0, 0.2, 1.0),
363 ] {
364 assert!((apply_easing(0.0, e) - 0.0).abs() < 1e-3, "{e:?}");
365 assert!((apply_easing(1.0, e) - 1.0).abs() < 1e-3, "{e:?}");
366 }
367 }
368
369 #[test]
370 fn interpolate_numbers_and_lengths() {
371 let n = interpolate_value(
372 &CssValue::Number(0.0),
373 &CssValue::Number(10.0),
374 0.5,
375 ColorScheme::Light,
376 )
377 .unwrap();
378 assert_eq!(n, CssValue::Number(5.0));
379 let l = interpolate_value(
380 &CssValue::Length(0.0, Unit::Px),
381 &CssValue::Length(100.0, Unit::Px),
382 0.25,
383 ColorScheme::Light,
384 )
385 .unwrap();
386 assert_eq!(l, CssValue::Length(25.0, Unit::Px));
387 }
388
389 fn rotate_fn(deg: f32) -> CssValue {
390 CssValue::Function(
391 "rotate".into(),
392 vec![vec![CssValue::Length(deg, Unit::Deg)]],
393 )
394 }
395
396 fn scale_fn(sx: f32, sy: f32) -> CssValue {
397 CssValue::Function(
398 "scale".into(),
399 vec![vec![CssValue::Number(sx)], vec![CssValue::Number(sy)]],
400 )
401 }
402
403 #[test]
404 fn interpolate_rotate_degrees() {
405 let v =
406 interpolate_value(&rotate_fn(0.0), &rotate_fn(90.0), 0.5, ColorScheme::Light).unwrap();
407 match v {
408 CssValue::Function(name, args) if name == "rotate" => {
409 let CssValue::Length(deg, Unit::Deg) = args[0][0] else {
410 panic!("expected Length deg, got {args:?}")
411 };
412 assert!((deg - 45.0).abs() < 1e-4, "got {deg}");
413 }
414 other => panic!("unexpected {other:?}"),
415 }
416 }
417
418 #[test]
419 fn interpolate_rotate_wraps_turns_and_degs() {
420 let from = CssValue::Function(
422 "rotate".into(),
423 vec![vec![CssValue::Length(0.5, Unit::Turn)]],
424 );
425 let to = rotate_fn(90.0);
426 let v = interpolate_value(&from, &to, 0.5, ColorScheme::Light).unwrap();
427 match v {
428 CssValue::Function(name, args) if name == "rotate" => {
429 let CssValue::Length(deg, Unit::Deg) = args[0][0] else {
430 panic!("expected Length deg, got {args:?}")
431 };
432 assert!((deg - 135.0).abs() < 1e-4, "got {deg}");
433 }
434 other => panic!("unexpected {other:?}"),
435 }
436 }
437
438 #[test]
439 fn interpolate_scale_uniform() {
440 let v = interpolate_value(
441 &scale_fn(1.0, 1.0),
442 &scale_fn(2.0, 2.0),
443 0.25,
444 ColorScheme::Light,
445 )
446 .unwrap();
447 match v {
448 CssValue::Function(name, args) if name == "scale" => {
449 assert_eq!(args.len(), 1);
450 let CssValue::Number(s) = args[0][0] else {
451 panic!("expected Number, got {args:?}")
452 };
453 assert!((s - 1.25).abs() < 1e-4, "got {s}");
454 }
455 other => panic!("unexpected {other:?}"),
456 }
457 }
458
459 #[test]
460 fn interpolate_scale_non_uniform() {
461 let v = interpolate_value(
462 &scale_fn(1.0, 2.0),
463 &scale_fn(3.0, 4.0),
464 0.5,
465 ColorScheme::Light,
466 )
467 .unwrap();
468 match v {
469 CssValue::Function(name, args) if name == "scale" => {
470 assert_eq!(args.len(), 2);
471 let CssValue::Number(sx) = args[0][0] else {
472 panic!("expected Number, got {args:?}")
473 };
474 let CssValue::Number(sy) = args[1][0] else {
475 panic!("expected Number, got {args:?}")
476 };
477 assert!((sx - 2.0).abs() < 1e-4, "got {sx}");
478 assert!((sy - 3.0).abs() < 1e-4, "got {sy}");
479 }
480 other => panic!("unexpected {other:?}"),
481 }
482 }
483
484 #[test]
485 fn interpolate_mismatched_transform_functions_returns_none() {
486 let v = interpolate_value(
487 &rotate_fn(0.0),
488 &scale_fn(1.0, 1.0),
489 0.5,
490 ColorScheme::Light,
491 );
492 assert!(v.is_none());
493 }
494
495 #[test]
496 fn interpolate_single_arg_scale_defaults_sy() {
497 let from = CssValue::Function("scale".into(), vec![vec![CssValue::Number(1.0)]]);
498 let to = CssValue::Function("scale".into(), vec![vec![CssValue::Number(3.0)]]);
499 let v = interpolate_value(&from, &to, 0.5, ColorScheme::Light).unwrap();
500 match v {
501 CssValue::Function(name, args) if name == "scale" => {
502 assert_eq!(args.len(), 1);
503 let CssValue::Number(s) = args[0][0] else {
504 panic!("expected Number, got {args:?}")
505 };
506 assert!((s - 2.0).abs() < 1e-4, "got {s}");
507 }
508 other => panic!("unexpected {other:?}"),
509 }
510 }
511
512 fn translate_fn(tx: f32, ty: f32) -> CssValue {
513 CssValue::Function(
514 "translate".into(),
515 vec![vec![
516 CssValue::Length(tx, Unit::Px),
517 CssValue::Length(ty, Unit::Px),
518 ]],
519 )
520 }
521
522 fn translatex_fn(tx: f32) -> CssValue {
523 CssValue::Function(
524 "translateX".into(),
525 vec![vec![CssValue::Length(tx, Unit::Px)]],
526 )
527 }
528
529 fn translatey_fn(ty: f32) -> CssValue {
530 CssValue::Function(
531 "translateY".into(),
532 vec![vec![CssValue::Length(ty, Unit::Px)]],
533 )
534 }
535
536 #[test]
537 fn interpolate_translate() {
538 let v = interpolate_value(
539 &translate_fn(0.0, 0.0),
540 &translate_fn(100.0, 200.0),
541 0.25,
542 ColorScheme::Light,
543 )
544 .unwrap();
545 match v {
546 CssValue::Function(name, args) if name == "translate" => {
547 assert_eq!(args.len(), 1);
548 let CssValue::Length(x, Unit::Px) = args[0][0] else {
549 panic!("expected Length px, got {args:?}")
550 };
551 let CssValue::Length(y, Unit::Px) = args[0][1] else {
552 panic!("expected Length px, got {args:?}")
553 };
554 assert!((x - 25.0).abs() < 1e-4, "got {x}");
555 assert!((y - 50.0).abs() < 1e-4, "got {y}");
556 }
557 other => panic!("unexpected {other:?}"),
558 }
559 }
560
561 #[test]
562 fn interpolate_translatex() {
563 let v = interpolate_value(
564 &translatex_fn(0.0),
565 &translatex_fn(100.0),
566 0.5,
567 ColorScheme::Light,
568 )
569 .unwrap();
570 match v {
571 CssValue::Function(name, args) if name == "translate" => {
572 let CssValue::Length(x, Unit::Px) = args[0][0] else {
573 panic!("expected Length px, got {args:?}")
574 };
575 let CssValue::Length(y, Unit::Px) = args[0][1] else {
576 panic!("expected Length px, got {args:?}")
577 };
578 assert!((x - 50.0).abs() < 1e-4, "got {x}");
579 assert_eq!(y, 0.0);
580 }
581 other => panic!("unexpected {other:?}"),
582 }
583 }
584
585 #[test]
586 fn interpolate_translatey() {
587 let v = interpolate_value(
588 &translatey_fn(0.0),
589 &translatey_fn(100.0),
590 0.5,
591 ColorScheme::Light,
592 )
593 .unwrap();
594 match v {
595 CssValue::Function(name, args) if name == "translate" => {
596 let CssValue::Length(x, Unit::Px) = args[0][0] else {
597 panic!("expected Length px, got {args:?}")
598 };
599 let CssValue::Length(y, Unit::Px) = args[0][1] else {
600 panic!("expected Length px, got {args:?}")
601 };
602 assert_eq!(x, 0.0);
603 assert!((y - 50.0).abs() < 1e-4, "got {y}");
604 }
605 other => panic!("unexpected {other:?}"),
606 }
607 }
608
609 #[test]
610 fn interpolate_translate_family_mixed_names() {
611 let v = interpolate_value(
613 &translatex_fn(0.0),
614 &translate_fn(100.0, 200.0),
615 0.5,
616 ColorScheme::Light,
617 )
618 .unwrap();
619 match v {
620 CssValue::Function(name, args) if name == "translate" => {
621 let CssValue::Length(x, Unit::Px) = args[0][0] else {
622 panic!("expected Length px, got {args:?}")
623 };
624 let CssValue::Length(y, Unit::Px) = args[0][1] else {
625 panic!("expected Length px, got {args:?}")
626 };
627 assert!((x - 50.0).abs() < 1e-4, "got {x}");
628 assert!((y - 100.0).abs() < 1e-4, "got {y}");
629 }
630 other => panic!("unexpected {other:?}"),
631 }
632 }
633
634 #[test]
635 fn interpolate_translate_vs_scale_returns_none() {
636 let v = interpolate_value(
637 &translate_fn(0.0, 0.0),
638 &CssValue::Function("scale".into(), vec![vec![CssValue::Number(2.0)]]),
639 0.5,
640 ColorScheme::Light,
641 );
642 assert!(v.is_none());
643 }
644
645 fn transform_list(functions: Vec<CssValue>) -> CssValue {
646 CssValue::List(functions)
647 }
648
649 #[test]
650 fn interpolate_transform_list_element_wise() {
651 let from = transform_list(vec![
652 translatex_fn(0.0),
653 CssValue::Function("scale".into(), vec![vec![CssValue::Number(1.0)]]),
654 ]);
655 let to = transform_list(vec![
656 translatex_fn(100.0),
657 CssValue::Function("scale".into(), vec![vec![CssValue::Number(3.0)]]),
658 ]);
659 let v = interpolate_value(&from, &to, 0.5, ColorScheme::Light).unwrap();
660 match v {
661 CssValue::List(items) => {
662 assert_eq!(items.len(), 2);
663 match &items[0] {
664 CssValue::Function(name, args) if name == "translate" => {
665 let CssValue::Length(x, Unit::Px) = args[0][0] else {
666 panic!("expected Length px, got {args:?}")
667 };
668 assert!((x - 50.0).abs() < 1e-4, "got {x}");
669 }
670 other => panic!("expected translate function, got {other:?}"),
671 }
672 match &items[1] {
673 CssValue::Function(name, args) if name == "scale" => {
674 let CssValue::Number(s) = args[0][0] else {
675 panic!("expected Number, got {args:?}")
676 };
677 assert!((s - 2.0).abs() < 1e-4, "got {s}");
678 }
679 other => panic!("expected scale function, got {other:?}"),
680 }
681 }
682 other => panic!("expected List, got {other:?}"),
683 }
684 }
685
686 #[test]
687 fn interpolate_transform_list_mismatched_length_returns_none() {
688 let from = transform_list(vec![translatex_fn(0.0)]);
689 let to = transform_list(vec![translatex_fn(0.0), translatex_fn(100.0)]);
690 let v = interpolate_value(&from, &to, 0.5, ColorScheme::Light);
691 assert!(v.is_none());
692 }
693
694 #[test]
695 fn interpolate_transform_list_non_color_values_do_not_log_error() {
696 let from = CssValue::Keyword("red".into());
699 let to = transform_list(vec![translatex_fn(0.0), translatex_fn(100.0)]);
700 let v = interpolate_value(&from, &to, 0.5, ColorScheme::Light);
701 assert!(v.is_none());
702 }
703
704 #[test]
705 fn interpolate_empty_transform_list_returns_none() {
706 let from = CssValue::List(vec![]);
707 let to = CssValue::List(vec![]);
708 let v = interpolate_value(&from, &to, 0.5, ColorScheme::Light);
709 assert!(v.is_none());
710 }
711
712 #[test]
713 fn steps_easing_jump_end() {
714 assert!((steps_easing(4, StepPosition::JumpEnd, 0.0) - 0.0).abs() < 1e-6);
716 assert!((steps_easing(4, StepPosition::JumpEnd, 0.25) - 0.25).abs() < 1e-6);
717 assert!((steps_easing(4, StepPosition::JumpEnd, 0.5) - 0.5).abs() < 1e-6);
718 assert!((steps_easing(4, StepPosition::JumpEnd, 0.75) - 0.75).abs() < 1e-6);
719 assert!((steps_easing(4, StepPosition::JumpEnd, 1.0) - 1.0).abs() < 1e-6);
720 assert!((steps_easing(4, StepPosition::JumpEnd, 0.1) - 0.0).abs() < 1e-6);
722 assert!((steps_easing(4, StepPosition::JumpEnd, 0.3) - 0.25).abs() < 1e-6);
723 }
724
725 #[test]
726 fn steps_easing_jump_start() {
727 assert!((steps_easing(4, StepPosition::JumpStart, 0.0) - 0.25).abs() < 1e-6);
729 assert!((steps_easing(4, StepPosition::JumpStart, 0.25) - 0.5).abs() < 1e-6);
730 assert!((steps_easing(4, StepPosition::JumpStart, 0.5) - 0.75).abs() < 1e-6);
731 assert!((steps_easing(4, StepPosition::JumpStart, 1.0) - 1.0).abs() < 1e-6);
732 }
733
734 #[test]
735 fn steps_easing_jump_none() {
736 assert!((steps_easing(4, StepPosition::JumpNone, 0.0) - 0.0).abs() < 1e-6);
738 assert!((steps_easing(4, StepPosition::JumpNone, 0.25) - 0.0).abs() < 1e-6);
739 assert!((steps_easing(4, StepPosition::JumpNone, 0.5) - 1.0 / 3.0).abs() < 1e-6);
740 assert!((steps_easing(4, StepPosition::JumpNone, 1.0) - 1.0).abs() < 1e-6);
741 }
742
743 #[test]
744 fn steps_easing_jump_both() {
745 assert!((steps_easing(4, StepPosition::JumpBoth, 0.0) - 0.25).abs() < 1e-6);
747 assert!((steps_easing(4, StepPosition::JumpBoth, 0.25) - 0.5).abs() < 1e-6);
748 assert!((steps_easing(4, StepPosition::JumpBoth, 0.5) - 0.75).abs() < 1e-6);
749 assert!((steps_easing(4, StepPosition::JumpBoth, 1.0) - 1.0).abs() < 1e-6);
750 }
751
752 #[test]
753 fn steps_easing_apply_easing_integration() {
754 let easing = Easing::Steps(4, StepPosition::JumpEnd);
756 assert!((apply_easing(0.0, easing) - 0.0).abs() < 1e-6);
757 assert!((apply_easing(0.25, easing) - 0.25).abs() < 1e-6);
758 assert!((apply_easing(0.5, easing) - 0.5).abs() < 1e-6);
759 assert!((apply_easing(0.75, easing) - 0.75).abs() < 1e-6);
760 assert!((apply_easing(1.0, easing) - 1.0).abs() < 1e-6);
761 }
762}