Implement crop mode for cropping selected layer

This commit is contained in:
Dariusz L
2025-08-02 19:05:11 +02:00
parent 9b0d4b3149
commit 7ed6f7ee93
11 changed files with 635 additions and 178 deletions

View File

@@ -539,7 +539,10 @@ export class CanvasInteractions {
width: layer.width, height: layer.height, width: layer.width, height: layer.height,
rotation: layer.rotation, rotation: layer.rotation,
centerX: layer.x + layer.width / 2, centerX: layer.x + layer.width / 2,
centerY: layer.y + layer.height / 2 centerY: layer.y + layer.height / 2,
originalWidth: layer.originalWidth,
originalHeight: layer.originalHeight,
cropBounds: layer.cropBounds ? { ...layer.cropBounds } : undefined
}; };
this.interaction.dragStart = { ...worldCoords }; this.interaction.dragStart = { ...worldCoords };
if (handle === 'rot') { if (handle === 'rot') {
@@ -692,12 +695,8 @@ export class CanvasInteractions {
let mouseY = worldCoords.y; let mouseY = worldCoords.y;
if (this.interaction.isCtrlPressed) { if (this.interaction.isCtrlPressed) {
const snapThreshold = 10 / this.canvas.viewport.zoom; const snapThreshold = 10 / this.canvas.viewport.zoom;
const snappedMouseX = snapToGrid(mouseX); mouseX = Math.abs(mouseX - snapToGrid(mouseX)) < snapThreshold ? snapToGrid(mouseX) : mouseX;
if (Math.abs(mouseX - snappedMouseX) < snapThreshold) mouseY = Math.abs(mouseY - snapToGrid(mouseY)) < snapThreshold ? snapToGrid(mouseY) : mouseY;
mouseX = snappedMouseX;
const snappedMouseY = snapToGrid(mouseY);
if (Math.abs(mouseY - snappedMouseY) < snapThreshold)
mouseY = snappedMouseY;
} }
const o = this.interaction.transformOrigin; const o = this.interaction.transformOrigin;
if (o.rotation === undefined || o.width === undefined || o.height === undefined || o.centerX === undefined || o.centerY === undefined) if (o.rotation === undefined || o.width === undefined || o.height === undefined || o.centerX === undefined || o.centerY === undefined)
@@ -707,43 +706,113 @@ export class CanvasInteractions {
const rad = o.rotation * Math.PI / 180; const rad = o.rotation * Math.PI / 180;
const cos = Math.cos(rad); const cos = Math.cos(rad);
const sin = Math.sin(rad); const sin = Math.sin(rad);
// Vector from anchor to mouse
const vecX = mouseX - anchor.x; const vecX = mouseX - anchor.x;
const vecY = mouseY - anchor.y; const vecY = mouseY - anchor.y;
let newWidth = vecX * cos + vecY * sin; // Rotate vector to align with layer's local coordinates
let newHeight = vecY * cos - vecX * sin; let localVecX = vecX * cos + vecY * sin;
if (isShiftPressed) { let localVecY = vecY * cos - vecX * sin;
const originalAspectRatio = o.width / o.height; // Determine sign based on handle
if (Math.abs(newWidth) > Math.abs(newHeight) * originalAspectRatio) { const signX = handle?.includes('e') ? 1 : (handle?.includes('w') ? -1 : 0);
newHeight = (Math.sign(newHeight) || 1) * Math.abs(newWidth) / originalAspectRatio; const signY = handle?.includes('s') ? 1 : (handle?.includes('n') ? -1 : 0);
} localVecX *= signX;
else { localVecY *= signY;
newWidth = (Math.sign(newWidth) || 1) * Math.abs(newHeight) * originalAspectRatio; // If not a corner handle, keep original dimension
}
}
let signX = handle?.includes('e') ? 1 : (handle?.includes('w') ? -1 : 0);
let signY = handle?.includes('s') ? 1 : (handle?.includes('n') ? -1 : 0);
newWidth *= signX;
newHeight *= signY;
if (signX === 0) if (signX === 0)
newWidth = o.width; localVecX = o.width;
if (signY === 0) if (signY === 0)
newHeight = o.height; localVecY = o.height;
if (newWidth < 10) if (layer.cropMode && o.cropBounds && o.originalWidth && o.originalHeight) {
newWidth = 10; // CROP MODE: Calculate delta based on mouse movement and apply to cropBounds.
if (newHeight < 10) // Calculate mouse movement since drag start, in the layer's local coordinate system.
newHeight = 10; const dragStartX_local = this.interaction.dragStart.x - (o.centerX ?? 0);
layer.width = newWidth; const dragStartY_local = this.interaction.dragStart.y - (o.centerY ?? 0);
layer.height = newHeight; const mouseX_local = mouseX - (o.centerX ?? 0);
const deltaW = newWidth - o.width; const mouseY_local = mouseY - (o.centerY ?? 0);
const deltaH = newHeight - o.height; // Rotate mouse delta into the layer's unrotated frame
const shiftX = (deltaW / 2) * signX; const deltaX_world = mouseX_local - dragStartX_local;
const shiftY = (deltaH / 2) * signY; const deltaY_world = mouseY_local - dragStartY_local;
const worldShiftX = shiftX * cos - shiftY * sin; const mouseDeltaX_local = deltaX_world * cos + deltaY_world * sin;
const worldShiftY = shiftX * sin + shiftY * cos; const mouseDeltaY_local = deltaY_world * cos - deltaX_world * sin;
const newCenterX = o.centerX + worldShiftX; // Convert the on-screen mouse delta to an image-space delta.
const newCenterY = o.centerY + worldShiftY; const screenToImageScaleX = o.originalWidth / o.width;
layer.x = newCenterX - layer.width / 2; const screenToImageScaleY = o.originalHeight / o.height;
layer.y = newCenterY - layer.height / 2; const delta_image_x = mouseDeltaX_local * screenToImageScaleX;
const delta_image_y = mouseDeltaY_local * screenToImageScaleY;
let newCropBounds = { ...o.cropBounds }; // Start with the bounds from the beginning of the drag
// Apply the image-space delta to the appropriate edges of the crop bounds
if (handle?.includes('w')) {
newCropBounds.x += delta_image_x;
newCropBounds.width -= delta_image_x;
}
if (handle?.includes('e')) {
newCropBounds.width += delta_image_x;
}
if (handle?.includes('n')) {
newCropBounds.y += delta_image_y;
newCropBounds.height -= delta_image_y;
}
if (handle?.includes('s')) {
newCropBounds.height += delta_image_y;
}
// Clamp crop bounds to stay within the original image and maintain minimum size
if (newCropBounds.width < 1) {
if (handle?.includes('w'))
newCropBounds.x = o.cropBounds.x + o.cropBounds.width - 1;
newCropBounds.width = 1;
}
if (newCropBounds.height < 1) {
if (handle?.includes('n'))
newCropBounds.y = o.cropBounds.y + o.cropBounds.height - 1;
newCropBounds.height = 1;
}
if (newCropBounds.x < 0) {
newCropBounds.width += newCropBounds.x;
newCropBounds.x = 0;
}
if (newCropBounds.y < 0) {
newCropBounds.height += newCropBounds.y;
newCropBounds.y = 0;
}
if (newCropBounds.x + newCropBounds.width > o.originalWidth) {
newCropBounds.width = o.originalWidth - newCropBounds.x;
}
if (newCropBounds.y + newCropBounds.height > o.originalHeight) {
newCropBounds.height = o.originalHeight - newCropBounds.y;
}
layer.cropBounds = newCropBounds;
}
else {
// TRANSFORM MODE: Resize the layer's main transform frame
let newWidth = localVecX;
let newHeight = localVecY;
if (isShiftPressed) {
const originalAspectRatio = o.width / o.height;
if (Math.abs(newWidth) > Math.abs(newHeight) * originalAspectRatio) {
newHeight = (Math.sign(newHeight) || 1) * Math.abs(newWidth) / originalAspectRatio;
}
else {
newWidth = (Math.sign(newWidth) || 1) * Math.abs(newHeight) * originalAspectRatio;
}
}
if (newWidth < 10)
newWidth = 10;
if (newHeight < 10)
newHeight = 10;
layer.width = newWidth;
layer.height = newHeight;
// Update position to keep anchor point fixed
const deltaW = layer.width - o.width;
const deltaH = layer.height - o.height;
const shiftX = (deltaW / 2) * signX;
const shiftY = (deltaH / 2) * signY;
const worldShiftX = shiftX * cos - shiftY * sin;
const worldShiftY = shiftX * sin + shiftY * cos;
const newCenterX = o.centerX + worldShiftX;
const newCenterY = o.centerY + worldShiftY;
layer.x = newCenterX - layer.width / 2;
layer.y = newCenterY - layer.height / 2;
}
this.canvas.render(); this.canvas.render();
} }
rotateLayerFromHandle(worldCoords, isShiftPressed) { rotateLayerFromHandle(worldCoords, isShiftPressed) {

View File

@@ -372,8 +372,24 @@ export class CanvasLayers {
// Create a temporary canvas for the masked layer // Create a temporary canvas for the masked layer
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height); const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height);
if (tempCtx) { if (tempCtx) {
// Draw the original image // This logic is now unified to handle both cropped and non-cropped images correctly.
tempCtx.drawImage(layer.image, 0, 0, layer.width, layer.height); const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
if (!layer.originalWidth || !layer.originalHeight) {
tempCtx.drawImage(layer.image, 0, 0, layer.width, layer.height);
}
else {
const layerScaleX = layer.width / layer.originalWidth;
const layerScaleY = layer.height / layer.originalHeight;
const dWidth = s.width * layerScaleX;
const dHeight = s.height * layerScaleY;
// The destination is the top-left of the temp canvas, plus the scaled offset of the crop area.
const dX = s.x * layerScaleX;
const dY = s.y * layerScaleY;
// We draw into a temp canvas of size layer.width x layer.height.
// The destination rect must be positioned correctly within this temp canvas.
// The dX/dY here are offsets from the top-left of the transform frame.
tempCtx.drawImage(layer.image, s.x, s.y, s.width, s.height, dX, dY, dWidth, dHeight);
}
// Apply the distance field mask using destination-in for transparency effect // Apply the distance field mask using destination-in for transparency effect
tempCtx.globalCompositeOperation = 'destination-in'; tempCtx.globalCompositeOperation = 'destination-in';
tempCtx.drawImage(maskCanvas, 0, 0, layer.width, layer.height); tempCtx.drawImage(maskCanvas, 0, 0, layer.width, layer.height);
@@ -384,26 +400,44 @@ export class CanvasLayers {
} }
else { else {
// Fallback to normal drawing // Fallback to normal drawing
ctx.globalCompositeOperation = layer.blendMode || 'normal'; this._drawLayerImage(ctx, layer);
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
} }
} }
else { else {
// Fallback to normal drawing // Fallback to normal drawing
ctx.globalCompositeOperation = layer.blendMode || 'normal'; this._drawLayerImage(ctx, layer);
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
} }
} }
else { else {
// Normal drawing without blend area effect // Normal drawing without blend area effect
ctx.globalCompositeOperation = layer.blendMode || 'normal'; this._drawLayerImage(ctx, layer);
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
} }
ctx.restore(); ctx.restore();
} }
_drawLayerImage(ctx, layer) {
ctx.globalCompositeOperation = layer.blendMode || 'normal';
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
// Use cropBounds if they exist, otherwise use the full image dimensions as the source
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
if (!layer.originalWidth || !layer.originalHeight) {
// Fallback for older layers without original dimensions or if data is missing
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
return;
}
// Calculate the on-screen scale of the layer's transform frame
const layerScaleX = layer.width / layer.originalWidth;
const layerScaleY = layer.height / layer.originalHeight;
// Calculate the on-screen size of the cropped portion
const dWidth = s.width * layerScaleX;
const dHeight = s.height * layerScaleY;
// Calculate the on-screen position of the top-left of the cropped portion.
// This is relative to the layer's center (the context's 0,0).
const dX = (-layer.width / 2) + (s.x * layerScaleX);
const dY = (-layer.height / 2) + (s.y * layerScaleY);
ctx.drawImage(layer.image, s.x, s.y, s.width, s.height, // source rect (from original image)
dX, dY, dWidth, dHeight // destination rect (scaled and positioned within the transform frame)
);
}
getDistanceFieldMaskSync(image, blendArea) { getDistanceFieldMaskSync(image, blendArea) {
// Check cache first // Check cache first
let imageCache = this.distanceFieldCache.get(image); let imageCache = this.distanceFieldCache.get(image);
@@ -527,30 +561,47 @@ export class CanvasLayers {
this.canvas.saveState(); this.canvas.saveState();
} }
getHandles(layer) { getHandles(layer) {
const centerX = layer.x + layer.width / 2; const layerCenterX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2; const layerCenterY = layer.y + layer.height / 2;
const rad = layer.rotation * Math.PI / 180; const rad = layer.rotation * Math.PI / 180;
const cos = Math.cos(rad); const cos = Math.cos(rad);
const sin = Math.sin(rad); const sin = Math.sin(rad);
const halfW = layer.width / 2; let handleCenterX, handleCenterY, halfW, halfH;
const halfH = layer.height / 2; if (layer.cropMode && layer.cropBounds && layer.originalWidth) {
// CROP MODE: Handles are relative to the cropped area
const layerScaleX = layer.width / layer.originalWidth;
const layerScaleY = layer.height / layer.originalHeight;
const cropRectW = layer.cropBounds.width * layerScaleX;
const cropRectH = layer.cropBounds.height * layerScaleY;
// Center of the CROP rectangle in the layer's local, un-rotated space
const cropCenterX_local = (-layer.width / 2) + ((layer.cropBounds.x + layer.cropBounds.width / 2) * layerScaleX);
const cropCenterY_local = (-layer.height / 2) + ((layer.cropBounds.y + layer.cropBounds.height / 2) * layerScaleY);
// Rotate this local center to find the world-space center of the crop rect
handleCenterX = layerCenterX + (cropCenterX_local * cos - cropCenterY_local * sin);
handleCenterY = layerCenterY + (cropCenterX_local * sin + cropCenterY_local * cos);
halfW = cropRectW / 2;
halfH = cropRectH / 2;
}
else {
// TRANSFORM MODE: Handles are relative to the full layer transform frame
handleCenterX = layerCenterX;
handleCenterY = layerCenterY;
halfW = layer.width / 2;
halfH = layer.height / 2;
}
const localHandles = { const localHandles = {
'n': { x: 0, y: -halfH }, 'n': { x: 0, y: -halfH }, 'ne': { x: halfW, y: -halfH },
'ne': { x: halfW, y: -halfH }, 'e': { x: halfW, y: 0 }, 'se': { x: halfW, y: halfH },
'e': { x: halfW, y: 0 }, 's': { x: 0, y: halfH }, 'sw': { x: -halfW, y: halfH },
'se': { x: halfW, y: halfH }, 'w': { x: -halfW, y: 0 }, 'nw': { x: -halfW, y: -halfH },
's': { x: 0, y: halfH },
'sw': { x: -halfW, y: halfH },
'w': { x: -halfW, y: 0 },
'nw': { x: -halfW, y: -halfH },
'rot': { x: 0, y: -halfH - 20 / this.canvas.viewport.zoom } 'rot': { x: 0, y: -halfH - 20 / this.canvas.viewport.zoom }
}; };
const worldHandles = {}; const worldHandles = {};
for (const key in localHandles) { for (const key in localHandles) {
const p = localHandles[key]; const p = localHandles[key];
worldHandles[key] = { worldHandles[key] = {
x: centerX + (p.x * cos - p.y * sin), x: handleCenterX + (p.x * cos - p.y * sin),
y: centerY + (p.x * sin + p.y * cos) y: handleCenterY + (p.x * sin + p.y * cos)
}; };
} }
return worldHandles; return worldHandles;

View File

@@ -431,38 +431,63 @@ export class CanvasRenderer {
drawSelectionFrame(ctx, layer) { drawSelectionFrame(ctx, layer) {
const lineWidth = 2 / this.canvas.viewport.zoom; const lineWidth = 2 / this.canvas.viewport.zoom;
const handleRadius = 5 / this.canvas.viewport.zoom; const handleRadius = 5 / this.canvas.viewport.zoom;
ctx.strokeStyle = '#00ff00'; if (layer.cropMode && layer.cropBounds && layer.originalWidth) {
ctx.lineWidth = lineWidth; // --- CROP MODE ---
// Rysuj ramkę z adaptacyjnymi liniami (ciągłe/przerywane w zależności od przykrycia) ctx.lineWidth = lineWidth;
const halfW = layer.width / 2; // 1. Draw dashed blue line for the full transform frame (the "original size" container)
const halfH = layer.height / 2; ctx.strokeStyle = '#007bff';
// Górna krawędź ctx.setLineDash([8 / this.canvas.viewport.zoom, 8 / this.canvas.viewport.zoom]);
this.drawAdaptiveLine(ctx, -halfW, -halfH, halfW, -halfH, layer); ctx.strokeRect(-layer.width / 2, -layer.height / 2, layer.width, layer.height);
// Prawa krawędź ctx.setLineDash([]);
this.drawAdaptiveLine(ctx, halfW, -halfH, halfW, halfH, layer); // 2. Draw solid blue line for the crop bounds
// Dolna krawędź const layerScaleX = layer.width / layer.originalWidth;
this.drawAdaptiveLine(ctx, halfW, halfH, -halfW, halfH, layer); const layerScaleY = layer.height / layer.originalHeight;
// Lewa krawędź const s = layer.cropBounds;
this.drawAdaptiveLine(ctx, -halfW, halfH, -halfW, -halfH, layer); const cropRectX = (-layer.width / 2) + (s.x * layerScaleX);
// Rysuj linię do uchwytu rotacji (zawsze ciągła) const cropRectY = (-layer.height / 2) + (s.y * layerScaleY);
ctx.setLineDash([]); const cropRectW = s.width * layerScaleX;
ctx.beginPath(); const cropRectH = s.height * layerScaleY;
ctx.moveTo(0, -layer.height / 2); ctx.strokeStyle = '#007bff'; // Solid blue
ctx.lineTo(0, -layer.height / 2 - 20 / this.canvas.viewport.zoom); this.drawAdaptiveLine(ctx, cropRectX, cropRectY, cropRectX + cropRectW, cropRectY, layer); // Top
ctx.stroke(); this.drawAdaptiveLine(ctx, cropRectX + cropRectW, cropRectY, cropRectX + cropRectW, cropRectY + cropRectH, layer); // Right
// Rysuj uchwyty this.drawAdaptiveLine(ctx, cropRectX + cropRectW, cropRectY + cropRectH, cropRectX, cropRectY + cropRectH, layer); // Bottom
this.drawAdaptiveLine(ctx, cropRectX, cropRectY + cropRectH, cropRectX, cropRectY, layer); // Left
}
else {
// --- TRANSFORM MODE ---
ctx.strokeStyle = '#00ff00'; // Green
ctx.lineWidth = lineWidth;
const halfW = layer.width / 2;
const halfH = layer.height / 2;
// Draw adaptive solid green line for transform frame
this.drawAdaptiveLine(ctx, -halfW, -halfH, halfW, -halfH, layer);
this.drawAdaptiveLine(ctx, halfW, -halfH, halfW, halfH, layer);
this.drawAdaptiveLine(ctx, halfW, halfH, -halfW, halfH, layer);
this.drawAdaptiveLine(ctx, -halfW, halfH, -halfW, -halfH, layer);
// Draw line to rotation handle
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(0, -halfH);
ctx.lineTo(0, -halfH - 20 / this.canvas.viewport.zoom);
ctx.stroke();
}
// --- DRAW HANDLES (Unified Logic) ---
const handles = this.canvas.canvasLayers.getHandles(layer); const handles = this.canvas.canvasLayers.getHandles(layer);
ctx.fillStyle = '#ffffff'; ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000'; ctx.strokeStyle = '#000000';
ctx.lineWidth = 1 / this.canvas.viewport.zoom; ctx.lineWidth = 1 / this.canvas.viewport.zoom;
for (const key in handles) { for (const key in handles) {
// Skip rotation handle in crop mode
if (layer.cropMode && key === 'rot')
continue;
const point = handles[key]; const point = handles[key];
ctx.beginPath(); // The handle position is already in world space, we need it in the layer's rotated space
const localX = point.x - (layer.x + layer.width / 2); const localX = point.x - (layer.x + layer.width / 2);
const localY = point.y - (layer.y + layer.height / 2); const localY = point.y - (layer.y + layer.height / 2);
const rad = -layer.rotation * Math.PI / 180; const rad = -layer.rotation * Math.PI / 180;
const rotatedX = localX * Math.cos(rad) - localY * Math.sin(rad); const rotatedX = localX * Math.cos(rad) - localY * Math.sin(rad);
const rotatedY = localX * Math.sin(rad) + localY * Math.cos(rad); const rotatedY = localX * Math.sin(rad) + localY * Math.cos(rad);
ctx.beginPath();
ctx.arc(rotatedX, rotatedY, handleRadius, 0, Math.PI * 2); ctx.arc(rotatedX, rotatedY, handleRadius, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();

View File

@@ -293,6 +293,43 @@ async function createCanvasWidget(node, widget, app) {
]), ]),
$el("div.painter-separator"), $el("div.painter-separator"),
$el("div.painter-button-group", {}, [ $el("div.painter-button-group", {}, [
$el("button.painter-button.requires-selection", {
id: `crop-mode-btn-${node.id}`,
textContent: "Crop Mode",
title: "Toggle crop mode for selected layer(s)",
onclick: () => {
const cropBtn = controlPanel.querySelector(`#crop-mode-btn-${node.id}`);
const selectedLayers = canvas.canvasSelection.selectedLayers;
if (selectedLayers.length === 0)
return;
// Toggle crop mode for all selected layers
const firstLayer = selectedLayers[0];
const newCropMode = !firstLayer.cropMode;
selectedLayers.forEach((layer) => {
layer.cropMode = newCropMode;
// Initialize crop bounds if entering crop mode
if (newCropMode && !layer.cropBounds) {
layer.cropBounds = {
x: 0,
y: 0,
width: layer.originalWidth,
height: layer.originalHeight
};
}
});
// Update button appearance
if (newCropMode) {
cropBtn.classList.add('primary');
cropBtn.title = "Exit crop mode for selected layer(s)";
}
else {
cropBtn.classList.remove('primary');
cropBtn.title = "Toggle crop mode for selected layer(s)";
}
canvas.saveState();
canvas.render();
}
}),
$el("button.painter-button.requires-selection", { $el("button.painter-button.requires-selection", {
textContent: "Rotate +90°", textContent: "Rotate +90°",
title: "Rotate selected layer(s) by +90 degrees", title: "Rotate selected layer(s) by +90 degrees",

View File

@@ -51,6 +51,32 @@
border-color: #3a76d6; border-color: #3a76d6;
} }
/* Crop mode button styling */
.painter-button#crop-mode-btn {
background-color: #444;
border-color: #555;
color: #fff;
transition: all 0.2s ease-in-out;
}
.painter-button#crop-mode-btn.primary {
background-color: #0080ff;
border-color: #0070e0;
color: #fff;
box-shadow: 0 0 8px rgba(0, 128, 255, 0.3);
}
.painter-button#crop-mode-btn.primary:hover {
background-color: #1090ff;
border-color: #0080ff;
box-shadow: 0 0 12px rgba(0, 128, 255, 0.4);
}
.painter-button#crop-mode-btn:hover {
background-color: #555;
border-color: #666;
}
.painter-button.success { .painter-button.success {
border-color: #4ae27a; border-color: #4ae27a;
background-color: #444; background-color: #444;

View File

@@ -626,7 +626,10 @@ export class CanvasInteractions {
width: layer.width, height: layer.height, width: layer.width, height: layer.height,
rotation: layer.rotation, rotation: layer.rotation,
centerX: layer.x + layer.width / 2, centerX: layer.x + layer.width / 2,
centerY: layer.y + layer.height / 2 centerY: layer.y + layer.height / 2,
originalWidth: layer.originalWidth,
originalHeight: layer.originalHeight,
cropBounds: layer.cropBounds ? { ...layer.cropBounds } : undefined
}; };
this.interaction.dragStart = {...worldCoords}; this.interaction.dragStart = {...worldCoords};
@@ -797,66 +800,137 @@ export class CanvasInteractions {
if (this.interaction.isCtrlPressed) { if (this.interaction.isCtrlPressed) {
const snapThreshold = 10 / this.canvas.viewport.zoom; const snapThreshold = 10 / this.canvas.viewport.zoom;
const snappedMouseX = snapToGrid(mouseX); mouseX = Math.abs(mouseX - snapToGrid(mouseX)) < snapThreshold ? snapToGrid(mouseX) : mouseX;
if (Math.abs(mouseX - snappedMouseX) < snapThreshold) mouseX = snappedMouseX; mouseY = Math.abs(mouseY - snapToGrid(mouseY)) < snapThreshold ? snapToGrid(mouseY) : mouseY;
const snappedMouseY = snapToGrid(mouseY);
if (Math.abs(mouseY - snappedMouseY) < snapThreshold) mouseY = snappedMouseY;
} }
const o = this.interaction.transformOrigin; const o = this.interaction.transformOrigin;
if (o.rotation === undefined || o.width === undefined || o.height === undefined || o.centerX === undefined || o.centerY === undefined) return; if (o.rotation === undefined || o.width === undefined || o.height === undefined || o.centerX === undefined || o.centerY === undefined) return;
const handle = this.interaction.resizeHandle; const handle = this.interaction.resizeHandle;
const anchor = this.interaction.resizeAnchor; const anchor = this.interaction.resizeAnchor;
const rad = o.rotation * Math.PI / 180; const rad = o.rotation * Math.PI / 180;
const cos = Math.cos(rad); const cos = Math.cos(rad);
const sin = Math.sin(rad); const sin = Math.sin(rad);
// Vector from anchor to mouse
const vecX = mouseX - anchor.x; const vecX = mouseX - anchor.x;
const vecY = mouseY - anchor.y; const vecY = mouseY - anchor.y;
let newWidth = vecX * cos + vecY * sin; // Rotate vector to align with layer's local coordinates
let newHeight = vecY * cos - vecX * sin; let localVecX = vecX * cos + vecY * sin;
let localVecY = vecY * cos - vecX * sin;
if (isShiftPressed) { // Determine sign based on handle
const originalAspectRatio = o.width / o.height; const signX = handle?.includes('e') ? 1 : (handle?.includes('w') ? -1 : 0);
const signY = handle?.includes('s') ? 1 : (handle?.includes('n') ? -1 : 0);
if (Math.abs(newWidth) > Math.abs(newHeight) * originalAspectRatio) { localVecX *= signX;
newHeight = (Math.sign(newHeight) || 1) * Math.abs(newWidth) / originalAspectRatio; localVecY *= signY;
} else {
newWidth = (Math.sign(newWidth) || 1) * Math.abs(newHeight) * originalAspectRatio; // If not a corner handle, keep original dimension
if (signX === 0) localVecX = o.width;
if (signY === 0) localVecY = o.height;
if (layer.cropMode && o.cropBounds && o.originalWidth && o.originalHeight) {
// CROP MODE: Calculate delta based on mouse movement and apply to cropBounds.
// Calculate mouse movement since drag start, in the layer's local coordinate system.
const dragStartX_local = this.interaction.dragStart.x - (o.centerX ?? 0);
const dragStartY_local = this.interaction.dragStart.y - (o.centerY ?? 0);
const mouseX_local = mouseX - (o.centerX ?? 0);
const mouseY_local = mouseY - (o.centerY ?? 0);
// Rotate mouse delta into the layer's unrotated frame
const deltaX_world = mouseX_local - dragStartX_local;
const deltaY_world = mouseY_local - dragStartY_local;
const mouseDeltaX_local = deltaX_world * cos + deltaY_world * sin;
const mouseDeltaY_local = deltaY_world * cos - deltaX_world * sin;
// Convert the on-screen mouse delta to an image-space delta.
const screenToImageScaleX = o.originalWidth / o.width;
const screenToImageScaleY = o.originalHeight / o.height;
const delta_image_x = mouseDeltaX_local * screenToImageScaleX;
const delta_image_y = mouseDeltaY_local * screenToImageScaleY;
let newCropBounds = { ...o.cropBounds }; // Start with the bounds from the beginning of the drag
// Apply the image-space delta to the appropriate edges of the crop bounds
if (handle?.includes('w')) {
newCropBounds.x += delta_image_x;
newCropBounds.width -= delta_image_x;
} }
if (handle?.includes('e')) {
newCropBounds.width += delta_image_x;
}
if (handle?.includes('n')) {
newCropBounds.y += delta_image_y;
newCropBounds.height -= delta_image_y;
}
if (handle?.includes('s')) {
newCropBounds.height += delta_image_y;
}
// Clamp crop bounds to stay within the original image and maintain minimum size
if (newCropBounds.width < 1) {
if (handle?.includes('w')) newCropBounds.x = o.cropBounds.x + o.cropBounds.width -1;
newCropBounds.width = 1;
}
if (newCropBounds.height < 1) {
if (handle?.includes('n')) newCropBounds.y = o.cropBounds.y + o.cropBounds.height - 1;
newCropBounds.height = 1;
}
if (newCropBounds.x < 0) {
newCropBounds.width += newCropBounds.x;
newCropBounds.x = 0;
}
if (newCropBounds.y < 0) {
newCropBounds.height += newCropBounds.y;
newCropBounds.y = 0;
}
if (newCropBounds.x + newCropBounds.width > o.originalWidth) {
newCropBounds.width = o.originalWidth - newCropBounds.x;
}
if (newCropBounds.y + newCropBounds.height > o.originalHeight) {
newCropBounds.height = o.originalHeight - newCropBounds.y;
}
layer.cropBounds = newCropBounds;
} else {
// TRANSFORM MODE: Resize the layer's main transform frame
let newWidth = localVecX;
let newHeight = localVecY;
if (isShiftPressed) {
const originalAspectRatio = o.width / o.height;
if (Math.abs(newWidth) > Math.abs(newHeight) * originalAspectRatio) {
newHeight = (Math.sign(newHeight) || 1) * Math.abs(newWidth) / originalAspectRatio;
} else {
newWidth = (Math.sign(newWidth) || 1) * Math.abs(newHeight) * originalAspectRatio;
}
}
if (newWidth < 10) newWidth = 10;
if (newHeight < 10) newHeight = 10;
layer.width = newWidth;
layer.height = newHeight;
// Update position to keep anchor point fixed
const deltaW = layer.width - o.width;
const deltaH = layer.height - o.height;
const shiftX = (deltaW / 2) * signX;
const shiftY = (deltaH / 2) * signY;
const worldShiftX = shiftX * cos - shiftY * sin;
const worldShiftY = shiftX * sin + shiftY * cos;
const newCenterX = o.centerX + worldShiftX;
const newCenterY = o.centerY + worldShiftY;
layer.x = newCenterX - layer.width / 2;
layer.y = newCenterY - layer.height / 2;
} }
let signX = handle?.includes('e') ? 1 : (handle?.includes('w') ? -1 : 0);
let signY = handle?.includes('s') ? 1 : (handle?.includes('n') ? -1 : 0);
newWidth *= signX;
newHeight *= signY;
if (signX === 0) newWidth = o.width;
if (signY === 0) newHeight = o.height;
if (newWidth < 10) newWidth = 10;
if (newHeight < 10) newHeight = 10;
layer.width = newWidth;
layer.height = newHeight;
const deltaW = newWidth - o.width;
const deltaH = newHeight - o.height;
const shiftX = (deltaW / 2) * signX;
const shiftY = (deltaH / 2) * signY;
const worldShiftX = shiftX * cos - shiftY * sin;
const worldShiftY = shiftX * sin + shiftY * cos;
const newCenterX = o.centerX + worldShiftX;
const newCenterY = o.centerY + worldShiftY;
layer.x = newCenterX - layer.width / 2;
layer.y = newCenterY - layer.height / 2;
this.canvas.render(); this.canvas.render();
} }

View File

@@ -433,8 +433,31 @@ export class CanvasLayers {
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height); const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height);
if (tempCtx) { if (tempCtx) {
// Draw the original image // This logic is now unified to handle both cropped and non-cropped images correctly.
tempCtx.drawImage(layer.image, 0, 0, layer.width, layer.height); const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
if (!layer.originalWidth || !layer.originalHeight) {
tempCtx.drawImage(layer.image, 0, 0, layer.width, layer.height);
} else {
const layerScaleX = layer.width / layer.originalWidth;
const layerScaleY = layer.height / layer.originalHeight;
const dWidth = s.width * layerScaleX;
const dHeight = s.height * layerScaleY;
// The destination is the top-left of the temp canvas, plus the scaled offset of the crop area.
const dX = s.x * layerScaleX;
const dY = s.y * layerScaleY;
// We draw into a temp canvas of size layer.width x layer.height.
// The destination rect must be positioned correctly within this temp canvas.
// The dX/dY here are offsets from the top-left of the transform frame.
tempCtx.drawImage(
layer.image,
s.x, s.y, s.width, s.height,
dX, dY, dWidth, dHeight
);
}
// Apply the distance field mask using destination-in for transparency effect // Apply the distance field mask using destination-in for transparency effect
tempCtx.globalCompositeOperation = 'destination-in'; tempCtx.globalCompositeOperation = 'destination-in';
@@ -446,26 +469,53 @@ export class CanvasLayers {
ctx.drawImage(tempCanvas, -layer.width / 2, -layer.height / 2, layer.width, layer.height); ctx.drawImage(tempCanvas, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
} else { } else {
// Fallback to normal drawing // Fallback to normal drawing
ctx.globalCompositeOperation = layer.blendMode as any || 'normal'; this._drawLayerImage(ctx, layer);
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
} }
} else { } else {
// Fallback to normal drawing // Fallback to normal drawing
ctx.globalCompositeOperation = layer.blendMode as any || 'normal'; this._drawLayerImage(ctx, layer);
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
} }
} else { } else {
// Normal drawing without blend area effect // Normal drawing without blend area effect
ctx.globalCompositeOperation = layer.blendMode as any || 'normal'; this._drawLayerImage(ctx, layer);
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
} }
ctx.restore(); ctx.restore();
} }
private _drawLayerImage(ctx: CanvasRenderingContext2D, layer: Layer): void {
ctx.globalCompositeOperation = layer.blendMode as any || 'normal';
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
// Use cropBounds if they exist, otherwise use the full image dimensions as the source
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
if (!layer.originalWidth || !layer.originalHeight) {
// Fallback for older layers without original dimensions or if data is missing
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
return;
}
// Calculate the on-screen scale of the layer's transform frame
const layerScaleX = layer.width / layer.originalWidth;
const layerScaleY = layer.height / layer.originalHeight;
// Calculate the on-screen size of the cropped portion
const dWidth = s.width * layerScaleX;
const dHeight = s.height * layerScaleY;
// Calculate the on-screen position of the top-left of the cropped portion.
// This is relative to the layer's center (the context's 0,0).
const dX = (-layer.width / 2) + (s.x * layerScaleX);
const dY = (-layer.height / 2) + (s.y * layerScaleY);
ctx.drawImage(
layer.image,
s.x, s.y, s.width, s.height, // source rect (from original image)
dX, dY, dWidth, dHeight // destination rect (scaled and positioned within the transform frame)
);
}
private getDistanceFieldMaskSync(image: HTMLImageElement, blendArea: number): HTMLCanvasElement | null { private getDistanceFieldMaskSync(image: HTMLImageElement, blendArea: number): HTMLCanvasElement | null {
// Check cache first // Check cache first
let imageCache = this.distanceFieldCache.get(image); let imageCache = this.distanceFieldCache.get(image);
@@ -606,23 +656,45 @@ export class CanvasLayers {
} }
getHandles(layer: Layer): Record<string, Point> { getHandles(layer: Layer): Record<string, Point> {
const centerX = layer.x + layer.width / 2; const layerCenterX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2; const layerCenterY = layer.y + layer.height / 2;
const rad = layer.rotation * Math.PI / 180; const rad = layer.rotation * Math.PI / 180;
const cos = Math.cos(rad); const cos = Math.cos(rad);
const sin = Math.sin(rad); const sin = Math.sin(rad);
const halfW = layer.width / 2; let handleCenterX, handleCenterY, halfW, halfH;
const halfH = layer.height / 2;
if (layer.cropMode && layer.cropBounds && layer.originalWidth) {
// CROP MODE: Handles are relative to the cropped area
const layerScaleX = layer.width / layer.originalWidth;
const layerScaleY = layer.height / layer.originalHeight;
const cropRectW = layer.cropBounds.width * layerScaleX;
const cropRectH = layer.cropBounds.height * layerScaleY;
// Center of the CROP rectangle in the layer's local, un-rotated space
const cropCenterX_local = (-layer.width / 2) + ((layer.cropBounds.x + layer.cropBounds.width / 2) * layerScaleX);
const cropCenterY_local = (-layer.height / 2) + ((layer.cropBounds.y + layer.cropBounds.height / 2) * layerScaleY);
// Rotate this local center to find the world-space center of the crop rect
handleCenterX = layerCenterX + (cropCenterX_local * cos - cropCenterY_local * sin);
handleCenterY = layerCenterY + (cropCenterX_local * sin + cropCenterY_local * cos);
halfW = cropRectW / 2;
halfH = cropRectH / 2;
} else {
// TRANSFORM MODE: Handles are relative to the full layer transform frame
handleCenterX = layerCenterX;
handleCenterY = layerCenterY;
halfW = layer.width / 2;
halfH = layer.height / 2;
}
const localHandles: Record<string, Point> = { const localHandles: Record<string, Point> = {
'n': { x: 0, y: -halfH }, 'n': { x: 0, y: -halfH }, 'ne': { x: halfW, y: -halfH },
'ne': { x: halfW, y: -halfH }, 'e': { x: halfW, y: 0 }, 'se': { x: halfW, y: halfH },
'e': { x: halfW, y: 0 }, 's': { x: 0, y: halfH }, 'sw': { x: -halfW, y: halfH },
'se': { x: halfW, y: halfH }, 'w': { x: -halfW, y: 0 }, 'nw': { x: -halfW, y: -halfH },
's': { x: 0, y: halfH },
'sw': { x: -halfW, y: halfH },
'w': { x: -halfW, y: 0 },
'nw': { x: -halfW, y: -halfH },
'rot': { x: 0, y: -halfH - 20 / this.canvas.viewport.zoom } 'rot': { x: 0, y: -halfH - 20 / this.canvas.viewport.zoom }
}; };
@@ -630,8 +702,8 @@ export class CanvasLayers {
for (const key in localHandles) { for (const key in localHandles) {
const p = localHandles[key]; const p = localHandles[key];
worldHandles[key] = { worldHandles[key] = {
x: centerX + (p.x * cos - p.y * sin), x: handleCenterX + (p.x * cos - p.y * sin),
y: centerY + (p.x * sin + p.y * cos) y: handleCenterY + (p.x * sin + p.y * cos)
}; };
} }
return worldHandles; return worldHandles;

View File

@@ -532,38 +532,66 @@ export class CanvasRenderer {
drawSelectionFrame(ctx: any, layer: any) { drawSelectionFrame(ctx: any, layer: any) {
const lineWidth = 2 / this.canvas.viewport.zoom; const lineWidth = 2 / this.canvas.viewport.zoom;
const handleRadius = 5 / this.canvas.viewport.zoom; const handleRadius = 5 / this.canvas.viewport.zoom;
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = lineWidth;
// Rysuj ramkę z adaptacyjnymi liniami (ciągłe/przerywane w zależności od przykrycia) if (layer.cropMode && layer.cropBounds && layer.originalWidth) {
const halfW = layer.width / 2; // --- CROP MODE ---
const halfH = layer.height / 2; ctx.lineWidth = lineWidth;
// Górna krawędź // 1. Draw dashed blue line for the full transform frame (the "original size" container)
this.drawAdaptiveLine(ctx, -halfW, -halfH, halfW, -halfH, layer); ctx.strokeStyle = '#007bff';
// Prawa krawędź ctx.setLineDash([8 / this.canvas.viewport.zoom, 8 / this.canvas.viewport.zoom]);
this.drawAdaptiveLine(ctx, halfW, -halfH, halfW, halfH, layer); ctx.strokeRect(-layer.width / 2, -layer.height / 2, layer.width, layer.height);
// Dolna krawędź ctx.setLineDash([]);
this.drawAdaptiveLine(ctx, halfW, halfH, -halfW, halfH, layer);
// Lewa krawędź
this.drawAdaptiveLine(ctx, -halfW, halfH, -halfW, -halfH, layer);
// Rysuj linię do uchwytu rotacji (zawsze ciągła) // 2. Draw solid blue line for the crop bounds
ctx.setLineDash([]); const layerScaleX = layer.width / layer.originalWidth;
ctx.beginPath(); const layerScaleY = layer.height / layer.originalHeight;
ctx.moveTo(0, -layer.height / 2); const s = layer.cropBounds;
ctx.lineTo(0, -layer.height / 2 - 20 / this.canvas.viewport.zoom);
ctx.stroke();
// Rysuj uchwyty const cropRectX = (-layer.width / 2) + (s.x * layerScaleX);
const cropRectY = (-layer.height / 2) + (s.y * layerScaleY);
const cropRectW = s.width * layerScaleX;
const cropRectH = s.height * layerScaleY;
ctx.strokeStyle = '#007bff'; // Solid blue
this.drawAdaptiveLine(ctx, cropRectX, cropRectY, cropRectX + cropRectW, cropRectY, layer); // Top
this.drawAdaptiveLine(ctx, cropRectX + cropRectW, cropRectY, cropRectX + cropRectW, cropRectY + cropRectH, layer); // Right
this.drawAdaptiveLine(ctx, cropRectX + cropRectW, cropRectY + cropRectH, cropRectX, cropRectY + cropRectH, layer); // Bottom
this.drawAdaptiveLine(ctx, cropRectX, cropRectY + cropRectH, cropRectX, cropRectY, layer); // Left
} else {
// --- TRANSFORM MODE ---
ctx.strokeStyle = '#00ff00'; // Green
ctx.lineWidth = lineWidth;
const halfW = layer.width / 2;
const halfH = layer.height / 2;
// Draw adaptive solid green line for transform frame
this.drawAdaptiveLine(ctx, -halfW, -halfH, halfW, -halfH, layer);
this.drawAdaptiveLine(ctx, halfW, -halfH, halfW, halfH, layer);
this.drawAdaptiveLine(ctx, halfW, halfH, -halfW, halfH, layer);
this.drawAdaptiveLine(ctx, -halfW, halfH, -halfW, -halfH, layer);
// Draw line to rotation handle
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(0, -halfH);
ctx.lineTo(0, -halfH - 20 / this.canvas.viewport.zoom);
ctx.stroke();
}
// --- DRAW HANDLES (Unified Logic) ---
const handles = this.canvas.canvasLayers.getHandles(layer); const handles = this.canvas.canvasLayers.getHandles(layer);
ctx.fillStyle = '#ffffff'; ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000'; ctx.strokeStyle = '#000000';
ctx.lineWidth = 1 / this.canvas.viewport.zoom; ctx.lineWidth = 1 / this.canvas.viewport.zoom;
for (const key in handles) { for (const key in handles) {
// Skip rotation handle in crop mode
if (layer.cropMode && key === 'rot') continue;
const point = handles[key]; const point = handles[key];
ctx.beginPath(); // The handle position is already in world space, we need it in the layer's rotated space
const localX = point.x - (layer.x + layer.width / 2); const localX = point.x - (layer.x + layer.width / 2);
const localY = point.y - (layer.y + layer.height / 2); const localY = point.y - (layer.y + layer.height / 2);
@@ -571,6 +599,7 @@ export class CanvasRenderer {
const rotatedX = localX * Math.cos(rad) - localY * Math.sin(rad); const rotatedX = localX * Math.cos(rad) - localY * Math.sin(rad);
const rotatedY = localX * Math.sin(rad) + localY * Math.cos(rad); const rotatedY = localX * Math.sin(rad) + localY * Math.cos(rad);
ctx.beginPath();
ctx.arc(rotatedX, rotatedY, handleRadius, 0, Math.PI * 2); ctx.arc(rotatedX, rotatedY, handleRadius, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();

View File

@@ -326,6 +326,47 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
$el("div.painter-separator"), $el("div.painter-separator"),
$el("div.painter-button-group", {}, [ $el("div.painter-button-group", {}, [
$el("button.painter-button.requires-selection", {
id: `crop-mode-btn-${node.id}`,
textContent: "Crop Mode",
title: "Toggle crop mode for selected layer(s)",
onclick: () => {
const cropBtn = controlPanel.querySelector(`#crop-mode-btn-${node.id}`) as HTMLButtonElement;
const selectedLayers = canvas.canvasSelection.selectedLayers;
if (selectedLayers.length === 0) return;
// Toggle crop mode for all selected layers
const firstLayer = selectedLayers[0];
const newCropMode = !firstLayer.cropMode;
selectedLayers.forEach((layer: Layer) => {
layer.cropMode = newCropMode;
// Initialize crop bounds if entering crop mode
if (newCropMode && !layer.cropBounds) {
layer.cropBounds = {
x: 0,
y: 0,
width: layer.originalWidth,
height: layer.originalHeight
};
}
});
// Update button appearance
if (newCropMode) {
cropBtn.classList.add('primary');
cropBtn.title = "Exit crop mode for selected layer(s)";
} else {
cropBtn.classList.remove('primary');
cropBtn.title = "Toggle crop mode for selected layer(s)";
}
canvas.saveState();
canvas.render();
}
}),
$el("button.painter-button.requires-selection", { $el("button.painter-button.requires-selection", {
textContent: "Rotate +90°", textContent: "Rotate +90°",
title: "Rotate selected layer(s) by +90 degrees", title: "Rotate selected layer(s) by +90 degrees",

View File

@@ -51,6 +51,32 @@
border-color: #3a76d6; border-color: #3a76d6;
} }
/* Crop mode button styling */
.painter-button#crop-mode-btn {
background-color: #444;
border-color: #555;
color: #fff;
transition: all 0.2s ease-in-out;
}
.painter-button#crop-mode-btn.primary {
background-color: #0080ff;
border-color: #0070e0;
color: #fff;
box-shadow: 0 0 8px rgba(0, 128, 255, 0.3);
}
.painter-button#crop-mode-btn.primary:hover {
background-color: #1090ff;
border-color: #0080ff;
box-shadow: 0 0 12px rgba(0, 128, 255, 0.4);
}
.painter-button#crop-mode-btn:hover {
background-color: #555;
border-color: #666;
}
.painter-button.success { .painter-button.success {
border-color: #4ae27a; border-color: #4ae27a;
background-color: #444; background-color: #444;

View File

@@ -21,6 +21,13 @@ export interface Layer {
flipH?: boolean; flipH?: boolean;
flipV?: boolean; flipV?: boolean;
blendArea?: number; blendArea?: number;
cropMode?: boolean; // czy warstwa jest w trybie crop
cropBounds?: { // granice przycinania
x: number; // offset od lewej krawędzi obrazu
y: number; // offset od górnej krawędzi obrazu
width: number; // szerokość widocznego obszaru
height: number; // wysokość widocznego obszaru
};
} }
export interface ComfyNode { export interface ComfyNode {