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

中我們講了產品新版本的特點,簡單來說就是三點:
- 使用二維展示方式,展示的資訊更多維,更豐富。
- 使用層級化展示,每個層級有對應的資訊重點,在展示更多資訊的同時,不產生視覺負擔。
- 高手可便捷地自行探索學習路徑,同時也為初學者提供了推薦的學習路徑。
那既然作為一個程式設計師,從本篇文章開始就要剖析產品中用到的技術了。整個產品前後端互動不多,核心在於後端演算法生成資料,和前端酷炫的互動實現兩部分。
演算法過程還涉及到機密啊專利啊等等亂七八糟的事情,不能說的太詳細,但前端部分本身就完全對外公開,所以也談不上技術保護。所以我們會著重對前端的實現部分進行分享和分析。
還沒有體驗過的同學,可以前往集智學園官網體驗後再繼續往下看。
模擬地圖功能
所有的課程以分佈在二維座標系上的點的形式呈現。那就有對檢視在二維平面中上下左右移動的需求。而且為了展示內部細節,還需要支援縮放。本質上就是一個地圖。所以我們首先需要實現地圖的基本互動,移動 + 縮放
之所以不使用google或者百度地圖這類現有的地圖框架,一是因為我們其實只需要地圖的部分互動,其實沒必要引入龐大的地相簿;二是我們希望能更靈活地對這個"地圖"進行自定義開發,後續可能會在現有基礎上增加更多的互動或者元素。
另外地圖元件本質是圖片的分片載入,所以難免在移動和縮放的時候出現中間載入時刻。所以在經過了一段時間的嘗試之後我們放棄了對地相簿的引入。
1. 核心繪圖
整個檢視的組成主要元素是那些課程點,這些點都是繪製在一個canvas上 核心繪圖函式很簡單
drawPoint (point) { ctx.arc(point.x, point.y, point.r, 0, 2 * Math.PI); } 複製程式碼
點位的座標生成是另外的技術話題,大致流程是將課程資訊(包括資料,文字,標籤等)提取出來轉化為高維課程特徵矩陣,再通過聚類和降維技術對映成二維座標。具體實現將另開篇幅。本文針對前端實現方式,不對此展開討論。
2. 引入監聽事件
- 移動功能用到了
mousedown mousestart mouseup
- 縮放功能用到了
dblclick mousewheel DOMMouseScroll
//設定事件 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
事件,這就需要對單純的“滑鼠移動”,和按下後的“拖拽”做一個區分,所以需要 mousedown
和 mouseup
事件的配合,來判斷當前是否為拖拽狀態。
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. 縮放功能
有很多操作會觸發縮放:
- 雙擊地圖
- 滑鼠滾動
- 筆記本觸控板
雙擊觸發 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)

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

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

從這道題中可以看出,要把一個點以某一中心進行縮放,還需要藉助平移的方法,所以講了這麼一堆,可以得出縮放函式應該這麼寫
// 縮放函式 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) scaleToValue(point, value) scaleToRange(range)
由於是完全canvas手擼的地圖,所以完全可以根據需求開發想要的功能,雖然可能一開始如果選擇了地圖框架來實現功能,前期進展肯定會比現在快,但到了後期開發,我相信一定是我們自己的框架更加靈活,更有利於實現我們的想法,而不會被技術所侷限。
本篇主要介紹了地圖的基礎操作 移動
和 縮放
是如何實現的。 在下一篇,我們來介紹一下更加精彩的“視窗”和“路徑”實現。 敬請期待。
