1

My goal is to achieve Adobe Illustrator like lines when my user swipes across the screen. At the moment it's very choppy - here's a screenshot of the current situation:

Noisy lines

I think it's obvious what I want. I want those straight lines to be smoothed out. Is this called anti-aliasing?

- (void)touchesBegan: (CGPoint)point
{
    currentStroke = [[Stroke alloc] initWithStartPoint:point];
    [strokes addObject:currentStroke];
}

- (void)touchesMoved: (CGPoint)point
{
    if (!currentStroke) {
        [self touchesBegan:point];
    }
    [currentStroke.points addObject:[NSValue valueWithCGPoint:point]]
    pathTwo = [self createPath];
    [self scaleToFit];
    [self setNeedsDisplay];
}

- (UIBezierPath *)createPath {
    UIBezierPath * bezierPath = [UIBezierPath bezierPath];
    for (Stroke * stroke in strokes) {
        [bezierPath moveToPoint:stroke.startPoint];
        for (NSValue * value in stroke.points) {
            [bezierPath addLineToPoint:[value CGPointValue]];
        }
    }
    return bezierPath;
}

- (void)touchesEnded: (CGPoint)point
{
    [points removeAllObjects];
    [self setNeedsDisplay];
}

drawRect has this in it:

[[UIColor whiteColor] setStroke];
[pathTwo setLineWidth: _strokeWidth];
[pathTwo stroke];

EDIT

I think I have found my answer... https://github.com/jnfisher/ios-curve-interpolation

I just don't know how to use this given my createPath formula.

Maybe someone can help me plug in my arrays?

Rob
  • 415,655
  • 72
  • 787
  • 1,044
noodlez
  • 89
  • 8
  • 6
    No this is not an antialiasing problem. You need to get access to the individual points that make up the entire line and apply a smoothing operation to them which will be something like 1) taking starting point and ending point and just replacing it with the straight line in between or 2) Averaging the slope of multiple segments, etc. Lots of ways to smoothe out curves. –  Nov 26 '17 at 17:31
  • Can you show me some code for option #2? – noodlez Nov 26 '17 at 17:33
  • How is your data stored? Do you have access to an array of points (x,y) values? I can help but it matters what you are working with to start out. In other words does each point also have the 'time' or some indication that puts it in the proper order relative to the line? AND how do you plan to redraw it? You need to be sure that will wokr first. –  Nov 26 '17 at 17:43
  • I have the data stored in NSMutableArray, first the "stroke" (touchBegan) and all associated taps between that and touchEnd.... – noodlez Nov 26 '17 at 22:44
  • I see the word Bezier in your post and that is actually a great algorithm for smoothing things out. You can maybe do some thing like just take every 5th or 10th point and make them your control points for bezier, and ignore the rest of the points. The thing you want to google is this: "curve fitting line points to smooth line", or "bezier curve fitting points". The Computer Science/Math term of art is "Curve Fitting" –  Nov 27 '17 at 00:32
  • see my edit, it's about interpolation – noodlez Nov 27 '17 at 00:36

2 Answers2

5

The problem is that you're stroking a path between a series of points captured in a gesture, and these points aren't all on the same line because they include natural deviations that take place as the users finger drags across the screen.

This is completely unrelated to "anti-aliasing", which is a technique to make a straight line look smoother, without visually distracting "jaggies". But the problem here is not that you're trying to improve the rendering of a single straight line, but rather that you're not drawing a straight line in the first place, but rather instead drawing a bunch of line segments that don't happen to line up.

My goal is to achieve Adobe Illustrator like lines when my user swipes across the screen

In that case, you don't want "anti-aliasing". All you need is a createPath that

  • moveToPoint to the first point; and
  • addLineToPoint to the last point.

Just don't worry about any of the points in between. This simple solution will yield a single straight line that will start wherever your touches started and stretch to where your touches ended, and will continue to do so until you lift your finger.

I think that will translate to something like:

- (UIBezierPath *)createPath {
    UIBezierPath * bezierPath = [UIBezierPath bezierPath];
    [bezierPath moveToPoint:stroke.startPoint];
    [bezierPath addLineToPoint:[[stroke.points lastObject] CGPointValue]];
    return bezierPath;
}

If you don't want an actual line, but rather just want to smooth this, I'd suggest (a) do rolling average to eliminate peaks and valleys in your bouncy data points; and (b) use Hermite or Catmull-Rom spline (as outlined in https://spin.atomicobject.com/2014/05/28/ios-interpolating-points/) so the resulting path is smooth rather than a series of line segments.

Thus

- (UIBezierPath * _Nullable)createActualPathFor:(Stroke *)stroke {
    if (stroke.points.count < 2) return nil;

    UIBezierPath * path = [UIBezierPath bezierPath];

    [path moveToPoint:[stroke.points.firstObject CGPointValue]];
    for (NSInteger i = 1; i < stroke.points.count; i++) {
        [path addLineToPoint:[stroke.points[i] CGPointValue]];
    }

    return path;
}

- (UIBezierPath * _Nullable)createSmoothedPathFor:(Stroke *)stroke interval:(NSInteger)interval {
    if (stroke.points.count < 2) return nil;

    NSMutableArray <NSValue *> *averagedPoints = [NSMutableArray arrayWithObject:stroke.points[0]];

    NSInteger current = 1;
    NSInteger count = 0;
    do {
        CGFloat sumX = 0;
        CGFloat sumY = 0;
        do {
            CGPoint point = [stroke.points[current] CGPointValue];
            sumX += point.x;
            sumY += point.y;
            current++;
            count++;
        } while (count < interval && current < stroke.points.count);

        if (count >= interval) {
            CGPoint average = CGPointMake(sumX / count, sumY / count);
            [averagedPoints addObject:[NSValue valueWithCGPoint:average]];
            count = 0;
        }
    } while (current < stroke.points.count);

    if (count != 0) {
        [averagedPoints addObject:stroke.points.lastObject];
    }

    return [UIBezierPath interpolateCGPointsWithHermite:averagedPoints closed:false];
}

Will yield the following (where the actual data points are in blue and the smoothed rendition is in red):

enter image description here

Note, I'm assuming that your array of points of Stroke includes the starting point, so you might have to adjust that accordingly.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Question: I didn't mean just one straight line, I meant I'd like my scribbles to be "smooth" – noodlez Nov 26 '17 at 22:42
  • 1
    If you’re just looking to smooth your scribbles you can use Hermite or Catmull-Rom splines like https://stackoverflow.com/a/34583708/1271826 or https://spin.atomicobject.com/2014/05/28/ios-interpolating-points/. But those will still try to go through all of your points, just smoothing it. If you don’t want to do that, you might have to look at some “best fit” or some higher order regression. – Rob Nov 26 '17 at 23:06
  • https://github.com/jnfisher/ios-curve-interpolation/blob/master/Curve%20Interpolation/UIBezierPath%2BInterpolation.h This looks good. I just can't manage to figure out how to plug in my points!? Can you show me an example given the createPath function I posted,,,, stroke = first Stroke (touchBegan) and the points within the stroke are those that correspond to its persence until touchend – noodlez Nov 27 '17 at 00:35
  • @noodlez - See revised answer. – Rob Nov 27 '17 at 20:08
1

Thank you all for your support. I was away for a while. But I found a resolution to my situation that is phenomenal. It was a class that does interpolation.

https://github.com/jnfisher/ios-curve-interpolation

All I had to do was pass my array of Points and it smooths them out!

noodlez
  • 89
  • 8