/** * Alignment operations for canvas images * Aligns multiple images relative to each other or to canvas */ import type Konva from 'konva'; export interface AlignOptions { onAlignComplete?: (imageIds: string[]) => void; } /** * Get bounding box of multiple images */ function getBounds( images: Map, imageIds: string[] ): { minX: number; minY: number; maxX: number; maxY: number; width: number; height: number; } | null { let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; imageIds.forEach((id) => { const image = images.get(id); if (!image) return; const box = image.getClientRect(); minX = Math.min(minX, box.x); minY = Math.min(minY, box.y); maxX = Math.max(maxX, box.x + box.width); maxY = Math.max(maxY, box.y + box.height); }); if (!isFinite(minX) || !isFinite(minY)) return null; return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY, }; } /** * Align images to top edge */ export function alignTop( images: Map, selectedIds: string[], options: AlignOptions = {} ): void { const bounds = getBounds(images, selectedIds); if (!bounds) return; selectedIds.forEach((id) => { const image = images.get(id); if (!image) return; const box = image.getClientRect(); const offsetY = bounds.minY - box.y; image.y(image.y() + offsetY); }); const firstImage = selectedIds.length > 0 ? images.get(selectedIds[0]) : null; if (firstImage) { firstImage.getLayer()?.batchDraw(); } if (options.onAlignComplete) { options.onAlignComplete(selectedIds); } } /** * Align images to bottom edge */ export function alignBottom( images: Map, selectedIds: string[], options: AlignOptions = {} ): void { const bounds = getBounds(images, selectedIds); if (!bounds) return; selectedIds.forEach((id) => { const image = images.get(id); if (!image) return; const box = image.getClientRect(); const offsetY = bounds.maxY - (box.y + box.height); image.y(image.y() + offsetY); }); const firstImage = selectedIds.length > 0 ? images.get(selectedIds[0]) : null; if (firstImage) { firstImage.getLayer()?.batchDraw(); } if (options.onAlignComplete) { options.onAlignComplete(selectedIds); } } /** * Align images to left edge */ export function alignLeft( images: Map, selectedIds: string[], options: AlignOptions = {} ): void { const bounds = getBounds(images, selectedIds); if (!bounds) return; selectedIds.forEach((id) => { const image = images.get(id); if (!image) return; const box = image.getClientRect(); const offsetX = bounds.minX - box.x; image.x(image.x() + offsetX); }); const firstImage = selectedIds.length > 0 ? images.get(selectedIds[0]) : null; if (firstImage) { firstImage.getLayer()?.batchDraw(); } if (options.onAlignComplete) { options.onAlignComplete(selectedIds); } } /** * Align images to right edge */ export function alignRight( images: Map, selectedIds: string[], options: AlignOptions = {} ): void { const bounds = getBounds(images, selectedIds); if (!bounds) return; selectedIds.forEach((id) => { const image = images.get(id); if (!image) return; const box = image.getClientRect(); const offsetX = bounds.maxX - (box.x + box.width); image.x(image.x() + offsetX); }); const firstImage = selectedIds.length > 0 ? images.get(selectedIds[0]) : null; if (firstImage) { firstImage.getLayer()?.batchDraw(); } if (options.onAlignComplete) { options.onAlignComplete(selectedIds); } } /** * Center images horizontally within their bounding box */ export function centerHorizontal( images: Map, selectedIds: string[], options: AlignOptions = {} ): void { const bounds = getBounds(images, selectedIds); if (!bounds) return; const centerX = bounds.minX + bounds.width / 2; selectedIds.forEach((id) => { const image = images.get(id); if (!image) return; const box = image.getClientRect(); const imageCenterX = box.x + box.width / 2; const offsetX = centerX - imageCenterX; image.x(image.x() + offsetX); }); const firstImage = selectedIds.length > 0 ? images.get(selectedIds[0]) : null; if (firstImage) { firstImage.getLayer()?.batchDraw(); } if (options.onAlignComplete) { options.onAlignComplete(selectedIds); } } /** * Center images vertically within their bounding box */ export function centerVertical( images: Map, selectedIds: string[], options: AlignOptions = {} ): void { const bounds = getBounds(images, selectedIds); if (!bounds) return; const centerY = bounds.minY + bounds.height / 2; selectedIds.forEach((id) => { const image = images.get(id); if (!image) return; const box = image.getClientRect(); const imageCenterY = box.y + box.height / 2; const offsetY = centerY - imageCenterY; image.y(image.y() + offsetY); }); const firstImage = selectedIds.length > 0 ? images.get(selectedIds[0]) : null; if (firstImage) { firstImage.getLayer()?.batchDraw(); } if (options.onAlignComplete) { options.onAlignComplete(selectedIds); } } /** * Center images both horizontally and vertically */ export function centerBoth( images: Map, selectedIds: string[], options: AlignOptions = {} ): void { centerHorizontal(images, selectedIds, options); centerVertical(images, selectedIds, options); }