beamgrid/Source/PluginEditor.cpp
Armin aeb4b83281 Add BOOST, 3D depth, and gradient-angle knobs
- BOOST: global display level multiplier (0.75 - 2.5, 1.0 default)
- 3D: pseudo-3D bevel/shading on bars, LEDs, peaks and curve (0 = flat)
- G-ANG: 3D gradient angle (-180 - 180, 0 = top-lit)
- Apply CURVING rounded corners to bars and peaks in BAR mode
2026-08-14 03:38:09 +02:00

349 lines
16 KiB
C++

#include "PluginEditor.h"
#include "GitVersion.h"
BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcessor& p)
: AudioProcessorEditor (&p),
processorRef (p),
analyserComponent (p.getAnalyser(), p.getParametersState(), lookAndFeel)
{
setLookAndFeel (&lookAndFeel);
setSize (980, 480);
// Make the editor freely resizable (with a corner resizer). The plugin
// reports IPlugView::canResize() == true to the host, which is what FL
// Studio (and other hosts) use to enable their native maximize button.
setResizable (true, true);
setResizeLimits (800, 260, 4000, 4000);
analyserComponent.setLookAndFeel (&lookAndFeel);
addAndMakeVisible (analyserComponent);
auto setupKnob = [&] (juce::Slider& s, const juce::String& name)
{
s.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
s.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 80, 18);
s.setColour (juce::Slider::textBoxTextColourId, juce::Colours::lightgrey);
s.setTextValueSuffix (" " + name);
s.setLookAndFeel (&lookAndFeel);
addAndMakeVisible (s);
};
setupKnob (graceSlider, "ms");
setupKnob (falloffSlider, "");
setupKnob (barfalloffSlider, "");
setupKnob (barsSlider, "");
setupKnob (hueSlider, "");
setupKnob (modeSlider, "");
setupKnob (ledsSlider, "");
setupKnob (smoothSlider, "");
setupKnob (gridSlider, "");
setupKnob (gridzoomSlider, "");
setupKnob (gridfadeSlider, "");
setupKnob (curvingSlider, "");
setupKnob (tiltSlider, "");
setupKnob (boostSlider, "");
setupKnob (depth3dSlider, "");
setupKnob (gradangleSlider, "");
barsSlider.setNumDecimalPlacesToDisplay (0);
modeSlider.setNumDecimalPlacesToDisplay (0);
ledsSlider.setNumDecimalPlacesToDisplay (0);
graceLabel.setText ("PEAK GRACE", juce::dontSendNotification);
graceLabel.setJustificationType (juce::Justification::centred);
graceLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (graceLabel);
falloffLabel.setText ("PEAK FALLOFF", juce::dontSendNotification);
falloffLabel.setJustificationType (juce::Justification::centred);
falloffLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (falloffLabel);
barfalloffLabel.setText ("BAR FALLOFF", juce::dontSendNotification);
barfalloffLabel.setJustificationType (juce::Justification::centred);
barfalloffLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (barfalloffLabel);
barsLabel.setText ("BARS", juce::dontSendNotification);
barsLabel.setJustificationType (juce::Justification::centred);
barsLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (barsLabel);
hueLabel.setText ("HUE", juce::dontSendNotification);
hueLabel.setJustificationType (juce::Justification::centred);
hueLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (hueLabel);
modeLabel.setText ("MODE", juce::dontSendNotification);
modeLabel.setJustificationType (juce::Justification::centred);
modeLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (modeLabel);
ledsLabel.setText ("LEDS", juce::dontSendNotification);
ledsLabel.setJustificationType (juce::Justification::centred);
ledsLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (ledsLabel);
smoothLabel.setText ("SMOOTH", juce::dontSendNotification);
smoothLabel.setJustificationType (juce::Justification::centred);
smoothLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (smoothLabel);
gridLabel.setText ("GRID", juce::dontSendNotification);
gridLabel.setJustificationType (juce::Justification::centred);
gridLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (gridLabel);
gridzoomLabel.setText ("GRID ZOOM", juce::dontSendNotification);
gridzoomLabel.setJustificationType (juce::Justification::centred);
gridzoomLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (gridzoomLabel);
gridfadeLabel.setText ("GRID FADE", juce::dontSendNotification);
gridfadeLabel.setJustificationType (juce::Justification::centred);
gridfadeLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (gridfadeLabel);
curvingLabel.setText ("CURVING", juce::dontSendNotification);
curvingLabel.setJustificationType (juce::Justification::centred);
curvingLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (curvingLabel);
tiltLabel.setText ("TILT", juce::dontSendNotification);
tiltLabel.setJustificationType (juce::Justification::centred);
tiltLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (tiltLabel);
boostLabel.setText ("BOOST", juce::dontSendNotification);
boostLabel.setJustificationType (juce::Justification::centred);
boostLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (boostLabel);
depth3dLabel.setText ("3D", juce::dontSendNotification);
depth3dLabel.setJustificationType (juce::Justification::centred);
depth3dLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (depth3dLabel);
gradangleLabel.setText ("G-ANG", juce::dontSendNotification);
gradangleLabel.setJustificationType (juce::Justification::centred);
gradangleLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (gradangleLabel);
scaleBox.addItemList (juce::StringArray ("75%", "100%", "150%", "200%", "250%", "300%"), 1);
scaleBox.setSelectedItemIndex (1); // 100%
scaleBox.setColour (juce::ComboBox::backgroundColourId, juce::Colours::black.brighter (0.15f));
scaleBox.setColour (juce::ComboBox::textColourId, juce::Colours::lightgrey);
scaleBox.setColour (juce::ComboBox::arrowColourId, juce::Colours::lightgrey);
scaleBox.onChange = [this]
{
const double pct = scaleBox.getText().retainCharacters ("0123456789.").getDoubleValue();
const double scale = juce::jlimit (0.5, 3.0, pct / 100.0);
setScaleFactor (scale);
processorRef.getParametersState().state.setProperty ("uiScale", scale, nullptr);
};
addAndMakeVisible (scaleBox);
titleLabel.setText ("BEAMGRID", juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centredRight);
titleLabel.setFont (juce::FontOptions (20.0f, juce::Font::bold));
titleLabel.setColour (juce::Label::textColourId, lookAndFeel.getTeal());
addAndMakeVisible (titleLabel);
statusLabel.setFont (juce::FontOptions (11.0f));
statusLabel.setColour (juce::Label::textColourId, juce::Colours::grey);
statusLabel.setJustificationType (juce::Justification::centredRight);
juce::String status;
status << "git " << juce::String (BEAMGRID_GIT_COMMIT).substring (0, 7);
status << " [" << (BEAMGRID_GIT_DIRTY ? "dirty" : "clean") << "]";
status << " build " << BEAMGRID_BUILD_DATE;
statusLabel.setText (status, juce::dontSendNotification);
addAndMakeVisible (statusLabel);
graceAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "grace", graceSlider);
falloffAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "falloff", falloffSlider);
barfalloffAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "barfalloff", barfalloffSlider);
barsAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "bars", barsSlider);
hueAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "hue", hueSlider);
modeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "mode", modeSlider);
ledsAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "leds", ledsSlider);
smoothAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "smooth", smoothSlider);
gridAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "grid", gridSlider);
gridzoomAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "gridzoom", gridzoomSlider);
gridfadeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "gridfade", gridfadeSlider);
curvingAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "curving", curvingSlider);
tiltAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "tilt", tiltSlider);
boostAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "boost", boostSlider);
depth3dAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "depth3d", depth3dSlider);
gradangleAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "gradangle", gradangleSlider);
// Set these AFTER the attachments: SliderAttachment installs its own
// textFromValueFunction, so assigning afterwards makes ours take effect.
barsSlider.textFromValueFunction = [] (double v)
{
return juce::String (juce::roundToInt (v));
};
modeSlider.textFromValueFunction = [] (double v)
{
const int m = juce::roundToInt (v);
return m == 2 ? juce::String ("SPECTRUM")
: m == 3 ? juce::String ("LED")
: juce::String ("BARS");
};
ledsSlider.textFromValueFunction = [] (double v)
{
return juce::String (juce::roundToInt (v));
};
tiltSlider.textFromValueFunction = [] (double v)
{
return (v >= 0.0 ? juce::String ("+") : juce::String (""))
+ juce::String (v, 2);
};
// Force an immediate repaint of the text boxes now that our formatters
// are installed (otherwise they keep the attachment's initial text).
barsSlider.updateText();
modeSlider.updateText();
ledsSlider.updateText();
tiltSlider.updateText();
processorRef.getParametersState().addParameterListener ("hue", this);
// Restore the saved UI size + scale from the plugin state (the host saves
// this state with the project), so the window reopens the same way.
auto& uiState = processorRef.getParametersState().state;
const double storedScale = uiState.getProperty ("uiScale", 1.0);
const double scales[] = { 0.75, 1.0, 1.5, 2.0, 2.5, 3.0 };
int scaleIdx = 1;
for (int i = 0; i < 6; ++i)
if (std::abs (scales[i] - storedScale) < 0.001)
scaleIdx = i;
scaleBox.setSelectedItemIndex (scaleIdx); // triggers onChange -> setScaleFactor
const int storedW = juce::jmax (800, static_cast<int> (uiState.getProperty ("uiWidth", getWidth())));
const int storedH = uiState.getProperty ("uiHeight", getHeight());
setSize (storedW, storedH);
}
BeamgridAudioProcessorEditor::~BeamgridAudioProcessorEditor()
{
processorRef.getParametersState().removeParameterListener ("hue", this);
setLookAndFeel (nullptr);
}
void BeamgridAudioProcessorEditor::parameterChanged (const juce::String& parameterID, float newValue)
{
if (parameterID == "hue")
{
// Update the shared LookAndFeel immediately and repaint the whole
// editor so every rotary knob's arc (and the title) recolours.
lookAndFeel.setHueTurns (newValue - 0.5f);
repaint();
}
}
void BeamgridAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (juce::Colours::black);
// Keep the title colour in sync with the HUE knob.
titleLabel.setColour (juce::Label::textColourId, lookAndFeel.getTeal());
}
void BeamgridAudioProcessorEditor::resized()
{
const int panelH = 114;
const int y = getHeight() - panelH + 14;
// 32px blank bar across the very top of the plugin (the dropdown sits
// 16px below the top edge so it isn't glued to it). 30px blank strip on
// the right so the spectrum doesn't touch the right border.
const int topBarH = 32;
const int rightPad = 30;
analyserComponent.setBounds (0, topBarH, getWidth() - rightPad, getHeight() - panelH - topBarH);
const int margin = 24;
const int gap = 14;
const int count = 16;
const int titleW = 180;
// Available width for the knob row (leave room on the right for the title).
const int avail = getWidth() - margin * 2 - titleW;
// Shrink the knobs only if necessary so all `count` of them fit within the
// available width down to the 800px minimum; otherwise keep them at 64.
const int knob = juce::jlimit (40, 64, (avail - (count - 1) * 4) / count);
int spacing = knob + gap;
const int minSpacing = knob + 4;
if ((count - 1) * spacing + knob > avail)
spacing = juce::jmax (minSpacing, (avail - knob) / (count - 1));
struct KnobRow { juce::Slider& s; juce::Label& l; };
const KnobRow knobs[] = {
{ graceSlider, graceLabel },
{ falloffSlider, falloffLabel },
{ barfalloffSlider, barfalloffLabel },
{ barsSlider, barsLabel },
{ hueSlider, hueLabel },
{ modeSlider, modeLabel },
{ ledsSlider, ledsLabel },
{ smoothSlider, smoothLabel },
{ gridSlider, gridLabel },
{ gridzoomSlider, gridzoomLabel },
{ gridfadeSlider, gridfadeLabel },
{ curvingSlider, curvingLabel },
{ tiltSlider, tiltLabel },
{ boostSlider, boostLabel },
{ depth3dSlider, depth3dLabel },
{ gradangleSlider, gradangleLabel },
};
const juce::Font labelFont (juce::FontOptions (11.0f));
// Knobs are drawn 16px higher than the panel baseline; the title and
// status bar below stay anchored to the original baseline.
const int knobY = y - 16;
for (int i = 0; i < count; ++i)
{
const int x = margin + i * spacing;
knobs[i].s.setBounds (x, knobY, knob, knob);
// Let the label span the full spacing (wider than the knob) so longer
// names like "BAR FALLOFF" aren't clipped, centred under the knob.
const int labelW = spacing;
const int labelX = x + (knob - labelW) / 2;
knobs[i].l.setFont (labelFont);
knobs[i].l.setBounds (labelX, knobY + knob + 2, labelW, 18);
}
titleLabel.setBounds (getWidth() - titleW - margin, y - 6, titleW, knob + 24);
statusLabel.setBounds (16, getHeight() - 16, getWidth() - 42, 14);
// UI scaling dropdown, top-right (16px below the top edge).
scaleBox.setBounds (getWidth() - 96 - margin, 16, 96, 16);
// Remember the current window size so it can be restored with the project.
processorRef.getParametersState().state.setProperty ("uiWidth", getWidth(), nullptr);
processorRef.getParametersState().state.setProperty ("uiHeight", getHeight(), nullptr);
}