在 swift 中绘制具有十个或更多控制点的圆
Draw circle with ten or more control points in swift
我正在尝试了解如何实现像 this video 中那样具有十多个控制点的圆形,它可以调整为任何形状并用 swift 语言实现。
我有foundjavascript类似的效果,但不知如何下手。我也试过用贝塞尔路径实现,代码如下,但是不知道怎么完成。
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)
}
}
解决此问题的一种方法
- 从几个曲线段创建一个圆(使用
addQuadCurve()
或 addCurve()
of UIBezierPath
class)
addQuadCurve()
添加一个 control point
的曲线,而 addCurve()
添加一个 2 control points
的曲线(您显示的视频似乎使用 2 [=17= 的路径], 所以最好使用 addCurve()
)
- 然后用户需要能够移动这些曲线中的任何
start/end
和 control points
。
- 对于每一个这些变化,您都必须重新绘制曲线
我用这个想法创建了一个示例游乐场。在这个操场上,我使用 addQuadCurve()
通过四条曲线创建了一个红色圆圈(不是完美的圆圈)。这个圆有 8 个点,您可以使用它来改变形状。如果您使用 4 条曲线 addCurve()
,那么您将有 12 个点来改变形状。
然后我改变了红色圆圈的一个点,并在原来的红色圆圈下方添加了绿色的更新形状。
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
我正在尝试了解如何实现像 this video 中那样具有十多个控制点的圆形,它可以调整为任何形状并用 swift 语言实现。
我有foundjavascript类似的效果,但不知如何下手。我也试过用贝塞尔路径实现,代码如下,但是不知道怎么完成。
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)
}
}
解决此问题的一种方法
- 从几个曲线段创建一个圆(使用
addQuadCurve()
或addCurve()
ofUIBezierPath
class) addQuadCurve()
添加一个control point
的曲线,而addCurve()
添加一个 2control points
的曲线(您显示的视频似乎使用 2 [=17= 的路径], 所以最好使用addCurve()
)- 然后用户需要能够移动这些曲线中的任何
start/end
和control points
。 - 对于每一个这些变化,您都必须重新绘制曲线
我用这个想法创建了一个示例游乐场。在这个操场上,我使用 addQuadCurve()
通过四条曲线创建了一个红色圆圈(不是完美的圆圈)。这个圆有 8 个点,您可以使用它来改变形状。如果您使用 4 条曲线 addCurve()
,那么您将有 12 个点来改变形状。
然后我改变了红色圆圈的一个点,并在原来的红色圆圈下方添加了绿色的更新形状。
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