How to Implement the Kentaro-style RotarySlider

Introduction

The other day, suzuki kentaro released Particle-Reverb v6.0. There is a RotarySlider that will be created in this article.

I’ve taken the liberty of calling this modern UI Kentaro-style. Trying to recreate the Kentaro-style UI with JUCE is quite a struggle, but there are many things I can learn.

In this article, we will finally complete the RotarySlider as shown below. Let’s do this!

Prerequisites

Nothing else is required except that you have created a new project. I created a new project with the name ModernStyle.

Creating a basic RotarySlider

In this chapter, we will quickly create a simple and basic RotarySlider.

Addding new CPP & HeaderFile

First, create a CPP and a HeaderFile for the RotarySlider to avoid cluttering up the PluginEditor.h file. We can easily create it with Projucer by following the steps below.

Customizing Slider class

Now, we will create a class that inherits from the Slider class named RotarySlider, and we will just override the function as shown below.

class RotarySlider  : public juce::Slider
{
public:
    RotarySlider();
    ~RotarySlider() override;

    void mouseDrag (const juce::MouseEvent& event) override;
    void mouseUp (const juce::MouseEvent& event) override;

private:    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RotarySlider)
};
RotarySlider::RotarySlider()
{
    setSliderStyle (juce::Slider::SliderStyle::RotaryVerticalDrag);
    setTextBoxStyle (juce::Slider::NoTextBox, true, 0, 0);
    setVelocityBasedMode (true);
    setVelocityModeParameters (0.5, 1, 0.09, false);
}

RotarySlider::~RotarySlider()
{
}

void RotarySlider::mouseDrag (const juce::MouseEvent& event) 
{
    juce::Slider::mouseDrag (event);
    event.source.enableUnboundedMouseMovement (true);
}

void RotarySlider::mouseUp (const juce::MouseEvent& event)
{
    juce::Slider::mouseUp (event);
    juce::Desktop::getInstance().getMainMouseSource().setScreenPosition (event.source.getLastMouseDownPosition());
}    

The things we implement in mouseDrag() and mouseUp() are really minor things, so you don’t have to do them. If you want the mouse pointer to disappear when you click and drag RotarySlider, or if you don’t want the mouse pointer to move even a little bit after you are done dragging, then this implementation is a must.

Declaring the object

Be careful not to forget to include RotarySlider.h, and declare an object of the RotarySlider class.

#pragma once

#include <JuceHeader.h>
#include "PluginProcessor.h"
#include "RotarySlider.h"

class ModernStyleAudioProcessorEditor  : public juce::AudioProcessorEditor
{
public:
・・・
private:
    RotarySlider rotarySlider;

Setting the properties

In the constructor below, call addAndMakeVisible() to make the rotarySlider object visible in the editor.

ModernStyleAudioProcessorEditor::ModernStyleAudioProcessorEditor (ModernStyleAudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
{
    setSize (400, 300);
    
    addAndMakeVisible (rotarySlider);
}

Then, in a rather minor detail, set the background color to a smoky black.

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

Finally, we’ll place the component in the center of the editor. setBounds() has fixed values for all arguments, since we only have one component to display and we don’t want to make it complicated.

void ModernStyleAudioProcessorEditor::resized()
{
    rotarySlider.setBounds (150, 100, 100, 100);
}

Okay, so let’s build it and see.

Customizing LookAndFeel

In this chapter, we will evolve the default appearance of RotarySlider to a Kentaro-Style one. Here’s the before & after:

Overriding drawRotarySlider()

First, prepare the CPP & HeaderFile for the CustomLookAndFeel class, which is a customized LookAndFeel class.

Then, override drawRotarySlider(), a member function that sets the appearance of the RotarySlider.

#pragma once

#include <JuceHeader.h>

class CustomLookAndFeel : public juce::LookAndFeel_V4
{
public:
    void drawRotarySlider (juce::Graphics&, int x, int y, int width, int height,
                           float sliderPosProportional, float rotaryStartAngle,
                           float rotaryEndAngle, juce::Slider&) override;
    
private:
    juce::Colour offWhite = juce::Colour::fromFloatRGBA (0.831373, 0.835294, 0.890196, 1.0);
    juce::Colour blackGrey = juce::Colour::fromFloatRGBA (0.2, 0.2, 0.2, 0.8);
};
void CustomLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height, float sliderPos,
                                          const float rotaryStartAngle, const float rotaryEndAngle, juce::Slider& slider)
{
    auto fill = slider.findColour (juce::Slider::rotarySliderFillColourId);

    auto bounds = juce::Rectangle<float> (x, y, width, height).reduced (5.0f);
    auto radius = juce::jmin (bounds.getWidth(), bounds.getHeight()) / 2.0f;

    auto toAngle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
    auto lineW = radius * 0.1f;
    auto arcRadius = radius - lineW * 0.5f;
    
    g.setColour (blackGrey);
    g.fillEllipse(bounds.reduced (4.8f));

    juce::Path valueArc;
    valueArc.addCentredArc (bounds.getCentreX(),
                            bounds.getCentreY(),
                            arcRadius,
                            arcRadius,
                            0.0f,
                            rotaryStartAngle,
                            toAngle,
                            true);

    g.setColour (fill);
    g.strokePath (valueArc, juce::PathStrokeType (lineW, juce::PathStrokeType::beveled, juce::PathStrokeType::butt));

    auto thumbWidth = lineW * 2.0f;
 
    juce::Path thumb;
    thumb.addRectangle (-thumbWidth / 2, -thumbWidth / 2, thumbWidth, radius + lineW);
    
    g.setColour (offWhite);
    g.fillPath (thumb, juce::AffineTransform::rotation (toAngle + 3.12f).translated(bounds.getCentre()));

    g.fillEllipse (bounds.reduced (12.0f));
}

The next step is to declare an object of this class in RotarySlider.h. Make sure you don’t forget to include CustomLookAndFeel.h.

#pragma once

#include <JuceHeader.h>
#include "CustomLookAndFeel.h"

class RotarySlider  : public juce::Slider
{
public:
・・・
private:    
    CustomLookAndFeel customLookAndFeel;

Finally, we call setLookAndFeel and specify the address of the object we just declared.

RotarySlider::RotarySlider()
{
    setSliderStyle (juce::Slider::SliderStyle::RotaryVerticalDrag);
    setTextBoxStyle (juce::Slider::NoTextBox, true, 0, 0);
    setLookAndFeel (&customLookAndFeel);
    setRotaryParameters (4.0f, 8.6f, true);
}

Now let’s build and check the appearance of RotarySlider.

Adding a NumberBox

In this chapter, we will add a NumberBox to the RotarySlider and display the values.

Creating a NumberBox

In a previous article, I explained how to implement the NumberBox. This can be dragged to change the value. The reason why we use this instead of labels is that if we use labels, we can’t drag over them. Of course, by overriding mouseDrag(), you can drag over the label, but it is very time-consuming. For this reason, we decided to use the high-performance NumberBox(Slider) that we had already created.

This time, we will just change this a little.

class RotarySlider  : public juce::Slider
{
public:
    RotarySlider();
    ~RotarySlider() override;
    
    void resized () override;
    
    void mouseDrag (const juce::MouseEvent& event) override;
    void mouseUp (const juce::MouseEvent& event) override;

private:
    struct NumberBox  : public juce::Slider
    {
        NumberBox()
        {
            setSliderStyle (juce::Slider::LinearBarVertical);
            setColour (juce::Slider::textBoxTextColourId, black);
            setColour (juce::Slider::textBoxOutlineColourId, juce::Colours::transparentWhite);
            setColour (juce::Slider::trackColourId, juce::Colours::transparentWhite);
            setTextBoxIsEditable (false);
            setVelocityBasedMode (true);
            setVelocityModeParameters (0.5, 1, 0.09, false);
            setRange (0.0, 100.0, 0.01);
            setTextValueSuffix (" %");
            onValueChange = [&]()
            {
                if (getValue() < 10)
                {
                    setNumDecimalPlacesToDisplay (2);
                }
                else if (getValue() >= 10 && getValue() < 100)
                {
                    setNumDecimalPlacesToDisplay (1);
                }
                else if (getValue() == 100)
                {
                    setNumDecimalPlacesToDisplay (0);
                }
            };
        }
        
        ~NumberBox() {};

        void mouseDrag (const juce::MouseEvent& event) override
        {
            juce::Slider::mouseDrag (event);
            event.source.enableUnboundedMouseMovement (true);
        }

        void mouseUp (const juce::MouseEvent& event) override
        {
            juce::Slider::mouseUp (event);
            juce::Desktop::getInstance().getMainMouseSource().setScreenPosition (event.source.getLastMouseDownPosition());
        }

        juce::Colour black = juce::Colour::fromFloatRGBA (0.08, 0.08, 0.08, 1.0);
    };
    
    NumberBox numberBox;
    
    CustomLookAndFeel customLookAndFeel;
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RotarySlider)
};

Some of you may wonder why we are using setNumDecimalPlacesToDisplay(). This is to prevent the % from shifting position when the value is being changed. For example, when the value changes from 0.0% to 10.0%, the % position shifts slightly to the right, and when the value changes to 100.0%, it shifts further to the right. Therefore, by making sure to align the three digits (0.00%, 100%, and 10.0%), we can eliminate such misalignment. It may be a small difference, but it seems to me that there is quite a difference between doing this and not doing it.

Connecting to RotarySlider

Next, we will implement the constructor of the RotarySlider class as follows

RotarySlider::RotarySlider()
{
    setSliderStyle (juce::Slider::SliderStyle::RotaryVerticalDrag);
    setTextBoxStyle (juce::Slider::NoTextBox, true, 0, 0);
    setLookAndFeel (&customLookAndFeel);
    setRotaryParameters (4.0f, 8.6f, true);
    setVelocityBasedMode (true);
    setVelocityModeParameters (0.5, 1, 0.09, false);
    setRange (0, 100, 0.01);
    
    addAndMakeVisible (numberBox);
    numberBox.getValueObject().referTo (getValueObject());

    onValueChange = [&]()
    {
        if (getValue() < 10)
        {
            numberBox.setNumDecimalPlacesToDisplay (2);
        }
        else if (getValue() >= 10 && getValue() < 100)
        {
            numberBox.setNumDecimalPlacesToDisplay (1);
        }
        else if (getValue() == 100)
        {
            numberBox.setNumDecimalPlacesToDisplay (0);
        }
    };
}

By calling referTo(), the value of the NumberBox is linked to the value of the RotarySlider. In addition, setNumbDecimalPlacesToDisplay() is implemented in this constructor so that this function is successfully called not only when dragging on the NumberBox, but also when dragging on the RotarySlider.

Then, override resized() as shown below so that the NumberBox is placed in the center of the RotarySlider.

void RotarySlider::resized ()
{
    juce::Slider::resized();
    numberBox.setBounds (getLocalBounds().reduced (20));
}

Okay, now let’s build and check it out.

Overriding createSliderTextBox()

It’s cool enough as it is, but I think the text could be made a little bit larger. To do so, we need to override the createSliderTextBox of the LookAndFeel class.

class CustomLookAndFeel : public juce::LookAndFeel_V4
{
public:
    juce::Label* createSliderTextBox (juce::Slider& slider) override;
juce::Label* CustomLookAndFeel::createSliderTextBox (juce::Slider& slider)
{
    auto* l = LookAndFeel_V2::createSliderTextBox (slider);
    l->setFont (18);

    return l;
}

Finally, apply CustomLookAndFeel to the NumberBox object.

RotarySlider::RotarySlider()
{
    setSliderStyle (juce::Slider::SliderStyle::RotaryVerticalDrag);
    setTextBoxStyle (juce::Slider::NoTextBox, true, 0, 0);
    setLookAndFeel (&customLookAndFeel);
    setVelocityBasedMode (true);
    setVelocityModeParameters (0.5, 1, 0.09, false);
    setRange (0, 100, 0.1);
    setRotaryParameters (4.0f, 8.6f, true);
    addAndMakeVisible (numberBox);
    numberBox.getValueObject().referTo (getValueObject());
    numberBox.setLookAndFeel (&customLookAndFeel);

That completes the implementation of the Kentaro-style RotarySlider! Let’s build and check it out!

Conclusion

In this article, I explained how to implement a Kentaro-style RotarySlider. Trying to recreate Kentaro-style UI in JUCE requires a tremendous amount of time and energy, but there are many things I can learn from it.

If there is a more efficient way to implement it, please let us know. Also, if there is a wrong interpretation. Thank you for reading to the end. I will continue to learn English and JUCE.

Comments

Copied title and URL