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>
网友评论