You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
477 lines
17 KiB
477 lines
17 KiB
<template>
|
|
<div class="slide-verify" :style="{width: w + 'px'}" id="slideVerify" onselectstart="return false;">
|
|
<!-- 图片加载遮蔽罩 -->
|
|
<div :class="{'slider-verify-loading': loadBlock}"></div>
|
|
<canvas :width="w" :height="h" ref="canvas" ></canvas>
|
|
<div v-if="show" @click="refresh" class="slide-verify-refresh-icon"></div>
|
|
<canvas :width="w" :height="h" ref="block" class="slide-verify-block"></canvas>
|
|
<!-- container -->
|
|
<div class="slide-verify-slider" :class="{'container-active': containerActive, 'container-success': containerSuccess, 'container-fail': containerFail}">
|
|
<div class="slide-verify-slider-mask" :style="{width: sliderMaskWidth}">
|
|
<!-- slider -->
|
|
<div @mousedown="sliderDown"
|
|
@touchstart="touchStartEvent"
|
|
@touchmove="touchMoveEvent"
|
|
@touchend="touchEndEvent"
|
|
class="slide-verify-slider-mask-item"
|
|
:style="{left: sliderLeft}">
|
|
<div class="slide-verify-slider-mask-item-icon"></div>
|
|
</div>
|
|
</div>
|
|
<span class="slide-verify-slider-text">{{sliderText}}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
const PI = Math.PI;
|
|
|
|
function sum(x, y) {
|
|
return x + y
|
|
}
|
|
|
|
function square(x) {
|
|
return x * x
|
|
}
|
|
export default {
|
|
name: 'SlideVerify',
|
|
props: {
|
|
// block length
|
|
l: {
|
|
type: Number,
|
|
default: 42,
|
|
},
|
|
// block radius
|
|
r: {
|
|
type: Number,
|
|
default: 10,
|
|
},
|
|
// canvas width
|
|
w: {
|
|
type: Number,
|
|
default: 310,
|
|
},
|
|
// canvas height
|
|
h: {
|
|
type: Number,
|
|
default: 155,
|
|
},
|
|
sliderText: {
|
|
type: String,
|
|
default: '向右滑动滑块填充拼图',
|
|
},
|
|
accuracy: {
|
|
type: Number,
|
|
default: 5, // 若为 -1 则不进行机器判断
|
|
},
|
|
show: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
imgs: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
containerActive: false, // container active class
|
|
containerSuccess: false, // container success class
|
|
containerFail: false, // container fail class
|
|
canvasCtx: null,
|
|
blockCtx: null,
|
|
block: null,
|
|
block_x: undefined, // container random position
|
|
block_y: undefined,
|
|
L: this.l + this.r * 2 + 3, // block real lenght
|
|
img: undefined,
|
|
originX: undefined,
|
|
originY: undefined,
|
|
isMouseDown: false,
|
|
trail: [],
|
|
sliderLeft: 0, // block right offset
|
|
sliderMaskWidth: 0, // mask width,
|
|
success: false, // Bug Fixes 修复了验证成功后还能滑动
|
|
loadBlock: true, // Features 图片加载提示,防止图片没加载完就开始验证
|
|
timestamp: null,
|
|
}
|
|
},
|
|
mounted() {
|
|
this.init()
|
|
},
|
|
methods: {
|
|
init() {
|
|
this.initDom()
|
|
this.initImg()
|
|
this.bindEvents()
|
|
},
|
|
initDom() {
|
|
this.block = this.$refs.block;
|
|
this.canvasCtx = this.$refs.canvas.getContext('2d')
|
|
this.blockCtx = this.block.getContext('2d')
|
|
},
|
|
initImg() {
|
|
const img = this.createImg(() => {
|
|
// 图片加载完关闭遮蔽罩
|
|
this.loadBlock = false;
|
|
this.drawBlock()
|
|
this.canvasCtx.drawImage(img, 0, 0, this.w, this.h)
|
|
this.blockCtx.drawImage(img, 0, 0, this.w, this.h)
|
|
let {
|
|
block_x: x,
|
|
block_y: y,
|
|
r,
|
|
L
|
|
} = this
|
|
let _y = y - r * 2 - 1
|
|
let ImageData = this.blockCtx.getImageData(x, _y, L, L);
|
|
this.block.width = L;
|
|
this.blockCtx.putImageData(ImageData, 0, _y)
|
|
});
|
|
this.img = img;
|
|
},
|
|
drawBlock() {
|
|
this.block_x = this.getRandomNumberByRange(this.L + 10, this.w - (this.L + 10))
|
|
this.block_y = this.getRandomNumberByRange(10 + this.r * 2, this.h - (this.L + 10))
|
|
this.draw(this.canvasCtx, this.block_x, this.block_y, 'fill')
|
|
this.draw(this.blockCtx, this.block_x, this.block_y, 'clip')
|
|
},
|
|
draw(ctx, x, y, operation) {
|
|
let {
|
|
l,
|
|
r
|
|
} = this;
|
|
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]()
|
|
// Bug Fixes 修复了火狐和ie显示问题
|
|
ctx.globalCompositeOperation = "destination-over"
|
|
},
|
|
createImg(onload) {
|
|
const img = document.createElement('img');
|
|
img.crossOrigin = "Anonymous";
|
|
img.onload = onload;
|
|
img.onerror = () => {
|
|
img.src = this.getRandomImg()
|
|
}
|
|
img.src = this.getRandomImg()
|
|
return img;
|
|
},
|
|
// 随机生成img src
|
|
getRandomImg() {
|
|
// return require('../assets/img.jpg')
|
|
const len = this.imgs.length;
|
|
return len > 0 ?
|
|
this.imgs[this.getRandomNumberByRange(0, len)] :
|
|
'https://picsum.photos/300/150/?image=' + this.getRandomNumberByRange(0, 1084);
|
|
},
|
|
getRandomNumberByRange(start, end) {
|
|
return Math.round(Math.random() * (end - start) + start)
|
|
},
|
|
refresh() {
|
|
this.reset()
|
|
this.$emit('refresh')
|
|
},
|
|
sliderDown(event) {
|
|
if (this.success) return;
|
|
this.originX = event.clientX;
|
|
this.originY = event.clientY;
|
|
this.isMouseDown = true;
|
|
this.timestamp = + new Date();
|
|
},
|
|
touchStartEvent(e) {
|
|
if (this.success) return;
|
|
this.originX = e.changedTouches[0].pageX;
|
|
this.originY = e.changedTouches[0].pageY;
|
|
this.isMouseDown = true;
|
|
this.timestamp = + new Date();
|
|
},
|
|
bindEvents() {
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!this.isMouseDown) return false;
|
|
const moveX = e.clientX - this.originX;
|
|
const moveY = e.clientY - this.originY;
|
|
if (moveX < 0 || moveX + 38 >= this.w) return false;
|
|
this.sliderLeft = moveX + 'px';
|
|
let blockLeft = (this.w - 40 - 20) / (this.w - 40) * moveX;
|
|
this.block.style.left = blockLeft + 'px';
|
|
|
|
this.containerActive = true; // add active
|
|
this.sliderMaskWidth = moveX + 'px';
|
|
this.trail.push(moveY);
|
|
});
|
|
document.addEventListener('mouseup', (e) => {
|
|
if (!this.isMouseDown) return false
|
|
this.isMouseDown = false
|
|
if (e.clientX === this.originX) return false;
|
|
this.containerActive = false; // remove active
|
|
this.timestamp = + new Date() - this.timestamp;
|
|
|
|
const {
|
|
spliced,
|
|
TuringTest
|
|
} = this.verify();
|
|
if (spliced) {
|
|
if(this.accuracy === -1) {
|
|
this.containerSuccess = true;
|
|
this.success = true;
|
|
this.$emit('success', this.timestamp);
|
|
return;
|
|
}
|
|
if (TuringTest) {
|
|
// succ
|
|
this.containerSuccess = true;
|
|
this.success = true;
|
|
this.$emit('success', this.timestamp)
|
|
} else {
|
|
this.containerFail = true;
|
|
this.$emit('again')
|
|
}
|
|
} else {
|
|
this.containerFail = true;
|
|
this.$emit('fail')
|
|
setTimeout(() => {
|
|
this.reset()
|
|
}, 1000)
|
|
}
|
|
})
|
|
},
|
|
touchMoveEvent(e) {
|
|
if (!this.isMouseDown) return false;
|
|
const moveX = e.changedTouches[0].pageX - this.originX;
|
|
const moveY = e.changedTouches[0].pageY - this.originY;
|
|
if (moveX < 0 || moveX + 38 >= this.w) return false;
|
|
this.sliderLeft = moveX + 'px';
|
|
let blockLeft = (this.w - 40 - 20) / (this.w - 40) * moveX;
|
|
this.block.style.left = blockLeft + 'px';
|
|
|
|
this.containerActive = true;
|
|
this.sliderMaskWidth = moveX + 'px';
|
|
this.trail.push(moveY);
|
|
},
|
|
touchEndEvent(e) {
|
|
if (!this.isMouseDown) return false
|
|
this.isMouseDown = false
|
|
if (e.changedTouches[0].pageX === this.originX) return false;
|
|
this.containerActive = false;
|
|
this.timestamp = + new Date() - this.timestamp;
|
|
|
|
const {
|
|
spliced,
|
|
TuringTest
|
|
} = this.verify();
|
|
if (spliced) {
|
|
if(this.accuracy === -1) {
|
|
this.containerSuccess = true;
|
|
this.success = true;
|
|
this.$emit('success', this.timestamp);
|
|
return;
|
|
}
|
|
if (TuringTest) {
|
|
// succ
|
|
this.containerSuccess = true;
|
|
this.success = true;
|
|
this.$emit('success', this.timestamp)
|
|
} else {
|
|
this.containerFail = true;
|
|
this.$emit('again')
|
|
}
|
|
} else {
|
|
this.containerFail = true;
|
|
this.$emit('fail')
|
|
setTimeout(() => {
|
|
this.reset()
|
|
}, 1000)
|
|
}
|
|
},
|
|
verify() {
|
|
const arr = this.trail // drag y move distance
|
|
const average = arr.reduce(sum) / arr.length // average
|
|
const deviations = arr.map(x => x - average) // deviation array
|
|
const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length) // standard deviation
|
|
const left = parseInt(this.block.style.left)
|
|
const accuracy = this.accuracy <= 1 ? 1 : this.accuracy > 10 ? 10 : this.accuracy;
|
|
return {
|
|
spliced: Math.abs(left - this.block_x) <= accuracy,
|
|
TuringTest: average !== stddev, // equal => not person operate
|
|
}
|
|
},
|
|
reset() {
|
|
this.success = false;
|
|
this.containerActive = false;
|
|
this.containerSuccess = false;
|
|
this.containerFail = false;
|
|
this.sliderLeft = 0;
|
|
this.block.style.left = 0;
|
|
this.sliderMaskWidth = 0;
|
|
// canvas
|
|
let {
|
|
w,
|
|
h
|
|
} = this;
|
|
this.canvasCtx.clearRect(0, 0, w, h)
|
|
this.blockCtx.clearRect(0, 0, w, h)
|
|
this.block.width = w
|
|
|
|
// generate img
|
|
this.img.src = this.getRandomImg();
|
|
this.$emit('fulfilled')
|
|
},
|
|
}
|
|
}
|
|
</script>
|
|
<style scoped>
|
|
.slide-verify {
|
|
position: relative;
|
|
}
|
|
|
|
/* 图片加载样式 */
|
|
.slider-verify-loading{
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
z-index: 999;
|
|
animation: loading 1.5s infinite;
|
|
}
|
|
|
|
@keyframes loading {
|
|
0%{
|
|
opacity: .7;
|
|
}
|
|
100% {
|
|
opacity: 9;
|
|
}
|
|
}
|
|
|
|
.slide-verify-block {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0
|
|
}
|
|
|
|
.slide-verify-refresh-icon {
|
|
position: absolute;
|
|
right: 0;
|
|
top: 0;
|
|
width: 34px;
|
|
height: 34px;
|
|
cursor: pointer;
|
|
background: url("../../assets/images/login/icon_light.png") 0 -437px;
|
|
background-size: 34px 471px
|
|
}
|
|
|
|
.slide-verify-slider {
|
|
position: relative;
|
|
text-align: center;
|
|
width: 100%;
|
|
height: 40px;
|
|
line-height: 40px;
|
|
margin-top: 15px;
|
|
background: #f7f9fa;
|
|
color: #45494c;
|
|
border: 1px solid #e4e7eb
|
|
}
|
|
|
|
.slide-verify-slider-mask {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
height: 40px;
|
|
border: 0 solid #1991FA;
|
|
background: #D1E9FE
|
|
}
|
|
|
|
.slide-verify-slider-mask-item {
|
|
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
|
|
}
|
|
|
|
.slide-verify-slider-mask-item:hover {
|
|
background: #1991FA
|
|
}
|
|
|
|
.slide-verify-slider-mask-item:hover .slide-verify-slider-mask-item-icon {
|
|
background-position: 0 -13px
|
|
}
|
|
|
|
.slide-verify-slider-mask-item-icon {
|
|
position: absolute;
|
|
top: 15px;
|
|
left: 13px;
|
|
width: 14px;
|
|
height: 12px;
|
|
background: url("../../assets/images/login/icon_light.png") 0 -26px;
|
|
background-size: 34px 471px
|
|
}
|
|
.container-active .slide-verify-slider-mask-item {
|
|
height: 38px;
|
|
top: -1px;
|
|
border: 1px solid #1991FA;
|
|
}
|
|
|
|
.container-active .slide-verify-slider-mask {
|
|
height: 38px;
|
|
border-width: 1px;
|
|
}
|
|
|
|
.container-success .slide-verify-slider-mask-item {
|
|
height: 38px;
|
|
top: -1px;
|
|
border: 1px solid #52CCBA;
|
|
background-color: #52CCBA !important;
|
|
}
|
|
|
|
.container-success .slide-verify-slider-mask {
|
|
height: 38px;
|
|
border: 1px solid #52CCBA;
|
|
background-color: #D2F4EF;
|
|
}
|
|
|
|
.container-success .slide-verify-slider-mask-item-icon {
|
|
background-position: 0 0 !important;
|
|
}
|
|
|
|
.container-fail .slide-verify-slider-mask-item {
|
|
height: 38px;
|
|
top: -1px;
|
|
border: 1px solid #f57a7a;
|
|
background-color: #f57a7a !important;
|
|
}
|
|
|
|
.container-fail .slide-verify-slider-mask {
|
|
height: 38px;
|
|
border: 1px solid #f57a7a;
|
|
background-color: #fce1e1;
|
|
}
|
|
|
|
.container-fail .slide-verify-slider-mask-item-icon {
|
|
top: 14px;
|
|
background-position: 0 -82px !important;
|
|
}
|
|
|
|
.container-active .slide-verify-slider-text,
|
|
.container-success .slide-verify-slider-text,
|
|
.container-fail .slide-verify-slider-text {
|
|
display: none;
|
|
}
|
|
</style>
|