/** * Image crop transformations * Non-destructive rectangular cropping */ import Konva from 'konva'; export interface CropRegion { x: number; y: number; width: number; height: number; } /** * Apply crop to image */ export function cropImage(image: Konva.Image | Konva.Group, cropRegion: CropRegion): void { const imageNode = image instanceof Konva.Image ? image : image.findOne('Image'); if (!imageNode) return; const img = imageNode as Konva.Image; // Validate crop region const imageWidth = img.width(); const imageHeight = img.height(); const validCrop = { x: Math.max(0, Math.min(cropRegion.x, imageWidth)), y: Math.max(0, Math.min(cropRegion.y, imageHeight)), width: Math.max(1, Math.min(cropRegion.width, imageWidth - cropRegion.x)), height: Math.max(1, Math.min(cropRegion.height, imageHeight - cropRegion.y)), }; // Apply crop using Konva's crop property img.crop(validCrop); } /** * Remove crop (reset to full image) */ export function removeCrop(image: Konva.Image | Konva.Group): void { const imageNode = image instanceof Konva.Image ? image : image.findOne('Image'); if (!imageNode) return; (imageNode as Konva.Image).crop(undefined); } /** * Get current crop region */ export function getCropRegion(image: Konva.Image | Konva.Group): CropRegion | null { const imageNode = image instanceof Konva.Image ? image : image.findOne('Image'); if (!imageNode) return null; const crop = (imageNode as Konva.Image).crop(); if (!crop) return null; return { x: crop.x || 0, y: crop.y || 0, width: crop.width || 0, height: crop.height || 0, }; } /** * Check if image is cropped */ export function isCropped(image: Konva.Image | Konva.Group): boolean { const crop = getCropRegion(image); return crop !== null; } /** * Crop to square (centered) */ export function cropToSquare(image: Konva.Image | Konva.Group): void { const imageNode = image instanceof Konva.Image ? image : image.findOne('Image'); if (!imageNode) return; const img = imageNode as Konva.Image; const width = img.width(); const height = img.height(); const size = Math.min(width, height); const cropRegion: CropRegion = { x: (width - size) / 2, y: (height - size) / 2, width: size, height: size, }; cropImage(image, cropRegion); } /** * Create interactive crop tool (returns cleanup function) */ export function enableCropTool( image: Konva.Image | Konva.Group, layer: Konva.Layer, onCropComplete: (cropRegion: CropRegion) => void ): () => void { let cropRect: Konva.Rect | null = null; let isDragging = false; let startPos: { x: number; y: number } | null = null; function handleMouseDown(e: Konva.KonvaEventObject) { const pos = e.target.getStage()?.getPointerPosition(); if (!pos) return; isDragging = true; startPos = pos; cropRect = new Konva.Rect({ x: pos.x, y: pos.y, width: 0, height: 0, stroke: '#3b82f6', strokeWidth: 2, dash: [4, 2], listening: false, }); layer.add(cropRect); } function handleMouseMove(e: Konva.KonvaEventObject) { if (!isDragging || !startPos || !cropRect) return; const pos = e.target.getStage()?.getPointerPosition(); if (!pos) return; const width = pos.x - startPos.x; const height = pos.y - startPos.y; cropRect.width(width); cropRect.height(height); layer.batchDraw(); } function handleMouseUp() { if (!isDragging || !startPos || !cropRect) return; const cropRegion: CropRegion = { x: Math.min(startPos.x, cropRect.x() + cropRect.width()), y: Math.min(startPos.y, cropRect.y() + cropRect.height()), width: Math.abs(cropRect.width()), height: Math.abs(cropRect.height()), }; if (cropRegion.width > 10 && cropRegion.height > 10) { onCropComplete(cropRegion); } cropRect.destroy(); cropRect = null; isDragging = false; startPos = null; layer.batchDraw(); } image.on('mousedown', handleMouseDown); image.on('mousemove', handleMouseMove); image.on('mouseup', handleMouseUp); return () => { image.off('mousedown', handleMouseDown); image.off('mousemove', handleMouseMove); image.off('mouseup', handleMouseUp); if (cropRect) { cropRect.destroy(); layer.batchDraw(); } }; }