Files
Genarrative/src/components/image-editor/ImageCanvasCropExpandModel.ts
T
kdletters 387a1c26e3 完成全量检查整改
清理已跟踪原始日志与个人本地配置
修复前后端有效门禁、测试和构建问题
补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束
收紧图片编辑器状态、附件与媒体引用测试
排除已下线旧创作入口测试并清理 warning
2026-07-17 16:56:46 +08:00

185 lines
4.5 KiB
TypeScript

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<ResizeCropExpandFrameOptions, 'ratio'> & {
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,
};
}
}