理解UIBezierPath的曲线机制、控制点和曲线点

8

我试图使用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),则最大点应该在最上方的中心位置。但是,在使用此特定点的函数时,结果看起来像这样:

enter image description here

那么这里路径到底是什么?或者换句话说,我如何从controlPoint到达我需要的实际最大点?我尝试在y值上添加和减去不同的值,基于boundingRect的高度,但我无法找到正确的组合,因为在具有不同y值的不同点上,它的行为不同。似乎有一种乘数被添加进去了,我该怎么解决呢?

一个有关贝塞尔曲线的实用解释:https://javascript.info/bezier-curve - Maxwell
4个回答

15
对于许多应用,adam.wulf的解决方案很好,但它实际上并没有创建一个抛物线。要创建一个抛物线,我们需要计算给定二次曲线中点的控制点。Bézier路径只是数学;我们可以很容易地计算出这个值。我们只需要反转Bézier函数并将其解决为t=0.5即可。
Bézier函数在0.5(中点)处的解导出得很好,可以参考通过三个给定点绘制二次Bézier曲线
2*Pc - P0/2 - P2/2

其中Pc是我们想要通过的点,P0P2是端点。

(在其他点计算贝塞尔曲线并不是非常直观。t = 0.25 的值不是“路径的四分之一”。但幸运的是,t = 0.5 很好地匹配了我们对于二次曲线“中点”的直觉。)

给定解决方案后,我们可以编写代码。请谅解翻译成Swift 3;我的Xcode 7.3不太适合iOS playgrounds,但应该很容易转换为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,我们只需要调用它两次即可。
如果我只能推荐一个理解贝塞尔曲线的资源,那可能会是维基百科的"构造贝塞尔曲线"部分。研究展示曲线如何产生的小动画非常有启发性。"特定情况"部分也很有用。对于这个主题的深入探讨(我建议所有开发人员都应该略知一二),我喜欢贝塞尔曲线入门。可以略读并只阅读目前感兴趣的部分。但是对这组函数的基本理解将大大消除在Core Graphics中绘图的神秘感,并使UIBezierPath成为工具而不是黑盒子。

请注意,如果您的目标是创建一个平滑曲线,该曲线位于一系列点上,则Catmull-Rom样条曲线可能比Bezier曲线更好。它们的计算成本比Beziers更高,但它们创建的曲线通过所有控制点。然而,就像Bezier曲线一样,如果您尝试弯曲得太厉害,Catmull-Rom样条曲线也会出现“折痕”。如果您有兴趣,Erica Sadun的优秀iOS开发者食谱书中有Catmull-Rom样条曲线的工作代码。 - Duncan C
关于Catmull-Rom样条的一个令人沮丧的问题是它们不包括第一个和最后一个点,因此您需要在“正确的位置”添加额外的扩展点。如果您使用离心C-R样条(出于这个原因,这是我通常的首选),则可以避免出现折痕。 - Rob Napier

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)

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


使用该代码确实会将最大点放置在顶部,但我需要抛物线从最左下方开始,并在最右下方结束,为什么你要在高度的一半处开始和结束抛物线呢?我已经通过在 controlPoint 处使用 -container.bounds.height 来使它成为我想要的样子,但问题是它并不适用于所有类型的点。因此,在控制点的 y 值上必须添加某些动态值或乘数。这就是我试图找到的东西。 - Eilon

0

诀窍在于将曲线分成两段,以便您可以控制曲线通过哪些点。正如 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()

同意这种方法,但是我认为如果你在 p1p3 上去掉 /2,曲线将更接近于请求。但这不会是一个二次曲线,因为你添加的是一个三次曲线而不是一个二次曲线。(但你可以使它“类似于二次曲线”,这可能是真正的目标。) - Rob Napier

0

我需要做类似的事情,我想要一个UIBezierPath恰好匹配特定的抛物线定义。因此,我创建了这个小类,它基于焦点和准线或一般方程的a、b、c创建抛物线。我加入了一个方便的初始化方法,可以使用您的boundingRectmaxPoint概念。可以适应这些内容或者使用初始化方法,其中框的上角是其1和2,底边的中心是顶点。

使用xform按需缩放和平移。您可以基于抛物线上的任意两个点创建/绘制路径。它们不必具有相同的y值。生成的形状仍将完全匹配指定的抛物线。

就旋转而言,这并不完全通用,但这是一个开始。


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接