js 實現多重羅盤轉動

AdityaSui發表於2017-11-17

引子

這幾天一直在忙一個可滑動的轉盤的demo,網上也有類似的例子,但是根據老闆的需求來改他們的程式碼,還不如重新寫個完全符合需求的外掛。
想法很美好,但是新手上路...

效果連結文末

需求

image
image

這個demo給的非常簡單,能轉動的地方有三處,內盤、外盤和指標,這三個上的集合的交集產生一個連結,通過中間的按鈕跳轉。

這個需求乍一看老簡單老簡單的,但是作為一個菜雞第一次上道,堪比開碰碰車,頭破血流。

分析

在做之前,也是根據自己的理解來寫的旋轉角度問題:

  • 轉盤轉動的做法是:設定圓心為轉動原點,動態的修改旋轉角度;
  • 在touchmove 計算兩點與中心點的角度。

在旋轉上大體上需要明白的也就這兩點,但是在實際計算角度上卻有很多問題。

彎道1之計算角度

計算角度首先要用到的一個數學方法就是反函式,在JS中表示反函式的方法有兩個:

  • Math.atan
  • Math.atan2

說實話它們兩個的區別對於本次demo真沒有測出什麼差異來,但是相比 atan在y特別大的時候會有誤差產生的情況下,果斷選擇了atan2

(function($){
        $.fn.CompassRotate=function(options){
            var defaults={
                trigger:document,           
                centerX:0,                     
                centerY:0,                      
                debug:false
            },_this=this;
            var ops=$.extend(defaults,options);
            function Init(){
                //初始化圓心點
                if(ops.centerX==0 && ops.centerY==0){
                    ops.centerX=_this.offset().left+_this.width()/2;
                    ops.centerY=_this.offset().top+_this.height()/2
                }
                $(ops.trigger).on("touchstart",function(event){
                    $(document).on("touchmove",movehandle);
                });
                $(ops.trigger).on("touchend",function(event) {
                    $(document).unbind("touchmove");
                });
            }
            //滑鼠移動時處理事件
            function movehandle(event){
                var touch = event.originalEvent.targetTouches[0];
                var dis = angle(ops.centerX,ops.centerY,touch.pageX, touch.pageY);

                if(ops.debug) console.log(ops.centerX+"-"+ops.centerY+"|"+touch.pageX+"-"+touch.pageY+" "+dis);

                rotate(dis);
            }
            //計算兩點的線在頁面中的角度
            function angle(centerx, centery, endx, endy) {
                var diff_x = endx - centerx,
                    diff_y = endy - centery;
                var c=360 * Math.atan2(diff_y , diff_x) / (2 * Math.PI);
                c=c<=0?(360+c):c;

                return c; 
            }
            //設定角度
            function rotate(angle,step){
                $(_this).css("transform", "translate3d(-50%,-50%,0) rotateZ(" + angle + "deg)");
            }
            // 指標指向角度變化和生成url
            function angleOrLink(angle) {
                Angle = angle;
            }
            Init();
        };
    })(jQuery);
    $(".box").CompassRotate({trigger:$(".box"),debug:true});複製程式碼

囉裡囉嗦不如直接貼上程式碼,大家看得更明白些。

彎道2之區域集合變化

做過轉盤抽獎的大佬都知道,每個獎品都對應一個角度集合,指標所轉的角度[0,360]看看對應落在哪個集合上,而這個轉盤也是同理,但是唯一不同的地方在於,內盤和外盤的集合是可變化的,並不是固定不變的。

var insideCollection = [
    {
        /* GC+S1 */
        min: 270,
        max: 360,
        reverse: false,
        mark: 's1gc'
    },
    {
        /* BC+AT */
        min: 0,
        max: 45,
        reverse: false,
        mark: 'bcat'
    },
    {
        /* BC+GT */
        min: 45,
        max: 90,
        reverse: false,
        index: 'bcgt'
    },
    {
        /* mCRC+FOLFOX */
        min: 90,
        max: 180,
        reverse: false,
        mark: 'mCRC'
    },
    {
        /* eCRC+化療 */
        min: 225,
        max: 270,
        reverse: false,
        mark: 'eCRC1'
    },
    {
        /* eCRC+FOLFOX */
        min: 180,
        max: 225,
        reverse: false,
        mark: 'eCRC2'
    }

];
var outsideCollection = [
    {
        /* 研究 */
        min: 270,
        max: 342,
        reverse: false,
        mark: '研究'
    },
    {
        /* 指南 */
        min: 342,
        max: 54,
        reverse: true,
        mark: '指南'
    },
    {
        /* 競品 */
        min: 54,
        max: 126,
        reverse: false,
        mark: '競品'

    },
    {
        /* 資料 */
        min: 126,
        max: 198,
        reverse: false,
        mark: '資料'
    },
    {
        /* 機制 */
        min: 198,
        max: 270,
        reverse: false,
        mark: '機制'
    }

];複製程式碼

min,max不用說了,就是表示集合,reverse 這個屬性代表的是什麼呢?
在做區間劃分的時候,角度的變化永遠都是0-360°,“0==360”。所以,當某個集合的區間是[340,25]的時候該怎麼表示呢?
當然,每次轉動都有且只有一個集合會面臨這樣的情況,所以我用一個屬性來表示這個區間跨角度了。

// 轉盤區間分佈變化
function collectionChange(angle,array) {
    array.forEach(function (ele,index) {
        ele.reverse = false;
    });
    array.forEach(function (ele,index) {
        ele.min = (Number(angle)+Number(ele.min))%360;
        ele.max = (Number(angle)+Number(ele.max))%360;
        if(ele.min > ele.max){
            ele.reverse = true;
        }
    });
    console.log(array)
}複製程式碼

mark 也不用多談,選中了集合該表示表示了呀。

程式碼貼到這也基本完成了大體功能,最後也是在點選連結的時候根據內外盤的 mark 來匹配連結了:

$('#compass_5').on('click',function(){
    var angle = Angle;
    // 內盤標號
    var link = contrast(insideCollection) + contrast(outsideCollection);
    console.log(link);
    function contrast(array){
        var link ;
        array.forEach(function (ele,index) {
            if(angle >= ele.min%360 && angle <= (ele.max%360 ==0?360:ele.max%360)){
                link = ele.mark;
            }
            else if(ele.reverse){
                if(angle<=360 && angle >=270){
                    if(angle >= ele.min%360 && angle <= (ele.max%360 ==0?360:ele.max%360+360)){
                        link = ele.mark;
                    }
                }
                else if(angle>=0&&angle<=90){
                    if(angle+360 >= ele.min%360 && angle+360 <= (ele.max%360 ==0?360:ele.max%360+360)){
                        link = ele.mark;
                    }
                }
            }
        });
        return link;
    }
})複製程式碼

彎道3之坑王之王

上面說到功能大體完成了,那只是按部就班的在輪盤上只選擇一個點進行轉動,如果在不同位置多次轉動,發現整個轉盤癱瘓了——mark對應不上了。

做這個demo第一步,我是從一個簡單的指標轉盤開始起手的,也就是完成一個轉動指標的基本操作,所以整套流程下來是可行的,因為這個指標訂好了轉動圓心,它的可選區域僅僅是辣麼一小塊,所以根本看不到為後面埋了多大坑。

反函式計算角度問題

var c=360 * Math.atan2(diff_y , diff_x) / (2 * Math.PI); 
// c [-180,180];
c=c<=0?(360+c):c;
// c [0,360];複製程式碼

這樣計算角度對於指標來說,沒什麼問題,但是對於轉盤上來說可能就是個噩夢。

因為它的著落點並不確定。

導致當你點到不同區域的時候,它會給你直接將轉動的角度賦值,所以會造永遠是中間那條線跟著手指滑動。

image
image

坑王之王連結(chrome偵錯程式裡檢視)

這樣的操作遇到的坑就是起始位置隨著手指的變動會導致各個區域的區間也應該發生相應的變化,所以在 touchstart 還要進行一步操作,計算上一次結束位置與目前位置的夾角,然後再次更改區間變化。

$(ops.trigger).on("touchstart",function(event){
    var touch = event.originalEvent.changedTouches[0];
    var dis = angle(ops.centerX,ops.centerY,touch.pageX, touch.pageY);
    startAngle = dis;
    //再次滑動轉盤後的角度與上一次結束角度不一致的情況(內盤)
    if(startAngle != ops.initAngle_in && ops.initAngle_in != 0){
        if(ops.initAngle_in>startAngle){
            insideDishAngleChangeSecondary((Number(startAngle+360)-ops.initAngle_in));
        }
        else if(ops.initAngle_in< startAngle){
            insideDishAngleChangeSecondary((startAngle-ops.initAngle_in));
        }

    }
    $(document).on("touchmove",movehandle);
});複製程式碼

修改後的羅盤

上一版的羅盤基本操作是將錯就錯,產生了一系列bug,雖然都克服了一系列bug,但還是都是在挖坑,只不過坑是平行挪動,這個坑挖不動了換了個方向繼續挖而已。

岔路

重新審視自己的思路時,才發現自己是多麼的蠢。

之前的演算法是手指指在哪裡,開始點為0,結束點為所指點 ,在 touchmove 給羅盤賦角度值時,直接將兩點形成的角度賦給了羅盤 rotate(angle)。之後的一系列操作都是為這個地方買單,無論是重新寫個函式記錄變換角度在 touchmove 開始之前賦給羅盤分佈區間、還是中心點僵硬隨著手指轉動。

重新思考了下羅盤的轉法,有了之前的鋪墊,所以思路也變得特別清晰了。
實現這個需求,記錄的資料一共有三個:

  • actual_angle :開始點和結束點與中心點的夾角,這就是羅盤每次轉動的度數,該值需要累加;
  • addAngle :每次轉動結束後,需要給羅盤分佈區間增加的值,該值等同於 actual_angle
  • startAngletouchstart 時手指著落點,即開始點。
$.fn.RotateH=function(options){
    var defaults={
        trigger:document,           
        centerX:0,                      
        centerY:0,                      
        debug:false
    },_this=this;
    var ops=$.extend(defaults,options);
    var startAngle,addAngle,
        actual_angle = 0;
    //初始化
    function Init(){
        //初始化圓心點
        if(ops.centerX==0 && ops.centerY==0){
            ops.centerX=_this.offset().left+_this.width()/2;
            ops.centerY=_this.offset().top+_this.height()/2
        }
        $(ops.trigger).on("touchstart",function(event){
            var touch = event.originalEvent.changedTouches[0];
            var dis = angle(ops.centerX,ops.centerY,touch.pageX, touch.pageY);
            startAngle = dis;
            $(document).on("touchmove",movehandle);
        });
        $(ops.trigger).on("touchend",function(event) {

            var touch = event.originalEvent.changedTouches[0];
            var dis = angle(ops.centerX,ops.centerY,touch.pageX, touch.pageY);

            //每次轉動的角度
            if(dis >=startAngle){
                //羅盤累加轉動度數
                actual_angle += (dis-startAngle);
                //區間每次增加度數
                addAngle = (dis-startAngle);
            }
            else if(dis <startAngle){
                actual_angle += (dis+360-startAngle);
                addAngle = (dis+360-startAngle)
            }
            if(ops.collection) collectionChange(addAngle,ops.collection);
            else angleOrLink(dis);
            $(document).unbind("touchmove");
        });
    }
    //滑鼠移動時處理事件
    function movehandle(event){

        // 獲取兩點之間角度
        var touch = event.originalEvent.targetTouches[0];
        var dis = angle(ops.centerX,ops.centerY,touch.pageX, touch.pageY);
        var Angle = 0;

        if(ops.debug) console.log(ops.centerX+"-"+ops.centerY+"|"+touch.pageX+"-"+touch.pageY+" "+dis);

        if(ops.pointer){
            rotate(dis);
        }
        else {
            //每次轉動的角度
            if(ops.debug) {
                console.log("——————————————————————");
                console.log('上次轉動的角度:'+actual_angle);
            }
            if(dis >=startAngle){
                Angle = dis-startAngle;
                if(ops.debug) {
                    console.log("轉動角度:"+Angle);
                    console.log("實際轉動角度:"+(Angle+actual_angle));
                }
                rotate((Angle+actual_angle));
            }
            else if(dis <startAngle){
                Angle = dis-startAngle+360;
                if(ops.debug){
                    console.log("轉動角度:" + Angle);
                    console.log("實際轉動角度:"+(Angle+actual_angle));
                }
                rotate((Angle+actual_angle));
            }
        }
    }
    //計算兩點的線在頁面中的角度
    function angle(centerx, centery, endx, endy) {
        var diff_x = endx - centerx,
            diff_y = endy - centery;
        var c=360 * Math.atan2(diff_y , diff_x) / (2 * Math.PI);
        c=c<=0?(360+c):c;

        return c;
    }
    //設定角度
    function rotate(angle,step){
        $(_this).css("transform", "translate3d(-50%,-50%,0) rotateZ(" + angle + "deg)");
    }
    // 轉盤區間分佈變化
    function collectionChange(angle,array) {
        array.forEach(function (ele,index) {
            ele.reverse = false;
        });
        array.forEach(function (ele,index) {
            ele.min = (Number(angle)+Number(ele.min))%360;
            ele.max = (Number(angle)+Number(ele.max))%360;
            if(ele.min > ele.max){
                ele.reverse = true;
            }
        });
        if(ops.debug) console.log(array);
    }
    // 指標所轉角度
    function angleOrLink(angle) {
        Angle = angle;
    }
    Init();
};複製程式碼

效果連結地址:perfectCompass.github.io (這是個ipad demo,請在chrome偵錯程式檢視)

github 地址:github.com/suiyang1714…

總結

這個demo最終是自己靠時間磨出來了的,沒有特別高的技術含量,主要是在這個過程中思考。如果一開始想明白了每一步要幹什麼,也不會拐那麼多的彎道了。
我一開始的想法是,羅盤先能轉動,然後再考慮的區間變化,出現問題解決解決問題,沒有看到為什麼會出現這個問題。基本是走一步看一步。心好累。

相關文章