Incorrect stroke rendering of rounded rectangles in paths?

This code

void ParamButton::paintButton( Graphics& g,
                               bool isMouseOverButton,
                               bool isButtonDown )
  Rectangle<int> bounds = getLocalBounds();
  Rectangle<int> r = bounds;
  r.setHeight( r.getHeight() - 2 );

  g.setColour( Colours::white );

  Path p;
  p.addRoundedRectangle( r.getX()+0.5, r.getY()+0.5, r.getWidth()-1, r.getHeight()-1, 2 );
  g.setColour( Colours::black );
  g.strokePath( p, 1 );

Produces the following:


If I’m doing something stupid, don’t hesitate to call me out. But why aren’t the flat sides uniform?

Well you add +0.5 to the x and y coordinate hence the anti aliasing no ?

It looks like a rounding error in the path stroking algorithm, though I’d never squinted hard enough to notice it before! I guess one of the curved corners has been subdivided in a way that doesn’t exactly line up with the vertical line…

No, if the line is 1px wide, then it should spread 0.5 pixel before and after the exact position you gave.
For example, a vertical line set to draw on column 36 of 1px wide should darken (virtually) from 35.5 to 36.5. Since it’s not possible with integer pixels, it’s antialiased.

If you give +0.5 px, then it should darken column 36 to 37 exactly, with no aliasing.

I took the time to write out a test application so this can be easily seen. Rounded rectangles with corner radius of 1, 2, and 3 are filled, stroked, and overlaid. This is the result:


And this is the test program:

#include "juce.h"

static void strokePath (Graphics& g,
                        Path& path,
                        const PathStrokeType& strokeType,
                        const AffineTransform& transform = AffineTransform::identity)
  Path stroke;
  strokeType.createStrokedPath (stroke, path, transform, 1.4142136);
  g.fillPath (stroke);

struct Panel : Component
  ~Panel() { deleteAllChildren(); }
  void paint (Graphics& g)
    g.setColour( Colours::grey );

    int x=getWidth()/2;
    int y=getHeight()/2;

    Path p;
    p.addRoundedRectangle( -9.5, -12.5, 19, 9, 1 );
    p.addRoundedRectangle( -9.5, -0.5, 19, 9, 2 );
    p.addRoundedRectangle( -9.5, 12.5, 19, 9, 3 );
    g.setColour (Colours::white);
    g.fillPath (p,AffineTransform::translation(x,y));
    g.fillPath (p,AffineTransform::translation(x-30,y));
    g.setColour (Colours::black);
    strokePath (g, p, 1, AffineTransform::translation(x,y));
    strokePath (g, p, 1, AffineTransform::translation(x+30,y));

struct MainWindow  : DocumentWindow, Button::Listener
  : DocumentWindow (JUCE_T("Test")
  , Colours::black
  , DocumentWindow::allButtons
  , true )
    Panel* p = new Panel;
    p->setSize( 512, 384 );
    setContentComponent (p, true, true);
    centreWithSize (getWidth(), getHeight());
    setVisible( true );
  ~MainWindow() {}

  void buttonClicked (Button* button)
    Component* c = getContentComponent()->getChildComponent(1);
    c->setVisible (true);
    c->setTopLeftPosition (64, 64);

  void closeButtonPressed() { JUCEApplication::quit(); }

struct MainApp : JUCEApplication
  MainApp() : mainWindow(0) { s_app=this; }
  ~MainApp() { s_app=0; }
  static MainApp& GetInstance() { return *s_app; }
  const String getApplicationName() { return JUCE_T("JuceTest"); }
  const String getApplicationVersion() { return JUCE_T("0.1.0"); }
  bool moreThanOneInstanceAllowed() { return true; }
  void anotherInstanceStarted (const String& commandLine) {}

  void initialise (const String& commandLine)
    mainWindow = new MainWindow;

  void shutdown()
    delete mainWindow;

  static MainApp* s_app;
  MainWindow* mainWindow;

MainApp* MainApp::s_app = 0;


Also want to point out that in the previous test application, I’m passing higher extraAccuracy when converting the stroke into a path. This is what it looks like using the original Graphics::strokePath(). Note that only the rounded rectangle with cornerRadius==1 has the problem. I hope this information is helpful in tracking down the defect.


Thanks - I’m on the case with this one…

Just out of curiosity, and please excuse my ignorance if I am wrong, but why not use double for all calculations? At least, on x86/x64 architectures, floating point registers are all double-sized anyway. For that matter, aren’t float arguments promoted and passed as double on the stack to functions? Or am I thinking of integer arguments that are always expanded to 32 bits?

Or is this not a problem of accuracy with respect to the underlying floating point type, but an algorithmic inaccuracy from the method used to quantize the curve?

Yes, it was just that the algorithm could produce silly angles when the values got too small. I’m just taking another look at it though, so will post a better version shortly.

All of the problems demonstrated here are fixed in the latest tip. Thanks!

No problem, I’d been meaning to sort out the tolerance calculations for a while.