Skip to main content

damped_springs/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(feature = "no_std", no_std)]
3
4#[cfg(any(
5    all(feature = "std", feature = "no_std"),
6    all(not(feature = "std"), not(feature = "no_std"))
7))]
8compile_error!("only one of `std` or `no_std` features must be enabled");
9
10// The following source is derived from Ryan Juckett's Damped Springs code:
11
12/******************************************************************************
13  Copyright (c) 2008-2012 Ryan Juckett
14  http://www.ryanjuckett.com/
15
16  This software is provided 'as-is', without any express or implied
17  warranty. In no event will the authors be held liable for any damages
18  arising from the use of this software.
19
20  Permission is granted to anyone to use this software for any purpose,
21  including commercial applications, and to alter it and redistribute it
22  freely, subject to the following restrictions:
23
24  1. The origin of this software must not be misrepresented; you must not
25     claim that you wrote the original software. If you use this software
26     in a product, an acknowledgment in the product documentation would be
27     appreciated but is not required.
28
29  2. Altered source versions must be plainly marked as such, and must not be
30     misrepresented as being the original software.
31
32  3. This notice may not be removed or altered from any source
33     distribution.
34******************************************************************************/
35
36use num_traits::Float;
37
38pub mod prelude {
39    //! Prelude module exporting common spring types.
40    pub use crate::{Spring, SpringCollection, SpringConfig, SpringParams, SpringTimeStep};
41}
42
43/// Configuration options for a spring. Composed of its `angular_freq` and `damping_ratio`.
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct SpringConfig<F> {
46    angular_freq: F,
47    damping_ratio: F,
48}
49
50impl<F: Float> SpringConfig<F> {
51    /// Construct a new spring configuration. Constrains spring parameters to non-negative values.
52    pub fn new(angular_freq: F, damping_ratio: F) -> Self {
53        Self {
54            angular_freq: angular_freq.max(F::zero()),
55            damping_ratio: damping_ratio.max(F::zero()),
56        }
57    }
58
59    /// Returns the angular frequency of this spring config. Guaranteed to be at least zero.
60    #[inline]
61    pub fn angular_freq(&self) -> F {
62        self.angular_freq
63    }
64
65    /// Returns the damping ratio of this spring config. Guaranteed to be at least zero.
66    #[inline]
67    pub fn damping_ratio(&self) -> F {
68        self.damping_ratio
69    }
70}
71
72/// Cached coefficients for a spring, based on its angular frequency and damping ratio.
73///
74/// Do not construct directly; instead, use [`SpringParams::from`] with a [`SpringConfig`].
75#[derive(Debug, Clone, Copy, PartialEq)]
76pub enum SpringParams<F> {
77    /// The spring has no angular frequency and will not move.
78    Static,
79    /// The spring is over-damped (damping > 1).
80    OverDamped { zb: F, z1: F, z2: F },
81    /// The spring is critically damped (damping = 1).
82    CriticallyDamped { angular_freq: F },
83    /// The spring is under-damped (damping < 1).
84    UnderDamped { oz: F, a: F },
85}
86
87impl<F: Float> From<SpringConfig<F>> for SpringParams<F> {
88    fn from(
89        SpringConfig {
90            angular_freq,
91            damping_ratio,
92        }: SpringConfig<F>,
93    ) -> Self {
94        if angular_freq < F::epsilon() {
95            return Self::Static;
96        }
97
98        if damping_ratio > F::one() + F::epsilon() {
99            // overdamped
100            let za = -angular_freq * damping_ratio;
101            let zb = angular_freq * (damping_ratio * damping_ratio - F::one()).sqrt();
102            let z1 = za - zb;
103            let z2 = za + zb;
104
105            Self::OverDamped { zb, z1, z2 }
106        } else if damping_ratio < F::one() - F::epsilon() {
107            // under-damped
108            let oz = angular_freq * damping_ratio;
109            let a = angular_freq * (F::one() - damping_ratio * damping_ratio).sqrt();
110
111            Self::UnderDamped { oz, a }
112        } else {
113            // critically damped
114            Self::CriticallyDamped { angular_freq }
115        }
116    }
117}
118
119/// Cached coefficients for a spring, based on its configuration and a particular time step.
120///
121/// Used to efficiently update one or more springs that share the same configuration.
122///
123/// ## Usage
124///
125/// To derive a `SpringTimeStep`, you must first get a [`SpringParams`] from a [`SpringConfig`]:
126///
127/// ```
128/// # use damped_springs::{Spring, SpringConfig, SpringParams, SpringTimeStep};
129/// # let delta_time = 0.1;
130/// // configure your spring...
131/// let config = SpringConfig::new(5.0, 0.75);
132/// let mut spring = Spring::new();
133///
134/// // derive its state
135/// let state = SpringParams::from(config);
136///
137/// // then, either create your own `SpringTimeStep`...
138/// let time_step = SpringTimeStep::new(state, delta_time);
139/// spring.update(time_step);
140///
141/// // or let `Spring::update_single` do it for you
142/// spring.update_single(state, delta_time);
143/// ```
144#[derive(Debug, Clone, Copy, PartialEq)]
145pub struct SpringTimeStep<F> {
146    pp: F,
147    pv: F,
148    vp: F,
149    vv: F,
150}
151
152impl<F: Float> Default for SpringTimeStep<F> {
153    fn default() -> Self {
154        Self {
155            pp: F::one(),
156            pv: F::zero(),
157            vp: F::zero(),
158            vv: F::one(),
159        }
160    }
161}
162
163impl<F: Float> SpringTimeStep<F> {
164    /// Derive a spring time step from a particular delta time.
165    pub fn new(state: impl Into<SpringParams<F>>, delta: F) -> Self {
166        match state.into() {
167            SpringParams::Static => Self::default(),
168
169            SpringParams::OverDamped { zb, z1, z2 } => {
170                let e1 = (z1 * delta).exp();
171                let e2 = (z2 * delta).exp();
172                let inv_2zb = F::one() / ((F::one() + F::one()) * zb);
173
174                let e1_2zb = e1 * inv_2zb;
175                let e2_2zb = e2 * inv_2zb;
176
177                let z1e1_2zb = z1 * e1_2zb;
178                let z2e2_2zb = z2 * e2_2zb;
179
180                Self {
181                    pp: e1_2zb * z2 - z2e2_2zb + e2,
182                    pv: -e1_2zb + e2_2zb,
183                    vp: (z1e1_2zb - z2e2_2zb + e2) * z2,
184                    vv: -z1e1_2zb + z2e2_2zb,
185                }
186            }
187
188            SpringParams::UnderDamped { oz, a } => {
189                let exp = (-oz * delta).exp();
190                let cos = (a * delta).cos();
191                let sin = (a * delta).sin();
192
193                let inv_alpha = F::one() / a;
194
195                let exp_sin = exp * sin;
196                let exp_cos = exp * cos;
197                let exp_ozs_alpha = exp * oz * sin * inv_alpha;
198
199                Self {
200                    pp: exp_cos + exp_ozs_alpha,
201                    pv: exp_sin * inv_alpha,
202                    vp: -exp_sin * a - oz * exp_ozs_alpha,
203                    vv: exp_cos - exp_ozs_alpha,
204                }
205            }
206
207            SpringParams::CriticallyDamped { angular_freq } => {
208                let exp = (-angular_freq * delta).exp();
209                let time_exp = delta * exp;
210                let time_exp_freq = time_exp * angular_freq;
211
212                Self {
213                    pp: time_exp_freq * exp,
214                    pv: time_exp,
215                    vp: -angular_freq * time_exp_freq,
216                    vv: -time_exp_freq * exp,
217                }
218            }
219        }
220    }
221
222    /// Update multiple springs using this time step. Simply calls [`Spring::update`]
223    /// on each passed spring.
224    #[inline]
225    pub fn update_many(self, springs: &mut [&mut Spring<F>]) {
226        for spring in springs {
227            spring.update(self);
228        }
229    }
230}
231
232/// An instance of a spring and its current physical properties, like its position, velocity, and target equilibrium.
233#[derive(Debug, Clone, Copy, PartialEq)]
234pub struct Spring<F> {
235    pub position: F,
236    pub velocity: F,
237    pub equilibrium: F,
238}
239
240impl<F: Float> Default for Spring<F> {
241    fn default() -> Self {
242        Self {
243            position: F::zero(),
244            velocity: F::zero(),
245            equilibrium: F::zero(),
246        }
247    }
248}
249
250impl<F: Float> Spring<F> {
251    /// Create a new stable spring at position 0.
252    pub fn new() -> Self {
253        Self::default()
254    }
255
256    /// Create a new spring from a start equilibrium.
257    pub fn from_equilibrium(equilibrium: F) -> Self {
258        Self {
259            position: F::zero(),
260            velocity: F::zero(),
261            equilibrium,
262        }
263    }
264
265    /// Builder function to set the spring's position.
266    pub fn with_position(mut self, position: F) -> Self {
267        self.position = position;
268        self
269    }
270
271    /// Builder function to set the spring's velocity.
272    pub fn with_velocity(mut self, velocity: F) -> Self {
273        self.velocity = velocity;
274        self
275    }
276
277    /// Builder function to set the spring's equilibrium.
278    pub fn with_equilibrium(mut self, equilibrium: F) -> Self {
279        self.equilibrium = equilibrium;
280        self
281    }
282
283    #[inline]
284    fn update_internal(
285        position: &mut F,
286        velocity: &mut F,
287        equilibrium: F,
288        time_step: SpringTimeStep<F>,
289    ) {
290        let op = *position - equilibrium;
291        let ov = *velocity;
292
293        *position = op * time_step.pp + ov * time_step.pv + equilibrium;
294        *velocity = op * time_step.vp + ov * time_step.vv;
295    }
296
297    /// Update this spring using a pre-computed [`SpringTimeStep`].
298    pub fn update(&mut self, time_step: SpringTimeStep<F>) {
299        Self::update_internal(
300            &mut self.position,
301            &mut self.velocity,
302            self.equilibrium,
303            time_step,
304        );
305    }
306
307    /// Update this spring (and this spring only) using a [`SpringParams`] and a delta time.
308    /// Will internally create a [`SpringTimeStep`] for this call.
309    ///
310    /// If you are updating multiple springs with the same properties,
311    /// consider using [`SpringTimeStep::new`] and [`SpringTimeStep::update_many`]
312    /// or create a [`SpringCollection`].
313    #[inline]
314    pub fn update_single(&mut self, state: SpringParams<F>, delta: F) {
315        self.update(SpringTimeStep::new(state, delta));
316    }
317}
318
319/// A fixed-size collection of springs that all share the same spring parameters.
320/// Useful for creating springs over multiple dimensions (i.e. 2D or 3D springs).
321#[derive(Debug, Clone, PartialEq)]
322pub struct SpringCollection<F, const N: usize> {
323    params: SpringParams<F>,
324    positions: [F; N],
325    velocities: [F; N],
326    equilibriums: [F; N],
327}
328
329impl<F: Float, const N: usize, T> From<T> for SpringCollection<F, N>
330where
331    T: Into<SpringParams<F>>,
332{
333    fn from(params: T) -> Self {
334        Self {
335            params: params.into(),
336            positions: [F::zero(); N],
337            velocities: [F::zero(); N],
338            equilibriums: [F::zero(); N],
339        }
340    }
341}
342
343impl<F: Float, const N: usize> SpringCollection<F, N> {
344    /// Construct `N` springs, all starting at a specified `equilibrium`.
345    pub fn from_equilibrium(params: impl Into<SpringParams<F>>, equilibrium: F) -> Self {
346        Self {
347            params: params.into(),
348            positions: [F::zero(); N],
349            velocities: [F::zero(); N],
350            equilibriums: [equilibrium; N],
351        }
352    }
353
354    /// Construct `N` springs, each with a particular equilibrium.
355    pub fn from_equilibriums(params: impl Into<SpringParams<F>>, equilibriums: [F; N]) -> Self {
356        Self {
357            params: params.into(),
358            positions: [F::zero(); N],
359            velocities: [F::zero(); N],
360            equilibriums,
361        }
362    }
363
364    /// Update all springs over the specified delta. Constructs a new [`SpringTimeStep`]
365    /// for this usage.
366    #[inline]
367    pub fn update(&mut self, delta: F) {
368        self.update_with(SpringTimeStep::new(self.params, delta));
369    }
370
371    /// Update all springs using the specified `time_step`.
372    ///
373    /// **Note:** this time step need not be derived from [`SpringCollection::params`].
374    /// The implementation of this method uses all constants from `time_step`.
375    #[inline]
376    pub fn update_with(&mut self, time_step: SpringTimeStep<F>) {
377        for i in 0..N {
378            Spring::update_internal(
379                &mut self.positions[i],
380                &mut self.velocities[i],
381                self.equilibriums[i],
382                time_step,
383            );
384        }
385    }
386}
387
388macro_rules! impl_collection_props {
389    ( $prop:ident , $prop_mut:ident ) => {
390        impl<F: Float, const N: usize> SpringCollection<F, N> {
391            #[doc = concat!("The array of current spring ", stringify!($plural), ".")]
392            #[inline]
393            pub fn $prop(&self) -> &[F; N] {
394                &self.$prop
395            }
396
397            #[doc = concat!("Mutable reference of current spring ", stringify!($prop), ".")]
398            #[inline]
399            pub fn $prop_mut(&mut self) -> &mut [F; N] {
400                &mut self.$prop
401            }
402        }
403    };
404}
405
406impl_collection_props!(positions, positions_mut);
407impl_collection_props!(velocities, velocities_mut);
408impl_collection_props!(equilibriums, equilibriums_mut);
409
410impl<F: Float, const N: usize> From<SpringCollection<F, N>> for [Spring<F>; N] {
411    fn from(value: SpringCollection<F, N>) -> Self {
412        let mut i = 0;
413
414        value.positions.map(|position| {
415            let spring = Spring {
416                position,
417                velocity: value.velocities[i],
418                equilibrium: value.equilibriums[i],
419            };
420            i += 1;
421            spring
422        })
423    }
424}