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