271 lines
6.0 KiB
TypeScript
271 lines
6.0 KiB
TypeScript
import {
|
|
DEFAULT_SHAPE_MASK_SHORT_SIDE_RATIO,
|
|
MIN_MASK_DIMENSION,
|
|
} from "@/lib/masks/dimensions";
|
|
import { computeFeatherUpdate } from "../param-update";
|
|
import type {
|
|
BaseMaskParams,
|
|
MaskDefaultContext,
|
|
MaskParamUpdateArgs,
|
|
RectangleMaskParams,
|
|
} from "@/lib/masks/types";
|
|
import type {
|
|
NumberParamDefinition,
|
|
ParamDefinition,
|
|
ParamValues,
|
|
} from "@/lib/params";
|
|
|
|
const PERCENTAGE_DISPLAY: Pick<
|
|
NumberParamDefinition,
|
|
"displayMultiplier" | "step"
|
|
> = {
|
|
displayMultiplier: 100,
|
|
step: 1,
|
|
};
|
|
|
|
export const BOX_LIKE_MASK_PARAMS: ParamDefinition<
|
|
keyof RectangleMaskParams & string
|
|
>[] = [
|
|
{
|
|
key: "centerX",
|
|
label: "X",
|
|
type: "number",
|
|
default: 0,
|
|
min: -100,
|
|
max: 100,
|
|
...PERCENTAGE_DISPLAY,
|
|
},
|
|
{
|
|
key: "centerY",
|
|
label: "Y",
|
|
type: "number",
|
|
default: 0,
|
|
min: -100,
|
|
max: 100,
|
|
...PERCENTAGE_DISPLAY,
|
|
},
|
|
{
|
|
key: "width",
|
|
label: "Width",
|
|
type: "number",
|
|
default: 0.6,
|
|
min: 1,
|
|
...PERCENTAGE_DISPLAY,
|
|
},
|
|
{
|
|
key: "height",
|
|
label: "Height",
|
|
type: "number",
|
|
default: 0.6,
|
|
min: 1,
|
|
...PERCENTAGE_DISPLAY,
|
|
},
|
|
{
|
|
key: "rotation",
|
|
label: "Rotation",
|
|
type: "number",
|
|
default: 0,
|
|
min: 0,
|
|
max: 360,
|
|
step: 1,
|
|
},
|
|
{
|
|
key: "scale",
|
|
label: "Scale",
|
|
type: "number",
|
|
default: 1,
|
|
min: 1,
|
|
max: 500,
|
|
...PERCENTAGE_DISPLAY,
|
|
},
|
|
{
|
|
key: "strokeAlign",
|
|
label: "Stroke Align",
|
|
type: "select",
|
|
default: "center",
|
|
options: [
|
|
{ value: "inside", label: "Inside" },
|
|
{ value: "center", label: "Center" },
|
|
{ value: "outside", label: "Outside" },
|
|
],
|
|
},
|
|
];
|
|
|
|
export function getDefaultBaseMaskParams(): BaseMaskParams {
|
|
return {
|
|
feather: 0,
|
|
inverted: false,
|
|
strokeColor: "#ffffff",
|
|
strokeWidth: 0,
|
|
strokeAlign: "center",
|
|
};
|
|
}
|
|
|
|
export function getStrokeOffset({
|
|
strokeAlign,
|
|
strokeWidth,
|
|
}: Pick<BaseMaskParams, "strokeAlign" | "strokeWidth">): number {
|
|
if (strokeAlign === "inside") {
|
|
return -(strokeWidth / 2);
|
|
}
|
|
|
|
if (strokeAlign === "outside") {
|
|
return strokeWidth / 2;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
export function getDefaultSquareMaskParams({
|
|
elementSize,
|
|
}: MaskDefaultContext): RectangleMaskParams {
|
|
const absWidth = Math.abs(elementSize?.width ?? 0);
|
|
const absHeight = Math.abs(elementSize?.height ?? 0);
|
|
const shortSide = Math.min(absWidth, absHeight);
|
|
const squareSide =
|
|
shortSide > 0 ? shortSide * DEFAULT_SHAPE_MASK_SHORT_SIDE_RATIO : 0;
|
|
const width =
|
|
absWidth > 0 ? squareSide / absWidth : DEFAULT_SHAPE_MASK_SHORT_SIDE_RATIO;
|
|
const height =
|
|
absHeight > 0
|
|
? squareSide / absHeight
|
|
: DEFAULT_SHAPE_MASK_SHORT_SIDE_RATIO;
|
|
|
|
return {
|
|
...getDefaultBaseMaskParams(),
|
|
centerX: 0,
|
|
centerY: 0,
|
|
width,
|
|
height,
|
|
rotation: 0,
|
|
scale: 1,
|
|
};
|
|
}
|
|
|
|
export function getBoxLikeGeometry({
|
|
params,
|
|
width,
|
|
height,
|
|
}: {
|
|
params: RectangleMaskParams;
|
|
width: number;
|
|
height: number;
|
|
}) {
|
|
return {
|
|
centerX: width / 2 + params.centerX * width,
|
|
centerY: height / 2 + params.centerY * height,
|
|
maskWidth: Math.max(params.width, MIN_MASK_DIMENSION) * width,
|
|
maskHeight: Math.max(params.height, MIN_MASK_DIMENSION) * height,
|
|
rotationRad: (params.rotation * Math.PI) / 180,
|
|
};
|
|
}
|
|
|
|
export function computeBoxMaskParamUpdate({
|
|
handleId,
|
|
startParams,
|
|
deltaX,
|
|
deltaY,
|
|
bounds,
|
|
}: MaskParamUpdateArgs<RectangleMaskParams>): ParamValues {
|
|
if (handleId === "position") {
|
|
return {
|
|
centerX: startParams.centerX + deltaX / bounds.width,
|
|
centerY: startParams.centerY + deltaY / bounds.height,
|
|
};
|
|
}
|
|
|
|
if (handleId === "rotation") {
|
|
const currentAngle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
|
|
const newRotation = (startParams.rotation + currentAngle) % 360;
|
|
return { rotation: newRotation < 0 ? newRotation + 360 : newRotation };
|
|
}
|
|
|
|
if (handleId === "feather") {
|
|
const angleRad = (startParams.rotation * Math.PI) / 180;
|
|
return computeFeatherUpdate({
|
|
startFeather: startParams.feather,
|
|
deltaX,
|
|
deltaY,
|
|
directionX: -Math.sin(angleRad),
|
|
directionY: Math.cos(angleRad),
|
|
});
|
|
}
|
|
|
|
const halfWidth = startParams.width * bounds.width;
|
|
const halfHeight = startParams.height * bounds.height;
|
|
|
|
if (handleId === "right" || handleId === "left") {
|
|
const sign = handleId === "right" ? 1 : -1;
|
|
return {
|
|
width: Math.max(
|
|
MIN_MASK_DIMENSION,
|
|
startParams.width + (sign * deltaX * 2) / bounds.width,
|
|
),
|
|
};
|
|
}
|
|
|
|
if (handleId === "bottom" || handleId === "top") {
|
|
const sign = handleId === "bottom" ? 1 : -1;
|
|
return {
|
|
height: Math.max(
|
|
MIN_MASK_DIMENSION,
|
|
startParams.height + (sign * deltaY * 2) / bounds.height,
|
|
),
|
|
};
|
|
}
|
|
|
|
if (
|
|
handleId === "top-left" ||
|
|
handleId === "top-right" ||
|
|
handleId === "bottom-left" ||
|
|
handleId === "bottom-right"
|
|
) {
|
|
const signX = handleId.includes("right") ? 1 : -1;
|
|
const signY = handleId.includes("bottom") ? 1 : -1;
|
|
const distance = Math.sqrt(
|
|
(signX * deltaX + halfWidth) ** 2 + (signY * deltaY + halfHeight) ** 2,
|
|
);
|
|
const originalDistance = Math.sqrt(halfWidth ** 2 + halfHeight ** 2);
|
|
const scale = originalDistance > 0 ? distance / originalDistance : 1;
|
|
return {
|
|
width: Math.max(MIN_MASK_DIMENSION, startParams.width * scale),
|
|
height: Math.max(MIN_MASK_DIMENSION, startParams.height * scale),
|
|
};
|
|
}
|
|
|
|
if (handleId === "scale") {
|
|
const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
|
|
const originalDistance = Math.sqrt(halfWidth ** 2 + halfHeight ** 2);
|
|
const scale = originalDistance > 0 ? 1 + distance / originalDistance : 1;
|
|
return {
|
|
scale: Math.max(MIN_MASK_DIMENSION, startParams.scale * scale),
|
|
};
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
export function rotatePoint({
|
|
x,
|
|
y,
|
|
centerX,
|
|
centerY,
|
|
rotationRad,
|
|
}: {
|
|
x: number;
|
|
y: number;
|
|
centerX: number;
|
|
centerY: number;
|
|
rotationRad: number;
|
|
}) {
|
|
const dx = x - centerX;
|
|
const dy = y - centerY;
|
|
const cos = Math.cos(rotationRad);
|
|
const sin = Math.sin(rotationRad);
|
|
|
|
return {
|
|
x: centerX + dx * cos - dy * sin,
|
|
y: centerY + dx * sin + dy * cos,
|
|
};
|
|
}
|