I have a node which is scaled and moved every frame.
The node has a custom draw function, so that only the visible part of that node is drawn each frame.
To determine which part is visible, I need to call:
CGPoint start = [MyNode convertToNodeSpace:_adjustedStart];
CGPoint finish = [MyNode convertToNodeSpace:_adjustedFinish];
where:
_adjustedStart = CGPointZero;
_adjustedFinish = CGPointMake(_winSize.width, 0);
start.x and finish.x are used by my draw method, to determine the width to draw.
Prior to using these methods, I had 60fps, even though sometimes I would draw much more than necessary. After using these methods, I draw exactly the region necessary, but the framerate sometimes drops to 50 (for an instant), making the graphics choppy.
How can I perform the same calculation as convertToNodeSpace / convertToWorldSpace but faster?
Related
I'm guessing that it's to make a string out of individual CATextLayers and then position them as required along the curve, then animate. Because that's what's I've got working now, but it loses Kerning. Here's how:
Why isn't my curved text centering itself?
But is Core Text more performant and able to avoid the whole "drawing into a context" nonsense that slows everything down compared to the lean, mean Core Animation way of doing things, and respect kerning? i.e. avoiding drawRect: and all other aspects that greatly slow things down, as in this manner of drawing to the screen:
https://github.com/darcyliu/CocoaSampleCode/tree/master/CoreTextArcCocoa
Imagine a string of 200 characters, bent around a circle, with the ability to animate the spacing between characters, hopefully at a stable 60fps. This is possible with Core Animation, but that's by breaking the string up into individual characters and placing them around the circle with equal spacing, which causes a complete loss of kerning information.
I'm hoping for a way to do this without losing the kerning information and still being able to dynamically adjust spacing at 60fps.
Sure, you can do that. With iOS 7, you don't need to go all the way down to Core Text, though. NSLayoutManager can handle it in many cases. See the CurvyText demo that I wrote for iOS:PTL. You can drag all the control points around and see the text layout along the curve.
To see just how fast this layout can get in pure Core Text and Core Animation, see the PinchText demo from Rich Text, Core Text. This one shows how to adjust Core Text layout to respond to multi-touch, so the text seems to bend towards your fingers. It includes examples of how to animate with Core Animation to get smooth adjustments (and even a small "splash" effect when you remove your finger).
I don't quite know what you mean by "the whole drawing into a context nonsense that slows everything down." I draw these into a context very, very quickly (and Core Animation also does a lot of drawing into contexts).
Bending text around a circle is easier than either of these demos. The trick is to calculate the points along your circle, and use those points to translate and rotate your context before asking the layout manager to draw the glyph. Here's an example drawText from CurvyTextView (which draws along a Bézier curve).
- (void)drawText {
if ([self.attributedString length] == 0) { return; }
NSLayoutManager *layoutManager = self.layoutManager;
CGContextRef context = UIGraphicsGetCurrentContext();
NSRange glyphRange;
CGRect lineRect = [layoutManager lineFragmentRectForGlyphAtIndex:0
effectiveRange:&glyphRange];
double offset = 0;
CGPoint lastGlyphPoint = self.P0;
CGFloat lastX = 0;
for (NSUInteger glyphIndex = glyphRange.location;
glyphIndex < NSMaxRange(glyphRange);
++glyphIndex) {
CGContextSaveGState(context);
CGPoint location = [layoutManager locationForGlyphAtIndex:glyphIndex];
CGFloat distance = location.x - lastX; // Assume single line
offset = [self offsetAtDistance:distance
fromPoint:lastGlyphPoint
andOffset:offset];
CGPoint glyphPoint = [self pointForOffset:offset];
double angle = [self angleForOffset:offset];
lastGlyphPoint = glyphPoint;
lastX = location.x;
CGContextTranslateCTM(context, glyphPoint.x, glyphPoint.y);
CGContextRotateCTM(context, angle);
[layoutManager drawGlyphsForGlyphRange:NSMakeRange(glyphIndex, 1)
atPoint:CGPointMake(-(lineRect.origin.x + location.x),
-(lineRect.origin.y + location.y))];
CGContextRestoreGState(context);
}
}
The "magic" of this is in calculating the transforms you need, which is done in offsetAtDistance:fromPoint:andOffset:, pointForOffset: and angleForOffset:. Those routines are much simpler to write for a circle than a generic Bézier curve, so this is probably a very good starting point. Note that this code is not particularly optimized. It was designed for readability more than speed, but it is still very fast on an iPad 3. If you need it to be faster, there are several techniques, including a lot of pre-calculating that can be done.
The PinchText demo is in pure Core Text and Core Animation, and is quite a bit more complicated since it does all of its math in Accelerate (and really needs to). I doubt you need that since your layout problem isn't that complicated. Some straightforward C can probably calculate everything you need in plenty of time. But the PinchText demo does show how to let Core Animation manage transitions more beautifully. Look at addTouches:inView:scale::
- (void)addTouches:(NSSet *)touches inView:(UIView *)view scale:(CGFloat)scale
{
for (UITouch *touch in touches) {
TouchPoint *touchPoint = [TouchPoint touchPointForTouch:touch inView:view scale:scale];
NSString *keyPath = [self touchPointScaleKeyPathForTouchPoint:touchPoint];
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:keyPath];
anim.duration = kStartTouchAnimationDuration;
anim.fromValue = #0;
anim.toValue = #(touchPoint.scale);
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self addAnimation:anim forKey:keyPath];
[self.touchPointForIdentifier setObject:touchPoint forKey:touchPoint.identifier];
}
}
What's going on here is that it's animating the model data ("scale" here is "how much does this touch impact the layout;" it has nothing to do with transforms). needsDisplayForKey: indicates that when that model data structure is modified, the layer needs to redraw itself. And it completely recomputes and redraws itself into its context every frame. Done correctly, this can be incredibly fast.
This code should hopefully get you started. Not to overly push the book, but the CurvyText demo is discussed extensively in iOS Pushing the Limits chapter 21.
I'm using CoreGraphics in my UIView to draw a graph and I want to be able to interact with the graph using touch input. Since touches are received in device coordinates, I need to transform it into user coordinates in order to relate it to the graph, but that has become an obstacle since CGContextConvertPointToUserSpace doesn't work outside of the graphics drawing context.
Here's what I've tried.
In drawRect:
CGContextScaleCTM(ctx,...);
CGContextTranslateCTM(ctx,...); // transform graph to fit the view nicely
self.ctm = CGContextGetCTM(ctx); // save for later
// draw points using user coordinates
In my touch event handler:
CGPoint touchDevice = [gesture locationInView:self]; // touch point in device coords
CGPoint touchUser = CGPointApplyAffineTransform(touchDevice, self.ctm); // doesn't give me what I want
// CGContextConvertPointToUserSpace(touchDevice) <- what I want, but doesn't work here
Using the inverse of ctm doesn't work either. I'll admit I'm having trouble getting my head around the meaning and relationships between device coordinates, user coordinates, and the transformation matrix. I think it's not as simple as I want it to be.
EDIT: Some background from Apple's documentation (iOS Coordinate Systems and Drawing Model).
"A window is positioned and sized in screen coordinates, which are defined by the coordinate system for the display."
"Drawing commands make reference to a fixed-scale drawing space, known as the user coordinate space. The operating system maps coordinate units in this drawing space onto the actual pixels of the corresponding target device."
"You can change a view’s default coordinate system by modifying the current transformation matrix (CTM). The CTM maps points in a view’s coordinate system to points on the device’s screen."
I discovered that the CTM already included a transformation to map view coordinates (with origin at the top left) to screen coordinates (with origin at the bottom left). So (0,0) got transformed to (0,800), where the height of my view was 800, and (0,2) mapped to (0,798) etc. So I gather there are 3 coordinate systems we're talking about: screen coordinates, view/device coordinates, user coordinates. (Please correct me if I am wrong.)
The CGContext transform (CTM) maps from user coordinates all the way to screen coordinates. My solution was to maintain my own transform separately which maps from user coordinates to view coordinates. Then I could use it to go back to user coordinates from view coordinates.
My Solution:
In drawRect:
CGAffineTransform scale = CGAffineTransformMakeScale(...);
CGAffineTransform translate = CGAffineTransformMakeTranslation(...);
self.myTransform = CGAffineTransformConcat(translate, scale);
// draw points using user coordinates
In my touch event handler:
CGPoint touch = [gesture locationInView:self]; // touch point in view coords
CGPoint touchUser = CGPointApplyAffineTransform(touchPoint, CGAffineTransformInvert(self.myTransform)); // this does the trick
Alternate Solution:
Another approach is to manually setup an identical context, but I think this is more of a hack.
In my touch event handler:
#import <QuartzCore/QuartzCore.h>
CGPoint touch = [gesture locationInView:self]; // view coords
CGSize layerSize = [self.layer frame].size;
UIGraphicsBeginImageContext(layerSize);
CGContextRef context = UIGraphicsGetCurrentContext();
// as in drawRect:
CGContextScaleCTM(...);
CGContextTranslateCTM(...);
CGPoint touchUser = CGContextConvertPointToUserSpace(context, touch); // now it gives me what I want
UIGraphicsEndImageContext();
I'm developing an iPhone app where the main views are presented the user on the surface of a cube. Users switch views by rotating the cube with a pan gesture.
To achieve this I am using the GKLCubeController class from this GitHub project.
In terms of adding views to a cube and rotating, it works fine. However the angular rotation of the cube doesn't map correctly to the current x position of the finger as it pans across the screen.
The problem is that the cube rotation lags behind the finger movement by about ½ second making the cube feel ‘heavy’ as illustrated in this short screencast.
The code handling the rotation is shown below:
-(void)panHandler:(UIPanGestureRecognizer*)panner{
CGPoint translatedPoint = [panner translationInView:self.view.window];
CGFloat halfWidth = self.view.bounds.size.width / 2.0;
// save our starting points
if([panner state] == UIGestureRecognizerStateBegan) {
startingX = translatedPoint.x;
if (!transformLayer) {
transformLayer = [[CATransformLayer alloc] init];
transformLayer.frame = self.view.layer.bounds;
for (UIView *viewToTranslate in views) {
[viewToTranslate removeFromSuperview];
[transformLayer addSublayer:viewToTranslate.layer];
}
// add in this new layer
[self.view.layer addSublayer:transformLayer];
}
} else if([panner state] == UIGestureRecognizerStateEnded) {
...
} else {
// instantly adjust our transformation layer
CATransform3D transform = CATransform3DIdentity;
transform.m34 = kPerspective;
double percentageOfWidth = (translatedPoint.x - startingX) / self.view.frame.size.width;
transform = CATransform3DTranslate(transform, 0, 0, -halfWidth);
double adjustmentAngle = percentageOfWidth * M_PI_2 + startingAngle;
transform = CATransform3DRotate(transform, adjustmentAngle, 0, 1, 0);
transform = CATransform3DTranslate(transform, 0, 0, halfWidth);
transformLayer.transform = transform;
finishingAngle = adjustmentAngle;
}
}
I've a suspicion the problem is something to do with the conversion of the CGPoint.x returned by UIPanGestureRecognizer translationInView: to a rotation angle. Can anyone confirm whether this is the case, and suggest what the correct maths should be for mapping the touch position x to the rotation of a cube such that the cube edge tracks the finger motion as it pans across the screen?
There are two issues here:
The major performance issue here is the way this class is performing the transform of the sides of the cube. It's giving each side of the cube a complicated transform, and then as you're dragging the cube around, it's taken the relevant sides of the cube, added them to a CATransformLayer, and performing a complicated transform upon that layer (thus, when you look at the individual sides of the cube, you're doing a transform of a transform).
I pulled out that CATransformLayer logic, and updated the transform for the individual sides, and it was dramatically more responsive.
By the way, you may might want to still employ something like this CATransformLayer logic when you animate the letting go of the rotated cube, as that's an excellent way of synchronizing the animation of the individual sides of the cube (otherwise you get some separation in the sides of the cube during the animation). But while dragging, there's too much of a performance hit.
As you continue to refine this, there are possibly other optimizations that can be done, but my testing suggests that getting rid of a transformation on a complicated transformation made a huge impact on performance.
And, by the way, make sure to test this on a device, not the simulator, as the simulator's graphics performance is very different than that of the device.
A minor factor that might contribute a slight initial delay in responsiveness may be the inherent delay in UIPanGestureRecognizer (which looks for a certain amount of movement before recognizing the gesture as a pan, so that other gestures such as taps and the like can trigger if appropriate). It's a modest delay and a very small part of your performance problem, but for the quickest of response times, you might not want to use the UIPanGestureRecognizer. Either subclass your own, or use a UILongPressGestureRecognizer with a minimumPressDuration of 0.0, and you can get instantaneous response to the gesture.
You'll see this respond more quickly to movement (but it's also a gesture that doesn't play well with others, that if you have tap gestures or the like inside the view, they won't be triggered).
I'm trying to add a "whiteboard", so that people can draw lines on it.
The only problem is, if I draw very fast, it spaces the sprites pretty far away, so it's barely even legible if they are trying to draw letters or numbers. There's a ton of space between the different sprites.
Here's my method where most of the drawing is happening I think.
-(void) update:(ccTime)delta
{
CCDirector* director = [CCDirector sharedDirector];
CCRenderTexture* rtx = (CCRenderTexture*)[self getChildByTag:1];
// explicitly don't clear the rendertexture
[rtx begin];
for (UITouch* touch in touches)
{
CGPoint touchLocation = [director convertToGL:[touch locationInView:director.openGLView]];
touchLocation = [rtx.sprite convertToNodeSpace:touchLocation];
// because the rendertexture sprite is flipped along its Y axis the Y coordinate must be flipped:
touchLocation.y = rtx.sprite.contentSize.height - touchLocation.y;
CCSprite* sprite = [[CCSprite alloc] initWithFile:#"Cube_Ones.png"];
sprite.position = touchLocation;
sprite.scale = 0.1f;
[self addChild:sprite];
[placedSprites addObject:sprite];
}
[rtx end];
}
Maybe this is the cause?
[self scheduleUpdate];
I'm not entirely sure how to decrease the time between updates though.
Thanks in advance
The problem is simply that the user can move the touch location (ie his/her finger) a great distance between two touch events. You may receive one event at 100x100 and the next the finger is already at 300x300. There's nothing you can do about that.
You can however assume that the change between two touch locations is a linear move. That means you can simply split any two touches that are farther apart than, say, 10 pixels distance and split them in 10 pixel distance intervals. So you'd actually generate the in-between touch locations yourself.
If you do that, it's a good idea to limit the minimum distance between two touches, otherwise the user could draw lots and lots of sprites in a very small area, which is not what you want. So you would only draw a new sprite if the new touch location is, say, 5 pixels away from the previous one.
I am experimenting with Key Frame animation of the position of a UIImageView object moving along a bezier path. This pic shows the initial state before animation. The blue line is the path - initially moving straight up, the light green box is the initial bounding box or the image, and the dark green "ghost" is the image that I am moving:
When I kick off the animation with rotationMode set to nil, the image keeps the same orientation all the way through the path as expected.
But when I kick off the animation with rotationMode set to kCAAnimationRotateAuto, the image immediately rotates 90 degrees anti-clockwise and keeps this orientation all the way through the path. When it reaches the end of the path it redraws in the correct orientation (well it actually shows the UIImageView that I repositioned in the final location)
I was naively expecting that the rotationMode would orientate the image to the tangent of the path and not to the normal, especially when the Apple docs for the CAKeyframeAnimation rotationMode state
Determines whether objects animating along the path rotate to match the path tangent.
So what is the solution here? Do I have to pre-rotate the image by 90 degrees clockwise? Or is there something that I am missing?
Thanks for your help.
Edit 2nd March
I added a rotation step before the path animation using an Affine rotation like:
theImage.transform = CGAffineTransformRotate(theImage.transform,90.0*M_PI/180);
and then after the path animation, resetting the rotation with:
theImage.transform = CGAffineTransformIdentity;
This makes the image follow the path in the expected manner. However I am now running into a different problem of the image flickering. I am already looking for a solution to the flickering issue in this SO question:
iOS CAKeyFrameAnimation scaling flickers at animation end
So now I don't know if I have made things better or worse!
Edit March 12
While Caleb pointed out that yes, I did have to pre rotate my image, Rob provided an awesome
package of code that almost completely solved my problems. The only thing that Rob didn't do was compensating for my assets being drawn with a vertical rather than horizontal orientation, thus still requiring to preRotate them by 90 degrees before doing the animation. But hey, its only fair that I have to do some of the work to get things running.
So my slight changes to Rob's solution to suite my requirements are:
When I add the UIView, I pre Rotate it to counter the inherent rotation added by setting the rotationMode:
theImage.transform = CGAffineTransformMakeRotation(90*M_PI/180.0);
I need to keep that rotation at the end of the animation, so instead of just blasting the view's transform with a new scale factor after the completion block is defined, I build the scale based on the current transform:
theImage.transform = CGAffineTransformScale(theImage.transform, scaleFactor, scaleFactor);
And that's all I had to do to get my image to follow the path as I expected!
Edit March 22
I have just uploaded to GitHub a demo project that shows off the moving of an object along a bezier path. The code can be found at PathMove
I also wrote about it in my blog at Moving objects along a bezier path in iOS
The issue here is that Core Animation's autorotation keeps the horizontal axis of the view parallel to the path's tangent. That's just how it works.
If you want your view's vertical axis to follow the path's tangent instead, rotating the contents of the view as you're currently doing is the reasonable thing to do.
Here's what you need to know to eliminate the flicker:
As Caleb sort of pointed out, Core Animation rotates your layer so that its positive X axis lies along the tangent of your path. You need to make your image's “natural” orientation work with that. So, supposing that's a green spaceship in your example images, you need the spaceship to point to the right when it doesn't have rotation applied to it.
Setting a transform that includes rotation interferes with the rotation applied by `kCAAnimationRotateAuto'. You need to remove the rotation from your transform before applying the animation.
Of course that means you need to reapply the transformation when the animation completes. And of course you want to do that without seeing any flicker in the appearance of the image. That's not hard, but there some secret sauce involved, which I explain below.
You presumably want your spaceship to start out pointing along the tangent of the path, even when the spaceship is sitting still having not been animated yet. If your spaceship image is pointing to the right, but your path goes up, then you need to set the transform of the image to include a 90° rotation. But perhaps you don't want to hardcode that rotation - instead you want to look at the path and figure out its starting tangent.
I'll show some of the important code here. You can find my test project on github. You may find some use in downloading it and trying it out. Just tap on the green “spaceship” to see the animation.
So, in my test project, I have connected my UIImageView to an action named animate:. When you touch it, the image moves along half of a figure 8 and doubles in size. When you touch it again, the image moves along the other half of the figure 8 (back to the starting position), and returns to its original size. Both animations use kCAAnimationRotateAuto, so the image points along the tangent of the path.
Here's the start of animate:, where I figure out what path, scale, and destination point the image should end up at:
- (IBAction)animate:(id)sender {
UIImageView* theImage = self.imageView;
UIBezierPath *path = _isReset ? _path0 : _path1;
CGFloat newScale = 3 - _currentScale;
CGPoint destination = [path currentPoint];
So, the first thing I need to do is remove any rotation from the image's transform, since as I mentioned, it will interfere with kCAAnimationRotateAuto:
// Strip off the image's rotation, because it interferes with `kCAAnimationRotateAuto`.
theImage.transform = CGAffineTransformMakeScale(_currentScale, _currentScale);
Next, I go into a UIView animation block so that the system will apply animations to the image view:
[UIView animateWithDuration:3 animations:^{
I create the keyframe animation for the position and set a couple of its properties:
// Prepare my own keypath animation for the layer position.
// The layer position is the same as the view center.
CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
positionAnimation.path = path.CGPath;
positionAnimation.rotationMode = kCAAnimationRotateAuto;
Next is the secret sauce for preventing flicker at the end of the animation. Recall that animations do not effect the properties of the “model layer“ that you attach them to (theImage.layer in this case). Instead, they update the properties of the “presentation layer“, which reflects what's actually on the screen.
So first I set removedOnCompletion to NO for the keyframe animation. This means the animation will stay attached to the model layer when the animation is complete, which means I can access the presentation layer. I get the transform from the presentation layer, remove the animation, and apply the transform to the model layer. Since this is all happening on the main thread, these property changes all happen in one screen refresh cycle, so there's no flicker.
positionAnimation.removedOnCompletion = NO;
[CATransaction setCompletionBlock:^{
CGAffineTransform finalTransform = [theImage.layer.presentationLayer affineTransform];
[theImage.layer removeAnimationForKey:positionAnimation.keyPath];
theImage.transform = finalTransform;
}];
Now that I've set up the completion block, I can actually change the view properties. The system will automatically attach animations to the layer when I do this.
// UIView will add animations for both of these changes.
theImage.transform = CGAffineTransformMakeScale(newScale, newScale);
theImage.center = destination;
I copy some key properties from the automatically-added position animation to my keyframe animation:
// Copy properties from UIView's animation.
CAAnimation *autoAnimation = [theImage.layer animationForKey:positionAnimation.keyPath];
positionAnimation.duration = autoAnimation.duration;
positionAnimation.fillMode = autoAnimation.fillMode;
and finally I replace the automatically-added position animation with the keyframe animation:
// Replace UIView's animation with my animation.
[theImage.layer addAnimation:positionAnimation forKey:positionAnimation.keyPath];
}];
Double-finally I update my instance variables to reflect the change to the image view:
_currentScale = newScale;
_isReset = !_isReset;
}
That's it for animating the image view with no flicker.
And now, as Steve Jobs would say, One Last Thing. When I load the view, I need to set the transform of the image view so that it's rotated to point along the tangent of the first path that I will use to animate it. I do that in a method named reset:
- (void)reset {
self.imageView.center = _path1.currentPoint;
self.imageView.transform = CGAffineTransformMakeRotation(startRadiansForPath(_path0));
_currentScale = 1;
_isReset = YES;
}
Of course, the tricky bit is hidden in that startRadiansForPath function. It's really not that hard. I use the CGPathApply function to process the elements of the path, picking out the first two points that actually form a subpath, and I compute the angle of the line formed by those two points. (A curved path section is either a quadratic or cubic bezier spline, and those splines have the property that the tangent at the first point of the spline is the line from the first point to the next control point.)
I'm just going to dump the code here without explanation, for posterity:
typedef struct {
CGPoint p0;
CGPoint p1;
CGPoint firstPointOfCurrentSubpath;
CGPoint currentPoint;
BOOL p0p1AreSet : 1;
} PathState;
static inline void updateStateWithMoveElement(PathState *state, CGPathElement const *element) {
state->currentPoint = element->points[0];
state->firstPointOfCurrentSubpath = state->currentPoint;
}
static inline void updateStateWithPoints(PathState *state, CGPoint p1, CGPoint currentPoint) {
if (!state->p0p1AreSet) {
state->p0 = state->currentPoint;
state->p1 = p1;
state->p0p1AreSet = YES;
}
state->currentPoint = currentPoint;
}
static inline void updateStateWithPointsElement(PathState *state, CGPathElement const *element, int newCurrentPointIndex) {
updateStateWithPoints(state, element->points[0], element->points[newCurrentPointIndex]);
}
static void updateStateWithCloseElement(PathState *state, CGPathElement const *element) {
updateStateWithPoints(state, state->firstPointOfCurrentSubpath, state->firstPointOfCurrentSubpath);
}
static void updateState(void *info, CGPathElement const *element) {
PathState *state = info;
switch (element->type) {
case kCGPathElementMoveToPoint: return updateStateWithMoveElement(state, element);
case kCGPathElementAddLineToPoint: return updateStateWithPointsElement(state, element, 0);
case kCGPathElementAddQuadCurveToPoint: return updateStateWithPointsElement(state, element, 1);
case kCGPathElementAddCurveToPoint: return updateStateWithPointsElement(state, element, 2);
case kCGPathElementCloseSubpath: return updateStateWithCloseElement(state, element);
}
}
CGFloat startRadiansForPath(UIBezierPath *path) {
PathState state;
memset(&state, 0, sizeof state);
CGPathApply(path.CGPath, &state, updateState);
return atan2f(state.p1.y - state.p0.y, state.p1.x - state.p0.x);
}
Yow mention that you kick off the animation with "rotationMode set to YES", but the documentation states that rotationMode should be set using an NSString...
In particular:
These constants are used by the rotationMode property.
NSString * const kCAAnimationRotateAuto
NSString * const kCAAnimationRotateAutoReverse
Have you tried setting:
keyframe.animationMode = kCAAnimationRotateAuto;
The documentation states:
kCAAnimationRotateAuto: The objects travel on a tangent to the path.