1use frus_core::animation::{Simulation, SpringDescription, SpringSimulation, Tolerance};
16
17use crate::overscroll::GlowEdge;
18
19const MAX_STRETCH: f32 = 1.0;
21const INTENSITY: f32 = 0.016;
23const EXPONENTIAL_SCALAR: f32 = std::f32::consts::E / 0.33;
25const IMPACT_FRICTION: f32 = 1.0 / 3000.0;
27const MAX_IMPACT_VELOCITY: f32 = 1.25;
29const NATURAL_FREQUENCY: f32 = 24.657;
31const DAMPING_RATIO: f32 = 0.98;
32const TIME_CORRECTION: f32 = 0.8;
35
36fn spring() -> SpringDescription {
38 let stiffness = NATURAL_FREQUENCY * NATURAL_FREQUENCY * TIME_CORRECTION * TIME_CORRECTION;
39 SpringDescription::with_damping_ratio(1.0, stiffness, DAMPING_RATIO)
40}
41
42#[derive(Copy, Clone, Debug, Default)]
47pub struct OverscrollStretch {
48 amount: f32,
49 pulled: f32,
51 spring: Option<(SpringSimulation, f32)>,
53 interrupted: f32,
56}
57
58impl OverscrollStretch {
59 pub fn amount(&self) -> f32 {
61 self.amount
62 }
63
64 pub fn is_idle(&self) -> bool {
66 self.spring.is_none() && self.amount == 0.0
67 }
68
69 fn set(&mut self, amount: f32) {
70 self.amount = amount.clamp(-MAX_STRETCH, MAX_STRETCH);
71 }
72
73 pub fn pull(&mut self, overscroll: f32, towards_start: bool, extent: f32) {
76 if let Some((spring, t)) = self.spring.take() {
77 self.interrupted = spring.x(t);
78 }
79 let sign = if towards_start { -1.0 } else { 1.0 };
80 self.pulled += sign * overscroll.abs();
81 let distance = (self.pulled / extent.max(1.0)).clamp(-1.0, 1.0);
83 let far = distance.abs();
84 let linear = INTENSITY * far;
85 let exponential = INTENSITY * (1.0 - (-far * EXPONENTIAL_SCALAR).exp());
86 self.set(distance.signum() * (linear + exponential) + self.interrupted);
87 }
88
89 pub fn absorb_impact(&mut self, velocity: f32, towards_start: bool) {
92 if velocity == 0.0 {
93 return;
94 }
95 let sign = if towards_start { -1.0 } else { 1.0 };
96 let thrown = (sign * velocity.abs() * IMPACT_FRICTION)
97 .clamp(-MAX_IMPACT_VELOCITY, MAX_IMPACT_VELOCITY);
98 self.release_with(thrown);
99 }
100
101 pub fn release(&mut self) {
103 self.pulled = 0.0;
104 if self.amount == 0.0 || self.spring.is_some() {
105 return;
106 }
107 self.release_with(0.0);
108 }
109
110 fn release_with(&mut self, velocity: f32) {
111 self.pulled = 0.0;
112 self.spring = Some((
113 SpringSimulation::new(
114 spring(),
115 self.amount,
116 0.0,
117 velocity * TIME_CORRECTION,
118 Tolerance::default(),
119 ),
120 0.0,
121 ));
122 }
123
124 pub fn advance(&mut self, dt: f32) -> bool {
126 let Some((spring, t)) = self.spring.as_mut() else {
127 return false;
128 };
129 *t += dt;
130 if spring.is_done(*t) {
131 self.spring = None;
132 self.amount = 0.0;
133 self.interrupted = 0.0;
134 return false;
135 }
136 let x = spring.x(*t);
137 self.set(x);
138 true
139 }
140}
141
142#[derive(Copy, Clone, Debug, Default)]
144pub struct ScrollStretch {
145 pub horizontal: OverscrollStretch,
147 pub vertical: OverscrollStretch,
149}
150
151impl ScrollStretch {
152 pub fn axis_mut(&mut self, edge: GlowEdge) -> &mut OverscrollStretch {
154 match edge {
155 GlowEdge::Top | GlowEdge::Bottom => &mut self.vertical,
156 GlowEdge::Left | GlowEdge::Right => &mut self.horizontal,
157 }
158 }
159
160 pub fn is_idle(&self) -> bool {
162 self.horizontal.is_idle() && self.vertical.is_idle()
163 }
164
165 pub fn advance(&mut self, dt: f32) -> bool {
167 let x = self.horizontal.advance(dt);
168 let y = self.vertical.advance(dt);
169 x || y
170 }
171
172 pub fn release(&mut self) {
174 self.horizontal.release();
175 self.vertical.release();
176 }
177
178 pub fn transform(&self, viewport: frus_core::Rect) -> Option<frus_core::Affine> {
181 let (h, v) = (self.horizontal.amount(), self.vertical.amount());
182 if h == 0.0 && v == 0.0 {
183 return None;
184 }
185 let anchor = frus_core::Point::new(
186 if h > 0.0 {
187 viewport.x + viewport.width
188 } else {
189 viewport.x
190 },
191 if v > 0.0 {
192 viewport.y + viewport.height
193 } else {
194 viewport.y
195 },
196 );
197 Some(frus_core::Affine::scale(1.0 + h.abs(), 1.0 + v.abs()).about(anchor))
198 }
199}
200
201pub(crate) fn is_start(edge: GlowEdge) -> bool {
203 matches!(edge, GlowEdge::Top | GlowEdge::Left)
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 fn settle(stretch: &mut OverscrollStretch) -> f32 {
211 let mut t = 0.0;
212 while stretch.advance(1.0 / 60.0) {
213 t += 1.0 / 60.0;
214 assert!(t < 5.0, "a stretch comes to rest");
215 }
216 t
217 }
218
219 #[test]
221 fn a_pull_follows_the_platform_curve() {
222 let mut stretch = OverscrollStretch::default();
223 stretch.pull(600.0, true, 600.0);
224 let expected = -(0.016 + 0.016 * (1.0 - (-EXPONENTIAL_SCALAR).exp()));
225 assert!(
226 (stretch.amount() - expected).abs() < 1e-6,
227 "{}",
228 stretch.amount()
229 );
230 }
231
232 #[test]
234 fn a_pull_accumulates_up_to_one_viewport() {
235 let mut halves = OverscrollStretch::default();
236 halves.pull(300.0, false, 600.0);
237 halves.pull(300.0, false, 600.0);
238 let mut whole = OverscrollStretch::default();
239 whole.pull(600.0, false, 600.0);
240 assert!((halves.amount() - whole.amount()).abs() < 1e-6);
241 assert!(whole.amount() > 0.0, "towards the end is positive");
242 let mut more = OverscrollStretch::default();
243 more.pull(2000.0, false, 600.0);
244 assert_eq!(more.amount(), whole.amount());
245 }
246
247 #[test]
249 fn a_longer_pull_stretches_more() {
250 let mut short = OverscrollStretch::default();
251 short.pull(20.0, true, 600.0);
252 let mut long = OverscrollStretch::default();
253 long.pull(200.0, true, 600.0);
254 assert!(long.amount() < short.amount() && short.amount() < 0.0);
255 }
256
257 #[test]
260 fn letting_go_springs_back() {
261 let mut stretch = OverscrollStretch::default();
262 stretch.pull(300.0, true, 600.0);
263 stretch.release();
264 assert!(!stretch.is_idle());
265 let took = settle(&mut stretch);
266 assert!(stretch.is_idle() && stretch.amount() == 0.0);
267 assert!(took < 1.0, "{took}");
268 }
269
270 #[test]
272 fn a_held_pull_stays() {
273 let mut stretch = OverscrollStretch::default();
274 stretch.pull(300.0, true, 600.0);
275 let held = stretch.amount();
276 assert!(!stretch.advance(0.5));
277 assert_eq!(stretch.amount(), held);
278 }
279
280 #[test]
283 fn an_impact_throws_the_stretch_and_it_returns() {
284 let peak = |velocity: f32| {
285 let mut stretch = OverscrollStretch::default();
286 stretch.absorb_impact(velocity, false);
287 let mut peak: f32 = 0.0;
288 while stretch.advance(1.0 / 240.0) {
289 peak = peak.max(stretch.amount());
290 }
291 assert_eq!(stretch.amount(), 0.0);
292 peak
293 };
294 let soft = peak(1000.0);
295 let hard = peak(3000.0);
296 assert!(soft > 0.0 && hard > soft, "{soft} {hard}");
297 }
298
299 #[test]
302 fn a_pull_mid_return_carries_on_from_there() {
303 let mut stretch = OverscrollStretch::default();
304 stretch.pull(600.0, true, 600.0);
305 stretch.release();
306 stretch.advance(0.02);
307 let reached = stretch.amount();
308 stretch.pull(1.0, true, 600.0);
309 assert!(stretch.amount() < reached, "{} {reached}", stretch.amount());
310 }
311
312 #[test]
314 fn the_transform_holds_the_pulled_edge() {
315 let viewport = frus_core::Rect::new(10.0, 20.0, 100.0, 200.0);
316 let mut stretch = ScrollStretch::default();
317 assert!(stretch.transform(viewport).is_none());
318 stretch.axis_mut(GlowEdge::Top).pull(200.0, true, 200.0);
319 let m = stretch.transform(viewport).expect("stretched");
320 let top = m.apply(frus_core::Point::new(10.0, 20.0));
321 let bottom = m.apply(frus_core::Point::new(110.0, 220.0));
322 assert!(
323 (top.x - 10.0).abs() < 1e-4 && (top.y - 20.0).abs() < 1e-4,
324 "{top:?}"
325 );
326 assert!((bottom.x - 110.0).abs() < 1e-4, "not across: {bottom:?}");
327 assert!(bottom.y > 220.0, "down the axis: {bottom:?}");
328
329 let mut end = ScrollStretch::default();
330 end.axis_mut(GlowEdge::Bottom).pull(200.0, false, 200.0);
331 let m = end.transform(viewport).unwrap();
332 let bottom = m.apply(frus_core::Point::new(10.0, 220.0));
333 let top = m.apply(frus_core::Point::new(10.0, 20.0));
334 assert!((bottom.y - 220.0).abs() < 1e-4, "{bottom:?}");
335 assert!(top.y < 20.0, "{top:?}");
336 }
337}