From d3b5f1bee9ef6dfc1fda860f922410874af88f1f Mon Sep 17 00:00:00 2001
From: Jerome Humbert <djeedai@gmail.com>
Date: Sun, 30 Jan 2022 20:47:14 +0000
Subject: [PATCH] `Tweenable`-based design

Add a `Tweenable<T>` trait describing a generic animatable tween-like
element.

Expose the three types of tweenables and implement `Tweenable<T>` for
them:
- `Tween<T>`, a single animation
- `Sequence<T>`, a sequence of consecutive animations
- `Tracks<T>`, a batch of animations running in parallel

Clean-up animators to hold a single top-level `Tweenable<T>` and let the
user build any kind of animation hierarchy for themselves.
---
 CHANGELOG                         |  16 +-
 examples/colormaterial_color.rs   |  85 +++---
 examples/sequence.rs              | 143 +++++++---
 examples/sprite_color.rs          |  84 +++---
 examples/transform_rotation.rs    |  86 +++---
 examples/transform_translation.rs |  84 +++---
 examples/ui_position.rs           | 104 ++++----
 src/lib.rs                        | 429 ++++++++++++++++++------------
 src/plugin.rs                     |  20 +-
 9 files changed, 618 insertions(+), 433 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 028c9ce..6834129 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -11,17 +11,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Implement `Default` for `AnimatorState` as `AnimatorState::Playing`.
 - Added `Animator::with_state()` and `AssetAnimator::with_state()`, builder-like functions to override the default `AnimatorState`.
 - Added `Tween::is_looping()` returning true for all but `TweeningType::Once`.
-- Publicly exposed `Sequence<T>`, a sequence of tweens running one after the other.
-- Publicly exposed `Animator<T>::tracks()` and `Animator<T>::tracks_mut()` to access the animation sequences running in parallel on multiple animation tracks.
+- Added the `Tweenable<T>` trait, implemented by the `Tween<T>` animation and the `Tracks<T>` and `Sequence<T>` animation collections.
+- Added `IntoBoxDynTweenable<T>`, a trait to convert a `Tweenable<T>` trait object into a boxed variant.
+- Publicly exposed `Sequence<T>`, a sequence of `Tweenable<T>` running one after the other.
+- Publicly exposed `Tracks<T>`, a collection of `Tweenable<T>` running in parallel.
+- Publicly exposed `TweenState`, the playback state of a single `Tweenable<T>` item.
+- Added `Tween<T>::then()` and `Sequence<T>::then()` to append a `Tweenable<T>` to a sequence (creating a new sequence in the case of `Tween<T>::then()`).
+- Added `tweenable()` and `tweenable_mut()` on the `Animator<T>` and `AssetAnimator<T>` to access their top-level `Tweenable<T>`.
+- Implemented `Default` for `Animator<T>` and `AssetAnimator<T>`, creating an animator without any tweenable item (no-op).
 
 ### Changed
 
 - Moved tween duration out of the `TweeningType` enum, which combined with the removal of the "pause" feature in loops makes it a C-like enum.
-- Updated the `sequence` example to add some text showing the current sequence active tween index and its progress.
+- Updated the `sequence` example to add some text showing the current sequence progress.
+- Modified the signature of `new()` for `Animator<T>` and `AssetAnimator<T>` to take a single `Tweenable<T>` instead of trying to build a `Tween<T>` internally. This allows passing any `Tweenable<T>` as the top-level animatable item of an animator, and avoids the overhead of maintaining a `Tracks<T>` internally in each animator when the most common use case is likely to use a single `Tween<T>` or a `Sequence<T>` without parallelism.
 
 ### Removed
 
-- Removed the "pause" feature in-between loops of `TweeningType::Loop` and `TweeningType::PingPong`, which can be replaced if needed with a sequence including a no-op tween of the desired duration. Removed `Tween::is_paused()`.
+- Removed the "pause" feature in-between loops of `TweeningType::Loop` and `TweeningType::PingPong`, which can be replaced if needed by a sequence including a no-op tween of the desired duration. Removed `Tween::is_paused()`.
+- Removed `new_single()` and `new_seq()` on the `Animator<T>` and `AssetAnimator<T>`. Users should explicitly create a `Tween<T>` or `Sequence<T>` instead, and use `new()`.
 
 ### Fixed
 
diff --git a/examples/colormaterial_color.rs b/examples/colormaterial_color.rs
index 51c7fa5..b7e850f 100644
--- a/examples/colormaterial_color.rs
+++ b/examples/colormaterial_color.rs
@@ -2,6 +2,8 @@ use bevy::{
     prelude::*,
     sprite::{MaterialMesh2dBundle, Mesh2dHandle},
 };
+use bevy_tweening::*;
+use std::time::Duration;
 
 fn main() -> Result<(), Box<dyn std::error::Error>> {
     App::default()
@@ -13,7 +15,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
             ..Default::default()
         })
         .add_plugins(DefaultPlugins)
-        .add_plugin(bevy_tweening::TweeningPlugin)
+        .add_plugin(TweeningPlugin)
         .add_startup_system(setup)
         .run();
 
@@ -38,36 +40,36 @@ fn setup(
     let quad_mesh: Mesh2dHandle = meshes.add(Mesh::from(shape::Quad::default())).into();
 
     for ease_function in &[
-        bevy_tweening::EaseFunction::QuadraticIn,
-        bevy_tweening::EaseFunction::QuadraticOut,
-        bevy_tweening::EaseFunction::QuadraticInOut,
-        bevy_tweening::EaseFunction::CubicIn,
-        bevy_tweening::EaseFunction::CubicOut,
-        bevy_tweening::EaseFunction::CubicInOut,
-        bevy_tweening::EaseFunction::QuarticIn,
-        bevy_tweening::EaseFunction::QuarticOut,
-        bevy_tweening::EaseFunction::QuarticInOut,
-        bevy_tweening::EaseFunction::QuinticIn,
-        bevy_tweening::EaseFunction::QuinticOut,
-        bevy_tweening::EaseFunction::QuinticInOut,
-        bevy_tweening::EaseFunction::SineIn,
-        bevy_tweening::EaseFunction::SineOut,
-        bevy_tweening::EaseFunction::SineInOut,
-        bevy_tweening::EaseFunction::CircularIn,
-        bevy_tweening::EaseFunction::CircularOut,
-        bevy_tweening::EaseFunction::CircularInOut,
-        bevy_tweening::EaseFunction::ExponentialIn,
-        bevy_tweening::EaseFunction::ExponentialOut,
-        bevy_tweening::EaseFunction::ExponentialInOut,
-        bevy_tweening::EaseFunction::ElasticIn,
-        bevy_tweening::EaseFunction::ElasticOut,
-        bevy_tweening::EaseFunction::ElasticInOut,
-        bevy_tweening::EaseFunction::BackIn,
-        bevy_tweening::EaseFunction::BackOut,
-        bevy_tweening::EaseFunction::BackInOut,
-        bevy_tweening::EaseFunction::BounceIn,
-        bevy_tweening::EaseFunction::BounceOut,
-        bevy_tweening::EaseFunction::BounceInOut,
+        EaseFunction::QuadraticIn,
+        EaseFunction::QuadraticOut,
+        EaseFunction::QuadraticInOut,
+        EaseFunction::CubicIn,
+        EaseFunction::CubicOut,
+        EaseFunction::CubicInOut,
+        EaseFunction::QuarticIn,
+        EaseFunction::QuarticOut,
+        EaseFunction::QuarticInOut,
+        EaseFunction::QuinticIn,
+        EaseFunction::QuinticOut,
+        EaseFunction::QuinticInOut,
+        EaseFunction::SineIn,
+        EaseFunction::SineOut,
+        EaseFunction::SineInOut,
+        EaseFunction::CircularIn,
+        EaseFunction::CircularOut,
+        EaseFunction::CircularInOut,
+        EaseFunction::ExponentialIn,
+        EaseFunction::ExponentialOut,
+        EaseFunction::ExponentialInOut,
+        EaseFunction::ElasticIn,
+        EaseFunction::ElasticOut,
+        EaseFunction::ElasticInOut,
+        EaseFunction::BackIn,
+        EaseFunction::BackOut,
+        EaseFunction::BackInOut,
+        EaseFunction::BounceIn,
+        EaseFunction::BounceOut,
+        EaseFunction::BounceInOut,
     ] {
         // Create a unique material per entity, so that it can be animated
         // without affecting the other entities. Note that we could share
@@ -75,6 +77,16 @@ fn setup(
         // asset would change the color of all entities using that material.
         let unique_material = materials.add(Color::BLACK.into());
 
+        let tween = Tween::new(
+            *ease_function,
+            TweeningType::PingPong,
+            Duration::from_secs(1),
+            ColorMaterialColorLens {
+                start: Color::RED,
+                end: Color::BLUE,
+            },
+        );
+
         commands
             .spawn_bundle(MaterialMesh2dBundle {
                 mesh: quad_mesh.clone(),
@@ -83,16 +95,7 @@ fn setup(
                 material: unique_material.clone(),
                 ..Default::default()
             })
-            .insert(bevy_tweening::AssetAnimator::new(
-                unique_material.clone(),
-                *ease_function,
-                bevy_tweening::TweeningType::PingPong,
-                std::time::Duration::from_secs(1),
-                bevy_tweening::ColorMaterialColorLens {
-                    start: Color::RED,
-                    end: Color::BLUE,
-                },
-            ));
+            .insert(AssetAnimator::new(unique_material.clone(), tween));
         y -= size * spacing;
         if y < -screen_y {
             x += size * spacing;
diff --git a/examples/sequence.rs b/examples/sequence.rs
index 04b6eb0..d5b19cf 100644
--- a/examples/sequence.rs
+++ b/examples/sequence.rs
@@ -21,19 +21,30 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
 }
 
 #[derive(Component)]
-struct IndexText;
+struct RedProgress;
 
 #[derive(Component)]
-struct ProgressText;
+struct BlueProgress;
+
+#[derive(Component)]
+struct RedSprite;
+
+#[derive(Component)]
+struct BlueSprite;
 
 fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
     commands.spawn_bundle(OrthographicCameraBundle::new_2d());
 
     let font = asset_server.load("fonts/FiraMono-Regular.ttf");
-    let text_style = TextStyle {
-        font,
+    let text_style_red = TextStyle {
+        font: font.clone(),
+        font_size: 50.0,
+        color: Color::RED,
+    };
+    let text_style_blue = TextStyle {
+        font: font.clone(),
         font_size: 50.0,
-        color: Color::WHITE,
+        color: Color::BLUE,
     };
 
     let text_alignment = TextAlignment {
@@ -47,12 +58,12 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
             text: Text {
                 sections: vec![
                     TextSection {
-                        value: "index: ".to_owned(),
-                        style: text_style.clone(),
+                        value: "progress: ".to_owned(),
+                        style: text_style_red.clone(),
                     },
                     TextSection {
-                        value: "0".to_owned(),
-                        style: text_style.clone(),
+                        value: "0%".to_owned(),
+                        style: text_style_red.clone(),
                     },
                 ],
                 alignment: text_alignment,
@@ -60,7 +71,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
             transform: Transform::from_translation(Vec3::new(0., 40., 0.)),
             ..Default::default()
         })
-        .insert(IndexText);
+        .insert(RedProgress);
 
     // Text with progress of the active tween in the sequence
     commands
@@ -69,11 +80,11 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
                 sections: vec![
                     TextSection {
                         value: "progress: ".to_owned(),
-                        style: text_style.clone(),
+                        style: text_style_blue.clone(),
                     },
                     TextSection {
                         value: "0%".to_owned(),
-                        style: text_style.clone(),
+                        style: text_style_blue.clone(),
                     },
                 ],
                 alignment: text_alignment,
@@ -81,7 +92,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
             transform: Transform::from_translation(Vec3::new(0., -40., 0.)),
             ..Default::default()
         })
-        .insert(ProgressText);
+        .insert(BlueProgress);
 
     let size = 25.;
 
@@ -98,20 +109,18 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
         Vec3::new(margin, screen_y - margin, 0.),
         Vec3::new(margin, margin, 0.),
     ];
-    let tweens = dests
-        .windows(2)
-        .map(|pair| {
-            Tween::new(
-                EaseFunction::QuadraticInOut,
-                TweeningType::Once,
-                Duration::from_secs(1),
-                TransformPositionLens {
-                    start: pair[0] - center,
-                    end: pair[1] - center,
-                },
-            )
-        })
-        .collect();
+    // Build a sequence from an iterator over a Tweenable (here, a Tween<Transform>)
+    let seq = Sequence::new(dests.windows(2).map(|pair| {
+        Tween::new(
+            EaseFunction::QuadraticInOut,
+            TweeningType::Once,
+            Duration::from_secs(1),
+            TransformPositionLens {
+                start: pair[0] - center,
+                end: pair[1] - center,
+            },
+        )
+    }));
 
     commands
         .spawn_bundle(SpriteBundle {
@@ -122,32 +131,86 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
             },
             ..Default::default()
         })
-        .insert(Animator::new_seq(tweens));
+        .insert(RedSprite)
+        .insert(Animator::new(seq));
+
+    // First move from left to right, then rotate around self 180 degrees while scaling
+    // size at the same time.
+    let tween_move = Tween::new(
+        EaseFunction::QuadraticInOut,
+        TweeningType::Once,
+        Duration::from_secs(1),
+        TransformPositionLens {
+            start: Vec3::new(-200., 100., 0.),
+            end: Vec3::new(200., 100., 0.),
+        },
+    );
+    let tween_rotate = Tween::new(
+        EaseFunction::QuadraticInOut,
+        TweeningType::Once,
+        Duration::from_secs(1),
+        TransformRotationLens {
+            start: Quat::IDENTITY,
+            end: Quat::from_rotation_z(180_f32.to_radians()),
+        },
+    );
+    let tween_scale = Tween::new(
+        EaseFunction::QuadraticInOut,
+        TweeningType::Once,
+        Duration::from_secs(1),
+        TransformScaleLens {
+            start: Vec3::ONE,
+            end: Vec3::splat(2.0),
+        },
+    );
+    // Build parallel tracks executing two tweens at the same time : rotate and scale.
+    let tracks = Tracks::new([tween_rotate, tween_scale]);
+    // Build a sequence from an heterogeneous list of tweenables by casting them manually
+    // to a boxed Tweenable<Transform> : first move, then { rotate + scale }.
+    let seq2 = Sequence::new([
+        Box::new(tween_move) as Box<dyn Tweenable<Transform> + Send + Sync + 'static>,
+        Box::new(tracks) as Box<dyn Tweenable<Transform> + Send + Sync + 'static>,
+    ]);
+
+    commands
+        .spawn_bundle(SpriteBundle {
+            sprite: Sprite {
+                color: Color::BLUE,
+                custom_size: Some(Vec2::new(size * 3., size)),
+                ..Default::default()
+            },
+            ..Default::default()
+        })
+        .insert(BlueSprite)
+        .insert(Animator::new(seq2));
 }
 
 fn update_text(
     // Note: need a QuerySet<> due to the "&mut Text" in both queries
     mut query_text: QuerySet<(
-        QueryState<&mut Text, With<IndexText>>,
-        QueryState<&mut Text, With<ProgressText>>,
+        QueryState<&mut Text, With<RedProgress>>,
+        QueryState<&mut Text, With<BlueProgress>>,
     )>,
-    query_anim: Query<&Animator<Transform>>,
+    query_anim_red: Query<&Animator<Transform>, With<RedSprite>>,
+    query_anim_blue: Query<&Animator<Transform>, With<BlueSprite>>,
 ) {
-    let anim = query_anim.single();
-    let seq = &anim.tracks()[0];
-    let index = seq.index();
-    let tween = seq.current();
-    let progress = tween.progress();
+    let anim_red = query_anim_red.single();
+    let tween_red = anim_red.tweenable().unwrap();
+    let progress_red = tween_red.progress();
+
+    let anim_blue = query_anim_blue.single();
+    let tween_blue = anim_blue.tweenable().unwrap();
+    let progress_blue = tween_blue.progress();
 
     // Use scopes to force-drop the mutable context before opening the next one
     {
         let mut q0 = query_text.q0();
-        let mut index_text = q0.single_mut();
-        index_text.sections[1].value = format!("{:1}", index).to_string();
+        let mut red_text = q0.single_mut();
+        red_text.sections[1].value = format!("{:5.1}%", progress_red * 100.).to_string();
     }
     {
         let mut q1 = query_text.q1();
-        let mut progress_text = q1.single_mut();
-        progress_text.sections[1].value = format!("{:5.1}%", progress * 100.).to_string();
+        let mut blue_text = q1.single_mut();
+        blue_text.sections[1].value = format!("{:5.1}%", progress_blue * 100.).to_string();
     }
 }
diff --git a/examples/sprite_color.rs b/examples/sprite_color.rs
index 4878509..475573a 100644
--- a/examples/sprite_color.rs
+++ b/examples/sprite_color.rs
@@ -1,4 +1,5 @@
 use bevy::prelude::*;
+use bevy_tweening::*;
 
 fn main() -> Result<(), Box<dyn std::error::Error>> {
     App::default()
@@ -10,7 +11,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
             ..Default::default()
         })
         .add_plugins(DefaultPlugins)
-        .add_plugin(bevy_tweening::TweeningPlugin)
+        .add_plugin(TweeningPlugin)
         .add_startup_system(setup)
         .run();
 
@@ -29,37 +30,47 @@ fn setup(mut commands: Commands) {
     let mut y = screen_y;
 
     for ease_function in &[
-        bevy_tweening::EaseFunction::QuadraticIn,
-        bevy_tweening::EaseFunction::QuadraticOut,
-        bevy_tweening::EaseFunction::QuadraticInOut,
-        bevy_tweening::EaseFunction::CubicIn,
-        bevy_tweening::EaseFunction::CubicOut,
-        bevy_tweening::EaseFunction::CubicInOut,
-        bevy_tweening::EaseFunction::QuarticIn,
-        bevy_tweening::EaseFunction::QuarticOut,
-        bevy_tweening::EaseFunction::QuarticInOut,
-        bevy_tweening::EaseFunction::QuinticIn,
-        bevy_tweening::EaseFunction::QuinticOut,
-        bevy_tweening::EaseFunction::QuinticInOut,
-        bevy_tweening::EaseFunction::SineIn,
-        bevy_tweening::EaseFunction::SineOut,
-        bevy_tweening::EaseFunction::SineInOut,
-        bevy_tweening::EaseFunction::CircularIn,
-        bevy_tweening::EaseFunction::CircularOut,
-        bevy_tweening::EaseFunction::CircularInOut,
-        bevy_tweening::EaseFunction::ExponentialIn,
-        bevy_tweening::EaseFunction::ExponentialOut,
-        bevy_tweening::EaseFunction::ExponentialInOut,
-        bevy_tweening::EaseFunction::ElasticIn,
-        bevy_tweening::EaseFunction::ElasticOut,
-        bevy_tweening::EaseFunction::ElasticInOut,
-        bevy_tweening::EaseFunction::BackIn,
-        bevy_tweening::EaseFunction::BackOut,
-        bevy_tweening::EaseFunction::BackInOut,
-        bevy_tweening::EaseFunction::BounceIn,
-        bevy_tweening::EaseFunction::BounceOut,
-        bevy_tweening::EaseFunction::BounceInOut,
+        EaseFunction::QuadraticIn,
+        EaseFunction::QuadraticOut,
+        EaseFunction::QuadraticInOut,
+        EaseFunction::CubicIn,
+        EaseFunction::CubicOut,
+        EaseFunction::CubicInOut,
+        EaseFunction::QuarticIn,
+        EaseFunction::QuarticOut,
+        EaseFunction::QuarticInOut,
+        EaseFunction::QuinticIn,
+        EaseFunction::QuinticOut,
+        EaseFunction::QuinticInOut,
+        EaseFunction::SineIn,
+        EaseFunction::SineOut,
+        EaseFunction::SineInOut,
+        EaseFunction::CircularIn,
+        EaseFunction::CircularOut,
+        EaseFunction::CircularInOut,
+        EaseFunction::ExponentialIn,
+        EaseFunction::ExponentialOut,
+        EaseFunction::ExponentialInOut,
+        EaseFunction::ElasticIn,
+        EaseFunction::ElasticOut,
+        EaseFunction::ElasticInOut,
+        EaseFunction::BackIn,
+        EaseFunction::BackOut,
+        EaseFunction::BackInOut,
+        EaseFunction::BounceIn,
+        EaseFunction::BounceOut,
+        EaseFunction::BounceInOut,
     ] {
+        let tween = Tween::new(
+            *ease_function,
+            TweeningType::PingPong,
+            std::time::Duration::from_secs(1),
+            SpriteColorLens {
+                start: Color::RED,
+                end: Color::BLUE,
+            },
+        );
+
         commands
             .spawn_bundle(SpriteBundle {
                 transform: Transform::from_translation(Vec3::new(x, y, 0.)),
@@ -70,15 +81,8 @@ fn setup(mut commands: Commands) {
                 },
                 ..Default::default()
             })
-            .insert(bevy_tweening::Animator::new(
-                *ease_function,
-                bevy_tweening::TweeningType::PingPong,
-                std::time::Duration::from_secs(1),
-                bevy_tweening::SpriteColorLens {
-                    start: Color::RED,
-                    end: Color::BLUE,
-                },
-            ));
+            .insert(Animator::new(tween));
+
         y -= size * spacing;
         if y < -screen_y {
             x += size * spacing;
diff --git a/examples/transform_rotation.rs b/examples/transform_rotation.rs
index 5bf6749..611535f 100644
--- a/examples/transform_rotation.rs
+++ b/examples/transform_rotation.rs
@@ -1,4 +1,5 @@
 use bevy::prelude::*;
+use bevy_tweening::*;
 
 fn main() -> Result<(), Box<dyn std::error::Error>> {
     App::default()
@@ -10,7 +11,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
             ..Default::default()
         })
         .add_plugins(DefaultPlugins)
-        .add_plugin(bevy_tweening::TweeningPlugin)
+        .add_plugin(TweeningPlugin)
         .add_startup_system(setup)
         .run();
 
@@ -29,37 +30,47 @@ fn setup(mut commands: Commands) {
     let mut y = screen_y;
 
     for ease_function in &[
-        bevy_tweening::EaseFunction::QuadraticIn,
-        bevy_tweening::EaseFunction::QuadraticOut,
-        bevy_tweening::EaseFunction::QuadraticInOut,
-        bevy_tweening::EaseFunction::CubicIn,
-        bevy_tweening::EaseFunction::CubicOut,
-        bevy_tweening::EaseFunction::CubicInOut,
-        bevy_tweening::EaseFunction::QuarticIn,
-        bevy_tweening::EaseFunction::QuarticOut,
-        bevy_tweening::EaseFunction::QuarticInOut,
-        bevy_tweening::EaseFunction::QuinticIn,
-        bevy_tweening::EaseFunction::QuinticOut,
-        bevy_tweening::EaseFunction::QuinticInOut,
-        bevy_tweening::EaseFunction::SineIn,
-        bevy_tweening::EaseFunction::SineOut,
-        bevy_tweening::EaseFunction::SineInOut,
-        bevy_tweening::EaseFunction::CircularIn,
-        bevy_tweening::EaseFunction::CircularOut,
-        bevy_tweening::EaseFunction::CircularInOut,
-        bevy_tweening::EaseFunction::ExponentialIn,
-        bevy_tweening::EaseFunction::ExponentialOut,
-        bevy_tweening::EaseFunction::ExponentialInOut,
-        bevy_tweening::EaseFunction::ElasticIn,
-        bevy_tweening::EaseFunction::ElasticOut,
-        bevy_tweening::EaseFunction::ElasticInOut,
-        bevy_tweening::EaseFunction::BackIn,
-        bevy_tweening::EaseFunction::BackOut,
-        bevy_tweening::EaseFunction::BackInOut,
-        bevy_tweening::EaseFunction::BounceIn,
-        bevy_tweening::EaseFunction::BounceOut,
-        bevy_tweening::EaseFunction::BounceInOut,
+        EaseFunction::QuadraticIn,
+        EaseFunction::QuadraticOut,
+        EaseFunction::QuadraticInOut,
+        EaseFunction::CubicIn,
+        EaseFunction::CubicOut,
+        EaseFunction::CubicInOut,
+        EaseFunction::QuarticIn,
+        EaseFunction::QuarticOut,
+        EaseFunction::QuarticInOut,
+        EaseFunction::QuinticIn,
+        EaseFunction::QuinticOut,
+        EaseFunction::QuinticInOut,
+        EaseFunction::SineIn,
+        EaseFunction::SineOut,
+        EaseFunction::SineInOut,
+        EaseFunction::CircularIn,
+        EaseFunction::CircularOut,
+        EaseFunction::CircularInOut,
+        EaseFunction::ExponentialIn,
+        EaseFunction::ExponentialOut,
+        EaseFunction::ExponentialInOut,
+        EaseFunction::ElasticIn,
+        EaseFunction::ElasticOut,
+        EaseFunction::ElasticInOut,
+        EaseFunction::BackIn,
+        EaseFunction::BackOut,
+        EaseFunction::BackInOut,
+        EaseFunction::BounceIn,
+        EaseFunction::BounceOut,
+        EaseFunction::BounceInOut,
     ] {
+        let tween = Tween::new(
+            *ease_function,
+            TweeningType::PingPong,
+            std::time::Duration::from_secs(1),
+            TransformRotationLens {
+                start: Quat::IDENTITY,
+                end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.),
+            },
+        );
+
         commands
             .spawn_bundle((
                 Transform::from_translation(Vec3::new(x, y, 0.)),
@@ -70,21 +81,14 @@ fn setup(mut commands: Commands) {
                     .spawn_bundle(SpriteBundle {
                         sprite: Sprite {
                             color: Color::RED,
-                            custom_size: Some(Vec2::new(size, size)),
+                            custom_size: Some(Vec2::new(size, size * 0.5)),
                             ..Default::default()
                         },
                         ..Default::default()
                     })
-                    .insert(bevy_tweening::Animator::new(
-                        *ease_function,
-                        bevy_tweening::TweeningType::PingPong,
-                        std::time::Duration::from_secs(1),
-                        bevy_tweening::TransformRotationLens {
-                            start: Quat::IDENTITY,
-                            end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.),
-                        },
-                    ));
+                    .insert(Animator::new(tween));
             });
+
         y -= size * spacing;
         if y < -screen_y {
             x += size * spacing;
diff --git a/examples/transform_translation.rs b/examples/transform_translation.rs
index 106c3d1..73a7ff3 100644
--- a/examples/transform_translation.rs
+++ b/examples/transform_translation.rs
@@ -1,4 +1,5 @@
 use bevy::prelude::*;
+use bevy_tweening::*;
 
 fn main() -> Result<(), Box<dyn std::error::Error>> {
     App::default()
@@ -10,7 +11,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
             ..Default::default()
         })
         .add_plugins(DefaultPlugins)
-        .add_plugin(bevy_tweening::TweeningPlugin)
+        .add_plugin(TweeningPlugin)
         .add_startup_system(setup)
         .run();
 
@@ -28,37 +29,47 @@ fn setup(mut commands: Commands) {
     let mut x = -screen_x;
 
     for ease_function in &[
-        bevy_tweening::EaseFunction::QuadraticIn,
-        bevy_tweening::EaseFunction::QuadraticOut,
-        bevy_tweening::EaseFunction::QuadraticInOut,
-        bevy_tweening::EaseFunction::CubicIn,
-        bevy_tweening::EaseFunction::CubicOut,
-        bevy_tweening::EaseFunction::CubicInOut,
-        bevy_tweening::EaseFunction::QuarticIn,
-        bevy_tweening::EaseFunction::QuarticOut,
-        bevy_tweening::EaseFunction::QuarticInOut,
-        bevy_tweening::EaseFunction::QuinticIn,
-        bevy_tweening::EaseFunction::QuinticOut,
-        bevy_tweening::EaseFunction::QuinticInOut,
-        bevy_tweening::EaseFunction::SineIn,
-        bevy_tweening::EaseFunction::SineOut,
-        bevy_tweening::EaseFunction::SineInOut,
-        bevy_tweening::EaseFunction::CircularIn,
-        bevy_tweening::EaseFunction::CircularOut,
-        bevy_tweening::EaseFunction::CircularInOut,
-        bevy_tweening::EaseFunction::ExponentialIn,
-        bevy_tweening::EaseFunction::ExponentialOut,
-        bevy_tweening::EaseFunction::ExponentialInOut,
-        bevy_tweening::EaseFunction::ElasticIn,
-        bevy_tweening::EaseFunction::ElasticOut,
-        bevy_tweening::EaseFunction::ElasticInOut,
-        bevy_tweening::EaseFunction::BackIn,
-        bevy_tweening::EaseFunction::BackOut,
-        bevy_tweening::EaseFunction::BackInOut,
-        bevy_tweening::EaseFunction::BounceIn,
-        bevy_tweening::EaseFunction::BounceOut,
-        bevy_tweening::EaseFunction::BounceInOut,
+        EaseFunction::QuadraticIn,
+        EaseFunction::QuadraticOut,
+        EaseFunction::QuadraticInOut,
+        EaseFunction::CubicIn,
+        EaseFunction::CubicOut,
+        EaseFunction::CubicInOut,
+        EaseFunction::QuarticIn,
+        EaseFunction::QuarticOut,
+        EaseFunction::QuarticInOut,
+        EaseFunction::QuinticIn,
+        EaseFunction::QuinticOut,
+        EaseFunction::QuinticInOut,
+        EaseFunction::SineIn,
+        EaseFunction::SineOut,
+        EaseFunction::SineInOut,
+        EaseFunction::CircularIn,
+        EaseFunction::CircularOut,
+        EaseFunction::CircularInOut,
+        EaseFunction::ExponentialIn,
+        EaseFunction::ExponentialOut,
+        EaseFunction::ExponentialInOut,
+        EaseFunction::ElasticIn,
+        EaseFunction::ElasticOut,
+        EaseFunction::ElasticInOut,
+        EaseFunction::BackIn,
+        EaseFunction::BackOut,
+        EaseFunction::BackInOut,
+        EaseFunction::BounceIn,
+        EaseFunction::BounceOut,
+        EaseFunction::BounceInOut,
     ] {
+        let tween = Tween::new(
+            *ease_function,
+            TweeningType::PingPong,
+            std::time::Duration::from_secs(1),
+            TransformPositionLens {
+                start: Vec3::new(x, screen_y, 0.),
+                end: Vec3::new(x, -screen_y, 0.),
+            },
+        );
+
         commands
             .spawn_bundle(SpriteBundle {
                 sprite: Sprite {
@@ -68,15 +79,8 @@ fn setup(mut commands: Commands) {
                 },
                 ..Default::default()
             })
-            .insert(bevy_tweening::Animator::new(
-                *ease_function,
-                bevy_tweening::TweeningType::PingPong,
-                std::time::Duration::from_secs(1),
-                bevy_tweening::TransformPositionLens {
-                    start: Vec3::new(x, screen_y, 0.),
-                    end: Vec3::new(x, -screen_y, 0.),
-                },
-            ));
+            .insert(Animator::new(tween));
+
         x += size * spacing;
     }
 }
diff --git a/examples/ui_position.rs b/examples/ui_position.rs
index 32a9ec5..dd221f0 100644
--- a/examples/ui_position.rs
+++ b/examples/ui_position.rs
@@ -1,4 +1,5 @@
 use bevy::prelude::*;
+use bevy_tweening::*;
 
 fn main() -> Result<(), Box<dyn std::error::Error>> {
     App::default()
@@ -10,7 +11,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
             ..Default::default()
         })
         .add_plugins(DefaultPlugins)
-        .add_plugin(bevy_tweening::TweeningPlugin)
+        .add_plugin(TweeningPlugin)
         .add_startup_system(setup)
         .run();
 
@@ -28,37 +29,57 @@ fn setup(mut commands: Commands) {
     let mut x = 10.;
 
     for ease_function in &[
-        bevy_tweening::EaseFunction::QuadraticIn,
-        bevy_tweening::EaseFunction::QuadraticOut,
-        bevy_tweening::EaseFunction::QuadraticInOut,
-        bevy_tweening::EaseFunction::CubicIn,
-        bevy_tweening::EaseFunction::CubicOut,
-        bevy_tweening::EaseFunction::CubicInOut,
-        bevy_tweening::EaseFunction::QuarticIn,
-        bevy_tweening::EaseFunction::QuarticOut,
-        bevy_tweening::EaseFunction::QuarticInOut,
-        bevy_tweening::EaseFunction::QuinticIn,
-        bevy_tweening::EaseFunction::QuinticOut,
-        bevy_tweening::EaseFunction::QuinticInOut,
-        bevy_tweening::EaseFunction::SineIn,
-        bevy_tweening::EaseFunction::SineOut,
-        bevy_tweening::EaseFunction::SineInOut,
-        bevy_tweening::EaseFunction::CircularIn,
-        bevy_tweening::EaseFunction::CircularOut,
-        bevy_tweening::EaseFunction::CircularInOut,
-        bevy_tweening::EaseFunction::ExponentialIn,
-        bevy_tweening::EaseFunction::ExponentialOut,
-        bevy_tweening::EaseFunction::ExponentialInOut,
-        bevy_tweening::EaseFunction::ElasticIn,
-        bevy_tweening::EaseFunction::ElasticOut,
-        bevy_tweening::EaseFunction::ElasticInOut,
-        bevy_tweening::EaseFunction::BackIn,
-        bevy_tweening::EaseFunction::BackOut,
-        bevy_tweening::EaseFunction::BackInOut,
-        bevy_tweening::EaseFunction::BounceIn,
-        bevy_tweening::EaseFunction::BounceOut,
-        bevy_tweening::EaseFunction::BounceInOut,
+        EaseFunction::QuadraticIn,
+        EaseFunction::QuadraticOut,
+        EaseFunction::QuadraticInOut,
+        EaseFunction::CubicIn,
+        EaseFunction::CubicOut,
+        EaseFunction::CubicInOut,
+        EaseFunction::QuarticIn,
+        EaseFunction::QuarticOut,
+        EaseFunction::QuarticInOut,
+        EaseFunction::QuinticIn,
+        EaseFunction::QuinticOut,
+        EaseFunction::QuinticInOut,
+        EaseFunction::SineIn,
+        EaseFunction::SineOut,
+        EaseFunction::SineInOut,
+        EaseFunction::CircularIn,
+        EaseFunction::CircularOut,
+        EaseFunction::CircularInOut,
+        EaseFunction::ExponentialIn,
+        EaseFunction::ExponentialOut,
+        EaseFunction::ExponentialInOut,
+        EaseFunction::ElasticIn,
+        EaseFunction::ElasticOut,
+        EaseFunction::ElasticInOut,
+        EaseFunction::BackIn,
+        EaseFunction::BackOut,
+        EaseFunction::BackInOut,
+        EaseFunction::BounceIn,
+        EaseFunction::BounceOut,
+        EaseFunction::BounceInOut,
     ] {
+        let tween = Tween::new(
+            *ease_function,
+            TweeningType::PingPong,
+            std::time::Duration::from_secs(1),
+            UiPositionLens {
+                start: Rect {
+                    left: Val::Px(x),
+                    top: Val::Px(10.),
+                    right: Val::Auto,
+                    bottom: Val::Auto,
+                },
+                end: Rect {
+                    left: Val::Px(x),
+                    top: Val::Px(screen_y - 10. - size),
+                    right: Val::Auto,
+                    bottom: Val::Auto,
+                },
+            },
+        );
+
         commands
             .spawn_bundle(NodeBundle {
                 style: Style {
@@ -79,25 +100,8 @@ fn setup(mut commands: Commands) {
                 color: UiColor(Color::RED),
                 ..Default::default()
             })
-            .insert(bevy_tweening::Animator::new(
-                *ease_function,
-                bevy_tweening::TweeningType::PingPong,
-                std::time::Duration::from_secs(1),
-                bevy_tweening::UiPositionLens {
-                    start: Rect {
-                        left: Val::Px(x),
-                        top: Val::Px(10.),
-                        right: Val::Auto,
-                        bottom: Val::Auto,
-                    },
-                    end: Rect {
-                        left: Val::Px(x),
-                        top: Val::Px(screen_y - 10. - size),
-                        right: Val::Auto,
-                        bottom: Val::Auto,
-                    },
-                },
-            ));
+            .insert(Animator::new(tween));
+
         x += offset_x;
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index 78e82e7..5ee6767 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -37,8 +37,26 @@
 //! # use std::time::Duration;
 //! # fn system(mut commands: Commands) {
 //! # let size = 16.;
+//! // Create a single animation (tween) to move an entity.
+//! let tween = Tween::new(
+//!     // Use a quadratic easing on both endpoints.
+//!     EaseFunction::QuadraticInOut,
+//!     // Loop animation back and forth.
+//!     TweeningType::PingPong,
+//!     // Animation time (one way only; for ping-pong it takes 2 seconds
+//!     // to come back to start).
+//!     Duration::from_secs(1),
+//!     // The lens gives access to the Transform component of the Sprite,
+//!     // for the Animator to animate it. It also contains the start and
+//!     // end values associated with the animation ratios 0. and 1.
+//!     TransformPositionLens {
+//!         start: Vec3::new(0., 0., 0.),
+//!         end: Vec3::new(1., 2., -4.),
+//!     },
+//! );
+//! 
 //! commands
-//!     // Spawn a Sprite entity to animate the position of
+//!     // Spawn a Sprite entity to animate the position of.
 //!     .spawn_bundle(SpriteBundle {
 //!         sprite: Sprite {
 //!             color: Color::RED,
@@ -47,23 +65,8 @@
 //!         },
 //!         ..Default::default()
 //!     })
-//!     // Add an Animator component to perform the animation
-//!     .insert(Animator::new(
-//!         // Use a quadratic easing on both endpoints
-//!         EaseFunction::QuadraticInOut,
-//!         // Loop animation back and forth
-//!         TweeningType::PingPong,
-//!         // Animation time (one way only; for ping-pong it takes 2 seconds
-//!         // to come back to start)
-//!         Duration::from_secs(1),
-//!         // The lens gives access to the Transform component of the Sprite,
-//!         // for the Animator to animate it. It also contains the start and
-//!         // end values associated with the animation ratios 0. and 1.
-//!         TransformPositionLens {
-//!             start: Vec3::new(0., 0., 0.),
-//!             end: Vec3::new(1., 2., -4.),
-//!         },
-//!     ));
+//!     // Add an Animator component to control and execute the animation.
+//!     .insert(Animator::new(tween));
 //! # }
 //! ```
 //!
@@ -218,12 +221,58 @@ impl std::ops::Not for TweeningDirection {
     }
 }
 
+/// Playback state of a [`Tweenable`].
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum TweenState {
+pub enum TweenState {
     /// Not animated.
     Stopped,
     /// Animating.
     Running,
+    /// Animation ended (but stop not called).
+    Ended,
+}
+
+/// An animatable entity, either a single [`Tween`] or a collection of them.
+pub trait Tweenable<T>: Send + Sync {
+    /// Get the total duration of the animation.
+    fn duration(&self) -> Duration;
+
+    /// Get the current progress in \[0:1\] of the animation.
+    fn progress(&self) -> f32;
+
+    /// Tick the animation, advancing it by the given delta time and mutating the
+    /// given target component or asset.
+    fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState;
+
+    /// Stop the animation.
+    fn stop(&mut self);
+}
+
+impl<T> Tweenable<T> for Box<dyn Tweenable<T> + Send + Sync + 'static> {
+    fn duration(&self) -> Duration {
+        self.as_ref().duration()
+    }
+    fn progress(&self) -> f32 {
+        self.as_ref().progress()
+    }
+    fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState {
+        self.as_mut().tick(delta, target)
+    }
+    fn stop(&mut self) {
+        self.as_mut().stop()
+    }
+}
+
+/// Trait for boxing a [`Tweenable`] trait object.
+pub trait IntoBoxDynTweenable<T> {
+    /// Convert the current object into a boxed [`Tweenable`].
+    fn into_box_dyn(this: Self) -> Box<dyn Tweenable<T> + Send + Sync + 'static>;
+}
+
+impl<T, U: Tweenable<T> + Send + Sync + 'static> IntoBoxDynTweenable<T> for U {
+    fn into_box_dyn(this: U) -> Box<dyn Tweenable<T> + Send + Sync + 'static> {
+        Box::new(this)
+    }
 }
 
 /// Single tweening animation instance.
@@ -238,6 +287,13 @@ pub struct Tween<T> {
     on_ended: Option<Box<dyn FnMut() + Send + Sync + 'static>>,
 }
 
+impl<T: 'static> Tween<T> {
+    /// Chain another [`Tweenable`] after this tween, making a sequence with the two.
+    pub fn then(self, tween: impl Tweenable<T> + Send + Sync + 'static) -> Sequence<T> {
+        Sequence::from_single(self).then(tween)
+    }
+}
+
 impl<T> Tween<T> {
     /// Create a new tween animation.
     pub fn new<L>(
@@ -268,18 +324,6 @@ impl<T> Tween<T> {
         self.direction
     }
 
-    /// Current animation progress ratio between 0 and 1.
-    ///
-    /// For reversed playback ([`TweeningDirection::Backward`]), the ratio goes from 0 at the
-    /// end point (beginning of backward playback) to 1 at the start point (end of backward
-    /// playback).
-    pub fn progress(&self) -> f32 {
-        match self.direction {
-            TweeningDirection::Forward => self.timer.percent(),
-            TweeningDirection::Backward => self.timer.percent_left(),
-        }
-    }
-
     /// Set a callback invoked when the animation starts.
     pub fn set_started<C>(&mut self, callback: C)
     where
@@ -310,8 +354,26 @@ impl<T> Tween<T> {
     pub fn is_looping(&self) -> bool {
         self.tweening_type != TweeningType::Once
     }
+}
 
-    fn tick(&mut self, delta: Duration, target: &mut T) {
+impl<T> Tweenable<T> for Tween<T> {
+    fn duration(&self) -> Duration {
+        self.timer.duration()
+    }
+
+    /// Current animation progress ratio between 0 and 1.
+    ///
+    /// For reversed playback ([`TweeningDirection::Backward`]), the ratio goes from 0 at the
+    /// end point (beginning of backward playback) to 1 at the start point (end of backward
+    /// playback).
+    fn progress(&self) -> f32 {
+        match self.direction {
+            TweeningDirection::Forward => self.timer.percent(),
+            TweeningDirection::Backward => self.timer.percent_left(),
+        }
+    }
+
+    fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState {
         let old_state = self.state;
         if old_state == TweenState::Stopped {
             self.state = TweenState::Running;
@@ -332,6 +394,7 @@ impl<T> Tween<T> {
         self.lens.lerp(target, factor);
 
         if self.timer.just_finished() {
+            self.state = TweenState::Ended;
             // This is always true for non ping-pong, and is true for ping-pong when
             // coming back to start after a full cycle start -> end -> start.
             if self.direction == TweeningDirection::Forward {
@@ -340,67 +403,99 @@ impl<T> Tween<T> {
                 }
             }
         }
+
+        self.state
     }
 
     fn stop(&mut self) {
-        if self.state == TweenState::Running {
-            self.state = TweenState::Stopped;
-            self.timer.reset();
-        }
+        self.state = TweenState::Stopped;
+        self.timer.reset();
     }
 }
 
 /// A sequence of tweens played back in order one after the other.
 pub struct Sequence<T> {
-    tweens: Vec<Tween<T>>,
+    tweens: Vec<Box<dyn Tweenable<T> + Send + Sync + 'static>>,
     index: usize,
     state: TweenState,
+    duration: Duration,
+    time: Duration,
 }
 
 impl<T> Sequence<T> {
     /// Create a new sequence of tweens.
-    pub fn new<I>(tweens: I) -> Self
-    where
-        I: IntoIterator<Item = Tween<T>>,
-    {
+    pub fn new(items: impl IntoIterator<Item = impl IntoBoxDynTweenable<T>>) -> Self {
+        let tweens: Vec<_> = items
+            .into_iter()
+            .map(IntoBoxDynTweenable::into_box_dyn)
+            .collect();
+        let duration = tweens.iter().map(|t| t.duration()).sum();
         Sequence {
-            tweens: tweens.into_iter().collect(),
+            tweens,
             index: 0,
             state: TweenState::Stopped,
+            duration,
+            time: Duration::from_secs(0),
         }
     }
 
     /// Create a new sequence containing a single tween.
-    pub fn from_single(tween: Tween<T>) -> Self {
+    pub fn from_single(tween: impl Tweenable<T> + Send + Sync + 'static) -> Self {
+        let duration = tween.duration();
         Sequence {
-            tweens: vec![tween],
+            tweens: vec![Box::new(tween)],
             index: 0,
             state: TweenState::Stopped,
+            duration,
+            time: Duration::from_secs(0),
         }
     }
 
+    /// Append a [`Tweenable`] to this sequence.
+    pub fn then(mut self, tween: impl Tweenable<T> + Send + Sync + 'static) -> Self {
+        self.duration += tween.duration();
+        self.tweens.push(Box::new(tween));
+        self
+    }
+
     /// Index of the current active tween in the sequence.
     pub fn index(&self) -> usize {
         self.index.min(self.tweens.len() - 1)
     }
 
     /// Get the current active tween in the sequence.
-    pub fn current(&self) -> &Tween<T> {
-        &self.tweens[self.index()]
+    pub fn current(&self) -> &dyn Tweenable<T> {
+        self.tweens[self.index()].as_ref()
+    }
+}
+
+impl<T> Tweenable<T> for Sequence<T> {
+    fn duration(&self) -> Duration {
+        self.duration
+    }
+
+    fn progress(&self) -> f32 {
+        self.time.as_secs_f32() / self.duration.as_secs_f32()
     }
 
-    fn tick(&mut self, delta: Duration, target: &mut T) {
+    fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState {
         if self.index < self.tweens.len() {
+            self.time = (self.time + delta).min(self.duration);
             let tween = &mut self.tweens[self.index];
-            tween.tick(delta, target);
-            if tween.progress() >= 1.0 {
+            let state = tween.tick(delta, target);
+            if state == TweenState::Ended {
+                tween.stop();
                 self.index += 1;
+                if self.index >= self.tweens.len() {
+                    self.state = TweenState::Ended;
+                }
             }
         }
+        self.state
     }
 
     fn stop(&mut self) {
-        if self.state == TweenState::Running {
+        if self.state != TweenState::Stopped {
             self.state = TweenState::Stopped;
             if self.index < self.tweens.len() {
                 let tween = &mut self.tweens[self.index];
@@ -410,8 +505,56 @@ impl<T> Sequence<T> {
     }
 }
 
-struct Tracks<T> {
-    tracks: Vec<Sequence<T>>,
+/// A collection of [`Tweenable`] executing in parallel.
+pub struct Tracks<T> {
+    tracks: Vec<Box<dyn Tweenable<T> + Send + Sync + 'static>>,
+    duration: Duration,
+    time: Duration,
+}
+
+impl<T> Tracks<T> {
+    /// Create a new [`Tracks`] from an iterator over a collection of [`Tweenable`].
+    pub fn new(items: impl IntoIterator<Item = impl IntoBoxDynTweenable<T>>) -> Self {
+        let tracks: Vec<_> = items
+            .into_iter()
+            .map(IntoBoxDynTweenable::into_box_dyn)
+            .collect();
+        let duration = tracks.iter().map(|t| t.duration()).max().unwrap();
+        Tracks {
+            tracks,
+            duration,
+            time: Duration::from_secs(0),
+        }
+    }
+}
+
+impl<T> Tweenable<T> for Tracks<T> {
+    fn duration(&self) -> Duration {
+        self.duration
+    }
+
+    fn progress(&self) -> f32 {
+        self.time.as_secs_f32() / self.duration.as_secs_f32()
+    }
+
+    fn tick(&mut self, delta: Duration, target: &mut T) -> TweenState {
+        let mut any_running = true;
+        for tweenable in &mut self.tracks {
+            any_running = any_running && (tweenable.tick(delta, target) == TweenState::Running);
+        }
+        if any_running {
+            self.time = (self.time + delta).min(self.duration);
+            TweenState::Running
+        } else {
+            TweenState::Ended
+        }
+    }
+
+    fn stop(&mut self) {
+        for seq in &mut self.tracks {
+            seq.stop();
+        }
+    }
 }
 
 /// Component to control the animation of another component.
@@ -420,7 +563,7 @@ pub struct Animator<T: Component> {
     /// Control if this animation is played or not.
     pub state: AnimatorState,
     prev_state: AnimatorState,
-    tracks: Tracks<T>,
+    tweenable: Option<Box<dyn Tweenable<T> + Send + Sync + 'static>>,
 }
 
 impl<T: Component + std::fmt::Debug> std::fmt::Debug for Animator<T> {
@@ -431,52 +574,22 @@ impl<T: Component + std::fmt::Debug> std::fmt::Debug for Animator<T> {
     }
 }
 
-impl<T: Component> Animator<T> {
-    /// Create a new animator component from an easing function, tweening type, and a lens.
-    /// The type `T` of the component to animate can generally be deducted from the lens type itself.
-    /// This creates a new [`Tween`] instance then assign it to a newly created animator.
-    pub fn new<L>(
-        ease_function: impl Into<EaseMethod>,
-        tweening_type: TweeningType,
-        duration: Duration,
-        lens: L,
-    ) -> Self
-    where
-        L: Lens<T> + Send + Sync + 'static,
-    {
-        let tween = Tween::new(ease_function, tweening_type, duration, lens);
-        Animator {
-            state: AnimatorState::default(),
-            prev_state: AnimatorState::default(),
-            tracks: Tracks {
-                tracks: vec![Sequence::from_single(tween)],
-            },
-        }
-    }
-
-    /// Create a new animator component from a single tween instance.
-    pub fn new_single(tween: Tween<T>) -> Self {
+impl<T: Component> Default for Animator<T> {
+    fn default() -> Self {
         Animator {
-            state: AnimatorState::default(),
-            prev_state: AnimatorState::default(),
-            tracks: Tracks {
-                tracks: vec![Sequence::from_single(tween)],
-            },
+            state: Default::default(),
+            prev_state: Default::default(),
+            tweenable: None,
         }
     }
+}
 
-    /// Create a new animator component from a sequence of tween instances.
-    /// The tweens are played in order, one after the other. They all must be non-looping.
-    pub fn new_seq(tweens: Vec<Tween<T>>) -> Self {
-        for t in &tweens {
-            assert!(matches!(t.tweening_type, TweeningType::Once { .. }));
-        }
+impl<T: Component> Animator<T> {
+    /// Create a new animator component from a single [`Tween`] or [`Sequence`].
+    pub fn new(tween: impl Tweenable<T> + Send + Sync + 'static) -> Self {
         Animator {
-            state: AnimatorState::Playing,
-            prev_state: AnimatorState::Playing,
-            tracks: Tracks {
-                tracks: vec![Sequence::new(tweens)],
-            },
+            tweenable: Some(Box::new(tween)),
+            ..Default::default()
         }
     }
 
@@ -487,14 +600,27 @@ impl<T: Component> Animator<T> {
         self
     }
 
+    /// Set the top-level tweenable item this animator controls.
+    pub fn set_tweenable(&mut self, tween: impl Tweenable<T> + Send + Sync + 'static) {
+        self.tweenable = Some(Box::new(tween));
+    }
+
     /// Get the collection of sequences forming the parallel tracks of animation.
-    pub fn tracks(&self) -> &[Sequence<T>] {
-        &self.tracks.tracks
+    pub fn tweenable(&self) -> Option<&(dyn Tweenable<T> + Send + Sync + 'static)> {
+        if let Some(tweenable) = &self.tweenable {
+            Some(tweenable.as_ref())
+        } else {
+            None
+        }
     }
 
     /// Get the mutable collection of sequences forming the parallel tracks of animation.
-    pub fn tracks_mut(&mut self) -> &mut [Sequence<T>] {
-        &mut self.tracks.tracks
+    pub fn tweenable_mut(&mut self) -> Option<&mut (dyn Tweenable<T> + Send + Sync + 'static)> {
+        if let Some(tweenable) = &mut self.tweenable {
+            Some(tweenable.as_mut())
+        } else {
+            None
+        }
     }
 }
 
@@ -504,7 +630,7 @@ pub struct AssetAnimator<T: Asset> {
     /// Control if this animation is played or not.
     pub state: AnimatorState,
     prev_state: AnimatorState,
-    tracks: Tracks<T>,
+    tweenable: Option<Box<dyn Tweenable<T> + Send + Sync + 'static>>,
     handle: Handle<T>,
 }
 
@@ -516,56 +642,24 @@ impl<T: Asset + std::fmt::Debug> std::fmt::Debug for AssetAnimator<T> {
     }
 }
 
-impl<T: Asset> AssetAnimator<T> {
-    /// Create a new asset animator component from an easing function, tweening type, and a lens.
-    /// The type `T` of the asset to animate can generally be deducted from the lens type itself.
-    /// The component can be attached on any entity.
-    pub fn new<L>(
-        handle: Handle<T>,
-        ease_function: impl Into<EaseMethod>,
-        tweening_type: TweeningType,
-        duration: Duration,
-        lens: L,
-    ) -> Self
-    where
-        L: Lens<T> + Send + Sync + 'static,
-    {
-        let tween = Tween::new(ease_function, tweening_type, duration, lens);
-        AssetAnimator {
-            state: AnimatorState::Playing,
-            prev_state: AnimatorState::Playing,
-            tracks: Tracks {
-                tracks: vec![Sequence::from_single(tween)],
-            },
-            handle,
-        }
-    }
-
-    /// Create a new animator component from a single tween instance.
-    pub fn new_single(handle: Handle<T>, tween: Tween<T>) -> Self {
+impl<T: Asset> Default for AssetAnimator<T> {
+    fn default() -> Self {
         AssetAnimator {
-            state: AnimatorState::Playing,
-            prev_state: AnimatorState::Playing,
-            tracks: Tracks {
-                tracks: vec![Sequence::from_single(tween)],
-            },
-            handle,
+            state: Default::default(),
+            prev_state: Default::default(),
+            tweenable: None,
+            handle: Default::default(),
         }
     }
+}
 
-    /// Create a new animator component from a sequence of tween instances.
-    /// The tweens are played in order, one after the other. They all must be non-looping.
-    pub fn new_seq(handle: Handle<T>, tweens: Vec<Tween<T>>) -> Self {
-        for t in &tweens {
-            assert!(matches!(t.tweening_type, TweeningType::Once { .. }));
-        }
+impl<T: Asset> AssetAnimator<T> {
+    /// Create a new animator component from a single [`Tween`] or [`Sequence`].
+    pub fn new(handle: Handle<T>, tween: impl Tweenable<T> + Send + Sync + 'static) -> Self {
         AssetAnimator {
-            state: AnimatorState::Playing,
-            prev_state: AnimatorState::Playing,
-            tracks: Tracks {
-                tracks: vec![Sequence::new(tweens)],
-            },
+            tweenable: Some(Box::new(tween)),
             handle,
+            ..Default::default()
         }
     }
 
@@ -576,18 +670,31 @@ impl<T: Asset> AssetAnimator<T> {
         self
     }
 
-    fn handle(&self) -> Handle<T> {
-        self.handle.clone()
+    /// Set the top-level tweenable item this animator controls.
+    pub fn set_tweenable(&mut self, tween: impl Tweenable<T> + Send + Sync + 'static) {
+        self.tweenable = Some(Box::new(tween));
     }
 
     /// Get the collection of sequences forming the parallel tracks of animation.
-    pub fn tracks(&self) -> &[Sequence<T>] {
-        &self.tracks.tracks
+    pub fn tweenable(&self) -> Option<&(dyn Tweenable<T> + Send + Sync + 'static)> {
+        if let Some(tweenable) = &self.tweenable {
+            Some(tweenable.as_ref())
+        } else {
+            None
+        }
     }
 
     /// Get the mutable collection of sequences forming the parallel tracks of animation.
-    pub fn tracks_mut(&mut self) -> &mut [Sequence<T>] {
-        &mut self.tracks.tracks
+    pub fn tweenable_mut(&mut self) -> Option<&mut (dyn Tweenable<T> + Send + Sync + 'static)> {
+        if let Some(tweenable) = &mut self.tweenable {
+            Some(tweenable.as_mut())
+        } else {
+            None
+        }
+    }
+
+    fn handle(&self) -> Handle<T> {
+        self.handle.clone()
     }
 }
 
@@ -668,6 +775,7 @@ mod tests {
                         (r, ec, dir)
                     }
                 };
+                println!("Expected; r={} ec={} dir={:?}", ratio, ec, dir);
 
                 // Tick the tween
                 tween.tick(tick_duration, &mut transform);
@@ -704,7 +812,7 @@ mod tests {
                 end: Quat::from_rotation_x(180_f32.to_radians()),
             },
         );
-        let mut seq = Sequence::new([tween1, tween2]);
+        let mut seq = Sequence::from_single(tween1).then(tween2);
         let mut transform = Transform::default();
         for i in 1..=11 {
             seq.tick(Duration::from_secs_f32(0.2), &mut transform);
@@ -729,7 +837,7 @@ mod tests {
     /// Animator::new()
     #[test]
     fn animator_new() {
-        let animator = Animator::new(
+        let tween = Tween::new(
             EaseFunction::QuadraticInOut,
             TweeningType::PingPong,
             std::time::Duration::from_secs(1),
@@ -738,21 +846,16 @@ mod tests {
                 end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.),
             },
         );
+        let animator = Animator::new(tween);
         assert_eq!(animator.state, AnimatorState::default());
-        let tracks = animator.tracks();
-        assert_eq!(tracks.len(), 1);
-        let seq = &tracks[0];
-        assert_eq!(seq.tweens.len(), 1);
-        let tween = &seq.tweens[0];
-        assert_eq!(tween.direction(), TweeningDirection::Forward);
+        let tween = animator.tweenable().unwrap();
         assert_eq!(tween.progress(), 0.);
     }
 
     /// AssetAnimator::new()
     #[test]
     fn asset_animator_new() {
-        let animator = AssetAnimator::new(
-            Handle::<ColorMaterial>::default(),
+        let tween = Tween::new(
             EaseFunction::QuadraticInOut,
             TweeningType::PingPong,
             std::time::Duration::from_secs(1),
@@ -761,13 +864,9 @@ mod tests {
                 end: Color::BLUE,
             },
         );
+        let animator = AssetAnimator::new(Handle::<ColorMaterial>::default(), tween);
         assert_eq!(animator.state, AnimatorState::default());
-        let tracks = animator.tracks();
-        assert_eq!(tracks.len(), 1);
-        let seq = &tracks[0];
-        assert_eq!(seq.tweens.len(), 1);
-        let tween = &seq.tweens[0];
-        assert_eq!(tween.direction(), TweeningDirection::Forward);
+        let tween = animator.tweenable().unwrap();
         assert_eq!(tween.progress(), 0.);
     }
 }
diff --git a/src/plugin.rs b/src/plugin.rs
index 6b91506..f344d7c 100644
--- a/src/plugin.rs
+++ b/src/plugin.rs
@@ -54,15 +54,12 @@ pub fn component_animator_system<T: Component>(
         animator.prev_state = animator.state;
         if animator.state == AnimatorState::Paused {
             if state_changed {
-                for seq in animator.tracks_mut() {
-                    seq.stop();
+                if let Some(tweenable) = animator.tweenable_mut() {
+                    tweenable.stop();
                 }
             }
-        } else {
-            // Play all tracks in parallel
-            for seq in animator.tracks_mut() {
-                seq.tick(time.delta(), target);
-            }
+        } else if let Some(tweenable) = animator.tweenable_mut() {
+            tweenable.tick(time.delta(), target);
         }
     }
 }
@@ -80,14 +77,13 @@ pub fn asset_animator_system<T: Asset>(
         animator.prev_state = animator.state;
         if animator.state == AnimatorState::Paused {
             if state_changed {
-                for seq in animator.tracks_mut() {
-                    seq.stop();
+                if let Some(tweenable) = animator.tweenable_mut() {
+                    tweenable.stop();
                 }
             }
         } else if let Some(target) = assets.get_mut(animator.handle()) {
-            // Play all tracks in parallel
-            for seq in animator.tracks_mut() {
-                seq.tick(time.delta(), target);
+            if let Some(tweenable) = animator.tweenable_mut() {
+                tweenable.tick(time.delta(), target);
             }
         }
     }
-- 
GitLab