Создание кривой фигурных скобок из двух точек

Я пытаюсь создать фигурку в Swift из двух точек. Идея работает отлично, с прямой линией, потому что в настоящее время она не динамична. Моя проблема заключается в поиске динамических контрольных точек и центра в зависимости от местоположения точек p1 и p2.

Это мой текущий код:

override func viewDidLoad() {
    super.viewDidLoad()

    let path = UIBezierPath()

    let p1 = CGPointMake(100, 100)
    let p2 = CGPointMake(300, 100)

    let c1 = CGPointMake(150, 80)
    let c2 = CGPointMake(250, 80)

    var midPoint = midPointForPoints(p1, p2: p2)

    var midP1 = midPoint
    midP1.x -= 10

    var midP2 = midPoint
    midP2.x += 10

    midPoint.y -= 20

    path.moveToPoint(p1)
    path.addQuadCurveToPoint(midP1, controlPoint: c1)
    path.addLineToPoint(midPoint)
    path.addLineToPoint(midP2)
    path.addQuadCurveToPoint(p2, controlPoint: c2)

    let shape = CAShapeLayer()
    shape.lineWidth = 5
    shape.strokeColor = UIColor.redColor().CGColor
    shape.fillColor = UIColor.clearColor().CGColor
    shape.path = path.CGPath

    self.view.layer.addSublayer(shape)

}


func midPointForPoints(p1: CGPoint, p2: CGPoint)->CGPoint{
    let deltaX = (p1.x + p2.x)/2
    let deltaY = (p1.y + p2.y)/2

    let midPoint = CGPointMake(deltaX, deltaY)

    return midPoint
}

Это не учитывает степени точек, поэтому, если бы я создал две точки:

let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 300)

Он не найдет правильные контрольные точки и среднюю точку.

Надеюсь, кто-то может помочь мне в правильном направлении. Идея, конечно же, заключается в том, чтобы просто узнать две точки (p1, p2) и динамически создавать все остальные точки, я просто набрал значения на данный момент, чтобы облегчить для себя. Я добавил изображения проблемы, чтобы лучше показать вам. введите описание изображения здесь введите описание изображения здесь

Ответ 1

Ключом к проблеме является поворот фигуры, ваш base vectors будет вращаться. Когда ваш figure выровнен по оси, ваши базовые векторы u (1, 0) и v (0, 1).

Итак, когда вы выполняете midPoint.y -= 20, вы можете видеть его таким же, как midPoint.x -= v.x * 20; midPoint.y -= v.y * 20, где v есть (0, 1). Результаты те же, проверьте сами.

Эта реализация будет делать то, что делает ваш код, только axis independent.

let path = UIBezierPath()

let p1 = CGPointMake(100, 100)  
let p2 = CGPointMake(300, 100)  

let o = p1.plus(p2).divide(2.0) // origo
let u = p2.minus(o)             // base vector 1
let v = u.turn90()              // base vector 2

let c1 = o.minus(u.times(0.5)).minus(v.times(0.2))  // CGPointMake(150, 80)
let c2 = o.plus(u.times(0.5)).minus(v.times(0.2))   // CGPointMake(250, 80)

var midPoint = o.minus(v.times(0.2))

var midP1 = o.minus(u.times(0.2))
var midP2 = o.plus(u.times(0.2))

Примечание. Я устанавливаю факторы, соответствующие исходным значениям в вашей реализации.

Также добавлено это CGPoint extension для удобства. Надеюсь, что это поможет.

extension CGPoint {
    public func plus(p: CGPoint) -> (CGPoint)
    {
        return CGPoint(x: self.x + p.x, y: self.y + p.y)
    }
    public func minus(p: CGPoint) -> (CGPoint)
    {
        return CGPoint(x: self.x - p.x, y: self.y - p.y)
    }
    public func times(f: CGFloat) -> (CGPoint)
    {
        return CGPoint(x: self.x * f, y: self.y * f)
    }
    public func divide(f: CGFloat) -> (CGPoint)
    {
        return self.times(1.0/f)
    }
    public func turn90() -> (CGPoint)
    {
        return CGPoint(x: -self.y, y: x)
    }
}

Ответ 2

Сначала создайте путь для фигурной скобки, которая начинается с (0, 0) и заканчивается на (1, 0). Затем примените аффинное преобразование, которое перемещает, масштабирует и вращает путь, чтобы охватить ваши разработанные конечные точки. Он должен преобразовать (0, 0) в начальную точку и (1, 0) в конечную точку. Для эффективного создания преобразования требуется некоторая тригонометрия, но я сделал домашнее задание для вас:

extension UIBezierPath {

    class func brace(from start: CGPoint, to end: CGPoint) -> UIBezierPath {
        let path = self.init()
        path.move(to: .zero)
        path.addCurve(to: CGPoint(x: 0.5, y: -0.1), controlPoint1: CGPoint(x: 0, y: -0.2), controlPoint2: CGPoint(x: 0.5, y: 0.1))
        path.addCurve(to: CGPoint(x: 1, y: 0), controlPoint1: CGPoint(x: 0.5, y: 0.1), controlPoint2: CGPoint(x: 1, y: -0.2))

        let scaledCosine = end.x - start.x
        let scaledSine = end.y - start.y
        let transform = CGAffineTransform(a: scaledCosine, b: scaledSine, c: -scaledSine, d: scaledCosine, tx: start.x, ty: start.y)
        path.apply(transform)
        return path
    }

}

Результат:

интерактивная демонстрация брекетов

Здесь вся игровая площадка Swift, которую я использовал для создания демо:

import UIKit
import PlaygroundSupport

extension UIBezierPath {

    class func brace(from start: CGPoint, to end: CGPoint) -> UIBezierPath {
        let path = self.init()
        path.move(to: .zero)
        path.addCurve(to: CGPoint(x: 0.5, y: -0.1), controlPoint1: CGPoint(x: 0, y: -0.2), controlPoint2: CGPoint(x: 0.5, y: 0.1))
        path.addCurve(to: CGPoint(x: 1, y: 0), controlPoint1: CGPoint(x: 0.5, y: 0.1), controlPoint2: CGPoint(x: 1, y: -0.2))

        let scaledCosine = end.x - start.x
        let scaledSine = end.y - start.y
        let transform = CGAffineTransform(a: scaledCosine, b: scaledSine, c: -scaledSine, d: scaledCosine, tx: start.x, ty: start.y)
        path.apply(transform)
        return path
    }

}

class ShapeView: UIView {

    override class var layerClass: Swift.AnyClass { return CAShapeLayer.self }

    lazy var shapeLayer: CAShapeLayer = { self.layer as! CAShapeLayer }()

}

class ViewController: UIViewController {

    override func loadView() {
        let view = UIView(frame: CGRect(x: 0, y: 0, width: 600, height: 200))
        view.backgroundColor = .white

        for (i, handle) in handles.enumerated() {
            handle.autoresizingMask = [ .flexibleTopMargin, .flexibleTopMargin, .flexibleBottomMargin, .flexibleRightMargin ]
            let frame = CGRect(x: view.bounds.width * 0.1 + CGFloat(i) * view.bounds.width * 0.8 - 22, y: view.bounds.height / 2 - 22, width: 44, height: 44)
            handle.frame = frame
            handle.shapeLayer.path = CGPath(ellipseIn: handle.bounds, transform: nil)
            handle.shapeLayer.lineWidth = 2
            handle.shapeLayer.lineDashPattern = [2, 6]
            handle.shapeLayer.lineCap = kCALineCapRound
            handle.shapeLayer.strokeColor = UIColor.blue.cgColor
            handle.shapeLayer.fillColor = nil
            view.addSubview(handle)

            let panner = UIPanGestureRecognizer(target: self, action: #selector(pannerDidFire(panner:)))
            handle.addGestureRecognizer(panner)
        }

        brace.shapeLayer.lineWidth = 2
        brace.shapeLayer.lineCap = kCALineCapRound
        brace.shapeLayer.strokeColor = UIColor.black.cgColor
        brace.shapeLayer.fillColor = nil
        view.addSubview(brace)
        setBracePath()

        self.view = view
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        setBracePath()
    }

    private let handles: [ShapeView] = [
        ShapeView(),
        ShapeView()
    ]

    private let brace = ShapeView()

    private func setBracePath() {
        brace.shapeLayer.path = UIBezierPath.brace(from: handles[0].center, to: handles[1].center).cgPath
    }

    @objc private func pannerDidFire(panner: UIPanGestureRecognizer) {
        let view = panner.view!
        let offset = panner.translation(in: view)
        panner.setTranslation(.zero, in: view)
        var center = view.center
        center.x += offset.x
        center.y += offset.y
        view.center = center
        setBracePath()
    }
}

let vc = ViewController()
PlaygroundPage.current.liveView = vc.view