如何使用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 相关文章推荐
纯CSS改变webkit内核浏览器的滚动条样式
Apr 17 HTML / CSS
CSS实现的一闪而过的图片闪光效果
Apr 23 HTML / CSS
CSS3为背景图设置遮罩并解决遮罩样式继承问题
Jun 22 HTML / CSS
HTML5 canvas实现雪花飘落特效
Mar 08 HTML / CSS
Html5剪切板功能的实现代码
Jun 29 HTML / CSS
HTML5 绘制图像(上)之:关于canvas元素引领下一代web页面的问题
Apr 24 HTML / CSS
HTML5 中新的全局属性(整理)
Jul 31 HTML / CSS
HTML5 Canvas阴影使用方法实例演示
Aug 02 HTML / CSS
HTML5实现无刷新修改URL的方法
Nov 14 HTML / CSS
HTML5表单验证特性(知识点小结)
Mar 10 HTML / CSS
CSS3实现的水平标题菜单
Apr 14 HTML / CSS
HTML5+CSS+JavaScript实现捉虫小游戏设计和实现
Oct 16 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的正则处理函数总结分析
2008/06/20 PHP
PHP函数spl_autoload_register()用法和__autoload()介绍
2012/02/04 PHP
PHP中Fatal error session_start()错误解决步骤
2014/08/05 PHP
PHP模块化安装教程
2016/06/01 PHP
Javascript 面向对象 命名空间
2010/05/13 Javascript
Javascript节点关系实例分析
2015/05/15 Javascript
JavaScript中的slice()方法使用详解
2015/06/06 Javascript
jQuery原生的动画效果
2015/07/10 Javascript
BootStrap 下拉菜单点击之后不会出现下拉菜单(下拉菜单不弹出)的解决方案
2016/12/14 Javascript
JavaScript获取ul中li个数的方法
2017/02/13 Javascript
axios对请求各种异常情况处理的封装方法
2018/09/25 Javascript
Intellij IDEA搭建vue-cli项目的方法步骤
2018/10/20 Javascript
给localStorage设置一个过期时间的方法分享
2018/11/06 Javascript
JS实现表单中点击小眼睛显示隐藏密码框中的密码
2020/04/13 Javascript
使用go和python递归删除.ds store文件的方法
2014/01/22 Python
python队列通信:rabbitMQ的使用(实例讲解)
2017/12/22 Python
15行Python代码带你轻松理解令牌桶算法
2018/03/21 Python
Python cookbook(数据结构与算法)将名称映射到序列元素中的方法
2018/03/22 Python
python使用zip将list转为json的方法
2018/12/31 Python
Python 实现大整数乘法算法的示例代码
2019/09/17 Python
Python操作MongoDb数据库流程详解
2020/03/05 Python
python实现斗地主分牌洗牌
2020/06/22 Python
Python利用myqr库创建自己的二维码
2020/11/24 Python
python进行二次方程式计算的实例讲解
2020/12/06 Python
python 基于PYMYSQL使用MYSQL数据库
2020/12/24 Python
使用HTML和CSS3绘制基本卡通图案的示例分享
2015/11/06 HTML / CSS
Maisons du Monde德国:法国家具和装饰的市场领导者
2019/07/26 全球购物
大学生会计职业生涯规划范文
2014/02/28 职场文书
外贸专业求职信
2014/03/09 职场文书
自动一体化专业求职信
2014/03/15 职场文书
第二课堂活动总结
2014/05/07 职场文书
韩语专业职业生涯规划范文:成功之路就在我们脚下
2014/09/11 职场文书
县政府办公室领导班子对照检查材料思想汇报
2014/09/28 职场文书
2015年部门工作总结范文
2015/03/31 职场文书
民间借贷纠纷案件代理词
2015/05/26 职场文书
python基础之//、/与%的区别详解
2022/06/10 Python