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
257 lines
5.7 KiB
TypeScript
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);
|
|
}
|