I have been playing with CABasicAnimation and CAAnimationGroup today, and I am already in love with it. I have couple of basic animations happening, in which the circle shape shrinks and also downscale to rounded square shape, just like "Voice memo" application in iOS 7.
Below is the code for it.
CABasicAnimation *corner = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
corner.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
corner.fromValue = [NSNumber numberWithFloat:recordingShape.layer.cornerRadius];
corner.toValue = [NSNumber numberWithFloat:30.0f];
corner.duration = 1.0;
//shrinking - scaling
CABasicAnimation* shrink = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
shrink.toValue = [NSNumber numberWithDouble:0.5];
shrink.duration = 0.5;
// Two animations concurrently so set up CAAnimationGroup
CAAnimationGroup *group = [CAAnimationGroup animation];
[group setDuration:0.5];
[group setAnimations:[NSArray arrayWithObjects:shrink, corner, nil]];
// Animate the layer
[[recordingShape layer] addAnimation:group forKey:#"bounceAndFade"];
The animation occurs nicely as expected, but after animation it goes back to its original state as round circle, could anyone guide me as of how can I persist the layer's frame?
Thanks.
Well I Didn't know you could setup a delegate for the CAAnimationGroup group, hence I change the actual cornerRadius and transform at animationDidStop call.
Related
The documentation for SCNMaterialProperty.contents states that it is an animatable property and indeed I can perform a crossfade between two colors. However I’m unable to crossfade between two images.
So I’m starting to wonder if this is possible at all or if I need to create a custom shader for this?
I’ve tried an implicit animation, in which case it immediately shows the ‘after’ image:
node.geometry.firstMaterial.diffuse.contents = [UIImage imageNamed:#"before"];
[SCNTransaction begin];
[SCNTransaction setAnimationDuration:5];
node.geometry.firstMaterial.diffuse.contents = [UIImage imageNamed:#"after"];
[SCNTransaction commit];
An explicit animation, which does nothing:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"contents"];
animation.fromValue = (__bridge id)[UIImage imageNamed:#"before"].CGImage;
animation.toValue = (__bridge id)[UIImage imageNamed:#"after"].CGImage;
animation.duration = 5;
[node.geometry.firstMaterial.diffuse addAnimation:animation forKey:nil];
As well as through a CALayer, which does nothing:
CALayer *textureLayer = [CALayer layer];
textureLayer.frame = CGRectMake(0, 0, 793, 1006);
textureLayer.contents = (__bridge id)[UIImage imageNamed:#"before"].CGImage;
node.geometry.firstMaterial.diffuse.contents = textureLayer;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"contents"];
animation.fromValue = (__bridge id)[UIImage imageNamed:#"before"].CGImage;
animation.toValue = (__bridge id)[UIImage imageNamed:#"after"].CGImage;
animation.duration = 5;
[textureLayer addAnimation:animation forKey:nil];
From my own testing, it doesn't look like this property is actually animatable when texture values are involved (rather than solid color values). Either that's a bug in SceneKit (i.e. it's intended to be animatable but that's not working) or it's a bug in Apple's docs (i.e. it's not intended to be animatable but they say it is). Either way, you should file that bug so you get notified when Apple fixes it.
(It also doesn't look like this is a tvOS-specific issue -- I see it on OS X as well.)
I can understand why animated texture transitions might not be there... from a GL/Metal perspective, that requires binding an extra texture unit and having two texture lookups per pixel (instead of one) during the transition.
I can think of a couple of decent potential workarounds:
Use a shader modifier. Write a GLSL(ish) snippet that looks something like this:
uniform sampler2D otherTexture;
uniform float fadeFactor;
#pragma body
vec4 otherTexel = texture2D(otherTexture, _surface.diffuseTexcoord);
_surface.diffuse = mix(_surface.diffuse, otherTexel, fadeFactor);
Set it on the material you want to animate using the SCNShaderModifierEntryPointSurface entry point. Then use setValue:forKey: to associate a SCNMaterialProperty with the otherTexture and a CABasicAnimation to animate the fadeFactor from 0 to 1.
Use something more animated (like a SpriteKit scene) as your material property contents, and animate that to perform the transition. (As a bonus, when you do it this way, you can use other transition styles.)
Your animation isn't happening because "contents" property is animatable only when set to a color not for image. You can read it on the apple's documentation about contents property.
I have a CALayer with an image in it, and it has several sublayers. I want to animate it to have no contents (no image), but continue showing the sublayers. This code does not work:
CABasicAnimation *backgroundOut = [CABasicAnimation animationWithKeyPath:#"contents"];
backgroundOut.toValue = [NSNull null];
backgroundOut.fillMode = kCAFillModeForwards;
backgroundOut.removedOnCompletion = NO;
backgroundOut.duration = 3.0;
[_backgroundLayer addAnimation:backgroundOut forKey:#"contents"];
Here is the only way I could get this to work:
backgroundOut.toValue = (__bridge id)([UIImage imageNamed:#"blankImage"].CGImage);
Note that I don't want to mess with the opacity or anything because this layer has sublayers that need to still be visible.
What is the proper way to animate to empty contents?
I decided this was the cleanest approach, since there was no other answer forthcoming:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 0);
UIImage *blank = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CABasicAnimation *backgroundOut = [CABasicAnimation animationWithKeyPath:#"contents"];
backgroundOut.toValue = (__bridge id)(blank.CGImage);
backgroundOut.fillMode = kCAFillModeForwards;
backgroundOut.removedOnCompletion = NO;
backgroundOut.duration = 3.0;
[_backgroundLayer addAnimation:backgroundOut forKey:#"contents"];
Another approach might be to create a separate layer to contain your image that is a peer of your other layers, but just behind the other layers (you can use zposition to place it behind the other layers explicitly)--all contained within a root layer. Then, you can animate the image layer's alpha for a fade and won't have the overhead of creating an image on the fly and it won't then fade the other layers.
I am working on a OSX/Cocoa graphics application which (for performance reasons) I would like to render at 640x480 when the user selects "full screen" mode. For what it's worth, the content is a custom NSView which draws using openGL.
I understand that rather than actually change the user's resolution, it's preferable to change the backbuffer (as explained on another SO question here: Programmatically change resolution OS X).
Following that advice, I end up with the following two methods (see below) to toggle between fullscreen and windowed. The trouble is that when I go fullscreen, the content does indeed render at 640x480 but is not scaled (IE it appears as if we stayed at the window's resolution and "zoomed" into a 640x480 corner of the render).
I'm probably missing something obvious here - I suppose I could translate the render according to the actual screen resolution to "center" it, but that seems overcomplicated?
- (void)goFullscreen{
// Bounce if we're already fullscreen
if(_isFullscreen){return;}
// Save original size and position
NSRect frame = [self.window.contentView frame];
original_size = frame.size;
original_position = frame.origin;
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO],NSFullScreenModeAllScreens,
nil];
// In lieu of changing resolution, we set the backbuffer to 640x480
GLint dim[2] = {640, 480};
CGLSetParameter([[self openGLContext] CGLContextObj], kCGLCPSurfaceBackingSize, dim);
CGLEnable ([[self openGLContext] CGLContextObj], kCGLCESurfaceBackingSize);
// Go fullscreen!
[self enterFullScreenMode:[NSScreen mainScreen] withOptions:options];
_isFullscreen=true;
}
- (void)goWindowed{
// Bounce if we're already windowed
if(!_isFullscreen){return;}
// Reset backbuffer
GLint dim[2] = {original_size.width, original_size.height};
CGLSetParameter([[self openGLContext] CGLContextObj], kCGLCPSurfaceBackingSize, dim);
CGLEnable ([[self openGLContext] CGLContextObj], kCGLCESurfaceBackingSize);
// Go windowed!
[self exitFullScreenModeWithOptions:nil];
[self.window makeFirstResponder:self];
_isFullscreen=false;
}
Update
Here's now to do something similar to datenwolf's suggestion below, but not using openGL (useful for non-gl content).
// Render into a specific size
renderDimensions = NSMakeSize(640, 480);
NSImage *drawIntoImage = [[NSImage alloc] initWithSize:renderDimensions];
[drawIntoImage lockFocus];
[self drawViewOfSize:renderDimensions];
[drawIntoImage unlockFocus];
[self syphonSendImage:drawIntoImage];
// Resize to fit preview area and draw
NSSize newSize = NSMakeSize(self.frame.size.width, self.frame.size.height);
[drawIntoImage setSize: newSize];
[[NSColor blackColor] set];
[self lockFocus];
[NSBezierPath fillRect:self.frame];
[drawIntoImage drawAtPoint:NSZeroPoint fromRect:self.frame operation:NSCompositeCopy fraction:1];
[self unlockFocus];
Use a FBO with a texture of the desired target resolution attached and render to that FBO/texture in said resolution. Then switch to the main framebuffer and draw a full screen quad using the texture rendered to just before. Use whatever magnification filter you like best. If you want to bring out the big guns you could implement a Lancosz / sinc interpolator in the fragment shader to upscaling the intermediary texture.
currently I am adding my background via the method below. how can I modify the background so that it auto scrolls along with the tilemap.
- (void)setupParallax {
NSString *backgroundName = [self.tilemap propertyNamed:#"BackgroundImage"];
CCSprite *background = [CCSprite spriteWithFile:[AssetHelper
getDeviceSpecificFileNameFor:[NSString stringWithFormat:#"background-%#.png",backgroundName]]];
background.anchorPoint = ccp(0,0);
background.position = CGPointMake(0, 0);
[self addChild:background z:0];
}
You may use a CCParallaxNode. Your background and map would be children of the parallaxNode, and when you move it, its children will automatically move accordingly. Assuming the background should be behind your map, the code will look something like this.-
CCParallaxNode *voidNode = [CCParallaxNode node];
[voidNode addChild:background z:10 parallaxRatio:ccp(0.8f, 0.8f) positionOffset:ccp(0, 0)];
[voidNode addChild:map z:20 parallaxRatio:ccp(1.0f, 1.0f) positionOffset:ccp(0, 0)];
[self addChild:voidNode];
ParallaxRatio will set the speed of each layer based on ParallaxNode movement:
(1.0, 1.0) means that the map will move at the same speed that the voidNode among x and y.
(0.8, 0.8) means that the background will move 80% slower than the ParallaxNode
I have a CAShapeLayer and it has to do a simple task of moving on the screen, guided by the user's finger.
The problem is that the movement is too slow. The layer does move, but there is a lag and it feels slow.
I have another test app where an UIImage is moved and there is no lag at all and the image moves instantly.
What can I do to overcome this?
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
currentPoint = [[touches anyObject] locationInView:self];
}
- (void) touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
CGPoint activePoint = [[touches anyObject] locationInView:self];
CGPoint newPoint = CGPointMake(activePoint.x - currentPoint.x,activePoint.y - currentPoint.y);
curLayer.position = CGPointMake(shapeLayer.position.x+newPoint.x,shapeLayer.position.y+newPoint.y);
currentPoint = activePoint;
}
Thanks!
Keep in mind that when you set the position on a layer (assuming it's not the root layer of a UIView on which actions are disabled by default), it implicitly animates to the new position, which takes 0.25 seconds. If you want to make it snappier, temporarily disable actions on the layer like this:
- (void) touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
CGPoint activePoint = [[touches anyObject] locationInView:self];
CGPoint newPoint = CGPointMake(activePoint.x -
currentPoint.x,activePoint.y - currentPoint.y);
[CATransaction begin];
[CATransaction setDisableActions:YES];
curLayer.position = CGPointMake(shapeLayer.position.x +
newPoint.x, shapeLayer.position.y + newPoint.y);
[CATransaction commit];
currentPoint = activePoint;
}
This should cause it to jump to the new position rather than animate. If that doesn't help, then let me take a look at your layer init code so I can see what properties and dimensions it has. Properties such as cornerRadius, for example, can affect performance.
Try setting shouldRasterize to YES on your CAShapeLayer, particularly if it is usually drawn at the same scale. If your app runs on high-DPI devices, you may also need to set rasterizationScale to match the layer’s contentsScale.
While rasterizing your shape can make it faster to move the shape around, you’ll probably want to temporarily disable rasterization while you’re animating the layer’s path or size.