前言
这两天在做QQ聊天机器人,目前也实现了一些功能
准备往里面加一些小游戏的功能,思来想去就准备自己实现一个简易版的你画我猜,但是QQ肯定不支持我发个画板过来,于是便诞生了这个网站picture_board (gitee.io)。网站部署在gitee pages上,感兴趣可以去picture_board(gitee.com)下载源代码。
实现思路
整个页面比较简单,上下的两栏布局。上面是canvas的绘画区域,下面是功能选择区域。(请忽略计时框,这个是为了在你画我猜中,提醒绘画时间用的)
canvas准备
首先在template模板里填入canvas元素
<template>
<div id="bottom">
<canvas
id="canvas"
@mousedown="down($event)"
@mousemove="move($event)"
@mouseup="up"
@mouseenter="enter"
@touchstart="start($event)"
@touchmove="move2($event)"
@touchend="up"
></canvas>
</div>
</template>
本来我是在canvas便签里面添加了style来设置宽高来实现剩余区域的自动填充(flex:1),但是一番调试之后,页面上怎么搞没有画的痕迹,整整一个上午的时间,都没有任何进展,后来吃饭的时候,我突然意识到可能是宽高的设置方式不对,改过之后发现真的是这个原因,直接老泪纵横。。。
let that = this;
that.canvas = document.getElementById("canvas");
that.cxt = that.canvas.getContext("2d");
canvas.width = that.width;
canvas.height = that.height;
width和height也是根据页面比例计算的
this.width = document.documentElement.clientWidth;
this.height = document.documentElement.clientHeight * 0.9;
pc端绘画
主要相关的是三个事件
- 点击鼠标
- 移动鼠标
- 松开鼠标
当鼠标点击时用一个变量记录下该坐标点(x1),鼠标开始滑动时记录滑动到的第一坐标点(x2),这样我们就有了2个坐标点,再通过canvas中的stroke()事件将第一个坐标点与第二个坐标点连接起来变成线。然后将(x2)坐标点赋值给(x1),继续滑动鼠标。这时(x1)的值为滑动到的第一个坐标点,(x2)为当前坐标点,继续连线。最后松开鼠标,触发我们人为设置的控制开关paint = false,循环终止,坐标轴停止赋值。一段线段便完成。
down(e) {
// 鼠标按下事件
const x = e.offsetX;
const y = e.offsetY;
this.startPoint = {
x: x,
y: y,
};
if (this.eraser) { // 启用橡皮擦
this.cxt.clearRect(x, y, 10, 10);
}
this.paint = true;
},
move(e) {
// 鼠标移动事件
e.preventDefault();
const x = e.offsetX;
const y = e.offsetY;
this.endPoint = {
x: x,
y: y,
};
if (this.paint) {
if (this.eraser) {
this.cxt.clearRect(x, y, 10, 10);
} else {
this.draw();
}
}
this.startPoint.x = this.endPoint.x;
this.startPoint.y = this.endPoint.y;
},
up() {
// 鼠标松开事件
// 绘画完成,推入历史记录
let image = this.cxt.getImageData(0, 0, this.width, this.height);
this.history.push(image);
// history中有记录,撤回按钮可用
bus.$emit("abled", "启用撤回");
this.paint = false;
},
同时,还要监听进入画布事件,否则会造成使用完橡皮擦,切换到画笔之后,鼠标移动进入画布直接开始绘画
enter() {
// 修复pc端使用橡皮擦之后进入canvas立即开始画线
this.paint = false;
},
移动端绘画
在移动端中是没有鼠标的,因此与其相对应的有一个触摸事件touch。步骤与上面同理,但需要注意一点:移动设备是支持多点触摸的,因此这里的x、y轴需要从e.touches[0]数组第一个中取。
start(e) {
//[0]表示touch第一个触碰点
let x = e.touches[0].clientX;
let y = e.touches[0].clientY;
this.startPoint = {
x: x,
y: y,
};
if (this.eraser) {
this.cxt.clearRect(x, y, 10, 10);
}
this.paint = true;
},
move2(e) {
e.preventDefault();
let x = e.touches[0].clientX;
let y = e.touches[0].clientY;
this.endPoint = {
x: x,
y: y,
};
if (this.paint) {
if (this.eraser) {
this.cxt.clearRect(x, y, 10, 10);
} else {
this.draw();
}
}
this.startPoint.x = this.endPoint.x;
this.startPoint.y = this.endPoint.y;
},
绘制
主要牵扯的canvas的一些基础api,不懂得掘友可以参考我之前写的文章 canvas学习--线条操作 - 掘金 (juejin.cn)。
draw() {
// 开始绘制
this.cxt.beginPath();
// 设置线条样式
this.cxt.lineWidth = this.value;
this.cxt.lineCap = "round";
this.cxt.lineJoin = "round";
this.cxt.strokeStyle = this.color;
//起始位置
this.cxt.moveTo(this.startPoint.x, this.startPoint.y);
//停止位置
this.cxt.lineTo(this.endPoint.x, this.endPoint.y);
//描绘线路
this.cxt.stroke();
//结束绘制
this.cxt.closePath();
},
橡皮擦
通过eraser是否等于true开启橡皮擦功能。如果等于true,则用clearRect绘制一个空白矩形,重复步骤点击、滑动、松开即可达到擦除的效果。
if (this.eraser) {
this.cxt.clearRect(x, y, 10, 10);
}
清空
清空画布主要通过canvas的api HTML canvas clearRect() 方法 (w3school.com.cn)
remove() {
// 清空画布
this.cxt.fillStyle = "#ffebce";
this.cxt.clearRect(0, 0, this.width, this.height);
},
画笔颜色和宽度
这个实现起来也比较简单,主要是通过组件通信来传递数据,我这里用的事件总线。utils文件夹新建一个js文件。
import Vue from 'vue'; // 事件总线
export default new Vue;
页面需要的话,直接导入
import bus from "../utils/eventBus";
同时,也可以选择挂载到Vue上,感兴趣的掘友可查看这篇文章 Vue事件总线(EventBus)使用详细介绍 - 知乎 (zhihu.com)
下载
下载的话,使用的file-saver的软件包,可以从npm下载 file-saver - npm (npmjs.com)
this.canvas.toBlob(function (blob) {
const time = new Date().getTime();
saveAs(blob, `${time}.png`);
});
撤回
这里主要牵扯的是两个api的应用,HTML canvas getImageData() 方法 (w3school.com.cn)和HTML canvas putImageData() 方法 (w3school.com.cn)
开始绘制的时候存入history数组,撤回的时候从数组中删除,使用putImageData放入尾元素到画布。
cancel() {
// 撤回上一步
this.history.pop();
if (this.history.length == 1) {
this.cxt.putImageData(this.history[this.history.length - 1], 0, 0);
bus.$emit("disabled", "禁用撤回");
} else {
this.cxt.putImageData(this.history[this.history.length - 1], 0, 0);
}
},
这里为了防止多次点击撤回,数组清空控制台报错,添加了撤回禁用功能,感兴趣的掘友可查看源码。
上传
上传的话,由于没有服务器的缘故,我这里使用的存储地是strapi(之前同学帮我部署过strapi) strapi.io Strapi是一款免费开源的Nodejs无头CMS内容管理框架,很好用,对于不懂后端的前端开发人员很友好!!
upload() {
this.$prompt("请输入答案", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
center: true,
})
.then(({ value }) => {
let that = this;
canvas.toBlob(function (blob) {
const formData = new FormData();
const time = new Date();
formData.append("files.img", blob, `${time.getTime()}.png`);
formData.append("data", JSON.stringify({ description: value }));
axios.post("你的url", formData).then(
(_res) => {
//console.log(res);
that.$message({
type: "success",
message: "图片上传成功",
});
},
(_err) => {
that.$message({
type: "error",
message: "图片上传失败",
});
}
);
});
})
.catch(() => {
this.$message({
type: "info",
message: "取消输入",
});
});
},
最后
画板的实现思路参考自实现一个canvas画板_A-Tione的博客-CSDN博客_canvas画板,感谢大佬的技术分享。