SpriteKit: SKShapeNode.calculateAccumulatedFrame returns a frame, which is bigger than its content - sprite-kit

I create a SKShapeNode rectangle with a fixed size using the following code
SKShapeNode *rect = [SKShapeNode shapeNodeWithRect: CGRectMake(0, 0, 100, 200)];
CGRect accumulatedFrame = rect.calculateAccumulatedFrame;
Debugging the code above, the accumulatedFrame holds the values below:
origin=(x=-0.5, y=-0.5) size=(width=101, height=201)
Why is the calculated, accumulated frame bigger than the intended 100x200 ?
Thanks in advance for any hint :)
Code sample:
SKShapeNode *rect = [SKShapeNode shapeNodeWithRect: CGRectMake(0, 0, 200, 100)];
CGRect accumulatedFrame = rect.calculateAccumulatedFrame;
CGRect frame = rect.frame;
debugger returns origin=(x=-0.5, y=-0.5) size=(width=201, height=101) for the accumulated frame property
debugger returns origin=(x=-0.5, y=-0.5) size=(width=201, height=101) for the frame propery
Using an origin > (0,0) adds only 0,5 to width and height; seems, that sprite-kit returns an accumulated frame, which really contains the node(s) and adds 0.5, so none of the node borders intersects the border of the accumulated frame. Didn't find anything about it in the api reference...

I believe the issue you are dealing with is that if you have a strokeColor defined, that border stroke adds to size of the rectangle.
Try this code :
SKShapeNode *rect = [SKShapeNode shapeNodeWithRect: CGRectMake(0, 0, 200, 100)];
rect.strokeColor = nil;
You have to look at the stroke as being additive, and adding to the dimensions of your shape.

Related

Scale SKShapeNode from center?

I have a SKShapeNode, and a label under one generic SKNode. If I try to scale that node, its position changes for no reason!. If I try to scale only the nodes it contains under it, it scales properly but not from the center, so it grows out to the up and right.
What am I doing wrong?
First before scale
After scale
Notice its position moves up. Can't figure out why, does it even if I do it as an action or not. Also, the position is not changing based on log, but it appears the frame got bigger (though that is expected)
Steps to recreate issue:
set height and width
#h = 50
#w = 256
create rect with height and width
CGRectMake(0, 0, #w, #h)
get path for rect
UIBezierPath.bezierPathWithRect(rect).CGPath
create skshape node with path, set position
antialiased: false,
lineWidth: 1.0,
strokeColor: SKColor.blackColor,
fillColor: SKColor.whiteColor
Create sklabel, vertical center alignment, set position
create generic SKNode, no properties, add skshapenode and sklabel to it as children
Scale the generic SKNode.
I had a similar problem: I created an SKShapeNode.ellipseInRect which look perfectly fine when I didn't need to scale it. Yet as soon as I tried to scale it down to fit the text it was behind, it would randomly shrink towards the bottom left of the screen!
I did a little debugging, and the number that surprised me was that the position was set to (0, 0) even though the ellipse was in the top left corner of the screen! I then realized that by initializing with a rect the "anchor point" (even though SKShapeNodes don't technically have them) was at (0, 0) too. This behavior would explain my shrinking towards that point and your growing away from that point.
What solved this problem for me was instead of putting my position in the rect, set the position afterwards. Then it scaled around my node's center-point and not the screen origin. Hope that helps!
Try this
SKLabelNode *labNode = [SKLabelNode labelNodeWithFontNamed:#"Arial"];
labNode.fontSize = 12.0f;
labNode.fontColor = [SKColor blackColor];
labNode.text = #"SMASH";
SKSpriteNode *topWithText = [SKSpriteNode spriteNodeWithColor:[SKColor greenColor] size:CGSizeMake(100, 30)];
topWithText.position = CGPointMake(100, 110);
[topWithText addChild:labNode];
SKSpriteNode *bottom = [SKSpriteNode spriteNodeWithColor:[SKColor redColor] size:CGSizeMake(100, 10)];
bottom.position = CGPointMake(100, 100);
[self addChild:topWithText];
[self addChild:bottom];
[topWithText runAction:[SKAction sequence:#[
[SKAction waitForDuration:3.0],
[SKAction scaleBy:3.0 duration:3]
]]];
With the SKShapeNode, this is impossible.
I solved it by adding a move Action to center the node manually.
You need to apply this to the y and x coordinates

How can I draw a curved shadow?

Like so:
I know that this will not work with NSShadow, drawing it in drawRect: will work just fine.
You can do this and many other kinds of shadows using Core Animations layers and the shadowPath property. The shadow that you are describing can be make with an elliptical shadow path.
The code to produce this shadow is below. You can tweak the size of the ellipse to have a rounder shape of the shadow. You can also tweak the position, opacity, color and blur radius using the shadow properties on the layer.
self.wantsLayer = YES;
NSView *viewWithRoundShadow = [[NSView alloc] initWithFrame:NSMakeRect(30, 30, 200, 100)];
[self addSubview:viewWithRoundShadow];
CALayer *backingLayer = viewWithRoundShadow.layer;
backingLayer.backgroundColor = [NSColor orangeColor].CGColor;
// Configure shadow
backingLayer.shadowColor = [NSColor blackColor].CGColor;
backingLayer.shadowOffset = CGSizeMake(0, -1.);
backingLayer.shadowRadius = 5.0;
backingLayer.shadowOpacity = 0.75;
CGRect shadowRect = backingLayer.bounds;
CGFloat shadowRectHeight = 25.;
shadowRect.size.height = shadowRectHeight;
// make narrow
shadowRect = CGRectInset(shadowRect, 5, 0);
backingLayer.shadowPath = CGPathCreateWithEllipseInRect(shadowRect, NULL);
Just to show some examples of other shadows than can be created using the same technique; a path like this
will produce a shadow like this
It's far from perfect but I think it does draw the sort of shadow you are looking for. Bear in mind that I have left a plain linear gradient in place from a total black to a clear color. Being so dark, this will not give you a super-realistic shadow unless you tweak the values a bit. You may want to play with the gradient by adding more locations with different alpha values to get whatever stepping you like. Some experimentation is probably required but the values are all there to play with.
As per your suggestion it's a drawRect:(CGRect)rect thing. Just create a custom view and only override it:
- (void)drawRect:(CGRect)rect {
// Get the context
CGContextRef context = UIGraphicsGetCurrentContext();
// Setup the gradient locations. We just want 0 and 1 as in the start and end of the gradient.
CGFloat locations[2] = { 0.0, 1.0 };
// Setup the two colors for the locations. A plain black and a plain black with alpha 0.0 ;-)
CGFloat colors[8] = { 0.0f, 0.0f, 0.0f, 1.0f, // Start color
0.0f, 0.0f, 0.0f, 0.0f }; // End color
// Build the gradient
CGGradientRef gradient = CGGradientCreateWithColorComponents(CGColorSpaceCreateDeviceRGB(),
colors,
locations,
2);
// Load a transformation matrix that will squash the gradient in the current context
CGContextScaleCTM(context,1.0f,0.1f);
// Draw the gradient
CGContextDrawRadialGradient(context, // The context
gradient, // The gradient
CGPointMake(self.bounds.size.width/2,0.0f), // Starting point
0.0f, // Starting redius
CGPointMake(self.bounds.size.width/2,0.0f), // Ending point
self.bounds.size.width/2, // Ending radius
kCGGradientDrawsBeforeStartLocation); // Options
// Release it an pray that everything was well written
CGGradientRelease(gradient);
}
This is how it looks like on my screen...
I simply placed an image just over the shadow but you can easily merge the shadow with an image if you subclass UIImageView and override it's drawRect method.
As you can see, what I did was to simply setup a circular gradient but I loaded a scaling matrix to squash it before drawing it to the context.
If you plan to do anything else in that method, remember that you have the matrix in place and everything you do will be deformed by it. You may want to save the the CTM with CGContextSaveGState() before loading the matrix and then restore the original state with CGContextRestoreGState()
Hope this was what you where looking for.
Cheers.
I could explain how to do this in code, or explain how to use a tool which generate this code for you. I choose the latter.
Using PaintCode (free demo available, 1 hour limit per session).
Draw an oval
Draw a Rectangle which intersects with the bottom of the oval.
CMD click both the rectangle and the oval, in the "Objects" list in the top left corner.
Press the Intersect button in the Toolbar.
Select the Bezier from the Objects list.
Set its Stroke to "No Stroke"
Click the Gradient button (located on the left, below the Selection Inspector)
Press the "+" button
Change the gradient color to light grey.
From the Selection inspector, change the Fill Style to "Gradient"
Select Gradient: Linear
adjust the gradient till you are satisfied.
- (void)viewDidLoad
{
UIImage *natureImage = [UIImage imageNamed:#"nature.jpg"];
CALayer *layer = [CALayer layer];
layer.bounds = CGRectMake(0, 0, 200, 200);
layer.position = CGPointMake(380, 200);
layer.contents = (id)natureImage.CGImage;
layer.shadowOffset = CGSizeMake(0,2);
layer.shadowOpacity = 0.70;
layer.shadowPath = (layer.shadowPath) ? nil : [self bezierPathWithCurvedShadowForRect:layer.bounds].CGPath;
[self.view.layer addSublayer:layer];
}
- (UIBezierPath*)bezierPathWithCurvedShadowForRect:(CGRect)rect {
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint topLeft = rect.origin;
CGPoint bottomLeft = CGPointMake(0.0, CGRectGetHeight(rect) + offset);
CGPoint bottomMiddle = CGPointMake(CGRectGetWidth(rect)/2, CGRectGetHeight(rect) - curve);
CGPoint bottomRight = CGPointMake(CGRectGetWidth(rect), CGRectGetHeight(rect) + offset);
CGPoint topRight = CGPointMake(CGRectGetWidth(rect), 0.0);
[path moveToPoint:topLeft];
[path addLineToPoint:bottomLeft];
[path addQuadCurveToPoint:bottomRight controlPoint:bottomMiddle];
[path addLineToPoint:topRight];
[path addLineToPoint:topLeft];
[path closePath];
return path;
}
Hope this will help you.

Setting a view's bounds changes the coordinates of the frame, WHY?

Why setting the bounds property of a UIView messes up it's frame's coordinates?
For example:
self.view.frame = CGRectMake(10, 10, 200, 200);
CGRect b = CGRectMake(0, 0, 399, 323);
self.view.bounds = b;
I would expect the view's frame to be (10, 10, 399, 323) but instead the coordinates get some weird values like (-89.5 -51.5; 399 323).
Thanks!
From the UIView class reference:
Changing the bounds size grows or shrinks the view relative to its center point.
So it is keeping the center point in the same place, which means the origin of the frame has to adjust.
If you want to resize the view but keep the origin in the same place, set the frame instead of the bounds.

How do I cut a notch out of a CALayer?

I have a set of views in a carousel, each using a CAGradientLayer as a background. The carousel sits over a textured background. I've been asked for the background to poke up in a triangle to show the selected view. I can't just use a triangular image with the background texture, as it won't necessarily match up with the main background. I'd like to cut a notch out of the background of the current view, so that the textured background is visible through the notch.
How should I go about this? Is it possible to make a polygonal layer?
I found I was able to do it using a CAShapeLayer:
CAShapeLayer *mask = [[[CAShapeLayer alloc] init] autorelease];
mask.frame = backgroundLayer.bounds;
mask.fillColor = [[UIColor blackColor] CGColor];
CGFloat width = backgroundLayer.frame.size.width;
CGFloat height = backgroundLayer.frame.size.height;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, 0, 0);
CGPathAddLineToPoint(path, nil, width, 0);
CGPathAddLineToPoint(path, nil, width, height);
CGPathAddLineToPoint(path, nil, (width/2) + 5, height);
CGPathAddLineToPoint(path, nil, width/2, height - 5);
CGPathAddLineToPoint(path, nil, (width/2) - 5, height);
CGPathAddLineToPoint(path, nil, 0, height);
CGPathAddLineToPoint(path, nil, 0, 0);
CGPathCloseSubpath(path);
mask.path = path;
CGPathRelease(path);
backgroundLayer.mask = mask;
I don't think that we can draw polygonal layers. However, I believe that the expected result can be achieved using two different ways:-
If your images are of the same size then you can use a PNG image with transparent notch in between (or outside as per your desire).
Draw a filled rectangle and transparent triangular polygon. Then you need to intersect both the polygon (rectangle and triangle). The resulted Shape should be placed over the carousel image. The benefit of this method is that you can dynamically change the size and shapes of the polygons if required.
Hope this helps!
Is it possible to make a polygonal layer?
No.
How should I go about this?
Use a mask - a png image that's the same size as your layer but has a bit cut out - this will make the CALayer appear to have a chunk cut out of it. (However, it won't so if you cut-out is big, you might get users trying to touch whatever is behind it, which won't work!).
See the documentation (and search stackoverflow) for more details.

Trying to create a rectangle filled with blue UIImage object

I want to create a blue rectangle image and see it in my view, but this code doesn't seem to work:
CGRect imageRect = CGRectMake(50, 50, 64, 40);
UIGraphicsBeginImageContext(imageRect.size);
[[UIColor blueColor] set];
UIRectFill(imageRect);
UIImage *aImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *myImageView = [[UIImageView alloc] initWithImage:aImage];
[self.view addSubview:myImageView];
Can someone fix it for me?
Thanks,
Sagiftw
Your context is 64 points by 40 points. You filled a rectangle starting 50 points from the origin in a 40-point-tall context. That put it out of bounds, and anything you draw outside the bounds of the context won't show up.
Set your rectangle's origin to 0,0, which is the origin of the context. Then, your 64×40-point rectangle will be completely within the bounds of your 64×40-point context.
If you actually want to draw the rectangle 50 points below and to the right of the context's origin, then you need to make the context's size at least big enough to hold that margin plus the size of the rectangle. If you also want the same amount of margin on the other size, then the context's size should be the rectangle's size plus 100 points wide by 100 points tall (50 points on each side of the rectangle on each axis).