/** * AKSRT Slider CAPTCHA * * Canvas-based slider puzzle CAPTCHA verification. * No jQuery dependency — pure vanilla JS. * * Usage: * var captcha = new AksrtSliderCaptcha(container, { * onSuccess: function(token) { ... }, * onFail: function() { ... }, * setSrc: function(callback) { callback(imageUrl); } * }); * captcha.reset(); * * @package AKSRT */ (function () { 'use strict'; var DEFAULTS = { width: 280, height: 170, sliderL: 42, sliderR: 9, offset: 8, barText: '向右拖动滑块完成验证', successText: '验证成功', failText: '验证失败,请重试', loadingText: '加载中...', maxLoadCount: 3, onSuccess: function () {}, onFail: function () {}, setSrc: function (callback) { callback(''); }, }; function AksrtSliderCaptcha(element, options) { this.el = typeof element === 'string' ? document.querySelector(element) : element; if (!this.el) return; this.options = {}; for (var k in DEFAULTS) this.options[k] = DEFAULTS[k]; if (options) { for (var k in options) this.options[k] = options[k]; } this.loadCount = 0; this.init(); } AksrtSliderCaptcha.prototype.init = function () { this.el.style.position = 'relative'; this.el.style.width = this.options.width + 'px'; this.el.style.margin = '0 auto'; this.el.innerHTML = ''; this.initDOM(); this.initImg(); }; AksrtSliderCaptcha.prototype.initDOM = function () { var o = this.options; // Canvas container this.canvasWrap = document.createElement('div'); this.canvasWrap.className = 'aksrt-captcha__canvas-wrap'; this.canvasWrap.style.cssText = 'position:relative;overflow:hidden;border-radius:8px;background:#f0f0f0;'; this.canvas = document.createElement('canvas'); this.canvas.width = o.width; this.canvas.height = o.height; this.canvasCtx = this.canvas.getContext('2d'); this.block = document.createElement('canvas'); this.block.width = o.width; this.block.height = o.height; this.blockCtx = this.block.getContext('2d'); this.canvasWrap.appendChild(this.canvas); this.canvasWrap.appendChild(this.block); // Refresh button this.refreshBtn = document.createElement('div'); this.refreshBtn.className = 'aksrt-captcha__refresh'; this.refreshBtn.innerHTML = '↻'; this.refreshBtn.style.cssText = 'position:absolute;top:6px;right:6px;cursor:pointer;font-size:18px;color:#fff;background:rgba(0,0,0,0.3);border-radius:50%;width:28px;height:28px;display:flex;align-items:center;justify-content:center;z-index:5;'; this.canvasWrap.appendChild(this.refreshBtn); // Slider bar this.sliderBar = document.createElement('div'); this.sliderBar.className = 'aksrt-captcha__bar'; this.sliderBar.style.cssText = 'position:relative;height:40px;border-radius:20px;background:#e8e8e8;margin-top:10px;user-select:none;'; this.sliderMask = document.createElement('div'); this.sliderMask.className = 'aksrt-captcha__mask'; this.sliderMask.style.cssText = 'position:absolute;top:0;left:0;height:100%;border-radius:20px;background:linear-gradient(90deg,#1890ff,#36cfc9);width:0;transition:background 0.3s;'; this.slider = document.createElement('div'); this.slider.className = 'aksrt-captcha__slider'; this.slider.style.cssText = 'position:absolute;top:0;left:0;width:44px;height:40px;border-radius:20px;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,0.15);cursor:grab;display:flex;align-items:center;justify-content:center;font-size:18px;z-index:2;transition:box-shadow 0.2s;'; this.slider.innerHTML = '❯'; this.text = document.createElement('div'); this.text.className = 'aksrt-captcha__text'; this.text.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#999;font-size:14px;pointer-events:none;'; this.text.textContent = o.barText; this.sliderBar.appendChild(this.sliderMask); this.sliderBar.appendChild(this.slider); this.sliderBar.appendChild(this.text); this.el.appendChild(this.canvasWrap); this.el.appendChild(this.sliderBar); this.bindEvents(); }; AksrtSliderCaptcha.prototype.initImg = function () { var self = this; this.loadCount++; if (this.loadCount > this.options.maxLoadCount) { this.text.textContent = '加载失败'; return; } this.text.textContent = this.options.loadingText; this.slider.style.display = 'none'; this.options.setSrc(function (src) { if (!src) { self.text.textContent = '验证服务不可用'; return; } var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function () { self.bgImg = img; self.drawBg(); self.drawBlock(); self.slider.style.display = ''; self.text.textContent = self.options.barText; }; img.onerror = function () { self.initImg(); }; img.src = src; }); }; AksrtSliderCaptcha.prototype.drawBg = function () { var o = this.options; this.canvasCtx.drawImage(this.bgImg, 0, 0, o.width, o.height); this.canvasCtx.globalAlpha = 0.6; this.canvasCtx.fillStyle = '#000'; this.canvasCtx.fillRect(0, 0, o.width, o.height); this.canvasCtx.globalAlpha = 1; }; AksrtSliderCaptcha.prototype.drawBlock = function () { var o = this.options; var x = o.sliderL + Math.random() * (o.width - o.sliderL * 2 - 20); var y = 10 + Math.random() * (o.height - o.sliderL - 20); this.x = x; this.y = y; var ctx = this.blockCtx; ctx.clearRect(0, 0, o.width, o.height); // Draw the puzzle piece shape ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + o.sliderL / 2, y); ctx.arc(x + o.sliderL / 2, y - o.sliderR, o.sliderR, 0.5 * Math.PI, 2.5 * Math.PI, false); ctx.lineTo(x + o.sliderL, y); ctx.lineTo(x + o.sliderL, y + o.sliderL / 2); ctx.arc(x + o.sliderL + o.sliderR, y + o.sliderL / 2, o.sliderR, Math.PI, 0, false); ctx.lineTo(x + o.sliderL, y + o.sliderL); ctx.lineTo(x, y + o.sliderL); ctx.closePath(); ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.fill(); ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 2; ctx.stroke(); // Clip and draw image portion ctx.save(); ctx.clip(); ctx.drawImage(this.bgImg, 0, 0, o.width, o.height); ctx.restore(); }; AksrtSliderCaptcha.prototype.bindEvents = function () { var self = this; var o = this.options; var dragging = false; var startX = 0; var currentX = 0; var maxMove = o.width - o.sliderL - 10; var trail = []; // y-axis trail for behavior verification function onStart(e) { e.preventDefault(); dragging = true; trail = []; startX = (e.touches ? e.touches[0].clientX : e.clientX) || 0; self.slider.style.cursor = 'grabbing'; self.slider.style.boxShadow = '0 4px 12px rgba(0,0,0,0.25)'; } function onMove(e) { if (!dragging) return; e.preventDefault(); var clientX = e.touches ? e.touches[0].clientX : e.clientX; var clientY = e.touches ? e.touches[0].clientY : e.clientY; currentX = Math.max(0, Math.min(maxMove, clientX - startX)); trail.push(Math.round(clientY)); self.slider.style.left = currentX + 'px'; self.sliderMask.style.width = (currentX + 22) + 'px'; self.block.style.left = currentX + 'px'; } function onEnd() { if (!dragging) return; dragging = false; self.slider.style.cursor = 'grab'; self.slider.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)'; var positionOk = Math.abs(currentX - self.x) < o.offset; var behaviorOk = verifyTrail(trail); if (positionOk && behaviorOk) { self.onSuccess(trail, currentX); } else { self.onFail(); } } this.slider.addEventListener('mousedown', onStart); this.slider.addEventListener('touchstart', onStart, { passive: false }); document.addEventListener('mousemove', onMove); document.addEventListener('touchmove', onMove, { passive: false }); document.addEventListener('mouseup', onEnd); document.addEventListener('touchend', onEnd); this.refreshBtn.addEventListener('click', function () { self.reset(); }); }; // Trail behavior verification: stddev of y-axis must be > 0 (not a robot straight line) function verifyTrail(trail) { if (!trail || trail.length < 5) return false; var sum = 0; for (var i = 0; i < trail.length; i++) sum += trail[i]; var avg = sum / trail.length; var variance = 0; for (var j = 0; j < trail.length; j++) variance += (trail[j] - avg) * (trail[j] - avg); var stddev = Math.sqrt(variance / trail.length); return stddev > 0; } // Encode trail + position for server verification function encodePayload(trail, spliced) { var str = JSON.stringify({ t: trail, s: Math.round(spliced), ts: Date.now() }); try { return btoa(str); } catch (e) { return str; } } AksrtSliderCaptcha.prototype.onSuccess = function (trail, spliced) { var self = this; var o = this.options; this.sliderBar.classList.add('sliderContainer_success'); this.sliderMask.style.background = '#52c41a'; this.text.textContent = o.successText; this.text.style.color = '#52c41a'; this.slider.innerHTML = '✓'; this.slider.style.color = '#52c41a'; this.slider.style.cursor = 'default'; var payload = encodePayload(trail || [], spliced || 0); // Server token verification if endpoint configured if (o.serverVerify) { var ajaxUrl = (window.aksrtData && aksrtData.ajaxUrl) || '/wp-admin/admin-ajax.php'; var params = new URLSearchParams(); params.set('action', 'aksrt_captcha_verify'); if (window.aksrtData && aksrtData.nonce) params.set('nonce', aksrtData.nonce); params.set('payload', payload); if (o.scene) params.set('scene', o.scene); fetch(ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, credentials: 'same-origin', body: params.toString() }) .then(function (r) { return r.json(); }) .then(function (json) { if (json && json.success && json.data && json.data.token) { if (typeof o.onSuccess === 'function') o.onSuccess(json.data.token); } else { self.onFail(); } }) .catch(function () { self.onFail(); }); } else { if (typeof o.onSuccess === 'function') { setTimeout(function () { o.onSuccess(payload); }, 300); } } }; AksrtSliderCaptcha.prototype.onFail = function () { var self = this; var o = this.options; this.sliderBar.classList.add('sliderContainer_fail'); this.sliderMask.style.background = '#ff4d4f'; this.text.textContent = o.failText; this.text.style.color = '#ff4d4f'; this.slider.style.color = '#ff4d4f'; if (typeof o.onFail === 'function') o.onFail(); setTimeout(function () { self.sliderBar.classList.remove('sliderContainer_fail'); self.reset(); }, 1000); }; AksrtSliderCaptcha.prototype.reset = function () { this.slider.style.left = '0px'; this.sliderMask.style.width = '0px'; this.block.style.left = '0px'; this.sliderMask.style.background = 'linear-gradient(90deg,#1890ff,#36cfc9)'; this.text.style.color = '#999'; this.text.textContent = this.options.loadingText; this.slider.innerHTML = '❯'; this.slider.style.color = ''; this.slider.style.cursor = 'grab'; this.loadCount = 0; this.initImg(); }; // ── Modal wrapper ── function SliderCaptchaModal(options) { var overlay = document.createElement('div'); overlay.className = 'aksrt-captcha-modal-overlay'; var dialog = document.createElement('div'); dialog.className = 'aksrt-captcha-modal-dialog'; overlay.appendChild(dialog); document.body.appendChild(overlay); var origSuccess = options.onSuccess; options.onSuccess = function (token) { overlay.classList.remove('is-open'); setTimeout(function () { overlay.remove(); }, 300); if (typeof origSuccess === 'function') origSuccess(token); }; if (!options.setSrc) { options.setSrc = function (cb) { cb('https://picsum.photos/' + (options.width || 280) + '/' + (options.height || 170) + '?random=' + Date.now()); }; } var captcha = new AksrtSliderCaptcha(dialog, options); requestAnimationFrame(function () { overlay.classList.add('is-open'); }); overlay.addEventListener('click', function (e) { if (e.target === overlay) { overlay.classList.remove('is-open'); setTimeout(function () { overlay.remove(); }, 300); } }); return { el: overlay, captcha: captcha }; } // ── Public API ── window.AksrtSliderCaptcha = AksrtSliderCaptcha; window.SliderCaptchaModal = SliderCaptchaModal; // ── CSS ── var STYLE_ID = 'aksrt-slider-captcha-style'; function injectStyle() { if (document.getElementById(STYLE_ID)) return; var style = document.createElement('style'); style.id = STYLE_ID; style.textContent = '.aksrt-captcha__canvas-wrap{position:relative;overflow:hidden;border-radius:8px;background:#f0f0f0;}' + '.aksrt-captcha__canvas-wrap canvas{display:block;}' + '.aksrt-captcha__canvas-wrap canvas:last-child{position:absolute;top:0;left:0;pointer-events:none;}' + '.aksrt-captcha__bar{position:relative;height:40px;border-radius:20px;background:#e8e8e8;margin-top:10px;user-select:none;}' + '.aksrt-captcha__mask{position:absolute;top:0;left:0;height:100%;border-radius:20px;background:linear-gradient(90deg,#1890ff,#36cfc9);width:0;transition:background 0.3s;}' + '.aksrt-captcha__slider{position:absolute;top:0;left:0;width:44px;height:40px;border-radius:20px;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,0.15);cursor:grab;display:flex;align-items:center;justify-content:center;font-size:18px;z-index:2;transition:box-shadow 0.2s;}' + '.aksrt-captcha__text{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#999;font-size:14px;pointer-events:none;}'; document.head.appendChild(style); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', injectStyle); } else { injectStyle(); } })();