The widget half of the widget catalogue's later work, ported onto makepad-widgets-core and the family crates. - libs/tween: a dependency-free tween and timeline engine -- nested timelines with positions and labels, stagger, repeat and yoyo, seek and reverse, time scale, a ticker with lag smoothing, a global pause and reduced motion, colour-space interpolation, keyframes and motion paths. The core binds it to the widget tree (tween, a widget's TweenHost), gives script a mod.tween (tween_script) and a live inspector (tween_inspect, tween_inspector). The Animator's Ease now answers from the engine's easing, so the curves have one source; the Animator keeps its states, its keyframe timelines and its own clock. - libs/gizmo and a Gizmo3d / ViewCube widget. - core: the ease editor, the sequencer, diagonal headings, hotkeys, the hotkey editor and keyboard map, the overlay layer host, a conceding row and the width a widget would take without drawing; a theme mix and a person's own theme edits that survive a style reload (theme_mix, set_theme_edit), both taken off by set_base_theme. - the surface material: buttons, check boxes, toggles, sliders and knobs carry the moulded material behind one uniform, a press idiom per sign of material_press, and four sheets that raise it: neumorphic, molded, glossy and milled. - the sheets: a row of fields is one height under every sheet. A sheet line that names a widget the app did not register (a family left out) is skipped when the sheet installs, so the date fields a sheet sizes need the dates family only where it is linked. - extras: the gradient editor, registered with the colour picker it builds on. - tweaker: the designer (a widget's literal located and edited in its source as hunks), the theme lab and the theme builder. The window reads the docked panel's band through the tweaker's hooks. - the panel no longer wears the app's theme (the design surface keeps its own palette), so the worn-theme tests go with it. Five theme builder tests fail here as they do on the branch this comes from (every_built_theme_reads and four more on text contrast); radial_menu's tab_under_an_open_menu_finds_nowhere_to_go fails here as it does on dev.
1083 lines
36 KiB
Rust
1083 lines
36 KiB
Rust
//! Timing (design 12.3): durations, iteration maths, repeats, yoyo, time
|
|
//! scales, stepping, landing; replayed against the GSAP 3.15 sample and time
|
|
//! scale goldens (tests/golden/samples.rs, timescale.rs) and the dt-stepping
|
|
//! trace. Times compare within 1e-9, values within 5e-7 (GSAP rounds
|
|
//! written values to 1e-6).
|
|
|
|
mod util;
|
|
|
|
#[path = "golden"]
|
|
mod golden {
|
|
pub mod callbacks;
|
|
pub mod common;
|
|
pub mod samples;
|
|
pub mod timescale;
|
|
}
|
|
|
|
use golden::common::{column, flag, num, text, Pairs, Series};
|
|
use golden::samples::{SampleCase, CASES};
|
|
use makepad_tween::*;
|
|
use util::*;
|
|
|
|
fn sample_case(id: &str) -> &'static SampleCase {
|
|
CASES
|
|
.iter()
|
|
.find(|c| c.id == id)
|
|
.unwrap_or_else(|| panic!("no sample case {id}"))
|
|
}
|
|
|
|
fn ease(name: &str) -> Easing {
|
|
parse_gsap_ease(name).unwrap_or_else(|| panic!("ease {name}"))
|
|
}
|
|
|
|
/// What a series column reads.
|
|
fn read(e: &TweenEngine, anim: TweenId, name: &str) -> f64 {
|
|
let r = e.anim_ref(anim);
|
|
match name {
|
|
"x" => val(e, 0, X),
|
|
"y" => val(e, 0, Y),
|
|
"anim.time" | "tl.time" => r.time(),
|
|
"anim.iteration" => r.iteration() as f64,
|
|
"anim.totalTime" => r.total_time(),
|
|
"anim.progress" => r.progress(),
|
|
"anim.totalProgress" => r.total_progress(),
|
|
other => {
|
|
// "o3.x"
|
|
let (o, p) = other.split_once('.').unwrap();
|
|
let i: u32 = o[1..].parse().unwrap();
|
|
val(e, i, if p == "x" { X } else { Y })
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Drives `anim` over the case's `t` column with `drive`, comparing every
|
|
/// series (times 1e-9, values 5e-7).
|
|
fn check_series(
|
|
e: &mut TweenEngine,
|
|
anim: TweenId,
|
|
c: &SampleCase,
|
|
mut drive: impl FnMut(&mut TweenEngine, f64),
|
|
) {
|
|
for (i, t) in c.t.iter().enumerate() {
|
|
drive(e, *t);
|
|
for s in c.series {
|
|
let want = s.v[i];
|
|
let got = read(e, anim, s.name);
|
|
let tol = if s.name.contains("time")
|
|
|| s.name.contains("rogress")
|
|
|| s.name.contains("iteration")
|
|
{
|
|
TIME_TOL
|
|
} else {
|
|
VAL_TOL
|
|
};
|
|
close(got, want, tol, &format!("{}: {} at t={t}", c.id, s.name));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn total_time(e: &mut TweenEngine, a: TweenId, t: f64) {
|
|
e.anim(a).set_total_time(t, Emit::Fire);
|
|
}
|
|
|
|
fn seed_xy(e: &mut TweenEngine, x: f64, y: f64) {
|
|
e.seed(tg(0), X, TweenValue::F64(x));
|
|
e.seed(tg(0), Y, TweenValue::F64(y));
|
|
}
|
|
|
|
/// A paused root-level tween of x and y on target 0.
|
|
fn tween_xy(e: &mut TweenEngine, x: f64, y: f64, o: TweenOpts) -> TweenId {
|
|
e.to(one(0), &[to(X, x), to(Y, y)], o.paused(true))
|
|
}
|
|
|
|
#[test]
|
|
fn golden_tween_samples_match_gsap() {
|
|
// to() power2.inOut.
|
|
let c = sample_case("to_power2_inout");
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 10.0);
|
|
let t = tween_xy(
|
|
&mut e,
|
|
100.0,
|
|
-50.0,
|
|
TweenOpts::new().duration(1.0).ease(ease("power2.inOut")),
|
|
);
|
|
assert_eq!(e.anim_ref(t).total_duration(), num(c.meta, "totalDuration"));
|
|
check_series(&mut e, t, c, |e, s| total_time(e, t, s));
|
|
|
|
// from() and fromTo(): immediate render.
|
|
let c = sample_case("from_power2_inout");
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 10.0);
|
|
let t = e.from(
|
|
one(0),
|
|
&[PropTo::from(X, 100.0.into()), PropTo::from(Y, 60.0.into())],
|
|
TweenOpts::new()
|
|
.duration(1.0)
|
|
.ease(ease("power2.inOut"))
|
|
.paused(true),
|
|
);
|
|
assert_eq!(val(&e, 0, X), num(c.meta, "immediate_after_create.x"));
|
|
assert_eq!(val(&e, 0, Y), num(c.meta, "immediate_after_create.y"));
|
|
check_series(&mut e, t, c, |e, s| total_time(e, t, s));
|
|
|
|
let c = sample_case("from_immediate_render_false");
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 10.0);
|
|
let t = e.from(
|
|
one(0),
|
|
&[PropTo::from(X, 100.0.into()), PropTo::from(Y, 60.0.into())],
|
|
TweenOpts::new()
|
|
.duration(1.0)
|
|
.ease(ease("power2.inOut"))
|
|
.paused(true)
|
|
.immediate_render(false),
|
|
);
|
|
assert_eq!(val(&e, 0, X), num(c.meta, "immediate_after_create.x"));
|
|
assert_eq!(val(&e, 0, Y), num(c.meta, "immediate_after_create.y"));
|
|
check_series(&mut e, t, c, |e, s| total_time(e, t, s));
|
|
|
|
let c = sample_case("from_unpaused_immediate_only");
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 10.0);
|
|
e.from(
|
|
one(0),
|
|
&[PropTo::from(X, 100.0.into()), PropTo::from(Y, 60.0.into())],
|
|
TweenOpts::new().duration(1.0).ease(ease("power2.inOut")),
|
|
);
|
|
assert_eq!(val(&e, 0, X), num(c.meta, "immediate_after_create.x"));
|
|
assert_eq!(val(&e, 0, Y), num(c.meta, "immediate_after_create.y"));
|
|
|
|
let c = sample_case("fromto_sine_inout");
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 0.0);
|
|
let t = e.from_to(
|
|
one(0),
|
|
&[
|
|
PropTo::from_to(X, (-20.0).into(), 80.0.into()),
|
|
PropTo::from_to(Y, 5.0.into(), 25.0.into()),
|
|
],
|
|
TweenOpts::new()
|
|
.duration(1.5)
|
|
.ease(ease("sine.inOut"))
|
|
.paused(true),
|
|
);
|
|
assert_eq!(val(&e, 0, X), num(c.meta, "immediate_after_create.x"));
|
|
assert_eq!(val(&e, 0, Y), num(c.meta, "immediate_after_create.y"));
|
|
check_series(&mut e, t, c, |e, s| total_time(e, t, s));
|
|
|
|
// Repeats and delays.
|
|
for (id, o) in [
|
|
(
|
|
"repeat2_yoyo_rdelay03",
|
|
TweenOpts::new()
|
|
.duration(1.0)
|
|
.repeat(2)
|
|
.yoyo(true)
|
|
.repeat_delay(0.3)
|
|
.ease(ease("power1.in")),
|
|
),
|
|
(
|
|
"repeat_infinite",
|
|
TweenOpts::new()
|
|
.duration(1.0)
|
|
.repeat(-1)
|
|
.ease(ease("power1.inOut")),
|
|
),
|
|
(
|
|
"delay05_standalone",
|
|
TweenOpts::new()
|
|
.duration(1.0)
|
|
.delay(0.5)
|
|
.ease(Easing::Linear),
|
|
),
|
|
("default_ease_power1_out", TweenOpts::new().duration(1.0)),
|
|
("default_duration", TweenOpts::new()),
|
|
] {
|
|
let c = sample_case(id);
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 0.0);
|
|
let t = tween_xy(&mut e, 100.0, 50.0, o);
|
|
if let Some(golden::common::Val::F(d)) = golden::common::val(c.meta, "totalDuration") {
|
|
assert_eq!(e.anim_ref(t).total_duration(), d, "{id}");
|
|
}
|
|
if let Some(golden::common::Val::F(d)) = golden::common::val(c.meta, "duration") {
|
|
assert_eq!(e.anim_ref(t).duration(), d, "{id}");
|
|
}
|
|
check_series(&mut e, t, c, |e, s| total_time(e, t, s));
|
|
}
|
|
let c = sample_case("delay05_standalone");
|
|
let mut e = TweenEngine::new();
|
|
let t = e.to(one(0), &[to(X, 1.0)], lin(1.0).delay(0.5).paused(true));
|
|
assert_eq!(e.anim_ref(t).start_time(), num(c.meta, "startTime_on_root"));
|
|
assert_eq!(e.anim_ref(t).delay(), num(c.meta, "delay"));
|
|
|
|
// Inside a timeline, and timeline defaults.
|
|
let c = sample_case("delay05_in_timeline");
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 0.0);
|
|
let tl = e.timeline(TimelineOpts::new().paused(true));
|
|
e.tl(tl).to(
|
|
one(0),
|
|
&[to(X, 100.0), to(Y, 50.0)],
|
|
lin(1.0).delay(0.5),
|
|
Position::END,
|
|
);
|
|
let child = e.tl(tl).last();
|
|
assert_eq!(
|
|
e.anim_ref(child).start_time(),
|
|
num(c.meta, "child_startTime")
|
|
);
|
|
assert_eq!(e.anim_ref(tl).duration(), num(c.meta, "timeline_duration"));
|
|
check_series(&mut e, tl, c, |e, s| {
|
|
e.anim(tl).seek(Seek::Time(s), Emit::Suppress);
|
|
});
|
|
|
|
let c = sample_case("timeline_defaults_ease");
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 0.0);
|
|
let tl = e.timeline(
|
|
TimelineOpts::new()
|
|
.paused(true)
|
|
.defaults(TweenOpts::new().ease(ease("power2.inOut")).duration(1.0)),
|
|
);
|
|
e.tl(tl)
|
|
.to(one(0), &[to(X, 100.0)], TweenOpts::new(), Position::END)
|
|
.to(
|
|
one(0),
|
|
&[to(Y, 100.0)],
|
|
TweenOpts::new().ease(Easing::Linear),
|
|
Position::END,
|
|
);
|
|
assert_eq!(e.anim_ref(tl).duration(), num(c.meta, "timeline_duration"));
|
|
check_series(&mut e, tl, c, |e, s| {
|
|
e.anim(tl).seek(Seek::Time(s), Emit::Suppress);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn golden_time_snapping_and_rounding() {
|
|
let c = sample_case("rounding_probes");
|
|
let n = num(c.meta, "probes.len") as usize;
|
|
for i in 0..n {
|
|
let k = |f: &str| format!("probes.{i}.{f}");
|
|
let kind = text(c.meta, &k("probe")).unwrap();
|
|
let t_in = num(c.meta, &k("t_in"));
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let anim = match kind {
|
|
"plain_tween" => e.to(one(0), &[to(X, 100.0)], lin(1.0).paused(true)),
|
|
"repeating_tween" => e.to(one(0), &[to(X, 100.0)], lin(1.0).repeat(1).paused(true)),
|
|
"tween_in_timeline" => {
|
|
let tl = e.timeline(TimelineOpts::new().paused(true));
|
|
e.tl(tl)
|
|
.to(one(0), &[to(X, 100.0)], lin(1.0), Position::END);
|
|
tl
|
|
}
|
|
other => panic!("{other}"),
|
|
};
|
|
e.anim(anim).set_total_time(t_in, Emit::Fire);
|
|
let ctx = format!("{kind} totalTime({t_in})");
|
|
close(val(&e, 0, X), num(c.meta, &k("x")), VAL_TOL, &ctx);
|
|
close(
|
|
e.anim_ref(anim).time(),
|
|
num(c.meta, &k("time")),
|
|
1e-12,
|
|
&ctx,
|
|
);
|
|
close(
|
|
e.anim_ref(anim).total_time(),
|
|
num(c.meta, &k("totalTime")),
|
|
1e-12,
|
|
&ctx,
|
|
);
|
|
}
|
|
// GSAP rounds written values to 1e-6; the engine keeps full precision
|
|
// (deviation) [golden: design_value_rounding].
|
|
let c = sample_case("design_value_rounding");
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let t = e.to(one(0), &[to(X, 100.0)], lin(1.0).paused(true));
|
|
check_series(&mut e, t, c, |e, s| total_time(e, t, s));
|
|
e.anim(t).set_total_time(0.123456789, Emit::Fire);
|
|
assert_eq!(val(&e, 0, X), 12.3456789, "not rounded");
|
|
}
|
|
|
|
#[test]
|
|
fn yoyo_repeat_delay_table() {
|
|
// T4 [golden: design_t4_yoyo_repeat_delay_table].
|
|
let c = sample_case("design_t4_yoyo_repeat_delay_table");
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 0.0);
|
|
let t = e.to(
|
|
one(0),
|
|
&[to(X, 100.0)],
|
|
lin(1.0).repeat(2).yoyo(true).repeat_delay(0.5).paused(true),
|
|
);
|
|
assert_eq!(e.anim_ref(t).total_duration(), num(c.meta, "totalDuration"));
|
|
check_series(&mut e, t, c, |e, s| total_time(e, t, s));
|
|
e.anim(t).seek(Seek::Time(1.75), Emit::Suppress);
|
|
let r = e.anim_ref(t);
|
|
assert_eq!(
|
|
(r.progress(), r.total_progress(), r.iteration()),
|
|
(0.75, 0.4375, 2)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn exact_cycle_boundary_belongs_to_the_ending_iteration() {
|
|
for id in ["design_t4b_exact_cycle_boundary"] {
|
|
let c = sample_case(id);
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 0.0);
|
|
let t = e.to(one(0), &[to(X, 100.0)], lin(1.0).repeat(2).paused(true));
|
|
check_series(&mut e, t, c, |e, s| total_time(e, t, s));
|
|
}
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let t = e.to(one(0), &[to(X, 100.0)], lin(1.0).repeat(2).paused(true));
|
|
e.anim(t).seek(Seek::Time(1.0), Emit::Suppress);
|
|
assert_eq!(val(&e, 0, X), 100.0);
|
|
assert_eq!(e.anim_ref(t).progress(), 1.0);
|
|
assert_eq!(e.anim_ref(t).iteration(), 1);
|
|
}
|
|
|
|
fn yoyo_tween(
|
|
e: &mut TweenEngine,
|
|
ease_: Easing,
|
|
yoyo_ease: Option<YoyoEase>,
|
|
yoyo: bool,
|
|
) -> TweenId {
|
|
let mut o = TweenOpts::new()
|
|
.duration(1.0)
|
|
.repeat(1)
|
|
.ease(ease_)
|
|
.paused(true);
|
|
if yoyo {
|
|
o = o.yoyo(true);
|
|
}
|
|
if let Some(y) = yoyo_ease {
|
|
o = o.yoyo_ease(y);
|
|
}
|
|
e.to(one(0), &[to(X, 100.0), to(Y, 50.0)], o)
|
|
}
|
|
|
|
/// The design_* yoyo cases tween x only.
|
|
fn yoyo_tween_x(
|
|
e: &mut TweenEngine,
|
|
ease_: Easing,
|
|
yoyo_ease: Option<YoyoEase>,
|
|
yoyo: bool,
|
|
) -> TweenId {
|
|
let mut o = TweenOpts::new()
|
|
.duration(1.0)
|
|
.repeat(1)
|
|
.ease(ease_)
|
|
.paused(true);
|
|
if yoyo {
|
|
o = o.yoyo(true);
|
|
}
|
|
if let Some(y) = yoyo_ease {
|
|
o = o.yoyo_ease(y);
|
|
}
|
|
e.to(one(0), &[to(X, 100.0)], o)
|
|
}
|
|
|
|
#[test]
|
|
fn yoyo_ease_invert_uses_the_opposite_curve() {
|
|
let out = Easing::OutQuad;
|
|
for (id, e_, y, yoyo) in [
|
|
(
|
|
"design_yoyo_ease_true_power1_out_step_0_05",
|
|
out,
|
|
Some(YoyoEase::Invert),
|
|
true,
|
|
),
|
|
("design_yoyo_plain_power1_out", out, None, true),
|
|
(
|
|
"design_yoyo_ease_implies_yoyo",
|
|
Easing::Linear,
|
|
Some(YoyoEase::Invert),
|
|
false,
|
|
),
|
|
(
|
|
"yoyoEase_power1_out_step_0_05",
|
|
Easing::Linear,
|
|
Some(YoyoEase::Ease(Easing::OutQuad)),
|
|
true,
|
|
),
|
|
(
|
|
"yoyoEase_true",
|
|
Easing::InQuad,
|
|
Some(YoyoEase::Invert),
|
|
true,
|
|
),
|
|
("yoyo_plain_power1_in", Easing::InQuad, None, true),
|
|
] {
|
|
let c = sample_case(id);
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 0.0);
|
|
let t = if id.starts_with("design_") {
|
|
yoyo_tween_x(&mut e, e_, y, yoyo)
|
|
} else {
|
|
yoyo_tween(&mut e, e_, y, yoyo)
|
|
};
|
|
if id == "design_yoyo_ease_implies_yoyo" {
|
|
assert_eq!(e.anim_ref(t).yoyo(), flag(c.meta, "yoyo_getter"));
|
|
}
|
|
check_series(&mut e, t, c, |e, s| total_time(e, t, s));
|
|
}
|
|
// The design's spot values.
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let t = yoyo_tween(&mut e, out, Some(YoyoEase::Invert), true);
|
|
e.anim(t).set_total_time(1.25, Emit::Fire);
|
|
close(val(&e, 0, X), 56.25, 1e-9, "Invert");
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let t = yoyo_tween(&mut e, out, None, true);
|
|
e.anim(t).set_total_time(1.25, Emit::Fire);
|
|
close(val(&e, 0, X), 93.75, 1e-9, "plain");
|
|
}
|
|
|
|
#[test]
|
|
fn yoyo_ease_deviate_is_path_independent() {
|
|
// GSAP 3.15 re-anchors the reverse ease wherever the local time changes
|
|
// direction; the engine's yoyo ease is a pure function of local time.
|
|
// Where 3.15 flips exactly at the iteration end the two agree (above);
|
|
// a fresh jump or a flip inside a frame differs.
|
|
let rule = |p: f64| 100.0 * (1.0 - Easing::OutQuad.map(1.0 - p));
|
|
for id in [
|
|
"design_yoyo_ease_true_power1_out_fresh",
|
|
"yoyoEase_power1_out_fresh_instance",
|
|
] {
|
|
let c = sample_case(id);
|
|
let lin_forward = id.starts_with("yoyoEase");
|
|
let mut differs = 0;
|
|
for (i, t) in c.t.iter().enumerate() {
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 0.0);
|
|
let (ease_, y) = if lin_forward {
|
|
(Easing::Linear, YoyoEase::Ease(Easing::OutQuad))
|
|
} else {
|
|
(Easing::OutQuad, YoyoEase::Invert)
|
|
};
|
|
let tw = yoyo_tween(&mut e, ease_, Some(y), true);
|
|
e.anim(tw).set_total_time(*t, Emit::Fire);
|
|
let got = val(&e, 0, X);
|
|
let want = if *t <= 1.0 {
|
|
100.0 * ease_.map(*t)
|
|
} else {
|
|
rule(2.0 - t)
|
|
};
|
|
close(got, want, 1e-9, &format!("{id} t={t}"));
|
|
if (got - column(c.series, "x")[i]).abs() > 1e-6 {
|
|
differs += 1;
|
|
}
|
|
}
|
|
assert!(differs > 0, "{id}: GSAP 3.15 differs on the return half");
|
|
}
|
|
let c = sample_case("yoyoEase_power1_out_step_0_03");
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 0.0);
|
|
let tw = yoyo_tween(
|
|
&mut e,
|
|
Easing::Linear,
|
|
Some(YoyoEase::Ease(Easing::OutQuad)),
|
|
true,
|
|
);
|
|
let mut differs = 0;
|
|
for (i, t) in c.t.iter().enumerate() {
|
|
e.anim(tw).set_total_time(*t, Emit::Fire);
|
|
let want = if *t <= 1.0 { 100.0 * t } else { rule(2.0 - t) };
|
|
close(val(&e, 0, X), want, 1e-9, &format!("step 0.03 t={t}"));
|
|
if (val(&e, 0, X) - column(c.series, "x")[i]).abs() > 1e-6 {
|
|
differs += 1;
|
|
}
|
|
}
|
|
assert!(differs > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn golden_repeat_refresh_renders_the_boundary_first() {
|
|
for id in [
|
|
"design_repeat_refresh_relative_step",
|
|
"design_repeat_refresh_relative_jump",
|
|
] {
|
|
let c = sample_case(id);
|
|
let mut e = TweenEngine::new();
|
|
seed_xy(&mut e, 0.0, 0.0);
|
|
let t = e.to(
|
|
one(0),
|
|
&[PropTo::by(X, 100.0.into())],
|
|
lin(1.0).repeat(2).repeat_refresh(true).paused(true),
|
|
);
|
|
check_series(&mut e, t, c, |e, s| total_time(e, t, s));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn total_duration_formula() {
|
|
let mut e = TweenEngine::new();
|
|
let mk = |e: &mut TweenEngine, o: TweenOpts| e.to(one(0), &[to(X, 1.0)], o.paused(true));
|
|
let a = mk(&mut e, lin(1.0));
|
|
let b = mk(&mut e, lin(1.0).repeat(2).repeat_delay(0.5));
|
|
let c = mk(&mut e, lin(1.0).repeat(-1));
|
|
let d = mk(&mut e, lin(0.0).repeat(3));
|
|
assert_eq!(e.anim_ref(a).total_duration(), 1.0);
|
|
assert_eq!(e.anim_ref(b).total_duration(), 4.0);
|
|
assert_eq!(e.anim_ref(c).total_duration(), INFINITE);
|
|
assert_eq!(e.anim_ref(d).total_duration(), 0.0);
|
|
}
|
|
|
|
/// R.totalTime(R.totalTime() + dt) in the goldens is the engine root's advance.
|
|
#[test]
|
|
fn golden_repeat_count_is_exact_at_any_dt() {
|
|
let tr = golden::callbacks::TRACES
|
|
.iter()
|
|
.find(|t| t.id == "design_dt_stepping_repeat_counts")
|
|
.unwrap();
|
|
for op in tr.ops {
|
|
let label = op.op;
|
|
let mut o = lin(1.0).events(EventMask::EDGES.with(EventMask::START));
|
|
let (repeat, rd, yoyo) = if label.contains("T4") {
|
|
(2, 0.5, true)
|
|
} else if label.contains("repeat -1") {
|
|
(-1, 0.0, false)
|
|
} else {
|
|
(3, 0.25, label.contains("yoyo"))
|
|
};
|
|
o = o.repeat(repeat).repeat_delay(rd).yoyo(yoyo);
|
|
let dt_of = |k: usize| -> f64 {
|
|
if label.contains("dt_k") {
|
|
(((k as u64 * 7919) % 97 + 1) as f64) / 500.0
|
|
} else if label.contains("dt 1/60") {
|
|
1.0 / 60.0
|
|
} else if label.contains("dt 1/7") {
|
|
1.0 / 7.0
|
|
} else {
|
|
0.37
|
|
}
|
|
};
|
|
let mut e = TweenEngine::new();
|
|
let mut names = Names::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
// A long filler keeps the root busy like the golden's R.
|
|
e.to(one(1), &[to(X, 1.0)], lin(1000.0));
|
|
let tw = e.to(one(0), &[to(X, 100.0)], o.keep(true));
|
|
names.name(tw, "tw");
|
|
let frames = num(op.values, "frames") as usize;
|
|
let mut fired = Vec::new();
|
|
let mut times = Vec::new();
|
|
// The fixture's root emulation R rounds its total time every step
|
|
// (`R.totalTime(R.totalTime() + dt)`); the engine root integrates
|
|
// `dt` unrounded like GSAP's real root, so feed R's own times.
|
|
let mut r = 0.0;
|
|
for k in 0..frames {
|
|
let next = round7(r + dt_of(k));
|
|
e.advance(next - r);
|
|
r = next;
|
|
for ev in names.raw(&mut e) {
|
|
if ev.kind != EventKind::Update {
|
|
fired.push(names.fmt(&ev));
|
|
times.push(ev.total_time);
|
|
}
|
|
}
|
|
}
|
|
assert_eq!(fired, op.fired, "{label}");
|
|
for (got, want) in times.iter().zip(op.fired_detail) {
|
|
close(
|
|
*got,
|
|
want.total_time,
|
|
1e-9,
|
|
&format!("{label}: {} total time", want.cb),
|
|
);
|
|
}
|
|
close(val(&e, 0, X), num(op.values, "x"), VAL_TOL, label);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_big_dt_crossing_a_child_fires_start_and_complete_and_lands_it() {
|
|
let mut e = TweenEngine::new();
|
|
let mut names = Names::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let tl = e.timeline(TimelineOpts::new());
|
|
e.tl(tl)
|
|
.to(one(0), &[to(X, 100.0)], lin(0.5).events(CBS), 0.5);
|
|
let c = e.tl(tl).last();
|
|
names.name(c, "c");
|
|
e.advance(2.0);
|
|
assert_eq!(names.drain(&mut e).1, ["c:onStart", "c:onComplete"]);
|
|
assert_eq!(val(&e, 0, X).to_bits(), 100.0f64.to_bits());
|
|
}
|
|
|
|
#[test]
|
|
fn infinite_repeat_never_completes() {
|
|
let mut e = TweenEngine::new();
|
|
let mut names = Names::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let t = e.to(one(0), &[to(X, 100.0)], lin(1.0).repeat(-1).events(CBS));
|
|
names.name(t, "t");
|
|
for _ in 0..1000 {
|
|
e.advance(1.0 / 60.0);
|
|
}
|
|
let (_, fired) = names.drain(&mut e);
|
|
assert!(!fired.iter().any(|s| s.ends_with("onComplete")));
|
|
assert_eq!(fired.iter().filter(|s| s.ends_with("onRepeat")).count(), 16);
|
|
assert!(e.is_active());
|
|
assert!(e.anim_ref(t).is_active());
|
|
}
|
|
|
|
#[test]
|
|
fn golden_timescale_children_match_gsap() {
|
|
for c in golden::timescale::CASES.iter().filter(|c| !c.t.is_empty()) {
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let outer = e.timeline(TimelineOpts::new().paused(true));
|
|
let inner = match c.id {
|
|
"tween_child_timescale_2" | "tween_child_reversed" | "negative_timescale_child" => {
|
|
let tw = e.to(one(0), &[to(X, 100.0)], lin(1.0));
|
|
if c.id == "tween_child_timescale_2" {
|
|
// The generator also adds a 1 s filler (not in gsap_calls).
|
|
e.tl(outer).to(one(9), &[to(X, 1.0)], lin(1.0), 0.0);
|
|
}
|
|
match c.id {
|
|
"tween_child_timescale_2" => {
|
|
e.anim(tw).set_time_scale(2.0);
|
|
}
|
|
"tween_child_reversed" => {
|
|
e.anim(tw).set_reversed(true);
|
|
}
|
|
_ => {}
|
|
}
|
|
e.tl(outer).add(tw, 0.0);
|
|
if c.id == "negative_timescale_child" {
|
|
e.anim(tw).set_time_scale(-1.0);
|
|
}
|
|
tw
|
|
}
|
|
"three_levels_outer_ts2_inner_ts0_5" => {
|
|
let mid = e.timeline(TimelineOpts::new());
|
|
let inner = e.timeline(TimelineOpts::new());
|
|
e.tl(inner)
|
|
.to(one(0), &[to(X, 100.0)], lin(2.0), Position::END);
|
|
e.anim(inner).set_time_scale(0.5);
|
|
e.tl(mid).add(inner, 0.0);
|
|
e.anim(mid).set_time_scale(2.0);
|
|
e.tl(outer).add(mid, 0.0);
|
|
assert_eq!(e.anim_ref(mid).start_time(), num(c.meta, "mid_startTime"));
|
|
assert_eq!(e.anim_ref(mid).end_time(true), num(c.meta, "mid_endTime"));
|
|
assert_eq!(e.anim_ref(mid).duration(), num(c.meta, "mid_duration"));
|
|
inner
|
|
}
|
|
_ => {
|
|
let repeat = c.id.contains("repeat1");
|
|
let inner = e.timeline(TimelineOpts::new().repeat(i32::from(repeat)).yoyo(repeat));
|
|
e.tl(inner)
|
|
.to(one(0), &[to(X, 100.0)], lin(2.0), Position::END);
|
|
match c.id {
|
|
"inner_timescale_2" | "inner_repeat1_yoyo_timescale_2" => {
|
|
e.anim(inner).set_time_scale(2.0);
|
|
}
|
|
"inner_timescale_0_5" => {
|
|
e.anim(inner).set_time_scale(0.5);
|
|
}
|
|
"inner_reversed" => {
|
|
e.anim(inner).set_reversed(true);
|
|
}
|
|
"inner_reversed_timescale_0_5" => {
|
|
e.anim(inner).set_time_scale(0.5).set_reversed(true);
|
|
}
|
|
other => panic!("{other}"),
|
|
}
|
|
e.tl(outer).add(inner, 1.0);
|
|
inner
|
|
}
|
|
};
|
|
let meta: Pairs = c.meta;
|
|
let m = |k: &str| golden::common::val(meta, k);
|
|
if let Some(golden::common::Val::F(d)) = m("outer_duration") {
|
|
assert_eq!(e.anim_ref(outer).duration(), d, "{}", c.id);
|
|
}
|
|
for (k, got) in [
|
|
("inner_startTime", e.anim_ref(inner).start_time()),
|
|
("tween_startTime", e.anim_ref(inner).start_time()),
|
|
("inner_endTime", e.anim_ref(inner).end_time(true)),
|
|
("tween_endTime", e.anim_ref(inner).end_time(true)),
|
|
("inner_timeScale", e.anim_ref(inner).time_scale()),
|
|
("inner_duration", e.anim_ref(inner).duration()),
|
|
("inner_totalDuration", e.anim_ref(inner).total_duration()),
|
|
] {
|
|
if let Some(golden::common::Val::F(want)) = m(k) {
|
|
assert_eq!(got, want, "{}: {k}", c.id);
|
|
}
|
|
}
|
|
if let Some(golden::common::Val::B(want)) = m("inner_reversed") {
|
|
assert_eq!(e.anim_ref(inner).reversed(), want, "{}", c.id);
|
|
}
|
|
let series: &[Series] = c.series;
|
|
for (i, t) in c.t.iter().enumerate() {
|
|
e.anim(outer).set_total_time(*t, Emit::Fire);
|
|
for s in series {
|
|
let got = match s.name {
|
|
"outer.time" => e.anim_ref(outer).time(),
|
|
"inner.time" => e.anim_ref(inner).time(),
|
|
"inner.totalTime" => e.anim_ref(inner).total_time(),
|
|
"inner.progress" => e.anim_ref(inner).progress(),
|
|
"tween_time" => e.anim_ref(inner).time(),
|
|
"x" => val(&e, 0, X),
|
|
other => panic!("{other}"),
|
|
};
|
|
let tol = if s.name == "x" { VAL_TOL } else { TIME_TOL };
|
|
close(got, s.v[i], tol, &format!("{}: {} at {t}", c.id, s.name));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn golden_negative_time_scale_is_reversal() {
|
|
let c = golden::timescale::CASES
|
|
.iter()
|
|
.find(|c| c.id == "negative_timescale")
|
|
.unwrap();
|
|
let mut e = TweenEngine::new();
|
|
let t = e.to(one(0), &[to(X, 100.0)], lin(1.0).paused(true));
|
|
for op in c.ops {
|
|
match text(op, "op").unwrap() {
|
|
"created" => {}
|
|
"timeScale(-1)" => {
|
|
e.anim(t).set_time_scale(-1.0);
|
|
}
|
|
"timeScale(-2)" => {
|
|
e.anim(t).set_time_scale(-2.0);
|
|
}
|
|
"timeScale(0)" => {
|
|
e.anim(t).set_time_scale(0.0);
|
|
}
|
|
"timeScale(1)" => {
|
|
e.anim(t).set_time_scale(1.0);
|
|
}
|
|
"reversed(false)" => {
|
|
e.anim(t).set_reversed(false);
|
|
}
|
|
"reversed(true)" | "reversed(true) while timeScale 0" => {
|
|
e.anim(t).set_reversed(true);
|
|
}
|
|
other => panic!("{other}"),
|
|
}
|
|
let r = e.anim_ref(t);
|
|
let name = text(op, "op").unwrap();
|
|
assert_eq!(r.time_scale(), num(op, "timeScale"), "{name}");
|
|
assert_eq!(r.reversed(), flag(op, "reversed"), "{name}");
|
|
assert_eq!(r.paused(), flag(op, "paused"), "{name}");
|
|
}
|
|
}
|
|
|
|
/// The time scale cases that drive a smooth root emulation R: the engine
|
|
/// root is R; it settles its own time shift on the next frame (advance(0)).
|
|
#[test]
|
|
fn golden_nested_time_scales_compose() {
|
|
let c = golden::timescale::CASES
|
|
.iter()
|
|
.find(|c| c.id == "design_t10_nested_time_scales")
|
|
.unwrap();
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let tl = e.timeline(TimelineOpts::new());
|
|
let tl2 = e.timeline(TimelineOpts::new());
|
|
e.tl(tl2)
|
|
.to(one(0), &[to(X, 100.0)], lin(2.0), Position::END);
|
|
e.anim(tl2).set_time_scale(2.0);
|
|
e.tl(tl).add(tl2, 1.0);
|
|
for op in c.ops {
|
|
let name = text(op, "op").unwrap();
|
|
match name {
|
|
"R +1.5" => e.advance(1.5),
|
|
"R +0.5" => e.advance(0.5),
|
|
"tl.timeScale(0.5)" => {
|
|
e.anim(tl).set_time_scale(0.5);
|
|
e.advance(0.0);
|
|
}
|
|
"tl.reverse()" => {
|
|
e.anim(tl).reverse();
|
|
e.advance(0.0);
|
|
}
|
|
other => panic!("{other}"),
|
|
}
|
|
let r = e.anim_ref(tl);
|
|
close(val(&e, 0, X), num(op, "x"), VAL_TOL, name);
|
|
close(r.time(), num(op, "tl_time"), TIME_TOL, name);
|
|
assert_eq!(r.time_scale(), num(op, "tl_time_scale"), "{name}");
|
|
assert_eq!(r.reversed(), flag(op, "tl_reversed"), "{name}");
|
|
close(r.start_time(), num(op, "tl_start"), TIME_TOL, name);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn golden_time_scale_change_does_not_jump() {
|
|
let c = golden::timescale::CASES
|
|
.iter()
|
|
.find(|c| c.id == "design_time_scale_no_jump_root_child")
|
|
.unwrap();
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let t = e.to(one(0), &[to(X, 100.0)], lin(1.0));
|
|
let mut root = 0.0;
|
|
for op in c.ops {
|
|
let name = text(op, "op").unwrap();
|
|
match name {
|
|
"R.totalTime(0.5)" | "R.totalTime(0.6)" | "R.totalTime(0.7)" => {
|
|
let target: f64 = name[12..15].parse().unwrap();
|
|
e.advance(target - root);
|
|
root = target;
|
|
}
|
|
"t.timeScale(2)" => {
|
|
e.anim(t).set_time_scale(2.0);
|
|
}
|
|
"t.timeScale(-1)" => {
|
|
e.anim(t).set_time_scale(-1.0);
|
|
}
|
|
other => panic!("{other}"),
|
|
}
|
|
let r = e.anim_ref(t);
|
|
close(val(&e, 0, X), num(op, "x"), VAL_TOL, name);
|
|
close(r.total_time(), num(op, "t_total_time"), 1e-9, name);
|
|
close(r.start_time(), num(op, "t_start"), TIME_TOL, name);
|
|
assert_eq!(r.time_scale(), num(op, "t_time_scale"), "{name}");
|
|
assert_eq!(r.reversed(), flag(op, "t_reversed"), "{name}");
|
|
}
|
|
|
|
let c = golden::timescale::CASES
|
|
.iter()
|
|
.find(|c| c.id == "design_time_scale_no_jump_nested_smooth")
|
|
.unwrap();
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let tl = e.timeline(TimelineOpts::new().smooth_child_timing(true));
|
|
e.tl(tl)
|
|
.to(one(0), &[to(X, 100.0)], lin(1.0), Position::END);
|
|
let t = e.tl(tl).last();
|
|
for op in c.ops {
|
|
let name = text(op, "op").unwrap();
|
|
match name {
|
|
"R +0.5" => e.advance(0.5),
|
|
"t.timeScale(0.5)" => {
|
|
e.anim(t).set_time_scale(0.5);
|
|
e.advance(0.0);
|
|
}
|
|
other => panic!("{other}"),
|
|
}
|
|
close(val(&e, 0, X), num(op, "x"), VAL_TOL, name);
|
|
close(
|
|
e.anim_ref(tl).duration(),
|
|
num(op, "tl_duration"),
|
|
TIME_TOL,
|
|
name,
|
|
);
|
|
close(
|
|
e.anim_ref(t).start_time(),
|
|
num(op, "t_start"),
|
|
TIME_TOL,
|
|
name,
|
|
);
|
|
close(
|
|
e.anim_ref(t).total_time(),
|
|
num(op, "t_total_time"),
|
|
TIME_TOL,
|
|
name,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn golden_smooth_child_timing_recomputes_the_parent() {
|
|
for (id, smooth) in [
|
|
("design_smooth_child_timing_duration", true),
|
|
("design_non_smooth_child_timing_duration", false),
|
|
] {
|
|
let c = golden::timescale::CASES
|
|
.iter()
|
|
.find(|c| c.id == id)
|
|
.unwrap();
|
|
let mut e = TweenEngine::new();
|
|
let tl = e.timeline(TimelineOpts::new().paused(true).smooth_child_timing(smooth));
|
|
e.tl(tl).to(one(0), &[to(X, 1.0)], lin(1.0), Position::END);
|
|
let ch = e.tl(tl).last();
|
|
for op in c.ops {
|
|
let name = text(op, "op").unwrap();
|
|
if name == "c.timeScale(0.5)" {
|
|
e.anim(ch).set_time_scale(0.5);
|
|
}
|
|
assert_eq!(
|
|
e.anim_ref(tl).duration(),
|
|
num(op, "tl_duration"),
|
|
"{id} {name}"
|
|
);
|
|
assert_eq!(
|
|
e.anim_ref(ch).start_time(),
|
|
num(op, "c_start"),
|
|
"{id} {name}"
|
|
);
|
|
assert_eq!(
|
|
e.anim_ref(ch).end_time(true),
|
|
num(op, "c_end"),
|
|
"{id} {name}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn paused_child_does_not_count_in_duration() {
|
|
let mut e = TweenEngine::new();
|
|
let tl = e.timeline(TimelineOpts::new().paused(true));
|
|
e.tl(tl).to(one(0), &[to(X, 1.0)], lin(1.0), 0.0);
|
|
let b = e.to(one(1), &[to(X, 1.0)], lin(2.0).paused(true));
|
|
e.tl(tl).add(b, 0.0);
|
|
assert_eq!(e.anim_ref(tl).duration(), 1.0);
|
|
e.anim(b).resume();
|
|
assert_eq!(e.anim_ref(tl).duration(), 2.0);
|
|
}
|
|
|
|
#[test]
|
|
fn split_dt_matches_single_dt() {
|
|
let run = |steps: usize, dt: f64| {
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
e.to(one(0), &[to(X, 100.0)], lin(1.0));
|
|
for _ in 0..steps {
|
|
e.advance(dt);
|
|
}
|
|
val(&e, 0, X)
|
|
};
|
|
let one_step = run(1, 0.5);
|
|
assert!((run(50, 0.01) - one_step).abs() <= 1e-5);
|
|
assert_eq!(run(64, 1.0 / 128.0).to_bits(), one_step.to_bits());
|
|
}
|
|
|
|
#[test]
|
|
fn same_dt_sequence_same_bits() {
|
|
let run = || {
|
|
let mut e = TweenEngine::new();
|
|
seed_all(&mut e, 8, X, 0.0);
|
|
for i in 0..8u32 {
|
|
e.to(
|
|
one(i),
|
|
&[to(X, 10.0 * i as f64)],
|
|
TweenOpts::new()
|
|
.duration(0.3 + 0.1 * i as f64)
|
|
.ease(Easing::OutElastic)
|
|
.repeat(1)
|
|
.yoyo(true),
|
|
);
|
|
}
|
|
let mut out = Vec::new();
|
|
let mut k = 1u64;
|
|
for _ in 0..200 {
|
|
k = splitmix64(k);
|
|
e.advance((k % 1000) as f64 / 40000.0);
|
|
for i in 0..8 {
|
|
out.push(val(&e, i, X).to_bits());
|
|
}
|
|
}
|
|
out
|
|
};
|
|
assert_eq!(run(), run());
|
|
}
|
|
|
|
#[test]
|
|
fn landing_is_exact_for_every_ease() {
|
|
let eases = [
|
|
Easing::InBack,
|
|
Easing::OutBack,
|
|
Easing::Constant(0.3),
|
|
Easing::Instant,
|
|
Easing::css_preset("ease_in_out_back").unwrap(),
|
|
Easing::css_preset("ease_out_quad").unwrap(),
|
|
];
|
|
for ease_ in eases {
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(3.0));
|
|
e.to(
|
|
one(0),
|
|
&[to(X, 7.1)],
|
|
TweenOpts::new().duration(0.3).ease(ease_),
|
|
);
|
|
for _ in 0..40 {
|
|
e.advance(1.0 / 60.0);
|
|
}
|
|
assert_eq!(val(&e, 0, X).to_bits(), 7.1f64.to_bits(), "{ease_:?}");
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(3.0));
|
|
e.to(
|
|
one(0),
|
|
&[to(X, 7.1)],
|
|
TweenOpts::new()
|
|
.duration(0.3)
|
|
.ease(ease_)
|
|
.repeat(1)
|
|
.yoyo(true),
|
|
);
|
|
for _ in 0..60 {
|
|
e.advance(1.0 / 60.0);
|
|
}
|
|
assert_eq!(val(&e, 0, X).to_bits(), 3.0f64.to_bits(), "yoyo {ease_:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nested_time_scales_compose() {
|
|
// T10 by frames: tl2 (ts 2) at 1 in tl, a 2 s tween.
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let tl = e.timeline(TimelineOpts::new());
|
|
let tl2 = e.timeline(TimelineOpts::new());
|
|
e.tl(tl2)
|
|
.to(one(0), &[to(X, 100.0)], lin(2.0), Position::END);
|
|
e.anim(tl2).set_time_scale(2.0);
|
|
e.tl(tl).add(tl2, 1.0);
|
|
e.advance(1.5);
|
|
close(val(&e, 0, X), 50.0, 1e-9, "1.5");
|
|
e.anim(tl).set_time_scale(0.5);
|
|
e.advance(0.5);
|
|
close(val(&e, 0, X), 75.0, 1e-9, "after slow-down");
|
|
e.anim(tl).reverse();
|
|
e.advance(0.5);
|
|
close(val(&e, 0, X), 50.0, 1e-9, "reversed");
|
|
}
|
|
|
|
#[test]
|
|
fn the_root_clock_does_not_drift() {
|
|
// GSAP's root reads the absolute ticker time, so its 1e-7 rounding never
|
|
// accumulates: 120 frames of 1/120 s complete a 1 s tween.
|
|
let mut e = TweenEngine::new();
|
|
let mut names = Names::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let t = e.to(
|
|
one(0),
|
|
&[to(X, 1.0)],
|
|
lin(1.0).events(EventMask::COMPLETE).keep(true),
|
|
);
|
|
names.name(t, "t");
|
|
for _ in 0..120 {
|
|
e.advance(1.0 / 120.0);
|
|
}
|
|
let (_, fired) = names.drain(&mut e);
|
|
assert_eq!(fired, ["t:onComplete"]);
|
|
assert_eq!(e.anim_ref(t).progress(), 1.0);
|
|
// Ten minutes at 144 Hz stay within a microsecond.
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let t = e.to(one(0), &[to(X, 600.0)], lin(600.0));
|
|
for _ in 0..(600 * 144 - 1) {
|
|
e.advance(1.0 / 144.0);
|
|
}
|
|
close(
|
|
e.anim_ref(t).total_time(),
|
|
600.0 - 1.0 / 144.0,
|
|
1e-6,
|
|
"drift",
|
|
);
|
|
}
|