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

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.

Related

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).

Circle center of arc created by CGPathAddArc has inconsistent coordinates

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.

Can I add a custom line cap to a UIBezierPath?

I'm drawing an arc by creating a CAShapeLayer and giving it a Bezier path like so:
self.arcLayer = [CAShapeLayer layer];
UIBezierPath *remainingLayerPath = [UIBezierPath bezierPathWithArcCenter:self.center
radius:100
startAngle:DEGREES_TO_RADIANS(135)
endAngle:DEGREES_TO_RADIANS(45)
clockwise:YES];
self.arcLayer.path = remainingLayerPath.CGPath;
self.arcLayer.position = CGPointMake(0,0);
self.arcLayer.fillColor = [UIColor clearColor].CGColor;
self.arcLayer.strokeColor = [UIColor blueColor].CGColor;
self.arcLayer.lineWidth = 15;
This all works well, and I can easily animate the arc from one side to the other. As it stands, this gives a very squared edge to the ends of my lines. Can I round the edges of these line caps with a custom radius, like 3 (one third the line width)? I have played with the lineCap property, but the only real options seem to be completely squared or rounded with a larger corner radius than I want. I also tried the cornerRadius property on the layer, but it didn't seem to have any effect (I assume because the line caps are not treated as actual layer corners).
I can only think of two real options and I'm not excited about either of them. I can come up with a completely custom Bezier path tracing the outside of the arc, complete with my custom rounded edges. I'm concerned however about being able to animate the arc in the same fashion (right now I'm just animating the stroke from 0 to 1). The other option is to leave the end caps square and mask the corners, but my understanding is that masking is relatively expensive, and I'm planning on doing some fairly intensive animations with this view.
Any suggestions would be helpful. Thanks in advance.
I ended up solving this by creating two completely separate layers, one for the left end cap and one for the right end cap. Here's the right end cap example:
self.rightEndCapLayer = [CAShapeLayer layer];
CGRect rightCapRect = CGRectMake(remainingLayerPath.currentPoint.x, remainingLayerPath.currentPoint.y, 0, 0);
rightCapRect = CGRectInset(rightCapRect, self.arcWidth / -2, -1 * endCapRadius);
self.rightEndCapLayer.frame = rightCapRect;
self.rightEndCapLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.rightEndCapLayer.bounds
byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight
cornerRadii:CGSizeMake(endCapRadius, endCapRadius)].CGPath;
self.rightEndCapLayer.fillColor = self.remainingColor.CGColor;
// Rotate the end cap
self.rightEndCapLayer.anchorPoint = CGPointMake(.5, 0);
self.rightEndCapLayer.transform = CATransform3DMakeRotation(DEGREES_TO_RADIANS(45), 0.0, 0.0, 1.0);
[self.layer addSublayer:self.rightEndCapLayer];
Using the bezier path's current point saves from doing a lot of math to calculate where the end point should appear. Moving the anchoring point also allows the layers to not overlap, which is important if your arc is at all transparent.
This still isn't entirely ideal, as animations have to be chained through multiple layers. It's better than the alternatives I could come up with though.

Animating a circular UIBezierPath

I've got a project where I'm animating a UIBezierPath based on a set progress. The BezierPath is in the shape of a circle and lies in a UIView and animation is done in drawRect using CADisplayLink right now. Simply put, based on a set progress x the path should radially extend (if xis larger than before) or shrink (if x is smaller).
self.drawProgress = (self.displayLink.timestamp - self.startTime)/DURATION;
CGFloat startAngle = -(float)M_PI_2;
CGFloat stopAngle = ((self.x * 2*(float)M_PI) + startAngle);
CGFloat currentEndAngle = ((self.oldX * 2*(float)M_PI) + startAngle);
CGFloat endAngle = currentEndAngle-((currentEndAngle-stopAngle)*drawProgress);
UIBezierPath *guideCirclePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
This is in the case of x shrinking since our last update. The issues I'm experiencing are actually a few:
The shape always starts drawing at 45º (unless I rotate the view). I have not found any way to change this, and setting the startAngleto -45º makes no difference really because it always "pops" to 45. Is there anything I can do about this, or do I have to resort to other methods of drawing?
Is there any other way that one should animate these things? I've read much about using CAShapeLayer but I haven't quite understood the actual difference (in terms of drawbacks and benefits) in using these two methods. If anyone could clarify I would be very much obliged!
UPDATE: I migrated the code over to CAShapeLayer instead, but now I'm facing a different issue. It's best described with this image:
What's happening is that when the layer is supposed to shrink, the thin outer line is still there (regardless of direction of movement). And when the bar shrinks, the delta of 1-xisn't removed unless I explicitly make a new white shape over it. The code for this follows. Any ideas?
UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:stopAngle clockwise:YES];
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [circlePath CGPath];
circle.strokeStart = 0;
circle.strokeEnd = 1.0*self.progress;
// Colour and other customizations here.
if (self.progress > self.oldProgress) {
drawAnimation.fromValue = #(1.0*self.oldProgress);
drawAnimation.toValue = #(circle.strokeEnd);
} else {
drawAnimation.fromValue = #(1.0*self.oldProgress);
drawAnimation.toValue = #(1.0*self.progress);
circle.strokeEnd = 1.0*self.progress;
}
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; //kCAMediaTimingFunctionEaseIn
[circle addAnimation:drawAnimation forKey:#"strokeEnd"];
UPDATE 2: I've ironed out most of the other bugs. Turned out it was just me being rather silly the whole time and overcomplicating the whole animation (not to mention multiplying by 1 everywhere, what?). I've made a gif of the bug I can't solve:
Any ideas?
UPDATE 3: (and closure). I managed to get rid of the bug by calling
[self.layer.sublayers makeObjectsPerformSelector:#selector(removeFromSuperlayer)];
And now everything works as it should. Thanks for all the help!
Using CAShapeLayer is much easier and cleaner. The reason is that CAShapeLayer includes properties strokeStart and strokeEnd. These values range from 0 (the beginning of the path) to 1 (the end of the path) and are animatable.
By changing them you can easily draw any arc of your circle (or any part of an arbitrary path, for that matter.) The properties are animatable, so you can create an animation of a growing/shrinking pie slice or section of a ring shape. It's much easier and more performant than implementing code in drawRect.

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