330 lines
8.0 KiB
TypeScript
330 lines
8.0 KiB
TypeScript
import type { NormalizedCropRect } from "@/services/canvas";
|
|
|
|
export type CropPanelGroupKey =
|
|
| "common"
|
|
| "instagram"
|
|
| "facebook"
|
|
| "tiktok"
|
|
| "youtube";
|
|
|
|
export type CropInteractionHandle =
|
|
| "move"
|
|
| "n"
|
|
| "s"
|
|
| "e"
|
|
| "w"
|
|
| "nw"
|
|
| "ne"
|
|
| "sw"
|
|
| "se";
|
|
|
|
export type CropPresetOption = {
|
|
id: string;
|
|
label: string;
|
|
ratio: number;
|
|
};
|
|
|
|
export type CropPresetGroup = {
|
|
key: CropPanelGroupKey;
|
|
label: string;
|
|
options: CropPresetOption[];
|
|
};
|
|
|
|
export interface BoxCutoutPoint {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export const DEFAULT_CROP_PRESET_ID = "1:1";
|
|
|
|
export const CROP_PRESET_GROUPS: CropPresetGroup[] = [
|
|
{
|
|
key: "common",
|
|
label: "通用",
|
|
options: [
|
|
{ id: "1:1", label: "1:1", ratio: 1 },
|
|
{ id: "3:4", label: "3:4", ratio: 3 / 4 },
|
|
{ id: "2:3", label: "2:3", ratio: 2 / 3 },
|
|
{ id: "9:16", label: "9:16", ratio: 9 / 16 },
|
|
{ id: "4:3", label: "4:3", ratio: 4 / 3 },
|
|
{ id: "3:2", label: "3:2", ratio: 3 / 2 },
|
|
{ id: "16:9", label: "16:9", ratio: 16 / 9 },
|
|
],
|
|
},
|
|
{
|
|
key: "instagram",
|
|
label: "Instagram",
|
|
options: [
|
|
{ id: "ig-square", label: "1:1", ratio: 1 },
|
|
{ id: "ig-portrait", label: "4:5", ratio: 4 / 5 },
|
|
{ id: "ig-story", label: "9:16", ratio: 9 / 16 },
|
|
],
|
|
},
|
|
{
|
|
key: "facebook",
|
|
label: "Facebook",
|
|
options: [
|
|
{ id: "fb-feed", label: "4:5", ratio: 4 / 5 },
|
|
{ id: "fb-cover", label: "16:9", ratio: 16 / 9 },
|
|
],
|
|
},
|
|
{
|
|
key: "tiktok",
|
|
label: "TikTok",
|
|
options: [{ id: "tt-video", label: "9:16", ratio: 9 / 16 }],
|
|
},
|
|
{
|
|
key: "youtube",
|
|
label: "YouTube",
|
|
options: [
|
|
{ id: "yt-cover", label: "16:9", ratio: 16 / 9 },
|
|
{ id: "yt-short", label: "9:16", ratio: 9 / 16 },
|
|
],
|
|
},
|
|
];
|
|
|
|
const clampCropNumber = (value: number, min: number, max: number) =>
|
|
Math.min(max, Math.max(min, value));
|
|
|
|
export const createNormalizedRect = (
|
|
start: BoxCutoutPoint,
|
|
end: BoxCutoutPoint
|
|
): NormalizedCropRect => {
|
|
const x = Math.min(start.x, end.x);
|
|
const y = Math.min(start.y, end.y);
|
|
const right = Math.max(start.x, end.x);
|
|
const bottom = Math.max(start.y, end.y);
|
|
|
|
return {
|
|
x,
|
|
y,
|
|
width: clampCropNumber(right - x, 0, 1),
|
|
height: clampCropNumber(bottom - y, 0, 1),
|
|
};
|
|
};
|
|
|
|
export const getCropPresetById = (presetId: string) => {
|
|
for (const group of CROP_PRESET_GROUPS) {
|
|
const matched = group.options.find((option) => option.id === presetId);
|
|
if (matched) return matched;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
export const getNormalizedMinCropSize = (
|
|
naturalWidth: number,
|
|
naturalHeight: number
|
|
) => ({
|
|
width: naturalWidth > 0 ? Math.min(1, 24 / naturalWidth) : 0.02,
|
|
height: naturalHeight > 0 ? Math.min(1, 24 / naturalHeight) : 0.02,
|
|
});
|
|
|
|
export const createCenteredCropRectForAspect = (
|
|
ratio: number,
|
|
centerX = 0.5,
|
|
centerY = 0.5
|
|
): NormalizedCropRect => {
|
|
const safeRatio = Number.isFinite(ratio) && ratio > 0 ? ratio : 1;
|
|
const maxHalfWidth = Math.min(centerX, 1 - centerX);
|
|
const maxHalfHeight = Math.min(centerY, 1 - centerY);
|
|
|
|
let width = maxHalfHeight * 2 * safeRatio;
|
|
let height = width / safeRatio;
|
|
|
|
if (width > maxHalfWidth * 2) {
|
|
width = maxHalfWidth * 2;
|
|
height = width / safeRatio;
|
|
}
|
|
|
|
if (height > maxHalfHeight * 2) {
|
|
height = maxHalfHeight * 2;
|
|
width = height * safeRatio;
|
|
}
|
|
|
|
return {
|
|
x: clampCropNumber(centerX - width / 2, 0, 1 - width),
|
|
y: clampCropNumber(centerY - height / 2, 0, 1 - height),
|
|
width: clampCropNumber(width, 0, 1),
|
|
height: clampCropNumber(height, 0, 1),
|
|
};
|
|
};
|
|
|
|
export const createCenteredCropRectFromSize = (
|
|
centerX: number,
|
|
centerY: number,
|
|
width: number,
|
|
height: number
|
|
): NormalizedCropRect => {
|
|
const safeWidth = clampCropNumber(width, 0.0001, 1);
|
|
const safeHeight = clampCropNumber(height, 0.0001, 1);
|
|
let x = centerX - safeWidth / 2;
|
|
let y = centerY - safeHeight / 2;
|
|
|
|
x = clampCropNumber(x, 0, 1 - safeWidth);
|
|
y = clampCropNumber(y, 0, 1 - safeHeight);
|
|
|
|
return {
|
|
x,
|
|
y,
|
|
width: safeWidth,
|
|
height: safeHeight,
|
|
};
|
|
};
|
|
|
|
export const updateCropRectWithPointer = ({
|
|
aspectRatio,
|
|
handle,
|
|
minHeight,
|
|
minWidth,
|
|
pointer,
|
|
startPoint,
|
|
startRect,
|
|
}: {
|
|
aspectRatio: number | null;
|
|
handle: CropInteractionHandle;
|
|
minHeight: number;
|
|
minWidth: number;
|
|
pointer: BoxCutoutPoint;
|
|
startPoint: BoxCutoutPoint;
|
|
startRect: NormalizedCropRect;
|
|
}): NormalizedCropRect => {
|
|
const startRight = startRect.x + startRect.width;
|
|
const startBottom = startRect.y + startRect.height;
|
|
const currentAspect =
|
|
aspectRatio && Number.isFinite(aspectRatio) && aspectRatio > 0
|
|
? aspectRatio
|
|
: null;
|
|
|
|
if (handle === "move") {
|
|
const nextX = clampCropNumber(
|
|
startRect.x + (pointer.x - startPoint.x),
|
|
0,
|
|
1 - startRect.width
|
|
);
|
|
const nextY = clampCropNumber(
|
|
startRect.y + (pointer.y - startPoint.y),
|
|
0,
|
|
1 - startRect.height
|
|
);
|
|
return {
|
|
x: nextX,
|
|
y: nextY,
|
|
width: startRect.width,
|
|
height: startRect.height,
|
|
};
|
|
}
|
|
|
|
if (!currentAspect) {
|
|
let x = startRect.x;
|
|
let y = startRect.y;
|
|
let width = startRect.width;
|
|
let height = startRect.height;
|
|
|
|
if (handle.includes("w")) {
|
|
x = clampCropNumber(pointer.x, 0, startRight - minWidth);
|
|
width = startRight - x;
|
|
}
|
|
if (handle.includes("e")) {
|
|
width = clampCropNumber(pointer.x - startRect.x, minWidth, 1 - startRect.x);
|
|
}
|
|
if (handle.includes("n")) {
|
|
y = clampCropNumber(pointer.y, 0, startBottom - minHeight);
|
|
height = startBottom - y;
|
|
}
|
|
if (handle.includes("s")) {
|
|
height = clampCropNumber(pointer.y - startRect.y, minHeight, 1 - startRect.y);
|
|
}
|
|
|
|
return {
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
};
|
|
}
|
|
|
|
if (handle === "n" || handle === "s") {
|
|
const anchorY = handle === "n" ? startBottom : startRect.y;
|
|
let height = Math.abs(anchorY - pointer.y);
|
|
height = clampCropNumber(
|
|
height,
|
|
minHeight,
|
|
handle === "n" ? anchorY : 1 - anchorY
|
|
);
|
|
let width = height * currentAspect;
|
|
const centerX = startRect.x + startRect.width / 2;
|
|
const maxWidth = Math.min(centerX, 1 - centerX) * 2;
|
|
if (width > maxWidth) {
|
|
width = maxWidth;
|
|
height = width / currentAspect;
|
|
}
|
|
return {
|
|
x: clampCropNumber(centerX - width / 2, 0, 1 - width),
|
|
y: handle === "n" ? anchorY - height : anchorY,
|
|
width,
|
|
height,
|
|
};
|
|
}
|
|
|
|
if (handle === "w" || handle === "e") {
|
|
const anchorX = handle === "w" ? startRight : startRect.x;
|
|
let width = Math.abs(anchorX - pointer.x);
|
|
width = clampCropNumber(width, minWidth, handle === "w" ? anchorX : 1 - anchorX);
|
|
let height = width / currentAspect;
|
|
const centerY = startRect.y + startRect.height / 2;
|
|
const maxHeight = Math.min(centerY, 1 - centerY) * 2;
|
|
if (height > maxHeight) {
|
|
height = maxHeight;
|
|
width = height * currentAspect;
|
|
}
|
|
return {
|
|
x: handle === "w" ? anchorX - width : anchorX,
|
|
y: clampCropNumber(centerY - height / 2, 0, 1 - height),
|
|
width,
|
|
height,
|
|
};
|
|
}
|
|
|
|
const isWest = handle.includes("w");
|
|
const isNorth = handle.includes("n");
|
|
const anchorX = isWest ? startRight : startRect.x;
|
|
const anchorY = isNorth ? startBottom : startRect.y;
|
|
const maxWidth = isWest ? anchorX : 1 - anchorX;
|
|
const maxHeight = isNorth ? anchorY : 1 - anchorY;
|
|
|
|
let width = Math.abs(anchorX - pointer.x);
|
|
let height = width / currentAspect;
|
|
|
|
if (height > maxHeight) {
|
|
height = maxHeight;
|
|
width = height * currentAspect;
|
|
}
|
|
if (width > maxWidth) {
|
|
width = maxWidth;
|
|
height = width / currentAspect;
|
|
}
|
|
if (width < minWidth) {
|
|
width = minWidth;
|
|
height = width / currentAspect;
|
|
}
|
|
if (height < minHeight) {
|
|
height = minHeight;
|
|
width = height * currentAspect;
|
|
}
|
|
if (height > maxHeight) {
|
|
height = maxHeight;
|
|
width = height * currentAspect;
|
|
}
|
|
if (width > maxWidth) {
|
|
width = maxWidth;
|
|
height = width / currentAspect;
|
|
}
|
|
|
|
return {
|
|
x: isWest ? anchorX - width : anchorX,
|
|
y: isNorth ? anchorY - height : anchorY,
|
|
width,
|
|
height,
|
|
};
|
|
};
|