vue2 + canvas 实现简易画板

653 阅读3分钟

前言

这两天在做QQ聊天机器人,目前也实现了一些功能

image.png

准备往里面加一些小游戏的功能,思来想去就准备自己实现一个简易版的你画我猜,但是QQ肯定不支持我发个画板过来,于是便诞生了这个网站picture_board (gitee.io)。网站部署在gitee pages上,感兴趣可以去picture_board(gitee.com)下载源代码。

实现思路

整个页面比较简单,上下的两栏布局。上面是canvas的绘画区域,下面是功能选择区域。(请忽略计时框,这个是为了在你画我猜中,提醒绘画时间用的)

image.png

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端绘画

主要相关的是三个事件

  1. 点击鼠标
  2. 移动鼠标
  3. 松开鼠标

当鼠标点击时用一个变量记录下该坐标点(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画板,感谢大佬的技术分享。