tx2_iff/
warp.rs

1//! Symplectic warping field (Layer 3: The Warping Field)
2//!
3//! This module implements symplectic spatial advection using 4-fold radial
4//! symmetry and vortex primitives. Instead of storing texture for every fold
5//! in fabric, we store the texture of a "flat" patch and a vector field that
6//! describes how to warp it.
7//!
8//! ## Mathematical Foundation
9//!
10//! The warping field preserves area (incompressibility):
11//! ```text
12//! ∇·u = 0  (divergence-free constraint)
13//! ```
14//!
15//! Implemented via stream function:
16//! ```text
17//! u = ∂ψ/∂y
18//! v = -∂ψ/∂x
19//! ```
20//!
21//! ## 4-Fold Radial Basis
22//!
23//! Flow field decomposition:
24//! ```text
25//! u(x,y) = ∑ₖ₌₁⁴ cₖ ψₖ(r,θ)
26//! ```
27//!
28//! Where:
29//! - ψ₁(r,θ) = r cos(θ)     - Radial outward
30//! - ψ₂(r,θ) = r sin(θ)     - Radial rotated 90°
31//! - ψ₃(r,θ) = r cos(4θ)    - 4-fold symmetric
32//! - ψ₄(r,θ) = r sin(4θ)    - 4-fold antisymmetric
33
34use crate::fixed::Fixed;
35use serde::{Deserialize, Serialize};
36
37/// Vortex primitive for local warping
38#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
39pub struct Vortex {
40    /// Center position X
41    pub x: u16,
42    /// Center position Y
43    pub y: u16,
44    /// Angular velocity (signed for rotation direction)
45    pub strength: i16,
46    /// Influence radius
47    pub radius: u8,
48    /// Exponential decay rate
49    pub decay: u8,
50}
51
52impl Vortex {
53    /// Create a new vortex
54    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    /// Calculate velocity field contribution at point (x, y)
65    pub fn velocity_at(&self, x: Fixed, y: Fixed) -> (Fixed, Fixed) {
66        // Vector from vortex center to point
67        let dx = x - Fixed::from(self.x);
68        let dy = y - Fixed::from(self.y);
69
70        // Distance from center
71        let r_sq = dx * dx + dy * dy;
72        let r = r_sq.sqrt().unwrap_or(Fixed::ZERO);
73
74        // Avoid singularity at center
75        if r < Fixed::ONE {
76            return (Fixed::ZERO, Fixed::ZERO);
77        }
78
79        // Angle
80        let theta = atan2_fixed(dy, dx);
81
82        // Falloff function: exp(-decay * r / radius)
83        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        // Vortex strength
89        let strength_fixed = Fixed::from_f32(self.strength as f32 / 100.0);
90
91        // Velocity field for vortex (perpendicular to radial direction)
92        // u = strength × sin(θ) × falloff
93        // v = -strength × cos(θ) × falloff
94        let u = strength_fixed * theta.sin() * falloff;
95        let v = -strength_fixed * theta.cos() * falloff;
96
97        (u, v)
98    }
99}
100
101/// 4-fold radial basis coefficients
102#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
103pub struct RadialBasis {
104    /// Coefficients for the 4 basis functions
105    pub coeffs: [Fixed; 4],
106}
107
108impl RadialBasis {
109    /// Create new radial basis with zero coefficients
110    pub fn zero() -> Self {
111        RadialBasis {
112            coeffs: [Fixed::ZERO; 4],
113        }
114    }
115
116    /// Create from raw coefficients
117    pub fn new(c1: Fixed, c2: Fixed, c3: Fixed, c4: Fixed) -> Self {
118        RadialBasis {
119            coeffs: [c1, c2, c3, c4],
120        }
121    }
122
123    /// Evaluate flow field at point (x, y) relative to center
124    pub fn velocity_at(&self, x: Fixed, y: Fixed, center_x: Fixed, center_y: Fixed) -> (Fixed, Fixed) {
125        // Relative coordinates
126        let dx = x - center_x;
127        let dy = y - center_y;
128
129        // Polar coordinates
130        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        // Four-fold symmetric angle
135        let theta_4 = theta * Fixed::from_int(4);
136
137        // Basis functions
138        // ψ₁(r,θ) = r cos(θ)
139        let psi1_x = r * theta.cos();
140        let psi1_y = Fixed::ZERO;
141
142        // ψ₂(r,θ) = r sin(θ)
143        let psi2_x = Fixed::ZERO;
144        let psi2_y = r * theta.sin();
145
146        // ψ₃(r,θ) = r cos(4θ)
147        let psi3_x = r * theta_4.cos();
148        let psi3_y = Fixed::ZERO;
149
150        // ψ₄(r,θ) = r sin(4θ)
151        let psi4_x = Fixed::ZERO;
152        let psi4_y = r * theta_4.sin();
153
154        // Combine with coefficients
155        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/// Warp field combining global radial basis and local vortices
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct WarpField {
167    /// Image dimensions
168    pub width: u32,
169    pub height: u32,
170    /// Global radial basis (4 coefficients)
171    pub global_basis: RadialBasis,
172    /// Center point for global basis
173    pub center_x: u16,
174    pub center_y: u16,
175    /// Local vortex primitives
176    pub vortices: Vec<Vortex>,
177}
178
179impl WarpField {
180    /// Create new warp field
181    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    /// Add a vortex
193    pub fn add_vortex(&mut self, vortex: Vortex) {
194        self.vortices.push(vortex);
195    }
196
197    /// Calculate total velocity field at point (x, y)
198    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        // Global contribution
203        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        // Local vortex contributions
211        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    /// Apply warping to get source coordinates (backtracing)
224    pub fn warp_backwards(&self, x: u16, y: u16) -> (Fixed, Fixed) {
225        let (u, v) = self.velocity_at(x, y);
226
227        // Source position = current position - velocity
228        let x_fixed = Fixed::from(x);
229        let y_fixed = Fixed::from(y);
230
231        (x_fixed - u, y_fixed - v)
232    }
233}
234
235/// Bicubic interpolation for warped sampling
236pub struct BicubicSampler;
237
238impl BicubicSampler {
239    /// Sample image at fixed-point coordinates using bicubic interpolation
240    pub fn sample(image: &[[u8; 3]], width: usize, height: usize, x: Fixed, y: Fixed) -> [u8; 3] {
241        // Get integer coordinates
242        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        // Get fractional parts
246        let xf = x.fract();
247        let yf = y.fract();
248
249        // Sample 4x4 neighborhood
250        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        // Bicubic weights: [-1, 9, 9, -1] / 16 for each dimension
263        Self::bicubic_interpolate(&pixels, xf, yf)
264    }
265
266    /// Bicubic interpolation kernel
267    fn bicubic_interpolate(pixels: &[[u8; 3]; 16], xf: Fixed, yf: Fixed) -> [u8; 3] {
268        let mut result = [0u8; 3];
269
270        // Weights for bicubic kernel
271        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    /// Calculate cubic interpolation weights
293    fn cubic_weights(t: Fixed) -> [Fixed; 4] {
294        // Catmull-Rom spline weights
295        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
309/// Fixed-point atan2 approximation
310fn atan2_fixed(y: Fixed, x: Fixed) -> Fixed {
311    // Special cases
312    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    // Use atan(y/x) approximation
323    let ratio = y / x;
324    let atan_val = atan_approx(ratio);
325
326    // Adjust for quadrant
327    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
336/// Fixed-point atan approximation using polynomial
337fn atan_approx(x: Fixed) -> Fixed {
338    // atan(x) ≈ x - x³/3 + x⁵/5 - x⁷/7 (for small x)
339    // For larger x, use atan(x) = π/2 - atan(1/x)
340
341    let abs_x = x.abs();
342
343    if abs_x > Fixed::ONE {
344        // Use identity: atan(x) = π/2 - atan(1/x)
345        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        // Taylor series
356        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
365/// Fixed-point exponential approximation
366fn exp_fixed(x: Fixed) -> Fixed {
367    // Clamp to reasonable range
368    let x_clamped = x.clamp(Fixed::from_int(-10), Fixed::from_int(10));
369
370    // For negative values, use exp(-x) = 1/exp(x)
371    if x_clamped < Fixed::ZERO {
372        let pos_exp = exp_fixed_positive(-x_clamped);
373        // Avoid division by zero
374        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
383/// Helper for positive exponent using Taylor series
384fn exp_fixed_positive(x: Fixed) -> Fixed {
385    // exp(x) ≈ 1 + x + x²/2 + x³/6 + x⁴/24 + x⁵/120
386    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        // At center, velocity should be near zero
407        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        // Away from center, should have non-zero velocity
412        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        // With only first coefficient, should have radial outward flow
434        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        // Should have velocity from vortex
446        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        // Add a vortex with proper parameters (lower decay = wider influence)
454        field.add_vortex(Vortex::new(100, 100, 500, 50, 50));
455
456        let (source_x, source_y) = field.warp_backwards(120, 100);
457
458        // Source should be different from target due to vortex warping
459        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        // At least one dimension should show warp effect
475        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        // Create a simple test image
486        let width = 10;
487        let height = 10;
488        let mut image = vec![[100u8; 3]; width * height];
489
490        // Set a few pixels to different colors
491        image[0] = [255, 0, 0];
492        image[width - 1] = [0, 255, 0];
493        image[(height - 1) * width] = [0, 0, 255];
494
495        // Sample at integer coordinate
496        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        // Sample at fractional coordinate
506        let pixel = BicubicSampler::sample(
507            &image,
508            width,
509            height,
510            Fixed::from_f32(0.5),
511            Fixed::from_f32(0.5),
512        );
513        // Should be interpolated value
514        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),      // atan2(1, 1) = π/4
521            (1.0, 0.0, std::f32::consts::PI / 2.0),      // atan2(1, 0) = π/2
522            (0.0, 1.0, 0.0),                              // atan2(0, 1) = 0
523            (-1.0, 1.0, -std::f32::consts::PI / 4.0),    // atan2(-1, 1) = -π/4
524        ];
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            // Allow 10% error in approximation
532            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            // Allow 10% error
557            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        // Test at 4 symmetric points (every 90 degrees)
574        let points = [
575            (60, 50), // 0 degrees
576            (50, 60), // 90 degrees
577            (40, 50), // 180 degrees
578            (50, 40), // 270 degrees
579        ];
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        // With 4-fold symmetry, magnitudes should be similar
593        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            // Should be within 20% of each other
601            assert!(ratio > Fixed::from_f32(0.8) && ratio < Fixed::from_f32(1.2));
602        }
603    }
604}