那既然做爲一個程序員,從本篇文章開始就要剖析產品中用到的技術了。整個產品先後端交互很少,核心在於後端算法生成數據,和前端酷炫的交互實現兩部分。前端
算法過程還涉及到機密啊專利啊等等亂七八糟的事情,不能說的太詳細,但前端部分自己就徹底對外公開,因此也談不上技術保護。因此咱們會着重對前端的實現部分進行分享和分析。程序員
尚未體驗過的同窗,能夠前往集智學園官網體驗後再繼續往下看。算法
全部的課程以分佈在二維座標系上的點的形式呈現。那就有對視圖在二維平面中上下左右移動的需求。並且爲了展現內部細節,還須要支持縮放。本質上就是一個地圖。因此咱們首先須要實現地圖的基本交互,移動 + 縮放canvas
之因此不使用google或者百度地圖這類現有的地圖框架,一是由於咱們其實只須要地圖的部分交互,其實不必引入龐大的地圖庫;二是咱們但願能更靈活地對這個"地圖"進行自定義開發,後續可能會在現有基礎上增長更多的交互或者元素。後端
另外地圖組件本質是圖片的分片加載,因此不免在移動和縮放的時候出現中間加載時刻。因此在通過了一段時間的嘗試以後咱們放棄了對地圖庫的引入。瀏覽器
整個視圖的組成主要元素是那些課程點,這些點都是繪製在一個canvas上 核心繪圖函數很簡單bash
drawPoint (point) {
ctx.arc(point.x, point.y, point.r, 0, 2 * Math.PI);
}
複製代碼
點位的座標生成是另外的技術話題,大體流程是將課程信息(包括資料,文本,標籤等)提取出來轉化爲高維課程特徵矩陣,再經過聚類和降維技術映射成二維座標。具體實現將另開篇幅。本文針對前端實現方式,不對此展開討論。框架
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);
});
},
複製代碼
設置好事件後,就是地圖功能實現的核心了。移動 + 縮放dom
移動主要監聽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();
})
}
複製代碼
到這裏,拖拽移動地圖的功能基本完成
接下去,咱們來講一說稍微複雜的縮放操做。
有不少操做會觸發縮放:
雙擊觸發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)
第三步,把座標系移回原來的位置,讓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
函數的實現,即把視圖移動到選中點爲中心的狀態。 敬請期待。