Circle center of arc created by CGPathAddArc has inconsistent coordinates - ios

I have a custom UIControl that looks like:
The white ring is drawn in drawRect as follows:
//Get current context
CGContextRef mainContext = UIGraphicsGetCurrentContext();
/** Draw the Path **/
//Create the path
CGContextAddArc(mainContext, self.frame.size.width/2, self.frame.size.height/2, radius, 0, 2*M_PI, 0);
//Set the stroke color to white
[[UIColor whiteColor]setStroke];
//Define line width and cap
CGContextSetLineWidth(mainContext, HOUR_PICKER_BACKGROUND_WIDTH);
CGContextSetLineCap(mainContext, kCGLineCapButt);
//Draw the path
CGContextDrawPath(mainContext, kCGPathStroke);
The black circle is a CAShapeLayer and is drawn as follows:
hourSelector = [CAShapeLayer layer];
hourSelector.path = [UIBezierPath bezierPathWithRoundedRect:selectorPosition cornerRadius:11.0].CGPath;
hourSelector.fillColor = [UIColor colorWithRed:65.0f/255.0f green:75.0f/255.0f blue:86.0f/255.0f alpha:1.0f].CGColor;
hourSelector.strokeColor = [UIColor colorWithRed:65.0f/255.0f green:75.0f/255.0f blue:86.0f/255.0f alpha:1.0f].CGColor;
hourSelector.lineWidth = 1;
[self.layer addSublayer:hourSelector];
I have been able to implement the dragging of the black circle anywhere on the white ring. When I let the black circle go, I want to be able to animate the black circle to a certain position on the ring. When the finger is lifted off the circle, I have the value in radians of the ending touch position as well as the desired final position of the circles. I also have the points in rectangular/Cartesian coordinates. I'm trying to implement the desired behavior by drawing an arc between the two points and changing the position of the circle along the arc with a CAKeyFrameAnimation.
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.duration = 1.0;
CGMutablePathRef arcPath = CGPathCreateMutable();
CGPathAddArc(arcPath, NULL, self.frame.size.width/2, self.frame.size.height/2, radius, endingTouchRadian, finalPositionRadian,0);
pathAnimation.path = arcPath;
CGPathRelease(arcPath);
[hourSelector addAnimation:pathAnimation forKey:#"moveHour"];
My issue is that the arc created is nowhere on the white ring. self.frame.size.height (and the corresponding width) is the same circle center used to create the white ring but the arc is created off the screen. Furthermore, using 0,0 for x,y does not return the top left of the frame but the exact center of the main window. The center of the arc circle also seems to change every time I rotate the circle and let go.
Am I missing out on how to get the center of the arc created to be the same as the center of the white circle?

I figured it out. I was creating the CGPath outside drawRect which was redrawing the view with a different origin. I don't understand what exactly was going on underneath but moving the CGPath creation code to drawRect resolved the issue. Thanks to #Unheilig for pointing me in the right direction with his comment.

Wouldn't the center if your circle be
CGRectGetMidX(selectorPosition), CGRectGetMidY(selectorPosition)
You're using half the view's width and height, ignoring it's origin.

Related

How can we draw Circle progress as "three fourths" - 3/4?

Currently am working on a circular chart,There is no issue on drawing full circle. I also need to draw a 3/4 circle, Is there any specify math to define 3/4 circle path. I tried by reducing missed position values with full circle but cant get accurate result. Could anyone help with this? If any need I
UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width/2.0f, self.frame.size.height/2.0f)
radius:(self.frame.size.height * 0.5) - ([_lineWidth floatValue]/2.0f)
startAngle:DEGREES_TO_RADIANS(startAngle)
endAngle:DEGREES_TO_RADIANS(endAngle)
clockwise:clockwise];
circle = [CAShapeLayer layer];
circle.path = circlePath.CGPath;
circle.lineCap = kCALineCapRound;
circle.fillColor = [UIColor clearColor].CGColor;
circle.lineWidth = [_lineWidth floatValue];
circle.zPosition = 1; [self.layer addSublayer:circle];
;
Please refer image below,
Note: I have attached the sample image from web but am sure i need the same results.
You already have the code that you need. The function bezierPathWithArcCenter:radius:startAngle:endAngle:clockwise: takes a start angle and an end angle. If you pass in a start angle of 0 and an end angle of 3π/2 you'll get a 3/4 circle. (3π/2 is 270 degrees, or 3/4 of a full circle.)
Note that if your goal is to animate a circle from 0 to 360 degrees then you need to use a different technique. For that you want to create a path of the full circle, install that into a CAShapeLayer, then animate the layer's strokeEnd property from 0 to 1.

Border clipped when drawing with bezierPathWithOvalInRect

I am drawing a circle in my custom UIView's drawRect:
- (void)drawRect:(CGRect)rect {
...
UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect:rect];
[UIColor.whiteColor setStroke];
ovalPath.lineWidth = 1;
[ovalPath stroke];
}
The oval is always clipped on the edge. How can I avoid the clipping? Is insetting the rect the only way?
CG draws the stroke centered on the path -- half inside the path, half outside the path. Therefore, part of the stroke is outside of your view, and you don't see it.
Inset the rect by half the stroke width.
CGRect rectToStroke = CGRectInset(rect, 0.5, 0.5);
UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect: rectToStroke];
Anything you draw inside of -drawRect: using CG or UIKit goes into a bitmap context which is the size of your view's bounds.
If you need to show something bigger than bounds.size, you have two options: make the view's bounds bigger, or draw via some other method, such as:
add a subview which is bigger (but it will appear on top of your view, so you'll need to make it partially transparent)
add a CALayer or CAShapeLayer to your view's layer (with the same caveat)
set your view's layer's borderWidth to draw a border on top of the contents
(For all of these, you may find that you also need to set your view's clipsToBounds property to NO, if it isn't already.)

Attempting to mask a circle around an image not working

I have an image that I am attempting to mask a circle around so the image appears round. This somewhat works but the circle comes to a point on the top and bottom.
profileImageView.layer.cornerRadius = profileImageView.frame.size.width/2;
profileImageView.layer.masksToBounds = YES;
Should this code be drawing a perfect circle? It seems to draw a circle in one place but in two other places, its not working correctly.
I have had the best results masking the image view with a CAShapeLayer:
CGFloat radius = self.profileImageView.frame.size.width / 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius) radius:radius startAngle:0 endAngle:M_PI * 2.0 clockwise:TRUE];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;
layer.lineWidth = 0;
self.profileImageView.layer.mask = layer;
Should this code be drawing a perfect circle?
Not necessarily. After all, the width and the height of this layer might not be the same. And even if they are, dividing by 2 might not give you a radius that fits perfectly into an integral number of points as they are mapped to pixels on the screen.
It really would be better, if what you want is a mask that's a circle, to give this layer an actual mask that is an actual circle. Misusing the corner radius as you are doing is just lazy (and, as you've discovered, it's error-prone).

How to create the shape using UIBezier Path

I want to achieve the shape shown in image using UIBezier Path, and too the shape is filled with blocks in image it shows one block is filled, how to achieve this.
I have tried the following code taken from here
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, 10)];
[path addQuadCurveToPoint:CGPointMake(200, 10) controlPoint:CGPointMake(100, 5)];
[path addLineToPoint:CGPointMake(200, 0)];
[path addLineToPoint:CGPointMake(0, 0)];
[path closePath];
Thanks.
It looks to me like both the outline and also each block has the same shape. What you would probably do is to make one shape for the outline, and stroke it, and one shape for each cell and fill it.
Creating the shape
Each shape could be created something like this (as I've previously explained in this answer). It's done by stroking one path (the orange arc) which is a simple arc from one angle to another to get another path (the dashed outline)
Before we can stroke the path we to create it. CGPath's work just like UIBezierPath but with a C API. First we move to the start point, then we add an arc around the center from the one angle to another angle.
CGMutablePathRef arc = CGPathCreateMutable();
CGPathMoveToPoint(arc, NULL,
startPoint.x, startPoint.y);
CGPathAddArc(arc, NULL,
centerPoint.x, centerPoint.y,
radius,
startAngle,
endAngle,
YES);
Now that we have the centered arc, we can create one shape path by stroking it with a certain width. The resulting path is going to have the two straight lines (because we specify the "butt" line cap style) and the two arcs (inner and outer). As you saw in the image above, the stroke happens from the center an equal distance inwards and outwards.
CGFloat lineWidth = 10.0;
CGPathRef strokedArc =
CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
You would do this a couple of times to create one path for the stroked outline and one path for each cell.
Drawing the shape
Depending on if it's the outline or a cell you would either stroke it or fill it. You can either do this with Core Graphics inside drawRect: or with Core Animation using CAShapeLayers. Choose one and don't between them :)
Core Graphics
When using Core Graphics (inside drawRect:) you get the graphics context, configure the colors on it and then stroke the path. For example, the outline with a gray fill color and a black stroke color would look like this:
I know that your shape is filled white (or maybe it's clear) with a light blue stroke but I already had a gray and black image and I didn't want to create a new one ;)
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc); // the path we created above
CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor);
CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor);
CGContextDrawPath(c, kCGPathFillStroke); // both fill and stroke
That will put something like this on screen
Core Animation
The same drawing could be done with a shape layer like this:
CAShapeLayer *outline = [CAShapeLayer layer];
outline.fillColor = [UIColor lightGrayColor].CGColor;
outline.strokeColor = [UIColor blackColor].CGColor;
outline.lineWidth = 1.0;
outline.path = strokedArc; // the path we created above
[self.view.layer addSublayer: outline];

keep unmasking image on mask animation

I am trying to make an effect where at first the entire screen is masked out. As a ball moves across the screen, the ball unmasks the area that it is in AND the areas that it WAS in remain unmasked.
I have the following code:
CALayer * ball = [CALayer layer];
ball.bounds = CGRectMake(0, 0, 42, 42);
ball.position = [[[alphabet controls] objectAtIndex:0] CGPointValue];
ball.contents = (id)([UIImage imageNamed:#"done.png"].CGImage);
[self.layer addSublayer:ball];
[self.layer setMask:ball];
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
anim.path = path;
anim.repeatCount = HUGE_VALF;
anim.duration = 8.0;
[ball addAnimation:anim forKey:#"race"];
This animation masks the entire view and shows only what is behind the ball layer.
My question is : How can I keep unmasked the parts of the screen that were revealed?
Hmmm.
What you want is an image that contains all the pixels that the ball shape traveled through.
If you were animating the ball with frame-based animation, you could create a grayscale (or 1-bit) image and install that as the content of your mask layer. Then, as you moved the ball, you could draw it into your mask image with each frame.
I'm not sure how to get the same effect with Core animation.
You could make your mask a CAShapeLayer, create a CGPath that describes the entire path of your ball, and make that the path for your shape layer. If your ball is round, you could set the line thickness of the shape layer to your ball size. That would work. If you ball is an irregular shape, though, that approach wouldn't work. Quartz graphics on iOS doesn't have any way I know of to stroke a path with an arbitrary-shaped brush.

Resources