use super::geom::{V2, v2};
pub const GRAVITY: f64 = 9.81 / 1.75;
pub fn spring_step(t: f64, omega: f64, zeta: f64) -> f64 {
if t <= 0.0 {
return 1.0;
}
let wd = omega * (1.0 - zeta * zeta).sqrt();
(-zeta * omega * t).exp() * ((wd * t).cos() + zeta * omega / wd * (wd * t).sin())
}
pub fn spring_impulse(t: f64, v: f64, omega: f64, zeta: f64) -> f64 {
if t <= 0.0 {
return 0.0;
}
let wd = omega * (1.0 - zeta * zeta).sqrt();
v / wd * (-zeta * omega * t).exp() * (wd * t).sin()
}
pub fn drifting(p0: V2, v0: V2, a: V2, drag: f64, t: f64) -> V2 {
let terminal = a * (1.0 / drag);
let decay = (1.0 - (-drag * t).exp()) / drag;
p0 + terminal * t + (v0 - terminal) * decay
}
#[derive(Clone, Copy, Debug)]
pub struct Flight {
pub from: V2,
pub velocity: V2,
pub time: f64,
}
impl Flight {
pub fn new(from: V2, to: V2, time: f64) -> Self {
let velocity = (to - from) * (1.0 / time) + v2(0.0, GRAVITY * time / 2.0);
Flight {
from,
velocity,
time,
}
}
pub fn at(&self, t: f64) -> V2 {
self.from + self.velocity * t - v2(0.0, GRAVITY * t * t / 2.0)
}
}
pub const SCARF_LINKS: usize = 10;
pub const SCARF_POINTS: usize = SCARF_LINKS + 1;
const SCARF_LINK: f64 = 0.036;
const SCARF_DRAG: f64 = 1.2;
const SCARF_ITERATIONS: usize = 8;
const STEP_RATE: f64 = 240.0;
fn breeze(t: f64, i: usize) -> V2 {
use std::f64::consts::TAU;
let i = i as f64;
v2(
1.4 + 0.8 * (TAU * t / 4.0).sin() + 0.5 * (TAU * t - 0.6 * i).sin(),
0.3 * (TAU * t / 2.0 + 0.8 * i).sin(),
)
}
pub fn scarf(anchor: impl Fn(f64) -> V2, frames: usize, fps: f64) -> Vec<[V2; SCARF_POINTS]> {
const WARM_UP_LOOPS: usize = 3;
let substeps = (STEP_RATE / fps).ceil().max(1.0) as usize;
let dt = 1.0 / (fps * substeps as f64);
let start = anchor(0.0);
let mut pos: [V2; SCARF_POINTS] =
std::array::from_fn(|i| start - v2(0.0, i as f64 * SCARF_LINK));
let mut prev = pos;
let mut recorded = Vec::with_capacity(2 * frames);
for n in 0..(WARM_UP_LOOPS + 2) * frames {
if n >= WARM_UP_LOOPS * frames {
recorded.push(pos);
}
for s in 1..=substeps {
let t = (n as f64 + s as f64 / substeps as f64) / fps;
for (i, (p, q)) in pos.iter_mut().zip(&mut prev).enumerate().skip(1) {
let velocity = *p - *q;
let accel = breeze(t, i) - v2(0.0, GRAVITY);
*q = *p;
*p += velocity * (1.0 - SCARF_DRAG * dt) + accel * (dt * dt);
}
pos[0] = anchor(t);
prev[0] = pos[0];
for _ in 0..SCARF_ITERATIONS {
for i in 0..SCARF_LINKS {
let d = pos[i + 1] - pos[i];
let len = d.len();
if len < 1e-9 {
continue;
}
let fix = d * ((len - SCARF_LINK) / len);
if i == 0 {
pos[1] -= fix;
} else {
pos[i] += fix * 0.5;
pos[i + 1] -= fix * 0.5;
}
}
for p in &mut pos[1..] {
p.y = p.y.max(0.004);
}
}
}
}
let (a, b) = recorded.split_at(frames);
(0..frames)
.map(|i| {
let s = i as f64 / frames as f64;
std::array::from_fn(|k| b[i][k].lerp(a[i][k], s))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_flight_lands_on_target() {
let f = Flight::new(v2(0.0, 1.0), v2(-0.3, 0.9), 0.75);
assert!((f.at(0.0) - v2(0.0, 1.0)).len() < 1e-12);
assert!((f.at(0.75) - v2(-0.3, 0.9)).len() < 1e-12);
let apex = (0..=75)
.map(|i| f.at(i as f64 / 100.0).y)
.fold(0.0, f64::max);
assert!(apex > 1.3 && apex < 1.45, "apex {apex}");
}
#[test]
fn springs_settle() {
assert_eq!(spring_step(0.0, 30.0, 0.4), 1.0);
let overshoot = (1..200)
.map(|i| spring_step(i as f64 / 1000.0, 30.0, 0.4))
.fold(1.0, f64::min);
assert!(overshoot < -0.1, "overshoots: {overshoot}");
assert!(spring_step(1.0, 30.0, 0.4).abs() < 1e-3);
let dt = 1e-6;
assert!((spring_impulse(dt, 1.4, 13.0, 0.55) / dt - 1.4).abs() < 1e-3);
assert!(spring_impulse(2.0, 1.4, 13.0, 0.55).abs() < 1e-3);
}
#[test]
fn drag_reaches_terminal_velocity() {
let a = v2(0.0, 0.6);
let p = |t| drifting(V2::default(), v2(1.0, 0.0), a, 2.0, t);
let v = (p(10.001) - p(10.0)) * 1000.0;
assert!((v - v2(0.0, 0.3)).len() < 1e-3);
}
#[test]
fn the_scarf_loops_seamlessly() {
use std::f64::consts::TAU;
let fps = 30.0;
let frames = 60;
let anchor = |t: f64| v2(1.0 + 0.2 * (TAU * t / 2.0).sin(), 0.8);
let s = scarf(anchor, frames, fps);
assert_eq!(s.len(), frames);
let step = |a: &[V2; SCARF_POINTS], b: &[V2; SCARF_POINTS]| {
a.iter()
.zip(b)
.map(|(p, q)| (*p - *q).len())
.fold(0.0, f64::max)
};
let largest = s.windows(2).map(|w| step(&w[0], &w[1])).fold(0.0, f64::max);
let seam = step(&s[frames - 1], &s[0]);
assert!(seam < 1.5 * largest, "seam {seam} vs {largest}");
for (i, points) in s.iter().enumerate() {
assert!((points[0] - anchor(i as f64 / fps)).len() < 1e-9);
for w in points.windows(2) {
assert!(((w[1] - w[0]).len() - SCARF_LINK).abs() < 0.01);
}
}
}
}