CATransform3DMakeRotation anchorpoint and position in Swift - ios

Hi I am trying to rotate a triangle shaped CAShapeLayer. The northPole CAShapeLayer is defined in a custom UIView class. I have two functions in the class one to setup the layer properties and one to animate the rotation. The rotation animate works fine but after the rotation completes it reverts back to the original position. I want the layer to stay at the angle given (positionTo = 90.0) after the rotation animation.
I am trying to set the position after the transformation to retain the correct position after the animation. I have also tried to get the position from the presentation() method but this has been unhelpful. I have read many articles on frame, bounds, anchorpoints but I still cannot seem to make sense of why this transformation rotation is not working.
private func setupNorthPole(shapeLayer: CAShapeLayer) {
shapeLayer.frame = CGRect(x: self.bounds.width / 2 - triangleWidth / 2, y: self.bounds.height / 2 - triangleHeight, width: triangleWidth, height: triangleHeight)//self.bounds
let polePath = UIBezierPath()
polePath.move(to: CGPoint(x: 0, y: triangleHeight))
polePath.addLine(to: CGPoint(x: triangleWidth / 2, y: 0))
polePath.addLine(to: CGPoint(x: triangleWidth, y: triangleHeight))
shapeLayer.path = polePath.cgPath
shapeLayer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
The animate function:
private func animate() {
let positionTo = CGFloat(DegreesToRadians(value: degrees))
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.fromValue = 0.0
rotate.toValue = positionTo //CGFloat(M_PI * 2.0)
rotate.duration = 0.5
northPole.add(rotate, forKey: nil)
let present = northPole.presentation()!
print("presentation position x " + "\(present.position.x)")
print("presentation position y " + "\(present.position.y)")
CATransaction.begin()
northPole.transform = CATransform3DMakeRotation(positionTo, 0.0, 0.0, 1.0)
northPole.position = CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2)
CATransaction.commit()
}

To fix the problem, in the custom UIView class I had to override layoutSubviews() with the following:
super.layoutSubviews()
let northPoleTransform: CATransform3D = northPole.transform
northPole.transform = CATransform3DIdentity
setupNorthPole(northPole)
northPole.transform = northPoleTransform

Related

CAShapeLayer path animation shrinking before completing animation

I am trying to animate a rotation of a layer's CGpath.
The rotation works perfectly, but for some reason, the path seems to shrink, then get bigger again. I just want it to show a rotating animation. I don't want it to shrink
here is a video.
here is the code
class ViewController: UIViewController {
#IBOutlet weak var overlayView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let maskLayer = CAShapeLayer()
let path = UIBezierPath(rect: view.bounds)
let mask = UIBezierPath.drawSquare(width: 100, center: view.center)
// Makes it so we can see through the center of the view
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
path.append(mask)
maskLayer.path = path.cgPath
overlayView.layer.mask = maskLayer
}
#IBAction func rotateView(_ sender: Any) {
let maskPath = UIBezierPath.drawSquare(width: 100, center: overlayView.center)
let path = UIBezierPath(rect: overlayView.frame)
let bounds: CGRect = maskPath.cgPath.boundingBox
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radians = 90 / 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)
maskPath.apply(transform)
path.append(maskPath)
// create new animation
let anim = CABasicAnimation(keyPath: "path")
anim.toValue = path.cgPath
(overlayView.layer.mask as? CAShapeLayer)?.add(anim, forKey: "path")
}
}
extension UIBezierPath{
static func drawSquare(width: CGFloat, center: CGPoint) -> UIBezierPath {
let rect = CGRect(x: center.x - width/2, y: center.y - width/2, width: width, height: width)
let path = UIBezierPath()
path.move(to: rect.origin)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.close()
return path
}
}
Your "cutout box" is shrinking and growing because you're using path animation.
When you animate a path from one set of points to another, each point will take a straight line from its starting point to its ending point.
Couple ways to visualize this...
First, we'll add an "outline" frame to the cutout mask box:
as you see, each corner is taking a direct route to its next position.
If we highlight one side, it's maybe a bit more obvious:
and, we can number the corners for even more clarity:
So, if we want to rotate the square without changing its size, we could use keyframe animation and run the corners along a circle:
or, much easier, rotate the mask layer instead of its path:
let radians = 90 / 180.0 * .pi
let anim = CABasicAnimation(keyPath: "transform.rotation.z")
anim.toValue = radians
anim.duration = 1.0
(overlayView.layer.mask as? CAShapeLayer)?.add(anim, forKey: "layerRotate")
No doubt we notice that the mask layer frame does not completely cover the overlayView frame.
So, we can fix that by making the mask layer frame larger.
We could calculate exactly how big it needs to be, but because shape layers are very efficient, we'll just make it a square, 1.5 times bigger than the longer axis:
so when it rotates, it covers the view:
and we finish with this:

UIBezierPath Rotation around a UIView's center

I'm creating a custom UIView, in which I implement its draw(rect:) method by drawing a circle with a large width using UIBezierPath, that draw a square on the top (as shown in the picture, don't consider the colors or the size). Then I try creating rotated copies of the square, to match a "settings" icon (picture 2, consider only the outer ring). To do that last thing, I need to rotate the square using a CGAffineTransform(rotationAngle:) but the problem is that this rotation's center is the origin of the frame, and not the center of the circle. How can I create a rotation around a certain point in my view?
As a demonstration of #DuncanC's answer (up voted), here is the drawing of a gear using CGAffineTransforms to rotate the gear tooth around the center of the circle:
class Gear: UIView {
var lineWidth: CGFloat = 16
let boxWidth: CGFloat = 20
let toothAngle: CGFloat = 45
override func draw(_ rect: CGRect) {
let radius = (min(bounds.width, bounds.height) - lineWidth) / 4.0
var path = UIBezierPath()
path.lineWidth = lineWidth
UIColor.white.set()
// Use the center of the bounds not the center of the frame to ensure
// this draws correctly no matter the location of the view
// (thanks #dulgan for pointing this out)
let center = CGPoint(x: bounds.maxX / 2, y: bounds.maxY / 2)
// Draw circle
path.move(to: CGPoint(x: center.x + radius, y: center.y))
path.addArc(withCenter: CGPoint(x: center.x, y: center.y), radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
path.stroke()
// Box for gear tooth
path = UIBezierPath()
let point = CGPoint(x: center.x - boxWidth / 2.0, y: center.y - radius)
path.move(to: point)
path.addLine(to: CGPoint(x: point.x, y: point.y - boxWidth))
path.addLine(to: CGPoint(x: point.x + boxWidth, y: point.y - boxWidth))
path.addLine(to: CGPoint(x: point.x + boxWidth, y: point.y))
path.close()
UIColor.red.set()
// Draw a tooth every toothAngle degrees
for _ in stride(from: toothAngle, through: 360, by: toothAngle) {
// Move origin to center of the circle
path.apply(CGAffineTransform(translationX: -center.x, y: -center.y))
// Rotate
path.apply(CGAffineTransform(rotationAngle: toothAngle * .pi / 180))
// Move origin back to original location
path.apply(CGAffineTransform(translationX: center.x, y: center.y))
// Draw the tooth
path.fill()
}
}
}
let view = Gear(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
Here it is running in a Playground:
Shift the origin of your transform,
Rotate,
Shift back
Apply your transform
Maybe would help someone. Source
extension UIBezierPath {
func rotate(degree: CGFloat) {
let bounds: CGRect = self.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)
self.apply(transform)
}
}
Example:
let progressLayerPath = UIBezierPath(ovalIn: CGRect(x: 0,
y: 0,
width: 70,
height: 70))
progressLayerPath.rotate(degree: -90) // <-------
progressLayer.path = progressLayerPath.cgPath
progressLayer.strokeColor = progressColor.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineWidth = lineWidth
layer.addSublayer(progressLayer)
If you want a quick n dirty version using UIViews instead:
UIView * dialView = [UIView new];
dialView.frame = CGRectMake(0, localY, w, 300);
dialView.backgroundColor = [UIColor whiteColor];
[analyticsView addSubview:dialView];
float lineWidth = 6;
float lineHeight = 20.0f;
int numberOfLines = 36;
for (int n = 0; n < numberOfLines; n++){
UIView * lineView = [UIView new];
lineView.frame = CGRectMake((w-lineWidth)/2, (300-lineHeight)/2, lineHeight, lineWidth);
lineView.backgroundColor = [UIColor redColor];
[dialView addSubview:lineView];
lineView.transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(360/numberOfLines*n));
lineView.transform = CGAffineTransformTranslate(lineView.transform, (150-lineHeight), 0);
}
Giving:
Where w is a holder width, and radians are calculated by:
#define DEGREES_TO_RADIANS(degrees)((M_PI * degrees)/180)

CAShaperLayer as mask show only 1/4 of UIView

I try to make UIView to show zig-zag bottom edge. Something like http://www.shutterstock.com/pic-373176370/stock-vector-receipt-vector-icon-invoice-flat-illustration-cheque-shadow-bill-with-total-cost-amount-and-dollar-symbol-abstract-text-receipt-paper-isolated-on-green.html?src=zMGBKj_5etMCcRB3cKmCoA-1-2
I have method that create a path and set as mask, but it show as 1/4 of the view. Do I need to set something else? Look like a retina problem or coordinate problem, but don't sure which one.
func layoutZigZag(bounds: CGRect) -> CALayer {
let maskLayer = CAShapeLayer()
maskLayer.bounds = bounds
let path = UIBezierPath()
let width = bounds.size.width
let height = bounds.size.height
let topRight = CGPoint(x: width , y: height)
let topLeft = CGPoint(x: 0 , y: height)
let bottomRight = CGPoint(x: width , y: 0)
let bottomLeft = CGPoint(x: 0 , y: 0)
let zigzagHeight: CGFloat = 10
let numberOfZigZag = Int(floor(width / 23.0))
let zigzagWidth = width / CGFloat(numberOfZigZag)
path.move(to: topLeft)
path.addLine(to: bottomLeft)
// zigzag
var currentX = bottomLeft.x
var currentY = bottomLeft.y
for i in 0..<numberOfZigZag {
let upper = CGPoint(x: currentX + zigzagWidth / 2, y: currentY + zigzagHeight)
let lower = CGPoint(x: currentX + zigzagWidth, y: currentY)
path.addLine(to: upper)
path.addLine(to: lower)
currentX += zigzagWidth
}
path.addLine(to: topRight)
path.close()
maskLayer.path = path.cgPath
return maskLayer
}
and
let rect = CGRect(x: 0, y: 0, width: 320, height: 400)
let view = UIView(frame: rect)
view.backgroundColor = UIColor.red
let zigzag = layoutZigZag(bounds: rect)
view.layer.mask = zigzag
Path look correct
Result is 1/4 of the view
Change maskLayer.bounds = bounds to maskLayer.frame = bounds
Update:
Upside down is because of difference between the UI and CG, we are creating the path in UIBezierPath and converting that path as a CGPath (maskLayer.path = path.cgPath). First we have to know the difference, where CGPath is Quartz 2D and origin is at the bottom left while in UIBezierPath is UIKit origin is at the top-left. As per your code, applied coordinates are as per the top-left ie UIBezierPath when we transform to CGPath (origin at bottom left) it becomes upside down. so change the code as below to get the desired effect.
let topRight = CGPoint(x: width , y: 0)
let topLeft = CGPoint(x: 0 , y: 0)
let bottomLeft = CGPoint(x: 0 , y: (height - zigzagHeight))
Quartz 2D Coordinate Systems
UIBezierPath Coordinate Systems

Animate bezier straight lines

I want how to animate a bezier straight line like see-saw. Can somebody please suggest. Reference link for animation how does it should look (https://www.youtube.com/watch?v=bjCx128BZK8).
You don't say how you rendered this UIBezierPath, but if you for example added it as a CAShapeLayer you can just apply a CAKeyframeAnimation to it:
// create simple path
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 50, y: view.bounds.size.height / 2))
path.addLineToPoint(CGPoint(x: view.bounds.size.width - 50, y: view.bounds.size.height / 2))
// create `CAShapeLayer` from that path
let layer = CAShapeLayer()
layer.path = path.CGPath
layer.lineWidth = 10
layer.strokeColor = UIColor.blueColor().CGColor
layer.fillColor = UIColor.clearColor().CGColor
view.layer.addSublayer(layer)
// animate it
let animation = CAKeyframeAnimation(keyPath: "transform")
var values = [NSValue]()
for var x: CGFloat = 0.0; x < CGFloat(M_PI * 4); x += CGFloat(M_PI * 4 / 100.0) {
var transform = CATransform3DMakeTranslation(view.frame.size.width/2, view.frame.size.height/2, 0.0)
transform = CATransform3DRotate(transform, sin(x) / 3.0, 0.0, 0.0, 1.0)
transform = CATransform3DTranslate(transform, -view.frame.size.width/2, -view.frame.size.height/2, 0.0)
values.append(NSValue(CATransform3D: transform))
}
animation.duration = 2
animation.values = values
layer.addAnimation(animation, forKey: "transform")
That yields:
Or you could add the CAShapeLayer to a view and then use the block-based UIView animation to rotate the view. The basic idea would be the same.
If you want to do something more sophisticated, you can use CADisplayLink, either updating the path for the CAShapeLayer (as outlined here) or updating the properties used by UIView subclass and then call setNeedsDisplay.

iOS Swift Xcode 6: CGAffineTransformRotate with auto-layout anchorpoint

I'm making an app with a rotatable pie chart. However, my pie chart rotates around (0,0) and I can't find a solution to make it rotate around its center.
Some code:
// DRAWING CODE
// Set constants for piePieces
let radius: CGFloat = CGFloat(screen.width * 0.43)
let pi: CGFloat = 3.1415926535
let sliceRad: CGFloat = 2.0 * pi / CGFloat(categoryArray.count)
var currentAngle: CGFloat = -0.5 * sliceRad - 0.5 * pi
let center: CGPoint = CGPoint(x: screen.width / 2.0, y: radius)
println("Center point: \(center)")
//container!.layer.anchorPoint = CGPoint(x: screen.width, y: radius)
// Draw all pie charts, add them to container (chartView)
for category in categoryArray {
let slice = UIView()
slice.frame = self.frame
let shapeLayer = CAShapeLayer()
let scoreIndex = CGFloat(category.calculateScoreIndex())
let sliceRadius: CGFloat = scoreIndex * radius
// Draw the path
var path:UIBezierPath = UIBezierPath()
path.moveToPoint(center)
path.addLineToPoint(CGPoint(x: center.x + sliceRadius * cos(currentAngle), y: center.y + sliceRadius * sin(currentAngle)))
path.addArcWithCenter(center, radius: sliceRadius, startAngle: currentAngle, endAngle: currentAngle + sliceRad, clockwise: true)
path.addLineToPoint(center)
path.closePath()
// For next slice, add 2*pi Rad / n categories
currentAngle += sliceRad
// Add path to shapeLayer
shapeLayer.path = path.CGPath
//shapeLayer.frame = self.frame
shapeLayer.fillColor = SurveyColors().getColor(category.categoryIndex).CGColor
shapeLayer.anchorPoint = center
// Add shapeLayer to sliceView
slice.layer.addSublayer(shapeLayer)
// Add slice to chartView
container!.addSubview(slice)
}
self.addSubview(container!)
//container!.center = center
container!.layer.anchorPoint = center
}
I sheduled a NSTimer to perform a rotation every 2 seconds (for testing purposes):
func rotate() {
let t: CGAffineTransform = CGAffineTransformRotate(container!.transform, -0.78)
container!.transform = t;
}
The pie chart rotates around (0,0). What is going wrong?
I believe a good solution to your problem would be to set a container view:
container = UIView()
container!.frame = CGRect(x: 0.0, y: 0.0, width: screen.width, height: screen.width)
container!.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
container!.backgroundColor = UIColor.clearColor()
And then add all slices to this container view:
let slice = UIView()
slice.frame = CGRect(x: 0.0 , y: 0.0 , width: container!.bounds.width, height: container!.bounds.height)
let shapeLayer = CAShapeLayer()
let scoreIndex = CGFloat(category.calculateScoreIndex())
let sliceRadius: CGFloat = scoreIndex * radius
/*
*/
container!.addSubview(slice)
Also, make sure not to add your chart as a subview to a view with auto-layout constraints (which might interfere with you CGAffineTransformRotate).

Resources