// Copyright 2026 Ling71671
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Physical spring configuration parameters governing harmonic oscillation and damping.
pub struct SpringConfig {
stiffness : Double
damping : Double
mass : Double
} derive(Eq, Debug, Default)
///|
pub fn SpringConfig::new(
stiffness : Double,
damping : Double,
mass? : Double = 1.0,
) -> SpringConfig {
let m = if mass <= 0.0 { 1.0 } else { mass }
let k = if stiffness <= 0.0 { 100.0 } else { stiffness }
let d = if damping < 0.0 { 0.0 } else { damping }
{ stiffness: k, damping: d, mass: m, }
}
///|
/// Default balanced spring (critically damped feel for interactive UI).
pub fn SpringConfig::default_spring() -> SpringConfig {
{ stiffness: 180.0, damping: 26.0, mass: 1.0, }
}
///|
/// Stiff spring with high responsiveness and minimal overshoot.
pub fn SpringConfig::stiff() -> SpringConfig {
{ stiffness: 320.0, damping: 35.0, mass: 1.0, }
}
///|
/// Gentle spring for smooth transitions and subtle deceleration.
pub fn SpringConfig::gentle() -> SpringConfig {
{ stiffness: 120.0, damping: 20.0, mass: 1.0, }
}
///|
/// Bouncy spring with lower damping for noticeable recoil.
pub fn SpringConfig::bouncy() -> SpringConfig {
{ stiffness: 200.0, damping: 14.0, mass: 1.0, }
}
///|
/// Single-degree-of-freedom spring simulation state.
pub struct Spring {
config : SpringConfig
mut current : Double
mut target : Double
mut velocity : Double
} derive(Eq, Debug)
///|
pub fn Spring::new(
initial_val : Double,
config? : SpringConfig = SpringConfig::default_spring(),
) -> Spring {
{ config, current: initial_val, target: initial_val, velocity: 0.0, }
}
///|
pub fn Spring::set_target(self : Spring, target : Double) -> Unit {
self.target = target
}
///|
pub fn Spring::value(self : Spring) -> Double {
self.current
}
///|
pub fn Spring::target_value(self : Spring) -> Double {
self.target
}
///|
pub fn Spring::velocity_value(self : Spring) -> Double {
self.velocity
}
///|
pub fn Spring::reset(self : Spring, value : Double) -> Unit {
self.current = value
self.target = value
self.velocity = 0.0
}
///|
pub fn Spring::is_settled(
self : Spring,
tolerance? : Double = 0.001,
velocity_tolerance? : Double = 0.001,
) -> Bool {
let diff = self.current - self.target
let abs_diff = if diff < 0.0 { -diff } else { diff }
let abs_v = if self.velocity < 0.0 { -self.velocity } else { self.velocity }
abs_diff < tolerance && abs_v < velocity_tolerance
}
///|
/// Advances the spring simulation by dt seconds using semi-implicit Euler integration
/// with adaptive sub-stepping to prevent numerical instability.
pub fn Spring::step(self : Spring, dt : Double) -> Unit {
if dt <= 0.0 || dt.is_nan() {
return
}
// If already at target with negligible velocity, snap to target
if self.is_settled() {
self.current = self.target
self.velocity = 0.0
return
}
// Clamp dt to at most 0.2s to prevent frame-spike freezes (e.g. backgrounded tabs)
let safe_dt = if dt > 0.2 { 0.2 } else { dt }
let max_sub_dt = 1.0 / 120.0
let mut remaining = safe_dt
let mut iter = 0
while remaining > 0.0 && iter < 30 {
iter = iter + 1
let sub_dt = if remaining < max_sub_dt { remaining } else { max_sub_dt }
let disp = self.current - self.target
let spring_force = -self.config.stiffness * disp
let damping_force = -self.config.damping * self.velocity
let total_force = spring_force + damping_force
let accel = total_force / self.config.mass
self.velocity = self.velocity + accel * sub_dt
self.current = self.current + self.velocity * sub_dt
remaining = remaining - sub_dt
}
// Snap if reached threshold
if self.is_settled() {
self.current = self.target
self.velocity = 0.0
}
}
///|
/// Two-dimensional spring simulation for positions and coordinates.
pub struct Spring2D {
x : Spring
y : Spring
} derive(Eq, Debug)
///|
pub fn Spring2D::new(
initial_pos : Vec2,
config? : SpringConfig = SpringConfig::default_spring(),
) -> Spring2D {
{
x: Spring::new(initial_pos.x, config~),
y: Spring::new(initial_pos.y, config~),
}
}
///|
pub fn Spring2D::set_target(self : Spring2D, target : Vec2) -> Unit {
self.x.set_target(target.x)
self.y.set_target(target.y)
}
///|
pub fn Spring2D::value(self : Spring2D) -> Vec2 {
Vec2::new(self.x.value(), self.y.value())
}
///|
pub fn Spring2D::target_value(self : Spring2D) -> Vec2 {
Vec2::new(self.x.target_value(), self.y.target_value())
}
///|
pub fn Spring2D::reset(self : Spring2D, pos : Vec2) -> Unit {
self.x.reset(pos.x)
self.y.reset(pos.y)
}
///|
pub fn Spring2D::is_settled(
self : Spring2D,
tolerance? : Double = 0.001,
velocity_tolerance? : Double = 0.001,
) -> Bool {
self.x.is_settled(tolerance~, velocity_tolerance~) &&
self.y.is_settled(tolerance~, velocity_tolerance~)
}
///|
pub fn Spring2D::step(self : Spring2D, dt : Double) -> Unit {
self.x.step(dt)
self.y.step(dt)
}