Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rotate CGPath without changing its position

I want to rotate a CGPath, I'm using the following code:

CGAffineTransform transform = CGAffineTransformMakeRotation(angleInRadians);
CGPathRef rotatedPath = CGPathCreateCopyByTransformingPath(myPath, &transform);

This code works fine but it changes path's position! I want the path remain at the same position as it was before rotating.

like image 495
Asif Mujteba Avatar asked Dec 06 '12 06:12

Asif Mujteba


2 Answers

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)
}
like image 123
Dmytryk Skorokhod Avatar answered Nov 15 '22 18:11

Dmytryk Skorokhod


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:

  1. Translate the origin to the rotation point.
  2. Rotate.
  3. 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)

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)

path translated to origin

Then we rotate it. All rotations happen around the origin:

let rotation = CGAffineTransformMakeRotation(CGFloat(M_PI / 3.0))
path.applyTransform(rotation)
showPath("rotated", path)

path rotated

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)

path translated from origin

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!

like image 45
rob mayoff Avatar answered Nov 15 '22 18:11

rob mayoff