How to draw an elliptical arc with CoreGraphics? - core-graphics

Is it possible to draw an elliptical arc like SVG path, in CoreGraphics and how?

I ran into the same thing tonight. CG doesn't offer an easy way to draw a non-circular arc, but you can do it using CGPath and a suitable transformation matrix. Suppose you want an arc of an axis-aligned ellipse starting at left,top and with a size of width,height. Then you can do something like this:
CGFloat cx = left + width*0.5;
CGFloat cy = top + height*0.5;
CGFloat r = width*0.5;
CGMutablePathRef path = CGPathCreateMutable();
CGAffineTransform t = CGAffineTransformMakeTranslation(cx, cy);
t = CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, height/width), t);
CGPathAddArc(path, &t, 0, 0, r, startAngle, endAngle, false);
CGContextAddPath(g->cg, path);
CGContextStrokePath(g);
CFRelease(path);
Note that if you want to draw a pie-shaped wedge, then just surround the "CGContextAddPath" call with a CGContextMoveToPoint(cx,cy) and a CGContextAddLineToPoint(cx,cy), and use CGContextFillPath instead of CGContextStrokePath. (Or if you want to fill and stroke at the same time, use CGContextDrawPath.)

Related

Drawing a Speedometer with Core Graphics on OSX in NSView

I'm trying draw elements of a Speed Gauge using Core Graphics on OSX. I've almost got it but need a little bit of help on the center ticks inside of the gauge. Here is the image of what I'm trying to do:
Here is an image of what I've got so far:
I know how to draw the circle rings and how to draw segments based around the center of the gauge like this:
- (void)drawOuterGaugeRingsInRect:(CGContextRef)contextRef rect:(NSRect)rect {
CGContextSetLineWidth(contextRef,self.gaugeRingWidth);
CGContextSetStrokeColorWithColor(contextRef, [MyColors SpeedGaugeOuterRingGray].CGColor);
CGFloat startRadians = 0;
CGFloat endRadians = M_PI*2;
CGFloat radius = self.bounds.size.width/2 - 5;
CGContextAddArc(contextRef, CGRectGetMidX(rect),CGRectGetMidY(rect),radius,startRadians,endRadians,YES);
//Render the outer gauge
CGContextStrokePath(contextRef);
//Draw the inner gauge ring.
radius -= self.gaugeRingWidth;
CGContextSetStrokeColorWithColor(contextRef, [MyColors SpeedGaugeInnerRingGray].CGColor);
CGContextAddArc(contextRef, CGRectGetMidX(rect),CGRectGetMidY(rect),radius,startRadians,endRadians,YES);
//Render the inner gauge
CGContextStrokePath(contextRef);
radius -= self.gaugeRingWidth;
//Draw and fill the gauge background
CGContextSetFillColorWithColor(contextRef, [MyColors SpeedGaugeCenterFillBlack ].CGColor);
CGContextSetStrokeColorWithColor(contextRef, [MyColors SpeedGaugeCenterFillBlack].CGColor);
CGContextAddArc(contextRef, CGRectGetMidX(rect),CGRectGetMidY(rect),radius,startRadians,endRadians,YES);
//Render and fill the gauge background
CGContextDrawPath(contextRef, kCGPathFillStroke);
/*BLUE CIRCULAR DIAL */
//Prepare to draw the blue circular dial.
radius -= self.gaugeRingWidth/2;
//Adjust gauge ring width
CGContextSetLineWidth(contextRef,self.gaugeRingWidth/2);
CGContextSetStrokeColorWithColor(contextRef, [MyColors SpeedGaugeBlue].CGColor);
CGFloat startingRadians = [MyMathHelper degressToRadians:135];
CGFloat endingRadians = [MyMathHelper degressToRadians:45];
CGContextAddArc(contextRef, CGRectGetMidX(rect),CGRectGetMidY(rect),radius,startingRadians,endingRadians,NO);
//Render the blue gauge line
CGContextStrokePath(contextRef);
}
The code above is called in the drawRect: method in my NSView
The key section is the code here:
- (void)drawInnerDividerLines:(CGContextRef)context rect:(NSRect)rect {
CGFloat centerX = CGRectGetMidX(rect);
CGFloat centerY = CGRectGetMidY(rect);
CGContextSetLineWidth (context, 3.0);
CGContextSetRGBStrokeColor (context, 37.0/255.0, 204.0/255.0, 227.0/255.0, 0.5);
CGFloat destinationX = centerX + (centerY * (cos((135)*(M_PI/180))));
CGFloat destinationY = centerY + (centerX * (sin((135)*(M_PI/180))));
NSPoint destinationPoint = NSMakePoint(destinationX, destinationY);
CGContextMoveToPoint(context, centerX, centerY);
CGContextAddLineToPoint(context, destinationPoint.x, destinationPoint.y);
CGContextStrokePath(context);
}
I understand what is going on here but the problem I'm trying to solve is drawing the little lines, off of the inner blue line that extend toward the center point of the View, but do not draw all the way to the center. I'm a little unsure on how to modify the math and drawing logic to achieve this. Here is the unit circle I based the angles off of for Core Graphics Drawing.
The main problems I'm trying to solve are:
How to define the proper starting point off of the light blue inner line as a staring point for each gauge tick. Right now, I'm drawing the full line from the center to the edge of the gauge.
How to control the length of the tick gauge as it draws pointed toward the center off of it's origin point on the blue line.
Any tips or advice that would point in me in the right direction to solve this would be appreciated.
I recommend using vectors. You can find a line to any point on the circle given an angle by calculating:
dirX = cos(angle);
dirY = sin(angle);
startPt.x = center.x + innerRadius * dirX;
startPt.y = center.y + innerRadius * dirY;
endPt.x = center.x + outerRadius * dirX;
endPt.y = center.y + outerRadius * dirY;
You can then plot a line between startPt and endPt.
Any tips or advice that would point in me in the right direction to solve this would be appreciated.
Given a point on the circumference of your circle at a certain angle around the centre you can form a right angled triangle, the radius is the hypotenuse, and the other two sides being parallel to the x & y axes (ignore for a moment the degenerate case where the point is at 0, 90, 180 or 270 deg). Given that with the sin & cos formula (remember SOHCAHTOA from school) and some basic math you can calculate the coordinates of the point, and using that draw a radius from the centre to the point.
The end points of a "tick" mark just lie on circles of different radii, so the same math will give you the end points and you can draw the tick. You just need to decide the radii of these circles, i.e. the distance along your original radius the end points of the tick should be.
HTH
Another approach to avoid the trigonometry is to rotate the transformation matrix and just draw a vertical or horizontal line.
// A vertical "line" of width "width" along the y axis at x==0.
NSRect tick = NSMakeRect(-width / 2.0, innerRadius, width, outerRadius - innerRadius);
NSAffineTransform* xform = [NSAffineTransform transform];
// Move the x and y axes to the center of your speedometer so rotation happens around it
[xform translateXBy:center.x yBy:center.y];
// Rotate the coordinate system so that straight up is actually at your speedometer's 0
[xform rotateByDegrees:135];
[xform concat];
// Create a new transform to rotate back around for each tick.
xform = [NSAffineTransform transform];
[xform rotateByDegrees:-270.0 / numTicks];
for (int i = 0; i < numTicks; i++)
{
NSRectFill(tick);
[xform concat];
}
You probably want to wrap this in [NSGraphicsContext saveGraphicsState] and [NSGraphicsContext restoreGraphicsState] so the transformation matrix is restored when you're done.
If you want two different kinds of tick marks (major and minor), then have two different rects and select one based on i % 10 == 0 or whatever. Maybe also toggle the color. Etc.

What is wrong with this way of using CGAffineTransform?

I want to make a graph in a UIView that shows numerical data. So I need to draw axis, a few coordinate lines, some tick marks, and then the data as connected straight lines. The data might typically consist of a few hundred x values in the range -500. to +1000. with corresponding y values in the range 300. to 350.
So I thought a good approach would be to transform the coordinates of the UIView so (for the example values given) the left side of the view is -500, and right side is 1000, the top is 400 and the bottom is 300. And y increases upwards. Then in drawRect: I could write a bunch of CGContextMoveToPoint() and CGContextAddLineToPoint() statements with my own coordinate system and not have to mentally translate each call to the UIView coordinates.
I wrote the following function to generate my own CGContextRef but it doesn't do what I expected. I've been trying variations on it for a couple days now and wasting so much time. Can someone say how to fix it? I realize I can't get clear in my mind whether the transform is supposed to specify the UIView coordinates in terms of my coordinates, or vice versa, or something else entirely.
static inline CGContextRef myCTX(CGRect rect, CGFloat xLeft, CGFloat xRight, CGFloat yBottom, CGFloat yTop) {
CGAffineTransform ctxTranslate = CGAffineTransformMakeTranslation(xLeft, rect.size.height - yTop);
CGAffineTransform ctxScale = CGAffineTransformMakeScale( rect.size.width / (xRight - xLeft), -rect.size.height / (yTop - yBottom) ); //minus so y increases toward top
CGAffineTransform combinedTransform = CGAffineTransformConcat(ctxTranslate, ctxScale);
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextConcatCTM(c, combinedTransform);
return c;
}
The way I'm using this is that inside drawRect I just have:
CGContextRef ctx = myCTX(rect, self.xLeft, self.xRight, self.yBottom, self.yTop);
and then a series of statements like:
CGContextAddLineToPoint(ctx, [x[i] floatValue], [y[i] floatValue]);
I figured this out by experimenting. The transform requires 3 steps instead of 2 (or, if not required, at least it works this way):
static inline CGContextRef myCTX(CGRect rect, CGFloat xLeft, CGFloat xRight, CGFloat yBottom, CGFloat yTop) {
CGAffineTransform translate1 = CGAffineTransformMakeTranslation(-xLeft, -yBottom);
CGAffineTransform scale = CGAffineTransformMakeScale( rect.size.width / (xRight - xLeft), -rect.size.height / (yTop - yBottom) );
CGAffineTransform transform = CGAffineTransformConcat(translate1, scale);
CGAffineTransform translate2 = CGAffineTransformMakeTranslation(1, rect.size.height);
transform = CGAffineTransformConcat(transform, translate2);
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextConcatCTM(c, transform);
return c;
}
You use this function inside drawRect. In my case the xLeft, xRight, etc. values are properties of a UIView subclass and are set by the viewController. So the view's drawRect looks like so:
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSaveGState(c);
CGContextRef ctx = myCTX(rect, self.xLeft, self.xRight, self.yBottom, self.yTop);
…
all of the CGContextMoveToPoint(), CGContextAddLineToPoint(), calls to
draw your desired lines, rectangles, curves, etc. but not stroke or fill them
…
CGContextRestoreGState(c);
CGContextSetLineWidth(c, 1);
CGContextStrokePath(c);
}
The CGContextSetLineWidth call isn't needed if you want a line width of 1. If you don't restore the graphics state before strokePath the path width is affected by the scaling.
Now I have to figure out how to draw text onto the view.

Clipping UIView to a circle using CGContextClip

I've looked through various similar questions and still can't seem to get my code to work. I have a UIView with an image drawn to it ([image drawInRect:bounds]), but I'm missing something in my context clipping:
// Get context & bounds, and calculate centre & radius
CGContextRef ctx=UIGraphicsGetCurrentContext();
CGRect bounds=[self bounds];
CGPoint centre;
centre.x=bounds.origin.x+0.5*bounds.size.width;
centre.y=bounds.origin.y+0.5*bounds.size.height;
CGFloat radius=centre.x;
// Draw image
UIImage *backImage=[UIImage imageNamed:#"backimage.png"];
[backImage drawInRect:bounds];
// Create clipping path and clip context
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, NULL, centre.x, centre.y, radius, 0, 2*M_PI, 0);
CGContextAddPath(ctx, path);
CGContextClip(ctx);
Any ideas of where I went wrong? Thanks for reading.
radius=centre.x seems to be wrong. The radius should be half the width or height:
CGFloat radius = 0.5*bounds.size.width;
You could also use the convenience function
CGPathRef path = CGPathCreateWithEllipseInRect(bounds, NULL);
UPDATE: It turned out that the actual problem was that the clipping path was modified after drawing the image.
The clipping path is used for all future drawing operations and must therefore be set before the drawing.

iOS CoreGraphics: Draw arc, determine arc angles from intersecting chord theorem

I'm trying to figure out how to draw an arc in CoreGraphics. I understand which method calls to make and how to compute the angles in the following scenario.
----------
| |
*--------*
When the points are both in the bottom of the rect. However when two points are in other locations, I don't know how to calculate the correct angle.
---------*
| |
*---------
See bottom portion of my image.
Ray Wenderlich has a great tutorial about creating arcs for only in the first mentioned point positions.
// sample code for creating arc for path from bottom of rect
CGMutablePathRef createArcPathFromBottomOfRect(CGRect rect, CGFloat arcHeight) {
CGRect arcRect = CGRectMake(rect.origin.x, rect.origin.y + rect.size.height
- arcHeight, rect.size.width, arcHeight);
CGFloat arcRadius = (arcRect.size.height/2) + (pow(arcRect.size.width, 2) /
(8 * arcRect.size.height));
CGPoint arcCenter = CGPointMake(arcRect.origin.x + arc.size.width/2,
arcRect.origin.y + arcRadius);
CGFloat angle = acos(arcRect.size.width/ (2*arcRadius));
CGFloat startAngle = radians(180) + angle;
CGFloat endAngle = radians(360) - angle;
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, NULL, arcCenter.x, arcCenter.y, arcRadius, startAngle,
endAngle, 0);
return path;
}
How do I calculate the angle when in other situations as depicted at the bottom of my image?
I find an easier way to make arcs is to use:
void CGContextAddArcToPoint (
CGContextRef c,
CGFloat x1,
CGFloat y1,
CGFloat x2,
CGFloat y2,
CGFloat radius
);
If you look at this image from Ray Wenderlich's site (https://www.raywenderlich.com/33330/core-graphics-tutorial-glossy-buttons), point (x1,y1) is your start point for the curve and point (x2,y2) is your end point. Then just specify the corner radius and voila! It looks like this may be an easier API to use for what you are looking to do.
You need at least 3 points to determine a circle.
In your first senario where two points are at the bottom of a rect, the top middle point is implicitly the third point when arcHeight is known. Therefore the three points determined the circle, thus the arc. So all angles and etc can be calculated.
In you second senario however, no third point is defined. Therefore you can draw infinite number of arcs passing through the two points with different curvatures. You will need additional requirements to fixed an arc. For example the radius or a third point supposed to be one the arc.

Simple way of using irregular shaped buttons

I've finally got my main app release (Tap Play MMO - check it out ;-) ) and I'm now working on expanding it.
To do this I need to have a circle that has four seperate buttons in it, these buttons will essentially be quarters. I've come to the conclusion that the circlular image will need to be constructed of four images, one for each quarter, but due to the necessity of rectangular image shapes I'm going to end up with some overlap, although the overlap will be transparent.
What's the best way of getting this to work? I need something really simple really, I've looked at this
http://iphonedevelopment.blogspot.com/2010/03/irregularly-shaped-uibuttons.html
Before but not yet succeeded in getting it to work. Anyone able to offer some advice?
In case it makes any difference I'll be deploying to a iOS 3.X framework (will be 4.2 down the line when 4.2 comes out for iPad)
Skip the buttons and simply respond to touches in your view that contains the circle.
Create a CGPath for each area that you want to capture touches, when your UIview receives a touch, check for membership inside the paths.
[Edited answer to show skeleton implementation details -- TomH]
Here's how I would approach the problem: (I haven't tested this code and the syntax may not be quite right, but this is the general idea)
1) Using PS or your favorite image creation application, create one png of the quarter circles. Add it to your XCode project.
2) Add a UIView to the UI. Set the UIView's layer's contents to the png.
self.myView = [[UIView alloc] initWithRect:CGRectMake(10.0, 10.0, 100.0, 100,0)];
[myView.layer setContents:(id)[UIImage loadImageNamed:#"my.png"]];
3) Create CGPaths that describe the region in the UIView that you are interested in.
self.quadrantOnePath = CGPathCreateMutable();
CGPathMoveToPoint(self.quadrantOnePath, NULL, 50.0, 50.0);
CGPathAddLineToPoint(self.quadrantOnePath, NULL, 100.0, 50.0);
CGPathAddArc(self.quadrantOnePath, NULL, 50.0, 50.0, 50.0, 0.0, M_PI2, 1);
CGPathCloseSubpath(self.quadrantOnePath);
// create paths for the other 3 circle quadrants too!
4) Add a UIGestureRecognizer and listen/observe for taps in the view
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
[tapRecognizer setNumberOfTapsRequired:2]; // default is 1
5) When tapRecognizer invokes its target selector
- (void)handleGesture:(UIGestureRecognizer *) recognizer {
CGPoint touchPoint = [recognizer locationOfTouch:0 inView:self.myView];
bool processTouch = CGPathContainsPoint(self.quadrantOnePath, NULL, touchPoint, true);
if(processTouch) {
// call your method to process the touch
}
}
Don't forget to release everything when appropriate -- use CGPathRelease to release paths.
Another thought: If the graphic that you are using to represent your circle quadrants is simply a filled color (i.e. no fancy graphics, layer effects, etc.), you could also use the paths you created in the UIView's drawRect method to draw the quadrants too. This would address one of the failings of the approach above: there isn't a tight integration between the graphic and the paths used to check for the touches. That is, if you swap out the graphic for something different, change the size of the graphic, etc., your paths used to check for touches will be out of sync. Potentially a high maintenance piece of code.
I can't see, why overlapping is needed.
Just create 4 buttons and give each one a slice of your image.
edit after comment
see this great project. One example is exactly what you want to do.
It works by incorporating the alpha-value of a pixel in the overwritten
pointInside:withEvent: and a category on UIImage, that adds this method
- (UIColor *)colorAtPixel:(CGPoint)point {
// Cancel if point is outside image coordinates
if (!CGRectContainsPoint(CGRectMake(0.0f, 0.0f, self.size.width, self.size.height), point)) {
return nil;
}
// Create a 1x1 pixel byte array and bitmap context to draw the pixel into.
// Reference: http://stackoverflow.com/questions/1042830/retrieving-a-pixel-alpha-value-for-a-uiimage
NSInteger pointX = trunc(point.x);
NSInteger pointY = trunc(point.y);
CGImageRef cgImage = self.CGImage;
NSUInteger width = self.size.width;
NSUInteger height = self.size.height;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
int bytesPerPixel = 4;
int bytesPerRow = bytesPerPixel * 1;
NSUInteger bitsPerComponent = 8;
unsigned char pixelData[4] = { 0, 0, 0, 0 };
CGContextRef context = CGBitmapContextCreate(pixelData,
1,
1,
bitsPerComponent,
bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGColorSpaceRelease(colorSpace);
CGContextSetBlendMode(context, kCGBlendModeCopy);
// Draw the pixel we are interested in onto the bitmap context
CGContextTranslateCTM(context, -pointX, pointY-(CGFloat)height);
CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, (CGFloat)width, (CGFloat)height), cgImage);
CGContextRelease(context);
// Convert color values [0..255] to floats [0.0..1.0]
CGFloat red = (CGFloat)pixelData[0] / 255.0f;
CGFloat green = (CGFloat)pixelData[1] / 255.0f;
CGFloat blue = (CGFloat)pixelData[2] / 255.0f;
CGFloat alpha = (CGFloat)pixelData[3] / 255.0f;
return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
}
Here's an awesome project that solves the problem of irregular shaped buttons so easily:
http://christinemorris.com/2011/06/ios-irregular-shaped-buttons/