I want to display sparkles for the short period when a user touches the screen (placed at the touch position).
The problem is when I change CAEmitterLayer's emitterPosition it acts as if it is trying to animate this position from old value to new one during about 0.5s period, instead of changing it instantly.
For example the emitter position is set to (0,0) by default, so when I try to touch at the center of the screen for the first time, it emits a line of sparkles from (0,0) to current position, instead of emitting sparkles form the center.
To stop sparkles, I set layer's birthRate to 0 after a short delay, then on touch I change emitterPosition and turn birthRate back to 1.
The only solution I could find is to have 0.5s delay between moving the position and turning birthRate on.
- (void)initSparkles {
layer = [CAEmitterLayer layer];
layer.birthRate = 0;
[sparkleView.layer addSublayer:layer];
}
- (void)showSparkles:(CGPoint)position {
layer.emitterPosition = position;
CAEmitterCell *sparkle = [CAEmitterCell emitterCell];
sparkle.props = ...;
layer.emitterCells = [NSArray arrayWithObjects:sparkle, nil];
layer.birthRate = 1.0;
[self performSelector:#selector(stopSparkles) withObject:nil afterDelay:0.05 ];
}
- (void)stopSparkles {
layer.birthRate = 0.0;
}
Related
I have a problem with disappearing objects after animation ends. It's all fine if animation gets to its end. But I have animation which is connected to UIPanGesturerecognizer. If user pans more than 33% of screen width, animation goes on. If not, animation gets speed = -1 and actually gets to its beginning.
The question is: how to force layer to redraw?
When I try to start new animation, object appears.
I tried call setNeedsDisplay: in - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
, but with no luck.
EDIT
Here is some code. I put just one animation for the sake of cleanness. Everything works.
So, first, here is my gesture handler.
When the gesture is in UIGestureRecognizerStateChanged state, we stop the animation by setting layer's speed and control execution of animation by setting layer's timeoffset.
When the gesture is in UIGestureRecognizerStateEnded state, we have to check whether translation of pan on x is more than 33% of screen width. If it is we call - (void)finishLayer:(CALayer*)layer animation:(CAKeyframeAnimation*)animation withForward:(BOOL)shouldGoForward
with YES which means animation should continue to next screen, otherwise, we pass NO which means animate backwards to inital state.
- (void)panGestureHandler:(UIPanGestureRecognizer*)recognizer
{
if (recognizer.state == UIGestureRecognizerStateEnded)
{
//if our gesture was enough to let animation roll to its end
if (fabsf(translation.x) > CGRectGetWidth(self.view.frame) * 0.33)
{
[self finishLayer:categoryControllerMain.view.layer animation:menuOpenSwipeAnimateMoveLeft withForward:NO];
}
//finger movement was below .33% of screen width so animate to initial state
else
{
[self finishLayer:categoryControllerMain.view.layer animation:menuOpenSwipeAnimateMoveLeft withForward:NO];
}
}
else if (recognizer.state == UIGestureRecognizerStateChanged)
{
//We put layer speed to zero so we can control animation with timeoffset in relation to finger pan movement
categoryControllerMain.view.layer.speed = 0.0;
[categoryControllerMain.view.layer addAnimation:menuOpenSwipeAnimateMoveLeft forKey:#"menuOpenMoveLeft"];
categoryControllerMain.view.layer.timeOffset = fabs(translation.x / CGRectGetWidth(self.view.frame));
}
}
Here is the method which sends animation to its end by setting layer's speed back to value 1.0 or if we want to animate backwards, we set speed to -1.0. We also want to set beginTime so that after we end pan gesture, animation knows exact time from which to continue.
- (void)finishLayer:(CALayer*)layer animation:(CAKeyframeAnimation*)animation withForward:(BOOL)shouldGoForward
{
pan.enabled = NO;
if (shouldGoForward)
{
//animation continues
CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
layer.timeOffset = 0.0;
layer.beginTime = 0.0;
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
layer.beginTime = timeSincePause;
layer.speed = 1.0;
}
else
{
//we want to take it back
layer.timeOffset = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
layer.beginTime = CACurrentMediaTime();
layer.speed = -1;
}
}
So, in case animation needs to run backwards, it really does, but when it's over, everything disappears. In case of forward animation, everything is in its place. After adding animation, I didn't change layer's position, and I don't use fillMode on animation, so after animation ends, everything should be as it was before animation started, everything happens on presentation layer of animation.
Here are some screenshots. There is pan gesture going on on first image. Since finger moves less than 33% of screen width, this where backwards animation should start. And it does. We animate UITableView which is empty now, white UILabel and grayish UIImageView in the background.
After pan gesture ends, animation starts rolling backwards and when it gets to its end, everything what animated just disappears. On the other hand, if we pan over 33% of screen width, animation gets forward and I can see my UITableView. Here is the photo of backwards animation end.
For the sake of simplicity, the scene has a circle sprite and a square sprite. The square sprite is the child of an SKNode that follows around the circle sprite so that rotation of the square always happens around the circle. However, when the node is rotated, the square randomly drifts upwards. This behavior stops once the rotation makes its way all the way around. Here is the code:
_square = [SKSpriteNode spriteNodeWithImageNamed:#"square"];
_circle = [SKSpriteNode spriteNodeWithImageNamed:#"circle"];
_circle.position = CGPointMake(-200, 300);
[self addChild:_circle];
_rotateNode = [SKNode node];
_rotateNode.position = CGPointMake(300, 300);
[self addChild:_rotateNode];
[_rotateNode addChild:_square];
SKAction *moveBall = [SKAction moveTo:CGPointMake(1200, 300) duration:12];
[_circle runAction:moveBall];
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
SKAction *rotate = [SKAction rotateByAngle:-M_PI_2 duration:0.2];
[_rotateNode runAction:rotate];
}
-(void)update:(CFTimeInterval)currentTime {
_rotateNode.position = _circle.position;
double xOffset = (300 -_circle.position.x);
double yOffset = (300 - _circle.position.y);
_square.position = CGPointMake(xOffset, yOffset);
}
Anyone know why this is happening?
If I understand the question, you are asking why the square moves in unexpected ways? The circle is moving, but on every frame you are setting the rotateNode's position to be the same as the circle, and they both have the same parent: self, so we can ignore circle for any debugging because it just executes its action every frame to get a new position.
The oddness, most likely, comes in because you are manually repositioning rotateNode, which would then move all of its children, including square. But in every frame you are also repositioning square manually. So rotateNode rotates, changes the position of square because it rotated, and then you reposition square to have a fixed offset. Further complicated depending on whether your goal is to position the square in world space or in parent space. But it's not clear what your goal is there.
I'm trying to animate a helicopter-thing in my game using a virtual joystick. The below code works well if your flying it actively, but now I want to animate the helicopter to a hover rotating it to the negative angle and apply a negative/positive vector to it to make it rapidly stop and "hover." And possibly bounce back and forth until its hovering. Any suggestions?
//Just move the hell with no physics
//[heli setPosition:CGPointMake(heli.position.x+self.joystick.x*2, heli.position.y+self.joystick.y*2)];
//Apply physics to the heli
float multiplier = 2.5;
[heli.physicsBody applyForce:CGVectorMake(_joystick.x*multiplier, _joystick.y*multiplier)];
//joystick x position is -99 to 99
SKAction *rotation = [SKAction rotateToAngle:-1*(_joystick.x/2.0) duration:0];
[heli runAction: rotation];
//if i let go of the joystick it returns to zero - bounce to a hover
if (_joystick.x == 0) { //not working
//check the current velocity?
//[heli.physicsBody velocity];
[heli.physicsBody applyForce:CGVectorMake(-1*(_joystick.x*multiplier*2), -1*(_joystick.y*multiplier*2))];
}
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;
I'm using a pair of CALayers as image masks, allowing me to animate a bulb filling or emptying at a set speed while also following the current touch position. That is, one mask jumps to follow the touch and the other slides to that position. Since I use an explicit animation I'm forced to set the position of the mask sliding mask when I add the animation. This means that if I start a fill and then start an empty before the fill completes the empty will begin from the completed fill position (the opposite is also true).
Is there a way to get the position of the animation, set the position at each step of the animation, or to have the new animation begin from the current state of the active animation?
The code handling the animating is below:
- (void)masksFillTo:(CGFloat)height {
// Clamp the height we fill to inside the bulb. Remember Y gets bigger going down.
height = MIN(MAX(BULB_TOP, height), BULB_BOTTOM);
// We can find the target Y location by subtracting the Y value for the top of the
// bulb from the height.
CGFloat targetY = height - BULB_TOP;
// Find the bottom of the transparent mask to determine where the solid fill
// is sitting. Then find how far that fill needs to move.
// TODO: This works with the new set position, so overriding old anime doesn't work
CGFloat bottom = transMask.frame.origin.y + transMask.frame.size.height;
// If the target is above the bottom of the solid, we want to fill up.
// This means the empty mask jumps and the transparent mask slides.
CALayer *jumper;
CALayer *slider;
if (bottom - targetY >= 0) {
jumper = emptyMask;
slider = transMask;
// We need to reset the bottom to the emptyMask
bottom = emptyMask.frame.origin.y + emptyMask.frame.size.height;
} else {
jumper = transMask;
slider = emptyMask;
}
[jumper removeAllAnimations];
[slider removeAllAnimations];
CGFloat dy = bottom - targetY;
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
[jumper setPosition:CGPointMake(jumper.position.x, jumper.position.y - dy)];
[self slideMaskFillTo:height withMask:slider]; // Do this inside here or an odd flash glitch appears.
[CATransaction commit];
}
// TODO: Always starts from new position, even if animation hasn't reached it.
- (void)slideMaskFillTo:(CGFloat)height withMask:(CALayer *)slider {
// We can find the target Y location by subtracting the Y value for the top of the
// bulb from the height.
CGFloat targetY = height - BULB_TOP;
// We then find the bottom of the mask.
CGFloat bottom = slider.frame.origin.y + slider.frame.size.height;
CGFloat dy = bottom - targetY;
// Do the animation. Animating with duration doesn't appear to work properly.
// Apparently "When modifying layer properties from threads that don’t have a runloop,
// you must use explicit transactions."
CABasicAnimation *a = [CABasicAnimation animationWithKeyPath:#"position"];
a.duration = (dy > 0 ? dy : -dy) / PXL_PER_SEC; // Should be 2 seconds for a full fill
a.fromValue = [NSValue valueWithCGPoint:slider.position];
CGPoint newPosition = slider.position;
newPosition.y -= dy;
a.toValue = [NSValue valueWithCGPoint:newPosition];
a.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[slider addAnimation:a forKey:#"colorize"];
// Update the actual position
slider.position = newPosition;
}
And an example of how this is called. Notice this means it can be called mid-animation.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.view];
[self masksFillTo:point.y];
}
If anyone finds it relevant, this is the creation of the images and masks.
// Instantiate the different bulb images - empty, transparent yellow, and solid yellow. This
// includes setting the frame sizes. This approach found at http://stackoverflow.com/a/11218097/264775
emptyBulb = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Light.png"]];
transBulb = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Light-moving.png"]];
solidBulb = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Light-on.png"]];
[emptyBulb setFrame:CGRectMake(10, BULB_TOP, 300, BULB_HEIGHT)]; // 298 x 280
[transBulb setFrame:CGRectMake(10, BULB_TOP, 300, BULB_HEIGHT)]; // 298 x 280
[solidBulb setFrame:CGRectMake(10, BULB_TOP, 300, BULB_HEIGHT)]; // 298 x 280
[self.view addSubview:solidBulb]; // Empty on top, then trans, then solid.
[self.view addSubview:transBulb];
[self.view addSubview:emptyBulb];
// Create a mask for the empty layer so it will cover the other layers.
emptyMask = [CALayer layer];
[emptyMask setContentsScale:emptyBulb.layer.contentsScale]; // handle retina scaling
[emptyMask setFrame:emptyBulb.layer.bounds];
[emptyMask setBackgroundColor:[UIColor blackColor].CGColor];
emptyBulb.layer.mask = emptyMask;
// Also create a mask for the transparent image.
transMask = [CALayer layer];
[transMask setContentsScale:transBulb.layer.contentsScale]; // handle retina scaling
[transMask setFrame:transBulb.layer.bounds];
[transMask setBackgroundColor:[UIColor blackColor].CGColor];
transBulb.layer.mask = transMask;
Was just led to a solution via this answer. If one looks in the right part of the docs, you'll find the following:
- (id)presentationLayer
Returns a copy of the layer containing all properties as they were at the start of the current transaction, with any active animations applied.
So if I add this code before I first check the transparent mask location (aka solid level), I grab the current animated position and can switch between fill up and fill down.
CALayer *temp;
if (transMask.animationKeys.count > 0) {
temp = transMask.presentationLayer;
transMask.position = temp.position;
}
if (emptyMask.animationKeys.count > 0) {
temp = emptyMask.presentationLayer;
emptyMask.position = temp.position;
}
layer.presentation() makes copy of original layer each time you get it, and you need to override init(layer: Any) for every custom CALayer you have.
Another possible way to start from current state is to set animation's beginTime using current animation's beginTime. It allows to start new animation with offset.
In your case it could be (Swift):
if let currentAnimation = slider.animation(forKey: "colorize") {
let currentTime = CACurrentMediaTime()
let animationElapsedTime = currentAnimation.beginTime + currentAnimation.duration - currentTime
a.beginTime = currentTime - animationElapsedTime
}
However this works great for linear timings, but for others it could be difficult to calculate the proper time.