“集智知识星空”产品技术解剖(一)

在上一篇文章开创史无前例的在线学习方式——“集智知识星空”产品技术解剖(一)中我们讲了产品新版本的特点,简单来说就是三点:

  1. 使用二维展示方式,展示的信息更多维,更丰富。
  2. 使用层级化展示,每个层级有对应的信息重点,在展示更多信息的同时,不产生视觉负担。
  3. 高手可便捷地自行探索学习路径,同时也为初学者提供了推荐的学习路径。

那既然作为一个程序员,从本篇文章开始就要剖析产品中用到的技术了。整个产品前后端交互不多,核心在于后端算法生成数据,和前端酷炫的交互实现两部分。

算法过程还涉及到机密啊专利啊等等乱七八糟的事情,不能说的太详细,但前端部分本身就完全对外公开,所以也谈不上技术保护。所以我们会着重对前端的实现部分进行分享和分析。

还没有体验过的同学,可以前往集智学园官网体验后再继续往下看。

模拟地图功能

所有的课程以分布在二维坐标系上的点的形式呈现。那就有对视图在二维平面中上下左右移动的需求。而且为了展示内部细节,还需要支持缩放。本质上就是一个地图。所以我们首先需要实现地图的基本交互,移动 + 缩放

之所以不使用google或者百度地图这类现有的地图框架,一是因为我们其实只需要地图的部分交互,其实没必要引入庞大的地图库;二是我们希望能更灵活地对这个"地图"进行自定义开发,后续可能会在现有基础上增加更多的交互或者元素。

另外地图组件本质是图片的分片加载,所以难免在移动和缩放的时候出现中间加载时刻。所以在经过了一段时间的尝试之后我们放弃了对地图库的引入。

1. 核心绘图

整个视图的组成主要元素是那些课程点,这些点都是绘制在一个canvas上
核心绘图函数很简单

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);
      });
    },

设置好事件后,就是地图功能实现的核心了。移动 + 缩放

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手撸的地图,所以完全可以根据需求开发想要的功能,虽然可能一开始如果选择了地图框架来实现功能,前期进展肯定会比现在快,但到了后期开发,我相信一定是我们自己的框架更加灵活,更有利于实现我们的想法,而不会被技术所局限。

本篇主要介绍了地图的基础操作移动缩放是如何实现的。
在下一篇,我们来介绍一下更加精彩的“窗口”和“路径”实现。
敬请期待。

20190322090155-da5a5d4806.[gif-2-mp4.com].gif
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,087评论 4 62
  • 1 MAP对象 该Map毒性代表您的网页上的地图。他公开了方法和属性,是您能够以编程方式更改地图,并在用户与之交互...
    Cyril丶阅读 823评论 0 0
  • 招商银行和贝恩公司联合发布《2017中国私人财富报告》指出,2016年中国个人可投资资产1千万人民币以上的高净值人...
    太平赵哲阅读 440评论 0 0
  • 《收获人脉~因平凡而非凡》 ~~~~~ ( 文.谷 主 ) 钱怎么赚,因付出而收获,朋友怎么交,因...
    谷主_7e37阅读 370评论 0 0