From c3ce440d48a190204044577743318c0f437464e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Fri, 21 Nov 2025 20:46:59 +0100 Subject: [PATCH] fix: resolve crop tool visual feedback issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplified the crop tool overlay rendering to fix the issue where the crop area was not visible during dragging. Changes: - Removed overlayCanvas property and complex image preservation logic - Added initialized flag for lazy initialization on first use - Rewrote drawCropOverlay to use ctx.save()/restore() pattern - Draw overlay elements directly on canvas context (darkened areas, border, rule of thirds grid, and resize handles) The crop tool now properly displays the crop rectangle, handles, and overlay during all interactions (defining, dragging, and resizing). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tools/crop-tool.ts | 101 ++++++++++++++++++--------------------------- 1 file changed, 40 insertions(+), 61 deletions(-) diff --git a/tools/crop-tool.ts b/tools/crop-tool.ts index 43e8a57..bc70676 100644 --- a/tools/crop-tool.ts +++ b/tools/crop-tool.ts @@ -11,41 +11,28 @@ export class CropTool extends BaseTool { private activeHandle: string | null = null; private startX = 0; private startY = 0; - private overlayCanvas: HTMLCanvasElement | null = null; private handleSize = 8; + private initialized = false; constructor() { super('Crop'); } - onActivate(): void { - // Initialize crop rect to full canvas size when activated - if (this.overlayCanvas) { - this.cropRect = { - x: 0, - y: 0, - width: this.overlayCanvas.width, - height: this.overlayCanvas.height, - }; - } - } - onPointerDown( pointer: PointerState, ctx: CanvasRenderingContext2D, settings: ToolSettings ): void { - // Create overlay canvas if not exists - if (!this.overlayCanvas) { - this.overlayCanvas = document.createElement('canvas'); - this.overlayCanvas.width = ctx.canvas.width; - this.overlayCanvas.height = ctx.canvas.height; + // Initialize crop rect to full canvas on first use + if (!this.initialized) { this.cropRect = { x: 0, y: 0, width: ctx.canvas.width, height: ctx.canvas.height, }; + this.initialized = true; + this.drawCropOverlay(ctx); } // Check if clicking on a resize handle @@ -236,30 +223,25 @@ export class CropTool extends BaseTool { } private drawCropOverlay(ctx: CanvasRenderingContext2D): void { - if (!this.overlayCanvas) return; - - const overlayCtx = this.overlayCanvas.getContext('2d'); - if (!overlayCtx) return; - - // Clear overlay - overlayCtx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height); + // Save context state + ctx.save(); // Draw darkened areas outside crop rect - overlayCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; // Top - overlayCtx.fillRect(0, 0, this.overlayCanvas.width, this.cropRect.y); + ctx.fillRect(0, 0, ctx.canvas.width, this.cropRect.y); // Bottom - overlayCtx.fillRect( + ctx.fillRect( 0, this.cropRect.y + this.cropRect.height, - this.overlayCanvas.width, - this.overlayCanvas.height - (this.cropRect.y + this.cropRect.height) + ctx.canvas.width, + ctx.canvas.height - (this.cropRect.y + this.cropRect.height) ); // Left - overlayCtx.fillRect( + ctx.fillRect( 0, this.cropRect.y, this.cropRect.x, @@ -267,17 +249,17 @@ export class CropTool extends BaseTool { ); // Right - overlayCtx.fillRect( + ctx.fillRect( this.cropRect.x + this.cropRect.width, this.cropRect.y, - this.overlayCanvas.width - (this.cropRect.x + this.cropRect.width), + ctx.canvas.width - (this.cropRect.x + this.cropRect.width), this.cropRect.height ); // Draw crop rect border - overlayCtx.strokeStyle = '#ffffff'; - overlayCtx.lineWidth = 2; - overlayCtx.strokeRect( + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + ctx.strokeRect( this.cropRect.x, this.cropRect.y, this.cropRect.width, @@ -285,39 +267,39 @@ export class CropTool extends BaseTool { ); // Draw rule of thirds grid - overlayCtx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; - overlayCtx.lineWidth = 1; + ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.lineWidth = 1; // Vertical lines - overlayCtx.beginPath(); - overlayCtx.moveTo(this.cropRect.x + this.cropRect.width / 3, this.cropRect.y); - overlayCtx.lineTo(this.cropRect.x + this.cropRect.width / 3, this.cropRect.y + this.cropRect.height); - overlayCtx.moveTo(this.cropRect.x + (this.cropRect.width * 2) / 3, this.cropRect.y); - overlayCtx.lineTo(this.cropRect.x + (this.cropRect.width * 2) / 3, this.cropRect.y + this.cropRect.height); - overlayCtx.stroke(); + ctx.beginPath(); + ctx.moveTo(this.cropRect.x + this.cropRect.width / 3, this.cropRect.y); + ctx.lineTo(this.cropRect.x + this.cropRect.width / 3, this.cropRect.y + this.cropRect.height); + ctx.moveTo(this.cropRect.x + (this.cropRect.width * 2) / 3, this.cropRect.y); + ctx.lineTo(this.cropRect.x + (this.cropRect.width * 2) / 3, this.cropRect.y + this.cropRect.height); + ctx.stroke(); // Horizontal lines - overlayCtx.beginPath(); - overlayCtx.moveTo(this.cropRect.x, this.cropRect.y + this.cropRect.height / 3); - overlayCtx.lineTo(this.cropRect.x + this.cropRect.width, this.cropRect.y + this.cropRect.height / 3); - overlayCtx.moveTo(this.cropRect.x, this.cropRect.y + (this.cropRect.height * 2) / 3); - overlayCtx.lineTo(this.cropRect.x + this.cropRect.width, this.cropRect.y + (this.cropRect.height * 2) / 3); - overlayCtx.stroke(); + ctx.beginPath(); + ctx.moveTo(this.cropRect.x, this.cropRect.y + this.cropRect.height / 3); + ctx.lineTo(this.cropRect.x + this.cropRect.width, this.cropRect.y + this.cropRect.height / 3); + ctx.moveTo(this.cropRect.x, this.cropRect.y + (this.cropRect.height * 2) / 3); + ctx.lineTo(this.cropRect.x + this.cropRect.width, this.cropRect.y + (this.cropRect.height * 2) / 3); + ctx.stroke(); // Draw resize handles const handles = this.getHandlePositions(); - overlayCtx.fillStyle = '#ffffff'; - overlayCtx.strokeStyle = '#000000'; - overlayCtx.lineWidth = 1; + ctx.fillStyle = '#ffffff'; + ctx.strokeStyle = '#000000'; + ctx.lineWidth = 1; for (const pos of Object.values(handles)) { - overlayCtx.fillRect( + ctx.fillRect( pos.x - this.handleSize / 2, pos.y - this.handleSize / 2, this.handleSize, this.handleSize ); - overlayCtx.strokeRect( + ctx.strokeRect( pos.x - this.handleSize / 2, pos.y - this.handleSize / 2, this.handleSize, @@ -325,11 +307,8 @@ export class CropTool extends BaseTool { ); } - // Draw overlay on main canvas - const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - ctx.putImageData(imageData, 0, 0); - ctx.drawImage(this.overlayCanvas, 0, 0); + // Restore context state + ctx.restore(); } getCursor(): string { @@ -371,6 +350,6 @@ export class CropTool extends BaseTool { width: ctx.canvas.width, height: ctx.canvas.height, }; - this.overlayCanvas = null; + this.initialized = true; } }