Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Core Graphics from ObjectiveC, how to you curve an arrow around a circle?

Our designer has asked me to recreate this: an orange arrow-bar rotating around a circle

Im subclassing UIView, and I've overridden the drawRect command like this:

[super drawRect:frame];

CGFloat x = self.frame.origin.x;
CGFloat y = self.frame.origin.y;
CGFloat w = self.frame.size.width;
CGFloat h = self.frame.size.height;
CGFloat lineWidth = lineWidthRequested;
CGPoint centerPoint = CGPointMake(w/2, h/2);
CGFloat radius = radiusRequested;
CGFloat startAngle = 3 * M_PI / 2;
CGFloat endAngle = startAngle + percentage * 2 * M_PI;

CGMutablePathRef arc = CGPathCreateMutable();
CGPathAddArc(arc, NULL,
             centerPoint.x, centerPoint.y,
             radius,
             startAngle,
             endAngle,
             NO);
CGPathRef strokedArc = CGPathCreateCopyByStrokingPath(arc, NULL,
                               lineWidth,
                               kCGLineCapButt,
                               kCGLineJoinMiter, // the default
                               10); // 10 is default miter limit

CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc);
CGContextSetFillColorWithColor(c, [UIColor colorWithRed:239/255.0 green:101/255.0 blue:47/255.0 alpha:1.0].CGColor);
CGContextDrawPath(c, kCGPathFill);

What I ended up with is this: an orange bar around the circle

Gotta still draw the arrowhead. Gonna be easy, right?

After struggling to remember my trig, I found rotation of points around a center on this page: Rotating a point around an origin in VB

But when I tried translation to objective C to draw the arrowhead, I'm getting very odd results. Here's the code further down in drawRect:

    CGFloat triangle[3][2] = {{centerPoint.x + 10, h - (centerPoint.y + radius)},
    {centerPoint.x, h - (centerPoint.y + radius + lineWidth/2)},
    {centerPoint.x, h - (centerPoint.y + radius - lineWidth/2)}};

for (int idx=0;idx < 3; idx++) {
    // translate to origin
    triangle[idx][0] -= centerPoint.x;
    triangle[idx][1] -= centerPoint.y;
}

CGFloat angDistance = endAngle - startAngle;
CGFloat ct = cos(angDistance);
CGFloat st = sin(angDistance);

for (int idx=0;idx < 3; idx++) {
    // rotate
    triangle[idx][0] = ct * triangle[idx][0] - st * triangle[idx][1];
    triangle[idx][1] = -st * triangle[idx][0] + ct * triangle[idx][1];
}

for (int idx=0;idx < 3; idx++) {
    // translate back to position
    triangle[idx][0] += centerPoint.x;
    triangle[idx][1] += centerPoint.y;
}

NSLog(@"Rotating through %g, %06.1f,%06.1f  , ct - %g, st - %g",angDistance, triangle[0][0],triangle[0][1],ct, st);

// XXX todo draw the filled triangle at end.
// draw a red triangle, the point of the arrow
CGContextSetFillColorWithColor(c, [[UIColor greenColor] CGColor]);
CGContextMoveToPoint(c, triangle[0][0], triangle[0][1]);
CGContextAddLineToPoint(c, triangle[1][0], triangle[1][1]);
CGContextAddLineToPoint(c, triangle[2][0], triangle[2][1]);
CGContextFillPath(c);

I was expecting that I make these points, then translate them to an origin, rotate, and then translate them back, I'd be laughing.

However, that's not what's happening...as the percentage increases from 0 to 2pi, the arrowhead draws itself in a vaguely triangular route. When the angDistance is zero or pi, the arrowhead is in the right place. As I head towards pi/2 or 3pi/2 though, the arrowhead heads off towards the lower corners of an enclosing rect.

I must be doing something blatantly stupid, but I can't for the life of me see it.

Any ideas?

Thanks,

-Ken

like image 583
Ken Corey Avatar asked Jan 28 '15 20:01

Ken Corey


1 Answers

I'd suggest constructing a path for the entire outline of the desired shape and then "fill" that path with the desired color. That eliminates any risk of any gaps or anything not quite lining up.

Thus, this path might consisting of an arc for the outside of the arrow, two lines for the head of the arrow, an arc back for the inside of the arrow, and then close the path. That might look like:

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, self.arrowColor.CGColor);

    CGPoint center = CGPointMake(rect.size.width / 2.0, rect.size.height / 2.0);

    CGContextMoveToPoint(context, center.x + cosf(self.startAngle) * (self.radius + self.lineWidth / 2.0),
                                  center.y + sinf(self.startAngle) * (self.radius + self.lineWidth / 2.0));

    CGContextAddArc(context, center.x, center.y, self.radius + self.lineWidth / 2.0, self.startAngle, self.endAngle, !self.clockwise);

    CGFloat theta = asinf(self.lineWidth / self.radius / 2.0) * (self.clockwise ? 1.0 : -1.0);
    CGFloat pointDistance = self.radius / cosf(theta);

    CGContextAddLineToPoint(context, center.x + cosf(self.endAngle + theta) * pointDistance,
                                     center.y + sinf(self.endAngle + theta) * pointDistance);

    CGContextAddLineToPoint(context, center.x + cosf(self.endAngle) * (self.radius - self.lineWidth / 2.0),
                                     center.y + sinf(self.endAngle) * (self.radius - self.lineWidth / 2.0));

    CGContextAddArc(context, center.x, center.y, self.radius - self.lineWidth / 2.0, self.endAngle, self.startAngle, self.clockwise);

    CGContextClosePath(context);

    CGContextDrawPath(context, kCGPathFill);
}

The only trick here was coming up with the right point for the end of the arrow. I've improved the choice to handle fatter arrows a little better, but you should feel free to use whatever you feel is best for your application.

Thus, the following code:

self.arrowView.radius = 100;
self.arrowView.arrowColor = [UIColor blueColor];
self.arrowView.lineWidth = 40;
self.arrowView.startAngle = -M_PI_2;
self.arrowView.endAngle = M_PI;
self.arrowView.clockwise = TRUE;

would yield the following (which I'm animating with a CADisplayLink):

arrow

This uses the start angle of zero as meaning the "3 o'clock" position, but you can obviously tweak this as you see fit. But hopefully it illustrates one approach to the problem.

By the way, while I've answered the question of how to do this with CoreGraphics, I wouldn't necessarily suggest doing so. For example, in https://github.com/robertmryan/CircularArrowDemo, I don't implement drawRect, but instead update a CAShapeLayer. By doing this, not only do I avoid drawRect inefficiencies, but one could theoretically also change how you use this CAShapeLayer (e.g. use it as a mask for some UIView, revealing some more interesting color gradation (or other image) behind it).

like image 116
Rob Avatar answered Oct 13 '22 09:10

Rob