Vue + Canvas项目总结

演示地址

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

引言

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

项目概述

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

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

知识点总结

拖拽

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

  1. 被拖拽元素设置draggable="true"
  2. 被拖拽元素还有三个相应的事件dragstart drag dragend,分别对应拖拽开始、拖拽中和拖拽结束,若是你但愿在这些过程加上特效,能够试试,但更多的仍是用做响应数据,好比让画布知道具体是哪一个元素被拖拽进来了;
  3. 被放置元素设置dragover drop两个事件,分别表示被拖拽元素在该元素范围内移动、被拖拽元素着陆,这里注意dragover事件函数内需设置event.preventDefault()防止弹出新页面,而后咱们就能够愉快地在drop事件函数里画图形到画布上啦。

HEX => RGBA

因为设计图上颜色都没有透明度,因此咱们须要手动加一个0.3的alpha,否则画布上图形相互层叠,会覆盖掉层级低的图形和背景图。git

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转换成RGBgithub

canvas基本用法

下面就是关于canvas的内容了,若是对它的基础用法还不太了解的小伙伴,能够看看JavaScript之Canvas画布vuex

save与restore

save能够保存当前canvas的状态,包括strokeStylefillStyle、变换矩阵、剪切区域等,restore能够恢复到canvas状态栈中的上一个状态,因此咱们在这两个函数中间作的canvas状态改变至关于被隔离起来了,不会污染外部的canvas操做vue-cli

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

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

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)便可,这样是否是性能又花了不少呢

代码地址

代码地址 虽然代码质量差,我本身都不忍直视,但仍是放出来吧,万一哪里看不懂了还能够翻翻源码嘛

相关文章
相关标签/搜索