Drawing a CGPath UIBezierCurve - objective-c

Hi all I am looking into ways of how I can draw a shape like the one in the illustration above.I have been looking and reading but getting slightly confused of how curves are drawn using UIBezierPath. I found really nice code which uses CAShapeLayer with animation to draw lines.
The code so far I have is :
#synthesize animationLayer = _animationLayer;
#synthesize pathLayer = _pathLayer;
#synthesize penLayer = _penLayer;
- (void) setupDrawingLayer
{
if (self.pathLayer != nil) {
[self.penLayer removeFromSuperlayer];
[self.pathLayer removeFromSuperlayer];
self.pathLayer = nil;
self.penLayer = nil;
}
CGPoint upperCurve = CGPointMake(101, 100);
CGPoint lowerCurve = CGPointMake(224,200);
UIBezierPath *path = [UIBezierPath bezierPath];
path.lineCapStyle = kCGLineCapRound;
path.miterLimit = -10.0f;
path.lineWidth = 10.0f;
[path moveToPoint:lowerCurve];
[path addQuadCurveToPoint:upperCurve controlPoint:lowerCurve];
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.frame = self.animationLayer.bounds;
pathLayer.path = path.CGPath;
pathLayer.strokeColor = [[UIColor blackColor] CGColor];
pathLayer.fillColor = nil;
pathLayer.lineWidth = 10.0f;
pathLayer.lineJoin = kCALineJoinBevel;
[self.animationLayer addSublayer:pathLayer];
self.pathLayer = pathLayer;
}
-(void) startAnimation
{
[self.pathLayer removeAllAnimations];
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 10.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.animationLayer = [CALayer layer];
self.animationLayer.frame = CGRectMake(20.0f, 64.0f,
CGRectGetWidth(self.view.layer.bounds) - 40.0f,
CGRectGetHeight(self.view.layer.bounds) - 84.0f);
[self.view.layer addSublayer:self.animationLayer];
[self setupDrawingLayer];
[self startAnimation];
}

The way I solve this kind of problem is to draw the shape in a drawing program such as Illustrator. This shows clearly where the bezier curve points need to go in order to get the curve I'm after.

A UIBezierPath generally starts with moveToPoint to set the starting point of the curve. Then it's followed by any number of curve segments using these methods:
– addLineToPoint:
– addArcWithCenter:radius:startAngle:endAngle:clockwise:
– addCurveToPoint:controlPoint1:controlPoint2:
– addQuadCurveToPoint:controlPoint:
You didn't state specifically what you're having trouble with, so I'm going to make a leap and assume it is addCurveToPoint:controlPoint1:controlPoint2 that you are struggling with.
The segment added by this call draws a segment starting at the most recently added or moved to point in the curve to the first argument. How it undulates is determined by the control points.
The simplest way to understand how it undulates is to imagine lines connecting the first point (established in the previous method call) to the first control point (let's call this control point line segment 1), and another line connecting the first argument (ending point of the segment being added) to the second control point (let's call this control point line segment 2).
The Bezier curve at the starting point is tangent to control point line segment 1. It is tangent to control point line segment 2 at the end of the curve.
So if you want to draw a curve with multiple Beziers such that they form a smooth line, you need to make sure that the slope of control point line segment 2 of one curve is the same as the slope of control point line segment 1 of the next curve that joins to it.
The distances from the starting point to the first control point, and the ending point to the second control point determine the curvature of the curve.
A friend of mine imagines it this way. Imagine a space ship traveling from point A to point B. The space ship starts out with a heading determined by the slope of control point line segment 1 and a speed determined by its length. The heading is gradually changed to the slope of control point line segment 2. Meanwhile, the speed is gradually changed to the length of of control point line segment 2. By the time the space ship arrives at point B, it is traveling tangent to that segment.

Related

UIBezierPath of a circle and rectangle intersection for a mask

I am trying to create a mask region of the intersection of a circle and a rectangle.
I am starting with this code that seems to create an XOR of the circle and rectangle for the mask region but I want just a plain old AND:
- (void)addMaskToHoleViewAtX:(CGFloat) x AtY:(CGFloat) y Radius:(CGFloat) kRadius {
CGRect bounds = holeView.bounds;
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = bounds;
maskLayer.fillColor = [UIColor blackColor].CGColor;
CGRect const rect = CGRectMake(CGRectGetMidX(bounds) - kRadius/2,
CGRectGetMidY(bounds) - kRadius,
kRadius,
2 * kRadius);
UIBezierPath *pathrect = [UIBezierPath bezierPathWithRect:rect];
CGRect const circ = CGRectMake(CGRectGetMidX(bounds) - kRadius,
CGRectGetMidY(bounds) - kRadius,
2 * kRadius,
2 * kRadius);
UIBezierPath *pathcirc= [UIBezierPath bezierPathWithOvalInRect:circ];
UIBezierPath *allPaths= [[UIBezierPath alloc] init];
[allPaths appendPath:pathrect];
[allPaths appendPath:pathcirc];
[allPaths appendPath:[UIBezierPath bezierPathWithRect:bounds]];
maskLayer.path = allPaths.CGPath;
maskLayer.fillRule = kCAFillRuleEvenOdd;
holeView.layer.mask = maskLayer;
holeView.center = CGPointMake(x, y);
}
Could someone help me with the syntax to do the AND, I think I might need to use addClip but it is not obvious to me how to do that with the above code?
MY SOLUTION: It appears to me that if I were able to figure how to use addClip to solve this problem in one manner, I would not actually end up with the closed NSBezierPath of the intersection. I prefer not to do it that way as having the intersection NSBezierPath is also needed to easily determine if a point is inside the path. SOOOO, I just created the NSBezierPath of the intersection through calculations and used my derived path to append to the masklayer bounds path. It sure would be nice to have a way of actually obtaining the intersection NSBezierPath without calculations but I just had to move on. Thanks for trying.
Thanks,
Carmen
EDIT : Here is the routine I am calling to put the 'intersection' mask over my _map View. Change _map to your view if you want to try this:
- (void)addHoleSubview {
holeView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10000, 10000)];
holeView.backgroundColor = [UIColor colorWithRed:255 green:0 blue:0 alpha:0.2];
holeView.autoresizingMask = 0;
[_map addSubview:holeView];
[self addMaskToHoleViewAtX:100 AtY:100 Radius:50];
}
If you aim for the intersection of two paths you should not create a compound path. Appending a path to another path will
result in a union of both paths if using the non-zero winding number rule and both paths have the same direction - or in other words it will have the effect of an AND
result in a shape that contains only the parts of the areas surrounded by the two paths that do not overlap if using the even-odd rule - or in other words it will have the effect of an XOR.
Instead I suggest you first add the first path pathrect to your graphics context and clip
[pathrect addClip];
and then you add the second path pathcirc to your context and clip again:
[pathcirc addClip];
You can now use any filling rule within that context and it will fill the intersection (the AND) of the two paths.

Is it possible to use a circle (SKShapeNode) as a mask in Sprite Kit?

I'm trying to create a circular mask in a Sprite Kit project. I create the circle like this (positioning it at the centre of the screen):
SKCropNode *cropNode = [[SKCropNode alloc] init];
SKShapeNode *circleMask = [[SKShapeNode alloc ]init];
CGMutablePathRef circle = CGPathCreateMutable();
CGPathAddArc(circle, NULL, CGRectGetMidX(self.frame), CGRectGetMidY(self.frame), 50, 0, M_PI*2, YES);
circleMask.path = circle;
circleMask.lineWidth = 0;
circleMask.fillColor = [SKColor blueColor];
circleMask.name=#"circleMask";
and further down the code, I set it as the mask for the cropNode:
[cropNode setMaskNode:circleMask];
... but instead of the content showing inside a circle, the mask appears as a square.
Is it possible to use a SKShapeNode as a mask, or do I need to use an image?
After much swearing, scouring the web, and experimentation in Xcode, I have a really hacky fix.
Keep in mind that this is a really nasty hack - but you can blame that on Sprite Kit's implementation of SKShapeNode. Adding a fill to a path causes the following:
adds an extra node to your scene
the path becomes unmaskable - it appears 'over' the mask
makes any non-SKSpriteNode sibling nodes unmaskable (e.g. SKLabelNodes)
Not an ideal state of affairs.
Inspired by Tony Chamblee's progress timer, the 'fix' is to dispense with the fill altogether, and just use the stroke of the path:
SKCropNode *cropNode = [[SKCropNode alloc] init];
SKShapeNode *circleMask = [[SKShapeNode alloc ]init];
CGMutablePathRef circle = CGPathCreateMutable();
CGPathAddArc(circle, NULL, CGRectGetMidX(self.frame), CGRectGetMidY(self.frame), 50, 0, M_PI*2, YES); // replace 50 with HALF the desired radius of the circle
circleMask.path = circle;
circleMask.lineWidth = 100; // replace 100 with DOUBLE the desired radius of the circle
circleMask.strokeColor = [SKColor whiteColor];
circleMask.name=#"circleMask";
[cropNode setMaskNode:circleMask];
As commented, you need to set the radius to half of what you'd normally have, and the line width to double the radius.
Hopefully Apple will look at this in future; for now, this hack is the best solution I've found (other than using an image, which doesn't really work if your mask needs to be dynamic).
Since I cannot use a SKShapeNode as mask I decided to convert it to a SKSpriteNode.
Here it is my Swift code:
let shape : SKShapeNode = ... // create your SKShapeNode
var view : SKView = ... // you can get this from GameViewController
let texture = view.textureFromNode(shape)
let sprite = SKSpriteNode(texture: texture)
sprite.position = ...
cropNode.mask = sprite
And it does work :)
Yes, it is impossible to use fill-colored shapenode in current Sprite-Kit realization. It's a bug, I think.
But!
You always can render the shape to texture and use it as mask!
For ex, let edge is SKShapeNode created before.
First, render it to texture before adding to view (in this case it will clean from another nodes)
SKTexture *Mask = [self.view textureFromNode:edge];
[self addChild:edge]; //if you need physics borders
Second,
SKCropNode *cropNode = [[SKCropNode alloc] init];
[cropNode setMaskNode:[SKSpriteNode spriteNodeWithTexture:Mask]];
[cropNode addChild: [SKSpriteNode spriteNodeWithTexture:rocksTiles size:CGSizeMake(w,h)]];
cropNode.position = CGPointMake(Mask.size.width/2,Mask.size.height/2);
//note, anchorpoint of shape in 0,0, but rendered texture is in 0.5,0.5, so we need to dispose it
[self addChild:cropNode];
There is a slightly awkward solution to this issue that I believe is slightly less nasty than any of the mooted hacks around this issue. Simply wrap your SKShapeNode in an SKNode. I haven't tested what kind of performance hit this might cause, but given that SKNode is non-drawing, I'm hoping it won't be too significant.
i.e.
let background = SKSpriteNode(imageNamed: "Background")
let maskNode = SKShapeNode(path: MyPath)
let wrapperNode = SKNode()
let cropNode = SKCropNode()
wrapperNode.addChild(maskNode)
cropNode.maskNode = wrapperNode
cropNode.addChild(background)
Setting alpha of mask node to .0 does the trick:
circleMask.alpha = .0;
EDIT:
Seems to work for Mac OS only.

How to draw a multiple point line using UIBezierPath with gradient

I need to draw a lot of lines.
I'm using UIBezierPath for drawing lines and small 5x10 pattern image for gradient.
Here's the part of code below:
l1 = [UIBezierPath bezierPath];
[l1 moveToPoint:CGPointMake(76, 373)];
[l1 addLineToPoint:CGPointMake(940, 373)];
CAShapeLayer *pathLayer = [CAShapeLayer layer];
CGRect pathRect = self.view.frame;
path = l1;
pathLayer.frame = self.view.bounds;
pathLayer.bounds = pathRect;
pathLayer.geometryFlipped = NO;
pathLayer.path = path.CGPath;
pathLayer.strokeColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"line1Pattern.png"]];
pathLayer.fillColor = nil;
pathLayer.lineWidth = 8.0f;
pathLayer.opacity = 1;
[pathLayer setShadowOffset:CGSizeMake(0, 5)];
[pathLayer setShadowOpacity:0.5];
pathLayer.lineCap = kCALineCapRound;
pathLayer.lineJoin = kCALineJoinRound;
[pathArray addObject:pathLayer];
self.pathLayer = pathLayer;
If the line is straight horizontal (like blue and red) - everything is ok and the result is great.
But if I draw tilted line, or straight and then tiled - the result is not ok.
Here's the link to image: left side - what I need, right side - what mu result: example
So, as I understand - I need to rotate the gradient to match the line. But I cant imagine how to realize that.
Also some lines could have zig-zag form like on this graph: example
Could anyone help me with this problem? Or suggest another way of drawing lines.
Thanks!
You can draw only horizontal lines and rotate them with transform. But if you'll need a curve, then everything will be much more complicated.
Also you can create a gradient and draw it instead of tiled image.

UIView coordinate systems - offsetting and translating

I'm pretty sure this is more a of a math question, but I'll phrase it in the context of UIView and iPad-related objective C
I am importing raw data from a mapping file I have created from some public domain material downloaded elsewhere, then split out to isolate various regions within the map. Each region has a number of sub-regions, much like, for example, the continental US and then the various states which appear within the US, and then each sub-region is broken down again, into, let's say, counties.
Each state, and each county has a bounding box which tells me the origin, the width, and height each is.
In my initial setup, I created a separate view for each state, and then another view for each county. The polygon representing the area of the state/county was rendered (obviously with the county on top of the state so it would be visible) relative to a view I created through interface builder, called mainContainerView. This initial setup worked correctly.
Now I am trying to change things a bit, by adding the counties to the UIView holding the polygon for the state, so I will be able to overlay the state as a clipping mask on the counties. The problem is that no matter what I try, I cannot seem to get the county to translate to the right place within the state's view.
It seems like it should be straightforward addition or subtraction as the scaling for each item is exactly the same, and I'm not trying to do any major transformations, so I do not believe the CFAffineTransformation family is needed.
I can post code if necessary, but I'm not trying to get someone to write my program for me; I just want someone to point me in the right direction here, by giving me a suggestion on how to set the county relative to the state within the state's view.
As per a request, here's the relevant code that I am working on right now. This code does not work, but it gives you the idea as to what I'm doing. Posting sample data is a little more difficult, as it involves arrays of points and data extracted from a .SHP file designed to produce a map (and subdivisions). I'll include some comments in the code with some real point values as I step through the program to show you what's happening to them.
MASK_MAX_EASTING, MASK_MAX_NORTHING, MASK_MIN_EASTING, and MASK_MIN_NORTHING are constants which define the bounding box for the entire map of the country when made up of states.
DIST_MAX_EASTING, DIST_MAX_NORTHING, DIST_MIN_EASTING, and DIST_MIN_NORTHING are constants which define the bounding box for a map of the country when made up of the counties. The scales of the two maps are slightly different, so, by using the different bounding boxes, I've been able to scale the two maps to the same size.
-(void)didLoadMap:(NSNotification *)notification {
id region = [notification object];
ShapePolyline *polygon = [region polygon];
if ([notification name] == #"MapsLoadingForState") {
// m_nBoundingBox is an array which contains the RAW northing and easting values for each subdivision. [0] - west limit, [1] - south limit, [2] - east limit, [3] - north limit.
// The code below, combined with the drawrect method in DrawMap.m (below) puts all the states on the map in precisely the right places, so for the state maps, it works just fine.
CGFloat originX = ((polygon->m_nBoundingBox[0]-MASK_MIN_EASTING)*stateScaleMultiplier)+([mainContainerView frame].size.width/2);
CGFloat originY = ((MASK_MAX_NORTHING-(polygon->m_nBoundingBox[3]))*stateScaleMultiplier)+[mainContainerView frame].origin.y;
CGFloat width = polygon->m_nBoundingBox[2] - polygon->m_nBoundingBox[0];
CGFloat height = polygon->m_nBoundingBox[3] - polygon->m_nBoundingBox[1];
CGFloat scaledWidth = width*stateScaleMultiplier;
CGFloat scaledHeight = height*stateScaleMultiplier;
UIColor *subViewColor = [UIColor colorWithRed:0.0 green:1.0 blue:1.0 alpha:0.0];
stateMapView = [[DrawMap alloc] initWithFrame:CGRectMake(originX, originY, scaledWidth, scaledHeight)];
[stateMapView setBackgroundColor:subViewColor];
[stateMapView setStateScale:stateScaleMultiplier];
[stateMapView setCountyScale:countyScaleMultiplier]; // Not actually needed.
[stateMapView setClippingMask:polygon];
UIColor *colorMask = [UIColor colorWithWhite:1.0 alpha:1.0];
[stateMapView setForeground:colorMask];
[states addObject:stateMapView]; // Add the state map view to an array (for future use)
[mapView addSubview:stateMapView]; // MapView is a UIView of equivalent size and shape as mainContainerView.
} else {
// This is where the problems occur.
CGFloat originX = (polygon->m_nBoundingBox[0]-DIST_MIN_EASTING); // 4431590 (raw data)
originX *= countyScaleMultiplier; // 303.929108
originX += ([mainContainerView frame].size.width/2); // 815.929077
CGFloat originY = (DIST_MAX_NORTHING-polygon->m_nBoundingBox[3]); 4328997
originY *= countyScaleMultiplier; // 296.893036
originY -= [mainContainerView frame].origin.y; // 340.893036
CGRect frame = [stateMapView frame]; // Dummy variable created for watches in the debugger. x=856.237183, y=332.169922 width=34.3800087, height=28.7534008
// When I was invoking DrawMap.h and the included drawrect method, the county map would easily be displayed in the right place, as you can see by the values above.
// This is where I think the problem is. The X value is WAY off as far as I can tell.
originX -= frame.origin.x; // -40.3081055
originY -= frame.origin.y; // 8.72311401
CGPoint countyOrigin = CGPointMake(originX,originY);
// Translate the county's origin so it is relative to the origin of stateMapView, not MainContainerView (doesn't work)
[stateMapView addCountyMap:[region polygon] withColor:winner translatedBy:countyOrigin];
[stateMapView setNeedsDisplay];
}
I am aware that there are several issues with this code and some stuff outside the scope of this question may make a few of you raise an eyebrow (or two) but this is definitely a work in progress...
Here's the relevant code from DrawMap.m; I've cut a bunch of stuff out because it is extraneous.
- (void)drawRect:(CGRect)rect {
// Set up
for (int i=0;i<[countyMaps count];i++) {
// Draw the polygon.
[[countyColors objectAtIndex:i] setFill];
[self drawPolygon:[countyMaps objectAtIndex:i]
usingScale:stateScale
translatedBy:CGPointMake([[countyTranslations objectAtIndex:2*i] floatValue],
[[countyTranslations objectAtIndex:2*i+1] floatValue])];
}
// Set the blend mode to multiply
CGContextSetBlendMode(context, kCGBlendModeMultiply);
// Draw a path with clippingMask
[[UIColor colorWithWhite:1.0 alpha:1.0] setFill];
// CGPoint translate = CGPointMake(0,0);
[self drawPolygon:clippingMask usingScale:stateScale translatedBy:CGPointMake(0,0)];
}
-(void)drawPolygon:(ShapePolyline *)aPolygon usingScale:(float)mapScale translatedBy:(CGPoint)trans {
for (int j=0;j<[aPolygon numParts];j++) {
UIBezierPath *path = [UIBezierPath bezierPath];
[path setLineJoinStyle:kCGLineJoinRound];
int startIndex = [[[aPolygon m_Parts] objectAtIndex:j] intValue];
int endIndex = [aPolygon numPoints];
CGPoint startPoint;
[[[aPolygon m_Points] objectAtIndex:startIndex] getValue:&startPoint];
startPoint.x *=mapScale;
startPoint.y *=mapScale;
startPoint.x -= trans.x;
startPoint.y -= trans.y;
[path moveToPoint:startPoint];
if (j+1 != [aPolygon numParts]){
endIndex = [[[aPolygon m_Parts] objectAtIndex:j+1] intValue];
}
for (int k=startIndex+1; k<endIndex; k++)
{
CGPoint nextPoint;
[[[aPolygon m_Points] objectAtIndex:k] getValue:&nextPoint];
nextPoint.x *= mapScale;
nextPoint.y *= mapScale;
nextPoint.x -= trans.x;
nextPoint.y -= trans.y;
[path addLineToPoint:nextPoint];
}
[path closePath];
// [path stroke];
[path fill];
}
}
This tome is really may be too much information, or it may not be enough. Either way, hopefully by adding code, I've given you some information to go on...
-SOLVED-
And it was so simple. I'm surprised it took me this long to figure it out, as I was right in my initial question - it was simple addition and subtraction:
All translations are now done inside the methods which render the polygons. For each point in the polygon, I needed to add the origin of the state's view, and subtract the origin of the county's bounding box, then subtract 44 from the Y-value (the height of the control bar).
This, I think, is an example of over-thinking a problem, getting frustrated, over-thinking more, only to find out three days later that the answer is staring you in the face, waving a red flag, and shouting, "I'M OVER HERE!!!!"

How to rotate an object around a arbitrary point?

I want to rotate an UILabel around an arbitrary point in a circular manner, not a straight line. This is my code.The final point is perfect but it goes through a straight line between the initial and the end points.
- (void)rotateText:(UILabel *)label duration:(NSTimeInterval)duration degrees:(CGFloat)degrees {
/* Setup the animation */
[UILabel beginAnimations:nil context:NULL];
[UILabel setAnimationDuration:duration];
CGPoint rotationPoint = CGPointMake(160, 236);
CGPoint transportPoint = CGPointMake(rotationPoint.x - label.center.x, rotationPoint.y - label.center.y);
CGAffineTransform t1 = CGAffineTransformTranslate(label.transform, transportPoint.x, -transportPoint.y);
CGAffineTransform t2 = CGAffineTransformRotate(label.transform,DEGREES_TO_RADIANS(degrees));
CGAffineTransform t3 = CGAffineTransformTranslate(label.transform, -transportPoint.x, +transportPoint.y);
CGAffineTransform t4 = CGAffineTransformConcat(CGAffineTransformConcat(t1, t2), t3);
label.transform = t4;
/* Commit the changes */
[UILabel commitAnimations];
}
You should set your own anchorPoint
Its very much overkill to use a keyframe animation for what really is a change of the anchor point.
The anchor point is the point where all transforms are applied from, the default anchor point is the center. By moving the anchor point to (0,0) you can instead make the layer rotate from the bottom most corner. By setting the anchor point to something where x or y is outside the range 0.0 - 1.0 you can have the layer rotate around a point that lies outside of its bounds.
Please read the section about Layer Geometry and Transforms in the Core Animation Programming Guide for more information. It goes through this in detail with images to help you understand.
EDIT: One thing to remember
The frame of your layer (which is also the frame of your view) is calculated using the position, bounds and anchor point. Changing the anchorPoint will change where your view appears on screen. You can counter this by re-setting the frame after changing the anchor point (this will set the position for you). Otherwise you can set the position to the point you are rotating to yourself. The documentation (linked to above) also mentions this.
Applied to you code
The point you called "transportPoint" should be updated to calculate the difference between the rotation point and the lower left corner of the label divided by the width and height.
// Pseudocode for the correct anchor point
transportPoint = ( (rotationX - labelMinX)/labelWidth,
(rotationX - labelMinY)/labelHeight )
I also made the rotation point an argument to your method. The full updated code is below:
#define DEGREES_TO_RADIANS(angle) (angle/180.0*M_PI)
- (void)rotateText:(UILabel *)label
aroundPoint:(CGPoint)rotationPoint
duration:(NSTimeInterval)duration
degrees:(CGFloat)degrees {
/* Setup the animation */
[UILabel beginAnimations:nil context:NULL];
[UILabel setAnimationDuration:duration];
// The anchor point is expressed in the unit coordinate
// system ((0,0) to (1,1)) of the label. Therefore the
// x and y difference must be divided by the width and
// height of the label (divide x difference by width and
// y difference by height).
CGPoint transportPoint = CGPointMake((rotationPoint.x - CGRectGetMinX(label.frame))/CGRectGetWidth(label.bounds),
(rotationPoint.y - CGRectGetMinY(label.frame))/CGRectGetHeight(label.bounds));
[label.layer setAnchorPoint:transportPoint];
[label.layer setPosition:rotationPoint]; // change the position here to keep the frame
[label.layer setTransform:CATransform3DMakeRotation(DEGREES_TO_RADIANS(degrees), 0, 0, 1)];
/* Commit the changes */
[UILabel commitAnimations];
}
I decided to post my solution as an answer. It works fine accept it doesn't have the old solutions's curve animations (UIViewAnimationCurveEaseOut), but I can sort that out.
#define DEGREES_TO_RADIANS(angle) (angle / 180.0 * M_PI)
- (void)rotateText:(UILabel *)label duration:(NSTimeInterval)duration degrees:(CGFloat)degrees {
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path,nil, 160, 236, 100, DEGREES_TO_RADIANS(0), DEGREES_TO_RADIANS(degrees), YES);
CAKeyframeAnimation *theAnimation;
// animation object for the key path
theAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
theAnimation.path=path;
CGPathRelease(path);
// set the animation properties
theAnimation.duration=duration;
theAnimation.removedOnCompletion = NO;
theAnimation.autoreverses = NO;
theAnimation.rotationMode = kCAAnimationRotateAutoReverse;
theAnimation.fillMode = kCAFillModeForwards;
[label.layer addAnimation:theAnimation forKey:#"position"];
}
CAKeyframeAnimation is the right tool for this job. Most UIKit animations are between start and end points. The middle points are not considered. CAKeyframeAnimation allows you to define those middle points to provide a non-linear animation. You will have to provide the appropriate bezier path for your animation. You should look at this example and the one's provided in the Apple documentation to see how it works.
translate, rotate around center, translate back.