Drawing sprites programatically for sprite kit with alpha channel? - ios

I'm trying to prototype an app for sprite kit, and it would be advantageous to me to be able to draw the sprites I need programatically for now, and then make actual sprites later. I've made a rough circle, and when I convert to a UIImage (or CGImage) using renderInContext, it shows up on the sprite view with a black background, ignoring the alpha channel, apparently. If I add the UIImage to a regular view as a UIImageView, the alpha channel is rendered properly. I was wondering what I'm doing wrong, or if theres' a workaround for this.
float rockSize = 100;
UIView* drawingView = [[UIView alloc] initWithFrame:CGRectMake(0,0,100,100)];
drawingView.backgroundColor = [UIColor clearColor];
CALayer* circleLayer = [CALayer layer];
circleLayer.frame = CGRectMake(0,0,rockSize,rockSize);
circleLayer.backgroundColor = [Utils getTiledColorFromColor:[UIColor purpleColor]].CGColor;
circleLayer.cornerRadius = rockSize/2.0;
circleLayer.masksToBounds = YES;
[drawingView.layer addSublayer:circleLayer];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(rockSize, rockSize), NO, [UIScreen mainScreen].scale);
CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [UIColor clearColor].CGColor);
CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0,0,rockSize,rockSize));
[drawingView.layer renderInContext: UIGraphicsGetCurrentContext()];
UIImage *layerImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
SKSpriteNode *rockNode = [SKSpriteNode spriteNodeWithTexture:[SKTexture textureWithImage:layerImage]];
CGPoint position = CGPointMake(200,100);
rockNode.position = position;
[self addChild:rockNode];

Such an affect when you see black view background frequently happens when the view has opaque property incorrectly set to YES without actually being opaque (usually this happens when the view is created programmatically, opaque = YES is default).
You can fix this for example by setting opaque to NO.

Related

How to draw a smooth circle with CAShapeLayer and UIBezierPath?

I am attempting to draw a stroked circle by using a CAShapeLayer and setting a circular path on it. However, this method is consistently less accurate when rendered to the screen than using borderRadius or drawing the path in a CGContextRef directly.
Here are the results of all three methods:
Notice that the third is poorly rendered, especially inside the stroke on the top and bottom.
I have set the contentsScale property to [UIScreen mainScreen].scale.
Here is my drawing code for these three circles. What’s missing to make the CAShapeLayer draw smoothly?
#interface BCViewController ()
#end
#interface BCDrawingView : UIView
#end
#implementation BCDrawingView
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
self.backgroundColor = nil;
self.opaque = YES;
}
return self;
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
[[UIColor whiteColor] setFill];
CGContextFillRect(UIGraphicsGetCurrentContext(), rect);
CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), NULL);
[[UIColor redColor] setStroke];
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 1);
[[UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)] stroke];
}
#end
#interface BCShapeView : UIView
#end
#implementation BCShapeView
+ (Class)layerClass
{
return [CAShapeLayer class];
}
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
self.backgroundColor = nil;
CAShapeLayer *layer = (id)self.layer;
layer.lineWidth = 1;
layer.fillColor = NULL;
layer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)].CGPath;
layer.strokeColor = [UIColor redColor].CGColor;
layer.contentsScale = [UIScreen mainScreen].scale;
layer.shouldRasterize = NO;
}
return self;
}
#end
#implementation BCViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UIView *borderView = [[UIView alloc] initWithFrame:CGRectMake(24, 104, 36, 36)];
borderView.layer.borderColor = [UIColor redColor].CGColor;
borderView.layer.borderWidth = 1;
borderView.layer.cornerRadius = 18;
[self.view addSubview:borderView];
BCDrawingView *drawingView = [[BCDrawingView alloc] initWithFrame:CGRectMake(20, 40, 44, 44)];
[self.view addSubview:drawingView];
BCShapeView *shapeView = [[BCShapeView alloc] initWithFrame:CGRectMake(20, 160, 44, 44)];
[self.view addSubview:shapeView];
UILabel *borderLabel = [UILabel new];
borderLabel.text = #"CALayer borderRadius";
[borderLabel sizeToFit];
borderLabel.center = CGPointMake(borderView.center.x + 26 + borderLabel.bounds.size.width/2.0, borderView.center.y);
[self.view addSubview:borderLabel];
UILabel *drawingLabel = [UILabel new];
drawingLabel.text = #"drawRect: UIBezierPath";
[drawingLabel sizeToFit];
drawingLabel.center = CGPointMake(drawingView.center.x + 26 + drawingLabel.bounds.size.width/2.0, drawingView.center.y);
[self.view addSubview:drawingLabel];
UILabel *shapeLabel = [UILabel new];
shapeLabel.text = #"CAShapeLayer UIBezierPath";
[shapeLabel sizeToFit];
shapeLabel.center = CGPointMake(shapeView.center.x + 26 + shapeLabel.bounds.size.width/2.0, shapeView.center.y);
[self.view addSubview:shapeLabel];
}
#end
EDIT: For those who cannot see the difference, I've drawn circles on top of each other and zoomed in:
Here I've drawn a red circle with drawRect:, and then drawn an identical circle with drawRect: again in green on top of it. Note the limited bleed of red. Both of these circles are "smooth" (and identical to the cornerRadius implementation):
In this second example, you'll see the issue. I've drawn once using a CAShapeLayer in red, and again on top with a drawRect: implementation of the same path, but in green. Note that you can see a lot more inconsistency with more bleed from the red circle underneath. It's clearly being drawn in a different (and worse) fashion.
Who knew there are so many ways to draw a circle?
TL;DR: If you want to use CAShapeLayer and still get smooth circles, you'll need to use shouldRasterize and rasterizationScale carefully.
Original
Here's your original CAShapeLayer and a diff from the drawRect version. I made a screenshot off my iPad Mini with Retina Display, then massaged it in Photoshop, and blew it up to 200%. As you can clearly see, the CAShapeLayer version has visible differences, especially on the left and right edges (darkest pixels in the diff).
Rasterize at screen scale
Let's rasterize at screen scale, which should be 2.0 on retina devices. Add this code:
layer.rasterizationScale = [UIScreen mainScreen].scale;
layer.shouldRasterize = YES;
Note that rasterizationScale defaults to 1.0 even on retina devices, which accounts for the fuzziness of default shouldRasterize.
The circle is now a little smoother, but the bad bits (darkest pixels in the diff) have moved to the top and bottom edges. Not appreciably better than no rasterizing!
Rasterize at 2x screen scale
layer.rasterizationScale = 2.0 * [UIScreen mainScreen].scale;
layer.shouldRasterize = YES;
This rasterizes the path at 2x screen scale, or up to 4.0 on retina devices.
The circle is now visibly smoother, the diffs are much lighter and spread out evenly.
I also ran this in Instruments: Core Animation and didn't see any major differences in the Core Animation Debug Options. However it may be slower since it's downscaling not just blitting an offscreen bitmap to the screen. You may also need to temporarily set shouldRasterize = NO while animating.
What doesn't work
Set shouldRasterize = YES by itself. On retina devices, this looks fuzzy because rasterizationScale != screenScale.
Set contentScale = screenScale. Since CAShapeLayer doesn't draw into contents, whether or not it is rasterizing, this doesn't affect the rendition.
Credit to Jay Hollywood of Humaan, a sharp graphic designer who first pointed it out to me.
Ah, i ran into the same problem some time ago (it was still iOS 5 then iirc), and I wrote the following comment in the code:
/*
ShapeLayer
----------
Fixed equivalent of CAShapeLayer.
CAShapeLayer is meant for animatable bezierpath
and also doesn't cache properly for retina display.
ShapeLayer converts its path into a pixelimage,
honoring any displayscaling required for retina.
*/
A filled circle underneath a circleshape would bleed its fillcolor. Depending on the colors this would be very noticeable. And during userinteraction the shape would render even worse, which let me to conclude that the shapelayer would always render with a scalefactor of 1.0, regardless of the layer scalefactor, because it is meant for animation purposes.
i.e. you only use a CAShapeLayer if you have a specific need for animatable changes to the shape of the bezierpath, not to any of the other properties that are animatable through the usual layer properties.
I eventually decided to write a simple ShapeLayer that would cache its own result, but you might try implementing the displayLayer: or the drawLayer:inContext:
Something like:
- (void)displayLayer:(CALayer *)layer
{
UIImage *image = nil;
CGContextRef context = UIImageContextBegin(layer.bounds.size, NO, 0.0);
if (context != nil)
{
[layer renderInContext:context];
image = UIImageContextEnd();
}
layer.contents = image;
}
I haven't tried that, but would be interesting to know the result...
I know this is an older question, but for those who are trying to work in the drawRect method and still having trouble, one small tweak that helped me immensely was using the correct method to fetch the UIGraphicsContext. Using the default:
let newSize = CGSize(width: 50, height: 50)
UIGraphicsBeginImageContext(newSize)
let context = UIGraphicsGetCurrentContext()
would result in blurry circles no matter which suggestion I followed from the other answers. What finally did it for me was realizing that the default method for getting an ImageContext sets the scaling to non-retina. To get an ImageContext for a retina display you need to use this method:
let newSize = CGSize(width: 50, height: 50)
UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
let context = UIGraphicsGetCurrentContext()
from there using the normal drawing methods worked fine. Setting the last option to 0 will tell the system to use the scaling factor of the device’s main screen. The middle option false is used to tell the graphics context whether or not you'll be drawing an opaque image (true means the image will be opaque) or one that needs an alpha channel included for transparencies. Here are the appropriate Apple Docs for more context: https://developer.apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc
I guess CAShapeLayer is backed by a more performant way of rendering its shapes and takes some shortcuts. Anyway CAShapeLayer can be a little bit slow on the main thread. Unless you need to animate between different paths I would suggest render asynchronously to a UIImage on a background thread.
Use this method to draw UIBezierPath
/*********draw circle where double tapped******/
- (UIBezierPath *)makeCircleAtLocation:(CGPoint)location radius:(CGFloat)radius
{
self.circleCenter = location;
self.circleRadius = radius;
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:self.circleCenter
radius:self.circleRadius
startAngle:0.0
endAngle:M_PI * 2.0
clockwise:YES];
return path;
}
And draw like this
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [[self makeCircleAtLocation:location radius:50.0] CGPath];
shapeLayer.strokeColor = [[UIColor whiteColor] CGColor];
shapeLayer.fillColor = nil;
shapeLayer.lineWidth = 3.0;
// Add CAShapeLayer to our view
[gesture.view.layer addSublayer:shapeLayer];

CALayer - How do I create an image with a solid background and transparent text?

I'm trying to create movie generation application by using AVComposition and have a trouble in making title frame.
Each frame is actually a calayer and title layer is on top of other frames.
Title(Text) needs to be transparent with black background so that they can see some part of the first content frame under title text letters.
I searched most articles about calayer mask, but nothing helped me.
I thought this article (How to make only the part covered by text/title transparent in a UIView in IOS) is helpful and coded like Dave's way, but got only white screen.
Here is what I have done:
// create UILabel from the title text
CGRect rectFrame = CGRectMake(0, 0, videoSize.width, videoSize.height);
UILabel *lbTitle = [[UILabel alloc] initWithFrame:rectFrame];
lbTitle.text = self.titleText;
lbTitle.font = [UIFont fontWithName:#"Helvetica" size:60];
lbTitle.textColor = [UIColor blackColor];
lbTitle.backgroundColor = [UIColor whiteColor];
// get title image and create mask layer
UIGraphicsBeginImageContextWithOptions(lbTitle.bounds.size, TRUE, [[UIScreen mainScreen] scale]);
[lbTitle.layer renderInContext:UIGraphicsGetCurrentContext()];
CGImageRef viewImage = [UIGraphicsGetImageFromCurrentImageContext() CGImage];
UIGraphicsEndImageContext();
CALayer *maskLayer = [CALayer layer];
maskLayer.contents = (__bridge id)viewImage;
maskLayer.frame = rectFrame;
// create title background layer and set mastLayer as mast layer of this layer
// this layer corresponds to "UIView's layer" in Dave's method
CALayer *animatedTitleLayer = [CALayer layer];
animatedTitleLayer.backgroundColor = [UIColor whiteColor].CGColor;
animatedTitleLayer.mask = maskLayer;
animatedTitleLayer.frame = rectFrame;
...
[view.layer addSubLayer:animatedTitleLayer];
Here I used animatedTitleLayer as title background(black background), but what I see is white screen.
Anyone can help me? Thanks in advance.
The mask uses the alpha channel to determine what parts to mask out and what parts to keep. However, your label that you render into an image is rendered as black text on a white background so there is no transparency in the image.
You have also specified that the graphics context you are using to render the image is opaque so even if the background color of the label as was clear you would get an opaque image.
So you need to set a clear background color on the label and pass NO as the second argument when you create the graphics context.

Darken view as if disabled

How do you darken a view as if it were disabled/highlighted, preferably without using any additional views?
By view I mean a UIView, with all its children. I want to achieve the same effect of a disabled/highlighted UIButton.
Do not assume that the view is fully opaque.
What I'm currently playing with:
Create a black layer with opacity (_highlightLayer). This is similar to the "black view with alpha" approach.
Mask _highlightLayer with an non-opaque image of the original view.
Add the _highlightLayer to the view's layer.
Only the non-transparent pixels of the view will be darkened.
The code:
- (void)highlight
{
// Black layer with opacity
_highlightLayer = [CALayer layer];
_highlightLayer.frame = CGRectMake(0, 0, self.layer.bounds.size.width, self.layer.bounds.size.height);
_highlightLayer.backgroundColor = [UIColor blackColor].CGColor;
_highlightLayer.opacity = 0.5;
// Create an image from the view
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *maskImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Create a mask layer for the black layer
CALayer *maskLayer = [CALayer layer];
maskLayer.contents = (__bridge id) maskImage.CGImage;
maskLayer.frame = _highlightLayer.frame;
_highlightLayer.mask = maskLayer;
[self.layer addSublayer:_highlightLayer];
}
And then:
- (void)unhighlight
{
[_highlightLayer removeFromSuperlayer];
_highlightLayer = nil;
}
Of course, this should only be used for small views.
Example:
You have a UIButton *button1 and UIView *view1
You can disable a button and a view in this way:
[view1 setHidden:YES];
[button1 setEnabled:NO];
Enabling a button and a view can be done this way:
[view1 setHidden:NO];
[button1 setEnabled:YES];
Hope this helps..
While this approach fails to meet your preference of not using additional views, simply adding a black view with alpha of 0.6 or so seems to achieve the effect, with the added benefit that you can use this new view to intercept UIEvents so that all the subviews are effectively disabled en masse.
You can even get graphically fancy and instead of just using a black background for the overlaid view, you can fill its background with a radial gradient to achieve the sort of spotlighted effect that happens in iOS when a popup view disables the view behind it...

add shadow to a layer

I can add shadow to imageView layer using the following code.
self.imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"test.png"]];
self.imageView.center = self.view.center;
CALayer *containerLayer= [CALayer layer];
containerLayer.shadowColor = [UIColor blackColor].CGColor;
containerLayer.shadowRadius = 10.0f;
containerLayer.shadowOffset = CGSizeMake(10.0f, 5.0f);
containerLayer.shadowOpacity = .8f;
[containerLayer addSublayer:self.imageView.layer];
[self.view.layer addSublayer:containerLayer];
1 . The problem is that I don't know why I have to add imageView.layer to containerLayer to get the imageView shadow effect. However, if I add containerLayer to imageView.layer, there's no shadow in imageView, why?
The error code is:
self.imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"test.png"]];
self.imageView.center = self.view.center;
CALayer *containerLayer= [CALayer layer];
/*same as before*/
[self.imageView.layer addSublayer:containerLayer];
[self.view.layer addSublayer:self.imageView.layer];
Question2: the containerLayer's(used to provide shadow to imageView) frame = {{0, 0}, {0, 0}}, but the final position is in the center of screen. why?
A layer needs something opaque inside it to create a shadow around (unless you've specified a shadowPath explicitly). So your first version of the code works because the containerLayer has the imageView's layer as a sublayer. But, as you noticed from Question #2, the containerLayer's frame indicates that it is actually located in the upper left corner with a size of (0,0). The reason you can still see the image is that containerLayer is not masking to its bounds. Add this line to your first version, and the image disappears:
[containerLayer setMasksToBounds: YES]; // kitten (and shadow) is gone
Your version #2 of the code does not display a shadow because, in part, the containerLayer does not "contain" anything. If you use version #2 but give the containerLayer a new frame and an opaque background color, a shadow appears. (But this obviously isn't a solution, because the image is covered up...) Also note that there is no shadow when the layer's background is [UIColor clearColor].
[self.imageView.layer addSublayer:containerLayer];
containerLayer.frame = self.imageView.layer.bounds;
containerLayer.backgroundColor = [UIColor yellowColor].CGColor; // yellow box w/shadow
// containerLayer.backgroundColor = [UIColor clearColor].CGColor; // no shadow here
If you wanted to have a container with a shadow that houses the UIImageView, you could do something like this:
UIView * shadowView = [[UIView alloc] initWithFrame: self.imageView.frame];
shadowView.layer.shadowColor = [UIColor blackColor].CGColor;
shadowView.layer.shadowRadius = 10.0f;
shadowView.layer.shadowOffset = CGSizeMake(10.0f, 5.0f);
shadowView.layer.shadowOpacity = .8f;
[self.view addSubview: shadowView];
self.imageView.frame = (CGRect) { CGPointZero, self.imageView.frame.size };
[shadowView addSubview: self.imageView];
A CALayer could be used instead of a UIView in a similar way. Or you could apply shadow attributes to the imageView's layer directly, making sure that neither the view nor the layer is clipping/masking to bounds.
If you want to add a shadow over the imageview just change the alpha value (0.0 min - 1.0 max) of that imageview. This will give you the shadow effect and whenever you want to remove the shadow just restore the alpha value to 1.0.
For ex:
self.imageview.alpha = 0.5 // shadow state
self.imageview.alpha = 1.0 // original state

Create UIImage from shadowed view while retaining alpha?

I have a somewhat unusual problem. In my app, I am shadowing a UIImageView using basic Quartz2d layer shadowing. Here's my code:
imageView.layer.shadowOpacity = 1.0; // Pretty self explanatory
imageView.layer.shadowRadius = 8.0; // My softness
imageView.layer.shadowColor = [UIColor blackColor].CGColor; // Color of the shadow
imageView.layer.shadowOffset = CGSizeMake(0.0, 0.0); // Offset of the shadow
This produces a nice blur behind the image view. However, I am doing some animation with this view, and the constant recalculation of the shadowing during the transitions causes a very choppy transition. What I'd like to be able to do is create a UIImage or .png file out of the image view with the blur and its alpha intact. Here's what I've already tried:
UIGraphicsBeginImageContext(CGSizeMake(320, 396));
[imageView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageWriteToSavedPhotosAlbum(viewImage, nil, nil, nil);
Since I have a shadow which "grows outside" of the image view, I can't just pass his size in the UIGraphicsBeginImageContext function. I set the correct size, but the resulting image doesn't save my alpha, instead it places a white background behind the shadow, which won't work for me because my real background is a wood texture. Also, the view isn't centered in the resulting file.
I'm pretty good with UIKit, but I'm a real noobie with Quartz 2d graphics, so if there's an obvious answer to this, send it anyway. :)
Try setting your UIImageView's backgroundColor to [UIColor clearColor] - this may enable your current solution to work.
imageView.backgroundColor = [UIColor clearColor];
UIGraphicsBeginImageContext(CGSizeMake(320, 396));
[imageView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Source: Converting a CGLayer to a *transparent* UIImage and PNG file in CoreGraphics iphone
I've had this issue before too. My solution was not to do an image, but instead to set the shadow path to just the outline of the view you are animating.
[imageView setContentMode:UIViewContentModeScaleAspectFit];
imageView.layer.masksToBounds = NO;
imageView.layer.shadowOffset = CGSizeMake(5, 5);
imageView.layer.shadowRadius = 200;
imageView.layer.shadowOpacity = 0.5;
CGRect rect = [self rectFromImageView:imageView];
imageView.layer.shadowPath = [UIBezierPath bezierPathWithRect:rect].CGPath;
Which uses the following function which assumes the image is set to content mode aspect fit:
-(CGRect)rectFromImageView:(UIImageView *)iv {
CGRect rect = (CGRect){{0,0},iv.frame.size};
if (iv.image.size.width/iv.frame.size.width > iv.image.size.height/iv.frame.size.height) {
//rect.origin.x == 0
CGFloat sf = iv.frame.size.width/iv.image.size.width;
rect.size.height = sf*iv.image.size.height;
rect.origin.y = floor((iv.frame.size.height - rect.size.height)/2);
} else {
//rect.origin.y == 0;
CGFloat sf = iv.frame.size.height/iv.image.size.height;
rect.size.width = sf*iv.image.size.width;
rect.origin.x = floor((iv.frame.size.width - rect.size.width)/2);
}
return rect;
}
If your image is just set to fill then just using the imageView.bounds should be sufficient
Add shadow path to view like this
[view.layer setShadowPath:[[UIBezierPath bezierPathWithRect:view.bounds] CGPath]]; //add path
view.layer.shadowColor = [[UIColor blackColor] CGColor];
view.layer.shadowOffset = CGSizeMake(x, y);
view.layer.shadowRadius = rad;
view.layer.shadowOpacity = 0.2f;

Resources