#[derive(Debug, Clone, Copy)]
pub struct Spring {
angular_frequency: f32,
damping_ratio: f32,
time_delta: f32,
}
impl Spring {
pub fn new(fps: f32, angular_frequency: f32, damping_ratio: f32) -> Self {
Self {
angular_frequency,
damping_ratio,
time_delta: 1.0 / fps,
}
}
pub fn from_time_delta(time_delta: f32, angular_frequency: f32, damping_ratio: f32) -> Self {
Self {
angular_frequency,
damping_ratio,
time_delta,
}
}
pub fn smooth(fps: f32) -> Self {
Self::new(fps, 6.0, 1.0)
}
pub fn bouncy(fps: f32) -> Self {
Self::new(fps, 6.0, 0.5)
}
pub fn stiff(fps: f32) -> Self {
Self::new(fps, 4.0, 1.5)
}
pub fn snappy(fps: f32) -> Self {
Self::new(fps, 10.0, 0.9)
}
pub fn gentle(fps: f32) -> Self {
Self::new(fps, 3.0, 1.0)
}
pub fn update(&self, position: f32, velocity: f32, target: f32) -> (f32, f32) {
let delta = position - target;
let omega = self.angular_frequency;
let zeta = self.damping_ratio;
let dt = self.time_delta;
let acceleration = -2.0 * zeta * omega * velocity - omega * omega * delta;
let new_velocity = velocity + acceleration * dt;
let new_position = position + new_velocity * dt;
(new_position, new_velocity)
}
pub fn is_settled(&self, position: f32, velocity: f32, target: f32, threshold: f32) -> bool {
let position_settled = (position - target).abs() < threshold;
let velocity_settled = velocity.abs() < threshold;
position_settled && velocity_settled
}
pub fn angular_frequency(&self) -> f32 {
self.angular_frequency
}
pub fn damping_ratio(&self) -> f32 {
self.damping_ratio
}
pub fn time_delta(&self) -> f32 {
self.time_delta
}
}
impl Default for Spring {
fn default() -> Self {
Self::smooth(60.0)
}
}
#[derive(Debug, Clone)]
pub struct SpringValue {
position: f32,
velocity: f32,
target: f32,
spring: Spring,
threshold: f32,
}
impl SpringValue {
pub fn new(initial: f32, spring: Spring) -> Self {
Self {
position: initial,
velocity: 0.0,
target: initial,
spring,
threshold: 0.01,
}
}
pub fn smooth(initial: f32) -> Self {
Self::new(initial, Spring::smooth(60.0))
}
pub fn bouncy(initial: f32) -> Self {
Self::new(initial, Spring::bouncy(60.0))
}
pub fn set_target(&mut self, target: f32) {
self.target = target;
}
pub fn snap_to(&mut self, value: f32) {
self.position = value;
self.target = value;
self.velocity = 0.0;
}
pub fn tick(&mut self) {
if !self.is_settled() {
let (pos, vel) = self
.spring
.update(self.position, self.velocity, self.target);
self.position = pos;
self.velocity = vel;
}
}
pub fn get(&self) -> f32 {
self.position
}
pub fn get_i32(&self) -> i32 {
self.position.round() as i32
}
pub fn get_usize(&self) -> usize {
self.position.round().max(0.0) as usize
}
pub fn target(&self) -> f32 {
self.target
}
pub fn velocity(&self) -> f32 {
self.velocity
}
pub fn is_settled(&self) -> bool {
self.spring
.is_settled(self.position, self.velocity, self.target, self.threshold)
}
pub fn with_threshold(mut self, threshold: f32) -> Self {
self.threshold = threshold;
self
}
pub fn with_spring(mut self, spring: Spring) -> Self {
self.spring = spring;
self
}
}
#[derive(Debug, Clone)]
pub struct SpringValue2D {
pub x: SpringValue,
pub y: SpringValue,
}
impl SpringValue2D {
pub fn new(x: f32, y: f32, spring: Spring) -> Self {
Self {
x: SpringValue::new(x, spring),
y: SpringValue::new(y, spring),
}
}
pub fn smooth(x: f32, y: f32) -> Self {
Self {
x: SpringValue::smooth(x),
y: SpringValue::smooth(y),
}
}
pub fn set_target(&mut self, x: f32, y: f32) {
self.x.set_target(x);
self.y.set_target(y);
}
pub fn snap_to(&mut self, x: f32, y: f32) {
self.x.snap_to(x);
self.y.snap_to(y);
}
pub fn tick(&mut self) {
self.x.tick();
self.y.tick();
}
pub fn get(&self) -> (f32, f32) {
(self.x.get(), self.y.get())
}
pub fn get_i32(&self) -> (i32, i32) {
(self.x.get_i32(), self.y.get_i32())
}
pub fn is_settled(&self) -> bool {
self.x.is_settled() && self.y.is_settled()
}
}
#[derive(Debug, Clone)]
pub struct SpringColor {
pub r: SpringValue,
pub g: SpringValue,
pub b: SpringValue,
}
impl SpringColor {
pub fn new(r: u8, g: u8, b: u8, spring: Spring) -> Self {
Self {
r: SpringValue::new(r as f32, spring),
g: SpringValue::new(g as f32, spring),
b: SpringValue::new(b as f32, spring),
}
}
pub fn smooth(r: u8, g: u8, b: u8) -> Self {
Self {
r: SpringValue::smooth(r as f32),
g: SpringValue::smooth(g as f32),
b: SpringValue::smooth(b as f32),
}
}
pub fn set_target(&mut self, r: u8, g: u8, b: u8) {
self.r.set_target(r as f32);
self.g.set_target(g as f32);
self.b.set_target(b as f32);
}
pub fn snap_to(&mut self, r: u8, g: u8, b: u8) {
self.r.snap_to(r as f32);
self.g.snap_to(g as f32);
self.b.snap_to(b as f32);
}
pub fn tick(&mut self) {
self.r.tick();
self.g.tick();
self.b.tick();
}
pub fn get(&self) -> (u8, u8, u8) {
(
self.r.get().clamp(0.0, 255.0) as u8,
self.g.get().clamp(0.0, 255.0) as u8,
self.b.get().clamp(0.0, 255.0) as u8,
)
}
pub fn is_settled(&self) -> bool {
self.r.is_settled() && self.g.is_settled() && self.b.is_settled()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spring_creation() {
let spring = Spring::new(60.0, 6.0, 1.0);
assert_eq!(spring.angular_frequency(), 6.0);
assert_eq!(spring.damping_ratio(), 1.0);
assert!((spring.time_delta() - 1.0 / 60.0).abs() < 0.0001);
}
#[test]
fn test_spring_presets() {
let smooth = Spring::smooth(60.0);
assert_eq!(smooth.damping_ratio(), 1.0);
let bouncy = Spring::bouncy(60.0);
assert!(bouncy.damping_ratio() < 1.0);
let stiff = Spring::stiff(60.0);
assert!(stiff.damping_ratio() > 1.0);
}
#[test]
fn test_spring_update() {
let spring = Spring::smooth(60.0);
let (pos, vel) = spring.update(0.0, 0.0, 100.0);
assert!(pos > 0.0);
assert!(vel > 0.0);
}
#[test]
fn test_spring_convergence() {
let spring = Spring::smooth(60.0);
let mut pos = 0.0;
let mut vel = 0.0;
let target = 100.0;
for _ in 0..1000 {
let (new_pos, new_vel) = spring.update(pos, vel, target);
pos = new_pos;
vel = new_vel;
}
assert!((pos - target).abs() < 0.1);
assert!(vel.abs() < 0.1);
}
#[test]
fn test_spring_is_settled() {
let spring = Spring::smooth(60.0);
assert!(spring.is_settled(100.0, 0.0, 100.0, 0.01));
assert!(!spring.is_settled(0.0, 0.0, 100.0, 0.01));
assert!(!spring.is_settled(100.0, 1.0, 100.0, 0.01));
}
#[test]
fn test_spring_value_creation() {
let value = SpringValue::smooth(50.0);
assert_eq!(value.get(), 50.0);
assert_eq!(value.target(), 50.0);
assert!(value.is_settled());
}
#[test]
fn test_spring_value_animation() {
let mut value = SpringValue::smooth(0.0);
value.set_target(100.0);
assert!(!value.is_settled());
for _ in 0..1000 {
value.tick();
}
assert!(value.is_settled());
assert!((value.get() - 100.0).abs() < 0.1);
}
#[test]
fn test_spring_value_snap() {
let mut value = SpringValue::smooth(0.0);
value.set_target(100.0);
value.tick();
value.snap_to(50.0);
assert_eq!(value.get(), 50.0);
assert_eq!(value.target(), 50.0);
assert_eq!(value.velocity(), 0.0);
assert!(value.is_settled());
}
#[test]
fn test_spring_value_get_i32() {
let value = SpringValue::smooth(42.7);
assert_eq!(value.get_i32(), 43);
}
#[test]
fn test_spring_value_get_usize() {
let value = SpringValue::smooth(42.7);
assert_eq!(value.get_usize(), 43);
let negative = SpringValue::smooth(-10.0);
assert_eq!(negative.get_usize(), 0);
}
#[test]
fn test_spring_value_2d() {
let mut value = SpringValue2D::smooth(0.0, 0.0);
value.set_target(100.0, 50.0);
for _ in 0..1000 {
value.tick();
}
let (x, y) = value.get();
assert!((x - 100.0).abs() < 0.1);
assert!((y - 50.0).abs() < 0.1);
}
#[test]
fn test_spring_color() {
let mut color = SpringColor::smooth(0, 0, 0);
color.set_target(255, 128, 64);
for _ in 0..1000 {
color.tick();
}
let (r, g, b) = color.get();
assert!((r as i32 - 255).abs() <= 1);
assert!((g as i32 - 128).abs() <= 1);
assert!((b as i32 - 64).abs() <= 1);
}
#[test]
fn test_spring_default() {
let spring = Spring::default();
assert_eq!(spring.damping_ratio(), 1.0);
}
}