import { BaseTool } from './base-tool'; import type { PointerState, ToolSettings } from '@/types'; import { distance } from '@/lib/utils'; /** * Clone Stamp Tool - Sample and paint from one location to another */ export class CloneStampTool extends BaseTool { private lastX = 0; private lastY = 0; private sourceX: number | null = null; private sourceY: number | null = null; private offsetX = 0; private offsetY = 0; private sourceCanvas: HTMLCanvasElement | null = null; constructor() { super('Clone Stamp'); } /** * Set the source point for cloning */ setSourcePoint(x: number, y: number, ctx: CanvasRenderingContext2D): void { this.sourceX = x; this.sourceY = y; // Create a canvas to store the source image data if (!this.sourceCanvas) { this.sourceCanvas = document.createElement('canvas'); } this.sourceCanvas.width = ctx.canvas.width; this.sourceCanvas.height = ctx.canvas.height; const sourceCtx = this.sourceCanvas.getContext('2d'); if (sourceCtx) { sourceCtx.drawImage(ctx.canvas, 0, 0); } } onPointerDown( pointer: PointerState, ctx: CanvasRenderingContext2D, settings: ToolSettings ): void { // Alt+Click to set source point if (pointer.altKey) { this.setSourcePoint(pointer.x, pointer.y, ctx); return; } // Can't clone without a source point if (this.sourceX === null || this.sourceY === null || !this.sourceCanvas) { return; } this.isDrawing = true; this.lastX = pointer.x; this.lastY = pointer.y; // Calculate offset between source and destination this.offsetX = pointer.x - this.sourceX; this.offsetY = pointer.y - this.sourceY; // Draw initial stamp this.drawStamp(pointer.x, pointer.y, ctx, settings); } onPointerMove( pointer: PointerState, ctx: CanvasRenderingContext2D, settings: ToolSettings ): void { if (!this.isDrawing) return; if (this.sourceX === null || this.sourceY === null || !this.sourceCanvas) return; // Calculate distance from last point const dist = distance(this.lastX, this.lastY, pointer.x, pointer.y); const spacing = settings.size * settings.spacing; if (dist >= spacing) { // Interpolate between points for smooth stroke const steps = Math.ceil(dist / spacing); for (let i = 1; i <= steps; i++) { const t = i / steps; const x = this.lastX + (pointer.x - this.lastX) * t; const y = this.lastY + (pointer.y - this.lastY) * t; this.drawStamp(x, y, ctx, settings); } this.lastX = pointer.x; this.lastY = pointer.y; } } onPointerUp( pointer: PointerState, ctx: CanvasRenderingContext2D, settings: ToolSettings ): void { this.isDrawing = false; } /** * Draw a single clone stamp */ private drawStamp( destX: number, destY: number, ctx: CanvasRenderingContext2D, settings: ToolSettings ): void { if (!this.sourceCanvas || this.sourceX === null || this.sourceY === null) return; const size = settings.size; const opacity = settings.opacity * settings.flow; const hardness = settings.hardness; // Calculate source position (moves with destination) const currentOffsetX = destX - this.sourceX; const currentOffsetY = destY - this.sourceY; const srcX = this.sourceX + currentOffsetX; const srcY = this.sourceY + currentOffsetY; // Create a temporary canvas for the stamp const stampCanvas = document.createElement('canvas'); stampCanvas.width = size; stampCanvas.height = size; const stampCtx = stampCanvas.getContext('2d'); if (!stampCtx) return; // Draw the source area to the stamp canvas stampCtx.drawImage( this.sourceCanvas, srcX - size / 2, srcY - size / 2, size, size, 0, 0, size, size ); // Apply soft brush effect if hardness < 1 if (hardness < 1) { // Create a mask for soft edges const maskCanvas = document.createElement('canvas'); maskCanvas.width = size; maskCanvas.height = size; const maskCtx = maskCanvas.getContext('2d'); if (maskCtx) { const gradient = maskCtx.createRadialGradient( size / 2, size / 2, 0, size / 2, size / 2, size / 2 ); gradient.addColorStop(0, 'rgba(0, 0, 0, 1)'); gradient.addColorStop(hardness, 'rgba(0, 0, 0, 1)'); gradient.addColorStop(1, 'rgba(0, 0, 0, 0)'); maskCtx.fillStyle = gradient; maskCtx.fillRect(0, 0, size, size); // Apply mask using composite operation stampCtx.globalCompositeOperation = 'destination-in'; stampCtx.drawImage(maskCanvas, 0, 0); stampCtx.globalCompositeOperation = 'source-over'; } } // Draw the stamp to the canvas ctx.save(); ctx.globalAlpha = opacity; ctx.drawImage(stampCanvas, destX - size / 2, destY - size / 2); ctx.restore(); } /** * Reset the source point when tool is deactivated */ onDeactivate(): void { super.onDeactivate(); this.sourceX = null; this.sourceY = null; this.sourceCanvas = null; } getCursor(settings: ToolSettings): string { if (this.sourceX === null || this.sourceY === null) { return 'crosshair'; // No source set yet } return 'crosshair'; // Has source, ready to clone } }