Desarrollo de proyectos VUE, utilizando lienzo para implementar la función de panel de escritura a mano de edición de firmas de imágenes

El proyecto vue utiliza lienzo para implementar la función del panel de escritura a mano.

representaciones

Insertar descripción de la imagen aquí

 ComponentCanvasDialog.vue

Cargue el código directamente. El siguiente código puede ser referenciado directamente como un componente. Puede cargar la imagen correspondiente según sus propias necesidades. El ícono de operación debe ser reemplazado por usted mismo y la función de guardar también debe implementarla usted mismo.

<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>

Selector de componentesColor.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>

Formato de transmisión de parámetros

Figura 1: La imagen de fondo es base64

Insertar descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/weixin_42504805/article/details/131659516
Recomendado
Clasificación