├── LICENSE ├── README.md ├── demo ├── css │ └── style.css ├── images │ └── image.png └── index.html ├── dist └── main.js ├── introduceImg ├── after.png └── before.jpg ├── lib ├── jquery-2.1.1.js └── magic-wand.js └── src └── main.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 todaylg 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.md: -------------------------------------------------------------------------------- 1 | Magic-Matting 2 | ================== 3 | 4 | 5 | Magic-Matting旨在解决只想简单的抠个图而不想开Ps这么个严重的问题,功能模仿Ps勾选连续像素取样后的魔棒工具,基于Flood-fill算法进行选区的生成。 6 | 7 | ### 主要功能 8 | 9 | 1. 导入图片(拖拽或选择) 10 | 2. 鼠标中键滚轮对图像进行放大或缩小 11 | 3. 鼠标左键点击生成选取,若按住左键向上滑动为小幅度增加容差值并动态扩大选区,若按住左键向下滑动则为大幅度增加容差值并动态扩大选区 12 | 4. 键盘“w”键增大容差值,“s”键减小容差值 13 | 5. ctrl/command + Z 撤销前一步选区的选取 14 | 6. ctrl/command + D 重置(填充像素与选取信息全部清空) 15 | 7. ctrl/command + delete/backspace 对当前选取进行扣除(显示时是以白色像素进行填充,下载后的图片是以透明像素进行填充) 16 | 8. 图片导出(PNG)及下载 17 | 18 | ### 实际测试 19 | ![image](https://github.com/todaylg/Magic-Matting/blob/master/introduceImg/before.jpg) 20 | 21 | ![image](https://github.com/todaylg/Magic-Matting/blob/master/introduceImg/after.png) 22 | 23 | 24 | 效果还并不理想,更适合对偏纯色背景的图案进行抠图(亚可真可爱~) 25 | 26 | ### Todo 27 | - [ ] 限制选取范围 28 | - [ ] 图片裁剪 29 | - [ ] 动态扩展选取的撤销 30 | - [ ] 多步撤销 31 | - [ ] 选取缩减 32 | - [ ] Chrome插件化 33 | 34 | -------------------------------------------------------------------------------- /demo/css/style.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | /* ----------------Reset Css--------------------- */ 3 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, 4 | a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, 6 | fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, 7 | article, aside, canvas, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary, 8 | time, mark, audio, video, input { 9 | margin: 0; 10 | padding: 0; 11 | border: none; 12 | outline: 0; 13 | font-size: 100%; 14 | font: inherit; 15 | vertical-align: baseline; 16 | } 17 | 18 | html, body, form, fieldset, p, div, h1, h2, h3, h4, h5, h6 { 19 | -webkit-text-size-adjust: none; 20 | } 21 | 22 | article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { 23 | display: block; 24 | } 25 | 26 | body { 27 | font-family: arial, sans-serif; 28 | } 29 | 30 | ol, ul { 31 | list-style: none; 32 | } 33 | 34 | blockquote, q { 35 | quotes: none; 36 | } 37 | 38 | blockquote:before, blockquote:after, q:before, q:after { 39 | content: ''; 40 | content: none; 41 | } 42 | 43 | ins { 44 | text-decoration: none; 45 | } 46 | 47 | del { 48 | text-decoration: line-through; 49 | } 50 | 51 | table { 52 | border-collapse: collapse; 53 | border-spacing: 0; 54 | } 55 | 56 | #wrapper { 57 | position: fixed; 58 | left: 0; 59 | right: 0; 60 | top: 0; 61 | bottom: 0; 62 | display: none 63 | } 64 | 65 | #content { 66 | position: fixed; 67 | left: 0; 68 | right: 0; 69 | top: 0; 70 | bottom: 0; 71 | display: flex; 72 | justify-content: center; 73 | align-items: center; 74 | max-width: 1920px; 75 | max-height: 1080px; 76 | } 77 | #test-picture{ 78 | width: 1280px; 79 | border: 3px solid #00b7ee; 80 | padding: 2px; 81 | } 82 | .canvas { 83 | position: absolute; 84 | } 85 | 86 | .canvas:hover { 87 | cursor: default; 88 | } 89 | 90 | .picture { 91 | position: absolute; 92 | } 93 | 94 | #uImgWrapper { 95 | margin: 20px; 96 | position: fixed; 97 | left: 0; 98 | right: 0; 99 | top: 0; 100 | bottom: 0; 101 | } 102 | 103 | #uImgContainer { 104 | border: 1px solid #dadada; 105 | color: #838383; 106 | font-size: 12px; 107 | margin-top: 10px; 108 | background-color: #FFF; 109 | } 110 | 111 | #uImgInner { 112 | margin: 20px; 113 | } 114 | 115 | #dndArea{ 116 | border: 3px dashed #e6e6e6; 117 | min-height: 238px; 118 | padding-top: 158px; 119 | text-align: center; 120 | background: url(../images/image.png) center 93px no-repeat; 121 | color: #cccccc; 122 | font-size: 18px; 123 | position: relative; 124 | } 125 | 126 | #uploader-pick { 127 | font-size: 18px; 128 | background: #00b7ee; 129 | border-radius: 3px; 130 | line-height: 44px; 131 | padding: 0 30px; 132 | color: #fff; 133 | display: inline-block; 134 | margin: 20px auto; 135 | cursor: pointer; 136 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); 137 | } 138 | #file-upload{ 139 | /*当一幅图像的尺寸大于包含它的元素时会发生什么呢?"clip" 属性允许您规定一个元素的可见尺寸,这样此元素就会被修剪并显示为这个形状。*/ 140 | position: absolute !important; 141 | clip: rect(1px 1px 1px 1px); 142 | } 143 | 144 | #dndArea.uploaderOver { 145 | border-color: #999999; 146 | } 147 | #panel{ 148 | display: none; 149 | } 150 | .panelBtn{ 151 | font-size: 14px; 152 | height: 20px; 153 | background: #00b7ee; 154 | border-radius: 3px; 155 | line-height: 20px; 156 | padding: 0 10px; 157 | color: #fff; 158 | display: inline-block; 159 | margin: 5px 5px; 160 | cursor: pointer; 161 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); 162 | } 163 | #thresholdLable{ 164 | font-size: 14px; 165 | color: white; 166 | padding: 3px; 167 | background-color: #00b7ee; 168 | border-radius: 3px; 169 | } 170 | #colorThreshold{ 171 | border-bottom: 2px solid #00b7ee; 172 | } -------------------------------------------------------------------------------- /demo/images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todaylg/Magic-Matting/e45d774bf20aab78ee06b57c36df2f5c648cb178/demo/images/image.png -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
点击选择图片
21 |
22 | 23 |

请将要抠的图片拖到这里~

24 |
25 |
26 |
27 |
28 |
30 |
加载新图片
31 |
下载图片
32 | 33 | 34 |
35 | 36 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var colorThreshold = 15, 4 | imageInfo = null, 5 | tempCanvas = null, 6 | cacheInd = null, 7 | delResData = null, 8 | predelResData = null, 9 | mask = null, 10 | downPoint = null, 11 | allowDraw = false, 12 | currentThreshold = colorThreshold, 13 | scaleStep = 0.1, 14 | newScale = 1, 15 | minScale = 0.5, 16 | maxScale = 3, 17 | hatchLength = 4, 18 | hatchOffset = 0; 19 | 20 | window.onload = function () { 21 | var imgConetent = document.getElementById('content'); 22 | imgConetent.style.maxWidth = window.innerWidth + 'px'; 23 | imgConetent.style.maxHeight = window.innerHeight + 'px'; 24 | 25 | EventInit(); 26 | setInterval(function () { 27 | hatchTick(); 28 | }, 300); 29 | }; 30 | 31 | function EventInit() { 32 | //Init MouseWheel Event 33 | var content = document.getElementById('content'); 34 | content.addEventListener('mousewheel', handleMouseWheel); 35 | 36 | document.getElementById('filePicker').addEventListener('click', function () { 37 | document.getElementById('file-upload').click(); 38 | }); 39 | 40 | //Drag Add Image 41 | var dropContainer = document.querySelector('#dndArea'); 42 | 43 | dropContainer.addEventListener('dragover', function (e) { 44 | e.stopPropagation(); 45 | e.preventDefault(); 46 | e.dataTransfer.dropEffect = 'link'; 47 | if (!dropContainer.classList.contains('uploaderOver')) dropContainer.classList.add('uploaderOver'); 48 | }, false); 49 | 50 | dropContainer.addEventListener('dragleave', function () { 51 | dropContainer.classList.remove('uploaderOver'); 52 | }, false); 53 | 54 | dropContainer.addEventListener('drop', function (e) { 55 | e.stopPropagation(); 56 | e.preventDefault(); 57 | var reader = new FileReader(); 58 | //validate 59 | var ext = e.dataTransfer.files[0].name.substring(e.dataTransfer.files[0].name.lastIndexOf('.') + 1).toLowerCase(); 60 | if (ext != 'png' && ext != 'jpg' && ext != 'jpeg') { 61 | alert('图片的格式必须为png或者jpg或者jpeg格式!'); 62 | return; 63 | } 64 | reader.onload = function (e) { 65 | var src = e.target.result; 66 | var img = document.getElementById('test-picture'); 67 | img.setAttribute('src', src); 68 | img.onload = function () { 69 | window.initCanvas(img); 70 | //Jquery 71 | $('#uImgWrapper').fadeOut('400', function () { 72 | $('#wrapper').fadeIn('400'); 73 | $('#panel').fadeIn('400'); 74 | }); 75 | }; 76 | dropContainer.classList.remove('uploaderOver'); 77 | }; 78 | reader.readAsDataURL(e.dataTransfer.files[0]); 79 | }, false); 80 | 81 | document.onkeydown = function (e) { 82 | // Revoke (command+Z or ctrl+Z) 83 | if (e.metaKey && e.keyCode === 90 || e.ctrlKey && e.keyCode === 90) { 84 | e.preventDefault(); 85 | if (mask && mask.data.length && mask.predata.length) { 86 | if (!predelResData) { 87 | var _ref = [mask.predata, mask.data]; 88 | mask.data = _ref[0]; 89 | mask.predata = _ref[1]; 90 | } else if (predelResData) { 91 | delResData = predelResData; 92 | predelResData = null; 93 | } 94 | drawBorder(); 95 | } 96 | } 97 | //Reset 98 | if (e.metaKey && e.keyCode === 68 || e.ctrlKey && e.keyCode === 68) { 99 | e.preventDefault(); 100 | if (mask) { 101 | var _ref2 = [[], mask.data]; 102 | mask.data = _ref2[0]; 103 | mask.predata = _ref2[1]; 104 | 105 | delResData = null; 106 | predelResData = null; 107 | MagicWand.clearPreData(); 108 | drawBorder(); 109 | } 110 | } 111 | 112 | //Delete 113 | if (e.metaKey && e.keyCode === 46 || e.metaKey && e.keyCode === 8 || e.ctrlKey && e.keyCode === 46 || e.ctrlKey && e.keyCode === 8) { 114 | e.preventDefault(); 115 | if (mask) { 116 | drawBorder(null, true); 117 | } 118 | } 119 | //Add colorThreshold 120 | if (e.keyCode === 87) { 121 | //W 122 | e.preventDefault(); 123 | var _colorThreshold = document.getElementById('colorThreshold').value; 124 | if (parseInt(_colorThreshold, 10) < 442) _colorThreshold = parseInt(_colorThreshold, 10) + 1; 125 | } 126 | //reduce colorThreshold 127 | if (e.keyCode == 83) { 128 | //S 129 | e.preventDefault(); 130 | var _colorThreshold2 = document.getElementById('colorThreshold').value; 131 | if (parseInt(_colorThreshold2, 10) > 0) _colorThreshold2 = parseInt(_colorThreshold2, 10) - 1; 132 | } 133 | }; 134 | } 135 | 136 | function initCanvas(img) { 137 | maxScale = window.innerWidth / document.getElementById('test-picture').width + 2; 138 | var imgTemp = new Image(); 139 | imgTemp.src = img.src; 140 | var cvs = document.getElementById('canvas'), 141 | imgContain = document.getElementById('test-picture'), 142 | sh = imgTemp.height / (imgTemp.width / imgContain.width); 143 | cvs.width = imgContain.width; 144 | cvs.height = sh; 145 | //getImageData pass to Magicwands 146 | imageInfo = { 147 | width: imgContain.width, 148 | height: sh, 149 | context: cvs.getContext('2d') 150 | }; 151 | mask = null; 152 | //this canvas use for save source image data and export 153 | tempCanvas = document.createElement('canvas'); 154 | var tempCtx = tempCanvas.getContext('2d'); 155 | tempCtx.canvas.width = imageInfo.width; 156 | tempCtx.canvas.height = imageInfo.height; 157 | tempCtx.drawImage(img, 0, 0, imageInfo.width, imageInfo.height); 158 | imageInfo.data = tempCtx.getImageData(0, 0, imageInfo.width, imageInfo.height); 159 | } 160 | 161 | function imgChange(inp) { 162 | if (inp.files && inp.files[0]) { 163 | var reader = new FileReader(); 164 | reader.onload = function (e) { 165 | var src = e.target.result; 166 | var img = document.getElementById('test-picture'); 167 | img.setAttribute('src', src); 168 | img.onload = function () { 169 | window.initCanvas(img); 170 | //Jquery 171 | $('#uImgWrapper').fadeOut('400', function () { 172 | $('#wrapper').fadeIn('400'); 173 | $('#panel').fadeIn('400'); 174 | }); 175 | }; 176 | }; 177 | reader.readAsDataURL(inp.files[0]); 178 | } 179 | } 180 | 181 | function getMousePosition(e) { 182 | //Jquery 183 | var p = $(e.target).offset(), 184 | x = Math.round((e.clientX || e.pageX) - p.left), 185 | //relative canvas 186 | y = Math.round((e.clientY || e.pageY) - p.top); 187 | return { x: x, y: y }; 188 | } 189 | 190 | function onMouseDown(e) { 191 | if (e.button == 0) { 192 | //union 193 | allowDraw = true; 194 | downPoint = getMousePosition(e); 195 | colorThreshold = parseInt(document.getElementById('colorThreshold').value, 10) || 15; 196 | currentThreshold = colorThreshold; 197 | //reduction 198 | drawMask(parseInt(downPoint.x / newScale, 10), parseInt(downPoint.y / newScale), 10); 199 | } else { 200 | allowDraw = false; 201 | } 202 | } 203 | 204 | function onMouseMove(e) { 205 | if (allowDraw) { 206 | var p = getMousePosition(e); 207 | if (p.x != downPoint.x || p.y != downPoint.y) { 208 | var dx = p.x - downPoint.x, 209 | dy = p.y - downPoint.y, 210 | len = Math.sqrt(dx * dx + dy * dy), 211 | sign = dy < 0 ? 1 / 10 : 1 / 2; //mouse move direction depend colorThreshold increase slow or quick(//TODO subtract) 212 | var thres = Math.min(Math.max(colorThreshold + Math.floor(sign * len), 1), 255); 213 | if (thres != currentThreshold) { 214 | currentThreshold = thres; 215 | drawMask(parseInt(downPoint.x / newScale, 10), parseInt(downPoint.y / newScale), 10); 216 | } 217 | } 218 | } 219 | } 220 | 221 | function onMouseUp() { 222 | allowDraw = false; 223 | currentThreshold = colorThreshold; 224 | } 225 | 226 | function drawMask(x, y) { 227 | if (!imageInfo) return; 228 | var image = { 229 | data: imageInfo.data.data, 230 | width: imageInfo.width, 231 | height: imageInfo.height, 232 | bytes: 4 233 | }; 234 | 235 | mask = MagicWand.floodFill(image, x, y, currentThreshold); 236 | drawBorder(); 237 | } 238 | 239 | function hatchTick() { 240 | hatchOffset = (hatchOffset + 1) % (hatchLength * 2); 241 | drawBorder(true); 242 | } 243 | 244 | function drawBorder(noBorder, noFill) { 245 | if (!mask) return; 246 | var x = void 0, 247 | y = void 0, 248 | i = void 0, 249 | j = void 0, 250 | k = void 0, 251 | w = imageInfo.width, 252 | h = imageInfo.height, 253 | ctx = imageInfo.context, 254 | imgData = ctx.createImageData(w, h), 255 | res = imgData.data; 256 | 257 | if (!noBorder) { 258 | cacheInd = MagicWand.getBorderIndices(mask); //cache 259 | predelResData = null; 260 | } 261 | 262 | ctx.clearRect(0, 0, w, h); 263 | 264 | var len = cacheInd.length; 265 | for (j = 0; j < len; j++) { 266 | i = cacheInd[j]; 267 | x = i % w; // calc x by index 268 | y = (i - x) / w; // calc y by index 269 | k = (y * w + x) * 4; 270 | if ((x + y + hatchOffset) % (hatchLength * 2) < hatchLength) { 271 | // detect hatch color 272 | res[k + 3] = 255; // black, change only alpha 273 | } else { 274 | res[k] = 255; // white 275 | res[k + 1] = 255; 276 | res[k + 2] = 255; 277 | res[k + 3] = 255; 278 | } 279 | } 280 | 281 | if (noFill) delResData = MagicWand.getCurrentResult(mask); 282 | if (delResData) { 283 | predelResData = null; 284 | for (j = 0; j < delResData.length; j++) { 285 | i = delResData[j]; 286 | x = i % w; // calc x by index 287 | y = (i - x) / w; // calc y by index 288 | k = (y * w + x) * 4; 289 | res[k] = 255; // white 290 | res[k + 1] = 255; 291 | res[k + 2] = 255; 292 | res[k + 3] = 255; 293 | } 294 | } 295 | ctx.putImageData(imgData, 0, 0); 296 | } 297 | 298 | function imgToCanvas() { 299 | var x = void 0, 300 | y = void 0, 301 | i = void 0, 302 | j = void 0, 303 | k = void 0, 304 | w = imageInfo.width, 305 | h = imageInfo.height, 306 | ctx = tempCanvas.getContext('2d'), 307 | imageData = ctx.getImageData(0, 0, w, h), 308 | res = imageData.data; 309 | var delResData = MagicWand.getCurrentResult(mask); 310 | if (delResData) { 311 | for (j = 0; j < delResData.length; j++) { 312 | i = delResData[j]; 313 | x = i % w; // calc x by index 314 | y = (i - x) / w; // calc y by index 315 | k = (y * w + x) * 4; 316 | res[k] = 0; // white 317 | res[k + 1] = 0; 318 | res[k + 2] = 0; 319 | res[k + 3] = 0; 320 | } 321 | } 322 | ctx.putImageData(imageData, 0, 0); 323 | } 324 | 325 | function downloadImg(e) { 326 | // First try a.download, then web filesystem, then object URLs 327 | // just use a.download 328 | e.stopPropagation(); 329 | imgToCanvas(); 330 | tempCanvas.toBlob(function (blob) { 331 | var aTemp = document.createElement('a'); 332 | aTemp.setAttribute('href', URL.createObjectURL(blob)); 333 | aTemp.setAttribute('download', 'Magic.png'); 334 | 335 | var evObj = document.createEvent('MouseEvents'); 336 | evObj.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, true, false, 0, null); 337 | aTemp.dispatchEvent(evObj); 338 | }); 339 | } 340 | 341 | function reloadImg() { 342 | //Jquery 343 | $('#wrapper').fadeOut('400', function () { 344 | $('#uImgWrapper').fadeIn('400'); 345 | if (mask && mask.data) { 346 | ; 347 | var _ref3 = [[], mask.data]; 348 | mask.data = _ref3[0]; 349 | mask.predata = _ref3[1]; 350 | }delResData = null; 351 | predelResData = null; 352 | MagicWand.clearPreData(); 353 | }); 354 | $('#panel').fadeOut('400'); 355 | } 356 | 357 | //TODO:完善 滚轮放大、空格拖拽(实现PS中的效果) 358 | //滚轮放大:在scale小于1的情况下滚轮放大以中心为焦点进行放大,大于1后跟随鼠标位置进行放大 359 | //空格拖拽:在放大的情况下,按住空格后对img、canvas的位置控制 360 | function handleMouseWheel(e) { 361 | var wd = e.wheelDelta; 362 | newScale += wd > 0 ? scaleStep : -scaleStep; 363 | newScale = newScale < minScale ? minScale : newScale; 364 | newScale = newScale > maxScale ? maxScale : newScale; 365 | //img、canvas change need synchronization 366 | var imgContain = document.getElementById('test-picture'), 367 | canvas = document.getElementById('canvas'), 368 | content = document.getElementById('content'); 369 | 370 | if (parseInt(canvas.width * newScale, 10) > window.innerWidth || parseInt(canvas.width * newScale, 10) > window.innerWidth) { 371 | imgContain.style.transformOrigin = 'left top'; 372 | canvas.style.transformOrigin = 'left top'; 373 | tempCanvas.style.transformOrigin = 'left top'; 374 | } else { 375 | imgContain.style.transformOrigin = 'center center'; 376 | canvas.style.transformOrigin = 'center center'; 377 | tempCanvas.style.transformOrigin = 'center center'; 378 | } 379 | imgContain.style.transform = 'scale(' + newScale + ')'; 380 | canvas.style.transform = 'scale(' + newScale + ')'; 381 | tempCanvas.style.transform = 'scale(' + newScale + ')'; 382 | 383 | if (parseInt(canvas.width * newScale, 10) > window.innerWidth) { 384 | content.style.overflowX = 'scroll'; 385 | } else if (parseInt(canvas.height * newScale, 10) > window.innerHeight) { 386 | content.style.overflowY = 'scroll'; 387 | } else { 388 | content.style.overflow = 'hidden'; 389 | } 390 | } -------------------------------------------------------------------------------- /introduceImg/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todaylg/Magic-Matting/e45d774bf20aab78ee06b57c36df2f5c648cb178/introduceImg/after.png -------------------------------------------------------------------------------- /introduceImg/before.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todaylg/Magic-Matting/e45d774bf20aab78ee06b57c36df2f5c648cb178/introduceImg/before.jpg -------------------------------------------------------------------------------- /lib/magic-wand.js: -------------------------------------------------------------------------------- 1 | /* 2 | * License: The MIT License (MIT) Ryasnoy Paul、todaylg 3 | */ 4 | 5 | MagicWand = (function () { 6 | var lib = {}; 7 | var tempData = new Uint8Array;//取每次选取的并集 8 | var preData = new Uint8Array;//撤销 9 | 10 | /** Create a binary(二进制) mask on the image by color threshold 11 | * Algorithm: Scanline flood fill (http://en.wikipedia.org/wiki/Flood_fill) 12 | * @param {Object} image: {Uint8Array} data, {int} width, {int} height, {int} bytes 13 | * @param {int} x of start pixel 14 | * @param {int} y of start pixel 15 | * @param {int} color threshold 16 | * @param {Uint8Array} mask of visited points (optional) 17 | * @return {Object} mask: {Uint8Array} data, {int} width, {int} height, {Object} bounds 18 | */ 19 | 20 | // 过程: 21 | // Flood-fill (node, target-color, replacement-color): 22 | // 1. If target-color is equal to replacement-color, return. 23 | // 2. If color of node is not equal to target-color, return. 24 | // 3. Set Q to the empty queue. 25 | // 4. Add node to Q. 26 | // 5. For each element N of Q: 27 | // 6. Set w and e equal to N. 28 | // 7. Move w to the west until the color of the node to the west of w no longer matches target-color. 29 | // 8. Move e to the east until the color of the node to the east of e no longer matches target-color. 30 | // 9. For each node n between w and e: 31 | // 10. Set the color of n to replacement-color. 32 | // 11. If the color of the node to the north of n is target-color, add that node to Q. 33 | // 12. If the color of the node to the south of n is target-color, add that node to Q. 34 | // 13. Continue looping until Q is exhausted. 35 | // 14. Return. 36 | 37 | lib.floodFill = function(image, px, py, colorThreshold, mask) { 38 | var c, x, newY, el, xr, xl, dy, dyl, dyr, checkY, 39 | data = image.data, 40 | w = image.width, 41 | h = image.height, 42 | bytes = image.bytes, // number of bytes in the color 43 | maxX = -1, minX = w + 1, maxY = -1, minY = h + 1, 44 | i = py * w + px, // start point index in the mask data 45 | result = new Uint8Array(w * h), // result mask 46 | visited = new Uint8Array(mask ? mask : w * h); // mask of visited points 47 | if (visited[i] === 1) return null; 48 | 49 | i = i * bytes; // start point index in the image data 50 | var sampleColor = [data[i], data[i + 1], data[i + 2], data[i + 3]]; // start point color (sample) 51 | var stack = [{ y: py, left: px - 1, right: px + 1, dir: 1 }]; // first scanning line 52 | do { 53 | el = stack.shift(); // get line for scanning 54 | 55 | checkY = false; 56 | for (x = el.left + 1; x < el.right; x++) { 57 | dy = el.y * w; 58 | i = (dy + x) * bytes; // point index in the image data 59 | if (visited[dy + x] === 1) continue; // check whether the point has been visited 60 | // compare the color of the sample 61 | c = data[i] - sampleColor[0]; // check by red 62 | if (c > colorThreshold || c < -colorThreshold) continue;//容差 63 | c = data[i + 1] - sampleColor[1]; // check by green 64 | if (c > colorThreshold || c < -colorThreshold) continue; 65 | c = data[i + 2] - sampleColor[2]; // check by blue 66 | if (c > colorThreshold || c < -colorThreshold) continue; 67 | 68 | checkY = true; // if the color of the new point(x,y) is similar to the sample color need to check minmax for Y 69 | 70 | result[dy + x] = 1; // mark a new point in mask 71 | visited[dy + x] = 1; // mark a new point as visited 72 | 73 | xl = x - 1;//向左偏移一个像素 74 | // walk to left side starting with the left neighbor 75 | while (xl > -1) {//直到走完一行的全部像素 76 | dyl = dy + xl; 77 | i = dyl * bytes; // point index in the image data 78 | if (visited[dyl] === 1) break; // check whether the point has been visited 79 | // compare the color of the sample 80 | c = data[i] - sampleColor[0]; // check by red 81 | if (c > colorThreshold || c < -colorThreshold) break; 82 | c = data[i + 1] - sampleColor[1]; // check by green 83 | if (c > colorThreshold || c < -colorThreshold) break; 84 | c = data[i + 2] - sampleColor[2]; // check by blue 85 | if (c > colorThreshold || c < -colorThreshold) break; 86 | //一旦有一个像素颜色不匹配,就直接break 87 | result[dyl] = 1; 88 | visited[dyl] = 1; 89 | xl--; 90 | } 91 | xr = x + 1;//向右偏移一个像素 92 | // walk to right side starting with the right neighbor 93 | while (xr < w) {//直到走完一行的全部像素 94 | dyr = dy + xr; 95 | i = dyr * bytes; // index point in the image data 96 | if (visited[dyr] === 1) break; // check whether the point has been visited 97 | // compare the color of the sample 98 | c = data[i] - sampleColor[0]; // check by red 99 | if (c > colorThreshold || c < -colorThreshold) break; 100 | c = data[i + 1] - sampleColor[1]; // check by green 101 | if (c > colorThreshold || c < -colorThreshold) break; 102 | c = data[i + 2] - sampleColor[2]; // check by blue 103 | if (c > colorThreshold || c < -colorThreshold) break; 104 | 105 | result[dyr] = 1; 106 | visited[dyr] = 1; 107 | xr++; 108 | } 109 | // check minmax for X 110 | if (xl < minX) minX = xl + 1; 111 | if (xr > maxX) maxX = xr - 1; 112 | 113 | newY = el.y - el.dir;//el.y仍然是点击时y轴的位置信息,没有变 114 | 115 | if (newY >= 0 && newY < h) { // add two scanning lines in the opposite direction (y - dir) if necessary //其实就是一行 116 | if (xl < el.left) stack.push({ y: newY, left: xl, right: el.left, dir: -el.dir }); // from "new left" to "current left" //0=>left 117 | if (el.right < xr) stack.push({ y: newY, left: el.right, right: xr, dir: -el.dir }); // from "current right" to "new right" //right=>width 118 | } 119 | newY = el.y + el.dir;//实现north和south!!!(+1/ -1) 120 | if (newY >= 0 && newY < h) { // add the scanning line in the direction (y + dir) if necessary 121 | if (xl < xr) stack.push({ y: newY, left: xl, right: xr, dir: el.dir }); // from "new left" to "new right" 122 | } 123 | } 124 | // check minmax for Y if necessary 125 | if (checkY) { 126 | if (el.y < minY) minY = el.y; 127 | if (el.y > maxY) maxY = el.y; 128 | } 129 | } while (stack.length > 0); 130 | 131 | //为了实现选取叠加而添加的代码部分 132 | if(tempData.length==0){ 133 | tempData = result; 134 | }else{ 135 | preData = new Uint8Array(tempData);//保存上一步 136 | for(var i=0;i{ 21 | let imgConetent = document.getElementById('content'); 22 | imgConetent.style.maxWidth = window.innerWidth+'px'; 23 | imgConetent.style.maxHeight = window.innerHeight+'px'; 24 | 25 | EventInit(); 26 | setInterval(()=>{ hatchTick(); }, 300); 27 | }; 28 | 29 | function EventInit(){ 30 | //Init MouseWheel Event 31 | let content = document.getElementById('content'); 32 | content.addEventListener('mousewheel', handleMouseWheel); 33 | 34 | document.getElementById('filePicker').addEventListener('click',()=>{ 35 | document.getElementById('file-upload').click(); 36 | }); 37 | 38 | //Drag Add Image 39 | let dropContainer = document.querySelector('#dndArea'); 40 | 41 | dropContainer.addEventListener('dragover',(e)=>{ 42 | e.stopPropagation(); 43 | e.preventDefault(); 44 | e.dataTransfer.dropEffect = 'link'; 45 | if(!dropContainer.classList.contains('uploaderOver')) 46 | dropContainer.classList.add('uploaderOver'); 47 | },false); 48 | 49 | dropContainer.addEventListener('dragleave', ()=> { 50 | dropContainer.classList.remove('uploaderOver'); 51 | }, false); 52 | 53 | dropContainer.addEventListener('drop',(e)=>{ 54 | e.stopPropagation(); 55 | e.preventDefault(); 56 | let reader = new FileReader(); 57 | //validate 58 | let ext = e.dataTransfer.files[0].name.substring(e.dataTransfer.files[0].name.lastIndexOf('.') + 1).toLowerCase(); 59 | if (ext != 'png' && ext != 'jpg' && ext != 'jpeg') { 60 | alert('图片的格式必须为png或者jpg或者jpeg格式!'); 61 | return; 62 | } 63 | reader.onload = (e) => { 64 | let src = e.target.result; 65 | let img = document.getElementById('test-picture'); 66 | img.setAttribute('src', src); 67 | img.onload = ()=> { 68 | window.initCanvas(img); 69 | //Jquery 70 | $('#uImgWrapper').fadeOut('400', ()=> { 71 | $('#wrapper').fadeIn('400'); 72 | $('#panel').fadeIn('400'); 73 | }); 74 | }; 75 | dropContainer.classList.remove('uploaderOver'); 76 | }; 77 | reader.readAsDataURL(e.dataTransfer.files[0]); 78 | }, false); 79 | 80 | document.onkeydown =(e)=>{ 81 | // Revoke (command+Z or ctrl+Z) 82 | if ((e.metaKey && e.keyCode === 90)||(e.ctrlKey && e.keyCode === 90) ) { 83 | e.preventDefault(); 84 | if(mask&&mask.data.length&&mask.predata.length){ 85 | if(!predelResData){ 86 | [mask.data,mask.predata] = [mask.predata,mask.data]; 87 | }else if(predelResData){ 88 | delResData = predelResData; 89 | predelResData = null; 90 | } 91 | drawBorder(); 92 | } 93 | } 94 | //Reset 95 | if ((e.metaKey && e.keyCode === 68)||(e.ctrlKey && e.keyCode === 68) ) { 96 | e.preventDefault(); 97 | if(mask){ 98 | [mask.data,mask.predata] = [[],mask.data]; 99 | delResData = null; 100 | predelResData = null; 101 | MagicWand.clearPreData(); 102 | drawBorder(); 103 | } 104 | } 105 | 106 | //Delete 107 | if ((e.metaKey && e.keyCode === 46)||(e.metaKey && e.keyCode === 8)||(e.ctrlKey && e.keyCode === 46)||(e.ctrlKey && e.keyCode === 8)) { 108 | e.preventDefault(); 109 | if(mask){ 110 | drawBorder(null,true); 111 | } 112 | } 113 | //Add colorThreshold 114 | if (e.keyCode === 87) {//W 115 | e.preventDefault(); 116 | let colorThreshold = document.getElementById('colorThreshold').value; 117 | if(parseInt(colorThreshold,10)<442) colorThreshold = parseInt(colorThreshold,10)+1; 118 | } 119 | //reduce colorThreshold 120 | if (e.keyCode ==83) {//S 121 | e.preventDefault(); 122 | let colorThreshold = document.getElementById('colorThreshold').value; 123 | if(parseInt(colorThreshold,10)>0) colorThreshold = parseInt(colorThreshold,10)-1; 124 | } 125 | }; 126 | } 127 | 128 | function initCanvas(img) { 129 | maxScale = window.innerWidth/document.getElementById('test-picture').width+2; 130 | let imgTemp = new Image(); 131 | imgTemp.src = img.src; 132 | let cvs = document.getElementById('canvas'), 133 | imgContain = document.getElementById('test-picture'), 134 | sh = imgTemp.height/(imgTemp.width/imgContain.width); 135 | cvs.width = imgContain.width; 136 | cvs.height = sh; 137 | //getImageData pass to Magicwands 138 | imageInfo = { 139 | width: imgContain.width, 140 | height: sh, 141 | context: cvs.getContext('2d') 142 | }; 143 | mask = null; 144 | //this canvas use for save source image data and export 145 | tempCanvas = document.createElement('canvas'); 146 | let tempCtx =tempCanvas.getContext('2d'); 147 | tempCtx.canvas.width = imageInfo.width; 148 | tempCtx.canvas.height = imageInfo.height; 149 | tempCtx.drawImage(img, 0, 0,imageInfo.width,imageInfo.height); 150 | imageInfo.data = tempCtx.getImageData(0, 0, imageInfo.width, imageInfo.height); 151 | } 152 | 153 | function imgChange(inp){ 154 | if (inp.files && inp.files[0]) { 155 | let reader = new FileReader(); 156 | reader.onload = function (e) { 157 | let src = e.target.result; 158 | let img = document.getElementById('test-picture'); 159 | img.setAttribute('src', src); 160 | img.onload = ()=> { 161 | window.initCanvas(img); 162 | //Jquery 163 | $('#uImgWrapper').fadeOut('400', ()=> { 164 | $('#wrapper').fadeIn('400'); 165 | $('#panel').fadeIn('400'); 166 | }); 167 | }; 168 | }; 169 | reader.readAsDataURL(inp.files[0]); 170 | } 171 | } 172 | 173 | function getMousePosition(e) { 174 | //Jquery 175 | let p = $(e.target).offset(), 176 | x = Math.round((e.clientX || e.pageX) - p.left),//relative canvas 177 | y = Math.round((e.clientY || e.pageY) - p.top); 178 | return { x: x, y: y }; 179 | } 180 | 181 | function onMouseDown(e) { 182 | if (e.button == 0) { 183 | //union 184 | allowDraw = true; 185 | downPoint = getMousePosition(e); 186 | colorThreshold = parseInt(document.getElementById('colorThreshold').value,10)||15; 187 | currentThreshold = colorThreshold; 188 | //reduction 189 | drawMask(parseInt(downPoint.x/newScale,10), parseInt(downPoint.y/newScale),10); 190 | }else{ 191 | allowDraw = false; 192 | } 193 | } 194 | 195 | function onMouseMove(e) { 196 | if (allowDraw) { 197 | let p = getMousePosition(e); 198 | if (p.x != downPoint.x || p.y != downPoint.y) { 199 | let dx = p.x - downPoint.x, 200 | dy = p.y - downPoint.y, 201 | len = Math.sqrt(dx * dx + dy * dy), 202 | sign = dy < 0 ? 1 / 10 : 1 / 2;//mouse move direction depend colorThreshold increase slow or quick(//TODO subtract) 203 | let thres = Math.min(Math.max(colorThreshold + Math.floor(sign * len), 1), 255); 204 | if (thres != currentThreshold) { 205 | currentThreshold = thres; 206 | drawMask(parseInt(downPoint.x/newScale,10), parseInt(downPoint.y/newScale),10); 207 | } 208 | } 209 | } 210 | } 211 | 212 | function onMouseUp() { 213 | allowDraw = false; 214 | currentThreshold = colorThreshold; 215 | } 216 | 217 | function drawMask(x, y) { 218 | if (!imageInfo) return; 219 | let image = { 220 | data: imageInfo.data.data, 221 | width: imageInfo.width, 222 | height: imageInfo.height, 223 | bytes: 4 224 | }; 225 | 226 | mask = MagicWand.floodFill(image, x, y, currentThreshold); 227 | drawBorder(); 228 | } 229 | 230 | function hatchTick() { 231 | hatchOffset = (hatchOffset + 1) % (hatchLength * 2); 232 | drawBorder(true); 233 | } 234 | 235 | function drawBorder(noBorder,noFill) { 236 | if (!mask) return; 237 | let x,y,i,j,k, 238 | w = imageInfo.width, 239 | h = imageInfo.height, 240 | ctx = imageInfo.context, 241 | imgData = ctx.createImageData(w, h), 242 | res = imgData.data; 243 | 244 | if (!noBorder){ 245 | cacheInd = MagicWand.getBorderIndices(mask);//cache 246 | predelResData = null; 247 | } 248 | 249 | ctx.clearRect(0, 0, w, h); 250 | 251 | let len = cacheInd.length; 252 | for (j = 0; j < len; j++) { 253 | i = cacheInd[j]; 254 | x = i % w; // calc x by index 255 | y = (i - x) / w; // calc y by index 256 | k = (y * w + x) * 4; 257 | if ((x + y + hatchOffset) % (hatchLength * 2) < hatchLength) { // detect hatch color 258 | res[k + 3] = 255; // black, change only alpha 259 | } else { 260 | res[k] = 255; // white 261 | res[k + 1] = 255; 262 | res[k + 2] = 255; 263 | res[k + 3] = 255; 264 | } 265 | } 266 | 267 | if (noFill) delResData = MagicWand.getCurrentResult(mask); 268 | if(delResData){ 269 | predelResData = null; 270 | for (j = 0; j < delResData.length; j++) { 271 | i = delResData[j]; 272 | x = i % w; // calc x by index 273 | y = (i - x) / w; // calc y by index 274 | k = (y * w + x) * 4; 275 | res[k] = 255; // white 276 | res[k + 1] = 255; 277 | res[k + 2] = 255; 278 | res[k + 3] = 255; 279 | } 280 | } 281 | ctx.putImageData(imgData, 0, 0); 282 | } 283 | 284 | function imgToCanvas(){ 285 | let x,y,i,j,k, 286 | w = imageInfo.width, 287 | h = imageInfo.height, 288 | ctx = tempCanvas.getContext('2d'), 289 | imageData = ctx.getImageData(0, 0, w, h), 290 | res = imageData.data; 291 | let delResData = MagicWand.getCurrentResult(mask); 292 | if(delResData){ 293 | for (j = 0; j < delResData.length; j++) { 294 | i = delResData[j]; 295 | x = i % w; // calc x by index 296 | y = (i - x) / w; // calc y by index 297 | k = (y * w + x) * 4; 298 | res[k] = 0; // white 299 | res[k + 1] = 0; 300 | res[k + 2] = 0; 301 | res[k + 3] = 0; 302 | } 303 | } 304 | ctx.putImageData(imageData, 0, 0); 305 | } 306 | 307 | function downloadImg(e){ 308 | // First try a.download, then web filesystem, then object URLs 309 | // just use a.download 310 | e.stopPropagation(); 311 | imgToCanvas(); 312 | tempCanvas.toBlob(function(blob){ 313 | let aTemp = document.createElement('a'); 314 | aTemp.setAttribute('href', URL.createObjectURL(blob)); 315 | aTemp.setAttribute('download', 'Magic.png'); 316 | 317 | let evObj = document.createEvent('MouseEvents'); 318 | evObj.initMouseEvent( 'click', true, true, window, 0, 0, 0, 0, 0, false, false, true, false, 0, null); 319 | aTemp.dispatchEvent(evObj); 320 | }); 321 | } 322 | 323 | function reloadImg(){ 324 | //Jquery 325 | $('#wrapper').fadeOut('400', ()=> { 326 | $('#uImgWrapper').fadeIn('400'); 327 | if(mask&&mask.data) [mask.data,mask.predata] = [[],mask.data]; 328 | delResData = null; 329 | predelResData = null; 330 | MagicWand.clearPreData(); 331 | }); 332 | $('#panel').fadeOut('400'); 333 | 334 | } 335 | 336 | //TODO:完善 滚轮放大、空格拖拽(实现PS中的效果) 337 | //滚轮放大:在scale小于1的情况下滚轮放大以中心为焦点进行放大,大于1后跟随鼠标位置进行放大 338 | //空格拖拽:在放大的情况下,按住空格后对img、canvas的位置控制 339 | function handleMouseWheel(e){ 340 | let wd = e.wheelDelta; 341 | newScale += (wd > 0 ? scaleStep : -scaleStep); 342 | newScale = newScale < minScale ? minScale : newScale; 343 | newScale = newScale > maxScale ? maxScale : newScale; 344 | //img、canvas change need synchronization 345 | let imgContain = document.getElementById('test-picture'), 346 | canvas = document.getElementById('canvas'), 347 | content = document.getElementById('content'); 348 | 349 | if((parseInt(canvas.width*newScale,10)>window.innerWidth)|| 350 | (parseInt(canvas.width*newScale,10)>window.innerWidth)){ 351 | imgContain.style.transformOrigin = 'left top'; 352 | canvas.style.transformOrigin = 'left top'; 353 | tempCanvas.style.transformOrigin = 'left top'; 354 | }else{ 355 | imgContain.style.transformOrigin = 'center center'; 356 | canvas.style.transformOrigin = 'center center'; 357 | tempCanvas.style.transformOrigin = 'center center'; 358 | } 359 | imgContain.style.transform = 'scale('+newScale+')'; 360 | canvas.style.transform = 'scale('+newScale+')'; 361 | tempCanvas.style.transform = 'scale('+newScale+')'; 362 | 363 | if((parseInt(canvas.width*newScale,10)>window.innerWidth)){ 364 | content.style.overflowX = 'scroll'; 365 | }else if(parseInt(canvas.height*newScale,10)>window.innerHeight){ 366 | content.style.overflowY = 'scroll'; 367 | }else{ 368 | content.style.overflow = 'hidden'; 369 | } 370 | } 371 | 372 | --------------------------------------------------------------------------------