Drawing a cursor synchronized with a Metronome


#1

I’m new to Juce. I want to draw a cursor synchronized with a Metronome. I tried to create the cursor as a child from AnimatedAppComponent, at the moment the cursor is a triangle.
The Metronome was done with libpd. The whole score coordinates are scored in a std::vector. I’m actualizing the coordinates inside the PdObject class. Always when I receive a bang I’m reading the next coordinates and I store it inside a struct. This struct is a std::shared_ptr , the Cursor class receive this shared_ptr and set the cursor coordinates. My problem here is that after the first time, the cursor doesn’t moves, the coordinates are actualized but it doesn’t have any effect in the cursor’s position, I’m calling addTriangle within the Cursor’s update method. I don’t know exactly how I have to refresh the draw, I called repaint() inside the update method too, but again it doesn’t have any effect.
The Cursor class is the following:

Cursor : public AnimatedAppComponent {
 public:
     Cursor(std::shared_ptr<coord>  _coords) {
         coords = _coords;
         setSize (600, 300);
         setFramesPerSecond (60);
        toFront(true);        
     }
     void update() override {        
         tri.addTriangle(coords->x3, coords->y3, coords->x1, coords->y1, coords->x2, coords->y2);
         //if(fGraphics)
             //fGraphics->fillPath(tri);
         repaint();
     }
     void paint (Graphics& g) override {
         fGraphics = &g;
         fGraphics->setColour(Colours::red);
         tri.addTriangle(coords->x3, coords->y3, coords->x1, coords->y1, coords->x2, coords->y2);
         fGraphics->fillPath(tri);
     }
     void resized() override {
         //this->setBounds(0, 0, getWidth(), getHeight());
     }
 private:
     Path tri;
     std::shared_ptr<coord> coords;
     juce::Graphics * fGraphics;
     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Cursor)
 };

The coordinates are right, Libpd is working fine without problems. I think the update() method is wrong, but I don’t know exactly how should I have to use it. Maybe is possible to do the same in another way. I’d be glad to become some help.
Regards.


#2

I haven’t read the whole code, but this statement has at best no effect, at worst it could lead to an infinite loop, since setting it’s own bounds will call the resized() callback.

Also please surround your code with three tickmarks: ```
That way your code is properly formatted…


#3

Thanks for your answer. I commented this line of code out, but this is not the actual Problem. I surrounded the code now with 3 tickmarks.


#4

Thank you, now it is much better to read.

You are adding triangles to the Path on several occasions. Do it only either in the paint, in which case a local path variable would be better. If you want to keep it a member, you have to call clear() before you add a new triangle:

void update() override {
    tri.clear ();
    tri.addTriangle(coords->x3, coords->y3, coords->x1, coords->y1, coords->x2, coords->y2);
    repaint();
}

Next thing to change: don’t keep the graphics pointer, it might not be valid to use it outside your paint method.
Remove the fGraphics variable completely.
Instead write it like this:

void paint (Graphics& g) override {
    g.setColour (Colours::red);
    g.fillPath (tri);
}

If that still doesn’t work, start printing the coords in update:

DBG ("New coords x3: " << coords->x3 << " y3: " << coords->y3
            << " x1: " << coords->x1 << " y1: " << coords->y1
            << " x2: " << coords->x2 << " y2: " << coords->y2);

Look in your output, if the numbers are actually changing…

Good luck


#5

Many thanks for your answer. I follow your instructions, the problem is that repaint() doesn’t have any effect, I think Juce should call the method paint again, but this doesn’t happen, therefore the object is not drawn. Is there a way to force the paint methode to be called, why repaint() doesn’t works?.
If I use the code I posted first, the “cursor” appears but its position is not actualized.
The coords are not the problem, I printed it out with std::cout , they are working perfectly. This piece of code is actually very easy, this should be not difficult to do.
Regards


#6

The OS may not update the display as often as you expect… I find on Windows it’s not updated as often as on MacOS. The redraw priority is lower.

Rail


#7

Yes, I suppose that. Is there a way to force the object to be redrawn each time at the new coordinates?


#8

Yes. Call repaint().

If that was true then literally no juce apps with GUIs would ever work at all!

Daniel’s advice is very good and we’ve got countless thousands of lines of example code that do every conceivable type of drawing - you should have a look through and get a feel for how it’s done. Like you say, it’s not difficult, but you seem pretty new to C++ so it’s probably just something simple that you haven’t quite grasped yet.


#9

Dear Jules,
Daniel advice is not working, repaint() is not drawing the object again in the new coordinates. This is not a C++ problem, and I’m not new to C++, I’m new to Juce. I made the same as in this example with GLFW (therefore OpenGL), RtAudio and libpd, there is working perfect, and I was using in another applications Portaudio/Portmidi before too. In those libraries you can start, stop open a stream or use a callback for yourself. Juce is implementing this things in a different way, that is not self-explanatory. Again, this is not a problem of the language (C++), is a library’s problem. I don’t want to create an OpenGL context only to draw a cursor, it looks a bit ridiculous.and yes, I looked at the examples before I wrote this post here.
I’m not saying the library where bad, only is not self explanatory, particularly if you were working with another C/C++ libraries before.
And sorry, but in this case, repaint() is not doing its work. The code is easy because I’m new to this library, I’m trying to implement the easy things before in order to understand how this library works.
Regards


#10

Ok back to the topic,
if you want something to be drawn in JUCE, you will have to accommodate yourself with the fact, that you cannot call a synchronous paint. Instead by using Component::repaint() you are scheduling an area (default the whole component) to be drawn as soon as possible. It is the OS, that will decide when this will be. It might be less often than you are calling repaint() - because you are calling repaint more often than the resources allow, or it might be more often, because another window is dragged over the component, it is being resized or whatever reason there might be.

The OS is calling then your Component::paint(Graphics& g) override; callback, where you define your drawing behaviour.

If you can’t see what you are drawing, check the state of the drawing:

  • are you drawing inside the component? Use Component::getLocalBounds() to find the area of valid coordinates
  • Did you set a colour different from the background? use g.setColour (Colours::green) or whatever you want to set
  • is Your component actually a child inside a window? Did you use addAndMakeVisible for the component, or setContent () if it is e.g. inside a DocumentWindow?
  • does it have a size? check the resized() callback of the parent. Also check, if another component is obstructing it

There are several things, that might have gone wrong…

Good luck


#11

Are the coordinates you have based on where the cursor should be? if so I think that’s your problem. The coordinates used inside of paint() should be relative to the component. To change the position of the cursor (i.e. your component) you would need to set the components position using setTopLeftPosition() or setCentrePosition() maybe? keep in mind that the position handed to those methods is relative to the parent component of Cursor.


#12

@daniel: I can see the “cursor/triangle” it appears inside the screen, I changed the initial coordinates manually , and it appears in another place. The problem is, when the coordinates are changed inside the Metronome, the cursor doesn’t change its position. The coordinates are always correct, i placed a std::cout inside the update() method. The Cursor is instantiated as a std::unique_ptr inside another class, I’m loading musical scores there. The cursor only have to appear on the screen at the moment when the metronome is started, when the metronome stops it is destroyed (disappears). As i say before, the cursor only appears at a fixed position, its position is not actualized.
@Anthony_Nicholls thanks for your answer. Yes, the coordinates are based on where the cursor should be. Where should I place setCentrePosition(), inside update()? The Cursor class is instantiated as a unique_ptr inside another class, is not a child from another component, it should appears on the screen when I start the metronome, and disappears when I stop it.
Regards.


#13

I’m glad you are making progress. Did you follow the suggestion, to remove the path from the members?
When you call addTriangle, you are adding a new primitive to the path. So after a few calls, the path contains a lot of triangles.
Since your coords struct is available in the paint as well, I would probably just create it directly in paint. You could also add a bool to your coords struct to mark, if it is playing or not:

void paint (Graphics& g) override {
    if (coords->isPlaying) {
        Path tri;
        tri.addTriangle (coords->x3, coords->y3, coords->x1, coords->y1, coords->x2, coords->y2);
        g.setColour (Colours::red);
        g.fillPath (tri);
    }
}

Now make sure, that repaint() is called regularly (e.g. from update() or using a Timer).

HTH


#14

Just for the record, the pattern of asynchronously invalidating parts of your UI and then redrawing in a callback is how every windowing system works!

Win32, OSX, iOS, Qt, Android, Java, X11… you name it, they ALL work like this. The function names may vary a bit, but they’re all basically the same as JUCE.

Sure, if all you’ve ever done is to use GL in a loop that blasts pixels to the screen, this might be unfamiliar, but it’s very normal in GUI programming.


#15

The Cursor … is not a child from another component
This sounds dubious to me. If it’s not a child of another component, in relation to what do you set it’s coordinates?!

The usual way to create components is something along this line

auto cursor = new Cursor();
parentComponent->addAndMakeVisible(cursor);

and then if you do cursor->setTopLeftPosition(10, 20) it will position the cursor´s topLeft corner the specified amount from parentComponent´s topLeft corner.


#16

It’s hard to grasp exactly how you’re trying to do what you want to do at the moment.

One way that you could achieve it is something like this…

DiscoCursor.h

#pragma once

#include "JuceHeader.h"

class DiscoCursor : public AnimatedAppComponent
{
public:
    DiscoCursor()
    {
        // this component is partially transparent
        setOpaque (false);
        // this component should ignore mouse events
        setInterceptsMouseClicks (false, false);
        setFramesPerSecond (6);
        setSize (20, 20);
    }
    
private:
    void update() override
    {
        colour = Colour (rand.nextInt()).withAlpha (1.f);
        // call repaint so the cursor repaints using the newly updated colour
        repaint();
    }
    
    void paint (Graphics& graphics) override
    {
        juce::Rectangle<float> bounds (getLocalBounds().toFloat());
        Line<float> line (bounds.getBottomRight(), bounds.getTopLeft());
        float arrowheadSize = line.getLength() / 2.f;
        
        Path path;
        path.addArrow (line, 2.f, arrowheadSize, arrowheadSize);
        
        graphics.setColour (colour);
        graphics.fillPath (path);
    }
    
    Random rand;
    Colour colour;
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DiscoCursor)
};

MainComponent.h

#pragma once

#include "JuceHeader.h"
#include "DiscoCursor.h"

class MainComponent : public Component
{
public:
    //==============================================================================
    MainComponent()
    {
        setMouseCursor (MouseCursor (MouseCursor::StandardCursorType::NoCursor));
        addAndMakeVisible (discoCursor);
        setSize (600, 400);
    }

    //==============================================================================
    void paint (Graphics& graphics) override
    {
        graphics.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
    }
    
    void mouseMove (const MouseEvent& event) override
    {
        discoCursor.setTopLeftPosition (event.getPosition());
    }

private:
    //==============================================================================
    DiscoCursor discoCursor;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

So the above is drawing an arrow in an ever changing colour at 6 frames per second. The position of the arrow is being set by the main component whenever it receives a mouseMove event.

To be honest though if you’re actually implementing a mouse cursor there is a MouseCursor class that you might want to check out.

You could do something like this…

class MainComponent : public AnimatedAppComponent
{
public:
    //==============================================================================
    MainComponent()
    {
        setFramesPerSecond (6);
        setSize (600, 400);
    }

    //==============================================================================
    void paint (Graphics& graphics) override
    {
        graphics.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
    }
    
    void update() override
    {
        Image image (Image::PixelFormat::RGB, 20, 20, true);
        Graphics graphics (image);
        
        juce::Rectangle<float> bounds (graphics.getClipBounds().toFloat());
        Line<float> line (bounds.getBottomRight(), bounds.getTopLeft());
        float arrowheadSize = line.getLength() / 2.f;
        
        Path path;
        path.addArrow (line, 2.f, arrowheadSize, arrowheadSize);
        
        graphics.setColour (Colour (rand.nextInt()).withAlpha (1.f));
        graphics.fillPath (path);
        
        setMouseCursor (MouseCursor (image, 0.f, 0.f));
    }

private:
    //==============================================================================
    Random rand;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

This is essentially the same as the first example except now there is no need to set the cursor position.


#17

@daniel Sorry for my delayed answer. Many thanks for your advice, I inherited another class from Timer, where I’m instantiating a unique_ptr to cursor, the class Cursor is inherited from Component:

void GuidoComponent::initializeCursor() {
    gDevice->shouldDrawCursorLine(true);
    computeCoordinates(LineCoordinates, pn);
    manager = std::make_unique<PdManager>(coords, LineCoordinates);
    cursor_c = std::make_unique<CursorComponent>(coords);
    coordTemp = coords->x1;
    manager->startMetronome();
    startTimer(1);
    repaint();
}

void GuidoComponent::timerCallback() {
    if(coords->isPlaying && coordTemp != coords->x1 && manager) {
        coordTemp = coords->x1;        
        repaint();
    }
    else if(!coords->isPlaying) {
        stopTimer();
        manager->stopMetronome();        
        cursor_c.reset();
        gDevice->shouldDrawCursorLine(false);
        LineCoordinates.clear();        
        manager.reset();
        repaint();
    }    
}

The only problem is that I have to repaint the whole score, what I want was to repaint only the cursor, in order to sparse resources, but I think that’s no possible. I don’t know exactly how accurate is the Timer class, I didn’t test it yet with high beats per minute velocities.
@oxxyyd it’s not mandatory tu use the keyword auto or new, it’s also possible to use std::make_unique or if you want to share the pointer’s ownership std::make_shared. I made a test with the Class Cursor inherited from the class GuidoComponent, the CPU performance was not better.
@jules I’m using OpenGL because the performance is better, specially on mobile Devices. This is not the theme from this thread, but now the Kronos Group announced Vulkan support for OSX and iOS with MoltenVK, https://www.khronos.org/news/press/vulkan-applications-enabled-on-apple-platforms and MoltenVK is now free
https://github.com/KhronosGroup/MoltenVK
Are you planning to add Vulkan support for Juce?
Regards.


#18

In the case of a cursor then whatever it is over (or was previously over) will need to be repainted because that part of the screen has just become invalidated.

If you split the rest of your gui into enough separated components then you can hopefully make sure there isn’t much for it to repaint. If the thing you need to repaint is expensive (more expensive than drawing an image), then you can set components to draw into a cached image.

With good management of the gui it is possible to get some significant speed ups in redrawing.


#19

My comment was not about coding standards but merely if you had forgot to make your cursor component the child of another (by calling addAndMakeVisible) or if it was left as a motherless child floating around somewhere and thus ignorant of which coordinate space to relate to…


#20

@Anthony_Nicholls Do you mean using the class CachedComponentImage?
Only repainting the Score is expensive, the Cursor itself is cheaper