原生javascript使用canvas实现移动端滑块拼图验证
程序员文章站
2022-05-24 18:51:39
...
参考:https://www.twle.cn/l/yufei/canvas/canvas-basic-index.html
此demo必须在服务端运行,如weblogic、tomcat等中间件或者vscode的Live Server。
很久没写JS了,练练手,我的实现方法并不是最优解,还差得远,各路高人请见谅。
这博客上传图片的功能找不到了,没有附件啊?
其中还有未解决的问题:
先使用ctx.save()保存状态,使用ctx.clip()剪裁图像之后,使用ctx.restore()无法恢复状态,最后没办法只好通过重置canvas宽高的办法重置画布,哪位朋友有解决办法请告知,不胜感谢!
另外感觉代码写得很啰嗦
需要自备一张背景图,放在images目录下,具体请看代码。
demo.html:
<!DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>原生javascript使用canvas实现移动端滑块拼图验证</title> <style> html, body{ width: 100%; height: 100%; } *{ padding: 0px; margin: 0px; font-size: 1em; } /* 图形验证码的遮罩 */ #sliderMask{ position: fixed; z-index: 5; left: 0px; top: 0px; background-color: black; opacity: 0.8; } /* 图形验证码的容器 */ #sliderCanvasContainer{ position: fixed; z-index: 6; background-color: white; border-radius: 8px; padding-bottom: 20px; } /* 图形验证码的画布 */ #sliderMainCanvas{ border-radius: 5px; } </style> <script src="slider.js"></script> <script> window.onload = function(){ let clientWidth = document.documentElement.clientWidth; let clientHeight = document.documentElement.clientHeight; console.log("界面分辨率:" + clientWidth + "," + clientHeight); document.getElementById("aa").onclick = function(){ mySlider.show(); } // 显示滑块的前置条件 function showCondition(){ } function picValidator(v){ if(v){ console.log("滑块验证通过"); }else{ console.log("滑块验证未通过"); } } mySlider.init(clientWidth, clientHeight, "images/slider1.jpg", picValidator); } </script> </head> <body> <p id="aa" style="margin-top: 100px; text-align: center;">打开图形验证</p> </body> </html>
slider.js:
//生成从minNum到maxNum的随机数 function randomNum(minNum, maxNum) { switch (arguments.length) { case 1: return parseInt(Math.random() * minNum + 1, 10); case 2: return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10); default: return 0; break; } } /** * 画拼图方块的外框 * @param startX 起始点坐标X * @param startY 起始点坐标Y * @param lineWidth 线粗 * @param topStart 顶部突起与左侧边的距离 * @param topRadius 顶部突起圆形的半径 * @param rightStart 右侧突起与顶边的距离 * @param rightRadius 右侧突起圆形的半径 * @param bottomStart 底部突起与右侧边的距离 * @param bottomRadius 底部突起圆形的半径 * @param leftStart 左侧突起与底边的距离 * @param leftRadius 左侧突起圆形的半径 */ function genBorderPath(startX, startY, lineWidth) { // debugger; let topStart = mySlider.data.jigsawShape.top.segmentLength; let topNeck = mySlider.data.jigsawShape.top.neck; let topRadius = mySlider.data.jigsawShape.top.radius; let rightStart = mySlider.data.jigsawShape.right.segmentLength; let rightNeck = mySlider.data.jigsawShape.right.neck; let rightRadius = mySlider.data.jigsawShape.right.radius; let bottomStart = mySlider.data.jigsawShape.bottom.segmentLength; let bottomNeck = mySlider.data.jigsawShape.bottom.neck; let bottomRadius = mySlider.data.jigsawShape.bottom.radius; let leftStart = mySlider.data.jigsawShape.left.segmentLength; let leftNeck = mySlider.data.jigsawShape.left.neck; let leftRadius = mySlider.data.jigsawShape.left.radius let ctx = mySlider.data.mainCtx; ctx.beginPath(); // 设置线的样式 ctx.strokeStyle = "#FFFFFF"; // 设置笔触的颜色 ctx.shadowColor = "#000000"; // 设置阴影的颜色 ctx.shadowBlur = "5"; // 设置阴影的模糊级别 ctx.shadowOffsetX = "1"; // 设置阴影距形状的水平距离 ctx.shadowOffsetY = "1"; // 设置阴影距开关的垂直距离 ctx.lineWidth = lineWidth; let x = startX, y = startY; // 从左上角开始 // 画顶边 ctx.moveTo(x, y); x += topStart; ctx.lineTo(x, y); // 从左上角到顶部突起的横线 y -= topNeck; ctx.lineTo(x, y); // 顶部突起的“脖子”长度为整个画板高度的1% x += topRadius; // 顶部突起的“脑袋”,就是个半圆 ctx.arc( x, // 圆心X坐标 y, // 圆心Y坐标 topRadius, // 圆的半径 Math.PI / 180 * 180, // 起始角,以弧度计(弧的圆形的三点钟位置是0度) Math.PI / 180 * 360 // 结束角,以弧度计 ); x += topRadius; y += topNeck; ctx.lineTo(x, y); x = startX + mySlider.data.jigsawSize; ctx.lineTo(x, y); // 画右边 y += rightStart; ctx.lineTo(x, y); x += rightNeck; ctx.lineTo(x, y); y += rightRadius; ctx.arc(x, y, rightRadius, Math.PI / 180 * 270, Math.PI / 180 * 450); x -= rightNeck; y += rightRadius; ctx.lineTo(x, y); y = startY + mySlider.data.jigsawSize; ctx.lineTo(x, y); // 画底边 x -= bottomStart; ctx.lineTo(x, y); y -= bottomNeck; ctx.lineTo(x, y); x -= bottomRadius; ctx.arc(x, y, bottomRadius, 0, -1 * Math.PI / 180 * 180, true); x -= bottomRadius; y += bottomNeck; ctx.lineTo(x, y); x = startX; ctx.lineTo(x, y); // 画左边 y -= bottomStart; ctx.lineTo(x, y); x += leftNeck; y -= leftRadius; ctx.arc(x, y, leftRadius, Math.PI / 180 * 90, Math.PI / 180 * 270, true); x -= leftNeck; y -= leftRadius; ctx.lineTo(x, y); // 最后那条就不用画了,使用closePath()自动闭合 ctx.closePath(); } // 绘制圆角矩形 function roundedRect(ctx, x, y, width, height, radius, fillStyle){ ctx.beginPath(); ctx.moveTo(x, y + radius); ctx.lineTo(x, y + height - radius); ctx.quadraticCurveTo(x, y + height, x + radius, y + height); ctx.lineTo(x + width - radius, y + height); ctx.quadraticCurveTo(x + width, y + height, x + width, y + height - radius); ctx.lineTo(x + width, y + radius); ctx.quadraticCurveTo(x + width, y, x + width - radius, y); ctx.lineTo(x + radius, y); ctx.quadraticCurveTo(x, y, x, y + radius); ctx.closePath(); if(fillStyle!=null){ ctx.fillStyle = fillStyle; ctx.fill(); } ctx.stroke(); } let mySlider = { data:{ mainData : null, // 大图图像数据 sliderData: null, // 滑块图像数据 clientWidth: 0, clientHeight: 0, // 屏幕大小 canvasWidth: 0, canvasHeight: 0, // 大图canvas的大小 img: null, bgSrc: "", // 背景图 maskDom: null, // 遮罩节点 canvasContainerDom : null, // canvas容器 closeContainerDom : null, // 关闭按钮容器 mainCanvas: null, // 大图canvas节点 sliderCanvas: null, // 滑块canvas节点 mainCtx : null, // 大图canvas的2D绘图对象 sliderCtx : null, // 滑块canvas的2D绘图对象 state : 0, // 状态(0:完成初始化|1:手指按下|2:拖动中|3:手指抬起) fingerCoordinate: { oldX: 0, x: 0 }, // 手指坐标 endCoordinate: { x: 0, y: 0 }, // 目标位置坐标 jigsawCoordinate: { x: 5, y: 0 }, // 拼图方块当前坐标 jigsawShape: { // 拼图方块形状信息 top: { segmentLength: 0, // 第一条线段长 neck: 0, // 脖子长度 radius: 0 // 突起半径 }, right: { segmentLength: 0, // 第一条线段长 neck: 0, // 脖子长度 radius: 0 // 突起半径 }, bottom: { segmentLength: 0, // 第一条线段长 neck: 0, // 脖子长度 radius: 0 // 突起半径 }, left: { segmentLength: 0, // 第一条线段长 neck: 0, // 脖子长度 radius: 0 // 突起半径 } }, jigsawSize: 100, // 拼图方块大小(里面正方形的边长) callbackFn: null // 回调函数 }, // 生成随机形状 randomShape(){ // 初始化顶边形状参数 this.data.jigsawShape.top.segmentLength = randomNum(this.data.jigsawSize/4, this.data.jigsawSize/2); this.data.jigsawShape.top.neck = randomNum(this.data.jigsawSize/20, this.data.jigsawSize/8); this.data.jigsawShape.top.radius = randomNum(this.data.jigsawSize/9, this.data.jigsawSize/5); // 初始化右边形状参数 this.data.jigsawShape.right.segmentLength = randomNum(this.data.jigsawSize / 4, this.data.jigsawSize / 2); this.data.jigsawShape.right.neck = randomNum(this.data.jigsawSize/20, this.data.jigsawSize/8); this.data.jigsawShape.right.radius = randomNum(this.data.jigsawSize / 9, this.data.jigsawSize / 5); // 初始化底边形状参数 this.data.jigsawShape.bottom.segmentLength = randomNum(this.data.jigsawSize / 4, this.data.jigsawSize / 2); this.data.jigsawShape.bottom.neck = randomNum(this.data.jigsawSize/20, this.data.jigsawSize/8); this.data.jigsawShape.bottom.radius = randomNum(this.data.jigsawSize / 9, this.data.jigsawSize / 5); // 初始化左边形状参数 this.data.jigsawShape.left.segmentLength = randomNum(this.data.jigsawSize / 4, this.data.jigsawSize / 2); this.data.jigsawShape.left.neck = randomNum(this.data.jigsawSize/20, this.data.jigsawSize/8); this.data.jigsawShape.left.radius = randomNum(this.data.jigsawSize / 9, this.data.jigsawSize / 5); }, // 初始化方法 init: function(width, height, bgSrc, callbackFn){ // debugger; this.data.clientWidth = width; this.data.clientHeight = height; this.data.bgSrc = bgSrc; this.data.canvasWidth = width * 0.9 * 0.9; this.data.canvasHeight = width * 0.9 * 0.8 * 0.5; this.data.jigsawSize = this.data.canvasHeight / 3; // 生成随机形状 this.randomShape(); // 创建遮罩 this.data.maskDom = document.createElement("div"); this.data.maskDom.id = "sliderMask"; this.data.maskDom.style.display = "none"; this.data.maskDom.style.width = width + "px"; this.data.maskDom.style.height = height + "px"; this.data.maskDom.onclick = mySlider.close; document.body.appendChild(this.data.maskDom); // 创建canvas容器 this.data.canvasContainerDom = document.createElement("div"); this.data.canvasContainerDom.id = "sliderCanvasContainer"; this.data.canvasContainerDom.style.display = "none"; this.data.canvasContainerDom.style.width = width * 0.9 + "px"; this.data.canvasContainerDom.style.height = width * 0.7 + "px"; this.data.canvasContainerDom.style.left = width * 0.05 + "px"; this.data.canvasContainerDom.style.top = (height - width * 0.8)/2 + "px"; document.body.appendChild(this.data.canvasContainerDom); // 创建刷新按钮 let titleTable = document.createElement("table"); titleTable.style.width = "100%"; let tr = document.createElement("tr"); let td1 = document.createElement("td"); td1.style.width = "4em"; let td2 = document.createElement("td"); td2.style.lineHeight = "50px"; td2.style.textAlign = "center"; td2.innerHTML = "图形验证"; let td3 = document.createElement("td"); td3.id = "closeContainer"; td3.style.color = "#009582"; td3.style.textAlign = "center"; td3.innerHTML = "刷新"; td3.style.width = "4em"; td3.onclick = function(){ mySlider.repaintAll(true); } tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3); titleTable.appendChild(tr); this.data.canvasContainerDom.appendChild(titleTable); // 创建大图canvas节点 let canvas = document.createElement("canvas"); canvas.id = "sliderMainCanvas"; canvas.width = this.data.canvasWidth; canvas.height = this.data.canvasHeight; canvas.style.marginLeft = (width*0.9 - canvas.width)/2 + "px"; this.data.mainCanvas = canvas; this.data.canvasContainerDom.appendChild(canvas); this.data.mainCtx = canvas.getContext("2d"); // 大图canvas的手指事件 canvas.addEventListener("touchstart", this.touchStart); canvas.addEventListener("touchmove", this.touchMove); canvas.addEventListener("touchend", this.touchEnd); // 创建滑块canvas节点 let sliderCanvas = document.createElement("canvas"); sliderCanvas.id = "sliderCanvas"; sliderCanvas.width = this.data.canvasWidth; sliderCanvas.height = 50; sliderCanvas.style.marginLeft = (width*0.9 - sliderCanvas.width)/2 + "px"; this.data.sliderCanvas = sliderCanvas; this.data.canvasContainerDom.appendChild(sliderCanvas); this.data.sliderCtx = sliderCanvas.getContext("2d"); // 滑块canvas的手指事件 sliderCanvas.addEventListener("touchstart", this.touchStart); sliderCanvas.addEventListener("touchmove", this.touchMove); sliderCanvas.addEventListener("touchend", this.touchEnd); // console.log(mySlider); let img = new Image(); img.src = this.data.bgSrc; this.data.img = img; img.onload = function(){ // 绘制基本图形 mySlider.paintBase(); // 绘制拼图方块 mySlider.paintJigsaw(); // 绘制小滑块 mySlider.paintRectBtn(); }; this.callbackFn = callbackFn; return this; }, // 显示完整滑块UI,包括遮罩、图片和拼图方块等 show: function(){ if(mySlider.data.mainCtx == null){ // this.mainData == null alert("canvas未初始化"); return; } // console.log("show()"); mySlider.data.maskDom.style.display = "block"; mySlider.data.canvasContainerDom.style.display = "block"; mySlider.repaintAll(true); return mySlider; }, close: function(){ // console.log("close()"); // console.log(this); mySlider.data.maskDom.style.display = "none"; mySlider.data.canvasContainerDom.style.display = "none"; }, // 第一次绘制基本图像 paintBase : function(){ // console.log("paintBase()"); mySlider.data.mainCanvas.width = mySlider.data.canvasWidth; mySlider.data.mainCanvas.height = mySlider.data.canvasHeight; // 生成拼图位置范围随机数 // X坐标最小值和最大值 let minX = mySlider.data.jigsawSize * 1.5; // 最小留出1.5个方块的距离 let maxX = mySlider.data.canvasWidth - minX - mySlider.data.jigsawSize; // Y坐标最小值和最大值 let minY = minX; let maxY = mySlider.data.canvasHeight - minY - mySlider.data.jigsawSize; // console.log("拼图方块大小:" + mySlider.data.jigsawSize); mySlider.data.mainCtx.drawImage(mySlider.data.img, 0, 0, mySlider.data.canvasWidth, mySlider.data.canvasHeight); // 生成目标位置信息 mySlider.data.endCoordinate.x = randomNum(minX, maxX); mySlider.data.endCoordinate.y = randomNum(minY, maxY); let x = mySlider.data.endCoordinate.x; let y = mySlider.data.endCoordinate.y; // console.log("初始化拼图方块随机位置:(" + x + "," + y + ")"); // 绘制目标位置 genBorderPath(x, y, 2); mySlider.data.mainCtx.fillStyle = "rgb(125, 125, 125, 0.3)"; // 填充半透明颜色 mySlider.data.mainCtx.fill(); mySlider.data.mainCtx.stroke(); // 保存已生成的大图图像 mySlider.data.mainData = mySlider.data.mainCtx.getImageData(0, 0, mySlider.data.canvasWidth, mySlider.data.canvasHeight); mySlider.data.mainCtx.save(); // 绘制下面的滑块UI // 外框 let sliderCtx = mySlider.data.sliderCtx; mySlider.data.sliderCanvas.width = mySlider.data.canvasWidth; mySlider.data.sliderCanvas.height = 50; sliderCtx.save(); sliderCtx.strokeStyle = "#999999"; roundedRect(sliderCtx, 0, 0, mySlider.data.canvasWidth-1, 49, 5); // 文字 sliderCtx.font = "24px Microsoft YaHei"; sliderCtx.textalign = "center"; sliderCtx.textBaseline='middle';//文本垂曲标的目的,基线位置 let msg = "拖动以完成拼图"; sliderCtx.fillText(msg, sliderCtx.measureText(msg).width/2, 25); // 保存已生成的滑块UI基本图像 mySlider.data.sliderData = sliderCtx.getImageData(0, 0, mySlider.data.canvasWidth, 50); }, // 绘制拼图方块 paintJigsaw: function(){ let x = mySlider.data.jigsawCoordinate.x; let y = mySlider.data.endCoordinate.y; let ctx = mySlider.data.mainCtx; // 绘制拼图方块外围的白边 genBorderPath(x, y, 2); ctx.stroke(); // 绘制拼图方块内的图形 genBorderPath(x, y, 0); ctx.fillStyle = "rgb(125, 125, 125, 0.3)"; ctx.fill(); ctx.clip(); ctx.stroke(); // 在路径内绘制图形 ctx.drawImage(mySlider.data.img, mySlider.data.jigsawCoordinate.x - mySlider.data.endCoordinate.x, 0, mySlider.data.canvasWidth, mySlider.data.canvasHeight); ctx.restore(); }, // 绘制小滑块 paintRectBtn: function(){ let ctx = mySlider.data.sliderCtx; ctx.save(); //ctx.fillRect(4, 4, 42, 42); ctx.fillStyle = "white"; ctx.lineWidth = 2; ctx.strokeStyle = "#009582"; let x = 4, y = 4; let width = 42, height = 42; let radius = 5; roundedRect(ctx, mySlider.data.jigsawCoordinate.x, y, width, height, radius, "white"); //绘制三条杠 ctx.restore(); ctx.lineWidth = 3; ctx.strokeStyle = "#009582"; let startX = mySlider.data.jigsawCoordinate.x + 10, endX = mySlider.data.jigsawCoordinate.x + 33; ctx.moveTo(startX, 16); ctx.lineTo(endX, 16); ctx.moveTo(startX, 25); ctx.lineTo(endX, 25); ctx.moveTo(startX, 34); ctx.lineTo(endX, 34); ctx.stroke(); }, // 重绘所有 // isRefresh: 是否刷新(true:刷新,重新生成图像|false:不刷新,使用之前创建的图像) repaintAll: function(isRefresh){ if(isRefresh){ // 生成随机形状 this.randomShape(); // 重绘基本图像 mySlider.data.jigsawCoordinate.x = 5; mySlider.paintBase(); }else{ // 使用之前创建的图像 mySlider.data.mainCtx.putImageData(mySlider.data.mainData, 0, 0); mySlider.data.sliderCtx.putImageData(mySlider.data.sliderData, 0, 0); } // 重绘拼图方块 mySlider.paintJigsaw(); // 重绘小滑块 mySlider.paintRectBtn(); }, // 手指按下事件 touchStart: function(e){ mySlider.data.state = 1; mySlider.data.fingerCoordinate.oldX = e.touches[0].clientX; mySlider.data.fingerCoordinate.x = e.touches[0].clientX; }, // 手指移动事件 touchMove: function(e){ mySlider.data.state = 2; mySlider.data.fingerCoordinate.oldX = mySlider.data.fingerCoordinate.x; mySlider.data.fingerCoordinate.x = e.touches[0].clientX; let distance = mySlider.data.fingerCoordinate.x - mySlider.data.fingerCoordinate.oldX; mySlider.data.jigsawCoordinate.x += distance; if(mySlider.data.jigsawCoordinate.x < 5){ mySlider.data.jigsawCoordinate.x = 5; }else if(mySlider.data.jigsawCoordinate.x > mySlider.data.canvasWidth - mySlider.data.jigsawSize){ mySlider.data.jigsawCoordinate.x = mySlider.data.canvasWidth - mySlider.data.jigsawSize; } // 这里用重设宽高的方法重置画布,否则拖动时,拼图方块无法正确显示(因为ctx.restore()不生效,很奇怪) mySlider.data.mainCanvas.width = mySlider.data.canvasWidth; mySlider.data.mainCanvas.height = mySlider.data.canvasHeight; mySlider.repaintAll(false); }, // 手指抬起事件 touchEnd: function(e){ mySlider.data.state = 3; // 检查拼图是否到达目标位置 if(Math.abs(mySlider.data.jigsawCoordinate.x - mySlider.data.endCoordinate.x)<5){ // console.log("验证成功"); mySlider.callbackFn(true); return; } mySlider.callbackFn(false); // 未验证成功,显示拼图方块回归动画 let backInterval = window.setInterval(function(){ if(mySlider.data.state == 3){ mySlider.data.jigsawCoordinate.x -= 5; // 这里用重设宽高的方法重置画布,否则拖动时,拼图方块无法正确显示(因为ctx.restore()不生效,很奇怪) mySlider.data.mainCanvas.width = mySlider.data.canvasWidth; mySlider.data.mainCanvas.height = mySlider.data.canvasHeight; mySlider.data.sliderCanvas.width = mySlider.data.canvasWidth; if(mySlider.data.jigsawCoordinate.x < 5){ mySlider.data.jigsawCoordinate.x = 5; mySlider.data.state = 0; window.clearInterval(backInterval); } }else{ window.clearInterval(backInterval); } mySlider.data.mainCtx.restore(); mySlider.data.sliderCtx.restore(); mySlider.repaintAll(false); }, 1000/60); // 1000/60代表一秒60帧 } };