Substract CALayer from another, blend? - ios

On the following image, the blue circle is only for debug purpose. My goal is that every layer behind the blue circle should be transparent.I only want to keep visible what's outside the blue circle.
Here is the code written in swift :
let croissantView = UIView(frame: CGRectMake(100, 100, 100, 100))
croissantView.backgroundColor = UIColor.clearColor()
self.view.addSubview(croissantView)
let radius = 50 as CGFloat
let width = 50 as CGFloat
let origin = CGPointMake(0, 100)
let end = CGPointMake(100,100)
let highAmplitude = CGPointMake(50,180)
let lowAmplitude = CGPointMake(50,150)
let quad = UIBezierPath()
quad.moveToPoint(origin)
quad.addQuadCurveToPoint(end, controlPoint: highAmplitude)
quad.closePath()
let quad2 = UIBezierPath()
quad2.moveToPoint(origin)
quad2.addQuadCurveToPoint(end, controlPoint: lowAmplitude)
quad2.closePath()
let layer = CAShapeLayer()
layer.path = quad.CGPath
layer.strokeColor = UIColor.greenColor().CGColor
layer.fillColor = UIColor.blackColor().CGColor
croissantView.layer.addSublayer(layer)
let anim = CABasicAnimation(keyPath: "path")
anim.duration = 3
anim.repeatCount = 20
anim.fromValue = quad.CGPath
anim.toValue = quad2.CGPath
anim.autoreverses = true
layer.addAnimation(anim, forKey: "animQuad")
let animRotate = CABasicAnimation(keyPath: "transform.rotation")
animRotate.duration = 5
animRotate.repeatCount = 20
animRotate.fromValue = 0
animRotate.toValue = 360 * CGFloat(M_PI) / 180
croissantView.layer.addAnimation(animRotate, forKey: "animRotate")
let circle = UIBezierPath(arcCenter: croissantView.center, radius: 75, startAngle: 0, endAngle: 360 * CGFloat(M_PI) / 180, clockwise: true);
let circleLayer = CAShapeLayer()
circleLayer.path = circle.CGPath
circleLayer.fillColor = UIColor.blueColor().CGColor
circleLayer.opacity = 0.6
self.view.layer.addSublayer(circleLayer)
Thoughts
Substract from Layer, don't know if feasible
Blend, don't know how to do it with CALayer
Thanks !! :)

There are three things I would suggest.
Change your croissant view to a layer (not sure this was necessary, but this is what I did)
Create a CGPath for your circle shape that can have its mask inverted. You do this by wrapping an outer rect around your circle so that the path looks like this:
Set your fill rule on your circle shape layer to even-odd:
circleLayer.fillRule = kCAFillRuleEvenOdd
This is what it looks like:
I adapted your code to attempt it and this is what I came up with. Your mileage may vary:
func applyMask() {
let mainLayer = CALayer()
mainLayer.bounds = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
mainLayer.position = CGPoint(x: 200.0, y: 200.0)
// Set the color for debugging
mainLayer.backgroundColor = UIColor.orangeColor().CGColor
self.view.layer.addSublayer(mainLayer)
let radius = 50 as CGFloat
let width = 50 as CGFloat
let origin = CGPointMake(0, 100)
let end = CGPointMake(100,100)
let highAmplitude = CGPointMake(50,180)
let lowAmplitude = CGPointMake(50,150)
let quad = UIBezierPath()
quad.moveToPoint(origin)
quad.addQuadCurveToPoint(end, controlPoint: highAmplitude)
quad.closePath()
let quad2 = UIBezierPath()
quad2.moveToPoint(origin)
quad2.addQuadCurveToPoint(end, controlPoint: lowAmplitude)
quad2.closePath()
let layer = CAShapeLayer()
layer.path = quad.CGPath
layer.strokeColor = UIColor.greenColor().CGColor
layer.fillColor = UIColor.blackColor().CGColor
mainLayer.addSublayer(layer)
let anim = CABasicAnimation(keyPath: "path")
anim.duration = 3
anim.repeatCount = 20
anim.fromValue = quad.CGPath
anim.toValue = quad2.CGPath
anim.autoreverses = true
layer.addAnimation(anim, forKey: "animQuad")
let animRotate = CABasicAnimation(keyPath: "transform.rotation")
animRotate.duration = 5
animRotate.repeatCount = 20
animRotate.fromValue = 0
animRotate.toValue = 360 * CGFloat(M_PI) / 180
mainLayer.addAnimation(animRotate, forKey: "animRotate")
// Build a Circle CGPath
var circlePath = CGPathCreateMutable()
CGPathAddEllipseInRect(circlePath, nil, CGRectInset(mainLayer.bounds, -20.0, -20.0))
// Invert the mask by adding a bounding rectangle
CGPathAddRect(circlePath, nil, CGRectInset(mainLayer.bounds, -100.0, -100.0))
CGPathCloseSubpath(circlePath)
let circleLayer = CAShapeLayer()
circleLayer.fillColor = UIColor.blueColor().CGColor
circleLayer.opacity = 0.6
// Use the even odd fill rule so that our path gets inverted
circleLayer.fillRule = kCAFillRuleEvenOdd
circleLayer.path = circlePath
mainLayer.mask = circleLayer
mainLayer.addSublayer(layer)
}
Here's the project where I tried it out: https://github.com/perlmunger/SwiftVertedMask
Hope that helps.

Related

How to animate object using bezier path?

Here is my code to draw dashed line
func drawLine() {
let shapeLayer = CAShapeLayer()
shapeLayer.bounds = viewDraw.bounds
shapeLayer.position = CGPoint(x: viewDraw.bounds.width/2, y: viewDraw.bounds.height/2)
shapeLayer.fillColor = nil
shapeLayer.strokeColor = ColorConstants.ThemeColor.cgColor
shapeLayer.lineWidth = 3
shapeLayer.lineJoin = CAShapeLayerLineJoin.round // Updated in swift 4.2
shapeLayer.lineDashPattern = [10]
let path = UIBezierPath(roundedRect: viewDraw.bounds, cornerRadius: viewDraw.bounds.size.height/2)
shapeLayer.path = path.cgPath
viewDraw.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 1
shapeLayer.add(animation, forKey: "MyAnimation")
}
Now as per my screenshot I want to animate blue dot to red dot within that dashed line infinite time. When blue dot reaches red dot then it will start again with blue dot points.
Note: This whole screen is dynamic so, Line draw is also dynamic according to screen height
Any help would be appreciated
You are almost there
Now you have to add animation using CAKeyframeAnimation which has property path where you can pass the path where you want to animate
here is working sample code
func drawLine() {
let shapeLayer = CAShapeLayer()
shapeLayer.bounds = self.view.bounds
shapeLayer.position = CGPoint(x: self.view.bounds.width/2, y: self.view.bounds.height/2)
shapeLayer.fillColor = nil
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 3
shapeLayer.lineJoin = CAShapeLayerLineJoin.round // Updated in swift 4.2
shapeLayer.lineDashPattern = [10]
let path = UIBezierPath(roundedRect: self.view.bounds, cornerRadius: self.view.bounds.size.height/2)
shapeLayer.path = path.cgPath
self.view.layer.addSublayer(shapeLayer)
let animation1 = CABasicAnimation(keyPath: "strokeEnd")
animation1.fromValue = 0
animation1.duration = 1
shapeLayer.add(animation1, forKey: "MyAnimation")
// Create squareView and add animation
let animation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
animation.duration = 1
animation.repeatCount = MAXFLOAT
animation.path = path.cgPath
let squareView = UIView()
// Don't worry about x,y and width and height it will not affect the animation
squareView.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
squareView.backgroundColor = .lightGray
view.addSubview(squareView)
// You can also pass any unique string value for key
squareView.layer.add(animation, forKey: nil)
}
You can see this tutorial for more help http://mathewsanders.com/animations-in-swift-part-two/

animate CAShapeLayer on circular path

I want to animate my thumb layer on the drawn path. I can animate the progress layer as per the value but, moving the entire layer doesn't work.
func setProgress(_ value:Double, _ animated :Bool) {
let progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
progressAnimation.duration = animated ? 0.6 : 0.0
progressAnimation.fromValue = currentValue
progressAnimation.toValue = value
progressAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
progressLayer.strokeEnd = CGFloat(value)
progressLayer.add(progressAnimation, forKey: "animateprogress")
}
Using this function I can animate the progress layer. and I'm stuck on animating thumb position.
I tried some things but, it's not working.
failed attempt 1
guard let path = thumbLayer.path else {return}
let center = CGPoint(x: frame.width/2, y: frame.height/2)
let radius = min(frame.width, frame.height)/2 - max(trackWidth, max(progressWidth, thumbWidth))/2
let thumbAngle = 2 * .pi * currentValue - (.pi/2)
let thumbX = CGFloat(cos(thumbAngle)) * radius
let thumbY = CGFloat(sin(thumbAngle)) * radius
let newThumbCenter = CGPoint(x: center.x + thumbX, y: center.y + thumbY)
let thumbPath = UIBezierPath(arcCenter: newThumbCenter, radius: thumbWidth/2, startAngle: -.pi/2, endAngle: .pi*1.5, clockwise: true)
let thumbAnimation = CABasicAnimation(keyPath: "path")
thumbAnimation.duration = animated ? 0.6 : 0.0
thumbAnimation.fromValue = path
thumbAnimation.toValue = thumbPath.cgPath
thumbAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
thumbLayer.strokeEnd = CGFloat(value)
thumbLayer.add(thumbAnimation, forKey: "animateprogress")
failed attempt 2
let intialPosition = thumbLayer.position
let center = CGPoint(x: frame.width/2, y: frame.height/2)
let radius = min(frame.width, frame.height)/2 - max(trackWidth, max(progressWidth, thumbWidth))/2
let thumbAngle = 2 * .pi * currentValue - (.pi/2)
let thumbX = CGFloat(cos(thumbAngle)) * radius
let thumbY = CGFloat(sin(thumbAngle)) * radius
let newThumbCenter = CGPoint(x: center.x + thumbX - thumbCenter.x, y: center.y + thumbY - thumbCenter.y)
thumbLayer.position.x = newThumbCenter.x
let thumbAnimationX = CABasicAnimation(keyPath: "progress.x")
thumbAnimationX.duration = animated ? 0.6 : 0.0
thumbAnimationX.fromValue = intialPosition.x
thumbAnimationX.toValue = newThumbCenter.x
thumbAnimationX.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
thumbLayer.add(thumbAnimationX, forKey: "progress.x")
thumbLayer.position.y = newThumbCenter.y
let thumbAnimationY = CABasicAnimation(keyPath: "progress.y")
thumbAnimationY.duration = animated ? 0.6 : 0.0
thumbAnimationY.fromValue = intialPosition.y
thumbAnimationY.toValue = newThumbCenter.y
thumbAnimationY.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
thumbLayer.add(thumbAnimationY, forKey: "progress.y")
In your first attempt, you're trying to animate the thumb's path (the outline of the red circle). In your second attempt, you're trying to animate the position of thumbLayer (but you animated the wrong key paths).
Since you're having difficulty making either of those work, let's try a different approach. Take a look at the standard Swiss railway clock:
Specifically, look at the red dot at the end of the second hand. How does the clock move that red dot around the face? By rotating the second hand around the center of the face.
That's what we're going to do, but we won't draw the whole second hand. We'll just draw the red dot at the end.
We'll put the origin of the thumbLayer's coordinate system at the center of the circular track, and we'll set its path so that the thumb appears on the path. Here's the layout:
Here's code to set up a circleLayer and a thumbLayer:
private let circleLayer = CAShapeLayer()
private let thumbLayer = CAShapeLayer()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let circleRadius: CGFloat = 100
let thumbRadius: CGFloat = 22
let circleCenter = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
circleLayer.fillColor = nil
circleLayer.strokeColor = UIColor.black.cgColor
circleLayer.lineWidth = 4
circleLayer.lineJoin = .round
circleLayer.path = CGPath(ellipseIn: CGRect(x: -circleRadius, y: -circleRadius, width: 2 * circleRadius, height: 2 * circleRadius), transform: nil)
circleLayer.position = circleCenter
view.layer.addSublayer(circleLayer)
thumbLayer.fillColor = UIColor.red.cgColor
thumbLayer.strokeColor = nil
thumbLayer.path = CGPath(ellipseIn: CGRect(x: -thumbRadius, y: -circleRadius - thumbRadius, width: 2 * thumbRadius, height: 2 * thumbRadius), transform: nil)
thumbLayer.position = circleCenter
view.layer.addSublayer(thumbLayer)
}
With this layout, we can move the thumb along the track without changing its position. Instead, we rotate thumbLayer around its origin:
#IBAction func buttonWasTapped() {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.fromValue = 0
animation.toValue = 2 * CGFloat.pi
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.duration = 1.5
animation.isAdditive = true
thumbLayer.add(animation, forKey: nil)
}
Result:

how to control strokeEnd animation Line Speed

I want to create an atomic motion animation in iOS, but there are some trouble could not solve.
here is my animation result
CAKeyframeAnimation position animation along bezierPath does not match the CABasicAnimation strokeEnd animation(particles animation does not match their wake animation).
2.The wake animation loop is incoherent, CABasicAnimation seems can't represent the Curving segment that strokeStart is positive and strokeEnd is negative.
my code:
let atom = CAShapeLayer()
var rect = CGRect(x: 0,y: 0,width: 5,height: 5)
var path = UIBezierPath(roundedRect: rect,cornerRadius: 5.0)
atom.path = path.CGPath;
atom.strokeColor = UIColor.greenColor().CGColor
atom.bounds = rect;
rect = CGRect(x: 0,y: 0,width: 300,height: 50);
path = UIBezierPath(ovalInRect:rect);
let orbit = CAShapeLayer()
orbit.path = path.CGPath;
orbit.strokeColor = UIColor.whiteColor().CGColor
orbit.lineWidth = 1;
orbit.fillColor = UIColor.clearColor().CGColor
orbit.bounds = CGPathGetPathBoundingBox(path.CGPath);
orbit.position = CGPoint(x:150,y:25)
rect = CGRect(x: 0,y: 0,width: 300,height: 50);
path = UIBezierPath(ovalInRect:rect);
let keyani = CAKeyframeAnimation(keyPath: "position");
keyani.path = path.CGPath
keyani.duration = 3.0;
keyani.repeatCount = HUGE;
keyani.additive = true
keyani.calculationMode = kCAAnimationPaced
keyani.rotationMode = kCAAnimationRotateAuto
keyani.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionLinear);
atom.addAnimation(keyani, forKey: "")
let orbitAni = CABasicAnimation(keyPath: "strokeStart")
orbitAni.byValue = 1.0
orbitAni.cumulative = true
let orbitAni1 = CABasicAnimation(keyPath: "strokeEnd")
orbitAni1.fromValue = 0.0
orbitAni1.toValue = 1.0
let animationGroup = CAAnimationGroup()
animationGroup.animations = [orbitAni,orbitAni1]
animationGroup.duration = 3.0
animationGroup.repeatCount = HUGE
orbit.strokeStart = -0.2
orbit.addAnimation(animationGroup, forKey: "");
orbit.shadowRadius = 5.0;
orbit.shadowColor = UIColor.greenColor().CGColor
orbit.shadowOpacity = 0.5
orbit.shadowOffset = CGSizeZero
var trans = CGAffineTransformIdentity
trans = CGAffineTransformRotate(trans, CGFloat(M_PI_4))
repeator.instanceTransform = CATransform3DMakeAffineTransform (trans)
repeator.instanceCount = 4;
repeator.instanceDelay = 0;
repeator.addSublayer(orbit)
repeator.addSublayer(atom)
repeator.bounds = CGRect(x: 0,y: 0,width: 300,height: 50)
repeator.position = CGPoint(x:200,y:200)
repeator.anchorPoint = CGPoint(x:0.5,y:0.5)
repeator.instanceAlphaOffset = 1.0

How to get an animation inside a view image frame?

I made an animation, but I want to put it in a UIImageView so that I can move it around, clear it or restart it.
This is my code:
#IBOutlet var circle: UIImageView!
// I want to make this inside the circle UIImageView or something like that.
let ovalStartAngle = CGFloat(90.01 * M_PI/180)
let ovalEndAngle = CGFloat(90 * M_PI/180)
let ovalRect = CGRectMake(97.5, 230, 125, 125)
let ovalPath = UIBezierPath()
ovalPath.addArcWithCenter(CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)),
radius: CGRectGetWidth(ovalRect) / 2,
startAngle: ovalStartAngle,
endAngle: ovalEndAngle, clockwise: true)
let progressLine = CAShapeLayer()
progressLine.path = ovalPath.CGPath
progressLine.strokeColor = UIColor.blueColor().CGColor
progressLine.fillColor = UIColor.clearColor().CGColor
progressLine.lineWidth = 10.0
progressLine.lineCap = kCALineCapRound
self.view.layer.addSublayer(progressLine)
let animateStrokeEnd = CABasicAnimation(keyPath: "strokeEnd")
animateStrokeEnd.duration = 10.0
animateStrokeEnd.fromValue = 0.0
animateStrokeEnd.toValue = 1.0
progressLine.addAnimation(animateStrokeEnd, forKey: "animate stroke end animation")

Animating circular progress View in Swift

I want to have an animation for my circular progressView with the code below. I want the animation start the progress from 0 to the value I want.
But I can never get the animation work.
func Popular(){
let circle = Pop;//Pop is a UIView
circle.bounds = CGRect(x: 0,y: 0, width: 100, height: 100);
circle.frame = CGRectMake(0,0, 100, 100);
circle.layoutIfNeeded()
var progressCircle = CAShapeLayer();
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2);
let circleRadius : CGFloat = circle.bounds.width / 2 * 0.83;
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.5 * M_PI), endAngle: CGFloat(1.5 * M_PI), clockwise: true );
progressCircle = CAShapeLayer ();
progressCircle.path = circlePath.CGPath;
progressCircle.strokeColor = UIColor.greenColor().CGColor;
progressCircle.fillColor = UIColor.clearColor().CGColor;
UIView.animateWithDuration(3.0,
delay: 1.0,
options: [],
animations: {
progressCircle.lineWidth = 5.0;
progressCircle.strokeStart = 0;
progressCircle.strokeEnd = 0.20;
circle.layer.addSublayer(progressCircle);
circle
progressCircle.strokeEnd = 0.2;
},
completion: { finished in
print("Picutre done")
})
}
Use CABasicAnimation
let circle = Pop
var progressCircle = CAShapeLayer()
let circlePath = UIBezierPath(ovalInRect: circle.bounds)
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.CGPath
progressCircle.strokeColor = UIColor.greenColor().CGColor
progressCircle.fillColor = UIColor.clearColor().CGColor
progressCircle.lineWidth = 5.0
circle.layer.addSublayer(progressCircle)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 0.2
animation.duration = 3
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
progressCircle.addAnimation(animation, forKey: "ani")
Two things -
First - Do not re-initialize progressCircle. You are calling progressCircle = CAShapeLayer (); twice. This is not bug here though.
Second - Add your sublayer before you start animating it. Move circle.layer.addSublayer(progressCircle); outside the animation block.

Resources