fix: resolve crop tool visual feedback issue

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 <noreply@anthropic.com>
This commit is contained in:
2025-11-21 20:46:59 +01:00
parent 40f624f5b7
commit c3ce440d48

View File

@@ -11,41 +11,28 @@ export class CropTool extends BaseTool {
private activeHandle: string | null = null; private activeHandle: string | null = null;
private startX = 0; private startX = 0;
private startY = 0; private startY = 0;
private overlayCanvas: HTMLCanvasElement | null = null;
private handleSize = 8; private handleSize = 8;
private initialized = false;
constructor() { constructor() {
super('Crop'); 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( onPointerDown(
pointer: PointerState, pointer: PointerState,
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
settings: ToolSettings settings: ToolSettings
): void { ): void {
// Create overlay canvas if not exists // Initialize crop rect to full canvas on first use
if (!this.overlayCanvas) { if (!this.initialized) {
this.overlayCanvas = document.createElement('canvas');
this.overlayCanvas.width = ctx.canvas.width;
this.overlayCanvas.height = ctx.canvas.height;
this.cropRect = { this.cropRect = {
x: 0, x: 0,
y: 0, y: 0,
width: ctx.canvas.width, width: ctx.canvas.width,
height: ctx.canvas.height, height: ctx.canvas.height,
}; };
this.initialized = true;
this.drawCropOverlay(ctx);
} }
// Check if clicking on a resize handle // Check if clicking on a resize handle
@@ -236,30 +223,25 @@ export class CropTool extends BaseTool {
} }
private drawCropOverlay(ctx: CanvasRenderingContext2D): void { private drawCropOverlay(ctx: CanvasRenderingContext2D): void {
if (!this.overlayCanvas) return; // Save context state
ctx.save();
const overlayCtx = this.overlayCanvas.getContext('2d');
if (!overlayCtx) return;
// Clear overlay
overlayCtx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
// Draw darkened areas outside crop rect // Draw darkened areas outside crop rect
overlayCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
// Top // Top
overlayCtx.fillRect(0, 0, this.overlayCanvas.width, this.cropRect.y); ctx.fillRect(0, 0, ctx.canvas.width, this.cropRect.y);
// Bottom // Bottom
overlayCtx.fillRect( ctx.fillRect(
0, 0,
this.cropRect.y + this.cropRect.height, this.cropRect.y + this.cropRect.height,
this.overlayCanvas.width, ctx.canvas.width,
this.overlayCanvas.height - (this.cropRect.y + this.cropRect.height) ctx.canvas.height - (this.cropRect.y + this.cropRect.height)
); );
// Left // Left
overlayCtx.fillRect( ctx.fillRect(
0, 0,
this.cropRect.y, this.cropRect.y,
this.cropRect.x, this.cropRect.x,
@@ -267,17 +249,17 @@ export class CropTool extends BaseTool {
); );
// Right // Right
overlayCtx.fillRect( ctx.fillRect(
this.cropRect.x + this.cropRect.width, this.cropRect.x + this.cropRect.width,
this.cropRect.y, this.cropRect.y,
this.overlayCanvas.width - (this.cropRect.x + this.cropRect.width), ctx.canvas.width - (this.cropRect.x + this.cropRect.width),
this.cropRect.height this.cropRect.height
); );
// Draw crop rect border // Draw crop rect border
overlayCtx.strokeStyle = '#ffffff'; ctx.strokeStyle = '#ffffff';
overlayCtx.lineWidth = 2; ctx.lineWidth = 2;
overlayCtx.strokeRect( ctx.strokeRect(
this.cropRect.x, this.cropRect.x,
this.cropRect.y, this.cropRect.y,
this.cropRect.width, this.cropRect.width,
@@ -285,39 +267,39 @@ export class CropTool extends BaseTool {
); );
// Draw rule of thirds grid // Draw rule of thirds grid
overlayCtx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
overlayCtx.lineWidth = 1; ctx.lineWidth = 1;
// Vertical lines // Vertical lines
overlayCtx.beginPath(); ctx.beginPath();
overlayCtx.moveTo(this.cropRect.x + this.cropRect.width / 3, this.cropRect.y); ctx.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); ctx.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); ctx.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); ctx.lineTo(this.cropRect.x + (this.cropRect.width * 2) / 3, this.cropRect.y + this.cropRect.height);
overlayCtx.stroke(); ctx.stroke();
// Horizontal lines // Horizontal lines
overlayCtx.beginPath(); ctx.beginPath();
overlayCtx.moveTo(this.cropRect.x, this.cropRect.y + this.cropRect.height / 3); ctx.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); ctx.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); ctx.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); ctx.lineTo(this.cropRect.x + this.cropRect.width, this.cropRect.y + (this.cropRect.height * 2) / 3);
overlayCtx.stroke(); ctx.stroke();
// Draw resize handles // Draw resize handles
const handles = this.getHandlePositions(); const handles = this.getHandlePositions();
overlayCtx.fillStyle = '#ffffff'; ctx.fillStyle = '#ffffff';
overlayCtx.strokeStyle = '#000000'; ctx.strokeStyle = '#000000';
overlayCtx.lineWidth = 1; ctx.lineWidth = 1;
for (const pos of Object.values(handles)) { for (const pos of Object.values(handles)) {
overlayCtx.fillRect( ctx.fillRect(
pos.x - this.handleSize / 2, pos.x - this.handleSize / 2,
pos.y - this.handleSize / 2, pos.y - this.handleSize / 2,
this.handleSize, this.handleSize,
this.handleSize this.handleSize
); );
overlayCtx.strokeRect( ctx.strokeRect(
pos.x - this.handleSize / 2, pos.x - this.handleSize / 2,
pos.y - this.handleSize / 2, pos.y - this.handleSize / 2,
this.handleSize, this.handleSize,
@@ -325,11 +307,8 @@ export class CropTool extends BaseTool {
); );
} }
// Draw overlay on main canvas // Restore context state
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.restore();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.putImageData(imageData, 0, 0);
ctx.drawImage(this.overlayCanvas, 0, 0);
} }
getCursor(): string { getCursor(): string {
@@ -371,6 +350,6 @@ export class CropTool extends BaseTool {
width: ctx.canvas.width, width: ctx.canvas.width,
height: ctx.canvas.height, height: ctx.canvas.height,
}; };
this.overlayCanvas = null; this.initialized = true;
} }
} }