refactor: centralize mask pixel primitives
This commit is contained in:
+2
-6
@@ -3,6 +3,7 @@ import { createModuleLogger } from "./log_system/log_funcs.js";
|
||||
import { showErrorNotification } from "./utils/NotificationUtils.js";
|
||||
import { webSocketManager } from "./utils/WebSocketManager.js";
|
||||
import { scaleImageToFit, loadImage, tensorToImageData, createImageFromImageData } from "./utils/ImageUtils.js";
|
||||
import { fillInverseAlphaMask } from "./utils/MaskPixelUtils.js";
|
||||
const log = createModuleLogger('CanvasIO');
|
||||
export class CanvasIO {
|
||||
constructor(canvas) {
|
||||
@@ -68,12 +69,7 @@ export class CanvasIO {
|
||||
log.debug(`Finished rendering layers`);
|
||||
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
for (let i = 0; i < visibilityData.data.length; i += 4) {
|
||||
const alpha = visibilityData.data[i + 3];
|
||||
const maskValue = 255 - alpha;
|
||||
maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue;
|
||||
maskData.data[i + 3] = 255;
|
||||
}
|
||||
fillInverseAlphaMask(visibilityData, maskData);
|
||||
maskCtx.putImageData(maskData, 0, 0);
|
||||
this.canvas.outputAreaShape = originalShape;
|
||||
// Use optimized getMaskForOutputArea() instead of getMask() for better performance
|
||||
|
||||
+2
-6
@@ -10,6 +10,7 @@ import { app } from "../../scripts/app.js";
|
||||
import { ComfyApp } from "../../scripts/app.js";
|
||||
import { ClipboardManager } from "./utils/ClipboardManager.js";
|
||||
import { createDistanceFieldMaskSync } from "./utils/ImageAnalysis.js";
|
||||
import { fillInverseAlphaMask } from "./utils/MaskPixelUtils.js";
|
||||
const log = createModuleLogger('CanvasLayers');
|
||||
export class CanvasLayers {
|
||||
constructor(canvas) {
|
||||
@@ -1538,12 +1539,7 @@ export class CanvasLayers {
|
||||
// Konwertuj przezroczystość warstw na maskę
|
||||
const visibilityData = visibilityCtx.getImageData(0, 0, bounds.width, bounds.height);
|
||||
const maskData = maskCtx.getImageData(0, 0, bounds.width, bounds.height);
|
||||
for (let i = 0; i < visibilityData.data.length; i += 4) {
|
||||
const alpha = visibilityData.data[i + 3];
|
||||
const maskValue = 255 - alpha; // Odwróć alpha żeby stworzyć maskę
|
||||
maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue;
|
||||
maskData.data[i + 3] = 255; // Solidna maska
|
||||
}
|
||||
fillInverseAlphaMask(visibilityData, maskData);
|
||||
maskCtx.putImageData(maskData, 0, 0);
|
||||
// Aplikuj maskę narzędzia jeśli istnieje - używaj zoptymalizowanej metody
|
||||
const toolMaskCanvas = this.canvas.maskTool.getMaskForOutputArea();
|
||||
|
||||
+6
-35
@@ -1,6 +1,6 @@
|
||||
import { createModuleLogger } from "./log_system/log_funcs.js";
|
||||
import { createCanvas, createCanvasWithContext } from "./utils/CommonUtils.js";
|
||||
import { calculateDistanceTransform } from "./utils/MaskPixelUtils.js";
|
||||
import { applyLuminanceAsAlpha, calculateDistanceTransform, imageDataToBinaryMask } from "./utils/MaskPixelUtils.js";
|
||||
const log = createModuleLogger('Mask_tool');
|
||||
export class MaskTool {
|
||||
constructor(canvasInstance, callbacks = {}) {
|
||||
@@ -438,11 +438,7 @@ export class MaskTool {
|
||||
const { canvas, ctx } = createCanvasWithContext(width, height);
|
||||
this.drawShapeOnCanvas(ctx, points);
|
||||
const maskImage = ctx.getImageData(0, 0, width, height);
|
||||
const binaryData = new Uint8Array(width * height);
|
||||
for (let i = 0; i < binaryData.length; i++) {
|
||||
binaryData[i] = maskImage.data[i * 4] > 0 ? 1 : 0;
|
||||
}
|
||||
return binaryData;
|
||||
return imageDataToBinaryMask(maskImage, width, height, 0);
|
||||
}
|
||||
/**
|
||||
* Creates output canvas with image data
|
||||
@@ -1235,10 +1231,7 @@ export class MaskTool {
|
||||
tempCtx.fill('evenodd'); // Use evenodd to handle holes correctly
|
||||
}
|
||||
const maskImage = tempCtx.getImageData(0, 0, width, height);
|
||||
const binaryData = new Uint8Array(width * height);
|
||||
for (let i = 0; i < binaryData.length; i++) {
|
||||
binaryData[i] = maskImage.data[i * 4] > 0 ? 1 : 0;
|
||||
}
|
||||
const binaryData = imageDataToBinaryMask(maskImage, width, height, 0);
|
||||
let resultMask;
|
||||
const scaledExpansionValue = Math.round(Math.abs(expansionValue * zoom));
|
||||
if (expansionValue >= 0) {
|
||||
@@ -1466,15 +1459,7 @@ export class MaskTool {
|
||||
throw new Error("Could not create mask processing context");
|
||||
ctx.drawImage(image, 0, 0);
|
||||
const imgData = ctx.getImageData(0, 0, image.width, image.height);
|
||||
const data = imgData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const r = data[i], g = data[i + 1], b = data[i + 2];
|
||||
const lum = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
||||
data[i] = 255; // force white color (color channels ignored downstream)
|
||||
data[i + 1] = 255;
|
||||
data[i + 2] = 255;
|
||||
data[i + 3] = lum; // alpha encodes mask strength: white -> strong, black -> 0
|
||||
}
|
||||
applyLuminanceAsAlpha(imgData);
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
// Clear target area and apply to chunked system at centered position
|
||||
this.clearMaskInArea(centerX, centerY, image.width, image.height);
|
||||
@@ -1527,16 +1512,7 @@ export class MaskTool {
|
||||
}
|
||||
sourceMaskCtx.drawImage(image, 0, 0, sourceWidth, sourceHeight);
|
||||
const sourceImageData = sourceMaskCtx.getImageData(0, 0, sourceWidth, sourceHeight);
|
||||
const sourceData = sourceImageData.data;
|
||||
for (let i = 0; i < sourceData.length; i += 4) {
|
||||
const luminance = Math.round(0.299 * sourceData[i] +
|
||||
0.587 * sourceData[i + 1] +
|
||||
0.114 * sourceData[i + 2]);
|
||||
sourceData[i] = 255;
|
||||
sourceData[i + 1] = 255;
|
||||
sourceData[i + 2] = 255;
|
||||
sourceData[i + 3] = luminance;
|
||||
}
|
||||
applyLuminanceAsAlpha(sourceImageData);
|
||||
sourceMaskCtx.putImageData(sourceImageData, 0, 0);
|
||||
ctx.save();
|
||||
ctx.translate(maskWidth / 2, maskHeight / 2);
|
||||
@@ -1852,12 +1828,7 @@ export class MaskTool {
|
||||
* Creates a feathered mask from existing ImageData (used when combining expansion + feather)
|
||||
*/
|
||||
_createFeatheredMaskFromImageData(imageData, featherRadius, width, height) {
|
||||
const data = imageData.data;
|
||||
const binaryData = new Uint8Array(width * height);
|
||||
// Convert ImageData to binary mask
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
binaryData[i] = data[i * 4 + 3] > 0 ? 1 : 0; // 1 = inside, 0 = outside
|
||||
}
|
||||
const binaryData = imageDataToBinaryMask(imageData, width, height, 3);
|
||||
// Use unified feathering logic
|
||||
return this.createFeatheredMaskFromBinaryData(binaryData, featherRadius, width, height);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,40 @@
|
||||
/**
|
||||
* Converts RGB luminance into an opaque white mask with luminance as alpha.
|
||||
*/
|
||||
export function applyLuminanceAsAlpha(imageData) {
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const luminance = Math.round(0.299 * data[i] +
|
||||
0.587 * data[i + 1] +
|
||||
0.114 * data[i + 2]);
|
||||
data[i] = 255;
|
||||
data[i + 1] = 255;
|
||||
data[i + 2] = 255;
|
||||
data[i + 3] = luminance;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Fills an opaque white mask from the inverse alpha of visibility data.
|
||||
*/
|
||||
export function fillInverseAlphaMask(visibilityData, maskData) {
|
||||
for (let i = 0; i < visibilityData.data.length; i += 4) {
|
||||
const maskValue = 255 - visibilityData.data[i + 3];
|
||||
maskData.data[i] = maskValue;
|
||||
maskData.data[i + 1] = maskValue;
|
||||
maskData.data[i + 2] = maskValue;
|
||||
maskData.data[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Converts one ImageData channel into a binary mask.
|
||||
*/
|
||||
export function imageDataToBinaryMask(imageData, width, height, channel) {
|
||||
const binaryMask = new Uint8Array(width * height);
|
||||
for (let i = 0; i < binaryMask.length; i++) {
|
||||
binaryMask[i] = imageData.data[i * 4 + channel] > 0 ? 1 : 0;
|
||||
}
|
||||
return binaryMask;
|
||||
}
|
||||
/**
|
||||
* Calculates the Euclidean distance transform of a binary mask.
|
||||
* Uses a two-pass algorithm for efficiency.
|
||||
|
||||
+2
-6
@@ -3,6 +3,7 @@ import { createModuleLogger } from "./log_system/log_funcs.js";
|
||||
import { showErrorNotification } from "./utils/NotificationUtils.js";
|
||||
import { webSocketManager } from "./utils/WebSocketManager.js";
|
||||
import { scaleImageToFit, loadImage, tensorToImageData, createImageFromImageData } from "./utils/ImageUtils.js";
|
||||
import { fillInverseAlphaMask } from "./utils/MaskPixelUtils.js";
|
||||
import type { Canvas } from './Canvas';
|
||||
import type { Layer, Shape } from './types';
|
||||
|
||||
@@ -80,12 +81,7 @@ export class CanvasIO {
|
||||
log.debug(`Finished rendering layers`);
|
||||
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
for (let i = 0; i < visibilityData.data.length; i += 4) {
|
||||
const alpha = visibilityData.data[i + 3];
|
||||
const maskValue = 255 - alpha;
|
||||
maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue;
|
||||
maskData.data[i + 3] = 255;
|
||||
}
|
||||
fillInverseAlphaMask(visibilityData, maskData);
|
||||
|
||||
maskCtx.putImageData(maskData, 0, 0);
|
||||
|
||||
|
||||
+2
-6
@@ -20,6 +20,7 @@ import {app} from "../../scripts/app.js";
|
||||
import {ComfyApp} from "../../scripts/app.js";
|
||||
import { ClipboardManager } from "./utils/ClipboardManager.js";
|
||||
import { createDistanceFieldMaskSync } from "./utils/ImageAnalysis.js";
|
||||
import { fillInverseAlphaMask } from "./utils/MaskPixelUtils.js";
|
||||
import type { Canvas } from './Canvas';
|
||||
import type { Layer, Point, AddMode, ClipboardPreference } from './types';
|
||||
|
||||
@@ -1807,12 +1808,7 @@ export class CanvasLayers {
|
||||
// Konwertuj przezroczystość warstw na maskę
|
||||
const visibilityData = visibilityCtx.getImageData(0, 0, bounds.width, bounds.height);
|
||||
const maskData = maskCtx.getImageData(0, 0, bounds.width, bounds.height);
|
||||
for (let i = 0; i < visibilityData.data.length; i += 4) {
|
||||
const alpha = visibilityData.data[i + 3];
|
||||
const maskValue = 255 - alpha; // Odwróć alpha żeby stworzyć maskę
|
||||
maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue;
|
||||
maskData.data[i + 3] = 255; // Solidna maska
|
||||
}
|
||||
fillInverseAlphaMask(visibilityData, maskData);
|
||||
maskCtx.putImageData(maskData, 0, 0);
|
||||
|
||||
// Aplikuj maskę narzędzia jeśli istnieje - używaj zoptymalizowanej metody
|
||||
|
||||
+10
-38
@@ -1,6 +1,10 @@
|
||||
import {createModuleLogger} from "./log_system/log_funcs.js";
|
||||
import { createCanvas, createCanvasWithContext } from "./utils/CommonUtils.js";
|
||||
import { calculateDistanceTransform } from "./utils/MaskPixelUtils.js";
|
||||
import {
|
||||
applyLuminanceAsAlpha,
|
||||
calculateDistanceTransform,
|
||||
imageDataToBinaryMask
|
||||
} from "./utils/MaskPixelUtils.js";
|
||||
import type { Canvas } from './Canvas';
|
||||
import type { Point, CanvasState, Layer } from './types';
|
||||
|
||||
@@ -591,11 +595,7 @@ export class MaskTool {
|
||||
this.drawShapeOnCanvas(ctx, points);
|
||||
|
||||
const maskImage = ctx.getImageData(0, 0, width, height);
|
||||
const binaryData = new Uint8Array(width * height);
|
||||
for (let i = 0; i < binaryData.length; i++) {
|
||||
binaryData[i] = maskImage.data[i * 4] > 0 ? 1 : 0;
|
||||
}
|
||||
return binaryData;
|
||||
return imageDataToBinaryMask(maskImage, width, height, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1533,10 +1533,7 @@ export class MaskTool {
|
||||
}
|
||||
|
||||
const maskImage = tempCtx.getImageData(0, 0, width, height);
|
||||
const binaryData = new Uint8Array(width * height);
|
||||
for (let i = 0; i < binaryData.length; i++) {
|
||||
binaryData[i] = maskImage.data[i * 4] > 0 ? 1 : 0;
|
||||
}
|
||||
const binaryData = imageDataToBinaryMask(maskImage, width, height, 0);
|
||||
|
||||
let resultMask: Uint8Array;
|
||||
const scaledExpansionValue = Math.round(Math.abs(expansionValue * zoom));
|
||||
@@ -1819,15 +1816,7 @@ export class MaskTool {
|
||||
ctx.drawImage(image, 0, 0);
|
||||
|
||||
const imgData = ctx.getImageData(0, 0, image.width, image.height);
|
||||
const data = imgData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const r = data[i], g = data[i + 1], b = data[i + 2];
|
||||
const lum = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
||||
data[i] = 255; // force white color (color channels ignored downstream)
|
||||
data[i + 1] = 255;
|
||||
data[i + 2] = 255;
|
||||
data[i + 3] = lum; // alpha encodes mask strength: white -> strong, black -> 0
|
||||
}
|
||||
applyLuminanceAsAlpha(imgData);
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
|
||||
// Clear target area and apply to chunked system at centered position
|
||||
@@ -1887,18 +1876,7 @@ export class MaskTool {
|
||||
|
||||
sourceMaskCtx.drawImage(image, 0, 0, sourceWidth, sourceHeight);
|
||||
const sourceImageData = sourceMaskCtx.getImageData(0, 0, sourceWidth, sourceHeight);
|
||||
const sourceData = sourceImageData.data;
|
||||
for (let i = 0; i < sourceData.length; i += 4) {
|
||||
const luminance = Math.round(
|
||||
0.299 * sourceData[i] +
|
||||
0.587 * sourceData[i + 1] +
|
||||
0.114 * sourceData[i + 2],
|
||||
);
|
||||
sourceData[i] = 255;
|
||||
sourceData[i + 1] = 255;
|
||||
sourceData[i + 2] = 255;
|
||||
sourceData[i + 3] = luminance;
|
||||
}
|
||||
applyLuminanceAsAlpha(sourceImageData);
|
||||
sourceMaskCtx.putImageData(sourceImageData, 0, 0);
|
||||
|
||||
ctx.save();
|
||||
@@ -2297,13 +2275,7 @@ export class MaskTool {
|
||||
* Creates a feathered mask from existing ImageData (used when combining expansion + feather)
|
||||
*/
|
||||
private _createFeatheredMaskFromImageData(imageData: ImageData, featherRadius: number, width: number, height: number): HTMLCanvasElement {
|
||||
const data = imageData.data;
|
||||
const binaryData = new Uint8Array(width * height);
|
||||
|
||||
// Convert ImageData to binary mask
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
binaryData[i] = data[i * 4 + 3] > 0 ? 1 : 0; // 1 = inside, 0 = outside
|
||||
}
|
||||
const binaryData = imageDataToBinaryMask(imageData, width, height, 3);
|
||||
|
||||
// Use unified feathering logic
|
||||
return this.createFeatheredMaskFromBinaryData(binaryData, featherRadius, width, height);
|
||||
|
||||
@@ -1,3 +1,45 @@
|
||||
/**
|
||||
* Converts RGB luminance into an opaque white mask with luminance as alpha.
|
||||
*/
|
||||
export function applyLuminanceAsAlpha(imageData: ImageData): void {
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const luminance = Math.round(
|
||||
0.299 * data[i] +
|
||||
0.587 * data[i + 1] +
|
||||
0.114 * data[i + 2]
|
||||
);
|
||||
data[i] = 255;
|
||||
data[i + 1] = 255;
|
||||
data[i + 2] = 255;
|
||||
data[i + 3] = luminance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an opaque white mask from the inverse alpha of visibility data.
|
||||
*/
|
||||
export function fillInverseAlphaMask(visibilityData: ImageData, maskData: ImageData): void {
|
||||
for (let i = 0; i < visibilityData.data.length; i += 4) {
|
||||
const maskValue = 255 - visibilityData.data[i + 3];
|
||||
maskData.data[i] = maskValue;
|
||||
maskData.data[i + 1] = maskValue;
|
||||
maskData.data[i + 2] = maskValue;
|
||||
maskData.data[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts one ImageData channel into a binary mask.
|
||||
*/
|
||||
export function imageDataToBinaryMask(imageData: ImageData, width: number, height: number, channel: 0 | 3): Uint8Array {
|
||||
const binaryMask = new Uint8Array(width * height);
|
||||
for (let i = 0; i < binaryMask.length; i++) {
|
||||
binaryMask[i] = imageData.data[i * 4 + channel] > 0 ? 1 : 0;
|
||||
}
|
||||
return binaryMask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the Euclidean distance transform of a binary mask.
|
||||
* Uses a two-pass algorithm for efficiency.
|
||||
|
||||
@@ -2,6 +2,13 @@ import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
import { MaskTool } from '../js/MaskTool.js';
|
||||
import {
|
||||
applyLuminanceAsAlpha,
|
||||
fillInverseAlphaMask,
|
||||
imageDataToBinaryMask,
|
||||
} from '../js/utils/MaskPixelUtils.js';
|
||||
|
||||
function installCanvasStub(sourceAlpha) {
|
||||
const originalDocument = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
const output = { data: null };
|
||||
@@ -52,6 +59,58 @@ function installCanvasStub(sourceAlpha) {
|
||||
};
|
||||
}
|
||||
|
||||
function installPixelCanvasStub(sourcePixels) {
|
||||
const originalDocument = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
const outputs = [];
|
||||
const context = {
|
||||
beginPath() {},
|
||||
closePath() {},
|
||||
drawImage() {},
|
||||
fill() {},
|
||||
getImageData(_x, _y, width, height) {
|
||||
return { width, height, data: new Uint8ClampedArray(sourcePixels) };
|
||||
},
|
||||
lineTo() {},
|
||||
moveTo() {},
|
||||
putImageData(imageData) {
|
||||
outputs.push([...imageData.data]);
|
||||
},
|
||||
restore() {},
|
||||
rotate() {},
|
||||
save() {},
|
||||
scale() {},
|
||||
translate() {},
|
||||
};
|
||||
const canvas = {
|
||||
height: 0,
|
||||
width: 0,
|
||||
getContext() {
|
||||
return context;
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
createElement(tagName) {
|
||||
assert.equal(tagName, 'canvas');
|
||||
return canvas;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
outputs,
|
||||
restore() {
|
||||
if (originalDocument) {
|
||||
Object.defineProperty(globalThis, 'document', originalDocument);
|
||||
} else {
|
||||
delete globalThis.document;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('distance-field mask preserves alpha-distance behavior for transparent pixels', async () => {
|
||||
const stubs = installCanvasStub([255, 128, 0]);
|
||||
|
||||
@@ -81,6 +140,137 @@ test('mask consumers use one shared distance transform implementation', async ()
|
||||
assert.match(imageAnalysisSource, /from "\.\/MaskPixelUtils\.js"/);
|
||||
assert.match(maskToolSource, /from "\.\/utils\/MaskPixelUtils\.js"/);
|
||||
assert.match(maskToolSource, /calculateDistanceTransform\(binaryData, width, height\)/);
|
||||
assert.match(maskToolSource, /applyLuminanceAsAlpha\(imgData\)/);
|
||||
assert.match(maskToolSource, /applyLuminanceAsAlpha\(sourceImageData\)/);
|
||||
assert.match(maskToolSource, /imageDataToBinaryMask\(maskImage, width, height, 0\)/);
|
||||
assert.match(maskToolSource, /imageDataToBinaryMask\(imageData, width, height, 3\)/);
|
||||
assert.doesNotMatch(imageAnalysisSource, /function calculateDistanceTransform\(/);
|
||||
assert.doesNotMatch(maskToolSource, /private _fastDistanceTransform\(/);
|
||||
});
|
||||
|
||||
test('shared mask pixel helpers preserve luminance, inverse alpha, and explicit channels', () => {
|
||||
const imageData = {
|
||||
width: 2,
|
||||
height: 1,
|
||||
data: new Uint8ClampedArray([
|
||||
255, 0, 0, 0,
|
||||
0, 255, 0, 255,
|
||||
]),
|
||||
};
|
||||
applyLuminanceAsAlpha(imageData);
|
||||
assert.deepEqual([...imageData.data], [255, 255, 255, 76, 255, 255, 255, 150]);
|
||||
|
||||
const visibilityData = {
|
||||
width: 3,
|
||||
height: 1,
|
||||
data: new Uint8ClampedArray([
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 128,
|
||||
0, 0, 0, 255,
|
||||
]),
|
||||
};
|
||||
const maskData = {
|
||||
width: 3,
|
||||
height: 1,
|
||||
data: new Uint8ClampedArray(12),
|
||||
};
|
||||
fillInverseAlphaMask(visibilityData, maskData);
|
||||
assert.deepEqual([...maskData.data], [
|
||||
255, 255, 255, 255,
|
||||
127, 127, 127, 255,
|
||||
0, 0, 0, 255,
|
||||
]);
|
||||
|
||||
const channelData = {
|
||||
width: 2,
|
||||
height: 1,
|
||||
data: new Uint8ClampedArray([
|
||||
255, 0, 0, 0,
|
||||
0, 0, 0, 255,
|
||||
]),
|
||||
};
|
||||
assert.deepEqual([...imageDataToBinaryMask(channelData, 2, 1, 0)], [1, 0]);
|
||||
assert.deepEqual([...imageDataToBinaryMask(channelData, 2, 1, 3)], [0, 1]);
|
||||
});
|
||||
|
||||
test('MaskTool converts shape pixels through the existing red-channel binary contract', () => {
|
||||
const stubs = installPixelCanvasStub([
|
||||
0, 255, 0, 0,
|
||||
255, 0, 0, 0,
|
||||
]);
|
||||
const maskTool = Object.create(MaskTool.prototype);
|
||||
|
||||
try {
|
||||
assert.deepEqual(
|
||||
[...maskTool.createBinaryMaskFromShape([{ x: 0, y: 0 }], 2, 1)],
|
||||
[0, 1]
|
||||
);
|
||||
} finally {
|
||||
stubs.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('MaskTool preserves luminance-to-alpha behavior for input and layer masks', () => {
|
||||
const pixels = [
|
||||
0, 0, 0, 255,
|
||||
255, 0, 0, 255,
|
||||
0, 255, 0, 255,
|
||||
0, 0, 255, 255,
|
||||
];
|
||||
const expected = [
|
||||
255, 255, 255, 0,
|
||||
255, 255, 255, 76,
|
||||
255, 255, 255, 150,
|
||||
255, 255, 255, 29,
|
||||
];
|
||||
|
||||
const inputStubs = installPixelCanvasStub(pixels);
|
||||
const inputMaskTool = Object.create(MaskTool.prototype);
|
||||
inputMaskTool.canvasInstance = {
|
||||
outputAreaBounds: { x: 0, y: 0, width: 2, height: 2 },
|
||||
canvasState: { saveMaskState() {} },
|
||||
render() {},
|
||||
};
|
||||
inputMaskTool.clearMaskInArea = () => {};
|
||||
inputMaskTool.applyMaskCanvasToChunks = () => {};
|
||||
inputMaskTool.updateActiveMaskCanvas = () => {};
|
||||
|
||||
try {
|
||||
inputMaskTool.setMask({ width: 2, height: 2 }, true);
|
||||
assert.deepEqual(inputStubs.outputs[0], expected);
|
||||
} finally {
|
||||
inputStubs.restore();
|
||||
}
|
||||
|
||||
const layerStubs = installPixelCanvasStub(pixels);
|
||||
const layerMaskTool = Object.create(MaskTool.prototype);
|
||||
layerMaskTool.canvasInstance = {
|
||||
canvasState: { saveMaskState() {} },
|
||||
render() {},
|
||||
};
|
||||
layerMaskTool.clearMaskInArea = () => {};
|
||||
layerMaskTool.applyMaskCanvasToChunks = () => {};
|
||||
layerMaskTool.updateActiveMaskCanvas = () => {};
|
||||
|
||||
try {
|
||||
layerMaskTool.setMaskForLayer(
|
||||
{ width: 2, height: 2, naturalWidth: 2, naturalHeight: 2 },
|
||||
{ x: 0, y: 0, width: 2, height: 2, originalWidth: 2, originalHeight: 2, rotation: 0 }
|
||||
);
|
||||
assert.deepEqual(layerStubs.outputs[0], expected);
|
||||
} finally {
|
||||
layerStubs.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('CanvasIO and CanvasLayers preserve inverse-alpha mask generation', async () => {
|
||||
const canvasIOSource = await readFile(new URL('../src/CanvasIO.ts', import.meta.url), 'utf8');
|
||||
const canvasLayersSource = await readFile(new URL('../src/CanvasLayers.ts', import.meta.url), 'utf8');
|
||||
|
||||
assert.match(canvasIOSource, /from "\.\/utils\/MaskPixelUtils\.js"/);
|
||||
assert.match(canvasLayersSource, /from "\.\/utils\/MaskPixelUtils\.js"/);
|
||||
assert.match(canvasIOSource, /fillInverseAlphaMask\(visibilityData, maskData\)/);
|
||||
assert.match(canvasLayersSource, /fillInverseAlphaMask\(visibilityData, maskData\)/);
|
||||
assert.doesNotMatch(canvasIOSource, /const maskValue = 255 - alpha/);
|
||||
assert.doesNotMatch(canvasLayersSource, /const maskValue = 255 - alpha/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user