|
| 1 | +<template> |
| 2 | + <div style="position: relative"> |
| 3 | + <canvas |
| 4 | + ref="canvasRef" |
| 5 | + @mousedown="mouseDown" |
| 6 | + @mousemove="mouseMove" |
| 7 | + @mouseup="mouseUp" |
| 8 | + @touchstart="touchStart" |
| 9 | + @touchmove="touchMove" |
| 10 | + @touchend="touchEnd" |
| 11 | + style="border: 1px solid lightgrey; max-width: 100%; display: block" |
| 12 | + > |
| 13 | + </canvas> |
| 14 | + |
| 15 | + <el-button |
| 16 | + style="position: absolute; bottom: 20px; right: 10px" |
| 17 | + type="primary" |
| 18 | + text |
| 19 | + size="small" |
| 20 | + @click="reset" |
| 21 | + > |
| 22 | + <Icon icon="ep:delete" class="mr-5px" />清除 |
| 23 | + </el-button> |
| 24 | + </div> |
| 25 | +</template> |
| 26 | + |
| 27 | +<script lang="ts" setup> |
| 28 | +import { propTypes } from '@/utils/propTypes' |
| 29 | +
|
| 30 | +defineOptions({ name: 'ESign' }) |
| 31 | +
|
| 32 | +const emits = defineEmits(['update:bgColor']) |
| 33 | +const props = defineProps({ |
| 34 | + // 画布宽度,即导出图片的宽度 |
| 35 | + width: propTypes.number.def(900), |
| 36 | + // 画布高度,即导出图片的高度 |
| 37 | + height: propTypes.number.def(400), |
| 38 | + // 画笔粗细 |
| 39 | + lineWidth: propTypes.number.def(10), |
| 40 | + // 画笔颜色 |
| 41 | + lineColor: propTypes.string.def('#000000'), |
| 42 | + // 画布背景色,为空时画布背景透明 |
| 43 | + bgColor: propTypes.string.def(''), |
| 44 | + // 是否裁剪,在画布设定尺寸基础上裁掉四周空白部分 |
| 45 | + isCrop: propTypes.bool.def(false), |
| 46 | + // 清空画布时是否同时清空设置的背景色 |
| 47 | + isClearBgColor: propTypes.bool.def(true), |
| 48 | + // 生成图片格式 |
| 49 | + format: propTypes.string.def('image/png'), |
| 50 | + // 生成图片质量,0 到 1 |
| 51 | + quality: propTypes.number.def(1) |
| 52 | +}) |
| 53 | +const canvasRef = ref() |
| 54 | +const hasDrew = ref(false) |
| 55 | +const resultImg = ref('') |
| 56 | +const points = ref<any>([]) |
| 57 | +const canvasTxt = ref() |
| 58 | +const startX = ref(0) |
| 59 | +const startY = ref(0) |
| 60 | +const isDrawing = ref(false) |
| 61 | +const sratio = ref(1) |
| 62 | +
|
| 63 | +const ratio = computed(() => { |
| 64 | + return props.height / props.width |
| 65 | +}) |
| 66 | +const stageInfo = computed(() => { |
| 67 | + return canvasRef.value.getBoundingClientRect() |
| 68 | +}) |
| 69 | +const bgColor = computed(() => { |
| 70 | + return props.bgColor ? props.bgColor : 'rgba(255, 255, 255, 0)' |
| 71 | +}) |
| 72 | +
|
| 73 | +watch( |
| 74 | + () => bgColor.value, |
| 75 | + () => { |
| 76 | + if (canvasRef.value) { |
| 77 | + canvasRef.value.style.background = bgColor.value |
| 78 | + } |
| 79 | + }, |
| 80 | + { |
| 81 | + immediate: true |
| 82 | + } |
| 83 | +) |
| 84 | +
|
| 85 | +const resizeHandler = () => { |
| 86 | + const canvas = canvasRef.value |
| 87 | + canvas.style.width = props.width + 'px' |
| 88 | + const realw = parseFloat(window.getComputedStyle(canvas).width) |
| 89 | + canvas.style.height = ratio.value * realw + 'px' |
| 90 | + canvasTxt.value = canvas.getContext('2d') |
| 91 | + canvasTxt.value.scale(1 * sratio.value, 1 * sratio.value) |
| 92 | + sratio.value = realw / props.width |
| 93 | + canvasTxt.value.scale(1 / sratio.value, 1 / sratio.value) |
| 94 | +} |
| 95 | +// For PC |
| 96 | +const mouseDown = (e) => { |
| 97 | + e.preventDefault() |
| 98 | + isDrawing.value = true |
| 99 | + hasDrew.value = true |
| 100 | + let obj = { |
| 101 | + x: e.offsetX, |
| 102 | + y: e.offsetY |
| 103 | + } |
| 104 | + drawStart(obj) |
| 105 | +} |
| 106 | +const mouseMove = (e) => { |
| 107 | + e.preventDefault() |
| 108 | + if (isDrawing.value) { |
| 109 | + let obj = { |
| 110 | + x: e.offsetX, |
| 111 | + y: e.offsetY |
| 112 | + } |
| 113 | + drawMove(obj) |
| 114 | + } |
| 115 | +} |
| 116 | +const mouseUp = (e) => { |
| 117 | + e.preventDefault() |
| 118 | + let obj = { |
| 119 | + x: e.offsetX, |
| 120 | + y: e.offsetY |
| 121 | + } |
| 122 | + drawEnd(obj) |
| 123 | + isDrawing.value = false |
| 124 | +} |
| 125 | +// For Mobile |
| 126 | +const touchStart = (e) => { |
| 127 | + e.preventDefault() |
| 128 | + hasDrew.value = true |
| 129 | + if (e.touches.length === 1) { |
| 130 | + let obj = { |
| 131 | + x: e.targetTouches[0].clientX - canvasRef.value.getBoundingClientRect().left, |
| 132 | + y: e.targetTouches[0].clientY - canvasRef.value.getBoundingClientRect().top |
| 133 | + } |
| 134 | + drawStart(obj) |
| 135 | + } |
| 136 | +} |
| 137 | +const touchMove = (e) => { |
| 138 | + e.preventDefault() |
| 139 | + if (e.touches.length === 1) { |
| 140 | + let obj = { |
| 141 | + x: e.targetTouches[0].clientX - canvasRef.value.getBoundingClientRect().left, |
| 142 | + y: e.targetTouches[0].clientY - canvasRef.value.getBoundingClientRect().top |
| 143 | + } |
| 144 | + drawMove(obj) |
| 145 | + } |
| 146 | +} |
| 147 | +const touchEnd = (e) => { |
| 148 | + e.preventDefault() |
| 149 | + if (e.touches.length === 1) { |
| 150 | + let obj = { |
| 151 | + x: e.targetTouches[0].clientX - canvasRef.value.getBoundingClientRect().left, |
| 152 | + y: e.targetTouches[0].clientY - canvasRef.value.getBoundingClientRect().top |
| 153 | + } |
| 154 | + drawEnd(obj) |
| 155 | + } |
| 156 | +} |
| 157 | +// 绘制 |
| 158 | +const drawStart = (obj) => { |
| 159 | + startX.value = obj.x |
| 160 | + startY.value = obj.y |
| 161 | + canvasTxt.value.beginPath() |
| 162 | + canvasTxt.value.moveTo(startX.value, startY.value) |
| 163 | + canvasTxt.value.lineTo(obj.x, obj.y) |
| 164 | + canvasTxt.value.lineCap = 'round' |
| 165 | + canvasTxt.value.lineJoin = 'round' |
| 166 | + canvasTxt.value.lineWidth = props.lineWidth * sratio.value |
| 167 | + canvasTxt.value.stroke() |
| 168 | + canvasTxt.value.closePath() |
| 169 | + points.value.push(obj) |
| 170 | +} |
| 171 | +const drawMove = (obj) => { |
| 172 | + canvasTxt.value.beginPath() |
| 173 | + canvasTxt.value.moveTo(startX.value, startY.value) |
| 174 | + canvasTxt.value.lineTo(obj.x, obj.y) |
| 175 | + canvasTxt.value.strokeStyle = props.lineColor |
| 176 | + canvasTxt.value.lineWidth = props.lineWidth * sratio.value |
| 177 | + canvasTxt.value.lineCap = 'round' |
| 178 | + canvasTxt.value.lineJoin = 'round' |
| 179 | + canvasTxt.value.stroke() |
| 180 | + canvasTxt.value.closePath() |
| 181 | + startY.value = obj.y |
| 182 | + startX.value = obj.x |
| 183 | + points.value.push(obj) |
| 184 | +} |
| 185 | +const drawEnd = (obj) => { |
| 186 | + canvasTxt.value.beginPath() |
| 187 | + canvasTxt.value.moveTo(startX.value, startY.value) |
| 188 | + canvasTxt.value.lineTo(obj.x, obj.y) |
| 189 | + canvasTxt.value.lineCap = 'round' |
| 190 | + canvasTxt.value.lineJoin = 'round' |
| 191 | + canvasTxt.value.stroke() |
| 192 | + canvasTxt.value.closePath() |
| 193 | + points.value.push(obj) |
| 194 | + points.value.push({ x: -1, y: -1 }) |
| 195 | +} |
| 196 | +// 生成 |
| 197 | +const generate = (options) => { |
| 198 | + let imgFormat = options && options.format ? options.format : props.format |
| 199 | + let imgQuality = options && options.quality ? options.quality : props.quality |
| 200 | + const pm = new Promise((resolve, reject) => { |
| 201 | + if (!hasDrew.value) { |
| 202 | + reject(`Warning: Not Signned!`) |
| 203 | + return |
| 204 | + } |
| 205 | + let resImgData = canvasTxt.value.getImageData( |
| 206 | + 0, |
| 207 | + 0, |
| 208 | + canvasRef.value.width, |
| 209 | + canvasRef.value.height |
| 210 | + ) |
| 211 | + canvasTxt.value.globalCompositeOperation = 'destination-over' |
| 212 | + canvasTxt.value.fillStyle = bgColor.value |
| 213 | + canvasTxt.value.fillRect(0, 0, canvasRef.value.width, canvasRef.value.height) |
| 214 | + resultImg.value = canvasRef.value.toDataURL(imgFormat, imgQuality) |
| 215 | + canvasTxt.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height) |
| 216 | + canvasTxt.value.putImageData(resImgData, 0, 0) |
| 217 | + canvasTxt.value.globalCompositeOperation = 'source-over' |
| 218 | + if (props.isCrop) { |
| 219 | + const crop_area = getCropArea(resImgData.data) |
| 220 | + let crop_canvas = document.createElement('canvas') |
| 221 | + const crop_ctx = crop_canvas.getContext('2d') |
| 222 | + crop_canvas.width = crop_area[2] - crop_area[0] |
| 223 | + crop_canvas.height = crop_area[3] - crop_area[1] |
| 224 | + const crop_imgData = canvasTxt.value.getImageData(...crop_area) |
| 225 | + crop_ctx.globalCompositeOperation = 'destination-over' |
| 226 | + crop_ctx.putImageData(crop_imgData, 0, 0) |
| 227 | + crop_ctx.fillStyle = bgColor.value |
| 228 | + crop_ctx.fillRect(0, 0, crop_canvas.width, crop_canvas.height) |
| 229 | + resultImg.value = crop_canvas.toDataURL(imgFormat, imgQuality) |
| 230 | + } |
| 231 | + resolve(resultImg.value) |
| 232 | + }) |
| 233 | + return pm |
| 234 | +} |
| 235 | +const reset = () => { |
| 236 | + canvasTxt.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height) |
| 237 | + if (props.isClearBgColor) { |
| 238 | + emits('update:bgColor', '') |
| 239 | + canvasRef.value.style.background = 'rgba(255, 255, 255, 0)' |
| 240 | + } |
| 241 | + points.value = [] |
| 242 | + hasDrew.value = false |
| 243 | + resultImg.value = '' |
| 244 | +} |
| 245 | +const getCropArea = (imgData) => { |
| 246 | + let topX = canvasRef.value.width |
| 247 | + let btmX = 0 |
| 248 | + let topY = canvasRef.value.height |
| 249 | + let btnY = 0 |
| 250 | + for (let i = 0; i < canvasRef.value.width; i++) { |
| 251 | + for (let j = 0; j < canvasRef.value.height; j++) { |
| 252 | + let pos = (i + canvasRef.value.width * j) * 4 |
| 253 | + if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) { |
| 254 | + btnY = Math.max(j, btnY) |
| 255 | + btmX = Math.max(i, btmX) |
| 256 | + topY = Math.min(j, topY) |
| 257 | + topX = Math.min(i, topX) |
| 258 | + } |
| 259 | + } |
| 260 | + } |
| 261 | + topX++ |
| 262 | + btmX++ |
| 263 | + topY++ |
| 264 | + btnY++ |
| 265 | + const data = [topX, topY, btmX, btnY] |
| 266 | + return data |
| 267 | +} |
| 268 | +
|
| 269 | +defineExpose({ |
| 270 | + generate |
| 271 | +}) |
| 272 | +onBeforeMount(() => { |
| 273 | + window.addEventListener('resize', resizeHandler) |
| 274 | +}) |
| 275 | +onBeforeUnmount(() => { |
| 276 | + window.removeEventListener('resize', resizeHandler) |
| 277 | +}) |
| 278 | +onMounted(() => { |
| 279 | + canvasRef.value.height = props.height |
| 280 | + canvasRef.value.width = props.width |
| 281 | + canvasRef.value.style.background = bgColor.value |
| 282 | + resizeHandler() |
| 283 | + // 在画板以外松开鼠标后冻结画笔 |
| 284 | + document.onmouseup = () => { |
| 285 | + isDrawing.value = false |
| 286 | + } |
| 287 | +}) |
| 288 | +</script> |
0 commit comments