diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9bd124a22c729396f0a08fd5e7661817ca289946..b8caeb957dcf333c39efd1ad7ba7fc05cff20303 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,24 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
-- Added `is_forward()` and `is_backward()` convenience helpers to `TweeningDirection`.
-- Added `Tween::set_direction()` and `Tween::with_direction()` which allow configuring the playback direction of a tween, allowing to play it backward from end to start.
-- Added support for dynamically changing an animation's speed with `Animator::set_speed`.
-- Added `AnimationSystem` label to tweening tick systems.
-- Added `BoxedTweenable` type to make working with `Box<dyn Tweenable + ...>` easier.
-- Added `RepeatCount` and `RepeatStrategy` for more granular control over animation looping.
-- Added `with_repeat_count()` and `with_repeat_strategy()` builder methods to `Tween<T>`.
+- Add `is_forward()` and `is_backward()` convenience helpers to `TweeningDirection`.
+- Add `Tween::set_direction()` and `Tween::with_direction()` which allow configuring the playback direction of a tween, allowing to play it backward from end to start.
+- Support dynamically changing an animation's speed with `Animator::set_speed`
+- Add `AnimationSystem` label to tweening tick systems
+- A `BoxedTweenable` type to make working with `Box<dyn Tweenable + ...>` easier
 
 ### Changed
 
-- Double boxing in `Sequence` and `Tracks` was fixed. As a result, any custom tweenables.
+- 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 3e4285b3f150025a7ddb70a6dfff6d3cb30991b0..ad6e950728074fffd354348e3e1ed81510dd6c3b 100644
--- a/examples/colormaterial_color.rs
+++ b/examples/colormaterial_color.rs
@@ -77,14 +77,13 @@ 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 8eefcfb0a5271e00519bcbc8ce58bb2557fb28e1..1990afaec1f178eee7070f6a162b3052cce58d57 100644
--- a/examples/menu.rs
+++ b/examples/menu.rs
@@ -50,6 +50,7 @@ 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 67e7ff36e6fede980eb1557c3a5274471854e911..6ae3f1923758fd9ddac19621bebbe8b2781f603f 100644
--- a/examples/sequence.rs
+++ b/examples/sequence.rs
@@ -1,8 +1,6 @@
-use std::time::Duration;
-
 use bevy::prelude::*;
-
 use bevy_tweening::{lens::*, *};
+use std::time::Duration;
 
 fn main() {
     App::default()
@@ -109,31 +107,19 @@ 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
-    // Tracks<Transform>)
+    // Build a sequence from an iterator over a Tweenable (here, a Tween<Transform>)
     let seq = Sequence::new(dests.windows(2).enumerate().map(|(index, pair)| {
-        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),
-        ])
+        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)
     }));
 
     commands
@@ -152,6 +138,7 @@ 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.),
@@ -161,6 +148,7 @@ 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,
@@ -169,6 +157,7 @@ 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 a21578552a41f49fce711cc6d52ed9bc507cb88d..3f1a94a883dc9f40644330b943930fb70150a690 100644
--- a/examples/sprite_color.rs
+++ b/examples/sprite_color.rs
@@ -61,14 +61,13 @@ 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 66ee419b80260e9faf31eb1baa09cbca663784c5..523233008d50744112319a13c7ec34aa33e6b491 100644
--- a/examples/text_color.rs
+++ b/examples/text_color.rs
@@ -68,15 +68,14 @@ 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 a98be3df0fe42d337dd39abd88d9b5119ed4db92..a259ff323ad9913c63f60de89ceae761b632be48 100644
--- a/examples/transform_rotation.rs
+++ b/examples/transform_rotation.rs
@@ -77,14 +77,13 @@ 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((
diff --git a/examples/transform_translation.rs b/examples/transform_translation.rs
index 8f5138a6c2815e2e07f0792ca92e9197a95d9e94..75cc678cdfbf4cb9b13ade645b79e26d2e6fe912 100644
--- a/examples/transform_translation.rs
+++ b/examples/transform_translation.rs
@@ -76,14 +76,13 @@ 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 9043dd27c44a920ccbe30f7cf61bb49a8458b7cd..24ff0e3ab90c97f8a20ce11c9a70f772df008dfd 100644
--- a/examples/ui_position.rs
+++ b/examples/ui_position.rs
@@ -76,6 +76,7 @@ fn setup(mut commands: Commands) {
     ] {
         let tween = Tween::new(
             *ease_function,
+            TweeningType::PingPong,
             std::time::Duration::from_secs(1),
             UiPositionLens {
                 start: Rect {
@@ -91,9 +92,7 @@ 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 4ce635fd2af70e18dcfac9b0e10872918acd982e..55a104c815b1681f7157c45ecb0499bfde544742 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -44,7 +44,10 @@
 //! let tween = Tween::new(
 //!     // Use a quadratic easing on both endpoints.
 //!     EaseFunction::QuadraticInOut,
-//!     // Animation time.
+//!     // 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 Entity,
 //!     // for the Animator to animate it. It also contains the start and
@@ -87,6 +90,7 @@
 //! let tween1 = Tween::new(
 //!     // [...]
 //! #    EaseFunction::BounceOut,
+//! #    TweeningType::Once,
 //! #    Duration::from_secs(2),
 //! #    TransformScaleLens {
 //! #        start: Vec3::ZERO,
@@ -96,6 +100,7 @@
 //! let tween2 = Tween::new(
 //!     // [...]
 //! #    EaseFunction::QuadraticInOut,
+//! #    TweeningType::Once,
 //! #    Duration::from_secs(1),
 //! #    TransformPositionLens {
 //! #        start: Vec3::ZERO,
@@ -151,12 +156,16 @@
 //! [`Sprite`]: https://docs.rs/bevy/0.7.0/bevy/sprite/struct.Sprite.html
 //! [`Transform`]: https://docs.rs/bevy/0.7.0/bevy/transform/components/struct.Transform.html
 
+use bevy::{asset::Asset, prelude::*};
 use std::time::Duration;
 
-use bevy::{asset::Asset, prelude::*};
 use interpolation::Ease as IEase;
 pub use interpolation::{EaseFunction, Lerp};
 
+pub mod lens;
+mod plugin;
+mod tweenable;
+
 pub use lens::Lens;
 pub use plugin::{
     asset_animator_system, component_animator_system, AnimationSystem, TweeningPlugin,
@@ -165,47 +174,24 @@ pub use tweenable::{
     BoxedTweenable, Delay, Sequence, Tracks, Tween, TweenCompleted, TweenState, Tweenable,
 };
 
-pub mod lens;
-mod plugin;
-mod tweenable;
-
-/// How many times to repeat a tween animation. See also: [`RepeatStrategy`].
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-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,
-}
-
-/// What to do when a tween animation needs to be repeated.
-///
-/// Only applicable when [`RepeatCount`] is greater than the animation duration.
+/// Type of looping for a tween animation.
 #[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,
+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,
 }
 
-impl Default for RepeatCount {
+impl Default for TweeningType {
     fn default() -> Self {
-        Self::Finite(1)
-    }
-}
-
-impl Default for RepeatStrategy {
-    fn default() -> Self {
-        Self::Repeat
+        Self::Once
     }
 }
 
@@ -289,12 +275,12 @@ impl From<EaseFunction> for EaseMethod {
 /// that target at the start bound of the lens, effectively making the animation
 /// play backward.
 ///
-/// For all but [`RepeatStrategy::MirroredRepeat`] this is always
+/// For all but [`TweeningType::PingPong`] this is always
 /// [`TweeningDirection::Forward`], unless manually configured with
-/// [`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.
+/// [`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.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum TweeningDirection {
     /// Animation playing from start to end.
@@ -395,8 +381,19 @@ macro_rules! animator_impl {
             }
         }
 
-        /// Get the current progress of the tweenable. See [`Tweenable::progress`] for
-        /// details.
+        /// 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.
         ///
         /// 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.
@@ -538,9 +535,8 @@ impl<T: Asset> AssetAnimator<T> {
 
 #[cfg(test)]
 mod tests {
-    use bevy::reflect::TypeUuid;
-
     use super::{lens::*, *};
+    use bevy::reflect::TypeUuid;
 
     struct DummyLens {
         start: f32,
@@ -571,15 +567,9 @@ mod tests {
     }
 
     #[test]
-    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);
+    fn tweening_type() {
+        let tweening_type = TweeningType::default();
+        assert_eq!(tweening_type, TweeningType::Once);
     }
 
     #[test]
@@ -629,6 +619,7 @@ mod tests {
     fn animator_new() {
         let tween = Tween::new(
             EaseFunction::QuadraticInOut,
+            TweeningType::PingPong,
             Duration::from_secs(1),
             DummyLens { start: 0., end: 1. },
         );
@@ -644,6 +635,7 @@ 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. },
             );
@@ -661,6 +653,7 @@ mod tests {
 
         let tween = Tween::<DummyComponent>::new(
             EaseFunction::QuadraticInOut,
+            TweeningType::PingPong,
             Duration::from_secs(1),
             DummyLens { start: 0., end: 1. },
         );
@@ -674,6 +667,7 @@ mod tests {
     fn animator_controls() {
         let tween = Tween::<DummyComponent>::new(
             EaseFunction::QuadraticInOut,
+            TweeningType::PingPong,
             Duration::from_secs(1),
             DummyLens { start: 0., end: 1. },
         );
@@ -712,6 +706,7 @@ mod tests {
     fn asset_animator_new() {
         let tween = Tween::<DummyAsset>::new(
             EaseFunction::QuadraticInOut,
+            TweeningType::PingPong,
             Duration::from_secs(1),
             DummyLens { start: 0., end: 1. },
         );
@@ -727,6 +722,7 @@ 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. },
             );
@@ -746,6 +742,7 @@ mod tests {
 
         let tween = Tween::new(
             EaseFunction::QuadraticInOut,
+            TweeningType::PingPong,
             Duration::from_secs(1),
             DummyLens { start: 0., end: 1. },
         );
@@ -760,6 +757,7 @@ 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 9b13dd1ed17ebb046d81ef6d7f6233625c3b8ed0..67d18c21814c5d4d78d1b46b71dd12349ce8258e 100644
--- a/src/tweenable.rs
+++ b/src/tweenable.rs
@@ -2,7 +2,7 @@ use std::time::Duration;
 
 use bevy::prelude::*;
 
-use crate::{EaseMethod, Lens, RepeatCount, RepeatStrategy, TweeningDirection};
+use crate::{EaseMethod, Lens, TweeningDirection, TweeningType};
 
 /// The dynamic tweenable type.
 ///
@@ -28,6 +28,7 @@ use crate::{EaseMethod, Lens, RepeatCount, RepeatStrategy, TweeningDirection};
 /// # 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!() }
@@ -59,20 +60,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.
-    ///
-    /// Note that [`RepeatCount::Infinite`] tweenables never reach this state.
+    /// time. This can only happen for [`TweeningType::Once`], since other
+    /// types loop indefinitely.
     Completed,
 }
 
 /// Event raised when a tween completed.
 ///
-/// 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).
+/// 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).
 ///
 /// # Note
 ///
@@ -96,94 +97,82 @@ pub struct TweenCompleted {
     pub user_data: u64,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Default, Clone, Copy)]
 struct AnimClock {
     elapsed: Duration,
     duration: Duration,
-    times_completed: u32,
-    total_duration: TotalDuration,
-    strategy: RepeatStrategy,
+    is_looping: bool,
 }
 
 impl AnimClock {
-    fn new(duration: Duration) -> Self {
+    fn new(duration: Duration, is_looping: bool) -> Self {
         Self {
             elapsed: Duration::ZERO,
             duration,
-            total_duration: compute_total_duration(duration, RepeatCount::default()),
-            times_completed: 0,
-            strategy: RepeatStrategy::default(),
+            is_looping,
         }
     }
 
-    fn record_completions(&mut self, times_completed: u32) {
-        self.times_completed = self.times_completed.saturating_add(times_completed);
-    }
+    fn tick(&mut self, duration: Duration) -> u32 {
+        self.elapsed = self.elapsed.saturating_add(duration);
 
-    fn tick(&mut self, tick: Duration) -> u32 {
-        let duration = self.duration.as_nanos();
+        if self.elapsed < self.duration {
+            0
+        } else if self.is_looping {
+            let elapsed = self.elapsed.as_nanos();
+            let duration = self.duration.as_nanos();
 
-        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 = Duration::from_nanos((elapsed % duration) as u64);
+            (elapsed / duration) as u32
+        } else {
+            self.elapsed = self.duration;
+            1
         }
-        (self.elapsed.as_nanos() / duration - before) as u32
     }
 
     fn set_progress(&mut self, progress: f32) {
-        self.elapsed = self.duration.mul_f32(progress.max(0.));
+        let progress = if self.is_looping {
+            progress.max(0.).fract()
+        } else {
+            progress.clamp(0., 1.)
+        };
+
+        self.elapsed = self.duration.mul_f32(progress);
     }
 
     fn progress(&self) -> f32 {
         self.elapsed.as_secs_f32() / self.duration.as_secs_f32()
     }
 
-    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 completed(&self) -> bool {
+        self.elapsed >= self.duration
     }
 
     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.
     ///
-    /// This is always the duration of a single iteration, even when looping.
+    /// 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.
     ///
-    /// 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.
+    /// 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.
     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.
@@ -191,12 +180,20 @@ pub trait Tweenable<T>: Send + Sync {
     /// [`progress()`]: Tweenable::progress
     fn set_progress(&mut self, progress: f32);
 
-    /// Get the current progress in \[0:1\] of the animation.
+    /// Get the current progress in \[0:1\] (non-looping) or \[0:1\[ (looping)
+    /// of the animation.
     ///
-    /// 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`.
+    /// 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.
     fn progress(&self) -> f32;
 
     /// Tick the animation, advancing it by the given delta time and mutating
@@ -226,10 +223,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
-    /// [`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).
+    /// 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).
     fn times_completed(&self) -> u32;
 
     /// Rewind the animation to its starting state.
@@ -273,6 +270,8 @@ 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>>>,
@@ -290,6 +289,7 @@ 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,6 +298,7 @@ impl<T: 'static> Tween<T> {
     /// );
     /// let tween2 = Tween::new(
     ///     EaseFunction::QuadraticInOut,
+    ///     TweeningType::Once,
     ///     Duration::from_secs_f32(1.0),
     ///     TransformRotationLens {
     ///         start: Quat::IDENTITY,
@@ -322,6 +323,7 @@ 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,
@@ -330,13 +332,20 @@ impl<T> Tween<T> {
     /// );
     /// ```
     #[must_use]
-    pub fn new<L>(ease_function: impl Into<EaseMethod>, duration: Duration, lens: L) -> Self
+    pub fn new<L>(
+        ease_function: impl Into<EaseMethod>,
+        tweening_type: TweeningType,
+        duration: Duration,
+        lens: L,
+    ) -> Self
     where
         L: Lens<T> + Send + Sync + 'static,
     {
         Self {
             ease_function: ease_function.into(),
-            clock: AnimClock::new(duration),
+            clock: AnimClock::new(duration, tweening_type != TweeningType::Once),
+            times_completed: 0,
+            tweening_type,
             direction: TweeningDirection::Forward,
             lens: Box::new(lens),
             on_completed: None,
@@ -358,6 +367,7 @@ impl<T> Tween<T> {
     /// let tween = Tween::new(
     ///     // [...]
     /// #    EaseFunction::QuadraticInOut,
+    /// #    TweeningType::Once,
     /// #    Duration::from_secs_f32(1.0),
     /// #    TransformPositionLens {
     /// #        start: Vec3::ZERO,
@@ -415,20 +425,6 @@ 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
@@ -473,6 +469,10 @@ 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,17 +488,22 @@ impl<T> Tweenable<T> for Tween<T> {
         entity: Entity,
         event_writer: &mut EventWriter<TweenCompleted>,
     ) -> TweenState {
-        if self.clock.state() == TweenState::Completed {
+        if !self.is_looping() && self.clock.completed() {
             return TweenState::Completed;
         }
 
         // Tick the animation clock
         let times_completed = self.clock.tick(delta);
-        self.clock.record_completions(times_completed);
-        if self.clock.strategy == RepeatStrategy::MirroredRepeat && times_completed & 1 != 0 {
+        self.times_completed += times_completed;
+        if times_completed & 1 != 0 && self.tweening_type == TweeningType::PingPong {
             self.direction = !self.direction;
         }
-        let progress = self.progress();
+        let state = if self.is_looping() || times_completed == 0 {
+            TweenState::Active
+        } else {
+            TweenState::Completed
+        };
+        let progress = self.clock.progress();
 
         // Apply the lens, even if the animation finished, to ensure the state is
         // consistent
@@ -522,15 +527,16 @@ impl<T> Tweenable<T> for Tween<T> {
             }
         }
 
-        self.clock.state()
+        state
     }
 
     fn times_completed(&self) -> u32 {
-        self.clock.times_completed
+        self.times_completed
     }
 
     fn rewind(&mut self) {
         self.clock.reset();
+        self.times_completed = 0;
     }
 }
 
@@ -617,6 +623,10 @@ 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
@@ -723,6 +733,10 @@ 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
@@ -804,6 +818,10 @@ 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();
@@ -873,8 +891,7 @@ mod tests {
     #[test]
     fn anim_clock_precision() {
         let duration = Duration::from_millis(1);
-        let mut clock = AnimClock::new(duration);
-        clock.total_duration = TotalDuration::Infinite;
+        let mut clock = AnimClock::new(duration, true);
 
         let test_ticks = [
             Duration::from_micros(123),
@@ -905,29 +922,27 @@ mod tests {
     #[test]
     fn tween_tick() {
         for tweening_direction in &[TweeningDirection::Forward, TweeningDirection::Backward] {
-            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),
+            for tweening_type in &[
+                TweeningType::Once,
+                TweeningType::Loop,
+                TweeningType::PingPong,
             ] {
                 println!(
-                    "TweeningType: count={count:?} strategy={strategy:?} dir={tweening_direction:?}",
+                    "TweeningType: type={:?} dir={:?}",
+                    tweening_type, 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_repeat_count(*count)
-                .with_repeat_strategy(*strategy);
+                .with_direction(*tweening_direction);
                 assert_eq!(tween.direction(), *tweening_direction);
                 assert!(tween.on_completed.is_none());
                 assert!(tween.event_data.is_none());
@@ -967,8 +982,8 @@ mod tests {
                 for i in 1..=11 {
                     // Calculate expected values
                     let (progress, times_completed, mut direction, expected_state, just_completed) =
-                        match count {
-                            RepeatCount::Finite(1) => {
+                        match tweening_type {
+                            TweeningType::Once => {
                                 let progress = (i as f32 * 0.2).min(1.0);
                                 let times_completed = if i >= 5 { 1 } else { 0 };
                                 let state = if i < 5 {
@@ -985,77 +1000,37 @@ mod tests {
                                     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::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::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,
-                                    )
+                            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
                                 } 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,
-                                        TweenState::Active,
-                                        just_completed,
-                                    )
-                                }
+                                    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;
@@ -1093,6 +1068,7 @@ 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);
@@ -1121,6 +1097,7 @@ 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);
 
@@ -1156,6 +1133,7 @@ mod tests {
     fn tween_dir() {
         let mut tween = Tween::new(
             EaseMethod::Linear,
+            TweeningType::Once,
             Duration::from_secs_f32(1.0),
             TransformPositionLens {
                 start: Vec3::ZERO,
@@ -1213,6 +1191,7 @@ mod tests {
     fn seq_tick() {
         let tween1 = Tween::new(
             EaseMethod::Linear,
+            TweeningType::Once,
             Duration::from_secs_f32(1.0),
             TransformPositionLens {
                 start: Vec3::ZERO,
@@ -1221,6 +1200,7 @@ mod tests {
         );
         let tween2 = Tween::new(
             EaseMethod::Linear,
+            TweeningType::Once,
             Duration::from_secs_f32(1.0),
             TransformRotationLens {
                 start: Quat::IDENTITY,
@@ -1251,13 +1231,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::ONE, 1e-5));
+                assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 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::ONE, 1e-5));
+                assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5));
                 assert!(transform
                     .rotation
                     .abs_diff_eq(Quat::from_rotation_x(90_f32.to_radians()), 1e-5));
@@ -1271,6 +1251,7 @@ 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,
@@ -1278,6 +1259,7 @@ mod tests {
                 },
             )
         }));
+        assert!(!seq.is_looping());
 
         let mut progress = 0.;
         for i in 1..5 {
@@ -1300,6 +1282,7 @@ mod tests {
     fn tracks_tick() {
         let tween1 = Tween::new(
             EaseMethod::Linear,
+            TweeningType::Once,
             Duration::from_secs_f32(1.),
             TransformPositionLens {
                 start: Vec3::ZERO,
@@ -1308,6 +1291,7 @@ mod tests {
         );
         let tween2 = Tween::new(
             EaseMethod::Linear,
+            TweeningType::Once,
             Duration::from_secs_f32(0.8), // shorter
             TransformRotationLens {
                 start: Quat::IDENTITY,
@@ -1316,6 +1300,7 @@ 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();
 
@@ -1347,7 +1332,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::ONE, 1e-5));
+                assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5));
                 assert!(transform
                     .rotation
                     .abs_diff_eq(Quat::from_rotation_x(90_f32.to_radians()), 1e-5));
@@ -1403,6 +1388,7 @@ mod tests {
         {
             let tweenable: &dyn Tweenable<Transform> = &delay;
             assert_eq!(tweenable.duration(), duration);
+            assert!(!tweenable.is_looping());
             assert!(tweenable.progress().abs() < 1e-5);
         }