示例
?
背景
驗證碼主要是防止機器暴力破解。之前的驗證碼都是以靜態為主,現在一些產品開始使用動態方式,增加破解的難度。動態方式以 gif 最為簡單可靠。gif 兼容性好,尺寸小。這里分享的就是一種:用 JS 實現 gif 動態驗證碼的思路。感謝關注。
?
任務分解
- 繪制旋轉的文字
- 計算每個字符出現位置和角度
- 生成 gif 圖片
?
逐步求精
如何繪制旋轉的文字?
?
了解能用的 API
-
context.rotate(angle)使當前坐標系旋轉 angle,單位弧度 -
context.translate(x, y)使當前坐標系偏移 x, y,單位像素 -
context.font設置字體 -
context.strokeText(text, x, y [, maxWidth ])給文本描邊 -
context.fillText(text, x, y [, maxWidth ])給文本填充
怎么以文字的中心位置旋轉?
void function() {
// ...
var x = 100;
var y = 100;
var angle = 1 / 8 * Math.PI;
context.translate(x, y);
context.rotate(angle);
context.strokeText('A', 0, 0);
// ...
}()
?
以文字的左下角為圓心旋轉,不符合預期,見下圖效果
本打算做一下偏移的計算,一想到要計算文本中心位置貌似還挺復雜。 還是看看其他人怎么做的,通過關鍵詞
canvas rotate text center
找到一點線索。
context.save();
context.translate(newx, newy);
context.rotate(-Math.PI / 2);
context.textAlign = "center";
context.fillText("Your Label Here", labelXposition, 0);
context.restore();
textAlign
是橫向對齊,再根據標準找到了一個縱向對齊
textBaseline
void function() {
// ...
context.textAlign = 'center'; // <<<<<<< insert
context.textBaseline = 'middle'; // <<<<<<< insert
var x = 100;
var y = 100;
var angle = 1 / 8 * Math.PI;
context.translate(x, y);
context.rotate(angle);
context.strokeText('A', 0, 0);
// ...
}()
?
修改以后,效果符合預期,見下圖:
按我的習慣就這種 “常用” 功能就封裝成獨立函數,方便以后使用。
/**
* 繪制旋轉的文字
* @param {CanvasRenderingContext2D} context 上下文
* @param {String} text 文本
* @param {Number} x 中心坐標 x
* @param {Number} y 中心坐標 y
* @param {Number} angle 角度,單位弧度
*/
function rotateText(context, text, x, y, angle) {
if (!context) {
return;
}
context.save(); // 保存上次的風格設置
context.textAlign = 'center'; // 橫向居中
context.textBaseline = 'middle'; // 縱向居中
context.translate(x, y); // 修改坐標系原點
context.rotate(angle); // 旋轉
context.strokeText(text, 0, 0); // 繪制文本
context.restore(); // 恢復上次的風格設置
}
?
如何計算每個字符出現位置和角度?
背景文字左右平移 + 旋轉,生成隨機的字符串計算中心坐標就好了
前景文字基本相似,只要上下來回移動和稍微搖擺,這里用的 cos 曲線控制搖擺。
?
如何生成 gif 圖片
生成 gif 有第三方庫可以使用 gifjs。 這里要注意的是,gifjs 用到 worker 技術,所以得在
http://
環境里調試,不能用
file://
環境
?
注意:由于添加的是同一個 canvas 對象,所以的是使用
copy
模式,將圖像數據保留給每一幀。
gif.addFrame(canvasTemp, { delay: 100, copy: true });
?
完整代碼
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
canvas {
border: black 1px solid;
}
</style>
<script src="../library/gif.js"></script>
</head>
<body>
<div>
Key: <input type="text" maxlength="8" /> <input type="button" value="build" />
</div>
<canvas width="300" height="70"></canvas>
<img width="300" height="70" /><a download="captcha.gif">download...</a>
<script>
/**
* 繪制旋轉的文字
* @param {CanvasRenderingContext2D} context 上下文
* @param {String} text 文本
* @param {Number} x 中心坐標 x
* @param {Number} y 中心坐標 y
* @param {Number} angle 角度,單位弧度
*/
function rotateText(context, text, x, y, angle) {
if (!context) {
return;
}
context.save(); // 保存上次的風格設置
context.textAlign = 'center'; // 橫向居中
context.textBaseline = 'middle'; // 縱向居中
context.translate(x, y); // 修改坐標系原點
context.rotate(angle); // 旋轉
context.strokeText(text, 0, 0); // 繪制文本
context.restore(); // 恢復上次的風格設置
}
/**
* 隨機字符串
* @param{String} chars 字符串
* @param{Number} len 長度
*/
function randomText(chars, len) {
var result = '';
for (var i = 0; i < len; i++) {
result += chars.charAt(parseInt(chars.length * Math.random()));
}
return result;
}
void function() {
// @see http://www.w3.org/TR/2dcontext/
var canvas = document.querySelector('canvas');
var context = canvas.getContext('2d');
context.font = '30px Verdana'; // 字體大小和字體名
var lineHeight = 15; // 行高
var backLength = 3;
var backTexts = {};
var backXOffsets = {};
var keyYOffsets = {};
var keyAOffsets = {};
var backSpeed = 10000 + parseInt(100 * Math.random());
var keySpeed = 12000 + parseInt(100 * Math.random());
var key = '';
function init(value) {
key = String(value).toUpperCase();
// 隨機備件
for (var i = 0; i < canvas.height / lineHeight; i++) {
backTexts[i] = randomText('ABCDEFGHIJKLMNOPQRST0123456789', backLength);
backXOffsets[i] = Math.random() * canvas.width;
}
for (var i = 0; i < key.length; i++) {
keyYOffsets[i] = Math.random() * lineHeight / 2;
keyAOffsets[i] = 0.05 - Math.random() * 0.1;
}
}
function renderBack(now, context, text, y, xOffset) {
var tick = now % backSpeed;
for (var i = 0; i < backLength; i++) {
var t = (xOffset + (tick / backSpeed) * canvas.width +
(canvas.width / backLength) * i) % canvas.width;
rotateText(context, text[i], t, y,
i / backLength * Math.PI * 2 + (tick / backSpeed) * Math.PI * 2);
}
}
function render(now, context) {
context.fillStyle = '#FFFFFF';
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = '#000000';
// 繪制背景文字
for (var i = 0; i < canvas.height / lineHeight; i++) {
renderBack(now, context, backTexts[i], lineHeight * i, backXOffsets[i]);
}
// 繪制 key
var tick = now % keySpeed;
var keyCharWidth = canvas.width / key.length;
for (var i = 0; i < key.length; i++) {
var tx = keyCharWidth + (((canvas.width - keyCharWidth) / key.length) * i) % canvas.width;
var ty = Math.cos(now / 1000) * Math.PI * keyYOffsets[i];
rotateText(context, key[i], tx,
canvas.height / 2 - ty, Math.cos(now / 1000) * Math.PI * 0.1 + keyAOffsets[i]);
}
}
init('zswang');
setInterval(function() {
render(Number(new Date), context);
}, 100);
document.querySelector('input[type=text]').addEventListener('input', function() {
init(this.value);
});
document.querySelector('input[type=button]').addEventListener('click', function() {
var self = this;
self.disabled = true;
var gif = new GIF({
repeat: 0,
workers: 2,
quality: 10,
workerScript: '../library/gif.worker.js'
});
// 生成 gif 圖片
var canvasTemp = document.createElement('canvas');
canvasTemp.width = canvas.width;
canvasTemp.height = canvas.height;
var context = canvasTemp.getContext('2d');
context.font = '30px Verdana'; // 字體大小和字體名
context.textAlign = 'center';
for (var i = 0; i < 5000; i += 100) {
render(i, context);
gif.addFrame(canvasTemp, { delay: 100, copy: true });
}
gif.on('finished', function(blob) {
var url = URL.createObjectURL(blob);
document.querySelector('img').src = url;
document.querySelector('a').href = url;
self.disabled = false;
});
gif.render();
});
}();
</script>
</body>
</html>
?
后記
功能比較簡單,也寫得比較簡單,僅供參考。如果要應用到實戰,還有很多細節要考慮
- gif 創建的過程必然得放到后端完成,否則 兼容性、性能、安全性 都是問題(這塊和傳統的驗證過程并無區別)。
- 緩存(背景效果可以重復利用一段時間)。
- 圖片大小需要優化,目前是 200K(通過調整幀率和壓縮比)。
- 提供方便的調用接口(模塊化)。
?
參考資料
?
作者: zswang (http://weibo.com/zswang) - 站在巨人的肩上也要成為巨人的一部分
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

