摘要:方向向量與向量的向量積的方向與這兩個向量所在平面垂直,且遵守右手定則。向量解決方案三方案一的問題在于,向量到向量之間的線性插值是直線均勻的,但是不是角度均勻的。
記得幾年前,我的一個同事J需要做一個動畫功能,大概的需求是
實現球面上一個點到另外一個點的動畫。當時他遇到了難度,在研究了一個上午無果的情況下,咨詢了我。我就告訴他說,你先嘗試一個簡化的版本,就是實現圓環上一個點到另外一個點的動畫。如下圖所示,要實現點A插值漸變到B的動畫過程。
同事J的解決方案是,先計算出來A點和圓心O的連線和水平方向(與X軸平行)的夾角1,再計算出B點和圓心O的連線和水平水平方向的夾角2。 計算出夾角以后,開始實現動畫效果,由于已經有了兩個角度,所以只需要實現一個角度不斷插值變化的效果即可,如下圖所示:
但是這兒存在一個問題,比如下圖中。
從A點和B點的位置變化從圖中可以看出,A點在第二象限,角度范圍是π/2~π,而A點在第三象限,角度范圍在 -π~-π/2(Math.atan2的計算結果)。此時從A點的角度動畫到B點的角度,動畫效果是從A點沿著順時針方向繞一大圈動畫到B,而不是直接從A點逆時針動畫到B點。
而實際上我們想要的結果是從A點逆時針到B點(運動的角度最小)。如果此時需要獲得正確的結果,就需要做各種角度的轉換適配。
首先假設OA的坐標點為(x1,y1),注意此處是A點相對于與圓心O點的坐標,這樣方便計算。然后計算出角度,我們知道可以通過Math.atan2(y,x)來計算角度。 那么計算出來的角度的范圍如下,以坐標系4個象限為分類標準:
第一象限的角度范圍是:0 ~ PI/2
第二象限的角度范圍是:PI/2 ~ PI
第三象限的角度范圍是:-PI ~-PI/2
第四象限的角度范圍是: -PI/2 ~-PI
如下圖所示:
從上面圖中可以看出,象限之間的角度變換不是線性的,比如從第二象限到第三象限,角度出現了跳躍式的變換。假設A點在第二象限,B點在第三象限,如下圖所示:
現在假設A點的角度為 3/4 PI, B點的角度為 - 3/4PI,如果按照角度插值的方式進行運動。示例代碼片段入下:
var i = 0,count = 200; var PI = Math.PI; function animateAngle() { var angle = (angle1 * (count-i) + angle2 * (i)) / count; var x = cx + Math.cos(angle) * r, y = cy + Math.sin(angle) * r; ctx.beginPath(); ctx.moveTo(cx,cy); ctx.lineTo(x,y); ctx.strokeStyle = "red"; ctx.stroke(); i ++; if(i > count){ i = 0; } }
運動的軌跡如下圖紅色弧線所示,
而實際,我們希望的效果是按照最短的路徑進行運動,如下圖藍色弧線:
為什么運動軌跡是紅色的弧線呢。 因為使用了角度的插值,A點角度是PI3/4,B點角度為-PI3/4,因此插值是從一個正的角度減少到一個負的角度,這正好是紅色路徑。下圖標記了主要節點的角度:
。
同樣的道理,從B點動畫到A點,也同樣會走紅色路徑。
要實現A點和B點之間沿著藍色弧線動畫,需要把B點的角度加上2 PI,此時B點的角度為PI5/4。看來把小于0的角度加上2*PI,可以解決上面的問題。
但是這種方式不能解決所有的情況,比如把A點移到第一象限,有下面兩種情況:
情況1: 紅色弧線的角度小于PI,此時應該沿著紅色弧線動畫,此時
B點的角度不應該加上PI*2
情況2: 紅色弧線的角度大于PI,此時應該沿著藍色弧線動畫,此時
B點的角度應該加上PI*2
可以看出情況比較復雜,需要考慮角度的各種情況進行轉換,才能得到正確的結果,所以很多人程序員會陷入其中熱找不到正解。
向量解決正是由于有了這個角度的問題,導致這個動畫實現的難度變大。同事J在經過各種實驗后未能找到好的解決方案,問我如何解決。我看了之后,給出的解決方案是,可以考慮直接用向量的插值,而不是用角度的插值。向量的基本概念,我們在高中就學習過,此處不做詳細說明。
向量解決方案一比如上面的問題,無論是A點到B點,還是A點到C點,都可以用統一的模式解決。首先,我們可以把問題簡化成一個線性運動的問題,比如從A點運動C點,由于是線性問題,這通過向量的插值(0~1)很容易計算出來,首先計算出向量OA,然后計算出向量OC,通過之后可以通過插值運算,計算出中間向量
OX = OA (1-x) + OC (x)
上面的公式計算出來的OX,其長度和OA和OC并不相等,所以點X并不是在圓環上運動。此時只需要通過向量的縮放操作,把OX的長度延長為OA的長度即可。
以下是代碼片段:
var v1 = new Vec3(x1-cx,y1-cy,0), v2 = new Vec3(x2-cx,y2-cy,0); var i = 0,count = 200; function animateVector(){ var a = i / count; var v = new Vec2().lerpVectors(v1,v2,a); v.setLength(r); i ++; if(i > count){ i = 0; } ctx.beginPath(); ctx.moveTo(cx,cy); ctx.lineTo(v.x + cx,v.y + cy); ctx.strokeStyle = "orange"; ctx.stroke(); }
其中Vec2是二維向量類。
當然上面的解決方案有個問題:上面的運動是基于直線均勻運動的,應此并不能保證動畫的角度均勻性。當角度小的時候,這種差異并不大,所以在不嚴格要求角度均勻的情況下,可以不用處理。 而如果角度大的時候,速度差異就會比較大。
如果一定要角度均勻,也是可以做的,可以用到向量的點乘、叉乘知識。首先我們需要學習兩個知識點
向量的點乘簡介向量A( x1,y1)和向量B(x2,y2)的點乘結果如下:
A*B = x1*x2 + y1*y2
向量A點乘向量B的點乘結果的另外一個公式如下:
a * b = |a| * |b| * cosθ
通過該公式可以推導出,兩個向量之間的夾角的計算公式:
cosθ = a * b /( |a| * |b| ) θ = Math.acos(a * b /( |a| * |b| ));
點乘計算出來的夾角的的范圍是在0~PI之間。
向量的叉乘二維向量沒有叉乘,叉乘是針對三維向量的。本文所述的問題,是一個二維的問題 ,但是為了方便使用叉乘來解決問題,把二維問題升級到三維問題,也就是,增加一個z坐標。
向量叉乘的結果叫做向量積,其本身也是一個向量,向量積的定義如下:
模長:(在這里θ表示兩向量之間的夾角(共起點的前提下)(0° ≤ θ ≤ 180°),它位于這兩個矢量所定義的平面上。)
方向:向量A與向量B的向量積的方向與這兩個向量所在平面垂直,且遵守右手定則。(一個簡單的確定滿足“右手定則”的結果向量的方向的方法是這樣的:若坐標系是滿足右手定則的,當右手的四指從A以不超過180度的轉角轉向B時,豎起的大拇指指向是向量C的方向。C = A ∧ B)
。
本文中,向量A和向量B都在xy平面,所以他們的叉乘結果C(向量積)和xy平面垂直,和z坐標平行。其方向和A到B的順序有關:
當A到B是順時針的時候,C指向z軸的負方向。
當A到B是逆時針的時候,C指向z軸的正方向。
有了相關的向量知識,現在給出問題的解決方案,代碼如下:
var v1 = new Vec3(x1-cx,y1-cy,0), v2 = new Vec3(x2-cx,y2-cy,0); var crossVector = new Vec3().crossVectors(v1,v2); var i = 0,count = 100; function animateVector2(){ var a = i / count; var vAngle = v1.angleTo(v2); if(crossVector.z > 0){//通過向量叉乘判斷是逆時針還是順時針,crossVector.z > 0是逆時針 angleEnd = angle1 + vAngle; }else{ angleEnd = angle1 - vAngle; } var angle = (angle1 * (count-i) + angleEnd * (i)) / count; var x = cx + Math.cos(angle) * r, y = cy + Math.sin(angle) * r; ctx.beginPath(); ctx.moveTo(cx,cy); ctx.lineTo(x,y); ctx.strokeStyle = "orange"; ctx.stroke(); i ++; if(i > count){ i = 0; } }
大致步驟如下:
通過三角函數知識,計算出A點的夾角angle1。
通過向量的點乘知識,可以計算出兩個向量之間的夾角vAngle。
通過向量叉乘計算出向量A和向量B的向量積crossVector。
通過crossVector的方向,來判斷向量A到向量B的運動方向是順時針還是逆時針。如果crossVector.z > 0說明是逆時針,反之是順時針。
如果是順時針,通過 angle1 - vAngle計算出角度angleEnd,如果是逆時針,通過 angle1 + vAngle計算出角度angleEnd。
通過在angle1和angleEnd之間進行角度插值來實現動畫效果。
總結: 上面的方法其實還是使用角度的插值來實現動畫效果,所以是角度均勻的動畫。 但是借助了向量工具,讓起始和結束角度的計算變得容易。
向量解決方案三方案一的問題在于,向量A到向量B之間的線性插值是直線均勻的,但是不是角度均勻的。如果我們把線性插值的插值因子改成角度均勻,而仍然使用線性插值的計算方式,就可以解決方案一的問題。這要借助三角函數的知識,先看下圖:
首先通過向量點乘,可以計算出角AOB的夾角vAngle,假定運動的角度為θ,此時運動點在X處,通過三角函數知識可以得到:
AM = MB = OA Math.sin(vAngle/2) = r Math.sin(vAngle/2) ;
其中r為半徑
OM = OA Math.cos(vAngle/2) = r Math.cos(vAngle/2) ;
因此可以算出
XM = OM * Math.tan(vAngle/2 - θ),
最終可以計算出AX的長度為
AX = AM - XM = r Math.sin(vAngle/2) - r Math.cos(vAngle/2) *Math.tan(vAngle/2 - θ)
通過以上計算公式,可以計算出基于角度的線性插值的插值因子 s = AX/AB。 帶入插值因子,結合向量的線性插值即可實現角度均勻的動畫效果,代碼如下:
function animateVector3(){ var a = i / count; var vAngle = v1.angleTo(v2); // 通過向量計算夾角 var stepAngle = a * vAngle; // var halfLength = r * Math.sin(vAngle/2); var stepLength = halfLength - r * Math.cos(vAngle/2)* Math.tan(vAngle/2 - stepAngle); a = stepLength / (halfLength * 2); // 弧線到直線上的映射關系:0.5 - Math.cos(vAngle/2)* Math.tan(vAngle/2 - stepAngle) / ( Math.sin(vAngle/2) * 2) // a = 0.5 - Math.cos(vAngle/2)* Math.tan(vAngle/2 - stepAngle) / ( Math.sin(vAngle/2) * 2); var v = new Vec2().lerpVectors(v1,v2,a); //向量插值 v.setLength(r); i ++; if(i > count){ i = 0; } ctx.beginPath(); ctx.moveTo(cx,cy); ctx.lineTo(v.x + cx,v.y + cy); ctx.strokeStyle = "orange"; ctx.stroke(); }回到角度適配方案
下面這段轉換代碼可以達到角度適配的效果,此處列出代碼,不進行說明,有興趣的讀者,可以自己研究。可以看出,稍顯復雜。
var i = 0,count = 200; var PI = Math.PI; function animateAngle2() { var angleStart,angleEnd; if(Math.sign(angle1) == Math.sign(angle2)){ return animateAngle(); }else{ if(angle1 < 0 && angle1 +2*PI > angle2 + PI){ return animateAngle(); }else if(angle2 < 0 && angle2 +2*PI > angle1 + PI){ return animateAngle(); }else if(angle1 < 0){ angleStart = angle1 + 2 * PI; angleEnd = angle2; }else{ angleStart = angle1; angleEnd = angle2 + 2 * PI; } } var angle = (angleStart * (count-i) + angleEnd * (i)) / count; var x = cx + Math.cos(angle) * r, y = cy + Math.sin(angle) * r; ctx.beginPath(); ctx.moveTo(cx,cy); ctx.lineTo(x,y); ctx.strokeStyle = "red"; ctx.stroke(); i ++; if(i > count){ i = 0; } }球面的情況
上面解決了圓環的情況,如果是球面的情況,如果是通過角度轉換的方式,則非常復雜。
而通過向量的方式:
向量解決方案一和向量解決方案三,可以平滑的移植到球面運動的情況,復雜度并沒有提高。
向量解決方案二,需要做一些的調整,才可以方便的移植到球面的情況,這里面涉及到一些坐標系變換的知識,稍微復雜,此處不講述。 有興趣的同學,可以留言點贊。 如果有很多人希望了解,我會在寫一篇文章來講解這個問題。
當然 如果學過三維的同學一定知道四元數的相關知識,通過四元數可以很方便的實現球面插值,這超過本文的范圍,不講述,有興趣的同學自己了解吧。總結
可以看出:
通過角度轉換的方式來實現圓環或者球面上面的動畫,要適配很多情況,比較復雜。
而通過向量來實現圓環或者球面上面的動畫,會變得簡單和容易理解。
這也是為什么當時同事J自己研究了一上午也沒有做出來,實現的效果,總是一會兒行,一會兒不行。而他在理解了向量的解決方案之后,10分鐘便寫出了健壯的動畫效果代碼。
本文整體代碼關注公眾號留言獲取。
歡迎關注公眾號“ITman彪叔”。彪叔,擁有10多年開發經驗,現任公司系統架構師、技術總監、技術培訓師、職業規劃師。熟悉Java、JavaScript、Python語言,熟悉數據庫。熟悉java、nodejs應用系統架構,大數據高并發、高可用、分布式架構。在計算機圖形學、WebGL、前端可視化方面有深入研究。對程序員思維能力訓練和培訓、程序員職業規劃有濃厚興趣。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/104801.html
摘要:即,把放大為倍時,顯示效果會被拉伸當不設置樣式寬高時,瀏覽器中大小由畫布大小決定在實際開發中,碰到一個例外,是在使用時,繪制的標簽如果只設置畫布大小時,在移動端的瀏覽器上顯示異常,正常。回到圓弧動畫,當前動畫有兩段,以順時針方向這段為例。 效果預覽 showImg(https://segmentfault.com/img/bVbm7UY?w=502&h=304); canvas 繪制基...
摘要:繪制表盤指針對指針的繪制,首先以原點為中心繪制一個圓,對延伸出來的指針思考了兩種繪制方法第一種以軸左半邊為例,點為起始點,以為控制點,為終點繪制三次貝塞爾曲線第二種以軸右半邊為例,直接從點繪制直線到。 不知道大家童年時候有沒有在手上畫手表的經歷,恰好最近在看 canvas ,于是就誕生了這個高仿表盤。 showImg(https://segmentfault.com/img/bV7y...
摘要:無線頁面本就分秒必爭,更不用說當我們在無線頁面中使用動畫的時候。頁面中元素的布局是相對的,因此一個元素的布局發生變化,會聯動地引發其他元素的布局發生變化。它通知瀏覽器在頁面重繪前執行你的回調函數。 無線頁面本就分秒必爭,更不用說當我們在無線頁面中使用動畫的時候。不管是css動畫還是canvas動畫,我們都需要時刻小心著,并且有必要掌握頁面性能的基本分析方法。 既然我們的目標是優化,那么...
摘要:渣渣成品圖最近對于圓形有種特別的感情呢因為寫了個就像到了用來做時鐘大概會比較有趣吧所以就著手寫了個這樣的一個東西大概代碼上錯漏還是蠻多的接下來分享下關于如何開發一個圓形時鐘條吧使用這次就沒有采用的方法來實現圓環了因為我想要做多層嵌套的圓環覺 渣渣成品圖:http://codepen.io/thewindswor... 最近對于圓形有種特別的感情呢...因為寫了個cricle_proce...
摘要:渣渣成品圖最近對于圓形有種特別的感情呢因為寫了個就像到了用來做時鐘大概會比較有趣吧所以就著手寫了個這樣的一個東西大概代碼上錯漏還是蠻多的接下來分享下關于如何開發一個圓形時鐘條吧使用這次就沒有采用的方法來實現圓環了因為我想要做多層嵌套的圓環覺 渣渣成品圖:http://codepen.io/thewindswor... 最近對于圓形有種特別的感情呢...因為寫了個cricle_proce...
閱讀 2006·2021-09-13 10:23
閱讀 2336·2021-09-02 09:47
閱讀 3798·2021-08-16 11:01
閱讀 1220·2021-07-25 21:37
閱讀 1601·2019-08-30 15:56
閱讀 539·2019-08-30 13:52
閱讀 3132·2019-08-26 10:17
閱讀 2447·2019-08-23 18:17