1use crate::fixed::Fixed;
35use serde::{Deserialize, Serialize};
36
37#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
39pub struct Vortex {
40 pub x: u16,
42 pub y: u16,
44 pub strength: i16,
46 pub radius: u8,
48 pub decay: u8,
50}
51
52impl Vortex {
53 pub fn new(x: u16, y: u16, strength: i16, radius: u8, decay: u8) -> Self {
55 Vortex {
56 x,
57 y,
58 strength,
59 radius,
60 decay,
61 }
62 }
63
64 pub fn velocity_at(&self, x: Fixed, y: Fixed) -> (Fixed, Fixed) {
66 let dx = x - Fixed::from(self.x);
68 let dy = y - Fixed::from(self.y);
69
70 let r_sq = dx * dx + dy * dy;
72 let r = r_sq.sqrt().unwrap_or(Fixed::ZERO);
73
74 if r < Fixed::ONE {
76 return (Fixed::ZERO, Fixed::ZERO);
77 }
78
79 let theta = atan2_fixed(dy, dx);
81
82 let decay_fixed = Fixed::from_f32(self.decay as f32 / 255.0);
84 let radius_fixed = Fixed::from_int(self.radius as i32);
85 let falloff_arg = -(decay_fixed * r / radius_fixed);
86 let falloff = exp_fixed(falloff_arg);
87
88 let strength_fixed = Fixed::from_f32(self.strength as f32 / 100.0);
90
91 let u = strength_fixed * theta.sin() * falloff;
95 let v = -strength_fixed * theta.cos() * falloff;
96
97 (u, v)
98 }
99}
100
101#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
103pub struct RadialBasis {
104 pub coeffs: [Fixed; 4],
106}
107
108impl RadialBasis {
109 pub fn zero() -> Self {
111 RadialBasis {
112 coeffs: [Fixed::ZERO; 4],
113 }
114 }
115
116 pub fn new(c1: Fixed, c2: Fixed, c3: Fixed, c4: Fixed) -> Self {
118 RadialBasis {
119 coeffs: [c1, c2, c3, c4],
120 }
121 }
122
123 pub fn velocity_at(&self, x: Fixed, y: Fixed, center_x: Fixed, center_y: Fixed) -> (Fixed, Fixed) {
125 let dx = x - center_x;
127 let dy = y - center_y;
128
129 let r_sq = dx * dx + dy * dy;
131 let r = r_sq.sqrt().unwrap_or(Fixed::ZERO);
132 let theta = atan2_fixed(dy, dx);
133
134 let theta_4 = theta * Fixed::from_int(4);
136
137 let psi1_x = r * theta.cos();
140 let psi1_y = Fixed::ZERO;
141
142 let psi2_x = Fixed::ZERO;
144 let psi2_y = r * theta.sin();
145
146 let psi3_x = r * theta_4.cos();
148 let psi3_y = Fixed::ZERO;
149
150 let psi4_x = Fixed::ZERO;
152 let psi4_y = r * theta_4.sin();
153
154 let u = self.coeffs[0] * psi1_x + self.coeffs[1] * psi2_x
156 + self.coeffs[2] * psi3_x + self.coeffs[3] * psi4_x;
157 let v = self.coeffs[0] * psi1_y + self.coeffs[1] * psi2_y
158 + self.coeffs[2] * psi3_y + self.coeffs[3] * psi4_y;
159
160 (u, v)
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct WarpField {
167 pub width: u32,
169 pub height: u32,
170 pub global_basis: RadialBasis,
172 pub center_x: u16,
174 pub center_y: u16,
175 pub vortices: Vec<Vortex>,
177}
178
179impl WarpField {
180 pub fn new(width: u32, height: u32) -> Self {
182 WarpField {
183 width,
184 height,
185 global_basis: RadialBasis::zero(),
186 center_x: (width / 2) as u16,
187 center_y: (height / 2) as u16,
188 vortices: Vec::new(),
189 }
190 }
191
192 pub fn add_vortex(&mut self, vortex: Vortex) {
194 self.vortices.push(vortex);
195 }
196
197 pub fn velocity_at(&self, x: u16, y: u16) -> (Fixed, Fixed) {
199 let x_fixed = Fixed::from(x);
200 let y_fixed = Fixed::from(y);
201
202 let (u_global, v_global) = self.global_basis.velocity_at(
204 x_fixed,
205 y_fixed,
206 Fixed::from(self.center_x),
207 Fixed::from(self.center_y),
208 );
209
210 let mut u_total = u_global;
212 let mut v_total = v_global;
213
214 for vortex in &self.vortices {
215 let (u_vortex, v_vortex) = vortex.velocity_at(x_fixed, y_fixed);
216 u_total = u_total + u_vortex;
217 v_total = v_total + v_vortex;
218 }
219
220 (u_total, v_total)
221 }
222
223 pub fn warp_backwards(&self, x: u16, y: u16) -> (Fixed, Fixed) {
225 let (u, v) = self.velocity_at(x, y);
226
227 let x_fixed = Fixed::from(x);
229 let y_fixed = Fixed::from(y);
230
231 (x_fixed - u, y_fixed - v)
232 }
233}
234
235pub struct BicubicSampler;
237
238impl BicubicSampler {
239 pub fn sample(image: &[[u8; 3]], width: usize, height: usize, x: Fixed, y: Fixed) -> [u8; 3] {
241 let xi = x.floor().to_int().clamp(0, width as i32 - 1) as usize;
243 let yi = y.floor().to_int().clamp(0, height as i32 - 1) as usize;
244
245 let xf = x.fract();
247 let yf = y.fract();
248
249 let mut pixels = [[0u8; 3]; 16];
251 for dy in 0..4 {
252 for dx in 0..4 {
253 let sample_x = (xi + dx).saturating_sub(1).min(width - 1);
254 let sample_y = (yi + dy).saturating_sub(1).min(height - 1);
255 let idx = sample_y * width + sample_x;
256 if idx < image.len() {
257 pixels[dy * 4 + dx] = image[idx];
258 }
259 }
260 }
261
262 Self::bicubic_interpolate(&pixels, xf, yf)
264 }
265
266 fn bicubic_interpolate(pixels: &[[u8; 3]; 16], xf: Fixed, yf: Fixed) -> [u8; 3] {
268 let mut result = [0u8; 3];
269
270 let weights = Self::cubic_weights(xf);
272 let weights_y = Self::cubic_weights(yf);
273
274 for c in 0..3 {
275 let mut sum = Fixed::ZERO;
276
277 for y in 0..4 {
278 let mut row_sum = Fixed::ZERO;
279 for x in 0..4 {
280 let pixel_val = Fixed::from_f32(pixels[y * 4 + x][c] as f32);
281 row_sum = row_sum + pixel_val * weights[x];
282 }
283 sum = sum + row_sum * weights_y[y];
284 }
285
286 result[c] = sum.to_int().clamp(0, 255) as u8;
287 }
288
289 result
290 }
291
292 fn cubic_weights(t: Fixed) -> [Fixed; 4] {
294 let t2 = t * t;
296 let t3 = t2 * t;
297
298 let half = Fixed::HALF;
299
300 [
301 -half * t3 + t2 - half * t,
302 Fixed::from_f32(1.5) * t3 - Fixed::from_f32(2.5) * t2 + Fixed::ONE,
303 -Fixed::from_f32(1.5) * t3 + Fixed::from_int(2) * t2 + half * t,
304 half * t3 - half * t2,
305 ]
306 }
307}
308
309fn atan2_fixed(y: Fixed, x: Fixed) -> Fixed {
311 if x == Fixed::ZERO {
313 if y > Fixed::ZERO {
314 return Fixed::PI / Fixed::from_int(2);
315 } else if y < Fixed::ZERO {
316 return -Fixed::PI / Fixed::from_int(2);
317 } else {
318 return Fixed::ZERO;
319 }
320 }
321
322 let ratio = y / x;
324 let atan_val = atan_approx(ratio);
325
326 if x > Fixed::ZERO {
328 atan_val
329 } else if y >= Fixed::ZERO {
330 atan_val + Fixed::PI
331 } else {
332 atan_val - Fixed::PI
333 }
334}
335
336fn atan_approx(x: Fixed) -> Fixed {
338 let abs_x = x.abs();
342
343 if abs_x > Fixed::ONE {
344 let recip = Fixed::ONE / abs_x;
346 let atan_recip = atan_approx(recip);
347 let result = Fixed::PI / Fixed::from_int(2) - atan_recip;
348
349 if x < Fixed::ZERO {
350 -result
351 } else {
352 result
353 }
354 } else {
355 let x2 = x * x;
357 let x3 = x2 * x;
358 let x5 = x3 * x2;
359 let x7 = x5 * x2;
360
361 x - x3 / Fixed::from_int(3) + x5 / Fixed::from_int(5) - x7 / Fixed::from_int(7)
362 }
363}
364
365fn exp_fixed(x: Fixed) -> Fixed {
367 let x_clamped = x.clamp(Fixed::from_int(-10), Fixed::from_int(10));
369
370 if x_clamped < Fixed::ZERO {
372 let pos_exp = exp_fixed_positive(-x_clamped);
373 if pos_exp == Fixed::ZERO {
375 return Fixed::ZERO;
376 }
377 return Fixed::ONE / pos_exp;
378 }
379
380 exp_fixed_positive(x_clamped)
381}
382
383fn exp_fixed_positive(x: Fixed) -> Fixed {
385 let x2 = x * x;
387 let x3 = x2 * x;
388 let x4 = x3 * x;
389 let x5 = x4 * x;
390
391 Fixed::ONE + x
392 + x2 / Fixed::from_int(2)
393 + x3 / Fixed::from_int(6)
394 + x4 / Fixed::from_int(24)
395 + x5 / Fixed::from_int(120)
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn test_vortex_velocity() {
404 let vortex = Vortex::new(100, 100, 100, 50, 128);
405
406 let (u, v) = vortex.velocity_at(Fixed::from(100), Fixed::from(100));
408 assert!(u.abs() < Fixed::from_int(1));
409 assert!(v.abs() < Fixed::from_int(1));
410
411 let (u, v) = vortex.velocity_at(Fixed::from(120), Fixed::from(100));
413 assert!(u.abs() > Fixed::ZERO || v.abs() > Fixed::ZERO);
414 }
415
416 #[test]
417 fn test_radial_basis() {
418 let basis = RadialBasis::new(
419 Fixed::from_int(1),
420 Fixed::ZERO,
421 Fixed::ZERO,
422 Fixed::ZERO,
423 );
424
425 let center = Fixed::from(50);
426 let (u, v) = basis.velocity_at(
427 Fixed::from(60),
428 Fixed::from(50),
429 center,
430 center,
431 );
432
433 assert!(u > Fixed::ZERO);
435 }
436
437 #[test]
438 fn test_warp_field() {
439 let mut field = WarpField::new(200, 200);
440
441 field.add_vortex(Vortex::new(100, 100, 50, 30, 128));
442
443 let (u, v) = field.velocity_at(120, 100);
444
445 assert!(u.abs() > Fixed::ZERO || v.abs() > Fixed::ZERO);
447 }
448
449 #[test]
450 fn test_warp_backwards() {
451 let mut field = WarpField::new(200, 200);
452
453 field.add_vortex(Vortex::new(100, 100, 500, 50, 50));
455
456 let (source_x, source_y) = field.warp_backwards(120, 100);
457
458 let target_x = Fixed::from(120);
460 let target_y = Fixed::from(100);
461
462 let dx = if source_x > target_x {
463 source_x - target_x
464 } else {
465 target_x - source_x
466 };
467
468 let dy = if source_y > target_y {
469 source_y - target_y
470 } else {
471 target_y - source_y
472 };
473
474 assert!(
476 dx > Fixed::from_f32(0.1) || dy > Fixed::from_f32(0.1),
477 "Expected warp to have visible effect, dx={}, dy={}",
478 dx.to_f32(),
479 dy.to_f32()
480 );
481 }
482
483 #[test]
484 fn test_bicubic_sampler() {
485 let width = 10;
487 let height = 10;
488 let mut image = vec![[100u8; 3]; width * height];
489
490 image[0] = [255, 0, 0];
492 image[width - 1] = [0, 255, 0];
493 image[(height - 1) * width] = [0, 0, 255];
494
495 let pixel = BicubicSampler::sample(
497 &image,
498 width,
499 height,
500 Fixed::from_int(0),
501 Fixed::from_int(0),
502 );
503 assert_eq!(pixel[0], 255);
504
505 let pixel = BicubicSampler::sample(
507 &image,
508 width,
509 height,
510 Fixed::from_f32(0.5),
511 Fixed::from_f32(0.5),
512 );
513 assert!(pixel[0] > 100 && pixel[0] < 255);
515 }
516
517 #[test]
518 fn test_atan2_approximation() {
519 let test_cases = vec![
520 (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), ];
525
526 for (y, x, expected) in test_cases {
527 let y_fixed = Fixed::from_f32(y);
528 let x_fixed = Fixed::from_f32(x);
529 let result = atan2_fixed(y_fixed, x_fixed);
530
531 assert!(
533 (result.to_f32() - expected).abs() < 0.1,
534 "atan2({}, {}) = {}, expected {}",
535 y,
536 x,
537 result.to_f32(),
538 expected
539 );
540 }
541 }
542
543 #[test]
544 fn test_exp_approximation() {
545 let test_cases = vec![
546 (0.0, 1.0),
547 (1.0, std::f32::consts::E),
548 (-1.0, 1.0 / std::f32::consts::E),
549 (2.0, std::f32::consts::E * std::f32::consts::E),
550 ];
551
552 for (input, expected) in test_cases {
553 let input_fixed = Fixed::from_f32(input);
554 let result = exp_fixed(input_fixed);
555
556 let error = (result.to_f32() - expected).abs() / expected;
558 assert!(error < 0.1, "exp({}) = {}, expected {}", input, result.to_f32(), expected);
559 }
560 }
561
562 #[test]
563 fn test_four_fold_symmetry() {
564 let basis = RadialBasis::new(
565 Fixed::ZERO,
566 Fixed::ZERO,
567 Fixed::ONE,
568 Fixed::ZERO,
569 );
570
571 let center = Fixed::from(50);
572
573 let points = [
575 (60, 50), (50, 60), (40, 50), (50, 40), ];
580
581 let mut velocities = Vec::new();
582 for (x, y) in &points {
583 let (u, v) = basis.velocity_at(
584 Fixed::from(*x),
585 Fixed::from(*y),
586 center,
587 center,
588 );
589 velocities.push((u, v));
590 }
591
592 let magnitudes: Vec<Fixed> = velocities
594 .iter()
595 .map(|(u, v)| (*u * *u + *v * *v).sqrt().unwrap_or(Fixed::ZERO))
596 .collect();
597
598 for i in 1..magnitudes.len() {
599 let ratio = magnitudes[i] / magnitudes[0];
600 assert!(ratio > Fixed::from_f32(0.8) && ratio < Fixed::from_f32(1.2));
602 }
603 }
604}