示例
?
背景
驗證碼主要是防止機器暴力破解。之前的驗證碼都是以靜態為主,現在一些產品開始使用動態方式,增加破解的難度。動態方式以 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元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
