個人github博客地址 https://github.com/hujiulong/...
在前端開發中,貝賽爾曲線無處不在:javascript
transition-timing-function
屬性,可使用貝塞爾曲線來描述過渡的緩動計算這篇文章我準備從實現一個很是簡單的曲線動畫效果入手,幫助你們完全地弄懂什麼是貝塞爾曲線,以及它有哪些特性,文章中有一點點數學公式,可是都很是簡單:)。css
實現這樣一個曲線動畫html
能夠點擊這裏查看在線演示前端
在寫代碼以前,先了解一下什麼是貝塞爾曲線吧。java
貝塞爾曲線(Bezier curve)是計算機圖形學中至關重要的參數曲線,它經過一個方程來描述一條曲線,根據方程的最高階數,又分爲線性貝賽爾曲線,二次貝塞爾曲線、三次貝塞爾曲線和更高階的貝塞爾曲線。git
下面詳細介紹一下用得比較多的二次貝塞爾曲線和三次貝塞爾曲線github
二次貝塞爾曲線由三個點P0
,P1
,P2
來肯定,這些點也被稱做控制點。曲線的方程爲:算法
這個方程其實有它的幾何意義,它表示能夠經過這樣的步驟來繪製一條曲線:canvas
0-1
的t
值P0
和P1
計算出點Q0
,Q0
在P0
P1
連成的直線上,而且length( P0, Q0 ) = length( P0, P1 ) * t
P1
和P2
計算出Q1
,使得length( P1, Q1 ) = length( P1, P2 ) * t
Q1
和Q2
計算出B
,使得length( Q0, Q1 ) = length( Q0, B ) * t
。B
就爲當前曲線上的點注:上面的length
表示兩點之間的長度echarts
圖:二次貝塞爾曲線結構
有了曲線方程,咱們直接代入具體的t
值就能算出點B
了。
若是將t
的值從0
過渡到1
,不斷計算點B
,就能夠獲得一條二次貝塞爾曲線:
圖:二次貝塞爾線繪製過程
在canvas中,繪製二次貝塞爾曲線的方法爲
ctx.quadraticCurveTo( p1x, p1y, p2x, p2y )
其中p1x, p1y, p2x, p2y
爲後兩個控制點(P1
和P2
)的橫縱座標,它默認將當前路徑的起點做爲一個控制點(P0
)。
三次貝塞爾曲線須要四個點P0
,P1
,P2
,P3
來肯定,曲線方程爲
它的計算過程和二次貝塞爾曲線相似,這裏再也不贅述,能夠看下圖:
圖:三次貝塞爾曲線結構
一樣,將t
的值從0
過渡到1
,就能夠繪製出一條三次貝塞爾曲線:
圖:三次貝塞爾曲線繪製過程
在canvas中,繪製三次貝塞爾曲線的方法爲
ctx.bezierCurveTo( p1x, p1y, p2x, p2y, p3x, p3y )
其中p1x, p1y, p2x, p2y, p3x, p3y
爲後三個控制點(P1
,P2
和P3
)的橫縱座標,它默認將當前路徑的起點做爲一個控制點(P0
)。
在三次貝塞爾曲線後面,還有更高階的貝塞爾曲線,一樣它們繪製的過程也更加複雜
圖:四次貝塞爾曲線
圖:五次貝塞爾曲線
咱們能夠概括出貝塞爾曲線有幾個重要的特徵:
複習完基礎概念,接下來就要講若是繪製貝塞爾曲線啦
爲簡單起見,咱們選擇使用二次貝塞爾曲線。
咱們先不考慮動畫的事,咱們先將問題簡化成:給定一個起點和一個終點,須要實現一個函數,它可以繪製出一條曲線。
也就是說咱們須要實現一個函數drawCurvePath
,除渲染上下文ctx外(不清楚ctx是什麼的同窗能夠先熟悉下canvas的基本概念),它接受三個參數,分別爲二次貝塞爾曲線的三個控制點。咱們將樣式控制移到函數外,drawCurvePath
只用來繪製路徑。
/** * 繪製二次貝賽爾曲線路徑 * @param {Object} ctx * @param {Array<number>} p0 * @param {Array<number>} p1 * @param {Array<number>} p2 */ function drawCurvePath( ctx, p0, p1, p2 ) { // ... }
前文提到過,在canvas中,繪製二次貝賽爾曲線的方法是quadraticCurveTo
,因此只要短短兩行就能完成這個方法。
/** * 繪製二次貝賽爾曲線路徑 * @param {CanvasRenderingContext2D} ctx * @param {Array<number>} p0 * @param {Array<number>} p1 * @param {Array<number>} p2 */ function drawCurvePath( ctx, p0, p1, p2 ) { ctx.moveTo( p0[ 0 ], p0[ 1 ] ); ctx.quadraticCurveTo( p1[ 0 ], p1[ 1 ], p2[ 0 ], p2[ 1 ] ); }
這樣就完成了基本的繪製二次貝塞爾曲線的方法了。
可是函數這樣設計有點小問題
若是咱們是在作一個圖形庫,咱們想給使用者提供一個繪製曲線的方法。
對於使用者來講,他只想在給定的起點和終點間間繪製一條曲線,他想要獲得的曲線儘可能美觀,可是又不想關心具體的實現細節,若是還須要給第三個點,使用者會有必定的學習成本(至少須要弄明白什麼是貝塞爾曲線)。
看到這裏你可能會比較疑惑,即便是二次貝塞爾曲線也須要三個控制點,只有起點和終點怎麼繪製曲線呢。
咱們能夠在起點和終點的垂直平分線上選一點做爲第三個控制點,能夠提供給使用者一個參數來控制曲線的彎曲程度,如今函數就變成了這樣
/** * 繪製一條曲線路徑 * @param {CanvasRenderingContext2D} ctx * @param {Array<number>} start 起點 * @param {Array<number>} end 終點 * @param {number} curveness 曲度(0-1) */ function drawCurvePath( ctx, start, end, curveness ) { // ... }
咱們用curveness
來表示曲線的彎曲程度,也就是第三個控制點的偏離程度。這樣很容易就能計算出中間點。
如今完整的函數變成了這樣:
/** * 繪製一條曲線路徑 * @param {Object} ctx canvas渲染上下文 * @param {Array<number>} start 起點 * @param {Array<number>} end 終點 * @param {number} curveness 曲度(0-1) */ function drawCurvePath( ctx, start, end, curveness ) { // 計算中間控制點 var cp = [ ( start[ 0 ] + end[ 0 ] ) / 2 - ( start[ 1 ] - end[ 1 ] ) * curveness, ( start[ 1 ] + end[ 1 ] ) / 2 - ( end[ 0 ] - start[ 0 ] ) * curveness ]; ctx.moveTo( start[ 0 ], start[ 1 ] ); ctx.quadraticCurveTo( cp[ 0 ], cp[ 1 ], end[ 0 ], end[ 1 ] ); }
對,就這麼短短几行,接下來咱們就能夠經過它來繪製一條曲線了,代碼以下
<!DOCTYPE html> <html lang="en"> <head> <title>draw curve</title> </head> <body> <canvas id="canvas" width="800" height="800"></canvas> <script> var canvas = document.getElementById( 'canvas' ); var ctx = canvas.getContext( '2d' ); ctx.lineWidth = 2; ctx.strokeStyle = '#000'; ctx.beginPath(); drawCurvePath( ctx, [ 100, 100 ], [ 200, 300 ], 0.4 ); ctx.stroke(); function drawCurvePath( ctx, start, end, curveness ) { // ... } </script> </body> </html>
繪製結果:
繪製一條曲線
終於來到文章的本體啦,咱們的目的不是繪製一條靜態的曲線,咱們想繪製一條有過渡效果的曲線。
簡化一下問題,那就是咱們但願繪製曲線的函數還接受另外一個參數,表示繪製曲線的百分比。咱們定時去調用這個函數,遞增百分比這個參數,就能畫出動畫了。
咱們新增一個參數percent
來表示百分比,如今函數變成了這樣:
/** * 繪製一條曲線路徑 * @param {Object} ctx canvas渲染上下文 * @param {Array<number>} start 起點 * @param {Array<number>} end 終點 * @param {number} curveness 曲度(0-1) * @param {number} percent 繪製百分比(0-100) */ function drawCurvePath( ctx, start, end, curveness, percent ) { // ... }
可是canvas提供的quadraticCurveTo
方法只能繪製一條完整的二次貝賽爾曲線,沒有辦法去控制它只畫一部分。
畫完後用clearRect
擦除掉一部分?這不太可行,由於很難肯定要擦除的範圍。若是曲線的線寬比較寬,就還須要保證擦除的邊界和曲線末端垂直,問題就變得很複雜了。
如今再從新看看這張圖
咱們是否是能夠將percent
這個參數理解成t
值,而後經過貝賽爾曲線方程去計算出中間全部的點,用直線鏈接起來,以此模擬繪製貝賽爾曲線的一部分呢?
咱們再也不用canvas提供的quadraticCurveTo
來繪製曲線,而是經過貝賽爾曲線的方程計算出一系列點,用多端直線來模擬曲線。
這樣作的好處時,咱們能夠很容易的控制繪製的範圍。
那麼函數實現就變成了這樣:
/** * 繪製一條曲線路徑 * @param {Object} ctx canvas渲染上下文 * @param {Array<number>} start 起點 * @param {Array<number>} end 終點 * @param {number} curveness 曲度(0-1) * @param {number} percent 繪製百分比(0-100) */ function drawCurvePath( ctx, start, end, curveness, percent ) { var cp = [ ( start[ 0 ] + end[ 0 ] ) / 2 - ( start[ 1 ] - end[ 1 ] ) * curveness, ( start[ 1 ] + end[ 1 ] ) / 2 - ( end[ 0 ] - start[ 0 ] ) * curveness ]; ctx.moveTo( start[ 0 ], start[ 1 ] ); for ( var t = 0; t <= percent / 100; t += 0.01 ) { var x = quadraticBezier( start[ 0 ], cp[ 0 ], end[ 0 ], t ); var y = quadraticBezier( start[ 1 ], cp[ 1 ], end[ 1 ], t ); ctx.lineTo( x, y ); } } function quadraticBezier( p0, p1, p2, t ) { var k = 1 - t; return k * k * p0 + 2 * ( 1 - t ) * t * p1 + t * t * p2; // 這個方程就是二次貝賽爾曲線方程 }
接下來就能夠經過設置定時器,每隔一段時間調用一次這個方法,而且遞增percent
爲了動畫更加平滑,咱們使用requestAnimationFrame
來代替定時器
<!DOCTYPE html> <html lang="en"> <head> <title>draw curve</title> </head> <body> <canvas id="canvas" width="800" height="800"></canvas> <script> var canvas = document.getElementById( 'canvas' ); var ctx = canvas.getContext( '2d' ); ctx.lineWidth = 2; ctx.strokeStyle = '#000'; var percent = 0; function animate() { ctx.clearRect( 0, 0, 800, 800 ); ctx.beginPath(); drawCurvePath( ctx, [ 100, 100 ], [ 200, 300 ], 0.2, percent ); ctx.stroke(); percent = ( percent + 1 ) % 100; requestAnimationFrame( animate ); } animate(); function drawCurvePath( ctx, start, end, curveness, percent ) { // ... } </script> </body> </html>
獲得的結果:
這樣基本實現了咱們的需求,但它有一個問題:
測試發現,進行一次lineTo
的時間和一次quadraticCurveTo
的時間差很少,可是quadraticCurveTo
只須要一次就能畫出曲線,而使用lineTo
則須要數十次。
換言之,用這樣的方式繪製曲線,和咱們前面的實現方式相比性能降低了數十倍之多。在繪製一條曲線時可能感受不到區別,可是若是須要同時繪製上千條曲線,性能就會受到很大的影響。
那有沒有什麼方法能夠作到用quadraticCurveTo
來實現繪製完整曲線的一部分呢?
咱們再次回到這張圖
在中間的某一時刻,例如t=0.25時,它是這樣的:
咱們注意到,曲線P0-B
這一段彷佛也是貝賽爾曲線,它的控制點變成了P0,Q0,B
。
如今問題就迎刃而解了,咱們只須要每次計算出Q0,B
,就能獲得其中一小段貝賽爾曲線的控制點,而後就能夠經過quadraticCurveTo
來繪製它了。
代碼以下:
/** * 繪製一條曲線路徑 * @param {Object} ctx canvas渲染上下文 * @param {Array<number>} start 起點 * @param {Array<number>} end 終點 * @param {number} curveness 曲度(0-1) * @param {number} percent 繪製百分比(0-100) */ function drawCurvePath( ctx, start, end, curveness, percent ) { var cp = [ ( start[ 0 ] + end[ 0 ] ) / 2 - ( start[ 1 ] - end[ 1 ] ) * curveness, ( start[ 1 ] + end[ 1 ] ) / 2 - ( end[ 0 ] - start[ 0 ] ) * curveness ]; var t = percent / 100; var p0 = start; var p1 = cp; var p2 = end; var v01 = [ p1[ 0 ] - p0[ 0 ], p1[ 1 ] - p0[ 1 ] ]; // 向量<p0, p1> var v12 = [ p2[ 0 ] - p1[ 0 ], p2[ 1 ] - p1[ 1 ] ]; // 向量<p1, p2> var q0 = [ p0[ 0 ] + v01[ 0 ] * t, p0[ 1 ] + v01[ 1 ] * t ]; var q1 = [ p1[ 0 ] + v12[ 0 ] * t, p1[ 1 ] + v12[ 1 ] * t ]; var v = [ q1[ 0 ] - q0[ 0 ], q1[ 1 ] - q0[ 1 ] ]; // 向量<q0, q1> var b = [ q0[ 0 ] + v[ 0 ] * t, q0[ 1 ] + v[ 1 ] * t ]; ctx.moveTo( p0[ 0 ], p0[ 1 ] ); ctx.quadraticCurveTo( q0[ 0 ], q0[ 1 ], b[ 0 ], b[ 1 ] ); }
將前面寫的頁面替換成上面的代碼,能夠看到獲得的結果是同樣的:
如今已經解決了最關鍵的問題,咱們能夠繪製動畫啦。
不過這一部分並不重要,我就不貼代碼了。
完整代碼能夠看這裏
個人博客地址: https://github.com/hujiulong/...
我會在這裏分享個人學習成果和經驗,特別是canvas/WebGL/svg這方面的技術。若是有對前端圖形繪製感興趣的同窗能夠關注一下個人博客,收藏點star,訂閱點watch。
最近纔將博客搬到github,因此文章並很少,我會堅持寫下去的!