继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Vue + Canvas项目总结

狐的传说
关注TA
已关注
手记 304
粉丝 88
获赞 555

演示地址

演示地址
PC端的项目啦,需要在电脑上看哦,而且最好用Chrome打开

引言

这是今年三月份帮学长做的一个项目,陪我度过了两个月的春招生活,整个项目做下来也是学到了很多东西,下面就开始我的分享啦,包括一些知识点总结和遇到的坑,dalao莫笑哈。

项目概述



https://img1.mukewang.com/5b57e5e400013e9d09760456.jpg

主要功能如上图,左边是图形工具栏,右边是canvas,上面是清除、删除、旋转、切换格子背景、保存并下载图片的操作。



代码是基于vue-cli码的,所以路由、vuex这些都不用讲啦,我们把重点放在canvas上面吧。

知识点总结

拖拽

这里的拖拽是指把左边工具栏里的图形图形拖拽到右边画布里,三步完成:

  1. 被拖拽元素设置draggable="true"

  2. 被拖拽元素还有三个相应的事件dragstart drag dragend,分别对应拖拽开始、拖拽中和拖拽结束,如果你希望在这些过程加上特效,可以试试,但更多的还是用作响应数据,比如让画布知道具体是哪个元素被拖拽进来了;

  3. 被放置元素设置dragover drop两个事件,分别表示被拖拽元素在该元素范围内移动、被拖拽元素着陆,这里注意dragover事件函数内需设置event.preventDefault()防止弹出新页面,然后我们就可以愉快地在drop事件函数里画图形到画布上啦。

HEX => RGBA

由于设计图上颜色都没有透明度,所以我们需要手动加一个0.3的alpha,不然画布上图形相互层叠,会覆盖掉层级低的图形和背景图。

function hex2rgba(hex) {       // hex格式如#ffffff       let colorArr = [];       for(let i = 1; i<7; i += 2){         colorArr.push(parseInt("0x" + hex.slice(i,i+2))); // 16进制值转10进制       }       return `rgba(${colorArr.join(",")},0.3)`; } 复制代码

另外如果有兴趣了解RGBA转RGB的小伙伴,可以看看这篇博客RGBA转换成RGB

canvas基本用法

下面就是关于canvas的内容了,如果对它的基础用法还不太了解的小伙伴,可以看看JavaScript之Canvas画布

save与restore

save可以保存当前canvas的状态,包括strokeStylefillStyle、变换矩阵、剪切区域等,restore可以恢复到canvas状态栈中的上一个状态,所以我们在这两个函数中间做的canvas状态改变相当于被隔离起来了,不会污染外部的canvas操作。

这样看来,我们最好在每次画图前调用save,画完后调用restore,从而保证每次绘制都有一个纯粹的状态。

这里有一篇讲得特别好的文章,如果嫌本直男没讲清楚的话,一定要看哦。Canvas学习:save()和restore()

drawImage

可能有些小伙伴会小看这个API,认为它只能绘制图片,实际上它还能svg、canvas绘制到画布上,我们先来看看如何绘制svg咯。

我们功能界面左侧工具栏里的图标其实都是svg,我一开始是想把他们截图下来切成一个个背景透明的png,然后画到canvas上,后来发现放大看的话会比较模糊,毕竟是像素图嘛,所以新的需求来了。

我自己的代码不好贴出来,那就看看dalao的吧,将 DOM 对象绘制到 canvas 中,他这里是将DOM塞到svg里再往canvas上画的,如果你只需要画现成的svg,则可以不用foreignObject包裹。

另外,如果你的svg有.svg格式图片,可以直接调用drawImage去绘制。

椭圆与贝赛尔曲线

canvas已经有画椭圆的API了,但兼容性还不够好,在其他所有模拟绘制椭圆的方式里,贝塞尔曲线可以说是最优雅的一种了,好吧,扫盲文 => 贝塞尔曲线原理(简单阐述)

三维贝塞尔曲线需要一个起始点、两个中间点、一个终止点确定,当然起始点一般默认当前点,所以bezierCurveTo的参数就是按顺序的后三个点坐标了;当这四个点恰好围成一个矩形时,就有点椭圆的模样啦。

 let a = this.width / 2;  let b = this.height / 2;  let ox = 0.5 * a,      oy = 0.6 * b;  this.ctx.beginPath();  // 从椭圆纵轴下端开始逆时针方向绘制  this.ctx.moveTo(0, b);  // 把椭圆划成四份分开来画  this.ctx.bezierCurveTo(ox, b, a, oy, a, 0);  this.ctx.bezierCurveTo(a, -oy, ox, -b, 0, -b);  this.ctx.bezierCurveTo(-ox, -b, -a, -oy, -a, 0);  this.ctx.bezierCurveTo(-a, oy, -ox, b, 0, b);  this.ctx.closePath();  this.ctx.fill(); 复制代码

这里有一篇整理得比较完整的椭圆绘制方法的文章 可以参考 HTML5 Canvas中绘制椭圆的5种方法

线条

带箭头的实线

实线好画,但是箭头怎么来做呢?Emmm,其实就是计算线段与画布x轴的夹角,然后在线段终点画偏移对应角度的三角形嘛

drawArrow(x1, y1, x2, y2) {     // (x1, y1)是线段起点  (x2, y2)是线段终点     // 反正切函数计算夹角     let endRadians = Math.atan((y2 - y1) / (x2 - x1));     // 三角形的底边与线段垂直,所以还要再转 π / 2     endRadians += ((x2 >= x1) ? 90 : -90) * Math.PI / 180;     this.ctx.save();     this.ctx.beginPath();     // 坐标原点 => (x2, y2)     this.ctx.translate(x2, y2);     this.ctx.rotate(endRadians);     this.ctx.moveTo(0, 0);     this.ctx.lineTo(5, 15);     this.ctx.lineTo(-5, 15);     this.ctx.closePath();     this.ctx.fill();     this.ctx.restore(); } 复制代码

虚线

  • 比较传统的一种做法是修改CanvasRenderingContext2D的原型,手动增加一个dashedLine的方法,原理大概是从起始点先画一段实线,然后跳过一段,moveTo到下一个点继续画实线,这样循环到终止点,就能得到虚线。具体实现见html5 实现画虚线

  • 其实canvas已经支持画虚线了,画线前用setLineDash即可指定虚线的样式,详见Canvas学习:绘制虚线和圆点线
    但是这个方法用起来有些问题,角度不好或者间隔太小的时候,画出来的虚线看起来就像是实线。

波浪线

一般常见的波浪线都是用正弦曲线来模拟的吧,y = A * sin(ω * x + φ),指定它的A和ω就可以确定波浪线的振幅和频率(或者说每个波浪的高度和宽度)

let len = Math.sqrt(width * width + height * height); this.ctx.save(); this.ctx.moveTo(this.start.x,this.start.y); // 起点 this.ctx.translate(this.start.x,this.start.y); this.ctx.beginPath(); let x = 0; let y = 0; let amplitude = 5; // 振幅 let frequency = 5; // 频率 while (x < len) {     y = amplitude * Math.sin(x / frequency);     this.ctx.lineTo(x, y);     x = x + 1; } this.ctx.stroke(); this.ctx.restore(); 复制代码

参考文章:Draw a Sine Wave in JavaScript

图形栈

保存

简单来说,我们画布上的图形都是一个类的实例,保存在一个数组中,每次有更新时都会清除画布,再全部重新绘制一遍(后面会将优化)。这个图形实例需要保存的属性一般有起始和终点坐标、颜色、偏移角度等,根据自己的需求设置,还至少需要一个方法去动态计算该图形的有效范围,以便鼠标事件找到它。

删除

选中某图形实例后,从图形栈数组中删除即可。

旋转

由于我们每次画图形的时候,都会把坐标原点暂时移到图形的中心,所以只需要rotate一个角度再画就可以实现旋转啦

拖拽移动

Emmm,每个图形不太一样,有兴趣的话看看项目源码呗

判断一个点是否在某个四边形内

  • 向量法
    详见 判断一个点是否在四边形内部,但是这种方法有点局限性,首先,图形边数必须事先确定,而且边数多起来了代码会很长;其次,这种方法只是适用于凸多边形,举个凹多边形的反例想想就能明白了。

  • 射线法
    详见射线法理论,代码实现如下:

function inRange(x, y, points){     // points表示多边形的顶点集合     let inside = false;     for (let i = 0, j = points.length - 1; i < points.length; j = i++) {         let xi = points[i][0], yi = points[i][1];         let xj = points[j][0], yj = points[j][1];         let intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);         if (intersect) inside = !inside;     }     return inside; } 复制代码
  • 一个公式
    任意点(x,y),绕一个坐标点(rx0,ry0)逆时针旋转a角度后的新的坐标设为(x0, y0),有公式:
    x0= (x - rx0)*cos(a) - (y - ry0)*sin(a) + rx0 ;
    y0= (x - rx0)*sin(a) + (y - ry0)*cos(a) + ry0 ;
    极坐标的知识啦,不想推就直接套公式呗。

撤销与回退

类似PS的功能嘛,我这个项目没做,但是思路不难,用past、present、future三个数组来保存图形栈,Emm好像讲起来还是有点长,可以参考实现撤销历史的思路。

优先级

图形栈里的实例被依次取出绘制,后画上去的图形会覆盖掉之前的图形,所以这里涉及到一个优先级,重要的东西放在后面画。

我们可以把保存图形的数组再细分类,数组的每个子元素都是一个Array,专门保存某一种图形,优先级越高,对应的索引值越大,这样我们就可以把重要的图形全部放在后面画了。

vuex中的状态实现双向绑定

一般我们用于双向绑定的值都会放在vue实例的data中,因为它默认提供了gettersetter;但vuex的状态一般都需要computed来读取,但computed默认是没有setter方法的,需要手动设置,代码如下:

computed:{       text : {         get(){           return this.$store.state.text;         },         set(value){           this.$store.commit('setText',value);         }       } } 复制代码

遇到的坑

html2canvas的一个小bug

在实现保存图片功能的时候,我希望能截取一段DOM的内容,而不仅仅是canvas的内容,所以找到了这个插件html2canvas,它可以把dom转换成canvas,然后我们就能canvas.toDataURL()把它转换成图片了。

转换并保存成图片下载的代码如下:

downImg() {         html2canvas( this.$refs.ground, {           onrendered: function(canvas) {             let url = canvas.toDataURL();             let a = document.createElement('a');             a.href = url;             a.download = new Date() + ".png";             document.body.appendChild(a);             a.click();             document.body.removeChild(a);           }         });     } 复制代码

但是出现了一个bug,就是下载下来的图片不清晰,左上角一大片空白。
于是我尝试了网上的很多方法,都行不通,最后只能把项目从零开始慢慢加东西,最后发现是我画虚线的时候改了CanvasRenderingContext2D的原型,我滴妈耶,做梦也没想到会是这里出问题,用插件有风险啊。

上传到gh-pages时的路径错误

如果上传到https://XXX.github.io/(GitHub的个人博客)上,则跟上传到服务器上操作一致,但如果是传到某个仓库的gh-pages,那么一堆问题都来了,解决步骤如下:

  1. .gitignore文件里的/dist删掉,忽略了的话,还怎么上传打包文件到master分支呢;

  2. /config/index.js里build部分里的assetsPublicPath由'/'改成'./',相当于说把服务器根目录改成了相对路径,仓库gh-pages的根目录不是'/'而是'/仓库名';

  3. 相对应的,如果使用了history模式,请改成hash模式,不然github可能会把前端路由识别成后端api;

  4. 还有一些static里的图片,使用了绝对路径,可能上传后显示不出来;

  5. git subtree push --prefix dist origin gh-pages敲完命令,应该就可以看到上传成功了。

优化

多层次画布

上面提到,我们的画布每次更新时,总是要全部清除,然后重新再画一遍,对于那些背景图片等不变的内容来说,是不是可以优化呢?Emmm,好尬的设问句。

我们用多个同样大小层叠的canvas来完成,层级低的下层canvas用来画背景图片等静态图形,层级高的上层canvas用来画动态变化的图形,这样就可以每次渲染都优化一点啦。

离屏渲染

当我们在画布上拖拽图形时,一般做法是随着鼠标移动mousemove,重新绘制所有图形,但其实这个过程中,要绘制的可以分为两部分,一个是被拖拽移动的图形,另一个就是其他图形;我们可以分别动态创建两个canvas,把两部分画在两个离屏画布上,mousemove时只要调用两次drawImage(离屏canvas)即可,这样是不是性能又花了很多呢

代码地址

代码地址
虽然代码质量差,我自己都不忍直视,但还是放出来吧,万一哪里看不懂了还可以翻翻源码嘛

原文地址:https://juejin.im/post/5b529ddaf265da0f504a552c

打开App,阅读手记
6人推荐
发表评论
随时随地看视频慕课网APP