mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Remove unused and redundant methods from canvas modules
Cleaned up the codebase by removing unused or redundant methods from Canvas, CanvasIO, CanvasLayers, CanvasLayersPanel, CanvasRenderer, and MaskTool. This reduces code complexity and improves maintainability without affecting core functionality.
This commit is contained in:
@@ -298,12 +298,6 @@ export class Canvas {
|
||||
removeSelectedLayers() {
|
||||
return this.canvasSelection.removeSelectedLayers();
|
||||
}
|
||||
/**
|
||||
* Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu)
|
||||
*/
|
||||
duplicateSelectedLayers() {
|
||||
return this.canvasSelection.duplicateSelectedLayers();
|
||||
}
|
||||
/**
|
||||
* Aktualizuje zaznaczenie warstw i powiadamia wszystkie komponenty.
|
||||
* To jest "jedyne źródło prawdy" o zmianie zaznaczenia.
|
||||
|
||||
104
js/CanvasIO.js
104
js/CanvasIO.js
@@ -481,21 +481,6 @@ export class CanvasIO {
|
||||
img.src = canvas.toDataURL();
|
||||
});
|
||||
}
|
||||
async retryDataLoad(maxRetries = 3, delay = 1000) {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
await this.initNodeData();
|
||||
return;
|
||||
}
|
||||
catch (error) {
|
||||
log.warn(`Retry ${i + 1}/${maxRetries} failed:`, error);
|
||||
if (i < maxRetries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
log.error("Failed to load data after", maxRetries, "retries");
|
||||
}
|
||||
async processMaskData(maskData) {
|
||||
try {
|
||||
if (!maskData)
|
||||
@@ -518,73 +503,6 @@ export class CanvasIO {
|
||||
log.error("Error processing mask data:", error);
|
||||
}
|
||||
}
|
||||
async loadImageFromCache(base64Data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = base64Data;
|
||||
});
|
||||
}
|
||||
async importImage(cacheData) {
|
||||
try {
|
||||
log.info("Starting image import with cache data");
|
||||
const img = await this.loadImageFromCache(cacheData.image);
|
||||
const mask = cacheData.mask ? await this.loadImageFromCache(cacheData.mask) : null;
|
||||
const scale = Math.min(this.canvas.width / img.width * 0.8, this.canvas.height / img.height * 0.8);
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = img.width;
|
||||
tempCanvas.height = img.height;
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!tempCtx)
|
||||
throw new Error("Could not create temp context");
|
||||
tempCtx.drawImage(img, 0, 0);
|
||||
if (mask) {
|
||||
const imageData = tempCtx.getImageData(0, 0, img.width, img.height);
|
||||
const maskCanvas = document.createElement('canvas');
|
||||
maskCanvas.width = img.width;
|
||||
maskCanvas.height = img.height;
|
||||
const maskCtx = maskCanvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!maskCtx)
|
||||
throw new Error("Could not create mask context");
|
||||
maskCtx.drawImage(mask, 0, 0);
|
||||
const maskData = maskCtx.getImageData(0, 0, img.width, img.height);
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
imageData.data[i + 3] = maskData.data[i];
|
||||
}
|
||||
tempCtx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
const finalImage = new Image();
|
||||
await new Promise((resolve) => {
|
||||
finalImage.onload = resolve;
|
||||
finalImage.src = tempCanvas.toDataURL();
|
||||
});
|
||||
const layer = {
|
||||
id: '', // This will be set in addLayerWithImage
|
||||
imageId: '', // This will be set in addLayerWithImage
|
||||
name: 'Layer',
|
||||
image: finalImage,
|
||||
x: (this.canvas.width - img.width * scale) / 2,
|
||||
y: (this.canvas.height - img.height * scale) / 2,
|
||||
width: img.width * scale,
|
||||
height: img.height * scale,
|
||||
originalWidth: img.width,
|
||||
originalHeight: img.height,
|
||||
rotation: 0,
|
||||
zIndex: this.canvas.layers.length,
|
||||
blendMode: 'normal',
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
};
|
||||
this.canvas.layers.push(layer);
|
||||
this.canvas.updateSelection([layer]);
|
||||
this.canvas.render();
|
||||
this.canvas.saveState();
|
||||
}
|
||||
catch (error) {
|
||||
log.error('Error importing image:', error);
|
||||
}
|
||||
}
|
||||
async importLatestImage() {
|
||||
try {
|
||||
log.info("Fetching latest image from server...");
|
||||
@@ -682,26 +600,4 @@ export class CanvasIO {
|
||||
clippedImage.src = canvas.toDataURL();
|
||||
});
|
||||
}
|
||||
createMaskFromShape(shape, width, height) {
|
||||
const { canvas, ctx } = createCanvas(width, height);
|
||||
if (!ctx) {
|
||||
throw new Error("Could not create canvas context for mask");
|
||||
}
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(shape.points[0].x, shape.points[0].y);
|
||||
for (let i = 1; i < shape.points.length; i++) {
|
||||
ctx.lineTo(shape.points[i].x, shape.points[i].y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
const imageData = ctx.getImageData(0, 0, width, height);
|
||||
const maskData = new Float32Array(width * height);
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
maskData[i / 4] = imageData.data[i] / 255;
|
||||
}
|
||||
return maskData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -764,21 +764,6 @@ export class CanvasLayers {
|
||||
menu.parentNode.removeChild(menu);
|
||||
}
|
||||
}
|
||||
showOpacitySlider(mode) {
|
||||
const slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
slider.min = '0';
|
||||
slider.max = '100';
|
||||
slider.value = String(this.blendOpacity);
|
||||
slider.className = 'blend-opacity-slider';
|
||||
slider.addEventListener('input', (e) => {
|
||||
this.blendOpacity = parseInt(e.target.value, 10);
|
||||
});
|
||||
const modeElement = document.querySelector(`[data-blend-mode="${mode}"]`);
|
||||
if (modeElement) {
|
||||
modeElement.appendChild(slider);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Zunifikowana funkcja do generowania blob z canvas
|
||||
* @param options Opcje renderowania
|
||||
|
||||
@@ -778,29 +778,6 @@ export class MaskTool {
|
||||
this.updateActiveMaskCanvas();
|
||||
return this.activeMaskCanvas;
|
||||
}
|
||||
getMaskImageWithAlpha() {
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = this.maskCanvas.width;
|
||||
tempCanvas.height = this.maskCanvas.height;
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!tempCtx) {
|
||||
throw new Error("Failed to get 2D context for temporary canvas");
|
||||
}
|
||||
tempCtx.drawImage(this.maskCanvas, 0, 0);
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const alpha = data[i];
|
||||
data[i] = 255;
|
||||
data[i + 1] = 255;
|
||||
data[i + 2] = 255;
|
||||
data[i + 3] = alpha;
|
||||
}
|
||||
tempCtx.putImageData(imageData, 0, 0);
|
||||
const maskImage = new Image();
|
||||
maskImage.src = tempCanvas.toDataURL();
|
||||
return maskImage;
|
||||
}
|
||||
resize(width, height) {
|
||||
this.initPreviewCanvas();
|
||||
const oldMask = this.maskCanvas;
|
||||
@@ -830,11 +807,6 @@ export class MaskTool {
|
||||
log.info(`Mask canvas resized to ${this.activeMaskCanvas.width}x${this.activeMaskCanvas.height}, position (${this.x}, ${this.y})`);
|
||||
log.info(`Canvas size change: width ${isIncreasingWidth ? 'increased' : 'decreased'}, height ${isIncreasingHeight ? 'increased' : 'decreased'}`);
|
||||
}
|
||||
updatePosition(dx, dy) {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
log.info(`Mask position updated to (${this.x}, ${this.y})`);
|
||||
}
|
||||
/**
|
||||
* Updates mask canvas to ensure it covers the current output area
|
||||
* This should be called when output area position or size changes
|
||||
@@ -1405,21 +1377,6 @@ export class MaskTool {
|
||||
}
|
||||
return distances;
|
||||
}
|
||||
/**
|
||||
* Creates an expanded mask using distance transform - much better for complex shapes
|
||||
* than the centroid-based approach. This version only does expansion without transparency calculations.
|
||||
*/
|
||||
_calculateExpandedPoints(points, expansionValue) {
|
||||
if (points.length < 3 || expansionValue === 0)
|
||||
return points;
|
||||
// For expansion, we need to create a temporary canvas to use the distance transform approach
|
||||
// This will give us much better results for complex shapes than the centroid method
|
||||
const tempCanvas = this._createExpandedMaskCanvas(points, expansionValue, this.maskCanvas.width, this.maskCanvas.height);
|
||||
// Extract the expanded shape outline from the canvas
|
||||
// For now, return the original points as a fallback - the real expansion happens in the canvas
|
||||
// The calling code will use the canvas directly instead of these points
|
||||
return points;
|
||||
}
|
||||
/**
|
||||
* Creates an expanded/contracted mask canvas using simple morphological operations
|
||||
* This gives SHARP edges without smoothing, unlike distance transform
|
||||
|
||||
@@ -174,7 +174,6 @@ export class Canvas {
|
||||
this.previewVisible = false;
|
||||
}
|
||||
|
||||
|
||||
async waitForWidget(name: any, node: any, interval = 100, timeout = 20000) {
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -194,7 +193,6 @@ export class Canvas {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Kontroluje widoczność podglądu canvas
|
||||
* @param {boolean} visible - Czy podgląd ma być widoczny
|
||||
@@ -271,7 +269,6 @@ export class Canvas {
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ładuje stan canvas z bazy danych
|
||||
*/
|
||||
@@ -323,7 +320,6 @@ export class Canvas {
|
||||
log.debug('Undo completed, layers count:', this.layers.length);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ponów cofniętą operację
|
||||
*/
|
||||
@@ -396,13 +392,6 @@ export class Canvas {
|
||||
return this.canvasSelection.removeSelectedLayers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu)
|
||||
*/
|
||||
duplicateSelectedLayers() {
|
||||
return this.canvasSelection.duplicateSelectedLayers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualizuje zaznaczenie warstw i powiadamia wszystkie komponenty.
|
||||
* To jest "jedyne źródło prawdy" o zmianie zaznaczenia.
|
||||
@@ -535,7 +524,6 @@ export class Canvas {
|
||||
log.debug('Auto-refresh handlers setup complete, reading from node widget: auto_refresh_after_generation');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Uruchamia edytor masek
|
||||
* @param {Image|HTMLCanvasElement|null} predefinedMask - Opcjonalna maska do nałożenia po otwarciu editora
|
||||
@@ -545,7 +533,6 @@ export class Canvas {
|
||||
return this.canvasMask.startMaskEditor(predefinedMask as any, sendCleanImage);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inicjalizuje podstawowe właściwości canvas
|
||||
*/
|
||||
|
||||
119
src/CanvasIO.ts
119
src/CanvasIO.ts
@@ -574,21 +574,6 @@ export class CanvasIO {
|
||||
});
|
||||
}
|
||||
|
||||
async retryDataLoad(maxRetries = 3, delay = 1000): Promise<void> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
await this.initNodeData();
|
||||
return;
|
||||
} catch (error) {
|
||||
log.warn(`Retry ${i + 1}/${maxRetries} failed:`, error);
|
||||
if (i < maxRetries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
log.error("Failed to load data after", maxRetries, "retries");
|
||||
}
|
||||
|
||||
async processMaskData(maskData: any): Promise<void> {
|
||||
try {
|
||||
if (!maskData) return;
|
||||
@@ -614,84 +599,6 @@ export class CanvasIO {
|
||||
}
|
||||
}
|
||||
|
||||
async loadImageFromCache(base64Data: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = base64Data;
|
||||
});
|
||||
}
|
||||
|
||||
async importImage(cacheData: { image: string, mask?: string }): Promise<void> {
|
||||
try {
|
||||
log.info("Starting image import with cache data");
|
||||
const img = await this.loadImageFromCache(cacheData.image);
|
||||
const mask = cacheData.mask ? await this.loadImageFromCache(cacheData.mask) : null;
|
||||
|
||||
const scale = Math.min(
|
||||
this.canvas.width / img.width * 0.8,
|
||||
this.canvas.height / img.height * 0.8
|
||||
);
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = img.width;
|
||||
tempCanvas.height = img.height;
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!tempCtx) throw new Error("Could not create temp context");
|
||||
|
||||
tempCtx.drawImage(img, 0, 0);
|
||||
|
||||
if (mask) {
|
||||
const imageData = tempCtx.getImageData(0, 0, img.width, img.height);
|
||||
const maskCanvas = document.createElement('canvas');
|
||||
maskCanvas.width = img.width;
|
||||
maskCanvas.height = img.height;
|
||||
const maskCtx = maskCanvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!maskCtx) throw new Error("Could not create mask context");
|
||||
maskCtx.drawImage(mask, 0, 0);
|
||||
const maskData = maskCtx.getImageData(0, 0, img.width, img.height);
|
||||
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
imageData.data[i + 3] = maskData.data[i];
|
||||
}
|
||||
|
||||
tempCtx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
const finalImage = new Image();
|
||||
await new Promise((resolve) => {
|
||||
finalImage.onload = resolve;
|
||||
finalImage.src = tempCanvas.toDataURL();
|
||||
});
|
||||
|
||||
const layer: Layer = {
|
||||
id: '', // This will be set in addLayerWithImage
|
||||
imageId: '', // This will be set in addLayerWithImage
|
||||
name: 'Layer',
|
||||
image: finalImage,
|
||||
x: (this.canvas.width - img.width * scale) / 2,
|
||||
y: (this.canvas.height - img.height * scale) / 2,
|
||||
width: img.width * scale,
|
||||
height: img.height * scale,
|
||||
originalWidth: img.width,
|
||||
originalHeight: img.height,
|
||||
rotation: 0,
|
||||
zIndex: this.canvas.layers.length,
|
||||
blendMode: 'normal',
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
};
|
||||
|
||||
this.canvas.layers.push(layer);
|
||||
this.canvas.updateSelection([layer]);
|
||||
this.canvas.render();
|
||||
this.canvas.saveState();
|
||||
} catch (error) {
|
||||
log.error('Error importing image:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async importLatestImage(): Promise<boolean> {
|
||||
try {
|
||||
log.info("Fetching latest image from server...");
|
||||
@@ -798,30 +705,4 @@ export class CanvasIO {
|
||||
clippedImage.src = canvas.toDataURL();
|
||||
});
|
||||
}
|
||||
|
||||
createMaskFromShape(shape: Shape, width: number, height: number): Float32Array {
|
||||
const { canvas, ctx } = createCanvas(width, height);
|
||||
if (!ctx) {
|
||||
throw new Error("Could not create canvas context for mask");
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(shape.points[0].x, shape.points[0].y);
|
||||
for (let i = 1; i < shape.points.length; i++) {
|
||||
ctx.lineTo(shape.points[i].x, shape.points[i].y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, width, height);
|
||||
const maskData = new Float32Array(width * height);
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
maskData[i / 4] = imageData.data[i] / 255;
|
||||
}
|
||||
return maskData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -879,24 +879,6 @@ export class CanvasLayers {
|
||||
}
|
||||
}
|
||||
|
||||
showOpacitySlider(mode: string): void {
|
||||
const slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
slider.min = '0';
|
||||
slider.max = '100';
|
||||
slider.value = String(this.blendOpacity);
|
||||
slider.className = 'blend-opacity-slider';
|
||||
|
||||
slider.addEventListener('input', (e) => {
|
||||
this.blendOpacity = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
});
|
||||
|
||||
const modeElement = document.querySelector(`[data-blend-mode="${mode}"]`);
|
||||
if (modeElement) {
|
||||
modeElement.appendChild(slider);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zunifikowana funkcja do generowania blob z canvas
|
||||
* @param options Opcje renderowania
|
||||
|
||||
@@ -534,7 +534,6 @@ export class CanvasLayersPanel {
|
||||
log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.canvasSelection.selectedLayers.length}`);
|
||||
}
|
||||
|
||||
|
||||
startEditingLayerName(nameElement: HTMLElement, layer: Layer): void {
|
||||
const currentName = layer.name;
|
||||
nameElement.classList.add('editing');
|
||||
@@ -572,7 +571,6 @@ export class CanvasLayersPanel {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
ensureUniqueName(proposedName: string, currentLayer: Layer): string {
|
||||
const existingNames = this.canvas.layers
|
||||
.filter((layer: Layer) => layer !== currentLayer)
|
||||
@@ -731,7 +729,6 @@ export class CanvasLayersPanel {
|
||||
this.draggedElements = [];
|
||||
}
|
||||
|
||||
|
||||
onLayersChanged(): void {
|
||||
this.renderLayers();
|
||||
}
|
||||
|
||||
@@ -275,7 +275,6 @@ export class CanvasRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
drawGrid(ctx: any) {
|
||||
const gridSize = 64;
|
||||
const lineWidth = 0.5 / this.canvas.viewport.zoom;
|
||||
|
||||
@@ -946,30 +946,6 @@ export class MaskTool {
|
||||
return this.activeMaskCanvas;
|
||||
}
|
||||
|
||||
getMaskImageWithAlpha(): HTMLImageElement {
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = this.maskCanvas.width;
|
||||
tempCanvas.height = this.maskCanvas.height;
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!tempCtx) {
|
||||
throw new Error("Failed to get 2D context for temporary canvas");
|
||||
}
|
||||
tempCtx.drawImage(this.maskCanvas, 0, 0);
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const alpha = data[i];
|
||||
data[i] = 255;
|
||||
data[i + 1] = 255;
|
||||
data[i + 2] = 255;
|
||||
data[i + 3] = alpha;
|
||||
}
|
||||
tempCtx.putImageData(imageData, 0, 0);
|
||||
const maskImage = new Image();
|
||||
maskImage.src = tempCanvas.toDataURL();
|
||||
return maskImage;
|
||||
}
|
||||
|
||||
resize(width: number, height: number): void {
|
||||
this.initPreviewCanvas();
|
||||
const oldMask = this.maskCanvas;
|
||||
@@ -1009,12 +985,6 @@ export class MaskTool {
|
||||
log.info(`Canvas size change: width ${isIncreasingWidth ? 'increased' : 'decreased'}, height ${isIncreasingHeight ? 'increased' : 'decreased'}`);
|
||||
}
|
||||
|
||||
updatePosition(dx: number, dy: number): void {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
log.info(`Mask position updated to (${this.x}, ${this.y})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates mask canvas to ensure it covers the current output area
|
||||
* This should be called when output area position or size changes
|
||||
@@ -1706,23 +1676,6 @@ export class MaskTool {
|
||||
return distances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an expanded mask using distance transform - much better for complex shapes
|
||||
* than the centroid-based approach. This version only does expansion without transparency calculations.
|
||||
*/
|
||||
private _calculateExpandedPoints(points: Point[], expansionValue: number): Point[] {
|
||||
if (points.length < 3 || expansionValue === 0) return points;
|
||||
|
||||
// For expansion, we need to create a temporary canvas to use the distance transform approach
|
||||
// This will give us much better results for complex shapes than the centroid method
|
||||
const tempCanvas = this._createExpandedMaskCanvas(points, expansionValue, this.maskCanvas.width, this.maskCanvas.height);
|
||||
|
||||
// Extract the expanded shape outline from the canvas
|
||||
// For now, return the original points as a fallback - the real expansion happens in the canvas
|
||||
// The calling code will use the canvas directly instead of these points
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an expanded/contracted mask canvas using simple morphological operations
|
||||
* This gives SHARP edges without smoothing, unlike distance transform
|
||||
|
||||
Reference in New Issue
Block a user