Drawing Smooth Curves - Methods Needed

前端 未结 12 2235
长情又很酷
长情又很酷 2020-11-28 17:13

How do you smooth a set of points in an iOS drawing app WHILE MOVING? I have tried UIBezierpaths but all I get are jagged ends where they intersect, when I just shift the po

相关标签:
12条回答
  • 2020-11-28 18:02

    Here is the code in Swift 4/5

    func quadCurvedPathWithPoint(points: [CGPoint] ) -> UIBezierPath {
        let path = UIBezierPath()
        if points.count > 1 {
            var prevPoint:CGPoint?
            for (index, point) in points.enumerated() {
                if index == 0 {
                    path.move(to: point)
                } else {
                    if index == 1 {
                        path.addLine(to: point)
                    }
                    if prevPoint != nil {
                        let midPoint = self.midPointForPoints(from: prevPoint!, to: point)
                        path.addQuadCurve(to: midPoint, controlPoint: controlPointForPoints(from: midPoint, to: prevPoint!))
                        path.addQuadCurve(to: point, controlPoint: controlPointForPoints(from: midPoint, to: point))
                    }
                }
                prevPoint = point
            }
        }
        return path
    }
    
    func midPointForPoints(from p1:CGPoint, to p2: CGPoint) -> CGPoint {
        return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2)
    }
    
    func controlPointForPoints(from p1:CGPoint,to p2:CGPoint) -> CGPoint {
        var controlPoint = midPointForPoints(from:p1, to: p2)
        let  diffY = abs(p2.y - controlPoint.y)
        if p1.y < p2.y {
            controlPoint.y = controlPoint.y + diffY
        } else if ( p1.y > p2.y ) {
            controlPoint.y = controlPoint.y - diffY
        }
        return controlPoint
    }
    
    0 讨论(0)
  • 2020-11-28 18:03

    I found a pretty nice tutorial that describes a slight modification to Bezier curve drawing that does tend to smooth out the edges pretty nicely. It's essentially what Caleb is referring to above about putting the joining end points on the same line as the control points. It's one of the best tutorials (on anything) that I've read in a while. And it comes with a fully working Xcode project.

    0 讨论(0)
  • 2020-11-28 18:05

    We need to observe some thing before applying any algorithm on captured points.

    1. Generally UIKit does not give the points at equal distance.
    2. We need to calculate the intermediate points in between two CGPoints[ Which has captured with Touch moved method]

    Now to get smooth line, there are so many ways.

    Some times we can achieve the by applying second degree polynomial or third degree polynomial or catmullRomSpline algorithms

    - (float)findDistance:(CGPoint)point lineA:(CGPoint)lineA lineB:(CGPoint)lineB
    {
        CGPoint v1 = CGPointMake(lineB.x - lineA.x, lineB.y - lineA.y);
        CGPoint v2 = CGPointMake(point.x - lineA.x, point.y - lineA.y);
        float lenV1 = sqrt(v1.x * v1.x + v1.y * v1.y);
        float lenV2 = sqrt(v2.x * v2.x + v2.y * v2.y);
        float angle = acos((v1.x * v2.x + v1.y * v2.y) / (lenV1 * lenV2));
        return sin(angle) * lenV2;
    }
    
    - (NSArray *)douglasPeucker:(NSArray *)points epsilon:(float)epsilon
    {
        int count = [points count];
        if(count < 3) {
            return points;
        }
    
        //Find the point with the maximum distance
        float dmax = 0;
        int index = 0;
        for(int i = 1; i < count - 1; i++) {
            CGPoint point = [[points objectAtIndex:i] CGPointValue];
            CGPoint lineA = [[points objectAtIndex:0] CGPointValue];
            CGPoint lineB = [[points objectAtIndex:count - 1] CGPointValue];
            float d = [self findDistance:point lineA:lineA lineB:lineB];
            if(d > dmax) {
                index = i;
                dmax = d;
            }
        }
    
        //If max distance is greater than epsilon, recursively simplify
        NSArray *resultList;
        if(dmax > epsilon) {
            NSArray *recResults1 = [self douglasPeucker:[points subarrayWithRange:NSMakeRange(0, index + 1)] epsilon:epsilon];
    
            NSArray *recResults2 = [self douglasPeucker:[points subarrayWithRange:NSMakeRange(index, count - index)] epsilon:epsilon];
    
            NSMutableArray *tmpList = [NSMutableArray arrayWithArray:recResults1];
            [tmpList removeLastObject];
            [tmpList addObjectsFromArray:recResults2];
            resultList = tmpList;
        } else {
            resultList = [NSArray arrayWithObjects:[points objectAtIndex:0], [points objectAtIndex:count - 1],nil];
        }
    
        return resultList;
    }
    
    - (NSArray *)catmullRomSplineAlgorithmOnPoints:(NSArray *)points segments:(int)segments
    {
        int count = [points count];
        if(count < 4) {
            return points;
        }
    
        float b[segments][4];
        {
            // precompute interpolation parameters
            float t = 0.0f;
            float dt = 1.0f/(float)segments;
            for (int i = 0; i < segments; i++, t+=dt) {
                float tt = t*t;
                float ttt = tt * t;
                b[i][0] = 0.5f * (-ttt + 2.0f*tt - t);
                b[i][1] = 0.5f * (3.0f*ttt -5.0f*tt +2.0f);
                b[i][2] = 0.5f * (-3.0f*ttt + 4.0f*tt + t);
                b[i][3] = 0.5f * (ttt - tt);
            }
        }
    
        NSMutableArray *resultArray = [NSMutableArray array];
    
        {
            int i = 0; // first control point
            [resultArray addObject:[points objectAtIndex:0]];
            for (int j = 1; j < segments; j++) {
                CGPoint pointI = [[points objectAtIndex:i] CGPointValue];
                CGPoint pointIp1 = [[points objectAtIndex:(i + 1)] CGPointValue];
                CGPoint pointIp2 = [[points objectAtIndex:(i + 2)] CGPointValue];
                float px = (b[j][0]+b[j][1])*pointI.x + b[j][2]*pointIp1.x + b[j][3]*pointIp2.x;
                float py = (b[j][0]+b[j][1])*pointI.y + b[j][2]*pointIp1.y + b[j][3]*pointIp2.y;
                [resultArray addObject:[NSValue valueWithCGPoint:CGPointMake(px, py)]];
            }
        }
    
        for (int i = 1; i < count-2; i++) {
            // the first interpolated point is always the original control point
            [resultArray addObject:[points objectAtIndex:i]];
            for (int j = 1; j < segments; j++) {
                CGPoint pointIm1 = [[points objectAtIndex:(i - 1)] CGPointValue];
                CGPoint pointI = [[points objectAtIndex:i] CGPointValue];
                CGPoint pointIp1 = [[points objectAtIndex:(i + 1)] CGPointValue];
                CGPoint pointIp2 = [[points objectAtIndex:(i + 2)] CGPointValue];
                float px = b[j][0]*pointIm1.x + b[j][1]*pointI.x + b[j][2]*pointIp1.x + b[j][3]*pointIp2.x;
                float py = b[j][0]*pointIm1.y + b[j][1]*pointI.y + b[j][2]*pointIp1.y + b[j][3]*pointIp2.y;
                [resultArray addObject:[NSValue valueWithCGPoint:CGPointMake(px, py)]];
            }
        }
    
        {
            int i = count-2; // second to last control point
            [resultArray addObject:[points objectAtIndex:i]];
            for (int j = 1; j < segments; j++) {
                CGPoint pointIm1 = [[points objectAtIndex:(i - 1)] CGPointValue];
                CGPoint pointI = [[points objectAtIndex:i] CGPointValue];
                CGPoint pointIp1 = [[points objectAtIndex:(i + 1)] CGPointValue];
                float px = b[j][0]*pointIm1.x + b[j][1]*pointI.x + (b[j][2]+b[j][3])*pointIp1.x;
                float py = b[j][0]*pointIm1.y + b[j][1]*pointI.y + (b[j][2]+b[j][3])*pointIp1.y;
                [resultArray addObject:[NSValue valueWithCGPoint:CGPointMake(px, py)]];
            }
        }
        // the very last interpolated point is the last control point
        [resultArray addObject:[points objectAtIndex:(count - 1)]]; 
    
        return resultArray;
    }
    
    0 讨论(0)
  • 2020-11-28 18:06

    The key to getting two bezier curves to join smoothly is that the relevant control points and the start/end points on the curves must be collinear. Think of the control point and the endpoint as forming a line that's tangent to the curve at the endpoint. If one curve starts at the same point where another ends, and if they both have the same tangent line at that point, the curve will be smooth. Here's a bit of code to illustrate:

    - (void)drawRect:(CGRect)rect
    {   
    #define commonY 117
    
        CGPoint point1 = CGPointMake(20, 20);
        CGPoint point2 = CGPointMake(100, commonY);
        CGPoint point3 = CGPointMake(200, 50);
        CGPoint controlPoint1 = CGPointMake(50, 60);
        CGPoint controlPoint2 = CGPointMake(20, commonY);
        CGPoint controlPoint3 = CGPointMake(200, commonY);
        CGPoint controlPoint4 = CGPointMake(250, 75);
    
        UIBezierPath *path1 = [UIBezierPath bezierPath];
        UIBezierPath *path2 = [UIBezierPath bezierPath];
    
        [path1 setLineWidth:3.0];
        [path1 moveToPoint:point1];
        [path1 addCurveToPoint:point2 controlPoint1:controlPoint1 controlPoint2:controlPoint2];
        [[UIColor blueColor] set];
        [path1 stroke];
    
        [path2 setLineWidth:3.0];
        [path2 moveToPoint:point2];
        [path2 addCurveToPoint:point3 controlPoint1:controlPoint3 controlPoint2:controlPoint4];
        [[UIColor orangeColor] set];
        [path2 stroke];
    }
    

    Notice that path1 ends at point2, path2 starts at point2, and control points 2 and 3 share the same Y-value, commonY, with point2. You can change any of the values in the code as you like; as long as those three points all fall on the same line, the two paths will join smoothly. (In the code above, the line is y = commonY. The line doesn't have to be parallel to the X axis; it's just easier to see that the points are collinear that way.)

    Here's the image that the code above draws:

    two paths joined smoothly

    After looking at your code, the reason that your curve is jagged is that you're thinking of control points as points on the curve. In a bezier curve, the control points are usually not on the curve. Since you're taking the control points from the curve, the control points and the point of intersection are not collinear, and the paths therefore don't join smoothly.

    0 讨论(0)
  • 2020-11-28 18:06

    I was inspired by the answer of u/User1244109 ... but it only works if the points are constantly fluctuating up and then down each time, so that every point should be joined by an S curve.

    I built off of his answer to include custom logic to check if the point is going to be a local minima or not, and then use the S-curve if so, otherwise determine if it should curve up or down based on the points before and after it or if it should curve tangentially and if so I use the intersection of tangents as the control point.

    #define AVG(__a, __b) (((__a)+(__b))/2.0)
    
    -(UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points {
        
        if (points.count < 2) {
            return [UIBezierPath new];
        }
        
        UIBezierPath *path = [UIBezierPath bezierPath];
        
        CGPoint p0 = [points[0] CGPointValue];
        CGPoint p1 = [points[1] CGPointValue];
        
        [path moveToPoint:p0];
        
        if (points.count == 2) {
            [path addLineToPoint:p1];
            return path;
        }
        
        for (int i = 1; i <= points.count-1; i++) {
            
            CGPoint p1 = [points[i-1] CGPointValue];
            CGPoint p2 = [points[i] CGPointValue];//current point
            CGPoint p0 = p1;
            CGPoint p3 = p2;
            if (i-2 >= 0) {
                p0 = [points[i-2] CGPointValue];
            }
            if (i+1 <= points.count-1) {
                p3 = [points[i+1] CGPointValue];
            }
        
            if (p2.y == p1.y) {
                [path addLineToPoint:p2];
                continue;
            }
            
            float previousSlope = p1.y-p0.y;
            float currentSlope = p2.y-p1.y;
            float nextSlope = p3.y-p2.y;
            
            BOOL shouldCurveUp = NO;
            BOOL shouldCurveDown = NO;
            BOOL shouldCurveS = NO;
            BOOL shouldCurveTangental = NO;
            
            if (previousSlope < 0) {//up hill
                
                if (currentSlope < 0) {//up hill
                    
                    if (nextSlope < 0) {//up hill
                        
                        shouldCurveTangental = YES;
                        
                    } else {//down hill
                        
                        shouldCurveUp = YES;
                        
                    }
                    
                } else {//down hill
                    
                    if (nextSlope > 0) {//down hill
                        
                        shouldCurveUp = YES;
                        
                    } else {//up hill
                        
                        shouldCurveS = YES;
                        
                    }
                    
                }
                
            } else {//down hill
                
                if (currentSlope > 0) {//down hill
                    
                    if (nextSlope > 0) {//down hill
                        
                        shouldCurveTangental = YES;
                        
                    } else {//up hill
                        
                        shouldCurveDown = YES;
                        
                    }
                    
                } else {//up hill
                    
                    if (nextSlope < 0) {//up hill
                        
                        shouldCurveDown = YES;
                        
                    } else {//down hill
                        
                        shouldCurveS = YES;
                        
                    }
                    
                }
                
            }
            
            if (shouldCurveUp) {
                [path addQuadCurveToPoint:p2 controlPoint:CGPointMake(AVG(p1.x, p2.x), MIN(p1.y, p2.y))];
            }
            if (shouldCurveDown) {
                [path addQuadCurveToPoint:p2 controlPoint:CGPointMake(AVG(p1.x, p2.x), MAX(p1.y, p2.y))];
            }
            if (shouldCurveS) {
                CGPoint midPoint = midPointForPoints(p1, p2);
                [path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)];
                [path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)];
            }
            if (shouldCurveTangental) {
                
                float nextTangent_dy = p3.y-p2.y;
                float nextTangent_dx = p3.x-p2.x;
                float previousTangent_dy = p1.y-p0.y;
                float previousTangent_dx = p1.x-p0.x;
                
                float nextTangent_m = 0;
                if (nextTangent_dx != 0) {
                    nextTangent_m = nextTangent_dy/nextTangent_dx;
                }
                float previousTangent_m = 0;
                if (nextTangent_dx != 0) {
                    previousTangent_m = previousTangent_dy/previousTangent_dx;
                }
                
                if (isnan(previousTangent_m) ||
                    isnan(nextTangent_m) ||
                    nextTangent_dx == 0 ||
                    previousTangent_dx == 0) {//division by zero would have occured, etc
                    [path addLineToPoint:p2];
                } else {
                    
                    CGPoint nextTangent_start = CGPointMake(p1.x, (nextTangent_m*p1.x) - (nextTangent_m*p2.x) + p2.y);
                    CGPoint nextTangent_end = CGPointMake(p2.x, (nextTangent_m*p2.x) - (nextTangent_m*p2.x) + p2.y);
                    
                    CGPoint previousTangent_start = CGPointMake(p1.x, (previousTangent_m*p1.x) - (previousTangent_m*p1.x) + p1.y);
                    CGPoint previousTangent_end = CGPointMake(p2.x, (previousTangent_m*p2.x) - (previousTangent_m*p1.x) + p1.y);
                    
                    NSValue *tangentIntersection_pointValue = [self intersectionOfLineFrom:nextTangent_start to:nextTangent_end withLineFrom:previousTangent_start to:previousTangent_end];
                    
                    if (tangentIntersection_pointValue) {
                        [path addQuadCurveToPoint:p2 controlPoint:[tangentIntersection_pointValue CGPointValue]];
                    } else {
                        [path addLineToPoint:p2];
                    }
                    
                }
                
            }
            
        }
        
        return path;
        
    }
    
    -(NSValue *)intersectionOfLineFrom:(CGPoint)p1 to:(CGPoint)p2 withLineFrom:(CGPoint)p3 to:(CGPoint)p4 {//from https://stackoverflow.com/a/15692290/2057171
        CGFloat d = (p2.x - p1.x)*(p4.y - p3.y) - (p2.y - p1.y)*(p4.x - p3.x);
        if (d == 0)
            return nil; // parallel lines
        CGFloat u = ((p3.x - p1.x)*(p4.y - p3.y) - (p3.y - p1.y)*(p4.x - p3.x))/d;
        CGFloat v = ((p3.x - p1.x)*(p2.y - p1.y) - (p3.y - p1.y)*(p2.x - p1.x))/d;
        if (u < 0.0 || u > 1.0)
            return nil; // intersection point not between p1 and p2
        if (v < 0.0 || v > 1.0)
            return nil; // intersection point not between p3 and p4
        CGPoint intersection;
        intersection.x = p1.x + u * (p2.x - p1.x);
        intersection.y = p1.y + u * (p2.y - p1.y);
        
        return [NSValue valueWithCGPoint:intersection];
    }
    
    static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) {
        return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
    }
    
    static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) {
        CGPoint controlPoint = midPointForPoints(p1, p2);
        CGFloat diffY = fabs(p2.y - controlPoint.y);
        
        if (p1.y < p2.y)
            controlPoint.y += diffY;
        else if (p1.y > p2.y)
            controlPoint.y -= diffY;
        
        return controlPoint;
    }
    
    0 讨论(0)
  • 2020-11-28 18:07

    @Rakesh is absolutely right - you dont need to use Catmull-Rom algorithm if you just want a curved line. And the link he suggested does exacly that. So here's an addition to his answer.

    The code bellow does NOT use Catmull-Rom algorithm & granularity, but draws a quad-curved line (control points are calculated for you). This is essentially what's done in the ios freehand drawing tutorial suggested by Rakesh, but in a standalone method that you can drop anywhere (or in a UIBezierPath category) and get a quad-curved spline out of the box.

    You do need to have an array of CGPoint's wrapped in NSValue's

    + (UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points
    {
        UIBezierPath *path = [UIBezierPath bezierPath];
    
        NSValue *value = points[0];
        CGPoint p1 = [value CGPointValue];
        [path moveToPoint:p1];
    
        if (points.count == 2) {
            value = points[1];
            CGPoint p2 = [value CGPointValue];
            [path addLineToPoint:p2];
            return path;
        }
    
        for (NSUInteger i = 1; i < points.count; i++) {
            value = points[i];
            CGPoint p2 = [value CGPointValue];
    
            CGPoint midPoint = midPointForPoints(p1, p2);
            [path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)];
            [path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)];
    
            p1 = p2;
        }
        return path;
    }
    
    static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) {
        return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
    }
    
    static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) {
        CGPoint controlPoint = midPointForPoints(p1, p2);
        CGFloat diffY = abs(p2.y - controlPoint.y);
    
        if (p1.y < p2.y)
            controlPoint.y += diffY;
        else if (p1.y > p2.y)
            controlPoint.y -= diffY;
    
        return controlPoint;
    }
    

    Here's the result: enter image description here

    0 讨论(0)
提交回复
热议问题