Draw circle with ten or more control points in swift

Viewed 534

I am trying to understand how to achieve kind of circle with more than ten control points like in this video, which can be adjusted to any shape and implemented in swift language.

I have found javascript similar effects, but I don’t know how to start. I also tried to use the Bezier path implementation, the code is as follows, but I don't know how to complete it.

class MyBezierPathView: UIView {
    
    private var path: UIBezierPath?
    
    // start point
    var startP = CGPoint.zero
    
    // end point
    var endP = CGPoint.zero
    
    // control point
    var controlP = CGPoint.zero
    
    var pathColor: UIColor?
     
    var pathWidth: CGFloat = 0.0
    
    // current touch point
    private var currentTouchP = 0
    
    // init
    override init(frame: CGRect) {

        super.init(frame: frame)
    }
    
    // draw BezierPath
    override func draw(_ rect: CGRect) {

        path = UIBezierPath()
        path?.move(to: startP)

        path?.addQuadCurve(to: endP, controlPoint: controlP)

        path?.lineWidth = pathWidth
        pathColor?.setStroke()
        path?.stroke()

        path = UIBezierPath()

        path?.lineWidth = 1
        UIColor.gray.setStroke()
        
        let lengths: [CGFloat] = [5]
        path?.setLineDash(lengths, count: 1, phase: 1)
        path?.move(to: controlP)
        path?.addLine(to: startP)
        path?.stroke()

        path?.move(to: controlP)
        path?.addLine(to: endP)
        path?.stroke()
    
        
        path = UIBezierPath(arcCenter: startP, radius: 4, startAngle: 0, endAngle: .pi * 2, clockwise: true)
        UIColor.black.setStroke()
        path?.fill()
        
        path = UIBezierPath(arcCenter: endP, radius: 4, startAngle: 0, endAngle: .pi * 2, clockwise: true)
        UIColor.black.setStroke()
        path?.fill()
        
        path = UIBezierPath(arcCenter: controlP, radius: 3, startAngle: 0, endAngle: .pi * 2, clockwise: true)
        path?.lineWidth = 2
        UIColor.black.setStroke()
        path?.stroke()
        
        let startMsgRect = CGRect(x: startP.x + 8, y: startP.y - 7, width: 50, height: 20)
        "start point".draw(in: startMsgRect, withAttributes: nil)

        let endMsgRect = CGRect(x: endP.x + 8, y: endP.y - 7, width: 50, height: 20)
        "end point".draw(in: endMsgRect, withAttributes: nil)

        let control1MsgRect = CGRect(x: controlP.x + 8, y: controlP.y - 7, width: 50, height: 20)
        "control point".draw(in: control1MsgRect, withAttributes: nil)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        let startPoint = touches.first?.location(in: self)     
        
        let startR = CGRect(x: startP.x - 4, y: startP.y - 4, width: 80, height: 80)
        let endR = CGRect(x: endP.x - 4, y: endP.y - 4, width: 80, height: 80)
        let controlR = CGRect(x: controlP.x - 4, y: controlP.y - 4, width: 80, height: 80)
        
        guard let startPoint = startPoint else {
            print("startPoint is nil.")
            return
        }
        
        if startR.contains(startPoint) {
            currentTouchP = 1
        } else if endR.contains(startPoint) {
            currentTouchP = 2
        } else if controlR.contains(startPoint) {
            currentTouchP = 3
        }
    }
    

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
       
        var touchPoint = touches.first?.location(in: self)
        
        
        if touchPoint!.x < 0 {
           touchPoint!.x = 0
        }

        if touchPoint!.x > bounds.size.width {
            touchPoint!.x = bounds.size.width
        }
        if touchPoint!.y < 0 {
            touchPoint!.y = 0
        }

        if touchPoint!.y > bounds.size.height {
            touchPoint!.y = bounds.size.height
        }
        
        switch currentTouchP {
        case 1:
            startP = touchPoint!
        case 2:
            endP = touchPoint!
        case 3:
            controlP = touchPoint!
        default:
            break
        }
               
        setNeedsDisplay()
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        currentTouchP = 0
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        touchesEnded(touches, with: event)
    }
    
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
}
class ViewController: UIViewController {

    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let frame = CGRect(x: 0, y: 0, width: view.bounds.size.width, height: view.bounds.size.height)

        let pathView = MyBezierPathView(frame: frame)
        
        pathView.startP = CGPoint(x: 110, y: 150)
        pathView.endP = CGPoint(x: 258.47, y: 211.53)
        pathView.controlP = CGPoint(x: 196.94, y: 150)
        pathView.pathColor = #colorLiteral(red: 1, green: 0.1491314173, blue: 0, alpha: 1)
        pathView.pathWidth = 2

        pathView.backgroundColor = UIColor.white
        pathView.layer.borderWidth = 1

        view.addSubview(pathView)
        
    }
}
1 Answers

One way you could approach this problem

  • create a circle from several curve segments (using addQuadCurve() or addCurve() of UIBezierPath class)
  • addQuadCurve() adds a curve with one control point while addCurve() adds a curve with 2 control points (the video you showed seems using paths with 2 control points, so it would be better using addCurve())
  • Then user needs to be able to move any of start/end and control points of these curves.
  • For each these change, you have to redraw the curves

I have created a sample playground with this idea. In this playground, I have created a red circle (not a perfect circle) by four curves using addQuadCurve(). This circle has 8 points you could use to alter the shape. If you use 4 curves with addCurve(), then you will have 12 points to alter the shape.

Then I changed a single point of the red circle and added the updated shape in green color below the original red circle.

import UIKit
import PlaygroundSupport

let container = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 700))

let view1 = UIView(frame: CGRect(x: 50, y: 50, width: 500, height: 350))

let layer1 = CAShapeLayer()
layer1.fillColor = UIColor.clear.cgColor
layer1.lineWidth = 5
layer1.strokeColor = UIColor.red.cgColor

//create a circle wich has 8 points to change it's shape (4 control points and 4 start/end points of curves)
let originalPath = UIBezierPath()
originalPath.move(to: CGPoint(x: 100, y: 0))
originalPath.addQuadCurve(to: CGPoint(x: 200, y: 100), controlPoint: CGPoint(x: 190, y: 10))
originalPath.addQuadCurve(to: CGPoint(x: 100, y: 200), controlPoint: CGPoint(x: 190, y: 190))
originalPath.addQuadCurve(to: CGPoint(x: 0, y: 100), controlPoint: CGPoint(x: 10, y: 190))
originalPath.addQuadCurve(to: CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 10, y: 10))

//add this path to the layer1
layer1.path = originalPath.cgPath


//suppose user move the CGPoint(x: 200, y: 100) to CGPoint(x: 220, y: 100)
//then we can redraw the 4 curves again

let view2 = UIView(frame: CGRect(x: 50, y: 350, width: 500, height: 350))

let layer2 = CAShapeLayer()
layer2.fillColor = UIColor.clear.cgColor
layer2.lineWidth = 5
layer2.strokeColor = UIColor.green.cgColor

//changedPath is almost same as originalPath except CGPoint(x: 250, y: 100)
let changedPath = UIBezierPath()
changedPath.move(to: CGPoint(x: 100, y: 0))
changedPath.addQuadCurve(to: CGPoint(x: 250, y: 100), controlPoint: CGPoint(x: 190, y: 10)) // <---- user has moved point CGPoint(x: 200, y: 100) to CGPoint(x: 250, y: 100). So add this curve to the new point
changedPath.addQuadCurve(to: CGPoint(x: 100, y: 200), controlPoint: CGPoint(x: 190, y: 190))
changedPath.addQuadCurve(to: CGPoint(x: 0, y: 100), controlPoint: CGPoint(x: 10, y: 190))
changedPath.addQuadCurve(to: CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 10, y: 10))

//adding changed path to layer2
layer2.path = changedPath.cgPath

view1.layer.addSublayer(layer1)
view2.layer.addSublayer(layer2)

container.addSubview(view1)
container.addSubview(view2)

PlaygroundPage.current.liveView = container
Related