Add popup help to elements
This commit is contained in:
@@ -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.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 (0–1) 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; }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.01–20 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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user