I created a circular mask and animate a sprite inside the mask by using sprite kit SKCropNode class. But the edge of the mask looks pixellated.
Is there a way to use anti-aliasing to smooth the edges?
If you want to mask any SKNode/SKSpriteNode with an anti-aliasing effect - you can use SKEffectNode instead of SKCropNode. It works with animated nodes as well. Here is an example:
// Set up your node
SKNode *nodeToMask = [SKNode node];
// ...
// Set up the mask node
SKEffectNode *maskNode = [SKEffectNode node];
// Create a filter
CIImage *maskImage = [[CIImage alloc] initWithCGImage:[UIImage imageNamed:#"your_mask_image"].CGImage];
CIFilter *maskFilter = [CIFilter filterWithName:#"CISourceInCompositing"
keysAndValues:#"inputBackgroundImage", maskImage, nil];
// Set the filter
maskNode.filter = maskFilter;
// Add childs
[maskNode addChild:nodeToMask];
[scene addChild:maskNode];
From the official docs:
If the pixel in the mask has an alpha value of less than 0.05, the
image pixel is masked out.
So, SKCropNode doesn't support per pixel alpha values. It either draws the pixels or it doesn't. If alpha (as is used by anti-aliasing) is important to you, you should consider alternative routes. For example:
How to Mask an UIImageView
As of now (June 2018) I can confirm that the SKCropNode will do per-pixel alpha blending. If the crop node has an SKSpriteNode as a mask, and the mask node uses a texture with an anti-aliased mask image, the crop node will do the proper blending.
Related
I'm using a CARenderer to render another CALayer tree into a CAMetalLayer, which I hope to use as the mask of yet another layer. For testing purposes, I've tried adding the CAMetalLayer as a normal sublayer instead of a mask.
The layer object below is not visible after adding it to a superlayer that is definitely visible. I've confirmed the frame of the layer is not a problem. Here's how I'm making the CAMetalLayer and its CARenderer.
CAMetalLayer *layer = [CAMetalLayer layer];
layer.frame = bounds;
layer.device = MTLCreateSystemDefaultDevice();
//layer.opaque = NO;
//layer.framebufferOnly = NO;
id<CAMetalDrawable> drawable = layer.nextDrawable;
_lastDrawable = drawable;
_renderer = [CARenderer rendererWithMTLTexture:drawable.texture options:nil];
_renderer.layer = self.superview.layer;
_renderer.bounds = bounds;
👇 By creating a CIImage and inspecting it with the debugger, I've confirmed the CARenderer is updating the Metal texture.
CIImage *img = [CIImage imageWithMTLTexture:_lastDrawable.texture options:nil];
But when I set the superlayer of the CAMetalLayer, it's nowhere to be seen.
[self.layer addSublayer:layer];
Here's how I'm using the CARenderer:
[_renderer beginFrameAtTime:CACurrentMediaTime() timeStamp:NULL];
[_renderer addUpdateRect:bounds];
[_renderer render];
[_renderer endFrame];
That last snippet runs frequently.
edit 1
I've added a backgroundColor and now the layer is visible, but its texture is not being rendered inside it.
layer.backgroundColor = NSColor.yellowColor.CGColor;
I would recommend just setting the original layer as a mask rather than trying to render it to a texture first; you’re sort of duplicating the work that CA would be doing anyway.
If you really need control over when the mask layer tree gets rendered—and again you should definitely try the standard method first—the right way to do this would be to create an IOSurface-backed MTLTexture rather than using a CAMetalLayer’s drawable, draw into the texture with your CARenderer, set the IOSurface as the contents of a regular CALayer, and use that layer as the mask.
I need to implement this functionality.Please suggest me.
It's not working properly means it is taking the end angle for the filling colour but here mentioned the "fromValue" and "toValue" but its not going through the fromValue and toValue.
Please anyone can edit my code.
Thanks in advance.
CAShapeLayer *circle=[CAShapeLayer layer];
circle.path=[UIBezierPath bezierPathWithArcCenter:CGPointMake(self.img_View.frame.origin.x, self.img_View.frame.origin.y) radius:50 startAngle:0 endAngle:90 clockwise:YES].CGPath;
circle.fillColor=[UIColor clearColor].CGColor;
circle.strokeColor=[UIColor greenColor].CGColor;
circle.lineWidth=16;
CABasicAnimation *animation=[CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration=10;
animation.removedOnCompletion=NO;
// animation.fromValue=#(0);
animation.fromValue=[NSNumber numberWithInt:0];
animation.toValue=[NSNumber numberWithInt:20];
animation.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[circle addAnimation:animation forKey:#"drawCircleAnimation"];
[img_View.layer.sublayers makeObjectsPerformSelector:#selector(removeFromSuperlayer)];
[img_View.layer addSublayer:circle];
you can do it with UIBezierPath it is very efficient to draw shapes.
The bezier path you use as a clip seems to be just a fraction of a circle, while in the image you show, the path is more complex : 2 fractions of a circle, linked by 2 lines, the whole path having a 'ring' shape.
This approach should work, I used it for a timer with the same kind of look. Although I didn't used directly AngleGradientLayer, I modified its - (CGImageRef)newImageGradientInRect:(CGRect)rect method to return a UIImage. But I had to rotate this image by + PI/2, as Pavlov gradient angular gradient starts horizontally.
I use a UIImage, because it's a background that DOESN'T change, so I saved an instance of this UIImage in my layer, and draw it whenever I update the clipping path
- (void)drawInContext:(CGContextRef)ctx {
UIBezierPath *currentPath = [self timerPath];
// other drawing code for glow (shadow) and white stroke)
CGContextAddPath(ctx, currentPath.CGPath);
// clip !
CGContextClip(ctx);
CGContextDrawImage(ctx, self.bounds, _circularGradientImage.CGImage);
//_circularGradientImage from modified newImageGradientInRect method.
}
I'm developing a iOS app for iPad. I'd like to mask a UIView with an image in black and white. So the black part of the image is what you can see of the view.
If tried different codes like one below, but they don't work...
UIImage *_maskingImage = [UIImage imageNamed:#"ipadmask.jpg"];
CALayer *_maskingLayer = [CALayer layer];
_maskingLayer.frame = vistafunda.bounds;
[_maskingLayer setContents:(id)[_maskingImage CGImage]];
[vistafunda.layer setMask:_maskingLayer];
vistafunda.layer.masksToBounds = YES;
They ipadmask.jpg is that:
Thanks for your help!
apple documentation about CALayer mask property:
An optional layer whose alpha channel is used as a mask to select
between the layer's background and the result of compositing the
layer's contents with its filtered background.
So you should fix the alpha channel of you image. Black pixels should be opaque and white - totaly transparent.
I'm building an iPad app that includes several bar graphs (30+ bars per graph) and a pull-up menu from the bottom of the screen. The design calls for these bars to have rounded corners on the top left and top right corner, but square corners on the bottom left and bottom right.
Right now, I am rounding the top corners of each individual graph bar by using a mask layer:
#import "UIView+RoundedCorners.h"
#import <QuartzCore/QuartzCore.h>
#implementation UIView (RoundedCorners)
-(void)setRoundedCorners:(UIRectCorner)corners radius:(CGFloat)radius {
CGRect rect = self.bounds;
// Create the path
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:rect
byRoundingCorners:corners
cornerRadii:CGSizeMake(radius, radius)];
// Create the shape layer and set its path
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = rect;
maskLayer.path = maskPath.CGPath;
// Set the newly created shape layer as the mask for the view's layer
self.layer.mask = maskLayer;
self.layer.shouldRasterize = YES;
}
#end
This works, but there is serious frame loss / lag when I pull up the pull-up menu (Note that the menu overlaps, appearing on top of the graph). When I take away the rounded corners, the menu pulls up beautifully. From what I gather, I need a way to round the corners by directly modifying the view, not by using a mask layer. Does anyone know how I could pull this off or have another possible solution? Any help greatly appreciated
From what I gather, I need a way to round the corners by directly
modifying the view, not by using a mask layer. Does anyone know how I
could pull this off or have another possible solution?
The usual way is to get the view's layer and set the cornerRadius property:
myView.layer.cornerRadius = 10.0;
That way there's no need for a mask layer or a bezier path. In the context of your method, it'd be:
-(void)setRoundedCorners:(UIRectCorner)corners radius:(CGFloat)radius
{
self.layer.cornerRadius = radius;
}
Update: Sorry -- I missed this part:
The design calls for these bars to have rounded corners on the top
left and top right corner, but square corners on the bottom left and
bottom right
If you only want some of the corners rounded, you can't do that by setting the layer's cornerRadius property. Two approaches you could take:
Create a bezier path with two corners rounded and two square, and then fill it. No need for a mask layer to do that.
Use a resizable image. See UIImage's -resizableImageWithCapInsets: and -resizableImageWithCapInsets:resizingMode: methods. There are even some methods for animating reizable images, so you could easily have your bars grow to the right length.
Try setting shouldRasterize on your layers. Should help.
I want to do some custom drawing with CoreGraphics. I need a linear gradient on my view, but the thing is that this view is a rounded rectangle so I want my gradient to be also rounded at angles. You can see what I want to achieve on the image below:
So is this possible to implement in CoreGraphics or some other programmatic and easy way?
Thank you.
I don't think there is an API for that, but you can get the same effect if you first draw a radial gradient, say, in an (N+1)x(N+1) size bitmap context, then convert the image from the context to a resizable image with left and right caps set to N.
Pseudocode:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(N+1,N+1), NO, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
// <draw the gradient into 'context'>
UIImage* gradientBase = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImage* gradientImage = [gradientBase resizableImageWithCapInsets:UIEdgeInsetsMake(0,N,0,N)];
In case you want the image to scale vertically as well, you just have to set the caps to UIEdgeInsetsMake(N,N,N,N).
I just want to add more sample code for this technique, as some things weren't obvious for. Maybe it will be useful for somebody:
So, let's say, we have our custom view class and in it's drawRect: method we put this:
// Defining the rect in which to draw
CGRect drawRect=self.bounds;
Float32 gradientSize=drawRect.size.height; // The size of original radial gradient
CGPoint center=CGPointMake(0.5f*gradientSize,0.5f*gradientSize); // Center of gradient
// Creating the gradient
Float32 colors[4]={0.f,1.f,1.f,0.2f}; // From opaque white to transparent black
CGGradientRef gradient=CGGradientCreateWithColorComponents(CGColorSpaceCreateDeviceGray(), colors, nil, 2);
// Starting image and drawing gradient into it
UIGraphicsBeginImageContextWithOptions(CGSizeMake(gradientSize, gradientSize), NO, 1.f);
CGContextRef context=UIGraphicsGetCurrentContext();
CGContextDrawRadialGradient(context, gradient, center, 0.f, center, center.x, 0); // Drawing gradient
UIImage* gradientImage=UIGraphicsGetImageFromCurrentImageContext(); // Retrieving image from context
UIGraphicsEndImageContext(); // Ending process
gradientImage=[gradientImage resizableImageWithCapInsets:UIEdgeInsetsMake(0.f, center.x-1.f, 0.f, center.x-1.f)]; // Leaving 2 pixels wide area in center which will be tiled to fill whole area
// Drawing image into view frame
[gradientImage drawInRect:drawRect];
That's all. Also if you're not going to ever change the gradient while app is running, you would want to put everything except last line in awakeFromNib method and then in drawRect: just draw the gradientImage into view's frame. Also don't forget to retain the gradientImage in this case.