黏糊糊贝塞尔曲线

最近看了很多关于贝塞尔曲线的文章,好好总结了一番,加上自己的一点思路,做了点微小的工作。废话不多说,直接上图:

bezierPopLine

主要使用到了二阶贝塞尔曲线,那么开始之前,先了解一下什么是二阶贝塞尔曲线

二阶贝塞尔曲线

首先,我们在平面内选3个不同线的点并且依次用线段连接。如下所示

接着,我们在AB和BC线段上找出点D和点E,使得AD/AB = BE/BC。

再接着,连接DE,并在DE上找出一点F,使得DF/DE = AD/AB = BE/BC。

然后,让选取的点D在第一条线段上从起点A,移动到终点B,找出所有点F,并将它们连起来。最后得到了一条非常光滑的曲线,这条就是传说中的。。。二阶贝塞尔曲线。
看这里,看这里,看这里:

二阶贝塞尔

仔细观察会发现,起始点P0,结束点P2,和曲线是相切的关系。
所以,如果要使两条贝塞尔曲线光滑连接,只要保证第一条贝塞尔曲线的结束点和第二条贝塞尔曲线相切就行。
如果使贝塞尔A的结束点A2与贝塞尔B的起始点B0重合,那么,贝塞尔A的控制点A1,结束点A2,贝塞尔B的起始点B0(即A2),贝塞尔B的控制点B1,连接起来就是一条直线。

原理就这些,是时候进入正文了,皮皮虾,我们走!

抽丝剥茧

bezierPopLine.gif

这样看是不是清晰很多,均匀添加7个View作为关键点,然后由这些点画出3条二阶贝塞尔曲线。
BezierPath明明只需要CGPoint就行了,为什么这里设置了7个View来作为贝塞尔曲线的关键点,而不是7个CGPoint,这个后面说
关键代码:

bezierDotCount = 7
for i in 0...bezierDotCount-1 {
     let view = UIView(frame: CGRect(x: Int(self.frame.width)*i/(bezierDotCount-1), y: 0, width: 10, height: 10))
     view.center = CGPoint(x: Int(self.frame.width)*i/(bezierDotCount-1), y: 0)
     self.addSubview(view)
     view.backgroundColor = UIColor.red
     view.layer.cornerRadius = 5
     viewArray.append(view)
 }

shapeLayer = CAShapeLayer()
shapeLayer?.strokeColor = UIColor.white.cgColor
shapeLayer?.fillColor = UIColor.clear.cgColor
shapeLayer?.path = currentPath()
self.layer.addSublayer(shapeLayer!)

生成曲线:

func currentPath() -> CGPath {
        
        let width = self.bounds.size.width
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 0, y: self.frame.height))
        path.addLine(to: CGPoint(x: 0, y: viewArray[0].center.y))
        for i in stride(from: 1, to: bezierDotCount-1, by: 2) {
            path.addQuadCurve(to: (viewArray[i+1].center), controlPoint: (viewArray[i].center))
        }
        path.addLine(to: CGPoint(x: width, y: self.frame.height))
        path.close()
        return path.cgPath
        
        
}

再往下剥一点。曲线是由点画出来了,但是这些点在移动中又是如何确定的呢?
如果依次把点命名为L3,L2,L1,C,R1,R2,R3,那么:
第一条贝塞尔曲线以L3为起始点,L2为控制点,L1为结束点,
第二条曲线以L1为起始点,C为控制点,R1为结束点,
第三条曲线以R1为起始点,R2为控制点,R3为结束点。
有图有真相:

bezierPopLine.gif

是不是更清晰了。
在使用手势操作时,我们需要一个控制点跟随手指来控制整个曲线的运动,显然中点C是最好的选择。
那么其他点应该如何移动呢?
就如前面说的,为了使连接的曲线平滑,我们得保证两个控制点和起始点(结束点)是一直线,所以L2,L1,C得保证是一条直线,C,R1,R2也是一条直线。
在移动中要保证3点一直线,就要让他们按比例来移动。
我们也别想得太复杂了。就把L3到C的距离三等分
用初中数学可以算出比例

灵魂画手

下面就可以算出关键点坐标了

灵魂画手
let additionalHeight = max(gesture.translation(in: self).y, 0)
let waveHeight = min(additionalHeight*2/3, 100)
let baseHeight = additionalHeight-waveHeight
let locationX = gesture.location(in: self).x
 
let width = self.bounds.size.width
let minLeftX = CGFloat(0)
let maxRightX = width
let leftPartWidth = locationX - minLeftX
let rightPartWidth = maxRightX - locationX
viewArray[0].center = CGPoint(x: minLeftX, y: baseHeight)
viewArray[1].center = CGPoint(x: minLeftX+leftPartWidth/3, y: baseHeight)
viewArray[2].center = CGPoint(x: minLeftX+leftPartWidth*2/3, y: baseHeight+waveHeight*2/3)
viewArray[3].center = CGPoint(x: locationX, y: baseHeight+waveHeight*4/3)
viewArray[4].center = CGPoint(x: maxRightX-rightPartWidth*2/3, y: baseHeight+waveHeight*2/3)
viewArray[5].center = CGPoint(x: maxRightX-(rightPartWidth/3), y: baseHeight)
viewArray[6].center = CGPoint(x: maxRightX, y: baseHeight)

到这里,所有点都出来了,加上手势,应该是这样

bezierPopLine.gif

我们发现是没有DuangDuangDuang~的特效
聪明的你应该想到了可以给关键点view加上弹簧动画

 UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions.curveEaseIn, animations: {
                
                
   for i in 0...self.bezierDotCount-1 {
                    
     self.viewArray[i].center = CGPoint(x: Int(self.frame.width)*i/(self.bezierDotCount-1), y: 0)
                    
    }
self.shapeLayer?.path = self.currentPath()

}, completion: {[weak self] (finish) -> Void in

}
bezierPopLine5.gif
Duang

纳尼!为什么曲线没有跟着动?

这个时候我们就要使用Presentation Layer,可以实时获取 Layer 属性的当前值。
这就是为什么一开始使用view作为关键点,而不是CGPoint,这里可以通过关键点view的Presentation Layer,生成一个新的view,然后放到CADisplayLink实时获取弹簧动画中的位置,最后重新绘制曲线。
千万记得在弹簧动画结束后,将CADisplayLink设置invalidate

 UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions.curveEaseIn, animations: {
      
      for i in 0...self.bezierDotCount-1 {
                    
            self.viewArray[i].center = CGPoint(x: Int(self.frame.width)*i/(self.bezierDotCount-1), y: 0)     
      }
           
      self.displaylink = CADisplayLink(target: self, selector: #selector(self.displayLinkAction))
      self.displaylink?.add(to: RunLoop.main, forMode: RunLoopMode.commonModes)

 }, completion: {[weak self] (finish) -> Void in
                
    self?.displaylink?.invalidate()
                 }
)

func displayLinkAction(dis:CADisplayLink) {
        
        let rectViewArray:Array = viewArray.map({
            (view) -> UIView in
            let layer = view.layer.presentation()
            let rect:CGRect = layer?.value(forKey: "frame") as! CGRect
            let rectView = UIView(frame: rect)
            return rectView
        })
        
        let width = self.bounds.size.width
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 0, y: self.frame.height))
        path.addLine(to: CGPoint(x: 0, y: (rectViewArray[0].center.y)))
        for i in stride(from: 1, to: rectViewArray.count-1, by: 2) {
            path.addQuadCurve(to: (rectViewArray[i+1].center), controlPoint: (rectViewArray[i].center))
        }
        path.addLine(to: CGPoint(x: width, y: self.frame.height))
        path.close()
        
        shapeLayer?.path = path.cgPath
}

到这里就完成啦。
GitHub源码,记得点星星哦

稍微修改一下也能做出不错的特效菜单哦DuangDuangDuang

duang2.gif

参考资料

A GUIDE TO IOS ANIMATION

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容

  • 谈谈贝塞尔曲线 最近在做项目的时候,需要用到一个动画,非常简单的动画,简单到就是直接对一个View做平移… 然而虽...
    雨润听潮阅读 5,994评论 1 16
  • 最近在做项目的时候,需要用到一个动画,非常简单的动画,简单到就是直接对一个View做平移... 然而虽然动画简单,...
    IAMDAEMON阅读 4,283评论 12 69
  • 背景: 给一系列顶点,如果只是用直线将其中的各个点依次连接起来,最终形成一个折线图,这种很容易实现。但是现实...
    狂风无迹阅读 39,202评论 12 70
  • 贝塞尔曲线开发的艺术 一句话概括贝塞尔曲线:将任意一条曲线转化为精确的数学公式。 很多绘图工具中的钢笔工具,就是典...
    eclipse_xu阅读 27,708评论 38 370
  • 本文主要内容为贝塞尔曲线原理解析并用 SurfaceView 实现其展示动画 关于SurfaceView 的使用,...
    涤生_Woo阅读 13,423评论 5 94