摘要:實現這樣一個曲線動畫可以點擊這里查看在線演示在寫代碼之前,先了解一下什么是貝塞爾曲線吧。繪制二次貝賽爾曲線路徑這樣就完成了基本的繪制二次貝塞爾曲線的方法了。
我的github博客地址 https://github.com/hujiulong/...前言
在前端開發中,貝賽爾曲線無處不在:
它可以用來繪制曲線,在svg和canvas中,原生提供的曲線繪制都是使用貝賽爾曲線
它也可以用來描述一個緩動算法,設置css的transition-timing-function屬性,可以使用貝塞爾曲線來描述過渡的緩動計算
幾乎所有前端2D或3D圖形圖表庫(echarts,d3,three.js)都會使用到貝塞爾曲線
這篇文章我準備從實現一個非常簡單的曲線動畫效果入手,幫助大家徹底地弄懂什么是貝塞爾曲線,以及它有哪些特性,文章中有一點點數學公式,但是都非常簡單:)。
實現這樣一個曲線動畫
可以點擊這里查看在線演示
在寫代碼之前,先了解一下什么是貝塞爾曲線吧。
貝塞爾曲線貝塞爾曲線(Bezier curve)是計算機圖形學中相當重要的參數曲線,它通過一個方程來描述一條曲線,根據方程的最高階數,又分為線性貝賽爾曲線,二次貝塞爾曲線、三次貝塞爾曲線和更高階的貝塞爾曲線。
下面詳細介紹一下用得比較多的二次貝塞爾曲線和三次貝塞爾曲線
二次貝塞爾曲線二次貝塞爾曲線由三個點P0,P1,P2來確定,這些點也被稱作控制點。曲線的方程為:
這個方程其實有它的幾何意義,它表示可以通過這樣的步驟來繪制一條曲線:
選定一個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表示兩點之間的長度
圖:二次貝塞爾曲線結構
有了曲線方程,我們直接代入具體的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)。
貝塞爾曲線的特征在三次貝塞爾曲線后面,還有更高階的貝塞爾曲線,同樣它們繪制的過程也更加復雜
四次貝塞爾曲線圖:四次貝塞爾曲線
五次貝塞爾曲線圖:五次貝塞爾曲線
我們可以歸納出貝塞爾曲線有幾個重要的特征:
n階貝塞爾曲線需要n+1個點來確定
貝塞爾曲線是平滑的
貝塞爾曲線的起點和終點與對應控制點的連線相切
繪制貝塞爾曲線復習完基礎概念,接下來就要講如果繪制貝塞爾曲線啦
為簡單起見,我們選擇使用二次貝塞爾曲線。
我們先不考慮動畫的事,我們先將問題簡化成:給定一個起點和一個終點,需要實現一個函數,它能夠繪制出一條曲線。
也就是說我們需要實現一個函數drawCurvePath,除渲染上下文ctx外(不清楚ctx是什么的同學可以先熟悉下canvas的基本概念),它接受三個參數,分別為二次貝塞爾曲線的三個控制點。我們將樣式控制移到函數外,drawCurvePath只用來繪制路徑。
/** * 繪制二次貝賽爾曲線路徑 * @param {Object} ctx * @param {Array} p0 * @param {Array } p1 * @param {Array } p2 */ function drawCurvePath( ctx, p0, p1, p2 ) { // ... }
前文提到過,在canvas中,繪制二次貝賽爾曲線的方法是quadraticCurveTo,所以只要短短兩行就能完成這個方法。
/** * 繪制二次貝賽爾曲線路徑 * @param {CanvasRenderingContext2D} ctx * @param {Array} p0 * @param {Array } p1 * @param {Array } 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} start 起點 * @param {Array } end 終點 * @param {number} curveness 曲度(0-1) */ function drawCurvePath( ctx, start, end, curveness ) { // ... }
我們用curveness來表示曲線的彎曲程度,也就是第三個控制點的偏離程度。這樣很容易就能計算出中間點。
現在完整的函數變成了這樣:
/** * 繪制一條曲線路徑 * @param {Object} ctx canvas渲染上下文 * @param {Array} start 起點 * @param {Array } 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 ] ); }
對,就這么短短幾行,接下來我們就可以通過它來繪制一條曲線了,代碼如下
draw curve
繪制結果:
繪制一條曲線
終于來到文章的本體啦,我們的目的不是繪制一條靜態的曲線,我們想繪制一條有過渡效果的曲線。
簡化一下問題,那就是我們希望繪制曲線的函數還接受另一個參數,表示繪制曲線的百分比。我們定時去調用這個函數,遞增百分比這個參數,就能畫出動畫了。
我們新增一個參數percent來表示百分比,現在函數變成了這樣:
/** * 繪制一條曲線路徑 * @param {Object} ctx canvas渲染上下文 * @param {Array} start 起點 * @param {Array } 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} start 起點 * @param {Array } 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來代替定時器
draw curve
得到的結果:
這樣基本實現了我們的需求,但它有一個問題:
測試發現,進行一次lineTo的時間和一次quadraticCurveTo的時間差不多,但是quadraticCurveTo只需要一次就能畫出曲線,而使用lineTo則需要數十次。
換言之,用這樣的方式繪制曲線,和我們前面的實現方式相比性能下降了數十倍之多。在繪制一條曲線時可能感覺不到區別,但是如果需要同時繪制上千條曲線,性能就會受到很大的影響。
方法二那有沒有什么方法可以做到用quadraticCurveTo來實現繪制完整曲線的一部分呢?
我們再次回到這張圖
在中間的某一時刻,例如t=0.25時,它是這樣的:
我們注意到,曲線P0-B這一段似乎也是貝賽爾曲線,它的控制點變成了P0,Q0,B。
現在問題就迎刃而解了,我們只需要每次計算出Q0,B,就能得到其中一小段貝賽爾曲線的控制點,然后就可以通過quadraticCurveTo來繪制它了。
代碼如下:
/** * 繪制一條曲線路徑 * @param {Object} ctx canvas渲染上下文 * @param {Array} start 起點 * @param {Array } 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 ] ]; // 向量 var v12 = [ p2[ 0 ] - p1[ 0 ], p2[ 1 ] - p1[ 1 ] ]; // 向量 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 ] ]; // 向量 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,所以文章并不多,我會堅持寫下去的!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/90609.html
摘要:動畫曲線的應用了解了如何用貝塞爾曲線來指定動畫曲線后,很多動畫涉及到速度方面的效果就可以實現了,例如小車加速剎車,彈簧動畫等速度軌跡都可以根據自己的需要來進行定制。 貝塞爾曲線又叫貝茲曲線,在大學高數中一度讓我非常頭疼。前陣子練手寫動畫的時候,發現貝塞爾曲線可以應用于軌跡的繪制以及定義動畫曲線。 本文就來探究一下,貝塞爾曲線到底是個什么樣的存在。 貝塞爾曲線原理 貝塞爾曲線由n個點來決...
摘要:由于工作需求需要寫一個翻角效果鏈接右上角需要從無的狀態撕開一個標記且有動畫過程上圖是實現的效果圖不是對這個翻角效果的難點在于沒有翻開的時候露出的是下面的內容實現角度來說純動畫的設計方案并沒有相出一個好的對策于是撿起了好久之前學的入門級別的下 由于工作需求 , 需要寫一個翻角效果;showImg(https://segmentfault.com/img/bVYVm4?w=135&h=12...
摘要:由于工作需求需要寫一個翻角效果鏈接右上角需要從無的狀態撕開一個標記且有動畫過程上圖是實現的效果圖不是對這個翻角效果的難點在于沒有翻開的時候露出的是下面的內容實現角度來說純動畫的設計方案并沒有相出一個好的對策于是撿起了好久之前學的入門級別的下 由于工作需求 , 需要寫一個翻角效果;showImg(https://segmentfault.com/img/bVYVm4?w=135&h=12...
閱讀 1870·2021-11-25 09:43
閱讀 2149·2021-11-19 09:40
閱讀 3428·2021-11-18 13:12
閱讀 1741·2021-09-29 09:35
閱讀 662·2021-08-24 10:00
閱讀 2508·2019-08-30 15:55
閱讀 1714·2019-08-30 12:56
閱讀 1818·2019-08-28 17:59