├── .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 | ![show](./docs/drag-en.png) 7 | ![Example](./docs/drag-en.gif) 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 | ![expand](./docs/confuse-expand.png) 39 | - cut 40 | 41 | ![expand](./docs/confuse-cut.png) 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 | ![show](./docs/drag-zh.png) 7 | ![示例](./docs/drag-zh.gif) 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 | ![expand](./docs/confuse-expand.png) 38 | - cut 39 | 40 | ![expand](./docs/confuse-cut.png) 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 | 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/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 | -------------------------------------------------------------------------------- /src/Confuse/ConfuseExpand.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/Confuse/ConfuseInterface.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 | } -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /src/Resources/bg/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/src/Resources/bg/1.png -------------------------------------------------------------------------------- /src/Resources/bg/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/src/Resources/bg/2.png -------------------------------------------------------------------------------- /src/Resources/bg/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/src/Resources/bg/3.png -------------------------------------------------------------------------------- /src/Resources/bg/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/src/Resources/bg/4.png -------------------------------------------------------------------------------- /src/Resources/bg/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/src/Resources/bg/5.png -------------------------------------------------------------------------------- /src/Resources/mask/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/src/Resources/mask/circle.png -------------------------------------------------------------------------------- /src/Resources/mask/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/src/Resources/mask/star.png -------------------------------------------------------------------------------- /src/Resources/mask/triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLOFLS/drag-captcha/94ed205abfb3abb51bb7a31c217f559c96a16187/src/Resources/mask/triangle.png -------------------------------------------------------------------------------- /src/Utils.php: -------------------------------------------------------------------------------- 1 |