Draw Space

usually i ask silly questions but this time i give you something back: a class for writing neat images of spaces :3 check it out:

the code: https://pastebin.com/xdfSArDK


That looks like a nice learning exercise with pleasant visual results, especially for someone who is just starting out. I didn’t watch the whole video, but I think I got the basic idea.

Looking at your code, while it is nicely structured and readable (something I wouldn’t say that to a lot of C++ newcomer code examples I came across), it seems that you found some solutions yourself that JUCE already has a ready-to-use solution for. In some cases, these are just easier to read, in some cases they might also be safer under certain edge case conditions. Furthermore there are some things you do that you probably wouldn’t do if you knew some cool C++ features and common design patterns. If you are interested in a more in-depth code review, I would go into detail…?

1 Like

sure as hell i’m interested in that :slight_smile: i always wanna get better.

btw i’m open for criticism on every level. no matter if it’s about coding style or even things that you feel like are missing out in the stylistic context of drawing space or whatever else you might think of

the thing that bugs me personally the most about this code is that there is no behaviour implement about how it should redraw things when it clips the screen btw. at some point in the video there is section about how it glitches out in funny looking rectangles then. but i also feel like this is one of those bugs where you’d easily say it’s a “nice glitch art feature” :smiley:

edit: however… the only thing i could think of that could save the image from getting redrawn everytime some part of the screen changes on it, would be if juce had some kinda “canvas”-style function that just saves the whole image into an array for further processing and printing. that would be pretty cool

edit 2:
i inserted the class into my current project. oh btw, for those who read that my laptop died in the other post. i could save my hard drive. yayy that’s why i can access my awesome first plugin ever again. :slight_smile: hurray however, here comes the image:

i think it’s pretty beautiful now that the background has something more complex going on. now became apparent that the clipping-thing really became a problem though.

it rerenders sections constantly when a knob is turned. that’s kinda… too glitchy now^^. i figured out that i’d probably need the image class to store the information at the start of the gui so it can’t be overwriten like that all the time, so i experimented a bit with Image to get into it:

ok. now i can just write whole picutres like that. really interesting! i’ll use this for something in the future. i still don’t understand how to actually draw a complex image onto an image-object yet. can someone give me a nice hint there?
here’s another awesome image of my gui, but with different space-settings :D:

i’m not surprised the juce logo showed up in the echo. ofc that had to happen since there is a splash screen. but it seems the snapshot-maker also makes a snapshot before even drawing the space. it seems to have a hierarchy in the background where it draws the knobs first. i guess this is one of the few moments where i’d really have to create another component object to get it right… tbh too lazy now but i try that later. a neat feature for the snapshot-method might be something like bool copyHierarchyFromCode in the constructor or so. if it’s false it does the normal hierarchy. if it’s true it takes the image from what’s first being drawn according to what comes first in the code.

Here is a refactored version which simplifies some of your functions a lot. Note that except for small changes I didn’t change anything on how it works in general. A major thing I would change is to separate drawing, size calculation and random value generation, which would allow you to re-draw the whole thing at a different size without changing the look. And if you went that far, I’d suggest you to let your class inherit Component, do all the painting into the paint method and all position calculations in the resized method. Then just add it as first child component to your plugins UI and draw all other stuff above it. This would furthermore remove the need to remember to re-apply your bounds every time your UI changes.

By the way, I like the look of your plugin UI :wink:

// General notes: 
// - I moved the curly braces according to the JUCE coding style, which I personally prefer. However this is just
//   style, nothing wrong with your version.
// - I changed all variables that don't need a type specifier to auto. This makes the code more readable in case
//   of long type names and allows the compiler to chose the right type.
class HDLSpace

    // You can have a constructor that allows you to directly specify the initial values instead of constructing an
    // object with some fixed parameters and changing them afterwards with the setter methods provided. However,
    // you can specify default values that will be set if you simply dont set them. So you can still construct
    // your object without passing arguments to it.
    HDLSpace (int   initialColourVariation = 255,
              float initialStarSize = 6.0f,
              int   initialNumPlanets = 1024,
              float initialPlanetSize = 3.0f)
     : colourVariation (initialColourVariation),
       starSize        (initialStarSize),
       planetSize      (initialPlanetSize),
       numPlanets      (initialNumPlanets)
        // Nothing to do here anymore. It's good practice to initialize member variables through the initializer list
        // as done above if possible. This way you can also set member variables declared const that should not be
        // changed after construction, as assigning them via = is not allowed. However, for more complex setup routines
        // using the constructors function body is totally fine and not wrong

    // Styling: If a member function is a one-liner a lot of people – me included – prefer this style.
    ~HDLSpace() {}

    // Changed bounds member to be a float type, therefore the conversion
    void setBounds(Rectangle<int> b) { bounds = b.toFloat(); }
    void setColourVariation(int v)   { colourVariation = v; }
    void setPlanets(int p)           { numPlanets = p; }
    void setPlanetSize(float size)   { planetSize = size; }
    void setStarSize(float size)     { starSize = size; }

    void drawFog(Graphics& graphics)
        // Made random a member, moved fogSize, fogX and fogY declaration into loop. All variables should be declared
        // in the smallest scope possible. Exception are objects that are heavy to create, but a simple float does
        // not create any more overhead if declared inside the loop body

        // Why calculating the centre of your bounds if rectangle already can do it for you?
        auto centre = bounds.getCentre();
        // there is no point in calculating this over and over again in the loop as it won't change
        auto shortestBoundsSide = jmin(bounds.getWidth(), bounds.getHeight());
        for (unsigned int i = 0; i < 5; ++i)
            auto fogSize = random.nextFloat() * shortestBoundsSide;
            auto fogPos = Point<float> (random.nextFloat() * bounds.getWidth(), random.nextFloat() * bounds.getHeight());

            if (fogPos.getX() > centre.getX () || fogPos.getY() > centre.getY())
                fogSize *= -1;

            // If fogPos already is a point, we can use the other constructor that accepts points. Create a temporary
            // Point through the curly braces operator for point2
            // Furthermore, magic numbers like hex colours in code are considered bad. Better find a meaningful name
            // for the colour or use a library-declared colour if existing
            ColourGradient fogGradient(fogGrey, fogPos, Colours::transparentBlack, {fogSize, fogSize}, true);

    // Renamed the g arg to graphics to make it consistent to the other functions
    void drawSpace(Graphics& graphics, ColourGradient& gradient)
        gradient.point1 = bounds.getTopLeft();
        // I might get your intention wrong, but is this what you really wanted? Just look at the original version,
        // where you were re-setting point 1 in this line
        gradient.point2 = bounds.getBottomRight();


        // Why that? I wouldn't expect a draw function to clear my gradient. Better pass in a const reference
        // and clear it outside


    // Why adding a 2 to the function name? Function overloading is a strong C++ feature that allows you to have multiple
    // versions of the same function with different function signatures
    void drawSpace(Graphics& graphics)
        // You don't need to type that Range<int> – just use {} and pass the arguments to construct a range
        // This is only needed in few cases where the compiler can't figure out what kind of object you want
        // construct in place, mostly because of function overloading
        int variation = random.nextInt({-colourVariation, colourVariation});

        auto r = rgbLimiter(40 + variation);
        auto g = rgbLimiter(0 + variation);
        auto b = rgbLimiter(20 + variation);
        Colour purple = Colour(r, g, b);

        r = rgbLimiter(0 + variation);
        g = rgbLimiter(20 + variation);
        b = rgbLimiter(30 + variation);
        Colour cyan = Colour(r, g, b);

        // A lot more compact and readable ;)
        auto gradientA = bounds.getBottomRight() * random.nextFloat();
        auto gradientB = bounds.getBottomRight() * random.nextFloat();

        ColourGradient gradient(purple, gradientA, cyan, gradientB, false);



    // Making arguments that are not changed inside and which are bigger than an int a const reference is more efficient
    void drawStar(Graphics& graphics, const Point<float>& centre, float size, Colour colour)
        size *= planetSize * starSize;
        Path path;
        const auto angle = MathConstants<float>::pi;

        // Added f to last value, as it was declared as double and caused a warning with my compiler.
        path.addStar(centre, 4, 8.0f * size, 0.025f * size, angle * 0.333f);
        path.addStar(centre, 4, 4.0f * size, 0.0125f * size, angle * 0.666f);

    void drawPlanets(Graphics& graphics)
        for (unsigned int i = 0; i < numPlanets; ++i)
            // unfortunately there is no nextFloat function that accepts a range as argument
            const auto x = bounds.getX() + random.nextFloat() * bounds.getWidth();
            const auto y = bounds.getY() + random.nextFloat() * bounds.getHeight();

            const auto distance = random.nextFloat();
            // pow is a C function. C has no overloading, so pow is for double arguments, while there is powf for float
            // arguments. But we have C++! And C++ defined overloading for std::pow, so the compiler chooses the overload 
            // type which is appropriate to whichever argument type you pass in
            const auto size = std::pow(distance, 5) * planetSize + 1.0f;

            const auto planetBounds = Rectangle<float> (x, y, size, size);

            auto yellowness = std::pow(distance, 35);
            auto planetColour = Colours::white.withAlpha(distance).interpolatedWith(Colours::yellow, yellowness);


            drawStar(graphics, planetBounds.getCentre(), yellowness, planetColour);
    // jlimit gives you this funcionality in one line. Cast to uint8 is safe as it won't exceed
    // the value range due to the limitation and won't trigger a compiler warning when assigning
    // the result to a Colour constructor which expects uint8 arguments
    uint8 rgbLimiter (int unlimited) { return static_cast<uint8> (jlimit (0, 255, unlimited)); }

    // Whatever this function does... it's obviously not used above, so better delete it...
    float mix(float a, float b, float p) { return a * (1.f - p) + b * p; }

    const Colour fogGrey {0xffaaaaaa};

    // Made random a member. As creating a Random instance creates some overhead, even if not that big, I'd prefer to
    // construct it once and keep the instance as a class member instead of recreating it with every function call
    Random random;

    int colourVariation;
    float starSize;
    float planetSize;
    // In 99% of the cases there is no point in using unsigned int instead of int, even if your number wont ever be
    // negative. Declaring it int saves typing and saves you from casting it to an int for a lot of functions that
    // expect ints. Have you noticed that there is nearly no JUCE class that wants an unsigned int as argument?
    int numPlanets;

    // You cast the bound values to float in nearly every place. Why not declaring it as float right away?
    Rectangle<float> bounds;
1 Like

ok, first of all thanks for all these tipps. now i’d like to discuss some aspects:

  1. the auto-keyword. i feel like it makes code very unreadable actually, because you never really know what it is unless you are familiar with the return-values of given functions. but when float, int or rectangle is in the code you know what it is, so i like that definitely more.

  2. good idea about the constructor. i totally forgot that it would make sense to use it in this case. often when i come up with random ideas like this i just make a default constructor so i can start adding stuff finally, but yeah, your change really makes the constructor perfect.

  3. yes, the destructor totally looks better when it’s just one line.

  4. is there a reason why you changed bounds to float?

  5. making random a member totally makes sense. it’s not that all randomizers need to have their own names. but this was my first project with randomizers in general, so i didn’t have this workflow yet. however… i ask myself if it’s even possible to use randomizers without making their objects, just by accessing the Random-class.

  6. yeah i always asked myself how to determine how big an object has to be to not make sense in an inner loop anymore. i believe it must be decided in relation to the “speed” of the loop. for example a framerate-loop could get away with bigger objects than a sampleRate-loop, am i right? but i still don’t know how to find out where that limit is.

  7. nice centre-function. totally forgot it exists.

  8. i don’t like the Colours-class with the library-declared colours. it kinda feels like pseudo code to me to write things like “red” or “darkseablue” to draw a colour. i wanna write my own colour-shifting functions in the future so i try to rather get familiar with the hex-values more. i also like that it’s possible to write Colour(r,g,b) to draw a colour. that seems the most intuitive to me from all things i tried yet, but i only found out about that yesterday so you didn’t find it in this class yet.

  9. yes, the resetting of point 1 in my drawSpace-method was a mistake.

  10. the reason why i cleared the colours is because i feel like when someone is done drawing some background space they won’t need their gradient anymore, because it just finished its purpose. maybe it would be cleaner to just ~gradient() it.

  11. drawSpace and drawSpace2 is not the same function. technically i could have used function overloading here because the constructors work differently but i decided to give them their own names because they do different stuff. drawSpace 1 is very simple function that works with set colours while drawSpace2 is the advanced version that creates random values.

  12. oh, cool to know that Range is actually not important. this will make some lines neater.

  13. btw you didn’t notice that the lines where it adds the variation to rgb, make no sense. i only noticed later that it’s not really a variation when r,g and b get the same variation-amount. they all need their own randomized value.

  14. why can you just multiply a point with a single number? i mean, ok i get it. that kinda makes sense. it’s like vector-multiplication. however, i didn’t think of this possibility before.

  15. yeah i didn’t even think of the fact that centre doesn’t change. good point.

  16. oh i really forgot some .f :o

  17. i’m not sure if i understood your argument about std::pow(x,y). but i guess it often makes sense to use pure c++ functions instead of juce functions whenever that’s possible and safe to do.

  18. oh nice. that rgbLimiter-improvement is neat! i still feel a bit unsure about heavily limited types of integers but you gave me some ideas about them.

  19. the mix-function is used to mix 2 parameters a and b with each other depending on how much p is the case (p = 0 to 1). yes, i didn’t use it anymore. i probably used it on something and then deleted that thing.

  20. yeah i noticed there are almost no functions in juce that use unsigned int, but i don’t understand that. i mean, if you think of things that are clearly NEVER EVER negative, like the amount of channels… or the amount of samples in a buffer… why would anyone want to waste ram for an integer where half the possible values will never be used? but ok i guess. at least everything works fine.

  21. yeah it totally makes sense to make the bounds float. i heard casting should be avoided.

  22. even more interesting than your code comments is what you have written in your actual comment in here because you gave me tipps on how to make it work in a way that would keep it from repainting whenever a knob is turned. i’m heavily looking forward to trying this and also your improved code myself. but now i go to sleep :slight_smile: however thanks for your code review. i think i really learned some new stuff.

There are very good reasons for auto, see Herb Sutter Almost always auto.

Avoiding unintentional implicit casts and easier refactorability are the key benefits IMHO.

Once you do calculations on them, it can easily happen to run in negative numbers. By using unsigned numbers, you get huge errors. While most of the time you don’t use the whole range anyway, so better keep the sign for safety.

You can even write

~HDLSpace() = default;

Or just don’t write one at all, why overriding it, if you have nothing to add…
The only case, when an empty destructor is needed, if you have virtual functions, but no virtual base class. (There is a warning that suggests this: “class has virtual functions but non-virtual destructor”).

1 Like

I get your point and I thought so too at first but I more and more start adopting the auto style. Why? Because it helps me to write better code. If my code needs the type to be understandable and it’s not obvious from the variable name, function name or context then I better change that. Think about it :wink: However, there is nothing technically wrong with specifying the type.

You answered the question yourself in aspect 21 :wink:

Only member functions declared static can be used without even creating an instance of the class. The Random class however needs to be instantiated to be used as it is a stateful object.

I’m not quite sure if I get what you mean. If a value is constant over the whole loop, there is no point in computing it again and again inside the loop. If an object takes some more time to be created (e.g. as it allocates internally when being constructed) it might make sense to create it before a loop and re-use it. For all other variables that are just simple objects on the stack, put them inside your loop body. No matter how performance critical your loop is

So called “magic numbers” and I would consider pure hex values as such are considered to be bad coding style. It’s the same as with the first point: The best code is readable like some piece of text with as much information as possible being encoded in variable and function naming. So I wouldn’t consider Colours::white as pseudo-code but as really good understandable code, but Colour(0xsomething) as code that doesn’t explain itself when reading it.If you want to declare your own colours instead of using the predefined library colours, I’d chose to create a colour variable like Colour myBlue (0xblue), and then pass it to a function.

Never ever call an objects destructor yourself – this will lead to crashes. If the gradient goes out of scope in the function that calls it, it will be destructed automatically, so I really see no gain in doing so inside the function.

Maybe you should think about more explicit function naming like drawSpaceWithGradient and drawSpaceWithRandomValues – that tells more about the purpose than 1 and 2

This is possible because the Point class has overloaded the * operator. You find that feature in the docs: https://docs.juce.com/master/classPoint.html#a2d3bff0f3de6837ab27a694a87f89dcf

This is not about juce and pure c++ functions. This is about C and C++ functions. pow is a C function, defined like double pow (double base, double exp). Now you are passing a float and an integer to a function that expects two doubles and returns a double and store the result into a float. C also defines float pow (float base, float exp) which is a bit more like the function that you want as it uses float arguments. However, the C++ standard library has std::pow which has multiple overloads, look here: https://en.cppreference.com/w/cpp/numeric/math/pow. This allows the compiler to chose whatever suits your argument types best. If you look at the documentation, you see the overload float pow ( float base, int iexp ) which is exactly what you want – and your compiler is smart enough to pick that variation for you if you use std::pow. If you use the old school C pow however, it will cast everything to double.

I wouldn’t use the limited integer types in most cases, but as the constructor of Colour expects uint8, a cast happens anyway if you pass ints into it. So it’s better to explicitly perform the cast here.

Which amount of RAM are you wasting? Both, an int and an unsigned int have the same size of 32 bits, just the interpretation of the MSB is different. The only thing is the max positive value that could be held by it, but this is far beyond every amount of planets you want to draw. However, if you are doing math on indices etc. it’s often better to use the signed type, so I would advise you to only use the unsigned type if the value range is really required which is a case I nearly never came across

Casting should not be avoided in general, there are good use cases. But I think it’s better to do the cast once in setBounds than every time you use the bounds in your code.

1 Like

i read a bit into that. even though i didn’t understand a lot of it i think i got the idea. auto keeps you from using the wrong types accidently.

true. i experienced that some time. i just didn’t think a channel-count could ever be negative.

good to know. i thought all objects need a destructor in c++. need to read more about it.

this is exactly what i mean. how “big” must an object be so it makes sense to create it before the loop instead of inside of the loop, even though it doesn’t have to remember how it was in the last iteration and does the placement of the loop matter?

sry, but for me that’s bad code. because white != white. white can be slightly different than FFFFFF and you’d still call it pretty much white. for example FFEFEF would still be pretty much white, but a little bit different. you wanna rename your variable everytime you make such a pedantic difference? the hex values give you the perfect representation of the colour. and on top of that the more you use hex values the more you see the patterns in the numbers, so you can try operations on them to get cool (potentially trippy) effects.

edit: also from a purely artistic point you don’t wanna give names to colours because that has an influence of your idea about how to use them. letting things abstract gives you more subconscience variety

yes, totally! i usually rewrite everything 30 times until everything really sits well but this was just my first try of making a space class, so everything was named kinda sloppy.

so i guess it’s save to say that std::pow is just better than all other pows because it apparently covers all pow-needs, except if you’re sure that all values are doubles, right?

oh ok. so i guess an unsigned int can just go to higher values than int for not having negative values, right? but yeah, that means as long as you don’t need these higher values it’s just safer to use the signed integer. that totally makes sense.

thanks for your feedback about everything :slight_smile: i have a lot ot learn

Those are no variables, but definitions. You don’t want them ever to change.
Instead you assign them like

auto background = Colours::red;

You don’t want to change red, since red is red, it never changes. But you want to change how your background looks. And when reading you want easily understand what’s in your variable.

Now if your background is too bright, you might want to change

auto background = Colours::red.darker();
// or
auto background = Colours::red.withAlpha (0.3f);

That is much more readable than 0xfafafa. If you think knowing hex codes of colours by heart makes you a better programmer, that is up to you, I think for most people that is wasted memory. Things I can explain in my language is certainly easier to understand and easier to communicate to colleagues.

Objects that fit easy on the stack you don’t need to keep. (My gut feeling is around 100-1000 bytes, but depends on architecture and I never measured it).
A path that contains only 50 points certainly is not worth storing. A path that shows a waveform: definitely. The cost here is allocating space for the dots. Even keeping the memory preallocated might speed things up.

Not just that. The old C-pow needs to specify, what type the argument is, hence pow, powf and powl exist.
But std::pow is a template with specializations for each type. You don’t need to call the right version, it is deduced from the argument type.
The old version cannot do this, because an implicit cast would happen. It falls into the context of auto and type deduction.

double foo = 2;
DBG (powf (foo)); // was converted to float, data loss
float bar = 2.0f;
DBG (pow (bar)); // was converted to double for no reason

and that’s where i see it a bit differently. for example when pluginpenguin improved my code they(he/she?) changed the planetColour from the hex-value that made it yellow to Colours::yellow. and while this would have been fine if i really wanted it defined like that i just noticed that the planetColour is actually a bit nicer when it’s not just yellow but also a little bit greenish, so i went to an rgb-generator and made this colour: 0xffC3FC27 and yeah, you can’t read that. it doesn’t say “hey, i’m yellow with a bit of green”, but it has just the right colour. from a designer perspective i want to have the full control over the colour and not just choose from a handful a nicely selected colours, even though they have names. even if i went on and stored this colour into a variable called auto greenishYellow = Colour(0xffC3FC27); i would commit to using a greenishYellow-type colour then and people who would read my code might feel like my goal was to find a greenishYellow-colour for the planets, but in fact i’m still open-minded about it and considering other colours as well. so using the juce-Colours-namespace is not really “bad” for me, i love it for just testing things really fast, but it’s also not something i’d use for a final plugin, except for the very generic colours in it, like transparentBlack.

Ok, then his improvement was actually introducing a bug. I didn’t follow his text completely. I just wanted to point out, what those defines (btw. defined by the API, not by you) are good for.

I would still prefer using the colour constructor, where you supply the numbers:

auto myCreamYellow = Colour (0xC3, 0xFC, 0x27, 0xFF); // you could even leave out FF, since opaque is default

But that is now my personal preference…

I agree on that one… usually I have a little mapping somewhere… or use the LookAndFeel colour roles, that each component has

do you guys know if there’s a way to just store the image when the gui has freshly opened and then just use this until it’s minimized or closed again? my goal is that when you turn knobs it won’t have to calculate all the planets’ and space’s look again but just load the image from an array of pixels or something, so it really takes almost no computation power from there, because that would mean that we can draw infinitely complex images since it would only have to be calculated at the start once. i tried with the snapshot-function of Image but it produced some very weird results. optimally it would also print it into the image-object without the splashscreen but i’d be very happy if it worked at all for now

You can use Juce’s Graphics object to draw on Images.

When you need the image generated :

        // Image img{ Image::ARGB,100,100, true }; declared as member variable
	Graphics g{ img };
	g.drawLine(1, 1, 99, 99);

And then in your component’s paint() method, you can paint the pregenerated image with one of the drawImage* methods of Graphics. It is not going to be completely cost free (copying the pixels from the image takes CPU too), but perhaps cheaper than drawing your graphics from scratch each time in the paint() method.

yeah i tried that, but that didn’t work out very nicely.

it pretended like i didn’t just drew a lot of stuff on the graphics object when snapshotting it. i think there must be some kinda hierarchy going on in the background that makes it draw sliders and the stuff from the paint-method first, then take the snapshot and then draw the space, even though the space-method was drawn from the paint-method as well. it’s really weird and i don’t understand the logic behind that

No, don’t attempt any of the component snapshot stuff, draw your things completely separately of the paint() method into the Image.

Something like this :

class MyComponent : public Component
	void paint(Graphics& g) override
		g.drawImage(cachedImage, 0, 0, getWidth(), getHeight(), 0, 0, getWidth(), getHeight());
	void resized() override
		cachedImage = Image(Image::ARGB, getWidth(), getHeight(), true);
	void generateImage()
		Graphics g{ cachedImage };
		g.drawLine(5, 5, getWidth() - 5, getHeight() - 5);
	Image cachedImage;
1 Like

your approach already helped so far that it only calculates the image on start. but now the image itself doesn’t make sense anymore. why did it draw just one star on a random colour? where did everything go? :smiley: that’s so weird.

edit: according to the api when you put an image-object in the graphics-constructor it draws everything into this image. so it’s kinda weird that the paint-method doesn’t draw the image, if it really contains the space at that point. even if i call repaint() from generateImage() it doesn’t seem to work so i suspect it doesn’t really draw the space onto my image-object, but idk how to debug this

You make the space.setSize call after setting the size of the main component, maybe that messes things up. (The resized() method of the main component is called immediately when setting the component’s size.)

1 Like

AWESOME!!! i can’t breathe… ok i can^^ but this is just too good. you know how incredible this is? because this means that we can literally draw anything of any complexity, just throw it into an image in the resized method and do whatever we want from there. that’s just so cool… now the space-class and its component are just perfect

1 Like

i made another video. decided to put it into the same post because this one’s is partially about how you helped me to get a better coding style in my space class and a more stable performance from images that avoids glitches. but it is also about making and then using post processing effects on these images, for example i drew a posterize-filter and a noise-adding-function. you can see both in the thumbnail of this video.
1 Like