功能描述:
利用canvas实现手机签名转化成图片( 包含清除,由于手机全屏展示 所以还添加了图片翻转功能)生成base64地址 上传后台
功能展示:
代码展示:
html:
<template>
<div style="height:100%">
<div class="content">
<div class="van-tabContent">
<div class="tip">
<span>请在虚线内进行签名</span>
<div class="rotate"
@click="rotateImg('testImg','right')"
v-if="isShowSub">
<span>旋转图片</span>
</div>
</div>
<img :src="url"
alt=""
id="testImg"
style="display:none">
<div class="signature"
@touchmove.prevent>
<div class="boardBox"
ref="boardBox">
<canvas ref="board"
id="canvas"
@touchstart="mStart"
@touchmove="mMove"
@touchend="mEnd"></canvas>
</div>
</div>
</div>
<div class="bar">
<button round
class="clear-btn"
@click="clearcanvas">清除</button>
<button round
class="disabled-btn"
disabled
v-if="!isShowSub">提交</button>
<button round
class="sub-btn"
v-else
@click="subMitPic">提交</button>
</div>
</div>
</div>
</template>
js:
<script>
export default {
data () {
return {
familysignatureurl: "",
basedata: "",
ctx: null,
point: {
x: 0,
y: 0,
},
moving: false, // 是否正在绘制中且移动
isShowSub: false,//是否可点击提交
url: "",
}
},
mounted () {
let board = this.$refs.board; // 获取DOM
board.width = this.$refs.boardBox.offsetWidth; // 设置画布宽
board.height = this.$refs.boardBox.offsetHeight; // 设置画布高
this.ctx = board.getContext("2d"); // 二维绘图
this.ctx.strokeStyle = "#000"; // 颜色
this.ctx.lineWidth = 3; // 线条宽度
},
methods: {
// 触摸(开始)
mStart (e) {
let x = e.touches[0].clientX - e.target.offsetLeft,
y = e.touches[0].clientY - e.target.offsetTop; // 获取触摸点在画板(canvas)的坐标
this.point.x = x;
this.point.y = y;
this.ctx.beginPath();
this.moving = true;
},
// 滑动中...
mMove (e) {
if (this.moving) {
let x = e.touches[0].clientX - e.target.offsetLeft,
y = e.touches[0].clientY - e.target.offsetTop; // 获取触摸点在画板(canvas)的坐标
this.ctx.moveTo(this.point.x, this.point.y); // 把路径移动到画布中的指定点,不创建线条(起始点)
this.ctx.lineTo(x, y); // 添加一个新点,然后创建从该点到画布中最后指定点的线条,不创建线条
this.ctx.stroke(); // 绘制
(this.point.x = x), (this.point.y = y); // 重置点坐标为上一个坐标
}
},
// 滑动结束
mEnd () {
if (this.moving) {
this.ctx.closePath(); // 停止绘制
this.moving = false; // 关闭绘制开关
this.isShowSub = true
document.getElementById("canvas").toDataURL("image/png");
document.getElementById("canvas").toBlob(async (blobObj) => {
var file = new File([blobObj], "pic.png", {
type: blobObj.type,
lastModified: Date.now(),
});
this.getCanvasPic(file)
});
}
},
//获取canvas图片
getCanvasPic (file) {
const _this = this
var oReader = new FileReader();
oReader.readAsDataURL(file);
oReader.onload = function (e) {
let image = new Image()
// image.setAttribute('crossOrigin', 'anonymous')
image.src = e.target.result
image.onload = function () {
let canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
let context = canvas.getContext('2d')
context.drawImage(image, 0, 0, image.width, image.height)
// 将canvas的透明背景设置成白色
var imageData = context.getImageData(
0,
0,
canvas.width,
canvas.height
);
for (var i = 0; i < imageData.data.length; i += 4) {
// 当该像素是透明的,则设置成白色
if (imageData.data[i + 3] == 0) {
imageData.data[i] = 255;
imageData.data[i + 1] = 255;
imageData.data[i + 2] = 255;
imageData.data[i + 3] = 255;
}
}
context.putImageData(imageData, 0, 0);
let url = canvas.toDataURL('image/jpg')
_this.url = url//绘制的图片base64地址
_this.basedata = url
console.log(_this.basedata, 'this.basedata')
}
}
},
//旋转图片
rotateImg (pid, direction) {
//最小与最大旋转方向,图片旋转4次后回到原方向
var min_step = 0;
var max_step = 3;
var img = document.getElementById(pid);
if (img == null) return;
//img的高度和宽度不能在img元素隐藏后获取,否则会出错
var height = img.height;
var width = img.width;
var step = img.getAttribute('step');
if (step == null) {
step = min_step;
}
if (direction == 'right') {
step++;
//旋转到原位置,即超过最大值
step > max_step && (step = min_step);
} else {
step--;
step < min_step && (step = max_step);
}
img.setAttribute('step', step);
var canvas = document.getElementById("canvas");
if (canvas == null) {
img.style.display = 'none';
canvas = document.createElement('canvas');
canvas.setAttribute('id', 'pic_' + pid);
img.parentNode.appendChild(canvas);
}
//旋转角度以弧度值为参数
var degree = step * 90 * Math.PI / 180;
var ctx = canvas.getContext('2d');
switch (step) {
case 0:
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0);
let url = canvas.toDataURL('image/jpg')
this.basedata = url
break;
case 1:
canvas.width = height;
canvas.height = width;
ctx.rotate(degree);
ctx.drawImage(img, 0, -height);
let url1 = canvas.toDataURL('image/jpg')
this.basedata = url1
break;
case 2:
canvas.width = width;
canvas.height = height;
ctx.rotate(degree);
ctx.drawImage(img, -width, -height);
let url2 = canvas.toDataURL('image/jpg')
this.basedata = url2
break;
case 3:
canvas.width = height;
canvas.height = width;
ctx.rotate(degree);
ctx.drawImage(img, -width, 0);
let url3 = canvas.toDataURL('image/jpg')
this.basedata = url3
break;
}
},
//base64转Blob
base64ToBlob (base64Data) {
let arr = base64Data.split(","),
fileType = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
l = bstr.length,
u8Arr = new Uint8Array(l);
while (l--) {
u8Arr[l] = bstr.charCodeAt(l);
}
return new Blob([u8Arr], {
type: fileType,
});
},
//上传图片
async subMitPic () {
//转成file文件
let blobObj = this.base64ToBlob(this.basedata);
let file = new File([blobObj], "pic.png", {
type: blobObj.type,
lastModified: Date.now(),
});
let newformData = new FormData()
newformData.append('file', file)
newformData.append('isCompress', "1")
console.log(newformData.get('file'), newformData.get('isCompress'), 'formData')
//此处为发送请求给后台获取图片路径
let res = await this.upload(newformData);
if (res.code === 200) {
//可在此处处理上传图片以后的业务操作
// this.familysignatureurl = res.data[0]
// console.log(this.familysignatureurl);//此处打印的为绘画的图片url
}
},
upload (formData) {
return postAxiosData('/venus/oss/upload', formData)
},
//清除画布
clearcanvas () {
var c = document.getElementById("canvas");
var cxt = c.getContext("2d");
c.height = c.height;
this.ctx.lineWidth = 3;
this.isShowSub = false
},
},
}
</script>
css:
<style lang="scss" scoped>
.content {
height: 100%;
background-color: #f3f3f3;
.van-tabContent {
padding: 15px 15px;
.tip {
display: flex;
align-items: center;
justify-content: space-between;
height: 18px;
font-size: 13px;
font-weight: 400;
line-height: 22px;
color: #9d9d9d;
opacity: 1;
.rotate {
color: #5e6fdb;
img {
width: 18px;
height: 18px;
vertical-align: top;
margin-right: 5px;
}
}
}
.signature {
width: 100%;
height: 400px;
border: 1px dashed #c1c1c1;
margin: 11px 0 14px;
}
.boardBox {
height: 100%;
background: #ffffff;
}
}
.bar {
display: flex;
justify-content: space-between;
padding: 10px 15px 16px;
position: fixed;
bottom: 0px;
width: 100%;
background-color: #f3f3f3;
.clear-btn,
.sub-btn,
.disabled-btn {
width: 50%;
}
.clear-btn {
border: 1px solid #5e6fdb;
font-size: 17px;
font-weight: 400;
line-height: 24px;
color: #5e6fdb;
opacity: 1;
}
.sub-btn,
.disabled-btn {
background: #c1c1c1;
font-size: 17px;
font-weight: 400;
line-height: 22px;
color: #ffffff;
opacity: 1;
margin-left: 15px;
}
.sub-btn {
background-color: #5e6fdb;
}
}
}
</style>
本文参考:
https://blog.youkuaiyun.com/weixin_50329459/article/details/108218091?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_title~default-0.no_search_link&spm=1001.2101.3001.4242
https://blog.51cto.com/qings/997998