maskeditor.js 22.9 KB
Newer Older
1
2
3
import { app } from "../../scripts/app.js";
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { ComfyApp } from "../../scripts/app.js";
4
5
import { api } from "../../scripts/api.js"
import { ClipspaceDialog } from "./clipspace.js";
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

// Helper function to convert a data URL to a Blob object
function dataURLToBlob(dataURL) {
	const parts = dataURL.split(';base64,');
	const contentType = parts[0].split(':')[1];
	const byteString = atob(parts[1]);
	const arrayBuffer = new ArrayBuffer(byteString.length);
	const uint8Array = new Uint8Array(arrayBuffer);
	for (let i = 0; i < byteString.length; i++) {
		uint8Array[i] = byteString.charCodeAt(i);
	}
	return new Blob([arrayBuffer], { type: contentType });
}

function loadedImageToBlob(image) {
	const canvas = document.createElement('canvas');

	canvas.width = image.width;
	canvas.height = image.height;

	const ctx = canvas.getContext('2d');

	ctx.drawImage(image, 0, 0);

	const dataURL = canvas.toDataURL('image/png', 1);
	const blob = dataURLToBlob(dataURL);

	return blob;
}

Dr.Lt.Data's avatar
Dr.Lt.Data committed
36
37
38
39
40
41
42
43
44
45
46
47
function loadImage(imagePath) {
	return new Promise((resolve, reject) => {
		const image = new Image();

		image.onload = function() {
			resolve(image);
		};

		image.src = imagePath;
	});
}

48
async function uploadMask(filepath, formData) {
49
	await api.fetchApi('/upload/mask', {
50
51
52
53
54
55
56
		method: 'POST',
		body: formData
	}).then(response => {}).catch(error => {
		console.error('Error:', error);
	});

	ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']] = new Image();
Jairo Correa's avatar
Jairo Correa committed
57
	ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src = api.apiURL("/view?" + new URLSearchParams(filepath).toString() + app.getPreviewFormatParam() + app.getRandParam());
58
59
60
61
62
63
64

	if(ComfyApp.clipspace.images)
		ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']] = filepath;

	ClipspaceDialog.invalidatePreview();
}

Dr.Lt.Data's avatar
Dr.Lt.Data committed
65
function prepare_mask(image, maskCanvas, maskCtx) {
66
	// paste mask data into alpha channel
Dr.Lt.Data's avatar
Dr.Lt.Data committed
67
68
	maskCtx.drawImage(image, 0, 0, maskCanvas.width, maskCanvas.height);
	const maskData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height);
69

Dr.Lt.Data's avatar
Dr.Lt.Data committed
70
71
72
73
	// invert mask
	for (let i = 0; i < maskData.data.length; i += 4) {
		if(maskData.data[i+3] == 255)
			maskData.data[i+3] = 0;
74
		else
Dr.Lt.Data's avatar
Dr.Lt.Data committed
75
			maskData.data[i+3] = 255;
76

Dr.Lt.Data's avatar
Dr.Lt.Data committed
77
78
79
		maskData.data[i] = 0;
		maskData.data[i+1] = 0;
		maskData.data[i+2] = 0;
80
81
	}

Dr.Lt.Data's avatar
Dr.Lt.Data committed
82
83
	maskCtx.globalCompositeOperation = 'source-over';
	maskCtx.putImageData(maskData, 0, 0);
84
85
86
87
}

class MaskEditorDialog extends ComfyDialog {
	static instance = null;
88
89
90
91
92
93
94
95
96
97
98

	static getInstance() {
		if(!MaskEditorDialog.instance) {
			MaskEditorDialog.instance = new MaskEditorDialog(app);
		}

		return MaskEditorDialog.instance;
	}

	is_layout_created =  false;

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
	constructor() {
		super();
		this.element = $el("div.comfy-modal", { parent: document.body }, 
			[ $el("div.comfy-modal-content", 
				[...this.createButtons()]),
			]);
	}

	createButtons() {
		return [];
	}

	createButton(name, callback) {
		var button = document.createElement("button");
		button.innerText = name;
		button.addEventListener("click", callback);
		return button;
	}
117

118
119
120
121
122
123
	createLeftButton(name, callback) {
		var button = this.createButton(name, callback);
		button.style.cssFloat = "left";
		button.style.marginRight = "4px";
		return button;
	}
124

125
126
127
128
129
130
	createRightButton(name, callback) {
		var button = this.createButton(name, callback);
		button.style.cssFloat = "right";
		button.style.marginLeft = "4px";
		return button;
	}
131

132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
	createLeftSlider(self, name, callback) {
		const divElement = document.createElement('div');
		divElement.id = "maskeditor-slider";
		divElement.style.cssFloat = "left";
		divElement.style.fontFamily = "sans-serif";
		divElement.style.marginRight = "4px";
		divElement.style.color = "var(--input-text)";
		divElement.style.backgroundColor = "var(--comfy-input-bg)";
		divElement.style.borderRadius = "8px";
		divElement.style.borderColor = "var(--border-color)";
		divElement.style.borderStyle = "solid";
		divElement.style.fontSize = "15px";
		divElement.style.height = "21px";
		divElement.style.padding = "1px 6px";
		divElement.style.display = "flex";
		divElement.style.position = "relative";
		divElement.style.top = "2px";
		self.brush_slider_input = document.createElement('input');
		self.brush_slider_input.setAttribute('type', 'range');
		self.brush_slider_input.setAttribute('min', '1');
		self.brush_slider_input.setAttribute('max', '100');
		self.brush_slider_input.setAttribute('value', '10');
		const labelElement = document.createElement("label");
		labelElement.textContent = name;

		divElement.appendChild(labelElement);
		divElement.appendChild(self.brush_slider_input);

		self.brush_slider_input.addEventListener("change", callback);

		return divElement;
	}

	setlayout(imgCanvas, maskCanvas) {
		const self = this;

		// If it is specified as relative, using it only as a hidden placeholder for padding is recommended
		// to prevent anomalies where it exceeds a certain size and goes outside of the window.
		var bottom_panel = document.createElement("div");
		bottom_panel.style.position = "absolute";
		bottom_panel.style.bottom = "0px";
		bottom_panel.style.left = "20px";
		bottom_panel.style.right = "20px";
		bottom_panel.style.height = "50px";

		var brush = document.createElement("div");
		brush.id = "brush";
		brush.style.backgroundColor = "transparent";
		brush.style.outline = "1px dashed black";
		brush.style.boxShadow = "0 0 0 1px white";
		brush.style.borderRadius = "50%";
		brush.style.MozBorderRadius = "50%";
		brush.style.WebkitBorderRadius = "50%";
		brush.style.position = "absolute";
186
		brush.style.zIndex = 8889;
187
188
189
190
191
192
193
		brush.style.pointerEvents = "none";
		this.brush = brush;
		this.element.appendChild(imgCanvas);
		this.element.appendChild(maskCanvas);
		this.element.appendChild(bottom_panel);
		document.body.appendChild(brush);

Dr.Lt.Data's avatar
Dr.Lt.Data committed
194
		this.brush_size_slider = this.createLeftSlider(self, "Thickness", (event) => {
195
196
197
198
199
200
201
202
203
204
205
206
			self.brush_size = event.target.value;
			self.updateBrushPreview(self, null, null);
		});
		var clearButton = this.createLeftButton("Clear",
			() => {
				self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height);
			});
		var cancelButton = this.createRightButton("Cancel", () => {
			document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
			document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
			self.close();
		});
207
208

		this.saveButton = this.createRightButton("Save", () => {
209
210
211
212
213
214
215
216
217
218
			document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
			document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
				self.save();
			});

		this.element.appendChild(imgCanvas);
		this.element.appendChild(maskCanvas);
		this.element.appendChild(bottom_panel);

		bottom_panel.appendChild(clearButton);
219
		bottom_panel.appendChild(this.saveButton);
220
		bottom_panel.appendChild(cancelButton);
Dr.Lt.Data's avatar
Dr.Lt.Data committed
221
222
223
224
		bottom_panel.appendChild(this.brush_size_slider);

		imgCanvas.style.position = "absolute";
		maskCanvas.style.position = "absolute";
225
226
227
228

		imgCanvas.style.top = "200";
		imgCanvas.style.left = "0";

Dr.Lt.Data's avatar
Dr.Lt.Data committed
229
230
		maskCanvas.style.top = imgCanvas.style.top;
		maskCanvas.style.left = imgCanvas.style.left;
231
232
	}

Dr.Lt.Data's avatar
Dr.Lt.Data committed
233
234
235
236
237
	async show() {
		this.zoom_ratio = 1.0;
		this.pan_x = 0;
		this.pan_y = 0;

238
239
240
241
242
243
244
245
246
247
248
249
250
		if(!this.is_layout_created) {
			// layout
			const imgCanvas = document.createElement('canvas');
			const maskCanvas = document.createElement('canvas');

			imgCanvas.id = "imageCanvas";
			maskCanvas.id = "maskCanvas";

			this.setlayout(imgCanvas, maskCanvas);

			// prepare content
			this.imgCanvas = imgCanvas;
			this.maskCanvas = maskCanvas;
Dr.Lt.Data's avatar
Dr.Lt.Data committed
251
			this.maskCtx = maskCanvas.getContext('2d', {willReadFrequently: true });
252
253
254
255
256
257
258
259
260
261
262

			this.setEventHandler(maskCanvas);

			this.is_layout_created = true;

			// replacement of onClose hook since close is not real close
			const self = this;
			const observer = new MutationObserver(function(mutations) {
			mutations.forEach(function(mutation) {
					if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
						if(self.last_display_style && self.last_display_style != 'none' && self.element.style.display == 'none') {
Dr.Lt.Data's avatar
Dr.Lt.Data committed
263
264
							document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
							self.brush.style.display = "none";
265
266
267
268
269
270
271
							ComfyApp.onClipspaceEditorClosed();
						}

						self.last_display_style = self.element.style.display;
					}
				});
			});
272

273
274
275
			const config = { attributes: true };
			observer.observe(this.element, config);
		}
276

Dr.Lt.Data's avatar
Dr.Lt.Data committed
277
278
		// The keydown event needs to be reconfigured when closing the dialog as it gets removed.
		document.addEventListener('keydown', MaskEditorDialog.handleKeyDown);
279

280
281
282
283
284
285
286
		if(ComfyApp.clipspace_return_node) {
			this.saveButton.innerText = "Save to node";
		}
		else {
			this.saveButton.innerText = "Save";
		}
		this.saveButton.disabled = false;
287

288
		this.element.style.display = "block";
Dr.Lt.Data's avatar
Dr.Lt.Data committed
289
290
291
292
293
		this.element.style.width = "85%";
		this.element.style.margin = "0 7.5%";
		this.element.style.height = "100vh";
		this.element.style.top = "50%";
		this.element.style.left = "42%";
294
		this.element.style.zIndex = 8888; // NOTE: alert dialog must be high priority.
Dr.Lt.Data's avatar
Dr.Lt.Data committed
295
296
297
298

		await this.setImages(this.imgCanvas);

		this.is_visible = true;
299
300
301
302
	}

	isOpened() {
		return this.element.style.display == "block";
303
304
	}

Dr.Lt.Data's avatar
Dr.Lt.Data committed
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
	invalidateCanvas(orig_image, mask_image) {
		this.imgCanvas.width = orig_image.width;
		this.imgCanvas.height = orig_image.height;

		this.maskCanvas.width = orig_image.width;
		this.maskCanvas.height = orig_image.height;

		let imgCtx = this.imgCanvas.getContext('2d', {willReadFrequently: true });
		let maskCtx = this.maskCanvas.getContext('2d', {willReadFrequently: true });

		imgCtx.drawImage(orig_image, 0, 0, orig_image.width, orig_image.height);
		prepare_mask(mask_image, this.maskCanvas, maskCtx);
	}

	async setImages(imgCanvas) {
		let self = this;

		const imgCtx = imgCanvas.getContext('2d', {willReadFrequently: true });
323
324
325
		const maskCtx = this.maskCtx;
		const maskCanvas = this.maskCanvas;

326
327
328
		imgCtx.clearRect(0,0,this.imgCanvas.width,this.imgCanvas.height);
		maskCtx.clearRect(0,0,this.maskCanvas.width,this.maskCanvas.height);

329
330
331
332
333
		// image load
		const filepath = ComfyApp.clipspace.images;

		const alpha_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src)
		alpha_url.searchParams.delete('channel');
334
		alpha_url.searchParams.delete('preview');
335
		alpha_url.searchParams.set('channel', 'a');
Dr.Lt.Data's avatar
Dr.Lt.Data committed
336
		let mask_image = await loadImage(alpha_url);
337
338
339
340
341

		// original image load
		const rgb_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src);
		rgb_url.searchParams.delete('channel');
		rgb_url.searchParams.set('channel', 'rgb');
Dr.Lt.Data's avatar
Dr.Lt.Data committed
342
343
344
345
346
347
348
349
350
		this.image = new Image();
		this.image.onload = function() {
			maskCanvas.width = self.image.width;
			maskCanvas.height = self.image.height;

			self.invalidateCanvas(self.image, mask_image);
			self.initializeCanvasPanZoom();
		};
		this.image.src = rgb_url;
351
	}
352

Dr.Lt.Data's avatar
Dr.Lt.Data committed
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
	initializeCanvasPanZoom() {
		// set initialize
		let drawWidth = this.image.width;
		let drawHeight = this.image.height;

		let width = this.element.clientWidth;
		let height = this.element.clientHeight;

		if (this.image.width > width) {
			drawWidth = width;
			drawHeight = (drawWidth / this.image.width) * this.image.height;
		}

		if (drawHeight > height) {
			drawHeight = height;
			drawWidth = (drawHeight / this.image.height) * this.image.width;
		}

		this.zoom_ratio = drawWidth/this.image.width;

		const canvasX = (width - drawWidth) / 2;
		const canvasY = (height - drawHeight) / 2;
		this.pan_x = canvasX;
		this.pan_y = canvasY;

		this.invalidatePanZoom();
	}


	invalidatePanZoom() {
		let raw_width = this.image.width * this.zoom_ratio;
		let raw_height = this.image.height * this.zoom_ratio;

		if(this.pan_x + raw_width < 10) {
			this.pan_x = 10 - raw_width;
		}

		if(this.pan_y + raw_height < 10) {
			this.pan_y = 10 - raw_height;
		}

		let width = `${raw_width}px`;
		let height = `${raw_height}px`;
396

Dr.Lt.Data's avatar
Dr.Lt.Data committed
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
		let left = `${this.pan_x}px`;
		let top = `${this.pan_y}px`;

		this.maskCanvas.style.width = width;
		this.maskCanvas.style.height = height;
		this.maskCanvas.style.left = left;
		this.maskCanvas.style.top = top;

		this.imgCanvas.style.width = width;
		this.imgCanvas.style.height = height;
		this.imgCanvas.style.left = left;
		this.imgCanvas.style.top = top;
	}


	setEventHandler(maskCanvas) {
413
		const self = this;
Dr.Lt.Data's avatar
Dr.Lt.Data committed
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439

		if(!this.handler_registered) {
			maskCanvas.addEventListener("contextmenu", (event) => {
				event.preventDefault();
			});

			this.element.addEventListener('wheel', (event) => this.handleWheelEvent(self,event));
			this.element.addEventListener('pointermove', (event) => this.pointMoveEvent(self,event));
			this.element.addEventListener('touchmove', (event) => this.pointMoveEvent(self,event));

			this.element.addEventListener('dragstart', (event) => {
				if(event.ctrlKey) {
					event.preventDefault();
				}
			});

			maskCanvas.addEventListener('pointerdown', (event) => this.handlePointerDown(self,event));
			maskCanvas.addEventListener('pointermove', (event) => this.draw_move(self,event));
			maskCanvas.addEventListener('touchmove', (event) => this.draw_move(self,event));
			maskCanvas.addEventListener('pointerover', (event) => { this.brush.style.display = "block"; });
			maskCanvas.addEventListener('pointerleave', (event) => { this.brush.style.display = "none"; });

			document.addEventListener('pointerup', MaskEditorDialog.handlePointerUp);

			this.handler_registered = true;
		}
440
441
442
443
444
445
446
447
448
449
450
451
	}

	brush_size = 10;
	drawing_mode = false;
	lastx = -1;
	lasty = -1;
	lasttime = 0;

	static handleKeyDown(event) {
		const self = MaskEditorDialog.instance;
		if (event.key === ']') {
			self.brush_size = Math.min(self.brush_size+2, 100);
Dr.Lt.Data's avatar
Dr.Lt.Data committed
452
			self.brush_slider_input.value = self.brush_size;
453
454
		} else if (event.key === '[') {
			self.brush_size = Math.max(self.brush_size-2, 1);
Dr.Lt.Data's avatar
Dr.Lt.Data committed
455
			self.brush_slider_input.value = self.brush_size;
456
457
		} else if(event.key === 'Enter') {
			self.save();
458
459
460
461
462
463
464
		}

		self.updateBrushPreview(self);
	}

	static handlePointerUp(event) {
		event.preventDefault();
Dr.Lt.Data's avatar
Dr.Lt.Data committed
465
466
467
468

		this.mousedown_x = null;
		this.mousedown_y = null;

469
470
471
472
473
474
475
476
477
		MaskEditorDialog.instance.drawing_mode = false;
	}

	updateBrushPreview(self) {
		const brush = self.brush;

		var centerX = self.cursorX;
		var centerY = self.cursorY;

Dr.Lt.Data's avatar
Dr.Lt.Data committed
478
479
480
481
		brush.style.width = self.brush_size * 2 * this.zoom_ratio + "px";
		brush.style.height = self.brush_size * 2 * this.zoom_ratio + "px";
		brush.style.left = (centerX - self.brush_size * this.zoom_ratio) + "px";
		brush.style.top = (centerY - self.brush_size * this.zoom_ratio) + "px";
482
483
484
	}

	handleWheelEvent(self, event) {
Dr.Lt.Data's avatar
Dr.Lt.Data committed
485
486
487
488
489
490
491
492
493
494
		event.preventDefault();

		if(event.ctrlKey) {
			// zoom canvas
			if(event.deltaY < 0) {
				this.zoom_ratio = Math.min(10.0, this.zoom_ratio+0.2);
			}
			else {
				this.zoom_ratio = Math.max(0.2, this.zoom_ratio-0.2);
			}
495

Dr.Lt.Data's avatar
Dr.Lt.Data committed
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
			this.invalidatePanZoom();
		}
		else {
			// adjust brush size
			if(event.deltaY < 0)
				this.brush_size = Math.min(this.brush_size+2, 100);
			else
				this.brush_size = Math.max(this.brush_size-2, 1);

			this.brush_slider_input.value = this.brush_size;

			this.updateBrushPreview(this);
		}
	}

	pointMoveEvent(self, event) {
		this.cursorX = event.pageX;
		this.cursorY = event.pageY;
514
515

		self.updateBrushPreview(self);
Dr.Lt.Data's avatar
Dr.Lt.Data committed
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534

		if(event.ctrlKey) {
			event.preventDefault();
			self.pan_move(self, event);
		}
	}

	pan_move(self, event) {
		if(event.buttons == 1) {
			if(this.mousedown_x) {
				let deltaX = this.mousedown_x - event.clientX;
				let deltaY = this.mousedown_y - event.clientY;

				self.pan_x = this.mousedown_pan_x - deltaX;
				self.pan_y = this.mousedown_pan_y - deltaY;

				self.invalidatePanZoom();
			}
		}
535
536
537
	}

	draw_move(self, event) {
Dr.Lt.Data's avatar
Dr.Lt.Data committed
538
539
540
541
		if(event.ctrlKey) {
			return;
		}

542
543
544
545
546
547
548
		event.preventDefault();

		this.cursorX = event.pageX;
		this.cursorY = event.pageY;

		self.updateBrushPreview(self);

549
		if (window.TouchEvent && event instanceof TouchEvent || event.buttons == 1) {
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
			var diff = performance.now() - self.lasttime;

			const maskRect = self.maskCanvas.getBoundingClientRect();

			var x = event.offsetX;
			var y = event.offsetY

			if(event.offsetX == null) {
				x = event.targetTouches[0].clientX - maskRect.left;
			}

			if(event.offsetY == null) {
				y = event.targetTouches[0].clientY - maskRect.top;
			}

Dr.Lt.Data's avatar
Dr.Lt.Data committed
565
566
567
			x /= self.zoom_ratio;
			y /= self.zoom_ratio;

568
569
570
571
572
			var brush_size = this.brush_size;
			if(event instanceof PointerEvent && event.pointerType == 'pen') {
				brush_size *= event.pressure;
				this.last_pressure = event.pressure;
			}
573
			else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
				// The firing interval of PointerEvents in Pen is unreliable, so it is supplemented by TouchEvents.
				brush_size *= this.last_pressure;
			}
			else {
				brush_size = this.brush_size;
			}

			if(diff > 20 && !this.drawing_mode)
				requestAnimationFrame(() => {
					self.maskCtx.beginPath();
					self.maskCtx.fillStyle = "rgb(0,0,0)";
					self.maskCtx.globalCompositeOperation = "source-over";
					self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
					self.maskCtx.fill();
					self.lastx = x;
					self.lasty = y;
				});
			else
				requestAnimationFrame(() => {
					self.maskCtx.beginPath();
					self.maskCtx.fillStyle = "rgb(0,0,0)";
					self.maskCtx.globalCompositeOperation = "source-over";

					var dx = x - self.lastx;
					var dy = y - self.lasty;

					var distance = Math.sqrt(dx * dx + dy * dy);
					var directionX = dx / distance;
					var directionY = dy / distance;

					for (var i = 0; i < distance; i+=5) {
						var px = self.lastx + (directionX * i);
						var py = self.lasty + (directionY * i);
						self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
						self.maskCtx.fill();
					}
					self.lastx = x;
					self.lasty = y;
				});

			self.lasttime = performance.now();
		}
		else if(event.buttons == 2 || event.buttons == 5 || event.buttons == 32) {
			const maskRect = self.maskCanvas.getBoundingClientRect();
Dr.Lt.Data's avatar
Dr.Lt.Data committed
618
619
			const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
			const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;
620
621
622
623
624
625

			var brush_size = this.brush_size;
			if(event instanceof PointerEvent && event.pointerType == 'pen') {
				brush_size *= event.pressure;
				this.last_pressure = event.pressure;
			}
626
			else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
				brush_size *= this.last_pressure;
			}
			else {
				brush_size = this.brush_size;
			}

			if(diff > 20 && !drawing_mode) // cannot tracking drawing_mode for touch event
				requestAnimationFrame(() => {
					self.maskCtx.beginPath();
					self.maskCtx.globalCompositeOperation = "destination-out";
					self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
					self.maskCtx.fill();
					self.lastx = x;
					self.lasty = y;
				});
			else
				requestAnimationFrame(() => {
					self.maskCtx.beginPath();
					self.maskCtx.globalCompositeOperation = "destination-out";
					
					var dx = x - self.lastx;
					var dy = y - self.lasty;

					var distance = Math.sqrt(dx * dx + dy * dy);
					var directionX = dx / distance;
					var directionY = dy / distance;

					for (var i = 0; i < distance; i+=5) {
						var px = self.lastx + (directionX * i);
						var py = self.lasty + (directionY * i);
						self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
						self.maskCtx.fill();
					}
					self.lastx = x;
					self.lasty = y;
				});

				self.lasttime = performance.now();
		}
	}

	handlePointerDown(self, event) {
Dr.Lt.Data's avatar
Dr.Lt.Data committed
669
670
671
672
673
674
675
676
677
678
679
		if(event.ctrlKey) {
			if (event.buttons == 1) {
				this.mousedown_x = event.clientX;
				this.mousedown_y = event.clientY;

				this.mousedown_pan_x = this.pan_x;
				this.mousedown_pan_y = this.pan_y;
			}
			return;
		}

680
681
682
683
684
685
686
687
688
689
690
		var brush_size = this.brush_size;
		if(event instanceof PointerEvent && event.pointerType == 'pen') {
			brush_size *= event.pressure;
			this.last_pressure = event.pressure;
		}

		if ([0, 2, 5].includes(event.button)) {
			self.drawing_mode = true;

			event.preventDefault();
			const maskRect = self.maskCanvas.getBoundingClientRect();
Dr.Lt.Data's avatar
Dr.Lt.Data committed
691
692
			const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
			const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708

			self.maskCtx.beginPath();
			if (event.button == 0) {
				self.maskCtx.fillStyle = "rgb(0,0,0)";
				self.maskCtx.globalCompositeOperation = "source-over";
			} else {
				self.maskCtx.globalCompositeOperation = "destination-out";
			}
			self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
			self.maskCtx.fill();
			self.lastx = x;
			self.lasty = y;
			self.lasttime = performance.now();
		}
	}

709
	async save() {
Dr.Lt.Data's avatar
Dr.Lt.Data committed
710
711
712
713
		const backupCanvas = document.createElement('canvas');
		const backupCtx = backupCanvas.getContext('2d', {willReadFrequently:true});
		backupCanvas.width = this.image.width;
		backupCanvas.height = this.image.height;
714

Dr.Lt.Data's avatar
Dr.Lt.Data committed
715
		backupCtx.clearRect(0,0, backupCanvas.width, backupCanvas.height);
716
717
		backupCtx.drawImage(this.maskCanvas,
			0, 0, this.maskCanvas.width, this.maskCanvas.height,
Dr.Lt.Data's avatar
Dr.Lt.Data committed
718
			0, 0, backupCanvas.width, backupCanvas.height);
719
720

		// paste mask data into alpha channel
Dr.Lt.Data's avatar
Dr.Lt.Data committed
721
		const backupData = backupCtx.getImageData(0, 0, backupCanvas.width, backupCanvas.height);
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743

		// refine mask image
		for (let i = 0; i < backupData.data.length; i += 4) {
			if(backupData.data[i+3] == 255)
				backupData.data[i+3] = 0;
			else
				backupData.data[i+3] = 255;

			backupData.data[i] = 0;
			backupData.data[i+1] = 0;
			backupData.data[i+2] = 0;
		}

		backupCtx.globalCompositeOperation = 'source-over';
		backupCtx.putImageData(backupData, 0, 0);

		const formData = new FormData();
		const filename = "clipspace-mask-" + performance.now() + ".png";

		const item =
			{
				"filename": filename,
comfyanonymous's avatar
comfyanonymous committed
744
745
				"subfolder": "clipspace",
				"type": "input",
746
747
748
749
750
751
752
753
754
755
756
757
			};

		if(ComfyApp.clipspace.images)
			ComfyApp.clipspace.images[0] = item;

		if(ComfyApp.clipspace.widgets) {
			const index = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image');

			if(index >= 0)
				ComfyApp.clipspace.widgets[index].value = item;
		}

Dr.Lt.Data's avatar
Dr.Lt.Data committed
758
		const dataURL = backupCanvas.toDataURL();
759
760
		const blob = dataURLToBlob(dataURL);

761
762
763
764
765
766
767
768
769
770
771
		let original_url = new URL(this.image.src);

		const original_ref = { filename: original_url.searchParams.get('filename') };

		let original_subfolder = original_url.searchParams.get("subfolder");
		if(original_subfolder)
			original_ref.subfolder = original_subfolder;

		let original_type = original_url.searchParams.get("type");
		if(original_type)
			original_ref.type = original_type;
772
773

		formData.append('image', blob, filename);
774
		formData.append('original_ref', JSON.stringify(original_ref));
comfyanonymous's avatar
comfyanonymous committed
775
776
		formData.append('type', "input");
		formData.append('subfolder', "clipspace");
777

778
779
780
781
		this.saveButton.innerText = "Saving...";
		this.saveButton.disabled = true;
		await uploadMask(item, formData);
		ComfyApp.onClipspaceEditorSave();
782
783
784
785
786
787
788
		this.close();
	}
}

app.registerExtension({
	name: "Comfy.MaskEditor",
	init(app) {
789
		ComfyApp.open_maskeditor =
790
			function () {
791
792
793
794
				const dlg = MaskEditorDialog.getInstance();
				if(!dlg.isOpened()) {
					dlg.show();
				}
795
796
797
			};

		const context_predicate = () => ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0
798
		ClipspaceDialog.registerButton("MaskEditor", context_predicate, ComfyApp.open_maskeditor);
799
	}
Jairo Correa's avatar
Jairo Correa committed
800
});