bugfix> ios > 投稿

Swift 4でCAShapeLayerとBezierPathを使用して、2つのポイント(ポイントAとポイントB)が与えられた場合、下の図に示すような曲線をどのようにレンダリングできるのか疑問に思いました。

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

回答 1 件
  • 数学を適用する必要があります。私の見方では、これは異なる半径の2つの円弧で構成されています。 2つのポイントからそれらを計算するのは非常に難しいかもしれませんが、幸いなことに、すでにこれを行うためのarc用のツールがあります。メソッド addQuadCurve   UIBezierPath で  これにぴったりのようです。

    2点を入力する必要があります。これは、弧がどの程度バインドされているかと、線の太さの要因です。ベンドファクターを使用して、ケースの2つのポイントの中心からコントロールポイントをどれだけ下に移動するかを決定します。下向きは2つのポイントに関連する可能性があるので、むしろ normal を使用しましょう 。私が得たのは次のとおりです:

    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
    }
    
    

    あなたのリクエストに似た形状を取得するために私が使用したデータは( 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()
    }
    
    

    負のベンド係数は下向きに曲がることを意味し、残りはかなり直感的です。

    編集 4つの曲線から形状を構成することにより、さらに多くの制御を実現できます。

    4つの曲線を使用すると、より良い形状を作成できますが、コードは非常に複雑になります。これは私があなたが望むものに近い形を取得しようとしたものです:

    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
    }
    
    

    CGPoint を拡張しました  読みやすさのためだけに。これらのメソッドの一部は一般的に意味をなさない可能性があるため、 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
        }
    }
    
    

    メソッドの開始時に形状を適切に制御できる2つの要素があり、それらを試すことができます(値 [0.0, 2.0] の2つのスライダーを追加しました  それをスタイルする)。また、デバッグポイントを残しました。これは、コントロールポイントを配置するときに非常に役立ちます。

    角を丸くすることも非常に良いことですが、現在のコードからはそれを達成できるかどうかわかりません。

あなたの答え