Skip to main content

jellyflow_runtime/runtime/viewport/
animation.rs

1use serde::{Deserialize, Serialize};
2
3use jellyflow_core::core::CanvasPoint;
4
5use super::transform::ViewportTransform;
6
7/// Built-in easing modes for renderer-neutral viewport animation.
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ViewportAnimationEasing {
11    #[default]
12    CubicInOut,
13    Linear,
14}
15
16impl ViewportAnimationEasing {
17    fn sample(self, progress: f32) -> f32 {
18        match self {
19            Self::CubicInOut => cubic_in_out(progress),
20            Self::Linear => progress.clamp(0.0, 1.0),
21        }
22    }
23}
24
25/// Renderer-neutral viewport animation options.
26#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
27pub struct ViewportAnimationOptions {
28    /// Animation duration in seconds.
29    pub duration_seconds: f32,
30    /// Easing mode used when sampling frames.
31    pub easing: ViewportAnimationEasing,
32}
33
34impl ViewportAnimationOptions {
35    pub fn new(duration_seconds: f32) -> Self {
36        Self {
37            duration_seconds,
38            easing: ViewportAnimationEasing::default(),
39        }
40    }
41
42    pub fn with_easing(mut self, easing: ViewportAnimationEasing) -> Self {
43        self.easing = easing;
44        self
45    }
46}
47
48/// Request to plan a viewport animation from one transform to another.
49#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
50pub struct ViewportAnimationRequest {
51    pub from: ViewportTransform,
52    pub to: ViewportTransform,
53    pub options: ViewportAnimationOptions,
54}
55
56impl ViewportAnimationRequest {
57    pub fn new(
58        from: ViewportTransform,
59        to: ViewportTransform,
60        options: ViewportAnimationOptions,
61    ) -> Self {
62        Self { from, to, options }
63    }
64}
65
66/// Deterministic viewport animation plan.
67#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
68pub struct ViewportAnimationPlan {
69    pub from: ViewportTransform,
70    pub to: ViewportTransform,
71    pub duration_seconds: f32,
72    pub easing: ViewportAnimationEasing,
73}
74
75impl ViewportAnimationPlan {
76    /// Samples this plan at an elapsed time in seconds.
77    pub fn frame_at(self, elapsed_seconds: f32) -> Option<ViewportAnimationFrame> {
78        if !elapsed_seconds.is_finite() {
79            return None;
80        }
81
82        let elapsed_seconds = elapsed_seconds.max(0.0);
83        let progress = if self.duration_seconds <= 0.0 {
84            1.0
85        } else {
86            (elapsed_seconds / self.duration_seconds).clamp(0.0, 1.0)
87        };
88        let eased_progress = self.easing.sample(progress);
89        let transform = interpolate_transform(self.from, self.to, eased_progress)?;
90
91        Some(ViewportAnimationFrame {
92            elapsed_seconds,
93            progress,
94            eased_progress,
95            transform,
96            done: progress >= 1.0,
97        })
98    }
99
100    pub fn is_immediate(self) -> bool {
101        self.duration_seconds <= 0.0
102    }
103}
104
105/// Sampled viewport animation frame.
106#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
107pub struct ViewportAnimationFrame {
108    pub elapsed_seconds: f32,
109    pub progress: f32,
110    pub eased_progress: f32,
111    pub transform: ViewportTransform,
112    pub done: bool,
113}
114
115pub fn plan_viewport_animation(
116    from: ViewportTransform,
117    to: ViewportTransform,
118    duration_seconds: f32,
119) -> Option<ViewportAnimationPlan> {
120    plan_viewport_animation_with_options(ViewportAnimationRequest::new(
121        from,
122        to,
123        ViewportAnimationOptions::new(duration_seconds),
124    ))
125}
126
127pub fn plan_viewport_animation_with_options(
128    request: ViewportAnimationRequest,
129) -> Option<ViewportAnimationPlan> {
130    if !request.from.is_valid()
131        || !request.to.is_valid()
132        || !request.options.duration_seconds.is_finite()
133    {
134        return None;
135    }
136
137    Some(ViewportAnimationPlan {
138        from: request.from,
139        to: request.to,
140        duration_seconds: request.options.duration_seconds.max(0.0),
141        easing: request.options.easing,
142    })
143}
144
145fn interpolate_transform(
146    from: ViewportTransform,
147    to: ViewportTransform,
148    progress: f32,
149) -> Option<ViewportTransform> {
150    ViewportTransform::new(
151        CanvasPoint {
152            x: lerp(from.pan.x, to.pan.x, progress),
153            y: lerp(from.pan.y, to.pan.y, progress),
154        },
155        lerp(from.zoom, to.zoom, progress),
156    )
157}
158
159fn lerp(from: f32, to: f32, progress: f32) -> f32 {
160    from + (to - from) * progress
161}
162
163fn cubic_in_out(t: f32) -> f32 {
164    let t = t.clamp(0.0, 1.0);
165    let doubled = t * 2.0;
166    if doubled <= 1.0 {
167        doubled * doubled * doubled / 2.0
168    } else {
169        let shifted = doubled - 2.0;
170        (shifted * shifted * shifted + 2.0) / 2.0
171    }
172}