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