longbow.slidercaptcha.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. (function ($) {
  2. 'use strict';
  3. var SliderCaptcha = function (element, options) {
  4. this.$element = $(element);
  5. this.options = $.extend({}, SliderCaptcha.DEFAULTS, options);
  6. this.$element.css({ 'position': 'relative', 'width': this.options.width + 'px', 'margin': '0 auto' });
  7. this.init();
  8. };
  9. SliderCaptcha.VERSION = '1.0';
  10. SliderCaptcha.Author = 'argo@163.com';
  11. SliderCaptcha.DEFAULTS = {
  12. width: 280, // canvas宽度
  13. height: 155, // canvas高度
  14. PI: Math.PI,
  15. sliderL: 42, // 滑块边长
  16. sliderR: 9, // 滑块半径
  17. offset: 5, // 容错偏差
  18. loadingText: '正在加载中...',
  19. failedText: '再试一次',
  20. barText: '向右滑动填充拼图',
  21. repeatIcon: 'fa fa-repeat',
  22. maxLoadCount: 3,
  23. localImages: function () {
  24. return 'images/Pic' + Math.round(Math.random() * 4) + '.jpg';
  25. },
  26. verify: function (arr, url) {
  27. var that = this;
  28. var ret = false;
  29. $.ajax({
  30. url: url,
  31. data: JSON.stringify(arr),
  32. async: false,
  33. cache: false,
  34. type: 'POST',
  35. contentType: 'application/json',
  36. dataType: 'json',
  37. success: function (result) {
  38. // console.log(result)
  39. ret = result;
  40. if (result.resultCode == "0000") {
  41. ret = true;
  42. } else {
  43. ret = false;
  44. }
  45. that.verifySuccess && that.verifySuccess(result);
  46. }
  47. });
  48. return ret;
  49. },
  50. remoteUrl: null,
  51. verifySuccess: null
  52. };
  53. function Plugin(option) {
  54. return this.each(function () {
  55. var $this = $(this);
  56. var data = $this.data('lgb.SliderCaptcha');
  57. var options = typeof option === 'object' && option;
  58. if (data && !/reset/.test(option)) return;
  59. if (!data) $this.data('lgb.SliderCaptcha', data = new SliderCaptcha(this, options));
  60. if (typeof option === 'string') data[option]();
  61. });
  62. }
  63. $.fn.sliderCaptcha = Plugin;
  64. $.fn.sliderCaptcha.Constructor = SliderCaptcha;
  65. var _proto = SliderCaptcha.prototype;
  66. _proto.init = function () {
  67. this.initDOM();
  68. this.initImg();
  69. this.bindEvents();
  70. };
  71. _proto.initDOM = function () {
  72. var createElement = function (tagName, className) {
  73. var elment = document.createElement(tagName);
  74. elment.className = className;
  75. return elment;
  76. };
  77. var createCanvas = function (width, height) {
  78. var canvas = document.createElement('canvas');
  79. canvas.width = width;
  80. canvas.height = height;
  81. return canvas;
  82. };
  83. var canvas = createCanvas(this.options.width - 2, this.options.height); // 画布
  84. var block = canvas.cloneNode(true); // 滑块
  85. var sliderContainer = createElement('div', 'sliderContainer');
  86. var refreshIcon = createElement('i', 'refreshIcon ' + this.options.repeatIcon);
  87. var sliderMask = createElement('div', 'sliderMask');
  88. var sliderbg = createElement('div', 'sliderbg');
  89. var slider = createElement('div', 'slider');
  90. var sliderIcon = createElement('i', 'fa fa-arrow-right sliderIcon');
  91. var text = createElement('span', 'sliderText');
  92. block.className = 'block';
  93. text.innerHTML = this.options.barText;
  94. var el = this.$element;
  95. el.append($(canvas));
  96. el.append($(refreshIcon));
  97. el.append($(block));
  98. slider.appendChild(sliderIcon);
  99. sliderMask.appendChild(slider);
  100. sliderContainer.appendChild(sliderbg);
  101. sliderContainer.appendChild(sliderMask);
  102. sliderContainer.appendChild(text);
  103. el.append($(sliderContainer));
  104. var _canvas = {
  105. canvas: canvas,
  106. block: block,
  107. sliderContainer: $(sliderContainer),
  108. refreshIcon: refreshIcon,
  109. slider: slider,
  110. sliderMask: sliderMask,
  111. sliderIcon: sliderIcon,
  112. text: $(text),
  113. canvasCtx: canvas.getContext('2d'),
  114. blockCtx: block.getContext('2d')
  115. };
  116. if ($.isFunction(Object.assign)) {
  117. Object.assign(this, _canvas);
  118. }
  119. else {
  120. $.extend(this, _canvas);
  121. }
  122. };
  123. _proto.initImg = function () {
  124. var that = this;
  125. var isIE = window.navigator.userAgent.indexOf('Trident') > -1;
  126. var L = this.options.sliderL + this.options.sliderR * 2 + 3; // 滑块实际边长
  127. var drawImg = function (ctx, operation) {
  128. var l = that.options.sliderL;
  129. var r = that.options.sliderR;
  130. var PI = that.options.PI;
  131. var x = that.x;
  132. var y = that.y;
  133. ctx.beginPath();
  134. ctx.moveTo(x, y);
  135. ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI);
  136. ctx.lineTo(x + l, y);
  137. ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI);
  138. ctx.lineTo(x + l, y + l);
  139. ctx.lineTo(x, y + l);
  140. ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true);
  141. ctx.lineTo(x, y);
  142. ctx.lineWidth = 2;
  143. ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
  144. ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
  145. ctx.stroke();
  146. ctx[operation]();
  147. ctx.globalCompositeOperation = isIE ? 'xor' : 'destination-over';
  148. };
  149. var getRandomNumberByRange = function (start, end) {
  150. return Math.round(Math.random() * (end - start) + start);
  151. };
  152. var img = new Image();
  153. img.crossOrigin = "Anonymous";
  154. var loadCount = 0;
  155. img.onload = function () {
  156. // 随机创建滑块的位置
  157. that.x = getRandomNumberByRange(L + 10, that.options.width - (L + 10));
  158. that.y = getRandomNumberByRange(10 + that.options.sliderR * 2, that.options.height - (L + 10));
  159. drawImg(that.canvasCtx, 'fill');
  160. drawImg(that.blockCtx, 'clip');
  161. that.canvasCtx.drawImage(img, 0, 0, that.options.width - 2, that.options.height);
  162. that.blockCtx.drawImage(img, 0, 0, that.options.width - 2, that.options.height);
  163. var y = that.y - that.options.sliderR * 2 - 1;
  164. var ImageData = that.blockCtx.getImageData(that.x - 3, y, L, L);
  165. that.block.width = L;
  166. that.blockCtx.putImageData(ImageData, 0, y + 1);
  167. that.text.text(that.text.attr('data-text'));
  168. };
  169. img.onerror = function () {
  170. loadCount++;
  171. if (window.location.protocol === 'file:') {
  172. loadCount = that.options.maxLoadCount;
  173. console.error("can't load pic resource file from File protocal. Please try http or https");
  174. }
  175. if (loadCount >= that.options.maxLoadCount) {
  176. that.text.text('加载失败').addClass('text-danger');
  177. return;
  178. }
  179. img.src = that.options.localImages();
  180. };
  181. img.setSrc = function () {
  182. var src = '';
  183. loadCount = 0;
  184. that.text.removeClass('text-danger');
  185. if ($.isFunction(that.options.setSrc)) src = that.options.setSrc();
  186. if (!src || src === '') src = 'https://picsum.photos/' + that.options.width + '/' + that.options.height + '/?image=' + Math.round(Math.random() * 20);
  187. if (isIE) { // IE浏览器无法通过img.crossOrigin跨域,使用ajax获取图片blob然后转为dataURL显示
  188. var xhr = new XMLHttpRequest();
  189. xhr.onloadend = function (e) {
  190. var file = new FileReader(); // FileReader仅支持IE10+
  191. file.readAsDataURL(e.target.response);
  192. file.onloadend = function (e) {
  193. img.src = e.target.result;
  194. };
  195. };
  196. xhr.open('GET', src);
  197. xhr.responseType = 'blob';
  198. xhr.send();
  199. } else img.src = src;
  200. };
  201. img.setSrc();
  202. this.text.attr('data-text', this.options.barText);
  203. this.text.text(this.options.loadingText);
  204. this.img = img;
  205. };
  206. _proto.clean = function () {
  207. this.canvasCtx.clearRect(0, 0, this.options.width, this.options.height);
  208. this.blockCtx.clearRect(0, 0, this.options.width, this.options.height);
  209. this.block.width = this.options.width;
  210. };
  211. _proto.bindEvents = function () {
  212. var that = this;
  213. this.$element.on('selectstart', function () {
  214. return false;
  215. });
  216. $(this.refreshIcon).on('click', function () {
  217. that.text.text(that.options.barText);
  218. that.reset();
  219. if ($.isFunction(that.options.onRefresh)) that.options.onRefresh.call(that.$element);
  220. });
  221. var originX, originY, trail = [],
  222. isMouseDown = false;
  223. var handleDragStart = function (e) {
  224. if (that.text.hasClass('text-danger')) return;
  225. originX = e.clientX || e.touches[0].clientX;
  226. originY = e.clientY || e.touches[0].clientY;
  227. isMouseDown = true;
  228. };
  229. var handleDragMove = function (e) {
  230. if (!isMouseDown) return false;
  231. var eventX = e.clientX || e.touches[0].clientX;
  232. var eventY = e.clientY || e.touches[0].clientY;
  233. var moveX = eventX - originX;
  234. var moveY = eventY - originY;
  235. if (moveX < 0 || moveX + 40 > that.options.width) return false;
  236. that.slider.style.left = (moveX - 1) + 'px';
  237. var blockLeft = (that.options.width - 40 - 20) / (that.options.width - 40) * moveX;
  238. that.block.style.left = blockLeft + 'px';
  239. that.sliderContainer.addClass('sliderContainer_active');
  240. that.sliderMask.style.width = (moveX + 4) + 'px';
  241. trail.push(Math.round(moveY));
  242. };
  243. var handleDragEnd = function (e) {
  244. if (!isMouseDown) return false;
  245. isMouseDown = false;
  246. var eventX = e.clientX || e.changedTouches[0].clientX;
  247. if (eventX === originX) return false;
  248. that.sliderContainer.removeClass('sliderContainer_active');
  249. that.trail = trail;
  250. var data = that.verify();
  251. if (data.spliced && data.verified) {
  252. that.sliderContainer.addClass('sliderContainer_success');
  253. if ($.isFunction(that.options.onSuccess)) that.options.onSuccess.call(that.$element);
  254. } else {
  255. that.sliderContainer.addClass('sliderContainer_fail');
  256. if ($.isFunction(that.options.onFail)) that.options.onFail.call(that.$element);
  257. setTimeout(function () {
  258. that.text.text(that.options.failedText);
  259. that.reset();
  260. }, 1000);
  261. }
  262. };
  263. this.slider.addEventListener('mousedown', handleDragStart);
  264. this.slider.addEventListener('touchstart', handleDragStart);
  265. document.addEventListener('mousemove', handleDragMove);
  266. document.addEventListener('touchmove', handleDragMove);
  267. document.addEventListener('mouseup', handleDragEnd);
  268. document.addEventListener('touchend', handleDragEnd);
  269. document.addEventListener('mousedown', function () { return false; });
  270. document.addEventListener('touchstart', function () { return false; });
  271. document.addEventListener('swipe', function () { return false; });
  272. };
  273. _proto.verify = function () {
  274. var arr = this.trail; // 拖动时y轴的移动距离
  275. var left = parseInt(this.block.style.left);
  276. var verified = false;
  277. if (this.options.remoteUrl !== null) {
  278. verified = this.options.verify(arr, this.options.remoteUrl);
  279. var sum = function (x, y) { return x + y; };
  280. var square = function (x) { return x * x; };
  281. var average = arr.reduce(sum) / arr.length;
  282. var deviations = arr.map(function (x) { return x - average; });
  283. var stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length);
  284. // console.log(stddev);
  285. /*verified = stddev !== 0;*/
  286. }
  287. else {
  288. var sum = function (x, y) { return x + y; };
  289. var square = function (x) { return x * x; };
  290. var average = arr.reduce(sum) / arr.length;
  291. var deviations = arr.map(function (x) { return x - average; });
  292. var stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length);
  293. verified = stddev !== 0;
  294. }
  295. return {
  296. spliced: Math.abs(left - this.x) < this.options.offset,
  297. verified: verified
  298. };
  299. };
  300. _proto.reset = function () {
  301. this.sliderContainer.removeClass('sliderContainer_fail sliderContainer_success');
  302. this.slider.style.left = 0;
  303. this.block.style.left = 0;
  304. this.sliderMask.style.width = 0;
  305. this.clean();
  306. this.text.attr('data-text', this.text.text());
  307. this.text.text(this.options.loadingText);
  308. this.img.setSrc();
  309. };
  310. })(jQuery);