[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:

2 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.

2 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