如何使用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背景_动力节点Java学院整理
Jul 11 HTML / CSS
CSS3 网页下拉菜单代码解释 中文翻译
Feb 27 HTML / CSS
css3圆角边框和边框阴影示例
May 05 HTML / CSS
css3实现动画的三种方式
Aug 24 HTML / CSS
CSS实现进度条和订单进度条的示例
Nov 05 HTML / CSS
HTML5 Canvas API中drawImage()方法的使用实例
Mar 25 HTML / CSS
HTML5 UTF-8 中文乱码的解决方法
Nov 18 HTML / CSS
用HTML5制作烟火效果的教程
May 12 HTML / CSS
用HTML5实现鼠标滚轮事件放大缩小图片的功能
Jun 25 HTML / CSS
深入解析HTML5 Canvas控制图形矩阵变换的方法
Mar 24 HTML / CSS
HTML5头部标签的一些常用信息小结
Oct 23 HTML / CSS
Html5让容器充满屏幕高度或自适应剩余高度的布局实现
May 14 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
将数组写入txt文件 var_export
2009/04/21 PHP
PHP-CGI进程CPU 100% 与 file_get_contents 函数的关系分析
2011/08/15 PHP
Zend Framework框架教程之Zend_Db_Table_Rowset用法实例分析
2016/03/21 PHP
Laravel框架实现的上传图片到七牛功能详解
2019/09/06 PHP
tp5 sum某个字段相加得到总数的例子
2019/10/18 PHP
Javascript attachEvent传递参数的办法
2009/12/14 Javascript
一个简单的网站访问JS计数器 刷新1次加1次访问
2012/09/20 Javascript
javascript不可用的问题探究
2013/10/01 Javascript
js中function()使用方法
2013/12/24 Javascript
ActiveX控件与Javascript之间的交互示例
2014/06/04 Javascript
封装了一个支持匿名函数的Javascript事件监听器
2014/06/05 Javascript
javascript自定义in_array()函数实现方法
2015/08/03 Javascript
jQuery实现瀑布流布局详解(PC和移动端)
2020/09/01 Javascript
jQuery密码强度检测插件passwordStrength用法实例分析
2015/10/30 Javascript
Node.js DES加密的简单实现
2016/07/07 Javascript
ThinkPHP+jquery实现“加载更多”功能代码
2017/03/11 Javascript
使用cropper.js裁剪头像的实例代码
2017/09/29 Javascript
JS实现的汉字与Unicode码相互转化功能分析
2018/05/25 Javascript
6种JavaScript继承方式及优缺点(小结)
2020/02/06 Javascript
Nodejs 微信小程序消息推送的实现
2021/01/20 NodeJs
python使用PyCharm进行远程开发和调试
2017/11/02 Python
使用Python更换外网IP的方法
2018/07/09 Python
python 实现对数据集的归一化的方法(0-1之间)
2018/07/17 Python
对python while循环和双重循环的实例详解
2019/08/23 Python
兰芝美国网上商城:购买LANEIGE睡眠面膜等
2017/06/30 全球购物
联想瑞士官方网站:Lenovo Switzerland
2017/11/19 全球购物
应届生文秘专业个人自荐信格式
2013/09/21 职场文书
本科生学习总结的自我评价
2013/10/02 职场文书
预备党员入党自我评价范文
2014/03/10 职场文书
税务干部群众路线教育实践活动对照检查材料
2014/09/20 职场文书
银行会计主管岗位职责
2014/10/01 职场文书
2015年团支书工作总结
2015/04/03 职场文书
结婚纪念日感言
2015/08/01 职场文书
2016年大学生党员公开承诺书
2016/03/24 职场文书
一道JS算法面试题——冒泡、选择排序
2021/04/21 Javascript
Python使用Opencv打开笔记本电脑摄像头报错解问题及解决
2022/06/21 Python