Reproducing the UI of MOD-D3 with JUCE (1) : XYZ RangeFaders

Introduction

Recently, I have been challenging myself to implement the UI of MOD-D3 with JUCE.

This is MOD-D3. The other day v.1.0 was released for free.

If I were to reproduce everything perfectly, it would take quite a lot of time and volume, so I will divide the article into several parts.

I’ll start by trying to reproduce the XYZ RangeFaders part below.

However, the XYZ RangeFaders part alone was quite large, so I will only reproduce it in this article up to the following part:

Let’s start with the creation of a new project, and then I will explain the steps and main points until the above objectives are completed.

Creating a new project

In this case, we will create a new project with the name ModD3Tutorial as shown below:

Creating XYZ RangeFaders

In this chapter, we will implement the basic part of XYZ RangeFader.

Customizing Slider

First, create a RangeFader class that inherits from the Slider class, and implement the constructor as shown below:

class RangeFader : public juce::Slider
{
public:
    RangeFader()
    {
        setSliderStyle (juce::Slider::TwoValueHorizontal);
        setTextBoxStyle (juce::Slider::NoTextBox, true, 0, 0);
        setVelocityBasedMode (true);
        setVelocityModeParameters (0.5, 1, 0.09, false);
        setRange (0.0, 100.0, 0.5);
        setMinAndMaxValues (0.0, 100.0);
    }
};

This way, we can reduce duplicated code when implementing multiple RangeFaders.

The description of each member function is as follows

setSliderStyle()Specify the type of slider.
TwoValueHorizontal is a slider that moves horizontally with two thumbs.
setTextBoxStyle()Specify the type, size, etc. of the text box.
NoTextBox hides the text box.
setVelocityBasedMode()Specify true to turn on velocity-based mode.
setVelocityModeParameters()Set various parameters related to velocity-based mode
setRange()Set the range of values.
setMinAndMaxValues()Set the minimum and maximum values.

Declaring XYZ RangeFaders object

MOD-D3 has three RangeFaders(X, Y, Z), so we will prepare three objects:

class ModD3TutorialAudioProcessorEditor  : public juce::AudioProcessorEditor
{
・・・
private:
    RangeFader xRangeFader;
    RangeFader yRangeFader;
    RangeFader zRangeFader;
    
    ModD3AudioProcessor& audioProcessor;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ModD3AudioProcessorEditor)
};

Setting the properties of RangeFader

We can set the properties of RangerFader with various functions in the editor constructor. 

However, many of the necessary member functions are already implemented in the constructor of the RangeFader class, so you can just call addAndMakeVisible() for now:

ModD3TutorialAudioProcessorEditor::ModD3TutorialAudioProcessorEditor (ModD3TutorialAudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
{
    setSize (200, 300);
    
    addAndMakeVisible (&xRangeFader);
    addAndMakeVisible (&yRangeFader);
    addAndMakeVisible (&zRangeFader);
}

Setting the placement and size.

Next, in the member function resize(), which is executed when the window is created or resized, set the placement and size of the RangeFader as shown below:

void ModD3TutorialAudioProcessorEditor::resized()
{
    xRangeFader.setBounds (5, getHeight() / 2 - 90, getWidth() - 10, 30);
    yRangeFader.setBounds (5, getHeight() / 2,      getWidth() - 10, 30);
    zRangeFader.setBounds (5, getHeight() / 2 + 90, getWidth() - 10, 30);
}

Specify the x-coordinate and y-coordinate values for the first and second arguments of setBounds(). Then, specify the width and height values of the RangeFader for the third and fourth arguments.

Also, if you use getHeight() and getWidth() as described above, it will give you a nice response when resizing. In this case, getHeight() will get the height of the window, and getWidth will get the width of the window.

Completing the basics of RangeFader

The implementation of the basic part of RangeFader is now complete. Let’s build it and see what it looks like now.

Of course, it is quite different from the appearance of MOD-D3 RangeFaders.

Customizing look-and-feel

In this chapter, we will adjust the appearance of RangeFaders.

Before
After

Overriding drawLinearSlider()

First, create a CustomLAF class that inherits from the LookAndFeel_V4 class, and override drawLinearSlider() as shown below:

class CustomLAF : public juce::LookAndFeel_V4
{
public:
    void drawLinearSlider (juce::Graphics& g, int x, int y, int width, int height,
                           float sliderPos, float minSliderPos, float maxSliderPos,
                           const juce::Slider::SliderStyle style, juce::Slider& slider) override
    {
        float trackWidth = 2.0f;

        juce::Point<float> startPoint ((float) x, (float) y + (float) height * 0.5f);
        juce::Point<float> endPoint ((float) (width + x), startPoint.y);

        // Dashed BackgroundTrack
        juce::Path backgroundTrack;
        backgroundTrack.startNewSubPath (startPoint);
        backgroundTrack.lineTo (endPoint);
        g.setColour (blackGrey);
        juce::PathStrokeType pathStrokeType (1.0);
        float dashedLength[2] = {2, 4};
        pathStrokeType.createDashedStroke (backgroundTrack, backgroundTrack, dashedLength, 2);
        g.strokePath (backgroundTrack, pathStrokeType);

        // ValueTrack
        juce::Path valueTrack;
        juce::Point<float> minPoint, maxPoint;
        minPoint = { minSliderPos, (float) height * 0.5f };
        maxPoint = { maxSliderPos, (float) height * 0.5f };
                    
        valueTrack.startNewSubPath (minPoint);
        valueTrack.lineTo (maxPoint);
        g.setColour (grey);
        g.strokePath (valueTrack, { trackWidth, juce::PathStrokeType::curved, juce::PathStrokeType::rounded });
                
        // TwoValueHorizontal SliderThumb
        auto minThumbWidth = getSliderThumbRadius (slider);
        auto maxThumbWidth = getSliderThumbRadius (slider) + 4;

        int thumbBeingDragged = slider.getThumbBeingDragged();
                
        if (thumbBeingDragged == 1 || thumbBeingDragged == 2)  // Either Thumb is being dragged(1 = min, 2 = max)
        {
            slider.setMouseCursor (juce::MouseCursor::NoCursor);
            
            // Thumb being dragged
            g.setColour (grey);
            g.fillEllipse (juce::Rectangle<float> (static_cast<float> (thumbBeingDragged == 1 ? minThumbWidth : maxThumbWidth),
                                                   static_cast<float> (thumbBeingDragged == 1 ? minThumbWidth : maxThumbWidth))
                                                   .withCentre (thumbBeingDragged == 1 ? minPoint : maxPoint));
            // The other Thumb not dragged
            g.setColour (offWhite);
            g.drawRoundedRectangle (juce::Rectangle<float> (static_cast<float> (thumbBeingDragged == 1 ? maxThumbWidth : minThumbWidth),
                                                            static_cast<float> (thumbBeingDragged == 1 ? maxThumbWidth : minThumbWidth))
                                                            .withCentre (thumbBeingDragged == 1 ? maxPoint : minPoint), 50, 1.0);
        }
        else  // Not dragging anything
        {
            slider.setMouseCursor (juce::MouseCursor::NormalCursor);
            
            g.setColour (offWhite);
            g.drawRoundedRectangle (juce::Rectangle<float> (static_cast<float> (maxThumbWidth), static_cast<float> (maxThumbWidth)).withCentre (maxPoint), 50, 1.0);
            g.drawRoundedRectangle (juce::Rectangle<float> (static_cast<float> (minThumbWidth), static_cast<float> (minThumbWidth)).withCentre (minPoint), 50, 1.0);
        }
    }

private:
    juce::Colour blue = juce::Colour::fromFloatRGBA (0.42, 0.83, 1.0, 1.0);
    juce::Colour stYellow = juce::Colour::fromFloatRGBA (0.89, 0.90, 0.27, 1.0);
    juce::Colour offWhite = juce::Colour::fromFloatRGBA (0.83, 0.83, 0.89, 1.0);
    juce::Colour grey = juce::Colour::fromFloatRGBA (0.42, 0.42, 0.42, 1.0);
    juce::Colour blackGrey = juce::Colour::fromFloatRGBA (0.2, 0.2, 0.2, 1.0);
};

Here is a summary of what is implemented in this member function.

  • Dashed Line Background Track
  • Gray Value Track
  • When a round thumb with a white border is being dragged, it will be filled in gray
  • The mouse pointer disappears from the moment you click thumb

Setting CustomLAF to RangeFaders

We can set the customized look-and-feel by calling setLookAndFeel().

Since it would be useless to set it in each RangeFader, we will set it in the constructor of the RangeFader class that we just implemented. Add the following two highlighted lines:

class RangeFader : public juce::Slider
{
public:
    RangeFader()
    {
        setLookAndFeel (&customLAF);
        setSliderStyle (juce::Slider::TwoValueHorizontal);
        setTextBoxStyle (juce::Slider::NoTextBox, true, 100, 20);
        setVelocityBasedMode (true);
        setVelocityModeParameters (0.5, 1, 0.09, false);
        setRange (0.0, 100.0, 0.5);
        setMinAndMaxValues (0.0, 100.0);
    }
private:
    CustomLAF customLAF;
};

In addition, adjust the background of the window as shown below:

void ModD3TutorialAudioProcessorEditor::paint (juce::Graphics& g)
{
    juce::Colour black = juce::Colour::fromFloatRGBA (0.08, 0.08, 0.08, 1.0);
    g.fillAll (black);
}

Completion

This completes the implementation of the appearance of RangeFader. Let’s build and check it out.

Conclusion

This time, I implemented the RangeFader of MOD-D3. In the next article, I will implement the draggable NumberBox. There is already an article posted on this subject, but it is written in Japanese, so I will rewrite it in English with my new findings.

Lastly, I am not used to English, so my writing may be difficult to understand. I will learn English everyday and devote myself to writing articles that are easier to understand. Thank you for reading to the end.

Comments

Copied title and URL