演示地址
PC端的项目啦,须要在电脑上看哦,并且最好用Chrome打开html
这是今年三月份帮学长作的一个项目,陪我度过了两个月的春招生活,整个项目作下来也是学到了不少东西,下面就开始个人分享啦,包括一些知识点总结和遇到的坑,dalao莫笑哈。前端
代码是基于vue-cli码的,因此路由、vuex这些都不用讲啦,咱们把重点放在canvas上面吧。vue
这里的拖拽是指把左边工具栏里的图形图形拖拽到右边画布里,三步完成:html5
draggable="true"
;dragstart
drag
dragend
,分别对应拖拽开始、拖拽中和拖拽结束,若是你但愿在这些过程加上特效,能够试试,但更多的仍是用做响应数据,好比让画布知道具体是哪一个元素被拖拽进来了;dragover
drop
两个事件,分别表示被拖拽元素在该元素范围内移动、被拖拽元素着陆,这里注意dragover
事件函数内需设置event.preventDefault()
防止弹出新页面,而后咱们就能够愉快地在drop
事件函数里画图形到画布上啦。因为设计图上颜色都没有透明度,因此咱们须要手动加一个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的内容了,若是对它的基础用法还不太了解的小伙伴,能够看看JavaScript之Canvas画布vuex
save
能够保存当前canvas的状态,包括strokeStyle
、fillStyle
、变换矩阵、剪切区域等,restore
能够恢复到canvas状态栈中的上一个状态,因此咱们在这两个函数中间作的canvas状态改变至关于被隔离起来了,不会污染外部的canvas操做。vue-cli
这样看来,咱们最好在每次画图前调用save
,画完后调用restore
,从而保证每次绘制都有一个纯粹的状态。redux
这里有一篇讲得特别好的文章,若是嫌本直男没讲清楚的话,必定要看哦。Canvas学习:save()和restore()canvas
可能有些小伙伴会小看这个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();
}
复制代码
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;
}
复制代码
相似PS的功能嘛,我这个项目没作,可是思路不难,用past、present、future三个数组来保存图形栈,Emm好像讲起来仍是有点长,能够参考实现撤销历史的思路。
图形栈里的实例被依次取出绘制,后画上去的图形会覆盖掉以前的图形,因此这里涉及到一个优先级,重要的东西放在后面画。
咱们能够把保存图形的数组再细分类,数组的每一个子元素都是一个Array,专门保存某一种图形,优先级越高,对应的索引值越大,这样咱们就能够把重要的图形所有放在后面画了。
通常咱们用于双向绑定的值都会放在vue实例的data
中,由于它默认提供了getter
和setter
;但vuex的状态通常都须要computed
来读取,但computed
默认是没有setter方法的,须要手动设置,代码以下:
computed:{
text : {
get(){
return this.$store.state.text;
},
set(value){
this.$store.commit('setText',value);
}
}
}
复制代码
在实现保存图片功能的时候,我但愿能截取一段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的原型,我滴妈耶,作梦也没想到会是这里出问题,用插件有风险啊。
若是上传到https://XXX.github.io/(GitHub的我的博客)上,则跟上传到服务器上操做一致,但若是是传到某个仓库的gh-pages,那么一堆问题都来了,解决步骤以下:
.gitignore
文件里的/dist
删掉,忽略了的话,还怎么上传打包文件到master分支呢;/config/index.js
里build部分里的assetsPublicPath
由'/'改为'./',至关于说把服务器根目录改为了相对路径,仓库gh-pages的根目录不是'/'而是'/仓库名';static
里的图片,使用了绝对路径,可能上传后显示不出来;git subtree push --prefix dist origin gh-pages
敲完命令,应该就能够看到上传成功了。上面提到,咱们的画布每次更新时,老是要所有清除,而后从新再画一遍,对于那些背景图片等不变的内容来讲,是否是能够优化呢?Emmm,好尬的设问句。
咱们用多个一样大小层叠的canvas来完成,层级低的下层canvas用来画背景图片等静态图形,层级高的上层canvas用来画动态变化的图形,这样就能够每次渲染都优化一点啦。
当咱们在画布上拖拽图形时,通常作法是随着鼠标移动mousemove
,从新绘制全部图形,但其实这个过程当中,要绘制的能够分为两部分,一个是被拖拽移动的图形,另外一个就是其余图形;咱们能够分别动态建立两个canvas,把两部分画在两个离屏画布上,mousemove
时只要调用两次drawImage(离屏canvas)便可,这样是否是性能又花了不少呢
代码地址 虽然代码质量差,我本身都不忍直视,但仍是放出来吧,万一哪里看不懂了还能够翻翻源码嘛