貝塞爾曲線(xiàn)原理及在iOS中使用介紹

貝塞爾曲線(xiàn)是指可以通過(guò)一些控制點(diǎn)去控制曲線(xiàn)的形狀并且保持曲線(xiàn)的平滑特性芥备,不會(huì)讓人感覺(jué)到突兀冬耿。在iOS開(kāi)發(fā)中,貝塞爾曲線(xiàn)的使用主要通過(guò)UIKit中的UIBezierPath類(lèi)萌壳,這個(gè)類(lèi)可以使用貝塞爾曲線(xiàn)和直線(xiàn)實(shí)現(xiàn)一些復(fù)雜的圖形結(jié)構(gòu)亦镶。本文會(huì)介紹貝塞爾曲線(xiàn)的原理和使用UIBezierPath繪制貝塞爾曲線(xiàn),文末會(huì)給兩個(gè)使用UIBezierPath實(shí)現(xiàn)的Demo袱瓮。

原文地址快捷方式 -->

貝塞爾曲線(xiàn)簡(jiǎn)介

歷史沿革 貝塞爾曲線(xiàn)(Bézier curve)又稱(chēng)曲線(xiàn)或貝濟(jì)埃曲線(xiàn)缤骨,是計(jì)算機(jī)圖形學(xué)中相當(dāng)重要的參數(shù)曲線(xiàn),它包含了大量的數(shù)學(xué)證明幾何推理尺借,經(jīng)過(guò)近半個(gè)世紀(jì)發(fā)展之后绊起,終于在1962年于經(jīng)就職于雷諾的法國(guó)工程師皮埃爾·貝塞爾(Pierre Bézier)在汽車(chē)車(chē)體工業(yè)設(shè)計(jì)中的成功實(shí)踐后廣泛宣傳推廣,之后被大家稱(chēng)之為貝塞爾曲線(xiàn)燎斩,貝塞爾可以說(shuō)是理論聯(lián)系實(shí)際的踐行者虱歪。當(dāng)然,貝塞爾曲線(xiàn)最初是由Paul de Casteljau于1959年運(yùn)用de Casteljau算法開(kāi)發(fā)栅表,以穩(wěn)定數(shù)值的方法求出貝茲曲線(xiàn)笋鄙。

貝塞爾曲線(xiàn)的一個(gè)簡(jiǎn)的定義即:通過(guò)某些控制點(diǎn),去生成的復(fù)雜平滑曲線(xiàn)

數(shù)學(xué)依據(jù) 沒(méi)有復(fù)雜的數(shù)學(xué)公式怪瓶,畢竟我們不做數(shù)學(xué)研究(o)/~局装,這里只列舉了一下幾何推導(dǎo)過(guò)程。
①首先想象有兩條直線(xiàn)AB劳殖、BC相交于B點(diǎn)铐尚,AB、BC長(zhǎng)度不做限定

image

②分別在兩條直線(xiàn)上任取一個(gè)點(diǎn)D哆姻、F宣增,滿(mǎn)足條件AD/AB = BF/BC
image

③在DF上任取一點(diǎn)G,滿(mǎn)足滿(mǎn)足條件DG/DF = AD/AB = BF/BC
image

④從A到C畫(huà)一條曲線(xiàn)(也可以反過(guò)來(lái))矛缨,以B為控制點(diǎn)爹脾,則剛剛的G點(diǎn)一定處于A到C點(diǎn)的貝塞爾曲線(xiàn)之上帖旨。不斷變化DF位置,遞歸剛才的過(guò)程①-③的步驟灵妨,嘗試完所有點(diǎn)并連接起來(lái)解阅,我們可以得到如下一條平滑的曲線(xiàn),此曲線(xiàn)即為貝塞爾曲線(xiàn)
image

如果將整個(gè)遞推過(guò)程行測(cè)GIF動(dòng)畫(huà)泌霍,將會(huì)是如下效果
image

如果不易理解货抄,我們將GIF動(dòng)畫(huà)停止到某幀(25)的時(shí)候停一下,可得到如下圖朱转,此時(shí)下圖如同我們第④個(gè)步驟中結(jié)果圖蟹地,它同樣滿(mǎn)足條件 Q0 B / Q0 Q1 = P0Q0 / P0P1 = P1Q1/P1P2
image

N次貝塞爾曲線(xiàn) 在上述在貝塞爾曲線(xiàn)中,將P1稱(chēng)為控制點(diǎn)藤为,而P0P2稱(chēng)為起點(diǎn)和終點(diǎn)怪与,由此可以推出,起點(diǎn)終點(diǎn)位置不變的情況下缅疟,調(diào)整P1的位置將導(dǎo)致曲線(xiàn)形狀發(fā)生變化分别,這也是稱(chēng)P1為控制點(diǎn)的由來(lái)。如上一個(gè)控制點(diǎn)的曲線(xiàn)我們稱(chēng)之為二次貝塞爾曲線(xiàn)存淫。當(dāng)然茎杂,該過(guò)程可以拓展至N次,使用迭代即可完成纫雁,如下圖情況是三次貝塞爾曲線(xiàn)

image

image

控制點(diǎn)為0(沒(méi)有控制點(diǎn))的曲線(xiàn)也就是一條直線(xiàn)煌往,下圖是一次貝塞爾和四次貝塞爾的動(dòng)畫(huà)示例


image

image

UIBezierPath

UIBezierPath提供了實(shí)現(xiàn)二次和三次貝塞爾曲線(xiàn)的方法,同時(shí)該類(lèi)還提供了繪制矩形轧邪、圓弧以及橢圓等其它功能刽脖,我們可以通過(guò)它實(shí)現(xiàn)某些UIKit不能提供的多邊形圖層,實(shí)現(xiàn)一些曲線(xiàn)動(dòng)畫(huà)等忌愚。由于繪制圖形需要一個(gè)上下文環(huán)境曲管,通常可以放到自定義UIView的draw(_ rect: CGRect)中或者CALayer的draw(in ctx: CGContext)硕糊,這兩個(gè)方法中系統(tǒng)已經(jīng)自動(dòng)配置了繪圖環(huán)境院水。如果不做任何操作,建議不要保留空方法简十,會(huì)導(dǎo)致無(wú)意義的性能開(kāi)銷(xiāo)檬某,因?yàn)橄到y(tǒng)要配置繪圖上下文環(huán)境。

繪圖上下文 可以理解為一塊畫(huà)布圖層螟蝙,一個(gè)應(yīng)用中可能有多個(gè)上下文環(huán)境即多塊畫(huà)布恢恼,存在一個(gè)畫(huà)布圖層棧stack中維護(hù),通常使用UIGraphicsGetCurrentContext()即可取得胰默。在UIView的draw(_ rect: CGRect)方法中UIKit其實(shí)已經(jīng)為我們維護(hù)好了一個(gè)context场斑,無(wú)需我們創(chuàng)建既可以在這里繪制圖形漓踢。而如果我們需要在某個(gè)地方臨時(shí)時(shí)使用也可以自己創(chuàng)建,例如可以通過(guò)如下方法繪制一張圖片漏隐。

CGContextRef ctx = UIGraphicsGetCurrentContext();  // 獲取當(dāng)前畫(huà)布
// draw in here...
// 這里默認(rèn)ctx就是當(dāng)前的繪制上下文
UIImage *image = > > > UIGraphicsGetImageFromCurrentImageContext(); // 取得圖片
UIGraphicsEndImageContext(); // 銷(xiāo)毀畫(huà)布 ```

上下文狀態(tài)棧 在一塊畫(huà)布上畫(huà)圖可能有多種狀態(tài)喧半,比如使用的筆的顏色、粗細(xì)等青责,畫(huà)出的線(xiàn)應(yīng)該是虛線(xiàn)挺据、實(shí)線(xiàn)又或者其他特性等等,這些筆或者線(xiàn)等的狀態(tài)需要記錄爽柒,也就記錄在我們的圖形上下文環(huán)境中吴菠,這個(gè)環(huán)境中有一個(gè)狀態(tài)棧stack專(zhuān)門(mén)用來(lái)保存當(dāng)前狀態(tài)者填。棧頂中保存的狀態(tài)即為當(dāng)前狀態(tài)(當(dāng)前繪制使用的狀態(tài))浩村,亦可稱(chēng)為當(dāng)前狀態(tài)。

二次貝塞爾曲線(xiàn)Demo 自定義UIView占哟,并覆蓋draw(_ rect: CGRect)心墅,使用UIBezierPath繪制的二次貝塞爾曲線(xiàn),如下面圖例和對(duì)應(yīng)的原理圖榨乎。

image

image

由于現(xiàn)在所處的的是UIKit配置好的context環(huán)境下怎燥,所以設(shè)置顏色、線(xiàn)寬以及路勁形狀等代碼只要在繪制提交(stroke方法調(diào)用)之前即可蜜暑。

override func draw(_ rect: CGRect) {
    UIColor.green.set() // 設(shè)置顏色

    let path = UIBezierPath() 
    path.lineWidth = 5 // 線(xiàn)寬
    path.move(to: CGPoint(x: 100, y: 200)) // 起點(diǎn)
      // 設(shè)置終點(diǎn)和控制點(diǎn)
    path.addQuadCurve(to: CGPoint(x: 300, y: 200), controlPoint: CGPoint(x: 100, y: 0)) 
    // 繪制路徑
    path.stroke()
}

CAShapeLayer繪制圖形 除了上述使用draw(_ rect: CGRect)方法繪制外铐姚,我們還可以將路徑計(jì)算完成后賦值給CAShapeLayer的屬性,讓CAShapeLayer去執(zhí)行繪制過(guò)程肛捍。由于draw(_ rect: CGRect)方法中繪制由CPU計(jì)算處理隐绵,而CALayer的屬性是由GPU計(jì)算處理,因此使用CAShapeLayer方式會(huì)加快繪制速度拙毫,不過(guò)也要是CPU個(gè)GPU整體負(fù)載情況均衡拿捏铜异,通惩ǎ可能觸發(fā)離屏渲染的操作(比如同時(shí)設(shè)置cornerRadius和maskToBounds)更推薦使用CPU提前處理,然后再交給GPU。

三次貝塞爾曲線(xiàn)Demo 如果使用CAShapeLayer繪制貝塞爾曲線(xiàn)蒂破,則無(wú)需自定義View。使用UIBezierPath繪制的三次貝塞爾曲線(xiàn)培愁,如下面圖例和對(duì)應(yīng)的原理圖监氢。

image

image

使用shapeLayer后,發(fā)現(xiàn)設(shè)置線(xiàn)寬衅码、顏色等屬性對(duì)path滞欠,而shapeLayer有效,即使用shapeLayer來(lái)后更多的渲染計(jì)算工作交由CALayer來(lái)控制肆良,而這部分控制更多的話(huà)轉(zhuǎn)嫁給GPU筛璧,這樣的好處可以釋放更多的CPU資源

override func viewDidLoad() {
    super.viewDidLoad()
  
    let path = UIBezierPath()
    path.move(to: CGPoint(x: 100, y: 250)) // 起點(diǎn)
    // 設(shè)置終點(diǎn)和控制點(diǎn)
    path.addCurve(to: CGPoint(x: 400, y: 200), controlPoint1: CGPoint(x: 240, y: 120), controlPoint2: CGPoint(x: 260, y: 280))
    let shapeLayer = CAShapeLayer()
    shapeLayer.path = path.cgPath
    shapeLayer.lineWidth = 5 // 線(xiàn)寬
    shapeLayer.strokeColor = UIColor.green.cgColor  // 線(xiàn)顏色
    shapeLayer.fillColor = nil
    view.layer.addSublayer(shapeLayer)
}

使用UIBezierPath實(shí)現(xiàn)一個(gè)進(jìn)度指示器

image

實(shí)現(xiàn)這個(gè)指示器主要考慮幾點(diǎn):圓心逸绎、半徑,開(kāi)始角度夭谤、終點(diǎn)角度棺牧。使用UIBezierPath繪扇形,繪制扇形只需要一個(gè)圓弧+一條圓心到終點(diǎn)的直線(xiàn)+填充模式即可朗儒。該案例完整的代碼如下:

class ProgressLayerView: UIView {
    var progress: CGFloat
    
    override func draw(_ rect: CGRect) {
        let pathRadius = bounds.size.width / 2 - 10
        let arcCenter = CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height * 0.5)
        let startAngle: CGFloat = CGFloat(-Double.pi * 0.5)
        let endAngle: CGFloat = CGFloat(Double.pi * 2) * progress + startAngle
        
        let placeholderPath = UIBezierPath(arcCenter: arcCenter, radius: pathRadius, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
        UIColor.gray.set()
        placeholderPath.fill()
        
        // 扇形
        let path = UIBezierPath(arcCenter: arcCenter, radius: pathRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        // 圓心到終點(diǎn)的直線(xiàn)
        path.addLine(to: arcCenter)
        UIColor.white.set()
          // 填充模式繪圖
        path.fill()
    }
    
    override init(frame: CGRect) {
        progress = 0.01
        super.init(frame: frame)
        backgroundColor = UIColor.black.withAlphaComponent(0.8)
        layer.cornerRadius = 5
        layer.masksToBounds = true
        
        // For test
        refreshProgress()
    }
    
    func refreshProgress() {
        progress += 0.01
        setNeedsDisplay()
        if progress >= 1.0 {
            progress = 0.0
        }
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now().advanced(by: .milliseconds(100))) {
            self.refreshProgress()
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

UIView實(shí)現(xiàn)透明孔洞

image

下圖紅線(xiàn)是UIBezierPath路徑颊乘,箭頭表示路徑方向,通常情況下路徑方向沒(méi)有什么作用醉锄,不過(guò)在做路勁繪制動(dòng)畫(huà)或者實(shí)現(xiàn)以下這種透明孔洞時(shí)顯得很有用乏悄,只需要控制一下填充規(guī)則就可以實(shí)現(xiàn)這種效果。


image

填充規(guī)則 CAShapeLayer有一個(gè)成員fullRule指定填充規(guī)則恳不,只適用哪一種算法判斷畫(huà)布上的某一個(gè)區(qū)域是否屬于該圖形路徑的內(nèi)部檩小,對(duì)一個(gè)沒(méi)有交叉的線(xiàn)框如矩形,要判斷哪塊區(qū)域是內(nèi)部比較直觀烟勋。但對(duì)于一個(gè)復(fù)查交叉路徑(比如自交或包含路徑關(guān)系)就不太明確了规求,此時(shí)就需要一定的規(guī)則才行。
fullRule 有兩個(gè)值卵惦,nonZero:非零evenOdd:奇偶

非零 按該規(guī)則阻肿,要判斷一個(gè)點(diǎn)是否在圖形內(nèi),從該點(diǎn)作任意方向的一條射線(xiàn)沮尿,然后檢測(cè)射線(xiàn)與圖形路徑的交點(diǎn)情況丛塌。從0開(kāi)始計(jì)數(shù),路徑從左向右穿過(guò)射線(xiàn)則計(jì)數(shù)加1畜疾,從右向左穿過(guò)射線(xiàn)則計(jì)數(shù)減1赴邻。得出計(jì)數(shù)結(jié)果后,如果結(jié)果是0庸疾,則認(rèn)為點(diǎn)在圖形外部乍楚,否則認(rèn)為在內(nèi)部。如下圖示例:*

image

奇偶 按該規(guī)則届慈,要判斷一個(gè)點(diǎn)是否在圖形內(nèi)徒溪,從該點(diǎn)作任意方向的一條射線(xiàn),然后檢測(cè)射線(xiàn)與圖形路徑的交點(diǎn)的數(shù)量金顿。如果結(jié)果是奇數(shù)則認(rèn)為點(diǎn)在內(nèi)部臊泌,是偶數(shù)則認(rèn)為點(diǎn)在外部。如下圖示例:*
image

明確以上規(guī)則后揍拆,就容易控制圖層某些地方區(qū)域透明和不透明渠概。由于該成員默認(rèn)值是nonZero:非零,除了通過(guò)修改fullRule的值為evenOdd:奇偶實(shí)現(xiàn)中心透明外,還可以使用UIBezierPath的reversing()方法改變路徑曲線(xiàn)的默認(rèn)方向來(lái)到達(dá)到相同的效果播揪。該案例的完整代碼如下:

@IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
    super.viewDidLoad()

    // 背景圖片
    if let path = Bundle.main.path(forResource: "1404103479048.jpg", ofType: nil) {
        let image = UIImage(contentsOfFile: path)
        imageView.image = image
    }

    // 覆蓋圖層
    let maskView = UIView(frame: self.view.bounds)
    maskView.backgroundColor = UIColor.black.withAlphaComponent(0.8)
    view.addSubview(maskView)

    // 孔洞位置(alpha透明的位置)
    let width: CGFloat = 300
    let height: CGFloat = width
    let cornerRadius: CGFloat = width * 0.5
    let pathRect = CGRect(x: (maskView.bounds.size.width - width)/2,
                          y: (maskView.bounds.size.height - height)/2 - 100,
                          width: width,
                          height: height)

    // 先創(chuàng)建一個(gè)路徑放和原來(lái)的覆蓋圖層maskView一樣大
    let path = UIBezierPath(roundedRect: maskView.frame, cornerRadius: 0)
    // 然后再創(chuàng)建一個(gè)路徑時(shí)需要alpha透明的位置
    path.append(UIBezierPath(roundedRect: pathRect, cornerRadius: cornerRadius))

    // 創(chuàng)建shapeLayer贮喧,并將path給到shapeLayer
    let shapeLayer = CAShapeLayer()
    shapeLayer.path = path.cgPath

    // 設(shè)置填充規(guī)則
    shapeLayer.fillRule = .evenOdd

    // 將shapeLayer賦值給maskView.layer.mask
    // 這個(gè)mask是求兩個(gè)layer(shapeLayer和maskView.layer)的交集
    // 這個(gè)交集可以理解為面積交集
    maskView.layer.mask = shapeLayer
}

參考資料
維基百科
百度百科
GPU vs CPU in iOS
iOS 圖形繪制框架
必須要理解掌握的貝塞爾曲線(xiàn)(原創(chuàng))
iOS UIBezierPath貝塞爾曲線(xiàn)常用方法
iOS 利用CAShapeLayer的FillRule屬性生成一個(gè)空心遮罩的layer

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市猪狈,隨后出現(xiàn)的幾起案子箱沦,更是在濱河造成了極大的恐慌,老刑警劉巖雇庙,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谓形,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡疆前,警方通過(guò)查閱死者的電腦和手機(jī)寒跳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)竹椒,“玉大人童太,你說(shuō)我怎么就攤上這事∧肱疲” “怎么了康愤?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵儡循,是天一觀的道長(zhǎng)舶吗。 經(jīng)常有香客問(wèn)我,道長(zhǎng)择膝,這世上最難降的妖魔是什么誓琼? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮肴捉,結(jié)果婚禮上腹侣,老公的妹妹穿的比我還像新娘。我一直安慰自己齿穗,他們只是感情好傲隶,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著窃页,像睡著了一般跺株。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上脖卖,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天乒省,我揣著相機(jī)與錄音,去河邊找鬼畦木。 笑死袖扛,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的十籍。 我是一名探鬼主播蛆封,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼唇礁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了惨篱?” 一聲冷哼從身側(cè)響起垒迂,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎妒蛇,沒(méi)想到半個(gè)月后机断,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绣夺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年吏奸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陶耍。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奋蔚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出烈钞,到底是詐尸還是另有隱情泊碑,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布毯欣,位于F島的核電站馒过,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏酗钞。R本人自食惡果不足惜腹忽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望砚作。 院中可真熱鬧窘奏,春花似錦、人聲如沸葫录。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)米同。三九已至骇扇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窍霞,已是汗流浹背匠题。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留但金,地道東北人韭山。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親钱磅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子梦裂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

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