Pixel perfect path drawing and transparency problem


#1

I’m creating my own look and feel method to draw my custom buttons and I need it to be pixel perfect because they will need to line up with other parts of the UI, but it’s not going well.

My (simplified) drawing code looks like this, which is almost directly lifted from the original L&F code:

void drawButtonBackground (Graphics& g, Button& button, const Colour& backgroundColour,
                                               bool isMouseOverButton, bool isButtonDown) override
{
       // outer shadow
        outline.addRoundedRectangle (0.5f, 0.5f, getWidth() - 1.0f, getHeight() - 1.0f,
                                     cornerSize, cornerSize,
                                     ! (flatOnLeft  || flatOnTop),
                                     ! (flatOnRight || flatOnTop),
                                     ! (flatOnLeft  || flatOnBottom),
                                     ! (flatOnRight || flatOnBottom));
        g.setColour(Colour::fromFloatRGBA(0.0f, 0.0f, 0.0f, 0.25f));
        g.fillPath(outline);
        
        // the problem...
        g.strokePath(outline, PathStrokeType(1.0f));
}

I’m creating a slight shadow around the button and I’m using a semi-transparent black so it will work with any background.

My test button is 50x21 pixels and on my retina screen with the standard L&F, it comes out exactly to 100x42 as expected. Without the last strokePath, my button comes out to 99x41. Not good. But if I add the strokePath, it comes out exactly to 100x42 as expected, but because of the now layered transparency, it’s slightly darker where the stroked path is. Not good either.

It seems like my choices are to have the correct size with incorrect shading, or the wrong size with correct shading. I don’t like either.

Anyone know how to correct this?


#2

I don’t think you need the 0.5 offset here, neither the strokePath. something like that should work?

Path outline;
outline.addRoundedRectangle (0.f, 0.f, (float) getWidth(), (float) getHeight(), cornerSize);
g.setColour (Colours::black.withAlpha (0.25f));
g.fillPath (outline);

#3

Talking about “pixel perfect” really doesn’t make any sense these days.

If you want to fill specific physical pixels on modern devices, you can’t just plug some integer constants in there. The only reliable way to do it would be to fetch the DPI scale factor of the graphics context and figure out some kind of rounding algorithm that snaps your target positions to the nearest physical pixels, bearing in mind that they could be tiny, or even rotated.


#4

Yes, it looks like @lalala is correct, I didn’t need the 0.5f offset for a fillPath, and then the strokePath is unneeded.

And maybe “pixel perfect” was not the best way to say it, more like predictable and understandable, especially for small details.

Thanks.


#5

Drawing a path with both a fill and a stroke without artefacts at the edges is a tricky problem even without transparency. I had the same problem with a circle with a black border and an arbitrary fill. What I ended up doing is first filling the “outer” circle with the border colour (no border, but the entire circle), and then filling a slightly smaller circle with the arbitrary colour. I tried the usual pattern of filling up to halfway the border stroke, followed by painting the stroke, but with dark colours you’d see the light background bleed through on the inside of the edge.

For a semi transparent button with a stroke I think you have to extend the fill to the outer edge of the stroke. Note that the border colour will then be the result of painting both the fill colour and the stroke colour.


#6

I don’t think it looks that bad really (?) :

g.fillAll (Colours::white);
Rectangle<float> circleArea (10.f, 10.f, 100.f, 100.f);
g.setColour (Colours::lightgreen);
g.fillEllipse (circleArea);
g.setColour (Colours::black);
g.drawEllipse (circleArea.reduced (0.5f), 1.f);

#7

Depends on the colours. For darker fills (try Colours::red) you’ll notice the outside of the circle looks jagged: the fill colour is visible through semi transparent pixels on the outside of the border, making them darker.

Usually it looks OK if your fill extends halfway through the outline stroke.