理解 UIBezierPath 曲线机制、controlPoint 和曲线点

Undestanding UIBezierPath curving mechanism, controlPoint and the curve point

我正在尝试使用 UIBezierPath 绘制一个简单的抛物线形状。我有一个 maxPoint 和一个 boundingRect,我基于抛物线的宽度和拉伸。
这是我绘制抛物线的函数(我在容器视图中绘制抛物线,rect 将是 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
    ...
}

我的问题是,我希望我在函数中发送的 maxPoint 是抛物线本身的实际极值点。因此,例如,如果我发送 (CGRectGetMidX(container.bounds), 0),最大点应该在最顶部的中心。但是在这个特定点上使用这个函数,结果是这样的:

那么这里的路径到底是做什么的?或者换句话说,我怎样才能从 controlPoint 到我需要的实际最大值?我已经尝试根据 boundingRect 的高度从 y 值中添加和减去不同的值,但我找不到正确的组合,因为在不同的点具有不同的 [=19] =] 值它的行为不同。好像加了个乘法器,怎么解决?

让路径 = 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)

这是原因:https://cdn.tutsplus.com/mobile/authors/legacy/Akiel%20Khan/2012/10/15/bezier.png你应该考虑切线的概念

诀窍是将曲线分成两段,以便您可以控制曲线通过哪些点。正如 Eduardo 的回答中提到的,控制点处理切线,端点在曲线上。这让你有一条曲线从左下角到顶部中心,然后从顶部中心到右下角:

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 的解决方案很好,但它实际上并没有创建抛物线。要创建抛物线,我们需要计算给定二次曲线中点的控制点。贝塞尔路径只是数学;我们可以很容易地计算出来。我们只需要反转贝塞尔函数并求解 t=0.5.

0.5(中点)处的 Bézier 解在 Draw a quadratic Bézier curve through three given points 处得到很好的导出。

2*Pc - P0/2 - P2/2

其中Pc是我们要经过的点,P0P2是终点

(在其他点计算贝塞尔曲线不是很直观。t=0.25 处的值不是 "a quarter of the way along the path." 但幸运的是,就我们的目的而言,t=0.5 与我们对 [=34= 的直觉非常吻合] 在二次方程上。)

鉴于我们的解决方案,我们可以编写我们的代码。原谅翻译成Swift3;我的 Xcode 7.3 版本对 iOS 游乐场不太满意,但应该很容易转换为 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
}

halfPoint1D 函数是我们解决方案的一维实现。对于我们的二维CGPoint,只需要调用两次即可。

如果我只能推荐一种理解贝塞尔曲线的资源,那可能就是 "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。可以略读一下,只阅读您目前感兴趣的部分。但是对这组函数的基本理解将大大有助于消除 Core Graphics 中绘图的魔力,并使 UIBezierPath 成为一个工具而不是一个黑盒子。

我需要做一些类似的事情,我希望有一个 UIBezierPath 与特定的抛物线定义完全匹配。所以我做了这个 class,它根据焦点和准线或一般方程的 a、b、c 创建了一个抛物线。我加入了一个方便的 init,它可以使用您的 boundingRectmaxPoint 概念。调整那些或 init,其中盒子的上角是它的 1 和 2,底边的中间是顶点。

根据需要使用 xform 进行缩放和平移。您可以 create/draw 基于抛物线上任意两点的路径。他们不必具有相同的 y-value。生成的形状仍将与指定的抛物线完全匹配。

这在旋转方面并不完全通用,但这是一个开始。


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
    }
}