use crate::bezier::{CubicBezierCurve, PathEvaluate, QuadBezier};
use crate::math;
use crate::poly::{CompoundPath, EllipticalArc, LineSegment, PathCommand, PathSegment};
use animato_core::{Easing, Playable, Update};
use animato_tween::{Loop, Tween};
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MotionPath {
inner: CompoundPath,
}
impl MotionPath {
pub fn new() -> Self {
Self::default()
}
pub fn from_commands(commands: &[PathCommand]) -> Self {
Self {
inner: CompoundPath::from_commands(commands),
}
}
pub fn from_svg(d: &str) -> Self {
Self {
inner: CompoundPath::from_svg(d),
}
}
pub fn try_from_svg(d: &str) -> Result<Self, crate::svg::SvgPathError> {
Ok(Self {
inner: CompoundPath::try_from_svg(d)?,
})
}
pub fn push_segment(mut self, segment: PathSegment) -> Self {
self.inner = self.inner.push_segment(segment);
self
}
pub fn segments(&self) -> &[PathSegment] {
self.inner.segments()
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl From<CompoundPath> for MotionPath {
fn from(inner: CompoundPath) -> Self {
Self { inner }
}
}
impl From<LineSegment> for MotionPath {
fn from(segment: LineSegment) -> Self {
Self {
inner: CompoundPath::new().push_segment(PathSegment::Line(segment)),
}
}
}
impl From<QuadBezier> for MotionPath {
fn from(segment: QuadBezier) -> Self {
Self {
inner: CompoundPath::new().push_segment(PathSegment::Quad(segment)),
}
}
}
impl From<CubicBezierCurve> for MotionPath {
fn from(segment: CubicBezierCurve) -> Self {
Self {
inner: CompoundPath::new().push_segment(PathSegment::Cubic(segment)),
}
}
}
impl From<EllipticalArc> for MotionPath {
fn from(segment: EllipticalArc) -> Self {
Self {
inner: CompoundPath::new().push_segment(PathSegment::Arc(segment)),
}
}
}
impl From<PathSegment> for MotionPath {
fn from(segment: PathSegment) -> Self {
Self {
inner: CompoundPath::new().push_segment(segment),
}
}
}
impl PathEvaluate for MotionPath {
fn position(&self, t: f32) -> [f32; 2] {
self.inner.position(t)
}
fn tangent(&self, t: f32) -> [f32; 2] {
self.inner.tangent(t)
}
fn arc_length(&self) -> f32 {
self.inner.arc_length()
}
}
#[derive(Clone, Debug)]
pub struct MotionPathTweenBuilder {
path: MotionPath,
duration: f32,
easing: Easing,
delay: f32,
time_scale: f32,
looping: Loop,
auto_rotate: bool,
start_offset: f32,
end_offset: f32,
}
impl MotionPathTweenBuilder {
pub fn new(path: impl Into<MotionPath>) -> Self {
Self {
path: path.into(),
duration: 1.0,
easing: Easing::Linear,
delay: 0.0,
time_scale: 1.0,
looping: Loop::Once,
auto_rotate: false,
start_offset: 0.0,
end_offset: 1.0,
}
}
pub fn duration(mut self, secs: f32) -> Self {
self.duration = secs.max(0.0);
self
}
pub fn easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
pub fn delay(mut self, secs: f32) -> Self {
self.delay = secs.max(0.0);
self
}
pub fn time_scale(mut self, scale: f32) -> Self {
self.time_scale = scale.max(0.0);
self
}
pub fn looping(mut self, mode: Loop) -> Self {
self.looping = mode;
self
}
pub fn auto_rotate(mut self, yes: bool) -> Self {
self.auto_rotate = yes;
self
}
pub fn start_offset(mut self, offset: f32) -> Self {
self.start_offset = math::clamp01(offset);
self
}
pub fn end_offset(mut self, offset: f32) -> Self {
self.end_offset = math::clamp01(offset);
self
}
pub fn build(self) -> MotionPathTween {
let tween = Tween::new(0.0_f32, 1.0)
.duration(self.duration)
.easing(self.easing)
.delay(self.delay)
.time_scale(self.time_scale)
.looping(self.looping)
.build();
MotionPathTween {
path: self.path,
tween,
auto_rotate: self.auto_rotate,
start_offset: self.start_offset,
end_offset: self.end_offset,
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MotionPathTween {
path: MotionPath,
tween: Tween<f32>,
auto_rotate: bool,
start_offset: f32,
end_offset: f32,
}
impl MotionPathTween {
#[allow(clippy::new_ret_no_self)]
pub fn new(path: impl Into<MotionPath>) -> MotionPathTweenBuilder {
MotionPathTweenBuilder::new(path)
}
pub fn from_tween(path: impl Into<MotionPath>, tween: Tween<f32>) -> Self {
Self {
path: path.into(),
tween,
auto_rotate: false,
start_offset: 0.0,
end_offset: 1.0,
}
}
pub fn path(&self) -> &MotionPath {
&self.path
}
pub fn tween(&self) -> &Tween<f32> {
&self.tween
}
pub fn tween_mut(&mut self) -> &mut Tween<f32> {
&mut self.tween
}
pub fn value(&self) -> [f32; 2] {
self.path.position(self.path_t())
}
pub fn rotation_deg(&self) -> f32 {
if self.auto_rotate {
self.path.rotation_deg(self.path_t())
} else {
0.0
}
}
pub fn path_progress(&self) -> f32 {
self.path_t()
}
pub fn is_auto_rotate(&self) -> bool {
self.auto_rotate
}
pub fn set_auto_rotate(&mut self, yes: bool) {
self.auto_rotate = yes;
}
pub fn set_offsets(&mut self, start: f32, end: f32) {
self.start_offset = math::clamp01(start);
self.end_offset = math::clamp01(end);
}
pub fn is_complete(&self) -> bool {
self.tween.is_complete()
}
pub fn reset(&mut self) {
self.tween.reset();
}
pub fn seek(&mut self, t: f32) {
self.tween.seek(t);
}
fn path_t(&self) -> f32 {
let progress = math::clamp01(self.tween.value());
math::clamp01(self.start_offset + (self.end_offset - self.start_offset) * progress)
}
}
impl Update for MotionPathTween {
fn update(&mut self, dt: f32) -> bool {
self.tween.update(dt)
}
}
impl Playable for MotionPathTween {
fn duration(&self) -> f32 {
Playable::duration(&self.tween)
}
fn reset(&mut self) {
MotionPathTween::reset(self);
}
fn seek_to(&mut self, progress: f32) {
Playable::seek_to(&mut self.tween, progress);
}
fn is_complete(&self) -> bool {
MotionPathTween::is_complete(self)
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn motion_path_from_cubic_evaluates() {
let curve = CubicBezierCurve::new([0.0, 0.0], [25.0, 50.0], [75.0, -50.0], [100.0, 0.0]);
let path = MotionPath::from(curve);
assert_eq!(path.position(0.0), [0.0, 0.0]);
assert_eq!(path.position(1.0), [100.0, 0.0]);
}
#[test]
fn motion_tween_updates_position() {
let line = LineSegment::new([0.0, 0.0], [100.0, 0.0]);
let mut tween = MotionPathTween::new(line).duration(1.0).build();
tween.update(0.5);
assert_eq!(tween.value(), [50.0, 0.0]);
}
#[test]
fn offsets_trim_path() {
let line = LineSegment::new([0.0, 0.0], [100.0, 0.0]);
let mut tween = MotionPathTween::new(line)
.duration(1.0)
.start_offset(0.25)
.end_offset(0.75)
.build();
assert_eq!(tween.value(), [25.0, 0.0]);
tween.update(1.0);
assert_eq!(tween.value(), [75.0, 0.0]);
}
#[test]
fn auto_rotate_uses_path_heading() {
let line = LineSegment::new([0.0, 0.0], [0.0, 100.0]);
let tween = MotionPathTween::new(line).auto_rotate(true).build();
assert!((tween.rotation_deg() - 90.0).abs() < 0.001);
}
#[test]
fn motion_path_constructors_and_accessors_work() {
let mut path = MotionPath::new();
assert!(path.is_empty());
assert_eq!(path.len(), 0);
assert_eq!(path.position(0.5), [0.0, 0.0]);
path = path.push_segment(PathSegment::Line(LineSegment::new([0.0, 0.0], [10.0, 0.0])));
assert_eq!(path.len(), 1);
assert_eq!(path.segments().len(), 1);
assert_eq!(path.position(0.5), [5.0, 0.0]);
let from_commands = MotionPath::from_commands(&[
PathCommand::MoveTo([0.0, 0.0]),
PathCommand::LineTo([10.0, 0.0]),
]);
assert_eq!(from_commands.len(), 1);
let from_svg = MotionPath::from_svg("M0 0 L10 0");
assert_eq!(from_svg.len(), 1);
assert!(MotionPath::try_from_svg("M0 0 L10 0").is_ok());
assert!(MotionPath::try_from_svg("M0 0 C").is_err());
}
#[test]
fn motion_path_from_each_segment_type() {
let paths = [
MotionPath::from(LineSegment::new([0.0, 0.0], [10.0, 0.0])),
MotionPath::from(QuadBezier::new([0.0, 0.0], [5.0, 5.0], [10.0, 0.0])),
MotionPath::from(CubicBezierCurve::new(
[0.0, 0.0],
[3.0, 5.0],
[7.0, -5.0],
[10.0, 0.0],
)),
MotionPath::from(EllipticalArc::from_svg(
[0.0, 0.0],
[10.0, 10.0],
0.0,
false,
true,
[10.0, 0.0],
)),
MotionPath::from(PathSegment::Line(LineSegment::new([0.0, 0.0], [10.0, 0.0]))),
];
for path in paths {
assert_eq!(path.len(), 1);
assert!(path.position(0.5)[0].is_finite());
assert!(path.tangent(0.5)[0].is_finite());
assert!(path.arc_length() >= 0.0);
}
}
#[test]
fn builder_clamps_values_and_exposes_state() {
let line = LineSegment::new([0.0, 0.0], [100.0, 0.0]);
let mut tween = MotionPathTween::new(line)
.duration(-1.0)
.delay(-1.0)
.time_scale(-1.0)
.looping(Loop::Forever)
.easing(Easing::EaseInQuad)
.start_offset(-1.0)
.end_offset(2.0)
.build();
assert!(!tween.is_auto_rotate());
assert_eq!(tween.rotation_deg(), 0.0);
assert_eq!(tween.path_progress(), 1.0);
assert_eq!(tween.value(), [100.0, 0.0]);
assert_eq!(Playable::duration(&tween), f32::INFINITY);
assert_eq!(tween.path().len(), 1);
assert!(!tween.tween().is_complete());
assert!(!tween.update(0.0));
assert!(tween.is_complete());
tween.tween_mut().reset();
tween.seek(0.25);
assert_eq!(tween.path_progress(), 1.0);
tween.reset();
assert!(!tween.is_complete());
assert!(!tween.update(0.0));
assert!(tween.is_complete());
}
#[test]
fn from_tween_and_playable_methods_work() {
let line = LineSegment::new([0.0, 0.0], [100.0, 0.0]);
let base = Tween::new(0.0_f32, 1.0).duration(1.0).build();
let mut tween = MotionPathTween::from_tween(line, base);
assert_eq!(Playable::duration(&tween), 1.0);
Playable::seek_to(&mut tween, 0.5);
assert_eq!(tween.value(), [50.0, 0.0]);
assert!(!Playable::is_complete(&tween));
assert!(Playable::as_any(&tween).is::<MotionPathTween>());
assert!(Playable::as_any_mut(&mut tween).is::<MotionPathTween>());
Playable::reset(&mut tween);
assert_eq!(tween.value(), [0.0, 0.0]);
}
}