滑动填充拼图验证

2021-11-08  本文已影响0人  如果俞天阳会飞
class Captcha {
  static l = 42; // 滑块边长 42 * 42

  static r = 9; // 滑块突出来的半径

  static PI = Math.PI;

  static L = this.l + this.r * 2 + 3; // 滑块实际边长 63

  constructor(options) {
    const {
      el, w, h, onSuccess, onFail, onRefresh,
    } = options;
    el.style.position = el.style.position || 'relative';
    this.el = el;
    this.w = w || 310;
    this.h = h || 155;
    this.onSuccess = onSuccess;
    this.onFail = onFail;
    this.onRefresh = onRefresh;
  }

  init() {
    this.initDOM();
    this.initImg();
    this.bindEvents();
  }

  static createElement(tagName, className) {
    const el = document.createElement(tagName);
    el.className = className;
    return el;
  }

  static getRandomNumberByRange(start, end) {
    return Math.round(Math.random() * (end - start) + start);
  }

  static getRandomImg() {
    return `https://picsum.photos/300/150/?image=${this.getRandomNumberByRange(0, 1084)}`;
  }

  static addClass(tag, className) {
    tag.classList.add(className);
  }

  static removeClass(tag, className) {
    tag.classList.remove(className);
  }

  static sum(x, y) {
    return x + y;
  }

  static square(x) {
    return x * x;
  }

  static createImg(onload) {
    const that = this;
    const img = this.createElement('img');
    img.crossOrigin = 'Anonymous';
    img.onload = onload;
    img.onerror = function () {
      img.src = that.getRandomImg();
    };
    img.src = that.getRandomImg();
    return img;
  }

  static draw(ctx, x, y, operation) {
    const { l, r, PI } = Captcha;
    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 = 'overlay';
  }

  initDOM() {
    const canvas = Captcha.createElement('canvas');
    canvas.width = this.w;
    canvas.height = this.h;
    // 滑块
    const block = canvas.cloneNode(true);
    const sliderContainer = Captcha.createElement('div', 'captcha-slide-container');
    const refreshIcon = Captcha.createElement('div', 'captcha-refresh-icon');
    const sliderMask = Captcha.createElement('div', 'captcha-slider-mask');
    const slider = Captcha.createElement('div', 'captcha-slider');
    const sliderIcon = Captcha.createElement('span', 'captcha-slider-icon');
    const text = Captcha.createElement('span', 'sliderText');
    block.className = 'captcha-block';
    text.innerHTML = '向右滑动填充拼图';
    const { el } = this;
    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 = Captcha.createImg(() => {
      this.canvasCtx.drawImage(img, 0, 0, this.w, this.h);
      // 被扣掉的模块 x 和 y 轴的随机位置 X-min 73 X-max: canvas 宽度 -  73
      this.x = Captcha.getRandomNumberByRange(Captcha.L + 10, this.w - (Captcha.L + 10));
      this.y = Captcha.getRandomNumberByRange(10 + Captcha.r * 2, this.h - (Captcha.L + 10));
      Captcha.draw(this.canvasCtx, this.x, this.y, 'fill');
      Captcha.draw(this.blockCtx, this.x, this.y, 'clip');
      this.blockCtx.drawImage(img, 0, 0, this.w, this.h);
      if (navigator.userAgent.indexOf('MSIE') > -1) {
        this.block.style.marginLeft = `-${this.x - 3}px`;// 不抵边,空3px
      } else {
        const { r, L } = Captcha;
        const y = this.y - r * 2 - 1;
        const ImageData = this.blockCtx.getImageData(this.x - 3, y, L, L);
        this.block.width = Captcha.L;
        this.blockCtx.putImageData(ImageData, 0, y);
      }
    });
    this.img = img;
  }

  bindEvents() {
    const {
      w, slider, block, sliderContainer, sliderMask,
    } = this;
    const that = this;
    this.refreshIcon.onclick = () => {
      this.reset();
      // that.reset();
      typeof this.onRefresh === 'function' && this.onRefresh();
    };
    let originX = 0;
    let originY = 0;
    this.trail = [];
    let isMouseDown = false;
    const handleDragStart = function handleDragStart(e) {
      originX = e.clientX || e.touches[0].clientX;
      originY = e.clientY || e.touches[0].clientY;
      isMouseDown = true;
    };
    const handleDragMove = function (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;
      slider.style.left = `${moveX}px`;
      const blockLeft = (w - 40 - 20) / (w - 40) * moveX;
      block.style.left = `${blockLeft}px`;
      Captcha.addClass(sliderContainer, 'captcha-slide-container-active');
      sliderMask.style.width = `${moveX}px`;
      that.trail.push(moveY);
      return true;
    };
    const handleDragEnd = function (e) {
      if (!isMouseDown) return false;
      isMouseDown = false;
      const eventX = e.clientX || e.changedTouches[0].clientX;
      if (eventX === originX) return false;
      Captcha.removeClass(sliderContainer, 'captcha-slide-container-active');
      const verifyVal = that.verify();
      const { spliced, verified } = verifyVal;
      if (spliced) {
        if (verified) {
          Captcha.addClass(that.sliderContainer, 'captcha-slide-container-success');
          typeof that.onSuccess === 'function' && that.onSuccess();
        } else {
          Captcha.addClass(that.sliderContainer, 'captcha-slide-container-fail');
          that.text.innerHTML = '再试一次';
          that.reset();
        }
      } else {
        Captcha.addClass(that.sliderContainer, 'captcha-slide-container-fail');
        typeof that.onFail === 'function' && that.onFail();
        setTimeout(() => {
          that.reset();
        }, 1000);
      }
      return true;
    };
    this.slider.addEventListener('mousedown', handleDragStart);
    this.slider.addEventListener('touchstart', handleDragStart);
    document.addEventListener('mousemove', handleDragMove);
    document.addEventListener('touchmove', handleDragMove);
    document.addEventListener('mouseup', handleDragEnd);
    document.addEventListener('touchend', handleDragEnd);
  }

  reset() {
    this.sliderContainer.className = 'captcha-slide-container';
    this.slider.style.left = 0;
    this.block.style.left = 0;
    this.sliderMask.style.width = 0;
    this.clear();
    this.img.src = Captcha.getRandomImg();
  }

  clear() {
    const { w, h } = this;
    this.canvasCtx.clearRect(0, 0, w, h);
    this.blockCtx.clearRect(0, 0, w, h);
    this.block.width = w;
  }

  verify() {
    const arr = this.trail; // 拖动时y轴的移动距离
    const average = arr.reduce(Captcha.sum) / arr.length;
    const deviations = arr.map(x => x - average);
    const stddev = Math.sqrt(deviations.map(Captcha.square).reduce(Captcha.sum) / arr.length);
    const left = parseInt(this.block.style.left, 0);
    return {
      spliced: Math.abs(left - this.x) < 10,
      verified: stddev !== 0, // 简单验证下拖动轨迹,为零时表示Y轴上下没有波动,可能非人为操作
    };
  }
}

export default Captcha;
<template>
<div class="captcha-container">
  <div ref="captcha"></div>
</div>
</template>

<script>
import Captcha from '../utils/Captcha';

export default {
  name: 'PackCaptcha',
  data() {
    return {
      jigsaw: null,
    };
  },
  mounted() {
    const that = this;
    this.jigsaw = new Captcha({
      el: this.$refs.captcha,
      onSuccess() {
        that.$emit('callback');
      },
      onFail() {
        console.log('登录成功');
      },
      onRefresh() {
        console.log('登录成功');
      },
    });
    this.jigsaw.init();
  },
};
</script>

<style lang="stylus">
  .captcha-container{
    padding 15PX
    box-sizing border-box
    overflow hidden
  }
.captcha-slide-container {
  position: relative;
  text-align: center;
  width: 310PX;
  height: 40PX;
  line-height: 40PX;
  background: #f7f9fa;
  color: #45494c;
  box-sizing border-box
  margin 15PX auto
}

.captcha-slide-container-active{
  .captcha-slider {
    height: 38PX;
    top: -1PX;
    border: 1PX solid $infoColor;
  }
  .captcha-slider-mask {
    height: 38PX;
    border-width: 1PX;
  }
}

.captcha-slide-container-success{
  .captcha-slider {
    height: 38PX;
    top: -1PX;
    border: 1PX solid $infoColor;
    background-color: $infoColor !important;
  }
  .captcha-slider-mask {
    height: 38PX;
    border: 1PX solid $infoColor;
    background-color: $infoColor;
  }
  .captcha-slider-icon {
    background-position: 0 0 !important;
  }
}

.captcha-slide-container-fail{
  .captcha-slider {
    height: 38PX;
    top: -1PX;
    border: 1PX solid $warningColor;
    background-color: $warningColor !important;
  }
  .captcha-slider-mask {
    height: 38PX;
    border: 1PX solid $warningColor;
    background-color: #fce1e1;
  }
  .captcha-slider-icon {
    top: 14PX;
    background-position: 0 -82PX !important;
  }
}

.captcha-slide-container-active .sliderText, .captcha-slide-container-success .sliderText, .captcha-slide-container-fail .sliderText {
  display: none;
}

.captcha-slider-mask {
  position: absolute;
  left: 0;
  top: 0;
  height: 40PX;
  border: 0 solid $infoColor;
  background: #D1E9FE;
}

.captcha-slider {
  position: absolute;
  top: 0;
  left: 0;
  width: 40PX;
  height: 40PX;
  background: #fff;
  box-shadow: 0 0 3PX rgba(0, 0, 0, 0.3);
  cursor: pointer;
  transition: background .2s linear;
}

.captcha-slider:hover {
  background: $infoColor;
}

.captcha-slider:hover .captcha-slider-icon {
  background-position: 0 -13PX;
}

.captcha-slider-icon {
  position: absolute;
  top: 15PX;
  left: 13PX;
  width: 14PX;
  height: 12PX;
  background: url("../assets/img/Home/spirit.png") 0 -26PX;
  background-size: 34PX 471PX;
}

.captcha-refresh-icon {
  position: absolute;
  right: 0;
  top: 0;
  width: 34PX;
  height: 34PX;
  cursor: pointer;
  background: url("../assets/img/Home/spirit.png") 0 -437PX;
  background-size: 34PX 471PX;
}
.captcha-block {
  position: absolute;
  left: 0;
  top: 0;
}
</style>


上一篇 下一篇

猜你喜欢

热点阅读