import type { CropExpandPanelState, CropExpandRatioValue, CropExpandResizeHandle, } from './ImageCanvasEditorTypes'; export const CROP_EXPAND_MIN_FRAME_SIZE = 16; type CropExpandFrame = CropExpandPanelState['frame']; type ResizeCropExpandFrameOptions = { frame: CropExpandFrame; handle: CropExpandResizeHandle; deltaX: number; deltaY: number; ratio: CropExpandRatioValue; minSize?: number; }; export function resolveCropExpandRatioNumber( ratio: CropExpandRatioValue, ): number | null { if (ratio === 'free') { return null; } const [widthValue, heightValue] = ratio.split(':').map(Number); if (!widthValue || !heightValue) { return null; } return widthValue / heightValue; } function normalizeFreeFrame(frame: CropExpandFrame, minSize: number) { const right = frame.x + frame.width; const bottom = frame.y + frame.height; return { x: Math.min(frame.x, right - minSize), y: Math.min(frame.y, bottom - minSize), width: Math.max(minSize, frame.width), height: Math.max(minSize, frame.height), }; } function buildFreeResizeFrame({ frame, handle, deltaX, deltaY, minSize, }: Omit & { minSize: number; }): CropExpandFrame { const nextFrame = { ...frame }; const right = frame.x + frame.width; const bottom = frame.y + frame.height; if (handle.includes('left')) { nextFrame.x = Math.min(frame.x + deltaX, right - minSize); nextFrame.width = right - nextFrame.x; } if (handle.includes('right')) { nextFrame.width = Math.max(minSize, frame.width + deltaX); } if (handle.includes('top')) { nextFrame.y = Math.min(frame.y + deltaY, bottom - minSize); nextFrame.height = bottom - nextFrame.y; } if (handle.includes('bottom')) { nextFrame.height = Math.max(minSize, frame.height + deltaY); } return normalizeFreeFrame(nextFrame, minSize); } function resolveFixedDimensions({ candidate, ratio, minSize, }: { candidate: CropExpandFrame; ratio: number; minSize: number; }) { const widthFromHeight = Math.max(minSize, candidate.height * ratio); const heightFromWidth = Math.max(minSize, candidate.width / ratio); const optionFromWidth = { width: Math.max(minSize, candidate.width), height: heightFromWidth, }; const optionFromHeight = { width: widthFromHeight, height: Math.max(minSize, candidate.height), }; const widthDistance = Math.abs(optionFromWidth.width - candidate.width) + Math.abs(optionFromWidth.height - candidate.height); const heightDistance = Math.abs(optionFromHeight.width - candidate.width) + Math.abs(optionFromHeight.height - candidate.height); return widthDistance <= heightDistance ? optionFromWidth : optionFromHeight; } export function resizeCropExpandFrame({ frame, handle, deltaX, deltaY, ratio, minSize = CROP_EXPAND_MIN_FRAME_SIZE, }: ResizeCropExpandFrameOptions): CropExpandFrame { const freeFrame = buildFreeResizeFrame({ frame, handle, deltaX, deltaY, minSize, }); const ratioNumber = resolveCropExpandRatioNumber(ratio); if (!ratioNumber) { return freeFrame; } const sourceRight = frame.x + frame.width; const sourceBottom = frame.y + frame.height; const sourceCenterX = frame.x + frame.width / 2; const sourceCenterY = frame.y + frame.height / 2; const dimensions = resolveFixedDimensions({ candidate: freeFrame, ratio: ratioNumber, minSize, }); switch (handle) { case 'left': return { x: sourceRight - dimensions.width, y: sourceCenterY - dimensions.height / 2, ...dimensions, }; case 'right': return { x: frame.x, y: sourceCenterY - dimensions.height / 2, ...dimensions, }; case 'top': return { x: sourceCenterX - dimensions.width / 2, y: sourceBottom - dimensions.height, ...dimensions, }; case 'bottom': return { x: sourceCenterX - dimensions.width / 2, y: frame.y, ...dimensions, }; case 'top-left': return { x: sourceRight - dimensions.width, y: sourceBottom - dimensions.height, ...dimensions, }; case 'top-right': return { x: frame.x, y: sourceBottom - dimensions.height, ...dimensions, }; case 'bottom-left': return { x: sourceRight - dimensions.width, y: frame.y, ...dimensions, }; case 'bottom-right': default: return { x: frame.x, y: frame.y, ...dimensions, }; } }