phase 8
This commit is contained in:
180
frontend/src/lib/canvas/transforms/crop.ts
Normal file
180
frontend/src/lib/canvas/transforms/crop.ts
Normal 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user