8

I'm trying to draw a simple Parabola shape using UIBezierPath. I have a maxPoint and a boundingRect of which I'm basing the width and stretch of the parabola.
Here's the function I made to draw the parabola (I draw the parabola in a container view, rect will be container.bounds):

func addParabolaWithMax(maxPoint: CGPoint, inRect boundingRect: CGRect) {
    let path = UIBezierPath()

    let p1 = CGPointMake(1, CGRectGetMaxY(boundingRect)-1)
    let p3 = CGPointMake(CGRectGetMaxX(boundingRect)-1, CGRectGetMaxY(boundingRect)-1)

    path.moveToPoint(p1)
    path.addQuadCurveToPoint(p3, controlPoint: maxPoint)

    // Drawing code
    ...
}

My problem is, that I want the maxPoint that I send in the function to be the actual extreme point in the parabola itself. So for example, if I send in (CGRectGetMidX(container.bounds), 0), The maximum point should be at the top-most center. But in using this function with this particular point, this is what the result looks like:

enter image description here

So what exactly the path does here? Or in other words, how can I get from the controlPoint to the actual max point that I need? I've tried adding and subtracting different values from the y value, based on the height of the boundingRect, but I couldn't quite find the right combination, as in different points with different y values it behaves differently. There seem to be some kind of multiplier being added in, how can I solve it?

Eilon
  • 2,698
  • 3
  • 16
  • 32

4 Answers4

15

For may applications adam.wulf's solution is fine, but it doesn't actually create a parabola. To create a parabola, we need to compute the control point given the midpoint of the quadratic curve. Bézier paths are just math; we can compute this quite easily. We just need to invert the Bézier function and solve it for t=0.5.

The Bézier solution at 0.5 (the midpoint) is derived nicely at Draw a quadratic Bézier curve through three given points.

2*Pc - P0/2 - P2/2

Where Pc is the point we want to go through and P0 and P2 are the end points.

(Computing the Bézier at other points is not very intuitive. The value at t=0.25 is not "a quarter of the way along the path." But luckily for our purposes, t=0.5 matches quite nicely to our intuition of "the midpoint" on a quadratic.)

Given our solution, we can write our code. Forgive the translation to Swift 3; my copy of Xcode 7.3 isn't very happy with iOS playgrounds, but it should be easy to convert to 2.2.

func addParabolaWithMax(maxPoint: CGPoint, inRect boundingRect: CGRect) -> UIBezierPath {

    func halfPoint1D(p0: CGFloat, p2: CGFloat, control: CGFloat) -> CGFloat {
        return 2 * control - p0 / 2 - p2 / 2
    }

    let path = UIBezierPath()

    let p0 = CGPoint(x: 0, y: boundingRect.maxY)
    let p2 = CGPoint(x: boundingRect.maxX, y: boundingRect.maxY)

    let p1 = CGPoint(x: halfPoint1D(p0: p0.x, p2: p2.x, control: maxPoint.x),
                     y: halfPoint1D(p0: p0.y, p2: p2.y, control: maxPoint.y))

    path.move(to: p0)
    path.addQuadCurve(to: p2, controlPoint: p1)
    return path
}

The halfPoint1D function is the the one-dimensional implementation of our solution. For our two-dimentional CGPoint, we just have to call it twice.

If I could recommend just one resource for understanding Bézier curves, it would probably be the "Constructing Bézier curves" section from Wikipedia. Studying the little animations that show how the curves come about I find very enlightening. The "Specific Cases" section is useful as well. For a deep exploration of the topic (and one that I recommend all developers have a passing familiarity with), I like A Primer on Bézier Curves. It's ok to skim it and just read the parts that interest you at the moment. But a basic understanding of this group of functions will go a long way to removing the magic from drawing in Core Graphics and make UIBezierPath a tool rather than a black box.

Community
  • 1
  • 1
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Note that if your goal is to create a smooth curve that lies on a series of points, Catmull-Rom splines might be a better choice than Bezier curves. They are computationally more expensive than Beziers, they create a curve that passes through all the control points. However, like Bezier curves, you can get "kinks" in Catmull-Rom splines if you try to curve too sharply. Erica Sadun's excellent iOS Developer's Cookbook books has working code for Catmull-Rom splines if you're interested. – Duncan C Dec 14 '16 at 21:26
  • One frustrating thing about Catmull-Rom splines is that they don't include the first and last points, so you need to add extra extension points in the "right places." You can avoid kinks if you use centripetal C-R splines (which are my usual preference for just that reason). – Rob Napier Dec 14 '16 at 21:47
0

let path = UIBezierPath()

        let p1 = CGPointMake(0,self.view.frame.height/2)
        let p3 = CGPointMake(self.view.frame.width,self.view.frame.height/2)

        path.moveToPoint(p1)
        path.addQuadCurveToPoint(p3, controlPoint: CGPoint(x: self.view.frame.width/2, y: -self.view.frame.height/2))

        let line = CAShapeLayer()
        line.path = path.CGPath;
        line.strokeColor = UIColor.blackColor().CGColor
        line.fillColor = UIColor.redColor().CGColor
        view.layer.addSublayer(line)

this is the reason: https://cdn.tutsplus.com/mobile/authors/legacy/Akiel%20Khan/2012/10/15/bezier.png you should have to consider the tangent concept

Learn Swift
  • 570
  • 5
  • 15
  • Having used that code indeed brings the max point to the top, but I need the parabola to start at the lower most left and end at the lowermost right, why do you start and end the parabola at half the height? I've managed to make it how I want with this particular point by having `-container.bounds.height` at the `controlPoint`, but the thing is that it doesn't work with all type of points. So there must be some dynamic value or multiplier that is being added to the y value of the control point. This is what I'm trying to find – Eilon Aug 02 '16 at 16:31
0

The trick is to split the curve into two pieces so that you can control which points the curve passes through. As mentioned in Eduardo's answer, control points handle tangent, and end points are on the curve. This lets you have a curve from the bottom left to top center, then from top center to bottom right:

let p1 = CGPointMake(0,self.view.frame.height/2)
let p3 = CGPointMake(self.view.frame.width,self.view.frame.height/2)
let ctrlRight = CGPointMake(self.view.frame.width,0)
let ctrlLeft = CGPointZero

let bezierPath = UIBezierPath()
bezierPath.moveToPoint(p1)
bezierPath.addCurveToPoint(maxPoint, controlPoint1: p1, controlPoint2: ctrlLeft)
bezierPath.addCurveToPoint(p3, controlPoint1: ctrlRight, controlPoint2: p3)

UIColor.blackColor().setStroke()
bezierPath.lineWidth = 1
bezierPath.stroke()
adam.wulf
  • 2,149
  • 20
  • 27
  • Agreed with the approach, but I think the curve will much more closely match the request if you remove the `/2` on `p1` and `p3`. But this won't be a parabola, since you're adding a cubic curve rather than a quad curve. (But you can make it "parabola-like" which may be the real goal.) – Rob Napier Aug 03 '16 at 20:18
0

I needed to do something similar where I wanted to have a UIBezierPath that exactly matched a specific parabola definition. So I made this little class that creates a parabola based on the focus and directrix or the a, b, c of the general equation. I threw in a convenience init which can use your boundingRect and maxPoint concepts. Either adapt those or the init where the upper corners of the box are its 1 and 2 and the middle of the bottom edge is the vertex.

Use the xform to scale and translate as needed. You can create/draw the path based on any two points on the parabola. They don't have to have the same y-value. The resulting shape will still exactly match the specified parabola.

This is not completely general in terms of rotation but it's a start.


class Parabola
{
    var focus: CGPoint
    var directrix: CGFloat
    var a, b, c: CGFloat
    
    init(_ f: CGPoint, _ y: CGFloat)
    {
        focus = f
        directrix = y
        let dy = f.y - y
        a = 1 / (2*dy)
        b = -f.x / dy
        c = (f.x*f.x + f.y*f.y - y*y) / (2*dy)
    }
    
    init(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat)
    {
        self.a = a
        self.b = b
        self.c = c
        focus = CGPoint(x: -b / (2*a), y: (4*a*c - b*b + 1) / (4*a))
        directrix = (4*a*c - b*b - 1) / (4*a)
    }
    
    convenience init(_ v: CGPoint, 
                     _ pt1: CGPoint, 
                     _ pt2: CGPoint)
    {
        let a = (pt2.y - v.y) / (pt2.x - v.x) / (pt2.x - v.x)
        self.init(CGPoint(x: v.x, y: v.y + 1/(4*a)), 
                  v.y - 1/(4*a))
    }

        
    func f(of x: CGFloat) -> CGFloat
    {
        a*x*x + b*x + c
    }
    
    func path(_ x1: CGFloat, _ x2: CGFloat,
              _ xform: CGAffineTransform? = .identity) -> UIBezierPath
    {
        let pt1 = CGPoint(x1, f(of: x1))
        let pt2 = CGPoint(x2, f(of: x2))
        let x = (x1 + x2) / 2
        let y = (2*a * x1 + b) * (x - x1) + pt1.y
        let path = UIBezierPath()
        path.move(to: pt1)
        path.addQuadCurve(to: pt2, controlPoint: CGPoint(x: x, y: y))
        path.apply(xform!)
        return path
    }
}
Troy Sartain
  • 163
  • 1
  • 4
  • 15