A web app to help you design things, local, offline, on device. In your browser.
TypeScript
53.1%
JSON
45.6%
CSS
0.9%
Markdown
0.3%
JavaScript
0.1%
import { useMemo } from 'react';
import type { Layer } from '@/types';
export interface AlignmentGuide {
type: 'vertical' | 'horizontal';
position: number;
start: number;
end: number;
label?: string;
}
export interface DistanceIndicator {
type: 'horizontal' | 'vertical';
start: { x: number; y: number };
end: { x: number; y: number };
distance: number;
}
export interface SnapResult {
x: number;
y: number;
guides: AlignmentGuide[];
distanceIndicators: DistanceIndicator[];
}
const SNAP_THRESHOLD = 8; // pixels within which snapping occurs
interface GuideTarget {
position: number;
type: 'edge' | 'center' | 'custom';
label?: string;
}
export interface CustomGuideInput {
type: 'horizontal' | 'vertical';
position: number;
}
export function calculateAlignmentGuides(
draggingLayer: Layer,
otherLayers: Layer[],
canvasWidth: number,
canvasHeight: number,
snapEnabled: boolean,
customGuides: CustomGuideInput[] = []
): SnapResult {
const guides: AlignmentGuide[] = [];
// Current layer bounds
const layerLeft = draggingLayer.position.x;
const layerRight = draggingLayer.position.x + draggingLayer.size.width;
const layerTop = draggingLayer.position.y;
const layerBottom = draggingLayer.position.y + draggingLayer.size.height;
const layerCenterX = draggingLayer.position.x + draggingLayer.size.width / 2;
const layerCenterY = draggingLayer.position.y + draggingLayer.size.height / 2;
let snappedX = draggingLayer.position.x;
let snappedY = draggingLayer.position.y;
// Collect all vertical snap targets (x positions)
const verticalTargets: GuideTarget[] = [
// Canvas edges
{ position: 0, type: 'edge', label: 'Left edge' },
{ position: canvasWidth, type: 'edge', label: 'Right edge' },
{ position: canvasWidth / 2, type: 'center', label: 'Canvas center' },
];
// Collect all horizontal snap targets (y positions)
const horizontalTargets: GuideTarget[] = [
// Canvas edges
{ position: 0, type: 'edge', label: 'Top edge' },
{ position: canvasHeight, type: 'edge', label: 'Bottom edge' },
{ position: canvasHeight / 2, type: 'center', label: 'Canvas center' },
];
// Add other layers' edges and centers
otherLayers.forEach((layer) => {
if (!layer.visible) return;
const left = layer.position.x;
const right = layer.position.x + layer.size.width;
const top = layer.position.y;
const bottom = layer.position.y + layer.size.height;
const centerX = layer.position.x + layer.size.width / 2;
const centerY = layer.position.y + layer.size.height / 2;
verticalTargets.push(
{ position: left, type: 'edge' },
{ position: right, type: 'edge' },
{ position: centerX, type: 'center' }
);
horizontalTargets.push(
{ position: top, type: 'edge' },
{ position: bottom, type: 'edge' },
{ position: centerY, type: 'center' }
);
});
// Add custom guides
customGuides.forEach((guide) => {
if (guide.type === 'vertical') {
verticalTargets.push({ position: guide.position, type: 'custom', label: 'Custom guide' });
} else {
horizontalTargets.push({ position: guide.position, type: 'custom', label: 'Custom guide' });
}
});
// Check vertical alignment (for X snapping)
const layerXPoints = [
{ point: layerLeft, offset: 0 },
{ point: layerCenterX, offset: draggingLayer.size.width / 2 },
{ point: layerRight, offset: draggingLayer.size.width },
];
let closestVerticalDistance = SNAP_THRESHOLD + 1;
let verticalSnapTarget: number | null = null;
let verticalSnapOffset = 0;
for (const { point, offset } of layerXPoints) {
for (const target of verticalTargets) {
const distance = Math.abs(point - target.position);
if (distance < closestVerticalDistance) {
closestVerticalDistance = distance;
verticalSnapTarget = target.position;
verticalSnapOffset = offset;
}
}
}
// Check horizontal alignment (for Y snapping)
const layerYPoints = [
{ point: layerTop, offset: 0 },
{ point: layerCenterY, offset: draggingLayer.size.height / 2 },
{ point: layerBottom, offset: draggingLayer.size.height },
];
let closestHorizontalDistance = SNAP_THRESHOLD + 1;
let horizontalSnapTarget: number | null = null;
let horizontalSnapOffset = 0;
for (const { point, offset } of layerYPoints) {
for (const target of horizontalTargets) {
const distance = Math.abs(point - target.position);
if (distance < closestHorizontalDistance) {
closestHorizontalDistance = distance;
horizontalSnapTarget = target.position;
horizontalSnapOffset = offset;
}
}
}
// Apply snapping if within threshold
if (snapEnabled && verticalSnapTarget !== null && closestVerticalDistance <= SNAP_THRESHOLD) {
snappedX = verticalSnapTarget - verticalSnapOffset;
// Add guide line
guides.push({
type: 'vertical',
position: verticalSnapTarget,
start: 0,
end: canvasHeight,
});
}
if (snapEnabled && horizontalSnapTarget !== null && closestHorizontalDistance <= SNAP_THRESHOLD) {
snappedY = horizontalSnapTarget - horizontalSnapOffset;
// Add guide line
guides.push({
type: 'horizontal',
position: horizontalSnapTarget,
start: 0,
end: canvasWidth,
});
}
// Even if not snapping, show guides when close (for visual feedback)
if (!snapEnabled) {
if (verticalSnapTarget !== null && closestVerticalDistance <= SNAP_THRESHOLD * 2) {
guides.push({
type: 'vertical',
position: verticalSnapTarget,
start: 0,
end: canvasHeight,
});
}
if (horizontalSnapTarget !== null && closestHorizontalDistance <= SNAP_THRESHOLD * 2) {
guides.push({
type: 'horizontal',
position: horizontalSnapTarget,
start: 0,
end: canvasWidth,
});
}
}
// Calculate distance indicators to nearby layers
const distanceIndicators: DistanceIndicator[] = [];
const DISTANCE_THRESHOLD = 200; // Show distances within 200px
// Use the snapped position for distance calculations
const finalLeft = snappedX;
const finalRight = snappedX + draggingLayer.size.width;
const finalTop = snappedY;
const finalBottom = snappedY + draggingLayer.size.height;
const finalCenterY = snappedY + draggingLayer.size.height / 2;
const finalCenterX = snappedX + draggingLayer.size.width / 2;
otherLayers.forEach((layer) => {
if (!layer.visible) return;
const otherLeft = layer.position.x;
const otherRight = layer.position.x + layer.size.width;
const otherTop = layer.position.y;
const otherBottom = layer.position.y + layer.size.height;
const otherCenterY = layer.position.y + layer.size.height / 2;
const otherCenterX = layer.position.x + layer.size.width / 2;
// Check if layers overlap vertically (for horizontal distance)
const verticalOverlap =
(finalTop < otherBottom && finalBottom > otherTop);
// Check if layers overlap horizontally (for vertical distance)
const horizontalOverlap =
(finalLeft < otherRight && finalRight > otherLeft);
if (verticalOverlap) {
// Distance from dragging layer's right to other layer's left
if (otherLeft > finalRight) {
const distance = otherLeft - finalRight;
if (distance < DISTANCE_THRESHOLD) {
const commonTop = Math.max(finalTop, otherTop);
const commonBottom = Math.min(finalBottom, otherBottom);
const midY = (commonTop + commonBottom) / 2;
distanceIndicators.push({
type: 'horizontal',
start: { x: finalRight, y: midY },
end: { x: otherLeft, y: midY },
distance: Math.round(distance),
});
}
}
// Distance from other layer's right to dragging layer's left
if (finalLeft > otherRight) {
const distance = finalLeft - otherRight;
if (distance < DISTANCE_THRESHOLD) {
const commonTop = Math.max(finalTop, otherTop);
const commonBottom = Math.min(finalBottom, otherBottom);
const midY = (commonTop + commonBottom) / 2;
distanceIndicators.push({
type: 'horizontal',
start: { x: otherRight, y: midY },
end: { x: finalLeft, y: midY },
distance: Math.round(distance),
});
}
}
}
if (horizontalOverlap) {
// Distance from dragging layer's bottom to other layer's top
if (otherTop > finalBottom) {
const distance = otherTop - finalBottom;
if (distance < DISTANCE_THRESHOLD) {
const commonLeft = Math.max(finalLeft, otherLeft);
const commonRight = Math.min(finalRight, otherRight);
const midX = (commonLeft + commonRight) / 2;
distanceIndicators.push({
type: 'vertical',
start: { x: midX, y: finalBottom },
end: { x: midX, y: otherTop },
distance: Math.round(distance),
});
}
}
// Distance from other layer's bottom to dragging layer's top
if (finalTop > otherBottom) {
const distance = finalTop - otherBottom;
if (distance < DISTANCE_THRESHOLD) {
const commonLeft = Math.max(finalLeft, otherLeft);
const commonRight = Math.min(finalRight, otherRight);
const midX = (commonLeft + commonRight) / 2;
distanceIndicators.push({
type: 'vertical',
start: { x: midX, y: otherBottom },
end: { x: midX, y: finalTop },
distance: Math.round(distance),
});
}
}
}
});
// Also show distance to canvas edges when near
const EDGE_THRESHOLD = 100;
// Distance to left edge
if (finalLeft < EDGE_THRESHOLD && finalLeft > 0) {
distanceIndicators.push({
type: 'horizontal',
start: { x: 0, y: finalCenterY },
end: { x: finalLeft, y: finalCenterY },
distance: Math.round(finalLeft),
});
}
// Distance to right edge
if (canvasWidth - finalRight < EDGE_THRESHOLD && finalRight < canvasWidth) {
distanceIndicators.push({
type: 'horizontal',
start: { x: finalRight, y: finalCenterY },
end: { x: canvasWidth, y: finalCenterY },
distance: Math.round(canvasWidth - finalRight),
});
}
// Distance to top edge
if (finalTop < EDGE_THRESHOLD && finalTop > 0) {
distanceIndicators.push({
type: 'vertical',
start: { x: finalCenterX, y: 0 },
end: { x: finalCenterX, y: finalTop },
distance: Math.round(finalTop),
});
}
// Distance to bottom edge
if (canvasHeight - finalBottom < EDGE_THRESHOLD && finalBottom < canvasHeight) {
distanceIndicators.push({
type: 'vertical',
start: { x: finalCenterX, y: finalBottom },
end: { x: finalCenterX, y: canvasHeight },
distance: Math.round(canvasHeight - finalBottom),
});
}
return {
x: snappedX,
y: snappedY,
guides,
distanceIndicators,
};
}
// Hook version for use in components
export function useAlignmentGuides(
draggingLayerId: string | null,
layers: Layer[],
canvasWidth: number,
canvasHeight: number,
snapEnabled: boolean
) {
return useMemo(() => {
if (!draggingLayerId) {
return { guides: [], snapPosition: null };
}
const draggingLayer = layers.find((l) => l.id === draggingLayerId);
if (!draggingLayer) {
return { guides: [], snapPosition: null };
}
const otherLayers = layers.filter((l) => l.id !== draggingLayerId);
const result = calculateAlignmentGuides(
draggingLayer,
otherLayers,
canvasWidth,
canvasHeight,
snapEnabled
);
return {
guides: result.guides,
snapPosition: { x: result.x, y: result.y },
};
}, [draggingLayerId, layers, canvasWidth, canvasHeight, snapEnabled]);
}
About
A web app to help you design things, local, offline, on device. In your browser.
0 stars
0 forks