├── .gitignore ├── LICENSE ├── README-en.md ├── README.md ├── composer.json ├── customBg ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png └── 6.png ├── docs ├── confuse-cut.png ├── confuse-expand.png ├── drag-en.gif ├── drag-en.png ├── drag-zh.gif └── drag-zh.png ├── dragCaptcha.css ├── dragCaptcha.js ├── index.html ├── index.php └── src ├── Confuse ├── ConfuseCut.php ├── ConfuseExpand.php └── ConfuseInterface.php ├── Drag.php ├── Maker.php ├── Resources.php ├── Resources ├── bg │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ └── 5.png └── mask │ ├── circle.png │ ├── star.png │ └── triangle.png └── Utils.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | .vscode 4 | .idea 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 RLOFLS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /customBg/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/customBg/1.png -------------------------------------------------------------------------------- /customBg/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/customBg/2.png -------------------------------------------------------------------------------- /customBg/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/customBg/3.png -------------------------------------------------------------------------------- /customBg/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/customBg/4.png -------------------------------------------------------------------------------- /customBg/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/customBg/5.png -------------------------------------------------------------------------------- /customBg/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/customBg/6.png -------------------------------------------------------------------------------- /docs/confuse-cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/docs/confuse-cut.png -------------------------------------------------------------------------------- /docs/confuse-expand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/docs/confuse-expand.png -------------------------------------------------------------------------------- /docs/drag-en.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/docs/drag-en.gif -------------------------------------------------------------------------------- /docs/drag-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/docs/drag-en.png -------------------------------------------------------------------------------- /docs/drag-zh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/docs/drag-zh.gif -------------------------------------------------------------------------------- /docs/drag-zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/docs/drag-zh.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |