diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 290eb4c..7de76c9 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -41,6 +41,7 @@ export class CanvasInteractions { canvasMoveRect: null, outputAreaTransformHandle: null, outputAreaTransformAnchor: { x: 0, y: 0 }, + hoveringGrabIcon: false, }; this.originalLayerPositions = new Map(); } @@ -151,6 +152,29 @@ export class CanvasInteractions { } return false; } + /** + * Sprawdza czy punkt znajduje się w obszarze ikony "grab" (środek layera) + * Zwraca layer, jeśli kliknięto w ikonę grab + */ + getGrabIconAtPosition(worldX, worldY) { + // Rozmiar ikony grab w pikselach światowych + const grabIconRadius = 20 / this.canvas.viewport.zoom; + for (const layer of this.canvas.canvasSelection.selectedLayers) { + if (!layer.visible) + continue; + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + // Sprawdź czy punkt jest w obszarze ikony grab (okrąg wokół środka) + const dx = worldX - centerX; + const dy = worldY - centerY; + const distanceSquared = dx * dx + dy * dy; + const radiusSquared = grabIconRadius * grabIconRadius; + if (distanceSquared <= radiusSquared) { + return layer; + } + } + return null; + } resetInteractionState() { this.interaction.mode = 'none'; this.interaction.resizeHandle = null; @@ -227,6 +251,14 @@ export class CanvasInteractions { this.startLayerTransform(transformTarget.layer, transformTarget.handle, coords.world); return; } + // Check if clicking on grab icon of a selected layer + const grabIconLayer = this.getGrabIconAtPosition(coords.world.x, coords.world.y); + if (grabIconLayer) { + // Start dragging the selected layer(s) without changing selection + this.interaction.mode = 'potential-drag'; + this.interaction.dragStart = { ...coords.world }; + return; + } const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y); if (clickedLayerResult) { this.prepareForDrag(clickedLayerResult.layer, coords.world); @@ -282,6 +314,13 @@ export class CanvasInteractions { } break; default: + // Check if hovering over grab icon + const wasHovering = this.interaction.hoveringGrabIcon; + this.interaction.hoveringGrabIcon = this.getGrabIconAtPosition(coords.world.x, coords.world.y) !== null; + // Re-render if hover state changed to show/hide grab icon + if (wasHovering !== this.interaction.hoveringGrabIcon) { + this.canvas.render(); + } this.updateCursor(coords.world); // Update brush cursor on overlay if mask tool is active if (this.canvas.maskTool.isActive) { @@ -617,6 +656,11 @@ export class CanvasInteractions { this.canvas.canvas.style.cursor = 'grabbing'; return; } + // Check if hovering over grab icon + if (this.interaction.hoveringGrabIcon) { + this.canvas.canvas.style.cursor = 'grab'; + return; + } const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y); if (transformTarget) { const handleName = transformTarget.handle; diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js index 31c9f3e..6223d6d 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -141,6 +141,10 @@ export class CanvasRenderer { ctx.restore(); } }); + // Draw grab icons for selected layers when hovering + if (this.canvas.canvasInteractions.interaction.hoveringGrabIcon) { + this.drawGrabIcons(ctx); + } this.drawCanvasOutline(ctx); this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines @@ -833,6 +837,55 @@ export class CanvasRenderer { // Just ensure it's the right size this.updateOverlaySize(); } + /** + * Draw grab icons in the center of selected layers + */ + drawGrabIcons(ctx) { + const selectedLayers = this.canvas.canvasSelection.selectedLayers; + if (selectedLayers.length === 0) + return; + const iconRadius = 20 / this.canvas.viewport.zoom; + const innerRadius = 12 / this.canvas.viewport.zoom; + selectedLayers.forEach((layer) => { + if (!layer.visible) + return; + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + ctx.save(); + // Draw outer circle (background) + ctx.beginPath(); + ctx.arc(centerX, centerY, iconRadius, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(0, 150, 255, 0.7)'; + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'; + ctx.lineWidth = 2 / this.canvas.viewport.zoom; + ctx.stroke(); + // Draw hand/grab icon (simplified) + ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; + ctx.strokeStyle = 'rgba(255, 255, 255, 0.95)'; + ctx.lineWidth = 1.5 / this.canvas.viewport.zoom; + // Draw four dots representing grab points + const dotRadius = 2 / this.canvas.viewport.zoom; + const dotDistance = 6 / this.canvas.viewport.zoom; + // Top-left + ctx.beginPath(); + ctx.arc(centerX - dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + // Top-right + ctx.beginPath(); + ctx.arc(centerX + dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + // Bottom-left + ctx.beginPath(); + ctx.arc(centerX - dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + // Bottom-right + ctx.beginPath(); + ctx.arc(centerX + dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + }); + } /** * Draw transform handles for output area when in transform mode */ diff --git a/src/CanvasInteractions.ts b/src/CanvasInteractions.ts index 3cdeaae..0381d46 100644 --- a/src/CanvasInteractions.ts +++ b/src/CanvasInteractions.ts @@ -51,6 +51,7 @@ interface InteractionState { canvasMoveRect: { x: number, y: number, width: number, height: number } | null; outputAreaTransformHandle: string | null; outputAreaTransformAnchor: Point; + hoveringGrabIcon: boolean; } export class CanvasInteractions { @@ -98,6 +99,7 @@ export class CanvasInteractions { canvasMoveRect: null, outputAreaTransformHandle: null, outputAreaTransformAnchor: { x: 0, y: 0 }, + hoveringGrabIcon: false, }; this.originalLayerPositions = new Map(); } @@ -234,6 +236,33 @@ export class CanvasInteractions { return false; } + /** + * Sprawdza czy punkt znajduje się w obszarze ikony "grab" (środek layera) + * Zwraca layer, jeśli kliknięto w ikonę grab + */ + getGrabIconAtPosition(worldX: number, worldY: number): Layer | null { + // Rozmiar ikony grab w pikselach światowych + const grabIconRadius = 20 / this.canvas.viewport.zoom; + + for (const layer of this.canvas.canvasSelection.selectedLayers) { + if (!layer.visible) continue; + + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + + // Sprawdź czy punkt jest w obszarze ikony grab (okrąg wokół środka) + const dx = worldX - centerX; + const dy = worldY - centerY; + const distanceSquared = dx * dx + dy * dy; + const radiusSquared = grabIconRadius * grabIconRadius; + + if (distanceSquared <= radiusSquared) { + return layer; + } + } + return null; + } + resetInteractionState(): void { this.interaction.mode = 'none'; this.interaction.resizeHandle = null; @@ -320,6 +349,15 @@ export class CanvasInteractions { return; } + // Check if clicking on grab icon of a selected layer + const grabIconLayer = this.getGrabIconAtPosition(coords.world.x, coords.world.y); + if (grabIconLayer) { + // Start dragging the selected layer(s) without changing selection + this.interaction.mode = 'potential-drag'; + this.interaction.dragStart = {...coords.world}; + return; + } + const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y); if (clickedLayerResult) { this.prepareForDrag(clickedLayerResult.layer, coords.world); @@ -378,6 +416,15 @@ export class CanvasInteractions { } break; default: + // Check if hovering over grab icon + const wasHovering = this.interaction.hoveringGrabIcon; + this.interaction.hoveringGrabIcon = this.getGrabIconAtPosition(coords.world.x, coords.world.y) !== null; + + // Re-render if hover state changed to show/hide grab icon + if (wasHovering !== this.interaction.hoveringGrabIcon) { + this.canvas.render(); + } + this.updateCursor(coords.world); // Update brush cursor on overlay if mask tool is active if (this.canvas.maskTool.isActive) { @@ -738,6 +785,12 @@ export class CanvasInteractions { return; } + // Check if hovering over grab icon + if (this.interaction.hoveringGrabIcon) { + this.canvas.canvas.style.cursor = 'grab'; + return; + } + const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y); if (transformTarget) { diff --git a/src/CanvasRenderer.ts b/src/CanvasRenderer.ts index a4844ae..1ba81b7 100644 --- a/src/CanvasRenderer.ts +++ b/src/CanvasRenderer.ts @@ -188,6 +188,11 @@ export class CanvasRenderer { } }); + // Draw grab icons for selected layers when hovering + if (this.canvas.canvasInteractions.interaction.hoveringGrabIcon) { + this.drawGrabIcons(ctx); + } + this.drawCanvasOutline(ctx); this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines @@ -1013,6 +1018,66 @@ export class CanvasRenderer { this.updateOverlaySize(); } + /** + * Draw grab icons in the center of selected layers + */ + drawGrabIcons(ctx: any): void { + const selectedLayers = this.canvas.canvasSelection.selectedLayers; + if (selectedLayers.length === 0) return; + + const iconRadius = 20 / this.canvas.viewport.zoom; + const innerRadius = 12 / this.canvas.viewport.zoom; + + selectedLayers.forEach((layer: any) => { + if (!layer.visible) return; + + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + + ctx.save(); + + // Draw outer circle (background) + ctx.beginPath(); + ctx.arc(centerX, centerY, iconRadius, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(0, 150, 255, 0.7)'; + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'; + ctx.lineWidth = 2 / this.canvas.viewport.zoom; + ctx.stroke(); + + // Draw hand/grab icon (simplified) + ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; + ctx.strokeStyle = 'rgba(255, 255, 255, 0.95)'; + ctx.lineWidth = 1.5 / this.canvas.viewport.zoom; + + // Draw four dots representing grab points + const dotRadius = 2 / this.canvas.viewport.zoom; + const dotDistance = 6 / this.canvas.viewport.zoom; + + // Top-left + ctx.beginPath(); + ctx.arc(centerX - dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + + // Top-right + ctx.beginPath(); + ctx.arc(centerX + dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + + // Bottom-left + ctx.beginPath(); + ctx.arc(centerX - dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + + // Bottom-right + ctx.beginPath(); + ctx.arc(centerX + dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + + ctx.restore(); + }); + } + /** * Draw transform handles for output area when in transform mode */