diff --git a/src/lens.rs b/src/lens.rs index 2f7104bdefb33dc58e1bda99259d7e006f8cb5f2..93fb9ef675b5cd171b97f7e0821c523952a1dd4e 100644 --- a/src/lens.rs +++ b/src/lens.rs @@ -1,12 +1,46 @@ //! Collection of predefined lenses for common Bevy components and assets. +//! +//! # Predefined lenses +//! +//! This module contains predefined lenses for common use cases. Those lenses are +//! entirely optional. They can be used if they fit your use case, to save some time, +//! but are not treated any differently from a custom user-provided lens. +//! +//! # Rotations +//! +//! Several rotation lenses are provided, with different properties. +//! +//! ## Shortest-path rotation +//! +//! The [`TransformRotationLens`] animates the [`rotation`] field of a [`Transform`] +//! component using [`Quat::slerp()`]. It inherits the properties of that method, and +//! in particular the fact it always finds the "shortest path" from start to end. This +//! is well suited for animating a rotation between two given directions, but will +//! provide unexpected results if you try to make an entity rotate around a given axis +//! for more than half a turn, as [`Quat::slerp()`] will then try to move "the other +//! way around". +//! +//! ## Angle-focused rotations +//! +//! Conversely, for cases where the rotation direction is important, like when trying +//! to do a full 360-degree turn, a series of angle-based interpolation lenses is +//! provided: +//! - [`TransformRotateXLens`] +//! - [`TransformRotateYLens`] +//! - [`TransformRotateZLens`] +//! - [`TransformRotateAxisLens`] +//! +//! [`rotation`]: https://docs.rs/bevy/0.6.0/bevy/transform/components/struct.Transform.html#structfield.rotation +//! [`Transform`]: https://docs.rs/bevy/0.6.0/bevy/transform/components/struct.Transform.html +//! [`Quat::slerp()`]: https://docs.rs/bevy/0.6.0/bevy/math/struct.Quat.html#method.slerp use bevy::prelude::*; /// A lens over a subset of a component. /// /// The lens takes a `target` component or asset from a query, as a mutable reference, -/// and animates (tweens) a subet of the fields of the component/asset based on the -/// linear ratio `ratio`, already sampled from the easing curve. +/// and animates (tweens) a subset of the fields of the component/asset based on the +/// linear ratio `ratio` in \[0:1\], already sampled from the easing curve. /// /// # Example /// @@ -83,8 +117,19 @@ impl Lens<Transform> for TransformPositionLens { /// A lens to manipulate the [`rotation`] field of a [`Transform`] component. /// +/// This lens interpolates the [`rotation`] field of a [`Transform`] component +/// from a `start` value to an `end` value using the spherical linear interpolation +/// provided by [`Quat::slerp()`]. This means the rotation always uses the shortest +/// path from `start` to `end`. In particular, this means it cannot make entities +/// do a full 360 degrees turn. Instead use [`TransformRotateXLens`] and similar +/// to interpolate the rotation angle around a given axis. +/// +/// See the [top-level `lens` module documentation] for a comparison of rotation lenses. +/// /// [`rotation`]: https://docs.rs/bevy/0.6.0/bevy/transform/components/struct.Transform.html#structfield.rotation /// [`Transform`]: https://docs.rs/bevy/0.6.0/bevy/transform/components/struct.Transform.html +/// [`Quat::slerp()`]: https://docs.rs/bevy/0.6.0/bevy/math/struct.Quat.html#method.slerp +/// [top-level `lens` module documentation]: crate::lens #[derive(Debug, Copy, Clone, PartialEq)] pub struct TransformRotationLens { /// Start value of the rotation. @@ -95,7 +140,117 @@ pub struct TransformRotationLens { impl Lens<Transform> for TransformRotationLens { fn lerp(&mut self, target: &mut Transform, ratio: f32) { - target.rotation = self.start.slerp(self.end, ratio); // FIXME - This slerps the shortest path only! https://docs.rs/bevy/latest/bevy/math/struct.Quat.html#method.slerp + target.rotation = self.start.slerp(self.end, ratio); + } +} + +/// A lens to rotate a [`Transform`] component around its local X axis. +/// +/// This lens interpolates the rotation angle of a [`Transform`] component from +/// a `start` value to an `end` value, for a rotation around the X axis. 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.6.0/bevy/transform/components/struct.Transform.html +/// [top-level `lens` module documentation]: crate::lens +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct TransformRotateXLens { + /// Start value of the rotation angle, in radians. + pub start: f32, + /// End value of the rotation angle, in radians. + pub end: f32, +} + +impl Lens<Transform> for TransformRotateXLens { + fn lerp(&mut self, target: &mut Transform, ratio: f32) { + let angle = self.start + (self.end - self.start) * ratio; + target.rotation = Quat::from_rotation_x(angle); + } +} + +/// A lens to rotate a [`Transform`] component around its local Y axis. +/// +/// This lens interpolates the rotation angle of a [`Transform`] component from +/// a `start` value to an `end` value, for a rotation around the Y axis. 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.6.0/bevy/transform/components/struct.Transform.html +/// [top-level `lens` module documentation]: crate::lens +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct TransformRotateYLens { + /// Start value of the rotation angle, in radians. + pub start: f32, + /// End value of the rotation angle, in radians. + pub end: f32, +} + +impl Lens<Transform> for TransformRotateYLens { + fn lerp(&mut self, target: &mut Transform, ratio: f32) { + let angle = self.start + (self.end - self.start) * ratio; + target.rotation = Quat::from_rotation_y(angle); + } +} + +/// A lens to rotate a [`Transform`] component around its local Z axis. +/// +/// This lens interpolates the rotation angle of a [`Transform`] component from +/// a `start` value to an `end` value, for a rotation around the Z axis. 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.6.0/bevy/transform/components/struct.Transform.html +/// [top-level `lens` module documentation]: crate::lens +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct TransformRotateZLens { + /// Start value of the rotation angle, in radians. + pub start: f32, + /// End value of the rotation angle, in radians. + pub end: f32, +} + +impl Lens<Transform> for TransformRotateZLens { + fn lerp(&mut self, target: &mut Transform, ratio: f32) { + let angle = self.start + (self.end - self.start) * ratio; + target.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 +/// a `start` value to an `end` value, for a rotation around a given axis. Unlike +/// [`TransformRotationLens`], it can produce an animation that rotates the entity +/// any number of turns around that axis. +/// +/// See the [top-level `lens` module documentation] for a comparison of rotation lenses. +/// +/// # Panics +/// +/// This method panics if the `axis` vector is not normalized. +/// +/// [`Transform`]: https://docs.rs/bevy/0.6.0/bevy/transform/components/struct.Transform.html +/// [top-level `lens` module documentation]: crate::lens +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct TransformRotateAxisLens { + /// The normalized rotation axis. + pub axis: Vec3, + /// Start value of the rotation angle, in radians. + pub start: f32, + /// End value of the rotation angle, in radians. + pub end: f32, +} + +impl Lens<Transform> for TransformRotateAxisLens { + fn lerp(&mut self, target: &mut Transform, ratio: f32) { + let angle = self.start + (self.end - self.start) * ratio; + target.rotation = Quat::from_axis_angle(self.axis, angle); } } @@ -196,6 +351,7 @@ impl Lens<Sprite> for SpriteColorLens { #[cfg(test)] mod tests { use super::*; + use std::f32::consts::TAU; #[test] fn text_color() { @@ -272,6 +428,136 @@ mod tests { assert!(transform.scale.abs_diff_eq(Vec3::ONE, 1e-5)); } + #[test] + fn transform_rotate_x() { + let mut lens = TransformRotateXLens { + start: 0., + end: 1440_f32.to_radians(), // 4 turns + }; + let mut transform = Transform::default(); + + for (index, ratio) in [0., 0.25, 0.5, 0.75, 1.].iter().enumerate() { + lens.lerp(&mut transform, *ratio); + assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); + if index == 1 || index == 3 { + // For odd-numbered turns, the opposite Quat is produced. This is equivalent in + // terms of rotation to the IDENTITY one, but numerically the w component is not + // the same so would fail an equality test. + assert!(transform + .rotation + .abs_diff_eq(Quat::from_xyzw(0., 0., 0., -1.), 1e-5)); + } else { + assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); + } + assert!(transform.scale.abs_diff_eq(Vec3::ONE, 1e-5)); + } + + lens.lerp(&mut transform, 0.1); + assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_rotation_x(0.1 * (4. * TAU)), 1e-5)); + assert!(transform.scale.abs_diff_eq(Vec3::ONE, 1e-5)); + } + + #[test] + fn transform_rotate_y() { + let mut lens = TransformRotateYLens { + start: 0., + end: 1440_f32.to_radians(), // 4 turns + }; + let mut transform = Transform::default(); + + for (index, ratio) in [0., 0.25, 0.5, 0.75, 1.].iter().enumerate() { + lens.lerp(&mut transform, *ratio); + assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); + if index == 1 || index == 3 { + // For odd-numbered turns, the opposite Quat is produced. This is equivalent in + // terms of rotation to the IDENTITY one, but numerically the w component is not + // the same so would fail an equality test. + assert!(transform + .rotation + .abs_diff_eq(Quat::from_xyzw(0., 0., 0., -1.), 1e-5)); + } else { + assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); + } + assert!(transform.scale.abs_diff_eq(Vec3::ONE, 1e-5)); + } + + lens.lerp(&mut transform, 0.1); + assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_rotation_y(0.1 * (4. * TAU)), 1e-5)); + assert!(transform.scale.abs_diff_eq(Vec3::ONE, 1e-5)); + } + + #[test] + fn transform_rotate_z() { + let mut lens = TransformRotateZLens { + start: 0., + end: 1440_f32.to_radians(), // 4 turns + }; + let mut transform = Transform::default(); + + for (index, ratio) in [0., 0.25, 0.5, 0.75, 1.].iter().enumerate() { + lens.lerp(&mut transform, *ratio); + assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); + if index == 1 || index == 3 { + // For odd-numbered turns, the opposite Quat is produced. This is equivalent in + // terms of rotation to the IDENTITY one, but numerically the w component is not + // the same so would fail an equality test. + assert!(transform + .rotation + .abs_diff_eq(Quat::from_xyzw(0., 0., 0., -1.), 1e-5)); + } else { + assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); + } + assert!(transform.scale.abs_diff_eq(Vec3::ONE, 1e-5)); + } + + lens.lerp(&mut transform, 0.1); + assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_rotation_z(0.1 * (4. * TAU)), 1e-5)); + assert!(transform.scale.abs_diff_eq(Vec3::ONE, 1e-5)); + } + + #[test] + fn transform_rotate_axis() { + let axis = Vec3::ONE.normalize(); + let mut lens = TransformRotateAxisLens { + axis, + start: 0., + end: 1440_f32.to_radians(), // 4 turns + }; + let mut transform = Transform::default(); + + for (index, ratio) in [0., 0.25, 0.5, 0.75, 1.].iter().enumerate() { + lens.lerp(&mut transform, *ratio); + assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); + if index == 1 || index == 3 { + // For odd-numbered turns, the opposite Quat is produced. This is equivalent in + // terms of rotation to the IDENTITY one, but numerically the w component is not + // the same so would fail an equality test. + assert!(transform + .rotation + .abs_diff_eq(Quat::from_xyzw(0., 0., 0., -1.), 1e-5)); + } else { + assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5)); + } + assert!(transform.scale.abs_diff_eq(Vec3::ONE, 1e-5)); + } + + lens.lerp(&mut transform, 0.1); + assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); + assert!(transform + .rotation + .abs_diff_eq(Quat::from_axis_angle(axis, 0.1 * (4. * TAU)), 1e-5)); + assert!(transform.scale.abs_diff_eq(Vec3::ONE, 1e-5)); + } + #[test] fn transform_scale() { let mut lens = TransformScaleLens {