阅读 339

集智学园知识星空——前端技术实现分析(二)

历史文章

集智学园知识星空——产品介绍篇

集智学园知识星空——前端技术实现分析(一)

欢迎前往集智学园官网体验

前情回顾: 上一篇文章中,我介绍了知识星空核心功能的实现方法:如何使用canvas模拟地图的平移和缩放。在这篇文章中,我们紧接着来说一说其他一些精彩的功能。

如何对canvas中一个元素设置事件

canvas其实是一张大画布,当你在上面开始绘图之后,元素就和画布变成了一个整体。而我们的需求是需要对绘制的每个点都要设置事件(这里主要指点击事件)。

各种地图都是使用dom生成地图上的标记(marker),所以可以很方便地对标记绑定事件。但在我们这里,所有的点都由是canvas绘制,并没有独立的“元素”概念。所以想要为某个元素添加事件就成了一件比较蛋疼的事情。

这里使用了一个比较笨的办法,就是监听整个画布,然后去比对这个点击时刻的位置,和每个圆心的距离是否小于这个圆的半径。

听上去运算量就很大,那最终效果怎么样呢?我们来试一试

clickHandler: e => {
    let { x, y } = { e.clientX, e.clientY }
    let point = null
    this.pointList.forEach(ele => {
    <!--计算当前鼠标点击的位置和圆心的距离,并与半径比较-->
        if (Math.sqrt(Math.pow(x - ele.x), 2)) + Math.pow((y - ele.y), 2)) <= ele.r) {
            //鼠标选中了这个圆
            point = ele
        }
    })
}
复制代码

这样看下来感觉也还行,只是在每次点击的时候去遍历一遍所有的点。当clickHandler函数返回不为null时,说明当前选中了一个点。

接下去还想加一个需求:监听到hover事件,hover到一个点上之后改变鼠标样式。

emm...

按照上面的思路应该是这样写

hoverhandler: e => {
        let { x, y } = { e.clientX, e.clientY }
    let point = null
    this.pointList.forEach(ele => {
    <!--计算当前鼠标点击的位置和圆心的距离,并与半径比较-->
        if (Math.sqrt(Math.pow(x - ele.x), 2)) + Math.pow((y - ele.y), 2)) <= ele.r) {
            //鼠标hover到了这个圆
            point = ele
        }
    })
}
复制代码

上面的函数本身没问题,问题就在于调用频次上,对画布监听mousemove事件的话,会以非常高的频率调用这个函数。假设鼠标一直在移动,1s中触发5次mousemove事件的话,那1s内就会遍历比较所有的点5遍。这就就会比较明显地对性能产生影响了。

这该怎么办?

后来想了一个优化的办法:就是只去遍历当前视图中的点。即把点位位置和当前浏览器窗口去比较,如果在屏幕之外的话,就不用去和这个点去比。

另外还可能对点的大小进行过滤,半径太小的点也不用去比较,对最后效果影响并不大。

最后实现是这样写。

hoverhandler: e => {
        let { x, y } = { e.clientX, e.clientY }
    let point = null
    this.pointList.forEach(ele => {
        <!--加上过滤条件-->
        if (ele.x- ele.value <= window.width && ele.x + ele.value >= 0 && ele.y - ele.value <= window.height && ele.y + ele.value >= 0 && ele.value > 15) {
             <!--计算当前鼠标点击的位置和圆心的距离,并与半径比较-->
            if (Math.sqrt(Math.pow(x - ele.x), 2)) + Math.pow((y - ele.y), 2)) <= ele.r) {
                //鼠标hover到了这个圆
                point = ele
            }
        }
    })
}
复制代码

到这里就算是完成了对画布中元素设置事件的过程。如果有需求需要加上的别的事件,也可以使用这个思路去写。

(我觉得还不是最优解决方案,大佬们有想法欢迎和我讨论)

接下去我们来说一说窗口的弹出动画部分的实现

窗口弹出动画

网站有很多的动画效果,从性能上考虑,原则是能用css实现的就绝不用js,实现效果如下:

看起来好像很复杂,其实都是由基本的keframe组成的,原理一点都不难。这里结合实际的业务逻辑,给大家复习下css3的动画实现。

点击点位后的生成动画分为三个步骤: 这是原始没有弹窗的状态

1. 生成和当前点相同大小的圆

这里需要用到上面所说的点击事件,步骤如下: a. 点击某个点 b. 把这个点移动到视图的中心位置 c. 在当前位置生成一个和这个圆半径相同的圆形窗口

点击一个圆获取它的参数,在上一个模块中已经实现。

生成圆的部分也比较简单,只要获取到当前点击的是哪个圆,根据当前圆的半径,在屏幕中心生成一个和它完全重叠的圆就可以。可以使用一个固定的dom,控制它是否显示,以及根据选中圆的半径控制它的宽高。

所以重点是点击某个点后,把这个圆移动到视图中心的过程应该如何实现。

上一篇文章集智学园知识星空——前端技术实现分析(一)中已经说到了移动地图的实现方式。核心是transform(x, y)函数,参数x, y为横纵坐标分别要移动的距离。肯定不能直接把中心坐标和当前点的位置做差直接传入,否则就是一个突变的现象,我们是要做一个丝滑的移动动画实现最终的功能。

所以需要把中心坐标当前点的坐标做差后,分段传入tranform函数。至于分几段比较合适,就需要把它作为一个超参细致地进行调整。

最终我是调整出了一种计算分段的方法,让分段参数和移动距离成负相关,这样就可以让移动距离长的点移动得快一些,而移动距离短的过程进行得慢一些。

<!--point为要移动到中心位置的点-->
panToCenter (point) {
    <!--选中的点和中心点之间的距离-->
    let dis = Math.sqrt(Math.pow(point.x - window.center, 2) + Math.pow(point.y - window.center, 2));
    <!--根据距离调整出的一个函数,让分段和距离成负相关-->
    let count = f(dis)
    //每个分段的距离:perLength = length / count
    let perLength = { x: (point.x - window.center) / count, y: (point.y -  window.center) / count };
    let panToTime = setInterval(() => {
        transform(-perLength.x, -perLength.y);
        count--;
        if (count <= 0) {
          clearInterval(panToTime);
          this.panToTime = null;
        }
    }, 20);
}
复制代码

到为止这里为止我们就完成了点位的移动和窗口的打开。

接下去就是css的事情

2. 圆半径变大,随后变成矩形窗口,并显示内容

keyframe里面定义一个初始状态,中间状态和最终状态,设置动画时间,动画效果等参数

  @keyframes openUp {
    <!--初始状态是一个园-->
    from {
      opacity: 0.7;
      border-radius: 50%;
    }
    <!--放大过程中间状态依旧是圆-->
    50% {
      opacity: 0.8;
      border-radius: 50%;
    }
    <!--最后变成一个矩形-->
    100% {
      opacity: 0.95;
      border-radius: 20px;
      height: 90%;
      width: 60%;
    }
  }
  .openUp {
    -webkit-animation-timing-function: ease-in-out;
    -webkit-animation-duration: 0.5s;
    -webkit-animation-name: openUp;
    animation-timing-function: ease-in-out;
    animation-name: openUp;
    animation-duration: 0.5s;
  }
复制代码

最后在窗口中加载内容即可

ps:本来还想加入路径的实现过程,觉得篇幅太长,路径的实现还是放在下一篇中。后续还会继续探讨点位生成背后的算法思路,尽情期待

关注下面的标签,发现更多相似文章
评论