Files
webref/frontend/src/lib/canvas/operations/align.ts
Danilo Reyes e5abcced74
All checks were successful
CI/CD Pipeline / VM Test - backend-integration (push) Successful in 8s
CI/CD Pipeline / VM Test - full-stack (push) Successful in 7s
CI/CD Pipeline / VM Test - performance (push) Successful in 7s
CI/CD Pipeline / VM Test - security (push) Successful in 7s
CI/CD Pipeline / Backend Linting (push) Successful in 2s
CI/CD Pipeline / Frontend Linting (push) Successful in 16s
CI/CD Pipeline / Nix Flake Check (push) Successful in 38s
CI/CD Pipeline / CI Summary (push) Successful in 0s
phase 12
2025-11-02 14:34:55 -06:00

257 lines
5.7 KiB
TypeScript

/**
* 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<string, Konva.Image | Konva.Group>,
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<string, Konva.Image | Konva.Group>,
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<string, Konva.Image | Konva.Group>,
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<string, Konva.Image | Konva.Group>,
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<string, Konva.Image | Konva.Group>,
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<string, Konva.Image | Konva.Group>,
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<string, Konva.Image | Konva.Group>,
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<string, Konva.Image | Konva.Group>,
selectedIds: string[],
options: AlignOptions = {}
): void {
centerHorizontal(images, selectedIds, options);
centerVertical(images, selectedIds, options);
}