import { BaseTool } from './base-tool'; import type { PointerState, ToolSettings } from '@/types'; import { distance } from '@/lib/utils'; /** * Dodge/Burn Tool - Lighten or darken specific areas * Hold Alt to switch between Dodge (lighten) and Burn (darken) */ export class DodgeBurnTool extends BaseTool { private lastX = 0; private lastY = 0; private mode: 'dodge' | 'burn' = 'dodge'; constructor() { super('Dodge/Burn'); } onPointerDown( pointer: PointerState, ctx: CanvasRenderingContext2D, settings: ToolSettings ): void { this.isDrawing = true; this.lastX = pointer.x; this.lastY = pointer.y; // Alt key switches to burn mode this.mode = pointer.altKey ? 'burn' : 'dodge'; // Apply initial effect this.applyEffect(pointer.x, pointer.y, ctx, settings); } onPointerMove( pointer: PointerState, ctx: CanvasRenderingContext2D, settings: ToolSettings ): void { if (!this.isDrawing) return; // Update mode based on Alt key this.mode = pointer.altKey ? 'burn' : 'dodge'; // 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.applyEffect(x, y, ctx, settings); } this.lastX = pointer.x; this.lastY = pointer.y; } } onPointerUp( pointer: PointerState, ctx: CanvasRenderingContext2D, settings: ToolSettings ): void { this.isDrawing = false; } /** * Apply dodge or burn effect */ private applyEffect( x: number, y: number, ctx: CanvasRenderingContext2D, settings: ToolSettings ): void { const size = Math.ceil(settings.size); const halfSize = Math.floor(size / 2); // Clamp to canvas bounds const sx = Math.max(0, Math.floor(x - halfSize)); const sy = Math.max(0, Math.floor(y - halfSize)); const sw = Math.min(size, ctx.canvas.width - sx); const sh = Math.min(size, ctx.canvas.height - sy); if (sw <= 0 || sh <= 0) return; // Get image data const imageData = ctx.getImageData(sx, sy, sw, sh); const data = imageData.data; // Calculate effect parameters const strength = settings.flow; // Use flow as effect strength const hardness = settings.hardness; const centerX = sw / 2; const centerY = sh / 2; const maxRadius = Math.min(centerX, centerY); // Apply effect to each pixel for (let py = 0; py < sh; py++) { for (let px = 0; px < sw; px++) { const i = (py * sw + px) * 4; // Calculate distance from center for soft brush effect const dx = px - centerX; const dy = py - centerY; const distFromCenter = Math.sqrt(dx * dx + dy * dy); const normalizedDist = Math.min(distFromCenter / maxRadius, 1); // Apply hardness to create soft/hard brush let falloff = 1; if (normalizedDist > hardness) { falloff = 1 - (normalizedDist - hardness) / (1 - hardness); falloff = Math.max(0, Math.min(1, falloff)); } const effectiveStrength = strength * falloff; // Get current RGB values const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; if (this.mode === 'dodge') { // Dodge: Lighten the image // Formula: result = color + (255 - color) * strength data[i] = Math.min(255, r + (255 - r) * effectiveStrength); data[i + 1] = Math.min(255, g + (255 - g) * effectiveStrength); data[i + 2] = Math.min(255, b + (255 - b) * effectiveStrength); } else { // Burn: Darken the image // Formula: result = color - color * strength data[i] = Math.max(0, r - r * effectiveStrength); data[i + 1] = Math.max(0, g - g * effectiveStrength); data[i + 2] = Math.max(0, b - b * effectiveStrength); } // Alpha channel remains unchanged } } // Put modified image data back ctx.putImageData(imageData, sx, sy); } getCursor(settings: ToolSettings): string { return 'crosshair'; } /** * Get current mode for UI display */ getMode(): 'dodge' | 'burn' { return this.mode; } }