用canvas绘制表格,canvas画布的使用

  用canvas绘制表格,canvas画布的使用

  本文主要介绍如何用canvas绘制可移动网格的示例代码,分享给大家,如下:

  

效果

  

说明

  这是一个真实项目中遇到的需求。我把它拔了出来,屏蔽了那些业务相关的东西,只从代码角度考虑这个问题。首先,可以配置网格大小,并且可以移动每个顶点。看到这个问题,不知道大家是怎么想的?先说说我自己的想法。

  

分析

  首先你需要有一个起点,这样你才能确定网格的位置。其次,网格中每个正方形的边长是多少(我们就当它是正方形吧,这样比较简单)?当每个顶点移动时,边也需要随之移动。

  其实要存储的对象只有两种,一种是直线,一种是顶点。

  顶点顶线怎么存储?在这里使用fabric.js库,可以更容易地创建顶点和边的对象,并且它还提供了移动边的方法。但问题也随之而来:根据上面的显示,一个点最多关联4条边,至少关联2条边。顶点和边的这种关系怎么表达?

  首先想到的是用数组存储顶点和线,然后根据线中包含的顶点坐标确定线是否连接到一个顶点。如果是这样,它将被添加到顶点的相关属性中。后期移动顶点时,根据顶点获取其关联的直线,并动态改变直线的坐标,从而达到上述效果。

  

实现

  我们根据上面的分析来实现代码吧。首先要存储的对象是顶点和边。然后,根据每个小矩形的起点坐标和边长,就可以很容易地计算出所有的顶点坐标。

  Grid ({node,unit,row,col,matrix=[]}){//存储顶点this . vertices=[];//存储边缘this . lines=[];//根据起点坐标和单位边长计算for(设I=0);i=rowi ) { for(设j=0;j=colj){ const new vertex=makeRect(node . x I * unit,node . y j * unit);this . vertexs . push(new vertex);} }//添加顶点对象的事件监听器this . addlistener();}那么怎么算边呢?在构造边的时候,只有两个顶点可以连接成边,所以我们可以选择遍历顶点来构造边,但是这样会产生重复的边,我们只需要一条边。否则,如果你移动,你会发现下面会显示一个重叠的边。其实最重要的原因还是效率,如果不去除权重会导致计算时间复杂度过高。

  现在有两种方法可以解决。一种方法是标记顶点。当前线两端的顶点都已经被标记了,所以跳过当前回合的遍历。另一种方法是根据网格的具体形状来获取边缘,如下图所示。水平边缘和垂直边缘根据两种不同的颜色计算。

  这样,在水平方向上,每一行形成成对的边,在垂直方向上,两个顶点以一定的间隔连接形成边。这里使用这个方法是因为稍后要传递给算法的格式是一个二维数组。

  //.//构造矩阵this.matrix=[]省略;设index=-1;for(设I=0;i this .顶点.长度;I){ if(I %(col 1)==0){ index;this . matrix[index]=[];} this.matrix[index]。push(this . vertexs[I]);}//根据矩阵添加一条边让idx=0;for(设I=0;I this . matrix . length;i ) { for(设j=0;j this.matrix。长度;J) {//交叉渲染边缘,使this.matrix [i] [j1]this . makeline(this . matrix[I][j],this . matrix[I][J1])优先显示在可视区域;this .顶点[idx col 1] this.makeLine(this .顶点[idx],this .顶点[idx col 1]);idx}}后面是求每个顶点关联多少条边。

  for(设I=0;i this .顶点.长度;I){ const vertex=this . vertexs[I];//根据顶点的坐标是边两端的起点坐标还是终点坐标来确定顶点是否与边关联。const associate lines=this . lines . filter(item={ return(item . x1==vertex . left item . y1==vertex . top) (item . x2==vertex . left item . y2=});vertex.lines=associateLines}眼睛细的同学肯定一眼就看出来了。这个时间复杂度太高了。所以虽然画了网格,但是当顶点数量太多时,计算时间太长,导致浏览器卡了差不多2s。当水平方向有50个顶点,垂直方向有50个顶点时,可以明显看到浏览器的卡顿。这时候如果有输入框之类的交互UI,你什么都做不了,肯定是不行的。

  

改进

  那么什么方法可以高效的找到顶点和边的关系呢?这里没有悬念。当然,可能还有其他更好的方法,但笔者学识有限,只能说到这里。

  解决方法是图的结构,因为图的边可以用邻接表或者邻接矩阵来存储,这样如果我存储一个顶点,实际上就确定了与这个顶点关联的边。也就是说,我们在添加一个顶点的时候,顺便解决了这个顶点关联的问题,就不需要再遍历所有的边来找关联了。(这里就不详细介绍graph的数据结构了。有兴趣的同学可以自己去查资料。其实这里用到图的地方是边和顶点的关系,没有用到其他图的遍历。)

  让我们改进我们的代码。

  函数网格({node,unit,row,col,matrix=[]}) { this .顶点=[];this . lines=[];this . edges=new Map();this.addEdges=addEdgesthis . add vertex=add vertex;}这里新增了一个属性edges,用来存储顶点和边的映射关系。除了添加顶点和边的方法有所改变之外,其他步骤与之前相同。你什么意思?看着代码,我明白了:

  网格({node,unit,row,col,matrix=[]}) {//.省略//根据矩阵添加边让idx=0;for(设I=0;I this . matrix . length;i ) { for(设j=0;j this.matrix。长度;J) {//交叉渲染边缘,使this.matrix [i] [j1] this.add边缘(this.matrix [i] [j],this.matrix [i] [j1])优先显示在可视区域;this . vertexs[idx col 1]this . add edges(this . vertexs[idx],this . vertexs[idx col 1]);idx} }//将边与顶点this.edges.foreach ((value,key)={ key . lines=value;});}这里我们将复杂度为O(mn)的计算简化为O(n),其中m是直线的长度,n是顶点的长度。那么我们来看看此时100*100的顶点数。计算时间只有200ms,已经可以满足我的需求了。那么一个图是如何实现这种关联的呢?实际上,每增加一条边,这条边的两个顶点就同时加入到关联关系中,也就是地图的结构。

  function addEdges(v,w){ const line=makeLine({ point 1:v,point 2:w });//顶点V与边缘线this.edges.get (v)关联。推(线);//顶点W也与边缘线this.edges.get (w)相关联。推(线);this.lines.push(线);}函数add vertex(v){ this . vertex es . push(v);//为每个顶点设置一个Map结构this.edges.set(v,[]);}在以这种方式计算了所有顶点之后,也确定了与实际顶点相关联的边。最后,你只需要遍历这些边。

  之后,你可以愉快地调用fabric的api,将这些对象添加到canvas中。

  //fabric的API,将fabric对象添加到canvas.add(.这.顶点)在画布上;canvas.add(.this . lines);好了,你完了。你可以做你的工作。运行页面,打开一看,好家伙,计算速度快多了,但是渲染速度太可怕了。顶点数是30*30,页面还是卡。这是怎么回事?

  仔细想想,在画布上添加这么多对象确实是一个巨大的计算量,但这里我们无法改变这种渲染消耗。于是我想到了一个折中的办法,就是用时间切片。简单来说,就是利用API requestAnimationFrame将渲染任务分割成块,在浏览器空闲时进行渲染,不至于阻塞其他浏览器的任务。这涉及到浏览器渲染的一些知识。

  render idle callback(canvas){//任务切片const points=this . points . slice();const lines=this . lines . slice();Const task=()={//清理canvas时,如果(this.interrupt)返回,中断下面的渲染;如果(!点数.长度!lines.length)返回;设slicePoint=[],slice line=[];for(设I=0;i 10I){ if(points . length){ const top=points . shift();slice point . push(top);} if(lines . length){ const top=lines . shift();slice line . push(top);} } canvas.add(.slice point);canvas.add(.slice line);window . requestanimationframe(task);} task();}上面的代码添加了一个标识符来中断渲染,因为有这样一种情况,这次在渲染完成之前,网格被清理,重新渲染。那么有必要停止上一次渲染并开始新的渲染。

  

总结

  好了,就这样。由于作者学识浅薄,只能做这种优化来满足需求。最极致的优化看你的建议了。同时,这次尝试也是作者第一次将所学的数据结构和优化手段结合到项目中。他还是很有成就感的,也感受到了数据结构算法对程序员的重要性。如果他想突破自己的技术瓶颈,那么这也是绕不过去的一个点。

  就是这样。本文介绍了如何使用canvas绘制移动网格的示例代码。更多相关画布可移动网格内容,请搜索之前的文章或继续浏览下面的相关文章。希望你以后能支持我!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: