jellyflow_runtime/runtime/viewport/
inertia.rs1use serde::{Deserialize, Serialize};
2
3use jellyflow_core::core::CanvasPoint;
4
5use crate::io::NodeGraphPanInertiaTuning;
6
7use super::transform::ViewportTransform;
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct ViewportPanInertiaRequest {
12 pub current: ViewportTransform,
13 pub initial_velocity_screen: CanvasPoint,
15 pub tuning: NodeGraphPanInertiaTuning,
16}
17
18impl ViewportPanInertiaRequest {
19 pub fn new(
20 current: ViewportTransform,
21 initial_velocity_screen: CanvasPoint,
22 tuning: NodeGraphPanInertiaTuning,
23 ) -> Self {
24 Self {
25 current,
26 initial_velocity_screen,
27 tuning,
28 }
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
34pub struct ViewportPanInertiaPlan {
35 pub from: ViewportTransform,
36 pub initial_velocity_screen: CanvasPoint,
37 pub duration_seconds: f32,
38 pub decay_per_s: f32,
39 pub min_speed: f32,
40}
41
42impl ViewportPanInertiaPlan {
43 pub fn frame_at(self, elapsed_seconds: f32) -> Option<ViewportPanInertiaFrame> {
44 if !elapsed_seconds.is_finite() {
45 return None;
46 }
47
48 let elapsed_seconds = elapsed_seconds.max(0.0);
49 let sample_elapsed = elapsed_seconds.min(self.duration_seconds);
50 let decay_factor = (-self.decay_per_s * sample_elapsed).exp();
51 let velocity_screen = scale_point(self.initial_velocity_screen, decay_factor);
52 let speed_screen = point_speed(velocity_screen);
53 let displacement_screen = scale_point(
54 self.initial_velocity_screen,
55 (1.0 - decay_factor) / self.decay_per_s,
56 );
57 let transform = ViewportTransform::new(
58 CanvasPoint {
59 x: self.from.pan.x + displacement_screen.x / self.from.zoom,
60 y: self.from.pan.y + displacement_screen.y / self.from.zoom,
61 },
62 self.from.zoom,
63 )?;
64
65 Some(ViewportPanInertiaFrame {
66 elapsed_seconds,
67 progress: if self.duration_seconds <= 0.0 {
68 1.0
69 } else {
70 (sample_elapsed / self.duration_seconds).clamp(0.0, 1.0)
71 },
72 speed_screen,
73 velocity_screen,
74 transform,
75 done: elapsed_seconds >= self.duration_seconds || speed_screen <= self.min_speed,
76 })
77 }
78
79 pub fn terminal_frame(self) -> Option<ViewportPanInertiaFrame> {
80 self.frame_at(self.duration_seconds)
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
86pub struct ViewportPanInertiaFrame {
87 pub elapsed_seconds: f32,
88 pub progress: f32,
89 pub speed_screen: f32,
90 pub velocity_screen: CanvasPoint,
91 pub transform: ViewportTransform,
92 pub done: bool,
93}
94
95pub fn plan_viewport_pan_inertia(
96 request: ViewportPanInertiaRequest,
97) -> Option<ViewportPanInertiaPlan> {
98 if !request.current.is_valid()
99 || !request.initial_velocity_screen.is_finite()
100 || !request.tuning.enabled
101 || !valid_positive(request.tuning.decay_per_s)
102 || !valid_positive(request.tuning.min_speed)
103 || !valid_positive(request.tuning.max_speed)
104 || request.tuning.max_speed <= request.tuning.min_speed
105 {
106 return None;
107 }
108
109 let initial_speed = point_speed(request.initial_velocity_screen);
110 if !initial_speed.is_finite() || initial_speed <= request.tuning.min_speed {
111 return None;
112 }
113
114 let initial_velocity_screen = clamp_velocity(
115 request.initial_velocity_screen,
116 initial_speed,
117 request.tuning.max_speed,
118 )?;
119 let clamped_speed = point_speed(initial_velocity_screen);
120 if clamped_speed <= request.tuning.min_speed {
121 return None;
122 }
123
124 let duration_seconds =
125 (clamped_speed / request.tuning.min_speed).ln() / request.tuning.decay_per_s;
126 if !duration_seconds.is_finite() || duration_seconds <= 0.0 {
127 return None;
128 }
129
130 Some(ViewportPanInertiaPlan {
131 from: request.current,
132 initial_velocity_screen,
133 duration_seconds,
134 decay_per_s: request.tuning.decay_per_s,
135 min_speed: request.tuning.min_speed,
136 })
137}
138
139fn clamp_velocity(velocity: CanvasPoint, speed: f32, max_speed: f32) -> Option<CanvasPoint> {
140 if speed <= max_speed {
141 return Some(velocity);
142 }
143
144 let scale = max_speed / speed;
145 let clamped = scale_point(velocity, scale);
146 clamped.is_finite().then_some(clamped)
147}
148
149fn scale_point(point: CanvasPoint, scale: f32) -> CanvasPoint {
150 CanvasPoint {
151 x: point.x * scale,
152 y: point.y * scale,
153 }
154}
155
156fn point_speed(point: CanvasPoint) -> f32 {
157 point.x.hypot(point.y)
158}
159
160fn valid_positive(value: f32) -> bool {
161 value.is_finite() && value > 0.0
162}