293 lines
12 KiB
JavaScript
293 lines
12 KiB
JavaScript
(function ($) {
|
||
'use strict';
|
||
|
||
var SliderCaptcha = function (element, options) {
|
||
this.$element = $(element);
|
||
this.options = $.extend({}, SliderCaptcha.DEFAULTS, options);
|
||
this.$element.css({ 'position': 'relative', 'width': this.options.width + 'px', 'margin': '0 auto' });
|
||
this.init();
|
||
};
|
||
|
||
SliderCaptcha.VERSION = '1.0';
|
||
SliderCaptcha.Author = 'argo@163.com';
|
||
SliderCaptcha.DEFAULTS = {
|
||
width: 280, // canvas宽度
|
||
height: 155, // canvas高度
|
||
PI: Math.PI,
|
||
sliderL: 42, // 滑块边长
|
||
sliderR: 9, // 滑块半径
|
||
offset: 5, // 容错偏差
|
||
loadingText: '正在加载中...',
|
||
failedText: '再试一次',
|
||
barText: '向右滑动填充拼图',
|
||
repeatIcon: 'fa fa-repeat',
|
||
maxLoadCount: 3,
|
||
localImages: function () {
|
||
return 'images/Pic' + Math.round(Math.random() * 4) + '.jpg';
|
||
}
|
||
};
|
||
|
||
function Plugin(option) {
|
||
return this.each(function () {
|
||
var $this = $(this);
|
||
var data = $this.data('lgb.SliderCaptcha');
|
||
var options = typeof option === 'object' && option;
|
||
|
||
if (!data && /init|reset|verify/.test(option)) return;
|
||
if (!data) $this.data('lgb.SliderCaptcha', data = new SliderCaptcha(this, options));
|
||
if (typeof option === 'string') data[option]();
|
||
});
|
||
}
|
||
|
||
$.fn.sliderCaptcha = Plugin;
|
||
$.fn.sliderCaptcha.Constructor = SliderCaptcha;
|
||
|
||
var _proto = SliderCaptcha.prototype;
|
||
_proto.init = function () {
|
||
this.initDOM();
|
||
this.initImg();
|
||
this.bindEvents();
|
||
};
|
||
|
||
_proto.initDOM = function () {
|
||
var createElement = function (tagName, className) {
|
||
var elment = document.createElement(tagName);
|
||
elment.className = className;
|
||
return elment;
|
||
};
|
||
|
||
var createCanvas = function (width, height) {
|
||
var canvas = document.createElement('canvas');
|
||
canvas.width = width;
|
||
canvas.height = height;
|
||
return canvas;
|
||
};
|
||
|
||
var canvas = createCanvas(this.options.width - 2, this.options.height) // 画布
|
||
var block = canvas.cloneNode(true) // 滑块
|
||
var sliderContainer = createElement('div', 'sliderContainer');
|
||
var refreshIcon = createElement('i', 'refreshIcon ' + this.options.repeatIcon);
|
||
var sliderMask = createElement('div', 'sliderMask');
|
||
var sliderbg = createElement('div', 'sliderbg');
|
||
var slider = createElement('div', 'slider');
|
||
var sliderIcon = createElement('i', 'fa fa-arrow-right sliderIcon');
|
||
var text = createElement('span', 'sliderText');
|
||
|
||
block.className = 'block'
|
||
text.innerHTML = this.options.barText;
|
||
|
||
var el = this.$element;
|
||
el.append($(canvas));
|
||
el.append($(refreshIcon));
|
||
el.append($(block));
|
||
slider.appendChild(sliderIcon);
|
||
sliderMask.appendChild(slider);
|
||
sliderContainer.appendChild(sliderbg);
|
||
sliderContainer.appendChild(sliderMask);
|
||
sliderContainer.appendChild(text);
|
||
el.append($(sliderContainer));
|
||
|
||
Object.assign(this, {
|
||
canvas,
|
||
block,
|
||
sliderContainer: $(sliderContainer),
|
||
refreshIcon,
|
||
slider,
|
||
sliderMask,
|
||
sliderIcon,
|
||
text: $(text),
|
||
canvasCtx: canvas.getContext('2d'),
|
||
blockCtx: block.getContext('2d')
|
||
})
|
||
};
|
||
|
||
_proto.initImg = function () {
|
||
var that = this;
|
||
var isIE = window.navigator.userAgent.indexOf('Trident') > -1;
|
||
var L = this.options.sliderL + this.options.sliderR * 2 + 3; // 滑块实际边长
|
||
var drawImg = function (ctx, operation) {
|
||
var l = that.options.sliderL;
|
||
var r = that.options.sliderR;
|
||
var PI = that.options.PI;
|
||
var x = that.x;
|
||
var y = that.y;
|
||
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 = isIE ? 'xor' : 'overlay'
|
||
}
|
||
|
||
var getRandomNumberByRange = function (start, end) {
|
||
return Math.round(Math.random() * (end - start) + start);
|
||
};
|
||
var img = new Image();
|
||
img.crossOrigin = "Anonymous";
|
||
var loadCount = 0;
|
||
img.onload = function () {
|
||
// 随机创建滑块的位置
|
||
that.x = getRandomNumberByRange(L + 10, that.options.width - (L + 10));
|
||
that.y = getRandomNumberByRange(10 + that.options.sliderR * 2, that.options.height - (L + 10));
|
||
drawImg(that.canvasCtx, 'fill');
|
||
drawImg(that.blockCtx, 'clip');
|
||
|
||
that.canvasCtx.drawImage(img, 0, 0, that.options.width - 2, that.options.height);
|
||
that.blockCtx.drawImage(img, 0, 0, that.options.width - 2, that.options.height);
|
||
var y = that.y - that.options.sliderR * 2 - 1;
|
||
var ImageData = that.blockCtx.getImageData(that.x - 3, y, L, L);
|
||
that.block.width = L;
|
||
that.blockCtx.putImageData(ImageData, 0, y);
|
||
that.text.text(that.text.attr('data-text'));
|
||
};
|
||
img.onerror = function () {
|
||
loadCount++;
|
||
if (window.location.protocol === 'file:') {
|
||
loadCount = that.options.maxLoadCount;
|
||
console.error("can't load pic resource file from File protocal. Please try http or https");
|
||
}
|
||
if (loadCount >= that.options.maxLoadCount) {
|
||
that.text.text('加载失败').addClass('text-danger');
|
||
return;
|
||
}
|
||
img.src = that.options.localImages();
|
||
};
|
||
img.setSrc = function () {
|
||
var src = '';
|
||
loadCount = 0;
|
||
that.text.removeClass('text-danger');
|
||
if ($.isFunction(that.options.setSrc)) src = that.options.setSrc();
|
||
if (!src || src === '') src = 'https://picsum.photos/' + that.options.width + '/' + that.options.height + '/?image=' + Math.round(Math.random() * 20);
|
||
if (isIE) { // IE浏览器无法通过img.crossOrigin跨域,使用ajax获取图片blob然后转为dataURL显示
|
||
var xhr = new XMLHttpRequest()
|
||
xhr.onloadend = function (e) {
|
||
var 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();
|
||
this.text.attr('data-text', this.options.barText);
|
||
this.text.text(this.options.loadingText);
|
||
this.img = img
|
||
};
|
||
|
||
_proto.clean = function () {
|
||
this.canvasCtx.clearRect(0, 0, this.options.width, this.options.height);
|
||
this.blockCtx.clearRect(0, 0, this.options.width, this.options.height);
|
||
this.block.width = this.options.width;
|
||
};
|
||
|
||
_proto.bindEvents = function () {
|
||
var that = this;
|
||
this.$element.on('selectstart', function () {
|
||
return false;
|
||
});
|
||
|
||
$(this.refreshIcon).on('click', function () {
|
||
that.text.text(that.options.barText);
|
||
that.reset();
|
||
if ($.isFunction(that.options.onRefresh)) that.options.onRefresh.call(that.$element);
|
||
});
|
||
|
||
var originX, originY, trail = [],
|
||
isMouseDown = false
|
||
|
||
var handleDragStart = function (e) {
|
||
if (that.text.hasClass('text-danger')) return;
|
||
originX = e.clientX || e.touches[0].clientX;
|
||
originY = e.clientY || e.touches[0].clientY;
|
||
isMouseDown = true;
|
||
};
|
||
|
||
var handleDragMove = function (e) {
|
||
if (!isMouseDown) return false;
|
||
var eventX = e.clientX || e.touches[0].clientX;
|
||
var eventY = e.clientY || e.touches[0].clientY;
|
||
var moveX = eventX - originX;
|
||
var moveY = eventY - originY;
|
||
if (moveX < 0 || moveX + 40 > that.options.width) return false;
|
||
that.slider.style.left = (moveX - 1) + 'px';
|
||
var blockLeft = (that.options.width - 40 - 20) / (that.options.width - 40) * moveX;
|
||
that.block.style.left = blockLeft + 'px';
|
||
|
||
that.sliderContainer.addClass('sliderContainer_active');
|
||
that.sliderMask.style.width = (moveX + 4) + 'px';
|
||
trail.push(moveY);
|
||
};
|
||
|
||
var handleDragEnd = function (e) {
|
||
if (!isMouseDown) return false
|
||
isMouseDown = false
|
||
var eventX = e.clientX || e.changedTouches[0].clientX
|
||
if (eventX == originX) return false
|
||
that.sliderContainer.removeClass('sliderContainer_active');
|
||
that.trail = trail
|
||
var {
|
||
spliced,
|
||
verified
|
||
} = that.verify()
|
||
if (spliced && verified) {
|
||
that.sliderContainer.addClass('sliderContainer_success');
|
||
if ($.isFunction(that.options.onSuccess)) that.options.onSuccess.call(that.$element);
|
||
} else {
|
||
that.sliderContainer.addClass('sliderContainer_fail');
|
||
if ($.isFunction(that.options.onFail)) that.options.onFail.call(that.$element);
|
||
setTimeout(() => {
|
||
that.text.text(that.options.failedText);
|
||
that.reset();
|
||
}, 1000);
|
||
}
|
||
};
|
||
|
||
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);
|
||
|
||
document.addEventListener('mousedown', function () { return false; });
|
||
document.addEventListener('touchstart', function () { return false; });
|
||
};
|
||
|
||
_proto.verify = function () {
|
||
var sum = function (x, y) { return x + y; };
|
||
var square = function (x) { return x * x; };
|
||
var arr = this.trail // 拖动时y轴的移动距离
|
||
var average = arr.reduce(sum) / arr.length;
|
||
var deviations = arr.map(function (x) { return x - average; });
|
||
var stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length);
|
||
var left = parseInt(this.block.style.left);
|
||
return {
|
||
spliced: Math.abs(left - this.x) < this.options.offset,
|
||
verified: stddev !== 0, // 简单验证下拖动轨迹,为零时表示Y轴上下没有波动,可能非人为操作
|
||
}
|
||
};
|
||
|
||
_proto.reset = function () {
|
||
this.sliderContainer.removeClass('sliderContainer_fail sliderContainer_success');
|
||
this.slider.style.left = 0
|
||
this.block.style.left = 0
|
||
this.sliderMask.style.width = 0
|
||
this.clean()
|
||
this.text.attr('data-text', this.text.text());
|
||
this.text.text(this.options.loadingText);
|
||
this.img.setSrc();
|
||
};
|
||
})(jQuery); |