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

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

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

那既然做爲一個程序員,從本篇文章開始就要剖析產品中用到的技術了。整個產品先後端交互很少,核心在於後端算法生成數據,和前端酷炫的交互實現兩部分。前端

算法過程還涉及到機密啊專利啊等等亂七八糟的事情,不能說的太詳細,但前端部分自己就徹底對外公開,因此也談不上技術保護。因此咱們會着重對前端的實現部分進行分享和分析。程序員

尚未體驗過的同窗,能夠前往集智學園官網體驗後再繼續往下看。算法

模擬地圖功能

全部的課程以分佈在二維座標系上的點的形式呈現。那就有對視圖在二維平面中上下左右移動的需求。並且爲了展現內部細節,還須要支持縮放。本質上就是一個地圖。因此咱們首先須要實現地圖的基本交互,移動 + 縮放canvas

之因此不使用google或者百度地圖這類現有的地圖框架,一是由於咱們其實只須要地圖的部分交互,其實不必引入龐大的地圖庫;二是咱們但願能更靈活地對這個"地圖"進行自定義開發,後續可能會在現有基礎上增長更多的交互或者元素。後端

另外地圖組件本質是圖片的分片加載,因此不免在移動和縮放的時候出現中間加載時刻。因此在通過了一段時間的嘗試以後咱們放棄了對地圖庫的引入。瀏覽器

1. 核心繪圖

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

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);
      });
    },
複製代碼

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

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函數的實現,即把視圖移動到選中點爲中心的狀態。 敬請期待。

相關文章
相關標籤/搜索