Graphics drawing artifacts

For instance, from code where I draw the grid lines for an EQ component, I have this helper function to draw those lines - using a Path:

    const auto drawPathLine = [&g] (float x1, float y1, float x2, float y2) -> void
    {
        juce::Path p;
        p.addLineSegment (juce::Line { x1, y1, x2, y2 }, 1.0f);
        g.fillPath (p);
    };

The function that I use for drawing frequency lines (the horizontal version for dB is similar) is:

    const auto drawFreqLine = [&] (float freq) -> void
    {
        const auto x = floor (freqToX (freq)) + 0.5f;
        drawPathLine (x, 0, x, height);
    };

so, I clearly forced the lines to be “in between” pixels.

If I had used the float results from my freqToX () function, I would have had “some” lines with aliasing, depending on them being closer to .0f then to .5f.

This works for me and helps me forget the differences between, drawLine, drawPath, fillRect, etc. (ok, there are some comments in the code, for the next time I run into such issues).

The issue I report here is no only the pixel offset. Based on your code :

void drawPathLine (Graphics& g, float x1, float y1, float x2, float y2) {
	Path p;
	p.addLineSegment(Line<float>(x1, y1, x2, y2), 1.0f);
	g.fillPath(p);
}
void BoxComponent::paint(Graphics& g) override {
	g.setColour(Colours::red);
	g.fillRect(getLocalBounds());
	
	g.setColour(Colours::green);
	drawPathLine(g, getWidth() - 1.0, 0, getWidth() - 1, getHeight());
}


We can see there is a line of 2px.

Even though I use your 0.5f :

void drawPathLine (Graphics& g, float x1, float y1, float x2, float y2) {
	x1 += 0.5f;
	x2 += 0.5f;
	Path p;
	p.addLineSegment(Line<float>(x1, y1, x2, y2), 1.0f);
	g.fillPath(p);
}

I think there is something wrong in the juce code because I’m pretty sure I had not the issue before, and I have no way to get a clean line from pretty simple example. (I’m using Juce 6.1.6 wich is the latest version)

If someone in the Juce team could help here it would be great…

Well, one thing is clear to me: you do not read well (enough)…

You just add 0.5f by scanning my reply, but you do not use std::floor. Where is it? Did you understand my explanation?

By throwing in the += 0.5f you make my advice look like a hack, which it certainly is not.

Then you say “We see a 2px line”.

Well, I see an “aliased 1px Juce line”, starting at a .0f pixel location.
Make sure it starts between pixels, thus with a value ending exactly at .5f.

A tip: get a “linter”, or some tool that helps you to show mistakes in your source code before you even compile it, I suggest ReSharper (for Visual Studio), and/or AppCode, for Mac - both from JetBrains.

You are mixing floats and ints. Don’t.
getWidth () returns an int and you are mixing that with floats.

Even in one single line you use:

getWidth() - 1.0  // that's a double! Not even a float

and

getWidth() - 1

No compilation errors, but this kind of coding will surely get you into troubles.

My approach nicely draws a single pixel thin line:

    g.setColour (juce::Colours::red);
    g.fillAll ();

    const auto drawPathLine = [&g] (float x1, float y1, float x2, float y2) -> void
    {
        juce::Path p;
        p.addLineSegment (juce::Line { x1, y1, x2, y2 }, 1.0f);
        g.fillPath (p);
    };

    g.setColour (juce::Colours::green);

    const auto x = std::floor (static_cast <float> (getWidth () - 1)) + 0.5f;

    drawPathLine (x, 0, x, static_cast <float> (getHeight () - 1));

line

In this particular case the floor function has no use, because we start with an integer getWidth () value. So the offset is nicely moved to a 0.5f value by the addition.

In situations with calculated floating point positions, like my Eq grid example, it is needed.

And I would not use a fillPath approach in a simple situation like this, but a fillRect () - this is just to continue with my example from above.

Sorry if I missed your point. I understood that we want to always be inbetween pixels in order to draw a clean one pixel line. So always using values like 1.5, 2.5, etc. You add 0.5 to be inbetween and add the floor to secure the case where your input value stacks up with an other decimal value as 0.5. Am I correct ?

Your advice was obviously good as it is more generic. I get rid of floor as I was focused on this specific use case where I know for sure the drawPathLine will not have a float input with decimal part.

Even using your last code example I have the artifacts…

    g.setColour (juce::Colours::red);
    g.fillAll ();

    const auto drawPathLine = [&g] (float x1, float y1, float x2, float y2) -> void
    {
        juce::Path p;
        p.addLineSegment (juce::Line { x1, y1, x2, y2 }, 1.0f);
        g.fillPath (p);
    };

    g.setColour (juce::Colours::green);

    const auto x = std::floor (static_cast <float> (getWidth () - 1)) + 0.5f;

    drawPathLine (x, 0, x, static_cast <float> (getHeight () - 1));

Looks almost normal without zoom,
image

But at pixel level :

Here we can see the line isn’t 1px. But 2. And blurry ones

On your first question: I use the floor () plus 0.5f for any calculated floating point value.
It is not about “stacking” other 0.5f values.
If I calculate the position for a frequency or a dB value I don’t think in values like 0.0f or 0.5f - they can be anything in between. And with Juce’s path, anything not ending with 0.5f will show aliasing. Simple.

And on your latest screen shots: really weird. I also zoomed in on the first picture, in order to check it is not a zooming issue (avoid the Windows zoom tool! Or be aware that it introduces aliasing, seriously). But the issue is indeed in the small image as well.

I checked my example also on a Mac with Retina (because I develop on Windows). Also OK. Latest Juce develop. Unfortunately I cannot test with another DPI environment.

Have you tried drawing a 0.5f wide line? :smile: I would start testing a lot of weird ideas in a situation like this…

1 Like

Please check in your Windows settings if you have a global scale factor set (possibly 125% based on the findings by @kamedin before):

image

1 Like

I tried 125 and 150 while doing these checks, no differences for me.
Maybe there are, if the graphics card has a high DPI?

Good shot ! I was effectively in 125%, I didnt knew about it. So is there a thing we should do in the code to handle this properly ?

It depends on what you mean with “handle this properly”: if you wish your coordinates to always match the pixels on the screen ignoring Windows’ scale setting, then you should disable DPI awareness for your window, but that comes at the cost that if a user wanted its scale set to 200% because he has a highDPI monitor, your window will still appear as if he had set 100% for your window, i.e. very small compared to his expectations.

If you want your window to follow the (non-integer) global scale set in Windows, then some blurring/antialiasing/artifacts are expected because logical coordinates no longer precisely match with the hardware pixels “underneath”

1 Like

Well, I went back to my above code and tested it again. And of course I did NOT check all combinations…

I apologize…

When I run my last example from above at 125% in Windows, I also see a “stretched” = aliasing line, doh…

Scaling issue

Thanks @yfede and others :+1:

1 Like