I'm trying to add a shader to a stroke I've drawn in SpriteKit using SKShapeNode's stokeShader property but I can't get any effect on the stroke color in any way. I have a custom path I have drawn and created a SKShapeNode along that path. I'm trying to get some custom coloring along the path's stroke but I can't get this shader to work.
SKShapeNode *shape = [SKShapeNode node];
shape.path = path.CGPath;
shape.lineWidth = 20.0;
shape.strokeColor = [UIColor whiteColor];
SKShader *shader = [SKShader shaderWithFileNamed:#"neonLineShader.fsh"];
NSLog(#"Shader = %#",shader);
shape.strokeShader = shader;
I'm using super simple shader code just for testing but can't get any change in the stroke color.
void main(){
gl_FragColor = vec4(0.0,1.0,0.0,1.0);
}
I've tried adding a texture to the stroke first but no luck there.
Related
Trying to create a tiled background image for a top down game. The SKScene is 8000,8000 and instead of creating a couple very large sprites I am trying to tile it to improve performance.
var coverageSize = CGSizeMake(8000,8000);
var textureSize = CGRectMake(0, 0, 100, 100);
var backgroundCGImage = [UIImage imageNamed:#"bg"].CGImage; //this line returns several errors.
UIGraphicsBeginImageContext(CGSizeMake(coverageSize.width, coverageSize.height));
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawTiledImage(context, textureSize, backgroundCGImage);
UIImage *tiledBackground = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
SKTexture *backgroundTexture = [SKTexture textureWithCGImage:tiledBackground.CGImage];
SKSpriteNode *backgroundTiles = [SKSpriteNode spriteNodeWithTexture:backgroundTexture];
backgroundTiles.yScale = -1;
backgroundTiles.position = CGPointMake(0,0);
[self addChild:backgroundTiles];
one way to do tiles is you create a node used to draw your tiles
You draw the amount of tiles that fits the screen into this special tile
use:
var texture = self.view!.textureFromNode(drawingTile, crop: CGRectMake(-column.width, -row.height, self.frame.width + column.width , self.frame.height + row.height));
To convert this node to a texture
now remove all nodes from the drawing node
add in the a new node to the drawing node using this texture.
we now have our background in one node.
when you need to scroll, shift the drawing node, then you draw the 1 row and 1 columns worth of tiles that are missing. Do this only when you need your drawing mode has shifted so much that you have a gap on the screen to fill in. Use the above code again to create a new drawing mode texture. Rinse, repeat
In the below the yellow circle does not show in the IOS emulator. If I add the setFillColor which is currently commented out, then I get a red circle.
Why does the outline of the shape not show?
Is there a way to trigger the outline?
-(void)addTargetNode2 {
float radius=90;
SKShapeNode *targetOuter = [SKShapeNode shapeNodeWithCircleOfRadius:radius];
//[targetOuter setFillColor:[UIColor redColor]];
[targetOuter setStrokeColor:[UIColor yellowColor]];
[targetOuter setLineWidth:1];
//Position the node.
targetOuter.position = CGPointMake(CGRectGetMidX(self.frame),
CGRectGetMidY(self.frame));
[self addChild:targetOuter];
}
I did find this article - but no answer.
Edit:
I've got a working work-around... but not particularly fond of it. Drawing a circle the color of the background over the top of a filled in circle:
-(void)addTarget {
float radius=50;
CGFloat borderWidth=3;
//Draw the Circle.
SKShapeNode *targetOuter = [SKShapeNode shapeNodeWithCircleOfRadius:radius];
[targetOuter setName:#"targetOuter"];
[targetOuter setFillColor:[UIColor yellowColor]];
//Following line should set the outline color but isn't working.
//[targetOuter setStrokeColor:[UIColor yellowColor]];
[targetOuter setLineWidth:1];
targetOuter.position = CGPointMake(CGRectGetMidX(self.frame),
CGRectGetMidY(self.frame));
[self addChild:targetOuter];
/* Work around
Add a circle the color of the background to emulate an outline.
Can be removed in targetOuter outline works.
*/
SKShapeNode *targetInner = [SKShapeNode shapeNodeWithCircleOfRadius:radius-borderWidth];
[targetInner setFillColor:self.backgroundColor];
targetInner.position = targetOuter.position;
[self addChild:targetInner];
}
It seems there is a bug with the iOS simulator and SKShapeNode. I've been able to replicate this problem on simulator but never on device so you shouldn't worry to add a workaround.
iOS Simulator is good to test low framerate conditions and different screen sizes but, aside from that, not very reliable to test SpriteKit games overall.
Is there a way to change a scene's background color gradually, instead of it swithcing colors at once?
In my scene, I have a button that sets the background color to a different color than the color that is called in the initial creation of the scene, simply like so:
-(void) newBackgroundColor
{
self.backgroundColor = [SKColor blackColor];
}
However, (of course) this switches the color from my initial color to black instantly whereas I would like it to change much the same like nodes do using colorizeWithColor: where two colors 'fade' from one to another. My guess is I would need to implement an SKAction before setting the new color, but the backgroundColor property does not seem to support the colorize action.
Is this possible? I have not been able to find out how so far.
Thanks
The action will only work on an actual SKSpriteNode object - for example, add a white, background-sized image to your scene to act as a backdrop that you will colorize, then run the colorizeWithColor: action on it. Remember to set the colorBlendFactor to 1.0.
SKSpriteNode *bg = [SKSpriteNode spriteNodeWithColor:[SKColor blueColor] size:self.size];
bg.position = CGPointMake(bg.size.width/2, bg.size.height/2);
[self addChild:bg];
SKAction *color1 = [SKAction colorizeWithColor:[SKColor orangeColor] colorBlendFactor:1 duration:5];
SKAction *color2 = [SKAction colorizeWithColor:[SKColor blackColor] colorBlendFactor:1 duration:10];
SKAction *color3 = [SKAction colorizeWithColor:[SKColor blueColor] colorBlendFactor:1 duration:15];
[bg runAction:[SKAction repeatActionForever:[SKAction sequence:#[color1,color2,color3]]]];
Updated for Swift4:
let colorBackground = SKAction.colorize(with: SKColor.white.withAlphaComponent(1.0), colorBlendFactor: 1.0, duration: 0.25)
scene.childNode(withName: BackgroundCategoryName)!.run(colorBackground)
where BackgroundCategoryName is the SKSpriteNode for example:
let background = childNode(withName: BackgroundCategoryName) as! SKSpriteNode
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.
OK, so I needed a rounded triangle. So what I did was use a technique similar to what I've used in other vector drawing programs. Draw the triangle and use the stroke to create the rounded corners. Worked like a charm too, until I needed to reduce the alpha of the color used to fill and stroke the UIBezierPath. For some reason I keep getting this inset outline that isn't the same color as the Fill and Stroke. Somehow the alpha value isn't being respected. Maybe I'm overlooking something silly here, but try as I might I can't get the triangle all one color with a alpha value lower than 1. This is what I get:
And heres the simple code:
- (void)drawRect:(CGRect)rect
{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint: CGPointMake(63.5, 10.5)];
[path addLineToPoint: CGPointMake(4.72, 119.5)];
[path addLineToPoint: CGPointMake(122.28, 119.5)];
[path addLineToPoint: CGPointMake(63.5, 10.5)];
[path closePath];
path.miterLimit = 7;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
path.lineWidth = 8;
UIColor *whiteAlph5 = [UIColor colorWithWhite:0.6 alpha:0.5];
[whiteAlph5 setFill];
[whiteAlph5 setStroke];
[path fill];
[path stroke];
}
I can't understand why the line would be anything other than the "whiteAlpha5" if that's the only color I've set for both fill and stroke. I suppose I can just draw the rounded triangle out adding the curves to to corners, but I'm just curious as to why this happens.Thanks in advance...
If you must have the stroke, alter your call to [UIBezierPath stroke] like so:
[path fill];
[path strokeWithBlendMode:kCGBlendModeCopy alpha:1.0];
This should achieve the effect you want (I think - haven't been able to test it)
This is a bit of a guess, but I think you're seeing here is essentially two layers of semitransparent white, one drawn on top of the other. When the triangle is just filled in, it would be what you're expecting. When you stroke, it's drawing the same colour - but it's adding it on top of the existing colour, not replacing it, which is the effect you might expect if you've done this before in paint programs or similar. Thus, where the stroke and fill overlap, you're getting a stronger white than you're after. Just using fill by itself could solve this, but might not get the rounded effect you're after.
If you need a visual demonstration of what I mean, you can do this in Photoshop. Create a new image with a black background and create a new layer above it, set to 50% opacity. Draw a white square on it (which will look grey due to the opacity). Then, without changing layers, draw a line through it. You won't see the line, because it's replacing the existing colour - this is what you expected to happen with your code. Then, add another layer above it, also set to 50% opacity. Draw a line on this layer, through the square. You'll see the line as a brighter grey. This is additive, the white overlapping on both layers - the effect that your code is creating.
The line is because your stroke and your fill are drawing to the same pixels. Since both the stroke and the fill are partially transparent, the colors accumulate.
One way to fix this is to just create a path that outlines your rounded triangle, and fill it without stroking it.
Here's the interface for a category that creates a path outlining a rounded polygon:
#interface UIBezierPath (MyRoundedPolygon)
+ (UIBezierPath *)my_roundedPolygonWithSides:(int)sides center:(CGPoint)center
vertexRadius:(CGFloat)vertexRadius cornerRadius:(CGFloat)cornerRadius
rotationOffset:(CGFloat)rotationOffset;
#end
Here's the implementation:
#implementation UIBezierPath (MyRoundedPolygon)
static CGPoint vertexForPolygon(int sides, CGPoint center, CGFloat circumradius, CGFloat index) {
CGFloat angle = index * 2 * M_PI / sides;
return CGPointMake(center.x + circumradius * cosf(angle),
center.y + circumradius * sinf(angle));
}
+ (UIBezierPath *)my_roundedPolygonWithSides:(int)sides center:(CGPoint)center
vertexRadius:(CGFloat)vertexRadius cornerRadius:(CGFloat)cornerRadius
rotationOffset:(CGFloat)rotationOffset
{
CGFloat circumradius = vertexRadius + cornerRadius;
CGPoint veryLastVertex = vertexForPolygon(sides, center, circumradius, rotationOffset - 1);
CGPoint currentVertex = vertexForPolygon(sides, center, circumradius, rotationOffset);
CGMutablePathRef cgpath = CGPathCreateMutable();
CGPathMoveToPoint(cgpath, NULL, (veryLastVertex.x + currentVertex.x) / 2,
(veryLastVertex.y + currentVertex.y) / 2);
for (CGFloat i = 0; i < sides; ++i) {
CGPoint nextVertex = vertexForPolygon(sides, center, circumradius,
i + 1 + rotationOffset);
CGPathAddArcToPoint(cgpath, NULL, currentVertex.x, currentVertex.y,
nextVertex.x, nextVertex.y, cornerRadius);
currentVertex = nextVertex;
}
CGPathCloseSubpath(cgpath);
UIBezierPath *path = [self bezierPathWithCGPath:cgpath];
CGPathRelease(cgpath);
return path;
}
#end
Here's how you use it:
#implementation MyView
- (void)drawRect:(CGRect)rect
{
CGRect bounds = self.bounds;
CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
UIBezierPath *path = [UIBezierPath my_roundedPolygonWithSides:3 center:center
vertexRadius:70 cornerRadius:8 rotationOffset:0.25];
[[UIColor colorWithWhite:0.6 alpha:0.5] setFill];
[path fill];
}
#end
And here's the result:
Note that setting rotationOffset to 0.25 rotated the triangle one quarter turn. Setting it to zero will give you a right-pointing triangle.