計(jì)算三次貝塞爾曲線控制點(diǎn)的一種方法

為什么不用二次曲線

  • 二次曲線的兩端的方向不易控制督暂, 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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末错览,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子煌往,更是在濱河造成了極大的恐慌倾哺,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,835評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刽脖,死亡現(xiàn)場(chǎng)離奇詭異羞海,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)曲管,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,900評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門却邓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人院水,你說我怎么就攤上這事腊徙。” “怎么了檬某?”我有些...
    開封第一講書人閱讀 156,481評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵撬腾,是天一觀的道長。 經(jīng)常有香客問我恢恼,道長时鸵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,303評(píng)論 1 282
  • 正文 為了忘掉前任厅瞎,我火速辦了婚禮,結(jié)果婚禮上初坠,老公的妹妹穿的比我還像新娘和簸。我一直安慰自己,他們只是感情好碟刺,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,375評(píng)論 5 384
  • 文/花漫 我一把揭開白布锁保。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪爽柒。 梳的紋絲不亂的頭發(fā)上吴菠,一...
    開封第一講書人閱讀 49,729評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音浩村,去河邊找鬼做葵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛心墅,可吹牛的內(nèi)容都是我干的酿矢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,877評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼怎燥,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼瘫筐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起铐姚,我...
    開封第一講書人閱讀 37,633評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤策肝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后隐绵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體之众,經(jīng)...
    沈念sama閱讀 44,088評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,443評(píng)論 2 326
  • 正文 我和宋清朗相戀三年氢橙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了酝枢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,563評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡悍手,死狀恐怖帘睦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情坦康,我是刑警寧澤竣付,帶...
    沈念sama閱讀 34,251評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站滞欠,受9級(jí)特大地震影響古胆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜筛璧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,827評(píng)論 3 312
  • 文/蒙蒙 一逸绎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧夭谤,春花似錦棺牧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,712評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽参淹。三九已至,卻和暖如春乏悄,著一層夾襖步出監(jiān)牢的瞬間浙值,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,943評(píng)論 1 264
  • 我被黑心中介騙來泰國打工檩小, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留开呐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,240評(píng)論 2 360
  • 正文 我出身青樓识啦,卻偏偏與公主長得像负蚊,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子颓哮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,435評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容