Custom Horizontal Slider

I want to be able to make a custom horizontal slider using an image film strip like the rotary image. I’m not able to find any examples of it online. Has anyone implemented this before?

I implemented an “EndlessSlider” that seems like it might be similar to what you are after - this basically takes an image and transforms it across the Component view according to slider events from the user. In the PolarDesigner plugin, we use this as a ‘trim’ to adjust all bands equally, but you could of course use it for whatever your needs are. Mine is vertical, but you can adjust for horizontal easily enough …

//  EndlessSlider.h
//  PolarDesigner - All
//  Created by Jay Vaughan on 23.02.22.
//  Copyright © 2022 Austrian Audio. All rights reserved.
// This Component implements an 'endless slider', which can be useful for implementing
// 'trim' controls, i.e. for applying trim to a set of EQ's, volume sliders, etc.
// To adjust the rate of trim, use the EndlessSlider.step value in the slider inc/dec
// callbacks.

#ifndef EndlessSlider_h
#define EndlessSlider_h

class EndlessSlider : public Slider {
    EndlessSlider () :
        sliderImage = getImageFromAssets("scrollImage.png");
        setTextBoxStyle (Slider::NoTextBox, false, 0, 0);
        setSliderStyle (Slider::LinearBarVertical);
    // Trim step value - modify it freely as needed
    double step = 0.012725f;
    // set these callbacks where you use this class in order to get inc/dec messages
    std::function<void()> sliderIncremented;
    std::function<void()> sliderDecremented;
    // calculate whether to callback to an increment or decrement, and update UI
    void mouseDrag(const MouseEvent &e) override
        int currentMoved;
        static int lastMoved;

        if (e.mouseWasDraggedSinceMouseDown()) {
            currentMoved = e.getDistanceFromDragStartY();
            sliderImageTransform = (AffineTransform::translation ((float) (sliderImage.getWidth()),
                                                                 (float) (sliderImage.getHeight()) + currentMoved)
                                      .followedBy (getTransform()));
            if ((currentMoved > lastMoved)){
            } else
            if (currentMoved < lastMoved) {

            lastMoved = currentMoved;

    void paint (Graphics&g) override
        Rectangle<int> bounds = getLocalBounds();
        Path endlessPath;

        g.setFillType(juce::FillType(sliderImage, sliderImageTransform));

    void mouseExit (const MouseEvent& e) override
    ~EndlessSlider () {}
    void resized() override
        auto& lf = getLookAndFeel();
        auto layout = lf.getSliderLayout (*this);
        sliderRect = layout.sliderBounds;

    Rectangle<int> sliderRect;
    Image sliderImage;
    AffineTransform sliderImageTransform;
    // utility functions - from DemoRunner utilities
    static juce::Colour getRandomColour()
        auto& random = juce::Random::getSystemRandom();
        return juce::Colour ((juce::uint8) random.nextInt (256),
                             (juce::uint8) random.nextInt (256),
                             (juce::uint8) random.nextInt (256));

    // creats a usable image asset from a file stream
    inline std::unique_ptr<InputStream> createAssetInputStream (const char* resourcePath)
        ZipFile apkZip (File::getSpecialLocation (File::invokedExecutableFile));
        return std::unique_ptr<InputStream> (apkZip.createStreamForEntry (apkZip.getIndexOfFileName ("assets/" + String (resourcePath))));
 auto assetsDir = File::getSpecialLocation (File::currentExecutableFile)
                       .getParentDirectory().getChildFile ("Assets");
#elif JUCE_MAC
 auto assetsDir = File::getSpecialLocation (File::currentExecutableFile)
                       .getParentDirectory().getParentDirectory().getChildFile ("Resources").getChildFile ("Assets");
        auto resourceFile = assetsDir.getChildFile (resourcePath);
        jassert (resourceFile.existsAsFile());
        return resourceFile.createInputStream();
    // creates an image asset from cache if possible
    inline Image getImageFromAssets (const char* assetName)
        auto hashCode = (String (assetName) + "@endless_slider_assets").hashCode64();
        auto img = ImageCache::getFromHashCode (hashCode);
        if (img.isNull())
            std::unique_ptr<InputStream> juceIconStream (createAssetInputStream (assetName));
            if (juceIconStream == nullptr)
                return {};
            img = ImageFileFormat::loadFrom (*juceIconStream);
            ImageCache::addImageToCache (img, hashCode);
        return img;

#endif /* EndlessSlider_h */

This is open source, btw … see our Plugin repositories here: Austrian Audio · GitHub

Thanks austrianaudioJV.

At the moment I have something I am developing but it doesn’t seem to stretch the image correctly to fit the container. Here is my code.

void knobTesting::drawLinearSlider(juce::Graphics &g, int x, int y, int width, int height,
                                      float sliderPos,  float minSliderPos,  float maxSliderPos, const juce::Slider::SliderStyle,
                                      juce::Slider &s)

    if (img1.isValid())
        //std::cout << "Positions: " + x;
        const int imgWidth = img1.getWidth();
        const int imgHeight = img1.getHeight();
        const int frames = imgHeight/ 201;
        const double sliderValuePos = (s.getValue() - s.getMinimum()) / (s.getMaximum() - s.getMinimum());
        const int frameId = (int)ceil(sliderValuePos * ((double)frames - 1.0));
        const double frameHeight = imgHeight/201;
        const float centerX = x + width * 0.5f;
        const float centerY = y + height * 0.5f;
        const float rx = centerX - (width/2) - 1.0f;
        const float ry = centerY - width;
        static const float textPpercent = 0.35f;
        juce::Rectangle<float> text_bounds(1.0f + width * (1.0f - textPpercent) / 2.0f,
            0.5f * height, width * textPpercent, 0.5f * height);


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

I would advise not using the manual methods to resize the image for your needs, but rather apply a Transform to it. This is a lot more performant and also more likely to give you the results you need. Remember you can do all sorts of operations in the Transform … see my code for an example, which is useful inasmuch as it wraps the image (the source image is only 6 pixels wide) across the Transform being applied. You can also do your stretching that way too …