Incorrect stroke rendering of rounded rectangles in paths?


#1

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 );
  g.fillAll();

  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:

[attachment=0]roundedRectangle.png[/attachment]

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


#2

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


#3

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…


#4

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.


#5

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:

[attachment=0]roundedrectangles.png[/attachment]

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 );
    g.fillAll();

    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
{
  MainWindow()
  : 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;

START_JUCE_APPLICATION (MainApp)

#6

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.

[attachment=0]roundedrectangles2.png[/attachment]


#7

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


#8

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?


#9

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.


#10

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


#11

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