UIBezierPath circle animation distortion - swift

So, I'm pretty new to animation and BezierPaths. Here's my code. Can you please help me figure out what's causing the distortion?
let circleLayer = CAShapeLayer()
let radius: CGFloat = 100.0
let beginPath = UIBezierPath(arcCenter: view.center, radius: 0, startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
let endPath = UIBezierPath(arcCenter: view.center, radius: radius, startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
circleLayer.fillColor = UIColor.red.cgColor
circleLayer.path = beginPath.cgPath
circleLayer.removeAllAnimations()
let scaleAnimation = CABasicAnimation(keyPath: "path")
scaleAnimation.fromValue = beginPath.cgPath
scaleAnimation.toValue = endPath.cgPath
let alphaAnimation = CABasicAnimation(keyPath: "opacity")
alphaAnimation.fromValue = 1
alphaAnimation.toValue = 0
let animations = CAAnimationGroup()
animations.duration = 2
animations.repeatCount = .greatestFiniteMagnitude
animations.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animations.animations = [scaleAnimation, alphaAnimation]
circleLayer.add(animations, forKey: "animations")
self.view.layer.addSublayer(circleLayer)

Your goal is to make the circle grow from a center point?
The problem you face with arc animations is that the beginning path and ending path must have the same number of points in them. My guess is that with a radius of 0, the system simplifies your starting path to a single point, or even an empty path.
You might want to instead use a starting radius of 0.01.
Another option would be to animate the layer's scale from 0 to 1 and set the size of the image to the desired end size.

Related

Swift Animate Mask Transition

How can you animate a mask moving down X pixels?
I have a circular mask applied to a black view that creates a spotlight effect. I want to animate the mask transitioning down 100 pixels, but using UIView.animate { mask.transform = CATransform3DTranslate(.init(), 0, 100, 0) } instantly moves it instead of animating the transition.
Here's the mask code:
private lazy var mask: CAShapeLayer = {
let mask = CAShapeLayer()
let path = CGMutablePath()
path.addArc(center: CGPoint(x: 0, y: 0), radius: 40, startAngle: 0, endAngle: 2.0 * .pi, clockwise: false)
path.addRect(CGRect(origin: .zero, size: view.frame.size))
mask.backgroundColor = UIColor.black.cgColor
mask.path = path
mask.fillRule = .evenOdd
return mask
}()
overlayView.layer.mask = mask
How would you animate this moving down 100 pixels?
To get the spotlight effect, you'll want to create paths that modify the arc shape and animate between them.
func spotlightPath(arcCenter: CGPoint) -> CGPath {
let path = CGMutablePath()
// move the spotlight
path.addArc(center: CGPoint(x: arcCenter.x, y: arcCenter.y), radius: 40, startAngle: 0, endAngle: 2.0 * .pi, clockwise: false)
path.addRect(CGRect(origin: .zero, size: view.frame.size))
return path
}
let startPath = spotlightPath(arcCenter: .zero)
And then later
let endPath = spotlightPath(arcCenter: CGPoint(x: 40, y: 100))
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 1
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.toValue = endPath
mask.add(animation, forKey: "path")
Try this Core animation block. CALayer animation is not supported by UIView animation block. To animate mask property we use the path key.
let anim = CABasicAnimation(keyPath: "path")
anim.toValue = createPath(center: .init(x: 100, y: 100))
anim.duration = 1
anim.fillMode = .forwards
anim.timingFunction = CAMediaTimingFunction.init(name: .easeInEaseOut)
anim.isRemovedOnCompletion = false
mask.add(anim, forKey: anim.keyPath)

How to call UIBezier outline in Swift Playground?

EDIT: Sorry, I wasn't clear originally. I want to get the "outline" path of a line or shape. I'm specifically trying to understand how to use:
context.replacePathWithStrokedPath()
and / or:
CGPathRef CGPathCreateCopyByStrokingPath(CGPathRef path, const CGAffineTransform *transform, CGFloat lineWidth, CGLineCap lineCap, CGLineJoin lineJoin, CGFloat miterLimit);
https://developer.apple.com/documentation/coregraphics/1411128-cgpathcreatecopybystrokingpath?language=objc
I'm not looking for workarounds, thanks.
=====
I'm really trying to wrap my head around drawing a line with an outline around it. i'm using UIBezier, but running into brick walls. So far, I've got this:
import UIKit
import PlaygroundSupport
let screenWidth = 375.0 // points
let screenHeight = 467.0 // points
let centerX = screenWidth / 2.0
let centerY = screenHeight / 2.0
let screenCenterCoordinate = CGPoint(x: centerX, y: centerY)
class LineDrawingView: UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
path.lineWidth = 5
path.lineCapStyle = .round
//Move to Drawing Point
path.move(to: CGPoint(x:20, y:120))
path.addLine(to: CGPoint(x:200, y:120))
path.stroke()
let dot = UIBezierPath()
dot.lineWidth = 1
dot.lineCapStyle = .round
dot.move(to: CGPoint(x:200, y:120))
dot.addArc(withCenter: CGPoint(x:200, y:120), radius: 5, startAngle: CGFloat(0.0), endAngle: CGFloat(8.0), clockwise: true)
UIColor.orange.setStroke()
UIColor.orange.setFill()
path.stroke()
dot.fill()
let myStrokedPath = UIBezierPath.copy(path)
myStrokedPath().stroke()
}
}
let tView = LineDrawingView(frame: CGRect(x: 0,y: 0, width: screenWidth, height: screenHeight))
tView.backgroundColor = UIColor.white
PlaygroundPage.current.liveView = tView
So, where am I going wrong in this? I cannot seem to figure out where to use CGPathCreateCopyByStrokingPath...or how...
EDIT 2:
Ok, now I've got this. Closer, but how do I fill the path again?
let c = UIGraphicsGetCurrentContext()!
c.setLineWidth(15.0)
let clipPath = UIBezierPath(arcCenter: CGPoint(x:centerX,y:centerY), radius: 90.0, startAngle: -0.5 * .pi, endAngle: 1.0 * .pi, clockwise: true).cgPath
c.addPath(clipPath)
c.saveGState()
c.replacePathWithStrokedPath()
c.setLineWidth(0.2)
c.setStrokeColor(UIColor.black.cgColor)
c.strokePath()
The class was modified slightly to produce this graphic:
The path was not copied in the modified code. Instead the existing path was used to draw, then modified and reused. The dot did not have a stroke so that was added. Since only closed paths can be filled, I drew a thinner path on top of a thicker path by changing the line width.
This is the modified code:
class LineDrawingView: UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
path.lineWidth = 7
path.lineCapStyle = .round
//Move to Drawing Point
path.move(to: CGPoint(x:20, y:120))
path.addLine(to: CGPoint(x:200, y:120))
path.stroke()
let dot = UIBezierPath()
dot.lineWidth = 1
dot.lineCapStyle = .round
dot.move(to: CGPoint(x:200, y:120))
dot.addArc(withCenter: CGPoint(x:200, y:120), radius: 5, startAngle: CGFloat(0.0), endAngle: CGFloat(8.0), clockwise: true)
dot.stroke()
UIColor.orange.setStroke()
UIColor.orange.setFill()
path.lineWidth = 5
path.stroke()
dot.fill()
}
}
So, I've found the (an) answer. I used CAShapeLayer:
let c = UIGraphicsGetCurrentContext()!
c.setLineCap(.round)
c.setLineWidth(15.0)
c.addArc(center: CGPoint(x:centerX,y:centerY), radius: 90.0, startAngle: -0.5 * .pi, endAngle: (-0.5 * .pi) + (3 / 2 * .pi ), clockwise: false)
c.replacePathWithStrokedPath()
let shape = CAShapeLayer()
shape.path = c.path
shape.fillColor = UIColor.yellow.cgColor
shape.strokeColor = UIColor.darkGray.cgColor
shape.lineWidth = 1
myView.layer.addSublayer(shape)
It works well enough, but not on overlapping layers. I need to learn how to connect contours or something.

How to move circle with lefteyetransform ARKit2

I'm trying to develop a game that moves the circle according the left eye
let screenSize = UIScreen.main.bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
let camera = self.sceneView.session.currentFrame?.camera
let pippo = simd_mul((anchor.transform),faceAnchor.leftEyeTransform)
var j: SCNVector3 = SCNVector3(pippo.columns.3.x ,pippo.columns.3.y , -pippo.columns.3.z);
self.circlePath = UIBezierPath(arcCenter: CGPoint(x: CGFloat( j.x * Float(screenHeight/2) ),y:CGFloat( j.y * Float(screenWidth/2) )), radius: CGFloat(20), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
self.shapeLayer = CAShapeLayer()
self.shapeLayer.path = self.circlePath.cgPath
self.shapeLayer.fillColor = UIColor.clear.cgColor
self.shapeLayer.strokeColor = UIColor.red.cgColor
self.shapeLayer.lineWidth = 3.0
self.view.layer.addSublayer(self.shapeLayer)
I move a bit the circle, but it is centered on the leftmost part of the screen (instead of being in the middle) and the movement doesn't cover the whole screen, but just a part of it, how can I solve it?
thanks in advance.

Resize radius from 0 to 100 with CABasicAnimation

I want to animate a circle. The circle radius should grow from 0 to 100. I tried it with the transform.scale animation. But of course it is not possible to scale a circle with a radius of 0. When I set the radius of the circle to 1 the circle is visible although it shouldn't be at the beginning of the animation.
minimal example:
let circleShape = CAShapeLayer()
let circlePath = UIBezierPath(arcCenter: .zero, radius: 0, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi*2), clockwise: true)
circleShape.path = circlePath.cgPath
circleShape.fillColor = UIColor.brown.cgColor
let circleAnimation = CABasicAnimation(keyPath: "transform.scale")
circleAnimation.fromValue = 0
circleAnimation.toValue = 100
circleAnimation.duration = 1.9
circleAnimation.beginTime = CACurrentMediaTime() + 1.5
circleShape.add(circleAnimation, forKey: nil)
At the end of the animation the radius should stay at the new value.
EDIT:
Thanks to #Rob mayoff
Final code:
let circleShape = CAShapeLayer()
let circlePath = UIBezierPath(arcCenter: .zero, radius: 400, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi*2), clockwise: true)
circleShape.path = circlePath.cgPath
circleShape.fillColor = UIColor.brown.cgColor
let circleAnimation = CABasicAnimation(keyPath: "transform.scale")
circleAnimation.fromValue = 0.0000001
circleAnimation.toValue = 1
circleAnimation.duration = 1.9
circleAnimation.beginTime = CACurrentMediaTime() + 1.5
circleShape.add(circleAnimation, forKey: nil)
circleAnimation.fillMode = .backwards
Probably what you want to do is set the radius of circlePath to your final radius:
let circlePath = UIBezierPath(arcCenter: .zero, radius: 0, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi*2), clockwise: true)
circlePath.clone()
And then animate from a near-zero number to 1.0:
circleAnimation.fromValue = 0.0001
circleAnimation.toValue = 1.0
If you want to have the animation start in the future (by setting beginTime), then you also probably want to set the fill mode so that the fromValue is applied to the layer until the animation starts:
circleAnimation.fillMode = .backwards
You should use a radius greater 0. Then you can scale it down with circleShape.transform = CATransform3DMakeScale(0.0, 0.0, 0.0)
before adding the layer. And then you could start the animation:
let circleShape = CAShapeLayer()
let center = CGPoint(x: view.frame.width / 2, y: view.frame.height / 2)
let circlePath = UIBezierPath(arcCenter: center, radius: 100.0, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi*2), clockwise: true)
circleShape.transform = CATransform3DMakeScale(0.0, 0.0, 0.0)
circleShape.path = circlePath.cgPath
circleShape.fillColor = UIColor.brown.cgColor
let circleAnimation = CABasicAnimation(keyPath: "transform.scale")
circleAnimation.fromValue = 0
circleAnimation.toValue = 1
circleAnimation.duration = 5.9
circleAnimation.beginTime = CACurrentMediaTime() + 1.5
circleShape.add(circleAnimation, forKey: nil)
view.layer.addSublayer(circleShape)

How to animate drawing shape fit in 60 second?

I'm new to Swift and I don't have too much information about it.I would like to create animated clock with filling 'CAShapeLayer' path in time interval than tried to make it with 'CABasicAnimation' in 60 seconds. The shape fills in 49 sec and animation finish fit in 60 seconds, so it's not working as desired. Than I changed animation.byValue = 0.1 but the result is same. Some one have an idea about this issue?
My code is :
var startAngle = -CGFloat.pi / 2
var endAngle = CGFloat.pi * 2
var shapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
let center = view.center
let circlePath = UIBezierPath(arcCenter: center, radius: 100, startAngle: startAngle, endAngle: endAngle, clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.lineWidth = 20
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
animate(layer: shapeLayer) //Animation func
view.layer.addSublayer(shapeLayer)
}
private func animate(layer: CAShapeLayer) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 60.0 //I think is counting by seconds?
animation.repeatCount = .infinity
animation.fillMode = kCAFillModeForwards
layer.add(animation, forKey: "strokeEndAnimation")
}
The problem is with the way you made the BezierPath.
Try this instead:
let circlePath = UIBezierPath(arcCenter: .zero, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.lineWidth = 20
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = 0
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
shapeLayer.position = self.view.center
shapeLayer.transform = CATransform3DRotate(CATransform3DIdentity, -CGFloat.pi / 2, 0, 0, 1)
The arcCenter must be at .zero and set the shape's center as the center of the view. Your circle will start animating from the right most point though, therefore add a CATransform to rotate the shape 90 degrees counterclockwise.