refactor: share canvas output rendering primitives
This commit is contained in:
+6
-18
@@ -3,7 +3,6 @@ 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) {
|
||||
@@ -50,27 +49,16 @@ export class CanvasIO {
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
||||
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
||||
const originalShape = this.canvas.outputAreaShape;
|
||||
this.canvas.outputAreaShape = null;
|
||||
const { canvas: visibilityCanvas, ctx: visibilityCtx } = createCanvas(this.canvas.width, this.canvas.height, '2d', { alpha: true });
|
||||
if (!visibilityCtx)
|
||||
throw new Error("Could not create visibility context");
|
||||
if (!maskCtx)
|
||||
throw new Error("Could not create mask context");
|
||||
if (!tempCtx)
|
||||
throw new Error("Could not create temp context");
|
||||
maskCtx.fillStyle = '#ffffff';
|
||||
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
const renderBounds = { x: 0, y: 0, width: this.canvas.width, height: this.canvas.height };
|
||||
const { canvas: tempCanvas } = this.canvas.canvasLayers.renderLayersToCanvas(renderBounds, this.canvas.layers, {});
|
||||
const { canvas: maskCanvas, ctx: maskCtx } = this.canvas.canvasLayers.renderLayerVisibilityMask(renderBounds, this.canvas.layers, {
|
||||
maskContextOptions: {},
|
||||
visibilityContextOptions: { alpha: true }
|
||||
});
|
||||
log.debug(`Canvas contexts created, starting layer rendering`);
|
||||
this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
|
||||
this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
|
||||
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);
|
||||
fillInverseAlphaMask(visibilityData, maskData);
|
||||
maskCtx.putImageData(maskData, 0, 0);
|
||||
this.canvas.outputAreaShape = originalShape;
|
||||
// Use optimized getMaskForOutputArea() instead of getMask() for better performance
|
||||
// This only processes chunks that overlap with the output area
|
||||
|
||||
+33
-34
@@ -985,6 +985,30 @@ export class CanvasLayers {
|
||||
drawLayersToContext(ctx, layers, options = {}) {
|
||||
this._drawLayers(ctx, layers, options);
|
||||
}
|
||||
renderLayersToCanvas(bounds, layers = this.canvas.layers, contextOptions = {}) {
|
||||
const { canvas, ctx } = createCanvas(bounds.width, bounds.height, '2d', contextOptions);
|
||||
if (!ctx) {
|
||||
throw new Error("Could not create canvas context");
|
||||
}
|
||||
ctx.translate(-bounds.x, -bounds.y);
|
||||
this._drawLayers(ctx, layers);
|
||||
return { canvas, ctx };
|
||||
}
|
||||
renderLayerVisibilityMask(bounds, layers = this.canvas.layers, options = {}) {
|
||||
const { maskContextOptions = { willReadFrequently: true }, visibilityContextOptions = { alpha: true } } = options;
|
||||
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(bounds.width, bounds.height, '2d', maskContextOptions);
|
||||
if (!maskCtx) {
|
||||
throw new Error("Could not create mask context");
|
||||
}
|
||||
maskCtx.fillStyle = '#ffffff';
|
||||
maskCtx.fillRect(0, 0, bounds.width, bounds.height);
|
||||
const { ctx: visibilityCtx } = this.renderLayersToCanvas(bounds, layers, visibilityContextOptions);
|
||||
const visibilityData = visibilityCtx.getImageData(0, 0, bounds.width, bounds.height);
|
||||
const maskData = maskCtx.getImageData(0, 0, bounds.width, bounds.height);
|
||||
fillInverseAlphaMask(visibilityData, maskData);
|
||||
maskCtx.putImageData(maskData, 0, 0);
|
||||
return { canvas: maskCanvas, ctx: maskCtx };
|
||||
}
|
||||
async mirrorHorizontal() {
|
||||
if (this.canvas.canvasSelection.selectedLayers.length === 0)
|
||||
return;
|
||||
@@ -1409,7 +1433,7 @@ export class CanvasLayers {
|
||||
*/
|
||||
async _generateCanvasBlob(options = {}) {
|
||||
const { layers = this.canvas.layers, useOutputBounds = true, applyMask = false, enableLogging = false, customBounds } = options;
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
let bounds;
|
||||
if (customBounds) {
|
||||
bounds = customBounds;
|
||||
@@ -1427,15 +1451,10 @@ export class CanvasLayers {
|
||||
}
|
||||
bounds = { x: layerBounds.x, y: layerBounds.y, width: newWidth, height: newHeight };
|
||||
}
|
||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(bounds.width, bounds.height, '2d', { willReadFrequently: true });
|
||||
if (!tempCtx) {
|
||||
reject(new Error("Could not create canvas context"));
|
||||
return;
|
||||
}
|
||||
if (enableLogging) {
|
||||
log.info("=== GENERATING OUTPUT CANVAS ===");
|
||||
log.info(`Bounds: x=${bounds.x}, y=${bounds.y}, w=${bounds.width}, h=${bounds.height}`);
|
||||
log.info(`Canvas Size: ${tempCanvas.width}x${tempCanvas.height}`);
|
||||
log.info(`Canvas Size: ${bounds.width}x${bounds.height}`);
|
||||
log.info(`Context Translation: translate(${-bounds.x}, ${-bounds.y})`);
|
||||
log.info(`Apply Mask: ${applyMask}`);
|
||||
// Log layer positions before rendering
|
||||
@@ -1449,9 +1468,7 @@ export class CanvasLayers {
|
||||
}
|
||||
});
|
||||
}
|
||||
// Renderuj fragment świata zdefiniowany przez bounds
|
||||
tempCtx.translate(-bounds.x, -bounds.y);
|
||||
this._drawLayers(tempCtx, layers);
|
||||
const { canvas: tempCanvas, ctx: tempCtx } = this.renderLayersToCanvas(bounds, layers, { willReadFrequently: true });
|
||||
// Aplikuj maskę jeśli wymagana
|
||||
if (applyMask) {
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||
@@ -1515,32 +1532,14 @@ export class CanvasLayers {
|
||||
});
|
||||
}
|
||||
async getFlattenedMaskAsBlob() {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
const bounds = this.canvas.outputAreaBounds;
|
||||
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(bounds.width, bounds.height, '2d', { willReadFrequently: true });
|
||||
if (!maskCtx) {
|
||||
reject(new Error("Could not create mask context"));
|
||||
return;
|
||||
}
|
||||
log.info("=== GENERATING MASK BLOB ===");
|
||||
log.info(`Mask Canvas Size: ${maskCanvas.width}x${maskCanvas.height}`);
|
||||
// Rozpocznij z białą maską (nic nie zamaskowane)
|
||||
maskCtx.fillStyle = '#ffffff';
|
||||
maskCtx.fillRect(0, 0, bounds.width, bounds.height);
|
||||
// Stwórz canvas do sprawdzenia przezroczystości warstw
|
||||
const { canvas: visibilityCanvas, ctx: visibilityCtx } = createCanvas(bounds.width, bounds.height, '2d', { alpha: true });
|
||||
if (!visibilityCtx) {
|
||||
reject(new Error("Could not create visibility context"));
|
||||
return;
|
||||
}
|
||||
// Renderuj warstwy z przesunięciem dla output bounds
|
||||
visibilityCtx.translate(-bounds.x, -bounds.y);
|
||||
this._drawLayers(visibilityCtx, this.canvas.layers);
|
||||
// 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);
|
||||
fillInverseAlphaMask(visibilityData, maskData);
|
||||
maskCtx.putImageData(maskData, 0, 0);
|
||||
log.info(`Mask Canvas Size: ${bounds.width}x${bounds.height}`);
|
||||
const { canvas: maskCanvas, ctx: maskCtx } = this.renderLayerVisibilityMask(bounds, this.canvas.layers, {
|
||||
maskContextOptions: { willReadFrequently: true },
|
||||
visibilityContextOptions: { alpha: true }
|
||||
});
|
||||
// Aplikuj maskę narzędzia jeśli istnieje - używaj zoptymalizowanej metody
|
||||
const toolMaskCanvas = this.canvas.maskTool.getMaskForOutputArea();
|
||||
if (toolMaskCanvas) {
|
||||
|
||||
+14
-18
@@ -3,7 +3,6 @@ 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';
|
||||
|
||||
@@ -61,29 +60,26 @@ export class CanvasIO {
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const {canvas: tempCanvas, ctx: tempCtx} = createCanvas(this.canvas.width, this.canvas.height);
|
||||
const {canvas: maskCanvas, ctx: maskCtx} = createCanvas(this.canvas.width, this.canvas.height);
|
||||
|
||||
const originalShape = this.canvas.outputAreaShape;
|
||||
this.canvas.outputAreaShape = null;
|
||||
|
||||
const { canvas: visibilityCanvas, ctx: visibilityCtx } = createCanvas(this.canvas.width, this.canvas.height, '2d', { alpha: true });
|
||||
if (!visibilityCtx) throw new Error("Could not create visibility context");
|
||||
if (!maskCtx) throw new Error("Could not create mask context");
|
||||
if (!tempCtx) throw new Error("Could not create temp context");
|
||||
maskCtx.fillStyle = '#ffffff';
|
||||
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
const renderBounds = { x: 0, y: 0, width: this.canvas.width, height: this.canvas.height };
|
||||
const { canvas: tempCanvas } = this.canvas.canvasLayers.renderLayersToCanvas(
|
||||
renderBounds,
|
||||
this.canvas.layers,
|
||||
{}
|
||||
);
|
||||
const { canvas: maskCanvas, ctx: maskCtx } = this.canvas.canvasLayers.renderLayerVisibilityMask(
|
||||
renderBounds,
|
||||
this.canvas.layers,
|
||||
{
|
||||
maskContextOptions: {},
|
||||
visibilityContextOptions: { alpha: true }
|
||||
}
|
||||
);
|
||||
|
||||
log.debug(`Canvas contexts created, starting layer rendering`);
|
||||
|
||||
this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
|
||||
this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
|
||||
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);
|
||||
fillInverseAlphaMask(visibilityData, maskData);
|
||||
|
||||
maskCtx.putImageData(maskData, 0, 0);
|
||||
|
||||
this.canvas.outputAreaShape = originalShape;
|
||||
|
||||
|
||||
+70
-41
@@ -1139,6 +1139,59 @@ export class CanvasLayers {
|
||||
this._drawLayers(ctx, layers, options);
|
||||
}
|
||||
|
||||
public renderLayersToCanvas(
|
||||
bounds: { x: number, y: number, width: number, height: number },
|
||||
layers: Layer[] = this.canvas.layers,
|
||||
contextOptions: CanvasRenderingContext2DSettings = {}
|
||||
): { canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D } {
|
||||
const { canvas, ctx } = createCanvas(bounds.width, bounds.height, '2d', contextOptions);
|
||||
if (!ctx) {
|
||||
throw new Error("Could not create canvas context");
|
||||
}
|
||||
|
||||
ctx.translate(-bounds.x, -bounds.y);
|
||||
this._drawLayers(ctx, layers);
|
||||
return { canvas, ctx };
|
||||
}
|
||||
|
||||
public renderLayerVisibilityMask(
|
||||
bounds: { x: number, y: number, width: number, height: number },
|
||||
layers: Layer[] = this.canvas.layers,
|
||||
options: {
|
||||
maskContextOptions?: CanvasRenderingContext2DSettings;
|
||||
visibilityContextOptions?: CanvasRenderingContext2DSettings;
|
||||
} = {}
|
||||
): { canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D } {
|
||||
const {
|
||||
maskContextOptions = { willReadFrequently: true },
|
||||
visibilityContextOptions = { alpha: true }
|
||||
} = options;
|
||||
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(
|
||||
bounds.width,
|
||||
bounds.height,
|
||||
'2d',
|
||||
maskContextOptions
|
||||
);
|
||||
if (!maskCtx) {
|
||||
throw new Error("Could not create mask context");
|
||||
}
|
||||
|
||||
maskCtx.fillStyle = '#ffffff';
|
||||
maskCtx.fillRect(0, 0, bounds.width, bounds.height);
|
||||
|
||||
const { ctx: visibilityCtx } = this.renderLayersToCanvas(
|
||||
bounds,
|
||||
layers,
|
||||
visibilityContextOptions
|
||||
);
|
||||
const visibilityData = visibilityCtx.getImageData(0, 0, bounds.width, bounds.height);
|
||||
const maskData = maskCtx.getImageData(0, 0, bounds.width, bounds.height);
|
||||
fillInverseAlphaMask(visibilityData, maskData);
|
||||
maskCtx.putImageData(maskData, 0, 0);
|
||||
|
||||
return { canvas: maskCanvas, ctx: maskCtx };
|
||||
}
|
||||
|
||||
async mirrorHorizontal(): Promise<void> {
|
||||
if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
|
||||
this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => {
|
||||
@@ -1656,7 +1709,7 @@ export class CanvasLayers {
|
||||
customBounds
|
||||
} = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
let bounds: { x: number, y: number, width: number, height: number };
|
||||
|
||||
if (customBounds) {
|
||||
@@ -1678,16 +1731,10 @@ export class CanvasLayers {
|
||||
bounds = { x: layerBounds.x, y: layerBounds.y, width: newWidth, height: newHeight };
|
||||
}
|
||||
|
||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(bounds.width, bounds.height, '2d', { willReadFrequently: true });
|
||||
if (!tempCtx) {
|
||||
reject(new Error("Could not create canvas context"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableLogging) {
|
||||
log.info("=== GENERATING OUTPUT CANVAS ===");
|
||||
log.info(`Bounds: x=${bounds.x}, y=${bounds.y}, w=${bounds.width}, h=${bounds.height}`);
|
||||
log.info(`Canvas Size: ${tempCanvas.width}x${tempCanvas.height}`);
|
||||
log.info(`Canvas Size: ${bounds.width}x${bounds.height}`);
|
||||
log.info(`Context Translation: translate(${-bounds.x}, ${-bounds.y})`);
|
||||
log.info(`Apply Mask: ${applyMask}`);
|
||||
|
||||
@@ -1703,9 +1750,11 @@ export class CanvasLayers {
|
||||
});
|
||||
}
|
||||
|
||||
// Renderuj fragment świata zdefiniowany przez bounds
|
||||
tempCtx.translate(-bounds.x, -bounds.y);
|
||||
this._drawLayers(tempCtx, layers);
|
||||
const { canvas: tempCanvas, ctx: tempCtx } = this.renderLayersToCanvas(
|
||||
bounds,
|
||||
layers,
|
||||
{ willReadFrequently: true }
|
||||
);
|
||||
|
||||
// Aplikuj maskę jeśli wymagana
|
||||
if (applyMask) {
|
||||
@@ -1778,38 +1827,18 @@ export class CanvasLayers {
|
||||
}
|
||||
|
||||
async getFlattenedMaskAsBlob(): Promise<Blob | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
const bounds = this.canvas.outputAreaBounds;
|
||||
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(bounds.width, bounds.height, '2d', { willReadFrequently: true });
|
||||
|
||||
if (!maskCtx) {
|
||||
reject(new Error("Could not create mask context"));
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("=== GENERATING MASK BLOB ===");
|
||||
log.info(`Mask Canvas Size: ${maskCanvas.width}x${maskCanvas.height}`);
|
||||
|
||||
// Rozpocznij z białą maską (nic nie zamaskowane)
|
||||
maskCtx.fillStyle = '#ffffff';
|
||||
maskCtx.fillRect(0, 0, bounds.width, bounds.height);
|
||||
|
||||
// Stwórz canvas do sprawdzenia przezroczystości warstw
|
||||
const { canvas: visibilityCanvas, ctx: visibilityCtx } = createCanvas(bounds.width, bounds.height, '2d', { alpha: true });
|
||||
if (!visibilityCtx) {
|
||||
reject(new Error("Could not create visibility context"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Renderuj warstwy z przesunięciem dla output bounds
|
||||
visibilityCtx.translate(-bounds.x, -bounds.y);
|
||||
this._drawLayers(visibilityCtx, this.canvas.layers);
|
||||
|
||||
// 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);
|
||||
fillInverseAlphaMask(visibilityData, maskData);
|
||||
maskCtx.putImageData(maskData, 0, 0);
|
||||
log.info(`Mask Canvas Size: ${bounds.width}x${bounds.height}`);
|
||||
const { canvas: maskCanvas, ctx: maskCtx } = this.renderLayerVisibilityMask(
|
||||
bounds,
|
||||
this.canvas.layers,
|
||||
{
|
||||
maskContextOptions: { willReadFrequently: true },
|
||||
visibilityContextOptions: { alpha: true }
|
||||
}
|
||||
);
|
||||
|
||||
// Aplikuj maskę narzędzia jeśli istnieje - używaj zoptymalizowanej metody
|
||||
const toolMaskCanvas = this.canvas.maskTool.getMaskForOutputArea();
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
const [canvasIOSource, canvasLayersSource] = await Promise.all([
|
||||
readFile(new URL('../src/CanvasIO.ts', import.meta.url), 'utf8'),
|
||||
readFile(new URL('../src/CanvasLayers.ts', import.meta.url), 'utf8'),
|
||||
]);
|
||||
|
||||
test('output paths preserve their current render bounds and mask responsibilities', () => {
|
||||
assert.match(canvasIOSource, /renderLayersToCanvas\(/);
|
||||
assert.match(canvasIOSource, /renderLayerVisibilityMask\(/);
|
||||
assert.match(canvasIOSource, /const renderBounds = \{ x: 0, y: 0, width: this\.canvas\.width, height: this\.canvas\.height \}/);
|
||||
assert.match(canvasIOSource, /const bounds = this\.canvas\.outputAreaBounds/);
|
||||
|
||||
assert.match(canvasLayersSource, /public renderLayersToCanvas\(/);
|
||||
assert.match(canvasLayersSource, /public renderLayerVisibilityMask\(/);
|
||||
assert.match(canvasLayersSource, /this\.renderLayersToCanvas\(/);
|
||||
assert.doesNotMatch(canvasLayersSource, /this\._drawLayers\(tempCtx, layers\)/);
|
||||
assert.doesNotMatch(canvasLayersSource, /visibilityCtx\.translate\(-bounds\.x, -bounds\.y\)/);
|
||||
assert.match(canvasLayersSource, /maskCtx\.globalCompositeOperation = 'screen'/);
|
||||
});
|
||||
|
||||
test('output rendering keeps persistence and transport in CanvasIO', () => {
|
||||
assert.match(canvasIOSource, /async _performSave\(fileName: string, outputMode: string\)/);
|
||||
assert.match(canvasIOSource, /tempCanvas\.toDataURL\('image\/png'\)/);
|
||||
assert.match(canvasIOSource, /fetch\("\/upload\/image"/);
|
||||
assert.match(canvasLayersSource, /async getFlattenedCanvasAsBlob\(\)/);
|
||||
assert.match(canvasLayersSource, /async getFlattenedMaskAsBlob\(\)/);
|
||||
});
|
||||
@@ -333,9 +333,10 @@ 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.doesNotMatch(canvasIOSource, /from "\.\/utils\/MaskPixelUtils\.js"/);
|
||||
assert.match(canvasLayersSource, /from "\.\/utils\/MaskPixelUtils\.js"/);
|
||||
assert.match(canvasIOSource, /fillInverseAlphaMask\(visibilityData, maskData\)/);
|
||||
assert.match(canvasIOSource, /renderLayerVisibilityMask\(/);
|
||||
assert.doesNotMatch(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