Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CAShapeLayer as a mask for CALayer: rounded corners are stretched

I want to mask a CALayer with CAShapeLayer, because changes to the shape can be animated.

When I use the CAShapeLayer as a mask, the rounded corners are stretched. However, if I take the same shape, create an NSImage with it, and use the image to mask my CALayer, the rounded corners are perfectly fine.

Rounded corners with NSImage on the left, CAShapeLayer on the right

Here's the code I'm using to mask the layers (you can also download the whole example project):

CGColorRef backgroundColor = CGColorCreateGenericRGB(0.0, 0.0, 0.0, 1.0f);

[self.window.contentView setLayer:[CALayer layer]];
[self.window.contentView setWantsLayer:YES];
[[self.window.contentView layer] setFrame:[self.window.contentView frame]];


CALayer *imageBasedMaskLayer = [CALayer layer];
[imageBasedMaskLayer setContents:(id)[[self maskWithSize:NSMakeSize(50, 50)] CGImageForProposedRect:NULL context:nil hints:nil]];
[imageBasedMaskLayer setFrame:CGRectMake(0, 0, 50, 50)];

CALayer *layerWithImageBasedMaskLayer = [CALayer layer];
[layerWithImageBasedMaskLayer setBackgroundColor:backgroundColor];
[layerWithImageBasedMaskLayer setMask:imageBasedMaskLayer];


CAShapeLayer *shapeBasedMaskLayer = [CAShapeLayer layer];
CGPathRef maskShape = [self newMaskPathWithFrame:NSMakeRect(0, 0, 50, 50)];
[shapeBasedMaskLayer setPath:maskShape];
[shapeBasedMaskLayer setFillRule:kCAFillRuleEvenOdd];
CGPathRelease(maskShape);
[shapeBasedMaskLayer setFrame:CGRectMake(0, 0, 50, 50)];

CALayer *layerWithShapeBasedMaskLayer = [CALayer layer];
[layerWithShapeBasedMaskLayer setBackgroundColor:backgroundColor];
[layerWithShapeBasedMaskLayer setMask:nil];
[layerWithShapeBasedMaskLayer setMask:shapeBasedMaskLayer];


[layerWithImageBasedMaskLayer setFrame:CGRectMake(50, 50, 50, 50)];
[layerWithShapeBasedMaskLayer setFrame:CGRectMake(120, 50, 50, 50)];
[[self.window.contentView layer] addSublayer:layerWithImageBasedMaskLayer];
[[self.window.contentView layer] addSublayer:layerWithShapeBasedMaskLayer];

CGColorRelease(backgroundColor);

And the two methods I'm using to create NSImage and CGPathRef.

- (NSImage *)maskWithSize:(CGSize)size;
{
    NSImage *maskImage = [[NSImage alloc] initWithSize:size];

    [maskImage lockFocus];
    [[NSColor blackColor] setFill];

    CGPathRef mask = [self newMaskPathWithFrame:CGRectMake(0, 0, size.width, size.height)];
    CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];
    CGContextAddPath(context, mask);
    CGContextFillPath(context);
    CGPathRelease(mask);

    [maskImage unlockFocus];

    return [maskImage autorelease];
}

- (CGPathRef)newMaskPathWithFrame:(CGRect)frame; 
{
    CGFloat cornerRadius = 3;

    CGFloat height = CGRectGetMaxY(frame);
    CGFloat width = CGRectGetMaxX(frame);

    CGMutablePathRef maskPath = CGPathCreateMutable();

    CGPathMoveToPoint(maskPath, NULL, 0, height-cornerRadius);
    CGPathAddArcToPoint(maskPath, NULL, 0, height, cornerRadius, height, cornerRadius);
    CGPathAddLineToPoint(maskPath, NULL, width-cornerRadius, height);
    CGPathAddArcToPoint(maskPath, NULL, width, height, width, height-cornerRadius, cornerRadius);
    CGPathAddLineToPoint(maskPath, NULL, width, cornerRadius);
    CGPathAddArcToPoint(maskPath, NULL, width, 0, width-cornerRadius, 0, cornerRadius);
    CGPathAddLineToPoint(maskPath, NULL, cornerRadius, 0);
    CGPathAddArcToPoint(maskPath, NULL, 0, 0, 0, cornerRadius, cornerRadius);

    CGPathCloseSubpath(maskPath);

    return maskPath;
}

Please note that the actual mask I want to create is more complicated than a rounded rect, otherwise I would've used some of the simpler drawing techniques. I've tried various CGPath drawing functions, the problem did not disappear.

What am I missing here?

like image 353
antons Avatar asked Oct 21 '11 19:10

antons


1 Answers

Thats because CAShapeLayer favors speed over accuracy. Thats why it might be off sometimes.

The shape will be drawn antialiased, and whenever possible it will be mapped into screen space before being rasterized to preserve resolution independence. However, certain kinds of image processing operations, such as CoreImage filters, applied to the layer or its ancestors may force rasterization in a local coordinate space.

Note: Shape rasterization may favor speed over accuracy. For example, pixels with multiple intersecting path segments may not give exact results.

http://developer.apple.com/library/Mac/#documentation/GraphicsImaging/Reference/CAShapeLayer_class/Reference/Reference.html

Solution

You could use

layerWithShapeBasedMaskLayer.cornerRadius = 3.0;

to round all your corners. You don't need the CAShapeLayer anymore. You can even continue to use your CAShapeLayer as a sublayer for animating the path. Just set

layerWithShapeBasedMaskLayer.masksToBounds = YES;
like image 109
Danilo Avatar answered Nov 01 '22 22:11

Danilo