记录一下两年前的一个关于canvas设置手写签名的需求
需求如下:
由于手写区域较大,部分用户手写的区域较小,在盖章的过程中,可能会使得签名笔迹看起来特别小,我们需要将用户真实手写的笔迹进行区域裁剪,得到红框中的部分,使得该图片在盖章展示的时候能看清用户的笔迹
分析设计:
- 1.由于需要对canvas中已经手写的笔迹进行处理,且手写区域是固定的,一次需要再有一个canvas(adjustCanvas)来处理
- 2.需要将用户的手写笔迹记录下来,并形成一个矩形区域,在二次绘制的时候,就能确定adjustCanvas的宽高
- 3.将手写签名的尺寸固定为400*160
要点
1.在touch事件中记录手写区域,包含最小和最大的x、y坐标,因此格式为mEffectivePoint = {
minX: -1,
minY: -1,
maxX: -1,
maxY: -1,
}
2.将手写区域的canvas作为原始图片,在重绘前将其赋值到image对象
var image = new Image() image.src = document.getElementById('myCanvas').toDataURL('image/png')
3.重绘时,需要将canvas的绘图方法需要将原本的图和最终图片的起点和宽高计算出来,该函数定义为:
drawImage(image: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
关键代码:
- 设置一个不展示的canvas节点,用来重绘手写签名图片
<div id="work_area" class="canvasContain">
<canvas id="idStampAdjustCanvas" class="canvasContentAdjust"></canvas>
<canvas id="myCanvas" class="canvasContent"></canvas>
</div>
- 在手写的过程中,记录手写区域
var mEffectivePoint = {
minX: -1,
minY: -1,
maxX: -1,
maxY: -1,
}
///
/**
* 重新计算手写签名的有效区域
* @param x
* @param y
*/
function checkEffectivePoint (x, y) {
if (x >= 0 && x <= mCanvasWidth) {
if (x - mPanWidth < mEffectivePoint.minX || mEffectivePoint.minX === -1) {
mEffectivePoint.minX = x - mPanWidth
if (mEffectivePoint.minX < 0) {
mEffectivePoint.minX = 0
}
}
if (x + mPanWidth > mEffectivePoint.maxX || mEffectivePoint.maxX === -1) {
mEffectivePoint.maxX = x + mPanWidth
if (mEffectivePoint.maxX > mCanvasWidth) {
mEffectivePoint.maxX = mCanvasWidth
}
}
}
if (y >= 0 && y <= mCanvasHeight) {
if (y - mPanWidth < mEffectivePoint.minY || mEffectivePoint.minY === -1) {
mEffectivePoint.minY = y - mPanWidth
if (mEffectivePoint.minY < 0) {
mEffectivePoint.minY = 0
}
}
if (y + mPanWidth > mEffectivePoint.maxY || mEffectivePoint.maxY === -1) {
mEffectivePoint.maxY = y + mPanWidth
if (mEffectivePoint.maxY > mCanvasHeight) {
mEffectivePoint.maxY = mCanvasHeight
}
}
}
}
function touchStart (event) {
event.preventDefault()
HAS_IMAGE = true
for (var i = 0; i < event.touches.length; i++) {
checkEffectivePoint(event.touches[i].pageX - mCanvasOffsetLeft, event.touches[i].pageY - mCanvasOffsetTop)
paths.push({
id: event.touches[i].identifier,
points: [{
x: event.touches[i].pageX - mCanvasOffsetLeft,
y: event.touches[i].pageY - mCanvasOffsetTop,
timestamp: new Date().getTime(),
drawn: false,
}],
complete: false,
})
}
}
function touchEnd (event) {
event.preventDefault()
for (var i = 0; i < event.changedTouches.length; i++) {
for (var j = 0; j < paths.length; j++) {
if (paths[j].id == event.changedTouches[i].identifier) {
paths[j].id = null
paths[j].complete = true
}
}
}
}
function touchMove (event) {
event.preventDefault()
for (var i = 0; i < event.touches.length; i++) {
for (var j = 0; j < paths.length; j++) {
if (paths[j].id == event.touches[i].identifier) {
checkEffectivePoint(event.touches[i].pageX - mCanvasOffsetLeft, event.touches[i].pageY - mCanvasOffsetTop)
paths[j].points.push({
x: event.touches[i].pageX - mCanvasOffsetLeft,
y: event.touches[i].pageY - mCanvasOffsetTop,
drawn: false,
timestamp: new Date().getTime(),
})
}
}
}
}
- 补充需求—将手写签名的区域固定为400*160
// 我们将图章设置为400*160的尺寸保存到服务端
var stampMaxX = 400
var stampMaxY = 160
/**
*
* 根据原始图片的宽高,在限定尺寸的canvas中配置好绘图起始点坐标和宽高,让图片能够不变形地完全绘制在限定尺寸的canvas中
* @param width 真实手写的宽
* @param height 真实手写区域的高度
* canvas的尺寸(maxX=400,maxY=160)
* 1-原始图片width<maxX
* 1-1 height<maxY
* 1-2 height>=maxY
* 2-原始图片width>=maxX
* 2-1 width/maxX > height/maxY (宽度的比例大于高度的比例)
* 2-2 width/maxX <= height/maxY
*
* @return {
{width: number, startY: number, startX: number, height: number}}
*/
function getDrawArea (width, height) {
var drawStartPoint = {
startX: 0, //重绘起点的x坐标
startY: 0, //重绘起点的y坐标
width: stampMaxX, //重绘图片的宽
height: stampMaxY, //重绘图片的高
}
if (width < stampMaxX) {
if (height < stampMaxY) {
drawStartPoint = {
startX: (stampMaxX - width) / 2,
startY: (stampMaxY - height) / 2,
width: width,
height: height,
}
} else {
var scale = stampMaxY / height
drawStartPoint = {
startX: (stampMaxX - width * scale) / 2,
startY: 0,
width: width * scale,
height: stampMaxY,
}
}
} else {
var scaleWidth = stampMaxX / width
var scaleHeight = stampMaxY / height
var scale = scaleWidth
if (width / height > stampMaxX / stampMaxY) {
// 说明图片相对比例,缩放后,width更小,所以以height为基准边
scale = scaleWidth
drawStartPoint = {
startX: 0,
startY: (stampMaxY - height * scale) / 2,
width: stampMaxX,
height: height * (stampMaxX / width),
}
} else {
scale = scaleHeight
drawStartPoint = {
startX: (stampMaxX - width * scale) / 2,
startY: 0,
width: width * (stampMaxY / height),
height: stampMaxY,
}
}
}
return drawStartPoint
}
// ....其他逻辑代码
/**
* 根据手写区域裁剪图片
*/
function getImageByEffectiveArea (imageSource, callback) {
var effectivePoint = {
minX: mEffectivePoint.minX,
maxX: mEffectivePoint.maxX === mEffectivePoint.minX ? mEffectivePoint.minX + 10 : mEffectivePoint.maxX,
minY: mEffectivePoint.minY,
maxY: mEffectivePoint.maxY === mEffectivePoint.minX ? mEffectivePoint.maxY + 10 : mEffectivePoint.maxY,
}
canvas2 = document.getElementById('idStampAdjustCanvas')
context2 = canvas2.getContext('2d')
//先清除
context2.clearRect(0, 0, canvas2.width, canvas2.height)
var width = effectivePoint.maxX - effectivePoint.minX
var height = effectivePoint.maxY - effectivePoint.minY
canvas2.width = stampMaxX
canvas2.height = stampMaxY
loadImage(imageSource.src, function () {
context2 = canvas2.getContext('2d')
var drawStartPoint = getDrawArea(width, height)
context2.drawImage(imageSource,
effectivePoint.minX, effectivePoint.minY, width, height,
drawStartPoint.startX, drawStartPoint.startY,
drawStartPoint.width, drawStartPoint.height)
callback(canvas2.toDataURL('image/png'))
})
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>签名笔迹设置</title>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<style type="text/css">
* {
padding: 0;
margin: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
html {
height: 100%;
width: 100%;
}
body {
height: 100%;
width: 100%;
box-sizing: border-box;
-moz-box-sizing: border-box; /* Firefox */
-webkit-box-sizing: border-box; /* Safari */
display: flex;
display: -webkit-flex;
display: inline-flex;
display: -moz-flex;
display: -ms-flexbox;
flex-direction: row;
-webkit-flex-direction: row;
}
.canvasContain {
display: flex;
flex: 1;
position: relative;
}
.canvasContent {
display: block;
width: 100%;
z-index: 99;
/*background-color: #FFF;*/
background: #FFF url(./image/bg_canvas.png) no-repeat fixed center;
}
.canvasContentAdjust {
position: absolute;
display: none;
background-color: #F2F2F2;
z-index: 1;
width: 400px;
height: 160px;
}
.rightContain {
display: flex;
display: -webkit-flex;
justify-content: flex-end;
-webkit-justify-content: flex-end;
flex-direction: column;
-webkit-flex-direction: column;
align-items: center;
width: 60px;
background-color: #F2F2F2;
padding-bottom: 20px;
}
.penContain {
width: 60px;
height: 45px;
text-align: center;
vertical-align: middle;
display: flex;
display: -webkit-flex;
justify-content: center;
-webkit-justify-content: center;
flex-direction: column;
-webkit-flex-direction: column;
box-sizing: border-box;
-moz-box-sizing: border-box; /* Firefox */
-webkit-box-sizing: border-box; /* Safari */
}
.pen {
margin: 0 auto;
display: flex;
border: solid 2px transparent;
background-color: #000;
border-radius: 50%;
}
.i1 {
width: 10px;
height: 10px;
}
.i2 {
width: 14px;
height: 14px;
}
.i3 {
width: 18px;
height: 18px;
}
.on {
border: solid 2px #999;
}
.delete {
color: #333;
background: url(./image/del.png) no-repeat center center;
background-size: 80% 100%;
outline: none;
}
.save {
color: #333;
background: url(./image/signed.png) no-repeat center center;
background-size: 80% 100%;
outline: none;
}
.click-image {
margin-top: 15px;
width: 60px;
height: 40px;
/*transform: rotate(-90deg);*/
border: 0 none;
position: relative;
z-index: auto;
}
</style>
</head>
<body>
<!-- 正文 -->
<div id="work_area" class="canvasContain">
<canvas id="idStampAdjustCanvas" class="canvasContentAdjust"></canvas>
<canvas id="myCanvas" class="canvasContent"></canvas>
<!-- <img id='signImg' src='draw/image/canvas_bg1.png'/>-->
</div>
<div class="rightContain">
<div class="penContain" onclick="setPenWidth(3)">
<div class="pen i1" id="idPenSmall"></div>
</div>
<div class="penContain" onclick="setPenWidth(5)">
<div class="pen i2 on" id="idPenNormal"></div>
</div>
<div class="penContain" onclick="setPenWidth(8)">
<div class="pen i3" id="idPenMax"></div>
</div>
<div class="H5_footer pull-left btncon">
<button class="click-image delete" onclick="clean()"></button>
<button class="click-image save" onclick="save()"></button>
</div>
</div>
<script type="text/javascript">
var CANVAS_WIDTH = 0
var CANVAS_HEIGHT = 0
// 我们将图章设置为400*160的尺寸保存到服务端
var stampMaxX = 400
var stampMaxY = 160
var PAN_WIDTH_SMALL = 3
var PAN_WIDTH_NORMAL = 5
var PAN_WIDTH_BIG = 8
var MAX_VELOCITY = 10
var context = null
var HAS_IMAGE = false
var scale = 1
var mCanvasWidth = 0
var mCanvasHeight = 0
var mCanvasOffsetLeft = 0
var mCanvasOffsetTop = 0
var base_url = null
var token = null
var mStartPoint = {
x: 0,
y: 0,
}
var mEndPoint = {
x: 0,
y: 0,
}
var mPanWidth = PAN_WIDTH_NORMAL
var canvas2
var context2
var mEffectivePoint = {
minX: -1,
minY: -1,
maxX: -1,
maxY: -1,
}
var paths = []
function clean () {
context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
HAS_IMAGE = false
initEffectivePoint()
return false
}
function setPenWidth (width) {
mPanWidth = width
removeSelectPenStyle()
if (width > PAN_WIDTH_NORMAL) {
document.getElementById('idPenMax').className = 'pen i3 on'
} else if (width < PAN_WIDTH_NORMAL) {
document.getElementById('idPenSmall').className = 'pen i1 on'
} else {
document.getElementById('idPenNormal').className = 'pen i2 on'
}
console.log(document.getElementById('idPenSmall').className)
console.log(document.getElementById('idPenNormal').className)
console.log(document.getElementById('idPenMax').className)
}
function removeSelectPenStyle () {
document.getElementById('idPenSmall').className = 'pen i1'
document.getElementById('idPenNormal').className = 'pen i2'
document.getElementById('idPenMax').className = 'pen i3'
}
function init () {
CANVAS_WIDTH = document.body.clientWidth - 60
CANVAS_HEIGHT = document.body.clientHeight
var canvas = document.getElementById('myCanvas')
mCanvasWidth = CANVAS_WIDTH
mCanvasHeight = CANVAS_HEIGHT
canvas.height = CANVAS_HEIGHT
canvas.width = CANVAS_WIDTH
context = canvas.getContext('2d')
var image = new Image()
if (image.src != 'data:image/png;base64,null') {
preImage(image.src, function () {
context.drawImage(this, 0, 0, this.width * scale, this.height * scale)
HAS_IMAGE = true
})
}
context.lineWidth = mPanWidth
context.fillStyle = '#000000'
context.strokeStyle = '#000000'
context.lineCap = 'round'
context.lineJoin = 'miter'
canvas.addEventListener('touchmove', touchMove, false)
canvas.addEventListener('touchstart', touchStart, false)
canvas.addEventListener('touchend', touchEnd, false)
function preImage (url, callback) {
var img = new Image() //创建一个Image对象,实现图片的预下载
img.src = url
if ((url.indexOf('null') <= 0) && (url.length > 50)) //判断图片非空
{
if (img.complete) {
//如果图片已经存在于浏览器缓存,直接调用回调函数
callback.call(img)
return // 直接返回,不用再处理onload事件
}
img.onload = function () { //图片下载完毕时异步调用callback函数。
callback.call(img)//将回调函数的this替换为Image对象
}
}
}
setInterval(draw, 20)
}
function drawLine (startPoint, endPoint) {
context.lineWidth = mPanWidth
context.beginPath()
context.moveTo(startPoint.x, startPoint.y)
context.lineTo(endPoint.x, endPoint.y)
context.stroke()
context.closePath()
mStartPoint = endPoint
}
function touchStart (event) {
event.preventDefault()
HAS_IMAGE = true
for (var i = 0; i < event.touches.length; i++) {
checkEffectivePoint(event.touches[i].pageX - mCanvasOffsetLeft, event.touches[i].pageY - mCanvasOffsetTop)
paths.push({
id: event.touches[i].identifier,
points: [{
x: event.touches[i].pageX - mCanvasOffsetLeft,
y: event.touches[i].pageY - mCanvasOffsetTop,
timestamp: new Date().getTime(),
drawn: false,
}],
complete: false,
})
}
}
function touchEnd (event) {
event.preventDefault()
for (var i = 0; i < event.changedTouches.length; i++) {
for (var j = 0; j < paths.length; j++) {
if (paths[j].id == event.changedTouches[i].identifier) {
paths[j].id = null
paths[j].complete = true
}
}
}
}
function touchMove (event) {
event.preventDefault()
for (var i = 0; i < event.touches.length; i++) {
for (var j = 0; j < paths.length; j++) {
if (paths[j].id == event.touches[i].identifier) {
checkEffectivePoint(event.touches[i].pageX - mCanvasOffsetLeft, event.touches[i].pageY - mCanvasOffsetTop)
paths[j].points.push({
x: event.touches[i].pageX - mCanvasOffsetLeft,
y: event.touches[i].pageY - mCanvasOffsetTop,
drawn: false,
timestamp: new Date().getTime(),
})
}
}
}
}
function draw () {
var DRAW_TIME_THRESHOLD = 10
var start = new Date()
context.lineWidth = mPanWidth
for (var i = 0; i < paths.length
&& new Date() - start < DRAW_TIME_THRESHOLD; i++) {
var firstPoint = true
var points = paths[i].points
if (points.length > 1 && points[points.length - 1].drawn == false) {
context.beginPath()
for (var j = 1; j < points.length; j++) {
if (firstPoint && points[j].drawn == false) {
firstPoint = false
context.moveTo(points[j - 1].x, points[j - 1].y)
points[j - 1].drawn = true
context.lineTo(points[j].x, points[j].y)
} else if (points[j].drawn == false) {
context.lineTo(points[j].x, points[j].y)
}
points[j].drawn = true
}
context.stroke()
context.closePath()
} else if (paths[i].complete && points[0].drawn == false) {
context.arc(points[0].x, points[0].y, context.lineWidth / 2, 0,
Math.PI * 2, false)
context.closePath()
context.fill()
points[0].drawn = true
}
}
}
/**
* 重新计算手写签名的有效区域
* @param x
* @param y
*/
function checkEffectivePoint (x, y) {
if (x >= 0 && x <= mCanvasWidth) {
if (x - mPanWidth < mEffectivePoint.minX || mEffectivePoint.minX === -1) {
mEffectivePoint.minX = x - mPanWidth
if (mEffectivePoint.minX < 0) {
mEffectivePoint.minX = 0
}
}
if (x + mPanWidth > mEffectivePoint.maxX || mEffectivePoint.maxX === -1) {
mEffectivePoint.maxX = x + mPanWidth
if (mEffectivePoint.maxX > mCanvasWidth) {
mEffectivePoint.maxX = mCanvasWidth
}
}
}
if (y >= 0 && y <= mCanvasHeight) {
if (y - mPanWidth < mEffectivePoint.minY || mEffectivePoint.minY === -1) {
mEffectivePoint.minY = y - mPanWidth
if (mEffectivePoint.minY < 0) {
mEffectivePoint.minY = 0
}
}
if (y + mPanWidth > mEffectivePoint.maxY || mEffectivePoint.maxY === -1) {
mEffectivePoint.maxY = y + mPanWidth
if (mEffectivePoint.maxY > mCanvasHeight) {
mEffectivePoint.maxY = mCanvasHeight
}
}
}
}
/**
* 保存图片
*/
function save () {
var image = new Image()
// var imgSrc = canvas.toDataURL("image/png");
image.src = document.getElementById('myCanvas').toDataURL('image/png')
if (HAS_IMAGE) {
getImageByEffectiveArea(image, function (img) {
if (window.ReactNativeWebView) {
// react-native的webview修改为react-native-webview组件,需要使用一下方式进行通信
window.ReactNativeWebView.postMessage(img)
initEffectivePoint()
} else if (window.postMessage) {
window.postMessage(img)
initEffectivePoint()
} else {
alert('浏览器暂不支持此操作')
}
})
} else {
/**
* 点击保存签章判断,画布是否有内容
* 如果画布为空,参数为null
* */
window.ReactNativeWebView.postMessage('null')
}
}
/**
* 根据手写区域裁剪图片
*/
function getImageByEffectiveArea (imageSource, callback) {
var effectivePoint = {
minX: mEffectivePoint.minX,
maxX: mEffectivePoint.maxX === mEffectivePoint.minX ? mEffectivePoint.minX + 10 : mEffectivePoint.maxX,
minY: mEffectivePoint.minY,
maxY: mEffectivePoint.maxY === mEffectivePoint.minX ? mEffectivePoint.maxY + 10 : mEffectivePoint.maxY,
}
canvas2 = document.getElementById('idStampAdjustCanvas')
context2 = canvas2.getContext('2d')
//先清除
context2.clearRect(0, 0, canvas2.width, canvas2.height)
var width = effectivePoint.maxX - effectivePoint.minX
var height = effectivePoint.maxY - effectivePoint.minY
canvas2.width = stampMaxX
canvas2.height = stampMaxY
loadImage(imageSource.src, function () {
context2 = canvas2.getContext('2d')
var drawStartPoint = getDrawArea(width, height)
console.log(width, height, drawStartPoint)
context2.drawImage(imageSource,
effectivePoint.minX, effectivePoint.minY, width, height,
drawStartPoint.startX, drawStartPoint.startY,
drawStartPoint.width, drawStartPoint.height)
callback(canvas2.toDataURL('image/png'))
})
}
/**
*
* 根据原始图片的宽高,在限定尺寸的canvas中配置好绘图起始点坐标和宽高,让图片能够不变形地完全绘制在限定尺寸的canvas中
* @param width 真实手写的宽
* @param height 真实手写区域的高度
* canvas的尺寸(maxX=400,maxY=160)
* 1-原始图片width<maxX
* 1-1 height<maxY
* 1-2 height>=maxY
* 2-原始图片width>=maxX
* 2-1 width/maxX > height/maxY (宽度的比例大于高度的比例)
* 2-2 width/maxX <= height/maxY
*
* @return {
{width: number, startY: number, startX: number, height: number}}
*/
function getDrawArea (width, height) {
var drawStartPoint = {
startX: 0, //重绘起点的x坐标
startY: 0, //重绘起点的y坐标
width: stampMaxX, //重绘图片的宽
height: stampMaxY, //重绘图片的高
}
if (width < stampMaxX) {
if (height < stampMaxY) {
drawStartPoint = {
startX: (stampMaxX - width) / 2,
startY: (stampMaxY - height) / 2,
width: width,
height: height,
}
console.log(111111)
} else {
var scale = stampMaxY / height
drawStartPoint = {
startX: (stampMaxX - width * scale) / 2,
startY: 0,
width: width * scale,
height: stampMaxY,
}
console.log(222222, scale)
}
} else {
var scaleWidth = stampMaxX / width
var scaleHeight = stampMaxY / height
var scale = scaleWidth
if (width / height > stampMaxX / stampMaxY) {
// 说明图片相对比例,缩放后,width更小,所以以height为基准边
scale = scaleWidth
drawStartPoint = {
startX: 0,
startY: (stampMaxY - height * scale) / 2,
width: stampMaxX,
height: height * (stampMaxX / width),
}
} else {
scale = scaleHeight
drawStartPoint = {
startX: (stampMaxX - width * scale) / 2,
startY: 0,
width: width * (stampMaxY / height),
height: stampMaxY,
}
}
}
return drawStartPoint
}
function loadImage (url, callback) {
var img = new Image() // 创建一个Image对象,实现图片的预下载
img.src = url
if (img.complete) {
// 如果图片已经存在于浏览器缓存,直接调用回调函数
callback.call(img)
}
img.onload = function () { // 图片下载完毕时异步调用callback函数。
callback.call(img)// 将回调函数的this替换为Image对象
}
}
function initEffectivePoint () {
mEffectivePoint = {
minX: -1,
minY: -1,
maxX: -1,
maxY: -1,
}
}
setTimeout(function () {
// 延时进行init,需要等页面渲染完成后再初始化canvas
init()
}, 600)
</script>
</body>
</html>