├── 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 | 
20 |
21 | 
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 |
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 |
--------------------------------------------------------------------------------