集智學園知識星空——前端技術實現分析(一)

如意同學發表於2019-03-23

image.png
在上一篇文章集智學園知識星空——產品介紹篇中我們講了產品新版本的特點,簡單來說就是三點:

  1. 使用二維展示方式,展示的資訊更多維,更豐富。
  2. 使用層級化展示,每個層級有對應的資訊重點,在展示更多資訊的同時,不產生視覺負擔。
  3. 高手可便捷地自行探索學習路徑,同時也為初學者提供了推薦的學習路徑。

那既然作為一個程式設計師,從本篇文章開始就要剖析產品中用到的技術了。整個產品前後端互動不多,核心在於後端演算法生成資料,和前端酷炫的互動實現兩部分。

演算法過程還涉及到機密啊專利啊等等亂七八糟的事情,不能說的太詳細,但前端部分本身就完全對外公開,所以也談不上技術保護。所以我們會著重對前端的實現部分進行分享和分析。

還沒有體驗過的同學,可以前往集智學園官網體驗後再繼續往下看。

模擬地圖功能

所有的課程以分佈在二維座標系上的點的形式呈現。那就有對檢視在二維平面中上下左右移動的需求。而且為了展示內部細節,還需要支援縮放。本質上就是一個地圖。所以我們首先需要實現地圖的基本互動,移動 + 縮放

之所以不使用google或者百度地圖這類現有的地圖框架,一是因為我們其實只需要地圖的部分互動,其實沒必要引入龐大的地相簿;二是我們希望能更靈活地對這個"地圖"進行自定義開發,後續可能會在現有基礎上增加更多的互動或者元素。

另外地圖元件本質是圖片的分片載入,所以難免在移動和縮放的時候出現中間載入時刻。所以在經過了一段時間的嘗試之後我們放棄了對地相簿的引入。

1. 核心繪圖

整個檢視的組成主要元素是那些課程點,這些點都是繪製在一個canvas上 核心繪圖函式很簡單

drawPoint (point) {
  ctx.arc(point.x, point.y, point.r, 0, 2 * Math.PI);
}
複製程式碼

點位的座標生成是另外的技術話題,大致流程是將課程資訊(包括資料,文字,標籤等)提取出來轉化為高維課程特徵矩陣,再通過聚類和降維技術對映成二維座標。具體實現將另開篇幅。本文針對前端實現方式,不對此展開討論。

2. 引入監聽事件
  1. 移動功能用到了
  • mousedown, // 滑鼠移動
  • mousestart // 滑鼠點下
  • mouseup // 滑鼠抬起
  1. 縮放功能用到了
  • dblclick // 滑鼠雙擊
  • mousewheel // 滑鼠滾輪
  • DOMMouseScroll // firfox的滑鼠滾輪 設定事件函式,將所有事件繫結在檢視的canvas上
//設定事件
    setHandler(dom) {
      //滑鼠雙擊
      dom.addEventListener( 'dblclick',e => {
          onDocumenDblClick(e, this, false);
        }, { passive: true });
      //滑鼠按下
      dom.addEventListener('mousedown', e => {
          moveDown(e, this, false);
        }, { passive: true });
      //滑鼠移動
      dom.addEventListener('mousemove', e => {
        moveMouse(e, this, point);
      });
      //滑鼠抬起
      dom.addEventListener( 'mouseup', e => {
         moveUP(e, this);
        }, { passive: true });
      //滑鼠滾輪
      dom.onmousewheel = e => { e.stopPropagation();
        mouseScroll(e, this, false);
      };
      // 滑鼠滾輪事件firfox
      dom.addEventListener('DOMMouseScroll', e => {
        mouseScroll(e, this, false);
      });
    },
複製程式碼

設定好事件後,就是地圖功能實現的核心了。移動 + 縮放

3. 拖拽移動功能

移動主要監聽mousemove事件,這就需要對單純的“滑鼠移動”,和按下後的“拖拽”做一個區分,所以需要mousedownmouseup事件的配合,來判斷當前是否為拖拽狀態。

let dragFlag = false; // 拖拽標識
   /*滑鼠點下事件   @param {*} e event */
  moveDown (e) => {
    dragFlag = true; // 滑鼠被按下,準備拖拽
  }
  /*滑鼠抬起事件   @param {*} e event */
  moveUP (e) => {
    dragFlag = false; //結束拖拽標識
  },
  /** 拖拽事件  @param {*} e event */
  moveMouse (e) => {
    if (dragFlag) {
      ...
      transform(x, y);  // x, y為地圖移動的距離
    }
  },

複製程式碼

至於拖拽的距離,則取決於上一時刻的位置,和當前位置的差值。所以在移動的過程中,需要去記錄上一時刻的位置。初始位置,為滑鼠按下的位置

let lastPointPos = [];
// 滑鼠按下
 moveDown (e) => {
    dragFlag = true; // 滑鼠被按下,準備拖拽
    lastPointPos = [e.clientX, e.clientY]
  }
// 滑鼠拖拽
 moveMouse (e) => {
   if (dragFlag) {
     let x = e.clientX - lastPoint[0];
     let y = e.clientY - lastPoint[1];
     lastPoint = [e.clientX, e.clientY];
     transform(x, y);
  }
}
複製程式碼

這樣一來, transform函式就能專注實現移動點位

//  移動點位函式
transform (x, y) => {
    this.x = this.x + x;
    this.y = this.y + y
    drawPoint();
  })
}
複製程式碼

到這裡,拖拽移動地圖的功能基本完成

接下去,我們來說一說稍微複雜的縮放操作。

4. 縮放功能

有很多操作會觸發縮放:

  1. 雙擊地圖
  2. 滑鼠滾動
  3. 筆記本觸控板

雙擊觸發dbclick事件 滑鼠滾動和觸控板的行為基本一致,都是觸發滑鼠滾輪mousewheel(firfox觸發的是DOMMouseScroll事件)

// 雙擊事件
onDocumenDblClick (e) => {
 ...
  let flag = 'large';
  scale(x, y, flag)  // scale為縮放函式,傳入縮放中心,和放大還是縮小標誌
}
// 滾動事件
mouseScroll (e) => {
  ...
  scale(x, y, flag)  // scale為縮放函式,傳入縮放中心,和放大還是縮小標誌
}
複製程式碼

因為每次雙擊的縮放尺度,和每次滾輪的縮放尺度,顯然是不一樣的。所以兩個行為的縮放倍數。肯定不一樣。我們可以設定,每觸發一次雙擊事件,就相當於觸發了n次的scale(n為一個自定義的引數), 即

onDocumenDblClick (e) => {
  ...
  let flag = 'large';
  let count = 0;
  let time = setInterval(() => {
  if (count <= n) {
      scale(x, y, flag)  // scale為縮放函式,傳入縮放中心,和放大還是縮小標誌
    } else {
      clearInterval(time)  
    }
  }, 100)
}
複製程式碼

這麼寫當然可以實現功能,但是一點都不優雅,而且使用setInterval做動畫對瀏覽器來說並不是一個最佳的渲染方案,點位多的時候容易有失幀現象。這裡鑽一下細節,使用requestAnimationFrame改寫下。

let scaleStartTime = 0; // 開始放大的起始時間
// 雙擊事件
onDocumenDblClick (e) => {
  ...
  let flag = 'large';
  scaleStartTime = performance.now();
  scaleOnceAnimation(e,  time,  flag);  //  time是自定義引數,自行設定動畫要執行的時間。
}
// 迴圈動畫
scaleOnceAnimation (e, time, flag) => {
   // 使用當前時間和起始時間做對比,每次迴圈都判斷是否已經達到設定的動畫執行時間。
   if (performance.now() - scaleStartTime > time) {
     scaleStartTime = 0;
      return;
    }
    scale(x,  y,  flag);
    window.requestAnimationFrame(() => {
      scaleOnceAnimation(e, time, flag);
    });
}
複製程式碼

最後就是scale函式的實現。在直接寫程式碼之前,我們先來做個簡單的數學題。

以p(1, 1)為中心,把圓(2, 2, r = 1)放大為原來的兩倍,求圓放大後的座標和半徑

初始狀態

第一步,移動整個座標,直至p位於(0, 0)點,此時圓座標為(1, 1, r = 1)

1
第二步,放大整個座標系至相應倍數,這裡為2倍, 得到圓(2, 2, r = 2)
2

第三步,把座標系移回原來的位置,讓p回到初始點,得到圓(3, 3, r = 2)

3

從這道題中可以看出,要把一個點以某一中心進行縮放,還需要藉助平移的方法,所以講了這麼一堆,可以得出縮放函式應該這麼寫

// 縮放函式
scale (x, y, flag) => {
  let scale = flag === 'large' ? 110 / 100 ? 100 / 110; // 縮放比例
  transform(-x, -y);
  this.x = this.x * scale;
  this.y = this.y * scale;
  transform(x, y);
  this.drawPoint()
  })
}
複製程式碼

到此為止,縮放的功能就也已經基本實現。一個模擬地圖行為的產品也已經實現了最核心的功能。

在此基礎上,我們還可以模擬其他衍伸功能,比如:

  • viewPort (pointArray):把傳入的點放置於檢視中合適的位置;
  • panTo (x, y):把檢視移動到某個位置,並以傳入的座標為檢視中心(或任何一個你想要的位置點)
  • openWindow (point) :開啟點位的資訊視窗 除了模擬地圖API的基本功能以外,還能根據需求開發自己的地圖新功能
  • scaleToValue(point, value):對某個點移動到檢視中心,並放大到指定大小
  • scaleToRange(range) :縮放地圖,直到滿足傳入到檢視範圍內 ....

由於是完全canvas手擼的地圖,所以完全可以根據需求開發想要的功能,雖然可能一開始如果選擇了地圖框架來實現功能,前期進展肯定會比現在快,但到了後期開發,我相信一定是我們自己的框架更加靈活,更有利於實現我們的想法,而不會被技術所侷限。

本篇主要介紹了地圖的基礎操作移動縮放是如何實現的。 在下一篇,我們來介紹一下更加精彩的“視窗”開啟的過程,期間涉及到panTo函式的實現,即把檢視移動到選中點為中心的狀態。 敬請期待。

集智學園知識星空——前端技術實現分析(一)

相關文章