为什么不用二次曲线
- 二次曲线的两端的方向不易控制, 2段曲线中间点容易不圆滑
如何用三次曲线解决以上问题
-
通过三次曲线的2个控制点控制结束点曲线的方向
-
每2段曲线之间的点的控制点与该点在同一条直线上
-
控制点计算,每段三次曲线需要2个控制点,第一个控制点control1、上一段曲线的control2、2段曲线的交点 保持在同一条直线上,这条直线如何确定
曲线 A-B-C-D-E: A-B 曲线 AB.control1 在AB的连接线上,AB.control2、 BC.control1、点B在一条直线上, 做角ABC 的角平分线 BM, 做BM的垂线 BN, BN可以作为AB.control2、 BC.control1所在直线,至于控制点距离点B的距离可以取AB、BC的1/3(没有固定的数字,可以根据需要调整);以此类推,分别求出中间每段曲线的2个控制点, 最后一段曲线DE的control2则在DE的连接线上。
- 效果图:
绿色圆圈为采样点,红色小圆点为control1, 红色方块为control2
Simulator Screen Shot - iPhone 14 Pro Max - 2024-06-13 at 14.31.46.png
- 代码
struct XPoint {
var time: TimeInterval
var point: CGPoint
init(time: TimeInterval, point: CGPoint) {
self.time = time
self.point = point
}
}
/// 最后的线段长度为0,每3个点A, O, B, 做角AOB的角平分线OM,在经过点O做与OM的垂线,射线方向与 AB方向一致, 求出该射线的单位向量
struct XCurvePoint {
/// 点 O
var point: CGPoint
var vector: CGPoint
/// point 到下一个点的直线长度
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)
}
// 根据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)
}
}
}













网友评论