如何使用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打造(无图像无js)的非常流行的讲话(语音)气泡效果
Dec 28 HTML / CSS
css3 仿写阿里云水纹效果的示例代码
Feb 10 HTML / CSS
HTML5 解析规则分析
Aug 14 HTML / CSS
HTML5 placeholder(空白提示)属性介绍
Aug 07 HTML / CSS
利用HTML5的新特点实现图片文件异步上传
May 29 HTML / CSS
HTML5 input placeholder 颜色修改示例
May 30 HTML / CSS
多视角3D逼真HTML5水波动画
Mar 03 HTML / CSS
HTML5 Canvas实现360度全景图的示例代码
Jan 29 HTML / CSS
基于MUI框架使用HTML5实现的二维码扫描功能
Mar 01 HTML / CSS
css3中transform属性实现的4种功能
Aug 07 HTML / CSS
HTML静态页面获取url参数和UserAgent的实现
Aug 05 HTML / CSS
CSS 鼠标选中文字后改变背景色的实现代码
May 21 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的开合式多级菜单程序
2006/10/09 PHP
PHP 内存缓存加速功能memcached安装与用法
2009/09/03 PHP
php语言的7种基本的排序方法
2020/12/28 PHP
PHP在弹框中获取foreach中遍历的id值并传递给地址栏
2017/06/13 PHP
Laravel框架中集成MongoDB和使用详解
2019/10/17 PHP
js中有关IE版本检测
2012/01/04 Javascript
jQuery下通过replace字符串替换实现大小图片切换
2012/05/22 Javascript
JS操作select下拉框动态变动(创建/删除/获取)
2013/06/02 Javascript
开启Javascript中apply、call、bind的用法之旅模式
2015/10/28 Javascript
jQuery插件编写步骤详解
2016/06/03 Javascript
jQuery自定义数值抽奖活动代码
2016/06/11 Javascript
浅谈JavaScript中数组的增删改查
2016/06/20 Javascript
基于Node.js + WebSocket打造即时聊天程序嗨聊
2016/11/29 Javascript
概述BootStrap中role=&quot;form&quot;及role作用角色
2016/12/08 Javascript
使用svg实现动态时钟效果
2018/07/17 Javascript
微信小程序有旋转动画效果的音乐组件实例代码
2018/08/22 Javascript
深入Node TCP模块的理解
2019/03/13 Javascript
Vue通过WebSocket建立长连接的实现代码
2019/11/05 Javascript
[06:50]DSPL次级职业联赛十强晋级之路
2014/11/18 DOTA
Python如何为图片添加水印
2016/11/25 Python
查看django执行的sql语句及消耗时间的两种方法
2018/05/29 Python
python爬虫框架scrapy实现模拟登录操作示例
2018/08/02 Python
python 与服务器的共享文件夹交互方法
2018/12/27 Python
django-crontab实现服务端的定时任务的示例代码
2020/02/17 Python
python爬虫基础知识点整理
2020/06/02 Python
细说CSS3中box属性中的overflow-x属性和overflow-y属性值的效果
2014/07/21 HTML / CSS
使用CSS3滤镜的filter:blur属性制作毛玻璃模糊效果的方法
2016/07/08 HTML / CSS
英国皇家造币厂:The Royal Mint
2018/10/05 全球购物
Oral-B荷兰:牙医最推荐的品牌
2020/02/25 全球购物
介绍一下Python下range()函数的用法
2013/11/07 面试题
领导干部对照检查材料
2014/08/24 职场文书
大二学年个人总结
2015/03/03 职场文书
代理词怎么写
2015/05/25 职场文书
Python编程源码报错解决方法总结经验分享
2021/10/05 Python
MySQL串行化隔离级别(间隙锁实现)
2022/06/16 MySQL
MySQL自定义函数及触发器
2022/08/05 MySQL