1use std::{cell::Cell, rc::Rc};
28
29use cranpose_core::{RuntimeHandle, with_current_composer};
30use cranpose_macros::composable;
31
32use crate::material::GlassDeformation;
33
34const STRETCH_PER_SPEED: f32 = 3.2e-4;
35const STRETCH_PER_ACCEL: f32 = 3.5e-5;
36pub const STRETCH_MIN: f32 = 0.78;
39pub const STRETCH_MAX: f32 = 1.50;
41const BULGE_PER_ACCELERATION: f32 = 4.5e-4;
42pub const BULGE_MAX: f32 = 8.0;
44const ATTACK_TAU: f32 = 0.03;
45const RELEASE_TAU: f32 = 0.11;
46const AXIS_MIN_SPEED: f32 = 60.0;
47const REST_SPEED: f32 = 1.0;
48const REST_STRETCH: f32 = 5.0e-4;
49const REST_BULGE: f32 = 1.0e-2;
50const DT_MIN: f32 = 1.0 / 1000.0;
51const DT_MAX: f32 = 1.0 / 15.0;
52const TELEPORT_SPEED: f32 = 30_000.0;
53
54#[derive(Clone, Copy, Debug, PartialEq)]
56pub struct LiquidPose {
57 pub stretch: f32,
59 pub ortho: f32,
61 pub axis: (f32, f32),
64 pub bulge_amplitude: f32,
66 pub bulge_direction: f32,
68 pub speed: f32,
71}
72
73impl Default for LiquidPose {
74 fn default() -> Self {
75 Self {
76 stretch: 1.0,
77 ortho: 1.0,
78 axis: (1.0, 0.0),
79 bulge_amplitude: 0.0,
80 bulge_direction: 0.0,
81 speed: 0.0,
82 }
83 }
84}
85
86impl LiquidPose {
87 pub fn deformation(&self) -> GlassDeformation {
92 GlassDeformation::incompressible(self.axis, self.stretch)
93 }
94
95 pub fn energy(&self) -> f32 {
98 (self.speed / 1100.0).clamp(0.0, 1.0)
99 }
100}
101
102pub struct LiquidDynamics {
105 runtime: RuntimeHandle,
106 last_nanos: Cell<Option<u64>>,
107 last_pos: Cell<Option<(f32, f32)>>,
108 velocity: Cell<(f32, f32)>,
109 stretch: Cell<f32>,
110 bulge_vector: Cell<(f32, f32)>,
111 speed: Cell<f32>,
112 axis: Cell<(f32, f32)>,
113 pose: Cell<LiquidPose>,
114}
115
116impl LiquidDynamics {
117 pub fn new(runtime: RuntimeHandle) -> Self {
119 Self {
120 runtime,
121 last_nanos: Cell::new(None),
122 last_pos: Cell::new(None),
123 velocity: Cell::new((0.0, 0.0)),
124 stretch: Cell::new(1.0),
125 bulge_vector: Cell::new((0.0, 0.0)),
126 speed: Cell::new(0.0),
127 axis: Cell::new((1.0, 0.0)),
128 pose: Cell::new(LiquidPose::default()),
129 }
130 }
131
132 pub fn reset(&self) {
135 self.last_nanos.set(None);
136 self.last_pos.set(None);
137 self.velocity.set((0.0, 0.0));
138 self.stretch.set(1.0);
139 self.bulge_vector.set((0.0, 0.0));
140 self.speed.set(0.0);
141 self.pose.set(LiquidPose {
142 axis: self.axis.get(),
143 ..LiquidPose::default()
144 });
145 }
146
147 pub fn update(&self, pos: (f32, f32)) -> LiquidPose {
152 redraw_until_rest(self.integrate(pos))
153 }
154
155 fn integrate(&self, pos: (f32, f32)) -> LiquidPose {
156 let Some(now) = self.runtime.last_frame_time_nanos() else {
157 self.last_pos.set(Some(pos));
158 return self.pose.get();
159 };
160 match self.last_nanos.get() {
161 Some(last) if last == now => {
162 self.last_pos.set(Some(pos));
163 self.pose.get()
164 }
165 Some(last) => {
166 let dt = (now.saturating_sub(last)) as f32 / 1_000_000_000.0;
167 self.last_nanos.set(Some(now));
168 self.advance(pos, dt)
169 }
170 None => {
171 self.last_nanos.set(Some(now));
172 self.last_pos.set(Some(pos));
173 self.pose.get()
174 }
175 }
176 }
177
178 pub fn advance(&self, pos: (f32, f32), dt: f32) -> LiquidPose {
181 let Some(last_pos) = self.last_pos.get() else {
182 self.last_pos.set(Some(pos));
183 return self.pose.get();
184 };
185 let dt = dt.clamp(DT_MIN, DT_MAX);
186 let delta = (pos.0 - last_pos.0, pos.1 - last_pos.1);
187 self.last_pos.set(Some(pos));
188
189 let velocity = (delta.0 / dt, delta.1 / dt);
190 let raw_speed = (velocity.0 * velocity.0 + velocity.1 * velocity.1).sqrt();
191 if raw_speed > TELEPORT_SPEED {
192 self.velocity.set((0.0, 0.0));
193 return self.pose.get();
194 }
195
196 self.advance_velocity(velocity, dt)
197 }
198
199 fn advance_velocity(&self, velocity: (f32, f32), dt: f32) -> LiquidPose {
200 let dt = dt.clamp(DT_MIN, DT_MAX);
201 let raw_speed = (velocity.0 * velocity.0 + velocity.1 * velocity.1).sqrt();
202 let previous_velocity = self.velocity.get();
203 self.velocity.set(velocity);
204 let accel = (
205 (velocity.0 - previous_velocity.0) / dt,
206 (velocity.1 - previous_velocity.1) / dt,
207 );
208
209 let mut axis = self.axis.get();
210 if raw_speed > AXIS_MIN_SPEED {
211 axis = (velocity.0 / raw_speed, velocity.1 / raw_speed);
212 self.axis.set(axis);
213 }
214 let accel_along = accel.0 * axis.0 + accel.1 * axis.1;
215
216 let target_stretch = (1.0 + STRETCH_PER_SPEED * raw_speed
217 - STRETCH_PER_ACCEL * accel_along)
218 .clamp(STRETCH_MIN, STRETCH_MAX);
219 let signed_bulge = (-BULGE_PER_ACCELERATION * accel_along).clamp(-BULGE_MAX, BULGE_MAX);
220 let target_bulge = (axis.0 * signed_bulge, axis.1 * signed_bulge);
221
222 let follow = |current: f32, target: f32, neutral: f32| {
223 let tau = if (target - neutral).abs() > (current - neutral).abs() {
224 ATTACK_TAU
225 } else {
226 RELEASE_TAU
227 };
228 current + (target - current) * (1.0 - (-dt / tau).exp())
229 };
230 let stretch = follow(self.stretch.get(), target_stretch, 1.0);
231 let current_bulge = self.bulge_vector.get();
232 let current_bulge_length = current_bulge.0.hypot(current_bulge.1);
233 let target_bulge_length = target_bulge.0.hypot(target_bulge.1);
234 let bulge_tau = if target_bulge_length > current_bulge_length {
235 ATTACK_TAU
236 } else {
237 RELEASE_TAU
238 };
239 let bulge_follow = 1.0 - (-dt / bulge_tau).exp();
240 let bulge_vector = (
241 current_bulge.0 + (target_bulge.0 - current_bulge.0) * bulge_follow,
242 current_bulge.1 + (target_bulge.1 - current_bulge.1) * bulge_follow,
243 );
244 let speed = follow(self.speed.get(), raw_speed, 0.0);
245 let at_rest = raw_speed <= REST_SPEED
246 && speed <= REST_SPEED
247 && (stretch - 1.0).abs() <= REST_STRETCH
248 && bulge_vector.0.hypot(bulge_vector.1) <= REST_BULGE;
249 let (stretch, bulge_vector, speed) = if at_rest {
250 (1.0, (0.0, 0.0), 0.0)
251 } else {
252 (stretch, bulge_vector, speed)
253 };
254 let bulge = bulge_vector.0.hypot(bulge_vector.1);
255 self.stretch.set(stretch);
256 self.bulge_vector.set(bulge_vector);
257 self.speed.set(speed);
258
259 let bulge_direction = if bulge > 1.0e-4 {
260 bulge_vector.1.atan2(bulge_vector.0)
261 } else {
262 axis.1.atan2(axis.0)
263 };
264
265 let pose = LiquidPose {
266 stretch,
267 ortho: 1.0 / stretch,
268 axis,
269 bulge_amplitude: bulge,
270 bulge_direction,
271 speed,
272 };
273 self.pose.set(pose);
274 pose
275 }
276
277 pub fn pose(&self) -> LiquidPose {
279 self.pose.get()
280 }
281}
282
283fn redraw_until_rest(pose: LiquidPose) -> LiquidPose {
284 if pose.stretch != 1.0 || pose.bulge_amplitude != 0.0 || pose.speed != 0.0 {
285 cranpose_ui::request_current_draw_redraw();
286 }
287 pose
288}
289
290#[composable]
292#[track_caller]
293pub fn rememberLiquidDynamics() -> Rc<LiquidDynamics> {
294 let caller = cranpose_core::caller_location_key();
295 with_current_composer(|composer| {
296 let runtime = composer.runtime_handle();
297 composer
298 .remember_at(caller, move || Rc::new(LiquidDynamics::new(runtime)))
299 .with(Rc::clone)
300 })
301}
302
303#[cfg(test)]
304#[path = "tests/dynamics_tests.rs"]
305mod tests;