vue3 签名组件

基于gitee 上的vue-asign插件改造。
修改部分:
1.使用setup 语法糖替换vue2 的选项式写法。
2.使用偏移量 offsetX,offsetY替换原来的获取笔尖位置的方法,解决在iframe 上使用的时候笔尖位置获取不正确的问题
3.添加base64 回显到画板的方法

<template>
  <canvas ref="vueSign" :style="{ background: props.bgColor }" @mousedown="mousedown" @mousemove="mousemove"
    @mouseup="mouseup" @mouseleave="mouseup" @touchstart.stop="touchDown" @touchmove.stop="touchMove"
    @touchend.stop="touchUp" @touchcancel.stop="touchUp" />
</template>
<script setup>
// 基于vue-asign改造, 主要是使用组合式语法替换选项式语法,同时解决笔尖偏移的问题
import {
  ref,
  reactive,
  onMounted,
  onUnmounted,
  createApp,
  computed, watch
} from "vue";

// ======================================组件属性
const props = defineProps({
  width: {  type: Number,  default: 600 },
  height: { type: Number,  default: 300 },
  bgColor: {  type: String, default: '' },
  lineWidth: {  type: Number,  default: 4  },
  lineColor: {  type: String, default: '#000000'  },
  gapLeft: { type: Number,  default: 5  },
  gapRight: {  type: Number,  default: 5 },
  gapTop: {  type: Number, default: 5  },
  gapBottom: { type: Number, default: 5 },
  format: {  type: String, default: '' },
  quality: { type: String,  default: '0.92'  },
  direction: {   type: Number,  default: 0  },
  isCrop: {  type: Boolean,  default: true  }
})

// =========================全局参数
let sratio = 1, ctx = null, resImg = '', isMove = false, lastX = 0, lastY = 0, offset = null;
const vueSign = ref()

const fillbg = computed(() => {
  return props.bgColor ? props.bgColor : 'rgba(255,255,255,0)'
})


// 初始化
function initCanvas() {

  const ratio = props.height / props.width
  ctx = vueSign.value.getContext('2d', { willReadFrequently: true })

  vueSign.value.height = props.height
  vueSign.value.width = props.width
  vueSign.value.style.width = props.width > window.innerWidth ? window.innerWidth + 'px' : props.width + 'px'
  const realw = parseFloat(window.getComputedStyle(vueSign.value).width)
  vueSign.value.style.height = ratio * realw + 'px'
  vueSign.value.style.background = fillbg.value
  ctx.scale(1 * sratio, 1 * sratio)
  sratio = realw / props.width
  ctx.scale(1 / sratio, 1 / sratio)
}
function mousedown(e) { 
  isMove = true
  drawLine(e.offsetX, e.offsetY, false)
}

function mousemove(e) { 
  if (isMove) {
    drawLine(e.offsetX, e.offsetY, true)
  }
}
function mouseup(e) {
  isMove = false
}

function touchDown(e) {
  isMove = true
  drawLine(
    e.changedTouches[0].clientX - offset.left,
    e.changedTouches[0].clientY - offset.top,
    false
  )
}

function touchMove(e) {
  if (isMove) {
    drawLine(
      e.changedTouches[0].clientX - offset.left,
      e.changedTouches[0].clientY - offset.top,
      true
    )
  }
}

function touchUp(e) {
  isMove = false
}

//画线
function drawLine(x, y, isT) {
  if (isT) {
    ctx.beginPath()
    ctx.lineWidth = props.lineWidth //设置线宽状态
    ctx.strokeStyle = props.lineColor //设置线的颜色状态
    ctx.lineCap = 'round'
    ctx.lineJoin = 'round'
    ctx.moveTo(lastX, lastY)
    ctx.lineTo(x, y)
    ctx.stroke()
    ctx.closePath()
  }
  // 每次移动都要更新坐标位置
  lastX = x
  lastY = y
}
//清空画图
function clearCanvas() {
  ctx.beginPath()
  ctx.clearRect(0, 0, props.width, props.height)
  ctx.closePath() //可加入,可不加入
}
//线条粗细
function lineCrude() {
  linWidthVal = selWidth[activeIndex].value
}
//改变颜色
function setColor() {
  let activeIndex = selColor.selectedIndex
  colorVal = selColor[activeIndex].value
}
//保存图片
function createImg() {
  return new Promise((resolve) => {
    const resImgData = ctx.getImageData(0, 0, vueSign.value.width, vueSign.value.height)
    const crop_area = getImgArea(resImgData.data)
    const crop_canvas = document.createElement('canvas')
    const crop_ctx = crop_canvas.getContext('2d')
    crop_canvas.width = crop_area[2] - crop_area[0]
    crop_canvas.height = crop_area[3] - crop_area[1]
    const crop_imgData = ctx.getImageData(...crop_area)
    crop_ctx.globalCompositeOperation = 'destination-over'
    crop_ctx.putImageData(crop_imgData, 0, 0)
    crop_ctx.fillStyle = fillbg.value
    crop_ctx.fillRect(0, 0, crop_canvas.width, crop_canvas.height)
    let imgType = 'image/' + props.format
    let resImg = crop_canvas.toDataURL(imgType, props.quality)
    if (!props.isCrop) {
      const ssign = vueSign.value
      ctx.globalCompositeOperation = "destination-over"
      ctx.fillStyle = fillbg.value
      ctx.fillRect(0, 0, ssign.width, ssign.height)
      resImg = ssign.toDataURL(imgType, props.quality)
      ctx.clearRect(0, 0, ssign.width, ssign.height)
      ctx.putImageData(resImgData, 0, 0)
      ctx.globalCompositeOperation = "source-over"
    }
    if (props.direction > 0 && props.direction % 90 == 0) {
      rotateBase64Img(resImg, props.direction, imgType).then(res => {
        resolve(res)
      })
    } else {
      resolve(resImg)
    }
  })
}
// 获取图片区域
function getImgArea(imgData) {
  // const vueSign = vueSign.value
  let left = vueSign.value.width,
    top = vueSign.value.height,
    right = 0,
    bottom = 0
  for (let i = 0; i < vueSign.value.width; i++) {
    for (let j = 0; j < vueSign.value.height; j++) {
      let k = (i + vueSign.value.width * j) * 4
      if (imgData[k] > 0 || imgData[k + 1] > 0 || imgData[k + 2] || imgData[k + 3] > 0) {
        bottom = Math.max(j, bottom)
        right = Math.max(i, right)
        top = Math.min(j, top)
        left = Math.min(i, left)
      }
    }
  }
  left++
  right++
  top++
  bottom++
  const data = [
    left - props.gapLeft,
    top - props.gapTop,
    right + props.gapRight,
    bottom + props.gapBottom
  ]
  return data
}
// 将base64图片转个角度并生成新的base64
function rotateBase64Img(src, edg, imgType) {
  return new Promise((resolve) => {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    let imgW, imgH, size// canvas初始大小
    if (edg % 90 != 0) {
      console.error('旋转角度必须是90的倍数!')
    }
    const quadrant = (edg / 90) % 4 // 旋转象限
    const cutCoor = { sx: 0, sy: 0, ex: 0, ey: 0 } // 裁剪坐标
    const image = new Image()
    image.crossOrigin = 'anonymous'
    image.src = src
    image.onload = function () {
      imgW = image.width
      imgH = image.height
      size = imgW > imgH ? imgW : imgH
      canvas.width = size * 2
      canvas.height = size * 2
      switch (quadrant) {
        case 0:
          cutCoor.sx = size
          cutCoor.sy = size
          cutCoor.ex = size + imgW
          cutCoor.ey = size + imgH
          break
        case 1:
          cutCoor.sx = size - imgH
          cutCoor.sy = size
          cutCoor.ex = size
          cutCoor.ey = size + imgW
          break
        case 2:
          cutCoor.sx = size - imgW
          cutCoor.sy = size - imgH
          cutCoor.ex = size
          cutCoor.ey = size
          break
        case 3:
          cutCoor.sx = size
          cutCoor.sy = size - imgW
          cutCoor.ex = size + imgH
          cutCoor.ey = size + imgW
          break
      }
      ctx.translate(size, size)
      ctx.rotate(edg * Math.PI / 180)
      ctx.drawImage(image, 0, 0)
      var imgData = ctx.getImageData(cutCoor.sx, cutCoor.sy, cutCoor.ex, cutCoor.ey)
      if (quadrant % 2 == 0) {
        canvas.width = imgW
        canvas.height = imgH
      } else {
        canvas.width = imgH
        canvas.height = imgW
      }
      ctx.putImageData(imgData, 0, 0)
      // 获取旋转后的base64图片
      resolve(canvas.toDataURL(imgType, this.quality))
    }
  })
}
  // 把base64 显示到画板上
  function setBase64toCanvas(bs) {
    const img = new Image()
    img.src = bs
    img.onload = function() {
        ctx.drawImage(img, 10,10)
    }
  }

  defineExpose({ clearCanvas, createImg,setBase64toCanvas })
  onMounted(() => {
    initCanvas()
  })
</script>
<style scoped>
canvas {
  max-width: 100%;
  display: block;
}
</style>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容