Painting canvas

gui

#1

Hello,

I’m new to JUCE and I’m trying to implement a drawing canvas in which I can pan and zoom. My first try was to create a new Component class called Canvas that stored an image. The main issue I have now is the zooming and the panning. I tried using the rescale method of the Image class but of course it changes the quality of the image a lot, so it’s not a suitable method. My next thought was to use a Viewport for the panning feature but still, I have no idea on how to implement the zooming.

And another problem I have is when I draw my strokes (between lastX and lastY) with a big line thickness, I got this stange behavior:

Here is the bit of code

void Canvas::mouseDown(const MouseEvent& e)
{
	isDrawing = true;

	lastX = e.x;
	lastY = e.y;

}

void Canvas::mouseDrag(const MouseEvent & e)
{
	
	Graphics g(*image);

	if (lastX != e.x || lastY != e.y)
	{

		g.drawLine(lastX, lastY, e.x, e.y, 100);
	}

	repaint();

	lastX = e.x;
	lastY = e.y;

}

Any thoughts ?

Have a nice day :slight_smile:


#2

TL;DR:
To implement rescaling, the only sensible approach (outside of very specific bandlimited situations) is to store all your paint events as paths instead, redrawing them when rasterization changes. Or any other structure allowing you to interpolate/resample your discrete mouse events into continuous functions. This will also solve your other problem.

Bla bla:
The above rendering is correct, given the information you provide it. Consider your algorithm: You tell it to paint rectangles once in a while, not curves. Your algorithm will only look like a perfect curving rasterization if the distance between your mouse events is so small that any angle curvature will be covered by adjacent rectangles. The resolution required is inversely propertional to your sample frequency, ie. 1/x. When you draw with a line width of 1, you have sufficient information to reconstruct any representable curve on a quantized pixel grid.


#3

Hello and thank you for your answer,

so what could be a good solution according to you for my rendering issue ? I remember doing the same thing in Qt with rounded lines without the same issue. I thought about placing a circle at the beginning and end of the line , but the issue I’ll encounter then will be with line transparency, the circle will be alpha blended on top of the line (or on the bottom depending on the order) and this is clearly something I don’t want.

I tried this but it’s very slow (not surprised :p)

void Canvas::mouseDown(const MouseEvent& e)
{
	isDrawing = true;

	lastX = e.position.x;
	lastY = e.position.y;

	p.startNewSubPath(lastX, lastY);

}

    void Canvas::mouseDrag(const MouseEvent & e)
    {
	Graphics g(*image);

	g.setColour(Colour(uint8(0), uint8(0), uint8(0), uint8(20)));

	p.lineTo(e.position.x, e.position.y);
	g.strokePath(p, PathStrokeType(20, PathStrokeType::JointStyle::mitered, PathStrokeType::EndCapStyle::rounded));

	p.clear();
	repaint();

	lastX = e.position.x;
	lastY = e.position.y;
	p.startNewSubPath(lastX, lastY);
}

And I got the circle issue too :stuck_out_tongue:

image


#4

If I were to write such thing, I would have the path as member variable. Instead of having the image cached, just call strokePath in the paint. The positive effect (additional to speedup) is, you can scale without rasterisation artefacts.

The scaling is simply done by using an AffineTransform at strokePath.


#5

The you for your answer, but I will still have the issue of transparency with paths.


#6

No, you’re misunderstanding. Daniel’s suggesting you draw the entire thing as a single Path, which is the only way this could possibly work. If you just draw each line segment separately, then of course it’ll get the result you see above - the same would be true for any drawing API that exists. The only way to create a seamless stroke is to build the whole path and rasterise it in a single operation.


#7

Thank you for your answer :slight_smile:

So indeed drawing the whole path resolve the visual issue but slows everything because for each mouseDrag I’m obliged to clear the image (fillRect, and by the way I want to render to an image) and stoke the path (with all the previous points). The path is quickly filling the memory with a bunch of points and it takes more time to render.

eg:

void Canvas::mouseDrag(const MouseEvent & e)
{
	Graphics g(*image);

	g.setColour(Colour(uint8(255), uint8(255), uint8(255), uint8(255)));

	g.fillRect(0, 0, getWidth(), getHeight());

	g.setColour(Colour(uint8(0), uint8(0), uint8(0), uint8(20 )));

	p.lineTo(e.position.x, e.position.y);
	g.strokePath(p, PathStrokeType(20 + 5 * e.pressure , PathStrokeType::JointStyle::curved, PathStrokeType::EndCapStyle::rounded));

	lastX = e.position.x;
	lastY = e.position.y;
	p.startNewSubPath(lastX, lastY);
	repaint();
}

#8

The problem is, that you clear the path each event. These are the steps necessary:

// Member:
Path p;
float scale = 1.0;
Point<float> translation;
Point<float> lastPos;  // for implementing translation e.g.

MainContentComponent::MainContentComponent()
{
    setSize (600, 400);
}
MainContentComponent::~MainContentComponent()
{
}

void MainContentComponent::mouseDown (const MouseEvent& e)
{
    p.startNewSubPath (e.position);
    repaint();
    lastPos = e.position;
}
void MainContentComponent::mouseDrag (const MouseEvent& e)
{
    if (e.mods.isShiftDown()) {
        translation += e.position - lastPos;
    }
    else {
        p.lineTo (e.position);
    }
    repaint();
    lastPos = e.position;
}
void MainContentComponent::paint (Graphics& g)
{
    // (Our component is opaque, so we must completely fill the background with a solid colour)
    g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));

    g.setColour  (Colours::white);
    AffineTransform trans = AffineTransform::scale (scale).translated (translation);
    g.strokePath (p, PathStrokeType (10), trans);
}

void MainContentComponent::resized()
{
}

I think this is a red herring. You can store a lot of points, until you even out the saved memory by removing the image.

N.B. if you start using the translation and scale, you have to apply these to the events as well… left as an exercise…


#9

There’s a similar thing in the demo app which may be helpful:


#10

Thank you very much for your help, it will help me for sure !
So I’ll draw on the component directly. Is there a way then to save the current painting state as an image ?


#11

If it is for export, you can use Component::createComponentSnapshot (getLocalBounds())


#12

Thank you very much !!!

Finally what I’ve done (and it’s not slow) is a ‘kinda’ brush system because I wanted to take the pressure into consideration. The cool thing is that now I can define a brush shape. It’s still not optimized but here is the result.

image

For those who are interested here is my code:

void Canvas::mouseDrag(const MouseEvent & e)
{
	Graphics g(*image);
	Random r;

	float dist = lastPos.getDistanceFrom(e.position);
	float step = dist; 

	Point<float> vecLastMouse = e.position - lastPos;
	vecLastMouse = vecLastMouse / dist; // Normalized

	g.setColour(Colour(0.1f,0.0f,0.0f,e.pressure * 0.2f));

	for (int i = 0; i < step; i++)
	{
		g.fillEllipse(lastPos.x + vecLastMouse.x * i, lastPos.y + vecLastMouse.y * i, 5 + e.pressure * 5, 5+ e.pressure * 5);
	}

	repaint();
	lastPos = e.position;
}

Have a nice day !