387a1c26e3
清理已跟踪原始日志与个人本地配置 修复前后端有效门禁、测试和构建问题 补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束 收紧图片编辑器状态、附件与媒体引用测试 排除已下线旧创作入口测试并清理 warning
185 lines
4.5 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|