snapchatdotcom snapchatdotcom - 5 months ago 44
iOS Question

Bezier Curve drawing is very laggy

So here's what's going on:
When I draw, the Bezier lines are very smooth. I applied the concept of making control points and end points in order to make the smoothness possible. However, I cannot find the problem of what's causing the lag.

When I draw, I check my CPU usage and it goes from around 50% to 90% in 5 seconds. I made sure that when I'm done drawing, my points are erased while a buffer is made to create an image of what I drew.

My guess is too many points are drawn at the same time in the touchesMoved? There must be something where points are being filled which could be too much for the program to handle.

#import "SmoothedBIView.h"

@implementation SmoothedBIView
{
UIBezierPath *path;
UIImage *incrementalImage;
CGPoint pts[5]; // need to keep track of the four points of a Bezier segment and the first control point of the next segment
uint ctr;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder])
{
[self setMultipleTouchEnabled:NO];
[self setBackgroundColor:[UIColor whiteColor]];
path = [UIBezierPath bezierPath];
[path setLineWidth:2.0];
}
return self;

}
/* - (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setMultipleTouchEnabled:NO];
path = [UIBezierPath bezierPath];
[path setLineWidth:2.0];
}
return self;
}
*/


animation.
- (void)drawRect:(CGRect)rect
{
[incrementalImage drawInRect:rect];
[path stroke];
}


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
ctr = 0;
UITouch *touch = [touches anyObject];
pts[0] = [touch locationInView:self];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint p = [touch locationInView:self];
ctr++;
pts[ctr] = p;
if (ctr == 4)
{
pts[3] = CGPointMake((pts[2].x + pts[4].x)/2.0, (pts[2].y + pts[4].y)/2.0); // move the endpoint to the middle of the line joining the second control point of the first Bezier segment and the first control point of the second Bezier segment

[path moveToPoint:pts[0]];
[path addCurveToPoint:pts[3] controlPoint1:pts[1] controlPoint2:pts[2]]; // add a cubic Bezier from pt[0] to pt[3], with control points pt[1] and pt[2]

[self setNeedsDisplay];
// replace points and get ready to handle the next segment
pts[0] = pts[3];
pts[1] = pts[4];
ctr = 1;
}

}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self drawBitmap];
/*[self setNeedsDisplay]; */
[path removeAllPoints];
ctr = 0;
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[self touchesEnded:touches withEvent:event];
}

- (void)drawBitmap
{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, 0.0);

if (!incrementalImage)
{
UIBezierPath *rectpath = [UIBezierPath bezierPathWithRect:self.bounds];
[[UIColor whiteColor] setFill];
[rectpath fill];
}
[incrementalImage drawAtPoint:CGPointZero];
[[UIColor blackColor] setStroke];
[path stroke];
incrementalImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}

@end

Rob Rob
Answer

There are a few issues here:

  1. If you're concerned about the constant lag from which this algorithm suffers, one issue is that you are updating the path only when your counter is 4 (and you're always behind at least one point), which will exaggerate lagginess. You could update the path more frequently as discussed here, http://stackoverflow.com/a/34997902/1271826, which is a Swift implementation very similar to yours.

    The basic idea is that rather than waiting until your counter hits 4, go ahead and draw a line when the counter is 1, draw a quad curve when the counter is 2, draw a cubic curve when the counter is 3, and draw the revised cubic curve when the counter is 4. This reduces the general lagginess that your algorithm will suffer from.

  2. You can reduced perceived lagginess by using predictive touches. It doesn't solve the problem, (and complicates the algorithm a little, because you have to contemplate the concept of backing out previously predicted touches when the real touches finally gets in), but it keeps the perceived lag down even more. See http://stackoverflow.com/a/34583708/1271826, for an example of how to use predictive touches (in conjunction with Catmull-Rom and Hermite splines, another smoothing algorithm). I apologize for the Swift reference, but if you search predictive touches for Objective-C, I suspect you'll find lots of examples, too.

  3. If you're worried about the lag as the bezier gets too long, rather than waiting until the touches end before generating the snapshot, do it after some fixed number of touches, even if in the middle of a gesture. This will prevent your bezier from getting so long that you start to suffer performance problems. Yes, you'd rather not do something computationally intensive in the middle of a drawing gesture, but you have to draw a line (no pun intended) at some point.

  4. There's an argument for using a CAShapeLayer for the bezier path rather than drawing it yourself. I've seen it suggested that this is more optimized than a simple drawRect implementation, but I confess that I've never benchmarked it.

Comments