1use std::f32::consts::PI;
8
9#[derive(Debug, Clone)]
14pub struct ElectricBorder {
15 pub width: f32,
17
18 pub height: f32,
20
21 pub border_radius: f32,
23
24 pub speed: f32,
26
27 pub chaos: f32,
29
30 time: f32,
32
33 sample_count: usize,
35
36 displacement: f32,
38}
39
40impl Default for ElectricBorder {
41 fn default() -> Self {
42 Self::new(400.0, 300.0)
43 }
44}
45
46impl ElectricBorder {
47 pub fn new(width: f32, height: f32) -> Self {
49 let perimeter = 2.0 * (width + height);
50 let sample_count = (perimeter / 2.0) as usize;
51
52 Self {
53 width,
54 height,
55 border_radius: 24.0,
56 speed: 1.0,
57 chaos: 0.12,
58 time: 0.0,
59 sample_count,
60 displacement: 60.0,
61 }
62 }
63
64 pub fn with_radius(mut self, radius: f32) -> Self {
66 self.border_radius = radius;
67 self
68 }
69
70 pub fn with_speed(mut self, speed: f32) -> Self {
72 self.speed = speed;
73 self
74 }
75
76 pub fn with_chaos(mut self, chaos: f32) -> Self {
78 self.chaos = chaos;
79 self
80 }
81
82 pub fn set_dimensions(&mut self, width: f32, height: f32) {
84 self.width = width;
85 self.height = height;
86
87 let perimeter = 2.0 * (width + height);
89 self.sample_count = (perimeter / 2.0) as usize;
90 }
91
92 pub fn update(&mut self, delta_time: f32) {
94 self.time += delta_time * self.speed;
95 }
96
97 pub fn set_time(&mut self, time: f32) {
99 self.time = time;
100 }
101
102 pub fn time(&self) -> f32 {
104 self.time
105 }
106
107 pub fn generate_points(&self) -> Vec<(f32, f32)> {
112 let mut points = Vec::with_capacity(self.sample_count + 1);
113
114 let max_radius = (self.width.min(self.height) / 2.0).min(self.border_radius);
115 let radius = max_radius.max(0.0);
116
117 for i in 0..=self.sample_count {
118 let t = i as f32 / self.sample_count as f32;
119
120 let (base_x, base_y) = self.get_rounded_rect_point(t, radius);
122
123 let (dx, dy) = self.get_displacement(t);
125
126 points.push((base_x + dx, base_y + dy));
127 }
128
129 points
130 }
131
132 fn get_rounded_rect_point(&self, t: f32, radius: f32) -> (f32, f32) {
134 let straight_width = self.width - 2.0 * radius;
135 let straight_height = self.height - 2.0 * radius;
136 let corner_arc = (PI * radius) / 2.0;
137
138 let total_perimeter =
139 2.0 * straight_width + 2.0 * straight_height + 4.0 * corner_arc;
140
141 let distance = t * total_perimeter;
142 let mut accumulated = 0.0;
143
144 if distance <= accumulated + straight_width {
146 let progress = (distance - accumulated) / straight_width;
147 return (radius + progress * straight_width, 0.0);
148 }
149 accumulated += straight_width;
150
151 if distance <= accumulated + corner_arc {
153 let progress = (distance - accumulated) / corner_arc;
154 return self.get_corner_point(
155 self.width - radius,
156 radius,
157 radius,
158 -PI / 2.0,
159 progress,
160 );
161 }
162 accumulated += corner_arc;
163
164 if distance <= accumulated + straight_height {
166 let progress = (distance - accumulated) / straight_height;
167 return (self.width, radius + progress * straight_height);
168 }
169 accumulated += straight_height;
170
171 if distance <= accumulated + corner_arc {
173 let progress = (distance - accumulated) / corner_arc;
174 return self.get_corner_point(
175 self.width - radius,
176 self.height - radius,
177 radius,
178 0.0,
179 progress,
180 );
181 }
182 accumulated += corner_arc;
183
184 if distance <= accumulated + straight_width {
186 let progress = (distance - accumulated) / straight_width;
187 return (
188 self.width - radius - progress * straight_width,
189 self.height,
190 );
191 }
192 accumulated += straight_width;
193
194 if distance <= accumulated + corner_arc {
196 let progress = (distance - accumulated) / corner_arc;
197 return self.get_corner_point(
198 radius,
199 self.height - radius,
200 radius,
201 PI / 2.0,
202 progress,
203 );
204 }
205 accumulated += corner_arc;
206
207 if distance <= accumulated + straight_height {
209 let progress = (distance - accumulated) / straight_height;
210 return (0.0, self.height - radius - progress * straight_height);
211 }
212 accumulated += straight_height;
213
214 let progress = (distance - accumulated) / corner_arc;
216 self.get_corner_point(radius, radius, radius, PI, progress)
217 }
218
219 fn get_corner_point(
221 &self,
222 center_x: f32,
223 center_y: f32,
224 radius: f32,
225 start_angle: f32,
226 progress: f32,
227 ) -> (f32, f32) {
228 let angle = start_angle + progress * (PI / 2.0);
229 (
230 center_x + radius * angle.cos(),
231 center_y + radius * angle.sin(),
232 )
233 }
234
235 fn get_displacement(&self, t: f32) -> (f32, f32) {
237 let octaves = 10;
238 let lacunarity = 1.6;
239 let gain = 0.7;
240 let amplitude = self.chaos;
241 let frequency = 10.0;
242
243 let x_noise = self.octaved_noise(
244 t * 8.0,
245 octaves,
246 lacunarity,
247 gain,
248 amplitude,
249 frequency,
250 self.time,
251 0.0,
252 );
253
254 let y_noise = self.octaved_noise(
255 t * 8.0,
256 octaves,
257 lacunarity,
258 gain,
259 amplitude,
260 frequency,
261 self.time,
262 1.0,
263 );
264
265 let scale = self.displacement;
266 (x_noise * scale, y_noise * scale)
267 }
268
269 #[allow(clippy::too_many_arguments)]
271 fn octaved_noise(
272 &self,
273 x: f32,
274 octaves: usize,
275 lacunarity: f32,
276 gain: f32,
277 base_amplitude: f32,
278 base_frequency: f32,
279 time: f32,
280 seed: f32,
281 ) -> f32 {
282 let mut result = 0.0;
283 let mut amplitude = base_amplitude;
284 let mut frequency = base_frequency;
285
286 for _i in 0..octaves {
287 result += amplitude
288 * self.noise_2d(
289 frequency * x + seed * 100.0,
290 time * frequency * 0.3,
291 );
292 frequency *= lacunarity;
293 amplitude *= gain;
294 }
295
296 result
297 }
298
299 fn noise_2d(&self, x: f32, y: f32) -> f32 {
301 let i = x.floor();
302 let j = y.floor();
303 let fx = x - i;
304 let fy = y - j;
305
306 let a = self.random(i + j * 57.0);
308 let b = self.random(i + 1.0 + j * 57.0);
309 let c = self.random(i + (j + 1.0) * 57.0);
310 let d = self.random(i + 1.0 + (j + 1.0) * 57.0);
311
312 let ux = fx * fx * (3.0 - 2.0 * fx);
314 let uy = fy * fy * (3.0 - 2.0 * fy);
315
316 a * (1.0 - ux) * (1.0 - uy)
318 + b * ux * (1.0 - uy)
319 + c * (1.0 - ux) * uy
320 + d * ux * uy
321 }
322
323 fn random(&self, x: f32) -> f32 {
325 ((x * 12.9898).sin() * 43_758.547) % 1.0
326 }
327
328 pub fn sample_count(&self) -> usize {
330 self.sample_count
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_electric_border_creation() {
340 let border = ElectricBorder::new(400.0, 300.0);
341 assert_eq!(border.width, 400.0);
342 assert_eq!(border.height, 300.0);
343 assert!(border.sample_count > 0);
344 }
345
346 #[test]
347 fn test_time_update() {
348 let mut border = ElectricBorder::new(400.0, 300.0);
349
350 border.update(0.1);
351 assert!((border.time() - 0.1).abs() < 0.01);
352
353 border.update(0.1);
354 assert!((border.time() - 0.2).abs() < 0.01);
355 }
356
357 #[test]
358 fn test_speed_multiplier() {
359 let mut border = ElectricBorder::new(400.0, 300.0).with_speed(2.0);
360
361 border.update(0.1);
362 assert!((border.time() - 0.2).abs() < 0.01); }
364
365 #[test]
366 fn test_generate_points() {
367 let border = ElectricBorder::new(400.0, 300.0);
368 let points = border.generate_points();
369
370 assert_eq!(points.len(), border.sample_count() + 1);
372
373 for (x, y) in points.iter() {
375 assert!(
376 x >= &-border.displacement && x <= &(border.width + border.displacement)
377 );
378 assert!(
379 y >= &-border.displacement && y <= &(border.height + border.displacement)
380 );
381 }
382 }
383
384 #[test]
385 fn test_rounded_rect_corners() {
386 let border = ElectricBorder::new(400.0, 300.0).with_radius(24.0);
387
388 let radius = border.border_radius;
390
391 let (x, y) = border.get_rounded_rect_point(0.0, radius);
393 assert!(x >= radius - 1.0 && x <= radius + 1.0);
394 assert!(y < 1.0);
395
396 let (x_end, y_end) = border.get_rounded_rect_point(1.0, radius);
398 assert!((x - x_end).abs() < 10.0);
399 assert!((y - y_end).abs() < 10.0);
400 }
401
402 #[test]
403 fn test_noise_consistency() {
404 let border = ElectricBorder::new(400.0, 300.0);
405
406 let noise1 = border.noise_2d(1.5, 2.5);
408 let noise2 = border.noise_2d(1.5, 2.5);
409 assert_eq!(noise1, noise2);
410 }
411
412 #[test]
413 fn test_random_function() {
414 let border = ElectricBorder::new(400.0, 300.0);
415
416 let r1 = border.random(42.0);
418 let r2 = border.random(42.0);
419 assert_eq!(r1, r2);
420
421 let r3 = border.random(43.0);
423 assert_ne!(r1, r3);
424 }
425
426 #[test]
427 fn test_dimensions_update() {
428 let mut border = ElectricBorder::new(400.0, 300.0);
429 let old_count = border.sample_count();
430
431 border.set_dimensions(800.0, 600.0);
432 assert_eq!(border.width, 800.0);
433 assert_eq!(border.height, 600.0);
434
435 assert!(border.sample_count() > old_count);
437 }
438
439 #[test]
440 fn test_displacement_changes_over_time() {
441 let mut border = ElectricBorder::new(400.0, 300.0);
442
443 let points_t0 = border.generate_points();
444
445 border.update(1.0);
446 let points_t1 = border.generate_points();
447
448 let mut different = false;
450 for i in 0..points_t0.len().min(points_t1.len()) {
451 if (points_t0[i].0 - points_t1[i].0).abs() > 0.1
452 || (points_t0[i].1 - points_t1[i].1).abs() > 0.1
453 {
454 different = true;
455 break;
456 }
457 }
458 assert!(different, "Points should change over time");
459 }
460}