Rotate CGPath without changing its position - iphone

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.

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

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.

In case you want to rotate and/or scale an UIBezierPath, or scale a CAShapeLayer while keep the center of the UIBezierPath. Here the function for Swift 4/5. It is based on answer from rob mayoff.
extension UIBezierPath {
/// perform scale on UIBezierPath and keep the position in the path's center
func scale(scale:CGFloat) {
let bounds = self.cgPath.boundingBox
let center = CGPoint(x:bounds.midX,y:bounds.midY)
var transform = CGAffineTransform.identity.translatedBy(x: center.x, y: center.y)
transform = transform.scaledBy(x: scale, y: scale)
transform = transform.translatedBy(x: -center.x, y: -center.y);
self.apply(transform)
}
/// perform rotate on UIBezierPath and keep the center unchanged
func rotate(radians:CGFloat) {
let bounds = self.cgPath.boundingBox
let center = CGPoint(x:bounds.midX,y:bounds.midY)
var transform = CGAffineTransform.identity.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: radians)
transform = transform.translatedBy(x: -center.x, y: -center.y);
self.apply(transform)
}
}

Related

How to rotate an SKShapeNode line around its bottom point

I'm confused on how to rotate a line (SKShapeNode) around it's bottom point (begin point, here), think of a clock here.
I have the following, but it doesn't seem to be rotating as expected.
public let line = SKShapeNode()
private let linePath = CGMutablePath()
init(begin: CGPoint, end: CGPoint) {
self.begin = begin
self.end = end
linePath.move(to: begin)
linePath.addLine(to: end)
line.path = linePath
line.strokeColor = UIColor.black
line.lineWidth = 3
SceneCoordinator.shared.gameScene.addChild(line)
}
public func rotate(angle: Double) {
var transform = CGAffineTransform(rotationAngle: CGFloat(angle))
line.path = linePath.mutableCopy(using: &transform)
}
Your function rotates the path around the
shapes position (which is (0, 0) by default) and not around the starting point of the line as intended.
To solve the problem, create the shape with a position equal to the starting
point of the line, and with a line relative to that point:
linePath.move(to: CGPoint.zero)
linePath.addLine(to: CGPoint(x: end.x - begin.x, y: end.y - begin.y))
line.path = linePath
line.position = begin
// ...
SceneCoordinator.shared.gameScene.addChild(line)
Note that instead of transforming the path you can rotate the node:
line.zRotation = angle
or with animation:
line.run(SKAction.rotate(toAngle: angle, duration: 0.2))
You can compute the position of the endpoint in the scene's
coordinate system with
let endPos = line.convert(CGPoint(x: end.x - begin.x, y: end.y - begin.y), to: line.scene!)

Drawing a gradient color in an arc with a rounded edge

I want to replicate this in Swift.. reading further: How to draw a linear gradient arc with Qt QPainter?
I've figured out how to draw a gradient inside of a rectangular view:
override func drawRect(rect: CGRect) {
let currentContext = UIGraphicsGetCurrentContext()
CGContextSaveGState(currentContext)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let startColor = MyDisplay.displayColor(myMetric.currentValue)
let startColorComponents = CGColorGetComponents(startColor.CGColor)
let endColor = MyDisplay.gradientEndDisplayColor(myMetric.currentValue)
let endColorComponents = CGColorGetComponents(endColor.CGColor)
var colorComponents
= [startColorComponents[0], startColorComponents[1], startColorComponents[2], startColorComponents[3], endColorComponents[0], endColorComponents[1], endColorComponents[2], endColorComponents[3]]
var locations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradientCreateWithColorComponents(colorSpace, &colorComponents, &locations, 2)
let startPoint = CGPointMake(0, 0)
let endPoint = CGPointMake(1, 8)
CGContextDrawLinearGradient(currentContext, gradient, startPoint, endPoint, CGGradientDrawingOptions.DrawsAfterEndLocation)
CGContextRestoreGState(currentContext)
}
and how to draw an arc
func drawArc(color: UIColor, x: CGFloat, y: CGFloat, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, lineWidth: CGFloat, clockwise: Int32) {
let context = UIGraphicsGetCurrentContext()
CGContextSetLineWidth(context, lineWidth)
CGContextSetStrokeColorWithColor(context,
color.CGColor)
CGContextAddArc(context, x, y, radius, startAngle, endAngle, clockwise)
CGContextStrokePath(context)
}
With the gradient, that took up the entire view. I was unsure how to limit the gradient to a particular area so I made a subclass of UIView, modified the frame and used the gradient code (first snippet).
For arcs, I've used the second snippet. I'm thinking I can draw an arc and then draw a circle at the end of the arc to smooth out the arc with a rounded finish. How can I do this and draw a gradient over the arc shape?
Please ignore the purple mark. The first picture is the beginning of the gradient arc from my specification, the second is the end of the gradient arc, below and to the left of the beginning arc.
How can I draw this in Swift with Core Graphics?
Exact code for 2019 with true circular gradient and totally flexible end positions:
These days basically you simply have a gradient layer and mask for that layer.
So step one, you need a "mask".
What the heck is a "mask" in iOS?
In iOS, you actually use a CAShapeLayer to make a mask.
This is confusing. It should be called something like 'CAShapeToUseAsAMaskLayer'
But that's how you do it, a CAShapeLayer.
Step two. In layout, you must resize the shape layer. And that's it.
So basically it's just this:
class SweetArc: UIView {
open override func layoutSubviews() {
super.layoutSubviews()
arcLayer.frame = bounds
gradientLayer.frame = bounds
}
private lazy var gradientLayer: CAGradientLayer = {
let l = CAGradientLayer()
...
l.mask = arcLayer
layer.addSublayer(l)
return l
}()
private lazy var arcLayer: CAShapeLayer = {
let l = CAShapeLayer()
...
return l
}()
}
That's the whole thing.
Note that the mask of the gradient layer is indeed set as the shape layer, which we have named "arcLayer".
A shape layer is used as a mask.
Here's the exact code for the two layers:
private lazy var gradientLayer: CAGradientLayer = {
// recall that .conic is finally available in 12 !
let l = CAGradientLayer()
l.type = .conic
l.colors = [
UIColor.yellow,
UIColor.red
].map{$0.cgColor}
l.locations = [0, 1]
l.startPoint = CGPoint(x: 0.5, y: 0.5)
l.endPoint = CGPoint(x: 0.5, y: 0)
l.mask = arcLayer
layer.addSublayer(l)
return l
}()
private lazy var arcLayer: CAShapeLayer = {
let l = CAShapeLayer()
l.path = arcPath
// to use a shape layer as a mask, you mask with white-on-clear:
l.fillColor = UIColor.clear.cgColor
l.strokeColor = UIColor.white.cgColor
l.lineWidth = lineThick
l.lineCap = CAShapeLayerLineCap.round
l.strokeStart = 0.4 // try values 0-1
l.strokeEnd = 0.5 // try values 0-1
return l
}()
Again, notice arcLayer is a shape layer you are using as a mask - you do NOT actually add it as a subview.
Remember that shape layers used as masks do not! get added as subviews.
That can be a source of confusion. It's not a "layer" at all.
Finally, notice arcLayer uses a path arcPath.
Construct the path perfectly so that strokeStart and strokeEnd work correctly.
You want to build the path perfectly.
So that it is very easy to set the stroke start and stroke end values.
And they make sense by perfectly and correctly running clockwise, from the top, from 0 to 1.
Here's the exact code for that:
private let lineThick: CGFloat = 10.0
private let outerGap: CGFloat = 10.0
private let beginFraction: CGFloat = 0 // 0 means from top; runs clockwise
private lazy var arcPath: CGPath = {
let b = beginFraction * .pi * 2.0
return UIBezierPath(
arcCenter: bounds.centerOfCGRect(),
radius: bounds.width / 2.0 - lineThick / 2.0 - outerGap,
startAngle: .pi * -0.5 + b,
endAngle: .pi * 1.5 + b,
clockwise: true
).cgPath
}()
You're done!
override func draw(_ rect: CGRect) {
//Add gradient layer
let gl = CAGradientLayer()
gl.frame = rect
gl.colors = [UIColor.red.cgColor, UIColor.blue.cgColor]
layer.addSublayer(gl)
//create mask in the shape of arc
let sl = CAShapeLayer()
sl.frame = rect
sl.lineWidth = 15.0
sl.strokeColor = UIColor.red.cgColor
let path = UIBezierPath()
path.addArc(withCenter: .zero, radius: 250.0, startAngle: 10.0.radians, endAngle: 60.0.radians, clockwise: true)
sl.fillColor = UIColor.clear.cgColor
sl.lineCap = kCALineCapRound
sl.path = path.cgPath
//Add mask to gradient layer
gl.mask = sl
}
Hope this helps!!

How to connect two SCNSpheres in 3D space using Bezier path in Swift?

I have the following code:
func createScene(){
count += 1
let sphereGeom = SCNSphere(radius: 1.5)
sphereGeom.firstMaterial?.diffuse.contents = UIColor.redColor()
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 0))
let radius = 3.0
var radians = Double(0)
var yPosition = Float(5.4)
while count <= 20 {
if radians >= 2{
radians -= 2
}
let sphereNode = SCNNode(geometry: sphereGeom)
let angle = Double(radians * M_PI)
let xPosition = Float(radius * cos(angle))
let zPosition = Float(radius * sin(angle))
sphereNode.position = SCNVector3(xPosition, yPosition, zPosition)
let cgX = CGFloat(xPosition)
let cgY = CGFloat(yPosition)
path.addQuadCurveToPoint(CGPoint(x: cgX, y: cgY), controlPoint: CGPoint(x: (cgX / 2), y: (cgY / 2)))
path.addLineToPoint(CGPoint(x: (cgX - (cgX * 0.01)), y: cgY))
path.addQuadCurveToPoint(CGPoint(x: 1, y: 0), controlPoint: CGPoint(x: (cgX / 2), y: ((cgY / 2) - (cgY * 0.01))))
let shape = SCNShape(path: path, extrusionDepth: 3.0)
shape.firstMaterial?.diffuse.contents = UIColor.blueColor()
let shapeNode = SCNNode(geometry: shape)
shapeNode.eulerAngles.y = Float(-M_PI_4)
self.rootNode.addChildNode(shapeNode)
count += 1
radians += 0.5556
yPosition -= 1.35
self.rootNode.addChildNode(sphereNode)
}
I want to add a Bezier path connecting each sphere to the next one, creating a spiral going down the helix. For some reason, when I add this code, the shape doesn't even appear. But when I use larger x and y values, I see the path fine, but it is no way oriented to the size of the spheres. I don't understand why it disappears when I try to make it smaller.
Your SCNShape doesn't ever get extruded. Per Apple doc,
An extrusion depth of zero creates a flat, one-sided shape.
With larger X/Y values your flat shape happens to become visible. You can't build a 3D helix with SCNShape, though: the start and end planes of the extrusion are parallel.
You'll have to use custom geometry, or approximate your helix with a series of elongated SCNBox nodes. And I bet someone out there knows how to do this with a shader.

Implementing anchorPoint in SKNode subclass

I have a custom class inheriting from SKNode and would like to implement SKSpriteNode's anchorPoint-like functionality on that class. Specifically, what I am trying to achieve is to center the custom node in the parent node automatically (anchorPoint at 0.5, 0.5) without having to offset it by half its width/height.
Any tips?
Edit: I ended up by first creating an anchorPoint variable and then overriding the position variable and set the super position modified by the anchorPoint.
var anchorPoint = CGPoint(x: 0.5, y: 0.5)
override var position: CGPoint {
set {
super.position = CGPoint(x: self.position.x - self.size.width * self.anchorPoint.x, y: self.position.y - self.size.height * self.anchorPoint.y)
}
get {
return super.position
}
}
You can over ride the setPosition function of the inherited class. In this function you would adjust for the offset-ed anchorPoint.
For example, if you sprite is 100x100. And you have set its anchor as (.5,.5), When someone sets the position, you should add or subtract the sprite dimensions from it and then set the position.
In this case, if user sets the sprite position as (0,0), you would add (100*.5,100*.5) = (50,50). And adding (0,0) to it would be (50,50).
This is the new position that you should set.
It might be easier to add a new "root" SKNode under your control's SKNode, add any child nodes to it, and then just move it when the anchorPoint property is changed. That's what I did.
var anchorPoint: CGPoint = CGPoint( x: 0.5, y: 0.5 )
{
didSet
{
let translateX = ( anchorPoint.x - 0.5 ) * controlSize.width
let translateY = ( anchorPoint.y - 0.5 ) * controlSize.height
rootNode!.position = CGPoint(x: translateX, y: translateY)
}
}

SpriteKit PhysicsBody is not applying for my bezier curve

Here is the test code:
var paths = UIBezierPath()
var bottomLeft = CGPoint(x: dim.minX, y:dim.minY)
var bottomRight = CGPoint(x: dim.maxX , y: dim.minY )
var topRight = CGPoint(x: dim.maxX, y: dim.maxY )
var topLeft = CGPoint(x: dim.minX, y: dim.maxY)
paths.moveToPoint(bottomRight)
paths.addLineToPoint(topRight)
paths.moveToPoint(topRight)
paths.addLineToPoint(topLeft)
paths.moveToPoint(topLeft)
paths.addLineToPoint(bottomLeft)
paths.moveToPoint(bottomLeft)
paths.addLineToPoint(bottomRight)
var n = SKShapeNode(path: paths.CGPath)
n.strokeColor = UIColor.greenColor()
n.physicsBody = SKPhysicsBody(edgeLoopFromPath: paths.CGPath)
shapeLayer.addChild(n)
It draws a standard rect. I will add some curve in the middle from topRight to topLeft points, but I wanted to test it with simple rect. I am testing the collision with a ball and it seems to be only colliding with the line from bottomRight to topRight and ignoring the rest. Does anyone have a idea on whats going on here?
Although I am not sure about the issue with bezier path and the physics collision setup code needed, I was however able to get it working with using raw paths and adding lines:
var polygonPath = CGPathCreateMutable()
CGPathMoveToPoint(polygonPath, nil, bottomRight.x, bottomRight.y)
CGPathAddLineToPoint(polygonPath, nil, topRight.x, topRight.y)
CGPathAddLineToPoint(polygonPath, nil, topLeft.x, topLeft.y)
CGPathAddLineToPoint(polygonPath, nil, bottomLeft.x, bottomLeft.y)
CGPathCloseSubpath(polygonPath)
var n = SKShapeNode(path: polygonPath)
n.strokeColor = UIColor.grayColor()
n.physicsBody = SKPhysicsBody(edgeLoopFromPath: polygonPath)
That sets up proper physics collision bounds. Although this solves my issue, any insight on why the original code was not working with bezier path would be great. Thanks.