示例
背景
驗證碼主要是防止機器暴力破解。之前的驗證碼都是以靜態爲主,現在一些產品開始使用動態方式,增加破解的難度。動態方式以 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 ])
給文本填充
怎麼以文字的中心位置旋轉?
1 2 3 4 5 6 7 8 9 10 | 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
找到一點線索。
1
2
3
4
5
6
|
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
1 2 3 4 5 6 7 8 9 10 11 12 | 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); // ... }() |
修改以後,效果符合預期,見下圖:
按我的習慣就這種 “常用” 功能就封裝成獨立函數,方便以後使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/**
*
繪製旋轉的文字
*
@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
模式,將圖像數據保留給每一幀。
1 | gif.addFrame(canvasTemp, { delay: 100, copy: true }); |
完整代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
<!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(通過調整幀率和壓縮比)。
- 提供方便的調用接口(模塊化)。