This commit is contained in:
Danilo Reyes
2025-11-02 14:13:56 -06:00
parent cd8ce33f5e
commit ce0b692aee
23 changed files with 2049 additions and 50 deletions

View File

@@ -0,0 +1,180 @@
/**
* 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<MouseEvent>) {
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<MouseEvent>) {
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();
}
};
}