diff --git a/supatrigga/CMakeLists.txt b/supatrigga/CMakeLists.txt index 3c09a98..1a44b5d 100644 --- a/supatrigga/CMakeLists.txt +++ b/supatrigga/CMakeLists.txt @@ -20,8 +20,14 @@ juce_add_plugin(SupaTrigga AU_MAIN_TYPE kAudioUnitType_Effect ) +juce_add_binary_data(SupaTrigga_Assets SOURCES + Resources/Inter-Bold.otf +) + target_sources(SupaTrigga PRIVATE Source/PluginProcessor.cpp + Source/SupaTriggaEditor.cpp ) smartelectronix_plugin_common(SupaTrigga) +target_link_libraries(SupaTrigga PRIVATE SupaTrigga_Assets) diff --git a/supatrigga/Resources/Inter-Bold.otf b/supatrigga/Resources/Inter-Bold.otf new file mode 100644 index 0000000..c74cc0c Binary files /dev/null and b/supatrigga/Resources/Inter-Bold.otf differ diff --git a/supatrigga/Source/PluginProcessor.cpp b/supatrigga/Source/PluginProcessor.cpp index 2100c42..563bad2 100644 --- a/supatrigga/Source/PluginProcessor.cpp +++ b/supatrigga/Source/PluginProcessor.cpp @@ -1,4 +1,5 @@ #include "PluginProcessor.h" +#include "SupaTriggaEditor.h" #include SupaTriggaProcessor::SupaTriggaProcessor() @@ -60,7 +61,7 @@ juce::AudioProcessorValueTreeState::ParameterLayout SupaTriggaProcessor::createP juce::AudioProcessorParameter::genericParameter, [](float value, int) { int slices = 1 << static_cast(value * (BITSLIDES + 0.5f)); - return juce::String(slices) + " slices/measure"; + return juce::String(slices); }, nullptr)); @@ -506,11 +507,11 @@ void SupaTriggaProcessor::randomize() } } -bool SupaTriggaProcessor::hasEditor() const { return false; } +bool SupaTriggaProcessor::hasEditor() const { return true; } juce::AudioProcessorEditor* SupaTriggaProcessor::createEditor() { - return new juce::GenericAudioProcessorEditor(*this); + return new SupaTriggaEditor(*this, apvts); } void SupaTriggaProcessor::getStateInformation(juce::MemoryBlock& destData) diff --git a/supatrigga/Source/PluginProcessor.h b/supatrigga/Source/PluginProcessor.h index 769d9f7..7ef3b71 100644 --- a/supatrigga/Source/PluginProcessor.h +++ b/supatrigga/Source/PluginProcessor.h @@ -4,6 +4,8 @@ #include #include +class SupaTriggaEditor; + // Constants from original constexpr int NUMBERIO = 2; constexpr int MAXSIZE = 2000000; diff --git a/supatrigga/Source/SupaTriggaEditor.cpp b/supatrigga/Source/SupaTriggaEditor.cpp new file mode 100644 index 0000000..7f6f6af --- /dev/null +++ b/supatrigga/Source/SupaTriggaEditor.cpp @@ -0,0 +1,350 @@ +#include "SupaTriggaEditor.h" +#include "Theme.h" +#include "BinaryData.h" + +SupaTriggaLookAndFeel::SupaTriggaLookAndFeel() +{ + // Configure default colors + setColour(juce::Slider::rotarySliderFillColourId, Theme::Colors::globalAccent); + setColour(juce::Slider::rotarySliderOutlineColourId, Theme::Colors::track); + setColour(juce::Slider::textBoxTextColourId, Theme::Colors::textValue); + + // Load embedded Inter Bold font + interBold = juce::Typeface::createSystemTypefaceFor( + BinaryData::InterBold_otf, BinaryData::InterBold_otfSize); +} + +void SupaTriggaLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, + int width, int height, + float sliderPos, + const float rotaryStartAngle, + const float rotaryEndAngle, + juce::Slider& slider) +{ + const float radius = 34.0f; + const float centreX = (float)x + (float)width * 0.5f; + const float centreY = (float)y + (float)height * 0.5f; + + // Track + const float trackThickness = 6.0f; + g.setColour(slider.findColour(juce::Slider::rotarySliderOutlineColourId)); + juce::Path trackPath; + trackPath.addCentredArc(centreX, centreY, radius, radius, 0.0f, + rotaryStartAngle, rotaryEndAngle, true); + g.strokePath(trackPath, juce::PathStrokeType(trackThickness, + juce::PathStrokeType::curved, juce::PathStrokeType::rounded)); + + // Fill + auto fillColour = slider.findColour(juce::Slider::rotarySliderFillColourId); + g.setColour(fillColour); + + // Only draw fill if greater than 0 + if (sliderPos > 0.0f) + { + const float angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle); + juce::Path fillPath; + fillPath.addCentredArc(centreX, centreY, radius, radius, 0.0f, + rotaryStartAngle, angle, true); + g.strokePath(fillPath, juce::PathStrokeType(trackThickness, + juce::PathStrokeType::curved, juce::PathStrokeType::rounded)); + } + + // Center Inner Circle + const float innerRadius = Theme::Metrics::innerRadius; + const float innerDiameter = innerRadius * 2.0f; + g.setColour(Theme::Colors::background); + g.fillEllipse(centreX - innerRadius, centreY - innerRadius, innerDiameter, innerDiameter); + g.setColour(Theme::Colors::innerCircle); + g.drawEllipse(centreX - innerRadius, centreY - innerRadius, innerDiameter, innerDiameter, 1.0f); + + // Text Value + g.setColour(slider.findColour(juce::Slider::textBoxTextColourId)); + + auto font = interBold != nullptr + ? juce::Font(juce::FontOptions(interBold).withHeight(12.0f)) + : juce::Font(juce::FontOptions{}.withHeight(12.0f).withStyle("Bold")); + g.setFont(font); + + juce::String text = slider.getTextFromValue(slider.getValue()); + g.drawText(text, x, y, width, height, juce::Justification::centred, false); +} + +void SupaTriggaLookAndFeel::drawToggleButton(juce::Graphics& g, juce::ToggleButton& button, + bool /*shouldDrawButtonAsHighlighted*/, + bool /*shouldDrawButtonAsDown*/) +{ + const float radius = 34.0f; + const float centreX = button.getLocalBounds().getCentreX(); + const float centreY = button.getLocalBounds().getCentreY(); + + // Toggle Track + g.setColour(Theme::Colors::track); + g.drawEllipse(centreX - radius, centreY - radius, radius * 2.0f, radius * 2.0f, 6.0f); + + // Toggle Fill (ON/OFF) + if (button.getToggleState()) + { + g.setColour(button.findColour(juce::ToggleButton::tickColourId)); + g.drawEllipse(centreX - radius, centreY - radius, radius * 2.0f, radius * 2.0f, 6.0f); + } + + // Center Inner Circle + const float innerRadius = Theme::Metrics::innerRadius; + const float innerDiameter = innerRadius * 2.0f; + g.setColour(Theme::Colors::background); + g.fillEllipse(centreX - innerRadius, centreY - innerRadius, innerDiameter, innerDiameter); + g.setColour(Theme::Colors::innerCircle); + g.drawEllipse(centreX - innerRadius, centreY - innerRadius, innerDiameter, innerDiameter, 1.0f); + + // Text Label + g.setColour(button.getToggleState() + ? Theme::Colors::textValue + : Theme::Colors::textSecondary); + + auto font = interBold != nullptr + ? juce::Font(juce::FontOptions(interBold).withHeight(12.0f)) + : juce::Font(juce::FontOptions{}.withHeight(12.0f).withStyle("Bold")); + g.setFont(font); + + g.drawText(button.getName(), button.getLocalBounds(), juce::Justification::centred, false); +} + +SupaTriggaEditor::SupaTriggaEditor(SupaTriggaProcessor& p, + juce::AudioProcessorValueTreeState& vts) + : AudioProcessorEditor(&p), audioProcessor(p), apvts(vts) +{ + setLookAndFeel(&customLookAndFeel); + + // Global section + setupKnob(rearrangeKnob, Section::Global); + rearrangeAttach = std::make_unique( + apvts, SupaTriggaProcessor::PROB_REARRANGE_ID, rearrangeKnob); + + setupKnob(slicesKnob, Section::Global); + slicesAttach = std::make_unique( + apvts, SupaTriggaProcessor::GRANULARITY_ID, slicesKnob); + + setupKnob(silenceKnob, Section::Global); + silenceAttach = std::make_unique( + apvts, SupaTriggaProcessor::PROB_SILENCE_ID, silenceKnob); + + // Brake section + setupKnob(brakeProbKnob, Section::Speed); + brakeProbAttach = std::make_unique( + apvts, SupaTriggaProcessor::PROB_SPEED_ID, brakeProbKnob); + + setupKnob(brakeTimeKnob, Section::Speed); + brakeTimeAttach = std::make_unique( + apvts, SupaTriggaProcessor::SPEED_ID, brakeTimeKnob); + + setupToggle(brakeInstantToggle, Section::Speed); + brakeInstantAttach = std::make_unique( + apvts, SupaTriggaProcessor::INSTANT_SPEED_ID, brakeInstantToggle); + + // Reverse section + setupKnob(reverseProbKnob, Section::Reverse); + reverseProbAttach = std::make_unique( + apvts, SupaTriggaProcessor::PROB_REVERSE_ID, reverseProbKnob); + + setupToggle(reverseInstantToggle, Section::Reverse); + reverseInstantAttach = std::make_unique( + apvts, SupaTriggaProcessor::INSTANT_REVERSE_ID, reverseInstantToggle); + + // Repeat section + setupKnob(repeatProbKnob, Section::Repeat); + repeatProbAttach = std::make_unique( + apvts, SupaTriggaProcessor::PROB_REPEAT_ID, repeatProbKnob); + + setupToggle(repeatInstantToggle, Section::Repeat); + repeatInstantAttach = std::make_unique( + apvts, SupaTriggaProcessor::INSTANT_REPEAT_ID, repeatInstantToggle); + + // Make window non-resizable + setResizable(false, false); + + // Set Window Size + setSize(Theme::Metrics::windowWidth, Theme::Metrics::windowHeight); +} + +SupaTriggaEditor::~SupaTriggaEditor() +{ + setLookAndFeel(nullptr); +} + +void SupaTriggaEditor::setupKnob(juce::Slider& slider, Section section) +{ + slider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); + slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0); + slider.setRotaryParameters(juce::MathConstants::pi * 1.25f, + juce::MathConstants::pi * 2.75f, true); + addAndMakeVisible(slider); + + juce::Colour accent; + switch (section) + { + case Section::Global: accent = Theme::Colors::globalAccent; break; + case Section::Speed: accent = Theme::Colors::speedAccent; break; + case Section::Reverse: accent = Theme::Colors::reverseAccent; break; + case Section::Repeat: accent = Theme::Colors::repeatAccent; break; + } + slider.setColour(juce::Slider::rotarySliderFillColourId, accent); +} + +void SupaTriggaEditor::setupToggle(juce::ToggleButton& toggle, Section section) +{ + toggle.setName(toggle.getToggleState() ? "On" : "Off"); + toggle.onClick = [&toggle] { + toggle.setName(toggle.getToggleState() ? "On" : "Off"); + }; + addAndMakeVisible(toggle); + + juce::Colour accent; + switch (section) + { + case Section::Global: accent = Theme::Colors::globalAccent; break; + case Section::Speed: accent = Theme::Colors::speedAccent; break; + case Section::Reverse: accent = Theme::Colors::reverseAccent; break; + case Section::Repeat: accent = Theme::Colors::repeatAccent; break; + } + toggle.setColour(juce::ToggleButton::tickColourId, accent); +} + +void SupaTriggaEditor::paint(juce::Graphics& g) +{ + // Background + g.fillAll(Theme::Colors::background); + + // Draw thick bottom line of header + g.setColour(Theme::Colors::border); + g.drawLine(0.0f, 56.0f, (float)getWidth(), 56.0f, 1.0f); + + // Draw header text + g.setColour(Theme::Colors::textPrimary); + g.setFont(juce::FontOptions(20.0f).withStyle("Bold")); + g.drawText("SUPATRIGGA", 20, 0, 200, 56, juce::Justification::centredLeft, true); + + g.setColour(Theme::Colors::textSecondaryDim); + g.setFont(juce::FontOptions(11.0f).withStyle("Bold")); + g.drawText("BY SMARTELECTRONIX", getWidth() - 200, 0, 180, 56, + juce::Justification::centredRight, true); + + // Section Dividers (the cross in the grid) + auto gridBounds = getLocalBounds().withTrimmedTop(56); + int midX = gridBounds.getCentreX(); + int midY = gridBounds.getCentreY(); + + g.setColour(Theme::Colors::border); + // Vertical line (indented top and bottom by 16px) + g.drawLine((float)midX, (float)(gridBounds.getY() + 16), + (float)midX, (float)(gridBounds.getBottom() - 16), 1.0f); + // Horizontal line (indented left/right by 20px, gap in center) + g.drawLine(20.0f, (float)midY, (float)(midX - 16), (float)midY, 1.0f); + g.drawLine((float)(midX + 16), (float)midY, (float)(getWidth() - 20), (float)midY, 1.0f); + + // Draw Section Titles + auto drawSectionTitle = [&](const juce::String& title, juce::Colour c, int x, int y) + { + g.setColour(c); + g.setFont(juce::FontOptions(14.0f).withStyle("Bold")); + g.drawText(title, x + 20, y + 16, 100, 20, juce::Justification::topLeft, true); + }; + + drawSectionTitle("GLOBAL", Theme::Colors::globalAccent, gridBounds.getX(), gridBounds.getY()); + drawSectionTitle("BRAKE", Theme::Colors::speedAccent, midX, gridBounds.getY()); + drawSectionTitle("REVERSE", Theme::Colors::reverseAccent, gridBounds.getX(), midY); + drawSectionTitle("REPEAT", Theme::Colors::repeatAccent, midX, midY); + + // Draw Sub-labels for controls + auto drawLabel = [&](const juce::String& text, juce::Component& comp) + { + g.setColour(Theme::Colors::textSecondary); + g.setFont(juce::FontOptions(11.0f).withStyle("Bold")); + auto bounds = comp.getBounds(); + g.drawText(text, bounds.getX() - 20, bounds.getY() - 28, + bounds.getWidth() + 40, 20, juce::Justification::centredBottom, false); + }; + + // Global Section Label centers + drawLabel("REARRANGE", rearrangeKnob); + drawLabel("SLICES", slicesKnob); + drawLabel("SILENCE", silenceKnob); + + // Brake Section Label centers + drawLabel("BRAKE PROB", brakeProbKnob); + drawLabel("BRAKE TIME", brakeTimeKnob); + drawLabel("INSTANT", brakeInstantToggle); + + // Reverse Section Label centers + drawLabel("REVERSE PROB", reverseProbKnob); + drawLabel("INSTANT", reverseInstantToggle); + + // Repeat Section Label centers + drawLabel("REPEAT PROB", repeatProbKnob); + drawLabel("INSTANT", repeatInstantToggle); +} + +void SupaTriggaEditor::resized() +{ + auto gridBounds = getLocalBounds().withTrimmedTop(56); + + int midX = gridBounds.getWidth() / 2; + int midY = gridBounds.getHeight() / 2; + + auto globalRect = gridBounds.withWidth(midX).withHeight(midY); + auto speedRect = globalRect.withX(midX); + auto reverseRect = globalRect.withY(gridBounds.getY() + midY); + auto repeatRect = reverseRect.withX(midX); + + // Controls Row bounds (leave room for title and labels) + auto globalRow = globalRect.withTrimmedTop(40).withTrimmedBottom(18); + auto speedRow = speedRect.withTrimmedTop(40).withTrimmedBottom(18); + auto reverseRow = reverseRect.withTrimmedTop(40).withTrimmedBottom(18); + auto repeatRow = repeatRect.withTrimmedTop(40).withTrimmedBottom(18); + + auto buildFlex = [](juce::FlexBox& fb) + { + fb.justifyContent = juce::FlexBox::JustifyContent::spaceAround; + fb.alignItems = juce::FlexBox::AlignItems::flexEnd; + fb.flexDirection = juce::FlexBox::Direction::row; + }; + + auto mapItem = [](juce::Component& c) + { + return juce::FlexItem(c).withWidth(80).withHeight(80).withMargin( + juce::FlexItem::Margin(0, 4, 0, 4)); + }; + + { + juce::FlexBox fb; + buildFlex(fb); + fb.items.add(mapItem(rearrangeKnob)); + fb.items.add(mapItem(slicesKnob)); + fb.items.add(mapItem(silenceKnob)); + fb.performLayout(globalRow); + } + + { + juce::FlexBox fb; + buildFlex(fb); + fb.items.add(mapItem(brakeProbKnob)); + fb.items.add(mapItem(brakeTimeKnob)); + fb.items.add(mapItem(brakeInstantToggle)); + fb.performLayout(speedRow); + } + + { + juce::FlexBox fb; + buildFlex(fb); + fb.items.add(mapItem(reverseProbKnob)); + fb.items.add(mapItem(reverseInstantToggle)); + fb.performLayout(reverseRow); + } + + { + juce::FlexBox fb; + buildFlex(fb); + fb.items.add(mapItem(repeatProbKnob)); + fb.items.add(mapItem(repeatInstantToggle)); + fb.performLayout(repeatRow); + } +} diff --git a/supatrigga/Source/SupaTriggaEditor.h b/supatrigga/Source/SupaTriggaEditor.h new file mode 100644 index 0000000..3eefa8b --- /dev/null +++ b/supatrigga/Source/SupaTriggaEditor.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include "PluginProcessor.h" + +class SupaTriggaLookAndFeel : public juce::LookAndFeel_V4 +{ +public: + SupaTriggaLookAndFeel(); + + void drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height, + float sliderPosProportional, float rotaryStartAngle, float rotaryEndAngle, + juce::Slider& slider) override; + + void drawToggleButton(juce::Graphics& g, juce::ToggleButton& button, + bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override; + +private: + juce::Typeface::Ptr interBold; +}; + +class SupaTriggaEditor : public juce::AudioProcessorEditor +{ +public: + SupaTriggaEditor(SupaTriggaProcessor&, juce::AudioProcessorValueTreeState&); + ~SupaTriggaEditor() override; + + void paint(juce::Graphics&) override; + void resized() override; + +private: + [[maybe_unused]] SupaTriggaProcessor& audioProcessor; + juce::AudioProcessorValueTreeState& apvts; + + SupaTriggaLookAndFeel customLookAndFeel; + + enum class Section { Global, Speed, Reverse, Repeat }; + + void setupKnob(juce::Slider& slider, Section section); + void setupToggle(juce::ToggleButton& toggle, Section section); + + // Header Components + // Not strictly needed as component, can be drawn in paint() instead to save overhead. + + // Global Section + juce::Slider rearrangeKnob; + juce::Slider slicesKnob; + juce::Slider silenceKnob; + + // Brake Section + juce::Slider brakeProbKnob; + juce::Slider brakeTimeKnob; + juce::ToggleButton brakeInstantToggle; + + // Reverse Section + juce::Slider reverseProbKnob; + juce::ToggleButton reverseInstantToggle; + + // Repeat Section + juce::Slider repeatProbKnob; + juce::ToggleButton repeatInstantToggle; + + // Attachments + using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment; + using ButtonAttachment = juce::AudioProcessorValueTreeState::ButtonAttachment; + + std::unique_ptr rearrangeAttach; + std::unique_ptr slicesAttach; + std::unique_ptr silenceAttach; + std::unique_ptr brakeProbAttach; + std::unique_ptr brakeTimeAttach; + std::unique_ptr brakeInstantAttach; + std::unique_ptr reverseProbAttach; + std::unique_ptr reverseInstantAttach; + std::unique_ptr repeatProbAttach; + std::unique_ptr repeatInstantAttach; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SupaTriggaEditor) +}; diff --git a/supatrigga/Source/Theme.h b/supatrigga/Source/Theme.h new file mode 100644 index 0000000..8317c02 --- /dev/null +++ b/supatrigga/Source/Theme.h @@ -0,0 +1,34 @@ +#pragma once +#include + +namespace Theme { + namespace Colors { + inline const juce::Colour background { 0xff16161e }; + inline const juce::Colour track { 0xff2a2a3a }; + inline const juce::Colour innerCircle { 0xff252535 }; + inline const juce::Colour border { 0xff2e2e40 }; + + // Text colors + inline const juce::Colour textPrimary { 0xffe8e8f0 }; + inline const juce::Colour textSecondary { 0xff8888a0 }; + inline const juce::Colour textSecondaryDim{ 0xcc8888a0 }; + inline const juce::Colour textValue { 0xffffffff }; + + // Accent colors + inline const juce::Colour globalAccent { 0xfff0c040 }; + inline const juce::Colour speedAccent { 0xffe05050 }; + inline const juce::Colour reverseAccent { 0xff40c8c8 }; + inline const juce::Colour repeatAccent { 0xffa060e0 }; + } // namespace Colors + + namespace Metrics { + constexpr int windowWidth = 640; + constexpr int windowHeight = 400; + constexpr int headerHeight = 56; + constexpr float knobRadius = 34.0f; + constexpr float innerRadius = 24.0f; + constexpr float trackThickness = 6.0f; + constexpr float rotaryStartAngle = juce::MathConstants::pi * 1.25f; + constexpr float rotaryEndAngle = juce::MathConstants::pi * 2.75f; + } +}