[Solved] Make Custom Slider - Rotary

Add a new Component for the Slider via Projucer:

image

To be able to change the Look and Feel create another Component for the LookAndFeel:

image

image

image

Copy your stitched Knob ( How to create a stitched Knob read below ) to your Source/Asset Folder and drag it to Projucer to make it a Binary Data:

Now launch Visual Studio to save your Changes:

image

// AulusSlider.h

#pragma once

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

//==============================================================================
/*
*/
class AulusSlider : public Slider
{
public:
    AulusSlider();
    ~AulusSlider();

    void mouseDown(const MouseEvent& event) override;
    void mouseUp(const MouseEvent& event) override;


private:
    AulusSliderLookAndFeel aulusSliderLookAndFeel;
    Point<int> mousePosition;
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AulusSlider)
};
// AulusSlider.cpp

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

//==============================================================================
AulusSlider::AulusSlider() : Slider()
{
    // In your constructor, you should add any child components, and
    // initialise any special settings that your component needs.
    setLookAndFeel(&aulusSliderLookAndFeel);
}

AulusSlider::~AulusSlider()
{
    setLookAndFeel(nullptr);
}

void AulusSlider::mouseDown(const MouseEvent& event)
{
    Slider::mouseDown(event);
    setMouseCursor(MouseCursor::NoCursor);
    mousePosition = Desktop::getMousePosition();
}

void AulusSlider::mouseUp(const MouseEvent& event)
{
    Slider::mouseUp(event);
    Desktop::setMousePosition(mousePosition);
    setMouseCursor(MouseCursor::NormalCursor);
}
// AulusSliderLookAndFeel.h

#pragma once

#include <JuceHeader.h>

//==============================================================================
/*
*/
class AulusSliderLookAndFeel : public LookAndFeel_V4
{
public:
    AulusSliderLookAndFeel();

    void drawRotarySlider(Graphics& g, int x, int y, int width, int height, float sliderPos, float rotaryStartAngle, float rotaryEndAngle, Slider& slider) override;
    void drawLabel(Graphics& g, Label& label);

private:
    Image knobRelease;
};
// AulusSliderLookAndFeel.cpp

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

//==============================================================================
AulusSliderLookAndFeel::AulusSliderLookAndFeel() : LookAndFeel_V4()
{
    // In your constructor, you should add any child components, and
    // initialise any special settings that your component needs.
    knobRelease = ImageCache::getFromMemory(BinaryData::knobRelease_png, BinaryData::knobRelease_pngSize);

}

void AulusSliderLookAndFeel::drawRotarySlider(Graphics& g, int x, int y, int width, int height, float sliderPos, float rotaryStartAngle, float rotaryEndAngle, Slider& slider)
{
    if (knobRelease.isValid())
    {
        const double rotation = (slider.getValue()
            - slider.getMinimum())
            / (slider.getMaximum()
                - slider.getMinimum());

        const int frames = 256;
        const int frameId = (int)ceil(rotation * ((double)frames - 1.0));
        const float radius = jmin(width / 1.0f, height / 1.0f);
        const float centerX = x + width * 0.5f;
        const float centerY = y + height * 0.5f;
        const float rx = centerX - radius - 1.0f;
        const float ry = centerY - radius;

        int imgWidth = knobRelease.getWidth();
        int imgHeight = knobRelease.getHeight() / frames;
        g.drawImage(knobRelease, 0, 0, imgWidth, imgHeight, 0, frameId * imgHeight, imgWidth, imgHeight);
    }
    else
    {
        static const float textPpercent = 0.35f;
        Rectangle<float> text_bounds(1.0f + width * (1.0f - textPpercent) / 2.0f, 0.5f * height, width * textPpercent, 0.5f * height);

        g.setColour(Colours::white);

        g.drawFittedText(String("No Image"), text_bounds.getSmallestIntegerContainer(), Justification::horizontallyCentred | Justification::centred, 1);
    }
}

void AulusSliderLookAndFeel::drawLabel(Graphics& g, Label& label)
{
    g.setColour(Colour(uint8(255), 255, 255, 1.0f));
    g.fillRoundedRectangle(label.getLocalBounds().toFloat(), 3.0f);


    if (!label.isBeingEdited())
    {
        auto alpha = label.isEnabled() ? 1.0f : 0.5f;
        const Font font(getLabelFont(label));

        g.setColour(Colour(uint8(0), 0, 0, 1.0f));
        g.setFont(font);

        auto textArea = getLabelBorderSize(label).subtractedFrom(label.getLocalBounds());

        g.drawFittedText(label.getText(), textArea, label.getJustificationType(),
            jmax(1, (int)(textArea.getHeight() / font.getHeight())),
            label.getMinimumHorizontalScale());

        g.setColour(Colour(uint8(255), 255, 255, 0.1f));
    }
    else if (label.isEnabled())
    {
        g.setColour(label.findColour(Label::outlineColourId));
    }

    //g.fillRoundedRectangle(label.getLocalBounds().toFloat(), 3.0f);
}

Add the new Slider to your Editor:

// PluginEditor.h

...
private:
    ...
    AulusSlider mReleaseSlider;
...
// PluginEditor.cpp
...
EightAudioProcessorEditor::EightAudioProcessorEditor (EightAudioProcessor& p) : AudioProcessorEditor (&p), processor (p), keyboardComponent(p.keyboardState, MidiKeyboardComponent::horizontalKeyboard)
{
    ...
    mReleaseSlider.onValueChange = [&]() { processor.changeSamplerRelease(); };
    addAndMakeVisible(mReleaseSlider);
    ...
}

...

void EightAudioProcessorEditor::resized()
{
    ...
    mReleaseSlider.setBounds(170, 255, 128, 150 + 20);
    mReleaseSlider.setSliderStyle(Slider::SliderStyle::RotaryVerticalDrag);
    mReleaseSlider.setTextBoxStyle(Slider::TextBoxBelow, false, 45, 20);
    mReleaseSlider.setTextValueSuffix(" s");
    mReleaseSlider.setNumDecimalPlacesToDisplay(0);
    mReleaseSlider.setTextBoxIsEditable(false);
    mReleaseSlider.setRange(0.001, 5.0, 0.01);
...

Create a stitched Knob

In the above Example we used a 128x150 Pixels by 256 Frames Y-Down stitched Image. It was rendered in Blender.

Blender created single Images per Frame:

To create one stitched Image out of those Images we used the following Python Script:

# stitchImages.py

# pip install Pillow

import sys
from PIL import Image

imagesArray = ['Rotating Knob Animation 256f 128x150/'+str('%04d'%image)+'.png' for image in range(1, 256+1)]
# Example: 'Rotating Knob Animation 256f 128x150/0001.png' to 'Rotating Knob Animation 256f 128x150/0256.png'

images = [Image.open(x) for x in imagesArray]
widths, heights = zip(*(i.size for i in images))

def stitchXRight():
    total_width = sum(widths)
    max_height = max(heights)
    new_im = Image.new('RGBA', (total_width, max_height))
    x_offset = 0
    for im in images:
        new_im.paste(im, (x_offset, 0))
        x_offset += im.size[0]
        print('Add Image "%s"..'%im.filename)
    return new_im

def stitchYDown():
    total_width = max(widths)
    max_height = sum(heights)
    new_im = Image.new('RGBA', (total_width, max_height))
    y_offset = 0
    for im in images:
        new_im.paste(im, (0, y_offset))
        y_offset += im.size[1]
        print('Add Image "%s"..'%im.filename)
    return new_im

stitchYDown().save('knobRelease.png')

Execute the Script:

image

That creates the knobRelease.png Image.

All together creates this Result:

8 Likes

AMAZING! 2 years later, your post helped me like no other. Thank you :raised_hands:

1 Like

Great… in Blender how did you do the white marks around the knob - using radial array ?

  • In Blender change the viewport to Top Orthographic by pressing 7
  • Press Shift + a and create a Mesh / Plane ( m, p )
  • Resize via s, x, ., 1, Enter
  • Rotate via r, 90, Enter
  • Move via g, y, -, 2, Enter
  • Apply all Transformations via Cmd + a, All Transformations ( a )
  • Press Shift + a and create an Empty / Plain Axes ( e, p )
  • Select the Plane and open Modifier Properties Tab
  • Click Add Modifier and select Array
  • Deactivate Relative Offset Checkbox
  • Activate Object Offset Checkbox
  • Open Object Offset Settings
  • Click the Object field and select Empty ( It’s the Plain Axes Empty we created before )
  • Set Count for the Array to 25
  • Select the Empty and rotate it via r, 7, ., 5, Enter

From now on you can change the Plane in Edit mode to your needs.

You then could duplicate the Plane ( Shift + d, Enter ) and also the Empty. Then you change the Array Count of the duplicated Plane Array Modifier to 5 and then you rotate ( 45 ) the duplicated Empty. And finally in Edit Mode ( Tab ) for the duplicated Plane you change it’s size.

3 Likes

thanks, appreciate the detail :+1:.
I finally got a decent GPU (3070) last week, and got HardOps for Blender.
I am thinking of designing my own knobs/sliders.

1 Like

Thank you very much for this guide!
I have one question: can this also be done with SVG, or any other vector image format?

Yes Michele, within AulusSliderLookAndFeel::drawRotarySlider(..) you can use this approach at SVG file as Image in drawImage of graphics

1 Like

What an amazing tutorial, guideline this is! Brilliant! Thx a lot!

1 Like

I’m not sure if it’s supposed to be the final product, but if it is and you’d like to optimize it, you could just have a render without the slider tick, and have the slider tick alone rotate around the center. And if the slider tick has to be shaded (which it doesn’t look like so far), you could have the knob rendered as white, and use the rotating slider tick to clip the white knob’s drawing.

Nice idea but that wouldn’t work if the whole knob wouldn’t be that simple. This is just an example on how to do it. In the real product the knob has variations on the surface which produces various shadows and then your approach will not work.

Yeah I figured :stuck_out_tongue:

I made this for my use. If it can be useful for others. :wink: . It 's an png image files to filmstrip converter . The code and executables for Apple / Windows / Linux are here:

1 Like

That is amazing Julien! Thank you for sharing this with us!

My pleasure! Glad it could be useful!

Your Dub Delay (GitHub - JuqaSolutions/Dub_Delay_Lite_Installers: Free audio delay plugin) looks promising! Any release date available ?

THANK YOU! I’ve updated the page with a link to download the versions. I’m a little behind my planned release date for the next stuff. I don’t have enough time to work on this at the moment.