基于Vue+Canvas实现的画板绘画以及保存功能,解决保存没有背景问题

news2024/11/16 3:23:41

基于Vue+Canvas实现的画板绘画以及保存功能

本文内容设计到的画板的js部分内容来源于灵感来源引用地址,然后我在此基础上,根据自己的需求做了修改,增加了其他功能。
在这里插入图片描述

下面展示了完整的前后端代码

这里写目录标题

  • 基于Vue+Canvas实现的画板绘画以及保存功能
    • 1. board-js.js
    • 2. 前端的vue文件
    • 3. 后端Controller

1. board-js.js

这个代码,接收一个容器参数,创建了一个画板类,里面实现了画板会用到的基本方法,保存到单独的js文件,在vue文件中导入,创建一个画板对象。
Canvas直接使用canvas.toDataURL()保存的图片是没有背景的,因为默认的是png,所以需要开始绘画之前先填充背景,下面代码做出了修改,在init()函数中。
代码中用到的Canvas的原生API的作用,可以参考这里Canvas参考手册

export default class BoardCanvas {
    constructor(container) {
        // 容器
        this.container = container
        // canvas画布
        this.canvas = this.createCanvas(container)
        // 绘制工具
        this.ctx = this.canvas.getContext('2d')
        // 起始点位置
        this.startX = 0
        this.stateY = 0
        // 画布历史栈
        this.pathSegmentHistory = []
        this.index = 0

        // 初始化
        this.init()
    }

    // 创建画布
    createCanvas(container) {
        const canvas = document.createElement('canvas')
        canvas.width = container.clientWidth
        canvas.height = container.clientHeight
        canvas.style.display = 'block'
        canvas.style.backgroundColor = 'white'
        container.appendChild(canvas)
        return canvas
    }

    // 初始化
    init() {
        this.addPathSegment()
        this.setContext2DStyle()
        //下面两行,原文件是没有的,如果没有会导致保存的图片没有背景,只有绘画轨迹
        this.ctx.fillStyle = "#ffffff";
        this.ctx.fillRect(0, 0,this.canvas.width, this.canvas.height);
        
        this.canvas.addEventListener('contextmenu', e => e.preventDefault())
        this.canvas.addEventListener('mousedown', this.mousedownEvent.bind(this))
        window.document.addEventListener('keydown', this.keydownEvent.bind(this))
    }

    // 设置画笔样式
    setContext2DStyle() {
        this.ctx.strokeStyle = 'black'
        this.ctx.lineWidth = 3
        this.ctx.lineCap = 'round'
        this.ctx.lineJoin = 'round'
    }

    // 鼠标事件
    mousedownEvent(e) {
        const that = this
        const ctx = this.ctx
        ctx.beginPath()
        ctx.moveTo(e.offsetX, e.offsetY)
        ctx.stroke()

        this.canvas.onmousemove = function (e) {
            ctx.lineTo(e.offsetX, e.offsetY)
            ctx.stroke()
        }
        this.canvas.onmouseup = this.canvas.onmouseout = function () {
            that.addPathSegment()
            this.onmousemove = null
            this.onmouseup = null
            this.onmouseout = null
        }
    }

    // 键盘事件
    keydownEvent(e) {
        if(!e.ctrlKey) return
        switch(e.keyCode) {
            case 90:
                this.undo()
                break
            case 89:
                this.redo()
                break
        }
    }

    // 添加路径片段
    addPathSegment() {
        const data = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
        // 删除当前索引后的路径片段,然后追加一个新的路径片段,更新索引
        this.pathSegmentHistory.splice(this.index + 1)
        this.pathSegmentHistory.push(data)
        this.index = this.pathSegmentHistory.length - 1
    }

    // 撤销
    undo() {
        if(this.index <= 0) return
        this.index--
        this.ctx.putImageData(this.pathSegmentHistory[this.index], 0, 0)
    }
    // 恢复
    redo() {
        if(this.index >= this.pathSegmentHistory.length - 1) return
        this.index++
        this.ctx.putImageData(this.pathSegmentHistory[this.index], 0, 0)
    }
	//获取画布内容
    getImage() {
        return this.canvas.toDataURL();
    }
    //清空画板
    cleanboard(){
        this.ctx.fillStyle = "#ffffff";
        this.ctx.fillRect(0, 0,this.canvas.width, this.canvas.height);
    }
}

2. 前端的vue文件

<template>
  <el-container direction="vertical" style="height: 100%;width: 100%">
    <!--头顶布局,用户操作提示语-->
    <el-header height="10%">
      <h2>在空白处进行绘画</h2>
    </el-header>
    <!--中间布局 -->
    <div style=display:flex;justify-content:center;align-items:center;>
      <!--中间画板-->
      <div class="drawing-board"
           style="width:66%;height:600px;border: 1px black solid;margin-left: 10px">
        <div id="container" ref="container" style="width: 100%; height: 100%"></div>
      </div>
    </div>
    <!--底部按钮-->
    <el-footer style="height: 300px;margin-top: 25px">
      <el-button type="primary" round @click="savedrawing">保存</el-button>
    </el-footer>
  </el-container>
</template>

//这里使用的vue的setup语法糖,所以data和method不需要封装,直接用
<script setup>
import { ref, onMounted } from 'vue'
import Board from '@/js/drawing-board.js'
import axios from "axios";

const container = ref(null)
let drawboard = null;
onMounted(() => {
  // 新建一个画板
  drawboard=new Board(container.value)
})
function savedrawing() {
  const drawdata = drawboard.getImage();				
  let formdata = new FormData();
  const timestamp = (new Date()).valueOf();
  let filename = timestamp;
  formdata.append("drawPictureId",filename);
  formdata.append("drawPictureData",drawdata.substring(22));
  axios({
    method:"post",
    url:"/savedrawdata",
    baseURL:"http://localhost:9999",
    data:formdata,
    contentType:false,
    processData:false
  }).then(response=>{
    if(response.status===200){
      alert("保存成功!")
    }
  }).catch(error=>{
    console.log(error);
  })
}
</script>

3. 后端Controller

Controller文件中,前端使用FormData格式传递参数,就相当于是个map键值对,所以在参数这里,使用 @RequestParam() ,取出表单中的值,括号中的字符串,是在前端传入的,表示将该key对应的value,赋值给后面的String 参数。
其次,这里注意Canvas得到的图片是经过base64编码过的,所以先解码成字节数组

    @RequestMapping("/savedrawdata")
    public ResponseEntity<?> savedrawdata(@RequestParam("drawPictureId")String drawPictureId,
                                          @RequestParam("drawPictureData")String drawPictureData){
        try {
            // 解码前端传过来的base64编码
            byte[] imageBytes = Base64.decodeBase64(drawPictureData);

            // 将字节流转为图片缓冲流
            BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(imageBytes));

            // 保存为png
            File output = new File("F:\\image\\" + drawPictureId + ".png");
            ImageIO.write(bufferedImage, "png", output);

        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(rawPictureId);
        System.out.println(drawPictureId);
        return ResponseEntity.ok(HttpStatus.OK);
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1408518.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

面向对象、封装、继承、多态、JavaBean

二、面向对象 什么是对象 什么是对象&#xff1f;之前我们讲过&#xff0c;对象就是计算机中的虚拟物体。例如 System.out&#xff0c;System.in 等等。然而&#xff0c;要开发自己的应用程序&#xff0c;只有这些现成的对象还远远不够。需要我们自己来创建新的对象。 1. 抽…

confluence模版注入漏洞_CVE-2023-22527

1. 漏洞简介 Confluence是Atlassian公司开发的一款专业的企业知识管理与协同软件&#xff0c;可用于构建企业wiki。 Confluence Data Center和Confluence Server多个受影响版本中存在模板注入漏洞&#xff0c;未经身份验证的威胁者可利用该漏洞在受影响的实例上实现远程代码执…

暗藏危险,警惕钓鱼邮件!

叮 您有一份福利待查收 您的信息资产需要排查 您的账户异常需要验证 这些看似“重要”的邮件 都藏着攻击者的恶意嘴脸 随着网络安全防护和建设的重要性日益凸显&#xff0c;国家安全、企业安全、合规需求及业务驱动等各个方面都亟需将网络安全作为基石。在企业业务转型发展…

v-on、事件修饰符、v-model、一些常用指令

v-on 事件修饰符 Vue.js 为 v-on 提供了事件修饰符来处理 DOM 事件细节&#xff0c;如&#xff1a;event.preventDefault() 或 event.stopPropagation()。 Vue.js 通过由点 . 表示的指令后缀来调用修饰符。 .stop - 阻止冒泡 .prevent - 阻止默认事件 .capture - 阻止捕获 .s…

【jetson笔记】解决vscode远程调试qt.qpa.xcb: could not connect to display报错

配置x11转发 jetson远程安装x11转发 安装Xming Xming下载 安装完成后打开安装目录C:\Program Files (x86)\Xming 用记事本打开X0.hosts文件&#xff0c;添加jetson IP地址 后续IP改变需要重新修改配置文件 localhost 192.168.107.57打开Xlaunch Win菜单搜索Xlaundch打开 一…

递归和尾递归(用C语言解斐波那契和阶乘问题)

很多人都对递归有了解&#xff0c;但是为尾递归很少&#xff0c;所以这次来专门讲一讲关于尾递归的一些问题。 什么是尾递归 如果一个函数中所有递归形式的调用都出现在函数的末尾&#xff0c;我们称这个递归函数是尾递归的。因为在一些题目的做法中&#xff0c;我们可以发现…

JavaFX场景入门

目录 JAVAFX jdk1.8以上引入javafx类库 JDK11JAVAFX(eclipse) 小知识点 舞台Stage platform、screen类 Scene场景类 查看电脑屏幕宽高 Group容器 JAVAFX项目 Image javafx场景 javaFx文本 javaFX颜色 JAVAFX jdk1.8以上引入javafx类库 JDK11JAVAFX(eclipse) 方式…

MySQL分组,获取组内最新的10条数据

一、记录 记录一次SQL&#xff0c;最近在项目中遇到了一个相对比较复杂的SQL。 要求依据分组&#xff0c;获取每个分组后的前10条数据。 分组查询最新的数据&#xff0c;应该都做过&#xff0c;但是获取前10条数据&#xff0c;还是没处理过的。 二、处理 2.1 前期数据准备 …

数据分析-Pandas如何用图把数据展示出来

数据分析-Pandas如何用图把数据展示出来 俗话说&#xff0c;一图胜千语&#xff0c;对人类而言一串数据很难立即洞察出什么&#xff0c;但如果展示图就能一眼看出来门道。数据整理后&#xff0c;如何画图&#xff0c;画出好的图在数据分析中成为关键的一环。 数据表&#xff…

【进入游戏行业选游戏特效还是技术美术?】

进入游戏行业选游戏特效还是技术美术&#xff1f; 游戏行业正处于蓬勃发展的黄金时期&#xff0c;科技的进步推动了游戏技术和视觉艺术的飞速革新。在这个创意和技术挑战交织的领域里&#xff0c;游戏特效和技术美术岗位成为了许多人追求的职业目标。 这两个岗位虽然紧密关联…

5.ROC-AUC机器学习模型性能的常用的评估指标

最近回顾机器学习基础知识部分的时候&#xff0c;看到了用于评估机器学习模型性能的ROC曲线。再次记录一下&#xff0c;想起之前学习的时候的茫然&#xff0c;希望这次可以更加清晰的了解这一指标。上课的时候听老师提起过&#xff0c;当时没有认真去看&#xff0c;所以这次可以…

硅像素传感器文献调研(八)

2000硅微带探测器的物理模拟&#xff1a;电极几何形状对临界电场的影响。 2000硅微带探测器的物理模拟&#xff1a;电极几何形状对临界电场的影响 摘要 本文介绍了交流耦合硅微带探测器的计算机分析。这项研究的目的是调查的主要几何参数负责潜在的关键影响&#xff0c;如早期…

【GitHub项目推荐--多功能 Steam 工具箱】【转载】

Steam 是一个开源跨平台的多功能 Steam 工具箱&#xff0c;此工具的大部分功能都是需要下载安装 Steam 才能使用。 功能包括网络加速、脚本配置、账号切换、库存管理、自动挂卡、游戏工具&#xff0c;比如强制游戏窗口使用无边框窗口化。 开源地址&#xff1a;https://github…

c++:类和对象(5),运算符重载

目录 运算符重载概念&#xff1a; 运算符重载 1.成员函数重载号 2.全局函数重载号 打印结果&#xff1a; <<运算符重载 递增运算符重载 简单例子 输出结果为&#xff1a; 赋值运算符重载 如何重载 输出结果为&#xff1a; 什么时候重载 关系运算符重载 简单例…

【并发编程】同步模式之保护性暂停

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;并发编程 ⛺️稳中求进&#xff0c;晒太阳 同步模式之保护性暂停 这个模式用到的基础就是wait-notify 详情可以看这篇文章》:【并发编程】wait/notify 即Guarded Suspension,用在一个线…

鸿蒙开发 状态管理

最近学习鸿蒙开发。 状态管理&#xff1a; State -> Prop 单向传递&#xff1b; stateprop: State -> Prop 单向传递 State -> Link 双向传递&#xff1b;

Windows脚本:监控并自动重启某个进程

Windows脚本&#xff1a;监控自动并重启某个进程 一、简介二 .bat脚本方式2.1 编制脚本2.2 创建并运行脚本2.3 设置关闭cmd窗口 三、使用VBScript脚本方式3.1 编制脚本3.2 运行脚本 四、设置脚本开机自启动五、某些软件加入启动项后&#xff0c;开机不会自动启动的解决方法 在实…

代码随想录算法训练营第十四天|二叉树基础-二叉树迭代-二叉树

文章目录 二叉树基础二叉树种类满二叉树完全二叉树二叉搜索树平衡二叉搜索树 二叉树的存储方式链式存储顺序存储 二叉树的遍历方式二叉树的定义 二叉树的递归遍历144.二叉树的前序遍历代码&#xff1a; 145.二叉树的后序遍历代码&#xff1a; 94. 二叉树的中序遍历代码 二叉树的…

Linux中并发程序设计

进程的创建和回收 进程概念 概念 程序 存放在磁盘上的指令和数据的有序集合&#xff08;文件&#xff09; 静态的 进程 执行一个程序所分配的资源的总称 动态的进程和程序比较 注&#xff1a;进程是存在RAM中&#xff0c;程序是存放在ROM(flash)中的进程内容 BSS段&#xff…

Unity通用渲染管线升级URP、HDRP

Unity通用渲染管线升级URP、HDRP 一、Build-in Pipline升级到 URP 一、Build-in Pipline升级到 URP 安装URP包 升级所有材质&#xff08;升级完成后材质会变成紫红色&#xff0c;Shader丢失&#xff0c;此为正常现象&#xff09; 创建 UniversalRenderPipelineAsset 配置文…