如何使用canvas绘制可移动网格的示例代码


Posted in HTML / CSS onDecember 14, 2020

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

效果

如何使用canvas绘制可移动网格的示例代码

说明

这个是真实项目中遇到的需求,我把它抽离出来,屏蔽了那些业务相关的东西,仅从代码角度来考虑这个问题。首先网格大小可配置,每个顶点是可以移动的。看到这个问题,不知道各位是怎么去思考的。就先来说说我自己的思路。

分析

首先需要有一个起点,这样就能确定网格所在的位置,其次就是网格中的每个正方形(我们就按正方形来思考,这样简单一点)的边长是多少,另外每个顶点移动的时候,边也需要跟着移动。

所以其实要存储的就只有两类对象,一类就是线,另外就是顶点。

如何存储顶点和线呢?这里用了一个库fabric.js,就比较容易的创建顶点和边的对象,并且它也提供了移动边的方法,但是问题也同时出现了:按照上面的显示,一个点最多关联4条边,最少也关联了2条边,如何表示这种顶点和边的关联关系呢?

先想到就是使用数组来存储顶点和线,然后再根据线中包含的顶点坐标来判断这个线是否和某个顶点相连,如果是的话,则将将其加入到顶点的关联属性中。后面当移动顶点的时候,根据顶点拿到其关联的线,去动态改变线的坐标,这样就能实现上面的那种效果了。

实现

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

function Grid({node, unit, row, col, matrix = []}) {
    // 存储顶点
    this.vertexes = [];
    // 存储边
    this.lines = [];
    
    // 根据起点坐标以及单位边长计算
    for (let i = 0; i <= row; i++) {
        for (let j = 0; j <= col; j++) {
            const newVertex = makeRect(node.x + i * unit, node.y + j * unit);
            this.vertexes.push(newVertex);
        }
    }
    
    // 添加顶点对象的事件监听器
    this.addListener();
}

那么边怎么计算呢,构造边的话,只需要两个顶点就可以连成边,因此我们可以选择遍历顶点来构造边,但是这样的话会造成重复的边,而我们只需要一条边就可以了,不然移动的话,你会发现移动完,下面还会显示一条重叠的边。当然其实最重要的原因就是效率问题,如果不去重的话,会导致计算的时间复杂度过高。

现在有两种方法来解决,一种就是给顶点做标记,当前做线的两端的顶点已经标记过了,那么就跳过当前轮的遍历。另外一种方法,就是可以根据网格这种特定的形状来获取边,如下图,按照两种不同的颜色来计算水平的边和垂直的边。

如何使用canvas绘制可移动网格的示例代码

这样的话,水平方向,就每行两两构成边,垂直方向,就按照一定的间隔连接两个顶点构成边。这里因为后面需要传给算法的格式是二维数组,因此就使用了这个方法。

// ...省略了

// 构造矩阵
this.matrix = [];
let index = -1;
for (let i = 0; i < this.vertexes.length; i++) {
    if (i % (col + 1) === 0) {
        index++;
        this.matrix[index] = [];
    }
    this.matrix[index].push(this.vertexes[i]);
}

// 根据矩阵添加边
let idx = 0;
for (let i = 0; i < this.matrix.length; i++) {
    for (let j = 0; j < this.matrix[i].length; j++) {
        // 交叉渲染边,这样能够在可视区内优先展示
        this.matrix[i][j+1] && this.makeLine(this.matrix[i][j], this.matrix[i][j+1]);
        this.vertexes[idx + col + 1] &&
            this.makeLine(this.vertexes[idx], this.vertexes[idx + col + 1]);
        idx++;
    }
}

后面就是找每个顶点关联了几条边

for (let i = 0; i < this.vertexes.length; i++) {
  const vertex = this.vertexes[i];
  // 根据顶点的坐标是否是边的两端的开始或结束坐标来判断顶点是否与这条边关联
  const associateLines = this.lines.filter(item => {
    return (item.x1 === vertex.left && item.y1 === vertex.top) ||
      (item.x2 === vertext.left && item.y2 === vertex.top);
  });
  vertex.lines = associateLines;
}

眼精的同学肯定一眼就看出来啦,这个时间复杂度太高了。所以虽然网格画出来了,但是当顶点数量过多的时候,计算时间太长,导致浏览器卡住了了差不多2s往上,当水平方向有50个顶点,垂直方向有50个顶点,就能明显看到浏览器的卡顿,此时如果有输入框之类的交互UI,是无法做任何操作的,这肯定也是不行滴。

改进

那么有什么方法能够高效的找到顶点和边之间的关联呢?这里就不卖关子了,当然可能还有其他更好的方法,但是笔者知识所限,只能到这啦。

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

我们来改进一下我们的代码。

function Grid({node, unit, row, col, matrix = []}) {
    this.vertexes = [];
    this.lines = [];
    this.edges = new Map();

    this.addEdges = addEdges;
    this.addVertexes = addVertexes;
}

这里添加了一个新的属性edges,来存储顶点和边的映射关系。其他的步骤和先前都是一样的,只是更换了添加顶点和边的方法,什么意思呢,看代码其实明白了:

function Grid({node, unit, row, col, matrix = []}) {
    // ...省略

    // 根据矩阵添加边
    let idx = 0;
    for (let i = 0; i < this.matrix.length; i++) {
        for (let j = 0; j < this.matrix[i].length; j++) {
            // 交叉渲染边,这样能够在可视区内优先展示
            this.matrix[i][j+1] && this.addEdges(this.matrix[i][j], this.matrix[i][j+1]);
            this.vertexes[idx + col + 1] &&
                this.addEdges(this.vertexes[idx], this.vertexes[idx + col + 1]);
            idx++;
        }
    }

    // 将边关联到顶点
    this.edges.forEach((value, key) => {
        key.lines = value;
    });
}

这里我们就将复杂度为O(mn)的计算降低为了O(n),这里mlines的长度,nvertexes的长度。然后再来看下此时计算100*100的顶点数,计算时间只有200ms,已经能够满足我的需求了。那么图是如何实现这种关联的呢,其实就是每次添加边的时候,将边的两个顶点同时添加进关联关系中,也就是Map的结构中。

function addEdges(v, w) {
    const line = makeLine({point1: v, point2: w});
    // 顶点v关联了边line
    this.edges.get(v).push(line);
    // 顶点w也同时关联了边line
    this.edges.get(w).push(line);
    this.lines.push(line);
}

function addVertexes(v) {
    this.vertexes.push(v);
    // 给每个顶点设置一个Map结构
    this.edges.set(v, []);
}

这样计算完所有的顶点之后,实际顶点关联的边也都确定了,最后只需要遍历一下这些edges就可以了。

完成了这些之后,开开心心的调用fabric的api,将这些对象添加进canvas中就可以了。

// fabric的API,添加fabric对象到画布中
canvas.add(...this.vertexes);
canvas.add(...this.lines);

好了,大功告成,可以交差了。运行页面,打开一看,好家伙,计算速度是快了很多,但是渲染的速度惨不忍睹,30*30的顶点数量,页面还是有卡顿的情况,这是怎么回事呢?

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

function renderIdleCallback(canvas) {
    // 任务切片
    const points = this.points.slice();
    const lines = this.lines.slice();
    const task = () => {
        // 清理canvas的时候,中断后面的渲染
        if (this.interrupt) return;
        if (!points.length && !lines.length) return;
        let slicePoint = [], sliceLine = [];
        for (let i = 0; i < 10; i++) {
            if (points.length) {
                const top = points.shift();
                slicePoint.push(top);
            }
            if (lines.length) {
                const top = lines.shift();
                sliceLine.push(top);
            }
        }
        canvas.add(...slicePoint);
        canvas.add(...sliceLine);
        window.requestAnimationFrame(task);
    }
    task();
}

上面的代码加入了一个标识符来中断渲染,因为存在这样一种情况,本次网格还没有渲染完,就被清理掉又重新渲染,那么就需要停止上次的渲染,重新开始新的渲染了。

总结

好了,到这里也就结束了。由于笔者知识浅薄,只能做到这种满足需求的优化了,更极致的优化就要看各位大佬指点。同时此次尝试也是笔者第一次将所学的数据结构、优化手段结合到项目中,成就感还是非常多的,也是感受到数据结构算法对于程序员的重要性,如果想要突破自己的技术瓶颈,那么这也是绕不开的一个点。

到此这篇关于如何使用canvas绘制可移动网格的示例代码的文章就介绍到这了,更多相关canvas 可移动网格内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章,希望大家以后多多支持三水点靠木!

HTML / CSS 相关文章推荐
纯css3制作的火影忍者写轮眼开眼至轮回眼及进化过程实例
Nov 11 HTML / CSS
一款纯css3实现的颜色渐变按钮的代码教程
Nov 12 HTML / CSS
CSS3哪些新特性值得称赞
Mar 02 HTML / CSS
深入理解css属性的选择对动画性能的影响
Apr 20 HTML / CSS
HTML5+CSS3 实现灵动的动画 TAB 切换效果(DEMO)
Sep 15 HTML / CSS
HTML5 用动画的表现形式装载图像
Mar 08 HTML / CSS
详解FireFox下Canvas使用图像合成绘制SVG的Bug
Jul 10 HTML / CSS
html5使用canvas绘制文字特效
Dec 15 HTML / CSS
使用html2canvas.js实现页面截图并显示或上传的示例代码
Dec 18 HTML / CSS
HTML5 语义化标签(移动端必备)
Aug 23 HTML / CSS
分享CSS盒子模型隐藏的几种方式
Feb 28 HTML / CSS
使用CSS定位HTML元素的实现方法
Jul 07 HTML / CSS
HTML5 body设置全屏背景图片的示例代码
Dec 08 #HTML / CSS
Html5基于canvas实现电子签名并生成PDF文档
Dec 07 #HTML / CSS
HTML5基于flash实现播放RTMP协议视频的示例代码
Dec 04 #HTML / CSS
前端水印的简单实现代码示例
Dec 02 #HTML / CSS
html5跳转小程序wx-open-launch-weapp踩坑
Dec 02 #HTML / CSS
Bootstrap File Input文件上传组件
Dec 01 #HTML / CSS
HTML5单选框、复选框、下拉菜单、文本域的实现代码
Dec 01 #HTML / CSS
You might like
PHP parse_url 一个好用的函数
2009/10/03 PHP
yii2中dropDownList实现二级和三级联动写法
2017/04/26 PHP
PHP操作MySQL中BLOB字段的方法示例【存储文本与图片】
2017/09/15 PHP
php使用socket调用http和smtp协议实例小结
2019/07/26 PHP
jquery插件jbox使用iframe关闭问题
2009/02/09 Javascript
javascript 文档的编码问题解决
2009/03/01 Javascript
jQuery 幻灯片插件(带缩略图功能)
2011/01/24 Javascript
JS通过ajax动态读取xml文件内容的方法
2015/03/24 Javascript
使用Node.js实现HTTP 206内容分片的教程
2015/06/23 Javascript
JavaScript生成二维码图片小结
2015/12/27 Javascript
Nodejs中的this详解
2016/03/26 NodeJs
多功能jQuery树插件zTree实现权限列表简单实例
2016/07/12 Javascript
bootstrap导航条实现代码
2016/12/28 Javascript
jquery仿微信聊天界面
2017/05/06 jQuery
Vue使用json-server进行后端数据模拟功能
2018/04/17 Javascript
vue裁切预览组件功能的实现步骤
2018/05/04 Javascript
详解React+Koa实现服务端渲染(SSR)
2018/05/23 Javascript
element ui table 增加筛选的方法示例
2018/11/02 Javascript
详解Vue中使用Axios拦截器
2019/04/22 Javascript
微信小程序页面间传值与页面取值操作实例分析
2019/04/30 Javascript
Vue Element UI + OSS实现上传文件功能
2019/07/31 Javascript
Node使用Selenium进行前端自动化操作的代码实现
2019/10/10 Javascript
JavaScript 实现HTML DOM增删改查操作的常见方法详解
2020/01/04 Javascript
javascript设计模式 ? 迭代器模式原理与用法实例分析
2020/04/17 Javascript
原生js实现照片墙效果
2020/10/13 Javascript
Vue.extend 登录注册模态框的实现
2020/12/29 Vue.js
Linux系统上Nginx+Python的web.py与Django框架环境
2015/12/25 Python
python实现七段数码管和倒计时效果
2019/11/23 Python
英国著名书店:Foyles
2018/12/01 全球购物
最新的小工具和卓越的产品设计:Oh That Tech!
2019/08/07 全球购物
JSF如何进行表格处理及取值
2012/08/06 面试题
住宿生擅自离校检讨书
2014/09/22 职场文书
2015年百日安全活动总结
2015/03/26 职场文书
检讨书怎么写
2015/05/07 职场文书
人生遥控器观后感
2015/06/11 职场文书
在Windows下安装配置CPU版的PyTorch的方法
2021/04/02 Python