• 原生js+canvas实现滑动拼图验证码


    上图为网易云盾的滑动拼图验证码,同一张图片裁剪的位置是固定的,猜想其应该有一个专门的图片库,保存了被裁剪的背景图和可移动的滑块。我的想法是,随机生成图片,随机生成位置,再用canvas裁剪出滑块和背景图。下面介绍具体步骤。

    1、首先随便找一张图片渲染到canvas上,这里#canvas作为画布,#block作为裁剪出来的小滑块。

    <canvas width="310" height="155" id="canvas"></canvas>
    <canvas width="310" height="155" id="block"></canvas>
      var canvas = document.getElementById('canvas')
      var block = document.getElementById('block')
      var canvas_ctx = canvas.getContext('2d')
      var block_ctx = block.getContext('2d')
      var img = document.createElement('img')
      img.onload = function() {
        canvas_ctx.drawImage(img, 0, 0, 310, 155)
        block_ctx.drawImage(img, 0, 0, 310, 155)
      };
      img.src = 'img.jpg'

    2.用canvas画一个正方形

    下面考虑如何裁剪出拼图的形状,拼图形状比较复杂,首先我们画一个正方形,接着上边的代码写:

     var x = 150, y = 40, w = 42, r = 10, PI = Math.PI 
      function draw(ctx) {
        ctx.beginPath()
        ctx.moveTo(x, y)
        ctx.lineTo(x + w, y)
        ctx.lineTo(x + w, y + w)
        ctx.lineTo(x, y + w)
        ctx.clip()
      }
      draw(canvas_ctx)
      draw(block_ctx)

    x,y为正方形左上角的坐标,现在先随便写后边生成的时候用随机数,w为正方形的边长,r为后边画缺口的圆的半径。我们先把绘图过程用函数封装起来,方便后面同时操作背景和滑块。用clip()方法裁剪图片后生成一个正方形。

    3.接下来用canvas画正方形上边和右边的圆形("+"号代表比前一步新增的代码,“-”号代表比前一步减少的代码):

    function draw(ctx) {
        ctx.beginPath()
        ctx.moveTo(x,y)
     +  ctx.lineTo(x+w/2,y)//绘制顶部圆
     +  ctx.arc(x+w/2,y-r+2, r,0,2*PI)//绘制顶部圆
     +  ctx.lineTo(x+w/2,y)//绘制顶部圆
        ctx.lineTo(x+w,y)
     +  ctx.lineTo(x+w,y+w/2)//绘制右部圆
     +  ctx.arc(x+w+r-2,y+w/2,r,0,2*PI) //绘制右部圆
     +  ctx.lineTo(x+w,y+w/2)//绘制右部圆
        ctx.lineTo(x+w,y+w)
        ctx.lineTo(x,y+w)
        ctx.lineTo(x,y)
        ctx.clip()
      }

    两处注释的位置将圆心往内偏移了2px,实现缺口的样式。

    4.用canvas画左边空心的部分

    然后是左边空心的部分,由于clip是裁剪路径内的部分,因此直接像上面画圆是不行的,我们开启一条新的路径,然后画圆将这个正方形“遮盖”出一个缺口,这里会用到globalCompositeOperation 属性,'xor'顾名思义。代码接上边("+"号代表比前一步新增的代码,“-”号代表比前一步减少的代码):

    function draw(ctx) {
        ctx.beginPath()
        ...
        ctx.lineTo(x,y)
        ctx.clip()
    +   ctx.beginPath()
    +   ctx.arc(x,y+w/2, r,1.5*PI,0.5*PI) // 只需要画正方形内的半圆就行,方便背景图片的裁剪
    +   ctx.globalCompositeOperation = "xor"
    +   ctx.fill()
    }

    5.调整#block的大小

    现在一个基本的拼图形状有了,我们调整#block的大小,并将裁剪出来的滑块放入#block中("+"号代表比前一步新增的代码,“-”号代表比前一步减少的代码):

    img.onload = function() {
        ctx.drawImage(img, 0, 0, 310, 155)
        block_ctx.drawImage(img, 0, 0, 310, 155)
    
    +   var blockWidth = w + r * 2
    +   var _y = y - r * 2 + 2 // 滑块实际的y坐标
    +   var ImageData = block_ctx.getImageData(x, _y, blockWidth, blockWidth)
    +   block.width = blockWidth
    +   block_ctx.putImageData(ImageData, 0, _y)
      }

    6.抠掉原图片的滑块部分

    现在我们需要把左边画布展示原来的图片,并且抠掉中间滑块的部分,这里画路径的过程都是一样的,唯一不同只是clip()那里改成fill()即可实现效果,我们前面已经把画路径的过程封装成函数了,稍作改动即可("+"号代表比前一步新增的代码,“-”号代表比前一步减少的代码):

    -  function draw(ctx) {
    +  function draw(ctx, operation) {
        ...
    -  ctx.clip()
    
    +  ctx.fillStyle = '#fff'
    +  ctx[operation]()
       ...
    }
    +  draw(canvas_ctx, 'fill')
    +  draw(block_ctx, 'clip')

    7.写拖动事件

    然后就是写拖动事件,我们可以在鼠标按下时记录鼠标位置,然后在拖动时给滑块设置left值。最后在松开鼠标时,判断滑块此时的left值和最开始裁剪滑块时的x值,如果在一定范围内就算验证通过,否则验证失败。

    最后再加上随机图片和随机剪切位置,基本就ok了。另外可以判断下鼠标移动时y轴的变化,以判断是否是“人”在操作,当然web安全这块神魔乱舞的,我就不班门弄斧了,只是做个简单判断就行。

    8.详细代码:

    8.1html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>滑动拼图验证码</title>
      <style>
        .container {
          width: 310px;
          margin: 100px auto;
        }
        input {
          display: block;
          width: 290px;
          line-height: 40px;
          margin: 10px 0;
          padding: 0 10px;
          outline: none;
          border:1px solid #c8cccf;
          border-radius: 4px;
          color:#6a6f77;
        }
        #msg {
          width: 100%;
          line-height: 40px;
          font-size: 14px;
          text-align: center;
        }
        a:link,a:visited,a:hover,a:active {
          margin-left: 100px;
          color: #0366D6;
        }
        
        .block {
      position: absolute;
      left: 0;
      top: 0;
      cursor: pointer;
      cursor: grab;
    }
    
    .block:active {
      cursor: grabbing;
    }
    
    .sliderContainer {
      position: relative;
      text-align: center;
      width: 310px;
      height: 40px;
      line-height: 40px;
      margin-top: 15px;
      background: #f7f9fa;
      color: #45494c;
      border: 1px solid #e4e7eb;
    }
    
    .sliderContainer_active .slider {
      height: 38px;
      top: -1px;
      border: 1px solid #1991FA;
    }
    
    .sliderContainer_active .sliderMask {
      height: 38px;
      border-width: 1px;
    }
    
    .sliderContainer_success .slider {
      height: 38px;
      top: -1px;
      border: 1px solid #52CCBA;
      background-color: #52CCBA !important;
    }
    
    .sliderContainer_success .sliderMask {
      height: 38px;
      border: 1px solid #52CCBA;
      background-color: #D2F4EF;
    }
    
    .sliderContainer_success .sliderIcon {
      background-position: 0 0 !important;
    }
    
    .sliderContainer_fail .slider {
      height: 38px;
      top: -1px;
      border: 1px solid #f57a7a;
      background-color: #f57a7a !important;
    }
    
    .sliderContainer_fail .sliderMask {
      height: 38px;
      border: 1px solid #f57a7a;
      background-color: #fce1e1;
    }
    
    .sliderContainer_fail .sliderIcon {
      top: 14px;
      background-position: 0 -82px !important;
    }
    .sliderContainer_active .sliderText, .sliderContainer_success .sliderText, .sliderContainer_fail .sliderText {
      display: none;
    }
    
    .sliderMask {
      position: absolute;
      left: 0;
      top: 0;
      height: 40px;
      border: 0 solid #1991FA;
      background: #D1E9FE;
    }
    
    .slider {
      position: absolute;
      top: 0;
      left: 0;
      width: 40px;
      height: 40px;
      background: #fff;
      box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
      transition: background .2s linear;
      cursor: pointer;
      cursor: grab;
    }
    
    .slider:active {
      cursor: grabbing;
    }
    
    .slider:hover {
      background: #1991FA;
    }
    
    .slider:hover .sliderIcon {
      background-position: 0 -13px;
    }
    
    .sliderIcon {
      position: absolute;
      top: 15px;
      left: 13px;
      width: 14px;
      height: 12px;
      background: url(http://cstaticdun.126.net//2.6.3/images/icon_light.f13cff3.png) 0 -26px;
      background-size: 34px 471px;
    }
    
    .refreshIcon {
      position: absolute;
      right: 0;
      top: 0;
      width: 34px;
      height: 34px;
      cursor: pointer;
      background: url(http://cstaticdun.126.net//2.6.3/images/icon_light.f13cff3.png) 0 -437px;
      background-size: 34px 471px;
    }
      </style>
    </head>
    <body>
    <h3><a href="https://github.com/yeild/jigsaw">返回GitHub</a></h3>
    <div class="container">
      <input value="admin" readonly/>
      <input type="password" value="1234567890" readonly/>
      <div id="captcha"></div>
      <div id="msg"></div>
    </div>
    <script src="./jigsaw.js"></script>
    <script>
      window.jigsaw.init({
        el: document.getElementById('captcha'),
        onSuccess: function() {
          document.getElementById('msg').innerHTML = '登录成功!'
        },
        onFail: cleanMsg,
        onRefresh: cleanMsg
      })
      function cleanMsg() {
        document.getElementById('msg').innerHTML = ''
      }
    </script>
    </body>
    </html>

    8.2jigsaw.js

    let w = 310 // canvas宽度
    let h = 155 // canvas高度
    const l = 42 // 滑块边长
    const r = 9 // 滑块半径
    const PI = Math.PI
    const L = l + r * 2 + 3 // 滑块实际边长
    
    function getRandomNumberByRange (start, end) {
      return Math.round(Math.random() * (end - start) + start)
    }
    
    function createCanvas (width, height) {
      const canvas = document.createElement('canvas')
      canvas.width = width
      canvas.height = height
      return canvas
    }
    
    function createImg (onload) {
      const img = new Image()
      img.crossOrigin = "Anonymous"
      img.onload = onload
      img.onerror = () => {
       img.setSrc(getRandomImgSrc())
      }
      
      img.setSrc = function (src) {
        const isIE = window.navigator.userAgent.indexOf('Trident') > -1
        if (isIE) { // IE浏览器无法通过img.crossOrigin跨域,使用ajax获取图片blob然后转为dataURL显示
          const xhr = new XMLHttpRequest()
          xhr.onloadend = function (e) {
            const file = new FileReader() // FileReader仅支持IE10+
            file.readAsDataURL(e.target.response)
            file.onloadend = function (e) {
              img.src = e.target.result
            }
          }
          xhr.open('GET', src)
          xhr.responseType = 'blob'
          xhr.send()
        }
        else img.src = src
      }
    
      img.setSrc(getRandomImgSrc())
      return img
    }
    
    function createElement (tagName, className) {
      const elment = document.createElement(tagName)
      elment.className = className
      return elment
    }
    
    function addClass (tag, className) {
      tag.classList.add(className)
    }
    
    function removeClass (tag, className) {
      tag.classList.remove(className)
    }
    
    function getRandomImgSrc () {
      return `https://picsum.photos/${w}/${h}/?image=${getRandomNumberByRange(0, 1084)}`
    }
    
    function draw (ctx, x, y, operation) {
      ctx.beginPath()
      ctx.moveTo(x, y)
      ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI)
      ctx.lineTo(x + l, y)
      ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI)
      ctx.lineTo(x + l, y + l)
      ctx.lineTo(x, y + l)
      ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true)
      ctx.lineTo(x, y)
      ctx.lineWidth = 2
      ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
      ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'
      ctx.stroke()
      ctx[operation]()
      ctx.globalCompositeOperation = 'destination-over'
    }
    
    function sum (x, y) {
      return x + y
    }
    
    function square (x) {
      return x * x
    }
    
    class jigsaw {
      constructor ({ el, width = 310, height = 155, onSuccess, onFail, onRefresh }) {
        w = width
        h = height
        el.style.position = 'relative'
        el.style.width = w + 'px'
        Object.assign(el.style, {
          position: 'relative',
           w + 'px',
          margin: '0 auto'
        })
        this.el = el
        this.onSuccess = onSuccess
        this.onFail = onFail
        this.onRefresh = onRefresh
      }
    
      init () {
        this.initDOM()
        this.initImg()
        this.bindEvents()
      }
    
      initDOM () {
        const canvas = createCanvas(w, h) // 画布
        const block = canvas.cloneNode(true) // 滑块
        const sliderContainer = createElement('div', 'sliderContainer')
        sliderContainer.style.width = w + 'px'
        const refreshIcon = createElement('div', 'refreshIcon')
        const sliderMask = createElement('div', 'sliderMask')
        const slider = createElement('div', 'slider')
        const sliderIcon = createElement('span', 'sliderIcon')
        const text = createElement('span', 'sliderText')
    
        block.className = 'block'
        text.innerHTML = '向右滑动填充拼图'
    
        const el = this.el
        el.appendChild(canvas)
        el.appendChild(refreshIcon)
        el.appendChild(block)
        slider.appendChild(sliderIcon)
        sliderMask.appendChild(slider)
        sliderContainer.appendChild(sliderMask)
        sliderContainer.appendChild(text)
        el.appendChild(sliderContainer)
    
        Object.assign(this, {
          canvas,
          block,
          sliderContainer,
          refreshIcon,
          slider,
          sliderMask,
          sliderIcon,
          text,
          canvasCtx: canvas.getContext('2d'),
          blockCtx: block.getContext('2d')
        })
      }
    
      initImg () {
        const img = createImg(() => {
          this.draw()
          this.canvasCtx.drawImage(img, 0, 0, w, h)
          this.blockCtx.drawImage(img, 0, 0, w, h)
          const y = this.y - r * 2 - 1
          const ImageData = this.blockCtx.getImageData(this.x - 3, y, L, L)
          this.block.width = L
          this.blockCtx.putImageData(ImageData, 0, y)
        })
        this.img = img
      }
    
      draw () {
        // 随机创建滑块的位置
        this.x = getRandomNumberByRange(L + 10, w - (L + 10))
        this.y = getRandomNumberByRange(10 + r * 2, h - (L + 10))
        draw(this.canvasCtx, this.x, this.y, 'fill')
        draw(this.blockCtx, this.x, this.y, 'clip')
      }
    
      clean () {
        this.canvasCtx.clearRect(0, 0, w, h)
        this.blockCtx.clearRect(0, 0, w, h)
        this.block.width = w
      }
    
      bindEvents () {
        this.el.onselectstart = () => false
        this.refreshIcon.onclick = () => {
          this.reset()
          typeof this.onRefresh === 'function' && this.onRefresh()
        }
    
        let originX, originY, trail = [], isMouseDown = false
    
        const handleDragStart = function (e) {
          originX = e.clientX || e.touches[0].clientX
          originY = e.clientY || e.touches[0].clientY
          isMouseDown = true
        }
    
        const handleDragMove = (e) => {
          if (!isMouseDown) return false
          const eventX = e.clientX || e.touches[0].clientX
          const eventY = e.clientY || e.touches[0].clientY
          const moveX = eventX - originX
          const moveY = eventY - originY
          if (moveX < 0 || moveX + 38 >= w) return false
          this.slider.style.left = moveX + 'px'
          const blockLeft = (w - 40 - 20) / (w - 40) * moveX
          this.block.style.left = blockLeft + 'px'
    
          addClass(this.sliderContainer, 'sliderContainer_active')
          this.sliderMask.style.width = moveX + 'px'
          trail.push(moveY)
        }
    
        const handleDragEnd = (e) => {
          if (!isMouseDown) return false
          isMouseDown = false
          const eventX = e.clientX || e.changedTouches[0].clientX
          if (eventX === originX) return false
          removeClass(this.sliderContainer, 'sliderContainer_active')
          this.trail = trail
          const { spliced, verified } = this.verify()
          if (spliced) {
            if (verified) {
              addClass(this.sliderContainer, 'sliderContainer_success')
              typeof this.onSuccess === 'function' && this.onSuccess()
            } else {
              addClass(this.sliderContainer, 'sliderContainer_fail')
              this.text.innerHTML = '请再试一次'
              this.reset()
            }
          } else {
            addClass(this.sliderContainer, 'sliderContainer_fail')
            typeof this.onFail === 'function' && this.onFail()
            setTimeout(() => {
              this.reset()
            }, 1000)
          }
        }
        this.slider.addEventListener('mousedown', handleDragStart)
        this.slider.addEventListener('touchstart', handleDragStart)
        this.block.addEventListener('mousedown', handleDragStart)
        this.block.addEventListener('touchstart', handleDragStart)
        document.addEventListener('mousemove', handleDragMove)
        document.addEventListener('touchmove', handleDragMove)
        document.addEventListener('mouseup', handleDragEnd)
        document.addEventListener('touchend', handleDragEnd)
      }
    
      verify () {
        const arr = this.trail // 拖动时y轴的移动距离
        const average = arr.reduce(sum) / arr.length
        const deviations = arr.map(x => x - average)
        const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length)
        const left = parseInt(this.block.style.left)
        return {
          spliced: Math.abs(left - this.x) < 10,
          verified: stddev !== 0, // 简单验证拖动轨迹,为零时表示Y轴上下没有波动,可能非人为操作
        }
      }
    
      reset () {
        this.sliderContainer.className = 'sliderContainer'
        this.slider.style.left = 0
        this.block.style.left = 0
        this.sliderMask.style.width = 0
        this.clean()
        this.img.setSrc(getRandomImgSrc())
      }
    }
    
    window.jigsaw = {
      init: function (opts) {
        return new jigsaw(opts).init()
      }
    }

    转自:https://www.cnblogs.com/huanglei-/p/8568405.html

  • 相关阅读:
    JDK介绍
    选择器——过滤选择器——内容过滤器
    选择器——过滤选择器——基本过滤器
    选择器——层次选择器
    选择器——基本选择器
    jQuery 对象转成 DOM 对象
    dom转换成jquery对象
    Android-bindService本地服务-初步-Service返回对象
    Android-bindService本地服务-初步
    Android-Service生命周期
  • 原文地址:https://www.cnblogs.com/vickylinj/p/12103860.html
Copyright © 2020-2023  润新知