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

// =========================全局參數(shù)
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 //設(shè)置線寬狀態(tài)
    ctx.strokeStyle = props.lineColor //設(shè)置線的顏色狀態(tài)
    ctx.lineCap = 'round'
    ctx.lineJoin = 'round'
    ctx.moveTo(lastX, lastY)
    ctx.lineTo(x, y)
    ctx.stroke()
    ctx.closePath()
  }
  // 每次移動都要更新坐標(biāo)位置
  lastX = x
  lastY = y
}
//清空畫圖
function clearCanvas() {
  ctx.beginPath()
  ctx.clearRect(0, 0, props.width, props.height)
  ctx.closePath() //可加入拦键,可不加入
}
//線條粗細(xì)
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)
    }
  })
}
// 獲取圖片區(qū)域
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圖片轉(zhuǎn)個角度并生成新的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('旋轉(zhuǎn)角度必須是90的倍數(shù)!')
    }
    const quadrant = (edg / 90) % 4 // 旋轉(zhuǎn)象限
    const cutCoor = { sx: 0, sy: 0, ex: 0, ey: 0 } // 裁剪坐標(biāo)
    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)
      // 獲取旋轉(zhuǎn)后的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>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末美尸,一起剝皮案震驚了整個濱河市季率,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌攀唯,老刑警劉巖蟀悦,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件媚朦,死亡現(xiàn)場離奇詭異,居然都是意外死亡熬芜,警方通過查閱死者的電腦和手機莲镣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涎拉,“玉大人,你說我怎么就攤上這事的圆」呐。” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵越妈,是天一觀的道長季俩。 經(jīng)常有香客問我,道長梅掠,這世上最難降的妖魔是什么酌住? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮阎抒,結(jié)果婚禮上酪我,老公的妹妹穿的比我還像新娘。我一直安慰自己且叁,他們只是感情好都哭,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般欺矫。 火紅的嫁衣襯著肌膚如雪纱新。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天穆趴,我揣著相機與錄音脸爱,去河邊找鬼。 笑死未妹,一個胖子當(dāng)著我的面吹牛阅羹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播教寂,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼捏鱼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了酪耕?” 一聲冷哼從身側(cè)響起导梆,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎迂烁,沒想到半個月后看尼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡盟步,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年藏斩,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片却盘。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡狰域,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出黄橘,到底是詐尸還是另有隱情兆览,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布塞关,位于F島的核電站抬探,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏帆赢。R本人自食惡果不足惜小压,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望椰于。 院中可真熱鬧怠益,春花似錦、人聲如沸廉羔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至孩饼,卻和暖如春髓削,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背镀娶。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工立膛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人梯码。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓宝泵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親轩娶。 傳聞我的和親對象是個殘疾皇子儿奶,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容