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 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;
}
}