1#![deny(
51 clippy::unwrap_used,
52 clippy::expect_used,
53 clippy::indexing_slicing,
54 clippy::panic,
55 clippy::unreachable
56)]
57
58use super::renderer::{LineRenderer, Segment3dInstance, joined_chord};
59use super::spectrum::{CURVE_MAX, CURVE_MIN, downsample, shape_and_ease};
60use super::{CapOverflow, ColorRamp, GeneratorConfig, OverflowContext};
61use crate::dsp::AnalysisFrame;
62use crate::preset::Easing;
63use crate::render::camera::{self, CameraParams};
64use crate::render::palette::Palette;
65use crate::render::scenes::common::{PaletteParams, PanParams};
66use crate::render::scenes::{FALLBACK_DT, ParamGroup, ParamKind, ParamSpec, Scene, default_of};
67
68pub const MIN_ROWS: u32 = 2;
71
72pub const MAX_ROWS: u32 = 1024;
75
76pub const DEFAULT_ROWS: u32 = 64;
78
79pub const DEFAULT_ELEMENTS: usize = 48;
81
82pub const MIN_ROW_PERIOD: f32 = 0.01;
85
86pub const MAX_ROW_PERIOD: f32 = 2.0;
88
89pub const DEFAULT_ROW_PERIOD: f32 = 0.04;
91
92const PUSH_SLACK: f64 = 1e-6;
100
101const FRONT_Z: f32 = 1.0;
105
106const HALF_WIDTH: f32 = 1.0;
108
109const GROUND_Y: f32 = 0.0;
112
113const SOFTNESS: f32 = 0.5;
116
117#[derive(Debug, Clone, Copy, PartialEq)]
119pub struct WaterfallConfig {
120 pub elements: usize,
122 pub rows: u32,
125 pub row_period: f32,
128 pub easing: Easing,
130}
131
132impl Default for WaterfallConfig {
133 fn default() -> Self {
134 Self {
135 elements: DEFAULT_ELEMENTS,
136 rows: DEFAULT_ROWS,
137 row_period: DEFAULT_ROW_PERIOD,
138 easing: Easing::INSTANT,
139 }
140 }
141}
142
143const DEFAULT_HEIGHT: f32 = default_of(PARAMS, "height");
144const DEFAULT_ROW_SPACING: f32 = default_of(PARAMS, "row_spacing");
145const DEFAULT_CURVE: f32 = default_of(PARAMS, "curve");
146const DEFAULT_FADE: f32 = default_of(PARAMS, "fade");
147const DEFAULT_LINE_WIDTH: f32 = default_of(PARAMS, "line_width");
148const DEFAULT_GLOW: f32 = default_of(PARAMS, "glow");
149const DEFAULT_HUE: f32 = default_of(PARAMS, "hue");
150const DEFAULT_HUE_SPREAD: f32 = default_of(PARAMS, "hue_spread");
151const DEFAULT_BRIGHTNESS: f32 = default_of(PARAMS, "brightness");
152const DEFAULT_PITCH: f32 = default_of(PARAMS, "pitch");
153const DEFAULT_ZOOM: f32 = default_of(PARAMS, "zoom");
154
155pub const PARAMS: &[ParamSpec] = &[
158 ParamSpec {
159 name: "height",
160 default: 0.6,
161 range: Some([0.0, 2.0]),
162 doc: "How tall a full band stands above the ground, in world units; a row is 2 across.",
163 kind: ParamKind::Modal,
164 group: ParamGroup::Shape,
165 main: true,
166 },
167 ParamSpec {
168 name: "row_spacing",
169 default: 0.06,
170 range: Some([0.01, 0.3]),
171 doc: "How far behind each row the next one lies, in world units; with rows, how deep \
172 the landscape reaches.",
173 kind: ParamKind::Modal,
174 group: ParamGroup::Shape,
175 main: true,
176 },
177 ParamSpec {
178 name: "curve",
179 default: 1.0,
180 range: Some([CURVE_MIN, CURVE_MAX]),
181 doc: "Exponent on each band's level: 1 is linear, below 1 lifts quiet detail, above 1 \
182 pushes it down.",
183 kind: ParamKind::Modal,
184 group: ParamGroup::Shape,
185 main: false,
186 },
187 ParamSpec {
188 name: "fade",
189 default: 0.8,
190 range: Some([0.0, 1.0]),
191 doc: "How much light the farthest row has lost against the front edge; 0 keeps every \
192 row as bright as the newest.",
193 kind: ParamKind::Modal,
194 group: ParamGroup::Light,
195 main: true,
196 },
197 ParamSpec {
198 name: "line_width",
199 default: 1.5,
200 range: Some([0.5, 8.0]),
201 doc: "Line width in pixels at the focal plane; nearer rows are wider and farther ones \
202 thinner.",
203 kind: ParamKind::Modal,
204 group: ParamGroup::Shape,
205 main: true,
206 },
207 crate::render::scenes::lines::GLOW,
208 crate::render::scenes::common::brightness(1.0),
209 crate::render::scenes::common::hue(0.55),
210 ParamSpec {
211 name: "hue_spread",
212 default: 0.6,
213 range: Some([0.0, 1.0]),
214 doc: "How far along the palette the colour travels from the lowest band to the highest.",
215 kind: ParamKind::Modal,
216 group: ParamGroup::Colour,
217 main: false,
218 },
219 crate::render::scenes::common::SATURATION,
220 crate::render::scenes::common::PALETTE_MIX,
221 crate::render::scenes::common::PALETTE_STEPS,
222 camera::YAW,
223 PITCH,
224 camera::DISTANCE,
225 camera::FOV,
226 camera::FOCUS,
227 camera::APERTURE,
228 camera::FOG,
229 camera::SOLID,
230 crate::render::scenes::common::zoom(1.0),
231 crate::render::scenes::common::PAN_X,
232 crate::render::scenes::common::PAN_Y,
233];
234
235const PITCH: ParamSpec = ParamSpec {
238 default: 0.35,
239 ..camera::PITCH
240};
241
242#[derive(Debug, Clone, Default)]
246pub(crate) struct Landscape {
247 raw: Vec<f32>,
249 live: Vec<f32>,
251 ring: Vec<f32>,
253 elements: usize,
255 capacity: usize,
257 head: usize,
259 len: usize,
261 since_push: f64,
264 period: f32,
266 easing: Easing,
268}
269
270impl Landscape {
271 pub(crate) fn resize(&mut self, elements: usize, rows: usize, period: f32, easing: Easing) {
274 self.elements = elements;
275 self.capacity = rows.saturating_sub(1);
276 self.period = period;
277 self.easing = easing;
278 self.raw.clear();
279 self.raw.resize(elements, 0.0);
280 self.live.clear();
281 self.live.resize(elements, 0.0);
282 self.ring.clear();
283 self.ring.resize(elements * self.capacity, 0.0);
284 self.head = 0;
285 self.len = 0;
286 self.since_push = 0.0;
287 }
288
289 pub(crate) fn step(&mut self, spectrum: &[f32], curve: f32, dt: f32) {
293 downsample(spectrum, &mut self.raw);
294 for (held, &raw) in self.live.iter_mut().zip(&self.raw) {
295 *held = shape_and_ease(*held, raw, curve, self.easing, dt);
296 }
297 let period = f64::from(self.period);
298 if period <= 0.0 || period.is_nan() || self.capacity == 0 {
299 return;
300 }
301 self.since_push += f64::from(dt);
302 let mut pushes = 0;
305 while self.since_push + PUSH_SLACK >= period && pushes < self.capacity {
306 self.push();
307 self.since_push -= period;
308 pushes += 1;
309 }
310 if self.since_push >= period {
311 self.since_push %= period;
312 }
313 self.since_push = self.since_push.max(0.0);
314 }
315
316 fn push(&mut self) {
319 let n = self.elements;
320 self.head = (self.head + 1) % self.capacity.max(1);
321 let start = self.head * n;
322 if let (Some(dst), Some(src)) = (self.ring.get_mut(start..start + n), self.live.get(..n)) {
323 dst.copy_from_slice(src);
324 }
325 self.len = (self.len + 1).min(self.capacity);
326 }
327
328 pub(crate) fn frac(&self) -> f32 {
331 if self.period > 0.0 {
332 ((self.since_push / f64::from(self.period)) as f32).clamp(0.0, 1.0)
333 } else {
334 0.0
335 }
336 }
337
338 pub(crate) fn live(&self) -> &[f32] {
340 &self.live
341 }
342
343 pub(crate) fn row(&self, k: usize) -> Option<&[f32]> {
345 if k >= self.len || self.capacity == 0 {
346 return None;
347 }
348 let slot = (self.head + self.capacity - k) % self.capacity;
349 let n = self.elements;
350 self.ring.get(slot * n..slot * n + n)
351 }
352
353 pub(crate) fn len(&self) -> usize {
355 self.len
356 }
357
358 pub(crate) fn capacity(&self) -> usize {
360 self.capacity
361 }
362
363 pub(crate) fn elements(&self) -> usize {
365 self.elements
366 }
367}
368
369pub struct WaterfallScene {
372 lines: LineRenderer,
375 seg3d_cap: usize,
377 max_coc: f32,
379 landscape: Landscape,
380 instances: Vec<Segment3dInstance>,
382 clamp: Option<CapOverflow>,
384 palette: Palette,
385 dt: f32,
386 target: (u32, u32),
388
389 height: f32,
390 row_spacing: f32,
391 curve: f32,
392 fade: f32,
393 line_width: f32,
394 glow: f32,
395 hue_spread: f32,
396 zoom: f32,
397 colour: PaletteParams,
398 pan: PanParams,
399 camera: CameraParams,
400}
401
402impl WaterfallScene {
403 pub(crate) fn new(
407 device: &wgpu::Device,
408 surface_format: wgpu::TextureFormat,
409 seg3d_cap: usize,
410 max_coc: f32,
411 ) -> Self {
412 let mut scene = Self {
413 lines: LineRenderer::new_3d(device, surface_format, seg3d_cap, "waterfall"),
414 seg3d_cap,
415 max_coc,
416 landscape: Landscape::default(),
417 instances: Vec::with_capacity(seg3d_cap),
418 clamp: None,
419 palette: Palette::default_spectrum(),
420 dt: FALLBACK_DT,
421 target: (1, 1),
422 height: DEFAULT_HEIGHT,
423 row_spacing: DEFAULT_ROW_SPACING,
424 curve: DEFAULT_CURVE,
425 fade: DEFAULT_FADE,
426 line_width: DEFAULT_LINE_WIDTH,
427 glow: DEFAULT_GLOW,
428 hue_spread: DEFAULT_HUE_SPREAD,
429 zoom: DEFAULT_ZOOM,
430 colour: PaletteParams::new(DEFAULT_HUE, DEFAULT_BRIGHTNESS),
431 pan: PanParams::default(),
432 camera: CameraParams::default(),
433 };
434 scene.camera.pitch = DEFAULT_PITCH;
435 let _ = scene.resize(&WaterfallConfig::default());
438 scene
439 }
440
441 fn resize(&mut self, config: &WaterfallConfig) -> Option<CapOverflow> {
445 let (rows, overflow) = rows_clamp(config.rows, config.elements, self.seg3d_cap, false);
446 self.landscape
447 .resize(config.elements, rows, config.row_period, config.easing);
448 overflow
449 }
450
451 fn ramp(&self) -> ColorRamp {
453 ColorRamp {
454 hue: self.colour.hue,
455 hue_spread: self.hue_spread,
456 palette_mix: self.colour.mix,
457 palette_steps: self.colour.steps,
458 saturation: self.colour.saturation,
459 brightness: self.colour.brightness,
460 }
461 }
462}
463
464pub(crate) fn rows_clamp(
470 rows: u32,
471 elements: usize,
472 cap: usize,
473 skirted: bool,
474) -> (usize, Option<CapOverflow>) {
475 let per_row = elements.saturating_sub(1).max(1) * if skirted { 2 } else { 1 };
476 let fits = cap / per_row;
477 let asked = rows as usize;
478 if asked <= fits {
479 return (asked, None);
480 }
481 (
482 fits,
483 Some(CapOverflow {
484 dropped: (asked - fits) * per_row,
485 context: OverflowContext::Rows(rows, u32::try_from(per_row).unwrap_or(u32::MAX)),
486 cap,
487 }),
488 )
489}
490
491impl Scene for WaterfallScene {
492 fn name(&self) -> &'static str {
493 "waterfall"
494 }
495
496 fn advance(&mut self, dt: f32) {
497 self.dt = dt;
498 }
499
500 fn set_target_size(&mut self, width: u32, height: u32) {
501 self.target = (width, height);
502 }
503
504 fn set_palette(&mut self, palette: &Palette) {
505 self.palette = palette.clone();
506 }
507
508 fn configure(&mut self, cfg: &GeneratorConfig) -> Option<CapOverflow> {
509 let GeneratorConfig::Waterfall(config) = cfg else {
510 return None;
511 };
512 let overflow = self.resize(config);
516 self.clamp = None;
518 overflow
519 }
520
521 fn mirror_overflow(&self) -> Option<&CapOverflow> {
522 self.clamp.as_ref()
523 }
524
525 fn reset_params(&mut self) {
526 self.height = DEFAULT_HEIGHT;
527 self.row_spacing = DEFAULT_ROW_SPACING;
528 self.curve = DEFAULT_CURVE;
529 self.fade = DEFAULT_FADE;
530 self.line_width = DEFAULT_LINE_WIDTH;
531 self.glow = DEFAULT_GLOW;
532 self.hue_spread = DEFAULT_HUE_SPREAD;
533 self.zoom = DEFAULT_ZOOM;
534 self.colour.reset();
535 self.pan.reset();
536 self.camera.reset();
537 self.camera.pitch = DEFAULT_PITCH;
539 }
540
541 fn set_param(&mut self, name: &str, value: f32) {
542 if self.colour.set(name, value) || self.pan.set(name, value) || self.camera.set(name, value)
545 {
546 return;
547 }
548 match name {
549 "height" => self.height = value,
550 "row_spacing" => self.row_spacing = value,
551 "curve" => self.curve = value,
552 "fade" => self.fade = value,
553 "line_width" => self.line_width = value,
554 "glow" => self.glow = value,
555 "hue_spread" => self.hue_spread = value,
556 "zoom" => self.zoom = value,
557 _ => {}
558 }
559 }
560
561 fn update(&mut self, frame: &AnalysisFrame) {
562 self.landscape.step(&frame.spectrum, self.curve, self.dt);
563 }
564
565 fn render(
566 &mut self,
567 queue: &wgpu::Queue,
568 encoder: &mut wgpu::CommandEncoder,
569 view: &wgpu::TextureView,
570 aspect: f32,
571 ) {
572 let geometry = RowGeometry::new(
573 self.height,
574 self.row_spacing,
575 self.fade,
576 self.landscape.capacity() + 1,
577 );
578 let frame = self.camera.frame(
580 aspect,
581 self.zoom,
582 [self.pan.x, self.pan.y],
583 self.target,
584 geometry.radius(),
585 self.max_coc,
586 );
587 let held = self.landscape.capacity() + 1;
591 let (row_budget, row_clamp) = if frame.solid {
592 rows_clamp(
593 u32::try_from(held).unwrap_or(u32::MAX),
594 self.landscape.elements(),
595 self.seg3d_cap,
596 true,
597 )
598 } else {
599 (held, None)
600 };
601 self.clamp = row_clamp.or(frame.blur);
602 let width = if self.line_width.is_finite() {
603 self.line_width.max(0.0)
604 } else {
605 DEFAULT_LINE_WIDTH
606 };
607 let ramp = self.ramp();
608 let frac = self.landscape.frac();
609 let full = self.landscape.len() == self.landscape.capacity();
610
611 let mut instances = std::mem::take(&mut self.instances);
612 instances.clear();
613 let rows = std::iter::once((0.0, 1.0, self.landscape.live())).chain(
615 (0..self.landscape.len()).filter_map(|k| {
616 let row = self.landscape.row(k)?;
617 let mut alpha = 1.0;
620 if k == 0 {
621 alpha *= frac;
622 }
623 if full && k + 1 == self.landscape.capacity() {
624 alpha *= 1.0 - frac;
625 }
626 Some((k as f32 + frac, alpha, row))
627 }),
628 );
629 for (depth, alpha, row) in rows.take(row_budget) {
630 if alpha <= 0.0 {
631 continue;
632 }
633 let light = geometry.light(depth);
634 let z = geometry.z(depth);
635 let n = row.len();
636 let span = n.saturating_sub(1).max(1) as f32;
637 let point = |j: usize| row.get(j).map(|&l| [geometry.x(j, n), geometry.y(l), z]);
640 let foot = |p: [f32; 3]| [p[0], GROUND_Y, p[2]];
641 for (i, pair) in row.windows(2).enumerate() {
642 let (Some(&l0), Some(&l1)) = (pair.first(), pair.get(1)) else {
643 continue;
644 };
645 let pa = [geometry.x(i, n), geometry.y(l0), z];
646 let pb = [geometry.x(i + 1, n), geometry.y(l1), z];
647 let before = i.checked_sub(1).and_then(point);
648 let Some([prev, a, b, next]) =
649 joined_chord(&frame.view, before, pa, pb, point(i + 2))
650 else {
651 continue;
652 };
653 let line_in = !frame.view.outside(a, b, frame.margin);
654 let skirt_in = frame.solid && {
658 let (fa, fb) = (foot(a), foot(b));
659 let in_front = frame.view.depth(fa).min(frame.view.depth(fb)) >= camera::NEAR;
660 line_in || !in_front || !frame.view.outside(fa, fb, frame.margin)
661 };
662 if !line_in && !skirt_in {
663 continue;
664 }
665 if instances.len() + usize::from(line_in) + usize::from(skirt_in) > self.seg3d_cap {
666 break;
667 }
668 if skirt_in {
669 instances.push(Segment3dInstance {
670 a,
671 b,
672 color: [0.0; 3],
673 width,
674 alpha,
675 prev: a,
676 next: b,
677 skirt: 1.0,
678 });
679 }
680 if line_in {
681 let [r, g, bl] = ramp.at(&self.palette, (i as f32 + 0.5) / span);
682 instances.push(Segment3dInstance {
683 a,
684 b,
685 color: [r * light, g * light, bl * light],
686 width,
687 alpha,
688 prev,
689 next,
690 skirt: 0.0,
691 });
692 }
693 }
694 }
695 let glow = if self.glow.is_finite() {
696 self.glow.max(0.0)
697 } else {
698 DEFAULT_GLOW
699 };
700 self.lines.draw_3d_terrain(
703 queue, encoder, view, &frame, glow, SOFTNESS, GROUND_Y, &instances,
704 );
705 self.instances = instances;
706 }
707}
708
709#[derive(Debug, Clone, Copy)]
711pub(crate) struct RowGeometry {
712 height: f32,
713 spacing: f32,
714 fade: f32,
715 deepest: f32,
717}
718
719impl RowGeometry {
720 pub(crate) fn new(height: f32, spacing: f32, fade: f32, rows: usize) -> Self {
723 let finite = |v: f32, fallback: f32| if v.is_finite() { v } else { fallback };
724 Self {
725 height: finite(height, DEFAULT_HEIGHT),
726 spacing: finite(spacing, DEFAULT_ROW_SPACING).max(0.0),
727 fade: finite(fade, DEFAULT_FADE).clamp(0.0, 1.0),
728 deepest: rows.saturating_sub(1).max(1) as f32,
729 }
730 }
731
732 pub(crate) fn x(&self, i: usize, n: usize) -> f32 {
734 let span = n.saturating_sub(1).max(1) as f32;
735 -HALF_WIDTH + 2.0 * HALF_WIDTH * i as f32 / span
736 }
737
738 pub(crate) fn y(&self, level: f32) -> f32 {
740 self.height * level
741 }
742
743 pub(crate) fn z(&self, depth: f32) -> f32 {
745 FRONT_Z - depth * self.spacing
746 }
747
748 pub(crate) fn light(&self, depth: f32) -> f32 {
751 (1.0 - self.fade * (depth / self.deepest).clamp(0.0, 1.0)).max(0.0)
752 }
753
754 pub(crate) fn radius(&self) -> f32 {
757 let far = (FRONT_Z - self.deepest * self.spacing).abs().max(FRONT_Z);
758 (HALF_WIDTH * HALF_WIDTH + far * far + self.height * self.height).sqrt()
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 #![allow(clippy::indexing_slicing, clippy::panic)]
765
766 use super::*;
767 use crate::dsp::SPECTRUM_BINS;
768
769 fn spectrum_at(window: u64) -> [f32; SPECTRUM_BINS] {
772 let mut out = [0.0; SPECTRUM_BINS];
773 for (b, band) in out.iter_mut().enumerate() {
774 let phase = window as f32 * 0.37 + b as f32 * 0.11;
775 *band = 0.5 + 0.45 * phase.sin();
776 }
777 out
778 }
779
780 fn run(rate: u64, frames: u64, period: f32, easing: Easing) -> Landscape {
785 let mut landscape = Landscape::default();
786 landscape.resize(16, 8, period, easing);
787 let dt = 1.0 / rate as f32;
788 for n in 1..=frames {
789 let window = (n - 1) * 12 / rate;
790 landscape.step(&spectrum_at(window), 1.0, dt);
791 }
792 landscape
793 }
794
795 #[test]
804 fn the_same_input_at_60_and_144_hz_leaves_the_same_rows() {
805 let period = 5.0 / 12.0;
806 let easing = Easing::symmetric(0.05);
807 let a = run(60, 120, period, easing);
808 let b = run(144, 288, period, easing);
809
810 assert_eq!(a.len(), 4, "two seconds at 5/12 s a row pushes four rows");
811 assert_eq!(
812 a.len(),
813 b.len(),
814 "both rates pushed the same number of rows"
815 );
816 for k in 0..a.len() {
817 let (ra, rb) = (a.row(k).unwrap_or(&[]), b.row(k).unwrap_or(&[]));
818 assert_eq!(ra.len(), 16);
819 for (i, (x, y)) in ra.iter().zip(rb).enumerate() {
820 assert!(
821 (x - y).abs() < 1e-4,
822 "row {k} band {i}: {x} at 60 Hz against {y} at 144 Hz"
823 );
824 }
825 }
826 let (r0, r1) = (a.row(0).unwrap_or(&[]), a.row(1).unwrap_or(&[]));
829 assert!(r0.iter().zip(r1).any(|(x, y)| (x - y).abs() > 1e-2));
830
831 let row_spacing = DEFAULT_ROW_SPACING;
832 let (fa, fb) = (a.frac() * row_spacing, b.frac() * row_spacing);
833 assert!(
834 (a.frac() - 0.8).abs() < 1e-3,
835 "2 s is 4.8 periods, so the rows sit 0.8 of a row back, got {}",
836 a.frac()
837 );
838 assert!(
839 (fa - fb).abs() <= row_spacing * 1e-3,
840 "depth offset {fa} at 60 Hz against {fb} at 144 Hz"
841 );
842 }
843
844 #[test]
847 fn a_full_ring_drops_its_oldest_row() {
848 let mut landscape = Landscape::default();
849 landscape.resize(4, 3, 0.1, Easing::INSTANT);
850 assert_eq!(landscape.capacity(), 2);
851 for level in [0.1f32, 0.2, 0.3] {
852 let spectrum = [level; SPECTRUM_BINS];
853 landscape.step(&spectrum, 1.0, 0.1);
854 }
855 assert_eq!(landscape.len(), 2);
856 assert!((landscape.row(0).unwrap_or(&[])[0] - 0.3).abs() < 1e-6);
857 assert!((landscape.row(1).unwrap_or(&[])[0] - 0.2).abs() < 1e-6);
858 assert!(landscape.row(2).is_none());
859 }
860
861 #[test]
863 fn a_long_frame_pushes_at_most_the_ring() {
864 let mut landscape = Landscape::default();
865 landscape.resize(4, 5, 0.05, Easing::INSTANT);
866 landscape.step(&[0.5; SPECTRUM_BINS], 1.0, 10.0);
867 assert_eq!(landscape.len(), 4);
868 assert!(landscape.frac() < 1.0);
869 }
870
871 #[test]
874 fn rows_are_held_to_whole_rows_of_the_buffer() {
875 assert_eq!(
876 rows_clamp(126, 64, 8000, false),
877 (126, None),
878 "exactly at the cap"
879 );
880 let (rows, overflow) = rows_clamp(200, 64, 8000, false);
881 assert_eq!(rows, 126, "8000 segments hold 126 rows of 63");
882 let overflow = overflow.unwrap_or_else(|| panic!("a clamp that bit is announced"));
883 assert_eq!(overflow.context, OverflowContext::Rows(200, 63));
884 assert_eq!(overflow.cap, 8000);
885 assert_eq!(overflow.dropped, 74 * 63);
886 let text = overflow.to_string();
887 assert!(
888 text.contains("rows 200") && text.contains("keeps 126 rows"),
889 "{text}"
890 );
891 }
892
893 #[test]
898 fn a_skirted_row_costs_twice_its_segments() {
899 assert_eq!(
900 rows_clamp(63, 64, 8000, true),
901 (63, None),
902 "63 skirted rows of 126 fit"
903 );
904 let (rows, overflow) = rows_clamp(126, 64, 8000, true);
905 assert_eq!(rows, 63, "8000 segments hold 63 skirted rows of 63 bands");
906 let overflow = overflow.unwrap_or_else(|| panic!("a clamp that bit is announced"));
907 assert_eq!(overflow.context, OverflowContext::Rows(126, 126));
908 assert_eq!(overflow.dropped, 63 * 126);
909 let text = overflow.to_string();
910 assert!(text.contains("keeps 63 rows"), "{text}");
911 }
912
913 #[test]
917 fn the_same_frames_give_the_same_ring_after_600_frames() {
918 let run = || {
919 let mut landscape = Landscape::default();
920 landscape.resize(32, 40, 0.04, Easing::symmetric(0.08));
921 for n in 0..600u64 {
922 landscape.step(&spectrum_at(n), 0.8, 1.0 / 60.0);
923 }
924 landscape
925 };
926 let (a, b) = (run(), run());
927 assert_eq!(a.len(), 39, "600 frames at 0.04 s a row fill a 39-row ring");
928 assert_eq!(a.frac().to_bits(), b.frac().to_bits());
929 for k in 0..a.len() {
930 let (ra, rb) = (a.row(k).unwrap_or(&[]), b.row(k).unwrap_or(&[]));
931 assert!(!ra.is_empty());
932 assert!(
933 ra.iter().zip(rb).all(|(x, y)| x.to_bits() == y.to_bits()),
934 "row {k} differs between two runs of the same frames"
935 );
936 }
937 assert!(
938 a.live()
939 .iter()
940 .zip(b.live())
941 .all(|(x, y)| x.to_bits() == y.to_bits()),
942 "the live row differs"
943 );
944 }
945
946 #[test]
948 fn fade_dims_the_deepest_row_by_its_value() {
949 let g = RowGeometry::new(0.6, 0.05, 0.8, 11);
950 assert!((g.light(0.0) - 1.0).abs() < 1e-6);
951 assert!((g.light(10.0) - 0.2).abs() < 1e-6);
952 assert!((g.light(5.0) - 0.6).abs() < 1e-6);
953 assert!(
954 g.z(10.0) < g.z(0.0),
955 "deeper rows lie farther from the camera"
956 );
957 }
958}