Imbue
Imbue

Reputation: 429

Creating a curly bracket curve from two points

I'm trying to create a curly bracket in Swift, from two points. The idea works fine, with a straight line, because it's currently not dynamic in anyway. My issue lies in finding the dynamic control points and center depending on the location of p1 and p2 points.

This is my current code:

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
}

This doesen't take the degrees of the points into account, so if I were to create the two points as:

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

It would not find the proper control points and midpoint.

Hope someone can help me in the right direction. The idea is of course in the end to just know the two points (p1, p2) and dynamically create every other points, I just typed in values for the moment, to make it easier for myself. I've added images of the issue to better show you. enter image description here enter image description here

Upvotes: 0

Views: 604

Answers (2)

rob mayoff
rob mayoff

Reputation: 385860

First create a path for a brace that starts at (0, 0) and ends at (1, 0). Then apply an affine transformation that moves, scales, and rotates the path to span your designed endpoints. It needs to transform (0, 0) to your start point and (1, 0) to your end point. Creating the transformation efficiently requires some trigonometry, but I've done the homework for you:

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
    }

}

Result:

interactive brace demo

Here's the entire Swift playground I used to make the demo:

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

Upvotes: 1

Aerows
Aerows

Reputation: 760

The key to the problem is when the figure is rotated your base vectors will rotate. When your figure is axis-aligned your base vectors are u (1, 0) and v (0, 1).

So when you are performing midPoint.y -= 20 you can see it as the same as midPoint.x -= v.x * 20; midPoint.y -= v.y * 20 where v is (0, 1). The results are the same, check for yourself.

This implementation will do what your code does, only 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))

Note: I set the factors to match the initial values in your implementation.

Also added this CGPoint extension for convenience. Hope it helps.

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

Upvotes: 0

Related Questions