Creating an Ableton Style Custom Dial

 

GitHub Repository

Introduction: Creating a custom dial?

Now that we have a custom look and feel class, we can use this to hook into the JUCE UI classes using the appropriate “LookAndFeelMethods” structs that reside within each class.  These have custom methods where we can adapt the look to our taste, while keeping the core functionality that JUCE provides.

In this tutorial, you will learn how to create an “Ableton style” dial by overriding drawRotaryDial() in Slider::LookAndFeelMethods.

Core Concepts

This tutorial assumes a basic understanding of C++ and the JUCE Framework.  Here are some concepts to familiarize yourself with to ensure a smooth journey through this tutorial.

Method

1. Create a rotary dial 

In MainComponent.h and MainComponent.cpp, create a rotary horizontal and vertical dial using the JUCE slider class. We will give it some basic settings and place it in the middle of our screen. 

// In MainComponent.h
juce::Slider mySlider;

// In MainComponent.cpp
MainComponent::MainComponent()
{
    juce::LookAndFeel::setDefaultLookAndFeel (&myCustomLNF);

    mySlider.setSliderStyle (juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag);
    mySlider.setRange (0.0f, 100.0f);
    mySlider.setValue (25.0f);
    mySlider.setTextBoxStyle (juce::Slider::NoTextBox, true, 100, 25);
    addAndMakeVisible (mySlider);

    setSize (400, 400);
}

void MainComponent::resized()
{
    mySlider.setBounds (getWidth() / 2 - 100, getHeight() / 2 - 100, 200, 200);
}

2. Override drawRotarySlider() in our LNF and paste the default JUCE code as a starting point. 

Now let’s override Slider::LookAndFeelMethods function drawRotarySlider().  Trying to create this from scratch can be a challenge, so let’s use the code from the default implementation as a starting point.  This resides in LookAndFeel_V4 class. Let’s comment each part so we know what’s happening.

void CustomLNF::drawRotarySlider (Graphics& g, int x, int y, int width, int height, float sliderPos, float rotaryStartAngle, float rotaryEndAngle, Slider& slider)
{
    auto outline = slider.findColour (Slider::rotarySliderOutlineColourId);
    auto fill    = slider.findColour (Slider::rotarySliderFillColourId);

    auto bounds = Rectangle<int> (x, y, width, height).toFloat().reduced (10);

    auto radius = jmin (bounds.getWidth(), bounds.getHeight()) / 2.0f;
    auto toAngle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
    auto lineW = jmin (8.0f, radius * 0.5f);
    auto arcRadius = radius - lineW * 0.5f;

    // Dial background path
    Path backgroundArc;
    backgroundArc.addCentredArc (bounds.getCentreX(),
                                 bounds.getCentreY(),
                                 arcRadius,
                                 arcRadius,
                                 0.0f,
                                 rotaryStartAngle,
                                 rotaryEndAngle,
                                 true);

    g.setColour (outline);
    g.strokePath (backgroundArc, PathStrokeType (lineW, PathStrokeType::curved, PathStrokeType::rounded));

    if (slider.isEnabled())
    {
        // Dial fill path
        Path valueArc;
        valueArc.addCentredArc (bounds.getCentreX(),
                                bounds.getCentreY(),
                                arcRadius,
                                arcRadius,
                                0.0f,
                                rotaryStartAngle,
                                toAngle,
                                true);

        g.setColour (fill);
        g.strokePath (valueArc, PathStrokeType (lineW, PathStrokeType::curved, PathStrokeType::rounded));
    }

    // Thumb
    auto thumbWidth = lineW * 2.0f;
    Point<float> thumbPoint (bounds.getCentreX() + arcRadius * std::cos (toAngle - MathConstants<float>::halfPi),
                             bounds.getCentreY() + arcRadius * std::sin (toAngle - MathConstants<float>::halfPi));

    g.setColour (slider.findColour (Slider::thumbColourId));
    g.fillEllipse (Rectangle<float> (thumbWidth, thumbWidth).withCentre (thumbPoint));
}

3. Adapting this code to our needs.

Let’s now adapt this code, replacing the circular “thumb” indicator with a line that extends from the center of the dial to the arc. To do this, we need to find the center of the arc by finding the bounds of the arc then returning the center point.  We then extend it to where our thumb used to be, but adapt that location so our line doesn’t draw all the way into the arc.

void CustomLNF::drawRotarySlider (Graphics& g, int x, int y, int width, int height, float sliderPos, float rotaryStartAngle, float rotaryEndAngle, Slider& slider)
{
    auto outline = slider.findColour (Slider::rotarySliderOutlineColourId);
    auto fill    = slider.findColour (Slider::rotarySliderFillColourId);

    auto bounds = Rectangle<int> (x, y, width, height).toFloat().reduced (10);

    auto radius = jmin (bounds.getWidth(), bounds.getHeight()) / 2.0f;
    auto toAngle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
    auto lineW = jmin (8.0f, radius * 0.5f);
    auto arcRadius = radius - lineW * 0.5f;

    // Dial background path
    Path backgroundArc;
    backgroundArc.addCentredArc (bounds.getCentreX(),
                                 bounds.getCentreY(),
                                 arcRadius,
                                 arcRadius,
                                 0.0f,
                                 rotaryStartAngle,
                                 rotaryEndAngle,
                                 true);

    g.setColour (outline);
    g.strokePath (backgroundArc, PathStrokeType (lineW, PathStrokeType::curved, PathStrokeType::rounded));

    if (slider.isEnabled())
    {
        // Dial fill path
        Path valueArc;
        valueArc.addCentredArc (bounds.getCentreX(),
                                bounds.getCentreY(),
                                arcRadius,
                                arcRadius,
                                0.0f,
                                rotaryStartAngle,
                                toAngle,
                                true);

        g.setColour (fill);
        g.strokePath (valueArc, PathStrokeType (lineW, PathStrokeType::curved, PathStrokeType::rounded));
    }

    // Thumb
    Point<float> thumbPoint (bounds.getCentreX() + (arcRadius - 10.0f) * std::cos (toAngle - MathConstants<float>::halfPi),
                             bounds.getCentreY() + (arcRadius - 10.0f) * std::sin (toAngle - MathConstants<float>::halfPi));

    g.setColour (slider.findColour (Slider::thumbColourId));
    g.drawLine (backgroundArc.getBounds().getCentreX(), backgroundArc.getBounds().getCentreY(), thumbPoint.getX(), thumbPoint.getY(), lineW);
}

4. Changing the start and end angle of the arc.

Now let’s change the start and end positions of the arc.  Fortunately, the JUCE slider class has a RotaryParameters struct where we can define the start and end angles.  Now it’s just a matter of finding the proper angles in radians.  The documentation states that counting begins from the top of the circle (12 o’clock) and goes clockwise.  Here’s a rough drawing of the calculations based on this information.  

5. Calculate new start and end angles of the dial, and create a new custom dial class.

We want our starting position to be 9 o’clock, so this calculates as pi * 1.5.

The end position is 6 o’clock.  Since this needs to be more than our start position, we need to go around the circle past this position another 1.5 pi, giving us a finishing position of pi * 3.0.

Since we potentially want to draw all of our sliders like this, we will inherit from the JUCE slider class and create a custom dial class.  Here, we will use getRotaryParameters() to update these start and end positions, and give custom colors to the dial using juce::Slider::ColourIds. We’ll also take our other slider information and move it from the MainComponent to this new slider class.  Let’s put this in our StyleSheet.

class CustomDial : public Slider
{
public:
    CustomDial()
    {
        auto rotaryParams = getRotaryParameters();
        rotaryParams.startAngleRadians = juce::MathConstants<float>::pi * 1.5f;
        rotaryParams.endAngleRadians = juce::MathConstants<float>::pi * 3.0f;
        setRotaryParameters (rotaryParams);
        
        setSliderStyle (juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag);
        setRange (0.0f, 100.0f);
        setValue (25.0f);
        setTextBoxStyle (juce::Slider::NoTextBox, true, 100, 25);
        
        setColour (juce::Slider::ColourIds::rotarySliderFillColourId, juce::Colours::orange);
        setColour (juce::Slider::ColourIds::thumbColourId, juce::Colours::orange);
    }
};

6. Calculate new start and end angles of the dial, and create a new custom dial class.

Now we’re ready to replace the default JUCE slider with our custom slider in our Main Component.

// In header
juce::CustomDial myDial;

// In cpp
MainComponent::MainComponent()
{
    juce::LookAndFeel::setDefaultLookAndFeel (&myCustomLNF);
    addAndMakeVisible (myDial);
    setSize (400, 400);
}

void MainComponent::resized()
{
    myDial.setBounds (getWidth() / 2 - 100, getHeight() / 2 - 100, 200, 200);
}

Here’s the finished result!  As you can see, once you get started there are many more creative directions you can explore.  Happy coding!

Privacy Preference Center

Necessary

These cookies are used to record GDPR choices and to provide the minimum necessary functioning of the website for both logged-in and non-logged in users. The third party cookies from Google power the search engine on our site.

wpc_wpc, wp_api_sec, _wpndash, wordpress_logged_in, recognized_logins, G_ENABLED_IDPS, usprivacy, wordpress_sec, wp_api, tk_ai, gdpr[consent_types], wp-settings-time-20, wp-settings-20, gdprprivacy_bar, wordpress_test_cookie, gdpr[allowed_cookies], wp-settings.time-1, last_active_role,
ANID, 1P_JAR, CGIC,DV, SEARCH_SAMESITE

Analytics

Part of our website uses Google cookies to provide site analytics (how our website is used). This helps us to improve our website and create content suitable for all our visitors. You can learn more about how Google uses cookies, and how to manage them at
https://policies.google.com/technologies/types?hl=en-US

_gat_gtag_UA*, _ga, _gid, , CONSENT
__Secure-3PSIDCC, __Secure-3PSID, SIDCC, __Secure-3PAPISID, SSID, SAPISID, APISID, SID, NID, OTZ, COMPASS

Learning Content

We use Dropbox to deliver some of our paid for learning content. This places cookies on our website managed by Dropbox.

jar, locale, __Host-js_csrf, t, lid, last_active_role, bjar

Shopping Cart

These cookies are used to process the payment for paid-for content and to grant access to that content. Our website uses the WooCommerce platform to handle the shopping cart and the PayPal gateway to handle payment processing.

You can find out more about PayPal's cookies (which do not appear on our site) and Privacy Policy by visiting paypal.com/privacy

wp_woocommerce_session, woocommerce_items_in_cart, woocommerce_cart_hash, tk_ai, tk_us, mailchimp_user_mail, mailchimp.cart.current_email

Mailing List Subscriptions

We use a Wordpress plugin to manage our email subscription sign up. We use Mailchimp to handle and manage email to our subscribers, but we don't use their cookies on our site. For more information on MailChimp Cookies, visit https://mailchimp.com/legal/cookies/

et_bloom_optin_optin*, et_bloom_subscribed_to_optin

Marketing and Tracking

_fbp

The Audio Programmer Logo

Connect with the Audio Programmer community and find out about new tutorials, jobs, and events!

You have Successfully Subscribed!