I am using Swift 1.2 and my goal is to animate an image mask over a static UIImage. What I have implemented is a swift version of masking an image that I originally found in Objective-C.
func maskImage(image: UIImage, mask: UIImage) -> UIImage! {
let maskRef = mask.CGImage;
let mask = CGImageMaskCreate(
CGImageGetWidth(maskRef),
CGImageGetHeight(maskRef),
CGImageGetBitsPerComponent(maskRef),
CGImageGetBitsPerPixel(maskRef),
CGImageGetBytesPerRow(maskRef),
CGImageGetDataProvider(maskRef), nil, false);
let masked = CGImageCreateWithMask(image.CGImage, mask);
let retImage = UIImage(CGImage: masked);
return retImage;
}
It works great! However, putting it in motion is my challenge.
Is there a way to either iteratively apply the mask with a different horizontal offset or a better way to approach this problem entirely - perhaps with a CALayer implementation?
Thanks for your help!
EDIT: Based on what was posted as an answer, I added this:
let image = UIImage(named: "clouds");
let imageView = UIImageView(image: image);
let layer = CALayer();
layer.contents = UIImage(named: "alpha-mask")?.CGImage;
layer.bounds = CGRectMake(0, 0, image.size.width, image.size.height);
// For other folks learning, this did not work
//let animation = CABasicAnimation(keyPath: "bounds.origin.x");
// This does work
let animation = CABasicAnimation(keyPath: "position.x");
animation.duration = 2;
animation.delegate = self;
animation.fillMode = kCAFillModeForwards;
animation.repeatCount = 0;
animation.fromValue = 0.0;
animation.toValue = image.size.width;
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear);
animation.removedOnCompletion = false;
layer.addAnimation(animation, forKey: "transform");
imageView.layer.mask = layer;
self.addSubview(imageView);
I am able to see the alpha mask properly, but the animation does not work. Any ideas?
EDIT: I modified the code above and it works! I needed to make the keyPath position.x. See above
You do indeed want to use a CALayer - or rather, a CAShapeLayer.
You can create a CAShapeLayer and install it as as the mask on another layer.
You can create a CAAnimation that animates changes to the shape layer's path, or you can animate changes to the layer's strokeStart and/or strokeEnd properties.
If you animate the path, the one rule you want to follow is to make sure that the starting and ending path have the same number and type of control points. Otherwise the animation is "undefined", and the results can be very strange.
I have a development blog post that outlines how it's done:
http://wareto.com/using-core-animation-groups-to-create-animation-sequences-2
It's primarily about using CAAnimationGroups, but it also includes a working example of animating changes to a CAShapeLayer that's used as the mask of an image view's layer.
Below is a GIF of the mask animation that it creates - a "clock wipe" that shows and hides an image view:
Unfortunately it's written in Objective-C, but the Core Animation calls are nearly identical in Swift. Let me know if you have any problems figuring out how to adapt it.
The meat of the animation code is this method:
- (IBAction)doMaskAnimation:(id)sender;
{
waretoLogoLarge.hidden = FALSE;//Show the image view
//Create a shape layer that we will use as a mask for the waretoLogoLarge image view
CAShapeLayer *maskLayer = [CAShapeLayer layer];
CGFloat maskHeight = waretoLogoLarge.layer.bounds.size.height;
CGFloat maskWidth = waretoLogoLarge.layer.bounds.size.width;
CGPoint centerPoint;
centerPoint = CGPointMake( maskWidth/2, maskHeight/2);
//Make the radius of our arc large enough to reach into the corners of the image view.
CGFloat radius = sqrtf(maskWidth * maskWidth + maskHeight * maskHeight)/2;
//Don't fill the path, but stroke it in black.
maskLayer.fillColor = [[UIColor clearColor] CGColor];
maskLayer.strokeColor = [[UIColor blackColor] CGColor];
maskLayer.lineWidth = radius; //Make the line thick enough to completely fill the circle we're drawing
CGMutablePathRef arcPath = CGPathCreateMutable();
//Move to the starting point of the arc so there is no initial line connecting to the arc
CGPathMoveToPoint(arcPath, nil, centerPoint.x, centerPoint.y-radius/2);
//Create an arc at 1/2 our circle radius, with a line thickess of the full circle radius
CGPathAddArc(arcPath,
nil,
centerPoint.x,
centerPoint.y,
radius/2,
3*M_PI/2,
-M_PI/2,
YES);
maskLayer.path = arcPath;
//Start with an empty mask path (draw 0% of the arc)
maskLayer.strokeEnd = 0.0;
CFRelease(arcPath);
//Install the mask layer into out image view's layer.
waretoLogoLarge.layer.mask = maskLayer;
//Set our mask layer's frame to the parent layer's bounds.
waretoLogoLarge.layer.mask.frame = waretoLogoLarge.layer.bounds;
//Create an animation that increases the stroke length to 1, then reverses it back to zero.
CABasicAnimation *swipe = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
swipe.duration = 2;
swipe.delegate = self;
[swipe setValue: theBlock forKey: kAnimationCompletionBlock];
swipe.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionLinear];
swipe.fillMode = kCAFillModeForwards;
swipe.removedOnCompletion = NO;
swipe.autoreverses = YES;
swipe.toValue = [NSNumber numberWithFloat: 1.0];
[maskLayer addAnimation: swipe forKey: #"strokeEnd"];
}
I have another blog entry that IS in Swift that shows how to create and animate a pie chart using a CAShapeLayer. That project animates shape, not a mask, but the only real difference is whether you install the shape layer as a regular content layer or as a mask on another layer like the backing layer of an image view.
You can check out that project at this link:
http://wareto.com/swift-piecharts
Related
I am using this code to create the slide to unlock like animation but I am unable to make it animate from right to left.
How can I make it start from right and animate to left?
-(void)slideToCancel {
CALayer *maskLayer = [CALayer layer];
// Mask image ends with 0.15 opacity on both sides. Set the background color of the layer
// to the same value so the layer can extend the mask image.
maskLayer.backgroundColor = [[UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.50f] CGColor];
maskLayer.contents = (id)[[UIImage imageNamed:#"slidetocancel.png"] CGImage];
// Center the mask image on twice the width of the text layer, so it starts to the left
// of the text layer and moves to its right when we translate it by width.
maskLayer.contentsGravity = kCAGravityCenter;
maskLayer.frame = CGRectMake(_slideToCancelLbl.frame.size.width * -1, 0.0f, _slideToCancelLbl.frame.size.width * 2, _slideToCancelLbl.frame.size.height);
// Animate the mask layer's horizontal position
CABasicAnimation *maskAnim = [CABasicAnimation animationWithKeyPath:#"position.x"];
maskAnim.byValue = [NSNumber numberWithFloat:_slideToCancelLbl.frame.size.width];
maskAnim.repeatCount = 1e100f;
maskAnim.duration = 1.5f;
[maskLayer addAnimation:maskAnim forKey:#"slideAnim"];
_slideToCancelLbl.layer.mask = maskLayer;
}
You should be able to simply use a negative byValue. However, the code you've posted is a canned animation, not one that responds to a slide gesture.
Some year ago i have used this code for realize a chronometer animation.
Was a cute animation but doesn't work with iOS8>
A circle should appears and lose a pie every 0.1sec but The animation doesn't start. It works on my iPhone 5 iOS 7.1
I tried to solve it but after 2 hours i have no solution.
Can someone with more experience with CABasicAnimation help me?
THANK YOU.
This is the code:
//Animazione CRONOMETRO
-(void) startCronometro{
//SetTime
counterStart = TIME;
[sfondoCronometro setImage:[UIImage imageNamed:#"cronoStart.png"]];
maskLayer = [CAShapeLayer layer];
CGFloat maskHeight = sfondoCronometro.frame.size.height;
CGFloat maskWidth = sfondoCronometro.frame.size.width;
CGPoint centerPoint;
centerPoint = CGPointMake(sfondoCronometro.frame.size.width/2, (sfondoCronometro.frame.size.height/2));
//Make the radius of our arc large enough to reach into the corners of the image view.
CGFloat radius = sqrtf(maskWidth * maskWidth + maskHeight * maskHeight)/2;
//Don't fill the path, but stroke it in black.
maskLayer.fillColor = [[UIColor clearColor] CGColor];
maskLayer.strokeColor = [[UIColor blackColor] CGColor];
maskLayer.lineWidth = 25;
CGMutablePathRef arcPath = CGPathCreateMutable();
//Move to the starting point of the arc so there is no initial line connecting to the arc
CGPathMoveToPoint(arcPath, nil, centerPoint.x, centerPoint.y-radius/2);
//Create an arc at 1/2 our circle radius, with a line thickess of the full circle radius
CGPathAddArc(arcPath, nil, centerPoint.x, centerPoint.y, radius/2, 3*M_PI/2, -M_PI/2, NO);
maskLayer.path = arcPath;//[aPath CGPath];//arcPath;
//Start with an empty mask path (draw 0% of the arc)
maskLayer.strokeEnd = 0;
CFRelease(arcPath);
//Install the mask layer into out image view's layer.
sfondoCronometro.layer.mask = maskLayer;
//Set our mask layer's frame to the parent layer's bounds.
sfondoCronometro.layer.mask.frame = sfondoCronometro.layer.bounds;
//Create an animation that increases the stroke length to 1, then reverses it back to zero.
CABasicAnimation *swipe = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
swipe.duration = TIME;
NSLog(#"TIME: %f", swipe.duration);
swipe.delegate = self;
// [swipe setValue:#"string" forKey:#"key"];
swipe.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
swipe.fillMode = kCAFillModeForwards;
swipe.removedOnCompletion = NO;
swipe.toValue = [NSNumber numberWithFloat: 1.0];
[maskLayer addAnimation: swipe forKey: #"strokeEnd"];
}
I seem to remember that at some point the function of CGPathAddArc changed, and depending on the values of your start and end angle, you had to flip the clockwise flag on the call. Try using clockwise = YES.
I just did a little testing in an app of mine and confirmed this. For the angles you're using, clockwise NO works for iOS <= 7, but fails for iOS >=8.
Switch the last parameter from NO to YES.
I have a CABasicAnimation that creates an iris wipe effect on an image. In short, the animation works fine on the simulator but there is no joy on device. The timers still fire correctly and the animationCompleted block gets called however there is no visible animation.
Here is the code to get the iris wipe working:
- (void)irisWipe
{
animationCompletionBlock theBlock;
_resultsImage.hidden = FALSE;//Show the image view
[_resultsImage setImage:[UIImage imageNamed:#"logoNoBoarder"]];
[_resultsImage setBackgroundColor:[UIColor clearColor]];
[_resultsImage setFrame:_imageView.bounds];
//Create a shape layer that we will use as a mask for the waretoLogoLarge image view
CAShapeLayer *maskLayer = [CAShapeLayer layer];
CGFloat maskHeight = _resultsImage.layer.bounds.size.height;
CGFloat maskWidth = _resultsImage.layer.bounds.size.width;
CGPoint centerPoint;
centerPoint = CGPointMake( maskWidth/2, maskHeight/2);
//Make the radius of our arc large enough to reach into the corners of the image view.
CGFloat radius = sqrtf(maskWidth * maskWidth + maskHeight * maskHeight)/2;
// CGFloat radius = MIN(maskWidth, maskHeight)/2;
//Don't fill the path, but stroke it in black.
maskLayer.fillColor = [[UIColor clearColor] CGColor];
maskLayer.strokeColor = [[UIColor blackColor] CGColor];
maskLayer.lineWidth = radius; //Make the line thick enough to completely fill the circle we're drawing
// maskLayer.lineWidth = 10; //Make the line thick enough to completely fill the circle we're drawing
CGMutablePathRef arcPath = CGPathCreateMutable();
//Move to the starting point of the arc so there is no initial line connecting to the arc
CGPathMoveToPoint(arcPath, nil, centerPoint.x, centerPoint.y-radius/2);
//Create an arc at 1/2 our circle radius, with a line thickess of the full circle radius
CGPathAddArc(arcPath,
nil,
centerPoint.x,
centerPoint.y,
radius/2,
3*M_PI/2,
-M_PI/2,
NO);
maskLayer.path = arcPath;
//Start with an empty mask path (draw 0% of the arc)
maskLayer.strokeEnd = 1.0;
CFRelease(arcPath);
//Install the mask layer into out image view's layer.
_resultsImage.layer.mask = maskLayer;
//Set our mask layer's frame to the parent layer's bounds.
_resultsImage.layer.mask.frame = _resultsImage.layer.bounds;
//Create an animation that increases the stroke length to 1, then reverses it back to zero.
CABasicAnimation *swipe = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
swipe.duration = 1;
swipe.delegate = self;
[swipe setValue: theBlock forKey: kAnimationCompletionBlock];
swipe.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
swipe.fillMode = kCAFillModeForwards;
swipe.removedOnCompletion = NO;
swipe.autoreverses = NO;
swipe.toValue = [NSNumber numberWithFloat: 0];
//Set up a completion block that will be called once the animation is completed.
theBlock = ^void(void)
{
NSLog(#"completed");
};
[swipe setValue: theBlock forKey: kAnimationCompletionBlock];
// doingMaskAnimation = TRUE;
[maskLayer addAnimation: swipe forKey: #"strokeEnd"];
}
Is there something in iOS7 I should be aware of when working with CAAnimations etc? OR is there a error in the code?
Note this code was sourced from: How do you achieve a "clock wipe"/ radial wipe effect in iOS?
I think the problem (or at least part of it) may be this line:
[_resultsImage setFrame:_imageView.bounds];
That should read
[_resultsImage setBounds:_imageView.bounds];
Instead. If you set the FRAME to the bounds of the image view, you're going to move the image view to 0.0 in its superview.
I'm also not clear what _imageView is, as opposed to _resultsImage.
I would step through your code in the debugger, looking at the frame rectangles that are being set for the image view and mask layer, and all the other values that are calculated.
Goal:
I am drawing a custom shape that has a gradient. I also want to animate the drawing of this shape by having it draw in from left to right.
Problem:
The code below works on an iPad simulator, but it doesn't work on my iPad 4 running iOS 7. Does anyone know how to make this work on the device? Is there a different way to achieve this same result?
Explanation of my Code:
My code works (only on simulator) using 3 CALayers.
gradientLayer holds my gradient.
shapeMaskLayer holds my custom shape.
animationMaskLayer animates it's path to simulate drawing from left to right
animationMaskLayer --masks--> shapeMaskLayer --masks--> gradientLayer
I then animate the frame of animationMaskLayer to animate the whole shape.
Code:
// Animation Mask Rects
CGPathRef leftStartingRectPath = CGPathCreateWithRect(CGRectMake(0, 0, 0, self.frame.size.height), 0);
CGPathRef fullViewRectPath = CGPathCreateWithRect(CGRectMake(0, 0, self.frame.size.width, self.frame.size.height), 0);
// Animation Mask Layer
CAShapeLayer *animationMaskLayer = [CAShapeLayer layer];
animationMaskLayer.path = fullViewRectPath;
animationMaskLayer.fillColor = UIColor.blackColor.CGColor;
// Shape Mask Layer
CAShapeLayer *shapeMaskLayer = [CAShapeLayer layer];
shapeMaskLayer.path = CGPathCreateWithEllipseInRect(self.bounds, 0);
shapeMaskLayer.fillColor = [UIColor blueColor].CGColor;
shapeMaskLayer.mask = animationMaskLayer;
// Gradient Layer
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.bounds;
gradientLayer.colors = self.colors;
gradientLayer.mask = shapeMaskLayer;
mountainLayer.anchorPoint = pt(0, 0);
mountainLayer.position = pt(0, 0);
[self.layer addSublayer:gradientLayer];
// Left To Right Animation
CABasicAnimation *leftToRightAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
leftToRightAnimation.duration = 0.5;
leftToRightAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
leftToRightAnimation.fromValue = (__bridge id)leftStartingRectPath;
leftToRightAnimation.toValue = (__bridge id)fullViewRectPath;
leftToRightAnimation.fillMode = kCAFillModeForwards;
leftToRightAnimation.removedOnCompletion = NO;
[animationMaskLayer addAnimation:leftToRightAnimation forKey:#"animatePath"];
// Memory Management
CGPathRelease(leftStartingRectPath);
CGPathRelease(fullViewRectPath);
Animating the mask of a mask? I'm surprised that even works in the simulator.
Have you tried nesting two layers, with the parent layer with masksToBounds on? You can then set an independent mask on both layers, and the outer layer will effectively mask your inner layer with its own mask (due to masksToBounds). It probably doesn't matter which layer gets which mask (because you want the intersection of both masks).
With the code you listed in your question, you would just need to add one line, and comment out one line to get the correct functionality:
self.layer.mask = animationMaskLayer;
// shapeMaskLayer.mask = animationMaskLayer;
I'm trying to make a donut shape with CALayers. One CALayer will be a large circle, the other one will be a smaller circle positioned in its center, masking it.
The large circle displays fine, but whenever I call circle.mask = circleMask; then the view appears empty.
Here's my code:
AriDonut.h
#import <UIKit/UIKit.h>
#interface AriDonut : UIView
-(id)initWithRadius:(float)radius;
#end
AriDonut.m
#import "AriDonut.h"
#import <QuartzCore/QuartzCore.h>
#implementation AriDonut
-(id)initWithRadius:(float)radius{
self = [super initWithFrame:CGRectMake(0, 0, radius, radius)];
if(self){
//LARGE CIRCLE
CALayer *circle = [CALayer layer];
circle.bounds = CGRectMake(0, 0, radius, radius);
circle.backgroundColor = [UIColor redColor].CGColor;
circle.cornerRadius = radius/2;
circle.position = CGPointMake(radius/2, radius/2);
//SMALL CIRLCE
CALayer *circleMask = [CALayer layer];
circleMask.bounds = CGRectMake(0, 0, 10, 10);
circleMask.cornerRadius = radius/2;
circleMask.position = circle.position;
//circle.mask = circleMask;
[self.layer addSublayer:circle];
}
return self;
}
I've tried setting the large circle's superlayer nil like this:
CALayer *theSuper = circle.superlayer;
theSuper = nil;
But it didin't make a difference.
I also tried setting Circle's masksToBounds property to YES and NO, but it didn't make a difference.
Any thoughts?
Indeed, as #David indicates the current (iOS 5.1) CALayer masks can't be reversed, which poses a problem if you want to use them to make a transparent hole a simple circular CALayer.
What you can do to get a donut is make a circular CALayer's backgroundColor transparent, but give it a borderColor and a wide borderWidth. Here's the dunkin' code:
CALayer *theDonut = [CALayer layer];
theDonut.bounds = CGRectMake(0,0, radius, radius);
theDonut.cornerRadius = radius/2;
theDonut.backgroundColor = [UIColor clearColor].CGColor;
theDonut.borderWidth = radius/5;
theDonut.borderColor = [UIColor orangeColor].CGColor;
[self.layer addSublayer:theDonut];
This is pretty easy using UIBezierPath and a CAShapeLayer as a masking layer. Code sample written as though it's in a UIView subclass.
Objective-C:
CGRect outerRect = self.bounds;
CGFloat inset = 0.2 * outerRect.size.width; // adjust as necessary for more or less meaty donuts
CGFloat innerDiameter = outerRect.size.width - 2.0 * inset;
CGRect innerRect = CGRectMake(inset, inset, innerDiameter, innerDiameter);
UIBezierPath *outerCircle = [UIBezierPath bezierPathWithRoundedRect:outerRect cornerRadius:outerRect.size.width * 0.5];
UIBezierPath *innerCircle = [UIBezierPath bezierPathWithRoundedRect:innerRect cornerRadius:innerRect.size.width * 0.5];
[outerCircle appendPath:innerCircle];
CAShapeLayer *maskLayer = [CAShapeLayer new];
maskLayer.fillRule = kCAFillRuleEvenOdd; // Going from the outside of the layer, each time a path is crossed, add one. Each time the count is odd, we are "inside" the path.
maskLayer.path = outerCircle.CGPath;
self.layer.mask = maskLayer;
Swift:
let outerRect = self.bounds
let inset: CGFloat = 0.2 * outerRect.width // adjust as necessary for more or less meaty donuts
let innerDiameter = outerRect.width - 2.0 * inset
let innerRect = CGRect(x: inset, y: inset, width: innerDiameter, height: innerDiameter)
let outerCircle = UIBezierPath(roundedRect: outerRect, cornerRadius: outerRect.width * 0.5)
let innerCircle = UIBezierPath(roundedRect: innerRect, cornerRadius: innerRect.width * 0.5)
outerCircle.appendPath(innerCircle)
let mask = CAShapeLayer()
mask.fillRule = kCAFillRuleEvenOdd
mask.path = outerCircle.CGPath
self.layer.mask = mask
It is the alpha value of the masking layers content that is used as a mask. (If you would add the mask as a sublayer instead of using it as a mask. Everything that is covered by the sublayer would be visible when used as a mask. Everything that is not covered by the sublayer would be hidden when used as a mask.)
Since your small circle is fully transparent , everything is masked away (is hidden). If you set the backgroundColor of it to any, fully opaque color (only the alpha value is used for the mask) then it will let those pixels through.
Note that this is the reverse of what you want. This will leave you with only "the hole of the donut" visible. There is no built in way to do a reverse mask Instead you would have to draw the content of the mask some other way like using a CAShapeLayer or using drawInContext:.
I succeeded with a CAShapeLayer masking a CALayer. To specify the shape of the masking CAShapeLayer I used UIBezierPath.
I posted the code in my answer to this question: How to Get the reverse path of a UIBezierPath. For the donut shape uncomment the commented line.