2

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
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • When you say lag, are you referring to a constant lag (which is a natural result of your algorithm that is only draw every fourth point)? Or are you referring to a lag that will be get increasingly long as the user holds their finger down (because you only rebuild the image snapshot when the gesture finishes). Both are problems here, but I'm not sure to which you are referring. – Rob Jun 05 '16 at 05:36
  • Maybe switching to `CGPath` and `CGImage` will be more efficient but hard to say unless you measure. – Coldsteel48 Jun 05 '16 at 05:36
  • @Rob This is a constant lag. –  Jun 05 '16 at 06:08
  • Then I'd personally (a) not wait until `ctr` becomes `4`, but rather update the path on every touch; and (b) use predictive touches. I'd also consider using `CAShapeLayer` as that is allegedly faster than simple `drawRect` implementation. – Rob Jun 05 '16 at 06:15

2 Answers2

1

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, https://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 https://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.

Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
1

One easy win for performance is to use setNeedsDisplayInRect: instead of setNeedsDisplay. You're currently redrawing your entire view instead of only the area that has changed.

adam.wulf
  • 2,149
  • 20
  • 27
  • I've tried that I've had trouble figuring out the points to put into my parameters when it comes to Bezier curves. setNeedsDisplayInRect works with just normal points as far as I know. –  Jun 05 '16 at 23:42
  • The curve will be entirely drawn inside the box defined by the 2 endpoints and 2 control points. so minX = MIN(a.x, b.x, c1.x, c2.x), minY = MIN(a.y, b.y, c1.y, c2.y), maxX = and so on. Since the bezier will stroke on the middle of the curve, you can add a buffer of the width of your line. – adam.wulf Jun 06 '16 at 05:06