I am trying to make use of path animation feature in CAShapeLayer that is not available in SpriteKit and hence having to combine objects drawn suing CAShapeLayer with SpriteKit objects within the same view.
The coordinate system seems to be inverse: CAShapeLayer seems to have +ve y-axis pointing downwards, whereas SKScene has it pointing upwards.
Below is a simple XCODE playground that tries to draw a yellow line from 0,0 to 200,100 and shadow it with a thicker red line.
import SpriteKit
import PlaygroundSupport
let bounds = CGRect(x: 0, y: 0, width: 400, height: 200)
let view = SKView(frame: bounds)
view.backgroundColor = UIColor.lightGray
PlaygroundPage.current.liveView = view
// Create SK Scene
let scene = SKScene(size: CGSize(width: 400, height: 200))
scene.scaleMode = SKSceneScaleMode.aspectFit
view.presentScene(scene);
// Define the path
let path: CGMutablePath = CGMutablePath();
path.move(to: CGPoint(x:0, y:0))
path.addLine(to: CGPoint(x:200, y:100))
// Use CAShapeLayer to draw the red line
var pathLayer: CAShapeLayer = CAShapeLayer()
pathLayer.path = path
pathLayer.strokeColor = UIColor.red.cgColor
pathLayer.fillColor = nil
pathLayer.lineWidth = 4.0
pathLayer.lineJoin = kCALineJoinBevel
pathLayer.zPosition = 1;
view.layer.addSublayer(pathLayer);
//Use SKShapeNode to draw the yellow line
let pathShape: SKShapeNode = SKShapeNode(path: path);
pathShape.strokeColor = .yellow;
pathShape.lineWidth = 1.0;
pathShape.zPosition = 10;
scene.addChild(pathShape);
I expected the yellow line to coincide with the red line. Whereas, the yellow and red lines appeared as mirror images.
Is there anyway to redefine the CAShapeLayer coordinate system to point +ve Y-axis upwards?
No, as written in the docs this is how it works, you may inverse the coordinates to match your requirements..
Sprite kit docs:
The unit coordinate system places the origin at the bottom left corner of the frame and (1,1) at the top right corner of the frame. A sprite’s anchor point defaults to (0.5,0.5), which corresponds to the center of the frame.
The rest of iOS:
iOS. The default coordinate system has its origin at the upper left of the drawing area, and positive values extend down and to the right from it. You cannot change the default orientation of a view’s coordinate system in iOS—that is, you cannot “flip” it.
Related
I have two buttons in a stack view. I have used an extension of UIButton to round the outside corners. This works on the 7Plus which I designed for in storyboard but as soon as I run on a smaller device size in the simulator it stops working and I can only round corners on the left side of either button and not the right. Any ideas?
On a 7Plus
On a 7
These are the extensions I'm using
extension CGSize{
init(_ width:CGFloat,_ height:CGFloat) {
self.init(width:width,height:height)
}
}
extension UIButton{
func roundOneSide(topCorner: UIRectCorner, bottomCorner: UIRectCorner){
let maskPAth1 = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [topCorner , bottomCorner],
cornerRadii:CGSize(6.0, 6.0))
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = self.bounds
maskLayer1.path = maskPAth1.cgPath
self.layer.mask = maskLayer1
}
}
I have also tried the following code to no avail. It can only successfully round corners on the left when constraints come into play.
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: [.topLeft, .bottomRight], cornerRadii: CGSize(width: 6, height: 6)).cgPath
facebookBtn.layer.mask = maskLayer
facebookBtn.layer.masksToBounds = true
Use you use segment Control as jerky said or
Try this below code:
// For login button
UIBezierPath *cornersPathLeft = [UIBezierPath bezierPathWithRoundedRect:buttonLogin.bounds byRoundingCorners:(UIRectCornerBottomLeft|
UIRectCornerTopLeft) cornerRadii:CGSizeMake(5, 5)];
//Create a new layer to use as a mask
CAShapeLayer *maskLayerLeft = [CAShapeLayer layer];
// Set the path of the layer
maskLayerLeft.path = cornersPathLeft.CGPath;
buttonLogin.layer.mask = maskLayerLeft;
// For FB button
UIBezierPath *cornersPathRight = [UIBezierPath bezierPathWithRoundedRect:buttonFB.bounds byRoundingCorners:(UIRectCornerTopRight|
UIRectCornerBottomRight) cornerRadii:CGSizeMake(5, 5)];
CAShapeLayer *maskLayerRight = [CAShapeLayer layer];
maskLayerRight.path = cornersPathRight.CGPath;
buttonFB.layer.mask = maskLayerRight;
NOTE:
do maskToBounds = true it allows to give effect on the layers.
-
Difference Between MaskToBounds and ClipsToBounds
MaskToBounds
Any sublayers of the layer that extend outside its boundaries will be clipped to those boundaries. Think of the layer, in that case, as a window onto its sublayers; anything outside the edges of the window will not be visible. When masksToBounds = NO, no clipping occurs.
When the value of this property is true, Core Animation creates an implicit clipping mask that matches the bounds of the layer and includes any corner radius effects. If a value for the mask property is also specified, the two masks are multiplied to get the final mask value.
ClipsToBounds
The use case for clipsToBounds is more for subviews which are partially outside the main view.
For example, I have a (circular) subview on the edge of its parent (rectangular) UIView. If you set clipsToBounds to YES, only half the circle/subview will be shown. If set to NO, the whole circle will show up. Just encountered this so wanted to share
Conclusion
MaskToBounds are applied for the sublayer of any view. Like here OP added layer over button but it does not give effects. I mean layer is not bounds properly.
ClipToBounds are applied on the subVies of any view. Assume you have you have a view( says, viewBG ) and and now you added another view (says, upperView), now you dont wanted to see view upper to look outside the viewBG. ViewUpper always bounded inside its superview. so in this case you have to true the clipstobounds.
Practical experience
Try this below code in swift
let viewBG : UIView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
viewBG.backgroundColor = UIColor.lightGray
self.view.addSubview(viewBG)
let viewUpper : UIView = UIView(frame: CGRect(x: -50, y: -50, width: 100, height: 100))
viewUpper.backgroundColor = UIColor.blue
viewBG.addSubview(viewUpper)
Output
1. When i done viewBG.clipsToBounds = false
When i done viewBG.clipsToBounds = true
You need to enable clipToBound property of button in storyboard;
You could try a different, very simple method of curving the corners on a UIButton or Label.
Click on button
Click on identity inspector
Then add an attribute in the identity section
Key path is layer.cornerRadius
Set as Number
Finally give a value, the higher the value, the more rounded the corners are.
Well that was an easy fix. I just needed to round the corners in viewWillLayoutSubviews.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
loginBtn.roundOneSide(topCorner: .topLeft, bottomCorner: .bottomLeft)
facebookBtn.roundOneSide(topCorner: .topRight, bottomCorner: .bottomRight)
}
I've encountered a problem with code I'd written to cut off the corners of a UILabel (or, indeed, any UIView-derived object to which you can add sublayers) -- I do have to thank Kurt Revis for his answer to Use a CALayer to add a diagonal banner/badge to the corner of a UITableViewCell that pointed me in this direction.
I don't have a problem if the corner overlays a solid color -- it's simple enough to make the cut-off corner match that color. But if the corner overlays an image, how would you let the image show through?
I've searched SO for anything similar to this problem, but most of those answers have to do with cells in tables and all I'm doing here is putting a label on a screen's view.
Here's the code I use:
-(void)returnChoppedCorners:(UIView *)viewObject
{
NSLog(#"Object Width = %f", viewObject.layer.frame.size.width);
NSLog(#"Object Height = %f", viewObject.layer.frame.size.height);
CALayer* bannerLeftTop = [CALayer layer];
bannerLeftTop.backgroundColor = [UIColor blackColor].CGColor;
// or whatever color the background is
bannerLeftTop.bounds = CGRectMake(0, 0, 25, 25);
bannerLeftTop.anchorPoint = CGPointMake(0.5, 1.0);
bannerLeftTop.position = CGPointMake(10, 10);
bannerLeftTop.affineTransform = CGAffineTransformMakeRotation(-45.0 / 180.0 * M_PI);
[viewObject.layer addSublayer:bannerLeftTop];
CALayer* bannerRightTop = [CALayer layer];
bannerRightTop.backgroundColor = [UIColor blackColor].CGColor;
bannerRightTop.bounds = CGRectMake(0, 0, 25, 25);
bannerRightTop.anchorPoint = CGPointMake(0.5, 1.0);
bannerRightTop.position = CGPointMake(viewObject.layer.frame.size.width - 10.0, 10.0);
bannerRightTop.affineTransform = CGAffineTransformMakeRotation(45.0 / 180.0 * M_PI);
[viewObject.layer addSublayer:bannerRightTop];
}
I'll be adding similar code to do the BottomLeft and BottomRight corners, but, right now, those are corners that overlay an image. The bannerLeftTop and bannerRightTop are actually squares that are rotated over the corner against a black background. Making them clear only lets the underlying UILabel background color appear, not the image. Same for using the z property. Is masking the answer? Oo should I be working with the underlying image instead?
I'm also encountering a problem with the Height and Width being passed to this method -- they don't match the constrained Height and Width of the object. But we'll save that for another question.
What you need to do, instead of drawing an opaque corner triangle over the label, is mask the label so its corners aren't drawn onto the screen.
Since iOS 8.0, UIView has a maskView property, so we don't actually need to drop to the Core Animation level to do this. We can draw an image to use as a mask, with the appropriate corners clipped. Then we'll create an image view to hold the mask image, and set it as the maskView of the label (or whatever).
The only problem is that (in my testing) UIKit won't resize the mask view automatically, either with constraints or autoresizing. We have to update the mask view's frame “manually” if the masked view is resized.
I realize your question is tagged objective-c, but I developed my answer in a Swift playground for convenience. It shouldn't be hard to translate this to Objective-C. I didn't do anything particularly “Swifty”.
So... here's a function that takes an array of corners (specified as UIViewContentMode cases, because that enum includes cases for the corners), a view, and a “depth”, which is how many points each corner triangle should measure along its square sides:
func maskCorners(corners: [UIViewContentMode], ofView view: UIView, toDepth depth: CGFloat) {
In Objective-C, for the corners argument, you could use a bitmask (e.g. (1 << UIViewContentModeTopLeft) | (1 << UIViewContentModeBottomRight)), or you could use an NSArray of NSNumbers (e.g. #[ #(UIViewContentModeTopLeft), #(UIViewContentModeBottomRight) ]).
Anyway, I'm going to create a square, 9-slice resizable image. The image will need one point in the middle for stretching, and since each corner might need to be clipped, the corners need to be depth by depth points. Thus the image will have sides of length 1 + 2 * depth points:
let s = 1 + 2 * depth
Now I'm going to create a path that outlines the mask, with the corners clipped.
let path = UIBezierPath()
So, if the top left corner is clipped, I need the path to avoid the top left point of the square (which is at 0, 0). Otherwise, the path includes the top left point of the square.
if corners.contains(.TopLeft) {
path.moveToPoint(CGPoint(x: 0, y: 0 + depth))
path.addLineToPoint(CGPoint(x: 0 + depth, y: 0))
} else {
path.moveToPoint(CGPoint(x: 0, y: 0))
}
Do the same for each corner in turn, going around the square:
if corners.contains(.TopRight) {
path.addLineToPoint(CGPoint(x: s - depth, y: 0))
path.addLineToPoint(CGPoint(x: s, y: 0 + depth))
} else {
path.addLineToPoint(CGPoint(x: s, y: 0))
}
if corners.contains(.BottomRight) {
path.addLineToPoint(CGPoint(x: s, y: s - depth))
path.addLineToPoint(CGPoint(x: s - depth, y: s))
} else {
path.addLineToPoint(CGPoint(x: s, y: s))
}
if corners.contains(.BottomLeft) {
path.addLineToPoint(CGPoint(x: 0 + depth, y: s))
path.addLineToPoint(CGPoint(x: 0, y: s - depth))
} else {
path.addLineToPoint(CGPoint(x: 0, y: s))
}
Finally, close the path so I can fill it:
path.closePath()
Now I need to create the mask image. I'll do this using an alpha-only bitmap:
let colorSpace = CGColorSpaceCreateDeviceGray()
let scale = UIScreen.mainScreen().scale
let gc = CGBitmapContextCreate(nil, Int(s * scale), Int(s * scale), 8, 0, colorSpace, CGImageAlphaInfo.Only.rawValue)!
I need to adjust the coordinate system of the context to match UIKit:
CGContextScaleCTM(gc, scale, -scale)
CGContextTranslateCTM(gc, 0, -s)
Now I can fill the path in the context. The use of white here is arbitrary; any color with an alpha of 1.0 would work:
CGContextSetFillColorWithColor(gc, UIColor.whiteColor().CGColor)
CGContextAddPath(gc, path.CGPath)
CGContextFillPath(gc)
Next I create a UIImage from the bitmap:
let image = UIImage(CGImage: CGBitmapContextCreateImage(gc)!, scale: scale, orientation: .Up)
If this were in Objective-C, you'd want to release the bitmap context at this point, with CGContextRelease(gc), but Swift takes care of it for me.
Anyway, I convert the non-resizable image to a 9-slice resizable image:
let maskImage = image.resizableImageWithCapInsets(UIEdgeInsets(top: depth, left: depth, bottom: depth, right: depth))
Finally, I set up the mask view. I might already have a mask view, because you might have clipped the view with different settings already, so I'll reuse an existing mask view if it is an image view:
let maskView = view.maskView as? UIImageView ?? UIImageView()
maskView.image = maskImage
Finally, if I had to create the mask view, I need to set it as view.maskView and set its frame:
if view.maskView != maskView {
view.maskView = maskView
maskView.frame = view.bounds
}
}
OK, how do I use this function? To demonstrate, I'll make a purple background view, and put an image on top of it:
let view = UIImageView(image: UIImage(named: "Kaz-256.jpg"))
view.autoresizingMask = [ .FlexibleWidth, .FlexibleHeight ]
let backgroundView = UIView(frame: view.frame)
backgroundView.backgroundColor = UIColor.purpleColor()
backgroundView.addSubview(view)
XCPlaygroundPage.currentPage.liveView = backgroundView
Then I'll mask some corners of the image view. Presumably you would do this in, say, viewDidLoad:
maskCorners([.TopLeft, .BottomRight], ofView: view, toDepth: 50)
Here's the result:
You can see the purple background showing through the clipped corners.
If I were to resize the view, I'd need to update the mask view's frame. For example, I might do this in my view controller:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.cornerClippedView.maskView?.frame = self.cornerClippedView.bounds
}
Here's a gist of all the code, so you can copy and paste it into a playground to try out. You'll have to supply your own adorable test image.
UPDATE
Here's code to create a label with a white background, and overlay it (inset by 20 points on each side) on the background image:
let backgroundView = UIImageView(image: UIImage(named: "Kaz-256.jpg"))
let label = UILabel(frame: backgroundView.bounds.insetBy(dx: 20, dy: 20))
label.backgroundColor = UIColor.whiteColor()
label.font = UIFont.systemFontOfSize(50)
label.text = "This is the label"
label.lineBreakMode = .ByWordWrapping
label.numberOfLines = 0
label.textAlignment = .Center
label.autoresizingMask = [ .FlexibleWidth, .FlexibleHeight ]
backgroundView.addSubview(label)
XCPlaygroundPage.currentPage.liveView = backgroundView
maskCorners([.TopLeft, .BottomRight], ofView: label, toDepth: 50)
Result:
I've got the following screen design :
I want to render MKMapView in UIImage, than apply elipsis UIBezierPath and clip top part of UIImage. How can i achieve this? Thanks in advance.
Here is a simple implementation that you can follow to have the similar effect using CAShapeLayer.
Create sufficiently ellipse path to fit height of your image view, but width can be adjusted to control curve.
Create rectangular path to fit the size of the imageView, width and height should be match the size of imageView.
Transform the circle in such a way that rectangular path is exactly at the middle of the circle.
Now, if you look at the image above, the rectangle has the same size as your imageView. If you somehow manage to remove the portion of shapes which are not intersected, you will have your desired effect.
And this will be the portion of the image that you will be masking,
This can be achieved quite easily using CAShapeLayer.
Here is a simple implementation that you can use,
let image = UIImage(named: "image.jpg")
let imageSize = image!.size
let imageView = UIImageView(frame: CGRect(origin: CGPoint.zero, size: imageSize))
imageView.clipsToBounds = true
imageView.image = image
let curveRadius: CGFloat = imageSize.width * 0.005
let invertedRadius: CGFloat = 1.0 / curveRadius
// draw ellipse in rect with big width, but same height
let ellipticalPath = UIBezierPath(ovalInRect: CGRectMake(0, 0, imageSize.width + 2 * invertedRadius * imageSize.width, imageSize.height))
// transform it to center of imageView
ellipticalPath.applyTransform(CGAffineTransformMakeTranslation(-imageSize.width * invertedRadius, 0))
// create rectangle path exactly similar to imageView
let rectanglePath = UIBezierPath(rect: imageView.bounds)
// translate it by 0.5 ratio in order to create intersection between circle and rectangle
rectanglePath.applyTransform(CGAffineTransformMakeTranslation(0, -imageSize.height * CGFloat(0.5)))
// append rectangle to elliptical path
ellipticalPath.appendPath(rectanglePath)
// create mask
let maskLayer = CAShapeLayer()
maskLayer.frame = imageView.bounds
maskLayer.path = ellipticalPath.CGPath
imageView.layer.mask = maskLayer
And here is how it looks,
You can adjust the value of curveRadius to suit your need.
Note: That the shape layer intersection is possible due to something called fillRule property on CAShapeLayer, which has a default value of kCAFillRuleNonZero. Read more about it here, https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CAShapeLayer_class/#//apple_ref/occ/instp/CAShapeLayer/fillRule
We have a CALayer which shows image with edit information. Such as rotate, scale and translate. I want to make a CGPath which exactly match the layer bounds. When the layer rotate, my path should also rotate and its four corners should match the CALayer's four corners. The CGPath is actually used as a gray mask to show the clipped area of the image.
I try following code, but it does not work.
f = self.imageLayer.frame;
t = self.imageLayer.affineTransform;
CGPathAddRect(path, &t, f);
The CALayer has its own CGAffineTransform. All edit information are applied via the CGAffineTransform.
Any hint will be appreciated, Thanks a lot.
If I got your question right you could be using UIBezierPath's usesEvenOddFillRule to cut around your image layer bounds dimming the rest of the visible screen. Basically you create a very large rectangular path (for some reasons CGRectInfinite doesn't work here) and cut out the area around your image layer. The trick is to use kCAFillRuleEvenOdd which flips what is considered inside and outside.
Something like should work:
let cutPath = UIBezierPath(roundedRect: imageLayer.bounds, cornerRadius: 0)
let clipPath = UIBezierPath(rect: CGRectMake(-10e6, -10e6, 10e7, 10e7)) // CGRectInfinite isn't working here ?
clipPath.appendPath(cutPath)
clipPath.usesEvenOddFillRule = true
let shape = CAShapeLayer()
shape.contentsScale = UIScreen.mainScreen().scale
shape.lineWidth = 2
shape.fillColor = UIColor(red:0.1, green:0.1, blue:0.1, alpha:0.5).CGColor
shape.fillRule = kCAFillRuleEvenOdd
shape.strokeColor = UIColor.whiteColor().CGColor
shape.path = clipPath.CGPath
imageLayer.addSublayer(shape)
// do a transformations here
imageLayer.transform = CATransform3DMakeRotation(10.0, 1.0, 1.0, 0.8)
Which results
I am working on a Sprite Kit project in which I need to display in some cases only half of an existing image.
I tried making the frame of the sprite smaller, but it just stretches the image.
Is there any possibility to use a mask or something in order to display only half of the sprite/image/texture?
So, in order to show only half of the image, texture, sprite, someone would need to use a SKCropNode. The only sensible thing is to crop the sprite you need starting from half, not just cropping with a predefined size. This can be achieved by setting the mask node position.
1) create a SkSpriteNode with that texture/image:
// Obj-C
SKSpriteNode *skelet = [SKSpriteNode spriteNodeWithImageNamed:imageName];
// Swift
let skelet = SKSpriteNode(imageNamed: imageName)
2) create the SKCropNode:
// Obj-C
SKCropNode * cropNode = [SKCropNode node];
// Swift
let cropNode = SKCropNode()
3) create the mask
// Obj-C
SKSpriteNode *mask = [SKSpriteNode spriteNodeWithColor:[UIColor blackColor] size:CGSizeMake(skelet.frame.size.width/2, skelet.frame.size.height)];
// Swift
let mask = SKSpriteNode(color: .black, size: CGSize(width: skelet.frame.size.width/2, height: skelet.frame.size.height))
** set the mask position to the half of the result you need (you need half of the skelet -> set the mask position to the half "of the half of the skelet")
// Obj-C
mask.position = CGPointMake(skelet.frame.size.width/4, 0);
// Swift
mask.position = CGPoint(x: skelet.frame.size.width/4, y: 0)
The division by 4 is because you need the center of the mask to be not in the center of the skelet node, but moved to the half of a half of the skelet (reminder the mask node works with a default anchor point of 0.5 0.5 - so the zero point corresponds with the center of the skelet node).
4) add the needed elements to the crop node
// Obj-C
[cropNode addChild:skelet];
[cropNode setMaskNode:mask];
[self addChild:cropNode];
// Swift
cropNode.addChild(skelet)
cropNode.maskNode = mask
self.addChild(cropNode)