為什么不用二次曲線
- 二次曲線的兩端的方向不易控制督暂, 2段曲線中間點(diǎn)容易不圓滑
如何用三次曲線解決以上問題
通過三次曲線的2個(gè)控制點(diǎn)控制結(jié)束點(diǎn)曲線的方向
每2段曲線之間的點(diǎn)的控制點(diǎn)與該點(diǎn)在同一條直線上
控制點(diǎn)計(jì)算,每段三次曲線需要2個(gè)控制點(diǎn)焦影,第一個(gè)控制點(diǎn)control1宣增、上一段曲線的control2玫膀、2段曲線的交點(diǎn) 保持在同一條直線上,這條直線如何確定
曲線 A-B-C-D-E: A-B 曲線 AB.control1 在AB的連接線上爹脾,AB.control2帖旨、 BC.control1、點(diǎn)B在一條直線上灵妨, 做角ABC 的角平分線 BM, 做BM的垂線 BN, BN可以作為AB.control2解阅、 BC.control1所在直線,至于控制點(diǎn)距離點(diǎn)B的距離可以取AB泌霍、BC的1/3(沒有固定的數(shù)字货抄,可以根據(jù)需要調(diào)整);以此類推,分別求出中間每段曲線的2個(gè)控制點(diǎn)碉熄, 最后一段曲線DE的control2則在DE的連接線上桨武。
- 效果圖:
綠色圓圈為采樣點(diǎn),紅色小圓點(diǎn)為control1锈津, 紅色方塊為control2
- 代碼
struct XPoint {
var time: TimeInterval
var point: CGPoint
init(time: TimeInterval, point: CGPoint) {
self.time = time
self.point = point
}
}
/// 最后的線段長度為0呀酸,每3個(gè)點(diǎn)A, O, B, 做角AOB的角平分線OM,在經(jīng)過點(diǎn)O做與OM的垂線琼梆,射線方向與 AB方向一致性誉, 求出該射線的單位向量
struct XCurvePoint {
/// 點(diǎn) O
var point: CGPoint
var vector: CGPoint
/// point 到下一個(gè)點(diǎn)的直線長度
var length: CGFloat
var duration: TimeInterval
init(point: CGPoint, vector: CGPoint, length: CGFloat, duration: TimeInterval) {
self.point = point
self.vector = vector
self.length = length
self.duration = duration
}
}
struct XCurve {
struct Item {
var end: CGPoint
var control1: CGPoint
var control2: CGPoint
init(end: CGPoint, control1: CGPoint, control2: CGPoint) {
self.end = end
self.control1 = control1
self.control2 = control2
}
}
var start: CGPoint
var items: [Item] = []
init(start: CGPoint) {
self.start = start
}
mutating func appenCurve(to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint) {
self.items.append(Item(end: endPoint, control1: controlPoint1, control2: controlPoint2))
}
}
public final class CGUtil : NSObject {
public static func pointToRect(_ point: CGPoint, size: CGFloat) -> CGRect {
return CGRect(x: point.x - size / 2, y: point.y - size / 2, width: size, height: size)
}
public static func vectorAdd(start: CGPoint, end: CGPoint) -> (CGPoint, CGFloat) {
let dx = end.x - start.x
let dy = end.y - start.y
let len = sqrt(dx * dx + dy * dy)
let scale = 1.0 / len
return (CGPoint(x: dx * scale, y: dy * scale), len)
}
// 返回start->end 的單位向量,長度
public static func vectorLength(start: CGPoint, end: CGPoint) -> (CGPoint, CGFloat) {
let dx = end.x - start.x
let dy = end.y - start.y
let len = sqrt(dx * dx + dy * dy)
let scale = 1.0 / len
return (CGPoint(x: dx * scale, y: dy * scale), len)
}
// 根據(jù)dx茎杂、dy生成單位向量
public static func vector(dx: CGFloat, dy: CGFloat) -> CGPoint {
let len = sqrt(dx * dx + dy * dy)
let scale = 1.0 / len
return CGPoint(x: dx * scale, y: dy * scale)
}
/// 返回 角start-point-end的角平分線的垂線(方向與start->end 方向一致) 的單位向量 和 point-end之間的長度
public static func vector(start: CGPoint, end: CGPoint, point: CGPoint) -> (CGPoint, CGFloat) {
let a = vectorLength(start: start, end: point)
let b = vectorLength(start: point, end: end)
return (vector(dx: b.0.x + a.0.x, dy: b.0.y + a.0.y), b.1)
}
// point + vector * length
public static func pointAdd(point: CGPoint, vector: CGPoint, length: CGFloat) -> CGPoint {
return CGPoint(x: point.x + vector.x * length, y: point.y + vector.y * length)
}
// point - vector * length
public static func pointSubtract(point: CGPoint, vector: CGPoint, length: CGFloat) -> CGPoint {
return CGPoint(x: point.x - vector.x * length, y: point.y - vector.y * length)
}
}
class XPath {
let needFill: Bool
let points: UIBezierPath
let controlPoints: UIBezierPath
let path: UIBezierPath
init(curve: XCurve) {
let points: UIBezierPath = UIBezierPath()
let controlPoints: UIBezierPath = UIBezierPath()
let path: UIBezierPath = UIBezierPath()
path.lineWidth = 2
points.lineWidth = 2
controlPoints.lineWidth = 2
if curve.items.isEmpty {
self.needFill = true
path.move(to: curve.start)
points.append(UIBezierPath(ovalIn: CGUtil.pointToRect(curve.start, size: 4)))
path.append(UIBezierPath(ovalIn: CGUtil.pointToRect(curve.start, size: 2)))
} else {
self.needFill = false
path.move(to: curve.start)
points.append(UIBezierPath(ovalIn: CGUtil.pointToRect(curve.start, size: 4)))
curve.items.forEach { item in
path.addCurve(to: item.end, controlPoint1: item.control1, controlPoint2: item.control2)
controlPoints.append(UIBezierPath(ovalIn: CGUtil.pointToRect(item.control1, size: 3)))
controlPoints.append(UIBezierPath(rect: CGUtil.pointToRect(item.control2, size: 3)))
points.append(UIBezierPath(ovalIn: CGUtil.pointToRect(item.end, size: 4)))
}
}
self.path = path
self.controlPoints = controlPoints
self.points = points
}
func draw(context: CGContext, rect: CGRect) {
context.setStrokeColor(UIColor.green.cgColor)
context.addPath(self.points.cgPath)
context.strokePath()
context.setFillColor(UIColor.red.cgColor)
context.addPath(self.controlPoints.cgPath)
context.fillPath()
context.setStrokeColor(UIColor.blue.cgColor)
context.setFillColor(UIColor.blue.cgColor)
context.addPath(self.path.cgPath)
context.strokePath()
if self.needFill {
context.fillPath()
}
}
}
class XPathBuilder {
private var points: [XPoint] = []
func append(_ point: XPoint) {
self.points.append(point)
}
func finish() ->XPath? {
guard !self.points.isEmpty else {
return nil
}
guard self.points.count >= 2 else {
let start = self.points[0].point
return XPath(curve: XCurve(start: start))
}
var prev = CGPoint(x: CGFloat.nan, y: CGFloat.nan)
let lines = self.points.filter { point in
if point.point == prev {
return false
} else {
prev = point.point
return true
}
}.enumerated().map({ (idx, point) in
var v: (CGPoint, CGFloat)
var duration: TimeInterval = 0
if idx == 0 {
let next = self.points[idx + 1]
v = CGUtil.vectorLength(start: point.point, end: next.point)
duration = next.time - point.time
} else {
let prev = self.points[idx - 1]
if idx == self.points.count - 1 {
v = CGUtil.vectorLength(start: prev.point, end: point.point)
v.1 = 0
duration = 1000
} else {
let next = self.points[idx + 1]
v = CGUtil.vector(start: prev.point, end: next.point, point: point.point)
duration = next.time - point.time
}
}
return XCurvePoint(point: point.point, vector: v.0, length: v.1, duration: duration)
})
var curve = XCurve(start: lines[0].point)
for i in 1 ..< lines.count {
let from = lines[i - 1]
let to = lines[i]
u
let control1 = CGUtil.pointAdd(point: from.point, vector: from.vector, length: from.length / 4)
let control2 = CGUtil.pointSubtract(point: to.point, vector: to.vector, length: from.length / 4)
curve.appenCurve(to: to.point, controlPoint1: control1, controlPoint2: control2)
}
return XPath(curve: curve)
}
}
class XDrawingView : UIView {
var paths: [XPath] = []
var pathBuilder: XPathBuilder? = nil
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
self.pathBuilder = XPathBuilder()
if let v = event?.touches(for: self)?.first {
self.handleDraw(v)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
if let v = event?.touches(for: self)?.first {
self.handleDraw(v)
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
self.finish()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
self.finish()
}
func finish() {
if let pathBuilder = self.pathBuilder {
if let path = pathBuilder.finish() {
self.paths.append(path)
self.setNeedsDisplay()
}
self.pathBuilder = nil
}
}
func makePoint(_ touch: UITouch) -> XPoint {
return XPoint(time: touch.timestamp, point: touch.preciseLocation(in: self))
}
func handleDraw(_ touch: UITouch) {
let point = self.makePoint(touch)
if let path = self.pathBuilder {
path.append(point)
}
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.setFillColor(UIColor.white.cgColor)
context.fill([rect])
self.paths.forEach { path in
path.draw(context: context, rect: rect)
}
}
}