Add popup help to elements

This commit is contained in:
2026-03-27 08:29:11 +00:00
parent b8d3dc48ab
commit ea381fe433
11 changed files with 233 additions and 4 deletions

View File

@@ -25,6 +25,16 @@ impl AudioOut {
params: &[
ParamDescriptor { id: "level", label: "Level", min: 0.0, max: 1.0, default: 0.8, unit: "", labels: &[] },
],
description: "\
## Output — Audio Sink
The final destination in the signal chain, representing the speakers or DAC.
Connect the last audio module's output jack to **In**. The green **● LIVE** badge lights up when a cable is connected and audio is flowing.
**Level** controls the master output volume.
> The oscilloscope and spectrum analyser always reflect what arrives at this node.",
};
}

View File

@@ -71,4 +71,6 @@ pub struct ComponentDescriptor {
pub jacks: &'static [JackDescriptor],
/// Ordered list of tunable parameters.
pub params: &'static [ParamDescriptor],
/// Markdown-formatted help text shown in the patch-bay tooltip.
pub description: &'static str,
}

View File

@@ -39,6 +39,19 @@ impl Adsr {
ParamDescriptor { id: "sustain", label: "Sustain", min: 0.0, max: 1.0, default: 0.7, unit: "", labels: &[] },
ParamDescriptor { id: "release_s", label: "Release", min: 0.001, max: 8.0, default: 0.3, unit: "s", labels: &[] },
],
description: "\
## ADSR — Envelope Generator
Produces a CV signal (01) that shapes amplitude over time in four stages:
- **Attack** — time to rise from 0 to peak after gate opens
- **Decay** — time to fall from peak to sustain level
- **Sustain** — level held while gate remains open
- **Release** — time to fall back to 0 after gate closes
**This module outputs CV, not audio.** Connect **Env** to a VCA `cv_gain` jack to shape volume, or to `cv_cutoff_hz` on the Filter for a filter envelope.
> **Tip:** VCO Out → VCA In, Keyboard Gate → ADSR Gate, ADSR Env → VCA cv_gain, VCA Out → Output In",
};
pub fn gate_on(&mut self) { self.stage = Stage::Attack; }

View File

@@ -42,6 +42,16 @@ impl Svf {
ParamDescriptor { id: "cutoff_hz", label: "Cutoff", min: 20.0, max: 20_000.0, default: 2_000.0, unit: "Hz", labels: &[] },
ParamDescriptor { id: "resonance", label: "Res", min: 0.0, max: 1.0, default: 0.5, unit: "", labels: &[] },
],
description: "\
## Filter (SVF) — State-Variable Filter
2-pole filter that shapes the tonal colour of an audio signal.
**In** accepts audio; **Out** emits the filtered result.
**Cutoff** sets the corner frequency — frequencies above this point are attenuated (in low-pass mode). **Res** (resonance) narrows the peak; at 1.0 the filter self-oscillates.
Connect an LFO to `cv_cutoff_hz` for a sweeping wah effect, or an ADSR for a filter envelope.",
};
#[inline]

View File

@@ -31,6 +31,18 @@ impl Lfo {
ParamDescriptor { id: "depth", label: "Depth", min: 0.0, max: 1.0, default: 0.5, unit: "", labels: &[] },
ParamDescriptor { id: "waveform", label: "Wave", min: 0.0, max: 4.0, default: 0.0, unit: "", labels: &["Sine", "Saw", "Sqr", "Tri", "Pls"] },
],
description: "\
## LFO — Low-Frequency Oscillator
Like a VCO but running at sub-audio speeds (0.0120 Hz). Outputs a CV signal (1 to +1) for modulation.
**Rate** controls oscillation speed. **Depth** scales the output amplitude (0 = no modulation, 1 = full swing).
**Typical uses:**
- LFO Out → VCO `cv_freq_hz` for **vibrato** (pitch wobble)
- LFO Out → Filter `cv_cutoff_hz` for a **wah/sweep** effect
- LFO Out → VCA `cv_gain` for **tremolo** (volume wobble)",
};
#[inline]

View File

@@ -37,6 +37,16 @@ impl Vco {
ParamDescriptor { id: "freq_hz", label: "Freq", min: 20.0, max: 20_000.0, default: 440.0, unit: "Hz", labels: &[] },
ParamDescriptor { id: "waveform", label: "Wave", min: 0.0, max: 4.0, default: 1.0, unit: "", labels: &["Sine", "Saw", "Sqr", "Tri", "Pls"] },
],
description: "\
## VCO — Voltage-Controlled Oscillator
Generates a periodic audio-rate waveform at the set frequency.
**Waveforms:** Sine · Saw · Square · Triangle · Pulse
**Freq** sets the base pitch. Connect an LFO or keyboard CV to `cv_freq_hz` for 1 V/oct pitch modulation (0 V = 440 Hz, +1 V = 880 Hz).
**Typical chain:** VCO Out → Filter In → VCA In → Output In",
};
#[inline]

View File

@@ -22,6 +22,16 @@ impl Vca {
params: &[
ParamDescriptor { id: "gain", label: "Gain", min: 0.0, max: 1.0, default: 1.0, unit: "", labels: &[] },
],
description: "\
## VCA — Voltage-Controlled Amplifier
Scales the audio level of a signal. Audio enters on **In** and exits at a controlled volume on **Out**.
**Gain** knob sets the base level (0 = silence, 1 = unity gain).
Connect an ADSR **Env** output to the `cv_gain` jack to apply an amplitude envelope — the CV value multiplies with the knob gain each sample.
**Typical chain:** VCO Out → VCA In → Output In, with ADSR Env → VCA cv_gain",
};
}

View File

@@ -141,6 +141,13 @@ impl Module {
&& y >= self.y && y <= self.y + MOD_HEADER
}
/// Hit-test the `?` help badge drawn in the top-right of the header.
fn hit_help_badge(&self, x: f32, y: f32) -> bool {
let bx = self.x + MOD_W - 10.0;
let by = self.y + MOD_HEADER / 2.0;
hypot(x - bx, y - by) <= 9.0
}
fn hit_body(&self, x: f32, y: f32) -> bool {
x >= self.x && x <= self.x + MOD_W
&& y >= self.y && y <= self.y + self.height()
@@ -490,6 +497,18 @@ impl PatchBay {
}
}
/// Returns the markdown description for the module whose `?` badge is at
/// (x, y), or an empty string if none is hovered. JS calls this on every
/// `mousemove` over the patch bay canvas to drive the tooltip overlay.
pub fn get_tooltip_at(&self, x: f32, y: f32) -> String {
for m in self.modules.iter().rev() {
if m.hit_help_badge(x, y) {
return m.descriptor().description.to_string();
}
}
String::new()
}
/// Double-click removes the topmost module under the cursor.
pub fn on_double_click(&mut self, x: f32, y: f32) {
if y < PALETTE_H { return; }
@@ -685,7 +704,25 @@ impl PatchBay {
(m.y + MOD_HEADER * 0.70) as f64,
);
// ? help badge — top-right corner of every module header
{
let bx = (m.x + MOD_W - 10.0) as f64;
let by = (m.y + MOD_HEADER / 2.0) as f64;
ctx.begin_path();
let _ = ctx.arc(bx, by, 7.0, 0.0, core::f64::consts::TAU);
ctx.set_fill_style_str("rgba(0,0,0,0.30)");
ctx.fill();
ctx.set_stroke_style_str("rgba(255,255,255,0.25)");
ctx.set_line_width(1.0);
ctx.stroke();
ctx.set_fill_style_str("rgba(255,255,255,0.70)");
ctx.set_font("bold 9px sans-serif");
ctx.set_text_align("center");
let _ = ctx.fill_text("?", bx, by + 3.5);
}
// Output node: LIVE / UNPATCHED badge on the right of the header
// (shifted left to clear the ? badge)
if is_out {
if has_signal {
ctx.set_fill_style_str("#22c55e");
@@ -693,7 +730,7 @@ impl PatchBay {
ctx.set_text_align("right");
let _ = ctx.fill_text(
"● LIVE",
(m.x + MOD_W - 6.0) as f64,
(m.x + MOD_W - 24.0) as f64,
(m.y + MOD_HEADER * 0.70) as f64,
);
} else {
@@ -702,7 +739,7 @@ impl PatchBay {
ctx.set_text_align("right");
let _ = ctx.fill_text(
"unpatched",
(m.x + MOD_W - 6.0) as f64,
(m.x + MOD_W - 24.0) as f64,
(m.y + MOD_HEADER * 0.70) as f64,
);
}