181 lines
4.3 KiB
TypeScript
181 lines
4.3 KiB
TypeScript
/**
|
|
* 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();
|
|
}
|
|
};
|
|
}
|