VUE项目开发,使用canvas实现图片签名编辑手写板功能

效果图

在这里插入图片描述

组件CanvasDialog.vue

直接上代码,下面代码可以当做组件直接引用,根据自己的需求传对应的图片即可,操作图标需要自己替换,保存功能也需要自己实现。

<template>
  <el-dialog
    :visible="true"
    title="图片编辑"
    style="font-size: 18px"
    width="1400px"
    :close-on-click-modal="false"
    @close="closeDialog"
    append-to-body
  >
    <div class="modal-body">
      <div class="container">
        <canvas height="570"
            id="canvas"
            ref="canvas"
            width="940"></canvas>
        <div class="tool-container">
          <div class="icon-div icon" @click="isShowDrawPane = !isShowDrawPane">
          	<!-- 举例子:svg-icon可换成<i class="el-icon-delete"></i> -->
            <svg-icon icon-class="draw" scale="4"></svg-icon>
          </div>
          <div class="icon-div icon" @click="filterObject('erase')">
            <svg-icon icon-class="erase" scale="4"></svg-icon>
          </div>
          <div class="icon-div icon" @click="filterObject('undefined')">
            <svg-icon icon-class="ziyoubi" scale="4"></svg-icon>
          </div>
          <div class="icon-div icon" @click="filterObject('line')">
            <svg-icon icon-class="line" scale="4"></svg-icon>
          </div>
          <div class="icon-div icon" @click="filterObject('arrows')">
            <svg-icon icon-class="arrows" scale="4"></svg-icon>
          </div>
          <div class="icon-div icon" @click="filterObject('rect')">
            <svg-icon icon-class="rect" scale="4"></svg-icon>
          </div>
          <div class="icon-div icon" @click="filterObject('circle')">
            <svg-icon icon-class="circle" scale="4"></svg-icon>
          </div>
          <div class="icon-div icon" @click="filterObject('text')">
            <svg-icon icon-class="text" scale="4"></svg-icon>
          </div>
          <div class="icon-div icon" @click="clearCanvas()">
            <svg-icon icon-class="clear" scale="4"></svg-icon>
          </div>
          <div class="icon-div icon" @click="redo()">
            <svg-icon :icon-class="historyImageData.length > 0 ? 'redo' : 'grey-redo' " scale="4"></svg-icon>
          </div>
          <div class="icon-div icon" @click="cancelRedo()">
            <svg-icon :icon-class="newHistoryImageData.length > 0 ? 'cancelRedo' : 'grey-cancelRedo' " scale="4"></svg-icon>
          </div>
          <div class="icon-div icon" @click="downLoad()">
            <svg-icon icon-class="download" scale="4"></svg-icon>
          </div>
          <div class="drawPane" v-show="isShowDrawPane">
            <div @click="isShowDrawPane = false">
              <svg-icon icon-class="close" class="close-draw-pane icon" scale="3"></svg-icon>
            </div>
            <div class="colorClass">画笔大小</div>
            <input type="range" id="lwRange" min="1" max="10" value="1" @change="LwRangeBtn"/>
            <div class="colorClass">画笔颜色</div>
            <input type="color" id="lcolor" value="#FF1493" @change="LcolorBtn"/>
          </div>
        </div>
        <textarea
          id="textarea"
          name="textBox"
          cols="9"
          rows="1"
          class="text-style"
          v-show="isShowText"
        ></textarea>
      </div>
      </div>
    <div slot="footer" class="dialog-footer">
      <el-button plain @click="closeDialog">取 消</el-button>
      <el-button type="primary" @click="submitBtn" class="g-background00BCD4" :disable="loading" :loading="loading">保 存</el-button>
    </div>
  </el-dialog>
</template>

<script>
//画笔颜色选择引入
import pickerColor from './pickerColor'
export default {
  props: {
    otherParameter: Object,//我这里传了对象是因为我的业务需求,可直接传baseUrl:String
  },
  components:{pickerColor},
  data() {
    return {
      form: {},

      isShowDrawPane: false,
      canvas: null,
      context: null,
      //线宽
      lwidth: 2,
      //画笔颜色
      lcolor: "#FF1493",
      textColor:"#FF1493",
      //维护绘画状态的数组
      paintTypeArr: {
        painting: false,
        erase: false,
        line: false,
        arrows: false,
        rect: false,
        circle: false,
        text: false,
      },
      //最近一次的canvas图片的数据
      imageData: null,
      //是否显示文字编写框
      isShowText: false,
      //保存画布图片历史的数据
      historyImageData:[],
      //保存已被撤销的历史画布图片数据
      newHistoryImageData:[],
      socket:null,
      img: null,
      filterType: undefined,
      loading: false
    };
  },
  watch: {
    color () {
      this.context.strokeStyle = this.color;
      // this.pickerVisible = false//颜色改变后消失
    }
  },
  mounted() {
    let self = this;
    self.init()  
    window.onresize = function () {
      self.init()  
    }  
    this.listen() 
  },
  methods: {
    LwRangeBtn() {
      this.lwidth = parseInt(document.getElementById("lwRange").value)  
    },
    LcolorBtn() {
      this.context.fillStyle = document.getElementById("lcolor").value  
      this.context.strokeStyle = document.getElementById("lcolor").value  
      this.textColor = document.getElementById("lcolor").value 
    },

    closeDialog() {
      this.$emit("onClose");
    },
    dataURLtoFile(dataURI, type) {
      let binary = atob(dataURI.split(',')[1]);
      let array = [];
      for(let i = 0; i < binary.length; i++) {
                array.push(binary.charCodeAt(i));
      }
      return new Blob([new Uint8Array(array)], {type:type });
    },
     //初始化画布
    init() {
      this.$nextTick(()=>{
        this.canvas = document.getElementById("canvas")  
        this.context = this.canvas.getContext('2d')
        this.imageData && this.context.putImageData(this.imageData, 0, 0)  
        let img = new Image()
        img.setAttribute('crossOrigin', 'anonymous');
        let url = this.otherParameter.base64;//重点之重,这是要编辑的图片base64,如图一
        img.src = url
        img.onload = () => {
          if (img.complete) {
            this.canvas.setAttribute('width', img.width)
            this.canvas.setAttribute('height', img.height)
            this.context .drawImage(img, 0, 0, img.width, img.height)
            this.img = img
            this.textColor = "#FF1493";
            this.context.fillStyle =  "#FF1493";
            this.context.strokeStyle =  "#FF1493";
          }
        }
      })
    },
    //监听鼠标,用于画笔任意绘制和橡皮擦
    listen() {
      this.$nextTick(()=>{
        let self = this  
        let lastPoint = { x: undefined, y: undefined }  
        let rect = self.canvas.getBoundingClientRect()  
        console.log(rect,"rect")
        var scaleX = self.canvas.width / rect.width  
        var scaleY = self.canvas.height / rect.height  
        console.log(scaleX,"scaleX")
        console.log(scaleY,"scaleY")
        let textPoint = { x: undefined, y: undefined }  

        self.canvas.onmousedown = function (e) {
          self.paintTypeArr["painting"] = true  

          let x1 = e.clientX  
          let y1 = e.clientY  
          x1 -= rect.left  
          y1 -= rect.top  
          lastPoint = { x: x1 * scaleX, y: y1 * scaleY }  
          console.log((self.paintTypeArr["text"]))
          if (self.paintTypeArr["text"]) {
            let textarea = document.getElementById("textarea")  
            if (self.isShowText) {
              let textContent = textarea.value  
              self.isShowText = false   
              textarea.value = ""  
              console.log(textPoint.x, textPoint.y,"textPoint.x, textPoint.y,")
              self.drawText(textPoint.x, textPoint.y, textContent) 
            } else if (!self.isShowText) {
              self.isShowText = true  
              textarea.style.left = lastPoint.x + "px"  
              textarea.style.top = lastPoint.y + 160 + "px"  
              textarea.style.color = self.textColor
              textPoint = { x: lastPoint.x, y: lastPoint.y }  
              // textarea.style['z-index'] = 6
            }
          }

          if (self.paintTypeArr["erase"]) {
            let ctx = self.context  
            ctx.save()  
            ctx.globalCompositeOperation = "destination-out"  
            ctx.beginPath()  
            let radius = self.lWidth / 2 > 5 ? self.lWidth / 2 : 5  
            ctx.arc(lastPoint.x, lastPoint.y, radius, 0, 2 * Math.PI)  
            ctx.clip()  
            ctx.clearRect(0, 0, self.canvas.width, self.canvas.height)  
            ctx.restore()  
          }

          var thee = e ? e : window.event  
          self.stopBubble(thee)  
        }  
        self.canvas.onmousemove = function (e) {
          let x2 = e.clientX  
          let y2 = e.clientY  
          x2 -= rect.left  
          y2 -= rect.top  
          let newPoint = { x: x2 * scaleX, y: y2 * scaleY }  

          if (self.isPainting()) {
            self.drawLine(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y)  
            lastPoint = newPoint  
          } else if (self.paintTypeArr["erase"]) {
            if(!lastPoint.x && !lastPoint.y){return}
            self.handleErase(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y)  
            lastPoint = newPoint  
          } else if (self.paintTypeArr["line"]) {
            // self.clearCanvas()  
            self.imageData && self.context.putImageData(self.imageData, 0, 0)  
            self.drawLine(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y)  
          } else if (self.paintTypeArr["arrows"]) {
            // self.clearCanvas()  
            self.imageData && self.context.putImageData(self.imageData, 0, 0)  
            self.drawArrow(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y)  
          } else if (self.paintTypeArr["rect"]) {
            // self.clearCanvas()  
            self.imageData && self.context.putImageData(self.imageData, 0, 0)  
            self.drawRect(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y)  
          } else if (self.paintTypeArr["circle"]) {
            // self.clearCanvas()  
            
            self.imageData && self.context.putImageData(self.imageData, 0, 0) 
            console.log(self.imageData) 
            self.drawCircle(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y)  
          }

          var thee = e ? e : window.event  
          self.stopBubble(thee)  
        }  
        self.canvas.onmouseup = function () {
          lastPoint = { x: undefined, y: undefined }  
          self.canvasDraw()  
          console.log(123)
          self.filterObject(self.filterType)  
        }  
      })
    },
    //更新绘画类型数组paintTypeArr的状态
    filterObject(type) {
      this.filterType = type
      if (!type) {
        for (const key in this.paintTypeArr) {
          this.paintTypeArr[key] = false  
        }
      } else {
        for (const key in this.paintTypeArr) {
          key === type
            ? (this.paintTypeArr[key] = true)
            : (this.paintTypeArr[key] = false)  
        }
      }
    },
    //阻止事件冒泡
    stopBubble(evt) {
      if (evt.stopPropagation) {
        evt.stopPropagation()  
      } else {
        //ie
        evt.cancelBubble = true  
      }
    },
    //判断是否是自由绘画模式
    isPainting() {
      for (let key in this.paintTypeArr) {
        if (key !== "painting" && this.paintTypeArr[key]) {
          return false  
        }
      }
      if (this.paintTypeArr["painting"]) {
        return true  
      }
      return false  
    },
    //橡皮擦
    handleErase(x1, y1, x2, y2) {
      let ctx = this.context  
      //获取两个点之间的剪辑区域四个端点
      var asin = radius * Math.sin(Math.atan((y2 - y1) / (x2 - x1)))  
      var acos = radius * Math.cos(Math.atan((y2 - y1) / (x2 - x1)))  
      var x3 = x1 + asin  
      var y3 = y1 - acos  
      var x4 = x1 - asin  
      var y4 = y1 + acos  
      var x5 = x2 + asin  
      var y5 = y2 - acos  
      var x6 = x2 - asin  
      var y6 = y2 + acos   //保证线条的连贯,所以在矩形一端画圆

      ctx.save()  
      ctx.beginPath()  
      ctx.globalCompositeOperation = "destination-out"  
      let radius = this.lWidth / 2 > 5 ? this.lWidth / 2 : 5  
      ctx.arc(x2, y2, radius, 0, 2 * Math.PI)  
      ctx.clip()  
      ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)  
      ctx.restore()   //清除矩形剪辑区域里的像素

      ctx.save()  
      ctx.beginPath()  
      ctx.globalCompositeOperation = "destination-out"  
      ctx.moveTo(x3, y3)  
      ctx.lineTo(x5, y5)  
      ctx.lineTo(x6, y6)  
      ctx.lineTo(x4, y4)  
      ctx.closePath()  
      ctx.clip()  
      ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)  
      ctx.restore()  
    },
    //画线
    drawLine(fromX, fromY, toX, toY) {
      
      let ctx = this.context  
      ctx.beginPath()  
      ctx.lineWidth = this.lwidth  
      ctx.lineCap = "round"  
      ctx.lineJoin = "round"  
      ctx.moveTo(fromX, fromY)  
      ctx.lineTo(toX, toY)  
      ctx.stroke()  
      ctx.closePath()  
    },
    //画箭头
    drawArrow(fromX, fromY, toX, toY) {
      let ctx = this.context  
      var headlen = 10   //自定义箭头线的长度
      var theta = 45   //自定义箭头线与直线的夹角,个人觉得45°刚刚好
      var arrowX, arrowY   //箭头线终点坐标
      // 计算各角度和对应的箭头终点坐标
      var angle = (Math.atan2(fromY - toY, fromX - toX) * 180) / Math.PI  
      var angle1 = ((angle + theta) * Math.PI) / 180  
      var angle2 = ((angle - theta) * Math.PI) / 180  
      var topX = headlen * Math.cos(angle1)  
      var topY = headlen * Math.sin(angle1)  
      var botX = headlen * Math.cos(angle2)  
      var botY = headlen * Math.sin(angle2)  
      ctx.beginPath()  
      //画直线
      ctx.moveTo(fromX, fromY)  
      ctx.lineTo(toX, toY)  

      arrowX = toX + topX  
      arrowY = toY + topY  
      //画上边箭头线
      ctx.moveTo(arrowX, arrowY)  
      ctx.lineTo(toX, toY)  

      arrowX = toX + botX  
      arrowY = toY + botY  
      //画下边箭头线
      ctx.lineTo(arrowX, arrowY)  

      ctx.stroke()  
      ctx.closePath()  
    },
    //绘制矩形
    drawRect(topLeftX, topLeftY, botRightX, botRightY) {
      let ctx = this.context  
      ctx.strokeRect(
        topLeftX,
        topLeftY,
        Math.abs(botRightX - topLeftX),
        Math.abs(botRightY - topLeftY)
      )  
    },
    //画圆
    drawCircle(circleX, circleY, endX, endY) {
      console.log(circleX, circleY, endX, endY)
      let ctx = this.context  
      let radius = Math.sqrt(
        (circleX - endX) * (circleX - endX) +
          (circleY - endY) * (circleY - endY)
      )  
      ctx.beginPath()  
      ctx.arc(circleX, circleY, radius, 0, Math.PI * 2, true)  
      ctx.stroke()  
    },
    //画文字
    drawText(startX, startY, content) {
      let ctx = this.context  
      ctx.save()  
      ctx.beginPath()  
      ctx.font = "25px orbitron"  
      ctx.textBaseline = "top"  
      ctx.fillText(content, parseInt(startX), parseInt(startY))  
      ctx.restore()  
      ctx.closePath()  
    },
    //清屏
    clearCanvas() {
      this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)  
      this.init()
      console.log(this.imageData) 
    },
    //定格画布图片
    canvasDraw() {
      this.imageData = this.context.getImageData(0,0,this.canvas.width,this.canvas.height)  
      this.historyImageData.push(this.imageData)
      console.log(this.historyImageData)
      console.log(this.imageData)
    },
    //撤销
    redo(){
      let historyImageData = this.historyImageData
      let newHistoryImageData = this.newHistoryImageData
      if(historyImageData.length > 0){
        let hisImg = historyImageData.pop()
        newHistoryImageData.push(hisImg)
        if(historyImageData.length === 0){
          this.imageData = null
          this.clearCanvas()
          this.init()
        }else{
          this.context.putImageData(historyImageData[historyImageData.length - 1],0,0)
        }
      }
    },
    //反撤销
    cancelRedo(){
      if(this.newHistoryImageData.length > 0){
        const newHisImg = this.newHistoryImageData.pop()
        this.imageData = newHisImg
        this.context.putImageData(newHisImg,0,0)
        this.historyImageData.push(newHisImg)
      }
    },
    //保存图片
    downLoad(){
      const imgUrl = this.canvas.toDataURL('image/png')
      const a = document.createElement('a')
      a.href = imgUrl
      a.download = '绘图保存记录' + (new Date).getTime()
      a.target = '_blank'
      document.body.appendChild(a)
      a.click()
      document.body.removeChild(a);
      console.log(this.imageData) 
    },
	submitBtn() {
		//防止多次点击提交
      this.loading = true;
      setTimeout(()=>{
          this.loading = false;
      },3000)

      let fileObj = {
        relativeType: 3,
        name:"编辑图片"
      }
      let canvas = document.getElementById('canvas')
      var file = canvas.toDataURL("image/png");
      var formData = new FormData();
      let blob= this.dataURLtoFile(file, 'image/jpg')
      let fileOfBlob = new File([blob], new Date()+'.jpg')
      formData.append('file', fileOfBlob);
      formData.append('relativeType', 3);
      formData.append('name', "编辑图片");
      //上传图片后提交保存,根据实际开发需求编写
      this.$axios
        .postUpload("/uxxxoad", formData)
        .then((response) => {
         this.$api.creatxxxxRule({taskBreakRule}).then((response)=>{
          if(response.success) {  
            this.$message({
              message: "保存成功",
              type: "success"
            });
            this.$emit("onClose",true)
          } else {
            this.$message({
              message: response.info,
              type: "error"
            });
          }
        })
    });
    },
  },
};
</script>

<style lang="scss" scoped>
.container {
  // width: 100%;
  // height: 100%;
  // margin: 10px auto;
  // overflow: hidden;
}
.tool-container {
  width: 580px;
  border: 2px solid orange;
  border-radius: 10px;
  display: flex;
  justify-content: center;
  position: relative;
}
.drawPane {
  padding: 25px 20px;
  height: 120px;
  position: absolute;
  top: -120px;
  left: 0px;
  border-radius: 5px;
  border: 2px solid orangered;
}
.close-draw-pane {
  position: absolute;
  right: 5px;
  top: 5px;
}
.icon-div {
  margin: 4px 12px;
}
.icon :hover {
  cursor: pointer;
}
input[type="range"] {
  -webkit-appearance: none;
  width: 180px;
  height: 24px;
  outline: none;
  margin-bottom: 3px;
}
input[type="range"]::-webkit-slider-runnable-track {
  background-color: orangered;
  height: 4px;
  border-radius: 5px;
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: orange;
  cursor: pointer;
  margin-top: -4px;
}
.text-style {
  float: left;
  position: absolute;
  font: 25px orbitron;
  word-break: break-all;
  background-color: transparent;
}
.colorClass {
  color: orange;
}
.svg-icon {
  font-size: 24px;
}
</style>

组件pickerColor.vue

<template>
  <div>
    <photoshop-picker v-if="type === 'photoshop'" v-model="colors"></photoshop-picker>
    <material-picker v-if="type === 'material'" v-model="colors"></material-picker>
    <compact-picker v-if="type === 'compact'" v-model="colors"></compact-picker>
    <swatches-picker v-if="type === 'swatches'" v-model="colors"></swatches-picker>
    <slider-picker v-if="type === 'slider'" v-model="colors"></slider-picker>
    <sketch-picker v-if="type === 'sketch'" v-model="colors"></sketch-picker>
    <chrome-picker v-if="type === 'chrome'" v-model="colors"></chrome-picker>
  </div>
</template>

<script>
//这些不需要单独引入,vue项目构建会安装了vue-color这个依赖包,在根目录node_modules可以找到vue-color依赖包。
import {
  Photoshop,
  Material,
  Compact,
  Swatches,
  Slider,
  Sketch,
  Chrome,
} from "vue-color";
export default {
  name: "pickerColor",
  props: {
    "color": String,
    type: {
      default: "photoshop",
    },
  },
  components: {
    "photoshop-picker": Photoshop,
    "material-picker": Material,
    "compact-picker": Compact,
    "swatches-picker": Swatches,
    "slider-picker": Slider,
    "sketch-picker": Sketch,
    "chrome-picker": Chrome,
  },
  data () {
    return {
      colors: "",
    };
  },
  methods: {},
  watch: {
    colors () {
      this.$emit("update:color", this.colors.hex);
    },
  },
};
</script>

<style></style>

传参格式

图一:
在这里插入图片描述
不敢说很全面,但是应该也够用了,也希望帮到有同样需求的你们。

寄语

在这里插入图片描述

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值