简体   繁体   English

如何在Swift 4中使用CAShapeLayer和BezierPath绘制曲线?

[英]How to draw a curved line using CAShapeLayer and BezierPath in Swift 4?

I was wondering how I could render a curved line like the one shown in the picture below given two points (Point A and Point B) using CAShapeLayer and BezierPath in Swift 4? 我想知道如何在Swift 4中使用CAShapeLayer和BezierPath渲染一条曲线,如下图所示,给出两个点(点A和点B)?

func drawCurvedLine(start: CGPoint, end: CGPoint) {
   //insert code here
}

在此处输入图片说明

You will need to apply some math. 您将需要应用一些数学。 The way I see it this is composed of 2 arcs with different radiuses. 我的看法是,这是由两个半径不同的弧组成的。 It might be quite challenging to compute those from 2 points but fortunately we have tools for arc that already do this for us. 从2个点计算出这些值可能会非常具有挑战性,但是幸运的是,我们已经有了用于弧的工具。 A method addQuadCurve on UIBezierPath seems perfect for this. addQuadCurve上的UIBezierPath方法似乎很完美。

We need to input 2 points, a factor on how much the arc is bound and some line thickness. 我们需要输入2个点,这是绑定圆弧和线宽的一个因素。 We use bend factor to determine how much is control point moved downward from center of the two points in your case. 我们使用折弯系数来确定控制点从您的案例中的两个点的中心向下移动了多少。 Downwards can be relative to the 2 points so let's just rather use normal . 向下可能相对于2点,所以我们只使用normal What I got to is the following: 我要做的是以下几点:

func generateSpecialCurve(from: CGPoint, to: CGPoint, bendFactor: CGFloat, thickness: CGFloat) -> UIBezierPath {

    let center = CGPoint(x: (from.x+to.x)*0.5, y: (from.y+to.y)*0.5)
    let normal = CGPoint(x: -(from.y-to.y), y: (from.x-to.x))
    let normalNormalized: CGPoint = {
        let normalSize = sqrt(normal.x*normal.x + normal.y*normal.y)
        guard normalSize > 0.0 else { return .zero }
        return CGPoint(x: normal.x/normalSize, y: normal.y/normalSize)
    }()

    let path = UIBezierPath()

    path.move(to: from)

    let midControlPoint: CGPoint = CGPoint(x: center.x + normal.x*bendFactor, y: center.y + normal.y*bendFactor)
    let closeControlPoint: CGPoint = CGPoint(x: midControlPoint.x + normalNormalized.x*thickness*0.5, y: midControlPoint.y + normalNormalized.y*thickness*0.5)
    let farControlPoint: CGPoint = CGPoint(x: midControlPoint.x - normalNormalized.x*thickness*0.5, y: midControlPoint.y - normalNormalized.y*thickness*0.5)


    path.addQuadCurve(to: to, controlPoint: closeControlPoint)
    path.addQuadCurve(to: from, controlPoint: farControlPoint)
    path.close()
    return path
}

The data I used to get to a shape similar to your request was (overriding drawRect was the quickest): 我用来获得与您的请求类似的形状的数据是(覆盖drawRect最快):

override func draw(_ rect: CGRect) {
    super.draw(rect)

    let from: CGPoint = CGPoint(x: 100.0, y: 300.0)
    let to: CGPoint = CGPoint(x: 200.0, y: 300.0)

    UIColor.blue.setFill()
    generateSpecialCurve(from: from, to: to, bendFactor: -0.25, thickness: 10.0).fill()
}

Now negative vend factor means bending downward, the rest should be pretty intuitive. 现在,负面的销售因素意味着向下弯曲,其余的应该非常直观。

EDIT Much more control can be achieved by composing the shape out of 4 curves: 编辑通过组合4条曲线中的形状可以实现更多控制:

Using 4 curves you can create nicer shapes but code may complicate quite nicely. 使用4条曲线可以创建更好的形状,但是代码可能会非常复杂。 This is what I tried to get a shape closer to the one you want: 这是我试图使形状更接近所需形状的方法:

func generateSpecialCurve(from: CGPoint, to: CGPoint, bendFactor: CGFloat, thickness: CGFloat, showDebug: Bool = false) -> UIBezierPath {

    var specialCurveScale: CGFloat = 0.2 // A factor to control sides
    var midControlPointsScale: CGFloat = 0.3 // A factor to cotnrol mid

    let center = from.adding(to).scaled(by: 0.5)
    let direction = from.direction(toward: to)
    let directionNormalized = direction.normalized
    let normal = direction.normal
    let normalNormalized = normal.normalized

    let middlePoints: (near: CGPoint, far: CGPoint) = {
        let middlePoint = center.adding(normal.scaled(by: bendFactor))
        return (middlePoint.subtracting(normalNormalized.scaled(by: thickness*0.5)), middlePoint.adding(normalNormalized.scaled(by: thickness*0.5)))
    }()

    let borderControlPoints: (start: CGPoint, end: CGPoint) = {
        let borderTangentScale: CGFloat = 1.0
        let normalDirectionFactor: CGFloat = bendFactor < 0.0 ? -1.0 : 1.0

        let startTangent = normal.scaled(by: normalDirectionFactor).adding(direction.scaled(by: specialCurveScale)).scaled(by: normalDirectionFactor)
        let endTangent = normal.scaled(by: normalDirectionFactor).subtracting(direction.scaled(by: specialCurveScale)).scaled(by: normalDirectionFactor)

        return (from.adding(startTangent.scaled(by: bendFactor)), to.adding(endTangent.scaled(by: bendFactor)))
    }()


    let farMidControlPoints: (start: CGPoint, end: CGPoint) = {
        let normalDirectionFactor: CGFloat = bendFactor < 0.0 ? -1.0 : 1.0
        return (start: middlePoints.far.adding(direction.scaled(by: bendFactor*normalDirectionFactor*midControlPointsScale)),
                end: middlePoints.far.adding(direction.scaled(by: -bendFactor*normalDirectionFactor*midControlPointsScale)))
    }()
    let nearMidControlPoints: (start: CGPoint, end: CGPoint) = {
        let normalDirectionFactor: CGFloat = bendFactor < 0.0 ? -1.0 : 1.0
        return (start: middlePoints.near.adding(direction.scaled(by: -bendFactor*normalDirectionFactor*midControlPointsScale)),
                end: middlePoints.near.adding(direction.scaled(by: bendFactor*normalDirectionFactor*midControlPointsScale)))
    }()

    if showDebug {
        func line(_ a: CGPoint, _ b: CGPoint) -> UIBezierPath {
            let path = UIBezierPath()
            path.move(to: a)
            path.addLine(to: b)
            path.lineWidth = 1
            return path
        }

        let debugAlpha: CGFloat = 0.3

        UIColor.green.withAlphaComponent(debugAlpha).setFill()
        UIColor.green.withAlphaComponent(debugAlpha).setStroke()
        line(from, borderControlPoints.start).stroke()
        line(to, borderControlPoints.end).stroke()
        UIBezierPath(arcCenter: borderControlPoints.start, radius: 3.0, startAngle: 0.0, endAngle: .pi*2.0, clockwise: true).fill()
        UIBezierPath(arcCenter: borderControlPoints.end, radius: 3.0, startAngle: 0.0, endAngle: .pi*2.0, clockwise: true).fill()



        UIColor.red.withAlphaComponent(debugAlpha).setFill()
        UIColor.red.withAlphaComponent(debugAlpha).setStroke()
        line(middlePoints.near, nearMidControlPoints.start).stroke()
        UIBezierPath(arcCenter: nearMidControlPoints.start, radius: 3.0, startAngle: 0.0, endAngle: .pi*2.0, clockwise: true).fill()

        UIColor.cyan.withAlphaComponent(debugAlpha).setFill()
        UIColor.cyan.withAlphaComponent(debugAlpha).setStroke()
        line(middlePoints.far, farMidControlPoints.start).stroke()
        UIBezierPath(arcCenter: farMidControlPoints.start, radius: 3.0, startAngle: 0.0, endAngle: .pi*2.0, clockwise: true).fill()

        UIColor.yellow.withAlphaComponent(debugAlpha).setFill()
        UIColor.yellow.withAlphaComponent(debugAlpha).setStroke()
        line(middlePoints.near, nearMidControlPoints.end).stroke()
        UIBezierPath(arcCenter: nearMidControlPoints.end, radius: 3.0, startAngle: 0.0, endAngle: .pi*2.0, clockwise: true).fill()

        UIColor.purple.withAlphaComponent(debugAlpha).setFill()
        UIColor.purple.withAlphaComponent(debugAlpha).setStroke()
        line(middlePoints.far, farMidControlPoints.end).stroke()
        UIBezierPath(arcCenter: farMidControlPoints.end, radius: 3.0, startAngle: 0.0, endAngle: .pi*2.0, clockwise: true).fill()
    }


    let path = UIBezierPath()

    path.move(to: from)
    path.addCurve(to: middlePoints.near,
                  controlPoint1: borderControlPoints.start,
                  controlPoint2: nearMidControlPoints.start)
    path.addCurve(to: to,
                  controlPoint1: nearMidControlPoints.end,
                  controlPoint2: borderControlPoints.end)

    path.addCurve(to: middlePoints.far,
                  controlPoint1: borderControlPoints.end,
                  controlPoint2: farMidControlPoints.start)
    path.addCurve(to: from,
                  controlPoint1: farMidControlPoints.end,
                  controlPoint2: borderControlPoints.start)

    path.close()
    return path
}

I extended CGPoint a bit just for readability. 我只是为了提高可读性而扩展了CGPoint Some of these methods may not make sense in general so I would not expose it more then fileprivate . 这些方法中的某些方法通常可能没有任何意义,因此,我不会像fileprivate那样公开它。

fileprivate extension CGPoint {

    var length: CGFloat { return sqrt(x*x + y*y) }
    var normal: CGPoint { return CGPoint(x: y, y: -x) }
    func scaled(by factor: CGFloat) -> CGPoint { return CGPoint(x: x*factor, y: y*factor) }
    func adding(_ point: CGPoint) -> CGPoint { return CGPoint(x: x+point.x, y: y+point.y) }
    func subtracting(_ point: CGPoint) -> CGPoint { return CGPoint(x: x-point.x, y: y-point.y) }
    func direction(toward point: CGPoint) -> CGPoint { return point.subtracting(self) }
    var normalized: CGPoint {
        let distance = length
        return distance > 0.0 ? scaled(by: 1.0/distance) : .zero
    }

}

There are 2 factors at the start of the method which may nicely control the shape, you can play with them (I added 2 sliders with values [0.0, 2.0] to style it). 该方法开始时有两个因素可以很好地控制形状,您可以使用它们(我添加了两个带有[0.0, 2.0]值的滑块来设置其样式)。 Also I left debug part in it which is really helpful when positioning control points. 另外,我在其中保留了调试部分,这对定位控制点很有帮助。

It would be very nice to also have rounded corners but from the current code I am not sure I would be able to achieve that. 圆角也很好,但是根据当前代码,我不确定是否能够实现。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM