How to animate the end and the control points SWIFT - swift

I have a CAShapeLayer im my view which has the to CGPath casted path of an instance of an Bezierpath. I now want to animate the end and the two control points so e.g. the end of that line is moving(not stopping). How to do that? I looked at CABasicAninmations but did not get how the access the end point. The same with animate with duration. How to make this happen for an endlessness time.

You can use a CADisplayLink (it's like a NSTimer, except optimally timed for animation frame updates) and update the path of the CAShapeLayer.
For example:
var displayLink: CADisplayLink?
var startTime: CFAbsoluteTime?
let duration = 2.0
var shapeLayer: CAShapeLayer?
override func viewDidLoad() {
super.viewDidLoad()
shapeLayer = CAShapeLayer()
shapeLayer?.fillColor = UIColor.clearColor().CGColor
shapeLayer?.strokeColor = UIColor.blueColor().CGColor
shapeLayer?.lineWidth = 10
shapeLayer?.frame = view.bounds
view.layer.addSublayer(shapeLayer!)
startTime = CFAbsoluteTimeGetCurrent()
displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:")
displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
func handleDisplayLink(displayLink: CADisplayLink) {
let elapsed = CFAbsoluteTimeGetCurrent() - startTime!
let percent = (elapsed % duration) / duration
let path = UIBezierPath()
let startPoint = CGPoint(x: 0, y: view.bounds.size.height / 2.0)
let controlPoint = CGPoint(x: view.bounds.size.width / 20, y: view.bounds.size.height * CGFloat(0.5 + sin(percent * M_PI * 2.0) / 2.0))
let endPoint = CGPoint(x: view.bounds.size.width - 1, y: view.bounds.size.height / 2.0)
path.moveToPoint(startPoint)
path.addQuadCurveToPoint(endPoint, controlPoint: controlPoint)
shapeLayer?.path = path.CGPath
}
That yields:

Here are some possibilities.
Configure the animation in advance, that is, by generating a series of drawings, just like the cells of a cartoon, and use simple UIImageView animation of those images.
Use CAShapeLayer and animate between two path values. Unfortunately this doesn't give you total control over the intermediate frames, but you could use keyframe animation to supply intermediate frames and thus provide additional control.
Animate the actual points of the drawing. This is the hardest to configure, but it is true animation and it gives you total control over the animation. That's what I'm doing here: I did it by creating a custom animatable property - in this case, a property representing the x-position of the bottom point of the triangle. You'd have to do that for all the points you want to animate.

Related

Smooth animation CAGradientLayer with long duration

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.

Custom shape to UISlider and update progress in Swift 5

I have checked some of the StackOverflow answers regarding custom UIView slider but using them I unable to make the slider like this. This makes a circle or half circle. I have figured out some library that makes circle slider using UIView but its not helpful to me so could anyone please help me out. How can I make slider like in below UIImage? Thanks!
You will probably just roll your own. (You obviously could search for third party implementations, but that would be out of scope for StackOverflow.) There are a lot of ways of tackling this, but the basic elements here are:
The pink arc for the overall path. Personally, I'd use a CAShapeLayer for that.
The white arc from the start to the current progress (measured from 0 to 1). Again, a CAShapeLayer would be logical.
The white dot placed at the spot of the current progress. Below I create a CALayer with white background and then apply a CAGradientLayer as a mask to that. You could also just create a UIImage for this.
In terms of how to set the progress, you would set the paths of the pink and white arcs to the same path, but just update the strokeEnd of the white arc. You would also adjust the position of the white dot layer accordingly.
The only complicated thing here is figuring out the center of the arc. In my example below, I calculate it with some trigonometry based upon the bounds of the view so that arc goes from lower left corner, to the top, and back down the the lower right corner. Or you might instead pass the center of the arc as a parameter.
Anyway, it might look like:
#IBDesignable
class ArcView: UIView {
#IBInspectable
var lineWidth: CGFloat = 7 { didSet { updatePaths() } }
#IBInspectable
var progress: CGFloat = 0 { didSet { updatePaths() } }
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
progress = 0.35
}
lazy var currentPositionDotLayer: CALayer = {
let layer = CALayer()
layer.backgroundColor = UIColor.white.cgColor
let rect = CGRect(x: 0, y: 0, width: lineWidth * 3, height: lineWidth * 3)
layer.frame = rect
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.white.cgColor, UIColor.clear.cgColor]
gradientLayer.type = .radial
gradientLayer.frame = rect
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
gradientLayer.locations = [0.5, 1]
layer.mask = gradientLayer
return layer
}()
lazy var progressShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = .round
shapeLayer.lineWidth = lineWidth
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
return shapeLayer
}()
lazy var totalShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = .round
shapeLayer.lineWidth = lineWidth
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = #colorLiteral(red: 0.9439327121, green: 0.5454334617, blue: 0.6426400542, alpha: 1)
return shapeLayer
}()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePaths()
}
}
// MARK: - Private utility methods
private extension ArcView {
func configure() {
layer.addSublayer(totalShapeLayer)
layer.addSublayer(progressShapeLayer)
layer.addSublayer(currentPositionDotLayer)
}
func updatePaths() {
let rect = bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
let halfWidth = rect.width / 2
let height = rect.height
let theta = atan(halfWidth / height)
let radius = sqrt(halfWidth * halfWidth + height * height) / 2 / cos(theta)
let center = CGPoint(x: rect.midX, y: rect.minY + radius)
let delta = (.pi / 2 - theta) * 2
let startAngle = .pi * 3 / 2 - delta
let endAngle = .pi * 3 / 2 + delta
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
progressShapeLayer.path = path.cgPath // white arc
totalShapeLayer.path = path.cgPath // pink arc
progressShapeLayer.strokeEnd = progress
let currentAngle = (endAngle - startAngle) * progress + startAngle
let dotCenter = CGPoint(x: center.x + radius * cos(currentAngle),
y: center.y + radius * sin(currentAngle))
currentPositionDotLayer.position = dotCenter
}
}
Above, I set the background color of the ArcView so you could see its bounds, but you would obviously set the background color to be transparent.
Now there are tons of additional features you might add (e.g. add user interaction so you could “scrub” it, etc.). See https://github.com/robertmryan/ArcView for example. But the key when designing this sort of stuff is to just break it down into its constituent elements, layering one on top of the other.
You can programmatically set the progress of the arcView to get it to change the current value between values of 0 and 1:
func startUpdating() {
arcView.progress = 0
var current = 0
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] timer in
current += 1
guard let self = self, current <= 10 else {
timer.invalidate()
return
}
self.arcView.progress = CGFloat(current) / 10
}
}
Resulting in:

CABasicAnimation on transform.scale translates CAShapeLayer on the X & Y axis

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

How to update NSView layer position and anchor point for transform simultaneously with NSView frame for mouse events

I am currently trying to implement some custom NSViews that will be animate by certain events in a macOS app. The controls are along the lines of slider you would find in an AudioUnit Plugin.
The question of animation has been answered in this question.
What I am uncertain of is how to alter a view's CALayer position and anchor point for the rotation and updating its frame for mouse events.
As a basic example, I wish to draw a square by creating an NSView and setting it's background colour. I then want to animate the rotation of the view, a la the previous link, on a mouseDown event.
With following setup, the view is moved to the centre of window, its anchor point is altered so that the view rotates around it and not around the origin of the window.contentView!. However, moving the view.layer.position and setting the view.layer!.anchorPoint = CGPoint(x: 0.5,y: 0.5) does not also move the frame that will detect events.
#IBOutlet weak var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification)
{
let view = customView(frame: NSRect(x: 0, y: 0, width: 50, height: 50))
window.contentView!.wantsLayer = true
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.blue.cgColor
window.contentView?.addSubview(view)
let containerFrame = window.contentView!.frame
let center = CGPoint(x: containerFrame.midX, y: containerFrame.midY)
view.layer?.position = center
view.layer?.anchorPoint = CGPoint(x: 0.5,y: 0.5)
}
The customView in this case is simply an NSView with the rotation animation from the previous link executed on mouseDown
override func mouseDown(with event: NSEvent)
{
let timeToRotate = 1
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = angle
rotateAnimation.duration = timeToRotate
rotateAnimation.repeatCount = .infinity
self.layer?.add(rotateAnimation, forKey: nil)
}
Moving the frame by setting view.frame = view.layer.frame results in the view rotating around one of its corners. So, instead I have altered the view.setFrameOrigin()
#IBOutlet weak var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification)
{
let view = customView(frame: NSRect(x: 0, y: 0, width: 50, height: 50))
window.contentView!.wantsLayer = true
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.blue.cgColor
window.contentView?.addSubview(view)
let containerFrame = window.contentView!.frame
let center = CGPoint(x: containerFrame.midX, y: containerFrame.midY)
let offsetFrameOrigin = CGPoint(x: center.x - view.bounds.width/2,
y: center.y - view.bounds.height/2)
view.setFrameOrigin(offsetFrameOrigin)
view.layer?.position = center
view.layer?.anchorPoint = CGPoint(x: 0.5,y: 0.5)
}
Setting the view.frame origin to an offset of the centre achieves what I want, but I cannot help but feel this is a little 'hacky' and that I may be approaching this the wrong way. Especially since any further change to view.layer or view.frame will result in either the animation being incorrect or events being detected outside what is drawn.
How can I alter NSView.layer so that it rotates around its centre at the same time as setting the NSView.frame so that mouse events are detected in the correct area?
Also, is altering NSView.layer.anchorPoint a correct way to set it up for rotation around its centre?
I think I'm in much the same position as you. Fortunately, I stumbled across a gist that extends NSView with a setAnchorPoint function that does the job for me (keeping the layer's anchor in sync with the main frame).
There's a fork for that gist for Swift 4. Here's the code itself:
extension NSView
{
func setAnchorPoint (anchorPoint:CGPoint)
{
if let layer = self.layer
{
var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: self.bounds.size.width * layer.anchorPoint.x, y: self.bounds.size.height * layer.anchorPoint.y)
newPoint = newPoint.applying(layer.affineTransform())
oldPoint = oldPoint.applying(layer.affineTransform())
var position = layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
layer.position = position
layer.anchorPoint = anchorPoint
}
}
}
You'd then use it like this on the NSView directly (i.e. not its layer):
func applicationDidFinishLaunching(_ aNotification: Notification)
{
// ... //
let view = customView(frame: NSRect(x: 0, y: 0, width: 50, height: 50))
view.wantsLayer = true
view.setAnchorPoint(anchorPoint: CGPoint(x: 0.5, y: 0.5))
// ... //
}

Swift, sprite kit game: Have circle disappear in clockwise manner? On timer?

Alright, so I don't know the name for this but I have a sprite kit game (a runner game) that, when it's game over, is going to have a "save me" button and a timer that runs out accordingly. When the timer runs out, you can no longer click the button and save the character.
I don't want to display this timer in text however- I want a circle that "unwinds itself," if you will, and disappears at the rate that the timer runs out. I.e. when the timer reaches 0, the circle has fully disappeared. The circle disappears degree by degree in a clockwise motion in accordance with the timer.
Here are some pictures to explain what I'm talking about.
How would I do this?
By changing the path property of an SKShapeNode at a fixed interval, you can create a frame-by-frame animation sequence. To create the animation, set the path property to a sequence of shapes that starts with a circle and ends with nothing. You can use UIBezierPath, a wrapper for CGPath, to create shapes for the animation using the following steps:
Move path's "pen" to the center of the circle
Add an arc to the path with addArcWithCenter from a startAngle to endAngle
Add a line to the path from the point on the circle corresponding to the ending angle to the center
Change the endAngle by a fixed amount
Repeat steps 1-4
Here's an implementation of the above steps:
override func didMove(to:SKView) {
let circle = SKShapeNode(circleOfRadius: 50)
circle.fillColor = SKColor.blue
circle.strokeColor = SKColor.clear
circle.zRotation = CGFloat.pi / 2
addChild(circle)
countdown(circle: circle, steps: 20, duration: 5) {
print("done")
}
}
// Creates an animated countdown timer
func countdown(circle:SKShapeNode, steps:Int, duration:TimeInterval, completion:#escaping ()->Void) {
guard let path = circle.path else {
return
}
let radius = path.boundingBox.width/2
let timeInterval = duration/TimeInterval(steps)
let incr = 1 / CGFloat(steps)
var percent = CGFloat(1.0)
let animate = SKAction.run {
percent -= incr
circle.path = self.circle(radius: radius, percent:percent)
}
let wait = SKAction.wait(forDuration:timeInterval)
let action = SKAction.sequence([wait, animate])
run(SKAction.repeat(action,count:steps-1)) {
self.run(SKAction.wait(forDuration:timeInterval)) {
circle.path = nil
completion()
}
}
}
// Creates a CGPath in the shape of a pie with slices missing
func circle(radius:CGFloat, percent:CGFloat) -> CGPath {
let start:CGFloat = 0
let end = CGFloat.pi * 2 * percent
let center = CGPoint.zero
let bezierPath = UIBezierPath()
bezierPath.move(to:center)
bezierPath.addArc(withCenter:center, radius: radius, startAngle: start, endAngle: end, clockwise: true)
bezierPath.addLine(to:center)
return bezierPath.cgPath
}
and a video clip: