JUCE 2D Graphics: Paths and Basic Geometry
Experiments with the JUCE 2D graphics API
A while ago I had to learn a bit of JUCE 2D graphics programming for some
experiments we were doing at TONZ. I thought it was super
interesting at the time and kind of fun, so I’ve spent a little bit more outside of work
playing with the graphics subsystem to learn to do more things.
I’ll walk through how to make a Venn Diagram using JUCE GUI geometry and graphics primitives.
Let’s start from the beginning.
The Graphics Context
Graphics are updated in UI components whenever the paint() method gets called,
which every UI Component can overload with custom code.
It is defined in the base Component class and inherited through the whole GUI API.
The paint() method gets
called when the object is drawn, and it is passed a reference to a Graphics context object.
This object controls how drawing will be performed, and provides information about where and how, at the time the paint() method is called.
Properties like color, opacity, bounds, clipping region, etc, are stored in Graphics context object and can be read or set
within the paint() method while it is drawing.
Geometry and Paths
The simplest way to draw in JUCE is to use methods available to draw basic geometrical shapes: rectangles, ellipses,
triangles. There are a number of ways to do this, but I am interested in using Paths, which represents
a sequence of connected lines and curves that can abstractly represent a shape that gets drawn or transformed. You can simply ask the Graphics
context object to draw a supported shape using points, but I think that’s less interesting than working with Path objects because these can do more
things.
Creating two overlapping circles
/* Define two paths */
juce::Path path1, path2;
/* Create two paths representing circles that overlap */
path1.addEllipse(getWidth() - 450, 50, 200, 200);
path2.addEllipse(getWidth() - 300, 50, 200, 200);
Now let’s draw them using the strokePath()
method in the Graphics context object. We should set a different color for each using the setColour() method. This needs to be done separately, before each
call to strokePath(). The Graphics context object is like a robot painter, one that needs to dip their brush into the right color as they
carry out their intended work in order.
g.setColour(juce::Colours::blue); /* Blue */
g.strokePath(path1, juce::PathStrokeType(1.5f)); /* Draw first circle */
g.setColour(juce::Colours::red) /* Red */
g.strokePath(path2, juce::PathStrokeType(1.5f)); /* Draw second circle */
Let’s see them
Making a Venn Diagram
The next challenge is to try to depict the classic Venn Diagram. In this exercise, I wanted to
have each circle be filled with a distinct color, with the shared region a third color.
There are many ways to do this but I wanted to use a clever trick with clipping region and paths. I’m going
to introduce a method I love using called reduceClipRegion(), which
tells the Graphics context where it is allowed to draw.
Idea:
- Create two Paths that are overlapping circles
- Fill each of them with their own colour
- Apply a clipping region of path 1 in the Graphics context object
- Fill path 2 again, so it will only have the region shared with path 1 filled, due to the clipping region reduction in step 3
/* Define two paths */
juce::Path path1, path2;
/* Step 1 */
path1.addEllipse(getWidth() - 450, 50, 200, 200);
path2.addEllipse(getWidth() - 300, 50, 200, 200);
/* Step 2 */
g.setColour(juce::Colours::blue); /* Blue */
g.fillPath(path1); /* Draw and fill the first circle */
g.setColour(juce::Colours::red); /* Red */
g.fillPath(path2); /* Draw and fill the second circle */
g.setColour(juce::Colours::purple); /* Red + Blue = Purple */
/* Step 3 */
g.reduceClipRegion(path1); /* Reduce the clip region to the area of path 1 */
/* Step 4 */
g.fillPath(path2); /* Fill circle #2, only within the clipping region (path 1) */
The Venn Diagram
Here is what it looks like after step 4.
That’s it for now. In the next post I’m going to talk about another reason using Path objects are so cool:
the ability to manipulate them using Affine transform
operations. Affine tranformations are represented compactly using a matrix, so I found myself in familiar territory and
was able to use some linear algebra know-how from ML to do some interesting things.