Bezier icon indicating copy to clipboard operation
Bezier copied to clipboard

Incorrect curve

Open wojciech-kulik opened this issue 6 years ago • 4 comments

Hi,

There is some issue with this code, it doesn't work well with all data. Please replace your points with: "100, 200, 300, 120, 400, 350, 350, 170, 200".

You will see this: simulator screen shot - iphone 8 - 2018-11-10 at 18 45 24

Top of the curve should be exactly in point, not above/below. Do you know how to fix it?

wojciech-kulik avatar Nov 10 '18 17:11 wojciech-kulik

I tried to modify the code for a closed curve and I also have a problem. Couldn't find where it is yet though. I keep digging!

roussetjc avatar Mar 03 '19 16:03 roussetjc

pretty sure this isn't solvable with Bezier curves -- afaik they are an approximation, so they'll never match the data exactly. You can get closer with higher-order curves, but they get computationally very expensive.

DominicHolmes avatar Jun 26 '19 16:06 DominicHolmes

It is solvable. The curve doesn't have to be pixel perfect, however as you can see on the screenshot the curve doesn't have turns in the middle of points.

I ended up with this code and it works perfectly fine ;)

(C# code)

    public static class BezierCurve
    {
        public static UIBezierPath GetCurvedPath(CGPoint[] points)
        {
            var path = new UIBezierPath();
            if (points.Length == 0)
            {
                return path;
            }

            var p1 = points[0];
            path.MoveTo(p1);

            if (points.Length == 2)
            {
                path.AddLineTo(points[1]);
                return path;
            }

            for (int k = 1; k < points.Length; k++)
            {
                var p2 = points[k];
                var midPoint = MidPointFor(p1, p2);

                path.AddQuadCurveToPoint(midPoint, ControlPointFor(midPoint, p1));
                path.AddQuadCurveToPoint(p2, ControlPointFor(midPoint, p2));

                p1 = p2;
            }

            return path;
        }

        private static CGPoint MidPointFor(CGPoint p1, CGPoint p2)
        {
            return new CGPoint((p1.X + p2.X) / 2, (p1.Y + p2.Y) / 2);
        }

        private static CGPoint ControlPointFor(CGPoint p1, CGPoint p2)
        {
            var controlPoint = MidPointFor(p1, p2);
            var diffY = (nfloat)Math.Abs(p2.Y - controlPoint.Y);

            if (p1.Y < p2.Y)
                controlPoint.Y += diffY;
            else if (p1.Y > p2.Y)
                controlPoint.Y -= diffY;

            return controlPoint;
        }
    }

wojciech-kulik avatar Jun 26 '19 16:06 wojciech-kulik

I can confirm, @wojciech-kulik's solution works fine. Here is swift version:

func path() -> UIBezierPath {
    ...
    let linePath = UIBezierPath()
    guard graphPoints.count > 0 else { return }
        
    var p1: CGPoint = graphPoints[0]
    linePath.move(to: p1)
        
    for i in 1..<graphPoints.count {
        let p2 = graphPoints[i]
        let midPoint = midPointFor(p1: p1, p2: p2)
            
        linePath.addQuadCurve(to: midPoint, controlPoint: controlPointFor(p1: midPoint, p2: p1))
        linePath.addQuadCurve(to: p2, controlPoint: controlPointFor(p1: midPoint, p2: p2))
            
        p1 = p2
    }
    return linePath
}
private func midPointFor(p1: CGPoint, p2: CGPoint) -> CGPoint {
    return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2)
}

private func controlPointFor(p1: CGPoint , p2: CGPoint ) -> CGPoint {
    var controlPoint = midPointFor(p1: p1, p2: p2)
    var 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
}

snowtema avatar May 09 '21 10:05 snowtema