使用npm包canvas在后端绘制一张背景图片,然后在这张背景图片中截取一部分作为拼图,让用户移动拼图到正确的位置,同时将此拼图所截取的区域用一个空白的区域覆盖。
import express from 'express';
const Canvas = require('canvas');
const path = require('path');
const router = express.Router();
// 背景图片的宽可以传参设置,默认值是320 * 180,小拼图默认是60 * 45
router.get('/drag_captcha', (req, res) => {
const { bgWidth: width } = req.query;
const bgWidth = parseInt(width) || 320;
const bgHeight = (width && parseInt(width * 180 / 320)) || 180;
const dragPicWidth = 60;
const dragPicHeight = 45;
const index = Math.floor(Math.random() * 13);
const positionX = Math.floor(Math.random() * (bgWidth - dragPicWidth - 10) + 11); // 空白拼图的定位X
const positionY = Math.floor(Math.random() * (bgHeight - dragPicHeight - 10) + 11);
const Image = Canvas.Image;
const bgCanvas = new Canvas(bgWidth, bgHeight);
const dragCanvas = new Canvas(dragPicWidth, dragPicHeight);
const background = bgCanvas.getContext('2d');
const dragPic = dragCanvas.getContext('2d');
const image = new Image();
image.onload = () => {
background.drawImage(image, 0, 0, 320, 180, 0, 0, bgWidth, bgHeight);
dragPic.drawImage(bgCanvas, positionX, positionY, dragPicWidth, dragPicHeight, 0, 0,
dragPicWidth, dragPicHeight);
background.clearRect(positionX, positionY, dragPicWidth, dragPicHeight);
};
image.src = path.join(__dirname, `../app/assets/images/validate/bg-${index}.png`);
if (req.session) {
req.session.dragCaptcha = {
positionX,
positionY
};
}
res.send({ bgCanvas: bgCanvas.toDataURL(), dragCanvas: dragCanvas.toDataURL() });
});
router.post('/check_captcha_position', (req, res) => {
const { offsetLeft, offsetTop } = req.body;
const { dragCaptcha = {} } = req.session;
const range = 5; // 误差范围
if (!offsetLeft || !offsetTop) return;
console.log(dragCaptcha.positionX, dragCaptcha.positionY);
const deviationX = Math.abs(offsetLeft - dragCaptcha.positionX);
const deviationY = Math.abs(offsetTop - dragCaptcha.positionY);
if (deviationX < range && deviationY < range) {
res.send({ error: false });
} else {
res.send({ error: true });
}
});
export default router;
在前端页面加载后生成背景图片和拼图,当移动拼图时判断拼图的正确位置。在这里,把它封装为在普通JS或者node.js都可以加载实现的文件。注意,在PC端鼠标按下拖动事件是mousedown,mousemove,mouseup
,对应的移动端事件是touchstart,touchmove,touchend
。
(function(factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = factory();
} else {
window.DragCaptcha = factory();
}
})(function () {
var extend = {
addEventListener: function(target, type, handler) {
var obj = target === 'document' ? document : target;
try {
obj.addEventListener(type, handler, false);
} catch (error) {
obj.attachEvent('on' + type, handler);
}
},
removeEventListener: function(target, type, handler) {
var obj = target === 'document' ? document : target;
try {
obj.removeEventListener(type, handler, false);
} catch (error) {
obj.detachEvent('on' + type, handler);
}
}
};
/**
* DragCaptcha
* @param targetId 添加到指定id的节点中
* @param callback 拖动时松开鼠标后的回调函数
* @param bgWidth 背景图片的宽度(不加单位px)
*/
function DragCaptcha(targetId, callback, bgWidth) {
this.targetId = targetId;
this.callback = callback;
this.bgWidth = bgWidth;
this.bgHeight = bgWidth * 180 / 320;
this.hasAddedEvent = false;
this.defaultValue = {
bgWidth: '320px',
bgHeight: '180px',
dragWidth: '60px',
dragHeight: '45px'
};
this._init();
}
DragCaptcha.prototype = {
constructor: DragCaptcha,
_init: function () {
this.createElement();
this.getImage(this.bgWidth);
},
appendHtml: function(targetId, child) {
var target = document.getElementById(targetId);
target.appendChild(child);
},
createElement: function() {
var canvasWrapper = document.createElement('div');
canvasWrapper.style.position = 'relative';
canvasWrapper.style.width = this.bgWidth ? this.bgWidth + 'px' : this.defaultValue.bgWidth;
canvasWrapper.style.margin = '5px auto';
var bgImage = document.createElement('img');
bgImage.setAttribute('id', 'bg-canvas');
bgImage.style.width = this.bgWidth ? this.bgWidth + 'px' : this.defaultValue.bgWidth;
bgImage.style.height = this.bgHeight ? this.bgHeight + 'px' : this.defaultValue.bgHeight;
var dragImage = document.createElement('img');
dragImage.setAttribute('id', 'drag-canvas');
dragImage.style.position = 'absolute';
dragImage.style.top = 0;
dragImage.style.left = 0;
dragImage.style.width = this.defaultValue.dragWidth;
dragImage.style.height = this.defaultValue.dragHeight;
var tip = document.createElement('div');
tip.innerHTML = '请拖动拼图填充完整图片';
canvasWrapper.appendChild(bgImage);
canvasWrapper.appendChild(dragImage);
this.appendHtml(this.targetId, canvasWrapper);
this.appendHtml(this.targetId, tip);
},
getImage: function(bgWidth) {
var that = this;
var xhr = new XMLHttpRequest();
var response;
var bgImage = document.querySelector('img#bg-canvas');
var dragImage = document.querySelector('img#drag-canvas');
xhr.open('get', '/drag_captcha?bgWidth=' + bgWidth);
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
response = JSON.parse(xhr.responseText);
dragImage.style.left = 0;
dragImage.style.top = 0;
bgImage.src = response.bgCanvas;
dragImage.src = response.dragCanvas;
if (that.hasAddedEvent) return;
that.hasAddedEvent = true;
that.drag();
}
}
},
drag: function() {
var that = this;
var bgImage = document.querySelector('img#bg-canvas');
var dragImage = document.querySelector('img#drag-canvas');
var moveMaxValueX = bgImage.width - dragImage.width; // 水平方向最大移动距离
var moveMaxValueY = bgImage.height - dragImage.height; // 垂直方向最大移动距离
var dragStartX = dragImage.offsetLeft; // 拼图初始水平位置
var dragStartY = dragImage.offsetTop; // 拼图初始垂直位置
if (/Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
extend.addEventListener(dragImage, 'touchstart', function(e) {
if (e.preventDefault) {
e.preventDefault();
} else {
window.event.returnValue = false;
}
var startClientX = e.targetTouches[0].pageX;
var startClientY = e.targetTouches[0].pageY;
var dragMoveLeft = dragImage.offsetLeft;
var dragMoveTop = dragImage.offsetTop;
var fnMouseMove = function(e) {
var mouseMoveX = e.targetTouches[0].pageX - startClientX;
var mouseMoveY = e.targetTouches[0].pageY - startClientY;
var toX = dragMoveLeft + mouseMoveX;
var toY = dragMoveTop + mouseMoveY;
if (toX > moveMaxValueX) {
toX = moveMaxValueX;
} else if (toX < dragStartX) {
toX = dragStartX;
}
if (toY > moveMaxValueY) {
toY = moveMaxValueY;
} else if (toY < dragStartY) {
toY = dragStartY;
}
dragImage.style.left = toX + 'px';
dragImage.style.top = toY + 'px';
};
extend.addEventListener(document, 'touchmove', fnMouseMove);
var fnMouseUp = function() {
extend.removeEventListener(document, 'touchmove', fnMouseMove);
extend.removeEventListener(document, 'touchend', fnMouseUp);
that.callback && that.callback();
}
extend.addEventListener(document, 'touchend', fnMouseUp);
});
return;
}
extend.addEventListener(dragImage, 'mousedown', function(e) {
if (e.preventDefault) {
e.preventDefault();
} else {
window.event.returnValue = false;
}
var startClientX = e.clientX;
var startClientY = e.clientY;
var dragMoveLeft = dragImage.offsetLeft;
var dragMoveTop = dragImage.offsetTop;
var fnMouseMove = function(e) {
var mouseMoveX = e.clientX - startClientX;
var mouseMoveY = e.clientY - startClientY;
var toX = dragMoveLeft + mouseMoveX;
var toY = dragMoveTop + mouseMoveY;
if (toX > moveMaxValueX) {
toX = moveMaxValueX;
} else if (toX < dragStartX) {
toX = dragStartX;
}
if (toY > moveMaxValueY) {
toY = moveMaxValueY;
} else if (toY < dragStartY) {
toY = dragStartY;
}
dragImage.style.left = toX + 'px';
dragImage.style.top = toY + 'px';
};
extend.addEventListener(document, 'mousemove', fnMouseMove);
var fnMouseUp = function() {
extend.removeEventListener(document, 'mousemove', fnMouseMove);
extend.removeEventListener(document, 'mouseup', fnMouseUp);
that.callback && that.callback();
}
extend.addEventListener(document, 'mouseup', fnMouseUp);
});
}
};
return DragCaptcha;
});