├── .gitignore
├── customBg
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
└── 6.png
├── docs
├── drag-en.gif
├── drag-en.png
├── drag-zh.gif
├── drag-zh.png
├── confuse-cut.png
└── confuse-expand.png
├── src
├── Resources
│ ├── bg
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ └── 5.png
│ └── mask
│ │ ├── circle.png
│ │ ├── star.png
│ │ └── triangle.png
├── Confuse
│ ├── ConfuseInterface.php
│ ├── ConfuseExpand.php
│ └── ConfuseCut.php
├── Maker.php
├── Utils.php
├── Resources.php
└── Drag.php
├── composer.json
├── LICENSE
├── index.html
├── index.php
├── README.md
├── README-en.md
├── dragCaptcha.css
└── dragCaptcha.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | composer.lock
3 | .vscode
4 | .idea
5 |
--------------------------------------------------------------------------------
/customBg/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/customBg/1.png
--------------------------------------------------------------------------------
/customBg/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/customBg/2.png
--------------------------------------------------------------------------------
/customBg/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/customBg/3.png
--------------------------------------------------------------------------------
/customBg/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/customBg/4.png
--------------------------------------------------------------------------------
/customBg/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/customBg/5.png
--------------------------------------------------------------------------------
/customBg/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/customBg/6.png
--------------------------------------------------------------------------------
/docs/drag-en.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/docs/drag-en.gif
--------------------------------------------------------------------------------
/docs/drag-en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/docs/drag-en.png
--------------------------------------------------------------------------------
/docs/drag-zh.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/docs/drag-zh.gif
--------------------------------------------------------------------------------
/docs/drag-zh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/docs/drag-zh.png
--------------------------------------------------------------------------------
/docs/confuse-cut.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/docs/confuse-cut.png
--------------------------------------------------------------------------------
/docs/confuse-expand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/docs/confuse-expand.png
--------------------------------------------------------------------------------
/src/Resources/bg/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/src/Resources/bg/1.png
--------------------------------------------------------------------------------
/src/Resources/bg/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/src/Resources/bg/2.png
--------------------------------------------------------------------------------
/src/Resources/bg/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/src/Resources/bg/3.png
--------------------------------------------------------------------------------
/src/Resources/bg/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/src/Resources/bg/4.png
--------------------------------------------------------------------------------
/src/Resources/bg/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/src/Resources/bg/5.png
--------------------------------------------------------------------------------
/src/Resources/mask/circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/src/Resources/mask/circle.png
--------------------------------------------------------------------------------
/src/Resources/mask/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/src/Resources/mask/star.png
--------------------------------------------------------------------------------
/src/Resources/mask/triangle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RLOFLS/drag-captcha/HEAD/src/Resources/mask/triangle.png
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rlofls/drag-captcha",
3 | "description": "Drag-and-drop graphics verification, small and easy to use",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "rlofls"
8 | }
9 | ],
10 | "autoload": {
11 | "psr-4": {
12 | "Rlofls\\DragCaptcha\\": "./src"
13 | }
14 | },
15 | "require": {
16 | "php": ">=7.2",
17 | "ext-gd": "*"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Confuse/ConfuseInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | demo drag verify
8 |
9 |
10 |
11 |
12 |
23 |
24 |
25 |
26 |
27 |
59 |
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | generate(true);
33 |
34 | //cache target value
35 |
36 | //Create a verification unique ID, user cache, and subsequent verification
37 | $cid = uniqid('drag-captcha');
38 | $_SESSION[$cid] = json_encode($dst);
39 | $data['cid'] = $cid;
40 |
41 | header('Content-Type:application/json');
42 | echo json_encode(['status' => 'success', 'data'=> $data]);
43 | break;
44 | case '/dragVerify':
45 | $post = json_decode(file_get_contents('php://input'), true);
46 | $cid = $post['cid'] ?? '';
47 | $mask = $post['mask'] ?? [];
48 |
49 | $dst = json_decode($_SESSION[$cid], true);
50 |
51 | $res = json_encode(['status' => 'error']);
52 | if ($dst && Drag::verify($dst, $mask)) {
53 | $res = json_encode(['status' => 'success']);
54 | }
55 | unset($_SESSION[$cid]);
56 | header('Content-Type:application/json');
57 | echo $res;
58 | break;
59 | default:
60 | include(__DIR__ . '/index.html');
61 | }
62 |
--------------------------------------------------------------------------------
/src/Maker.php:
--------------------------------------------------------------------------------
1 | useConfuse = $useConfuse;
28 | }
29 |
30 | /**
31 | * @param $dst
32 | * @param $mask
33 | * @param $imgDst
34 | * @param $imgMask
35 | * @throws \Exception
36 | */
37 | public function swapPixels($dst, $mask, $imgDst, $imgMask): void
38 | {
39 | if ($this->useConfuse) {
40 | $this->confuse($dst, $mask, $imgDst, $imgMask)->swapPixels();
41 | return;
42 | }
43 | //normal
44 | Utils::swapAndDeepMask($imgDst, $imgMask, $dst, function ($x, $y, $tx, $ty, $tRgb) use ($imgMask, $imgDst) {
45 | $color = imagecolorallocate($imgMask, $tRgb['red'], $tRgb['green'], $tRgb['blue']);
46 | imagesetpixel($imgMask, $x, $y, $color);
47 |
48 | $tColor = Utils::deepColor($imgDst, $tRgb);
49 | imagesetpixel($imgDst, $tx, $ty, $tColor);
50 | });
51 | }
52 |
53 | /**
54 | * @param $dstPosition
55 | * @param $mask
56 | * @param $imgDst
57 | * @param $imgMask
58 | * @return ConfuseInterface
59 | * @throws \Exception
60 | */
61 | private function confuse($dstPosition, $mask, $imgDst, $imgMask): ConfuseInterface
62 | {
63 | $class = Utils::randValue($this->confuseClass);
64 | return new $class($dstPosition, $mask, $imgDst, $imgMask);
65 | }
66 | }
--------------------------------------------------------------------------------
/src/Utils.php:
--------------------------------------------------------------------------------
1 | dst = $dst;
37 | $this->mask = $mask;
38 | $this->imgMask = $imgMask;
39 | $this->imgDst = $imgDst;
40 |
41 | $this->initCDst();
42 | $this->initCImgMask();
43 | }
44 |
45 | /**
46 | * @inheritDoc
47 | * @return void
48 | */
49 | public function swapPixels(): void
50 | {
51 | $maps = [];
52 |
53 | Utils::swapAndDeepMask($this->imgDst, $this->imgMask, $this->dst, function ($x, $y, $tx, $ty, $tRgb) use (&$maps) {
54 | $color = imagecolorallocate($this->imgMask, $tRgb['red'], $tRgb['green'], $tRgb['blue']);
55 | imagesetpixel($this->imgMask, $x, $y, $color);
56 |
57 | $tColor = Utils::deepColor($this->imgDst, $tRgb);
58 | imagesetpixel($this->imgDst, $tx, $ty, $tColor);
59 |
60 | $maps[] = $tx . '-' . $ty;
61 | });
62 |
63 | Utils::swapAndDeepMask($this->imgDst, $this->cImgMask, $this->cDst, function ($x, $y, $tx, $ty, $tRgb) use (&$maps) {
64 | if (in_array($tx . '-' . $ty, $maps, true)) {
65 | return;
66 | }
67 | $tColor = Utils::deepColor($this->imgDst, $tRgb);
68 | imagesetpixel($this->imgDst, $tx, $ty, $tColor);
69 | });
70 | }
71 |
72 | /**
73 | * @throws Exception
74 | */
75 | private function initCDst(): void
76 | {
77 | $offset = (int)(imagesx($this->imgMask) * 0.5);
78 | $this->cDst = [
79 | 'left' => $this->dst['left'] + (random_int(-$offset, $offset)),
80 | 'top' => $this->dst['top'] + (random_int(-$offset, $offset)),
81 | ];
82 | }
83 |
84 | /**
85 | * @throws Exception
86 | */
87 | private function initCImgMask(): void
88 | {
89 | $mask = Resources::uniqueMask($this->mask['img']);
90 | $this->cImgMask = imagecreatefrompng($mask['img']);
91 | imagesavealpha($this->cImgMask, true);
92 | }
93 |
94 |
95 | public function __destruct()
96 | {
97 | if ($this->cImgMask) {
98 | imagedestroy($this->cImgMask);
99 | $this->cImgMask = null;
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/src/Resources.php:
--------------------------------------------------------------------------------
1 | self::BASE_PATH . 'mask/star.png',
46 | 'path' => 'M 1.136655,3.3203515 6.1060004,3.0099206 9.468514,0.50570386 c 0,0 0.2931707,-0.23202947 0.3872529,0.0956479 0.094082,0.32767733 1.8656861,4.71463374 1.8656861,4.71463374 l 3.447523,2.4491389 c 0,0 0.121937,0.1474338 0.0163,0.2898124 C 15.079641,8.1973152 11.25402,11.277177 11.25402,11.277177 L 9.9496262,15.38326 c 0,0 -0.040019,0.138767 -0.2671061,0.07276 C 9.4554327,15.39001 5.3588636,12.69956 5.3588636,12.69956 l -4.2185818,0.05788 c 0,0 -0.25781398,0.03713 -0.20864011,-0.292809 C 0.98081559,12.13469 2.2303792,7.5899824 2.2303792,7.5899824 L 0.85933882,3.5082599 c 0,0 -0.10626247,-0.2139054 0.27731618,-0.1879084 z',
47 | 'viewBox' => 15.875,
48 | ],
49 | [
50 | 'img' => self::BASE_PATH . 'mask/circle.png',
51 | 'path' => 'M 15.274299,7.9750312 C 15.2429,17.708162 0.59930244,17.685739 0.63279101,7.9568732 0.6127643,-2.1097735 15.231862,-1.9835272 15.274299,7.9750312 Z',
52 | 'viewBox' => 15.875,
53 | ],
54 | [
55 | 'img' => self::BASE_PATH . 'mask/triangle.png',
56 | 'path' => 'M 1.1498175,4.0726686 13.863502,1.0830571 c 0,0 0.527302,-0.24653047 0.691878,0.1895674 0.160777,0.4260304 -3.783944,13.4809525 -3.783944,13.4809525 0,0 -0.113272,0.552973 -0.576934,0.291881 C 9.7414735,14.790355 1.0158163,4.496519 1.0158163,4.496519 c 0,0 -0.13712788,-0.2798387 0.1340012,-0.4238504 z',
57 | 'viewBox' => 15.875,
58 | ]
59 | ];
60 |
61 | /**
62 | * @return string
63 | * @throws Exception
64 | */
65 | public static function bg(): string
66 | {
67 | if (! empty(self::$customBg)) {
68 | return Utils::randValue(self::$customBg);
69 | }
70 | return Utils::randValue(self::$bg);
71 | }
72 |
73 | /**
74 | * @return string[]
75 | * @throws Exception
76 | */
77 | public static function mask(): array
78 | {
79 | return Utils::randValue(self::$mask);
80 | }
81 |
82 | /**
83 | * @param string $img
84 | * @return array|string[]
85 | * @throws Exception
86 | */
87 | public static function uniqueMask(string $img): array
88 | {
89 | $copyMask = [];
90 | foreach (self::$mask as $item) {
91 | if ($item['img'] !== $img) {
92 | array_push($copyMask, $item);
93 | }
94 | }
95 | return Utils::randValue($copyMask);
96 | }
97 |
98 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Drag-Captcha
2 |
3 | 拖拽图形验证,简单易用, 只需要绑定按钮即可使用。 [english](./README-en.md)\
4 | `composer require rlofls/drag-captcha`
5 |
6 | 
7 | 
8 |
9 | - [功能介绍](#features)
10 | - [自定义背景](#custom-bg)
11 | - [开启干扰混淆](#confuse)
12 | - [运行示例](#run-demo)
13 | - [实践](#practice)
14 | - [api](#api)
15 | ## 功能介绍
16 |
17 | ### 自定义背景
18 |
19 | 先设置 `Resources::$customBg` 数组值,一定要是 **png** 图片, 存放的是图片路径。 eg:
20 |
21 | ```php
22 | //设置自定义背景
23 | Resources::$customBg = [
24 | __DIR__ . '/customBg/1.png',
25 | __DIR__ . '/customBg/2.png',
26 | __DIR__ . '/customBg/3.png',
27 | //...
28 | ];
29 | ```
30 |
31 | ### 添加干扰混淆
32 |
33 | 能够使目标不那么明显,会随机对目标进行扩展/剪切,在调用 `generate()` 方法时候, 传入`true` 值即可。 eg: `$drag->generate(true);`
34 |
35 | - expand:
36 |
37 | 
38 | - cut
39 |
40 | 
41 |
42 | ## 运行示例
43 |
44 | 1. 切换到此目录下
45 | 2. `composer install`
46 | 3. `php -S 127.0.0.1:8087`
47 | 4. 浏览器访问 `http://127.0.0.1:8087`
48 |
49 | ## 实践
50 |
51 | 参考 `index.php` `index.html`\
52 | 复制 `dragCaptcha.css` `dragCaptcha.js` 到自己项目应用 \
53 | 参考 html
54 | ```html
55 |
56 |
57 |
58 |
59 |
91 |
92 | ```
93 |
94 | ## api
95 |
96 | - `Rlofls\DragCaptcha\Drag`
97 | - `generate` 生成渲染数据 `dst, data`
98 | - `verify` 验证匹配结果
99 |
100 | - 服务器api 要求, Content-Type: application/json
101 | - `/dragData`---[GET]---获取验证码数据\
102 | 响应数据格式:
103 | ```json
104 | {
105 | "status": "success" // error 表示失败
106 | "data": {
107 | //一次验证 事务id 用户自己生成
108 | "cid": "drag-captcha63c0c18566074"
109 | //以下数据由 Rlofls\DragCaptcha\Drag::generate 生成
110 | "bgBase64": "data:image/png;base64,iV..."
111 | "bgH": 160
112 | "bgW": 250
113 | "maskBase64": "data:image/png;base64,..."
114 | "maskLeft": 114
115 | "maskPath": "M 15.27429..."
116 | "maskTop": 0
117 | "maskViewBox": 15.875
118 | "maskWH": 57
119 | }
120 |
121 | }
122 | ```
123 | - `/dragVerify`---[POST]---进行验证码验证\
124 | 请求参数
125 | ```json
126 | {
127 | "cid": "drag-captcha63c0c1881db17"
128 | "mask": {
129 | "left": 149,
130 | "top": 76
131 | }
132 | }
133 | ```
134 | 响应数据格式:
135 | ```json
136 | {"status": "error"}
137 | ```
138 |
139 | ## todo
140 |
141 | - 欢迎提交 `issuse`
142 |
--------------------------------------------------------------------------------
/src/Drag.php:
--------------------------------------------------------------------------------
1 | bgWidth, $this->bgHeight);
48 | $imgMask = imagecreatefrompng($mask['img']);
49 |
50 | imagesavealpha($imgMask, true);
51 | imagesavealpha($imgDst, true);
52 |
53 | imagecopyresized($imgDst, $imgBG, 0, 0, 0,0, $this->bgWidth, $this->bgHeight, imagesx($imgBG), imagesy($imgBG));
54 |
55 | [$dstPosition, $maskPosition] = $this->getPosition();
56 |
57 | $maker = new Maker($useConfuse);
58 | $maker->swapPixels($dstPosition, $mask, $imgDst, $imgMask);
59 |
60 | ob_start();
61 | imagepng($imgDst);
62 | imagedestroy($imgDst);
63 | $bgData = ob_get_contents();
64 | ob_end_clean();
65 |
66 | ob_start();
67 | imagepng($imgMask);
68 | imagedestroy($imgMask);
69 | $maskData = ob_get_contents();
70 | ob_end_clean();
71 | imagedestroy($imgBG);
72 |
73 | return [
74 | $dstPosition,
75 | [
76 | 'bgBase64' => self::BASE64_HEADER . base64_encode($bgData),
77 | 'bgW' => $this->bgWidth,
78 | 'bgH' => $this->bgHeight,
79 | 'maskBase64' => self::BASE64_HEADER . base64_encode($maskData),
80 | 'maskPath' => $mask['path'],
81 | 'maskLeft' => $maskPosition['left'],
82 | 'maskTop' => $maskPosition['top'],
83 | 'maskViewBox' => $mask['viewBox'],
84 | //Better mask drag to target location display
85 | 'maskWH' => $this->maskWH - 3,
86 |
87 | ]
88 | ];
89 | }
90 |
91 | /**
92 | * Calculate the position, drag the target $dst, and drag the initial position of the mask, Temporary random
93 | * @return array[]
94 | */
95 | private function getPosition(): array
96 | {
97 | $dst = [
98 | 'left' => rand( 0, $this->bgWidth - $this->maskWH),
99 | 'top' => rand( 0, $this->bgHeight - $this->maskWH),
100 | ];
101 |
102 | $mask = [
103 | 'left' => rand( 0, $this->bgWidth - $this->maskWH),
104 | 'top' => rand( 0, $this->bgHeight - $this->maskWH),
105 | ];
106 |
107 | return [$dst, $mask];
108 | }
109 |
110 | /**
111 | * @param array $dst ['left' => 160, 'top' => 50]
112 | * @param array $mask ['left' => 162, 'top' => 51]
113 | * @param int $offset 3 verify offset default
114 | * @return bool
115 | */
116 | public static function verify(array $dst, array $mask, int $offset = 3): bool
117 | {
118 | if (! isset($mask['left'], $mask['top'])) {
119 | return false;
120 | }
121 | $answerLeft = $dst['left'];
122 | $answerTop = $dst['top'];
123 |
124 | $dataLeft = (int)$mask['left'];
125 | $dataTop = (int)$mask['top'];
126 |
127 | if (abs($answerLeft - $dataLeft) < $offset && abs($answerTop - $dataTop) < $offset) {
128 | return true;
129 | }
130 |
131 | return false;
132 | }
133 | }
--------------------------------------------------------------------------------
/README-en.md:
--------------------------------------------------------------------------------
1 | # Drag-Captcha
2 |
3 | Drag-and-drop graphics verification, easy to use. [中文](./README.md)\
4 | `composer require rlofls/drag-captcha`
5 |
6 | 
7 | 
8 |
9 | - [features](#features)
10 | - [custom background](#custom-bg)
11 | - [Enable Interference Confuse](#confuse)
12 | - [run example](#run-demo)
13 | - [practice](#practice)
14 | - [api](#api)
15 |
16 | ## features
17 |
18 | ### custom background
19 |
20 | First set the value of the `Resources::$customBg` array, which must be a **png** image, which stores the image path. eg:
21 |
22 | ```php
23 | //Set custom background
24 | Resources::$customBg = [
25 | __DIR__ . '/customBg/1.png',
26 | __DIR__ . '/customBg/2.png',
27 | __DIR__ . '/customBg/3.png',
28 | //...
29 | ];
30 | ```
31 |
32 | ### Enable Interference Confuse
33 |
34 | It can make the target less obvious, and the target will be expanded/cut randomly. When calling the `generate()` method, you can pass in the `true` value. eg: `$drag->generate(true);`
35 |
36 | - expand:
37 |
38 | 
39 | - cut
40 |
41 | 
42 | ## run the example
43 |
44 | 1. Switch to this directory
45 | 2. `composer install`
46 | 3. `php -S 127.0.0.1:8087`
47 | 4. Browser access `http://127.0.0.1:8087`
48 |
49 | ## practice
50 |
51 | Reference `index.php` `index.html` \
52 | Copy `dragCaptcha.css` `dragCaptcha.js` to your own project application \
53 | Reference html
54 | ```html
55 |
56 |
57 |
58 |
59 |
91 |
92 | ```
93 |
94 | ## api
95 |
96 | - `Rlofls\DragCaptcha\Drag`
97 | - `generate()` generates rendering data `dst, front`
98 | - `verify()` to verify matching results
99 |
100 | - Server api requirements, Content-Type: application/json
101 | - `/dragData`---[GET]---Get verification code data\
102 | Response data format:
103 | ```json
104 | {
105 | "status": "success" // error: express fail
106 | "data": {
107 | //One-time verification transaction ID generated by the user
108 | "cid": "drag-captcha63c0c18566074"
109 | //The following data by Rlofls\DragCaptcha\Drag::generate generated
110 | "bgBase64": "data:image/png;base64,iV..."
111 | "bgH": 160
112 | "bgW": 250
113 | "maskBase64": "data:image/png;base64,..."
114 | "maskLeft": 114
115 | "maskPath": "M 15.27429..."
116 | "maskTop": 0
117 | "maskViewBox": 15.875
118 | "maskWH": 57
119 | }
120 |
121 | }
122 | ```
123 | - `/dragVerify`---[POST]---Perform verification code verification\
124 | Request parameters
125 | ```json
126 | {
127 | "cid": "drag-captcha63c0c1881db17"
128 | "mask": {
129 | "left": 149,
130 | "top": 76
131 | }
132 | }
133 | ```
134 | Response data format:
135 | ```json
136 | {"status": "error"}
137 | ```
138 |
139 | ## todo
140 |
141 | - Welcome to submit `issue`
142 |
--------------------------------------------------------------------------------
/src/Confuse/ConfuseCut.php:
--------------------------------------------------------------------------------
1 | dst = $dst;
58 | $this->mask = $mask;
59 | $this->imgMask = $imgMask;
60 | $this->imgDst = $imgDst;
61 |
62 | $this->maskWH = imagesx($imgMask);
63 |
64 | $this->initCutPoint();
65 | $this->initCurDir();
66 | $this->initBLEAB();
67 | }
68 |
69 | /**
70 | * @inheritDoc
71 | */
72 | public function swapPixels(): void
73 | {
74 | Utils::swapAndDeepMask($this->imgDst, $this->imgMask, $this->dst, function ($x, $y, $tx, $ty, $tRgb) {
75 |
76 | $color = imagecolorallocate($this->imgMask, $tRgb['red'], $tRgb['green'], $tRgb['blue']);
77 | imagesetpixel($this->imgMask, $x, $y, $color);
78 |
79 | if ($this->shouldSwap($x, $y) === false) {
80 | return;
81 | }
82 | $tColor = Utils::deepColor($this->imgDst, $tRgb);
83 | imagesetpixel($this->imgDst, $tx, $ty, $tColor);
84 | });
85 | }
86 |
87 | /**
88 | * @param $x
89 | * @param $y
90 | * @return bool
91 | */
92 | private function shouldSwap($x, $y): bool
93 | {
94 | [$cx, $cy] = $this->cutPoint;
95 | $bleY = $this->bleA * $x + $this->bleB;
96 | switch ($this->curDir) {
97 | case self::DIR_LT:
98 | case self::DIR_TR:
99 | return $y > $bleY;
100 | case self::DIR_RB:
101 | case self::DIR_BL:
102 | return $y < $bleY;
103 | case self::DIR_TB:
104 | return $x < $cx;
105 | case self::DIR_LR:
106 | return $y < $cy;
107 | }
108 | return true;
109 | }
110 |
111 | private function initBLEAB(): void
112 | {
113 | [$cx, $cy] = $this->cutPoint;
114 | switch ($this->curDir) {
115 | case self::DIR_LT:
116 | //$cy = 0 * a + b
117 | //0 = $cx * a + b
118 | $this->bleA = -$cy/$cx;
119 | $this->bleB = $cy;
120 | break;
121 | case self::DIR_TR:
122 | // 0 = $cx * a + b
123 | // $cy = $wh * a + b
124 | //
125 | // $cy = ($wh - $cx) * a => a = ($wh - $cx) / $cy ; $b = - $cx * ($wh - $cx) / $cy
126 | $this->bleA = ($this->maskWH - $cx) / $cy;
127 | $this->bleB = -$cx * $this->bleA;
128 | break;
129 | case self::DIR_RB:
130 | //$cx = $wh * a + b
131 | //$wh = $cy * a + b
132 | //=> a = ($wh - $cx) / ($cy - $wh); b = $cx - $wh * a
133 | $this->bleA = ($this->maskWH - $cx) / ($cy - $this->maskWH);
134 | $this->bleB = $cx - $this->maskWH * $this->bleA;
135 | break;
136 | case self::DIR_BL:
137 | //$wh = $cx * a + b
138 | //$cy = 0 * a + b
139 | $this->bleA = ($this->maskWH - $cy) / $cx;
140 | $this->bleB = $cy;
141 | break;
142 | }
143 | }
144 |
145 | /**
146 | * @throws \Exception
147 | */
148 | private function initCutPoint(): void
149 | {
150 | $min = (int)($this->maskWH * 0.4);
151 | $max = (int)($this->maskWH * 0.6);
152 | $this->cutPoint = [
153 | random_int($min, $max),
154 | random_int($min, $max)
155 | ];
156 | }
157 |
158 | /**
159 | * @throws \Exception
160 | */
161 | public function initCurDir(): void
162 | {
163 | $this->curDir = Utils::randValue(self::DIR_ARR);
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/dragCaptcha.css:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of drag-captcha.
3 | *
4 | * Licensed under The MIT License
5 | *
6 | * @author rlofls
7 | */
8 | .dc-captcha {
9 | position: fixed;
10 | left: 0px;
11 | top: 0px;
12 | background-color: #00000000;
13 | height: 100%;
14 | width: 100%;
15 | display: none;
16 | place-items: center;
17 | user-select: none;
18 | }
19 |
20 | .dc-content {
21 | background-color: #f5f8fa;
22 | width: auto;
23 | height: auto;
24 | z-index: 999;
25 | box-sizing: border-box;
26 | padding: 9px;
27 | border-radius: 6px;
28 | box-shadow: 0 0 11px 0 #999999;
29 | position: relative;
30 | overflow: hidden;
31 | }
32 |
33 | .dc-title {
34 | margin: 0;
35 | padding: 0;
36 | height: 30px;
37 | font-size: medium;
38 | color: #0000009e;
39 | text-align: left;
40 | line-height: 30px;
41 | letter-spacing: 2px;
42 | font-weight: 600;
43 | border-radius: 5px;
44 | position: relative
45 | }
46 |
47 | .dc-action {
48 | padding-top: 7px;
49 | position: relative
50 | }
51 |
52 | .dc-body {
53 | width: 100%;
54 | height: auto;
55 | position: relative;
56 | overflow: hidden;
57 | }
58 |
59 | .dc-body-bg {
60 | margin: 0;
61 | padding: 0;
62 | border-radius: 5px;
63 | -webkit-user-drag: none;
64 | }
65 |
66 | .dc-body-mask {
67 | position: absolute;
68 | z-index: 100;
69 | -webkit-user-drag: none;
70 | }
71 |
72 | .dc-body-mask:hover {
73 | cursor:move
74 | }
75 |
76 | .dc-body-mask-svg {
77 | position: absolute;
78 | fill: none;
79 | stroke: Yellow;
80 | fill:none;
81 | stroke-width: 1.2;
82 | z-index: 50;
83 | }
84 |
85 | .dc-body-mask-svg:hover {
86 | stroke: LightYellow;
87 | }
88 |
89 | .dc-body-tip {
90 | position: absolute;
91 | font-size: 14px;
92 | line-height: 30px;
93 | text-align: center;
94 | width: 100%;
95 | border-radius: 0 0 5px 5px;
96 | font-weight: 700;
97 | bottom: -30px;
98 | color: #fff;
99 | z-index: 200;
100 | letter-spacing: 2px;
101 | }
102 |
103 | .dc-body-mask-path {}
104 |
105 | .dc-icon {
106 | fill:none;
107 | fill-rule:evenodd;
108 | stroke:#000000;
109 | stroke-width:0.6;
110 | stroke-linecap:round;
111 | stroke-linejoin:round;
112 | stroke-miterlimit:0;
113 | stroke-opacity:0.47;
114 | paint-order:stroke;
115 | margin-right: 7px;
116 | }
117 |
118 | .dc-icon:hover {
119 | stroke-opacity:0.67;
120 | }
121 |
122 |
123 | .dc-ani-close {
124 | animation: aniClose .5s ease-in-out both;
125 | }
126 | .dc-ani-open {
127 | animation: aniOpen .5s ease-in-out both, aniOpenBg .4s ease-out both .4s;
128 | }
129 |
130 | @keyframes aniClose {
131 | to {
132 | opacity: 0;
133 | transform: scale(0.3);
134 | }
135 | }
136 |
137 | @keyframes aniOpen {
138 | from {
139 | opacity: 0;
140 | transform: scale(0.3);
141 | }
142 | to {
143 | opacity: 1;
144 | transform: scale(1);
145 | }
146 | }
147 |
148 | @keyframes aniOpenBg {
149 | to {
150 | background-color: #00000030;
151 | }
152 | }
153 |
154 | .dc-ani-left-hide {
155 | animation: aniLeftHide ease-in .5s both;
156 | }
157 |
158 | .dc-ani-right-show {
159 | animation: aniRightShow ease-out .5s forwards;
160 | }
161 |
162 | .dc-ani-text-show {
163 | animation: aniTextShow ease-out .5s forwards;
164 | }
165 |
166 | @keyframes aniTextShow {
167 | 0% {
168 |
169 | transform: scale(0.9);
170 | }
171 | 30% {
172 |
173 | transform: scale(0.9);
174 | }
175 | 100% {
176 | transform: scale(1);
177 | }
178 | }
179 |
180 | @keyframes aniLeftHide {
181 | 0% {
182 | transform: scale(1) translateZ(0);
183 | }
184 | 30% {
185 | transform: scale(0.95) translateZ(0);
186 | }
187 | 100% {
188 | transform: scale(0.95) translate3d(-120%,0,0);
189 | }
190 | }
191 | @keyframes aniRightShow {
192 | 0% {
193 |
194 | transform: scale(0.95) translate3d(120%,0,0);
195 | }
196 | 80% {
197 |
198 | transform: scale(0.95) translateZ(0);
199 | }
200 | 100% {
201 | transform: scale(1) translateZ(0);
202 | }
203 | }
204 |
205 | .dc-ani-shake {
206 | animation: aniShakeX .5s;
207 |
208 | }
209 |
210 | @keyframes aniShadowSuccess {
211 | 70% {
212 | box-shadow: 0 0 11px 5px #4eee53
213 | }
214 | to {
215 | box-shadow: 0 0 11px 0 #999999;
216 | }
217 | }
218 |
219 | @keyframes aniShadowFail {
220 | 70% {
221 | box-shadow: 0 0 11px 5px #f44336
222 | }
223 | to {
224 | box-shadow: 0 0 11px 0 #999999;
225 | }
226 | }
227 |
228 | @keyframes aniShakeX {
229 | from,
230 | to {
231 | transform: translate3d(0, 0, 0);
232 | }
233 | 10%,
234 | 40%,
235 | 70% {
236 | transform: translate3d(-0.3rem, 0, 0);
237 | }
238 | 20%,
239 | 60%,
240 | 90% {
241 | transform: translate3d(0.3rem, 0, 0);
242 | }
243 | }
244 |
245 | .dc-ani-success-tip {
246 | background-color: #64e768;
247 | animation: aniTip 1s linear;
248 | }
249 |
250 | .dc-ani-fail-tip {
251 | background-color: #f34c40;
252 | animation: aniTip 1s linear;
253 | }
254 |
255 | @keyframes aniTip {
256 |
257 | 50% {
258 | bottom: 0px
259 | }
260 | 80% {
261 | bottom: 0px
262 | }
263 | 100% {
264 | bottom: -30px;
265 | }
266 | }
--------------------------------------------------------------------------------
/dragCaptcha.js:
--------------------------------------------------------------------------------
1 | export {Utils, DragCaptcha}
2 |
3 | /**
4 | * This file is part of drag-captcha.
5 | *
6 | * Licensed under The MIT License
7 | *
8 | * @author rlofls
9 | */
10 | let Lang = {
11 | 'zh': {
12 | 'move': '拖拽图标到目标位置',
13 | 'success': '验证成功',
14 | 'fail': '验证失败'
15 | },
16 | 'en': {
17 | 'move': 'Drag mask to the target',
18 | 'success': 'verify successfully',
19 | 'fail': 'verify failed'
20 | }
21 | }
22 |
23 |
24 | let Utils = {
25 | appendChildren: function(parent, ...children) {
26 | children.forEach((c) => parent.appendChild(c))
27 | },
28 | doActionByClass: function(className, cb = null) {
29 | let e = document.getElementsByClassName(className).item(0);
30 | if (e instanceof Element && cb) {
31 | cb(e)
32 | }
33 | return e;
34 | },
35 | setStyles: function(node, valObj) {
36 | Object.keys(valObj).forEach((k) => {
37 | node.style[k] = valObj[k]
38 | });
39 | },
40 | setAttrs: function(node, valObj) {
41 | Object.keys(valObj).forEach((k) => {
42 | node.setAttribute(k, valObj[k])
43 | });
44 | },
45 | createElement: function(tagName, attrs = {}, styles = {}) {
46 | let e = document.createElement(tagName)
47 | Utils.setAttrs(e, attrs)
48 | Utils.setStyles(e, styles)
49 | return e
50 | },
51 | createSvg: function (attrs = {}, styles = {}) {
52 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
53 | Utils.setAttrs(svg, attrs)
54 | Utils.setStyles(svg, styles)
55 | return svg
56 | },
57 | createPath: function (attrs = {}, styles = {}) {
58 | let path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
59 | Utils.setAttrs(path, attrs)
60 | Utils.setStyles(path, styles)
61 | return path
62 | },
63 | createIcon: function(svg, path) {
64 | let s = Utils.createSvg(svg.attrs, svg.styles);
65 | let p = Utils.createPath(path.attrs, svg.styles);
66 | s.appendChild(p)
67 | return s
68 | },
69 | request: function(method, url, data = null, successFunc = null, errorFunc = null) {
70 | let xhr = new XMLHttpRequest();
71 | xhr.open(method, url, true);
72 | xhr.responseType = 'json';
73 | xhr.setRequestHeader('Content-Type', 'application/json');
74 | xhr.onreadystatechange = function(){
75 | if (xhr.readyState === 4){
76 | if (xhr.status === 200){
77 | if (successFunc && successFunc instanceof Function) {
78 | successFunc(xhr.response);
79 | }
80 | } else {
81 | if (errorFunc && errorFunc instanceof Function) {
82 | errorFunc(xhr.status);
83 | }
84 | }
85 | }
86 | };
87 | xhr.onerror = function (e) {
88 | if (errorFunc && errorFunc instanceof Function) {
89 | errorFunc(e);
90 | }
91 | };
92 | xhr.send(data)
93 |
94 | },
95 |
96 | bind: function(obj, func)
97 | {
98 | return function()
99 | {
100 | return func.apply(obj, arguments);
101 | };
102 | },
103 | }
104 |
105 | /**
106 | *
107 | * @param {Element} btn
108 | * @param {String} lang
109 | */
110 | function DragCaptcha(btn) {
111 | this.btn = btn
112 | this.init();
113 | this.initEventListener();
114 |
115 | this.addClickEventListener();
116 |
117 | }
118 |
119 | /**
120 | * 语言 zh:中文 en: 英文
121 | */
122 | DragCaptcha.prototype.lang = 'zh'
123 |
124 | /**
125 | * 开始调式 会打印日志
126 | */
127 | DragCaptcha.prototype.debug = true
128 |
129 | /**
130 | * 验证码根 node
131 | */
132 | DragCaptcha.prototype.rootNode = null
133 |
134 | /**
135 | * 一次验证 事务 id
136 | */
137 | DragCaptcha.prototype.cid = ''
138 |
139 | /**
140 | * 验证码 获取数据接口 url get
141 | * api 返回数据格式 json
142 | * {
143 | * "status": "success" // error 表示失败
144 | * "data": {
145 | * "bgBase64: "...",
146 | * 'bgW': 200,
147 | * 'bgH': 160,
148 | * 'maskBase64': "...",
149 | * 'maskPath': "...",
150 | * 'maskLeft': 10,
151 | * 'maskTop': 100,
152 | * 'maskViewBox': 15.875,
153 | * 'maskWH': 60
154 | * }
155 | * }
156 | */
157 | DragCaptcha.prototype.apiDataUrl = '/dragData'
158 |
159 | /**
160 | * 验证码 进行验证接口 url post
161 | * api 返回数据格式 json
162 | * {
163 | * "status": "success" // error 表示失败
164 | * }
165 | */
166 | DragCaptcha.prototype.apiVerifyUrl = '/dragVerify'
167 |
168 | /**
169 | * 验证码进行验证 成功 回调函数
170 | */
171 | DragCaptcha.prototype.cbSuccess = null
172 |
173 | /**
174 | * 验证码进行验证 失败 回调函数
175 | */
176 | DragCaptcha.prototype.cbFail = null
177 |
178 |
179 | DragCaptcha.prototype.call = function (cb, ...args) {
180 | if (cb instanceof Function) {
181 | cb(this, args);
182 | }
183 | }
184 |
185 | DragCaptcha.prototype.addClickEventListener = function() {
186 | this.btn.addEventListener('click', this.eventClickListener);
187 | }
188 |
189 | /**
190 | * 渲染验证码
191 | */
192 | DragCaptcha.prototype.render = function() {
193 | let that = this
194 |
195 | Utils.request('GET', that.apiDataUrl, null, function(response) {
196 | if (response.status && response.status == 'success') {
197 | that.cid = response.data.cid
198 | that.log('cid:', that.cid)
199 | that.showAni(response.data)
200 | that.log("render complete")
201 | return
202 | }
203 | that.log('render 失败', response)
204 | }, function(error) {
205 | that.log('render 失败', error)
206 | })
207 | }
208 |
209 | /**
210 | * 验证
211 | */
212 | DragCaptcha.prototype.verify = function(data) {
213 | this.log('verify', data)
214 | let that = this
215 |
216 | let postData = {
217 | cid: this.cid,
218 | mask: data
219 | }
220 | Utils.request('POST', this.apiVerifyUrl, JSON.stringify(postData), function(response) {
221 | if (response.status && response.status == 'success') {
222 | that.aniVerifySuccess()
223 | setTimeout(() => {
224 | that.log('verify success', response)
225 | that.close();
226 | that.btn.removeEventListener('click', that.eventClickListener)
227 | that.call(that.cbSuccess)
228 | }, 1000)
229 | return;
230 | }
231 | that.aniVerifyFail()
232 | setTimeout(() => {
233 | that.log("verify 失败")
234 | that.call(that.cbFail)
235 |
236 | that.render()
237 | }, 1000)
238 |
239 | }, function (error) {
240 | that.aniVerifyFail()
241 | setTimeout(() => {
242 | that.log("verify 失败", error)
243 | that.call(that.cbFail)
244 |
245 | that.render()
246 | }, 1000)
247 |
248 | })
249 | }
250 |
251 | DragCaptcha.prototype.pathClose = 'M 0.8152883,3.8174125 A 2.168649,2.168649 0 0 1 1.5133233,0.83183563 2.168649,2.168649 0 0 1 4.4992823,1.5282345 2.168649,2.168649 0 0 1 3.8045197,4.5145746 2.168649,2.168649 0 0 1 0.81779934,3.8214486 M 3.2991312,2.0071143 C 1.9825694,3.3246797 1.9787213,3.3285308 1.9787213,3.3285308 m -2.886e-4,-1.317938 c 1.3165617,1.3175655 1.3204098,1.3214166 1.3204098,1.3214166'
252 | DragCaptcha.prototype.pathRefresh = 'M 4.5838557,1.8853027 H 3.4135916 M 4.6032021,0.71263702 V 1.882927 M 4.4200613,1.7607213 A 2.0031751,1.9692227 3.8521096 0 0 2.4349437,0.73989014 2.0031751,1.9692227 3.8521096 0 0 0.73109426,2.1629366 2.0031751,1.9692227 3.8521096 0 0 1.4358413,4.2531269 2.0031751,1.9692227 3.8521096 0 0 3.6692781,4.4007701'
253 |
254 | /**
255 | * 初始化 验证码 elements
256 | */
257 | DragCaptcha.prototype.init = function () {
258 | this.log("drag capthca init")
259 |
260 | if (this.rootNode instanceof Element) {
261 | this.rootNode.parentNode.removeChild(this.rootNode)
262 | }
263 |
264 | this.rootNode = Utils.createElement('div', {
265 | "id": "dc-captcha",
266 | "class": "dc-captcha"
267 | })
268 |
269 | let content = Utils.createElement('div', {"class": "dc-content"})
270 |
271 | let title = Utils.createElement('div', {"class": "dc-title"})
272 | title.innerHTML = Lang[this.lang]['move']
273 |
274 | let body = Utils.createElement('div', {"class": "dc-body"})
275 | let bodyBg = Utils.createElement('img', {"class": "dc-body-bg"})
276 | let bodyMask = Utils.createElement('img', {"class": "dc-body-mask"})
277 | let bodyMaskSvg = Utils.createIcon({attrs: {"class": "dc-body-mask-svg"}, styles: {}}, {attrs: {"class": "dc-body-mask-path"}, styles: {}})
278 | let bodyTip = Utils.createElement('div', {"class": "dc-body-tip"})
279 | Utils.appendChildren(body, bodyBg, bodyMask, bodyMaskSvg, bodyTip)
280 |
281 | let action = Utils.createElement('div', {"class": "dc-action"})
282 | let svg = {
283 | attrs: {
284 | 'draggable': 'false',
285 | 'viewBox': '0 0 5.2916665 5.2916666',
286 | 'class': 'dc-icon'
287 | },
288 | styles: {
289 | 'width': '23px',
290 | 'height': '23px',
291 | }
292 | }
293 | let actionClose = Utils.createIcon(svg, {attrs: { d: this.pathClose}, styles: {}})
294 | let actionRefresh = Utils.createIcon(svg, {attrs: { d: this.pathRefresh}, styles: {}})
295 | Utils.appendChildren(action, actionClose, actionRefresh)
296 |
297 | Utils.appendChildren(content, title, body, action)
298 | this.rootNode.appendChild(content)
299 |
300 | document.body.appendChild(this.rootNode)
301 |
302 | actionClose.addEventListener('click', Utils.bind(this, this.close))
303 | actionRefresh.addEventListener('click', Utils.bind(this, this.render))
304 | }
305 |
306 | /**
307 | * 初始化 事件 cb
308 | */
309 | DragCaptcha.prototype.initEventListener = function () {
310 | this.eventClickListener = Utils.bind(this, this.render)
311 | this.eventMaskDownListener = Utils.bind(this, this.maskMouseDown)
312 | this.eventMaskMoveListener = Utils.bind(this, this.maskMouseMove)
313 | this.eventMaskUpListener = Utils.bind(this, this.maskMouseUp)
314 | }
315 |
316 |
317 | DragCaptcha.prototype.bgW = 200
318 | DragCaptcha.prototype.bgH = 160
319 |
320 | /**
321 | * 重置验证码
322 | * data:
323 | */
324 | DragCaptcha.prototype.reset = function({
325 | bgBase64,
326 | bgW = 200,
327 | bgH = 160,
328 | maskBase64,
329 | maskPath,
330 | maskLeft,
331 | maskTop,
332 | maskViewBox = 15.875,
333 | maskWH = 60 }) {
334 |
335 | this.bgW = bgW
336 | this.bgH = bgH
337 | this.maskWH = maskWH
338 | this.maskLeft = maskLeft
339 | this.maskTop = maskTop
340 |
341 | this.log("dc reset")
342 | Utils.doActionByClass('dc-body', e => {
343 | Utils.setStyles(e, {
344 | 'width': bgW + 'px',
345 | 'height': bgH + 'px'
346 | });
347 | })
348 | Utils.doActionByClass('dc-body-bg', e => {
349 | Utils.setStyles(e, {
350 | 'width': bgW + 'px',
351 | 'height': bgH + 'px'
352 | });
353 | Utils.setAttrs(e, {'src': bgBase64 });
354 | })
355 | Utils.doActionByClass('dc-body-mask', e => {
356 | Utils.setStyles(e, {
357 | 'width': maskWH + 'px',
358 | 'height': maskWH + 'px',
359 | 'left': maskLeft + 'px',
360 | 'top': maskTop + 'px'
361 | });
362 | Utils.setAttrs(e, {'src': maskBase64});
363 | })
364 | Utils.doActionByClass('dc-body-mask-path', e => {
365 | Utils.setAttrs(e, {'d': maskPath})
366 | })
367 | Utils.doActionByClass('dc-body-mask-svg', e => {
368 | e.style.removeProperty('stroke')
369 | e.style.removeProperty('transform')
370 | Utils.setAttrs(e, {'viewBox': '0 0 ' + maskViewBox + ' ' + maskViewBox});
371 | Utils.setStyles(e, {
372 | 'width': maskWH + 'px',
373 | 'height': maskWH + 'px',
374 | 'left': maskLeft + 'px',
375 | 'top': maskTop + 'px'
376 | });
377 | })
378 |
379 | Utils.doActionByClass('dc-content', e => e.classList.remove('dc-ani-shake'))
380 | Utils.doActionByClass('dc-body-tip', e => {
381 | e.classList.remove('dc-ani-success-tip')
382 | e.classList.remove('dc-ani-fail-tip')
383 | })
384 |
385 |
386 | this.addMaskListeners()
387 | }
388 |
389 | DragCaptcha.prototype.maskMouseDown = function(evt) {
390 | this.log('drag down')
391 | this.isDraging = true
392 |
393 | //moveEvent
394 | if (evt instanceof TouchEvent) {
395 | let touch = evt.touches.item(0)
396 | this.moveX = touch.clientX
397 | this.moveY = touch.clientY
398 | }
399 |
400 | this.log('drag down end')
401 | Utils.doActionByClass('dc-body-mask-svg', e => {
402 | e.style.transform = 'scale(1.1)'
403 | e.style.opacity = '0.5'
404 | })
405 | }
406 |
407 | /**
408 | *
409 | * @param {} evt
410 | */
411 | DragCaptcha.prototype.getMoveOffset = function(evt) {
412 |
413 |
414 |
415 | if (evt instanceof MouseEvent) {
416 | return {offsetX: evt.movementX, offsetY: evt.movementY}
417 | }
418 |
419 | if (evt instanceof TouchEvent) {
420 | let touch = evt.touches.item(0)
421 | let x = touch.clientX - this.moveX
422 | let y = touch.clientY - this.moveY
423 | this.moveX = touch.clientX
424 | this.moveY = touch.clientY
425 | return {offsetX: x, offsetY: y}
426 | }
427 | }
428 |
429 | DragCaptcha.prototype.maskMouseMove = function(evt) {
430 | if (! this.isDraging) {
431 | return;
432 | }
433 |
434 | let maskSvg = Utils.doActionByClass('dc-body-mask-svg')
435 | let mask = Utils.doActionByClass('dc-body-mask')
436 |
437 | let {offsetX, offsetY} = this.getMoveOffset(evt)
438 | let oriL = parseInt(mask.style.left);
439 | let abLeft = (isNaN(oriL) ? this.maskLeft : oriL) + offsetX
440 |
441 | let oriT = parseInt(mask.style.top);
442 | let abTop = (isNaN(oriT) ? this.maskTop : oriT) + offsetY
443 |
444 | let dstL = abLeft + 'px';
445 | let dstT = abTop + 'px';
446 |
447 | let clt = - this.maskWH / 2;
448 | let cr = this.bgW - this.maskWH / 2;
449 | let cb = this.bgH - this.maskWH / 2
450 | if (abLeft < clt) {
451 | dstL = clt + 'px';
452 | }
453 | if (abLeft > cr) {
454 | dstL = cr + 'px';
455 | }
456 | if (abTop < clt) {
457 | dstT = clt + 'px';
458 | }
459 | if (abTop > cb) {
460 | dstT = cb + 'px';
461 | }
462 |
463 | let pos = {
464 | 'left': dstL,
465 | 'top': dstT
466 | };
467 | Utils.setStyles(mask, pos);
468 | Utils.setStyles(maskSvg, pos);
469 | }
470 |
471 | DragCaptcha.prototype.maskMouseUp = function(evy) {
472 |
473 | this.log('drag up')
474 | if (! this.isDraging) {
475 | return;
476 | }
477 | this.isDraging = false
478 |
479 | let maskSvg = Utils.doActionByClass('dc-body-mask-svg', e => {
480 | //e.style.stroke = "DarkSlateGray"
481 | e.style.transform = 'scale(1.1)'
482 | e.style.opacity = '1'
483 |
484 | })
485 |
486 | let l = parseInt(maskSvg.style.left)
487 | let t = parseInt(maskSvg.style.top)
488 | this.verify({
489 | 'left': l,
490 | 'top': t
491 | });
492 |
493 | this.removeMaskListeners();
494 | }
495 |
496 | DragCaptcha.prototype.addMaskListeners = function () {
497 | Utils.doActionByClass('dc-body-mask', e => {
498 | e.addEventListener('mousedown', this.eventMaskDownListener)
499 | e.addEventListener('mousemove', this.eventMaskMoveListener)
500 | e.addEventListener('mouseup', this.eventMaskUpListener)
501 | e.addEventListener('mouseleave', this.eventMaskUpListener)
502 |
503 | e.addEventListener('touchstart', this.eventMaskDownListener)
504 | e.addEventListener('touchmove', this.eventMaskMoveListener)
505 | e.addEventListener('touchend', this.eventMaskUpListener)
506 | })
507 | }
508 |
509 | DragCaptcha.prototype.removeMaskListeners = function () {
510 | Utils.doActionByClass('dc-body-mask', e => {
511 | e.removeEventListener('mousedown', this.eventMaskDownListener)
512 | e.removeEventListener('mousemove', this.eventMaskMoveListener)
513 | e.removeEventListener('mouseup', this.eventMaskUpListener)
514 | e.removeEventListener('mouseleave', this.eventMaskUpListener)
515 |
516 | e.removeEventListener('touchstart', this.eventMaskDownListener)
517 | e.removeEventListener('touchmove', this.eventMaskMoveListener)
518 | e.removeEventListener('touchend', this.eventMaskUpListener)
519 | })
520 | }
521 |
522 | /**
523 | * 显示 验证码
524 | */
525 | DragCaptcha.prototype.show = function() {
526 |
527 | this.log("dc show")
528 | Utils.doActionByClass('dc-captcha', e => {
529 | e.classList.remove("dc-ani-close")
530 | e.classList.add("dc-ani-open")
531 | e.style.display = "grid"
532 | })
533 | }
534 |
535 | DragCaptcha.prototype.hasShowAni = false
536 |
537 | /**
538 | * 显示动画
539 | */
540 | DragCaptcha.prototype.showAni = function(data) {
541 |
542 | this.hasShowAni && this.aniLeftHide()
543 |
544 | let that = this
545 | setTimeout(() => {
546 | that.reset(data)
547 | that.show()
548 | that.hasShowAni && that.aniRightShow()
549 | if (that.hasShowAni == false) { that.hasShowAni = true }
550 | }, this.hasShowAni ? 600 : 0);
551 | }
552 |
553 | DragCaptcha.prototype.aniVerifySuccess = function () {
554 | let that = this
555 | Utils.doActionByClass('dc-body-tip', e => {
556 | e.innerHTML = Lang[that.lang].success
557 | e.classList.add('dc-ani-success-tip')
558 | })
559 | Utils.doActionByClass('dc-body-mask-svg', e => e.style.stroke = "#4eee53")
560 | }
561 |
562 | DragCaptcha.prototype.aniVerifyFail = function () {
563 | let that = this
564 | Utils.doActionByClass('dc-content', e => {
565 | e.classList.add('dc-ani-shake')
566 | })
567 | Utils.doActionByClass('dc-body-tip', e => {
568 | e.innerHTML = Lang[that.lang].fail
569 | e.classList.add('dc-ani-fail-tip')
570 | })
571 | Utils.doActionByClass('dc-body-mask-svg', e => e.style.stroke = "#f44336")
572 | }
573 |
574 | DragCaptcha.prototype.aniLeftHide = function() {
575 | let remove = e => {
576 | e.classList.remove('dc-ani-right-show')
577 | e.classList.remove('dc-ani-text-show')
578 |
579 | e.classList.add("dc-ani-left-hide")
580 | }
581 | Utils.doActionByClass('dc-title', remove)
582 | Utils.doActionByClass('dc-body', remove)
583 | }
584 |
585 | DragCaptcha.prototype.aniRightShow = function() {
586 | Utils.doActionByClass('dc-title', e => {
587 | e.classList.remove("dc-ani-left-hide")
588 | e.classList.add('dc-ani-text-show')
589 | })
590 | Utils.doActionByClass('dc-body', e => {
591 | e.classList.remove("dc-ani-left-hide")
592 | e.classList.add('dc-ani-right-show')
593 | })
594 | }
595 |
596 |
597 |
598 | /**
599 | * 关闭验证码
600 | */
601 | DragCaptcha.prototype.close = function() {
602 | this.log('dc close')
603 | Utils.doActionByClass('dc-captcha', e => {
604 | e.classList.remove("dc-ani-open")
605 | e.style.backgroundColor = "#00000000"
606 | e.classList.add("dc-ani-close")
607 | setTimeout(() => {
608 | e.style.display = "none"
609 | }, 500);
610 | })
611 | }
612 |
613 | /**
614 | * 日志打印
615 | */
616 | DragCaptcha.prototype.log = function (...args) {
617 | if (this.debug) {
618 | console.log(args);
619 | }
620 | }
621 |
--------------------------------------------------------------------------------