“集智知識(shí)星空”產(chǎn)品技術(shù)解剖(一)

在上一篇文章開創(chuàng)史無前例的在線學(xué)習(xí)方式——“集智知識(shí)星空”產(chǎn)品技術(shù)解剖(一)中我們講了產(chǎn)品新版本的特點(diǎn),簡單來說就是三點(diǎn):

  1. 使用二維展示方式,展示的信息更多維,更豐富。
  2. 使用層級(jí)化展示,每個(gè)層級(jí)有對應(yīng)的信息重點(diǎn),在展示更多信息的同時(shí),不產(chǎn)生視覺負(fù)擔(dān)。
  3. 高手可便捷地自行探索學(xué)習(xí)路徑,同時(shí)也為初學(xué)者提供了推薦的學(xué)習(xí)路徑。

那既然作為一個(gè)程序員,從本篇文章開始就要剖析產(chǎn)品中用到的技術(shù)了。整個(gè)產(chǎn)品前后端交互不多,核心在于后端算法生成數(shù)據(jù),和前端酷炫的交互實(shí)現(xiàn)兩部分。

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

還沒有體驗(yàn)過的同學(xué),可以前往集智學(xué)園官網(wǎng)體驗(yàn)后再繼續(xù)往下看。

模擬地圖功能

所有的課程以分布在二維坐標(biāo)系上的點(diǎn)的形式呈現(xiàn)。那就有對視圖在二維平面中上下左右移動(dòng)的需求。而且為了展示內(nèi)部細(xì)節(jié),還需要支持縮放。本質(zhì)上就是一個(gè)地圖。所以我們首先需要實(shí)現(xiàn)地圖的基本交互,移動(dòng) + 縮放

之所以不使用google或者百度地圖這類現(xiàn)有的地圖框架,一是因?yàn)槲覀兤鋵?shí)只需要地圖的部分交互,其實(shí)沒必要引入龐大的地圖庫;二是我們希望能更靈活地對這個(gè)"地圖"進(jìn)行自定義開發(fā),后續(xù)可能會(huì)在現(xiàn)有基礎(chǔ)上增加更多的交互或者元素。

另外地圖組件本質(zhì)是圖片的分片加載,所以難免在移動(dòng)和縮放的時(shí)候出現(xiàn)中間加載時(shí)刻。所以在經(jīng)過了一段時(shí)間的嘗試之后我們放棄了對地圖庫的引入。

1. 核心繪圖

整個(gè)視圖的組成主要元素是那些課程點(diǎn),這些點(diǎn)都是繪制在一個(gè)canvas上
核心繪圖函數(shù)很簡單

drawPoint (point) {
  ctx.arc(point.x, point.y, point.r, 0, 2 * Math.PI);
}

點(diǎn)位的坐標(biāo)生成是另外的技術(shù)話題,大致流程是將課程信息(包括資料,文本,標(biāo)簽等)提取出來轉(zhuǎn)化為高維課程特征矩陣,再通過聚類和降維技術(shù)映射成二維坐標(biāo)。具體實(shí)現(xiàn)將另開篇幅。本文針對前端實(shí)現(xiàn)方式,不對此展開討論。

2. 引入監(jiān)聽事件
  1. 移動(dòng)功能用到了
  • mousedown, // 鼠標(biāo)移動(dòng)
  • mousestart // 鼠標(biāo)點(diǎn)下
  • mouseup // 鼠標(biāo)抬起
  1. 縮放功能用到了
  • dblclick // 鼠標(biāo)雙擊
  • mousewheel // 鼠標(biāo)滾輪
  • DOMMouseScroll // firfox的鼠標(biāo)滾輪
    設(shè)置事件函數(shù),將所有事件綁定在視圖的canvas上
//設(shè)置事件
    setHandler(dom) {
      //鼠標(biāo)雙擊
      dom.addEventListener( 'dblclick',e => {
          onDocumenDblClick(e, this, false);
        }, { passive: true });
      //鼠標(biāo)按下
      dom.addEventListener('mousedown', e => {
          moveDown(e, this, false);
        }, { passive: true });
      //鼠標(biāo)移動(dòng)
      dom.addEventListener('mousemove', e => {
        moveMouse(e, this, point);
      });
      //鼠標(biāo)抬起
      dom.addEventListener( 'mouseup', e => {
         moveUP(e, this);
        }, { passive: true });
      //鼠標(biāo)滾輪
      dom.onmousewheel = e => { e.stopPropagation();
        mouseScroll(e, this, false);
      };
      // 鼠標(biāo)滾輪事件firfox
      dom.addEventListener('DOMMouseScroll', e => {
        mouseScroll(e, this, false);
      });
    },

設(shè)置好事件后,就是地圖功能實(shí)現(xiàn)的核心了。移動(dòng) + 縮放

3. 拖拽移動(dòng)功能

移動(dòng)主要監(jiān)聽mousemove事件,這就需要對單純的“鼠標(biāo)移動(dòng)”,和按下后的“拖拽”做一個(gè)區(qū)分,所以需要mousedownmouseup事件的配合,來判斷當(dāng)前是否為拖拽狀態(tài)。

let dragFlag = false; // 拖拽標(biāo)識(shí)
   /*鼠標(biāo)點(diǎn)下事件   @param {*} e event */
  moveDown (e) => {
    dragFlag = true; // 鼠標(biāo)被按下,準(zhǔn)備拖拽
  }
  /*鼠標(biāo)抬起事件   @param {*} e event */
  moveUP (e) => {
    dragFlag = false; //結(jié)束拖拽標(biāo)識(shí)
  },
  /** 拖拽事件  @param {*} e event */
  moveMouse (e) => {
    if (dragFlag) {
      ...
      transform(x, y);  // x, y為地圖移動(dòng)的距離
    }
  },

至于拖拽的距離,則取決于上一時(shí)刻的位置,和當(dāng)前位置的差值。所以在移動(dòng)的過程中,需要去記錄上一時(shí)刻的位置。初始位置,為鼠標(biāo)按下的位置

let lastPointPos = [];
// 鼠標(biāo)按下
 moveDown (e) => {
    dragFlag = true; // 鼠標(biāo)被按下,準(zhǔn)備拖拽
    lastPointPos = [e.clientX, e.clientY]
  }
// 鼠標(biāo)拖拽
 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函數(shù)就能專注實(shí)現(xiàn)移動(dòng)點(diǎn)位

//  移動(dòng)點(diǎn)位函數(shù)
transform (x, y) => {
    this.x = this.x + x;
    this.y = this.y + y
    drawPoint();
  })
}

到這里,拖拽移動(dòng)地圖的功能基本完成

接下去,我們來說一說稍微復(fù)雜的縮放操作。

4. 縮放功能

有很多操作會(huì)觸發(fā)縮放:

  1. 雙擊地圖
  2. 鼠標(biāo)滾動(dòng)
  3. 筆記本觸控板

雙擊觸發(fā)dbclick事件
鼠標(biāo)滾動(dòng)和觸控板的行為基本一致,都是觸發(fā)鼠標(biāo)滾輪mousewheel(firfox觸發(fā)的是DOMMouseScroll事件)

// 雙擊事件
onDocumenDblClick (e) => {
 ...
  let flag = 'large';
  scale(x, y, flag)  // scale為縮放函數(shù),傳入縮放中心,和放大還是縮小標(biāo)志
}
// 滾動(dòng)事件
mouseScroll (e) => {
  ...
  scale(x, y, flag)  // scale為縮放函數(shù),傳入縮放中心,和放大還是縮小標(biāo)志
}

因?yàn)槊看坞p擊的縮放尺度,和每次滾輪的縮放尺度,顯然是不一樣的。所以兩個(gè)行為的縮放倍數(shù)??隙ú灰粯?。我們可以設(shè)置,每觸發(fā)一次雙擊事件,就相當(dāng)于觸發(fā)了n次的scale(n為一個(gè)自定義的參數(shù)), 即

onDocumenDblClick (e) => {
  ...
  let flag = 'large';
  let count = 0;
  let time = setInterval(() => {
  if (count <= n) {
      scale(x, y, flag)  // scale為縮放函數(shù),傳入縮放中心,和放大還是縮小標(biāo)志
    } else {
      clearInterval(time)  
    }
  }, 100)
}

這么寫當(dāng)然可以實(shí)現(xiàn)功能,但是一點(diǎn)都不優(yōu)雅,而且使用setInterval做動(dòng)畫對瀏覽器來說并不是一個(gè)最佳的渲染方案,點(diǎn)位多的時(shí)候容易有失幀現(xiàn)象。這里鉆一下細(xì)節(jié),使用requestAnimationFrame改寫下。

let scaleStartTime = 0; // 開始放大的起始時(shí)間
// 雙擊事件
onDocumenDblClick (e) => {
  ...
  let flag = 'large';
  scaleStartTime = performance.now();
  scaleOnceAnimation(e,  time,  flag);  //  time是自定義參數(shù),自行設(shè)置動(dòng)畫要運(yùn)行的時(shí)間。
}
// 循環(huán)動(dòng)畫
scaleOnceAnimation (e, time, flag) => {
   // 使用當(dāng)前時(shí)間和起始時(shí)間做對比,每次循環(huán)都判斷是否已經(jīng)達(dá)到設(shè)置的動(dòng)畫運(yùn)行時(shí)間。
   if (performance.now() - scaleStartTime > time) {
     scaleStartTime = 0;
      return;
    }
    scale(x,  y,  flag);
    window.requestAnimationFrame(() => {
      scaleOnceAnimation(e, time, flag);
    });
}

最后就是scale函數(shù)的實(shí)現(xiàn)。在直接寫代碼之前,我們先來做個(gè)簡單的數(shù)學(xué)題。

以p(1, 1)為中心,把圓(2, 2, r = 1)放大為原來的兩倍,求圓放大后的坐標(biāo)和半徑

初始狀態(tài)

第一步,移動(dòng)整個(gè)坐標(biāo),直至p位于(0, 0)點(diǎn),此時(shí)圓坐標(biāo)為(1, 1, r = 1)


1

第二步,放大整個(gè)坐標(biāo)系至相應(yīng)倍數(shù),這里為2倍, 得到圓(2, 2, r = 2)


2

第三步,把坐標(biāo)系移回原來的位置,讓p回到初始點(diǎn),得到圓(3, 3, r = 2)


3

從這道題中可以看出,要把一個(gè)點(diǎn)以某一中心進(jìn)行縮放,還需要借助平移的方法,所以講了這么一堆,可以得出縮放函數(shù)應(yīng)該這么寫

// 縮放函數(shù)
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()
  })
}

到此為止,縮放的功能就也已經(jīng)基本實(shí)現(xiàn)。一個(gè)模擬地圖行為的產(chǎn)品也已經(jīng)實(shí)現(xiàn)了最核心的功能。

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

  • viewPort (pointArray):把傳入的點(diǎn)放置于視圖中合適的位置;
  • panTo (x, y):把視圖移動(dòng)到某個(gè)位置,并以傳入的坐標(biāo)為視圖中心(或任何一個(gè)你想要的位置點(diǎn))
  • openWindow (point) :打開點(diǎn)位的信息窗口
    除了模擬地圖API的基本功能以外,還能根據(jù)需求開發(fā)自己的地圖新功能
  • scaleToValue(point, value):對某個(gè)點(diǎn)移動(dòng)到視圖中心,并放大到指定大小
  • scaleToRange(range) :縮放地圖,直到滿足傳入到視圖范圍內(nèi)
    ....

由于是完全canvas手?jǐn)]的地圖,所以完全可以根據(jù)需求開發(fā)想要的功能,雖然可能一開始如果選擇了地圖框架來實(shí)現(xiàn)功能,前期進(jìn)展肯定會(huì)比現(xiàn)在快,但到了后期開發(fā),我相信一定是我們自己的框架更加靈活,更有利于實(shí)現(xiàn)我們的想法,而不會(huì)被技術(shù)所局限。

本篇主要介紹了地圖的基礎(chǔ)操作移動(dòng)縮放是如何實(shí)現(xiàn)的。
在下一篇,我們來介紹一下更加精彩的“窗口”和“路徑”實(shí)現(xiàn)。
敬請期待。

20190322090155-da5a5d4806.[gif-2-mp4.com].gif
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,694評論 4 61
  • 1 MAP對象 該Map毒性代表您的網(wǎng)頁上的地圖。他公開了方法和屬性,是您能夠以編程方式更改地圖,并在用戶與之交互...
    Cyril丶閱讀 886評論 0 0
  • 招商銀行和貝恩公司聯(lián)合發(fā)布《2017中國私人財(cái)富報(bào)告》指出,2016年中國個(gè)人可投資資產(chǎn)1千萬人民幣以上的高凈值人...
    太平趙哲閱讀 492評論 0 0
  • 《收獲人脈~因平凡而非凡》 ~~~~~ ( 文.谷 主 ) 錢怎么賺,因付出而收獲,朋友怎么交,因...
    谷主_7e37閱讀 398評論 0 0

友情鏈接更多精彩內(nèi)容