Feat: lissajous

This commit is contained in:
2026-02-23 22:06:09 +01:00
parent 502f7afe8f
commit 8b745a77a6
17 changed files with 241 additions and 49 deletions

View File

@@ -9,7 +9,8 @@ use crate::app::App;
use crate::state::{DeviceKind, EngineSection, SettingKind};
use crate::theme;
use crate::widgets::{
render_scroll_indicators, render_section_header, IndicatorAlign, Orientation, Scope, Spectrum,
render_scroll_indicators, render_section_header, IndicatorAlign, Lissajous, Orientation, Scope,
Spectrum,
};
pub fn layout(area: Rect) -> [Rect; 3] {
@@ -148,14 +149,17 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
}
fn render_visualizers(frame: &mut Frame, app: &App, area: Rect) {
let [scope_area, _, spectrum_area] = Layout::vertical([
Constraint::Percentage(50),
let [scope_area, _, lissajous_area, _gap, spectrum_area] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Percentage(50),
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Fill(1),
])
.areas(area);
render_scope(frame, app, scope_area);
render_lissajous(frame, app, lissajous_area);
render_spectrum(frame, app, spectrum_area);
}
@@ -175,6 +179,21 @@ fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
frame.render_widget(scope, inner);
}
fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let block = Block::default()
.borders(Borders::ALL)
.title(" Lissajous ")
.border_style(Style::new().fg(theme.engine.border_green));
let inner = block.inner(area);
frame.render_widget(block, area);
let lissajous = Lissajous::new(&app.metrics.scope, &app.metrics.scope_right)
.color(theme.meter.low);
frame.render_widget(lissajous, inner);
}
fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let block = Block::default()

View File

@@ -12,7 +12,7 @@ use crate::engine::SequencerSnapshot;
use crate::state::MainLayout;
use crate::theme;
use crate::views::render::highlight_script_lines;
use crate::widgets::{Orientation, Scope, Spectrum, VuMeter};
use crate::widgets::{Lissajous, Orientation, Scope, Spectrum, VuMeter};
pub fn layout(area: Rect) -> [Rect; 3] {
Layout::horizontal([
@@ -31,6 +31,7 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area:
} else {
let has_viz = app.audio.config.show_scope
|| app.audio.config.show_spectrum
|| app.audio.config.show_lissajous
|| app.audio.config.show_preview;
let (viz_area, sequencer_area) =
viz_seq_split(main_area, app.audio.config.layout, has_viz);
@@ -49,7 +50,9 @@ fn render_top_layout(
snapshot: &SequencerSnapshot,
main_area: Rect,
) {
let has_audio_viz = app.audio.config.show_scope || app.audio.config.show_spectrum;
let has_audio_viz = app.audio.config.show_scope
|| app.audio.config.show_spectrum
|| app.audio.config.show_lissajous;
let has_preview = app.audio.config.show_preview;
let mut constraints = Vec::new();
@@ -85,16 +88,23 @@ fn render_top_layout(
}
fn render_audio_viz(frame: &mut Frame, app: &App, area: Rect) {
match (app.audio.config.show_scope, app.audio.config.show_spectrum) {
(true, true) => {
let [scope_area, spectrum_area] =
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(area);
render_scope(frame, app, scope_area, Orientation::Horizontal);
render_spectrum(frame, app, spectrum_area);
let mut panels: Vec<VizPanel> = Vec::new();
if app.audio.config.show_scope { panels.push(VizPanel::Scope); }
if app.audio.config.show_spectrum { panels.push(VizPanel::Spectrum); }
if app.audio.config.show_lissajous { panels.push(VizPanel::Lissajous); }
if panels.is_empty() { return; }
let constraints: Vec<Constraint> = panels.iter().map(|_| Constraint::Fill(1)).collect();
let areas: Vec<Rect> = Layout::horizontal(&constraints).split(area).to_vec();
for (panel, panel_area) in panels.iter().zip(areas.iter()) {
match panel {
VizPanel::Scope => render_scope(frame, app, *panel_area, Orientation::Horizontal),
VizPanel::Spectrum => render_spectrum(frame, app, *panel_area),
VizPanel::Lissajous => render_lissajous(frame, app, *panel_area),
VizPanel::Preview => {}
}
(true, false) => render_scope(frame, app, area, Orientation::Horizontal),
(false, true) => render_spectrum(frame, app, area),
(false, false) => {}
}
}
@@ -104,7 +114,9 @@ fn preview_height(has_audio_viz: bool) -> u16 {
pub fn sequencer_rect(app: &App, main_area: Rect) -> Rect {
if matches!(app.audio.config.layout, MainLayout::Top) {
let has_audio_viz = app.audio.config.show_scope || app.audio.config.show_spectrum;
let has_audio_viz = app.audio.config.show_scope
|| app.audio.config.show_spectrum
|| app.audio.config.show_lissajous;
let has_preview = app.audio.config.show_preview;
let mut constraints = Vec::new();
@@ -121,6 +133,7 @@ pub fn sequencer_rect(app: &App, main_area: Rect) -> Rect {
} else {
let has_viz = app.audio.config.show_scope
|| app.audio.config.show_spectrum
|| app.audio.config.show_lissajous
|| app.audio.config.show_preview;
let (_, seq_area) = viz_seq_split(main_area, app.audio.config.layout, has_viz);
seq_area
@@ -130,6 +143,7 @@ pub fn sequencer_rect(app: &App, main_area: Rect) -> Rect {
enum VizPanel {
Scope,
Spectrum,
Lissajous,
Preview,
}
@@ -142,11 +156,13 @@ fn render_viz_area(
let is_vertical_layout = matches!(app.audio.config.layout, MainLayout::Left | MainLayout::Right);
let show_scope = app.audio.config.show_scope;
let show_spectrum = app.audio.config.show_spectrum;
let show_lissajous = app.audio.config.show_lissajous;
let show_preview = app.audio.config.show_preview;
let mut panels = Vec::new();
if show_scope { panels.push(VizPanel::Scope); }
if show_spectrum { panels.push(VizPanel::Spectrum); }
if show_lissajous { panels.push(VizPanel::Lissajous); }
if show_preview { panels.push(VizPanel::Preview); }
let constraints: Vec<Constraint> = panels.iter().map(|_| Constraint::Fill(1)).collect();
@@ -173,6 +189,7 @@ fn render_viz_area(
match panel {
VizPanel::Scope => render_scope(frame, app, *panel_area, orientation),
VizPanel::Spectrum => render_spectrum(frame, app, *panel_area),
VizPanel::Lissajous => render_lissajous(frame, app, *panel_area),
VizPanel::Preview => {
let user_words = user_words_once.as_ref().unwrap();
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
@@ -491,6 +508,19 @@ fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
frame.render_widget(spectrum, inner);
}
fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(theme.ui.border));
let inner = block.inner(area);
frame.render_widget(block, area);
let lissajous = Lissajous::new(&app.metrics.scope, &app.metrics.scope_right)
.color(theme.meter.low);
frame.render_widget(lissajous, inner);
}
fn render_script_preview(
frame: &mut Frame,
app: &App,

View File

@@ -78,6 +78,16 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
focus == OptionsFocus::ShowSpectrum,
&theme,
),
render_option_line(
"Show lissajous",
if app.audio.config.show_lissajous {
"On"
} else {
"Off"
},
focus == OptionsFocus::ShowLissajous,
&theme,
),
render_option_line(
"Completion",
if app.ui.show_completion { "On" } else { "Off" },
@@ -346,6 +356,7 @@ fn option_description(focus: OptionsFocus) -> Option<&'static str> {
OptionsFocus::RuntimeHighlight => Some("Highlight executed code spans during playback"),
OptionsFocus::ShowScope => Some("Oscilloscope on the engine page"),
OptionsFocus::ShowSpectrum => Some("Spectrum analyzer on the engine page"),
OptionsFocus::ShowLissajous => Some("XY stereo phase scope (left vs right)"),
OptionsFocus::ShowCompletion => Some("Word completion popup in the editor"),
OptionsFocus::ShowPreview => Some("Step script preview on the sequencer grid"),
OptionsFocus::PerformanceMode => Some("Hide header and footer bars"),