347 lines
7.8 KiB
TypeScript
347 lines
7.8 KiB
TypeScript
|
|
import type {
|
||
|
|
TransformMatrix,
|
||
|
|
TransformState,
|
||
|
|
TransformBounds,
|
||
|
|
TransformHandle,
|
||
|
|
} from '@/types/transform';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create an identity transform matrix
|
||
|
|
*/
|
||
|
|
export function createIdentityMatrix(): TransformMatrix {
|
||
|
|
return {
|
||
|
|
a: 1,
|
||
|
|
b: 0,
|
||
|
|
c: 0,
|
||
|
|
d: 1,
|
||
|
|
e: 0,
|
||
|
|
f: 0,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a translation matrix
|
||
|
|
*/
|
||
|
|
export function createTranslationMatrix(x: number, y: number): TransformMatrix {
|
||
|
|
return {
|
||
|
|
a: 1,
|
||
|
|
b: 0,
|
||
|
|
c: 0,
|
||
|
|
d: 1,
|
||
|
|
e: x,
|
||
|
|
f: y,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a scale matrix
|
||
|
|
*/
|
||
|
|
export function createScaleMatrix(
|
||
|
|
scaleX: number,
|
||
|
|
scaleY: number
|
||
|
|
): TransformMatrix {
|
||
|
|
return {
|
||
|
|
a: scaleX,
|
||
|
|
b: 0,
|
||
|
|
c: 0,
|
||
|
|
d: scaleY,
|
||
|
|
e: 0,
|
||
|
|
f: 0,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a rotation matrix
|
||
|
|
*/
|
||
|
|
export function createRotationMatrix(degrees: number): TransformMatrix {
|
||
|
|
const radians = (degrees * Math.PI) / 180;
|
||
|
|
const cos = Math.cos(radians);
|
||
|
|
const sin = Math.sin(radians);
|
||
|
|
|
||
|
|
return {
|
||
|
|
a: cos,
|
||
|
|
b: sin,
|
||
|
|
c: -sin,
|
||
|
|
d: cos,
|
||
|
|
e: 0,
|
||
|
|
f: 0,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Multiply two transform matrices
|
||
|
|
*/
|
||
|
|
export function multiplyMatrices(
|
||
|
|
m1: TransformMatrix,
|
||
|
|
m2: TransformMatrix
|
||
|
|
): TransformMatrix {
|
||
|
|
return {
|
||
|
|
a: m1.a * m2.a + m1.c * m2.b,
|
||
|
|
b: m1.b * m2.a + m1.d * m2.b,
|
||
|
|
c: m1.a * m2.c + m1.c * m2.d,
|
||
|
|
d: m1.b * m2.c + m1.d * m2.d,
|
||
|
|
e: m1.a * m2.e + m1.c * m2.f + m1.e,
|
||
|
|
f: m1.b * m2.e + m1.d * m2.f + m1.f,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a transform matrix from transform state
|
||
|
|
*/
|
||
|
|
export function createTransformMatrix(
|
||
|
|
state: TransformState,
|
||
|
|
bounds: TransformBounds
|
||
|
|
): TransformMatrix {
|
||
|
|
const centerX = bounds.x + bounds.width / 2;
|
||
|
|
const centerY = bounds.y + bounds.height / 2;
|
||
|
|
|
||
|
|
// Translate to origin
|
||
|
|
let matrix = createTranslationMatrix(-centerX, -centerY);
|
||
|
|
|
||
|
|
// Apply scale
|
||
|
|
matrix = multiplyMatrices(
|
||
|
|
matrix,
|
||
|
|
createScaleMatrix(state.scaleX, state.scaleY)
|
||
|
|
);
|
||
|
|
|
||
|
|
// Apply rotation
|
||
|
|
if (state.rotation !== 0) {
|
||
|
|
matrix = multiplyMatrices(matrix, createRotationMatrix(state.rotation));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Translate back and apply position offset
|
||
|
|
matrix = multiplyMatrices(
|
||
|
|
matrix,
|
||
|
|
createTranslationMatrix(centerX + state.x, centerY + state.y)
|
||
|
|
);
|
||
|
|
|
||
|
|
return matrix;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Apply transform matrix to a point
|
||
|
|
*/
|
||
|
|
export function transformPoint(
|
||
|
|
x: number,
|
||
|
|
y: number,
|
||
|
|
matrix: TransformMatrix
|
||
|
|
): { x: number; y: number } {
|
||
|
|
return {
|
||
|
|
x: matrix.a * x + matrix.c * y + matrix.e,
|
||
|
|
y: matrix.b * x + matrix.d * y + matrix.f,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Calculate transformed bounds
|
||
|
|
*/
|
||
|
|
export function getTransformedBounds(
|
||
|
|
bounds: TransformBounds,
|
||
|
|
state: TransformState
|
||
|
|
): TransformBounds {
|
||
|
|
const matrix = createTransformMatrix(state, bounds);
|
||
|
|
|
||
|
|
const corners = [
|
||
|
|
{ x: bounds.x, y: bounds.y },
|
||
|
|
{ x: bounds.x + bounds.width, y: bounds.y },
|
||
|
|
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height },
|
||
|
|
{ x: bounds.x, y: bounds.y + bounds.height },
|
||
|
|
];
|
||
|
|
|
||
|
|
const transformedCorners = corners.map((corner) =>
|
||
|
|
transformPoint(corner.x, corner.y, matrix)
|
||
|
|
);
|
||
|
|
|
||
|
|
const xs = transformedCorners.map((c) => c.x);
|
||
|
|
const ys = transformedCorners.map((c) => c.y);
|
||
|
|
|
||
|
|
const minX = Math.min(...xs);
|
||
|
|
const maxX = Math.max(...xs);
|
||
|
|
const minY = Math.min(...ys);
|
||
|
|
const maxY = Math.max(...ys);
|
||
|
|
|
||
|
|
return {
|
||
|
|
x: minX,
|
||
|
|
y: minY,
|
||
|
|
width: maxX - minX,
|
||
|
|
height: maxY - minY,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get handle position for transform bounds
|
||
|
|
*/
|
||
|
|
export function getHandlePosition(
|
||
|
|
handle: TransformHandle,
|
||
|
|
bounds: TransformBounds,
|
||
|
|
state: TransformState
|
||
|
|
): { x: number; y: number } {
|
||
|
|
const matrix = createTransformMatrix(state, bounds);
|
||
|
|
|
||
|
|
const handleMap: Record<TransformHandle, { x: number; y: number }> = {
|
||
|
|
'top-left': { x: bounds.x, y: bounds.y },
|
||
|
|
'top-center': { x: bounds.x + bounds.width / 2, y: bounds.y },
|
||
|
|
'top-right': { x: bounds.x + bounds.width, y: bounds.y },
|
||
|
|
'middle-left': { x: bounds.x, y: bounds.y + bounds.height / 2 },
|
||
|
|
'middle-right': {
|
||
|
|
x: bounds.x + bounds.width,
|
||
|
|
y: bounds.y + bounds.height / 2,
|
||
|
|
},
|
||
|
|
'bottom-left': { x: bounds.x, y: bounds.y + bounds.height },
|
||
|
|
'bottom-center': { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height },
|
||
|
|
'bottom-right': { x: bounds.x + bounds.width, y: bounds.y + bounds.height },
|
||
|
|
rotate: {
|
||
|
|
x: bounds.x + bounds.width / 2,
|
||
|
|
y: bounds.y - 30,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const point = handleMap[handle];
|
||
|
|
return transformPoint(point.x, point.y, matrix);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if a point is near a handle
|
||
|
|
*/
|
||
|
|
export function isNearHandle(
|
||
|
|
x: number,
|
||
|
|
y: number,
|
||
|
|
handle: TransformHandle,
|
||
|
|
bounds: TransformBounds,
|
||
|
|
state: TransformState,
|
||
|
|
threshold: number = 10
|
||
|
|
): boolean {
|
||
|
|
const handlePos = getHandlePosition(handle, bounds, state);
|
||
|
|
const dx = x - handlePos.x;
|
||
|
|
const dy = y - handlePos.y;
|
||
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||
|
|
return distance <= threshold;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get cursor for transform handle
|
||
|
|
*/
|
||
|
|
export function getHandleCursor(handle: TransformHandle, rotation: number): string {
|
||
|
|
const cursors: Record<TransformHandle, string> = {
|
||
|
|
'top-left': 'nwse-resize',
|
||
|
|
'top-center': 'ns-resize',
|
||
|
|
'top-right': 'nesw-resize',
|
||
|
|
'middle-left': 'ew-resize',
|
||
|
|
'middle-right': 'ew-resize',
|
||
|
|
'bottom-left': 'nesw-resize',
|
||
|
|
'bottom-center': 'ns-resize',
|
||
|
|
'bottom-right': 'nwse-resize',
|
||
|
|
rotate: 'crosshair',
|
||
|
|
};
|
||
|
|
|
||
|
|
// TODO: Adjust cursor based on rotation
|
||
|
|
return cursors[handle];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Calculate scale from handle drag
|
||
|
|
*/
|
||
|
|
export function calculateScaleFromHandle(
|
||
|
|
handle: TransformHandle,
|
||
|
|
dragStartX: number,
|
||
|
|
dragStartY: number,
|
||
|
|
dragCurrentX: number,
|
||
|
|
dragCurrentY: number,
|
||
|
|
bounds: TransformBounds,
|
||
|
|
maintainAspectRatio: boolean
|
||
|
|
): { scaleX: number; scaleY: number } {
|
||
|
|
const dx = dragCurrentX - dragStartX;
|
||
|
|
const dy = dragCurrentY - dragStartY;
|
||
|
|
|
||
|
|
let scaleX = 1;
|
||
|
|
let scaleY = 1;
|
||
|
|
|
||
|
|
switch (handle) {
|
||
|
|
case 'top-left':
|
||
|
|
scaleX = 1 - dx / bounds.width;
|
||
|
|
scaleY = 1 - dy / bounds.height;
|
||
|
|
break;
|
||
|
|
case 'top-center':
|
||
|
|
scaleY = 1 - dy / bounds.height;
|
||
|
|
break;
|
||
|
|
case 'top-right':
|
||
|
|
scaleX = 1 + dx / bounds.width;
|
||
|
|
scaleY = 1 - dy / bounds.height;
|
||
|
|
break;
|
||
|
|
case 'middle-left':
|
||
|
|
scaleX = 1 - dx / bounds.width;
|
||
|
|
break;
|
||
|
|
case 'middle-right':
|
||
|
|
scaleX = 1 + dx / bounds.width;
|
||
|
|
break;
|
||
|
|
case 'bottom-left':
|
||
|
|
scaleX = 1 - dx / bounds.width;
|
||
|
|
scaleY = 1 + dy / bounds.height;
|
||
|
|
break;
|
||
|
|
case 'bottom-center':
|
||
|
|
scaleY = 1 + dy / bounds.height;
|
||
|
|
break;
|
||
|
|
case 'bottom-right':
|
||
|
|
scaleX = 1 + dx / bounds.width;
|
||
|
|
scaleY = 1 + dy / bounds.height;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (maintainAspectRatio) {
|
||
|
|
const scale = Math.max(Math.abs(scaleX), Math.abs(scaleY));
|
||
|
|
scaleX = scaleX < 0 ? -scale : scale;
|
||
|
|
scaleY = scaleY < 0 ? -scale : scale;
|
||
|
|
}
|
||
|
|
|
||
|
|
return { scaleX, scaleY };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Calculate rotation from handle drag
|
||
|
|
*/
|
||
|
|
export function calculateRotationFromHandle(
|
||
|
|
centerX: number,
|
||
|
|
centerY: number,
|
||
|
|
currentX: number,
|
||
|
|
currentY: number
|
||
|
|
): number {
|
||
|
|
const dx = currentX - centerX;
|
||
|
|
const dy = currentY - centerY;
|
||
|
|
const radians = Math.atan2(dy, dx);
|
||
|
|
return (radians * 180) / Math.PI + 90;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Apply transform to canvas
|
||
|
|
*/
|
||
|
|
export function applyTransformToCanvas(
|
||
|
|
sourceCanvas: HTMLCanvasElement,
|
||
|
|
state: TransformState,
|
||
|
|
bounds: TransformBounds
|
||
|
|
): HTMLCanvasElement {
|
||
|
|
const transformedBounds = getTransformedBounds(bounds, state);
|
||
|
|
|
||
|
|
const canvas = document.createElement('canvas');
|
||
|
|
canvas.width = Math.ceil(transformedBounds.width);
|
||
|
|
canvas.height = Math.ceil(transformedBounds.height);
|
||
|
|
|
||
|
|
const ctx = canvas.getContext('2d');
|
||
|
|
if (!ctx) return canvas;
|
||
|
|
|
||
|
|
ctx.save();
|
||
|
|
|
||
|
|
// Translate to account for new bounds
|
||
|
|
ctx.translate(-transformedBounds.x, -transformedBounds.y);
|
||
|
|
|
||
|
|
// Apply transform matrix
|
||
|
|
const matrix = createTransformMatrix(state, bounds);
|
||
|
|
ctx.transform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f);
|
||
|
|
|
||
|
|
// Draw source canvas
|
||
|
|
ctx.drawImage(sourceCanvas, 0, 0);
|
||
|
|
|
||
|
|
ctx.restore();
|
||
|
|
|
||
|
|
return canvas;
|
||
|
|
}
|