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.
Related
I am working on a Layer-hosting View where I'm setting up a layer hierarchy as:
auto view = [NSView new];
view.layer = [CALayer new];
view.layer.masksToBounds = NO;
view.wantsLayer = YES;
and then using this later as follows:
auto rootLayer = view.layer;
auto layer = [CALayer new];
layer.frame = CGRectMake(0, 0, 100, 100);
layer.backgroundColor = rootLayer.backgroundColor;
[rootLayer addSublayer:layer]; //and similarly adding more sublayers
This works fine and I'm able to see all the layers on the UI. The problem arises when I try to apply a mask to this rootLayer
CAShapeLayer* maskLayer = [CAShapeLayer new];
maskLayer.path = getPath();//sets a CGMutablePathRef
rootLayer.mask = maskLayer;
After this none of the sublayers of rootLayer show up. I've even tried setting the frame of the maskLayer using
rootLayer.mask.frame = CGRectMake(0, 0, rootLayer.frame.size.width, rootLayer.frame.size.height);
but to no avail. I've tried setting the maskToBounds to NO for the rootLayer but it was already NO by default. I can't seem to figure out what's the issue here. Does setting a mask lead all the sublayers to be clipped? I didn't find this documented anywhere. From the documentation of mask:
An optional layer whose alpha channel is used to mask the layer’s
content.
So I interpreted that as to mean that it doesn't impact the sublayers.
I was trying to co-relate this to what's shown here in the Core Animation guide where despite setting the cornerRadius (a mask) to the parent layer, the child layers weren't clipped. But I'm not sure if the same applies when an explicit mask is set.
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.
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 have some troubles with CATextLayer, that could be due to me, but I didn't find any help on this topic. I am on OS X (on iOS it should be the same).
I create a CATextLayer layers with scale factor > 1 and what I get is a blurred text. The layer is rasterized before applying the scale, I think. Is this the expected behavior? I hope it is not, because it just makes no sense... A CAShapeLayer is rasterized after that its transformation matrix is applied, why the CATextLayer should be different?
In case I am doing something wrong... what is it??
CATextLayer *layer = [CATextLayer layer];
layer.string = #"I like what I am doing";
layer.font = (__bridge CFTypeRef)[NSFont systemFontOfSize:24];
layer.fontSize = 24;
layer.anchorPoint = CGPointZero;
layer.frame = CGRectMake(0, 0, 400, 100);
layer.foregroundColor = [NSColor blackColor].CGColor;
layer.transform = CATransform3DMakeScale(2., 2., 1.);
layer.shouldRasterize = NO;
[self.layer addSublayer:layer];
The solution I use at the moment is to set the contentsScale property of the layer to the scale factor. The problem is that this solution doesn't scale: if the scale factor of any of the parent layers changes, then contentsScale should be updated too. I should write code to traverse the layers tree to update the contentsScale properties of all CATextLayers... not exactly what I would like to do.
Another solution, that is not really a solution, is to convert the text to a shape and use a CAShapeLayer. But then I don't see the point of having CATextLayers.
A custom subclass of CALayer could help in solving this problem?
EDIT: Even CAGradientLayer is able to render its contents, like CAShapeLayer, after that its transformation matrix is applied. Can someone explain how it is possible?
EDIT 2: My guess is that paths and gradients are rendered as OpenGL display lists, so they are rasterized at the actual size on the screen by OpenGL itself. Texts are rasterized by Core Animation, so they are bitmaps for OpenGL.
I think that I will go with the contentsScale solution for the moment. Maybe, in the future, I will convert texts to shapes. In order to get best results with little work, this is the code I use now:
[CATransaction setDisableActions:YES];
CGFloat contentsScale = ceilf(scaleOfParentLayer);
// _scalableTextLayer is a CATextLayer
_scalableTextLayer.contentsScale = contentsScale;
[_scalableTextLayer displayIfNeeded];
[CATransaction setDisableActions:NO];
After trying all the approaches, the solution I am using now is a custom subclass of CALayer. I don't use CATextLayer at all.
I override the contentsScale property with this custom setter method:
- (void)setContentsScale:(CGFloat)cs
{
CGFloat scale = MAX(ceilf(cs), 1.); // never less than 1, always integer
if (scale != self.contentsScale) {
[super setContentsScale:scale];
[self setNeedsDisplay];
}
}
The value of the property is always rounded to the upper integer value. When the rounded value changes, then the layer must be redrawn.
The display method of my CALayer subclass creates a bitmap image of the size of the text multiplied by the contentsScale factor and by the screen scale factor.
- (void)display
{
CGFloat scale = self.contentsScale * [MyUtils screenScale];
CGFloat width = self.bounds.size.width * scale;
CGFloat height = self.bounds.size.height * scale;
CGContextRef bitmapContext = [MyUtils createBitmapContextWithSize:CGSizeMake(width, height)];
CGContextScaleCTM(bitmapContext, scale, scale);
CGContextSetShouldSmoothFonts(bitmapContext, 0);
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)(_text));
CGContextSetTextPosition(bitmapContext, 0., self.bounds.size.height-_ascender);
CTLineDraw(line, bitmapContext);
CFRelease(line);
CGImageRef image = CGBitmapContextCreateImage(bitmapContext);
self.contents = (__bridge id)(image);
CGImageRelease(image);
CGContextRelease(bitmapContext);
}
When I change the scale factor of the root layer of my hierarchy, I loop on all text layers and set the contentsScale property to the same factor. The display method is called only if the rounded value of the scale factor changes (i.e. if the previous value was 1.6 and now I set 1.7, nothing happens. But if the new value is 2.1, then the layer is redisplayed).
The cost in terms of speed of the redraw is little. My test is to change continuously the scale factor of a hierarchy of 40 text layers on an 3rd gen. iPad. It works like butter.
CATextLayer is different because the underlying CoreText renders the glyphs with the specified font size (educated guess based on experiments).
You could add an action to the parent layer so as soon as it's scale changes, it changes the font size of the text layer.
Blurriness could also come from misaligned pixels. That can happen if you put the text layer to non integral position or any transformation in the superlayer hierarchy.
Alternatively you could subclass CALayer and then draw the text using Cocoa in drawInContext:
see example here:
http://lists.apple.com/archives/Cocoa-dev/2009/Jan/msg02300.html
http://people.omnigroup.com/bungi/TextDrawing-20090129.zip
If you want to have the exact behaviour of a CAShapeLayer then you will need to convert your string into a bezier path and have CAShapeLayer render it. It's a bit of work but then you will have the exact behaviour you are looking for. An alternate approach, is to scale the fontSize instead. This yields crisp text every time but it might not fit to you exact situation.
To draw text as CAShapeLayer have a look at Apple Sample Code "CoreAnimationText":
http://developer.apple.com/library/mac/#samplecode/CoreAnimationText/Listings/Readme_txt.html
I am wondering if there is a way to do this using Core Animation. Specifically, I am adding a sub-layer to a layer-backed custom NSView and setting its delegate to another custom NSView. That class's drawInRect method draws a single CGPath:
- (void)drawInRect:(CGRect)rect inContext:(CGContextRef)context
{
CGContextSaveGState(context);
CGContextSetLineWidth(context, 12);
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 0, 0);
CGPathAddLineToPoint(path, NULL, rect.size.width, rect.size.height);
CGContextBeginPath(context);
CGContextAddPath(context, path);
CGContextStrokePath(context);
CGContextRestoreGState(context);
}
My desired effect would be to animate the drawing of this line. That is, I'd like for the line to actually "stretch" in an animated way. It seems like there would be a simple way to do this using Core Animation, but I haven't been able to come across any.
Do you have any suggestions as to how I could accomplish this goal?
I found this animated paths example and wanted to share it for anyone else looking for how to do this with some code examples.
You will be using CAShapeLayer's strokeStart and strokeEnd which requires sdk 4.2, so if you are looking to support older iOS SDKs unfortunately this isn't what you want.
The really nice thing about these properties is that they are animatable. By animating strokeEnd from 0.0 to 1.0 over a duration of a few seconds, we can easily display the path as it is being drawn:
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 10.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
Finally, add a second layer containing the image of a pen and use a
CAKeyframeAnimation to animate it along the path with the same speed
to make the illusion perfect:
CAKeyframeAnimation *penAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
penAnimation.duration = 10.0;
penAnimation.path = self.pathLayer.path;
penAnimation.calculationMode = kCAAnimationPaced;
[self.penLayer addAnimation:penAnimation forKey:#"penAnimation"];
Which the source can be viewed here and a demo video here. Read the creators blog for more information.
Sure—don't draw the line yourself. Add a 12-pixel-high sublayer with a flat background color, starting with a zero-width frame and animating out to your view's width. If you need the ends to be rounded, set the layer's cornerRadius to half its height.