Strange issue I can't seem to resolve where on iOS 7 only, CAEmitterLayer will spawn particles on the screen incorrectly when birth rate is initially set to a nonzero value. It's as if it calculates the state the layer would be in the future.
// Create black image particle
CGRect rect = CGRectMake(0, 0, 20, 20);
UIGraphicsBeginImageContext(rect.size);
CGContextFillRect(UIGraphicsGetCurrentContext(), rect);
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Create cell
CAEmitterCell *cell = [CAEmitterCell emitterCell];
cell.contents = (__bridge id)img.CGImage;
cell.birthRate = 100.0;
cell.lifetime = 10.0;
cell.velocity = 100.0;
// Create emitter with particles emitting from a line on the
// bottom of the screen
CAEmitterLayer *emitter = [CAEmitterLayer layer];
emitter.emitterShape = kCAEmitterLayerLine;
emitter.emitterSize = CGSizeMake(self.view.bounds.size.width,0);
emitter.emitterPosition = CGPointMake(self.view.bounds.size.width/2,
self.view.bounds.size.height);
emitter.emitterCells = #[cell];
[self.view.layer addSublayer:emitter];
I saw on the DevForums one post where a few people mentioned they had similar problems with iOS 7 and CAEmitterLayer, but no one had any ideas how to fix it. Now that iOS 7 is no longer beta, I figured I should ask here and see if anyone can crack it. I really hope this isn't just a bug that we have to wait for 7.0.1 or 7.1 to get fixed. Any ideas would be much appreciated. Thanks!
YES!
I spent hours on this problem myself.
To get the same kind of animation of the birthRate we had before we use a couple of strategies.
Firstly, if you want the layer to look like it begins emitting when added to the view you need to remember that CAEmitterLayer is a subclass of CALayer which conforms to the CAMediaTiming protocol. We have to set the whole emitter layer to begin at the current moment:
emitter.beginTime = CACurrentMediaTime();
[self.view.layer addSublayer:emitter];
It's as if it calculates the state the layer would be in the future.
You were eerily close, but actually its that the emitter was beginning in the past.
Secondly, to animate between a birthrate of 0 and n, with the effect that we had before we can manipulate the lifetime property instead:
if (shouldBeEmitting){
emitter.lifetime = 1.0;
}
else{
emitter.lifetime = 0;
}
Note that i set the lifetime on the emitter layer itself. This is because when emitting the emitter cell's version of this property gets multiplied by the value in the emitter layer. Setting the lifetime of the emitter layer sets a multiple of the lifetimes of all your emitter cells, allowing you to turn them all on and off with ease.
For me, the issue with my CAEmitterLayer, when moving to iOS7 was the following:
In iOS7 setting the CAEmitterLayerCell's duration resulted in the particle not showing at all!
The only thing I had to change was remove the cell.duration = XXX and then my particles began showing up again. I am going to eat an Apple over this unexpected, unexplained hassle.
Related
I am running into an issue when I create an explicit animation to change the value of a CAShapeLayer's path from an ellipse to a rect.
In my canvas controller I setup a basic CAShapeLayer and add it to the root view's layer:
CAShapeLayer *aLayer;
aLayer = [CAShapeLayer layer];
aLayer.frame = CGRectMake(100, 100, 100, 100);
aLayer.path = CGPathCreateWithEllipseInRect(aLayer.frame, nil);
aLayer.lineWidth = 10.0f;
aLayer.strokeColor = [UIColor blackColor].CGColor;
aLayer.fillColor = [UIColor clearColor].CGColor;
[self.view.layer addSublayer:aLayer];
Then, when I animate the path I get a strange glitch / flicker in the last few frames of the animation when the shape becomes a rect, and in the first few frames when it animates away from being a rect. The animation is set up as follows:
CGPathRef newPath = CGPathCreateWithRect(aLayer.frame, nil);
[CATransaction lock];
[CATransaction begin];
[CATransaction setAnimationDuration:5.0f];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"path"];
ba.autoreverses = YES;
ba.fillMode = kCAFillModeForwards;
ba.repeatCount = HUGE_VALF;
ba.fromValue = (id)aLayer.path;
ba.toValue = (__bridge id)newPath;
[aLayer addAnimation:ba forKey:#"animatePath"];
[CATransaction commit];
[CATransaction unlock];
I have tried many different things like locking / unlocking the CATransaction, playing with various fill modes, etc...
Here's an image of the glitch:
http://www.postfl.com/outgoing/renderingglitch.png
A video of what I am experiencing can be found here:
http://vimeo.com/37720876
I received this feedback from the quartz-dev list:
David Duncan wrote:
Animating the path of a shape layer is only guaranteed to work when
you are animating from like to like. A rectangle is a sequence of
lines, while an ellipse is a sequence of arcs (you can see the
sequence generated by using CGPathApply), and as such the animation
between them isn't guaranteed to look very good, or work well at all.
To do this, you basically have to create an analog of a rectangle by
using the same curves that you would use to create an ellipse, but
with parameters that would cause the rendering to look like a
rectangle. This shouldn't be too difficult (and again, you can use
what you get from CGPathApply on the path created with
CGPathAddEllipseInRect as a guide), but will likely require some
tweaking to get right.
Unfortunately this is a limitation of the otherwise awesome animatable path property of CAShapeLayers.
Basically it tries to interpolate between the two paths. It hits trouble when the destination path and start path have a different number of control points - and curves and straight edges will have this problem.
You can try to minimise the effect by drawing your ellipse as 4 curves instead of a single ellipse, but it still isn't quite right. I haven't found a way to go smoothly from curves to polygons.
You may be able to get most of the way there, then transfer to a fade animation for the last part - this won't look as nice, though.
I am creating a very simple particle system with CoreAnimation.
This is my cell:
UIImage *image = [UIImage imageNamed:#"spark"];
CAEmitterCell *cell = [CAEmitterCell emitterCell];
[cell setContents:(id)image.CGImage];
[cell setBirthRate:250.f];
[cell setScale:.25f];
[cell setColor:[UIColor colorWithRed:1.0 green:0.2 blue:0.1 alpha:0.5].CGColor];
[cell setLifetime:5.0f];
and layer:
CAEmitterLayer *emitterLayer = [CAEmitterLayer layer];
[emitterLayer setEmitterCells:#[cell]];
[emitterLayer setFrame:bounds];
[emitterLayer setRenderMode:kCAEmitterLayerAdditive];
[self.view.layer addSublayer:emitterLayer];
Now, I move the emitterLayer's position to wherever I'm touching:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
self.emitterLayer.emitterPosition = [[touches anyObject] locationInView:self.view];
}
However, the problem is, it doesn't emit the particles continuously. But rather, at regular intervals(dotted as oppose to a line):
I thought perhaps it's because I'm not animating it. So I tried to add a simple animation to move the emitter from top left to bottom right of the screen just to see if that works:
CGPoint startPos = CGPointZero;
CGPoint endPos = CGPointMake(bounds.size.width, bounds.size.height);
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"emitterPosition"];
ba.fromValue = [NSValue valueWithCGPoint:startPos];
ba.toValue = [NSValue valueWithCGPoint:endPos];
ba.duration = .5f;
[emitterLayer addAnimation:ba forKey:nil];
What I expected is a line be be formed with particles emitted continuously. However, what I see particles being emitted at intervals, like this:
Is it possible to have the emitterLayer follow your touch up and continuously emit these particles like a drawing and not be dotted?
Thanks
The effect you are looking to achieve (I saw a link in a comment) is probably best achieved without a particle system. What you usually use for this effect depends on individual approach, but I've had success with both.
One, is to create a render buffer: You draw directly onto this buffer with a colour at maximum value. This could be a UIView for instance. At a timed interval, you decrease every pixel value in this view by a percentage (say 0.05%). New pixels added start bright and fade over time this way, so you have no gaps between points. This is CPU intensive!
The other is to create a 'trianglestrip': you will need to go into a lower level graphics API for this such as OpenGL. The idea is that you create a section of triangles (or quads) that form a 'ribbon' under your finger. Each new section that is added begins fading to 0% alpha and is then removed. There is an implementation of this technique in cocos2d which is called CCMotionStreak that is perfect for this. The benefit is that you supply a texture (as you would a particle system) so it keeps designers happy
I'd suggest removing the animation and to create the line effect via the CAEmitterCell's velocity and emissionLongitude properties:
UIImage *image = [UIImage imageNamed:#"spark"];
CAEmitterCell *cell = [CAEmitterCell emitterCell];
[cell setContents:(id)image.CGImage];
[cell setBirthRate:250.f];
[cell setScale:.25f];
[cell setColor:[UIColor colorWithRed:1.0 green:0.2 blue:0.1 alpha:0.5].CGColor];
[cell setLifetime:5.0f];
[cell setEmissionLongitude:M_PI_4]; //The emission angle
[cell setVelocity:500]; //adjust as needed, together with BirthRate
CAEmitterLayer *emitterLayer = [CAEmitterLayer layer];
[emitterLayer setEmitterCells:#[cell]];
[emitterLayer setFrame:bounds];
[emitterLayer setRenderMode:kCAEmitterLayerAdditive];
[self.view.layer addSublayer:emitterLayer];
That looks awfully like the pattern you'd see using an ease-in ease-out animation curve, which I think is the default for CAAnimations. The points are closer together at either end, suggesting it is slower at the beginning and end.
Change the timing function of the animation to linear and you should see the effect you want.
To get a continuous line of emitters the only thing I could get to work was to slow the animation down, which doesn't really give the right effect. It looks like moving the emitter position skips a few renders, I'm not sure why.
You also need to play with the scaling a bit and probably use a CAShapeLayer to draw the actual line you're after.
In fact, a better solution is probably to use a smaller emitter layer which you move along at the end of the line, with a CAShapeLayer which you animate the strokeEnd of to leave the drawing in there.
It seems to me like your birthRate is set way too low. Try changing it from 250.f to something like 1500.f and post the results.
I started to learn SpriteKit and immediatly run into a problem. I'm trying to draw an empty rectangle with a SKShapeNode but does appear on the screen. If i set a color to the fill property the rectangle appears. What am i doing wrong ?
CGRect box = CGRectMake(0, 0, self.frame.size.width/2, self.frame.size.height/2);
SKShapeNode *shapeNode = [[SKShapeNode alloc] init];
shapeNode.path = [UIBezierPath bezierPathWithRect:box].CGPath;
shapeNode.fillColor = nil;
shapeNode.strokeColor = SKColor.redColor;
shapeNode.lineWidth = 3;
[self addChild:shapeNode];
Welcome to sprite kit, I am learning it as well and haven't had much experience with shapeNodes, but here is what I would suggest:
//If you want the shape to be that of a rectangle I would suggest using a simpler allocation method such as the following:
SKShapeNode *shapeNode = [SKShapeNode shapeNodeWithRectOfSize:CGSizeMake(self.frame.size.width/2, self.frame.size.height/2))];
/*shapeNodeWithRectOfSize is a built in allocation method for SKShapeNodes that handles
allocation and initialization for you, and will also create the rectangle shape for you.
The CGSizeMake method will return a CGSizeMake object for you*/
/*a CGSizeMake object is an object with two properties: width, and height. It is used to hold
the dimensions of objects. self.frame.size is a CGSize object*/
/*You do not need to set the fill color to nil. This is because the default is [SKColor clearColor]
which is an empty color already*/
//Make sure that you use an initializer method when setting the colour as below
shapeNode.strokeColor = [SKColor redColor];
shapeNode.lineWidth = 3;
[self addChild:shapeNode];
If you would like a reference to the details of the SKShapeNode object then I would suggest looking here: Apple - SKShapeNode Reference
If you would like a source of excellent quality tutorials I would suggest looking here:enter link description here
I haven't tested the code as I am not able to at the moment, so let me know if it does not work and I will see what I can do to help you. Once again welcome to Sprite-Kit, I hope it is a pleasant experience.
I've been having a problem that I can't seem to figure out. In my app I use a custom UIImageView class to represent movable objects. Some are loaded with static .png images and use frame animation, while others are loaded with CAShapeLayer paths from .svg files. For some of those, I'm getting stutter when the layer is animating from one path to another, while the UIImageView is moving.
You can see it in the linked video. When I touch the mermaid horn, a note spawns (svg path) and animates into a fish (another svg path), all while drifting upwards. The stuttering occurs during that animation / drift. It's most noticeable the third time I spawn the fish, around 19 seconds into the video. (The jumping at the end of each animation I need to fix separately so don't worry about that)
http://www.youtube.com/watch?v=lnrNWuvqQ4w
This does not occur when I test the app in iOS Simulator - everything is smooth. On my iPad 2 it stutters. When I turn off the note/fish movement (drifting upward), it animates smooth on the iPad, so obviously it has to do with moving the view at the same time. There's something I'm missing but I can't figure it out.
Here is how I set up the animation. PocketSVG is a class I found on Github that converts an .svg to a Bezier path.
animShape = [CAShapeLayer layer];
[animShape setShouldRasterize:YES];
[animShape setRasterizationScale:[[UIScreen mainScreen] scale]];
PocketSVG *tSVG;
UIBezierPath *tBezier;
PocketSVG *tSVG2;
UIBezierPath *tBezier2;
CAShapeLayer *tLayer2;
CABasicAnimation *animBezier;
CABasicAnimation *animScale;
tSVG = [[PocketSVG alloc] initFromSVGFileNamed:[NSString stringWithFormat:#"%#", tName]];
// Use PocketSVG to convert the SVG to a Bezier Path
tBezier = tSVG.bezier;
animShape.path = tBezier.CGPath;
animShape.lineWidth = 1;
animShape.strokeColor = [[UIColor blackColor] CGColor];
animShape.fillColor = [[UIColor blackColor] CGColor];
animShape.fillRule = kCAFillRuleNonZero;
// Set the frame & bounds to position the piece and set anchor point for rotation
// parentPaper is the Mermaid the note spawns from
CGPoint newCenter;
CGFloat imageScale = fabsf(parentPaper.transform.a);
newCenter = // Code excised, set center based on custom spawn points in relation to parent position and scale
animShape.bounds = CGPathGetBoundingBox(animShape.path);
CGRect lBounds = CGRectMake(newCenter.x, newCenter.y, animShape.bounds.size.width, animShape.bounds.size.height);
[animShape setFrame:lBounds];
halfSize = CGPointMake(animShape.bounds.size.width/2, animShape.bounds.size.height/2);
animShape.anchorPoint = CGPointMake(prp.childSpawnX, prp.childSpawnY);
// Create the end path for animation
tSVG2 = [[PocketSVG alloc] initFromSVGFileNamed:[NSString stringWithFormat:#"%#2", tName]];
tBezier2 = tSVG2.bezier;
tLayer2 = [CAShapeLayer layer];
tLayer2.path = tBezier2.CGPath;
// Create the animation and add it to the layer
animBezier = [CABasicAnimation animationWithKeyPath:#"path"];
animBezier.delegate = self;
animBezier.duration = prp.frameDur;
animBezier.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animBezier.fillMode = kCAFillModeForwards;
animBezier.removedOnCompletion = NO;
animBezier.autoreverses = behavior.autoReverse;
if (prp.animID < 0) {
animBezier.repeatCount = FLT_MAX;
}
else {
animBezier.repeatCount = prp.animID;
}
animBezier.fromValue = (id)animShape.path;
animBezier.toValue = (id)tLayer2.path;
[animShape addAnimation:animBezier forKey:#"animatePath"];
// also scale the animation if spawned from a parent
// i.e. small note to normal-sized fish
if (parentPaper != nil) {
animScale = [CABasicAnimation animationWithKeyPath:#"transform"];
animScale.delegate = self;
animScale.duration = prp.frameDur;
animScale.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animScale.fillMode = kCAFillModeForwards;
animScale.removedOnCompletion = YES;
animScale.autoreverses = NO;
animScale.repeatCount = 1;
animScale.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(imageScale, imageScale, 0.0)];
animScale.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 0.0)];
[animShape addAnimation:animScale forKey:#"animateScale"];
}
[self.layer addSublayer:animShape];
And this is basically how I'm moving the UIImageView. I use a CADisplayLink at 60 frames, figure out the new spot based on existing position of animShape.position, and update like this, where self is the UIImageView:
[self.animShape setPosition:paperCenter];
Everything else runs smooth, it's just this one set of objects that stutter and jump about when running on the iPad itself. Any ideas of what I'm doing wrong? Moving it the wrong way, perhaps? I do get confused with layers, frames and bounds still.
To fix this I ended up changing the way the UIImageView moves. Instead of changing the frame position through my game loop, I switched it to a CAKeyframeAnimation on the position since it's a temporary movement that only runs once. I used CGPathAddCurveToPoint to replicate the sine wave movement that I originally had. It was rather simple and I probably should have done it that way to begin with.
I can only surmise that the stuttering was due to moving it through the CADisplayLink loop while its animation separately.
I'm trying to get CAEmitterLayers and CAEmitterCells to start their animation from somewhere in the middle of their parent's duration. Is this possible at all? I tried playing with the beginTime and timeOffset properties but I can't seem to get this working.
Added some code for posterity: (lets say I want the emitter to start at the 5th second)
CAEmitterLayer *emitter = [CAEmitterLayer new];
// emitter.beginTime = -5.0f; // I tried this
// emitter.timeOffset = 5.0f; // I also tried this, with beginTime = 0.0, and with beginTime = AVCoreAnimationBeginTimeAtZero
/* set some other CAEmitterLayer properties */
CAEmitterCell *cell = [CAEmitterCell new];
// cell.beginTime = -5.0f; // Then I saw that CAEmitterCell implements CAMediaTiming protocol so I tried this
// cell.timeOffset = 5.0f; // and this
/* set some other CAEmitterCell properties */
emitter.emitterCells = #[cell];
[viewLayer addSubLayer:emitter];
But still the animation starts from where the emitter generates the particles.
Edited again to explain what I'm trying to do:
Lets say I have a CAEmitterLayer that animates rain, so I setup the cells to do a "falling" animation that starts from the top of the screen. During the start of rendering, I don't want to start in a state that's "not raining yet". I want to start where the screen is already covered with rain.
The beginTime isn't relative to now. You need to grab the current time relative to the current layer time space, which you can get by using the CACurrentMediaTime() function. So in your case, you'd do something like this:
emitter.beginTime = CACurrentMediaTime() + 5.f;