diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 250619e..290eb4c 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -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; + } } diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js index 38938e4..31c9f3e 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -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); + } } diff --git a/js/CanvasView.js b/js/CanvasView.js index 92171e9..bdc0f65 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -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", { diff --git a/src/CanvasInteractions.ts b/src/CanvasInteractions.ts index de92bbc..3cdeaae 100644 --- a/src/CanvasInteractions.ts +++ b/src/CanvasInteractions.ts @@ -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; + } + } diff --git a/src/CanvasRenderer.ts b/src/CanvasRenderer.ts index 0c6a368..a4844ae 100644 --- a/src/CanvasRenderer.ts +++ b/src/CanvasRenderer.ts @@ -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); + } } diff --git a/src/CanvasView.ts b/src/CanvasView.ts index c3f79fa..955f4cc 100644 --- a/src/CanvasView.ts +++ b/src/CanvasView.ts @@ -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", {