diff --git a/.gitignore b/.gitignore index 1a4595e..3f6df96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target *.log +dist/ diff --git a/src/ui.rs b/src/ui.rs index f30e10f..c66537e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -843,7 +843,55 @@ fn draw_path_spectrum(frame: &mut Frame, area: Rect, view: &View<'_>) { ); } } + if view.show_peaks { + let primary_peak = view + .peaks + .get(index) + .copied() + .unwrap_or(level) + .clamp(0.0, 1.0) + * reactive_height(view) + * maximum; + let secondary_peak = view + .secondary_peaks + .and_then(|peaks| peaks.get(index)) + .copied() + .map(|peak| peak.clamp(0.0, 1.0) * reactive_height(view) * maximum) + .unwrap_or(primary_peak); + for (marker_x, marker_y) in + path_peak_points(view.side, (x, y), (nx, ny), primary_peak, secondary_peak) + { + plot_cell(buffer, area, marker_x, marker_y, '\u{25cf}', outside); + } + } + } +} + +fn path_peak_points( + side: Side, + origin: (f32, f32), + normal: (f32, f32), + primary_distance: f32, + secondary_distance: f32, +) -> impl Iterator { + let length = (normal.0 * normal.0 + normal.1 * normal.1) + .sqrt() + .max(0.001); + let primary = ( + origin.0 + normal.0 / length * primary_distance, + origin.1 + normal.1 / length * primary_distance, + ); + let secondary = ( + origin.0 - normal.0 / length * secondary_distance, + origin.1 - normal.1 / length * secondary_distance, + ); + match side { + Side::A => [Some(primary), None], + Side::B => [Some(secondary), None], + Side::Both => [Some(primary), Some(secondary)], } + .into_iter() + .flatten() } fn side_normals(side: Side, nx: f32, ny: f32) -> impl Iterator { @@ -1339,7 +1387,7 @@ fn palette(theme: Theme, position: f32) -> Color { Theme::Aurora => [(37, 99, 235), (45, 212, 191), (217, 249, 157)], Theme::Sunset => [(124, 58, 237), (244, 63, 94), (253, 224, 71)], Theme::Ice => [(30, 64, 175), (34, 211, 238), (240, 249, 255)], - Theme::Crimson => [(35, 0, 8), (190, 0, 36), (255, 72, 72)], + Theme::Crimson => [(65, 6, 16), (190, 0, 36), (255, 72, 72)], Theme::Vaporwave => [(76, 29, 149), (236, 72, 153), (103, 232, 249)], Theme::Toxic => [(15, 45, 20), (57, 255, 20), (225, 255, 90)], Theme::Gold => [(68, 35, 4), (245, 158, 11), (255, 244, 180)], @@ -1578,6 +1626,21 @@ mod tests { })); } + #[test] + fn path_peaks_cover_every_visible_side() { + let both = + path_peak_points(Side::Both, (10.0, 10.0), (0.0, 2.0), 3.0, 4.0).collect::>(); + assert_eq!(both, vec![(10.0, 13.0), (10.0, 6.0)]); + assert_eq!( + path_peak_points(Side::A, (10.0, 10.0), (0.0, 2.0), 3.0, 4.0).collect::>(), + vec![(10.0, 13.0)] + ); + assert_eq!( + path_peak_points(Side::B, (10.0, 10.0), (0.0, 2.0), 3.0, 4.0).collect::>(), + vec![(10.0, 6.0)] + ); + } + #[test] fn bar_styles_use_distinct_glyphs() { assert_eq!(bar_glyph(BarStyle::Solid, 4, true, 0, 1), '\u{2584}'); diff --git a/src/window.rs b/src/window.rs index 88f9cce..bbbbdad 100644 --- a/src/window.rs +++ b/src/window.rs @@ -821,6 +821,8 @@ fn draw_path_levels( let (inside, outside) = path_colors(view, index); let inside = with_alpha(inside, alpha); let outside = with_alpha(outside, alpha); + let (primary_peak, secondary_peak) = + peak_levels(view.peaks, view.secondary_peaks, index, level); if matches!(view.side, Side::Both | Side::A) { let end = add(origin, mul(normalized(normal), level * maximum)); if dotted { @@ -829,8 +831,7 @@ fn draw_path_levels( mesh.line(origin, end, width, inside, outside); } if view.show_peaks && alpha >= 1.0 { - let peak = view.peaks.get(index).copied().unwrap_or(level); - let point = add(origin, mul(normalized(normal), peak * maximum)); + let point = add(origin, mul(normalized(normal), primary_peak * maximum)); mesh.disc(point[0], point[1], width + 1.5, outside, 8); } } @@ -846,18 +847,27 @@ fn draw_path_levels( mesh.line(origin, end, width, inside, outside); } if view.show_peaks && alpha >= 1.0 { - let peak = view - .secondary_peaks - .and_then(|peaks| peaks.get(index)) - .copied() - .unwrap_or(secondary_level); - let point = add(origin, mul(normalized(normal), -peak * maximum)); + let point = add(origin, mul(normalized(normal), -secondary_peak * maximum)); mesh.disc(point[0], point[1], width + 1.5, outside, 8); } } } } +fn peak_levels( + primary_peaks: &[f32], + secondary_peaks: Option<&[f32]>, + index: usize, + level: f32, +) -> (f32, f32) { + let primary = primary_peaks.get(index).copied().unwrap_or(level); + let secondary = secondary_peaks + .and_then(|peaks| peaks.get(index)) + .copied() + .unwrap_or(primary); + (primary, secondary) +} + fn draw_scope(mesh: &mut Mesh, area: Area, view: &GpuView<'_>) { if view.phase_scope <= 0.0 || view.scope_samples.is_empty() { return; @@ -1151,7 +1161,7 @@ fn palette(theme: Theme, position: f32) -> [f32; 4] { Theme::Aurora => [(37, 99, 235), (45, 212, 191), (217, 249, 157)], Theme::Sunset => [(124, 58, 237), (244, 63, 94), (253, 224, 71)], Theme::Ice => [(30, 64, 175), (34, 211, 238), (240, 249, 255)], - Theme::Crimson => [(35, 0, 8), (190, 0, 36), (255, 72, 72)], + Theme::Crimson => [(65, 6, 16), (190, 0, 36), (255, 72, 72)], Theme::Vaporwave => [(76, 29, 149), (236, 72, 153), (103, 232, 249)], Theme::Toxic => [(15, 45, 20), (57, 255, 20), (225, 255, 90)], Theme::Gold => [(68, 35, 4), (245, 158, 11), (255, 244, 180)], @@ -1381,6 +1391,12 @@ mod tests { assert!(lightest_bar[2] < background[2]); } + #[test] + fn mirrored_path_uses_the_delayed_primary_peak_on_both_sides() { + assert_eq!(peak_levels(&[0.8], None, 0, 0.2), (0.8, 0.8)); + assert_eq!(peak_levels(&[0.8], Some(&[0.6]), 0, 0.2), (0.8, 0.6)); + } + #[test] fn solid_bar_fade_changes_alpha_without_darkening_rgb() { let color = [0.74, 0.29, 0.49, 1.0];