成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

繞圓弧動(dòng)畫的向量解決方式

ybak / 1825人閱讀

摘要:方向向量與向量的向量積的方向與這兩個(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)換適配。

角度的難點(diǎ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

相關(guān)文章

  • 使用canvas繪制圓弧動(dòng)畫

    摘要:即,把放大為倍時(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 繪制基...

    Kyxy 評(píng)論0 收藏0
  • canvas 高仿 Apple Watch 表盤

    摘要:繪制表盤指針對(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...

    Fourierr 評(píng)論0 收藏0
  • 無線頁面動(dòng)畫優(yōu)化實(shí)例

    摘要:無線頁面本就分秒必爭(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)化,那么...

    ivydom 評(píng)論0 收藏0
  • circle_clock 簡(jiǎn)單canvas實(shí)現(xiàn)圓弧時(shí)鐘

    摘要:渣渣成品圖最近對(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...

    boredream 評(píng)論0 收藏0
  • circle_clock 簡(jiǎn)單canvas實(shí)現(xiàn)圓弧時(shí)鐘

    摘要:渣渣成品圖最近對(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...

    寵來也 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<