Masking an Image in Swift Using Calayer and Uiimage

Masking an image in Swift using CALayer and UIImage

Unfortunately you've asked your question rather badly - you have not said what it is that you are actually trying to do! It looks, however, as if you might be trying to punch a rectangular hole in your image view using a mask. If so, your code has at least three huge flaws.

  • One reason your code is not working is that a mask is based on transparency, not on color. You are using an opaque white and an opaque black, which are both opaque, so there is no difference there. You need your two colors to be like this:

     var color = UIColor(white: 1.0, alpha: 1.0)
    // ... and then, later ...
    color = UIColor(white: 1.0, alpha: 0.0)
  • The second problem is that your layer has no size. You need to give it one:

    var maskLayer = CALayer()
    maskLayer.frame = CGRectMake(
    0, 0, self.imageView.bounds.width, self.imageView.bounds.height)
  • The third and biggest problem is that your mask image is never getting into your mask layer, because you have forgotten to extract its CGImage:

    maskLayer.contents = maskImage.CGImage

That last one is really the killer, because if you set the contents to a UIImage without extracting its CGImage, the image fails silently to get into the layer. There is no error message, no crash - and no image.

Making those three corrections in your code, I was able to make the mask punch a rectangular hole in an image. So if that's your purpose, those changes will achieve it.

Shape/mask on UIImage?

It can be achieved by giving header view UIBezierPath but if you don't want to do that stuff.

I found a cool way of doing.

Your Header view contain a image (Lets say it HeaderImage).

  • Make a Image of that shape (Lets say it MaskImage).

    let path = UIImageView.init(image: #imageLiteral(resourceName: "MaskImage"))
  • Than apply this mask to Header Image.

    HeaderImage.mask = path

Hope it work for you.

Adding a mask with CALayers

Try

maskLayer.contents = (id)mask.CGImage;

Yes, the cast sucks, but it's necessary.


I think you'll also need to say

maskLayer.bounds = (CGRect){CGPointZero, mask.size};

How to align a mask onto an image

The problem here is that if you use aspect fit then the size of the image as displayed depends upon both the size of the original image and the size of the image view. What you are trying to do is to imitate what the image view does to the image - and you have no way of knowing exactly what that is.

The best you can do is guess. What you are looking for is the size of the largest rectangle that will fit into your image view's bounds while keeping the aspect ratio of the original image. It happens that if you import the AV Foundation framework, there's a handy function that gives you that information, AVMakeRectWithAspectRatioInsideRect (documented here: https://developer.apple.com/library/ios/documentation/AVFoundation/Reference/AVFoundation_Functions/index.html#//apple_ref/c/func/AVMakeRectWithAspectRatioInsideRect).

iOS Animate Mask Over UIImage

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:

Sample Image

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

Getting masked layer as UIImage on Swift on top of UIImageView

I know what you mean now. Here is the answer, just update the size of imageContext.

          UIGraphicsBeginImageContextWithOptions((shapeLayer.path?.boundingBoxOfPath)!.size, false, 1)

If it's not so simple, can try CIImage pipeline to achieve.

    let context =  CIContext()
let m1 = newImage?.cgImage
let m = CIImage.init(cgImage: m1!)
let bounds = imageView.layer.bounds
let cgImage = context.createCGImage(m, from: CGRect.init(x: 0, y: bounds.size.height, width: bounds.size.width, height: bounds.size.height))
let newUIImage = UIImage.init(cgImage: cgImage!)

You may need to adjust transform.

Make a see-through layer

All views have a "Backing layer", including UILabel views.

As such, you can install a mask layer on the view's layer. You can make that mask layer a CAShapeLayer, or a regular layer with a CGImage as it's contents.

Don't mess with blend modes. Just add a mask layer to any layer that you want to be able to erase/un-erase, including a UILabel's layer

The mask layer will show/hide the contents of the layer it masks. (Opaque pixels in the mask reveal the layer underneath, but clear pixels hide it.)

If you want to be able to do freehand erasing/revealing of things like labels, I would suggest using a layer with an image in it as your mask. Draw opaque pixels into the masks image to "un-erase" the masked image, and draw with clear pixels to erase the masked image. Because you're just changing the mask, the masked image is left untouched.

@IBOutlet label: UILabel
var labelMask = CALayer()

...

func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// Set up the mask to be the same size as the label view
labelMask.frame = label.bounds
label.layer.mask = labelMask
labelMask.contents = // install a UIImage's CGImage as the contents
// The rest of your viewDidAppear code goes here...
}

Edit:

I posted a demo app on Github it that illustrates how to mask ANY view using a CALayer. This demo app uses a CGImage to create a raster mask. You can also use a CAShapeLayer as a vector mask.

Here is the readme from that project:



MaskableImageView

This project demonstrates how to use a CALayer to mask a UIView.

It defines a custom subclass of UIImageView, MaskableView.

The MaskableView class has a property maskLayer that contains a CALayer.

MaskableView defines a didSet method on its bounds property so that when the view's bounds change, it resizes the mask layer to match the size of the image view.

The MaskableView has a method installSampleMask which builds an image the same size as the image view, mostly filled with opaque black, but with a small rectangle in the center filled with black at an alpha of 0.7. The translucent center rectangle causes the image view to become partly transparent and show the view underneath.

The demo app installs a couple of subviews into the MaskableView, a sample image of Scampers, one of my dogs, and a UILabel. It also installs an image of a checkerboard under the MaskableView so that you can see the translucent parts more easily.

The MaskableView has properties circleRadius, maskDrawingAlpha, and drawingAction that it uses to let the user erase/un-erase the image by tapping on the view to update the mask.

The MaskableView attaches a UIPanGestureRecognizer and a UITapGestureRecognizer to itself, with an action of gestureRecognizerUpdate. The gestureRecognizerUpdate method takes the tap/drag location from the gesture recognizer and uses it to draw a circle onto the image mask that either decreases the image mask's alpha (to partly erase pixels) or increase the image mask's alpha (to make those pixels more opaque.)

The MaskableView's mask drawing is crude, and only meant for demonstration purposes. It draws a series of discrete circles intstead of rendering a path into the mask based on the user's drag gesture. A better solution would be to connect the points from the gesture recognizer and use them to render a smoothed curve into the mask.

The app's screen looks like this:

Sample Image



Related Topics



Leave a reply



Submit