I built this for my company: https://github.com/busycm/BZYStrokeTimer and during the course of building, I noticed an interesting "bug" that I can't seem to mitigate when using UIBezierPath. Right when the animation starts, the path jumps a certain number of pixels forward (or backwards depending if it's counterclockwise) instead of starting up with a smooth, incremental animation. And what I found that's really interesting is how much the path jumps forward is actually the value of the line width for the CAShaperLayer.
So for example, if my bezier path starts off at CGRectGetMidX(self.bounds) and the line with is 35, the animation actually starts from CGRectGetMidX(self.bounds)+35 and the larger the line width, the more noticeable the jump is. Is there any way to get rid of that so that path will smoothly animate out from the start point?
Here's a picture of the first frame. This is what it looks like immediately after the animation starts.
Then when I resume the animation and pause again, the distance moved is about 1/100th of the distance you see in the picture.
Here's my bezier path code:
- (UIBezierPath *)generatePathWithXInset:(CGFloat)dx withYInset:(CGFloat)dy clockWise:(BOOL)clockwise{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(CGRectGetMidX(self.bounds)+dx/2, dy/2)];
[path addLineToPoint:CGPointMake(CGRectGetMaxX(self.bounds)-dx/2, dy/2)];
[path addLineToPoint:CGPointMake(CGRectGetMaxX(self.bounds)-dx/2, CGRectGetMaxY(self.bounds)-dy/2)];
[path addLineToPoint:CGPointMake(dx/2, CGRectGetMaxY(self.bounds)-dy/2)];
[path addLineToPoint:CGPointMake(dx/2, dy/2)];
[path closePath];
return clockwise ? path : [path bezierPathByReversingPath];
}
Here's the animation code:
CABasicAnimation *wind = [self generateAnimationWithDuration:self.duration == 0 ? kDefaultDuration : self.duration fromValue:#(self.shapeLayer.strokeStart) toValue:#(self.shapeLayer.strokeEnd) withKeypath:keypath withFillMode:kCAFillModeForwards];
wind.timingFunction = [CAMediaTimingFunction functionWithName:self.timingFunction];
wind.removedOnCompletion = NO;
self.shapeLayer.path = [self generatePathWithXInset:self.lineWidth withYInset:self.lineWidth clockWise:self.clockwise].CGPath;
[self.shapeLayer addAnimation:wind forKey:#"strokeEndAnimation"];
And here's how I construct the CAShapeLayer.
- (CAShapeLayer *)shapeLayer {
return !_shapeLayer ? _shapeLayer = ({
CAShapeLayer *layer = [CAShapeLayer layer];
layer.lineWidth = kDefaultLineWidth;
layer.fillColor = UIColor.clearColor.CGColor;
layer.strokeColor = [UIColor blackColor].CGColor;
layer.lineCap = kCALineCapSquare;
layer.frame = self.bounds;
layer.strokeStart = 0;
layer.strokeEnd = 1;
layer;
}) : _shapeLayer;
}
I think what's happening here is that, in this frame of the animation, you are drawing a line that consists of a single point. Since the line has a thickness associated with it, and the line cap type is kCALineCapSquare, that'll get rendered as a square with height and width equal to the line width.
You can think of it as if you are drawing a line with a square marker, and you are going to drag the midpoint of the marker so that it goes through every point in the curve you specified. For the first point in the line, it's as if the marker touches down at that point, leaving a square behind.
Here's a visual representation the different line cap types that will hopefully make it more intuitive. You should probably change the line cap style to kCALineCapButt.
Sidenote:
After you make that change, in this line of code
[path moveToPoint:CGPointMake(CGRectGetMidX(self.bounds)+dx/2, dy/2)];
you probably don't have to offset the x coordinate by dx/2 anymore.
Related
I am able to generate the UIBezierPath of characters with whatever selected font and size. Now I want to make an etched line in between the bezier path. Can I get the center points of the bezier path along? Or any other way that I can make the center dotted line and follow that path?
Here is the code how I do so
Reference Link. I want something like this. :
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 568, 568) cornerRadius:0];
UIBezierPath *circlePath = [self createArcPath];
[path appendPath:circlePath];
[path setUsesEvenOddFillRule:YES];
shapeView = [CAShapeLayer layer];
shapeView.geometryFlipped = false;
shapeView.path = path.CGPath;
shapeView.fillRule = kCAFillRuleEvenOdd;
shapeView.fillColor = [UIColor grayColor].CGColor;
shapeView.opacity = 1.0;
shapeView.lineDashPattern = #[#2, #3];
[self.view.layer addSublayer:shapeView];
CGFloat dashes[] = {2, 3};
[path setLineDash:dashes count:2 phase:0];
- (UIBezierPath *)createArcPath
{
// Create path from text
// See: http://www.codeproject.com/KB/iPhone/Glyph.aspx
// License: The Code Project Open License (CPOL) 1.02 http://www.codeproject.com/info/cpol10.aspx
letters = CGPathCreateMutable();
CTFontRef font = CTFontCreateWithName(CFSTR("Helvetica-Bold"),80, NULL);
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
(__bridge id)font, kCTFontAttributeName,//د
nil];//ج
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:#"H"
attributes:attrs];
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)attrString);
CFArrayRef runArray = CTLineGetGlyphRuns(line);
// for each RUN
for (CFIndex runIndex = 0; runIndex < CFArrayGetCount(runArray); runIndex++)
{
// Get FONT for this run
CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);
// for each GLYPH in run
for (CFIndex runGlyphIndex = 0; runGlyphIndex < CTRunGetGlyphCount(run); runGlyphIndex++)
{
// get Glyph & Glyph-data
CFRange thisGlyphRange = CFRangeMake(runGlyphIndex, 1);
CGGlyph glyph;
CGPoint position;
CTRunGetGlyphs(run, thisGlyphRange, &glyph);
CTRunGetPositions(run, thisGlyphRange, &position);
// Get PATH of outline
{
CGPathRef letter = CTFontCreatePathForGlyph(runFont, glyph, NULL);
CGAffineTransform t = CGAffineTransformMakeTranslation(position.x+200, position.y+80);
CGPathAddPath(letters, &t, letter);
CGPathRelease(letter);
}
}
}
CFRelease(line);
self.path = [UIBezierPath bezierPath];
[self.path appendPath:[UIBezierPath bezierPathWithCGPath:letters]];
return self.path;
}
Can I get the center points of the bezier path along? Or any other way
that I can make the center dotted line and follow that path?
To know that user's finger is going along the path or not you have to Hit-Detection on a Path.
To determine whether a touch event occurred on the filled portion of a
path, you can use the containsPoint: method of UIBezierPath. This
method tests the specified point against all closed subpaths in the
path object and returns YES if it lies on or inside any of those
subpaths.
If you want to do hit-testing on the stroked portion of the path
(instead of the fill area), you must use Core Graphics. The
CGContextPathContainsPoint function lets you test points on either the
fill or stroke portion of the path currently assigned to the graphics
context.
Below method tests to see whether the specified point intersects the specified path. The inFill parameter lets the caller specify whether the point should be tested against the filled or stroked portion of the path. The path passed in by the caller must contain one or more closed subpaths for the hit detection to succeed.
Testing points against a path object.
- (BOOL)containsPoint:(CGPoint)point onPath:(UIBezierPath *)path inFillArea:(BOOL)inFill
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGPathRef cgPath = path.CGPath;
BOOL isHit = NO;
// Determine the drawing mode to use. Default to
// detecting hits on the stroked portion of the path.
CGPathDrawingMode mode = kCGPathStroke;
if (inFill)
{
// Look for hits in the fill area of the path instead.
if (path.usesEvenOddFillRule)
mode = kCGPathEOFill;
else
mode = kCGPathFill;
}
// Save the graphics state so that the path can be removed later.
CGContextSaveGState(context);
CGContextAddPath(context, cgPath);
// Do the hit detection.
isHit = CGContextPathContainsPoint(context, point, mode);
CGContextRestoreGState(context);
return isHit;
}
Check this link to know more about Hit-Detection on a Path
Getting center of path
You can get the width of path as myPath.bounds.size.width; and to get the center of the path just divide width by 2.
And to draw dashed line check this answer
To make dashed line on any UIBezierPath as:
CGFloat dashes[] = {2, 3};
//passing an array with the values {2,3} sets a dash pattern that alternates between a 2 space-unit-long painted segment and a 3 space-unit-long unpainted segment.
UIBezierPath *path = [UIBezierPath bezierPath];
[path setLineDash:dashes count:2 phase:0];
and to dash on CAShapeLayer use the property lineDashPattern as:
shapeView.lineDashPattern = #[#2, #3];
While bezier paths are a convenient option for simple drawing, they are complex calculations, so it will be prohibitively expensive and complicated to use them for this purpose. That's because you need to be able to calculate arbitrary points along the path and to know the prior point in order to know the current direction.
You need some other description of the characters to allow you to know these details more easily. That means using a simple vector (or stroke) description of the fonts (as mentioned by rob mayoff in the comments). This description breaks the characters down into a number of straight line segments which are easy to work with. They are all governed by a y=mx+c calculation and you always know 2 points on each line segment so it's easy to interpolate between them and to know the direction of movement.
If the descriptions at the link provided by rob (here) aren't 'accurate' enough for the size at which you want to display the characters you can create new versions with more points in order to achieve a closer approximation to bezier curve options.
Now, with this description you have a lot of options...
For dragging a finger along the path you can interpolate between points to find the closest point on the path to the current touch point and determine when the touch point has strayed too far from the path, or has intersected it. This intersection processing is also how you can determine the coverage percentage. You just need to choose the interpolation gap distance (chosen for a suitable resolution without creating too many points that are really close together) and a tolerance for how far touch points can be from the 'next' path point.
This also allows for other things in the video you link, like dropping images at each point along the (interpolated) path and animating those images around.
You can calculate points along a UIBezierPath with the code in this open source repo:
https://github.com/ImJCabus/UIBezierPath-Length
If you have your 'a' character represented as a bezier path, you can calculate the red points in the image above by writing something like:
UIBezierPath *path = /* ... */;
NSUInteger subdivisions = 100;//however precise you want to be
for(NSUInteger i = 0; i < subdivisions; i++) {
CGFloat percent = (CGFloat)i/subdivisions;
CGPoint point = [path pointAtPercentOfLength:percent];
//draw a dot at this point, or trace it, or whatever you want to do
}
Try this code to draw red dot line:
CAShapeLayer* dotLayer = [CAShapeLayer layer];
dotLayer.path = path.CGPath;
dotLayer.strokeColor = [UIColor redColor].CGColor;
dotLayer.lineWidth = 2;//or the diameter of the dots
dotLayer.lineDashPattern = #[#0, #10];//change 10 to the distance between dots
dotLayer.lineJoin = kCALineJoinRound;
dotLayer.lineCap = kCALineCapRound;
What you want is rather complicated, but I believe it can be achieved with the following steps:
1) get the letter outline path using your method
2) create a new path using CGPathCreateCopyByStrokingPath with lineWidth equal 1
3) get all segments of the new path
4) find all intersection points of all segments
5) determine which intersecting lines are adjacent and use their intersection points to form a center line
6) repeat steps 2-6 increasing the lineWidth value in step 2
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 2.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
Instead of the path, how can I set animation to last line ?
You cannot just animate "the last line in a path". But you can achieve the visual effect by using two entirely separate shape layers, one with the path thus far that is not to be animated (the blue path in my example below) and a completely separate layer whose path is just the last the segment of the line, which will be animated (a red path in my example). You can then:
Make the path of the blue shape layer to be the just the existing, unanimated portion of the final path.
Make the path for the red shape layer to be just the last segment of the new path;
Animate the red layer's strokeEnd from #(0.0) to #(1.0);
When that's done (e.g. in animationDidStop:finished:), remove that red shape layer's path altogether and extend the path of the blue shape layer's path; and
Repeat as needed.
When you do this, it will look like you animated another segment onto the blue shape layer, but what you really did is that you create a separate layer with just the portion to be animated, animate that, and when done, remove that layer and update the original layer's path accordingly
Note, in the above animated example, the red path and the blue paths are on different shape layers, but it almost feels like it's one path because one layer's path starts right where the other one left off. Clearly, in your final product, you would make the two shape layers appear the same, which would be less jarring than the red/blue combination above. I just used different colors here so you could understand what the two different shape layers were doing.
FYI, this is the code that generated the above drawing:
- (void)addLineToPoint:(CGPoint)point {
// create a path that consists just of the portion to be animated
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:self.lastPoint];
[path addLineToPoint:point];
// set the redAnimatedPathLayer's path to be this short segment
self.redAnimatedPathLayer.path = [path CGPath];
// and animate it
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
self.redAnimatedPathLayer.path = [path CGPath];
pathAnimation.duration = 0.5;
pathAnimation.fromValue = #(0.0);
pathAnimation.toValue = #(1.0);
pathAnimation.delegate = self;
[self.redAnimatedPathLayer addAnimation:pathAnimation forKey:#"strokeEndKey"];
// save the point for future reference
self.lastPoint = point;
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
// update the blue layer's path to include this new point
[self.mainPath addLineToPoint:self.lastPoint];
self.blueNonAnimatedLayer.path = self.mainPath.CGPath;
// reset the red layer's path
self.redAnimatedPathLayer.path = nil;
[self repeat];
}
I'm trying to animate a bezier curve I made with Paintcode (great app, btw) and am drawing in a custom UIView in the "drawRect" method.
The drawing works fine but I want to animate a single point in the bezier curve.
Here's my non-working method:
-(void)animateFlame{
NSLog(#"Animating");
// Create the starting path. Your curved line.
//UIBezierPath * startPath;
// Create the end path. Your straight line.
//UIBezierPath * endPath = [self generateFlame];
//[endPath moveToPoint: CGPointMake(167.47, 214)];
int displacementX = (((int)arc4random()%50))-25;
int displacementY = (((int)arc4random()%30))-15;
NSLog(#"%i %i",displacementX,displacementY);
UIBezierPath* theBezierPath = [UIBezierPath bezierPath];
[theBezierPath moveToPoint: CGPointMake(167.47, 214)];
[theBezierPath addCurveToPoint: CGPointMake(181+displacementX, 100+displacementY) controlPoint1: CGPointMake(89.74, 214) controlPoint2: CGPointMake(192.78+displacementX, 76.52+displacementY)];
[theBezierPath addCurveToPoint: CGPointMake(167.47, 214) controlPoint1: CGPointMake(169.22+displacementX, 123.48+displacementY) controlPoint2: CGPointMake(245.2, 214)];
[theBezierPath closePath];
// Create the shape layer to display and animate the line.
CAShapeLayer * myLineShapeLayer = [[CAShapeLayer alloc] init];
CABasicAnimation * pathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
pathAnimation.fromValue = (__bridge id)[bezierPath CGPath];
pathAnimation.toValue = (__bridge id)[theBezierPath CGPath];
pathAnimation.duration = 0.39f;
[myLineShapeLayer addAnimation:pathAnimation forKey:#"pathAnimation"];
bezierPath = theBezierPath;
}
Using this, nothing moves on the screen at all. The random displacements generated are good and the bezierPath variable is a UIBezierPath that's declared with a class scope.
Am I missing something? (The goal is to do a sort of candle-like animation)
Quick Edit
Just seen your layer code. You are mixing up several different concepts. Like drawRect, CAShapeLayer, old animation code etc...
By doing the method below you should be able to get this working.
You can't do this :( you can't animate the contents of drawRect (i.e. you can't get it to draw multiple times over the course of the animation).
You may be able to use a timer or something and create your own animation type code. (i.e. create a timer that fires 30 times a second and runs a function that calculates where you are in the animation and updates the values you want to change and calls [self setNeedsDisplay]; to trigger the draw rect method.
Other than that there isn't much you can do.
ANOTHER EDIT
Just adding another option here. Doing this with a CAShapeLayer might be very poor performance. You might be best using a UIImageView with a series of UIImages.
There are built in properties, animationImages, animationDuration, etc... on UIImageView.
POSSIBLE SOLUTION
The path property of CAShapeLayer is animatable and so you could possible use this.
Something like...
// set up properties for path
#property (nonatomic, assign) CGPath startPath;
#property (nonatomic, assign) CGPath endPath;
#property (nonatomic, strong) CAShapeLayer *pathLayer;
// create the startPath
UIBezierPath *path = [UIBezierPath //create your path using the paint code code
self.startPath = path.CGPath;
// create the end path
path = [UIBezierPath //create your path using the paint code code
self.endPath = path.CGPath;
// create the shapee layer
self.pathLayer = [CAShapeLayer layer];
self.pathLayer.path = self.startPath;
//also set line width, colour, shadows, etc...
[self.view.layer addSubLayer:self.pathLayer];
Then you should be able to animate the path like this...
- (void)animatePath
{
[UIView animateWithDuration:2.0
animations^() {
self.pathlayer.path = self.endPath;
}];
}
There are lots of notes in the docs about CAShapeLayer and animating the path property.
This should work though.
Also, get rid of the old animation code. It has been gone since iOS4.3 you should be using the updated block animations.
I've got a project where I'm animating a UIBezierPath based on a set progress. The BezierPath is in the shape of a circle and lies in a UIView and animation is done in drawRect using CADisplayLink right now. Simply put, based on a set progress x the path should radially extend (if xis larger than before) or shrink (if x is smaller).
self.drawProgress = (self.displayLink.timestamp - self.startTime)/DURATION;
CGFloat startAngle = -(float)M_PI_2;
CGFloat stopAngle = ((self.x * 2*(float)M_PI) + startAngle);
CGFloat currentEndAngle = ((self.oldX * 2*(float)M_PI) + startAngle);
CGFloat endAngle = currentEndAngle-((currentEndAngle-stopAngle)*drawProgress);
UIBezierPath *guideCirclePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
This is in the case of x shrinking since our last update. The issues I'm experiencing are actually a few:
The shape always starts drawing at 45º (unless I rotate the view). I have not found any way to change this, and setting the startAngleto -45º makes no difference really because it always "pops" to 45. Is there anything I can do about this, or do I have to resort to other methods of drawing?
Is there any other way that one should animate these things? I've read much about using CAShapeLayer but I haven't quite understood the actual difference (in terms of drawbacks and benefits) in using these two methods. If anyone could clarify I would be very much obliged!
UPDATE: I migrated the code over to CAShapeLayer instead, but now I'm facing a different issue. It's best described with this image:
What's happening is that when the layer is supposed to shrink, the thin outer line is still there (regardless of direction of movement). And when the bar shrinks, the delta of 1-xisn't removed unless I explicitly make a new white shape over it. The code for this follows. Any ideas?
UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:stopAngle clockwise:YES];
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [circlePath CGPath];
circle.strokeStart = 0;
circle.strokeEnd = 1.0*self.progress;
// Colour and other customizations here.
if (self.progress > self.oldProgress) {
drawAnimation.fromValue = #(1.0*self.oldProgress);
drawAnimation.toValue = #(circle.strokeEnd);
} else {
drawAnimation.fromValue = #(1.0*self.oldProgress);
drawAnimation.toValue = #(1.0*self.progress);
circle.strokeEnd = 1.0*self.progress;
}
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; //kCAMediaTimingFunctionEaseIn
[circle addAnimation:drawAnimation forKey:#"strokeEnd"];
UPDATE 2: I've ironed out most of the other bugs. Turned out it was just me being rather silly the whole time and overcomplicating the whole animation (not to mention multiplying by 1 everywhere, what?). I've made a gif of the bug I can't solve:
Any ideas?
UPDATE 3: (and closure). I managed to get rid of the bug by calling
[self.layer.sublayers makeObjectsPerformSelector:#selector(removeFromSuperlayer)];
And now everything works as it should. Thanks for all the help!
Using CAShapeLayer is much easier and cleaner. The reason is that CAShapeLayer includes properties strokeStart and strokeEnd. These values range from 0 (the beginning of the path) to 1 (the end of the path) and are animatable.
By changing them you can easily draw any arc of your circle (or any part of an arbitrary path, for that matter.) The properties are animatable, so you can create an animation of a growing/shrinking pie slice or section of a ring shape. It's much easier and more performant than implementing code in drawRect.
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.