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