Rotate CGPath without changing its position
A Swift 3 method to rotate a rectangle in place around its center:
func createRotatedCGRect(rect: CGRect, radians: CGFloat) -> CGPath {
let center = CGPoint(x: rect.midX, y: rect.midY)
let path = UIBezierPath(rect: rect)
path.apply(CGAffineTransform(translationX: center.x, y: center.y).inverted())
path.apply(CGAffineTransform(rotationAngle: radians))
path.apply(CGAffineTransform(translationX: center.x, y: center.y))
return path.cgPath
}
All of rob mayoff's code for applying this to a path and its bounding box would still apply, so I didn't see cause to repeat them.
A path doesn't have “a position”. A path is a set of points (defined by line and curve segments). Every point has its own position.
Perhaps you want to rotate the path around a particular point, instead of around the origin. The trick is to create a composite transform that combines three individual transforms:
- Translate the origin to the rotation point.
- Rotate.
- Invert the translation from step 1.
For example, here's a function that takes a path and returns a new path that is the original path rotated around the center of its bounding box:
static CGPathRef createPathRotatedAroundBoundingBoxCenter(CGPathRef path, CGFloat radians) {
CGRect bounds = CGPathGetBoundingBox(path); // might want to use CGPathGetPathBoundingBox
CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, center.x, center.y);
transform = CGAffineTransformRotate(transform, radians);
transform = CGAffineTransformTranslate(transform, -center.x, -center.y);
return CGPathCreateCopyByTransformingPath(path, &transform);
}
Note that this function returns a new path with a +1 retain count that you are responsible for releasing when you're done with it. For example, if you're trying to rotate the path of a shape layer:
- (IBAction)rotateButtonWasTapped:(id)sender {
CGPathRef path = createPathRotatedAroundBoundingBoxCenter(shapeLayer_.path, M_PI / 8);
shapeLayer_.path = path;
CGPathRelease(path);
}
UPDATE
Here's a demonstration using a Swift playground. We'll start with a helper function that displays a path and marks the origin with a crosshair:
import UIKit
import XCPlayground
func showPath(label: String, path: UIBezierPath) {
let graph = UIBezierPath()
let r = 40
graph.moveToPoint(CGPoint(x:0,y:r))
graph.addLineToPoint(CGPoint(x:0,y:-r))
graph.moveToPoint(CGPoint(x:-r,y:0))
graph.addLineToPoint(CGPoint(x:r,y:0))
graph.appendPath(path)
XCPCaptureValue(label, graph)
}
Next, here's our test path:
var path = UIBezierPath()
path.moveToPoint(CGPoint(x:1000,y:1000))
path.addLineToPoint(CGPoint(x:1000,y:1200))
path.addLineToPoint(CGPoint(x:1100,y:1200))
showPath("original", path)
(Remember, the crosshair is the origin and is not part of the path we're transforming.)
We get the center and transform the path so it's centered at the origin:
let bounds = CGPathGetBoundingBox(path.CGPath)
let center = CGPoint(x:CGRectGetMidX(bounds), y:CGRectGetMidY(bounds))
let toOrigin = CGAffineTransformMakeTranslation(-center.x, -center.y)
path.applyTransform(toOrigin)
showPath("translated center to origin", path)
Then we rotate it. All rotations happen around the origin:
let rotation = CGAffineTransformMakeRotation(CGFloat(M_PI / 3.0))
path.applyTransform(rotation)
showPath("rotated", path)
Finally, we translate it back, exactly inverting the original translation:
let fromOrigin = CGAffineTransformMakeTranslation(center.x, center.y)
path.applyTransform(fromOrigin)
showPath("translated back to original center", path)
Note that we must invert the original translation. We don't translate by the center of its new bounding box. Recall that (in this example), the original center is at (1050,1100). But after we've translated it to the origin and rotated it, the new bounding box's center is at (-25,0). Translating the path by (-25,0) will not put it anywhere close to its original position!
A Swift 5 version:
func rotate(path: UIBezierPath, degree: CGFloat) {
let bounds: CGRect = path.cgPath.boundingBox
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radians = degree / 180.0 * .pi
var transform: CGAffineTransform = .identity
transform = transform.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: radians)
transform = transform.translatedBy(x: -center.x, y: -center.y)
path.apply(transform)
}