In SpriteKit, SKCropNode has no effect over a SKShapeNode - objective-c

I've been trying to apply a mask to a SKShapeNode using SKCropNode, and so far without success. Thinking that it's a SpriteKit bug - Here is the code snippet:
SKNode* contentNode = [SKNode node];
// picture - use an image bigger than 50x50
SKSpriteNode *pictureNode = [SKSpriteNode spriteNodeWithImageNamed:#"tree"];
// triangle
SKShapeNode* triangleNode = [SKShapeNode node];
UIBezierPath* triangleNodeBezierPath = [[UIBezierPath alloc] init];
[triangleNodeBezierPath moveToPoint:CGPointMake(0.0, 0.0)];
[triangleNodeBezierPath addLineToPoint:CGPointMake(0.0, 100.0)];
[triangleNodeBezierPath addLineToPoint:CGPointMake(50.0, 100.0)];
[triangleNodeBezierPath closePath];
triangleNode.path = triangleNodeBezierPath.CGPath;
triangleNode.fillColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1];
// create a mask
SKSpriteNode *mask = [SKSpriteNode spriteNodeWithColor:[SKColor blackColor] size: CGSizeMake(50, 50)]; //50 by 50 is the size of the mask
// create a SKCropNode
SKCropNode *cropNode = [SKCropNode node];
[cropNode addChild: contentNode];
[cropNode setMaskNode: mask];
[self addChild: cropNode];
[contentNode addChild:pictureNode]; // pictureNode is being cropped
[contentNode addChild:triangleNode]; // triangleNode is not
cropNode.position = CGPointMake( CGRectGetMidX (self.frame), CGRectGetMidY (self.frame));
Does anyone have a workaround about this issue? Thanks a lot!

This had been bugging me for most of the day. I had planned to create a timer similar to the excellent TCProgressTimer by Tony Chamblee. However, as my application uses multiple progress timers I didn't want to have to design dozens of different sized sprites for use at different resolutions.
My solution was to convert SKShapeNode objects to SKSpriteNode objects. I ended up having to go back to basics and use Core Graphics to do the heavy lifting. This is a rather messy way of doing things, I'm sure, but I wanted quick results to dynamically create objects that would resemble the results obtained when using SKShapeNode.
I am only interested in making circle objects at present, so I did it like this:
-(SKSpriteNode *)createSpriteMatchingSKShapeNodeWithRadius:(float)radius color:(SKColor *)color {
CALayer *drawingLayer = [CALayer layer];
CALayer *circleLayer = [CALayer layer];
circleLayer.frame = CGRectMake(0,0,radius*2.0f,radius*2.0f);
circleLayer.backgroundColor = color.CGColor;
circleLayer.cornerRadius = circleLayer.frame.size.width/2.0;
circleLayer.masksToBounds = YES;
[drawingLayer addSublayer:circleLayer];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(circleLayer.frame.size.width, circleLayer.frame.size.height), NO, [UIScreen mainScreen].scale);
CGContextSetAllowsAntialiasing(UIGraphicsGetCurrentContext(), TRUE);
CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [UIColor clearColor].CGColor);
CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0,0,circleLayer.frame.size.width,circleLayer.frame.size.height));
[drawingLayer renderInContext: UIGraphicsGetCurrentContext()];
UIImage *layerImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithTexture:[SKTexture textureWithImage:layerImage]];
return sprite;
}
The resulting sprite can now be masked by an SKCropNode as expected. As these sprites are all generated before the scene begins, I do not notice a performance hit. However, I would imagine this method is highly inefficient if you are generating multiple nodes on the fly.
I would be eager to hear solutions from other users. Hope that helps.
-DC

I have the similar task in my app. I need to draw multiple irregular shapes based on user input and then use them as crop node's masks.
The solution that works for me is to:
create a SKShapeNode with the required path
retrieve a SKTexture from it using SKView method textureFromNode:crop:
create a SKSpriteNode from that texture.
use SKSprite node as a mask.

Your title is correct. I've also discovered having a ShapeNode in the CropNode's hierarchy affects the children above it as well. I created a quick experiment. You can create a new game project and try it yourself. By commenting out one of the addChild:shapeContent lines you can see how it affects the cropping of the spaceship image.
As DoctorClod points out the current solution seems to be making sure all children of the cropNode are SpriteNodes.
-(void)didMoveToView:(SKView *)view {
SKSpriteNode* colorBackground = [SKSpriteNode spriteNodeWithColor:[SKColor redColor] size:CGSizeMake(800, 600)];
SKSpriteNode *spaceshipImage = [SKSpriteNode spriteNodeWithImageNamed:#"Spaceship"];
SKShapeNode* shapeContent = [SKShapeNode shapeNodeWithRect:CGRectMake(0, 0, 200, 100)];
shapeContent.fillColor = [SKColor greenColor];
SKSpriteNode *maskShape = [SKSpriteNode spriteNodeWithColor:[SKColor blackColor] size:CGSizeMake(500, 100)];
SKCropNode *cropNode = [SKCropNode new];
[cropNode addChild:colorBackground];
[cropNode addChild:shapeContent]; // comment this one out
[cropNode addChild:spaceshipImage];
[cropNode addChild:shapeContent]; // or comment this one out
[cropNode setMaskNode:maskShape];
cropNode.position = CGPointMake(CGRectGetMidX (self.frame), CGRectGetMidY(self.frame));
[self addChild:cropNode];
}

Related

NSView with masked CIFilter for OS X app

I am developing an app that contains lots of custom NSView objects being moved around. I have implemented a gaussian blur background filter for one of the custom NSView subclasses like so:
- (id)init {
self = [super init];
if (self) {
...
CIFilter *saturationFilter = [CIFilter filterWithName:#"CIColorControls"];
[saturationFilter setDefaults];
[saturationFilter setValue:#.5 forKey:#"inputSaturation"];
CIFilter *blurFilter = [CIFilter filterWithName:#"CIGaussianBlur"];
[blurFilter setDefaults];
[blurFilter setValue:#2.0 forKey:#"inputRadius"];
self.wantsLayer = YES;
self.layer.backgroundColor = [NSColor clearColor].CGColor;
self.layer.masksToBounds = YES;
self.layer.needsDisplayOnBoundsChange = YES;
self.layerUsesCoreImageFilters = YES;
[self updateFrame]; //this is where the frame size is set
self.layer.backgroundFilters = #[saturationFilter, blurFilter];
...
return self;
}
else return nil;
}
This works great and creates a gaussian blur effect within the entire contents of the view. The problem is that I do not want the gaussian blur to cover the entire view. There is about an (intentional) 12px padding between the actual size of the NSView and the drawing of its content box:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSColor* strokeColor = [NSColor colorWithRed:.5 green:.8 blue:1 alpha:1];
NSColor* fillColor = [NSColor colorWithRed:.5 green:.8 blue:1 alpha:.2];
...
[strokeColor setStroke];
[fillColor setFill];
NSBezierPath *box = [NSBezierPath bezierPathWithRoundedRect:NSMakeRect(self.bounds.origin.x + 12, self.bounds.origin.y + 12, self.bounds.size.width - 24, self.bounds.size.height - 24) xRadius:6 yRadius:6];
box.lineWidth = 6;
[box stroke];
[box fill];
...
}
The reason for this padding is that there are some pieces of the GUI that inhabit this region and are drawn seamlessly into the containing box. I would like to mask the Blur effect to only have effect on the interior of the drawn box rather than the entire view. Here is what I have tried.
ATTEMPT 1: Create a sublayer
I created a sublayer in the NSView with the appropriately sized frame, and added the blur effect to this sublayer. PROBLEM: The blur effect seems to only apply to the immediate parent layer, so rather than blur the contents behind the NSView, it blurs the contents of the NSView's self.layer (which is basically empty).
ATTEMPT 2: Create a masking layer
I tried to create a masking layer and set it to self.layer.mask. However, since the positions of the GUI content do change (via the DrawRect function), I would need to get a copy of the current layer to use as the masking layer. I tried the following code, but it had no effect.
self.layer.mask = nil;
NSArray *bgFilters = self.layer.backgroundFilters;
self.layer.backgroundFilters = nil;
CALayer *maskingLayer = self.layer.presentationLayer;
self.layer.mask = maskingLayer;
self.layer.backgroundFilters = bgFilters;
ATTEMPT 3: Draw a masking layer directly
I could not find any examples of how to draw directly on a layer. I can not use a static UIImage to mast with, because, as I said above, the mask has to change with user interaction. I was looking for something equivalent to the DrawRect function. Any help would be appreciated.
SO...
It seems to me that the sublayer way would be the best and simplest way to go, if I could just figure out how to change the priority of the blur effect to be the background behind the NSView not the NSView's background layer behind the sublayer.
Well, I would still like to know if there is a more elegant way, but I have found a solution that works. Basically, I have created a masking layer from an NSImage drawn from a modified version of the drawRect function:
- (id)init {
self = [super init];
if (self) {
// SETUP VIEW SAME AS ABOVE
CALayer *maskLayer = [CALayer layer];
maskLayer.contents = [NSImage imageWithSize:self.frame.size flipped:YES drawingHandler:^BOOL(NSRect dstRect) {
[self drawMask:self.bounds];
return YES;
}];
maskLayer.frame = self.bounds;
self.layer.mask = maskLayer;
return self;
}
else return nil;
}
- (void)drawMask:(NSRect)dirtyRect {
[[NSColor clearColor] set];
NSRectFill(self.bounds);
[[NSColor blackColor] set];
// SAME DRAWING CODE AS drawRect
// EXCEPT EVERYTHING IS SOLID BLACK (NO ALPHA TRANSPARENCY)
// AND ONLY NEED TO DRAW PARTS THAT EFFECT THE EXTERNAL BOUNDARIES
}

Incorrect work of SKPhysicsBody with moved SKSpriteNode anchorPoint?

Here is my code:
// setting background
_ground = [[SKSpriteNode alloc]
initWithColor:[SKColor greenColor]
size:CGSizeMake([Helpers landscapeScreenSize].width, 50)];
_ground.name = #"ground";
_ground.zPosition = 1;
_ground.anchorPoint = CGPointMake(0, 0);
_ground.position = CGPointMake(0, 0);
_ground.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake([Helpers landscapeScreenSize].width, 50)];
_ground.physicsBody.dynamic = NO;
_ground.physicsBody.affectedByGravity = NO;
_ground.physicsBody.categoryBitMask = groundCategory;
_ground.physicsBody.collisionBitMask = elementCategory;
_ground.physicsBody.contactTestBitMask = elementCategory;
Green rectangle positioning is ok, but SKPhysicsBody is not representing this rectangle correctly. It looks like SKPhysicsBody is not "moving" its body according to sprite position and anchorPoint. SKPhysicsBody is moved left for _ground.width points.
What am I doing wrong?
PS.
Changing to this (code) solved my problem, but I really dont like this solution, it seems an ugly way to position an element.
_ground.position = CGPointMake([Helpers landscapeScreenSize].width / 2, 0);
+removing:
_ground.position = CGPointMake(0, 0);
I had exactly the same issue yesterday ;)
For me I solved this by using the default sprite positioning by SpriteKit. I just set the position and leave the anchor point at it's default. Then you set the physicsbody to the size of the sprite.
Change your code to the following and check if it works:
_ground = [[SKSpriteNode alloc]
initWithColor:[SKColor greenColor]
size:CGSizeMake([Helpers landscapeScreenSize].width, 50)];
_ground.position = CGPointMake([Helpers landscapeScreenSize].width / 2, 0);
_ground.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:_ground.size];
If you still have no luck with this you can also try to use another initialisation for your sprite (this works in my project). Try it with a texture:
[[SKSpriteNode alloc] initWithTexture:[SKTexture textureWithCGImage:[UIImage imageWithColor:[UIColor whiteColor]].CGImage] color:[UIColor whiteColor] size:CGSizeMake(PLAYER_WIDTH, PLAYER_HEIGHT)];
don't forget to implement the (UIImage*)imageWithColor: method to create a 1x1 pixel image of your desired color for you (see here).

SKPhysicsBody and [SKNode setScale:]

In my current project using SpriteKit, I have a bunch of sprites that need to be scaled up and down independently at various times. The problem is that when I scale the node, the physics body doesn't scale with it so it screws up the physics. Here's a small example I put together for the purpose of this question:
CGSize objectSize = CGSizeMake(100, 100);
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:CGRectMake(0, 0, self.size.width, self.size.height)];
SKSpriteNode *n1 = [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:objectSize];
n1.position = CGPointMake(self.size.width/2, 2*self.size.height/3);
n1.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:objectSize];
n1.physicsBody.affectedByGravity = YES;
[self addChild:n1];
SKSpriteNode *n2 = [SKSpriteNode spriteNodeWithColor:[UIColor redColor] size:objectSize];
n2.position = CGPointMake(self.size.width/2, self.size.height/3);
n2.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:objectSize];
n2.physicsBody.affectedByGravity = YES;
[self addChild:n2];
[n1 setScale:0.5];
Notice how the blue sprite (scaled down) sits on top of the red one but you can tell its physics body still has the dimension I told it, and it didn't scale.
So obviously, scaling down the node doesn't scale down the physicsBody. So my question is if I have to manually do it, how do I go about it?
I tried swapping the body with one of the right size when scaling, but then things get really convoluted if the old body had joints, etc... It'd be a lot simpler if I could just scale the existing body somehow.
Using either an SKAction or the Update loop, you can create a new SKPhysicsBody with the proper scale and apply it to the object. However, by doing so, you will lose the velocity. To fix this, you can do the following:
SKPhysicsBody *newBody = [SKPhysicsBody bodyWithRectangleOfSize:boxObject.size];
newBody.velocity = boxObject.physicsBody.velocity;
boxObject.physicsBody = newBody;
Physics body shapes can't be scaled. Definitely not in Box2D which Sprite Kit uses internally.
Besides internal optimizations, scaling a physics body would have far reaching implications. For example scaling a body's shape up could get the body stuck in collisions. It would affect how joints interact. It would definitely change the body's density or mass and thus its behavior.
You could use a SKNode to which you add the sprite without a physics body, and then add additional SKNode with bodies of given sizes. You could then enable or disable the bodies when you start scaling the sprite. If you time this right the player won't notice that the collision shape simply went from full size to half size while the sprite animates that scaling transition.
You would have to calculate the body's density and perhaps other properties according to its size though.
For anyone else reading this answer, I believe that now Physics Bodies scale with the SKNode. I am using Xcode 8.2.1 and Swift 3.0.2. I start a new Project, select Game, fill in the details. Change 2 files:
GameViewController.swift (add one line)
view.showsFPS = true
view.showsNodeCount = true
// add this to turn on Physics Edges
view.showsPhysics = true
Game Scene.swift (replace the sceneDidLoad()) function with this:
override func sceneDidLoad() {
var found = false
self.enumerateChildNodes(withName: "testSprite") {
node, stop in
found = true
}
if !found {
let testSprite = SKSpriteNode(color: UIColor.white, size: CGSize(width: 200.0, height: 200.0))
testSprite.physicsBody = SKPhysicsBody(polygonFrom: CGPath(rect: CGRect(x: -100.0, y: -100.0, width: 200.0, height: 200.0), transform: nil))
testSprite.physicsBody?.affectedByGravity = false
testSprite.name = "testSprite"
let scaleAction = SKAction.repeatForever(SKAction.sequence([
SKAction.scale(to: 2.0, duration: 2.0),
SKAction.scale(to: 0.5, duration: 2.0)
]))
testSprite.run(scaleAction)
self.addChild(testSprite)
}
}
The physics boundary is scaling with the sprite node.
I had the exact same problem, I think its a bug in sprite kit.
Try using an action to scale, this works on whole scene.
[self runAction:[SKAction scaleTo:0.5 duration:0]];
My original question
Is this what you want to happen
- (void) anthoertest
{
CGSize objectSize = CGSizeMake(100, 100);
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:CGRectMake(0, 0, self.size.width, self.size.height)];
SKSpriteNode *n1 = [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:objectSize];
n1.position = CGPointMake(self.size.width/2, 2*self.size.height/3);
n1.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:n1.size];
n1.physicsBody.affectedByGravity = YES;
[self addChild:n1];
SKSpriteNode *n2 = [SKSpriteNode spriteNodeWithColor:[UIColor redColor] size:objectSize];
n2.position = CGPointMake(self.size.width/2, self.size.height/3);
n2.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:n2.size];
n2.physicsBody.affectedByGravity = YES;
[self addChild:n2];
[self shrinkMe:n1];
}
- (void) shrinkMe:(SKSpriteNode *) s
{
[s setScale:0.5];
s.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:s.size];
}
The physics body depends on the CGPath of the SKNode; you'll need to change that somehow. You might try scaling the CGPath with something like the following:
//scale up
CGFloat scaleFactor = 1.1;
CGAffineTransform scaleTransform = CGAffineTransformIdentity;
scaleTransform = CGAffineTransformScale(scaleTransform, scaleFactor, scaleFactor);
CGPathRef scaledPathRef = CGPathCreateCopyByTransformingPath(exampleNode.path, &scaleTransform);
exampleNode.path = scaledPathRef;
But keep in mind this won't update the appearance of nodes already in an SKScene. You might need to combine resizing the CGPath with an SKAction that scales the node.
I had this same problem, all I did was define my physics body in my update current time function and it would scale with it.
For you it would be something like this:
override func sceneDidLoad() {
SKSpriteNode *n1 = [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:objectSize];
n1.position = CGPointMake(self.size.width/2, 2*self.size.height/3);
}
override func update(_ currentTime: TimeInterval) {
n1.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:objectSize];
n1.physicsBody.affectedByGravity = YES;
}
The only problem is that you have to redefine your physics properties every time in updateCurrentTime. You can make some of these properties like it's restitution equal a variable so you can change its properties by changing the variable elsewhere. Redefining it anywhere else only works until the next frame is called.

Draw Rectangle/ Circle and Triangle in Spritekit with Two Colors. . .

I can draw Rectangle using simple SKSpriteNode. But i can not draw other types of drawings in it like Triangle, Circle etc with TWO SPLIT COLORS. Someone suggested to go with CGPath. But i am newbie and dont know to draw such type of complex things . Please can anyone illustrate simple way to go with these drawings with MULTICOLOR in SPRITEKIT. Mean their upper part is one color and lower part in 2nd color. More concise to say that Shape is divided into two colors whether that is star, rectangle, triangle or else.
Any Help will be greatly appreciated.
Thanks .
You can use SKShapeNode to draw shapes in sprite kit, but each SKShapeNode is limited to one line color (strokeColor) and one fill color.
However, you can create a custom SKNode subclass that contains two SKShapeNodes as children, each with different strokeColors/fillColors.
Something like this will work for a custom SKNode that draws a square with left and top red, right and bottom green:
- (id) init {
if (self = [super init]) {
SKShapeNode* topLeft = [SKShapeNode node];
UIBezierPath* topLeftBezierPath = [[UIBezierPath alloc] init];
[topLeftBezierPath moveToPoint:CGPointMake(0.0, 0.0)];
[topLeftBezierPath addLineToPoint:CGPointMake(0.0, 100.0)];
[topLeftBezierPath addLineToPoint:CGPointMake(100.0, 100.0)];
topLeft.path = topLeftBezierPath.CGPath;
topLeft.lineWidth = 10.0;
topLeft.strokeColor = [UIColor redColor];
topLeft.antialiased = NO;
[self addChild:topLeft];
SKShapeNode* bottomRight = [SKShapeNode node];
UIBezierPath* bottomRightBezierPath = [[UIBezierPath alloc] init];
[bottomRightBezierPath moveToPoint:CGPointMake(0.0, 0.0)];
[bottomRightBezierPath addLineToPoint:CGPointMake(100.0, 0.0)];
[bottomRightBezierPath addLineToPoint:CGPointMake(100.0, 100.0)];
bottomRight.path = bottomRightBezierPath.CGPath;
bottomRight.lineWidth = 10.0;
bottomRight.strokeColor = [UIColor greenColor];
bottomRight.antialiased = NO;
[self addChild:bottomRight];
}
return self;
}

Lag with CALayer when double tap on the home button

When I put shadow etc. with CALayer my app is lagging when I double tap on the home button to see tasks running. I don't have any other lag, just when I double tap.
I call this method 20 times to put 20 images :
- (UIView *)createImage:(CGFloat)posX posY:(CGFloat)posY imgName:(NSString *)imgName
{
UIView *myView = [[UIView alloc] init];
CALayer *sublayer = [CALayer layer];
sublayer.backgroundColor = [UIColor blueColor].CGColor;
sublayer.shadowOffset = CGSizeMake(0, 3);
sublayer.shadowRadius = 5.0;
sublayer.shadowColor = [UIColor blackColor].CGColor;
sublayer.shadowOpacity = 0.8;
sublayer.frame = CGRectMake(posX, posY, 65, 65);
sublayer.borderColor = [UIColor blackColor].CGColor;
sublayer.borderWidth = 2.0;
sublayer.cornerRadius = 10.0;
CALayer *imageLayer = [CALayer layer];
imageLayer.frame = sublayer.bounds;
imageLayer.cornerRadius = 10.0;
imageLayer.contents = (id) [UIImage imageNamed:imgName].CGImage;
imageLayer.masksToBounds = YES;
[sublayer addSublayer:imageLayer];
[myView.layer addSublayer:sublayer];
return myView;
}
I have commented all my code except this, so I'm sure the lag comes from here. Also I've checked with the Allocations tools and my app never exceeded 1Mo. When I'm just putting images without shadow etc. everything works fine.
Try setting a shadowPath on the layer as well. It will need to be a rounded rect since you've got rounded corners on your layer.
CALayer has to calculate where it is drawing, and where to put the shadow, if it doesn't have a shadow path. This has a big effect on animation performance.
Another way to improve performance with CALayers is to set the shouldRasterize property to YES. This stores the layer contents as a bitmap and prevents it having to re-render everything.