需求
公司业务想要在手机端h5搞个签名需求。签名好像在哪里见到过;好像钉钉上请假时有个签名可以仿照写一个
思路想法
- 既然是绘制 那想到的是用 canvas 或者svg 应该都可以
- 既然canvas 或者svg 那用哪个比较好呢
- Canvas提供的功能更原始,适合像素处理,动态渲染和大数据量绘制;SVG功能更完善,适合静态图片展示,高保真文档查看和打印的应用场景;
- Canvas 像素图在元素特别多的情况 1000+
- 性能高,可以自己控制绘制过程,还能使用 WebGL
- 可控性高,像素级控制
- 内存占用恒定,就是像素点个数
- SVG 矢量图:
- 不失真,放大缩小图像都很清晰
- 学习成本低,也是一种 DOM 结构
- 使用方便,设计软件可以直接导出
- Canvas 像素图在元素特别多的情况 1000+
优点都是网上找的,具体的差别小弟不是很清楚,为啥用canvas来制作,主要还是用的多,svg我比较少用
遇到问题:
1、使用canvas绘制签名在使用情况下感觉不是很流畅,有点卡顿的感觉
2、使用canvas绘制玩后不知各位打扰会不会觉得绘制区域有些模糊,不是很清晰
对于第一个问题我是通过对比 钉钉的签名来的,发现钉钉如德福一样纵享丝滑,自己的就有点捞了,于是在网上找,发现大部分都是用的canvas来绘制,我在万军丛中找到了那么几个使用第三方包vue-esign,使用他们的示例,好像有内味了,看了一下他们源码,发现该插件内部也是用的canvas,既然都是那为何还要用插件呢,直接看着源码改造出来,最终有那丝滑般的感觉的,问题就出在,点击绘制和移除绘制的过程中。但是哪个模糊的问题没有解决。待后续处理技术有限,期待大佬们的交流
上图
具体代码如下
img那个橡皮擦,是拿的阿里图标库的我这就不给出了
touchend一般是手机移动端设备的触碰事件,如果是移动设备可以把click事件去除,不然点击会触发两次事件,所以酌情选择
<template>
<div class="myBrush-container" ref="myBrush">
<div class="brush_btn">
<div class="resign" @click="closeBrush" @touchend="closeBrush">取消</div>
<span class="title">手写签名</span>
<div class="confirm" :disabled="!this.hasDrew" @click="generate" @touchend="generate">完成</div>
</div>
<div class="brush_content">
<canvas ref="canvas" @mousedown="mouseDown" @mousemove="mouseMove" @mouseup="mouseUp"
@touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd"></canvas>
<img @click="resetDraw" @touchend="resetDraw" class="content_img" src="../assets/clear.png" alt="">
</div>
</div>
</template>
<script>
export default {
name: "myBrush",
mounted() {
this.$_init()
// 在画板以外松开鼠标后冻结画笔
document.onmouseup = () => {
this.isDrawing = false
}
},
props: {
//线宽
lineWidth: {
type: Number,
default: 8
},
//线的颜色
lineColor: {
type: String,
default: '#000000'
},
//背景颜色
bgColor: {
type: String,
default: '#FFF'
},
//是否裁剪 在画布设定尺寸基础上裁掉四周空白部分
isCrop: {
type: Boolean,
default: false
}
},
data() {
return {
ctx: {},
hasDrew: false,
resultImg: '',
points: [],
canvasTxt: null,
startX: 0,
startY: 0,
isDrawing: false,//是否绘制
sratio: 1,//宽比率
}
},
computed: {
myBg() {
return this.bgColor ? this.bgColor : 'rgba(255, 255, 255, 0)'
}
},
watch: {
'myBg': function (newVal) {
this.$refs.canvas.style.background = newVal
}
},
beforeMount() {
window.addEventListener('resize', this.$_resizeHandler)
},
beforeDestroy() {
window.removeEventListener('resize', this.$_resizeHandler)
},
methods: {
$_init() {
const canvas = this.$refs.canvas
canvas.height = this.$refs.myBrush.offsetHeight;
canvas.width = this.$refs.myBrush.offsetWidth;
canvas.style.background = this.myBg
const realW = parseFloat(window.getComputedStyle(canvas).width)
this.canvasTxt = canvas.getContext('2d')
this.canvasTxt.scale(1 * this.sratio, 1 * this.sratio)
this.sratio = realW / this.$refs.myBrush.offsetWidth
this.canvasTxt.scale(1 / this.sratio, 1 / this.sratio)
},
// pc
mouseDown(e) {
e = e || event
e.preventDefault()
this.isDrawing = true
this.hasDrew = true
let obj = {
x: e.offsetX,
y: e.offsetY
}
this.drawStart(obj)
},
mouseMove(e) {
e = e || event
e.preventDefault()
if (this.isDrawing) {
let obj = {
x: e.offsetX,
y: e.offsetY
}
this.drawMove(obj)
}
},
mouseUp(e) {
e = e || event
e.preventDefault()
let obj = {
x: e.offsetX,
y: e.offsetY
}
this.drawEnd(obj)
this.isDrawing = false
},
// mobile
touchStart(e) {
e = e || event
e.preventDefault()
this.hasDrew = true
if (e.touches.length === 1) {
let obj = {
x: e.targetTouches[0].clientX - this.$refs.canvas.getBoundingClientRect().left,
y: e.targetTouches[0].clientY - this.$refs.canvas.getBoundingClientRect().top
}
this.drawStart(obj)
}
},
touchMove(e) {
e = e || event
e.preventDefault()
if (e.touches.length === 1) {
let obj = {
x: e.targetTouches[0].clientX - this.$refs.canvas.getBoundingClientRect().left,
y: e.targetTouches[0].clientY - this.$refs.canvas.getBoundingClientRect().top
}
this.drawMove(obj)
}
},
touchEnd(e) {
e = e || event
e.preventDefault()
if (e.touches.length === 1) {
let obj = {
x: e.targetTouches[0].clientX - this.$refs.canvas.getBoundingClientRect().left,
y: e.targetTouches[0].clientY - this.$refs.canvas.getBoundingClientRect().top
}
this.drawEnd(obj)
}
},
// 绘制
drawStart(obj) {
this.startX = obj.x
this.startY = obj.y
this.canvasTxt.beginPath()
this.canvasTxt.moveTo(this.startX, this.startY)
this.canvasTxt.lineTo(obj.x, obj.y)
this.canvasTxt.lineCap = 'round'
this.canvasTxt.lineJoin = 'round'
this.canvasTxt.lineWidth = this.lineWidth * this.sratio
this.canvasTxt.stroke()
this.canvasTxt.closePath()
this.points.push(obj)
},
/**
* 移动
*/
drawMove(obj) {
this.canvasTxt.beginPath()
this.canvasTxt.moveTo(this.startX, this.startY)
this.canvasTxt.lineTo(obj.x, obj.y)
this.canvasTxt.strokeStyle = this.lineColor
this.canvasTxt.lineWidth = this.lineWidth * this.sratio
this.canvasTxt.lineCap = 'round'
this.canvasTxt.lineJoin = 'round'
this.canvasTxt.stroke()
this.canvasTxt.closePath()
this.startY = obj.y
this.startX = obj.x
this.points.push(obj)
},
/**
*结束绘制
*/
drawEnd(obj) {
this.canvasTxt.beginPath()
this.canvasTxt.moveTo(this.startX, this.startY)
this.canvasTxt.lineTo(obj.x, obj.y)
this.canvasTxt.lineCap = 'round'
this.canvasTxt.lineJoin = 'round'
this.canvasTxt.stroke()
this.canvasTxt.closePath()
this.points.push(obj)
this.points.push({x: -1, y: -1})
},
// 操作
async generate() {
if (!this.hasDrew) {
this.$message.warning(`Warning: Not Signned!`)
return
}
var resImgData = this.canvasTxt.getImageData(0, 0, this.$refs.canvas.width, this.$refs.canvas.height)
this.canvasTxt.globalCompositeOperation = "destination-over"
this.canvasTxt.fillStyle = this.myBg
this.canvasTxt.fillRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height)
this.resultImg = this.$refs.canvas.toDataURL('image/png');
this.$refs.canvas.toBlob(async (blobObj) => {
const file1 = new File([blobObj], "signature.png", {
type: blobObj.type,
lastModified: Date.now(),
});
console.log(file1);
})
var resultImg = this.resultImg
this.canvasTxt.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height)
this.canvasTxt.putImageData(resImgData, 0, 0)
this.canvasTxt.globalCompositeOperation = "source-over"
if (this.isCrop) {
const crop_area = this.getCropArea(resImgData.data)
var 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 = this.canvasTxt.getImageData(...crop_area)
crop_ctx.globalCompositeOperation = "destination-over"
crop_ctx.putImageData(crop_imgData, 0, 0)
crop_ctx.fillStyle = this.myBg
crop_ctx.fillRect(0, 0, crop_canvas.width, crop_canvas.height)
resultImg = crop_canvas.toDataURL()
crop_canvas = null
}
console.log(resultImg)
},
/**
* 重绘
*/
resetDraw() {
this.canvasTxt.clearRect(
0,
0,
this.$refs.canvas.width,
this.$refs.canvas.height
)
this.$emit('update:bgColor', '')
this.$refs.canvas.style.background = 'rgba(255, 255, 255, 0)'
this.points = []
this.hasDrew = false
this.resultImg = ''
},
/**
* 关闭画板
*/
closeBrush() {
this.resetDraw();
this.$emit("closeBrush")
},
/**
* 修剪区域
* @param imgData
* @returns {(number|number)[]}
*/
getCropArea(imgData) {
var topX = this.$refs.canvas.width;
var btmX = 0;
var topY = this.$refs.canvas.height;
var btnY = 0
for (var i = 0; i < this.$refs.canvas.width; i++) {
for (var j = 0; j < this.$refs.canvas.height; j++) {
var pos = (i + this.$refs.canvas.width * j) * 4
if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
btnY = Math.max(j, btnY)
btmX = Math.max(i, btmX)
topY = Math.min(j, topY)
topX = Math.min(i, topX)
}
}
}
topX++
btmX++
topY++
btnY++
return [topX, topY, btmX, btnY]
}
}
}
</script>
<style scoped lang="scss">
.myBrush-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 50%;
position: fixed;
bottom: 0;
background-color: #fff;
border-radius: 10px 10px 0 0;
padding: 1%;
box-sizing: border-box;
z-index: 2011;
.brush_btn {
height: 5vh;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 2%;
border-bottom: 1px solid #dcdfe6;
box-sizing: border-box;
.confirm, .resign {
height: 80%;
box-sizing: border-box;
line-height: 4vh;
color: #fff;
border-radius: 18px;
}
.title {
font-weight: bold;
}
.resign {
background: #fff;
color: #606266;
width: 10%;
}
.confirm {
background: #409EFF;
width: 18%;
}
}
.brush_content {
width: 100%;
height: calc(100% - 5vh);
padding: 4%;
box-sizing: border-box;
canvas {
border: 1px dashed #dcdfe6;
height: 100%;
width: 100%;
box-sizing: border-box;
}
.content_img {
position: absolute;
bottom: 6%;
right: 10%;
width: 24px;
height: 24px;
}
}
}
</style>