diff --git a/CHANGELOG.md b/CHANGELOG.md
index a3a5d5cbb5e0fe8e3c841da99a82ca1eb6e5cf70..e1e32758f5aab30d6d2cbfd7fd6278d3505bf960 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,13 @@
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [Unreleased]
+
+### Added
+
+- Added `RepeatCount` and `RepeatStrategy` for more granular control over animation looping.
+- Added `with_repeat_count()` and `with_repeat_strategy()` builder methods to `Tween<T>`.
+
 ## [0.5.0] - 2022-08-04
 
 ### Added
@@ -18,6 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Compatible with Bevy 0.8
 - Double boxing in `Sequence` and `Tracks` was fixed. As a result, any custom tweenables
   should implement `From` for `BoxedTweenable` to make those APIs easier to use.
+- Removed the `tweening_type` parameter from the signature of `Tween<T>::new()`; use `with_repeat_count()` and `with_repeat_strategy()` instead.
+
+### Removed
+
+- Removed `Tweenable::is_looping()`, which was not implemented for most tweenables.
+- Removed `TweeningType` in favor of `RepeatCount` and `RepeatStrategy`.
 
 ## [0.4.0] - 2022-04-16
 
diff --git a/examples/colormaterial_color.rs b/examples/colormaterial_color.rs
index edf90e3634cc6dc29477d1bed77baab93ead13ef..391bfc706ab053904e10a09165ecb17865095e39 100644
--- a/examples/colormaterial_color.rs
+++ b/examples/colormaterial_color.rs
@@ -77,13 +77,14 @@ fn setup(
 
         let tween = Tween::new(
             *ease_function,
-            TweeningType::PingPong,
             Duration::from_secs(1),
             ColorMaterialColorLens {
                 start: Color::RED,
                 end: Color::BLUE,
             },
-        );
+        )
+        .with_repeat_count(RepeatCount::Infinite)
+        .with_repeat_strategy(RepeatStrategy::MirroredRepeat);
 
         commands
             .spawn_bundle(MaterialMesh2dBundle {
diff --git a/examples/menu.rs b/examples/menu.rs
index 3c0fefd59b38d870264a51038264d2aaf544c125..fccc6f7503fd92a2167895e185dbf0c7d246909d 100644
--- a/examples/menu.rs
+++ b/examples/menu.rs
@@ -49,7 +49,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
                 start_time_ms += 500;
                 let tween_scale = Tween::new(
                     EaseFunction::BounceOut,
-                    TweeningType::Once,
                     Duration::from_secs(2),
                     TransformScaleLens {
                         start: Vec3::splat(0.01),
diff --git a/examples/sequence.rs b/examples/sequence.rs
index 9ed183f0148cc9777b0eeb96dd60b11ed358eb3b..6526df0808baedc1ef1247af10d4d112de5419f5 100644
--- a/examples/sequence.rs
+++ b/examples/sequence.rs
@@ -1,6 +1,8 @@
+use std::time::Duration;
+
 use bevy::prelude::*;
+
 use bevy_tweening::{lens::*, *};
-use std::time::Duration;
 
 fn main() {
     App::default()
@@ -107,19 +109,31 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
         Vec3::new(margin, screen_y - margin, 0.),
         Vec3::new(margin, margin, 0.),
     ];
-    // Build a sequence from an iterator over a Tweenable (here, a Tween<Transform>)
+    // Build a sequence from an iterator over a Tweenable (here, a
+    // Tracks<Transform>)
     let seq = Sequence::new(dests.windows(2).enumerate().map(|(index, pair)| {
-        Tween::new(
-            EaseFunction::QuadraticInOut,
-            TweeningType::Once,
-            Duration::from_secs(1),
-            TransformPositionLens {
-                start: pair[0] - center,
-                end: pair[1] - center,
-            },
-        )
-        // Get an event after each segment
-        .with_completed_event(index as u64)
+        Tracks::new([
+            Tween::new(
+                EaseFunction::QuadraticInOut,
+                Duration::from_millis(250),
+                TransformRotateZLens {
+                    start: 0.,
+                    end: 180_f32.to_radians(),
+                },
+            )
+            .with_repeat_count(RepeatCount::Finite(4))
+            .with_repeat_strategy(RepeatStrategy::MirroredRepeat),
+            Tween::new(
+                EaseFunction::QuadraticInOut,
+                Duration::from_secs(1),
+                TransformPositionLens {
+                    start: pair[0] - center,
+                    end: pair[1] - center,
+                },
+            )
+            // Get an event after each segment
+            .with_completed_event(index as u64),
+        ])
     }));
 
     commands
@@ -138,7 +152,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
     // 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.),
@@ -148,7 +161,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
     .with_completed_event(99); // Get an event once move completed
     let tween_rotate = Tween::new(
         EaseFunction::QuadraticInOut,
-        TweeningType::Once,
         Duration::from_secs(1),
         TransformRotationLens {
             start: Quat::IDENTITY,
@@ -157,7 +169,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
     );
     let tween_scale = Tween::new(
         EaseFunction::QuadraticInOut,
-        TweeningType::Once,
         Duration::from_secs(1),
         TransformScaleLens {
             start: Vec3::ONE,
diff --git a/examples/sprite_color.rs b/examples/sprite_color.rs
index e42aa4e21ff32358297aec7754289eba498688cd..a85d3bb83a0ef59e84de11b1c2e4151b69bab464 100644
--- a/examples/sprite_color.rs
+++ b/examples/sprite_color.rs
@@ -61,13 +61,14 @@ fn setup(mut commands: Commands) {
     ] {
         let tween = Tween::new(
             *ease_function,
-            TweeningType::PingPong,
             std::time::Duration::from_secs(1),
             SpriteColorLens {
                 start: Color::RED,
                 end: Color::BLUE,
             },
-        );
+        )
+        .with_repeat_count(RepeatCount::Infinite)
+        .with_repeat_strategy(RepeatStrategy::MirroredRepeat);
 
         commands
             .spawn_bundle(SpriteBundle {
diff --git a/examples/text_color.rs b/examples/text_color.rs
index a1d3e9fbb68768f201707d609775897df51acbb2..e02d8349d7499af1dfe7926f1a037d988dd9b7e2 100644
--- a/examples/text_color.rs
+++ b/examples/text_color.rs
@@ -68,14 +68,15 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
     ] {
         let tween = Tween::new(
             *ease_function,
-            TweeningType::PingPong,
             std::time::Duration::from_secs(1),
             TextColorLens {
                 start: Color::RED,
                 end: Color::BLUE,
                 section: 0,
             },
-        );
+        )
+        .with_repeat_count(RepeatCount::Infinite)
+        .with_repeat_strategy(RepeatStrategy::MirroredRepeat);
 
         commands
             .spawn_bundle(TextBundle {
diff --git a/examples/transform_rotation.rs b/examples/transform_rotation.rs
index aa68b87a1e58e2ad0f882f901595f5269507b5d3..8efa704816c5ccb83ff32b8d0ec8f8ad053b3b88 100644
--- a/examples/transform_rotation.rs
+++ b/examples/transform_rotation.rs
@@ -77,13 +77,14 @@ fn setup(mut commands: Commands) {
     ] {
         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.),
             },
-        );
+        )
+        .with_repeat_count(RepeatCount::Infinite)
+        .with_repeat_strategy(RepeatStrategy::MirroredRepeat);
 
         commands
             .spawn_bundle(SpatialBundle {
diff --git a/examples/transform_translation.rs b/examples/transform_translation.rs
index 29088b0e675a31b95351fcbdc614a569354f3901..85b52b44531389ff135eebc1ee1a024695007bb1 100644
--- a/examples/transform_translation.rs
+++ b/examples/transform_translation.rs
@@ -76,13 +76,14 @@ fn setup(mut commands: Commands) {
     ] {
         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.),
             },
-        );
+        )
+        .with_repeat_count(RepeatCount::Infinite)
+        .with_repeat_strategy(RepeatStrategy::MirroredRepeat);
 
         commands
             .spawn_bundle(SpriteBundle {
diff --git a/examples/ui_position.rs b/examples/ui_position.rs
index 236b6fbd5cb74cd8146accbed029ee4aaded1d77..928e0213de9b026a8010a6bf71b42fb41d887473 100644
--- a/examples/ui_position.rs
+++ b/examples/ui_position.rs
@@ -76,7 +76,6 @@ fn setup(mut commands: Commands) {
     ] {
         let tween = Tween::new(
             *ease_function,
-            TweeningType::PingPong,
             std::time::Duration::from_secs(1),
             UiPositionLens {
                 start: UiRect {
@@ -92,7 +91,9 @@ fn setup(mut commands: Commands) {
                     bottom: Val::Auto,
                 },
             },
-        );
+        )
+        .with_repeat_count(RepeatCount::Infinite)
+        .with_repeat_strategy(RepeatStrategy::MirroredRepeat);
 
         commands
             .spawn_bundle(NodeBundle {
diff --git a/src/lib.rs b/src/lib.rs
index 84ac75597ff73616d734088af10afccef381e68e..551b92e3f8ddf4233646f8df95231b2d697c46fa 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -44,10 +44,7 @@
 //! 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).
+//!     // Animation time.
 //!     Duration::from_secs(1),
 //!     // The lens gives access to the Transform component of the Entity,
 //!     // for the Animator to animate it. It also contains the start and
@@ -90,7 +87,6 @@
 //! let tween1 = Tween::new(
 //!     // [...]
 //! #    EaseFunction::BounceOut,
-//! #    TweeningType::Once,
 //! #    Duration::from_secs(2),
 //! #    TransformScaleLens {
 //! #        start: Vec3::ZERO,
@@ -100,7 +96,6 @@
 //! let tween2 = Tween::new(
 //!     // [...]
 //! #    EaseFunction::QuadraticInOut,
-//! #    TweeningType::Once,
 //! #    Duration::from_secs(1),
 //! #    TransformPositionLens {
 //! #        start: Vec3::ZERO,
@@ -178,24 +173,43 @@ pub use tweenable::{
 #[cfg(feature = "bevy_asset")]
 pub use plugin::asset_animator_system;
 
-/// Type of looping for a tween animation.
+/// How many times to repeat a tween animation. See also: [`RepeatStrategy`].
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum TweeningType {
-    /// Run the animation once from start to end only.
-    Once,
-    /// Loop the animation indefinitely, restarting from the start each time the
-    /// end is reached.
-    Loop,
-    /// Loop the animation back and forth, changing direction each time an
-    /// endpoint is reached. A complete cycle start -> end -> start always
-    /// counts as 2 loop iterations for the various operations where looping
-    /// matters.
-    PingPong,
+pub enum RepeatCount {
+    /// Run the animation N times.
+    Finite(u32),
+    /// Run the animation for some amount of time.
+    For(Duration),
+    /// Loop the animation indefinitely.
+    Infinite,
 }
 
-impl Default for TweeningType {
+/// What to do when a tween animation needs to be repeated.
+///
+/// Only applicable when [`RepeatCount`] is greater than the animation duration.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RepeatStrategy {
+    /// Reset the animation back to its starting position.
+    Repeat,
+    /// Follow a ping-pong pattern, changing the direction each time an endpoint
+    /// is reached.
+    ///
+    /// A complete cycle start -> end -> start always counts as 2 loop
+    /// iterations for the various operations where looping matters. That
+    /// is, a 1 second animation will take 2 seconds to end up back where it
+    /// started.
+    MirroredRepeat,
+}
+
+impl Default for RepeatCount {
     fn default() -> Self {
-        Self::Once
+        Self::Finite(1)
+    }
+}
+
+impl Default for RepeatStrategy {
+    fn default() -> Self {
+        Self::Repeat
     }
 }
 
@@ -279,12 +293,12 @@ impl From<EaseFunction> for EaseMethod {
 /// that target at the start bound of the lens, effectively making the animation
 /// play backward.
 ///
-/// For all but [`TweeningType::PingPong`] this is always
+/// For all but [`RepeatStrategy::MirroredRepeat`] this is always
 /// [`TweeningDirection::Forward`], unless manually configured with
-/// [`Tween::set_direction()`] in which case the value is constant equal
-/// to the value set. For the [`TweeningType::PingPong`] tweening type, this is
-/// either forward (from start to end; ping) or backward (from end to start;
-/// pong), depending on the current iteration of the loop.
+/// [`Tween::set_direction()`] in which case the value is constant equal to the
+/// value set. When using [`RepeatStrategy::MirroredRepeat`], this is either
+/// forward (from start to end; ping) or backward (from end to start; pong),
+/// depending on the current iteration of the loop.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum TweeningDirection {
     /// Animation playing from start to end.
@@ -385,19 +399,8 @@ macro_rules! animator_impl {
             }
         }
 
-        /// Get the current progress in \[0:1\] (non-looping) or \[0:1\[ (looping) of
-        /// the animation.
-        ///
-        /// For looping animations, this reports the progress of the current iteration,
-        /// in the current direction:
-        /// - [`TweeningType::Loop`] is 0 at start and 1 at end. The exact value 1.0 is
-        ///   never reached, since the tweenable loops over to 0.0 immediately.
-        /// - [`TweeningType::PingPong`] is 0 at the source endpoint and 1 and the
-        ///   destination one, which are respectively the start/end for
-        ///   [`TweeningDirection::Forward`], or the end/start for
-        ///   [`TweeningDirection::Backward`]. The exact value 1.0 is never reached,
-        ///   since the tweenable loops over to 0.0 immediately when it changes
-        ///   direction at either endpoint.
+        /// Get the current progress of the tweenable. See [`Tweenable::progress`] for
+        /// details.
         ///
         /// For sequences, the progress is measured over the entire sequence, from 0 at
         /// the start of the first child tweenable to 1 at the end of the last one.
@@ -543,11 +546,11 @@ impl<T: Asset> AssetAnimator<T> {
 
 #[cfg(test)]
 mod tests {
-    use super::{lens::*, *};
-
     #[cfg(feature = "bevy_asset")]
     use bevy::reflect::TypeUuid;
 
+    use super::{lens::*, *};
+
     struct DummyLens {
         start: f32,
         end: f32,
@@ -579,9 +582,15 @@ mod tests {
     }
 
     #[test]
-    fn tweening_type() {
-        let tweening_type = TweeningType::default();
-        assert_eq!(tweening_type, TweeningType::Once);
+    fn repeat_count() {
+        let count = RepeatCount::default();
+        assert_eq!(count, RepeatCount::Finite(1));
+    }
+
+    #[test]
+    fn repeat_strategy() {
+        let strategy = RepeatStrategy::default();
+        assert_eq!(strategy, RepeatStrategy::Repeat);
     }
 
     #[test]
@@ -631,7 +640,6 @@ mod tests {
     fn animator_new() {
         let tween = Tween::new(
             EaseFunction::QuadraticInOut,
-            TweeningType::PingPong,
             Duration::from_secs(1),
             DummyLens { start: 0., end: 1. },
         );
@@ -647,7 +655,6 @@ mod tests {
         for state in [AnimatorState::Playing, AnimatorState::Paused] {
             let tween = Tween::<DummyComponent>::new(
                 EaseFunction::QuadraticInOut,
-                TweeningType::PingPong,
                 Duration::from_secs(1),
                 DummyLens { start: 0., end: 1. },
             );
@@ -665,7 +672,6 @@ mod tests {
 
         let tween = Tween::<DummyComponent>::new(
             EaseFunction::QuadraticInOut,
-            TweeningType::PingPong,
             Duration::from_secs(1),
             DummyLens { start: 0., end: 1. },
         );
@@ -679,7 +685,6 @@ mod tests {
     fn animator_controls() {
         let tween = Tween::<DummyComponent>::new(
             EaseFunction::QuadraticInOut,
-            TweeningType::PingPong,
             Duration::from_secs(1),
             DummyLens { start: 0., end: 1. },
         );
@@ -719,7 +724,6 @@ mod tests {
     fn asset_animator_new() {
         let tween = Tween::<DummyAsset>::new(
             EaseFunction::QuadraticInOut,
-            TweeningType::PingPong,
             Duration::from_secs(1),
             DummyLens { start: 0., end: 1. },
         );
@@ -736,7 +740,6 @@ mod tests {
         for state in [AnimatorState::Playing, AnimatorState::Paused] {
             let tween = Tween::<DummyAsset>::new(
                 EaseFunction::QuadraticInOut,
-                TweeningType::PingPong,
                 Duration::from_secs(1),
                 DummyLens { start: 0., end: 1. },
             );
@@ -757,7 +760,6 @@ mod tests {
 
         let tween = Tween::new(
             EaseFunction::QuadraticInOut,
-            TweeningType::PingPong,
             Duration::from_secs(1),
             DummyLens { start: 0., end: 1. },
         );
@@ -773,7 +775,6 @@ mod tests {
     fn asset_animator_controls() {
         let tween = Tween::new(
             EaseFunction::QuadraticInOut,
-            TweeningType::PingPong,
             Duration::from_secs(1),
             DummyLens { start: 0., end: 1. },
         );
diff --git a/src/tweenable.rs b/src/tweenable.rs
index 67d18c21814c5d4d78d1b46b71dd12349ce8258e..9b13dd1ed17ebb046d81ef6d7f6233625c3b8ed0 100644
--- a/src/tweenable.rs
+++ b/src/tweenable.rs
@@ -2,7 +2,7 @@ use std::time::Duration;
 
 use bevy::prelude::*;
 
-use crate::{EaseMethod, Lens, TweeningDirection, TweeningType};
+use crate::{EaseMethod, Lens, RepeatCount, RepeatStrategy, TweeningDirection};
 
 /// The dynamic tweenable type.
 ///
@@ -28,7 +28,6 @@ use crate::{EaseMethod, Lens, TweeningDirection, TweeningType};
 /// # struct MyTweenable;
 /// # impl Tweenable<Transform> for MyTweenable {
 /// #     fn duration(&self) -> Duration  { unimplemented!() }
-/// #     fn is_looping(&self) -> bool  { unimplemented!() }
 /// #     fn set_progress(&mut self, progress: f32)  { unimplemented!() }
 /// #     fn progress(&self) -> f32  { unimplemented!() }
 /// #     fn tick(&mut self, delta: Duration, target: &mut Transform, entity: Entity, event_writer: &mut EventWriter<TweenCompleted>) -> TweenState  { unimplemented!() }
@@ -60,20 +59,20 @@ pub enum TweenState {
     /// The tweenable is still active, and did not reach its end state yet.
     Active,
     /// Animation reached its end state. The tweenable is idling at its latest
-    /// time. This can only happen for [`TweeningType::Once`], since other
-    /// types loop indefinitely.
+    /// time.
+    ///
+    /// Note that [`RepeatCount::Infinite`] tweenables never reach this state.
     Completed,
 }
 
 /// Event raised when a tween completed.
 ///
-/// This event is raised when a tween completed. For non-looping tweens, this is
-/// raised once at the end of the animation. For looping animations, this is
-/// raised once per iteration. In case the animation direction changes
-/// ([`TweeningType::PingPong`]), an iteration corresponds to a single progress
-/// from one endpoint to the other, whatever the direction. Therefore a complete
-/// cycle start -> end -> start counts as 2 iterations and raises 2 events (one
-/// when reaching the end, one when reaching back the start).
+/// This event is raised when a tween completed. When looping, this is raised
+/// once per iteration. In case the animation direction changes
+/// ([`RepeatStrategy::MirroredRepeat`]), an iteration corresponds to a single
+/// progress from one endpoint to the other, whatever the direction. Therefore a
+/// complete cycle start -> end -> start counts as 2 iterations and raises 2
+/// events (one when reaching the end, one when reaching back the start).
 ///
 /// # Note
 ///
@@ -97,82 +96,94 @@ pub struct TweenCompleted {
     pub user_data: u64,
 }
 
-#[derive(Debug, Default, Clone, Copy)]
+#[derive(Debug)]
 struct AnimClock {
     elapsed: Duration,
     duration: Duration,
-    is_looping: bool,
+    times_completed: u32,
+    total_duration: TotalDuration,
+    strategy: RepeatStrategy,
 }
 
 impl AnimClock {
-    fn new(duration: Duration, is_looping: bool) -> Self {
+    fn new(duration: Duration) -> Self {
         Self {
             elapsed: Duration::ZERO,
             duration,
-            is_looping,
+            total_duration: compute_total_duration(duration, RepeatCount::default()),
+            times_completed: 0,
+            strategy: RepeatStrategy::default(),
         }
     }
 
-    fn tick(&mut self, duration: Duration) -> u32 {
-        self.elapsed = self.elapsed.saturating_add(duration);
+    fn record_completions(&mut self, times_completed: u32) {
+        self.times_completed = self.times_completed.saturating_add(times_completed);
+    }
 
-        if self.elapsed < self.duration {
-            0
-        } else if self.is_looping {
-            let elapsed = self.elapsed.as_nanos();
-            let duration = self.duration.as_nanos();
+    fn tick(&mut self, tick: Duration) -> u32 {
+        let duration = self.duration.as_nanos();
 
-            self.elapsed = Duration::from_nanos((elapsed % duration) as u64);
-            (elapsed / duration) as u32
-        } else {
-            self.elapsed = self.duration;
-            1
+        let before = self.elapsed.as_nanos() / duration;
+        self.elapsed = self.elapsed.saturating_add(tick);
+        if let TotalDuration::Finite(duration) = self.total_duration {
+            self.elapsed = self.elapsed.min(duration);
         }
+        (self.elapsed.as_nanos() / duration - before) as u32
     }
 
     fn set_progress(&mut self, progress: f32) {
-        let progress = if self.is_looping {
-            progress.max(0.).fract()
-        } else {
-            progress.clamp(0., 1.)
-        };
-
-        self.elapsed = self.duration.mul_f32(progress);
+        self.elapsed = self.duration.mul_f32(progress.max(0.));
     }
 
     fn progress(&self) -> f32 {
         self.elapsed.as_secs_f32() / self.duration.as_secs_f32()
     }
 
-    fn completed(&self) -> bool {
-        self.elapsed >= self.duration
+    fn state(&self) -> TweenState {
+        match self.total_duration {
+            TotalDuration::Finite(duration) => {
+                if self.elapsed >= duration {
+                    TweenState::Completed
+                } else {
+                    TweenState::Active
+                }
+            }
+            TotalDuration::Infinite => TweenState::Active,
+        }
     }
 
     fn reset(&mut self) {
+        self.times_completed = 0;
         self.elapsed = Duration::ZERO;
     }
 }
 
+#[derive(Debug)]
+enum TotalDuration {
+    Finite(Duration),
+    Infinite,
+}
+
+fn compute_total_duration(duration: Duration, count: RepeatCount) -> TotalDuration {
+    match count {
+        RepeatCount::Finite(times) => TotalDuration::Finite(duration.saturating_mul(times)),
+        RepeatCount::For(duration) => TotalDuration::Finite(duration),
+        RepeatCount::Infinite => TotalDuration::Infinite,
+    }
+}
+
 /// 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.
     ///
-    /// For non-looping tweenables ([`TweeningType::Once`]), this is the total
-    /// animation duration. For looping ones, this is the duration of a
-    /// single iteration, since the total animation duration is infinite.
+    /// This is always the duration of a single iteration, even when looping.
     ///
-    /// Note that for [`TweeningType::PingPong`], this is the duration of a
-    /// single way, either from start to end or back from end to start. The
-    /// total "loop" duration start -> end -> start to reach back the same
-    /// state in this case is the double of the returned value.
+    /// Note that for [`RepeatStrategy::MirroredRepeat`], this is the duration
+    /// of a single way, either from start to end or back from end to start.
+    /// The total "loop" duration start -> end -> start to reach back the
+    /// same state in this case is the double of the returned value.
     fn duration(&self) -> Duration;
 
-    /// Return `true` if the animation is looping.
-    ///
-    /// Looping tweenables are of type [`TweeningType::Loop`] or
-    /// [`TweeningType::PingPong`].
-    fn is_looping(&self) -> bool;
-
     /// Set the current animation playback progress.
     ///
     /// See [`progress()`] for details on the meaning.
@@ -180,20 +191,12 @@ pub trait Tweenable<T>: Send + Sync {
     /// [`progress()`]: Tweenable::progress
     fn set_progress(&mut self, progress: f32);
 
-    /// Get the current progress in \[0:1\] (non-looping) or \[0:1\[ (looping)
-    /// of the animation.
+    /// Get the current progress in \[0:1\] of the animation.
     ///
-    /// For looping animations, this reports the progress of the current
-    /// iteration, in the current direction:
-    /// - [`TweeningType::Loop`] is `0` at start and `1` at end. The exact value
-    ///   `1.0` is never reached, since the tweenable loops over to `0.0`
-    ///   immediately.
-    /// - [`TweeningType::PingPong`] is `0` at the source endpoint and `1` and
-    ///   the destination one, which are respectively the start/end for
-    ///   [`TweeningDirection::Forward`], or the end/start for
-    ///   [`TweeningDirection::Backward`]. The exact value `1.0` is never
-    ///   reached, since the tweenable loops over to `0.0` immediately when it
-    ///   changes direction at either endpoint.
+    /// While looping, the exact value `1.0` is never reached, since the
+    /// tweenable loops over to `0.0` immediately when it changes direction at
+    /// either endpoint. Upon completion, the tweenable always reports exactly
+    /// `1.0`.
     fn progress(&self) -> f32;
 
     /// Tick the animation, advancing it by the given delta time and mutating
@@ -223,10 +226,10 @@ pub trait Tweenable<T>: Send + Sync {
     /// Get the number of times this tweenable completed.
     ///
     /// For looping animations, this returns the number of times a single
-    /// playback was completed. In the case of [`TweeningType::PingPong`]
-    /// this corresponds to a playback in a single direction, so tweening
-    /// from start to end and back to start counts as two completed times (one
-    /// forward, one backward).
+    /// playback was completed. In the case of
+    /// [`RepeatStrategy::MirroredRepeat`] this corresponds to a playback in
+    /// a single direction, so tweening from start to end and back to start
+    /// counts as two completed times (one forward, one backward).
     fn times_completed(&self) -> u32;
 
     /// Rewind the animation to its starting state.
@@ -270,8 +273,6 @@ pub type CompletedCallback<T> = dyn Fn(Entity, &Tween<T>) + Send + Sync + 'stati
 pub struct Tween<T> {
     ease_function: EaseMethod,
     clock: AnimClock,
-    times_completed: u32,
-    tweening_type: TweeningType,
     direction: TweeningDirection,
     lens: Box<dyn Lens<T> + Send + Sync + 'static>,
     on_completed: Option<Box<CompletedCallback<T>>>,
@@ -289,7 +290,6 @@ impl<T: 'static> Tween<T> {
     /// # use std::time::Duration;
     /// let tween1 = Tween::new(
     ///     EaseFunction::QuadraticInOut,
-    ///     TweeningType::Once,
     ///     Duration::from_secs_f32(1.0),
     ///     TransformPositionLens {
     ///         start: Vec3::ZERO,
@@ -298,7 +298,6 @@ impl<T: 'static> Tween<T> {
     /// );
     /// let tween2 = Tween::new(
     ///     EaseFunction::QuadraticInOut,
-    ///     TweeningType::Once,
     ///     Duration::from_secs_f32(1.0),
     ///     TransformRotationLens {
     ///         start: Quat::IDENTITY,
@@ -323,7 +322,6 @@ impl<T> Tween<T> {
     /// # use std::time::Duration;
     /// let tween = Tween::new(
     ///     EaseFunction::QuadraticInOut,
-    ///     TweeningType::Once,
     ///     Duration::from_secs_f32(1.0),
     ///     TransformPositionLens {
     ///         start: Vec3::ZERO,
@@ -332,20 +330,13 @@ impl<T> Tween<T> {
     /// );
     /// ```
     #[must_use]
-    pub fn new<L>(
-        ease_function: impl Into<EaseMethod>,
-        tweening_type: TweeningType,
-        duration: Duration,
-        lens: L,
-    ) -> Self
+    pub fn new<L>(ease_function: impl Into<EaseMethod>, duration: Duration, lens: L) -> Self
     where
         L: Lens<T> + Send + Sync + 'static,
     {
         Self {
             ease_function: ease_function.into(),
-            clock: AnimClock::new(duration, tweening_type != TweeningType::Once),
-            times_completed: 0,
-            tweening_type,
+            clock: AnimClock::new(duration),
             direction: TweeningDirection::Forward,
             lens: Box::new(lens),
             on_completed: None,
@@ -367,7 +358,6 @@ impl<T> Tween<T> {
     /// let tween = Tween::new(
     ///     // [...]
     /// #    EaseFunction::QuadraticInOut,
-    /// #    TweeningType::Once,
     /// #    Duration::from_secs_f32(1.0),
     /// #    TransformPositionLens {
     /// #        start: Vec3::ZERO,
@@ -425,6 +415,20 @@ impl<T> Tween<T> {
         self.direction
     }
 
+    /// Set the number of times to repeat the animation.
+    #[must_use]
+    pub fn with_repeat_count(mut self, count: RepeatCount) -> Self {
+        self.clock.total_duration = compute_total_duration(self.clock.duration, count);
+        self
+    }
+
+    /// Choose how the animation behaves upon a repetition.
+    #[must_use]
+    pub fn with_repeat_strategy(mut self, strategy: RepeatStrategy) -> Self {
+        self.clock.strategy = strategy;
+        self
+    }
+
     /// Set a callback invoked when the animation completes.
     ///
     /// The callback when invoked receives as parameters the [`Entity`] on which
@@ -469,10 +473,6 @@ impl<T> Tweenable<T> for Tween<T> {
         self.clock.duration
     }
 
-    fn is_looping(&self) -> bool {
-        self.tweening_type != TweeningType::Once
-    }
-
     fn set_progress(&mut self, progress: f32) {
         self.clock.set_progress(progress);
     }
@@ -488,22 +488,17 @@ impl<T> Tweenable<T> for Tween<T> {
         entity: Entity,
         event_writer: &mut EventWriter<TweenCompleted>,
     ) -> TweenState {
-        if !self.is_looping() && self.clock.completed() {
+        if self.clock.state() == TweenState::Completed {
             return TweenState::Completed;
         }
 
         // Tick the animation clock
         let times_completed = self.clock.tick(delta);
-        self.times_completed += times_completed;
-        if times_completed & 1 != 0 && self.tweening_type == TweeningType::PingPong {
+        self.clock.record_completions(times_completed);
+        if self.clock.strategy == RepeatStrategy::MirroredRepeat && times_completed & 1 != 0 {
             self.direction = !self.direction;
         }
-        let state = if self.is_looping() || times_completed == 0 {
-            TweenState::Active
-        } else {
-            TweenState::Completed
-        };
-        let progress = self.clock.progress();
+        let progress = self.progress();
 
         // Apply the lens, even if the animation finished, to ensure the state is
         // consistent
@@ -527,16 +522,15 @@ impl<T> Tweenable<T> for Tween<T> {
             }
         }
 
-        state
+        self.clock.state()
     }
 
     fn times_completed(&self) -> u32 {
-        self.times_completed
+        self.clock.times_completed
     }
 
     fn rewind(&mut self) {
         self.clock.reset();
-        self.times_completed = 0;
     }
 }
 
@@ -623,10 +617,6 @@ impl<T> Tweenable<T> for Sequence<T> {
         self.duration
     }
 
-    fn is_looping(&self) -> bool {
-        false // TODO - implement looping sequences...
-    }
-
     fn set_progress(&mut self, progress: f32) {
         self.times_completed = if progress >= 1. { 1 } else { 0 };
         let progress = progress.clamp(0., 1.); // not looping
@@ -733,10 +723,6 @@ impl<T> Tweenable<T> for Tracks<T> {
         self.duration
     }
 
-    fn is_looping(&self) -> bool {
-        false // TODO - implement looping tracks...
-    }
-
     fn set_progress(&mut self, progress: f32) {
         self.times_completed = if progress >= 1. { 1 } else { 0 }; // not looping
         let progress = progress.clamp(0., 1.); // not looping
@@ -818,10 +804,6 @@ impl<T> Tweenable<T> for Delay {
         self.timer.duration()
     }
 
-    fn is_looping(&self) -> bool {
-        false
-    }
-
     fn set_progress(&mut self, progress: f32) {
         // need to reset() to clear finished() unfortunately
         self.timer.reset();
@@ -891,7 +873,8 @@ mod tests {
     #[test]
     fn anim_clock_precision() {
         let duration = Duration::from_millis(1);
-        let mut clock = AnimClock::new(duration, true);
+        let mut clock = AnimClock::new(duration);
+        clock.total_duration = TotalDuration::Infinite;
 
         let test_ticks = [
             Duration::from_micros(123),
@@ -922,27 +905,29 @@ mod tests {
     #[test]
     fn tween_tick() {
         for tweening_direction in &[TweeningDirection::Forward, TweeningDirection::Backward] {
-            for tweening_type in &[
-                TweeningType::Once,
-                TweeningType::Loop,
-                TweeningType::PingPong,
+            for (count, strategy) in &[
+                (RepeatCount::Finite(1), RepeatStrategy::default()),
+                (RepeatCount::Infinite, RepeatStrategy::Repeat),
+                (RepeatCount::Finite(2), RepeatStrategy::Repeat),
+                (RepeatCount::Infinite, RepeatStrategy::MirroredRepeat),
+                (RepeatCount::Finite(2), RepeatStrategy::MirroredRepeat),
             ] {
                 println!(
-                    "TweeningType: type={:?} dir={:?}",
-                    tweening_type, tweening_direction
+                    "TweeningType: count={count:?} strategy={strategy:?} dir={tweening_direction:?}",
                 );
 
                 // Create a linear tween over 1 second
                 let mut tween = Tween::new(
                     EaseMethod::Linear,
-                    *tweening_type,
                     Duration::from_secs_f32(1.0),
                     TransformPositionLens {
                         start: Vec3::ZERO,
                         end: Vec3::ONE,
                     },
                 )
-                .with_direction(*tweening_direction);
+                .with_direction(*tweening_direction)
+                .with_repeat_count(*count)
+                .with_repeat_strategy(*strategy);
                 assert_eq!(tween.direction(), *tweening_direction);
                 assert!(tween.on_completed.is_none());
                 assert!(tween.event_data.is_none());
@@ -982,8 +967,8 @@ mod tests {
                 for i in 1..=11 {
                     // Calculate expected values
                     let (progress, times_completed, mut direction, expected_state, just_completed) =
-                        match tweening_type {
-                            TweeningType::Once => {
+                        match count {
+                            RepeatCount::Finite(1) => {
                                 let progress = (i as f32 * 0.2).min(1.0);
                                 let times_completed = if i >= 5 { 1 } else { 0 };
                                 let state = if i < 5 {
@@ -1000,37 +985,77 @@ mod tests {
                                     just_completed,
                                 )
                             }
-                            TweeningType::Loop => {
-                                let progress = (i as f32 * 0.2).fract();
-                                let times_completed = i / 5;
-                                let just_completed = i % 5 == 0;
-                                (
-                                    progress,
-                                    times_completed,
-                                    TweeningDirection::Forward,
-                                    TweenState::Active,
-                                    just_completed,
-                                )
+                            RepeatCount::Finite(count) => {
+                                let progress = (i as f32 * 0.2).min(1.0 * *count as f32);
+                                if *strategy == RepeatStrategy::Repeat {
+                                    let times_completed = i / 5;
+                                    let just_completed = i % 5 == 0;
+                                    (
+                                        progress,
+                                        times_completed,
+                                        TweeningDirection::Forward,
+                                        if i < 10 {
+                                            TweenState::Active
+                                        } else {
+                                            TweenState::Completed
+                                        },
+                                        just_completed,
+                                    )
+                                } else {
+                                    let i5 = i % 5;
+                                    let times_completed = i / 5;
+                                    let i10 = i % 10;
+                                    let direction = if i10 >= 5 {
+                                        TweeningDirection::Backward
+                                    } else {
+                                        TweeningDirection::Forward
+                                    };
+                                    let just_completed = i5 == 0;
+                                    (
+                                        progress,
+                                        times_completed,
+                                        direction,
+                                        if i < 10 {
+                                            TweenState::Active
+                                        } else {
+                                            TweenState::Completed
+                                        },
+                                        just_completed,
+                                    )
+                                }
                             }
-                            TweeningType::PingPong => {
-                                let i5 = i % 5;
-                                let progress = i5 as f32 * 0.2;
-                                let times_completed = i / 5;
-                                let i10 = i % 10;
-                                let direction = if i10 >= 5 {
-                                    TweeningDirection::Backward
+                            RepeatCount::Infinite => {
+                                let progress = i as f32 * 0.2;
+                                if *strategy == RepeatStrategy::Repeat {
+                                    let times_completed = i / 5;
+                                    let just_completed = i % 5 == 0;
+                                    (
+                                        progress,
+                                        times_completed,
+                                        TweeningDirection::Forward,
+                                        TweenState::Active,
+                                        just_completed,
+                                    )
                                 } else {
-                                    TweeningDirection::Forward
-                                };
-                                let just_completed = i5 == 0;
-                                (
-                                    progress,
-                                    times_completed,
-                                    direction,
-                                    TweenState::Active,
-                                    just_completed,
-                                )
+                                    let i5 = i % 5;
+                                    let times_completed = i / 5;
+                                    let i10 = i % 10;
+                                    let direction = if i10 >= 5 {
+                                        TweeningDirection::Backward
+                                    } else {
+                                        TweeningDirection::Forward
+                                    };
+                                    let just_completed = i5 == 0;
+                                    (
+                                        progress,
+                                        times_completed,
+                                        direction,
+                                        TweenState::Active,
+                                        just_completed,
+                                    )
+                                }
                             }
+                            RepeatCount::For(_) => panic!("Untested"),
                         };
                     let factor = if tweening_direction.is_backward() {
                         direction = !direction;
@@ -1068,7 +1093,6 @@ mod tests {
 
                     // Check actual values
                     assert_eq!(tween.direction(), direction);
-                    assert_eq!(tween.is_looping(), *tweening_type != TweeningType::Once);
                     assert_eq!(actual_state, expected_state);
                     assert!(abs_diff_eq(tween.progress(), progress, 1e-5));
                     assert_eq!(tween.times_completed(), times_completed);
@@ -1097,7 +1121,6 @@ mod tests {
                 // Rewind
                 tween.rewind();
                 assert_eq!(tween.direction(), *tweening_direction); // does not change
-                assert_eq!(tween.is_looping(), *tweening_type != TweeningType::Once);
                 assert!(abs_diff_eq(tween.progress(), 0., 1e-5));
                 assert_eq!(tween.times_completed(), 0);
 
@@ -1133,7 +1156,6 @@ mod tests {
     fn tween_dir() {
         let mut tween = Tween::new(
             EaseMethod::Linear,
-            TweeningType::Once,
             Duration::from_secs_f32(1.0),
             TransformPositionLens {
                 start: Vec3::ZERO,
@@ -1191,7 +1213,6 @@ mod tests {
     fn seq_tick() {
         let tween1 = Tween::new(
             EaseMethod::Linear,
-            TweeningType::Once,
             Duration::from_secs_f32(1.0),
             TransformPositionLens {
                 start: Vec3::ZERO,
@@ -1200,7 +1221,6 @@ mod tests {
         );
         let tween2 = Tween::new(
             EaseMethod::Linear,
-            TweeningType::Once,
             Duration::from_secs_f32(1.0),
             TransformRotationLens {
                 start: Quat::IDENTITY,
@@ -1231,13 +1251,13 @@ mod tests {
             } else if i < 10 {
                 assert_eq!(state, TweenState::Active);
                 let alpha_deg = (18 * (i - 5)) as f32;
-                assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5));
+                assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5));
                 assert!(transform
                     .rotation
                     .abs_diff_eq(Quat::from_rotation_x(alpha_deg.to_radians()), 1e-5));
             } else {
                 assert_eq!(state, TweenState::Completed);
-                assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5));
+                assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5));
                 assert!(transform
                     .rotation
                     .abs_diff_eq(Quat::from_rotation_x(90_f32.to_radians()), 1e-5));
@@ -1251,7 +1271,6 @@ mod tests {
         let mut seq = Sequence::new((1..5).map(|i| {
             Tween::new(
                 EaseMethod::Linear,
-                TweeningType::Once,
                 Duration::from_secs_f32(0.2 * i as f32),
                 TransformPositionLens {
                     start: Vec3::ZERO,
@@ -1259,7 +1278,6 @@ mod tests {
                 },
             )
         }));
-        assert!(!seq.is_looping());
 
         let mut progress = 0.;
         for i in 1..5 {
@@ -1282,7 +1300,6 @@ mod tests {
     fn tracks_tick() {
         let tween1 = Tween::new(
             EaseMethod::Linear,
-            TweeningType::Once,
             Duration::from_secs_f32(1.),
             TransformPositionLens {
                 start: Vec3::ZERO,
@@ -1291,7 +1308,6 @@ mod tests {
         );
         let tween2 = Tween::new(
             EaseMethod::Linear,
-            TweeningType::Once,
             Duration::from_secs_f32(0.8), // shorter
             TransformRotationLens {
                 start: Quat::IDENTITY,
@@ -1300,7 +1316,6 @@ mod tests {
         );
         let mut tracks = Tracks::new([tween1, tween2]);
         assert_eq!(tracks.duration(), Duration::from_secs_f32(1.)); // max(1., 0.8)
-        assert!(!tracks.is_looping());
 
         let mut transform = Transform::default();
 
@@ -1332,7 +1347,7 @@ mod tests {
                 assert_eq!(state, TweenState::Completed);
                 assert_eq!(tracks.times_completed(), 1);
                 assert!((tracks.progress() - 1.).abs() < 1e-5);
-                assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5));
+                assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5));
                 assert!(transform
                     .rotation
                     .abs_diff_eq(Quat::from_rotation_x(90_f32.to_radians()), 1e-5));
@@ -1388,7 +1403,6 @@ mod tests {
         {
             let tweenable: &dyn Tweenable<Transform> = &delay;
             assert_eq!(tweenable.duration(), duration);
-            assert!(!tweenable.is_looping());
             assert!(tweenable.progress().abs() < 1e-5);
         }