欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

原生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了,练练手,我的实现方法并不是最优解,还差得远,各路高人请见谅。

 原生javascript使用canvas实现移动端滑块拼图验证
            
    
    博客分类: JavaScriptWEB编程 js javascript canvas 滑块验证 

  这博客上传图片的功能找不到了,没有附件啊?原生javascript使用canvas实现移动端滑块拼图验证
            
    
    博客分类: JavaScriptWEB编程 js javascript canvas 滑块验证 

 

  其中还有未解决的问题:

  先使用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帧
    }
};