feat: add interactive output area transform handles

Implemented drag-to-resize functionality for the output area with visual transform handles on corners and edges. Users can now interactively resize the output area by dragging handles instead of using dialogs, with support for grid snapping and aspect ratio preservation.
This commit is contained in:
Dariusz L
2025-08-14 13:54:10 +02:00
parent 7ce7194cbf
commit 0f4f2cb1b0
6 changed files with 495 additions and 161 deletions

View File

@@ -39,6 +39,8 @@ export class CanvasInteractions {
keyMovementInProgress: false,
canvasResizeRect: null,
canvasMoveRect: null,
outputAreaTransformHandle: null,
outputAreaTransformAnchor: { x: 0, y: 0 },
};
this.originalLayerPositions = new Map();
}
@@ -157,6 +159,7 @@ export class CanvasInteractions {
this.interaction.canvasMoveRect = null;
this.interaction.hasClonedInDrag = false;
this.interaction.transformingLayer = null;
this.interaction.outputAreaTransformHandle = null;
this.canvas.canvas.style.cursor = 'default';
}
handleMouseDown(e) {
@@ -168,6 +171,18 @@ export class CanvasInteractions {
// Don't render here - mask tool will handle its own drawing
return;
}
if (this.interaction.mode === 'transformingOutputArea') {
// Check if clicking on output area transform handle
const handle = this.getOutputAreaHandle(coords.world);
if (handle) {
this.startOutputAreaTransform(handle, coords.world);
return;
}
// If clicking outside, exit transform mode
this.interaction.mode = 'none';
this.canvas.render();
return;
}
if (this.canvas.shapeTool.isActive) {
this.canvas.shapeTool.addPoint(coords.world);
return;
@@ -258,6 +273,14 @@ export class CanvasInteractions {
case 'movingCanvas':
this.updateCanvasMove(coords.world);
break;
case 'transformingOutputArea':
if (this.interaction.outputAreaTransformHandle) {
this.resizeOutputAreaFromHandle(coords.world, e.shiftKey);
}
else {
this.updateOutputAreaTransformCursor(coords.world);
}
break;
default:
this.updateCursor(coords.world);
// Update brush cursor on overlay if mask tool is active
@@ -285,6 +308,10 @@ export class CanvasInteractions {
if (this.interaction.mode === 'movingCanvas') {
this.finalizeCanvasMove();
}
if (this.interaction.mode === 'transformingOutputArea' && this.interaction.outputAreaTransformHandle) {
this.finalizeOutputAreaTransform();
return;
}
// Log layer positions when dragging ends
if (this.interaction.mode === 'dragging' && this.canvas.canvasSelection.selectedLayers.length > 0) {
this.logDragCompletion(coords);
@@ -1128,4 +1155,168 @@ export class CanvasInteractions {
}
await this.canvas.canvasLayers.clipboardManager.handlePaste('mouse', preference);
}
// New methods for output area transformation
activateOutputAreaTransform() {
// Clear any existing interaction state before starting transform
this.resetInteractionState();
// Deactivate any active tools that might conflict
if (this.canvas.shapeTool.isActive) {
this.canvas.shapeTool.deactivate();
}
if (this.canvas.maskTool.isActive) {
this.canvas.maskTool.deactivate();
}
// Clear selection to avoid confusion
this.canvas.canvasSelection.updateSelection([]);
// Set transform mode
this.interaction.mode = 'transformingOutputArea';
this.canvas.render();
}
getOutputAreaHandle(worldCoords) {
const bounds = this.canvas.outputAreaBounds;
const threshold = 10 / this.canvas.viewport.zoom;
// Define handle positions
const handles = {
'nw': { x: bounds.x, y: bounds.y },
'n': { x: bounds.x + bounds.width / 2, y: bounds.y },
'ne': { x: bounds.x + bounds.width, y: bounds.y },
'e': { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 },
'se': { x: bounds.x + bounds.width, y: bounds.y + bounds.height },
's': { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height },
'sw': { x: bounds.x, y: bounds.y + bounds.height },
'w': { x: bounds.x, y: bounds.y + bounds.height / 2 },
};
for (const [name, pos] of Object.entries(handles)) {
const dx = worldCoords.x - pos.x;
const dy = worldCoords.y - pos.y;
if (Math.sqrt(dx * dx + dy * dy) < threshold) {
return name;
}
}
return null;
}
startOutputAreaTransform(handle, worldCoords) {
this.interaction.outputAreaTransformHandle = handle;
this.interaction.dragStart = { ...worldCoords };
const bounds = this.canvas.outputAreaBounds;
this.interaction.transformOrigin = {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
rotation: 0,
centerX: bounds.x + bounds.width / 2,
centerY: bounds.y + bounds.height / 2
};
// Set anchor point (opposite corner for resize)
const anchorMap = {
'nw': { x: bounds.x + bounds.width, y: bounds.y + bounds.height },
'n': { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height },
'ne': { x: bounds.x, y: bounds.y + bounds.height },
'e': { x: bounds.x, y: bounds.y + bounds.height / 2 },
'se': { x: bounds.x, y: bounds.y },
's': { x: bounds.x + bounds.width / 2, y: bounds.y },
'sw': { x: bounds.x + bounds.width, y: bounds.y },
'w': { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 },
};
this.interaction.outputAreaTransformAnchor = anchorMap[handle];
}
resizeOutputAreaFromHandle(worldCoords, isShiftPressed) {
const o = this.interaction.transformOrigin;
if (!o)
return;
const handle = this.interaction.outputAreaTransformHandle;
const anchor = this.interaction.outputAreaTransformAnchor;
let newX = o.x;
let newY = o.y;
let newWidth = o.width;
let newHeight = o.height;
// Calculate new dimensions based on handle
if (handle?.includes('w')) {
const deltaX = worldCoords.x - anchor.x;
newWidth = Math.abs(deltaX);
newX = Math.min(worldCoords.x, anchor.x);
}
if (handle?.includes('e')) {
const deltaX = worldCoords.x - anchor.x;
newWidth = Math.abs(deltaX);
newX = Math.min(worldCoords.x, anchor.x);
}
if (handle?.includes('n')) {
const deltaY = worldCoords.y - anchor.y;
newHeight = Math.abs(deltaY);
newY = Math.min(worldCoords.y, anchor.y);
}
if (handle?.includes('s')) {
const deltaY = worldCoords.y - anchor.y;
newHeight = Math.abs(deltaY);
newY = Math.min(worldCoords.y, anchor.y);
}
// Maintain aspect ratio if shift is held
if (isShiftPressed && o.width > 0 && o.height > 0) {
const aspectRatio = o.width / o.height;
if (handle === 'n' || handle === 's') {
newWidth = newHeight * aspectRatio;
}
else if (handle === 'e' || handle === 'w') {
newHeight = newWidth / aspectRatio;
}
else {
// Corner handles
const proposedRatio = newWidth / newHeight;
if (proposedRatio > aspectRatio) {
newHeight = newWidth / aspectRatio;
}
else {
newWidth = newHeight * aspectRatio;
}
}
}
// Snap to grid if Ctrl is held
if (this.interaction.isCtrlPressed) {
newX = snapToGrid(newX);
newY = snapToGrid(newY);
newWidth = snapToGrid(newWidth);
newHeight = snapToGrid(newHeight);
}
// Apply minimum size
if (newWidth < 10)
newWidth = 10;
if (newHeight < 10)
newHeight = 10;
// Update output area bounds temporarily for preview
this.canvas.outputAreaBounds = {
x: newX,
y: newY,
width: newWidth,
height: newHeight
};
this.canvas.render();
}
updateOutputAreaTransformCursor(worldCoords) {
const handle = this.getOutputAreaHandle(worldCoords);
if (handle) {
const cursorMap = {
'n': 'ns-resize', 's': 'ns-resize',
'e': 'ew-resize', 'w': 'ew-resize',
'nw': 'nwse-resize', 'se': 'nwse-resize',
'ne': 'nesw-resize', 'sw': 'nesw-resize',
};
this.canvas.canvas.style.cursor = cursorMap[handle] || 'default';
}
else {
this.canvas.canvas.style.cursor = 'default';
}
}
finalizeOutputAreaTransform() {
const bounds = this.canvas.outputAreaBounds;
// Update canvas size and mask tool
this.canvas.updateOutputAreaSize(bounds.width, bounds.height);
// Update mask canvas for new output area
this.canvas.maskTool.updateMaskCanvasForOutputArea();
// Save state
this.canvas.saveState();
// Reset transform handle but keep transform mode active
this.interaction.outputAreaTransformHandle = null;
}
}

View File

@@ -147,6 +147,7 @@ export class CanvasRenderer {
this.renderInteractionElements(ctx);
this.canvas.shapeTool.render(ctx);
this.drawMaskAreaBounds(ctx); // Draw mask area bounds when mask tool is active
this.renderOutputAreaTransformHandles(ctx); // Draw output area transform handles
this.renderLayerInfo(ctx);
// Update custom shape menu position and visibility
if (this.canvas.outputAreaShape) {
@@ -832,4 +833,40 @@ export class CanvasRenderer {
// Just ensure it's the right size
this.updateOverlaySize();
}
/**
* Draw transform handles for output area when in transform mode
*/
renderOutputAreaTransformHandles(ctx) {
if (this.canvas.canvasInteractions.interaction.mode !== 'transformingOutputArea') {
return;
}
const bounds = this.canvas.outputAreaBounds;
const handleRadius = 5 / this.canvas.viewport.zoom;
// Define handle positions
const handles = {
'nw': { x: bounds.x, y: bounds.y },
'n': { x: bounds.x + bounds.width / 2, y: bounds.y },
'ne': { x: bounds.x + bounds.width, y: bounds.y },
'e': { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 },
'se': { x: bounds.x + bounds.width, y: bounds.y + bounds.height },
's': { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height },
'sw': { x: bounds.x, y: bounds.y + bounds.height },
'w': { x: bounds.x, y: bounds.y + bounds.height / 2 },
};
// Draw handles
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1 / this.canvas.viewport.zoom;
for (const [name, pos] of Object.entries(handles)) {
ctx.beginPath();
ctx.arc(pos.x, pos.y, handleRadius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
// Draw a highlight around the output area
ctx.strokeStyle = 'rgba(0, 150, 255, 0.8)';
ctx.lineWidth = 3 / this.canvas.viewport.zoom;
ctx.setLineDash([]);
ctx.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height);
}
}

View File

@@ -234,86 +234,11 @@ async function createCanvasWidget(node, widget, app) {
}),
$el("button.painter-button", {
textContent: "Output Area Size",
title: "Set the size of the output area",
title: "Transform output area - drag handles to resize",
onclick: () => {
const dialog = $el("div.painter-dialog", {
style: {
position: 'fixed',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
zIndex: '9999'
}
}, [
$el("div", {
style: {
color: "white",
marginBottom: "10px"
}
}, [
$el("label", {
style: {
marginRight: "5px"
}
}, [
$el("span", {}, ["Width: "])
]),
$el("input", {
type: "number",
id: "canvas-width",
value: String(canvas.width),
min: "1",
max: "4096"
})
]),
$el("div", {
style: {
color: "white",
marginBottom: "10px"
}
}, [
$el("label", {
style: {
marginRight: "5px"
}
}, [
$el("span", {}, ["Height: "])
]),
$el("input", {
type: "number",
id: "canvas-height",
value: String(canvas.height),
min: "1",
max: "4096"
})
]),
$el("div", {
style: {
textAlign: "right"
}
}, [
$el("button", {
id: "cancel-size",
textContent: "Cancel"
}),
$el("button", {
id: "confirm-size",
textContent: "OK"
})
])
]);
document.body.appendChild(dialog);
document.getElementById('confirm-size').onclick = () => {
const widthInput = document.getElementById('canvas-width');
const heightInput = document.getElementById('canvas-height');
const width = parseInt(widthInput.value) || canvas.width;
const height = parseInt(heightInput.value) || canvas.height;
canvas.setOutputAreaSize(width, height);
document.body.removeChild(dialog);
};
document.getElementById('cancel-size').onclick = () => {
document.body.removeChild(dialog);
};
// Activate output area transform mode
canvas.canvasInteractions.activateOutputAreaTransform();
showInfoNotification("Click and drag the handles to resize the output area. Click anywhere else to exit.", 3000);
}
}),
$el("button.painter-button.requires-selection", {

View File

@@ -31,7 +31,7 @@ interface TransformOrigin {
}
interface InteractionState {
mode: 'none' | 'panning' | 'dragging' | 'resizing' | 'rotating' | 'drawingMask' | 'resizingCanvas' | 'movingCanvas' | 'potential-drag' | 'drawingShape';
mode: 'none' | 'panning' | 'dragging' | 'resizing' | 'rotating' | 'drawingMask' | 'resizingCanvas' | 'movingCanvas' | 'potential-drag' | 'drawingShape' | 'transformingOutputArea';
panStart: Point;
dragStart: Point;
transformOrigin: TransformOrigin | null;
@@ -49,6 +49,8 @@ interface InteractionState {
keyMovementInProgress: boolean;
canvasResizeRect: { x: number, y: number, width: number, height: number } | null;
canvasMoveRect: { x: number, y: number, width: number, height: number } | null;
outputAreaTransformHandle: string | null;
outputAreaTransformAnchor: Point;
}
export class CanvasInteractions {
@@ -94,6 +96,8 @@ export class CanvasInteractions {
keyMovementInProgress: false,
canvasResizeRect: null,
canvasMoveRect: null,
outputAreaTransformHandle: null,
outputAreaTransformAnchor: { x: 0, y: 0 },
};
this.originalLayerPositions = new Map();
}
@@ -238,6 +242,7 @@ export class CanvasInteractions {
this.interaction.canvasMoveRect = null;
this.interaction.hasClonedInDrag = false;
this.interaction.transformingLayer = null;
this.interaction.outputAreaTransformHandle = null;
this.canvas.canvas.style.cursor = 'default';
}
@@ -252,6 +257,19 @@ export class CanvasInteractions {
return;
}
if (this.interaction.mode === 'transformingOutputArea') {
// Check if clicking on output area transform handle
const handle = this.getOutputAreaHandle(coords.world);
if (handle) {
this.startOutputAreaTransform(handle, coords.world);
return;
}
// If clicking outside, exit transform mode
this.interaction.mode = 'none';
this.canvas.render();
return;
}
if (this.canvas.shapeTool.isActive) {
this.canvas.shapeTool.addPoint(coords.world);
return;
@@ -352,6 +370,13 @@ export class CanvasInteractions {
case 'movingCanvas':
this.updateCanvasMove(coords.world);
break;
case 'transformingOutputArea':
if (this.interaction.outputAreaTransformHandle) {
this.resizeOutputAreaFromHandle(coords.world, e.shiftKey);
} else {
this.updateOutputAreaTransformCursor(coords.world);
}
break;
default:
this.updateCursor(coords.world);
// Update brush cursor on overlay if mask tool is active
@@ -384,6 +409,11 @@ export class CanvasInteractions {
this.finalizeCanvasMove();
}
if (this.interaction.mode === 'transformingOutputArea' && this.interaction.outputAreaTransformHandle) {
this.finalizeOutputAreaTransform();
return;
}
// Log layer positions when dragging ends
if (this.interaction.mode === 'dragging' && this.canvas.canvasSelection.selectedLayers.length > 0) {
this.logDragCompletion(coords);
@@ -1313,4 +1343,189 @@ export class CanvasInteractions {
await this.canvas.canvasLayers.clipboardManager.handlePaste('mouse', preference);
}
// New methods for output area transformation
public activateOutputAreaTransform(): void {
// Clear any existing interaction state before starting transform
this.resetInteractionState();
// Deactivate any active tools that might conflict
if (this.canvas.shapeTool.isActive) {
this.canvas.shapeTool.deactivate();
}
if (this.canvas.maskTool.isActive) {
this.canvas.maskTool.deactivate();
}
// Clear selection to avoid confusion
this.canvas.canvasSelection.updateSelection([]);
// Set transform mode
this.interaction.mode = 'transformingOutputArea';
this.canvas.render();
}
private getOutputAreaHandle(worldCoords: Point): string | null {
const bounds = this.canvas.outputAreaBounds;
const threshold = 10 / this.canvas.viewport.zoom;
// Define handle positions
const handles = {
'nw': { x: bounds.x, y: bounds.y },
'n': { x: bounds.x + bounds.width / 2, y: bounds.y },
'ne': { x: bounds.x + bounds.width, y: bounds.y },
'e': { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 },
'se': { x: bounds.x + bounds.width, y: bounds.y + bounds.height },
's': { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height },
'sw': { x: bounds.x, y: bounds.y + bounds.height },
'w': { x: bounds.x, y: bounds.y + bounds.height / 2 },
};
for (const [name, pos] of Object.entries(handles)) {
const dx = worldCoords.x - pos.x;
const dy = worldCoords.y - pos.y;
if (Math.sqrt(dx * dx + dy * dy) < threshold) {
return name;
}
}
return null;
}
private startOutputAreaTransform(handle: string, worldCoords: Point): void {
this.interaction.outputAreaTransformHandle = handle;
this.interaction.dragStart = { ...worldCoords };
const bounds = this.canvas.outputAreaBounds;
this.interaction.transformOrigin = {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
rotation: 0,
centerX: bounds.x + bounds.width / 2,
centerY: bounds.y + bounds.height / 2
};
// Set anchor point (opposite corner for resize)
const anchorMap: { [key: string]: Point } = {
'nw': { x: bounds.x + bounds.width, y: bounds.y + bounds.height },
'n': { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height },
'ne': { x: bounds.x, y: bounds.y + bounds.height },
'e': { x: bounds.x, y: bounds.y + bounds.height / 2 },
'se': { x: bounds.x, y: bounds.y },
's': { x: bounds.x + bounds.width / 2, y: bounds.y },
'sw': { x: bounds.x + bounds.width, y: bounds.y },
'w': { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 },
};
this.interaction.outputAreaTransformAnchor = anchorMap[handle];
}
private resizeOutputAreaFromHandle(worldCoords: Point, isShiftPressed: boolean): void {
const o = this.interaction.transformOrigin;
if (!o) return;
const handle = this.interaction.outputAreaTransformHandle;
const anchor = this.interaction.outputAreaTransformAnchor;
let newX = o.x;
let newY = o.y;
let newWidth = o.width;
let newHeight = o.height;
// Calculate new dimensions based on handle
if (handle?.includes('w')) {
const deltaX = worldCoords.x - anchor.x;
newWidth = Math.abs(deltaX);
newX = Math.min(worldCoords.x, anchor.x);
}
if (handle?.includes('e')) {
const deltaX = worldCoords.x - anchor.x;
newWidth = Math.abs(deltaX);
newX = Math.min(worldCoords.x, anchor.x);
}
if (handle?.includes('n')) {
const deltaY = worldCoords.y - anchor.y;
newHeight = Math.abs(deltaY);
newY = Math.min(worldCoords.y, anchor.y);
}
if (handle?.includes('s')) {
const deltaY = worldCoords.y - anchor.y;
newHeight = Math.abs(deltaY);
newY = Math.min(worldCoords.y, anchor.y);
}
// Maintain aspect ratio if shift is held
if (isShiftPressed && o.width > 0 && o.height > 0) {
const aspectRatio = o.width / o.height;
if (handle === 'n' || handle === 's') {
newWidth = newHeight * aspectRatio;
} else if (handle === 'e' || handle === 'w') {
newHeight = newWidth / aspectRatio;
} else {
// Corner handles
const proposedRatio = newWidth / newHeight;
if (proposedRatio > aspectRatio) {
newHeight = newWidth / aspectRatio;
} else {
newWidth = newHeight * aspectRatio;
}
}
}
// Snap to grid if Ctrl is held
if (this.interaction.isCtrlPressed) {
newX = snapToGrid(newX);
newY = snapToGrid(newY);
newWidth = snapToGrid(newWidth);
newHeight = snapToGrid(newHeight);
}
// Apply minimum size
if (newWidth < 10) newWidth = 10;
if (newHeight < 10) newHeight = 10;
// Update output area bounds temporarily for preview
this.canvas.outputAreaBounds = {
x: newX,
y: newY,
width: newWidth,
height: newHeight
};
this.canvas.render();
}
private updateOutputAreaTransformCursor(worldCoords: Point): void {
const handle = this.getOutputAreaHandle(worldCoords);
if (handle) {
const cursorMap: { [key: string]: string } = {
'n': 'ns-resize', 's': 'ns-resize',
'e': 'ew-resize', 'w': 'ew-resize',
'nw': 'nwse-resize', 'se': 'nwse-resize',
'ne': 'nesw-resize', 'sw': 'nesw-resize',
};
this.canvas.canvas.style.cursor = cursorMap[handle] || 'default';
} else {
this.canvas.canvas.style.cursor = 'default';
}
}
private finalizeOutputAreaTransform(): void {
const bounds = this.canvas.outputAreaBounds;
// Update canvas size and mask tool
this.canvas.updateOutputAreaSize(bounds.width, bounds.height);
// Update mask canvas for new output area
this.canvas.maskTool.updateMaskCanvasForOutputArea();
// Save state
this.canvas.saveState();
// Reset transform handle but keep transform mode active
this.interaction.outputAreaTransformHandle = null;
}
}

View File

@@ -195,6 +195,7 @@ export class CanvasRenderer {
this.renderInteractionElements(ctx);
this.canvas.shapeTool.render(ctx);
this.drawMaskAreaBounds(ctx); // Draw mask area bounds when mask tool is active
this.renderOutputAreaTransformHandles(ctx); // Draw output area transform handles
this.renderLayerInfo(ctx);
// Update custom shape menu position and visibility
@@ -1011,4 +1012,46 @@ export class CanvasRenderer {
// Just ensure it's the right size
this.updateOverlaySize();
}
/**
* Draw transform handles for output area when in transform mode
*/
renderOutputAreaTransformHandles(ctx: any): void {
if (this.canvas.canvasInteractions.interaction.mode !== 'transformingOutputArea') {
return;
}
const bounds = this.canvas.outputAreaBounds;
const handleRadius = 5 / this.canvas.viewport.zoom;
// Define handle positions
const handles = {
'nw': { x: bounds.x, y: bounds.y },
'n': { x: bounds.x + bounds.width / 2, y: bounds.y },
'ne': { x: bounds.x + bounds.width, y: bounds.y },
'e': { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 },
'se': { x: bounds.x + bounds.width, y: bounds.y + bounds.height },
's': { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height },
'sw': { x: bounds.x, y: bounds.y + bounds.height },
'w': { x: bounds.x, y: bounds.y + bounds.height / 2 },
};
// Draw handles
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1 / this.canvas.viewport.zoom;
for (const [name, pos] of Object.entries(handles)) {
ctx.beginPath();
ctx.arc(pos.x, pos.y, handleRadius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
// Draw a highlight around the output area
ctx.strokeStyle = 'rgba(0, 150, 255, 0.8)';
ctx.lineWidth = 3 / this.canvas.viewport.zoom;
ctx.setLineDash([]);
ctx.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height);
}
}

View File

@@ -289,88 +289,11 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
}),
$el("button.painter-button", {
textContent: "Output Area Size",
title: "Set the size of the output area",
title: "Transform output area - drag handles to resize",
onclick: () => {
const dialog = $el("div.painter-dialog", {
style: {
position: 'fixed',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
zIndex: '9999'
}
}, [
$el("div", {
style: {
color: "white",
marginBottom: "10px"
}
}, [
$el("label", {
style: {
marginRight: "5px"
}
}, [
$el("span", {}, ["Width: "])
]),
$el("input", {
type: "number",
id: "canvas-width",
value: String(canvas.width),
min: "1",
max: "4096"
})
]),
$el("div", {
style: {
color: "white",
marginBottom: "10px"
}
}, [
$el("label", {
style: {
marginRight: "5px"
}
}, [
$el("span", {}, ["Height: "])
]),
$el("input", {
type: "number",
id: "canvas-height",
value: String(canvas.height),
min: "1",
max: "4096"
})
]),
$el("div", {
style: {
textAlign: "right"
}
}, [
$el("button", {
id: "cancel-size",
textContent: "Cancel"
}),
$el("button", {
id: "confirm-size",
textContent: "OK"
})
])
]);
document.body.appendChild(dialog);
(document.getElementById('confirm-size') as HTMLButtonElement).onclick = () => {
const widthInput = document.getElementById('canvas-width') as HTMLInputElement;
const heightInput = document.getElementById('canvas-height') as HTMLInputElement;
const width = parseInt(widthInput.value) || canvas.width;
const height = parseInt(heightInput.value) || canvas.height;
canvas.setOutputAreaSize(width, height);
document.body.removeChild(dialog);
};
(document.getElementById('cancel-size') as HTMLButtonElement).onclick = () => {
document.body.removeChild(dialog);
};
// Activate output area transform mode
canvas.canvasInteractions.activateOutputAreaTransform();
showInfoNotification("Click and drag the handles to resize the output area. Click anywhere else to exit.", 3000);
}
}),
$el("button.painter-button.requires-selection", {