fn clamp_unit(v: [f32; 2]) -> [f32; 2] {
let mag = (v[0] * v[0] + v[1] * v[1]).sqrt();
if mag > 1.0 {
[v[0] / mag, v[1] / mag]
} else {
v
}
}
pub(crate) fn radial_deadzone(v: [f32; 2], deadzone: f32) -> [f32; 2] {
let v = clamp_unit(v);
let dz = deadzone.clamp(0.0, 0.95);
let mag = (v[0] * v[0] + v[1] * v[1]).sqrt();
if mag <= dz {
return [0.0, 0.0];
}
let scaled = (mag - dz) / (1.0 - dz);
[v[0] / mag * scaled, v[1] / mag * scaled]
}
pub(crate) fn response_curve(v: [f32; 2], exponent: f32) -> [f32; 2] {
let mag = (v[0] * v[0] + v[1] * v[1]).sqrt();
if mag <= f32::EPSILON {
return [0.0, 0.0];
}
let curved = mag.powf(exponent.max(f32::EPSILON));
[v[0] / mag * curved, v[1] / mag * curved]
}
#[cfg(test)]
mod tests {
use super::*;
fn mag(v: [f32; 2]) -> f32 {
(v[0] * v[0] + v[1] * v[1]).sqrt()
}
#[test]
fn deadzone_zeroes_small_deflections() {
assert_eq!(radial_deadzone([0.1, 0.05], 0.15), [0.0, 0.0]);
assert_eq!(radial_deadzone([0.0, 0.0], 0.15), [0.0, 0.0]);
assert_eq!(radial_deadzone([0.15, 0.0], 0.15), [0.0, 0.0]);
}
#[test]
fn deadzone_rescales_to_the_full_range() {
let near = radial_deadzone([0.16, 0.0], 0.15);
assert!(near[0] > 0.0 && near[0] < 0.02, "{near:?}");
let full = radial_deadzone([1.0, 0.0], 0.15);
assert!((full[0] - 1.0).abs() < 1e-6, "{full:?}");
let mid = radial_deadzone([0.575, 0.0], 0.15);
assert!((mid[0] - 0.5).abs() < 1e-6, "{mid:?}");
}
#[test]
fn deadzone_preserves_direction() {
let v = radial_deadzone([0.6, 0.6], 0.15);
assert!((v[0] - v[1]).abs() < 1e-6, "diagonal stays diagonal: {v:?}");
let v = radial_deadzone([-0.8, 0.4], 0.15);
assert!(v[0] < 0.0 && v[1] > 0.0, "signs preserved: {v:?}");
assert!((v[0] / v[1] + 2.0).abs() < 1e-5, "ratio preserved: {v:?}");
}
#[test]
fn deadzone_clamps_overshooting_diagonals() {
let v = radial_deadzone([1.0, 1.0], 0.15);
assert!(mag(v) <= 1.0 + 1e-6, "{v:?}");
}
#[test]
fn zero_deadzone_is_identity_inside_the_disc() {
let v = radial_deadzone([0.3, -0.4], 0.0);
assert!(
(v[0] - 0.3).abs() < 1e-6 && (v[1] + 0.4).abs() < 1e-6,
"{v:?}"
);
}
#[test]
fn response_curve_softens_small_and_keeps_full() {
let small = response_curve([0.5, 0.0], 2.0);
assert!((small[0] - 0.25).abs() < 1e-6, "{small:?}");
let full = response_curve([0.0, 1.0], 2.0);
assert!((full[1] - 1.0).abs() < 1e-6, "{full:?}");
assert_eq!(response_curve([0.0, 0.0], 2.0), [0.0, 0.0]);
}
#[test]
fn response_curve_preserves_direction() {
let v = response_curve([-0.4, 0.4], 2.0);
assert!(v[0] < 0.0 && v[1] > 0.0, "{v:?}");
assert!((v[0] + v[1]).abs() < 1e-6, "diagonal stays diagonal: {v:?}");
}
#[test]
fn response_curve_exponent_one_is_linear() {
let v = response_curve([0.5, -0.25], 1.0);
assert!(
(v[0] - 0.5).abs() < 1e-6 && (v[1] + 0.25).abs() < 1e-6,
"{v:?}"
);
}
}