|
|
<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>
|