`
stride
  • 浏览: 59740 次
  • 性别: Icon_minigender_1
  • 来自: 长春
社区版块
存档分类
最新评论

原生javascript使用canvas实现移动端滑块拼图验证

阅读更多

参考: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帧
    }
};

 

0
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics