diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d041b81..a04e1f8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -70,11 +70,6 @@ jobs: env: CARGO_INCREMENTAL: 1 RUSTFLAGS: "-C debuginfo=0 -D warnings" - - name: Build & run tests (asset) - run: cargo test --no-default-features --features="bevy_asset" - env: - CARGO_INCREMENTAL: 1 - RUSTFLAGS: "-C debuginfo=0 -D warnings" - name: Build & run tests (all) run: cargo test --all-features env: diff --git a/CHANGELOG.md b/CHANGELOG.md index a5fa151..dfcc8f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,112 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +_This change introduces a major API redesign compared to v0.13. +It removes the need for generics, +and unifies all animations of all components and assets, +executing them from a single exclusive system. +See the [Migration Guide for v0.14](./docs/migration-guide-0.14.md) for details._ + +### Added + +- Added `TweenAnim`, a representation of the runtime parameters of an active animation. +- Added `AnimTarget`, a component describing (via its `AnimTargetKind`) the target mutated by an animation. +- Added some extension functions on `EntityCommands` to enable simplified create-and-spawn patterns: + + ```rust + commands + // Spawn an entity to animate the position of + .spawn(Transform::default()) + // Create-and-spawn a new Transform::translation animation + .move_to( + Vec3::new(1., 2., -4.), + Duration::from_secs(1), + EaseFunction::QuadraticInOut, + ); + ``` + + See `EntityCommandsTweeningExtensions` for all the possible animations. + +- Added `AnimCompletedEvent`, raised when the entire tweenable animation completed. +- Added `CycleCompletedEvent`, optionally raised when a single tweenable animation + cycle completed. Enable it with `Tween::with_cycle_completed_event()`. +- Added `TotalDuration::from_cycles()` to simply creating an animation duration + from a number of individual cycles and their duration. +- Added `TotalDuration::is_finite()` helper to check an animation duration is finite. +- Added `TotalDuration::as_finite()` helper to convert an animation duration into + a `Duration` type if it's finite. +- Implemented various operations on `TotalDuration`: `From`, `Add`, `Sum`, + `PartialOrd`, `Ord`. +- Added `Tweenable::cycle_fraction()` to query the current position of the animation + inside a cycle. This is roughly equivalent to the previous `progress()`. +- Added `Tween::cycle_fraction()` and `Tween::cycle_index()` to query the position + of the animation inside a cycle and the cycle number, respectively. +- Added helper `Tween::is_cycle_mirrored()`, which returns `true` if the playback + of the current cycle the animation is at is mirrored. + This always returns `false` unless `RepeatStrategy::MirroredRepeat` is used, + and the animation contains more than 1 cycle. +- Added a new example `follow` demonstrating how to achieve following-with-smoothing. + The example moves an entity to follow the mouse cursor on screen. +- Added a new example `ambient_light` showing how to animate resources (here, `AmbientLight`). +- Added new built-in lenses applying a rotation on top of the existing `Transform::rotation`: + + - `TransformRotateAdditiveXLens` + - `TransformRotateAdditiveYLens` + - `TransformRotateAdditiveZLens` + + Those are useful to create animations with infinitely-rotating objects. + +### Changed + +- The `bevy_asset` feature was removed; `bevy_tweening` now depends on `bevy/bevy_asset` always. + You can just delete that feature from your `Cargo.toml` if you were adding it explicitly. +- `Sequence` now accepts infinite duration child animations. + It's your responsibility to ensure the resulting sequence makes sense, + which generally means only using an infinite animation as the last item. +- The following types lost their generic parameter ``: + - `Tweenable` + - `Tween` + - `Sequence` + - `Delay` + They still implicitly depend on a target type, but this is not encoded in the Rust type anymore. +- The `Lens` trait now takes its target as `Mut` instead of `&mut dyn Targetable`. + + ```rust + fn lerp(&mut self, target: Mut, ratio: f32) + ``` + + The functioning is the same, but this removes one level of indirection. + The use of `Mut` should be familiar to most Bevy users. + +- `Tweenable::duration()` was renamed to `Tweenable::cycle_duration()` for clarity. +- `Tweenable::times_completed()` was renamed to `Tweenable::cycles_completed()` for clarity. +- `Tweenable::tick()` was renamed to `Tweenable::step()`, to insist on the fact + the step computes a new state, but doesn't necessarily moves any time back or forth. +- `Tween::with_completed_event()` doesn't take `user_data` anymore, which was removed. + Instead it takes a `bool` indicating whether to send completion events or not. +- Renamed `Tween::set_direction()` into `Tween::set_playback_direction()` for clarity, + and `Tween::direction()` into `Tween::playback_direction()`. + Note the clarified semantic of playback direction vs. mirroring repeat; + see the migration guide for details. +- Renamed `AnimatorState` into `PlaybackState` for clarity. +- Renamed `TweeningDirection` into `PlaybackDirection` to clarify the fact it only affects + animation playback, and is completely unrelated to cycle mirroring repeat. + +### Removed + +- Removed the `component_animator_system` and `asset_animator_system`. + Animations are now auto-played based on the presence of a `TweenAnim` component. + There's no need for you to manually register anything. +- Removed `Tracks`. Use multiple animations instead. +- Removed `Targetable`, `ComponentTarget`, `AssetTarget`. Those were workarounds + for the inability to use `Mut` directly. In Bevy 0.16 they're not necessary. +- Removed all references to "progress" in all APIs, in favor of `Duration`s. +- Removed the `user_data` value from completed events. +- Removed callback-based completion events. Use Bevy-style events or one-shot systems instead. + Observers are also supported. + +## [0.13.0] - 2025-04-28 + ### Changed - Compatible with Bevy 0.16 diff --git a/Cargo.toml b/Cargo.toml index 5f92e56..1d247d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_tweening" -version = "0.13.0" +version = "0.14.0-dev" authors = [ "François Mockers ", "Jerome Humbert ", @@ -16,9 +16,7 @@ readme = "README.md" exclude = ["examples/*.gif", ".github", "release.md", "run_examples.bat"] [features] -default = ["bevy_sprite", "bevy_ui", "bevy_asset", "bevy_text"] -# Enable support for Asset animation -bevy_asset = ["bevy/bevy_asset"] +default = ["bevy_sprite", "bevy_ui", "bevy_text"] # Enable built-in lenses for Bevy sprites bevy_sprite = ["bevy/bevy_sprite", "bevy/bevy_render"] # Enable built-in lenses for Bevy UI @@ -28,18 +26,27 @@ bevy_text = ["bevy/bevy_text", "bevy/bevy_render", "bevy/bevy_sprite"] [dependencies] # Note: abuse 'bevy_color' to force 'bevy_math/curve' feature, which defines EaseFunction -bevy = { version = "0.16", default-features = false, features = [ "bevy_color" ]} +bevy = { version = "0.16", default-features = false, features = [ "bevy_color", "bevy_asset", "bevy_log" ]} +thiserror = "2" [dev-dependencies] bevy-inspector-egui = { version = "0.31" } +[[example]] +name = "ambient_light" +required-features = ["bevy_ui", "bevy_text", "bevy/bevy_winit", "bevy/bevy_picking", "bevy/bevy_pbr", "bevy/hdr", "bevy/tonemapping_luts"] + +[[example]] +name = "follow" +required-features = ["bevy_sprite", "bevy_text", "bevy/bevy_winit", "bevy/bevy_picking"] + [[example]] name = "menu" required-features = ["bevy_ui", "bevy_text", "bevy/bevy_winit", "bevy/bevy_picking"] [[example]] name = "colormaterial_color" -required-features = ["bevy_asset", "bevy_sprite", "bevy/bevy_winit", "bevy/bevy_picking"] +required-features = ["bevy_sprite", "bevy/bevy_winit", "bevy/bevy_picking"] [[example]] name = "sprite_color" diff --git a/README.md b/README.md index d1ad9bb..3121397 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Tweening animation plugin for the Bevy game engine. - [x] Animate any field of any component or asset, including custom ones. - [x] Run multiple tweens (animations) per component/asset in parallel. - [x] Chain multiple tweens (animations) one after the other for complex animations. -- [x] Raise a Bevy event or invoke a callback when an tween completed. +- [x] Raise a Bevy event or invoke a one-shot system when an animation completed. ## Usage @@ -24,14 +24,13 @@ Add to `Cargo.toml`: ```toml [dependencies] -bevy_tweening = "0.13" +bevy_tweening = "0.14-dev" ``` This crate supports the following features: | Feature | Default | Description | |---|---|---| -| `bevy_asset` | Yes | Enable animating Bevy assets (`Asset`) in addition of components. | | `bevy_sprite` | Yes | Includes built-in lenses for some `Sprite`-related components. | | `bevy_ui` | Yes | Includes built-in lenses for some UI-related components. | | `bevy_text` | Yes | Includes built-in lenses for some `Text`-related components. | @@ -47,48 +46,21 @@ App::default() .run(); ``` -This provides the basic setup for using 🍃 Bevy Tweening. However, additional setup is required depending on the components and assets you want to animate: - -- To ensure a component `C` is animated, the `component_animator_system::` system must run each frame, in addition of adding an `Animator::` component to the same Entity as `C`. - -- To ensure an asset `A` is animated, the `asset_animator_system::` system must run each frame, in addition of adding an `AssetAnimator` component to any Entity. Animating assets also requires the `bevy_asset` feature (enabled by default). - -By default, 🍃 Bevy Tweening adopts a minimalist approach, and the `TweeningPlugin` will only add systems to animate components and assets for which a `Lens` is provided by 🍃 Bevy Tweening itself. This means that any other Bevy component or asset (either built-in from Bevy itself, or custom) requires manually scheduling the appropriate system. - -| Component or Asset | Animation system added by `TweeningPlugin`? | -|---|---| -| `Transform` | Yes | -| `Sprite` | Only if `bevy_sprite` feature | -| `ColorMaterial` | Only if `bevy_sprite` feature | -| `Node` | Only if `bevy_ui` feature | -| `TextColor` | Only if `bevy_text` feature | -| All other components | No | - -To add a system for a component `C`, use: - -```rust -app.add_systems(Update, component_animator_system::.in_set(AnimationSystem::AnimationUpdate)); -``` - -Similarly for an asset `A`, use: - -```rust -app.add_systems(Update, asset_animator_system::.in_set(AnimationSystem::AnimationUpdate)); -``` +This provides enough setup for using 🍃 Bevy Tweening and animating any Bevy built-in or custom component or asset. Animations update as part of the `Update` schedule of Bevy. ### Animate a component -Animate the transform position of an entity by creating a `Tween` animation for the transform, and adding an `Animator` component with that tween: +Animate the transform position of an entity by creating a `Tween` animation for the transform, and enqueuing the animation with the `tween()` command extension: ```rust -// Create a single animation (tween) to move an entity. +// Create a single animation (tween) to move an entity back and forth. let tween = Tween::new( // Use a quadratic easing on both endpoints. EaseFunction::QuadraticInOut, // Animation time (one way only; for ping-pong it takes 2 seconds // to come back to start). Duration::from_secs(1), - // The lens gives the Animator access to the Transform component, + // The lens gives the TweenAnimator access to the Transform component, // to animate it. It also contains the start and end values associated // with the animation ratios 0. and 1. TransformPositionLens { @@ -96,21 +68,33 @@ let tween = Tween::new( end: Vec3::new(1., 2., -4.), }, ) -// Repeat twice (one per way) +// Repeat twice (once per direction) .with_repeat_count(RepeatCount::Finite(2)) -// After each iteration, reverse direction (ping-pong) +// After each cycle, reverse direction (ping-pong) .with_repeat_strategy(RepeatStrategy::MirroredRepeat); -commands.spawn(( - // Spawn a Sprite entity to animate the position of. - Sprite { - color: Color::RED, - custom_size: Some(Vec2::new(size, size)), - ..default() - }, - // Add an Animator component to control and execute the animation. - Animator::new(tween), -)); +commands + // Spawn an entity to animate the position of. + .spawn(Transform::default()) + // Queue the tweenable animation + .tween(tween); +``` + +This example shows the general pattern to add animations for any component +or asset. Since moving the position of an object is a very common +task, 🍃 Bevy Tweening provides a shortcut for it. The above example can be +rewritten more concicely as: + +```rust +commands + // Spawn an entity to animate the position of. + .spawn((Transform::default(),)) + // Create-and-queue a new Transform::translation animation + .move_to( + Vec3::new(1., 2., -4.), + Duration::from_secs(1), + EaseFunction::QuadraticInOut, + ); ``` ### Chaining animations @@ -119,7 +103,6 @@ Bevy Tweening supports several types of _tweenables_, building blocks that can b - **`Tween`** - A simple tween (easing) animation between two values. - **`Sequence`** - A series of tweenables executing in series, one after the other. -- **`Tracks`** - A collection of tweenables executing in parallel. - **`Delay`** - A time delay. Most tweenables can be chained with the `then()` operator: @@ -131,41 +114,43 @@ let tween2 = Tween { [...] } let seq = tween1.then(tween2); ``` -## Predefined Lenses +To execute multiple animations in parallel, simply enqueue each animation +independently. This require careful selection of timings. + +Note that some tweenable animations can be of infinite duration; this is the +case for example when using `RepeatCount::Infinite`. If you add such an +infinite animation in a sequence, and append more tweenable after it, those +tweenable will never play because playback will be stuck forever repeating +the first animation. You're responsible for creating sequences that make +sense. In general, only use infinite tweenable animations alone or as the +last element of a sequence. + +## Built-in Lenses A small number of predefined lenses are available for the most common use cases, which also serve as examples. **Users are encouraged to write their own lens to tailor the animation to their use case.** The naming scheme for predefined lenses is `"Lens"`, where `` is the name of the target Bevy component or asset type which is queried by the internal animation system to be modified, and `` is the field which is mutated in place by the lens. All predefined lenses modify a single field. Custom lenses can be written which modify multiple fields at once. -### Bevy Components - -| Target Component | Animated Field | Lens | Feature | +| Target | Animated Field | Lens | Feature | |---|---|---|---| -| [`Transform`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html) | [`translation`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.translation) | [`TransformPositionLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformPositionLens.html) | | -| | [`rotation`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.rotation) (`Quat`)¹ | [`TransformRotationLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformRotationLens.html) | | -| | [`rotation`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.rotation) (angle)² | [`TransformRotateXLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformRotateXLens.html) | | -| | [`rotation`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.rotation) (angle)² | [`TransformRotateYLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformRotateYLens.html) | | -| | [`rotation`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.rotation) (angle)² | [`TransformRotateZLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformRotateZLens.html) | | -| | [`rotation`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.rotation) (angle)² | [`TransformRotateAxisLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformRotateAxisLens.html) | | -| | [`scale`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.scale) | [`TransformScaleLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformScaleLens.html) | | +| [`Transform`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html) | [`translation`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.translation) | [`TransformPositionLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformPositionLens.html) | (builtin) | +| | [`rotation`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.rotation) (`Quat`)¹ | [`TransformRotationLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformRotationLens.html) | (builtin) | +| | [`rotation`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.rotation) (angle)² | [`TransformRotateXLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformRotateXLens.html) | (builtin) | +| | [`rotation`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.rotation) (angle)² | [`TransformRotateYLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformRotateYLens.html) | (builtin) | +| | [`rotation`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.rotation) (angle)² | [`TransformRotateZLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformRotateZLens.html) | (builtin) | +| | [`rotation`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.rotation) (angle)² | [`TransformRotateAxisLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformRotateAxisLens.html) | (builtin) | +| | [`scale`](https://docs.rs/bevy/0.16/bevy/transform/components/struct.Transform.html#structfield.scale) | [`TransformScaleLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TransformScaleLens.html) | (builtin) | | [`Sprite`](https://docs.rs/bevy/0.16/bevy/sprite/struct.Sprite.html) | [`color`](https://docs.rs/bevy/0.16/bevy/sprite/struct.Sprite.html#structfield.color) | [`SpriteColorLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.SpriteColorLens.html) | `bevy_sprite` | | [`Node`](https://docs.rs/bevy/0.16/bevy/ui/struct.Node.html) | [`position`](https://docs.rs/bevy/0.16/bevy/ui/struct.Node.html) | [`UiPositionLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.UiPositionLens.html) | `bevy_ui` | | [`BackgroundColor`](https://docs.rs/bevy/0.16/bevy/ui/struct.BackgroundColor.html) | | [`UiBackgroundColorLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.UiBackgroundColorLens.html) | `bevy_ui` | | [`TextColor`](https://docs.rs/bevy/0.16/bevy/text/struct.TextColor.html) | | [`TextColorLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.TextColorLens.html) | `bevy_text` | +| [`ColorMaterial`](https://docs.rs/bevy/0.16/bevy/sprite/struct.ColorMaterial.html) | [`color`](https://docs.rs/bevy/0.16/bevy/sprite/struct.ColorMaterial.html#structfield.color) | [`ColorMaterialColorLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.ColorMaterialColorLens.html) | `bevy_sprite` | There are two ways to interpolate rotations. See the [comparison of rotation lenses](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/index.html#rotations) for details: - ¹ Shortest-path interpolation between two rotations, using `Quat::slerp()`. - ² Angle-based interpolation, valid for rotations over ½ turn. -### Bevy Assets - -Asset animation always requires the `bevy_asset` feature. - -| Target Asset | Animated Field | Lens | Feature | -|---|---|---|---| -| [`ColorMaterial`](https://docs.rs/bevy/0.16/bevy/sprite/struct.ColorMaterial.html) | [`color`](https://docs.rs/bevy/0.16/bevy/sprite/struct.ColorMaterial.html#structfield.color) | [`ColorMaterialColorLens`](https://docs.rs/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/bevy_tweening/lens/struct.ColorMaterialColorLens.html) | `bevy_asset` + `bevy_sprite` | - ## Custom lens A custom lens allows animating any field or group of fields of a Bevy component or asset. A custom lens is a type implementing the `Lens` trait, which is generic over the type of component or asset. @@ -177,10 +162,11 @@ struct MyXAxisLens { } impl Lens for MyXAxisLens { - fn lerp(&mut self, target: &mut Transform, ratio: f32) { - let start = Vec3::new(self.start, 0., 0.); - let end = Vec3::new(self.end, 0., 0.); - target.translation = start + (end - start) * ratio; + fn lerp(&mut self, target: Mut, ratio: f32) { + let x = self.start * (1. - ratio) + self.end * ratio; + let y = target.translation.y; + let z = target.translation.z; + target.translation = Vec3::new(x, y, z); } } ``` @@ -192,7 +178,8 @@ The basic formula for lerp (linear interpolation) is either of: - `start + (end - start) * scalar` - `start * (1.0 - scalar) + end * scalar` -The two formulations are mathematically equivalent, but one may be more suited than the other depending on the type interpolated and the operations available, and the potential floating-point precision errors. +The two formulations are mathematically equivalent, but one may be more suited than the other depending on the type interpolated and the operations available, and the potential floating-point precision errors. Some types like `Vec3` also provide a `lerp()` function +which can be used directly. ## Custom component support @@ -208,17 +195,13 @@ struct MyCustomLens { } impl Lens for MyCustomLens { - fn lerp(&mut self, target: &mut MyCustomComponent, ratio: f32) { + fn lerp(&mut self, target: Mut, ratio: f32) { target.0 = self.start + (self.end - self.start) * ratio; } } ``` -Then, in addition, the system `component_animator_system::` needs to be added to the application, as described in [System Setup](#system-setup). This system will extract each frame all `CustomComponent` instances with an `Animator` on the same entity, and animate the component via its animator. - -## Custom asset support - -The process is similar to custom components, creating a custom lens for the custom asset. The system to add is `asset_animator_system::`, as described in [System Setup](#system-setup). This requires the `bevy_asset` feature (enabled by default). +Unlike previous versions of 🍃 Bevy Tweening, there's no other setup to animate custom components or assets. ## Examples @@ -280,77 +263,6 @@ cargo run --example sequence --features="bevy/bevy_winit" ![sequence](https://raw.githubusercontent.com/djeedai/bevy_tweening/8b3cad18a090078d9055d77a632be44e701aecc7/examples/sequence.gif) -## Ease Functions - -Many [ease functions](https://docs.rs/bevy/0.16/bevy/math/curve/enum.EaseFunction.html) are available from `bevy_math`: - -- Linear - > `f(t) = t` -- QuadraticIn - > `f(t) = t²` -- QuadraticOut - > `f(t) = -(t * (t - 2.0))` -- QuadraticInOut - > Behaves as `EaseFunction::QuadraticIn` for t < 0.5 and as `EaseFunction::QuadraticOut` for t >= 0.5 -- CubicIn - > `f(t) = t³` -- CubicOut - > `f(t) = (t - 1.0)³ + 1.0` -- CubicInOut - > Behaves as `EaseFunction::CubicIn` for t < 0.5 and as `EaseFunction::CubicOut` for t >= 0.5 -- QuarticIn - > `f(t) = t⁴` -- QuarticOut - > `f(t) = (t - 1.0)³ * (1.0 - t) + 1.0` -- QuarticInOut - > Behaves as `EaseFunction::QuarticIn` for t < 0.5 and as `EaseFunction::QuarticOut` for t >= 0.5 -- QuinticIn - > `f(t) = t⁵` -- QuinticOut - > `f(t) = (t - 1.0)⁵ + 1.0` -- QuinticInOut - > Behaves as `EaseFunction::QuinticIn` for t < 0.5 and as `EaseFunction::QuinticOut` for t >= 0.5 -- SineIn - > `f(t) = 1.0 - cos(t * π / 2.0)` -- SineOut - > `f(t) = sin(t * π / 2.0)` -- SineInOut - > Behaves as `EaseFunction::SineIn` for t < 0.5 and as `EaseFunction::SineOut` for t >= 0.5 -- CircularIn - > `f(t) = 1.0 - sqrt(1.0 - t²)` -- CircularOut - > `f(t) = sqrt((2.0 - t) * t)` -- CircularInOut - > Behaves as `EaseFunction::CircularIn` for t < 0.5 and as `EaseFunction::CircularOut` for t >= 0.5 -- ExponentialIn - > `f(t) = 2.0^(10.0 * (t - 1.0))` -- ExponentialOut - > `f(t) = 1.0 - 2.0^(-10.0 * t)` -- ExponentialInOut - > Behaves as `EaseFunction::ExponentialIn` for t < 0.5 and as `EaseFunction::ExponentialOut` for t >= 0.5 -- ElasticIn - > `f(t) = -2.0^(10.0 * t - 10.0) * sin((t * 10.0 - 10.75) * 2.0 * π / 3.0)` -- ElasticOut - > `f(t) = 2.0^(-10.0 * t) * sin((t * 10.0 - 0.75) * 2.0 * π / 3.0) + 1.0` -- ElasticInOut - > Behaves as `EaseFunction::ElasticIn` for t < 0.5 and as `EaseFunction::ElasticOut` for t >= 0.5 -- BackIn - > `f(t) = 2.70158 * t³ - 1.70158 * t²` -- BackOut - > `f(t) = 1.0 + 2.70158 * (t - 1.0)³ - 1.70158 * (t - 1.0)²` -- BackInOut - > Behaves as `EaseFunction::BackIn` for t < 0.5 and as `EaseFunction::BackOut` for t >= 0.5 -- BounceIn - > bouncy at the start! -- BounceOut - > bouncy at the end! -- BounceInOut - > Behaves as `EaseFunction::BounceIn` for t < 0.5 and as `EaseFunction::BounceOut` for t >= 0.5 -- Steps(usize) - > `n` steps connecting the start and the end -- Elastic(f32) - > `f(omega,t) = 1 - (1 - t)²(2sin(omega * t) / omega + cos(omega * t))`, parametrized by omega - ## Compatible Bevy versions The `main` branch is compatible with the latest Bevy release. diff --git a/benchmarks/benches/lens.rs b/benchmarks/benches/lens.rs index 3f764b3..1ce600e 100644 --- a/benchmarks/benches/lens.rs +++ b/benchmarks/benches/lens.rs @@ -6,7 +6,7 @@ use bevy::{ ecs::{change_detection::MaybeLocation, component::Tick}, prelude::*, }; -use bevy_tweening::{lens::*, ComponentTarget}; +use bevy_tweening::lens::*; use criterion::{black_box, Criterion}; fn text_color_lens(c: &mut Criterion) { @@ -18,16 +18,16 @@ fn text_color_lens(c: &mut Criterion) { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let mut target = Mut::new( &mut text_color, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); c.bench_function("TextColorLens", |b| { - b.iter(|| lens.lerp(&mut target, black_box(0.3))) + b.iter(|| lens.lerp(target.reborrow(), black_box(0.3))) }); } @@ -40,16 +40,16 @@ fn transform_position_lens(c: &mut Criterion) { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let mut target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); c.bench_function("TransformPositionLens", |b| { - b.iter(|| lens.lerp(&mut target, black_box(0.3))) + b.iter(|| lens.lerp(target.reborrow(), black_box(0.3))) }); } @@ -62,16 +62,16 @@ fn transform_rotation_lens(c: &mut Criterion) { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let mut target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); c.bench_function("TransformRotationLens", |b| { - b.iter(|| lens.lerp(&mut target, black_box(0.3))) + b.iter(|| lens.lerp(target.reborrow(), black_box(0.3))) }); } @@ -84,16 +84,16 @@ fn transform_scale_lens(c: &mut Criterion) { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let mut target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); c.bench_function("TransformScaleLens", |b| { - b.iter(|| lens.lerp(&mut target, black_box(0.3))) + b.iter(|| lens.lerp(target.reborrow(), black_box(0.3))) }); } diff --git a/docs/migration-guide-0.14.md b/docs/migration-guide-0.14.md new file mode 100644 index 0000000..6409b18 --- /dev/null +++ b/docs/migration-guide-0.14.md @@ -0,0 +1,91 @@ +# 🍃 Bevy Tweening 0.14 migration guide + +This migration guide describes the major changes to the 🍃 Bevy Tweening API +operated in v0.14, and how to upgrade code using previous versions. + +## Semantic + +The new version 0.14 introduces new and/or clarified semantics for many concepts. + +### Animation timeline and `Duration` + +In previous verisons, the mental model for time related quantities was often poorly defined, +and led to various confusions and edge cases. + +In v0.14, we removed all references to "progress", which was a floating-point value +loosely defined as the fraction of the animation already performed, over its total duration. +Instead, all time-related operations are performed directly with the `Duration` Rust type. + +Users should now adopt the following mental model for animation time: + +- Each animation has a _timeline_ starting at `t=0` and extending to the _total duration_ + of the animation, which can be infinite if _e.g._ the animation loops forever. + The timeline is a concept to help build a mental model; there's no timeline type in code. +- The animation is composed of one or more _cycles_, which repeat eith a given number of times, + or until a duration is elapsed. This is configured by the `RepeatCount` type. +- The _elapsed_ time of an animation is the position on its timeline. It ranges from `0` + to its total duration (except for infinitely repeating animations; see details below). +- In general, cycles are repeated identically. However, as an alternative, the user can configure + the _repeat strategy_ to mirror every other cycle. Mirrored cycles animate their target + in reverse order. However, note that **the elapsed time value is unaffected by mirroring, + and continues to be expressed as an absolute time position** on the animation timeline. + This is a major semantic change compared to previous versions, and greatly clarifies + the implementation and the timeline mental model. + +Standard (non-mirrored) repeating of cycles: + +```txt + ratio cycle duration + ^ |<->| +1 | / / / / /| /| / / / / + | / / / / / | / | / / / / + |/ / / / / |/ |/ / / / / +0 *-----------------------------------------*------> timeline + 0 <------ total duration ------> | +``` + +Mirrored repeating of cycles: + +```txt + ratio cycle duration + ^ |<->| +1 | /\ /\ /\ | /|\ /\ /\ /\ + | / \ / \ / \ | / | \ / \ / \ / + |/ \/ \/ \|/ | \/ \/ \/ +0 *-----------------------------------------*-----> timeline + 0 <------ total duration ------> | +``` + +Note that **the cycle duration is half the duration of the "loop" formed by the mirroring**. +For this reason, 🍃 Bevy Tweening now avoids using the term "loop", to prevent confusion. + +In the above, the `ratio` is a value in `[0:1]` which gets passed to the easing function. +The output is then fed to `Lens::lerp()` to calculate the animation target state. +When using a linear easing function, `ratio` is exactly the lerp fraction. + +Consequence of the above, functions like `Tweenable::set_elapsed()` operate on the timeline, +using absolute duration. This means that calling `set_elapsed(2.5s)` on an animation composed +of 3 cycles of 1s each effectively puts the animation position at half of the third cycle. + +### Playback direction + +Related to the animation timeline and absolute duration position, +the _playback direction_ determines whether calls to `Tweenable::step()` move the animation +time forward or backward **on the animation timeline**. + +**This means the playback direction is completely unrelated to cycles mirroring.** + +This is a major clarification over the confusing semantic of previous versions, which tended +to mix the two concepts and produced often unexpected results. + +### Completion events and completed state + +_Completion events_ are emitted when an animation complete a cycle. + +To prevent any confusion, the reversed playback direction is considerd to be an editing tool, +not typical of normal use in a game or other application, and therefore **completion events are +never emitted in reverse playback**. + +Related but distinct, the _completion state_ of the animation determines if the animation +can continue playback or not. The animation is said to be completed when its elapsed time +reaches its total duration, at the end of its timeline. Infinite animations never complete. diff --git a/examples/ambient_light.rs b/examples/ambient_light.rs new file mode 100644 index 0000000..2a3df7b --- /dev/null +++ b/examples/ambient_light.rs @@ -0,0 +1,162 @@ +//! Example demonstrating resource animation and various transform shortcuts. +//! +//! The example animates the `AmbientLight` resource of Bevy's PBR renderer. +//! This is mostly for example purpose; you probably want to animate some other +//! (custom) resource. It also moves a capsule object back and forth with the +//! `move_to()` command extension, and make it "resonate" by quickly scaling it +//! between 100% and 110% size with the `scale_to()` command extension. + +use std::{ + f32::consts::{FRAC_PI_2, FRAC_PI_4}, + time::Duration, +}; + +use bevy::{color::palettes::css::*, core_pipeline::bloom::Bloom, prelude::*}; +use bevy_tweening::{lens::*, *}; + +mod utils; + +// Define our own `Lens` to animate the `AmbientLight` resource. +struct AmbientLightBrightnessLens { + pub start: f32, + pub end: f32, +} + +// Implement the `Lens` trait. +impl Lens for AmbientLightBrightnessLens { + fn lerp(&mut self, mut target: Mut, ratio: f32) { + target.brightness = self.start.lerp(self.end, ratio); + } +} + +fn main() { + App::default() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "AmbientLightLens".to_string(), + resolution: (1200., 600.).into(), + present_mode: bevy::window::PresentMode::Fifo, // vsync + ..default() + }), + ..default() + })) + .add_systems(Update, utils::close_on_esc) + .add_plugins(TweeningPlugin) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut ambient_light: ResMut, +) -> Result<(), BevyError> { + // Some fancy 3D camera with HDR and bloom, to emphasize the change of ambient + // brightness. + commands.spawn(( + Camera { + hdr: true, + clear_color: Color::BLACK.into(), + ..default() + }, + Bloom { + intensity: 0.2, + ..default() + }, + Camera3d::default(), + Transform::from_xyz(0., -7., 2.).looking_at(Vec3::ZERO, Vec3::Z), + )); + + // Set some default ambient color, and zero out brightness for now + ambient_light.color = Color::linear_rgb(1., 1., 1.); + ambient_light.brightness = 40.0; + + // Some sample "ground" circle, slowly rotating. + commands + .spawn(( + Mesh3d(meshes.add(Circle::new(4.))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: LIGHT_GREEN.into(), + ..default() + })), + Transform::default(), + )) + .rotate_z(Duration::from_secs(20)); + + // Animate the ambient light's brightness between fairly extreme values, for + // example purpose only (please don't do that). + let tween = Tween::new( + EaseFunction::CubicIn, + Duration::from_secs(2), + AmbientLightBrightnessLens { + start: 40.0, // very dark + end: 10000., // ahhhh, my eyes! + }, + ) + .with_repeat(RepeatCount::Infinite, RepeatStrategy::MirroredRepeat); + commands.spawn(( + TweenAnim::new(tween), + AnimTarget::resource::(), + )); + + // Spawn some animated character-like capsule... + commands + .spawn(( + Mesh3d(meshes.add(Capsule3d::new(0.5, 1.))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: PURPLE.into(), + ..default() + })), + Transform::from_rotation(Quat::from_rotation_x(FRAC_PI_2)) + .with_translation(Vec3::new(-1., 0., 1.)), + )) + // ...moving left and right (the start position is the Transform::translation)... + .move_to( + Vec3::new(1., 0., 1.), + Duration::from_secs(1), + EaseFunction::CircularInOut, + ) + .with_repeat(RepeatCount::Infinite, RepeatStrategy::MirroredRepeat) + // This demonstrates that we can continue using the regular EntityCommands to insert more + // components on the current entity for example. + .insert(Name::new("NPC")) + // However, after doing so reborrow() is required because insert() returns &mut + // EntityCommands, but the animation extensions need it by value. + .reborrow() + // ...slightly scaling up and back to normal size (the start scale is the Transform::scale). + .scale_to( + Vec3::splat(1.1), + Duration::from_millis(200), + EaseFunction::BounceInOut, + ) + .with_repeat(RepeatCount::Infinite, RepeatStrategy::MirroredRepeat); + + // A gold cube rotating indefinitely over itself + commands + .spawn(( + Mesh3d(meshes.add(Cuboid::from_length(1.))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: GOLD.into(), + ..default() + })), + Transform::from_rotation(Quat::from_axis_angle(Vec3::ONE, FRAC_PI_4)) + .with_translation(Vec3::new(4., 0., 2.)), + )) + .rotate_x(Duration::from_secs(5)); + + // A blue cuboid rotating back and forth + commands + .spawn(( + Mesh3d(meshes.add(Cuboid::new(1., 1., 2.))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: BLUE.into(), + ..default() + })), + Transform::from_translation(Vec3::new(-4., 0., 1.5)), + )) + .rotate_x_by(FRAC_PI_2, Duration::from_secs(2), EaseFunction::BounceInOut) + .with_repeat(RepeatCount::Infinite, RepeatStrategy::MirroredRepeat); + + Ok(()) +} diff --git a/examples/colormaterial_color.rs b/examples/colormaterial_color.rs index 5f17730..d4f2f62 100644 --- a/examples/colormaterial_color.rs +++ b/examples/colormaterial_color.rs @@ -1,102 +1,107 @@ -use bevy::{color::palettes::css::*, prelude::*}; -use bevy_tweening::{lens::*, *}; -use std::time::Duration; - -mod utils; - -fn main() { - App::default() - .add_plugins(DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - title: "ColorMaterialColorLens".to_string(), - resolution: (1200., 600.).into(), - present_mode: bevy::window::PresentMode::Fifo, // vsync - ..default() - }), - ..default() - })) - .add_systems(Update, utils::close_on_esc) - .add_plugins(TweeningPlugin) - .add_systems(Startup, setup) - .run(); -} - -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - commands.spawn(Camera2d::default()); - - let size = 80.; - - let spacing = 1.25; - let screen_x = 450.; - let screen_y = 120.; - let mut x = -screen_x; - let mut y = screen_y; - - let quad_mesh = meshes.add(Rectangle::new(1., 1.)); - - for ease_function in &[ - EaseFunction::QuadraticIn, - EaseFunction::QuadraticOut, - EaseFunction::QuadraticInOut, - EaseFunction::CubicIn, - EaseFunction::CubicOut, - EaseFunction::CubicInOut, - EaseFunction::QuarticIn, - EaseFunction::QuarticOut, - EaseFunction::QuarticInOut, - EaseFunction::QuinticIn, - EaseFunction::QuinticOut, - EaseFunction::QuinticInOut, - EaseFunction::SineIn, - EaseFunction::SineOut, - EaseFunction::SineInOut, - EaseFunction::CircularIn, - EaseFunction::CircularOut, - EaseFunction::CircularInOut, - EaseFunction::ExponentialIn, - EaseFunction::ExponentialOut, - EaseFunction::ExponentialInOut, - EaseFunction::ElasticIn, - EaseFunction::ElasticOut, - EaseFunction::ElasticInOut, - EaseFunction::BackIn, - EaseFunction::BackOut, - EaseFunction::BackInOut, - EaseFunction::BounceIn, - EaseFunction::BounceOut, - EaseFunction::BounceInOut, - ] { - // Create a unique material per entity, so that it can be animated - // without affecting the other entities. Note that we could share - // that material among multiple entities, and animating the material - // asset would change the color of all entities using that material. - let unique_material = materials.add(Color::BLACK); - - let tween = Tween::new( - *ease_function, - Duration::from_secs(1), - ColorMaterialColorLens { - start: RED.into(), - end: BLUE.into(), - }, - ) - .with_repeat_count(RepeatCount::Infinite) - .with_repeat_strategy(RepeatStrategy::MirroredRepeat); - - commands.spawn(( - Mesh2d(quad_mesh.clone()), - MeshMaterial2d(unique_material), - Transform::from_translation(Vec3::new(x, y, 0.)).with_scale(Vec3::splat(size)), - AssetAnimator::new(tween), - )); - y -= size * spacing; - if y < -screen_y { - x += size * spacing; - y = screen_y; - } - } -} +use std::time::Duration; + +use bevy::{color::palettes::css::*, prelude::*}; +use bevy_tweening::{lens::*, *}; + +mod utils; + +fn main() { + App::default() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "ColorMaterialColorLens".to_string(), + resolution: (1200., 600.).into(), + present_mode: bevy::window::PresentMode::Fifo, // vsync + ..default() + }), + ..default() + })) + .add_systems(Update, utils::close_on_esc) + .add_plugins(TweeningPlugin) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) -> Result<(), BevyError> { + commands.spawn(Camera2d::default()); + + let size = 80.; + + let spacing = 1.25; + let screen_x = 450.; + let screen_y = 120.; + let mut x = -screen_x; + let mut y = screen_y; + + let quad_mesh = meshes.add(Rectangle::new(1., 1.)); + + for ease_function in &[ + EaseFunction::QuadraticIn, + EaseFunction::QuadraticOut, + EaseFunction::QuadraticInOut, + EaseFunction::CubicIn, + EaseFunction::CubicOut, + EaseFunction::CubicInOut, + EaseFunction::QuarticIn, + EaseFunction::QuarticOut, + EaseFunction::QuarticInOut, + EaseFunction::QuinticIn, + EaseFunction::QuinticOut, + EaseFunction::QuinticInOut, + EaseFunction::SineIn, + EaseFunction::SineOut, + EaseFunction::SineInOut, + EaseFunction::CircularIn, + EaseFunction::CircularOut, + EaseFunction::CircularInOut, + EaseFunction::ExponentialIn, + EaseFunction::ExponentialOut, + EaseFunction::ExponentialInOut, + EaseFunction::ElasticIn, + EaseFunction::ElasticOut, + EaseFunction::ElasticInOut, + EaseFunction::BackIn, + EaseFunction::BackOut, + EaseFunction::BackInOut, + EaseFunction::BounceIn, + EaseFunction::BounceOut, + EaseFunction::BounceInOut, + ] { + // Create a unique material per entity, so that it can be animated + // without affecting the other entities. Note that we could share + // that material among multiple entities, and animating the material + // asset would change the color of all entities using that material. + let unique_material = materials.add(Color::BLACK); + + let tween = Tween::new( + *ease_function, + Duration::from_secs(1), + ColorMaterialColorLens { + start: RED.into(), + end: BLUE.into(), + }, + ) + .with_repeat_count(RepeatCount::Infinite) + .with_repeat_strategy(RepeatStrategy::MirroredRepeat); + + commands.spawn(( + Mesh2d(quad_mesh.clone()), + MeshMaterial2d(unique_material.clone()), + Transform::from_translation(Vec3::new(x, y, 0.)).with_scale(Vec3::splat(size)), + TweenAnim::new(tween), + AnimTarget::asset(&unique_material), + )); + + y -= size * spacing; + if y < -screen_y { + x += size * spacing; + y = screen_y; + } + } + + Ok(()) +} diff --git a/examples/follow.rs b/examples/follow.rs new file mode 100644 index 0000000..fbcc2d6 --- /dev/null +++ b/examples/follow.rs @@ -0,0 +1,157 @@ +//! Example demonstrating resource animation and various transform shortcuts. +//! +//! The example animates the `AmbientLight` resource of Bevy's PBR renderer. +//! This is mostly for example purpose; you probably want to animate some other +//! (custom) resource. + +use std::time::Duration; + +use bevy::{color::palettes::css::*, prelude::*}; +use bevy_tweening::{lens::TransformPositionLens, *}; + +mod utils; + +#[derive(Component)] +struct Follower; + +#[derive(Debug, Default, Clone, Copy)] +enum AnimOption { + OverwriteComponent, + #[default] + CallSetTweenable, +} + +// Simple way to toggle between the two options. Don't do that in real code. +impl std::ops::Not for AnimOption { + type Output = Self; + + fn not(self) -> Self::Output { + match self { + Self::OverwriteComponent => Self::CallSetTweenable, + Self::CallSetTweenable => Self::OverwriteComponent, + } + } +} + +#[derive(Default, Component)] +struct Anim { + pub option: AnimOption, +} + +fn main() { + App::default() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Follow".to_string(), + resolution: (1200., 600.).into(), + present_mode: bevy::window::PresentMode::Fifo, // vsync + ..default() + }), + ..default() + })) + .add_systems(Update, utils::close_on_esc) + .add_plugins(TweeningPlugin) + .add_systems(Startup, setup) + .add_systems(PreUpdate, change_option) + .add_systems(Update, follow) + .run(); +} + +fn make_tween(start: Vec3, end: Vec3) -> Tween { + let lens = TransformPositionLens { start, end }; + Tween::new( + // Start fast to catch-up with target, slow down when closer + EaseFunction::QuadraticOut, + // Short duration for the appareance to remain snappy + Duration::from_millis(200), + lens, + ) +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(Camera2d::default()); + + // Spawn the follower entity + let entity = commands + .spawn(( + Mesh2d(meshes.add(Rectangle::new(10., 10.))), + MeshMaterial2d(materials.add(ColorMaterial { + color: WHITE.into(), + ..default() + })), + Follower, + )) + .id(); + + // Spawn the TweenAnim animating the follower. We want to keep overwriting it as + // the cursor move, so we need to remember its Entity (we use a marker component + // here). + commands.spawn(( + // Our marker + Anim::default(), + // The animation itself + TweenAnim::new(make_tween(Vec3::ZERO, Vec3::ZERO)) + // For performance reason, we keep this animation around and overwrite it. Otherwise it + // will keep being removed and re-inserted. + .with_destroy_on_completed(false), + // The target of the animation, here a component on the given entity + AnimTarget::component::(entity), + )); +} + +fn change_option(keyboard: Res>, mut q_anim: Single<&mut Anim>) { + if keyboard.just_pressed(KeyCode::Space) { + q_anim.option = !q_anim.option; + println!("Anim option : {:?}", q_anim.option); + } +} + +fn follow( + mut commands: Commands, + mut ev: EventReader, + q_follower: Single<&Transform, With>, + q_camera: Single<(&GlobalTransform, &Camera)>, + mut q_anim: Single<(Entity, &Anim, &mut TweenAnim)>, +) { + let Some(ev) = ev.read().last() else { + return; + }; + let (camera_transform, camera) = *q_camera; + + let target_transform = *q_follower; + if let Ok(pos) = camera.viewport_to_world_2d(camera_transform, ev.position) { + if pos != target_transform.translation.truncate() { + // Note: Do NOT use move_to(), because it spawns a new separate TweenAnim and + // Entity each time, and we will end up with multiple of them animating the same + // target but aiming at different end positions, which will produce some visual + // jittering. We want to overwrite the same TweenAnim again and again with a new + // target position. + + match q_anim.1.option { + // Option 1. Use the fact spawning a component overwrites a previous instance. + AnimOption::OverwriteComponent => { + commands.entity(q_anim.0).insert( + TweenAnim::new(make_tween(target_transform.translation, pos.extend(0.))) + // We again keep that component around even when the animation + // completed, both for performance and because we use a Single<> query + // which otherwise would fail once the animation completed and the + // TweenAnim was auto-destroyed. + .with_destroy_on_completed(false), + ); + } + // Option 2. Keep the same component, simply use set_tweenable() to + // update the Tween with a new one targetting the new cursor position. + AnimOption::CallSetTweenable => { + q_anim + .2 + .set_tweenable(make_tween(target_transform.translation, pos.extend(0.))) + .unwrap(); + } + } + } + } +} diff --git a/examples/menu.rs b/examples/menu.rs index 2e2428c..c32e5d4 100644 --- a/examples/menu.rs +++ b/examples/menu.rs @@ -1,7 +1,8 @@ +use std::time::Duration; + use bevy::{color::palettes::css::*, prelude::*}; use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin}; use bevy_tweening::{lens::*, *}; -use std::time::Duration; mod utils; @@ -9,19 +10,26 @@ const NORMAL_COLOR: Color = Color::srgba(162. / 255., 226. / 255., 95. / 255., 1 const HOVER_COLOR: Color = Color::Srgba(AZURE); const CLICK_COLOR: Color = Color::Srgba(ALICE_BLUE); const TEXT_COLOR: Color = Color::srgba(83. / 255., 163. / 255., 130. / 255., 1.); -const INIT_TRANSITION_DONE: u64 = 1; -/// The menu in this example has two set of animations: -/// one for appearance, one for interaction. Interaction animations -/// are only enabled after appearance animations finished. +#[derive(Component)] +struct InitialAnimMarker; + +/// The menu in this example has two set of animations: one for appearance, one +/// for interaction. Interaction animations are only enabled after appearance +/// animations finished. /// /// The logic is handled as: -/// 1. Appearance animations send a `TweenComplete` event with -/// `INIT_TRANSITION_DONE` 2. The `enable_interaction_after_initial_animation` -/// system adds a label component `InitTransitionDone` to any button component -/// which completed its appearance animation, to mark it as active. -/// 3. The `interaction` system only queries buttons with a `InitTransitionDone` -/// marker. +/// 1. Appearance animations send an `AnimCompletedEvent` +/// 2. The `enable_interaction_after_initial_animation()` system adds a +/// `HoverAnim` component to any button component which completed its +/// appearance animation, to mark it as active. This component also contains +/// the entity of the current hover animation being played, if any. +/// 3. The `interaction()` system only queries buttons with a `HoverAnim` +/// component, and override the tweenable animation based on the hover state. +/// +/// For simplicity step 2. is handled via an observer. Note that the observer is +/// on the Entity which owns the TweenAnim, and not on the one owning the +/// animated component. fn main() { App::default() .add_plugins(( @@ -42,7 +50,6 @@ fn main() { )) .add_systems(Update, utils::close_on_esc) .add_systems(Update, interaction) - .add_systems(Update, enable_interaction_after_initial_animation) .add_systems(Startup, setup) .run(); } @@ -52,6 +59,7 @@ fn setup(mut commands: Commands, asset_server: Res) { let font = asset_server.load("fonts/FiraMono-Regular.ttf"); + // The menu "container" node, parent of all menu buttons commands .spawn(( Name::new("menu"), @@ -72,6 +80,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, )) .with_children(|container| { + // The individual menu buttons let mut start_time_ms = 0; for (text, label) in [ ("Continue", ButtonLabel::Continue), @@ -87,17 +96,9 @@ fn setup(mut commands: Commands, asset_server: Res) { end: Vec3::ONE, }, ) - .with_completed_event(INIT_TRANSITION_DONE); - - let animator = if start_time_ms > 0 { - let delay = Delay::new(Duration::from_millis(start_time_ms)); - Animator::new(delay.then(tween_scale)) - } else { - Animator::new(tween_scale) - }; + .with_cycle_completed_event(true); - start_time_ms += 500; - container + let target = container .spawn(( Name::new(format!("button:{}", text)), Button, @@ -114,11 +115,8 @@ fn setup(mut commands: Commands, asset_server: Res) { }, BackgroundColor(NORMAL_COLOR), Transform::from_scale(Vec3::splat(0.01)), - animator, label, - )) - .with_children(|parent| { - parent.spawn(( + children![( Text::new(text.to_string()), TextFont { font: font.clone(), @@ -127,25 +125,63 @@ fn setup(mut commands: Commands, asset_server: Res) { }, TextColor(TEXT_COLOR), TextLayout::new_with_justify(JustifyText::Center), - )); - }); + )], + )) + .id(); + + let tweenable = if start_time_ms > 0 { + let delay = Delay::new(Duration::from_millis(start_time_ms)); + delay.then(tween_scale).into_boxed() + } else { + tween_scale.into_boxed() + }; + container + .spawn(( + InitialAnimMarker, + TweenAnim::new(tweenable), + AnimTarget::component::(target), + )) + .observe(enable_interaction_after_initial_animation); + + start_time_ms += 500; } }); } fn enable_interaction_after_initial_animation( + trigger: Trigger, mut commands: Commands, - mut reader: EventReader, + q_names: Query<&Name>, ) { - for event in reader.read() { - if event.user_data == INIT_TRANSITION_DONE { - commands.entity(event.entity).insert(InitTransitionDone); - } + if let AnimTargetKind::Component { + entity: target_entity, + } = &trigger.target + { + // Resolve the Entity to a friendly name through the Name component. This is + // optional, just to make the message nicer. + let name = q_names + .get(*target_entity) + .ok() + .map(Into::into) + .unwrap_or(format!("{:?}", target_entity)); + + println!("Button on entity {name} completed initial animation, activating...",); + + // Spawn an Entity to hold the animation itself. We add the AnimTarget, which + // doesn't change, but not yet any TweenAnim since we have no animation to play. + let anim_entity = commands + .spawn(AnimTarget::component::(*target_entity)) + .id(); + + // Add the HoverAnim component which also acts as a marker + commands + .entity(*target_entity) + .insert(HoverAnim(anim_entity)); } } #[derive(Component)] -struct InitTransitionDone; +struct HoverAnim(pub Entity); #[derive(Component, Clone, Copy)] enum ButtonLabel { @@ -156,18 +192,21 @@ enum ButtonLabel { } fn interaction( + mut commands: Commands, mut interaction_query: Query< ( - &mut Animator, &Transform, &Interaction, &mut BackgroundColor, &ButtonLabel, + &HoverAnim, ), - (Changed, With), + Changed, >, ) { - for (mut animator, transform, interaction, mut color, button_label) in &mut interaction_query { + for (transform, interaction, mut color, button_label, hover_anim) in &mut interaction_query { + let anim_entity = hover_anim.0; + match *interaction { Interaction::Pressed => { *color = CLICK_COLOR.into(); @@ -189,27 +228,35 @@ fn interaction( } Interaction::Hovered => { *color = HOVER_COLOR.into(); - animator.set_tweenable(Tween::new( + let tween = Tween::new( EaseFunction::QuadraticIn, Duration::from_millis(200), TransformScaleLens { - start: Vec3::ONE, + start: transform.scale, end: Vec3::splat(1.1), }, - )); + ); + + // Set the animation by overwriting the TweenAnim component. This way we don't + // need to check if the previous animation was finished or not (and therefore if + // the TweenAnim component was deleted or not). + commands.entity(anim_entity).insert(TweenAnim::new(tween)); } Interaction::None => { *color = NORMAL_COLOR.into(); - let start_scale = transform.scale; - - animator.set_tweenable(Tween::new( + let tween = Tween::new( EaseFunction::QuadraticIn, Duration::from_millis(200), TransformScaleLens { - start: start_scale, + start: transform.scale, end: Vec3::ONE, }, - )); + ); + + // Set the animation by overwriting the TweenAnim component. This way we don't + // need to check if the previous animation was finished or not (and therefore if + // the TweenAnim component was deleted or not). + commands.entity(anim_entity).insert(TweenAnim::new(tween)); } } } diff --git a/examples/sequence.rs b/examples/sequence.rs index e510211..1472567 100644 --- a/examples/sequence.rs +++ b/examples/sequence.rs @@ -1,6 +1,7 @@ +use std::time::Duration; + use bevy::{color::palettes::css::*, prelude::*}; use bevy_tweening::{lens::*, *}; -use std::time::Duration; mod utils; @@ -29,15 +30,15 @@ struct RedProgress; struct BlueProgress; #[derive(Component)] -struct RedSprite; +struct RedAnimMarker; #[derive(Component)] -struct BlueSprite; +struct BlueAnimMarker; #[derive(Component)] struct ProgressValue; -fn setup(mut commands: Commands, asset_server: Res) { +fn setup(mut commands: Commands, asset_server: Res) -> Result<()> { commands.spawn(Camera2d::default()); let font = asset_server.load("fonts/FiraMono-Regular.ttf"); @@ -48,7 +49,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }; let text_color_red = TextColor(RED.into()); - let text_color_blue = TextColor(BLUE.into()); + let text_color_blue = TextColor(AQUA.into()); let justify = JustifyText::Center; @@ -111,20 +112,22 @@ fn setup(mut commands: Commands, asset_server: Res) { Vec3::new(margin, screen_y - margin, 0.), Vec3::new(margin, margin, 0.), ]; - // Build a sequence from an iterator over a Tweenable (here, a - // Tracks) - 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), + + // Red sprite + { + let entity = commands + .spawn(Sprite { + color: RED.into(), + custom_size: Some(Vec2::new(size, size)), + ..default() + }) + // Insert a rotation animation via the commands extension, to rotate over self, forever. + // This will continue even after the move along path animation added below finished. + .rotate_z(Duration::from_secs(2)) + .id(); + + // Build a sequence from an iterator over a Tweenable + let anim_move_along_path = Sequence::new(dests.windows(2).map(|pair| { Tween::new( EaseFunction::QuadraticInOut, Duration::from_secs(1), @@ -133,88 +136,129 @@ fn setup(mut commands: Commands, asset_server: Res) { end: pair[1] - center, }, ) - // Get an event after each segment - .with_completed_event(index as u64), - ]) - })); - - commands.spawn(( - Sprite { - color: RED.into(), - custom_size: Some(Vec2::new(size, size)), - ..default() - }, - RedSprite, - Animator::new(seq), - )); - - // First move from left to right, then rotate around self 180 degrees while - // scaling size at the same time. - let tween_move = Tween::new( - EaseFunction::QuadraticInOut, - Duration::from_secs(1), - TransformPositionLens { - start: Vec3::new(-200., 100., 0.), - end: Vec3::new(200., 100., 0.), - }, - ) - .with_completed_event(99); // Get an event once move completed - let tween_rotate = Tween::new( - EaseFunction::QuadraticInOut, - Duration::from_secs(1), - TransformRotationLens { - start: Quat::IDENTITY, - end: Quat::from_rotation_z(180_f32.to_radians()), - }, - ); - let tween_scale = Tween::new( - EaseFunction::QuadraticInOut, - Duration::from_secs(1), - TransformScaleLens { - start: Vec3::ONE, - end: Vec3::splat(2.0), - }, - ); - // Build parallel tracks executing two tweens at the same time: rotate and - // scale. - let tracks = Tracks::new([tween_rotate, tween_scale]); - // Build a sequence from an heterogeneous list of tweenables by casting them - // manually to a BoxedTweenable: first move, then { rotate + scale }. - let seq2 = Sequence::new([Box::new(tween_move) as BoxedTweenable<_>, tracks.into()]); - - commands.spawn(( - Sprite { - color: BLUE.into(), - custom_size: Some(Vec2::new(size * 3., size)), - ..default() - }, - BlueSprite, - Animator::new(seq2), - )); + })); + commands.spawn(( + RedAnimMarker, + TweenAnim::new(anim_move_along_path), + AnimTarget::component::(entity), + )); + } + + // Blue sprite + { + let entity = commands + .spawn(Sprite { + color: AQUA.into(), + custom_size: Some(Vec2::new(size * 3., size)), + ..default() + }) + .id(); + + // First move from left to right, then rotate around self 180 degrees while + // scaling size at the same time. + + // In previous versions of bevy_tweening, this could be accomplished with a + // Tracks, which allowed to run in parallel animations of different duration. + // That interface was confusing and had too many corner cases, so was removed. + // + // Instead, we have 2 solutions: + // 1. Insert one sequence which moves then rotates, and another which waits + // (Delay tweenable) then starts to scale at the same time the first sequence + // starts to rotate. In most cases this is the simplest, but requires + // controlling the timings of the animations. + // 2. Insert a single sequence which moves then {rotates+scales}, using a custom + // Lens which can apply both the rotation and scale with a single Tween. This + // guarantees perfect timing alignment, and doesn't require knowing the + // duration of the first (move) animation. A minor drawback is that we have + // to write a custom Lens. + // + // Here we show how option 1. is implemented, which is often the simplest. + + let move_duration = Duration::from_secs(1); + let tween_move = Tween::new( + EaseFunction::QuadraticInOut, + move_duration, + TransformPositionLens { + start: Vec3::new(-200., 100., 0.), + end: Vec3::new(200., 100., 0.), + }, + ); + + let tween_delay = Delay::new(move_duration); + + let tween_rotate = Tween::new( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), + TransformRotationLens { + start: Quat::IDENTITY, + end: Quat::from_rotation_z(180_f32.to_radians()), + }, + ); + + let tween_scale = Tween::new( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), + TransformScaleLens { + start: Vec3::ONE, + end: Vec3::splat(2.0), + }, + ); + + // Build a sequence from an heterogeneous list of tweenables by casting them + // manually to a BoxedTweenable. This is only to demonstrate how it's done; in + // general prefer using then() as below. + let seq1 = Sequence::new([Box::new(tween_move) as BoxedTweenable, tween_rotate.into()]); + let seq2 = tween_delay.then(tween_scale); + + // Because we want to monitor the progress of the animations, we need to fetch + // their Entity. This requires inserting them manually in the TweenAnimator + // resource, instead of using the extensions of EntityCommands. + commands.spawn(( + BlueAnimMarker, + TweenAnim::new(seq1), + AnimTarget::component::(entity), + )); + commands.spawn(( + TweenAnim::new(seq2), + AnimTarget::component::(entity), + )); + } + + Ok(()) } fn update_text( red_text_children: Single<&Children, With>, blue_text_children: Single<&Children, With>, - mut text_spans: Query<&mut TextSpan, With>, - anim_red: Single<&Animator, With>, - anim_blue: Single<&Animator, With>, - mut query_event: EventReader, + mut q_textspans: Query<&mut TextSpan, With>, + q_anim_red: Single, With>, + q_anim_blue: Single, With>, + mut q_event_completed: EventReader, ) { - let progress_red = anim_red.tweenable().progress(); + let anim_red = *q_anim_red; + let progress_red = if let Some(anim) = anim_red { + anim.tweenable().cycle_fraction() + } else { + 1. + }; - let progress_blue = anim_blue.tweenable().progress(); + let anim_blue = *q_anim_blue; + let progress_blue = if let Some(anim) = anim_blue { + anim.tweenable().cycle_fraction() + } else { + 1. + }; - let mut red_text = text_spans.get_mut(red_text_children[1]).unwrap(); + let mut red_text = q_textspans.get_mut(red_text_children[1]).unwrap(); red_text.0 = format!("{:5.1}%", progress_red * 100.); - let mut blue_text = text_spans.get_mut(blue_text_children[1]).unwrap(); + let mut blue_text = q_textspans.get_mut(blue_text_children[1]).unwrap(); blue_text.0 = format!("{:5.1}%", progress_blue * 100.); - for ev in query_event.read() { + for ev in q_event_completed.read() { println!( - "Event: TweenCompleted entity={:?} user_data={}", - ev.entity, ev.user_data + "Event: AnimCompletedEvent anim_entity={:?} target={:?}", + ev.anim_entity, ev.target ); } } diff --git a/examples/sprite_color.rs b/examples/sprite_color.rs index 14674a4..31b682f 100644 --- a/examples/sprite_color.rs +++ b/examples/sprite_color.rs @@ -1,93 +1,96 @@ -use bevy::{color::palettes::css::*, prelude::*}; -use bevy_tweening::{lens::*, *}; - -mod utils; - -fn main() { - App::default() - .add_plugins(DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - title: "SpriteColorLens".to_string(), - resolution: (1200., 600.).into(), - present_mode: bevy::window::PresentMode::Fifo, // vsync - ..default() - }), - ..default() - })) - .add_systems(Update, utils::close_on_esc) - .add_plugins(TweeningPlugin) - .add_systems(Startup, setup) - .run(); -} - -fn setup(mut commands: Commands) { - commands.spawn(Camera2d::default()); - - let size = 80.; - - let spacing = 1.25; - let screen_x = 450.; - let screen_y = 120.; - let mut x = -screen_x; - let mut y = screen_y; - - for ease_function in &[ - EaseFunction::QuadraticIn, - EaseFunction::QuadraticOut, - EaseFunction::QuadraticInOut, - EaseFunction::CubicIn, - EaseFunction::CubicOut, - EaseFunction::CubicInOut, - EaseFunction::QuarticIn, - EaseFunction::QuarticOut, - EaseFunction::QuarticInOut, - EaseFunction::QuinticIn, - EaseFunction::QuinticOut, - EaseFunction::QuinticInOut, - EaseFunction::SineIn, - EaseFunction::SineOut, - EaseFunction::SineInOut, - EaseFunction::CircularIn, - EaseFunction::CircularOut, - EaseFunction::CircularInOut, - EaseFunction::ExponentialIn, - EaseFunction::ExponentialOut, - EaseFunction::ExponentialInOut, - EaseFunction::ElasticIn, - EaseFunction::ElasticOut, - EaseFunction::ElasticInOut, - EaseFunction::BackIn, - EaseFunction::BackOut, - EaseFunction::BackInOut, - EaseFunction::BounceIn, - EaseFunction::BounceOut, - EaseFunction::BounceInOut, - ] { - let tween = Tween::new( - *ease_function, - std::time::Duration::from_secs(1), - SpriteColorLens { - start: RED.into(), - end: BLUE.into(), - }, - ) - .with_repeat_count(RepeatCount::Infinite) - .with_repeat_strategy(RepeatStrategy::MirroredRepeat); - - commands.spawn(( - Sprite { - color: Color::BLACK, - custom_size: Some(Vec2::new(size, size)), - ..default() - }, - Transform::from_translation(Vec3::new(x, y, 0.)), - Animator::new(tween), - )); - - y -= size * spacing; - if y < -screen_y { - x += size * spacing; - y = screen_y; - } - } -} +use bevy::{color::palettes::css::*, prelude::*}; +use bevy_tweening::{lens::*, *}; + +mod utils; + +fn main() { + App::default() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "SpriteColorLens".to_string(), + resolution: (1200., 600.).into(), + present_mode: bevy::window::PresentMode::Fifo, // vsync + ..default() + }), + ..default() + })) + .add_systems(Update, utils::close_on_esc) + .add_plugins(TweeningPlugin) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d::default()); + + let size = 80.; + + let spacing = 1.25; + let screen_x = 450.; + let screen_y = 120.; + let mut x = -screen_x; + let mut y = screen_y; + + for ease_function in &[ + EaseFunction::QuadraticIn, + EaseFunction::QuadraticOut, + EaseFunction::QuadraticInOut, + EaseFunction::CubicIn, + EaseFunction::CubicOut, + EaseFunction::CubicInOut, + EaseFunction::QuarticIn, + EaseFunction::QuarticOut, + EaseFunction::QuarticInOut, + EaseFunction::QuinticIn, + EaseFunction::QuinticOut, + EaseFunction::QuinticInOut, + EaseFunction::SineIn, + EaseFunction::SineOut, + EaseFunction::SineInOut, + EaseFunction::CircularIn, + EaseFunction::CircularOut, + EaseFunction::CircularInOut, + EaseFunction::ExponentialIn, + EaseFunction::ExponentialOut, + EaseFunction::ExponentialInOut, + EaseFunction::ElasticIn, + EaseFunction::ElasticOut, + EaseFunction::ElasticInOut, + EaseFunction::BackIn, + EaseFunction::BackOut, + EaseFunction::BackInOut, + EaseFunction::BounceIn, + EaseFunction::BounceOut, + EaseFunction::BounceInOut, + ] { + let tween = Tween::new( + *ease_function, + std::time::Duration::from_secs(1), + SpriteColorLens { + start: RED.into(), + end: BLUE.into(), + }, + ) + .with_repeat_count(RepeatCount::Infinite) + .with_repeat_strategy(RepeatStrategy::MirroredRepeat); + + commands.spawn(( + Transform::from_translation(Vec3::new(x, y, 0.)), + Sprite { + color: Color::BLACK, + custom_size: Some(Vec2::new(size, size)), + ..default() + }, + // In this example we add the TweenAnim on the same Entity as the component being + // animated (Sprite). Because of that, the target is implicitly a component on this + // Entity, and we don't need to add an AnimTarget component. + TweenAnim::new(tween), + )); + + y -= size * spacing; + if y < -screen_y { + x += size * spacing; + y = screen_y; + } + } +} diff --git a/examples/text_color.rs b/examples/text_color.rs index 3a84e7b..c99f61a 100644 --- a/examples/text_color.rs +++ b/examples/text_color.rs @@ -103,7 +103,10 @@ fn setup(mut commands: Commands, asset_server: Res) { justify_content: JustifyContent::Center, ..default() }, - Animator::new(tween), + // In this example we add the TweenAnim on the same Entity as the component being + // animated (TextColor). Because of that, the target is implicitly a component on this + // Entity, and we don't need to add an AnimTarget component. + TweenAnim::new(tween), )); y += delta_y; diff --git a/examples/transform_rotation.rs b/examples/transform_rotation.rs index 955be00..2937320 100644 --- a/examples/transform_rotation.rs +++ b/examples/transform_rotation.rs @@ -1,6 +1,5 @@ use bevy::{color::palettes::css::*, prelude::*}; use bevy_inspector_egui::{bevy_egui::EguiPlugin, prelude::*, quick::ResourceInspectorPlugin}; - use bevy_tweening::{lens::*, *}; mod utils; @@ -35,7 +34,7 @@ fn main() { #[reflect(InspectorOptions)] struct Options { #[inspector(min = 0.01, max = 100.)] - speed: f32, + speed: f64, } impl Default for Options { @@ -110,7 +109,11 @@ fn setup(mut commands: Commands) { custom_size: Some(Vec2::new(size, size * 0.5)), ..default() }, - Animator::new(tween), + // In this example we add the TweenAnim on the same Entity as the component + // being animated (Transform, automatically added because it's required by + // Sprite). Because of that, the target is implicitly a component on this + // Entity, and we don't need to add an AnimTarget component. + TweenAnim::new(tween), )); }); @@ -122,12 +125,12 @@ fn setup(mut commands: Commands) { } } -fn update_animation_speed(options: Res, mut animators: Query<&mut Animator>) { +fn update_animation_speed(options: Res, mut q_anims: Query<&mut TweenAnim>) { if !options.is_changed() { return; } - for mut animator in animators.iter_mut() { - animator.set_speed(options.speed); + for mut anim in &mut q_anims { + anim.speed = options.speed; } } diff --git a/examples/transform_translation.rs b/examples/transform_translation.rs index 9d429e6..f06245e 100644 --- a/examples/transform_translation.rs +++ b/examples/transform_translation.rs @@ -1,21 +1,21 @@ use bevy::{color::palettes::css::*, prelude::*}; use bevy_inspector_egui::{bevy_egui::EguiPlugin, prelude::*, quick::ResourceInspectorPlugin}; - use bevy_tweening::{lens::*, *}; mod utils; fn main() { App::default() - .add_plugins((DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - title: "TransformPositionLens".to_string(), - resolution: (1400., 600.).into(), - present_mode: bevy::window::PresentMode::Fifo, // vsync + .add_plugins(( + DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "TransformPositionLens".to_string(), + resolution: (1400., 600.).into(), + present_mode: bevy::window::PresentMode::Fifo, // vsync + ..default() + }), ..default() }), - ..default() - }), EguiPlugin { enable_multipass_for_primary_context: true, }, @@ -25,7 +25,7 @@ fn main() { )) .init_resource::() .register_type::() - .add_systems(Update, utils::close_on_esc) + .add_systems(Update, utils::close_on_esc) .add_systems(Startup, setup) .add_systems(Update, update_animation_speed) .run(); @@ -35,7 +35,7 @@ fn main() { #[reflect(InspectorOptions)] struct Options { #[inspector(min = 0.01, max = 100.)] - speed: f32, + speed: f64, } impl Default for Options { @@ -103,19 +103,23 @@ fn setup(mut commands: Commands) { custom_size: Some(Vec2::new(size, size)), ..default() }, - Animator::new(tween), + // In this example we add the TweenAnim on the same Entity as the component being + // animated (Transform, automatically added because it's required by Sprite). Because + // of that, the target is implicitly a component on this Entity, and we don't need to + // add an AnimTarget component. + TweenAnim::new(tween), )); x += size * spacing; } } -fn update_animation_speed(options: Res, mut animators: Query<&mut Animator>) { +fn update_animation_speed(options: Res, mut q_anims: Query<&mut TweenAnim>) { if !options.is_changed() { return; } - for mut animator in animators.iter_mut() { - animator.set_speed(options.speed); + for mut anim in &mut q_anims { + anim.speed = options.speed; } } diff --git a/examples/ui_position.rs b/examples/ui_position.rs index c8af219..3a844b2 100644 --- a/examples/ui_position.rs +++ b/examples/ui_position.rs @@ -1,6 +1,5 @@ use bevy::{color::palettes::css::*, prelude::*}; use bevy_inspector_egui::{bevy_egui::EguiPlugin, prelude::*, quick::ResourceInspectorPlugin}; - use bevy_tweening::{lens::*, *}; mod utils; @@ -9,14 +8,14 @@ fn main() { App::default() .add_plugins(( DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - title: "UiPositionLens".to_string(), - resolution: (1400., 600.).into(), - present_mode: bevy::window::PresentMode::Fifo, // vsync + primary_window: Some(Window { + title: "UiPositionLens".to_string(), + resolution: (1400., 600.).into(), + present_mode: bevy::window::PresentMode::Fifo, // vsync + ..default() + }), ..default() }), - ..default() - }), EguiPlugin { enable_multipass_for_primary_context: true, }, @@ -35,7 +34,7 @@ fn main() { #[reflect(InspectorOptions)] struct Options { #[inspector(min = 0.01, max = 100.)] - speed: f32, + speed: f64, } impl Default for Options { @@ -123,19 +122,22 @@ fn setup(mut commands: Commands) { ..default() }, BackgroundColor(RED.into()), - Animator::new(tween), + // In this example we add the TweenAnim on the same Entity as the component being + // animated (Node). Because of that, the target is implicitly a component on this + // Entity, and we don't need to add an AnimTarget component. + TweenAnim::new(tween), )); x += offset_x; } } -fn update_animation_speed(mut animators: Query<&mut Animator>, options: Res) { +fn update_animation_speed(options: Res, mut q_anims: Query<&mut TweenAnim>) { if !options.is_changed() { return; } - for mut animator in animators.iter_mut() { - animator.set_speed(options.speed); + for mut anim in &mut q_anims { + anim.speed = options.speed; } } diff --git a/images/tween_cycles.svg b/images/tween_cycles.svg new file mode 100644 index 0000000..a563416 --- /dev/null +++ b/images/tween_cycles.svg @@ -0,0 +1,400 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ratio + time + total duration + + + + + + 0 + 1 + cycle duration + + diff --git a/images/tween_mirrored.svg b/images/tween_mirrored.svg new file mode 100644 index 0000000..52fa7b7 --- /dev/null +++ b/images/tween_mirrored.svg @@ -0,0 +1,326 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ratio + time + total duration + + + + + + 0 + 1 + cycle duration + + diff --git a/release.md b/release.md index 96ace4c..8ef8fb2 100644 --- a/release.md +++ b/release.md @@ -10,7 +10,6 @@ - `cargo test --no-default-features --features="bevy_ui"` - `cargo test --no-default-features --features="bevy_sprite"` - `cargo test --no-default-features --features="bevy_text"` -- `cargo test --no-default-features --features="bevy_asset"` - `cargo test --all-features` - `cargo +nightly build --all-features` (for `docs.rs`) - `cargo +nightly doc --all-features --no-deps` diff --git a/run_examples.bat b/run_examples.bat index beef613..cefc6fa 100644 --- a/run_examples.bat +++ b/run_examples.bat @@ -1,15 +1,16 @@ @echo on echo Run all examples REM Default -cargo r --example menu --no-default-features --features="bevy_ui bevy_text bevy/bevy_winit" -cargo r --example transform_translation --no-default-features --features="bevy_sprite bevy/bevy_winit" -cargo r --example transform_rotation --no-default-features --features="bevy_sprite bevy/bevy_winit" -cargo r --example sequence --no-default-features --features="bevy_sprite bevy_text bevy/bevy_winit" +cargo r --example menu --no-default-features --features="bevy_ui bevy_text bevy/bevy_winit bevy/bevy_picking" +cargo r --example transform_translation --no-default-features --features="bevy_sprite bevy/bevy_winit bevy/bevy_picking" +cargo r --example transform_rotation --no-default-features --features="bevy_sprite bevy/bevy_winit bevy/bevy_picking" +cargo r --example sequence --no-default-features --features="bevy_sprite bevy_text bevy/bevy_winit bevy/bevy_picking" +cargo r --example ambient_light --no-default-features --features="bevy_ui bevy_text bevy/bevy_winit bevy/bevy_picking bevy/bevy_pbr bevy/hdr bevy/tonemapping_luts" +cargo r --example follow --no-default-features --features="bevy_sprite bevy_text bevy/bevy_winit bevy/bevy_picking" REM bevy_sprite -cargo r --example sprite_color --no-default-features --features="bevy_sprite bevy/bevy_winit" +cargo r --example sprite_color --no-default-features --features="bevy_sprite bevy/bevy_winit bevy/bevy_picking" +cargo r --example colormaterial_color --no-default-features --features="bevy_sprite bevy/bevy_winit bevy/bevy_picking" REM bevy_ui -cargo r --example ui_position --no-default-features --features="bevy_sprite bevy_ui bevy/bevy_winit" +cargo r --example ui_position --no-default-features --features="bevy_sprite bevy_ui bevy/bevy_winit bevy/bevy_picking" REM bevy_text -cargo r --example text_color --no-default-features --features="bevy_text bevy_ui bevy/bevy_winit" -REM bevy_sprite + bevy_asset -cargo r --example colormaterial_color --no-default-features --features="bevy_asset bevy_sprite bevy/bevy_winit" \ No newline at end of file +cargo r --example text_color --no-default-features --features="bevy_text bevy_ui bevy/bevy_winit bevy/bevy_picking" diff --git a/run_examples.sh b/run_examples.sh new file mode 100755 index 0000000..86d7dc4 --- /dev/null +++ b/run_examples.sh @@ -0,0 +1,15 @@ +echo Run all examples +# Default +cargo r --example menu --no-default-features --features="bevy_ui bevy_text bevy/bevy_winit bevy/bevy_picking" +cargo r --example transform_translation --no-default-features --features="bevy_sprite bevy/bevy_winit bevy/bevy_picking" +cargo r --example transform_rotation --no-default-features --features="bevy_sprite bevy/bevy_winit bevy/bevy_picking" +cargo r --example sequence --no-default-features --features="bevy_sprite bevy_text bevy/bevy_winit bevy/bevy_picking" +cargo r --example ambient_light --no-default-features --features="bevy_ui bevy_text bevy/bevy_winit bevy/bevy_picking bevy/bevy_pbr bevy/hdr bevy/tonemapping_luts" +cargo r --example follow --no-default-features --features="bevy_sprite bevy_text bevy/bevy_winit bevy/bevy_picking" +# bevy_sprite +cargo r --example sprite_color --no-default-features --features="bevy_sprite bevy/bevy_winit bevy/bevy_picking" +cargo r --example colormaterial_color --no-default-features --features="bevy_sprite bevy/bevy_winit bevy/bevy_picking" +# bevy_ui +cargo r --example ui_position --no-default-features --features="bevy_sprite bevy_ui bevy/bevy_winit bevy/bevy_picking" +# bevy_text +cargo r --example text_color --no-default-features --features="bevy_text bevy_ui bevy/bevy_winit bevy/bevy_picking" diff --git a/src/lens.rs b/src/lens.rs index 5204384..2a5bfa4 100644 --- a/src/lens.rs +++ b/src/lens.rs @@ -37,8 +37,6 @@ use bevy::prelude::*; -use crate::Targetable; - /// A lens over a subset of a component. /// /// The lens takes a `target` component or asset from a query, as a mutable @@ -54,17 +52,17 @@ use crate::Targetable; /// # use bevy::prelude::*; /// # use bevy_tweening::*; /// struct MyLens { -/// start: f32, -/// end: f32, +/// start: f32, +/// end: f32, /// } /// /// #[derive(Component)] /// struct MyStruct(f32); /// /// impl Lens for MyLens { -/// fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { -/// target.0 = self.start + (self.end - self.start) * ratio; -/// } +/// fn lerp(&mut self, mut target: Mut, ratio: f32) { +/// target.0 = self.start + (self.end - self.start) * ratio; +/// } /// } /// ``` pub trait Lens { @@ -73,7 +71,7 @@ pub trait Lens { /// `ratio`. The `target` component or asset is mutated in place. The /// implementation decides which fields are interpolated, and performs /// the animation in-place, overwriting the target. - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32); + fn lerp(&mut self, target: Mut<'_, T>, ratio: f32); } /// A lens to manipulate the [`color`] field of a section of a [`Text`] @@ -91,7 +89,7 @@ pub struct TextColorLens { #[cfg(feature = "bevy_text")] impl Lens for TextColorLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { + fn lerp(&mut self, mut target: Mut, ratio: f32) { target.0 = self.start.mix(&self.end, ratio); } } @@ -109,9 +107,8 @@ pub struct TransformPositionLens { } impl Lens for TransformPositionLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { - let value = self.start + (self.end - self.start) * ratio; - target.translation = value; + fn lerp(&mut self, mut target: Mut, ratio: f32) { + target.translation = self.start.lerp(self.end, ratio); } } @@ -141,7 +138,7 @@ pub struct TransformRotationLens { } impl Lens for TransformRotationLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { + fn lerp(&mut self, mut target: Mut, ratio: f32) { target.rotation = self.start.slerp(self.end, ratio); } } @@ -167,7 +164,7 @@ pub struct TransformRotateXLens { } impl Lens for TransformRotateXLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { + fn lerp(&mut self, mut target: Mut, ratio: f32) { let angle = (self.end - self.start).mul_add(ratio, self.start); target.rotation = Quat::from_rotation_x(angle); } @@ -194,7 +191,7 @@ pub struct TransformRotateYLens { } impl Lens for TransformRotateYLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { + fn lerp(&mut self, mut target: Mut, ratio: f32) { let angle = (self.end - self.start).mul_add(ratio, self.start); target.rotation = Quat::from_rotation_y(angle); } @@ -221,12 +218,111 @@ pub struct TransformRotateZLens { } impl Lens for TransformRotateZLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { + fn lerp(&mut self, mut target: Mut, ratio: f32) { let angle = (self.end - self.start).mul_add(ratio, self.start); target.rotation = Quat::from_rotation_z(angle); } } +/// A lens to rotate a [`Transform`] component around its local X axis +/// additively. +/// +/// This lens interpolates the rotation angle of a local rotation from +/// a `start` value to an `end` value, for a rotation around the local X axis, +/// and compose this with the `base_rotation`, applying the result to a +/// [`Transform`] component. Unlike [`TransformRotationLens`], it can produce an +/// animation that rotates the entity any number of turns around its local X +/// axis. +/// +/// See the [top-level `lens` module documentation] for a comparison of rotation +/// lenses. +/// +/// [`Transform`]: https://docs.rs/bevy/0.16.0/bevy/transform/components/struct.Transform.html +/// [top-level `lens` module documentation]: crate::lens +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct TransformRotateAdditiveXLens { + /// The base rotation of the object, which is composed with the animated + /// rotation. + pub base_rotation: Quat, + /// Start value of the rotation angle, in radians. + pub start: f32, + /// End value of the rotation angle, in radians. + pub end: f32, +} + +impl Lens for TransformRotateAdditiveXLens { + fn lerp(&mut self, mut target: Mut, ratio: f32) { + let angle = (self.end - self.start).mul_add(ratio, self.start); + target.rotation = self.base_rotation * Quat::from_rotation_x(angle); + } +} + +/// A lens to rotate a [`Transform`] component around its local Y axis +/// additively. +/// +/// This lens interpolates the rotation angle of a local rotation from +/// a `start` value to an `end` value, for a rotation around the local Y axis, +/// and compose this with the `base_rotation`, applying the result to a +/// [`Transform`] component. Unlike [`TransformRotationLens`], it can produce an +/// animation that rotates the entity any number of turns around its local Y +/// axis. +/// +/// See the [top-level `lens` module documentation] for a comparison of rotation +/// lenses. +/// +/// [`Transform`]: https://docs.rs/bevy/0.16.0/bevy/transform/components/struct.Transform.html +/// [top-level `lens` module documentation]: crate::lens +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct TransformRotateAdditiveYLens { + /// The base rotation of the object, which is composed with the animated + /// rotation. + pub base_rotation: Quat, + /// Start value of the rotation angle, in radians. + pub start: f32, + /// End value of the rotation angle, in radians. + pub end: f32, +} + +impl Lens for TransformRotateAdditiveYLens { + fn lerp(&mut self, mut target: Mut, ratio: f32) { + let angle = (self.end - self.start).mul_add(ratio, self.start); + target.rotation = self.base_rotation * Quat::from_rotation_y(angle); + } +} + +/// A lens to rotate a [`Transform`] component around its local Z axis +/// additively. +/// +/// This lens interpolates the rotation angle of a local rotation from +/// a `start` value to an `end` value, for a rotation around the local Z axis, +/// and compose this with the `base_rotation`, applying the result to a +/// [`Transform`] component. Unlike [`TransformRotationLens`], it can produce an +/// animation that rotates the entity any number of turns around its local Z +/// axis. +/// +/// See the [top-level `lens` module documentation] for a comparison of rotation +/// lenses. +/// +/// [`Transform`]: https://docs.rs/bevy/0.16.0/bevy/transform/components/struct.Transform.html +/// [top-level `lens` module documentation]: crate::lens +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct TransformRotateAdditiveZLens { + /// The base rotation of the object, which is composed with the animated + /// rotation. + pub base_rotation: Quat, + /// Start value of the rotation angle, in radians. + pub start: f32, + /// End value of the rotation angle, in radians. + pub end: f32, +} + +impl Lens for TransformRotateAdditiveZLens { + fn lerp(&mut self, mut target: Mut, ratio: f32) { + let angle = (self.end - self.start).mul_add(ratio, self.start); + target.rotation = self.base_rotation * Quat::from_rotation_z(angle); + } +} + /// A lens to rotate a [`Transform`] component around a given fixed axis. /// /// This lens interpolates the rotation angle of a [`Transform`] component from @@ -254,7 +350,7 @@ pub struct TransformRotateAxisLens { } impl Lens for TransformRotateAxisLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { + fn lerp(&mut self, mut target: Mut, ratio: f32) { let angle = (self.end - self.start).mul_add(ratio, self.start); target.rotation = Quat::from_axis_angle(self.axis, angle); } @@ -273,7 +369,7 @@ pub struct TransformScaleLens { } impl Lens for TransformScaleLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { + fn lerp(&mut self, mut target: Mut, ratio: f32) { target.scale = self.start + (self.end - self.start) * ratio; } } @@ -308,7 +404,7 @@ fn lerp_val(start: &Val, end: &Val, ratio: f32) -> Val { #[cfg(feature = "bevy_ui")] impl Lens for UiPositionLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { + fn lerp(&mut self, mut target: Mut, ratio: f32) { target.left = lerp_val(&self.start.left, &self.end.left, ratio); target.right = lerp_val(&self.start.right, &self.end.right, ratio); target.top = lerp_val(&self.start.top, &self.end.top, ratio); @@ -328,7 +424,7 @@ pub struct UiBackgroundColorLens { #[cfg(feature = "bevy_ui")] impl Lens for UiBackgroundColorLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { + fn lerp(&mut self, mut target: Mut, ratio: f32) { target.0 = self.start.mix(&self.end, ratio); } } @@ -337,7 +433,7 @@ impl Lens for UiBackgroundColorLens { /// /// [`color`]: https://docs.rs/bevy/0.16.0/bevy/sprite/struct.ColorMaterial.html#structfield.color /// [`ColorMaterial`]: https://docs.rs/bevy/0.16.0/bevy/sprite/struct.ColorMaterial.html -#[cfg(all(feature = "bevy_sprite", feature = "bevy_asset"))] +#[cfg(feature = "bevy_sprite")] #[derive(Debug, Copy, Clone, PartialEq)] pub struct ColorMaterialColorLens { /// Start color. @@ -346,9 +442,9 @@ pub struct ColorMaterialColorLens { pub end: Color, } -#[cfg(all(feature = "bevy_sprite", feature = "bevy_asset"))] +#[cfg(feature = "bevy_sprite")] impl Lens for ColorMaterialColorLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { + fn lerp(&mut self, mut target: Mut, ratio: f32) { target.color = self.start.mix(&self.end, ratio); } } @@ -368,7 +464,7 @@ pub struct SpriteColorLens { #[cfg(feature = "bevy_sprite")] impl Lens for SpriteColorLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { + fn lerp(&mut self, mut target: Mut, ratio: f32) { let value = self.start.mix(&self.end, ratio); target.color = value; } @@ -376,19 +472,14 @@ impl Lens for SpriteColorLens { #[cfg(test)] mod tests { - use bevy::ecs::{change_detection::MaybeLocation, component::Tick}; use std::f32::consts::TAU; #[cfg(any(feature = "bevy_sprite", feature = "bevy_text"))] use bevy::color::palettes::css::{BLUE, RED}; + use bevy::ecs::{change_detection::MaybeLocation, component::Tick}; use super::*; - use crate::tweenable::ComponentTarget; - - #[cfg(all(feature = "bevy_sprite", feature = "bevy_asset"))] - use crate::tweenable::AssetTarget; - #[cfg(feature = "bevy_text")] #[test] fn text_color() { @@ -403,16 +494,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut text_color, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.); + lens.lerp(target, 0.); } assert_eq!(text_color.0, RED.into()); @@ -420,16 +511,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut text_color, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 1.); + lens.lerp(target, 1.); } assert_eq!(text_color.0, BLUE.into()); @@ -437,16 +528,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut text_color, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.3); + lens.lerp(target, 0.3); } assert_eq!(text_color.0, Color::srgba(0.7, 0., 0.3, 1.0)); } @@ -463,16 +554,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.); + lens.lerp(target, 0.); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); @@ -482,16 +573,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 1.); + lens.lerp(target, 1.); } assert!(transform .translation @@ -503,16 +594,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.3); + lens.lerp(target, 0.3); } assert!(transform .translation @@ -533,16 +624,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.); + lens.lerp(target, 0.); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); @@ -552,16 +643,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 1.); + lens.lerp(target, 1.); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); assert!(transform @@ -573,16 +664,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.3); + lens.lerp(target, 0.3); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); assert!(transform @@ -604,16 +695,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, *ratio); + lens.lerp(target, *ratio); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); if index == 1 || index == 3 { @@ -633,16 +724,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.1); + lens.lerp(target, 0.1); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); assert!(transform @@ -664,16 +755,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, *ratio); + lens.lerp(target, *ratio); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); if index == 1 || index == 3 { @@ -693,16 +784,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.1); + lens.lerp(target, 0.1); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); assert!(transform @@ -724,16 +815,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, *ratio); + lens.lerp(target, *ratio); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); if index == 1 || index == 3 { @@ -753,16 +844,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.1); + lens.lerp(target, 0.1); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); assert!(transform @@ -786,16 +877,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, *ratio); + lens.lerp(target, *ratio); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); if index == 1 || index == 3 { @@ -815,16 +906,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.1); + lens.lerp(target, 0.1); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); assert!(transform @@ -845,16 +936,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.); + lens.lerp(target, 0.); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); @@ -864,16 +955,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 1.); + lens.lerp(target, 1.); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); @@ -883,16 +974,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut transform, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.3); + lens.lerp(target, 0.3); } assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); @@ -922,16 +1013,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut node, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.); + lens.lerp(target, 0.); } assert_eq!(node.left, Val::Px(0.)); assert_eq!(node.top, Val::Px(0.)); @@ -942,16 +1033,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut node, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 1.); + lens.lerp(target, 1.); } assert_eq!(node.left, Val::Px(1.)); assert_eq!(node.top, Val::Px(5.)); @@ -962,16 +1053,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut node, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.3); + lens.lerp(target, 0.3); } assert_eq!(node.left, Val::Px(0.3)); assert_eq!(node.top, Val::Px(1.5)); @@ -979,7 +1070,7 @@ mod tests { assert_eq!(node.bottom, Val::Percent(31.)); } - #[cfg(all(feature = "bevy_sprite", feature = "bevy_asset"))] + #[cfg(feature = "bevy_sprite")] #[test] fn colormaterial_color() { let mut lens = ColorMaterialColorLens { @@ -997,17 +1088,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = AssetTarget::new(Mut::new( - &mut assets, + let asset = assets.get_mut(handle.id()).unwrap(); + let target = Mut::new( + asset, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); - target.handle = handle.clone(); - - lens.lerp(&mut target, 0.); + ); + lens.lerp(target, 0.); } assert_eq!(assets.get(handle.id()).unwrap().color, RED.into()); @@ -1015,17 +1105,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = AssetTarget::new(Mut::new( - &mut assets, + let asset = assets.get_mut(handle.id()).unwrap(); + let target = Mut::new( + asset, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); - target.handle = handle.clone(); - - lens.lerp(&mut target, 1.); + ); + lens.lerp(target, 1.); } assert_eq!(assets.get(handle.id()).unwrap().color, BLUE.into()); @@ -1033,17 +1122,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = AssetTarget::new(Mut::new( - &mut assets, + let asset = assets.get_mut(handle.id()).unwrap(); + let target = Mut::new( + asset, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); - target.handle = handle.clone(); - - lens.lerp(&mut target, 0.3); + ); + lens.lerp(target, 0.3); } assert_eq!( assets.get(handle.id()).unwrap().color, @@ -1067,16 +1155,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut sprite, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.); + lens.lerp(target, 0.); } assert_eq!(sprite.color, RED.into()); @@ -1084,16 +1172,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut sprite, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 1.); + lens.lerp(target, 1.); } assert_eq!(sprite.color, BLUE.into()); @@ -1101,16 +1189,16 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( + let target = Mut::new( &mut sprite, &mut added, &mut last_changed, Tick::new(0), Tick::new(0), caller.as_mut(), - )); + ); - lens.lerp(&mut target, 0.3); + lens.lerp(target, 0.3); } assert_eq!(sprite.color, Color::srgba(0.7, 0., 0.3, 1.0)); } diff --git a/src/lib.rs b/src/lib.rs index af9d043..4683e12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,17 +10,32 @@ missing_docs )] -//! Tweening animation plugin for the Bevy game engine +//! Tweening animation plugin for the Bevy game engine. //! //! 🍃 Bevy Tweening provides interpolation-based animation between ("tweening") -//! two values, for Bevy components and assets. Each field of a component or -//! asset can be animated via a collection or predefined easing functions, -//! or providing a custom animation curve. Custom components and assets are also -//! supported. +//! two values, to animate any field of any component, resource, or asset, +//! including both built-in Bevy ones and custom user-defined ones. Each field +//! of a component, resource, or asset, can be animated via a collection of +//! predefined easing functions, or providing a custom animation curve. The +//! library supports any number of animations queued in parallel, even on the +//! same component, resource, or asset type, and allows runtime control over +//! playback and animation speed. +//! +//! # Quick start +//! +//! Look at the documentation for: +//! - [`Tween`] -- the description of a tweening animation and the explanation +//! of some core concepts +//! - [`TweenAnim`] -- the component representing the runtime animation +//! - [`AnimTarget`] -- the component defining the target that the animation +//! mutates +//! - [`TweeningPlugin`] -- the plugin to add to your app +//! - [`EntityCommandsTweeningExtensions`] -- the simplest way to spawn +//! animations //! //! # Example //! -//! Add the tweening plugin to your app: +//! Add the [`TweeningPlugin`] to your app: //! //! ```no_run //! use bevy::prelude::*; @@ -39,15 +54,14 @@ //! # use bevy_tweening::{lens::*, *}; //! # use std::time::Duration; //! # fn system(mut commands: Commands) { -//! # let size = 16.; //! // Create a single animation (tween) to move an entity. //! let tween = Tween::new( //! // Use a quadratic easing on both endpoints. //! EaseFunction::QuadraticInOut, -//! // Animation time. +//! // It takes 1 second to go from start to end points. //! 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 +//! // for the TweenAnimator to animate it. It also contains the start and //! // end values respectively associated with the progress ratios 0. and 1. //! TransformPositionLens { //! start: Vec3::ZERO, @@ -55,64 +69,81 @@ //! }, //! ); //! +//! // Spawn an entity to animate the position of. //! commands.spawn(( -//! // Spawn an entity to animate the position of. //! Transform::default(), -//! // Add an Animator component to control and execute the animation. -//! Animator::new(tween), +//! // Create a tweenable animation targetting the current entity. Without AnimTarget, +//! // the target is implicitly a component on this same entity. The exact component +//! // type is derived from the type of the Lens used by the Tweenable itself. +//! TweenAnim::new(tween), //! )); //! # } //! ``` //! -//! Note that this example leverages the fact [`TweeningPlugin`] automatically -//! adds the necessary system to animate [`Transform`] components. However, for -//! most other components and assets, you need to manually add those systems to -//! your `App`. -//! -//! # System setup +//! If the target of the animation is not a component on the current entity, +//! then an [`AnimTarget`] component is necessary to specify that target. Note +//! that **[`AnimTarget`] is always mandatory for resource and asset +//! animations**. //! -//! Adding the [`TweeningPlugin`] to your app provides the basic setup for using -//! 🍃 Bevy Tweening. However, additional setup is required depending on the -//! components and assets you want to animate: -//! -//! - To ensure a component `C` is animated, the -//! [`component_animator_system::`] system must run each frame, in addition -//! of adding an [`Animator::`] component to the same Entity as `C`. -//! -//! - To ensure an asset `A` is animated, the [`asset_animator_system::`] -//! system must run each frame, in addition of adding an [`AssetAnimator`] -//! component to any Entity. Animating assets also requires the `bevy_asset` -//! feature (enabled by default). +//! ``` +//! # use bevy::prelude::*; +//! # use bevy_tweening::{lens::*, *}; +//! # use std::time::Duration; +//! # fn make_tween() -> Tween { unimplemented!() } +//! #[derive(Resource)] +//! struct MyResource; //! -//! By default, 🍃 Bevy Tweening adopts a minimalist approach, and the -//! [`TweeningPlugin`] will only add systems to animate components and assets -//! for which a [`Lens`] is provided by 🍃 Bevy Tweening itself. This means that -//! any other Bevy component or asset (either built-in from Bevy itself, or -//! custom) requires manually scheduling the appropriate system. +//! # fn system(mut commands: Commands) { +//! // Create a single animation (tween) to animate a resource. +//! let tween = make_tween::(); //! -//! | Component or Asset | Animation system added by `TweeningPlugin`? | -//! |---|---| -//! | [`Transform`] | Yes | -//! | [`Sprite`] | Only if `bevy_sprite` feature | -//! | [`ColorMaterial`] | Only if `bevy_sprite` feature | -//! | [`Node`] | Only if `bevy_ui` feature | -//! | [`Text`] | Only if `bevy_text` feature | -//! | All other components | No | +//! // Spawn an entity to own the resource animation. +//! commands.spawn(( +//! TweenAnim::new(tween), +//! // The AnimTarget is necessary here: +//! AnimTarget::resource::(), +//! )); +//! # } +//! ``` //! -//! To add a system for a component `C`, use: +//! This example shows the general pattern to add animations for any component, +//! resource, or asset. Since moving the position of an object is a very common +//! task, 🍃 Bevy Tweening provides a shortcut for it. The above example can be +//! rewritten more concicely as: //! //! ``` //! # use bevy::prelude::*; -//! # use bevy_tweening::*; -//! # let mut app = App::default(); -//! # #[derive(Component)] struct C; -//! app.add_systems(Update, -//! component_animator_system:: -//! .in_set(AnimationSystem::AnimationUpdate)); +//! # use bevy_tweening::{lens::*, *}; +//! # use std::time::Duration; +//! # fn system(mut commands: Commands) { +//! commands +//! // Spawn an entity to animate the position of. +//! .spawn((Transform::default(),)) +//! // Create a new Transform::translation animation +//! .move_to( +//! Vec3::new(1., 2., -4.), +//! Duration::from_secs(1), +//! EaseFunction::QuadraticInOut, +//! ); +//! # } //! ``` //! -//! Similarly for an asset `A`, use the `asset_animator_system`. This is only -//! available with the `bevy_asset` feature. +//! The [`move_to()`] extension is convenient helper for animations, which +//! creates a [`Tween`] that animates the [`Transform::translation`]. It has the +//! added benefit that the starting point is automatically read from the +//! component itself; you only need to specify the end position. See the +//! [`EntityCommandsTweeningExtensions`] extension trait defining helpers for +//! other common animations. +//! +//! # Ready to animate +//! +//! Unlike previous versions of 🍃 Bevy Tweening, **you don't need any +//! particular system setup** aside from adding the [`TweeningPlugin`] to your +//! [`App`]. In particular, per-component-type and per-asset-type systems are +//! gone. Instead, the plugin adds a _single_ system executing during the +//! [`Update`] schedule, which calls [`TweenAnim::step_all()`]. Each +//! [`TweenAnim`] acts as a controller for one animation, and mutates its +//! target. //! //! # Tweenables //! @@ -123,9 +154,13 @@ //! - [`Tween`] - A simple tween (easing) animation between two values. //! - [`Sequence`] - A series of tweenables executing in series, one after the //! other. -//! - [`Tracks`] - A collection of tweenables executing in parallel. //! - [`Delay`] - A time delay. This doesn't animate anything. //! +//! To execute multiple animations in parallel (like the `Tracks` tweenable used +//! to do in older versions of 🍃 Bevy Tweening; it's now removed), simply +//! enqueue each animation independently. This require careful selection of +//! individual timings though if you want to synchronize those animations. +//! //! ## Chaining animations //! //! Most tweenables can be chained with the `then()` operator to produce a @@ -157,66 +192,132 @@ //! let seq = tween1.then(tween2); //! ``` //! -//! # Animators and lenses +//! Note that some tweenable animations can be of infinite duration; this is the +//! case for example when using [`RepeatCount::Infinite`]. If you add such an +//! infinite animation in a sequence, and append more tweenables after it, +//! **those tweenables will never play** because playback will be stuck forever +//! repeating the first animation. You're responsible for creating sequences +//! that make sense. In general, only use infinite tweenable animations alone or +//! as the last element of a sequence (for example, move to position and then +//! rotate forever on self). //! -//! Bevy components and assets are animated with tweening _animator_ components, -//! which take a tweenable and apply it to another component on the same -//! [`Entity`]. Those animators determine that other component and its fields to -//! animate using a _lens_. +//! # `TweenAnim` //! -//! ## Components animation +//! Bevy components, resources, and assets, are animated with the [`TweenAnim`] +//! component. This component acts as a controller for a single animation. It +//! determines the target component, resource, or asset, to animate, via an +//! [`AnimTarget`], and accesses the field(s) of that target using a [`Lens`]. //! -//! Components are animated with the [`Animator`] component, which is generic -//! over the type of component it animates. This is a restriction imposed by -//! Bevy, to access the animated component as a mutable reference via a -//! [`Query`] and comply with the ECS rules. -//! -//! The [`Animator`] itself is not generic over the subset of fields of the -//! components it animates. This limits the proliferation of generic types when -//! animating e.g. both the position and rotation of an entity. -//! -//! ## Assets animation -//! -//! Assets are animated in a similar way to component, via the [`AssetAnimator`] -//! component. This requires the `bevy_asset` feature (enabled by default). +//! - Components are animated via the [`AnimTargetKind::Component`], which +//! identifies a component instance on an entity via the [`Entity`] itself. If +//! that target entity is the same as the one owning the [`TweenAnim`], then +//! the [`AnimTarget`] can be omitted, for convenience. +//! - Resources are animated via the [`AnimTargetKind::Resource`]. +//! - Assets are animated via the [`AnimTargetKind::Asset`] which identifies an +//! asset via the type of its [`Assets`] collection (and so indirectly the +//! type of asset itself) and the [`AssetId`] referencing that asset inside +//! that collection. //! //! Because assets are typically shared, and the animation applies to the asset //! itself, all users of the asset see the animation. For example, animating the -//! color of a [`ColorMaterial`] will change the color of all the -//! 2D meshes using that material. +//! color of a [`ColorMaterial`] will change the color of all the 2D meshes +//! using that material. If you want to animate the color of a single mesh, you +//! need to duplicate the asset and assign a unique copy to that mesh, +//! then animate that copy alone. +//! +//! After that, you can use the [`TweenAnim`] component to control the animation +//! playback: +//! +//! ```no_run +//! # use bevy::prelude::Single; +//! # use bevy_tweening::*; +//! fn my_system(mut anim: Single<&mut TweenAnim>) { +//! anim.speed = 0.8; // 80% playback speed +//! } +//! ``` //! //! ## Lenses //! -//! Both [`Animator`] and [`AssetAnimator`] access the field(s) to animate via a -//! lens, a type that implements the [`Lens`] trait. +//! The [`AnimTarget`] references the target (component, resource, or asset) +//! being animated. However, only a part of that target is generally animated. +//! To that end, the [`TweenAnim`] (or, more exactly, the [`Tweenable`] it uses) +//! accesses the field(s) to animate via a _lens_, a type that implements the +//! [`Lens`] trait and allows mapping a target to the actual value(s) animated. //! -//! Several predefined lenses are provided in the [`lens`] module for the most -//! commonly animated fields, like the components of a [`Transform`]. A custom -//! lens can also be created by implementing the trait, allowing to animate -//! virtually any field of any Bevy component or asset. +//! For example, the [`TransformPositionLens`] uses a [`Transform`] component as +//! input, and animates its [`Transform::translation`] field only, leaving the +//! rotation and scale unchanged. +//! +//! ```no_run +//! # use bevy::{prelude::{Transform, Vec3}, ecs::change_detection::Mut}; +//! # use bevy_tweening::Lens; +//! # struct TransformPositionLens { start: Vec3, end: Vec3 }; +//! impl Lens for TransformPositionLens { +//! fn lerp(&mut self, mut target: Mut, ratio: f32) { +//! target.translation = self.start.lerp(self.end, ratio); +//! } +//! } +//! ``` +//! +//! Several built-in lenses are provided in the [`lens`] module for the most +//! commonly animated fields, like the components of a [`Transform`]. Those are +//! provided for convenience and mainly as examples. 🍃 Bevy Tweening expects +//! you to write your own lenses by implementing the [`Lens`] trait, which as +//! you can see above is very simple. This allows animating virtually any field +//! of any component, resource, or asset, whether shipped with Bevy or defined +//! by the user. +//! +//! # Tweening vs. keyframed animation +//! +//! 🍃 Bevy Tweening is a "tweening" animation library. It focuses on simple +//! animations often used in applications and games to breathe life into a user +//! interface or the objects of a game world. The API design favors simplicity, +//! often for quick one-shot animations created from code. This type of +//! animation is inherently simpler than a full-blown animation solution, like +//! `bevy_animation`, which typically works with complex keyframe-based +//! animation curves authored via Digital Content Creation (DCC) tools like 3D +//! modellers and exported as assets, and whose most common usage is skeletal +//! animation of characters. There's a grey area between those two approaches, +//! and you can use both to achieve most animations, but 🍃 Bevy Tweening will +//! shine for simpler animations while `bevy_animation` while offer a more +//! extensive support for larger, more complex ones. //! //! [`Transform::translation`]: https://docs.rs/bevy/0.16.0/bevy/transform/components/struct.Transform.html#structfield.translation //! [`Entity`]: https://docs.rs/bevy/0.16.0/bevy/ecs/entity/struct.Entity.html -//! [`Query`]: https://docs.rs/bevy/0.16.0/bevy/ecs/system/struct.Query.html //! [`ColorMaterial`]: https://docs.rs/bevy/0.16.0/bevy/sprite/struct.ColorMaterial.html -//! [`Sprite`]: https://docs.rs/bevy/0.16.0/bevy/sprite/struct.Sprite.html -//! [`Node`]: https://docs.rs/bevy/0.16.0/bevy/ui/struct.Node.html#structfield.position -//! [`TextColor`]: https://docs.rs/bevy/0.16.0/bevy/text/struct.TextColor.html //! [`Transform`]: https://docs.rs/bevy/0.16.0/bevy/transform/components/struct.Transform.html +//! [`TransformPositionLens`]: crate::lens::TransformPositionLens +//! [`move_to()`]: crate::EntityCommandsTweeningExtensions::move_to -use std::time::Duration; - -use bevy::prelude::*; +use std::{ + any::TypeId, + ops::{Deref, DerefMut}, + time::Duration, +}; +use bevy::{ + asset::UntypedAssetId, + ecs::{ + change_detection::MutUntyped, + component::{ComponentId, Components, Mutable}, + }, + platform::collections::HashMap, + prelude::*, +}; pub use lens::Lens; -#[cfg(feature = "bevy_asset")] -pub use plugin::asset_animator_system; -pub use plugin::{component_animator_system, AnimationSystem, TweeningPlugin}; -#[cfg(feature = "bevy_asset")] -pub use tweenable::AssetTarget; +use lens::{ + TransformRotateAdditiveXLens, TransformRotateAdditiveYLens, TransformRotateAdditiveZLens, +}; +pub use plugin::{AnimationSystem, TweeningPlugin}; +use thiserror::Error; pub use tweenable::{ - BoxedTweenable, ComponentTarget, Delay, Sequence, Targetable, TotalDuration, Tracks, Tween, - TweenCompleted, TweenState, Tweenable, + BoxedTweenable, CycleCompletedEvent, Delay, IntoBoxedTweenable, Sequence, TotalDuration, Tween, + TweenState, Tweenable, +}; + +use crate::{ + lens::{TransformPositionLens, TransformScaleLens}, + tweenable::TweenConfig, }; pub mod lens; @@ -266,56 +367,59 @@ impl From for RepeatCount { } } -/// What to do when a tween animation needs to be repeated. +impl RepeatCount { + /// Calculate the total duration for this repeat count. + pub fn total_duration(&self, cycle_duration: Duration) -> TotalDuration { + match self { + RepeatCount::Finite(count) => TotalDuration::Finite(cycle_duration * *count), + RepeatCount::For(duration) => TotalDuration::Finite(*duration), + RepeatCount::Infinite => TotalDuration::Infinite, + } + } +} + +/// Repeat strategy for animation cycles. /// -/// Only applicable when [`RepeatCount`] is greater than the animation duration. +/// Only applicable when [`RepeatCount`] is greater than the total duration of +/// the tweenable animation. /// /// Default: `Repeat`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum RepeatStrategy { - /// Reset the animation back to its starting position. + /// Reset the cycle back to its starting position. /// - /// When playback reaches the end of the animation, it jumps directly back - /// to the animation start. This can create discontinuities if the animation - /// is not authored to be looping. + /// When playback reaches the end of the animation cycle, it jumps directly + /// back to the cycle start. This can create discontinuities if the + /// animation is not authored to be looping. + #[default] Repeat, - /// Follow a ping-pong pattern, changing the direction each time an endpoint - /// is reached. + + /// Follow a ping-pong pattern, changing the cycle 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. + /// A complete loop start -> end -> start always counts as 2 cycles for the + /// various operations where cycle count matters. That is, an animation with + /// a 1-second cycle and a mirrored repeat strategy will take 2 seconds + /// to end up back in the state where it started. /// /// This strategy ensures that there's no discontinuity in the animation, /// since there's no jump. MirroredRepeat, } -impl Default for RepeatStrategy { - fn default() -> Self { - Self::Repeat - } -} - -/// Playback state of an animator. +/// Playback state of a [`TweenAnim`]. /// /// Default: `Playing`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AnimatorState { +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum PlaybackState { /// The animation is playing. This is the default state. + #[default] Playing, /// The animation is paused in its current state. Paused, } -impl Default for AnimatorState { - fn default() -> Self { - Self::Playing - } -} - -impl std::ops::Not for AnimatorState { +impl std::ops::Not for PlaybackState { type Output = Self; fn not(self) -> Self::Output { @@ -328,24 +432,28 @@ impl std::ops::Not for AnimatorState { /// Describe how eased value should be computed. /// -/// This function is applied to the animation fraction `t` representing the -/// playback position over the animation duration. The result is used to -/// interpolate the animator target. +/// This function is applied to the cycle fraction `t` representing the playback +/// position over the cycle duration. The result is used to interpolate the +/// animation target. /// /// In general a [`Lens`] should perform a linear interpolation over its target, /// and the non-linear behavior (for example, bounciness, etc.) comes from this /// function. This ensures the same [`Lens`] can be reused in multiple contexts, /// while the "shape" of the animation is controlled independently. /// -/// Default: `Linear`. -#[derive(Clone, Copy)] +/// Default: `EaseFunction::Linear`. +#[derive(Debug, Clone, Copy)] pub enum EaseMethod { /// Follow [`EaseFunction`]. EaseFunction(EaseFunction), /// Discrete interpolation. The eased value will jump from start to end when /// stepping over the discrete limit, which must be value between 0 and 1. Discrete(f32), - /// Use a custom function to interpolate the value. + /// Use a custom function to interpolate the value. The function is called + /// with the cycle ratio, in `[0:1]`, as parameter, and must return the + /// easing factor, typically also in `[0:1]`. Note that values outside this + /// unit range may not work well with some animations; for example if + /// animating a color, a negative red values have no meaning. CustomFunction(fn(f32) -> f32), } @@ -380,51 +488,43 @@ impl From for EaseMethod { /// Direction a tweening animation is playing. /// -/// When playing a tweenable forward, the progress values `0` and `1` are -/// respectively mapped to the start and end bounds of the lens(es) being used. -/// Conversely, when playing backward, this mapping is reversed, such that a -/// progress value of `0` corresponds to the state of the target at the end -/// bound of the lens, while a progress value of `1` corresponds to the state of -/// that target at the start bound of the lens, effectively making the animation -/// play backward. -/// -/// 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. 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. +/// The playback direction determines if the delta animation time passed to +/// [`Tweenable::step()`] is added or subtracted to the current time position on +/// the animation's timeline. +/// - In `Forward` direction, time passes forward from `t=0` to the total +/// duration of the animation. +/// - Conversely, in `Backward` direction, time passes backward from the total +/// duration back to `t=0`. +/// +/// Note that backward playback is supported for infinite animations (when the +/// repeat count is [`RepeatCount::Infinite`]), but [`Tweenable::rewind()`] is +/// not supported and will panic. /// /// Default: `Forward`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TweeningDirection { +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum PlaybackDirection { /// Animation playing from start to end. + #[default] Forward, /// Animation playing from end to start, in reverse. Backward, } -impl TweeningDirection { - /// Is the direction equal to [`TweeningDirection::Forward`]? +impl PlaybackDirection { + /// Is the direction equal to [`PlaybackDirection::Forward`]? #[must_use] pub fn is_forward(&self) -> bool { *self == Self::Forward } - /// Is the direction equal to [`TweeningDirection::Backward`]? + /// Is the direction equal to [`PlaybackDirection::Backward`]? #[must_use] pub fn is_backward(&self) -> bool { *self == Self::Backward } } -impl Default for TweeningDirection { - fn default() -> Self { - Self::Forward - } -} - -impl std::ops::Not for TweeningDirection { +impl std::ops::Not for PlaybackDirection { type Output = Self; fn not(self) -> Self::Output { @@ -435,276 +535,2453 @@ impl std::ops::Not for TweeningDirection { } } -macro_rules! animator_impl { - () => { - /// Set the initial playback state of the animator. - #[must_use] - pub fn with_state(mut self, state: AnimatorState) -> Self { - self.state = state; - self - } +/// Extension trait for [`EntityCommands`], adding animation functionalities for +/// commonly used tweening animations. +/// +/// This trait extends [`EntityCommands`] to provide convenience helpers to +/// common tweening animations like moving the position of an entity by +/// animating its [`Transform::translation`]. +/// +/// One of the major source of convenience provided by these helpers is the fact +/// that some of the data necessary to create the tween animation is +/// automatically derived from the current value of the component at the time +/// when the command is processed. For example, the [`move_to()`] helper only +/// requires specifying the end position, and will automatically read the start +/// position from the current [`Transform::translation`] value. This avoids +/// having to explicitly access that component to read that value and manually +/// store it into a [`Lens`]. +/// +/// [`move_to()`]: Self::move_to +pub trait EntityCommandsTweeningExtensions<'a> { + /// Queue a new tween animation to move the current entity. + /// + /// The entity must have a [`Transform`] component. The tween animation will + /// be initialized with the current [`Transform::translation`] as its + /// starting point, and the given endpoint, duration, and ease method. + /// + /// Note that the starting point position is saved when the command is + /// applied, generally after the current system when [`apply_deferred()`] + /// runs. So any change to [`Transform::translation`] between this call and + /// [`apply_deferred()`] will be taken into account. + /// + /// This function is a fire-and-forget convenience helper, and doesn't give + /// access to the [`Entity`] created. To retrieve the entity and control + /// the animation playback, you should spawn a [`TweenAnim`] component + /// manually. + /// + /// # Example + /// + /// ``` + /// # use bevy::{prelude::*, ecs::world::CommandQueue}; + /// # use bevy_tweening::*; + /// # use std::time::Duration; + /// # let mut queue = CommandQueue::default(); + /// # let mut world = World::default(); + /// # let mut commands = Commands::new(&mut queue, &mut world); + /// commands.spawn(Transform::default()).move_to( + /// Vec3::new(3.5, 0., 0.), + /// Duration::from_secs(1), + /// EaseFunction::QuadraticIn, + /// ); + /// ``` + /// + /// [`apply_deferred()`]: bevy::ecs::system::System::apply_deferred + fn move_to( + self, + end: Vec3, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand>; + + /// Queue a new tween animation to move the current entity. + /// + /// The entity must have a [`Transform`] component. The tween animation will + /// be initialized with the current [`Transform::translation`] as its + /// ending point, and the given starting point, duration, and ease method. + /// + /// Note that the ending point position is saved when the command is + /// applied, generally after the current system when [`apply_deferred()`] + /// runs. So any change to [`Transform::translation`] between this call and + /// [`apply_deferred()`] will be taken into account. + /// + /// This function is a fire-and-forget convenience helper, and doesn't give + /// access to the [`Entity`] created. To retrieve the entity and control + /// the animation playback, you should spawn a [`TweenAnim`] component + /// manually. + /// + /// # Example + /// + /// ``` + /// # use bevy::{prelude::*, ecs::world::CommandQueue}; + /// # use bevy_tweening::*; + /// # use std::time::Duration; + /// # let mut queue = CommandQueue::default(); + /// # let mut world = World::default(); + /// # let mut commands = Commands::new(&mut queue, &mut world); + /// commands.spawn(Transform::default()).move_from( + /// Vec3::new(3.5, 0., 0.), + /// Duration::from_secs(1), + /// EaseFunction::QuadraticIn, + /// ); + /// ``` + /// + /// [`apply_deferred()`]: bevy::ecs::system::System::apply_deferred + fn move_from( + self, + start: Vec3, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand>; + + /// Queue a new tween animation to scale the current entity. + /// + /// The entity must have a [`Transform`] component. The tween animation will + /// be initialized with the current [`Transform::scale`] as its starting + /// point, and the given endpoint, duration, and ease method. + /// + /// Note that the starting point scale is saved when the command is applied, + /// generally after the current system when [`apply_deferred()`] + /// runs. So any change to [`Transform::scale`] between this call and + /// [`apply_deferred()`] will be taken into account. + /// + /// This function is a fire-and-forget convenience helper, and doesn't give + /// access to the [`Entity`] created. To retrieve the entity and control + /// the animation playback, you should spawn a [`TweenAnim`] component + /// manually. + /// + /// # Example + /// + /// ``` + /// # use bevy::{prelude::*, ecs::world::CommandQueue}; + /// # use bevy_tweening::*; + /// # use std::time::Duration; + /// # let mut queue = CommandQueue::default(); + /// # let mut world = World::default(); + /// # let mut commands = Commands::new(&mut queue, &mut world); + /// commands.spawn(Transform::default()).scale_to( + /// Vec3::splat(2.), // 200% size + /// Duration::from_secs(1), + /// EaseFunction::QuadraticIn, + /// ); + /// ``` + /// + /// [`apply_deferred()`]: bevy::ecs::system::System::apply_deferred + fn scale_to( + self, + end: Vec3, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand>; + + /// Queue a new tween animation to scale the current entity. + /// + /// The entity must have a [`Transform`] component. The tween animation will + /// be initialized with the current [`Transform::scale`] as its ending + /// point, and the given start scale, duration, and ease method. + /// + /// Note that the ending point scale is saved when the command is applied, + /// generally after the current system when [`apply_deferred()`] + /// runs. So any change to [`Transform::scale`] between this call and + /// [`apply_deferred()`] will be taken into account. + /// + /// This function is a fire-and-forget convenience helper, and doesn't give + /// access to the [`Entity`] created. To retrieve the entity and control + /// the animation playback, you should spawn a [`TweenAnim`] component + /// manually. + /// + /// # Example + /// + /// ``` + /// # use bevy::{prelude::*, ecs::world::CommandQueue}; + /// # use bevy_tweening::*; + /// # use std::time::Duration; + /// # let mut queue = CommandQueue::default(); + /// # let mut world = World::default(); + /// # let mut commands = Commands::new(&mut queue, &mut world); + /// commands.spawn(Transform::default()).scale_from( + /// Vec3::splat(0.8), // 80% size + /// Duration::from_secs(1), + /// EaseFunction::QuadraticIn, + /// ); + /// ``` + /// + /// [`apply_deferred()`]: bevy::ecs::system::System::apply_deferred + fn scale_from( + self, + start: Vec3, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand>; + + /// Queue a new tween animation to rotate the current entity around its X + /// axis continuously (repeats forever, linearly). + /// + /// The entity must have a [`Transform`] component. + /// + /// This function is a fire-and-forget convenience helper, and doesn't give + /// access to the [`Entity`] created. To retrieve the entity and control + /// the animation playback, you should spawn a [`TweenAnim`] component + /// manually. + /// + /// # Example + /// + /// ``` + /// # use bevy::{prelude::*, ecs::world::CommandQueue}; + /// # use bevy_tweening::*; + /// # use std::time::Duration; + /// # let mut queue = CommandQueue::default(); + /// # let mut world = World::default(); + /// # let mut commands = Commands::new(&mut queue, &mut world); + /// commands + /// .spawn(Transform::default()) + /// .rotate_x(Duration::from_secs(1)); + /// ``` + fn rotate_x(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand>; + + /// Queue a new tween animation to rotate the current entity around its Y + /// axis continuously (repeats forever, linearly). + /// + /// The entity must have a [`Transform`] component. + /// + /// This function is a fire-and-forget convenience helper, and doesn't give + /// access to the [`Entity`] created. To retrieve the entity and control + /// the animation playback, you should spawn a [`TweenAnim`] component + /// manually. + /// + /// # Example + /// + /// ``` + /// # use bevy::{prelude::*, ecs::world::CommandQueue}; + /// # use bevy_tweening::*; + /// # use std::time::Duration; + /// # let mut queue = CommandQueue::default(); + /// # let mut world = World::default(); + /// # let mut commands = Commands::new(&mut queue, &mut world); + /// commands + /// .spawn(Transform::default()) + /// .rotate_y(Duration::from_secs(1)); + /// ``` + fn rotate_y(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand>; + + /// Queue a new tween animation to rotate the current entity around its Z + /// axis continuously (repeats forever, linearly). + /// + /// The entity must have a [`Transform`] component. + /// + /// This function is a fire-and-forget convenience helper, and doesn't give + /// access to the [`Entity`] created. To retrieve the entity and control + /// the animation playback, you should spawn a [`TweenAnim`] component + /// manually. + /// + /// # Example + /// + /// ``` + /// # use bevy::{prelude::*, ecs::world::CommandQueue}; + /// # use bevy_tweening::*; + /// # use std::time::Duration; + /// # let mut queue = CommandQueue::default(); + /// # let mut world = World::default(); + /// # let mut commands = Commands::new(&mut queue, &mut world); + /// commands + /// .spawn(Transform::default()) + /// .rotate_z(Duration::from_secs(1)); + /// ``` + fn rotate_z(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand>; + + /// Queue a new tween animation to rotate the current entity around its X + /// axis by a given angle. + /// + /// The entity must have a [`Transform`] component. The animation applies a + /// rotation on top of the value of the [`Transform`] at the time the + /// animation is queued. + /// + /// This function is a fire-and-forget convenience helper, and doesn't give + /// access to the [`Entity`] created. To retrieve the entity and control + /// the animation playback, you should spawn a [`TweenAnim`] component + /// manually. + /// + /// # Example + /// + /// ``` + /// # use bevy::{prelude::*, ecs::world::CommandQueue}; + /// # use bevy_tweening::*; + /// # use std::time::Duration; + /// # let mut queue = CommandQueue::default(); + /// # let mut world = World::default(); + /// # let mut commands = Commands::new(&mut queue, &mut world); + /// commands.spawn(Transform::default()).rotate_x_by( + /// std::f32::consts::FRAC_PI_4, + /// Duration::from_secs(1), + /// EaseFunction::QuadraticIn, + /// ); + /// ``` + fn rotate_x_by( + self, + angle: f32, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand>; + + /// Queue a new tween animation to rotate the current entity around its Y + /// axis by a given angle. + /// + /// The entity must have a [`Transform`] component. The animation applies a + /// rotation on top of the value of the [`Transform`] at the time the + /// animation is queued. + /// + /// This function is a fire-and-forget convenience helper, and doesn't give + /// access to the [`Entity`] created. To retrieve the entity and control + /// the animation playback, you should spawn a [`TweenAnim`] component + /// manually. + /// + /// # Example + /// + /// ``` + /// # use bevy::{prelude::*, ecs::world::CommandQueue}; + /// # use bevy_tweening::*; + /// # use std::time::Duration; + /// # let mut queue = CommandQueue::default(); + /// # let mut world = World::default(); + /// # let mut commands = Commands::new(&mut queue, &mut world); + /// commands.spawn(Transform::default()).rotate_y_by( + /// std::f32::consts::FRAC_PI_4, + /// Duration::from_secs(1), + /// EaseFunction::QuadraticIn, + /// ); + /// ``` + fn rotate_y_by( + self, + angle: f32, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand>; + + /// Queue a new tween animation to rotate the current entity around its Z + /// axis by a given angle. + /// + /// The entity must have a [`Transform`] component. The animation applies a + /// rotation on top of the value of the [`Transform`] at the time the + /// animation is queued. + /// + /// This function is a fire-and-forget convenience helper, and doesn't give + /// access to the [`Entity`] created. To retrieve the entity and control + /// the animation playback, you should spawn a [`TweenAnim`] component + /// manually. + /// + /// # Example + /// + /// ``` + /// # use bevy::{prelude::*, ecs::world::CommandQueue}; + /// # use bevy_tweening::*; + /// # use std::time::Duration; + /// # let mut queue = CommandQueue::default(); + /// # let mut world = World::default(); + /// # let mut commands = Commands::new(&mut queue, &mut world); + /// commands.spawn(Transform::default()).rotate_z_by( + /// std::f32::consts::FRAC_PI_4, + /// Duration::from_secs(1), + /// EaseFunction::QuadraticIn, + /// ); + /// ``` + fn rotate_z_by( + self, + angle: f32, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand>; +} - /// Set the initial speed of the animator. See [`Animator::set_speed`] for - /// details. - #[must_use] - pub fn with_speed(mut self, speed: f32) -> Self { - self.speed = speed; - self - } +/// Helper trait to abstract a tweening animation command. +/// +/// This is mostly used internally by the [`AnimatedEntityCommands`] to tweak +/// the current animation, while also abstracting the various commands used to +/// implement the [`EntityCommandsTweeningExtensions`]. In general, you probably +/// don't have any use for that trait. +pub trait TweenCommand: EntityCommand { + /// Get read-only access to the tween configuration of the command. + #[allow(unused)] + fn config(&self) -> &TweenConfig; + + /// Get mutable access to the tween configuration of the command. + fn config_mut(&mut self) -> &mut TweenConfig; +} - /// Set the animation speed. Defaults to 1. - /// - /// A speed of 2 means the animation will run twice as fast while a speed of 0.1 - /// will result in a 10x slowed animation. - pub fn set_speed(&mut self, speed: f32) { - self.speed = speed; - } +/// Animation command to move an entity to a target position. +#[derive(Clone, Copy)] +pub(crate) struct MoveToCommand { + end: Vec3, + config: TweenConfig, +} - /// Get the animation speed. - /// - /// See [`set_speed()`] for a definition of what the animation speed is. - /// - /// [`set_speed()`]: Animator::speed - pub fn speed(&self) -> f32 { - self.speed +impl EntityCommand for MoveToCommand { + fn apply(self, mut entity: EntityWorldMut) { + if let Some(start) = entity.get::().map(|tr| tr.translation) { + let lens = TransformPositionLens { + start, + end: self.end, + }; + let tween = Tween::from_config(self.config, lens); + let anim_target = AnimTarget::component::(entity.id()); + entity.world_scope(|world| { + world.spawn((TweenAnim::new(tween), anim_target)); + }); } + } +} - /// Set the top-level tweenable item this animator controls. - pub fn set_tweenable(&mut self, tween: impl Tweenable + 'static) { - self.tweenable = Box::new(tween); - } +impl TweenCommand for MoveToCommand { + #[inline] + fn config(&self) -> &TweenConfig { + &self.config + } - /// Get the top-level tweenable this animator is currently controlling. - #[must_use] - pub fn tweenable(&self) -> &dyn Tweenable { - self.tweenable.as_ref() - } + #[inline] + fn config_mut(&mut self) -> &mut TweenConfig { + &mut self.config + } +} - /// Get the top-level mutable tweenable this animator is currently controlling. - #[must_use] - pub fn tweenable_mut(&mut self) -> &mut dyn Tweenable { - self.tweenable.as_mut() - } +/// Animation command to move an entity from a source position. +#[derive(Clone, Copy)] +pub(crate) struct MoveFromCommand { + start: Vec3, + config: TweenConfig, +} - /// Stop animation playback and rewind the animation. - /// - /// This changes the animator state to [`AnimatorState::Paused`] and rewind its - /// tweenable. - pub fn stop(&mut self) { - self.state = AnimatorState::Paused; - self.tweenable_mut().rewind(); +impl EntityCommand for MoveFromCommand { + fn apply(self, mut entity: EntityWorldMut) { + if let Some(end) = entity.get::().map(|tr| tr.translation) { + let lens = TransformPositionLens { + start: self.start, + end, + }; + let tween = Tween::from_config(self.config, lens); + let anim_target = AnimTarget::component::(entity.id()); + entity.world_scope(|world| { + world.spawn((TweenAnim::new(tween), anim_target)); + }); } - }; + } } -/// Component to control the animation of another component. -/// -/// By default, the animated component is the component located on the same -/// entity as the [`Animator`] itself. But if [`Animator::target`] is set, -/// that entity will be used instead. -#[derive(Component)] -pub struct Animator { - /// Control if this animation is played or not. - pub state: AnimatorState, - /// When set, the animated component will be the one located on this entity. - pub target: Option, - tweenable: BoxedTweenable, - speed: f32, -} +impl TweenCommand for MoveFromCommand { + #[inline] + fn config(&self) -> &TweenConfig { + &self.config + } -impl std::fmt::Debug for Animator { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Animator") - .field("state", &self.state) - .finish() + #[inline] + fn config_mut(&mut self) -> &mut TweenConfig { + &mut self.config } } -impl Animator { - /// Create a new animator component from a single tweenable. - #[must_use] - pub fn new(tween: impl Tweenable + 'static) -> Self { - Self { - state: default(), - tweenable: Box::new(tween), - target: None, - speed: 1., +/// Animation command to scale an entity to a target size. +#[derive(Clone, Copy)] +pub(crate) struct ScaleToCommand { + end: Vec3, + config: TweenConfig, +} + +impl EntityCommand for ScaleToCommand { + fn apply(self, mut entity: EntityWorldMut) { + if let Some(start) = entity.get::().map(|tr| tr.scale) { + let lens = TransformScaleLens { + start, + end: self.end, + }; + let tween = Tween::from_config(self.config, lens); + let anim_target = AnimTarget::component::(entity.id()); + entity.world_scope(|world| { + world.spawn((TweenAnim::new(tween), anim_target)); + }); } } +} - /// Create a new version of this animator with the `target` set to the given entity. - pub fn with_target(mut self, entity: Entity) -> Self { - self.target = Some(entity); - self +impl TweenCommand for ScaleToCommand { + #[inline] + fn config(&self) -> &TweenConfig { + &self.config } - animator_impl!(); + #[inline] + fn config_mut(&mut self) -> &mut TweenConfig { + &mut self.config + } } -/// Component to control the animation of an asset. -/// -/// The animated asset is the asset referenced by a [`Handle`] component -/// located on the same entity as the [`AssetAnimator`] itself. -#[cfg(feature = "bevy_asset")] -#[derive(Component)] -pub struct AssetAnimator { - /// Control if this animation is played or not. - pub state: AnimatorState, - tweenable: BoxedTweenable, - speed: f32, +/// Animation command to scale an entity from a source size. +#[derive(Clone, Copy)] +pub(crate) struct ScaleFromCommand { + start: Vec3, + config: TweenConfig, } -#[cfg(feature = "bevy_asset")] -impl std::fmt::Debug for AssetAnimator { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("AssetAnimator") - .field("state", &self.state) - .finish() +impl EntityCommand for ScaleFromCommand { + fn apply(self, mut entity: EntityWorldMut) { + if let Some(end) = entity.get::().map(|tr| tr.scale) { + let lens = TransformScaleLens { + start: self.start, + end, + }; + let tween = Tween::from_config(self.config, lens); + let anim_target = AnimTarget::component::(entity.id()); + entity.world_scope(|world| { + world.spawn((TweenAnim::new(tween), anim_target)); + }); + } } } -#[cfg(feature = "bevy_asset")] -impl AssetAnimator { - /// Create a new asset animator component from a single tweenable. - #[must_use] - pub fn new(tween: impl Tweenable + 'static) -> Self { - Self { - state: default(), - tweenable: Box::new(tween), - speed: 1., - } +impl TweenCommand for ScaleFromCommand { + #[inline] + fn config(&self) -> &TweenConfig { + &self.config } - animator_impl!(); + #[inline] + fn config_mut(&mut self) -> &mut TweenConfig { + &mut self.config + } } -#[cfg(test)] -mod tests { - use bevy::ecs::{change_detection::MaybeLocation, component::Tick}; - - use self::tweenable::ComponentTarget; - - use super::*; - use crate::test_utils::*; +/// Animation command to rotate an entity around its X axis. +#[derive(Clone, Copy)] +pub(crate) struct RotateXCommand { + config: TweenConfig, +} - struct DummyLens { - start: f32, - end: f32, +impl EntityCommand for RotateXCommand { + fn apply(self, mut entity: EntityWorldMut) { + if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { + let lens = TransformRotateAdditiveXLens { + base_rotation, + start: 0., + end: std::f32::consts::TAU, + }; + let tween = Tween::from_config(self.config, lens) + .with_repeat(RepeatCount::Infinite, RepeatStrategy::Repeat); + let anim_target = AnimTarget::component::(entity.id()); + entity.world_scope(|world| { + world.spawn((TweenAnim::new(tween), anim_target)); + }); + } } +} - #[derive(Debug, Default, Clone, Copy, Component)] - struct DummyComponent { - value: f32, +impl TweenCommand for RotateXCommand { + #[inline] + fn config(&self) -> &TweenConfig { + &self.config } - #[cfg(feature = "bevy_asset")] - #[derive(Asset, Debug, Default, Reflect)] - struct DummyAsset { - value: f32, + #[inline] + fn config_mut(&mut self) -> &mut TweenConfig { + &mut self.config } +} - impl Lens for DummyLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { - target.value = self.start.lerp(self.end, ratio); +/// Animation command to rotate an entity around its Y axis. +#[derive(Clone, Copy)] +pub(crate) struct RotateYCommand { + config: TweenConfig, +} + +impl EntityCommand for RotateYCommand { + fn apply(self, mut entity: EntityWorldMut) { + if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { + let lens = TransformRotateAdditiveYLens { + base_rotation, + start: 0., + end: std::f32::consts::TAU, + }; + let tween = Tween::from_config(self.config, lens) + .with_repeat(RepeatCount::Infinite, RepeatStrategy::Repeat); + let anim_target = AnimTarget::component::(entity.id()); + entity.world_scope(|world| { + world.spawn((TweenAnim::new(tween), anim_target)); + }); } } +} - #[test] - fn dummy_lens_component() { - let mut c = DummyComponent::default(); - let mut l = DummyLens { start: 0., end: 1. }; - for r in [0_f32, 0.01, 0.3, 0.5, 0.9, 0.999, 1.] { - { - let mut added = Tick::new(0); - let mut last_changed = Tick::new(0); - let mut caller = MaybeLocation::caller(); - let mut target = ComponentTarget::new(Mut::new( - &mut c, - &mut added, - &mut last_changed, - Tick::new(0), - Tick::new(1), - caller.as_mut(), - )); +impl TweenCommand for RotateYCommand { + #[inline] + fn config(&self) -> &TweenConfig { + &self.config + } - l.lerp(&mut target, r); + #[inline] + fn config_mut(&mut self) -> &mut TweenConfig { + &mut self.config + } +} - assert!(target.to_mut().is_changed()); - } +/// Animation command to rotate an entity around its Z axis. +#[derive(Clone, Copy)] +pub(crate) struct RotateZCommand { + config: TweenConfig, +} - assert_approx_eq!(c.value, r); +impl EntityCommand for RotateZCommand { + fn apply(self, mut entity: EntityWorldMut) { + if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { + let lens = TransformRotateAdditiveZLens { + base_rotation, + start: 0., + end: std::f32::consts::TAU, + }; + let tween = Tween::from_config(self.config, lens) + .with_repeat(RepeatCount::Infinite, RepeatStrategy::Repeat); + let anim_target = AnimTarget::component::(entity.id()); + entity.world_scope(|world| { + world.spawn((TweenAnim::new(tween), anim_target)); + }); } } +} - #[cfg(feature = "bevy_asset")] - impl Lens for DummyLens { - fn lerp(&mut self, target: &mut dyn Targetable, ratio: f32) { - target.value = self.start.lerp(self.end, ratio); - } +impl TweenCommand for RotateZCommand { + #[inline] + fn config(&self) -> &TweenConfig { + &self.config } - #[cfg(feature = "bevy_asset")] - #[test] - fn dummy_lens_asset() { - use self::tweenable::AssetTarget; - - let mut assets = Assets::::default(); - let handle = assets.add(DummyAsset::default()); + #[inline] + fn config_mut(&mut self) -> &mut TweenConfig { + &mut self.config + } +} - let mut l = DummyLens { start: 0., end: 1. }; - for r in [0_f32, 0.01, 0.3, 0.5, 0.9, 0.999, 1.] { - { - let mut added = Tick::new(0); - let mut last_changed = Tick::new(0); - let mut caller = MaybeLocation::caller(); - let mut target = AssetTarget::new(Mut::new( - &mut assets, - &mut added, - &mut last_changed, - Tick::new(0), - Tick::new(0), - caller.as_mut(), - )); - target.handle = handle.clone(); +/// Animation command to rotate an entity around its X axis by a given angle. +#[derive(Clone, Copy)] +pub(crate) struct RotateXByCommand { + angle: f32, + config: TweenConfig, +} - l.lerp(&mut target, r); - } - assert_approx_eq!(assets.get(handle.id()).unwrap().value, r); +impl EntityCommand for RotateXByCommand { + fn apply(self, mut entity: EntityWorldMut) { + if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { + let lens = TransformRotateAdditiveXLens { + base_rotation, + start: 0., + end: self.angle, + }; + let tween = Tween::from_config(self.config, lens); + let anim_target = AnimTarget::component::(entity.id()); + entity.world_scope(|world| { + world.spawn((TweenAnim::new(tween), anim_target)); + }); } } +} - #[test] - fn repeat_count() { - let count = RepeatCount::default(); - assert_eq!(count, RepeatCount::Finite(1)); +impl TweenCommand for RotateXByCommand { + #[inline] + fn config(&self) -> &TweenConfig { + &self.config } - #[test] - fn repeat_strategy() { - let strategy = RepeatStrategy::default(); - assert_eq!(strategy, RepeatStrategy::Repeat); + #[inline] + fn config_mut(&mut self) -> &mut TweenConfig { + &mut self.config } +} - #[test] - fn tweening_direction() { - let tweening_direction = TweeningDirection::default(); - assert_eq!(tweening_direction, TweeningDirection::Forward); +/// Animation command to rotate an entity around its Y axis by a given angle. +#[derive(Clone, Copy)] +pub(crate) struct RotateYByCommand { + angle: f32, + config: TweenConfig, +} + +impl EntityCommand for RotateYByCommand { + fn apply(self, mut entity: EntityWorldMut) { + if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { + let lens = TransformRotateAdditiveYLens { + base_rotation, + start: 0., + end: self.angle, + }; + let tween = Tween::from_config(self.config, lens); + let anim_target = AnimTarget::component::(entity.id()); + entity.world_scope(|world| { + world.spawn((TweenAnim::new(tween), anim_target)); + }); + } } +} - #[test] - fn animator_state() { - let mut state = AnimatorState::default(); - assert_eq!(state, AnimatorState::Playing); - state = !state; - assert_eq!(state, AnimatorState::Paused); - state = !state; - assert_eq!(state, AnimatorState::Playing); +impl TweenCommand for RotateYByCommand { + #[inline] + fn config(&self) -> &TweenConfig { + &self.config } - #[test] - fn ease_method() { - let ease = EaseMethod::default(); - assert!(matches!( + #[inline] + fn config_mut(&mut self) -> &mut TweenConfig { + &mut self.config + } +} + +/// Animation command to rotate an entity around its Z axis by a given angle. +#[derive(Clone, Copy)] +pub(crate) struct RotateZByCommand { + angle: f32, + config: TweenConfig, +} + +impl EntityCommand for RotateZByCommand { + fn apply(self, mut entity: EntityWorldMut) { + if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { + let lens = TransformRotateAdditiveZLens { + base_rotation, + start: 0., + end: self.angle, + }; + let tween = Tween::from_config(self.config, lens); + let anim_target = AnimTarget::component::(entity.id()); + entity.world_scope(|world| { + world.spawn((TweenAnim::new(tween), anim_target)); + }); + } + } +} + +impl TweenCommand for RotateZByCommand { + #[inline] + fn config(&self) -> &TweenConfig { + &self.config + } + + #[inline] + fn config_mut(&mut self) -> &mut TweenConfig { + &mut self.config + } +} + +/// Wrapper over an [`EntityCommands`] which stores an animation command. +/// +/// The wrapper acts as, and dereferences to, a regular [`EntityCommands`] as +/// _e.g._ returned by [`Commands::spawn()`]. In addition, it stores a pending +/// animation command, which can be further tweaked before being queued into the +/// entity commands queue. This deferred queuing allows fluent patterns like: +/// +/// ``` +/// # use std::time::Duration; +/// # use bevy::prelude::*; +/// # use bevy_tweening::*; +/// # fn my_system(mut commands: Commands) { +/// commands +/// .spawn(Transform::default()) +/// // Consume the EntityCommands, and wrap it into an AnimatedEntityCommands, +/// // which stores an animation command to move an entity. +/// .move_to( +/// Vec3::ONE, +/// Duration::from_millis(400), +/// EaseFunction::QuadraticIn, +/// ) +/// // Tweak the stored animation to set the repeat count of the Tween. +/// .with_repeat_count(2); +/// # } +/// ``` +/// +/// The animation commands always stores the last animation inserted. When the +/// commands is mutably dereferenced, it first flushes the pending animation +/// command, if any, by inserting it into the underlying [`EntityCommands`] +/// queue. It also flushes the animation when dropped, to ensure the last +/// animation is queued too. +/// +/// To move from an [`AnimatedEntityCommands`] to its underlying +/// [`EntityCommands`], the former automatically dereferences to the latter. +/// Note however that once you're back on the base [`EntityCommands`], you can +/// only get a new [`AnimatedEntityCommands`] via functions consuming the +/// [`EntityCommands`] by value. In that case, you need to call [`reborrow()`]: +/// +/// ``` +/// # use std::time::Duration; +/// # use bevy::prelude::*; +/// # use bevy_tweening::*; +/// # fn my_system(mut commands: Commands) { +/// commands +/// .spawn(Transform::default()) +/// .move_to( +/// Vec3::ONE, +/// Duration::from_millis(400), +/// EaseFunction::QuadraticIn, +/// ) +/// // This call invokes std::ops::DerefMut, and returns a mutable ref +/// // to the underlying EntityCommands +/// .insert(Name::new("my_object")) +/// // Here we need to reborrow() to convert from `&mut EntityCommands` +/// // (by mutable ref) to `EntityCommands` (by value) +/// .reborrow() +/// // This call requires an `EntityCommands` (by value) +/// .scale_to( +/// Vec3::splat(1.1), +/// Duration::from_millis(400), +/// EaseFunction::Linear, +/// ); +/// # } +/// ``` +/// +/// [`reborrow()`]: bevy::prelude::EntityCommands::reborrow +pub struct AnimatedEntityCommands<'a, C: TweenCommand> { + commands: EntityCommands<'a>, + cmd: Option, +} + +impl<'a, C: TweenCommand> AnimatedEntityCommands<'a, C> { + /// Wrap an [`EntityCommands`] into an animated one. + pub fn new(commands: EntityCommands<'a>, cmd: C) -> Self { + Self { + commands, + cmd: Some(cmd), + } + } + + /// Set the repeat count of this animation. + #[inline] + pub fn with_repeat_count(mut self, repeat_count: impl Into) -> Self { + if let Some(cmd) = self.cmd.as_mut() { + cmd.config_mut().repeat_count = repeat_count.into(); + } + self + } + + /// Set the repeat strategy of this animation. + #[inline] + pub fn with_repeat_strategy(mut self, repeat_strategy: RepeatStrategy) -> Self { + if let Some(cmd) = self.cmd.as_mut() { + cmd.config_mut().repeat_strategy = repeat_strategy; + } + self + } + + /// Configure the repeat parameters of this animation. + /// + /// This is a shortcut for: + /// + /// ```no_run + /// # use bevy_tweening::*; + /// # struct AnimatedEntityCommands {} + /// # impl AnimatedEntityCommands { + /// # fn with_repeat_count(self, r: RepeatCount) -> Self { unimplemented!() } + /// # fn with_repeat_strategy(self, r: RepeatStrategy) -> Self { unimplemented!() } + /// # fn xxx(self) -> Self { + /// # let repeat_count = RepeatCount::Infinite; + /// # let repeat_strategy = RepeatStrategy::Repeat; + /// self.with_repeat_count(repeat_count) + /// .with_repeat_strategy(repeat_strategy) + /// # }} + /// ``` + #[inline] + pub fn with_repeat( + self, + repeat_count: impl Into, + repeat_strategy: RepeatStrategy, + ) -> Self { + self.with_repeat_count(repeat_count) + .with_repeat_strategy(repeat_strategy) + } + + /// Consume self and return the inner [`EntityCommands`]. + /// + /// The current animation is inserted into the commands queue, before that + /// wrapped commands queue is returned. + pub fn into_inner(mut self) -> EntityCommands<'a> { + self.flush(); + // Since we already flushed above, we don't need Drop. And trying to keep would + // allow it to access self.commands after it was stolen (even though we know the + // implementation doesn't in practice). Still, it's safer to just short-circuit + // Drop here. + let this = std::mem::ManuallyDrop::new(self); + // SAFETY: We have flushed self.cmd which is now None, and we're stealing + // self.commands, after which the this object is forgotten and never + // accessed again. + #[allow(unsafe_code)] + unsafe { + std::ptr::read(&this.commands) + } + } + + /// Flush the current animation, inserting it into the commands queue. + /// + /// This makes it impossible to further tweak the animation. This is + /// automatically called when a new animation is created and when the + /// commands queue is dropped with the last animation pending. + fn flush(&mut self) { + if let Some(cmd) = self.cmd.take() { + self.queue(cmd); + } + } +} + +impl<'a, C: TweenCommand> EntityCommandsTweeningExtensions<'a> for AnimatedEntityCommands<'a, C> { + #[inline] + fn move_to( + self, + end: Vec3, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + self.into_inner().move_to(end, duration, ease_method) + } + + #[inline] + fn move_from( + self, + start: Vec3, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + self.into_inner().move_from(start, duration, ease_method) + } + + #[inline] + fn scale_to( + self, + end: Vec3, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + self.into_inner().scale_to(end, duration, ease_method) + } + + #[inline] + fn scale_from( + self, + start: Vec3, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + self.into_inner().scale_from(start, duration, ease_method) + } + + #[inline] + fn rotate_x(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand> { + self.into_inner().rotate_x(cycle_duration) + } + + #[inline] + fn rotate_y(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand> { + self.into_inner().rotate_y(cycle_duration) + } + + #[inline] + fn rotate_z(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand> { + self.into_inner().rotate_z(cycle_duration) + } + + #[inline] + fn rotate_x_by( + self, + angle: f32, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + self.into_inner().rotate_x_by(angle, duration, ease_method) + } + + #[inline] + fn rotate_y_by( + self, + angle: f32, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + self.into_inner().rotate_y_by(angle, duration, ease_method) + } + + #[inline] + fn rotate_z_by( + self, + angle: f32, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + self.into_inner().rotate_z_by(angle, duration, ease_method) + } +} + +impl<'a, C: TweenCommand> Deref for AnimatedEntityCommands<'a, C> { + type Target = EntityCommands<'a>; + + fn deref(&self) -> &Self::Target { + &self.commands + } +} + +impl DerefMut for AnimatedEntityCommands<'_, C> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.flush(); + &mut self.commands + } +} + +impl Drop for AnimatedEntityCommands<'_, C> { + fn drop(&mut self) { + self.flush(); + } +} + +impl<'a> EntityCommandsTweeningExtensions<'a> for EntityCommands<'a> { + #[inline] + fn move_to( + self, + end: Vec3, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + AnimatedEntityCommands::new( + self, + MoveToCommand { + end, + config: TweenConfig { + ease_method: ease_method.into(), + cycle_duration: duration, + ..default() + }, + }, + ) + } + + #[inline] + fn move_from( + self, + start: Vec3, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + AnimatedEntityCommands::new( + self, + MoveFromCommand { + start, + config: TweenConfig { + ease_method: ease_method.into(), + cycle_duration: duration, + ..default() + }, + }, + ) + } + + #[inline] + fn scale_to( + self, + end: Vec3, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + AnimatedEntityCommands::new( + self, + ScaleToCommand { + end, + config: TweenConfig { + ease_method: ease_method.into(), + cycle_duration: duration, + ..default() + }, + }, + ) + } + + #[inline] + fn scale_from( + self, + start: Vec3, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + AnimatedEntityCommands::new( + self, + ScaleFromCommand { + start, + config: TweenConfig { + ease_method: ease_method.into(), + cycle_duration: duration, + ..default() + }, + }, + ) + } + + #[inline] + fn rotate_x(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand> { + AnimatedEntityCommands::new( + self, + RotateXCommand { + config: TweenConfig { + ease_method: EaseFunction::Linear.into(), + cycle_duration, + ..default() + }, + }, + ) + } + + #[inline] + fn rotate_y(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand> { + AnimatedEntityCommands::new( + self, + RotateYCommand { + config: TweenConfig { + ease_method: EaseFunction::Linear.into(), + cycle_duration, + ..default() + }, + }, + ) + } + + #[inline] + fn rotate_z(self, cycle_duration: Duration) -> AnimatedEntityCommands<'a, impl TweenCommand> { + AnimatedEntityCommands::new( + self, + RotateZCommand { + config: TweenConfig { + ease_method: EaseFunction::Linear.into(), + cycle_duration, + ..default() + }, + }, + ) + } + + #[inline] + fn rotate_x_by( + self, + angle: f32, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + AnimatedEntityCommands::new( + self, + RotateXByCommand { + angle, + config: TweenConfig { + ease_method: ease_method.into(), + cycle_duration: duration, + ..default() + }, + }, + ) + } + + #[inline] + fn rotate_y_by( + self, + angle: f32, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + AnimatedEntityCommands::new( + self, + RotateYByCommand { + angle, + config: TweenConfig { + ease_method: ease_method.into(), + cycle_duration: duration, + ..default() + }, + }, + ) + } + + #[inline] + fn rotate_z_by( + self, + angle: f32, + duration: Duration, + ease_method: impl Into, + ) -> AnimatedEntityCommands<'a, impl TweenCommand> { + AnimatedEntityCommands::new( + self, + RotateZByCommand { + angle, + config: TweenConfig { + ease_method: ease_method.into(), + cycle_duration: duration, + ..default() + }, + }, + ) + } +} + +/// Event raised when a [`TweenAnim`] completed. +#[derive(Debug, Clone, Copy, Event)] +pub struct AnimCompletedEvent { + /// The entity owning the [`TweenAnim`] which completed. + /// + /// Note that commonly the [`TweenAnim`] is despawned on completion, so + /// can't be queried anymore with this entity. You can prevent a completed + /// animation from being automatically destroyed by + /// setting [`TweenAnim::destroy_on_completion`] to `false`. + pub anim_entity: Entity, + /// The animation target. + /// + /// This is provided both as a convenience for [`TweenAnim`]s not destroyed + /// on completion, and because for those animations which are destroyed + /// on completion the information is not available anymore when this + /// event is received. + pub target: AnimTargetKind, +} + +/// Errors returned by various animation functions. +#[derive(Debug, Error, Clone, Copy)] +pub enum TweeningError { + /// The asset resolver for the given asset is not registered. + #[error("Asset resolver for asset with resource ID {0:?} is not registered.")] + AssetResolverNotRegistered(ComponentId), + /// The entity was not found in the World. + #[error("Entity {0:?} not found in the World.")] + EntityNotFound(Entity), + /// The entity should have had a TweenAnim but it was not found. + #[error("Entity {0:?} doesn't have a TweenAnim.")] + MissingTweenAnim(Entity), + /// The component of the given type is not registered. + #[error("Component of type {0:?} is not registered in the World.")] + ComponentNotRegistered(TypeId), + /// The resource of the given type is not registered. + #[error("Resource of type {0:?} is not registered in the World.")] + ResourceNotRegistered(TypeId), + /// The asset container for the given asset type is not registered. + #[error("Asset container Assets for asset type A = {0:?} is not registered in the World.")] + AssetNotRegistered(TypeId), + /// The component of the given type is not registered. + #[error("Component of type {0:?} is not present on entity {1:?}.")] + MissingComponent(TypeId, Entity), + /// The asset cannot be found. + #[error("Asset ID {0:?} is invalid.")] + InvalidAssetId(UntypedAssetId), + /// The asset ID references a different type than expected. + #[error("Expected type of asset ID to be {expected:?} but got {actual:?} instead.")] + InvalidAssetIdType { + /// The expected asset type. + expected: TypeId, + /// The actual type the asset ID references. + actual: TypeId, + }, + /// Expected [`Tweenable::target_type_id()`] to return a value, but it + /// returned `None`. + #[error("Expected a typed Tweenable.")] + UntypedTweenable, + /// Invalid [`Entity`]. + #[error("Invalid Entity {0:?}.")] + InvalidTweenId(Entity), + /// Cannot change target kind. + #[error("Unexpected target kind: was component={0}, now component={1}")] + MismatchingTargetKind(bool, bool), + /// Cannot change component type. + #[error("Cannot change component type: was component_id={0:?}, now component_id={1:?}")] + MismatchingComponentId(ComponentId, ComponentId), + /// Cannot change asset type. + #[error("Cannot change asset type: was component_id={0:?}, now component_id={1:?}")] + MismatchingAssetResourceId(ComponentId, ComponentId), +} + +type RegisterAction = dyn Fn(&Components, &mut TweenResolver) + Send + Sync + 'static; + +/// Enumeration of the types of animation targets. +/// +/// This type holds the minimum amount of data to reference ananimation target, +/// aside from the actual type of the target. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AnimTargetKind { + /// Component animation target. + Component { + /// The entity owning the component instance. + entity: Entity, + }, + /// Resource animation target. + Resource, + /// Asset animation target. + Asset { + /// The asset ID inside the [`Assets`] collection. + asset_id: UntypedAssetId, + /// Type ID of the [`Assets`] collection itself. + assets_type_id: TypeId, + }, +} + +/// Component defining the target of an animation. +/// +/// References an object used as the target of the animation stored in the +/// [`TweenAnim`] component on the same entity. +#[derive(Component)] +pub struct AnimTarget { + /// Target kind and additional data to identify it. + pub kind: AnimTargetKind, + + /// Self-registering action for assets and resources. + pub(crate) register_action: Option>, +} + +impl AnimTarget { + /// Create a target mutating a component on the given entity. + pub fn component>(entity: Entity) -> Self { + Self { + kind: AnimTargetKind::Component { entity }, + register_action: None, + } + } + + /// Create a target mutating the given resource. + pub fn resource() -> Self { + let register_action = |components: &Components, resolver: &mut TweenResolver| { + resolver.register_resource_resolver_for::(components); + }; + Self { + kind: AnimTargetKind::Resource, + register_action: Some(Box::new(register_action)), + } + } + + /// Create a target mutating the given asset. + /// + /// The asset is identified by its type, and its [`AssetId`]. + pub fn asset(asset_id: impl Into>) -> Self { + let register_action = |components: &Components, resolver: &mut TweenResolver| { + resolver.register_asset_resolver_for::(components); + }; + Self { + kind: AnimTargetKind::Asset { + asset_id: asset_id.into().untyped(), + assets_type_id: TypeId::of::>(), + }, + register_action: Some(Box::new(register_action)), + } + } + + /// Register any resolver for this target. + pub(crate) fn register(&self, components: &Components, resolver: &mut TweenResolver) { + if let Some(register_action) = self.register_action.as_ref() { + register_action(components, resolver); + } + } +} + +/// Animation controller instance. +/// +/// The [`TweenAnim`] represents a single animation instance for a single +/// target (component or resource or asset). Each instance is independent, even +/// if it mutates the same target as another instance. Spawning this component +/// adds an active animation, and destroying it stops that animation. The +/// component can also be used to control the animation playback at runtime, +/// like the playback speed. +/// +/// The target is described by the [`AnimTarget`] component. If that component +/// is absent, then the animation implicitly targets a component on the current +/// Entity. The type of the component is derived from the type that the [`Lens`] +/// animates. +/// +/// _If you're looking for the basic tweenable animation description, see +/// [`Tween`] instead._ +/// +/// # Example +/// +/// ``` +/// # use bevy::prelude::*; +/// # use bevy_tweening::*; +/// # fn make_tweenable() -> Tween { unimplemented!() } +/// fn my_system(mut commands: Commands) { +/// let tweenable = make_tweenable::(); +/// let id1 = commands +/// .spawn(( +/// Transform::default(), +/// // Implicitly targets the current entity's Transform +/// TweenAnim::new(tweenable), +/// )) +/// .id(); +/// +/// let tweenable2 = make_tweenable::(); +/// commands.spawn(( +/// TweenAnim::new(tweenable2), +/// // Explicitly targets the Transform component of entity 'id1' +/// AnimTarget::component::(id1), +/// )); +/// } +/// ``` +#[derive(Component)] +pub struct TweenAnim { + /// The animation itself. Note that the tweenable is stateful, so can't be + /// shared with another [`TweenAnim`] instance. + tweenable: BoxedTweenable, + /// Control if the animation is played or not. Defaults to + /// [`PlaybackState::Playing`]. + /// + /// Pausing an animation with [`PlaybackState::Paused`] is functionaly + /// equivalent to setting its [`speed`] to zero. The two fields remain + /// independent though, for convenience. + /// + /// [`speed`]: Self::speed + pub playback_state: PlaybackState, + /// Relative playback speed. Defaults to `1.` (normal speed; 100%). + /// + /// Setting a negative or zero speed value effectively pauses the animation + /// (although the [`playback_state`] remains unchanged). Negative values may + /// be clamped to 0. when the animation is stepped, but positive or zero + /// values are never modified by the library. + /// + /// # Time precision + /// + /// _This note is an implementation detail which can usually be ignored._ + /// + /// Despite the use of `f64`, setting a playback speed different from `1.` + /// (100% speed) may produce small inaccuracies in durations, especially + /// for longer animations. However those are often negligible. + /// This is due to the very large precision of `Duration` (typically 96 + /// bits or more), even compared to `f64` (64 bits), and the fact this speed + /// factor is a multiplier whereas most other time quantities are added or + /// subtracted. + /// + /// [`playback_state`]: Self::playback_state + pub speed: f64, + /// Destroy the animation once completed. This defaults to `true`, and makes + /// the stepping functions like [`TweenAnim::step_all()`] destroy this + /// animation once it completed. To keep the animation queued, and allow + /// access after it completed, set this to `false`. Note however that + /// you should avoid leaving all animations queued if they're unused, as + /// this wastes memory and may degrade performances if too many + /// completed animations are kept around for no good reason. + pub destroy_on_completion: bool, + /// Current tweening completion state. + tween_state: TweenState, +} + +impl TweenAnim { + /// Create a new tween animation. + /// + /// This component represents the runtime animation being played to mutate a + /// specific target. + /// + /// # Panics + /// + /// Panics if the tweenable is "typeless", that is + /// [`Tweenable::target_type_id()`] returns `None`. Animations must + /// target a concrete component or asset type. This means in particular + /// that you can't use a single [`Delay`] alone. You can however use a + /// [`Delay`] or other typeless tweenables as part of a [`Sequence`], + /// provided there's at least one other typed tweenable in the sequence + /// to make it typed too. + #[inline] + pub fn new(tweenable: impl IntoBoxedTweenable) -> Self { + let tweenable = tweenable.into_boxed(); + assert!( + tweenable.target_type_id().is_some(), + "The top-level Tweenable of a TweenAnim must be typed (Tweenable::target_type_id() returns Some)." + ); + Self { + tweenable, + playback_state: PlaybackState::Playing, + speed: 1., + destroy_on_completion: true, + tween_state: TweenState::Active, + } + } + + /// Configure the playback speed. + pub fn with_speed(mut self, speed: f64) -> Self { + self.speed = speed; + self + } + + /// Enable or disable destroying this component on animation completion. + /// + /// If enabled, the component is automatically removed from its `Entity` + /// when the animation completed. + pub fn with_destroy_on_completed(mut self, destroy_on_completed: bool) -> Self { + self.destroy_on_completion = destroy_on_completed; + self + } + + /// Step a single animation. + /// + /// _The [`step_all()`] function is called automatically by the animation + /// system registered by the [`TweeningPlugin`], you generally don't + /// need to call this one._ + /// + /// This is a shortcut for `step_many(world, delta_time, [entity])`, with + /// the added benefit that it returns some error if the entity is not valid. + /// See [`step_many()`] for details. + /// + /// # Example + /// + /// ``` + /// # use std::time::Duration; + /// # use bevy::prelude::*; + /// # use bevy_tweening::*; + /// # fn make_tweenable() -> Tween { unimplemented!() } + /// #[derive(Component)] + /// struct MyMarker; + /// + /// fn my_system(world: &mut World) -> Result<()> { + /// let mut q_anims = world.query_filtered::, With)>(); + /// let entity = q_anims.single(world)?; + /// let delta_time = Duration::from_millis(200); + /// TweenAnim::step_one(world, delta_time, entity); + /// Ok(()) + /// } + /// ``` + /// + /// # Returns + /// + /// This returns an error if the entity is not found or doesn't own a + /// [`TweenAnim`] component. + /// + /// [`step_all()`]: Self::step_all + /// [`step_many()`]: Self::step_many + #[inline] + pub fn step_one( + world: &mut World, + delta_time: Duration, + entity: Entity, + ) -> Result<(), TweeningError> { + let num = Self::step_many(world, delta_time, &[entity]); + if num > 0 { + Ok(()) + } else { + Err(TweeningError::EntityNotFound(entity)) + } + } + + /// Step some animation(s). + /// + /// _The [`step_all()`] function is called automatically by the animation + /// system registered by the [`TweeningPlugin`], you generally don't + /// need to call this one._ + /// + /// Step the given animation(s) by a given `delta_time`, which may be + /// [`Duration::ZERO`]. Passing a zero delta time may be useful to force the + /// current animation state to be applied to a target, in case you made + /// change which do not automatically do so (for example, retargeting an + /// animation). The `anims` are the entities which own a [`TweenAnim`] + /// component to step; any entity without a [`TweenAnim`] component is + /// silently ignored. + /// + /// The function doesn't check that all input entities are unique. If an + /// entity is duplicated in `anims`, the behavior is undefined, including + /// (but not guaranteed) stepping the animation multiple times. You're + /// responsible for ensuring the input entity slice contains distinct + /// entities. + /// + /// # Example + /// + /// ``` + /// # use std::time::Duration; + /// # use bevy::prelude::*; + /// # use bevy_tweening::*; + /// # fn make_tweenable() -> Tween { unimplemented!() } + /// #[derive(Component)] + /// struct MyMarker; + /// + /// fn my_system(world: &mut World) -> Result<()> { + /// let mut q_anims = world.query_filtered::, With)>(); + /// let entities = q_anims.iter(world).collect::>(); + /// let delta_time = Duration::from_millis(200); + /// TweenAnim::step_many(world, delta_time, &entities[..]); + /// Ok(()) + /// } + /// ``` + /// + /// # Returns + /// + /// Returns the number of [`TweenAnim`] component found and stepped, which + /// is always less than or equal to the input `anims` slice length. + /// + /// [`step_all()`]: Self::step_all + pub fn step_many(world: &mut World, delta_time: Duration, anims: &[Entity]) -> usize { + let mut targets = vec![]; + world.resource_scope(|world, mut resolver: Mut| { + let mut q_anims = world.query::<(Entity, &TweenAnim, Option<&AnimTarget>)>(); + targets.reserve(anims.len()); + for entity in anims { + if let Ok((entity, anim, maybe_target)) = q_anims.get(world, *entity) { + // Lazy registration with resolver if needed + if let Some(anim_target) = maybe_target { + anim_target.register(world.components(), &mut resolver); + } + + // Actually step the tweenable and update the target + if let Ok((target_type_id, component_id, target, is_retargetable)) = + Self::resolve_target( + world.components(), + maybe_target, + entity, + anim.tweenable(), + ) + { + targets.push(( + entity, + target_type_id, + component_id, + target, + is_retargetable, + )); + } + } + } + }); + Self::step_impl(world, delta_time, &targets[..]); + targets.len() + } + + /// Step all animations on the given world. + /// + /// _This function is called automatically by the animation system + /// registered by the [`TweeningPlugin`], you generally don't need to call + /// it._ + /// + /// Step all the [`TweenAnim`] components of the input world by a given + /// `delta_time`, which may be [`Duration::ZERO`]. Passing a zero delta + /// time may be useful to force the current animation state to be + /// applied to a target, in case you made change which do not + /// automatically do so (for example, retargeting an animation). + pub fn step_all(world: &mut World, delta_time: Duration) { + let targets = world.resource_scope(|world, mut resolver: Mut| { + let mut q_anims = world.query::<(Entity, &TweenAnim, Option<&AnimTarget>)>(); + q_anims + .iter(world) + .filter_map(|(entity, anim, maybe_target)| { + // Lazy registration with resolver if needed + if let Some(anim_target) = maybe_target { + anim_target.register(world.components(), &mut resolver); + } + + // Actually step the tweenable and update the target + match Self::resolve_target( + world.components(), + maybe_target, + entity, + anim.tweenable(), + ) { + Ok((target_type_id, component_id, target, is_retargetable)) => Some(( + entity, + target_type_id, + component_id, + target, + is_retargetable, + )), + Err(err) => { + bevy::log::error!( + "Error while stepping TweenAnim on entity {:?}: {:?}", + entity, + err + ); + None + } + } + }) + .collect::>() + }); + Self::step_impl(world, delta_time, &targets[..]); + } + + fn resolve_target( + components: &Components, + maybe_target: Option<&AnimTarget>, + anim_entity: Entity, + tweenable: &dyn Tweenable, + ) -> Result<(TypeId, ComponentId, AnimTargetKind, bool), TweeningError> { + let type_id = tweenable + .target_type_id() + .ok_or(TweeningError::UntypedTweenable)?; + if let Some(target) = maybe_target { + // Target explicitly specified with AnimTarget component + let component_id = match &target.kind { + AnimTargetKind::Component { .. } => components + .get_id(type_id) + .ok_or(TweeningError::ComponentNotRegistered(type_id))?, + AnimTargetKind::Resource => components + .get_resource_id(type_id) + .ok_or(TweeningError::ResourceNotRegistered(type_id))?, + AnimTargetKind::Asset { assets_type_id, .. } => components + .get_resource_id(*assets_type_id) + .ok_or(TweeningError::AssetNotRegistered(type_id))?, + }; + let is_retargetable = false; // explicit target + Ok((type_id, component_id, target.kind, is_retargetable)) + } else { + // Target implicitly self; this can only be a component target + let is_retargetable = true; + if let Some(component_id) = components.get_id(type_id) { + Ok(( + type_id, + component_id, + AnimTargetKind::Component { + entity: anim_entity, + }, + is_retargetable, + )) + } else { + // We can't implicitly target an asset without its AssetId + Err(TweeningError::ComponentNotRegistered(type_id)) + } + } + } + + fn step_impl( + world: &mut World, + delta_time: Duration, + anims: &[(Entity, TypeId, ComponentId, AnimTargetKind, bool)], + ) { + let mut to_remove = Vec::with_capacity(anims.len()); + world.resource_scope(|world, resolver: Mut| { + world.resource_scope( + |world, mut cycle_events: Mut>| { + world.resource_scope( + |world, mut anim_events: Mut>| { + let anim_comp_id = world.component_id::().unwrap(); + for ( + anim_entity, + target_type_id, + component_id, + anim_target, + is_retargetable, + ) in anims + { + let retain = match anim_target { + AnimTargetKind::Component { + entity: comp_entity, + } => { + let (mut entities, commands) = + world.entities_and_commands(); + let ret = if *anim_entity == *comp_entity { + // The TweenAnim animates another component on the same + // entity + let Ok([mut ent]) = entities.get_mut([*anim_entity]) + else { + continue; + }; + let Ok([anim, target]) = + ent.get_mut_by_id([anim_comp_id, *component_id]) + else { + continue; + }; + // SAFETY: We fetched the EntityMut from the component + // ID of + // TweenAnim + #[allow(unsafe_code)] + let mut anim = unsafe { anim.with_type::() }; + anim.step_self( + commands, + *anim_entity, + delta_time, + anim_target, + target, + target_type_id, + cycle_events.reborrow(), + anim_events.reborrow(), + ) + } else { + // The TweenAnim animates a component on a different + // entity + let Ok([mut anim, mut target]) = + entities.get_mut([*anim_entity, *comp_entity]) + else { + continue; + }; + let Some(mut anim) = anim.get_mut::() else { + continue; + }; + let Ok(target) = target.get_mut_by_id(*component_id) + else { + continue; + }; + anim.step_self( + commands, + *anim_entity, + delta_time, + anim_target, + target, + target_type_id, + cycle_events.reborrow(), + anim_events.reborrow(), + ) + }; + match ret { + Ok(res) => { + if res.needs_retarget { + assert!(res.retain); + if *is_retargetable { + //to_retarget.push(anim_entity); + //true + bevy::log::warn!("TODO: Multi-target tweenable sequence is not yet supported. Ensure the animation of the TweenAnim component on entity {:?} targets a single component type.", *anim_entity); + false + } else { + bevy::log::warn!("Multi-target tweenable sequence cannot be used with an explicit single target. Remove the AnimTarget component from entity {:?}, or ensure all tweenables in the sequence target the same component.", *anim_entity); + false + } + } else { + res.retain + } + } + Err(_) => false, + } + } + AnimTargetKind::Resource => resolver + .resolve_resource( + world, + target_type_id, + *component_id, + *anim_entity, + delta_time, + cycle_events.reborrow(), + anim_events.reborrow(), + ) + .unwrap_or_else(|err| { + bevy::log::error!( + "Deleting resource animation due to error: {err:?}" + ); + false + }), + AnimTargetKind::Asset { asset_id, .. } => resolver + .resolve_asset( + world, + target_type_id, + *component_id, + *asset_id, + *anim_entity, + delta_time, + cycle_events.reborrow(), + anim_events.reborrow(), + ) + .unwrap_or_else(|err| { + bevy::log::error!( + "Deleting asset animation due to error: {err:?}" + ); + false + }), + }; + + if !retain { + to_remove.push(*anim_entity); + } + } + }, + ); + }, + ); + }); + + for entity in to_remove.drain(..) { + world.entity_mut(entity).remove::(); + } + + world.flush(); + } + + #[allow(clippy::too_many_arguments)] + fn step_self( + &mut self, + mut commands: Commands, + anim_entity: Entity, + delta_time: Duration, + target_kind: &AnimTargetKind, + mut mut_untyped: MutUntyped, + target_type_id: &TypeId, + mut cycle_events: Mut>, + mut anim_events: Mut>, + ) -> Result { + let mut completed_events = Vec::with_capacity(8); + + // Sanity checks on fields which can be freely modified by the user + self.speed = self.speed.max(0.); + + // Retain completed animations only if requested + if self.tween_state == TweenState::Completed { + let ret = StepResult { + retain: !self.destroy_on_completion, + needs_retarget: false, + }; + return Ok(ret); + } + + // Skip paused animations (but retain them) + if self.playback_state == PlaybackState::Paused || self.speed <= 0. { + let ret = StepResult { + retain: true, + needs_retarget: false, + }; + return Ok(ret); + } + + // Scale delta time by this animation's speed. Reject negative speeds; use + // backward playback to play in reverse direction. + // Note: must use f64 for precision; f32 produces visible roundings. + let delta_time = delta_time.mul_f64(self.speed); + + // Step the tweenable animation + let mut notify_completed = || { + completed_events.push(CycleCompletedEvent { + anim_entity, + target: *target_kind, + }); + }; + let (state, needs_retarget) = self.tweenable.step( + anim_entity, + delta_time, + mut_untyped.reborrow(), + target_type_id, + &mut notify_completed, + ); + self.tween_state = state; + + // Send tween completed events once we reclaimed mut access to world and can get + // a Commands. + if !completed_events.is_empty() { + for event in completed_events.drain(..) { + // Send buffered event + cycle_events.send(event); + + // Trigger all entity-scoped observers + commands.trigger_targets(event, anim_entity); + } + } + + // Raise animation completed event + if state == TweenState::Completed { + let event: AnimCompletedEvent = AnimCompletedEvent { + anim_entity, + target: *target_kind, + }; + + // Send buffered event + anim_events.send(event); + + // Trigger all entity-scoped observers + commands.trigger_targets(event, anim_entity); + } + + let ret = StepResult { + retain: state == TweenState::Active || !self.destroy_on_completion, + needs_retarget, + }; + Ok(ret) + } + + /// Stop animation playback and rewind the animation. + /// + /// This changes the animator state to [`PlaybackState::Paused`] and rewinds + /// its tweenable. + /// + /// # Panics + /// + /// Like [`Tweenable::rewind()`], this panics if the current playback + /// direction is [`PlaybackDirection::Backward`] and the animation is + /// infinitely repeating. + pub fn stop(&mut self) { + self.playback_state = PlaybackState::Paused; + self.tweenable.rewind(); + self.tween_state = TweenState::Active; + } + + /// Get the tweenable describing this animation. + /// + /// To change the tweenable, use [`TweenAnim::set_tweenable()`]. + #[inline] + pub fn tweenable(&self) -> &dyn Tweenable { + self.tweenable.as_ref() + } + + /// Set a new animation description. + /// + /// Attempt to change the tweenable of an animation already spawned. + /// + /// If the tweenable is successfully swapped, this resets the + /// [`tween_state()`] to [`TweenState::Active`], even if the tweenable would + /// otherwise be completed _e.g._ because its current elapsed time is past + /// its total duration. Conversely, this doesn't update the target + /// component or asset, as this function doesn't have mutable access to + /// it. To force applying the new state to the target without stepping the + /// animation forward or backward, call one of the stepping functions like + /// [`TweenAnim::step_one()`] passing a delta time of [`Duration::ZERO`]. + /// + /// To ensure the old and new animations have the same elapsed time (for + /// example if they need to be synchronized, if they're variants of each + /// other), call [`set_elapsed()`] first on the input `tweenable`, with + /// the duration value of the old tweenable returned by [`elapsed()`]. + /// + /// ``` + /// # use std::time::Duration; + /// # use bevy::prelude::*; + /// # use bevy_tweening::*; + /// # fn make_tweenable() -> Tween { unimplemented!() } + /// fn my_system(mut anim: Single<&mut TweenAnim>) { + /// let mut tweenable = make_tweenable(); + /// let elapsed = anim.tweenable().elapsed(); + /// tweenable.set_elapsed(elapsed); + /// anim.set_tweenable(tweenable); + /// } + /// ``` + /// + /// # Returns + /// + /// On success, returns the previous tweenable which has been swapped out. + /// + /// [`tween_state()`]: Self::tween_state + /// [`set_elapsed()`]: crate::Tweenable::set_elapsed + /// [`elapsed()`]: crate::Tweenable::elapsed + /// [`step_one()`]: Self::step_one + pub fn set_tweenable(&mut self, tweenable: T) -> Result + where + T: Tweenable + 'static, + { + let mut old_tweenable: BoxedTweenable = Box::new(tweenable); + std::mem::swap(&mut self.tweenable, &mut old_tweenable); + // Reset tweening state, the new tweenable is at t=0 + self.tween_state = TweenState::Active; + Ok(old_tweenable) + } + + /// Get the tweening completion state. + /// + /// In general this is [`TweenState::Active`], unless the animation + /// completed and [`destroy_on_completion`] is `false`. + /// + /// [`destroy_on_completion`]: Self::destroy_on_completion + #[inline] + pub fn tween_state(&self) -> TweenState { + self.tween_state + } +} + +type ResourceResolver = Box< + dyn for<'w> Fn( + &mut World, + Entity, + &TypeId, + Duration, + Mut>, + Mut>, + ) -> Result + + Send + + Sync + + 'static, +>; + +type AssetResolver = Box< + dyn for<'w> Fn( + &mut World, + UntypedAssetId, + Entity, + &TypeId, + Duration, + Mut>, + Mut>, + ) -> Result + + Send + + Sync + + 'static, +>; + +/// Resolver for resources and assets. +/// +/// _This resource is largely an implementation detail. You can safely ignore +/// it._ +/// +/// Bevy doesn't provide a suitable untyped API to access resources and assets +/// at runtime without knowing their compile-time type. +/// - For resources, most of the API is in place, but unfortunately there's no +/// `World::resource_scope_untyped()` to temporarily extract a resource by ID +/// to allow concurrent mutability of the resource with other parts of the +/// [`World`], in particular the animation target. +/// - For assets, there's simply no untyped API. [`Assets`] doesn't allow +/// untyped asset access. +/// +/// To work around those limitations, this resolver resource contains +/// type-erased closures allowing to resolve an animation target definition into +/// a mutable pointer [`MutUntyped`] to that instance, to allow the animation +/// engine to apply the animation on it. +#[derive(Default, Resource)] +pub struct TweenResolver { + /// Resource resolver allowing to call `World::resource_scope()` to extract + /// that resource type form the `World` while in parallel accessing mutably + /// the animation entity itself. + resource_resolver: HashMap, + /// Asset resolver allowing to convert a pair of { untyped pointer to + /// `Assets`, untyped `AssetId` } into an untyped pointer to the asset A + /// itself. This is necessary because there's no UntypedAssets interface in + /// Bevy. The TypeId key must be the type of the `Assets` type itself. + /// The resolver is allowed to fail (return `None`), for example when the + /// asset ID doesn't reference a valid asset. + asset_resolver: HashMap, +} + +impl TweenResolver { + /// Register a resolver for the given resource type. + pub(crate) fn register_resource_resolver_for(&mut self, components: &Components) { + let resource_id = components.resource_id::().unwrap(); + let resolver = |world: &mut World, + entity: Entity, + target_type_id: &TypeId, + delta_time: Duration, + mut cycle_events: Mut>, + mut anim_events: Mut>| + -> Result { + // First, remove the resource R from the world so we can access it mutably in + // parallel of the TweenAnim + world.resource_scope(|world, resource: Mut| { + let target = AnimTargetKind::Resource; + + let (mut entities, commands) = world.entities_and_commands(); + + // Resolve the TweenAnim component + let Ok([mut ent]) = entities.get_mut([entity]) else { + return Err(TweeningError::EntityNotFound(entity)); + }; + let Some(mut anim) = ent.get_mut::() else { + return Err(TweeningError::MissingTweenAnim(ent.id())); + }; + + // Finally, step the TweenAnim and mutate the target + let ret = anim.step_self( + commands, + entity, + delta_time, + &target, + resource.into(), + target_type_id, + cycle_events.reborrow(), + anim_events.reborrow(), + ); + ret.map(|result| { + assert!(!result.needs_retarget, "Cannot use a multi-target sequence of tweenable animations with a resource target."); + result.retain + }) + }) + }; + self.resource_resolver + .entry(resource_id) + .or_insert(Box::new(resolver)); + } + + /// Register a resolver for the given asset type. + pub(crate) fn register_asset_resolver_for(&mut self, components: &Components) { + let resource_id = components.resource_id::>().unwrap(); + let resolver = |world: &mut World, + asset_id: UntypedAssetId, + entity: Entity, + target_type_id: &TypeId, + delta_time: Duration, + mut cycle_events: Mut>, + mut anim_events: Mut>| + -> Result { + let asset_id = asset_id.typed::(); + // First, remove the Assets from the world so we can access it mutably in + // parallel of the TweenAnim + world.resource_scope(|world, assets: Mut>| { + // Next, fetch the asset A itself from its Assets based on its asset ID + let Some(asset) = assets.filter_map_unchanged(|assets| assets.get_mut(asset_id)) + else { + return Err(TweeningError::InvalidAssetId(asset_id.into())); + }; + + let target = AnimTargetKind::Asset { + asset_id: asset_id.untyped(), + assets_type_id: TypeId::of::>(), + }; + + let (mut entities, commands) = world.entities_and_commands(); + + // Resolve the TweenAnim component + let Ok([mut ent]) = entities.get_mut([entity]) else { + return Err(TweeningError::EntityNotFound(entity)); + }; + let Some(mut anim) = ent.get_mut::() else { + return Err(TweeningError::MissingTweenAnim(ent.id())); + }; + + // Finally, step the TweenAnim and mutate the target + let ret = anim.step_self( + commands, + entity, + delta_time, + &target, + asset.into(), + target_type_id, + cycle_events.reborrow(), + anim_events.reborrow(), + ); + ret.map(|result| { + assert!(!result.needs_retarget, "Cannot use a multi-target sequence of tweenable animations with an asset target."); + result.retain + }) + }) + }; + self.asset_resolver + .entry(resource_id) + .or_insert(Box::new(resolver)); + } + + #[allow(clippy::too_many_arguments)] + #[inline] + pub(crate) fn resolve_resource( + &self, + world: &mut World, + target_type_id: &TypeId, + resource_id: ComponentId, + entity: Entity, + delta_time: Duration, + cycle_events: Mut>, + anim_events: Mut>, + ) -> Result { + let Some(resolver) = self.resource_resolver.get(&resource_id) else { + println!("ERROR: resource not registered {:?}", resource_id); + return Err(TweeningError::AssetResolverNotRegistered(resource_id)); + }; + resolver( + world, + entity, + target_type_id, + delta_time, + cycle_events, + anim_events, + ) + } + + #[allow(clippy::too_many_arguments)] + #[inline] + pub(crate) fn resolve_asset( + &self, + world: &mut World, + target_type_id: &TypeId, + resource_id: ComponentId, + untyped_asset_id: UntypedAssetId, + entity: Entity, + delta_time: Duration, + cycle_events: Mut>, + anim_events: Mut>, + ) -> Result { + let Some(resolver) = self.asset_resolver.get(&resource_id) else { + println!("ERROR: asset not registered {:?}", resource_id); + return Err(TweeningError::AssetResolverNotRegistered(resource_id)); + }; + resolver( + world, + untyped_asset_id, + entity, + target_type_id, + delta_time, + cycle_events, + anim_events, + ) + } +} + +pub(crate) struct StepResult { + /// Whether to retain the current [`TweenAnim`]? If `false`, the + /// [`TweenAnim`] is destroyed unless [`TweenAnim::destroy_on_completion`] + /// is `false`. + pub retain: bool, + /// Whether to recompute the new animation target and step again. This is + /// used by sequences when the animation target changes type in a sequence. + pub needs_retarget: bool, +} + +#[cfg(test)] +mod tests { + use std::{ + f32::consts::{FRAC_PI_2, TAU}, + marker::PhantomData, + }; + + use bevy::ecs::{change_detection::MaybeLocation, component::Tick}; + + use super::*; + use crate::test_utils::*; + + struct DummyLens { + start: f32, + end: f32, + } + + struct DummyLens2 { + start: i32, + end: i32, + } + + #[derive(Debug, Default, Clone, Copy, Component)] + struct DummyComponent { + value: f32, + } + + #[derive(Debug, Default, Clone, Copy, Component)] + struct DummyComponent2 { + value: i32, + } + + #[derive(Debug, Default, Clone, Copy, Resource)] + struct DummyResource { + value: f32, + } + + #[derive(Asset, Debug, Default, Reflect)] + struct DummyAsset { + value: f32, + } + + impl Lens for DummyLens { + fn lerp(&mut self, mut target: Mut, ratio: f32) { + target.value = self.start.lerp(self.end, ratio); + } + } + + impl Lens for DummyLens2 { + fn lerp(&mut self, mut target: Mut, ratio: f32) { + target.value = ((self.start as f32) * (1. - ratio) + (self.end as f32) * ratio) as i32; + } + } + + #[test] + fn dummy_lens_component() { + let mut c = DummyComponent::default(); + let mut l = DummyLens { start: 0., end: 1. }; + for r in [0_f32, 0.01, 0.3, 0.5, 0.9, 0.999, 1.] { + { + let mut added = Tick::new(0); + let mut last_changed = Tick::new(0); + let mut caller = MaybeLocation::caller(); + let mut target = Mut::new( + &mut c, + &mut added, + &mut last_changed, + Tick::new(0), + Tick::new(1), + caller.as_mut(), + ); + + l.lerp(target.reborrow(), r); + + assert!(target.is_changed()); + } + assert_approx_eq!(c.value, r); + } + } + + impl Lens for DummyLens { + fn lerp(&mut self, mut target: Mut, ratio: f32) { + target.value = self.start.lerp(self.end, ratio); + } + } + + #[test] + fn dummy_lens_resource() { + let mut res = DummyResource::default(); + let mut l = DummyLens { start: 0., end: 1. }; + for r in [0_f32, 0.01, 0.3, 0.5, 0.9, 0.999, 1.] { + { + let mut added = Tick::new(0); + let mut last_changed = Tick::new(0); + let mut caller = MaybeLocation::caller(); + let mut target = Mut::new( + &mut res, + &mut added, + &mut last_changed, + Tick::new(0), + Tick::new(0), + caller.as_mut(), + ); + l.lerp(target.reborrow(), r); + } + assert_approx_eq!(res.value, r); + } + } + + impl Lens for DummyLens { + fn lerp(&mut self, mut target: Mut, ratio: f32) { + target.value = self.start.lerp(self.end, ratio); + } + } + + #[test] + fn dummy_lens_asset() { + let mut assets = Assets::::default(); + let handle = assets.add(DummyAsset::default()); + + let mut l = DummyLens { start: 0., end: 1. }; + for r in [0_f32, 0.01, 0.3, 0.5, 0.9, 0.999, 1.] { + { + let mut added = Tick::new(0); + let mut last_changed = Tick::new(0); + let mut caller = MaybeLocation::caller(); + let asset = assets.get_mut(handle.id()).unwrap(); + let target = Mut::new( + asset, + &mut added, + &mut last_changed, + Tick::new(0), + Tick::new(0), + caller.as_mut(), + ); + l.lerp(target, r); + } + assert_approx_eq!(assets.get(handle.id()).unwrap().value, r); + } + } + + #[test] + fn repeat_count() { + let cycle_duration = Duration::from_millis(100); + + let repeat = RepeatCount::default(); + assert_eq!(repeat, RepeatCount::Finite(1)); + assert_eq!( + repeat.total_duration(cycle_duration), + TotalDuration::Finite(cycle_duration) + ); + + let repeat: RepeatCount = 3u32.into(); + assert_eq!(repeat, RepeatCount::Finite(3)); + assert_eq!( + repeat.total_duration(cycle_duration), + TotalDuration::Finite(cycle_duration * 3) + ); + + let duration = Duration::from_secs(5); + let repeat: RepeatCount = duration.into(); + assert_eq!(repeat, RepeatCount::For(duration)); + assert_eq!( + repeat.total_duration(cycle_duration), + TotalDuration::Finite(duration) + ); + + let repeat = RepeatCount::Infinite; + assert_eq!( + repeat.total_duration(cycle_duration), + TotalDuration::Infinite + ); + } + + #[test] + fn repeat_strategy() { + let strategy = RepeatStrategy::default(); + assert_eq!(strategy, RepeatStrategy::Repeat); + } + + #[test] + fn playback_direction() { + let tweening_direction = PlaybackDirection::default(); + assert_eq!(tweening_direction, PlaybackDirection::Forward); + } + + #[test] + fn playback_state() { + let mut state = PlaybackState::default(); + assert_eq!(state, PlaybackState::Playing); + state = !state; + assert_eq!(state, PlaybackState::Paused); + state = !state; + assert_eq!(state, PlaybackState::Playing); + } + + #[test] + fn ease_method() { + let ease = EaseMethod::default(); + assert!(matches!( ease, EaseMethod::EaseFunction(EaseFunction::Linear) )); @@ -730,233 +3007,929 @@ mod tests { assert_eq!(1., ease.sample(0.)); } + // TweenAnim::playback_state is entirely user-controlled; stepping animations + // won't change it. + #[test] + fn animation_playback_state() { + for state in [PlaybackState::Playing, PlaybackState::Paused] { + let tween = Tween::new::( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), + DummyLens { start: 0., end: 1. }, + ); + let mut env = TestEnv::::new(tween); + let mut anim = env.anim_mut().unwrap(); + anim.playback_state = state; + anim.destroy_on_completion = false; + + // Tick once + let dt = Duration::from_millis(100); + env.step_all(dt); + assert_eq!(env.anim().unwrap().tween_state(), TweenState::Active); + assert_eq!(env.anim().unwrap().playback_state, state); + + // Check elapsed + let elapsed = match state { + PlaybackState::Playing => dt, + PlaybackState::Paused => Duration::ZERO, + }; + assert_eq!(env.anim().unwrap().tweenable.elapsed(), elapsed); + + // Force playback, otherwise we can't complete + env.anim_mut().unwrap().playback_state = PlaybackState::Playing; + + // Even after completion, the playback state is untouched + env.step_all(Duration::from_secs(10) - elapsed); + assert_eq!(env.anim().unwrap().tween_state(), TweenState::Completed); + assert_eq!(env.anim().unwrap().playback_state, PlaybackState::Playing); + } + } + #[test] - fn animator_new() { - let tween = Tween::new( + fn animation_events() { + let tween = Tween::new::( EaseFunction::QuadraticInOut, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, - ); - let animator = Animator::::new(tween); - assert_eq!(animator.state, AnimatorState::default()); - assert_eq!(animator.tweenable().progress(), 0.); + ) + .with_repeat_count(2) + .with_cycle_completed_event(true); + let mut env = TestEnv::::new(tween); + + // Tick until one cycle is completed, but not the entire animation + let dt = Duration::from_millis(1200); + env.step_all(dt); + assert_eq!(env.anim().unwrap().tween_state(), TweenState::Active); + + // Check events + assert_eq!(env.event_count::(), 1); + assert_eq!(env.event_count::(), 0); + + // Tick until completion + let dt = Duration::from_millis(1000); + env.step_all(dt); + assert!(env.anim().is_none()); + + // Check events (note that we didn't clear previous events, so that's a + // cumulative count). + assert_eq!(env.event_count::(), 1); + assert_eq!(env.event_count::(), 1); } + #[derive(Debug, Resource)] + struct Count { + pub count: i32, + pub phantom: PhantomData, + pub phantom2: PhantomData, + } + + impl Default for Count { + fn default() -> Self { + Self { + count: 0, + phantom: PhantomData, + phantom2: PhantomData, + } + } + } + + struct GlobalMarker; + #[test] - fn animator_with_state() { - for state in [AnimatorState::Playing, AnimatorState::Paused] { - let tween = Tween::::new( - EaseFunction::QuadraticInOut, - Duration::from_secs(1), - DummyLens { start: 0., end: 1. }, - ); - let animator = Animator::new(tween).with_state(state); - assert_eq!(animator.state, state); - - // impl Debug - let debug_string = format!("{:?}", animator); - assert_eq!( - debug_string, - format!("Animator {{ state: {:?} }}", animator.state) - ); + fn animation_observe() { + let tween = Tween::new::( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), + DummyLens { start: 0., end: 1. }, + ) + .with_repeat_count(2) + .with_cycle_completed_event(true); + let mut env = TestEnv::::new(tween); + + env.world.init_resource::>(); + assert_eq!(env.world.resource::>().count, 0); + env.world + .init_resource::>(); + assert_eq!( + env.world + .resource::>() + .count, + 0 + ); + + fn observe_global( + _trigger: Trigger, + mut count: ResMut>, + ) { + count.count += 1; + } + env.world.add_observer(observe_global); + + fn observe_entity( + _trigger: Trigger, + mut count: ResMut>, + ) { + count.count += 1; } + env.world.entity_mut(env.entity).observe(observe_entity); + + // Tick until one cycle is completed, but not the entire animation + let dt = Duration::from_millis(1200); + env.step_all(dt); + assert_eq!(env.anim().unwrap().tween_state(), TweenState::Active); + + // Check observer system ran + assert_eq!(env.world.resource::>().count, 1); + assert_eq!( + env.world + .resource::>() + .count, + 1 + ); + + // Tick until completion + let dt = Duration::from_millis(1000); + env.step_all(dt); + assert!(env.anim().is_none()); + + // Check observer system ran (note that we didn't clear previous events, so + // that's a cumulative count). + assert_eq!(env.world.resource::>().count, 2); + assert_eq!( + env.world + .resource::>() + .count, + 2 + ); + } + + // #[test] + // fn animator_controls() { + // let tween = Tween::::new( + // EaseFunction::QuadraticInOut, + // Duration::from_secs(1), + // DummyLens { start: 0., end: 1. }, + // ); + // let mut animator = Animator::new(tween); + // assert_eq!(animator.state, AnimatorState::Playing); + // assert_approx_eq!(animator.tweenable().progress(), 0.); + + // animator.stop(); + // assert_eq!(animator.state, AnimatorState::Paused); + // assert_approx_eq!(animator.tweenable().progress(), 0.); + + // animator.tweenable_mut().set_progress(0.5); + // assert_eq!(animator.state, AnimatorState::Paused); + // assert_approx_eq!(animator.tweenable().progress(), 0.5); + + // animator.tweenable_mut().rewind(); + // assert_eq!(animator.state, AnimatorState::Paused); + // assert_approx_eq!(animator.tweenable().progress(), 0.); + + // animator.tweenable_mut().set_progress(0.5); + // animator.state = AnimatorState::Playing; + // assert_eq!(animator.state, AnimatorState::Playing); + // assert_approx_eq!(animator.tweenable().progress(), 0.5); + + // animator.tweenable_mut().rewind(); + // assert_eq!(animator.state, AnimatorState::Playing); + // assert_approx_eq!(animator.tweenable().progress(), 0.); + + // animator.stop(); + // assert_eq!(animator.state, AnimatorState::Paused); + // assert_approx_eq!(animator.tweenable().progress(), 0.); + // } + + #[test] + fn animation_speed() { + let tween = Tween::new::( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), + DummyLens { start: 0., end: 1. }, + ); + + let mut env = TestEnv::::new(tween); + + assert_approx_eq!(env.anim().unwrap().speed, 1.); // default speed + + env.anim_mut().unwrap().speed = 2.4; + assert_approx_eq!(env.anim().unwrap().speed, 2.4); + + env.step_all(Duration::from_millis(100)); + // Here we have enough precision for exact equality, but that may not always be + // the case for larger durations or speed values. + assert_eq!( + env.anim().unwrap().tweenable.elapsed(), + Duration::from_millis(240) + ); + + env.anim_mut().unwrap().speed = -1.; + env.step_all(Duration::from_millis(100)); + // Safety: invalid negative speed clamped to 0. + assert_eq!(env.anim().unwrap().speed, 0.); + // At zero speed, step is a no-op so elapse() didn't change + assert_eq!( + env.anim().unwrap().tweenable.elapsed(), + Duration::from_millis(240) + ); + } + + #[test] + fn animator_set_tweenable() { + let tween = Tween::new::( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), + DummyLens { start: 0., end: 1. }, + ); + let tween2 = Tween::new::( + EaseFunction::SmoothStep, + Duration::from_secs(2), + DummyLens { start: 2., end: 3. }, + ); + + let mut env = TestEnv::::new(tween); + env.anim_mut().unwrap().destroy_on_completion = false; + + let dt = Duration::from_millis(1500); + + env.step_all(dt); + assert_eq!(env.component().value, 1.); + assert_eq!(env.anim().unwrap().tween_state(), TweenState::Completed); + + // Swap tweens + let old_tweenable = env.anim_mut().unwrap().set_tweenable(tween2).unwrap(); + + assert_eq!(env.anim().unwrap().tween_state(), TweenState::Active); + // The elapsed is stored inside the tweenable + assert_eq!(old_tweenable.elapsed(), Duration::from_secs(1)); // capped at total_duration() + assert_eq!(env.anim().unwrap().tweenable.elapsed(), Duration::ZERO); + + env.step_all(dt); + assert!(env.component().value >= 2. && env.component().value <= 3.); } + // Currently multi-target sequences are not implemented. This _could_ work with + // implicit targets (so, multiple components on the same entity), but is a bit + // complex to implement with the current code. So leave that out for now, and + // test we assert if the user attempts it. The workaround is to create separate + // animations for each comopnent/target. Anyway multi-target sequence can't work + // with other target types, since they need an explicit TweenAnim, and we + // can't have more than one per entity. #[test] - fn animator_controls() { - let tween = Tween::::new( + #[should_panic( + expected = "TODO: Cannot use tweenable animations with different targets inside the same Sequence. Create separate animations for each target." + )] + fn seq_multi_target() { + let tween = Tween::new::( EaseFunction::QuadraticInOut, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, + ) + .then(Tween::new::( + EaseFunction::SmoothStep, + Duration::from_secs(1), + DummyLens2 { start: -5, end: 5 }, + )); + let mut env = TestEnv::::new(tween); + let entity = env.entity; + env.world + .entity_mut(entity) + .insert(DummyComponent2 { value: -42 }); + TweenAnim::step_one(&mut env.world, Duration::from_millis(1100), entity).unwrap(); + } + + // #[test] + // fn animator_set_target() { + // let tween = Tween::new::( + // EaseFunction::QuadraticInOut, + // Duration::from_secs(1), + // DummyLens { start: 0., end: 1. }, + // ); + // let mut env = TestEnv::::new(tween); + + // // Register our custom asset type + // env.world.init_resource::>(); + + // // Invalid ID + // { + // let entity = env.entity; + // let target = + // + // ComponentAnimTarget::new::(env.world.components(), + // entity).unwrap(); let err = env + // .animator_mut() + // .set_target(Entity::PLACEHOLDER, target.into()) + // .err() + // .unwrap(); + // let TweeningError::InvalidTweenId(err_id) = err else { + // panic!(); + // }; + // assert_eq!(err_id, Entity::PLACEHOLDER); + // } + + // // Spawn a second entity without any animation + // let entity1 = env.entity; + // let entity2 = env.world_mut().spawn(DummyComponent { value: 0. + // }).id(); assert_ne!(entity1, entity2); + // assert_eq!(env.component().value, 0.); + + // // Step the current target + // let dt = Duration::from_millis(100); + // env.step_all(dt); + // assert!(env.component().value > 0.); + // assert_eq!( + // env.world + // .entity(entity2) + // .get_components::<&DummyComponent>() + // .unwrap() + // .value, + // 0. + // ); + + // // Now retarget + // let id = env.entity; + // let target2 = + // ComponentAnimTarget::new::(env.world. + // components(), entity2).unwrap(); let target1 = + // env.animator_mut().set_target(id, target2.into()).unwrap(); + // assert!(target1.is_component()); + // let comp1 = target1.as_component().unwrap(); + // assert_eq!(comp1.entity, entity1); + // assert_eq!( + // comp1.component_id, + // env.world.component_id::().unwrap() + // ); + + // // Step the new target + // env.step_all(dt); + // assert!(env.component().value > 0.); + // assert!( + // env.world + // .entity(entity1) + // .get_components::<&DummyComponent>() + // .unwrap() + // .value + // > 0. + // ); + + // // Invalid target + // { + // let target3 = + // AssetAnimTarget::new(env.world.components(), + // Handle::::default().id()) .unwrap(); + // let err3 = env.animator_mut().set_target(id, target3.into()); + // assert!(err3.is_err()); + // let err3 = err3.err().unwrap(); + // let TweeningError::MismatchingTargetKind(oc, nc) = err3 else { + // panic!(); + // }; + // assert_eq!(oc, true); + // assert_eq!(nc, false); + // } + // } + + #[test] + fn anim_target_component() { + let mut env = TestEnv::::empty(); + let entity = env.world.spawn(Transform::default()).id(); + let tween = Tween::new::( + EaseFunction::Linear, + Duration::from_secs(1), + TransformPositionLens { + start: Vec3::ZERO, + end: Vec3::ONE, + }, ); - let mut animator = Animator::new(tween); - assert_eq!(animator.state, AnimatorState::Playing); - assert_approx_eq!(animator.tweenable().progress(), 0.); + let target = AnimTarget::component::(entity); + let anim_entity = env + .world + .spawn(( + TweenAnim::new(tween) + .with_speed(2.) + .with_destroy_on_completed(true), + target, + )) + .id(); + + // Step + assert!( + TweenAnim::step_one(&mut env.world, Duration::from_millis(100), anim_entity).is_ok() + ); + let tr = env.world.entity(entity).get::().unwrap(); + assert_eq!(tr.translation, Vec3::ONE * 0.2); + + // Complete + assert_eq!( + TweenAnim::step_many(&mut env.world, Duration::from_millis(400), &[anim_entity]), + 1 + ); + + // Destroyed on completion + assert!(env.world.entity(anim_entity).get::().is_none()); + } - animator.stop(); - assert_eq!(animator.state, AnimatorState::Paused); - assert_approx_eq!(animator.tweenable().progress(), 0.); + #[test] + fn anim_target_resource() { + let mut env = TestEnv::::empty(); + env.world.init_resource::(); + let tween = Tween::new::( + EaseFunction::Linear, + Duration::from_secs(1), + DummyLens { start: 0., end: 1. }, + ); + let target = AnimTarget::resource::(); + let anim_entity = env + .world + .spawn(( + TweenAnim::new(tween) + .with_speed(2.) + .with_destroy_on_completed(true), + target, + )) + .id(); + + // Step + assert!( + TweenAnim::step_one(&mut env.world, Duration::from_millis(100), anim_entity).is_ok() + ); + let res = env.world.resource::(); + assert_eq!(res.value, 0.2); - animator.tweenable_mut().set_progress(0.5); - assert_eq!(animator.state, AnimatorState::Paused); - assert_approx_eq!(animator.tweenable().progress(), 0.5); + // Complete + assert_eq!( + TweenAnim::step_many(&mut env.world, Duration::from_millis(400), &[anim_entity]), + 1 + ); - animator.tweenable_mut().rewind(); - assert_eq!(animator.state, AnimatorState::Paused); - assert_approx_eq!(animator.tweenable().progress(), 0.); + // Destroyed on completion + assert!(env.world.entity(anim_entity).get::().is_none()); + } - animator.tweenable_mut().set_progress(0.5); - animator.state = AnimatorState::Playing; - assert_eq!(animator.state, AnimatorState::Playing); - assert_approx_eq!(animator.tweenable().progress(), 0.5); + #[test] + fn anim_target_asset() { + let mut env = TestEnv::::empty(); + let mut assets = Assets::::default(); + let handle = assets.add(DummyAsset::default()); + env.world.insert_resource(assets); + let tween = Tween::new::( + EaseFunction::Linear, + Duration::from_secs(1), + DummyLens { start: 0., end: 1. }, + ); + let target = AnimTarget::asset::(&handle); + let anim_entity = env + .world + .spawn(( + TweenAnim::new(tween) + .with_speed(2.) + .with_destroy_on_completed(true), + target, + )) + .id(); + + // Step + assert!( + TweenAnim::step_one(&mut env.world, Duration::from_millis(100), anim_entity).is_ok() + ); + let assets = env.world.resource::>(); + let asset = assets.get(&handle).unwrap(); + assert_eq!(asset.value, 0.2); + + // Complete + assert_eq!( + TweenAnim::step_many(&mut env.world, Duration::from_millis(400), &[anim_entity]), + 1 + ); - animator.tweenable_mut().rewind(); - assert_eq!(animator.state, AnimatorState::Playing); - assert_approx_eq!(animator.tweenable().progress(), 0.); + // Destroyed on completion + assert!(env.world.entity(anim_entity).get::().is_none()); + } - animator.stop(); - assert_eq!(animator.state, AnimatorState::Paused); - assert_approx_eq!(animator.tweenable().progress(), 0.); + #[test] + fn animated_entity_commands_common() { + let dummy_tween = Tween::new::( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), + DummyLens { start: 0., end: 1. }, + ); + let mut env = TestEnv::::new(dummy_tween); + + let entity = env + .world + .commands() + .spawn(Transform::default()) + .move_to(Vec3::ONE, Duration::from_secs(1), EaseFunction::Linear) + .with_repeat_count(4) + .with_repeat_strategy(RepeatStrategy::MirroredRepeat) + .id(); + let entity2 = env + .world + .commands() + .spawn(Transform::default()) + .move_to(Vec3::ONE, Duration::from_secs(1), EaseFunction::Linear) + .with_repeat(4, RepeatStrategy::MirroredRepeat) + .into_inner() + .id(); + env.world.flush(); + + env.step_all(Duration::from_millis(3300)); + + let tr = env.world.entity(entity).get::().unwrap(); + assert_eq!(tr.translation, Vec3::ONE * 0.7); + let tr = env.world.entity(entity2).get::().unwrap(); + assert_eq!(tr.translation, Vec3::ONE * 0.7); } #[test] - fn animator_speed() { - let tween = Tween::::new( + fn animated_entity_commands_move_to() { + let dummy_tween = Tween::new::( EaseFunction::QuadraticInOut, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); + let mut env = TestEnv::::new(dummy_tween); - let mut animator = Animator::new(tween); - assert_approx_eq!(animator.speed(), 1.); // default speed + let entity = env + .world + .commands() + .spawn(Transform::default()) + .move_to(Vec3::ONE, Duration::from_secs(1), EaseFunction::Linear) + .id(); + env.world.flush(); - animator.set_speed(2.4); - assert_approx_eq!(animator.speed(), 2.4); + env.step_all(Duration::from_millis(300)); + + let tr = env.world.entity(entity).get::().unwrap(); + assert_eq!(tr.translation, Vec3::ONE * 0.3); + } - let tween = Tween::::new( + #[test] + fn animated_entity_commands_move_from() { + let dummy_tween = Tween::new::( EaseFunction::QuadraticInOut, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); + let mut env = TestEnv::::new(dummy_tween); + + let entity = env + .world + .commands() + .spawn(Transform::default()) + .move_from(Vec3::ONE, Duration::from_secs(1), EaseFunction::Linear) + .id(); + env.world.flush(); - let animator = Animator::new(tween).with_speed(3.5); - assert_approx_eq!(animator.speed(), 3.5); + env.step_all(Duration::from_millis(300)); + + let tr = env.world.entity(entity).get::().unwrap(); + assert_eq!(tr.translation, Vec3::ONE * 0.7); } #[test] - fn animator_set_tweenable() { - let tween = Tween::::new( + fn animated_entity_commands_scale_to() { + let dummy_tween = Tween::new::( EaseFunction::QuadraticInOut, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); - let mut animator = Animator::new(tween); + let mut env = TestEnv::::new(dummy_tween); + + let entity = env + .world + .commands() + .spawn(Transform::default()) + .scale_to(Vec3::ONE * 2., Duration::from_secs(1), EaseFunction::Linear) + .id(); + env.world.flush(); + + env.step_all(Duration::from_millis(300)); + + let tr = env.world.entity(entity).get::().unwrap(); + assert_eq!(tr.scale, Vec3::ONE * 1.3); + } - let tween2 = Tween::::new( + #[test] + fn animated_entity_commands_scale_from() { + let dummy_tween = Tween::new::( EaseFunction::QuadraticInOut, - Duration::from_secs(2), + Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); - animator.set_tweenable(tween2); + let mut env = TestEnv::::new(dummy_tween); + + let entity = env + .world + .commands() + .spawn(Transform::default()) + .scale_from(Vec3::ONE * 2., Duration::from_secs(1), EaseFunction::Linear) + .id(); + env.world.flush(); - assert_eq!(animator.tweenable().duration(), Duration::from_secs(2)); + env.step_all(Duration::from_millis(300)); + + let tr = env.world.entity(entity).get::().unwrap(); + assert_eq!(tr.scale, Vec3::ONE * 1.7); } - #[cfg(feature = "bevy_asset")] #[test] - fn asset_animator_new() { - let tween = Tween::::new( + fn animated_entity_commands_rotate_x() { + let dummy_tween = Tween::new::( EaseFunction::QuadraticInOut, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); - let animator = AssetAnimator::new(tween); - assert_eq!(animator.state, AnimatorState::default()); - let tween = animator; - assert_eq!(tween.tweenable().progress(), 0.); + let mut env = TestEnv::::new(dummy_tween); + + let entity = env + .world + .commands() + .spawn(Transform::default()) + .rotate_x(Duration::from_secs(1)) + .id(); + env.world.flush(); + + env.step_all(Duration::from_millis(1300)); + + let tr = env.world.entity(entity).get::().unwrap(); + assert_eq!(tr.rotation, Quat::from_rotation_x(TAU * 0.3)); } - #[cfg(feature = "bevy_asset")] #[test] - fn asset_animator_with_state() { - for state in [AnimatorState::Playing, AnimatorState::Paused] { - let tween = Tween::::new( - EaseFunction::QuadraticInOut, - Duration::from_secs(1), - DummyLens { start: 0., end: 1. }, - ); - let animator = AssetAnimator::new(tween).with_state(state); - assert_eq!(animator.state, state); - - // impl Debug - let debug_string = format!("{:?}", animator); - assert_eq!( - debug_string, - format!("AssetAnimator {{ state: {:?} }}", animator.state) - ); - } + fn animated_entity_commands_rotate_y() { + let dummy_tween = Tween::new::( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), + DummyLens { start: 0., end: 1. }, + ); + let mut env = TestEnv::::new(dummy_tween); + + let entity = env + .world + .commands() + .spawn(Transform::default()) + .rotate_y(Duration::from_secs(1)) + .id(); + env.world.flush(); + + env.step_all(Duration::from_millis(1300)); + + let tr = env.world.entity(entity).get::().unwrap(); + assert_eq!(tr.rotation, Quat::from_rotation_y(TAU * 0.3)); } - #[cfg(feature = "bevy_asset")] #[test] - fn asset_animator_controls() { - let tween: Tween = Tween::new( + fn animated_entity_commands_rotate_z() { + let dummy_tween = Tween::new::( EaseFunction::QuadraticInOut, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); - let mut animator = AssetAnimator::new(tween); - assert_eq!(animator.state, AnimatorState::Playing); - assert_approx_eq!(animator.tweenable().progress(), 0.); + let mut env = TestEnv::::new(dummy_tween); + + let entity = env + .world + .commands() + .spawn(Transform::default()) + .rotate_z(Duration::from_secs(1)) + .id(); + env.world.flush(); - animator.stop(); - assert_eq!(animator.state, AnimatorState::Paused); - assert_approx_eq!(animator.tweenable().progress(), 0.); + env.step_all(Duration::from_millis(1300)); - animator.tweenable_mut().set_progress(0.5); - assert_eq!(animator.state, AnimatorState::Paused); - assert_approx_eq!(animator.tweenable().progress(), 0.5); + let tr = env.world.entity(entity).get::().unwrap(); + assert_eq!(tr.rotation, Quat::from_rotation_z(TAU * 0.3)); + } - animator.tweenable_mut().rewind(); - assert_eq!(animator.state, AnimatorState::Paused); - assert_approx_eq!(animator.tweenable().progress(), 0.); + #[test] + fn animated_entity_commands_rotate_x_by() { + let dummy_tween = Tween::new::( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), + DummyLens { start: 0., end: 1. }, + ); + let mut env = TestEnv::::new(dummy_tween); - animator.tweenable_mut().set_progress(0.5); - animator.state = AnimatorState::Playing; - assert_eq!(animator.state, AnimatorState::Playing); - assert_approx_eq!(animator.tweenable().progress(), 0.5); + let entity = env + .world + .commands() + .spawn(Transform::default()) + .rotate_x_by(FRAC_PI_2, Duration::from_secs(1), EaseFunction::Linear) + .id(); + env.world.flush(); - animator.tweenable_mut().rewind(); - assert_eq!(animator.state, AnimatorState::Playing); - assert_approx_eq!(animator.tweenable().progress(), 0.); + env.step_all(Duration::from_millis(1300)); // 130% - animator.stop(); - assert_eq!(animator.state, AnimatorState::Paused); - assert_approx_eq!(animator.tweenable().progress(), 0.); + let tr = env.world.entity(entity).get::().unwrap(); + assert_eq!(tr.rotation, Quat::from_rotation_x(FRAC_PI_2)); // 100% } - #[cfg(feature = "bevy_asset")] #[test] - fn asset_animator_speed() { - let tween: Tween = Tween::new( + fn animated_entity_commands_rotate_y_by() { + let dummy_tween = Tween::new::( EaseFunction::QuadraticInOut, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); + let mut env = TestEnv::::new(dummy_tween); - let mut animator = AssetAnimator::new(tween); - assert_approx_eq!(animator.speed(), 1.); // default speed + let entity = env + .world + .commands() + .spawn(Transform::default()) + .rotate_y_by(FRAC_PI_2, Duration::from_secs(1), EaseFunction::Linear) + .id(); + env.world.flush(); - animator.set_speed(2.4); - assert_approx_eq!(animator.speed(), 2.4); + env.step_all(Duration::from_millis(1300)); // 130% - let tween: Tween = Tween::new( + let tr = env.world.entity(entity).get::().unwrap(); + assert_eq!(tr.rotation, Quat::from_rotation_y(FRAC_PI_2)); // 100% + } + + #[test] + fn animated_entity_commands_rotate_z_by() { + let dummy_tween = Tween::new::( EaseFunction::QuadraticInOut, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); + let mut env = TestEnv::::new(dummy_tween); + + let entity = env + .world + .commands() + .spawn(Transform::default()) + .rotate_z_by(FRAC_PI_2, Duration::from_secs(1), EaseFunction::Linear) + .id(); + env.world.flush(); - let animator = AssetAnimator::new(tween).with_speed(3.5); - assert_approx_eq!(animator.speed(), 3.5); + env.step_all(Duration::from_millis(1300)); // 130% + + let tr = env.world.entity(entity).get::().unwrap(); + assert_eq!(tr.rotation, Quat::from_rotation_z(FRAC_PI_2)); // 100% } - #[cfg(feature = "bevy_asset")] #[test] - fn asset_animator_set_tweenable() { - let tween: Tween = Tween::new( + fn resolver_resource() { + let dummy_tween = Tween::new::( EaseFunction::QuadraticInOut, Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); - let mut animator = AssetAnimator::new(tween); + let mut env = TestEnv::::new(dummy_tween); - let tween2 = Tween::new( + // Register the resource and create a TweenAnim for it + env.world.init_resource::(); + let tween = Tween::new::( EaseFunction::QuadraticInOut, - Duration::from_secs(2), + Duration::from_secs(1), + DummyLens { start: 0., end: 1. }, + ); + let entity = env.world.commands().spawn(TweenAnim::new(tween)).id(); + + // Ensure all commands are applied before starting the test + env.world.flush(); + + let delta_time = Duration::from_millis(200); + let resource_id = env.world.resource_id::().unwrap(); + + // Resource resolver not registered; fails + env.world + .resource_scope(|world, resolver: Mut| { + world.resource_scope( + |world, mut cycle_events: Mut>| { + world.resource_scope( + |world, mut anim_events: Mut>| { + assert!(resolver + .resolve_resource( + world, + &TypeId::of::(), + resource_id, + entity, + delta_time, + cycle_events.reborrow(), + anim_events.reborrow(), + ) + .is_err()); + }, + ); + }, + ); + }); + + // Register the resource resolver + env.world + .resource_scope(|world, mut resolver: Mut| { + resolver.register_resource_resolver_for::(world.components()); + }); + + // Resource resolver registered; succeeds + env.world + .resource_scope(|world, resolver: Mut| { + world.resource_scope( + |world, mut cycle_events: Mut>| { + world.resource_scope( + |world, mut anim_events: Mut>| { + assert!(resolver + .resolve_resource( + world, + &TypeId::of::(), + resource_id, + entity, + delta_time, + cycle_events.reborrow(), + anim_events.reborrow(), + ) + .unwrap()); + }, + ); + }, + ); + }); + } + + #[test] + fn resolver_asset() { + let dummy_tween = Tween::new::( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), DummyLens { start: 0., end: 1. }, ); - animator.set_tweenable(tween2); + let mut env = TestEnv::::new(dummy_tween); - assert_eq!(animator.tweenable().duration(), Duration::from_secs(2)); + // Register the asset and create a TweenAnim for it + let mut assets = Assets::::default(); + let handle = assets.add(DummyAsset::default()); + let untyped_asset_id = handle.id().untyped(); + env.world.insert_resource(assets); + let tween = Tween::new::( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), + DummyLens { start: 0., end: 1. }, + ); + let entity = env.world.commands().spawn(TweenAnim::new(tween)).id(); + + // Ensure all commands are applied before starting the test + env.world.flush(); + + let delta_time = Duration::from_millis(200); + let resource_id = env.world.resource_id::>().unwrap(); + + // Asset resolver not registered; fails + env.world + .resource_scope(|world, resolver: Mut| { + world.resource_scope( + |world, mut cycle_events: Mut>| { + world.resource_scope( + |world, mut anim_events: Mut>| { + assert!(resolver + .resolve_asset( + world, + &TypeId::of::(), + resource_id, + untyped_asset_id, + entity, + delta_time, + cycle_events.reborrow(), + anim_events.reborrow(), + ) + .is_err()); + }, + ); + }, + ); + }); + + // Register the asset resolver + env.world + .resource_scope(|world, mut resolver: Mut| { + resolver.register_asset_resolver_for::(world.components()); + }); + + // Asset resolver registered; succeeds + env.world + .resource_scope(|world, resolver: Mut| { + world.resource_scope( + |world, mut cycle_events: Mut>| { + world.resource_scope( + |world, mut anim_events: Mut>| { + assert!(resolver + .resolve_asset( + world, + &TypeId::of::(), + resource_id, + untyped_asset_id, + entity, + delta_time, + cycle_events.reborrow(), + anim_events.reborrow(), + ) + .unwrap()); + }, + ); + }, + ); + }); } } diff --git a/src/plugin.rs b/src/plugin.rs index db24ca5..418ac5c 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,161 +1,59 @@ -use bevy::{ecs::component::Mutable, prelude::*}; +use bevy::prelude::*; -#[cfg(feature = "bevy_asset")] -use crate::{tweenable::AssetTarget, AssetAnimator}; -use crate::{tweenable::ComponentTarget, Animator, AnimatorState, TweenCompleted}; +use crate::{AnimCompletedEvent, CycleCompletedEvent, TweenAnim, TweenResolver}; -/// Plugin to add systems related to tweening of common components and assets. +/// Plugin to register the 🍃 Bevy Tweening animation framework. /// -/// This plugin adds systems for a predefined set of components and assets, to -/// allow their respective animators to be updated each frame: -/// - [`Transform`] -/// - [`TextColor`] -/// - [`Node`] -/// - [`Sprite`] -/// - [`ColorMaterial`] +/// This plugin registers the common resources and events used by 🍃 Bevy +/// Tweening as well as the core animation system which steps all pending +/// tweenable animations. That system runs in the +/// [`AnimationSystem::AnimationUpdate`] system set, during the [`Update`] +/// schedule. /// -/// This ensures that all predefined lenses work as intended, as well as any -/// custom lens animating the same component or asset type. +/// ```no_run +/// use bevy::prelude::*; +/// use bevy_tweening::*; /// -/// For other components and assets, including custom ones, the relevant system -/// needs to be added manually by the application: -/// - For components, add [`component_animator_system::`] where `T: -/// Component` -/// - For assets, add [`asset_animator_system::`] where `T: Asset` -/// -/// This plugin is entirely optional. If you want more control, you can instead -/// add manually the relevant systems for the exact set of components and assets -/// actually animated. -/// -/// [`Transform`]: https://docs.rs/bevy/0.16.0/bevy/transform/components/struct.Transform.html -/// [`TextColor`]: https://docs.rs/bevy/0.16.0/bevy/text/struct.TextColor.html -/// [`Node`]: https://docs.rs/bevy/0.16.0/bevy/ui/struct.Node.html -/// [`Sprite`]: https://docs.rs/bevy/0.16.0/bevy/sprite/struct.Sprite.html -/// [`ColorMaterial`]: https://docs.rs/bevy/0.16.0/bevy/sprite/struct.ColorMaterial.html +/// App::default() +/// .add_plugins(DefaultPlugins) +/// .add_plugins(TweeningPlugin) +/// .run(); +/// ``` #[derive(Debug, Clone, Copy)] pub struct TweeningPlugin; impl Plugin for TweeningPlugin { fn build(&self, app: &mut App) { - app.add_event::().add_systems( - Update, - component_animator_system::.in_set(AnimationSystem::AnimationUpdate), - ); - - #[cfg(feature = "bevy_ui")] - app.add_systems( - Update, - component_animator_system::.in_set(AnimationSystem::AnimationUpdate), - ); - #[cfg(feature = "bevy_ui")] - app.add_systems( - Update, - component_animator_system::.in_set(AnimationSystem::AnimationUpdate), - ); - - #[cfg(feature = "bevy_sprite")] - app.add_systems( - Update, - component_animator_system::.in_set(AnimationSystem::AnimationUpdate), - ); - - #[cfg(all(feature = "bevy_sprite", feature = "bevy_asset"))] - app.add_systems( - Update, - asset_animator_system::> - .in_set(AnimationSystem::AnimationUpdate), - ); - - #[cfg(feature = "bevy_text")] - app.add_systems( - Update, - component_animator_system::.in_set(AnimationSystem::AnimationUpdate), - ); + app.init_resource::() + .add_event::() + .add_event::() + .add_systems( + Update, + animator_system.in_set(AnimationSystem::AnimationUpdate), + ); } } /// Label enum for the systems relating to animations #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, SystemSet)] +#[non_exhaustive] pub enum AnimationSystem { - /// Ticks animations + /// Steps all animations. This executes during the [`Update`] schedule. AnimationUpdate, } -/// Animator system for components. -/// -/// This system extracts all components of type `T` with an [`Animator`] -/// attached to the same entity, and tick the animator to animate the component. -pub fn component_animator_system>( - time: Res