use crate::fixed::Fixed;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Vortex {
pub x: u16,
pub y: u16,
pub strength: i16,
pub radius: u8,
pub decay: u8,
}
impl Vortex {
pub fn new(x: u16, y: u16, strength: i16, radius: u8, decay: u8) -> Self {
Vortex {
x,
y,
strength,
radius,
decay,
}
}
pub fn velocity_at(&self, x: Fixed, y: Fixed) -> (Fixed, Fixed) {
let dx = x - Fixed::from(self.x);
let dy = y - Fixed::from(self.y);
let r_sq = dx * dx + dy * dy;
let r = r_sq.sqrt().unwrap_or(Fixed::ZERO);
if r < Fixed::ONE {
return (Fixed::ZERO, Fixed::ZERO);
}
let theta = atan2_fixed(dy, dx);
let decay_fixed = Fixed::from_f32(self.decay as f32 / 255.0);
let radius_fixed = Fixed::from_int(self.radius as i32);
let falloff_arg = -(decay_fixed * r / radius_fixed);
let falloff = exp_fixed(falloff_arg);
let strength_fixed = Fixed::from_f32(self.strength as f32 / 100.0);
let u = strength_fixed * theta.sin() * falloff;
let v = -strength_fixed * theta.cos() * falloff;
(u, v)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct RadialBasis {
pub coeffs: [Fixed; 4],
}
impl RadialBasis {
pub fn zero() -> Self {
RadialBasis {
coeffs: [Fixed::ZERO; 4],
}
}
pub fn new(c1: Fixed, c2: Fixed, c3: Fixed, c4: Fixed) -> Self {
RadialBasis {
coeffs: [c1, c2, c3, c4],
}
}
pub fn velocity_at(&self, x: Fixed, y: Fixed, center_x: Fixed, center_y: Fixed) -> (Fixed, Fixed) {
let dx = x - center_x;
let dy = y - center_y;
let r_sq = dx * dx + dy * dy;
let r = r_sq.sqrt().unwrap_or(Fixed::ZERO);
let theta = atan2_fixed(dy, dx);
let theta_4 = theta * Fixed::from_int(4);
let psi1_x = r * theta.cos();
let psi1_y = Fixed::ZERO;
let psi2_x = Fixed::ZERO;
let psi2_y = r * theta.sin();
let psi3_x = r * theta_4.cos();
let psi3_y = Fixed::ZERO;
let psi4_x = Fixed::ZERO;
let psi4_y = r * theta_4.sin();
let u = self.coeffs[0] * psi1_x + self.coeffs[1] * psi2_x
+ self.coeffs[2] * psi3_x + self.coeffs[3] * psi4_x;
let v = self.coeffs[0] * psi1_y + self.coeffs[1] * psi2_y
+ self.coeffs[2] * psi3_y + self.coeffs[3] * psi4_y;
(u, v)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WarpField {
pub width: u32,
pub height: u32,
pub global_basis: RadialBasis,
pub center_x: u16,
pub center_y: u16,
pub vortices: Vec<Vortex>,
}
impl WarpField {
pub fn new(width: u32, height: u32) -> Self {
WarpField {
width,
height,
global_basis: RadialBasis::zero(),
center_x: (width / 2) as u16,
center_y: (height / 2) as u16,
vortices: Vec::new(),
}
}
pub fn add_vortex(&mut self, vortex: Vortex) {
self.vortices.push(vortex);
}
pub fn velocity_at(&self, x: u16, y: u16) -> (Fixed, Fixed) {
let x_fixed = Fixed::from(x);
let y_fixed = Fixed::from(y);
let (u_global, v_global) = self.global_basis.velocity_at(
x_fixed,
y_fixed,
Fixed::from(self.center_x),
Fixed::from(self.center_y),
);
let mut u_total = u_global;
let mut v_total = v_global;
for vortex in &self.vortices {
let (u_vortex, v_vortex) = vortex.velocity_at(x_fixed, y_fixed);
u_total = u_total + u_vortex;
v_total = v_total + v_vortex;
}
(u_total, v_total)
}
pub fn warp_backwards(&self, x: u16, y: u16) -> (Fixed, Fixed) {
let (u, v) = self.velocity_at(x, y);
let x_fixed = Fixed::from(x);
let y_fixed = Fixed::from(y);
(x_fixed - u, y_fixed - v)
}
}
pub struct BicubicSampler;
impl BicubicSampler {
pub fn sample(image: &[[u8; 3]], width: usize, height: usize, x: Fixed, y: Fixed) -> [u8; 3] {
let xi = x.floor().to_int().clamp(0, width as i32 - 1) as usize;
let yi = y.floor().to_int().clamp(0, height as i32 - 1) as usize;
let xf = x.fract();
let yf = y.fract();
let mut pixels = [[0u8; 3]; 16];
for dy in 0..4 {
for dx in 0..4 {
let sample_x = (xi + dx).saturating_sub(1).min(width - 1);
let sample_y = (yi + dy).saturating_sub(1).min(height - 1);
let idx = sample_y * width + sample_x;
if idx < image.len() {
pixels[dy * 4 + dx] = image[idx];
}
}
}
Self::bicubic_interpolate(&pixels, xf, yf)
}
fn bicubic_interpolate(pixels: &[[u8; 3]; 16], xf: Fixed, yf: Fixed) -> [u8; 3] {
let mut result = [0u8; 3];
let weights = Self::cubic_weights(xf);
let weights_y = Self::cubic_weights(yf);
for c in 0..3 {
let mut sum = Fixed::ZERO;
for y in 0..4 {
let mut row_sum = Fixed::ZERO;
for x in 0..4 {
let pixel_val = Fixed::from_f32(pixels[y * 4 + x][c] as f32);
row_sum = row_sum + pixel_val * weights[x];
}
sum = sum + row_sum * weights_y[y];
}
result[c] = sum.to_int().clamp(0, 255) as u8;
}
result
}
fn cubic_weights(t: Fixed) -> [Fixed; 4] {
let t2 = t * t;
let t3 = t2 * t;
let half = Fixed::HALF;
[
-half * t3 + t2 - half * t,
Fixed::from_f32(1.5) * t3 - Fixed::from_f32(2.5) * t2 + Fixed::ONE,
-Fixed::from_f32(1.5) * t3 + Fixed::from_int(2) * t2 + half * t,
half * t3 - half * t2,
]
}
}
fn atan2_fixed(y: Fixed, x: Fixed) -> Fixed {
if x == Fixed::ZERO {
if y > Fixed::ZERO {
return Fixed::PI / Fixed::from_int(2);
} else if y < Fixed::ZERO {
return -Fixed::PI / Fixed::from_int(2);
} else {
return Fixed::ZERO;
}
}
let ratio = y / x;
let atan_val = atan_approx(ratio);
if x > Fixed::ZERO {
atan_val
} else if y >= Fixed::ZERO {
atan_val + Fixed::PI
} else {
atan_val - Fixed::PI
}
}
fn atan_approx(x: Fixed) -> Fixed {
let abs_x = x.abs();
if abs_x > Fixed::ONE {
let recip = Fixed::ONE / abs_x;
let atan_recip = atan_approx(recip);
let result = Fixed::PI / Fixed::from_int(2) - atan_recip;
if x < Fixed::ZERO {
-result
} else {
result
}
} else {
let x2 = x * x;
let x3 = x2 * x;
let x5 = x3 * x2;
let x7 = x5 * x2;
x - x3 / Fixed::from_int(3) + x5 / Fixed::from_int(5) - x7 / Fixed::from_int(7)
}
}
fn exp_fixed(x: Fixed) -> Fixed {
let x_clamped = x.clamp(Fixed::from_int(-10), Fixed::from_int(10));
if x_clamped < Fixed::ZERO {
let pos_exp = exp_fixed_positive(-x_clamped);
if pos_exp == Fixed::ZERO {
return Fixed::ZERO;
}
return Fixed::ONE / pos_exp;
}
exp_fixed_positive(x_clamped)
}
fn exp_fixed_positive(x: Fixed) -> Fixed {
let x2 = x * x;
let x3 = x2 * x;
let x4 = x3 * x;
let x5 = x4 * x;
Fixed::ONE + x
+ x2 / Fixed::from_int(2)
+ x3 / Fixed::from_int(6)
+ x4 / Fixed::from_int(24)
+ x5 / Fixed::from_int(120)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vortex_velocity() {
let vortex = Vortex::new(100, 100, 100, 50, 128);
let (u, v) = vortex.velocity_at(Fixed::from(100), Fixed::from(100));
assert!(u.abs() < Fixed::from_int(1));
assert!(v.abs() < Fixed::from_int(1));
let (u, v) = vortex.velocity_at(Fixed::from(120), Fixed::from(100));
assert!(u.abs() > Fixed::ZERO || v.abs() > Fixed::ZERO);
}
#[test]
fn test_radial_basis() {
let basis = RadialBasis::new(
Fixed::from_int(1),
Fixed::ZERO,
Fixed::ZERO,
Fixed::ZERO,
);
let center = Fixed::from(50);
let (u, v) = basis.velocity_at(
Fixed::from(60),
Fixed::from(50),
center,
center,
);
assert!(u > Fixed::ZERO);
}
#[test]
fn test_warp_field() {
let mut field = WarpField::new(200, 200);
field.add_vortex(Vortex::new(100, 100, 50, 30, 128));
let (u, v) = field.velocity_at(120, 100);
assert!(u.abs() > Fixed::ZERO || v.abs() > Fixed::ZERO);
}
#[test]
fn test_warp_backwards() {
let mut field = WarpField::new(200, 200);
field.add_vortex(Vortex::new(100, 100, 500, 50, 50));
let (source_x, source_y) = field.warp_backwards(120, 100);
let target_x = Fixed::from(120);
let target_y = Fixed::from(100);
let dx = if source_x > target_x {
source_x - target_x
} else {
target_x - source_x
};
let dy = if source_y > target_y {
source_y - target_y
} else {
target_y - source_y
};
assert!(
dx > Fixed::from_f32(0.1) || dy > Fixed::from_f32(0.1),
"Expected warp to have visible effect, dx={}, dy={}",
dx.to_f32(),
dy.to_f32()
);
}
#[test]
fn test_bicubic_sampler() {
let width = 10;
let height = 10;
let mut image = vec![[100u8; 3]; width * height];
image[0] = [255, 0, 0];
image[width - 1] = [0, 255, 0];
image[(height - 1) * width] = [0, 0, 255];
let pixel = BicubicSampler::sample(
&image,
width,
height,
Fixed::from_int(0),
Fixed::from_int(0),
);
assert_eq!(pixel[0], 255);
let pixel = BicubicSampler::sample(
&image,
width,
height,
Fixed::from_f32(0.5),
Fixed::from_f32(0.5),
);
assert!(pixel[0] > 100 && pixel[0] < 255);
}
#[test]
fn test_atan2_approximation() {
let test_cases = vec![
(1.0, 1.0, std::f32::consts::PI / 4.0), (1.0, 0.0, std::f32::consts::PI / 2.0), (0.0, 1.0, 0.0), (-1.0, 1.0, -std::f32::consts::PI / 4.0), ];
for (y, x, expected) in test_cases {
let y_fixed = Fixed::from_f32(y);
let x_fixed = Fixed::from_f32(x);
let result = atan2_fixed(y_fixed, x_fixed);
assert!(
(result.to_f32() - expected).abs() < 0.1,
"atan2({}, {}) = {}, expected {}",
y,
x,
result.to_f32(),
expected
);
}
}
#[test]
fn test_exp_approximation() {
let test_cases = vec![
(0.0, 1.0),
(1.0, std::f32::consts::E),
(-1.0, 1.0 / std::f32::consts::E),
(2.0, std::f32::consts::E * std::f32::consts::E),
];
for (input, expected) in test_cases {
let input_fixed = Fixed::from_f32(input);
let result = exp_fixed(input_fixed);
let error = (result.to_f32() - expected).abs() / expected;
assert!(error < 0.1, "exp({}) = {}, expected {}", input, result.to_f32(), expected);
}
}
#[test]
fn test_four_fold_symmetry() {
let basis = RadialBasis::new(
Fixed::ZERO,
Fixed::ZERO,
Fixed::ONE,
Fixed::ZERO,
);
let center = Fixed::from(50);
let points = [
(60, 50), (50, 60), (40, 50), (50, 40), ];
let mut velocities = Vec::new();
for (x, y) in &points {
let (u, v) = basis.velocity_at(
Fixed::from(*x),
Fixed::from(*y),
center,
center,
);
velocities.push((u, v));
}
let magnitudes: Vec<Fixed> = velocities
.iter()
.map(|(u, v)| (*u * *u + *v * *v).sqrt().unwrap_or(Fixed::ZERO))
.collect();
for i in 1..magnitudes.len() {
let ratio = magnitudes[i] / magnitudes[0];
assert!(ratio > Fixed::from_f32(0.8) && ratio < Fixed::from_f32(1.2));
}
}
}