Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the join not rounded when the line doubles back on itself?

I have the following code:

- (void)drawRect:(CGRect)rect {
    CGContextRef c = UIGraphicsGetCurrentContext();

    CGContextSetFillColorWithColor(c, [UIColor blackColor].CGColor);
    CGContextFillRect(c, rect);

    CGContextSetLineJoin(c, kCGLineJoinRound);
    CGContextSetLineCap(c, kCGLineCapRound);
    CGContextSetLineWidth(c, 50.0);

    CGContextSetStrokeColorWithColor(c, [UIColor redColor].CGColor);
    CGContextBeginPath(c);
    CGContextMoveToPoint(c, 60, 60);
    CGContextAddLineToPoint(c, 60, 250);
    CGContextAddLineToPoint(c, 60, 249);
    CGContextStrokePath(c);

    CGContextSetStrokeColorWithColor(c, [UIColor blueColor].CGColor);
    CGContextBeginPath(c);
    CGContextMoveToPoint(c, 160, 60);
    CGContextAddLineToPoint(c, 160, 250);
    CGContextAddLineToPoint(c, 160.01, 249);
    CGContextStrokePath(c);
}

This generates the following output:

Output of the code

Is there a good reason that the red shape's bottom edge is not rounded? Or is it a bug in Core Graphics when the line exactly doubles back on itself?

like image 789
Anomie Avatar asked May 03 '11 14:05

Anomie


2 Answers

It's definitely a bug. If you try adding another line to the path, you can see how Core Graphics is unable to handle it.

CGContextMoveToPoint(c, 60.0, 60.0);
CGContextAddLineToPoint(c, 60.0, 250.0);
CGContextAddLineToPoint(c, 60.0, 249.0);
CGContextAddLineToPoint(c, 60.0, 250.0);

enter image description here

It's as if the masking that creates the rounded caps and joins gets inverted when it's doubled.

like image 61
Morten Fast Avatar answered Nov 19 '22 20:11

Morten Fast


mortenfast proved this is a bug. But I'll post this answer to offer my workaround.

A workaround is to detect this case and add a very short line segment perpendicular to the existing line, something like this:

- (void)addPtToPath:(CGPoint)newPt {
    // CoreGraphics seems to have a bug if a path doubles back on itself.
    // Detect that and apply a workaround.
    CGPoint curPt = CGPathGetCurrentPoint(self.currentPath);
    if (!CGPointEqualToPoint(newPt, curPt)) {
        CGFloat slope1 = (curPt.y - prevPt.y) / (curPt.x - prevPt.x);
        CGFloat slope2 = (curPt.y - newPt.y) / (curPt.x - newPt.x);
        CGFloat diff;
        BOOL between;
        if (isinf(slope1) && isinf(slope2)) {
            // Special-case vertical lines
            diff = 0;
            between = ((prevPt.y < curPt.y) != (curPt.y < newPt.y));
        } else {
            diff = slope1 - slope2;
            between = ((prevPt.x < curPt.x) != (curPt.x < newPt.x));
        }
        if (between && diff > -0.1 && diff < 0.1) {
            //NSLog(@"Hack alert! (%g,%g) (%g,%g) (%g,%g) => %g %g => %g", prevPt.x, prevPt.y, curPt.x, curPt.y, newPt.x, newPt.y, slope1, slope2, diff);
            if (isinf(slope1)) {
                curPt.x += 0.1;
            } else if (slope1 == 0) {
                curPt.y += 0.1;
            } else if (slope1 < -1 || slope1 > 1) {
                curPt.x += 0.1; curPt.y -= 0.1 / slope1;
            } else {
                curPt.x -= 0.1 * slope1; curPt.y += 0.1;
            }
            CGPathAddLineToPoint(self.currentPath, NULL, curPt.x, curPt.y);
        }
        prevPt = curPt;
    }
    CGPathAddLineToPoint(self.currentPath, NULL, newPt.x, newPt.y);
}

This needs one ivar named prevPt, and operates on the path in the ivar currentPath.

like image 3
Anomie Avatar answered Nov 19 '22 19:11

Anomie