1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
//! Distortion and warp effects operating on normalised UV coordinates [0.0, 1.0].
//!
//! These effects remap UV coordinates; the caller samples the source image at
//! the returned coordinates to produce the distorted output.
#![allow(dead_code)]
/// Axis for the mirror effect.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MirrorAxis {
/// Mirror left→right (flip on the vertical axis).
Horizontal,
/// Mirror top→bottom (flip on the horizontal axis).
Vertical,
/// Mirror on both axes.
Both,
}
/// Sine-wave distortion applied along one axis.
#[derive(Debug, Clone)]
pub struct WaveDistortion {
/// Pixel-space amplitude of the wave (in UV units, e.g. 0.05).
pub amplitude: f32,
/// Number of full cycles across the image width (or height).
pub frequency: f32,
/// Phase offset in radians.
pub phase: f32,
/// If `true`, the wave displaces horizontally; otherwise vertically.
pub horizontal: bool,
}
impl WaveDistortion {
/// Compute the signed UV displacement for a pixel at normalised position `pos`
/// along the axis perpendicular to the displacement direction.
#[must_use]
pub fn offset_at(&self, pos: f32) -> f32 {
use std::f32::consts::TAU;
self.amplitude * (TAU * self.frequency * pos + self.phase).sin()
}
/// Apply the wave to a UV coordinate pair and return the warped UV.
#[must_use]
pub fn apply_uv(&self, u: f32, v: f32) -> (f32, f32) {
if self.horizontal {
(u + self.offset_at(v), v)
} else {
(u, v + self.offset_at(u))
}
}
}
/// Barrel/pincushion lens distortion.
///
/// Positive `strength` produces barrel distortion; negative produces pincushion.
#[derive(Debug, Clone)]
pub struct BarrelDistortion {
/// Distortion strength. Typical range: [−1.0, 1.0].
pub strength: f32,
}
impl BarrelDistortion {
/// Remap normalised UV coordinates `(u, v)` using the barrel distortion model.
///
/// UV coordinates are expected in [0.0, 1.0]; values outside that range may
/// map to off-screen pixels.
#[must_use]
pub fn distort_uv(&self, u: f32, v: f32) -> (f32, f32) {
// Work in [-1, 1] centred space
let cx = u * 2.0 - 1.0;
let cy = v * 2.0 - 1.0;
let r2 = cx * cx + cy * cy;
let factor = 1.0 + self.strength * r2;
let cx2 = cx * factor;
let cy2 = cy * factor;
// Back to [0, 1]
((cx2 + 1.0) * 0.5, (cy2 + 1.0) * 0.5)
}
}
/// Twirl effect: rotates pixels by an angle proportional to their distance
/// from the image centre.
#[derive(Debug, Clone)]
pub struct TwirlEffect {
/// Maximum rotation angle in degrees (applied at the centre).
pub angle_deg: f32,
/// Radius of effect (in UV units from centre, typically 0.5 for half the image).
pub radius: f32,
}
impl TwirlEffect {
/// Apply twirl to normalised UV coordinates.
#[must_use]
pub fn apply_uv(&self, u: f32, v: f32) -> (f32, f32) {
let cx = u - 0.5;
let cy = v - 0.5;
let dist = (cx * cx + cy * cy).sqrt();
if self.radius <= 0.0 || dist >= self.radius {
return (u, v);
}
// Rotation angle proportional to (1 - normalised distance)
let t = 1.0 - (dist / self.radius).clamp(0.0, 1.0);
let angle_rad = self.angle_deg.to_radians() * t;
let cos_a = angle_rad.cos();
let sin_a = angle_rad.sin();
let rx = cx * cos_a - cy * sin_a;
let ry = cx * sin_a + cy * cos_a;
(rx + 0.5, ry + 0.5)
}
}
/// Mirror (flip) effect along one or both axes.
#[derive(Debug, Clone)]
pub struct MirrorEffect {
/// Which axis to mirror around.
pub axis: MirrorAxis,
}
impl MirrorEffect {
/// Apply the mirror transform to normalised UV coordinates.
#[must_use]
pub fn apply_uv(&self, u: f32, v: f32) -> (f32, f32) {
match self.axis {
MirrorAxis::Horizontal => (1.0 - u, v),
MirrorAxis::Vertical => (u, 1.0 - v),
MirrorAxis::Both => (1.0 - u, 1.0 - v),
}
}
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
// ── WaveDistortion ────────────────────────────────────────────────────────
#[test]
fn test_wave_zero_amplitude() {
let w = WaveDistortion {
amplitude: 0.0,
frequency: 2.0,
phase: 0.0,
horizontal: true,
};
assert!((w.offset_at(0.5)).abs() < 1e-6);
}
#[test]
fn test_wave_max_offset() {
let w = WaveDistortion {
amplitude: 0.1,
frequency: 0.25, // pos=1.0 → 0.25 full cycles → 90° → sin=1
phase: 0.0,
horizontal: true,
};
// At pos=1.0: sin(TAU * 0.25 * 1.0) = sin(π/2) = 1.0
let offset = w.offset_at(1.0);
assert!((offset - 0.1).abs() < 1e-5, "offset: {offset}");
}
#[test]
fn test_wave_horizontal_modifies_u() {
let w = WaveDistortion {
amplitude: 0.05,
frequency: 1.0,
phase: 0.0,
horizontal: true,
};
let (u2, v2) = w.apply_uv(0.5, 0.25);
assert!((v2 - 0.25).abs() < 1e-6, "V should not change: {v2}");
assert!((u2 - 0.5).abs() > 1e-6 || true, "U may change"); // sine may be 0
}
#[test]
fn test_wave_vertical_modifies_v() {
let w = WaveDistortion {
amplitude: 0.05,
frequency: 1.0,
phase: 0.0,
horizontal: false,
};
let (u2, v2) = w.apply_uv(0.25, 0.5);
assert!((u2 - 0.25).abs() < 1e-6, "U should not change: {u2}");
let _ = v2; // V may have changed
}
// ── BarrelDistortion ──────────────────────────────────────────────────────
#[test]
fn test_barrel_center_unchanged() {
let b = BarrelDistortion { strength: 0.5 };
let (u, v) = b.distort_uv(0.5, 0.5);
assert!((u - 0.5).abs() < 1e-6, "u: {u}");
assert!((v - 0.5).abs() < 1e-6, "v: {v}");
}
#[test]
fn test_barrel_zero_strength_passthrough() {
let b = BarrelDistortion { strength: 0.0 };
for (u, v) in [(0.2, 0.3), (0.8, 0.7), (0.5, 0.5)] {
let (ou, ov) = b.distort_uv(u, v);
assert!((ou - u).abs() < 1e-6, "u: {ou} != {u}");
assert!((ov - v).abs() < 1e-6, "v: {ov} != {v}");
}
}
#[test]
fn test_barrel_positive_pushes_corners_out() {
let b = BarrelDistortion { strength: 0.5 };
// A corner (0, 0) in UV space → centred at (-1, -1)
let (ou, ov) = b.distort_uv(0.0, 0.0);
// Positive strength: corner should move further from centre
assert!(
ou < 0.0 || ov < 0.0,
"corners should move outward: ({ou}, {ov})"
);
}
// ── TwirlEffect ───────────────────────────────────────────────────────────
#[test]
fn test_twirl_center_unchanged() {
let t = TwirlEffect {
angle_deg: 45.0,
radius: 0.5,
};
let (u, v) = t.apply_uv(0.5, 0.5);
// distance = 0 → rotation = angle_deg → but cx=cy=0 so output is still 0.5
assert!((u - 0.5).abs() < 1e-5, "u: {u}");
assert!((v - 0.5).abs() < 1e-5, "v: {v}");
}
#[test]
fn test_twirl_outside_radius_unchanged() {
let t = TwirlEffect {
angle_deg: 90.0,
radius: 0.3,
};
// Point at (0.0, 0.5) → distance from (0.5, 0.5) is 0.5 > radius
let (u, v) = t.apply_uv(0.0, 0.5);
assert!((u - 0.0).abs() < 1e-5, "u unchanged: {u}");
assert!((v - 0.5).abs() < 1e-5, "v unchanged: {v}");
}
#[test]
fn test_twirl_zero_angle_passthrough() {
let t = TwirlEffect {
angle_deg: 0.0,
radius: 0.5,
};
let (u, v) = t.apply_uv(0.7, 0.4);
assert!((u - 0.7).abs() < 1e-5, "u: {u}");
assert!((v - 0.4).abs() < 1e-5, "v: {v}");
}
#[test]
fn test_twirl_zero_radius_passthrough() {
let t = TwirlEffect {
angle_deg: 90.0,
radius: 0.0,
};
let (u, v) = t.apply_uv(0.3, 0.6);
assert!((u - 0.3).abs() < 1e-5, "u: {u}");
assert!((v - 0.6).abs() < 1e-5, "v: {v}");
}
// ── MirrorEffect ──────────────────────────────────────────────────────────
#[test]
fn test_mirror_horizontal() {
let m = MirrorEffect {
axis: MirrorAxis::Horizontal,
};
let (u, v) = m.apply_uv(0.3, 0.7);
assert!((u - 0.7).abs() < 1e-6, "u mirrored: {u}");
assert!((v - 0.7).abs() < 1e-6, "v unchanged: {v}");
}
#[test]
fn test_mirror_vertical() {
let m = MirrorEffect {
axis: MirrorAxis::Vertical,
};
let (u, v) = m.apply_uv(0.3, 0.7);
assert!((u - 0.3).abs() < 1e-6, "u unchanged: {u}");
assert!((v - 0.3).abs() < 1e-6, "v mirrored: {v}");
}
#[test]
fn test_mirror_both() {
let m = MirrorEffect {
axis: MirrorAxis::Both,
};
let (u, v) = m.apply_uv(0.2, 0.8);
assert!((u - 0.8).abs() < 1e-6, "u mirrored: {u}");
assert!((v - 0.2).abs() < 1e-6, "v mirrored: {v}");
}
#[test]
fn test_mirror_center_unchanged() {
for axis in [
MirrorAxis::Horizontal,
MirrorAxis::Vertical,
MirrorAxis::Both,
] {
let m = MirrorEffect { axis };
let (u, v) = m.apply_uv(0.5, 0.5);
assert!((u - 0.5).abs() < 1e-6, "u at center: {u}");
assert!((v - 0.5).abs() < 1e-6, "v at center: {v}");
}
}
}