Vue + Canvas项目总结

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

项目概述

主要功能如上图,左边是图形工具栏,右边是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)即可,这样是不是性能又花了很多呢

代码地址

代码地址

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

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章