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 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);
}
}