I am using this code to move a UIImage along a curve:
// paint curve for sun
let path = UIBezierPath()
let imageSunName = "small_sun.png"
let imageSun = UIImage(named: imageSunName)
let imageView = UIImageView(image: imageSun!)
imageView.frame = CGRect(x: xStart, y: yStart, width: 24, height: 24)
self.view.addSubview(imageView)
path.moveToPoint(CGPoint(x: xStart,y: yStart))
path.addQuadCurveToPoint(CGPoint(x: xEnd, y: yEnd), controlPoint: CGPoint(x: xMiddleTop, y: yMiddleTop))
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = path.CGPath
animation.repeatCount = 0
animation.duration = 5.0
imageView.layer.addAnimation(animation, forKey: "animate position along path")
Nevertheless, two issues remain:
I would like to move the image only partly along the path (according to the current time vs sunrise/sunset) - how can this be done?
The image jumps to its origin position after the animation stopped - how can I avoid this?
Cheers
To avoid jump to its original position add:
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
To avoid jumping on animation end, just before starting animation move your image layer to the end position:
imageView.layer.position = CGPoint(x: xEnd, y: yEnd)
To move image partly Im afraid that there is no easy way to do it. An alternate way would be by calculating the endPosition and a path based on sunset/sunrise so you don't have to mess things up with animation time at all
Related
Created CAGradientLayer with a sharp color transition. Position of the color transition changes during the time with using CABasicAnimation.
Everything works, but if I use long duration, animation doesn't produce a smooth change.
Code of gradient creation
override func draw(_ rect: CGRect) {
let path = CGMutablePath()
path.addRect(CGRect(x: 0, y: 0, width: 50, height: 50))
path.move(to: CGPoint(x: 75, y: 75))
path.addRect(CGRect(x: 75, y: 75, width: 50, height: 50))
let mask = CAShapeLayer()
mask.path = path
gradient = CAGradientLayer()
gradient.mask = mask
gradient.colors = [UIColor.orange.cgColor, UIColor.blue.cgColor]
gradient.locations = [0, 0]
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 0)
gradient.frame = bounds
layer.insertSublayer(gradient, at: 0)
}
Animation code
func play() {
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [0, 0]
animation.toValue = [1, 1]
animation.duration = 120
animation.fillMode = .backwards
animation.isRemovedOnCompletion = false
gradient.add(animation, forKey: nil)
}
Demonstration of how the code works
https://i.stack.imgur.com/eUvOo.gif
Are there any options to make the changes smooth with long animation duration?
This looks like a limitation of CAGradientLayer
You can check current progress of CABasicAnimation using layer presentation().
And using CADisplayLink you can check updated value which presentation layer has for each frame, something like this:
...
gradient.add(animation, forKey: nil)
let link = CADisplayLink(target: self, selector: #selector(linkHandler))
link.add(to: .main, forMode: .default)
link.isPaused = false
}
#objc func linkHandler() {
NSLog("\(gradient.presentation()?.locations)")
}
From this test you'll see that value gest updated totally fine, but CAGradientLayer rounds them to around 0.004. Imagine that inside there's code like
locations = locations.map { round($0 * 250) / 250 }
I doubt there's anything you can do about that.
What you can do is use Metal do draw gradient by hand. It's a big and kind of scary topic, but drawing gradient isn't that hard. Check out Rainbows, it's outdated you will have to update the syntax to the current one, but it should be possible
Your draw method is inserting a new gradient layer every time it is called. But this method will be called repeatedly, resulting in multiple layers being inserted. The only thing you should be doing in draw is stroking/filling paths, and in this case, you probably should not implement draw at all.
The insertion of layers does not belong in draw.
I'm creating an IOS application. I have 5 buttons in star shapes created with UIBezierPath. And I want fill them when I tapped specific star. For ex. if I tapped 3-rd star then gradient will fill 3 first stars etc.
I have already created view with 5 stars. In the draw view method I have added code for animating gradient and it works fine only when view is launched.
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
UIColor.black.setStroke()
path.lineWidth = 2
let numberOfPoints = 5
var angle: CGFloat = CGFloat.pi / 2
let angleOfIncrement = CGFloat.pi*2/CGFloat(numberOfPoints)
let radius: CGFloat = rect.origin.x + rect.width/2
let offset = CGPoint(x: rect.origin.x + rect.width/2, y: rect.origin.y + rect.height/2)
let midleRadius = radius*0.45
var firstStar = true
for _ in 1...numberOfPoints {
let firstPoint = getPointLocation(angle: angle, radius: midleRadius, offset: offset)
let midlePoint = getPointLocation(angle: angle+angleOfIncrement/2, radius: radius, offset: offset)
let secondPoint = getPointLocation(angle: angle+angleOfIncrement, radius: midleRadius, offset: offset)
if firstStar {
firstStar = false
path.move(to: firstPoint)
path.addLine(to: midlePoint)
path.addLine(to: secondPoint)
}else {
path.addLine(to: firstPoint)
path.addLine(to: midlePoint)
path.addLine(to: secondPoint)
}
angle += angleOfIncrement
}
path.stroke()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
let gradient = CAGradientLayer(layer: layer)
gradient.colors = [UIColor.black.cgColor, UIColor.white.cgColor]
gradient.locations = [1]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 1)
gradient.frame = path.bounds
self.layer.addSublayer(gradient)
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [0, 0]
animation.toValue = [0, 1.0]
animation.duration = 3
gradient.add(animation, forKey: nil)
self.layer.mask = shapeLayer
}
I expect that when I tapped 3-rd star then gradient will fill first 3 stars.
You have to separate your drawing code from your animation code, and (btw.) you should also (re-)move the layer creation out of draw().
For the separation move
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [0, 0]
animation.toValue = [0, 1.0]
animation.duration = 3
gradient.add(animation, forKey: nil)
into a separate method and call it when needed.
You must also remove the layer creation from draw()because this method is always called if the view needs to be (re-)drawn. This produce a serious memory leak, because for instance every rotation of your iPhone will create a new gradient layer.
You have another issue in your code. Your gradient layer lies over the layer of the view. That cannot work. You may make the gradient layer to the view's layer, and work with mask, to cut the stars out of the gradient.
I am trying to add a pulsating effect around a button, however, the code I am using translates the CAShapeLayer as well as increasing its size.
How do I only increase the scale of a CAShapeLayer during this animation whilst keeping its position in the view static?
I have isolated the code out into a simple project which performs this animation and it is still occurring.
See effect in a video here: https://imgur.com/a/AbTtLKe
To test this:
Create a new project
Add a button into the centre of the view
Link it to the viewControllers code file as an IBOutlet with the name beginButton
Here is my code:
let pulsatingLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
let circularPath = UIBezierPath(arcCenter: beginButton.center, radius: beginButton.bounds.midX, startAngle: -CGFloat.pi / 2, endAngle: 2 * .pi, clockwise: true)
pulsatingLayer.path = circularPath.cgPath
pulsatingLayer.strokeColor = UIColor.clear.cgColor
pulsatingLayer.lineWidth = 10
pulsatingLayer.fillColor = UIColor.red.cgColor
pulsatingLayer.lineCap = kCALineCapRound
view.layer.addSublayer(pulsatingLayer)
animatePulsatingLayer()
}
private func animatePulsatingLayer() {
let pulseAnimation = CABasicAnimation(keyPath: "transform.scale")
pulseAnimation.toValue = 1.5
pulseAnimation.duration = 1
pulseAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
pulseAnimation.autoreverses = true
pulseAnimation.repeatCount = Float.infinity
pulsatingLayer.add(pulseAnimation, forKey: "pulsing")
}
Thanks!
Your animation is relative to the origin of the frame of the view.
By changing the center of the circular path to be CGPoint.zero, you get an animation that pulses centered on the origin of the layer. Then by adding that to a new pulsatingView whose origin is centered on the button, the pulsing layer is centered on the button.
override func viewDidLoad() {
super.viewDidLoad()
let circularPath = UIBezierPath(arcCenter: CGPoint.zero, radius: beginButton.bounds.midX, startAngle: -CGFloat.pi / 2, endAngle: 2 * .pi, clockwise: true)
pulsatingLayer.path = circularPath.cgPath
pulsatingLayer.strokeColor = UIColor.clear.cgColor
pulsatingLayer.lineWidth = 10
pulsatingLayer.fillColor = UIColor.red.cgColor
pulsatingLayer.lineCap = kCALineCapRound
let pulsatingView = UIView(frame: .zero)
view.addSubview(pulsatingView)
view.bringSubview(toFront: beginButton)
// use Auto Layout to place the new pulsatingView relative to the button
pulsatingView.translatesAutoresizingMaskIntoConstraints = false
pulsatingView.leadingAnchor.constraint(equalTo: beginButton.centerXAnchor).isActive = true
pulsatingView.topAnchor.constraint(equalTo: beginButton.centerYAnchor).isActive = true
pulsatingView.layer.addSublayer(pulsatingLayer)
animatePulsatingLayer()
}
You need to read up on transformation matrixes. (Any book on 3D computer graphics should have a section that covers it.)
A scale transformation is centered on the current origin. So if your shape is not centered on the origin, it will be drawn in towards the origin. To scale a shape in toward it's own center you need to do something like this:
Create an identity transform
Translate it by (-x, -y) of your shape's center
Add the desired scale to the transform
Add a Translate by (x, y) to shift the shape back
Concat your transform to the layer's transform.
(I always have to think really hard to get this stuff right. I wrote the above off the top of my head, before 7:00 AM local time on a weekend, without sufficient caffeine. It probably isn't exactly right, but should give you the idea.)
I have a circle in my view, With a little oval that can be moved around that big circle, the idea of moving the little oval is to change the time on the clock. I am trying to move the oval but only by following the path so it won't get out of the circle. As if it was a rail road and the train only follows the rail roads but at the time follows the finger position.
Here I attach the image Image
I tried creating an animation, and maybe use that animation to then follow the finger. I don't really know how to do this. Any help please?
Here is my code for the animation (that don't work):
let customPath = UIBezierPath()
customPath.move(to: CGPoint.init(x: 160, y: 165))
customPath.addLine(to: CGPoint.init(x: 260, y: 245))
customPath.addLine(to: CGPoint.init(x: 165, y: 365))
customPath.addLine(to: CGPoint.init(x: 60, y: 260))
let movingImage = UIImage(named: "MoveOval")
let movingLayer = CALayer()
movingLayer.anchorPoint = CGPoint.zero
movingLayer.contents = moveOval
movingLayer.frame = CGRect.init(x: 0.0, y: 0.0, width: (movingImage?.size.width)!, height: (movingImage?.size.height)!)
self.view.layer.addSublayer(movingLayer)
let pathAnimation = CAKeyframeAnimation(keyPath: "anim")
pathAnimation.duration = 4.0
pathAnimation.path = customPath.cgPath
pathAnimation.calculationMode = kCAAnimationLinear
movingLayer.add(pathAnimation, forKey: "anim")
I am pretty new to iOS, I am facing a small problem about the CAKeyframeAnimation, I want to animate a view to a certain path of another view layer. it is already successfully animating. However, the position of the animation is not what i expected.
As you can see in my code. I create a UIView(myView) with round bounds. I want another view(square) to follow the orbit of myView's bounds. I already set the myView's centre to the middle of the screen. then I try to get the CGPath of myView and set it to CAKeyframeAnimation's path. However, the square is rotate somewhere else, not on the bounds of myView. Could anyone help me? thanks
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
let square = UIView()
square.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
square.backgroundColor = UIColor.redColor()
self.view.addSubview(square)
let bounds = CGRect(x: 0, y: 0, width: 200, height: 200)
let myView = UIView(frame: bounds)
myView.center = self.view.center
myView.layer.cornerRadius = bounds.width/2
myView.layer.borderWidth = 10
myView.layer.borderColor = UIColor.brownColor().CGColor
self.view.addSubview(myView)
var orbit = CAKeyframeAnimation(keyPath: "position")
orbit.path = CGPathCreateWithEllipseInRect(myView.layer.bounds, nil)
orbit.rotationMode = kCAAnimationRotateAuto
orbit.repeatCount = Float.infinity
orbit.duration = 5.0
square.layer.addAnimation(orbit, forKey: "orbit")
}
The frame "describes the view’s location and size in its superview’s coordinate system," whereas bounds "describes the view’s location and size in its own coordinate system."
So if you're building the path from bounds, then square would have to be a subview of myView. If you constructed the path using the frame of myView, then square could be a subview of view.
So, I find another solution, instead of create the CGPath based on some view already existed, I used
let myPath = UIBezierPath(arcCenter: myView.center, radius: bounds.width/2, startAngle: CGFloat(-(CGFloat(M_PI_2))), endAngle: CGFloat((2.0 * M_PI - M_PI_2)), clockwise: true).CGPath
in this way, I can specify the centre point of the UIBezierPath.