OpenCut/apps/web/src/lib/masks/definitions/star.ts

141 lines
3.4 KiB
TypeScript

import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
computeBoxMaskParamUpdate,
getBoxLikeGeometry,
getDefaultSquareMaskParams,
getStrokeOffset,
rotatePoint,
} from "./box-like";
const STAR_INNER_RADIUS_RATIO = 0.45;
const STAR_VERTEX_COUNT = 10;
function buildStarPath({
centerX,
centerY,
halfWidth,
halfHeight,
rotationRad,
}: {
centerX: number;
centerY: number;
halfWidth: number;
halfHeight: number;
rotationRad: number;
}): Path2D {
const path = new Path2D();
for (let index = 0; index < STAR_VERTEX_COUNT; index++) {
const isOuterVertex = index % 2 === 0;
const radiusX = isOuterVertex
? halfWidth
: halfWidth * STAR_INNER_RADIUS_RATIO;
const radiusY = isOuterVertex
? halfHeight
: halfHeight * STAR_INNER_RADIUS_RATIO;
const angle = (index * Math.PI) / 5 - Math.PI / 2;
const point = rotatePoint({
x: centerX + radiusX * Math.cos(angle),
y: centerY + radiusY * Math.sin(angle),
centerX,
centerY,
rotationRad,
});
if (index === 0) {
path.moveTo(point.x, point.y);
} else {
path.lineTo(point.x, point.y);
}
}
path.closePath();
return path;
}
function buildOverlayStarPath({
width,
height,
}: {
width: number;
height: number;
}): string {
const centerX = width / 2;
const centerY = height / 2;
const halfWidth = width / 2;
const halfHeight = height / 2;
const segments: string[] = [];
for (let index = 0; index < STAR_VERTEX_COUNT; index++) {
const isOuterVertex = index % 2 === 0;
const radiusX = isOuterVertex
? halfWidth
: halfWidth * STAR_INNER_RADIUS_RATIO;
const radiusY = isOuterVertex
? halfHeight
: halfHeight * STAR_INNER_RADIUS_RATIO;
const angle = (index * Math.PI) / 5 - Math.PI / 2;
const x = centerX + radiusX * Math.cos(angle);
const y = centerY + radiusY * Math.sin(angle);
segments.push(`${index === 0 ? "M" : "L"} ${x},${y}`);
}
return `${segments.join(" ")} Z`;
}
export const starMaskDefinition: MaskDefinition<RectangleMaskParams> = {
type: "star",
name: "Star",
features: {
hasPosition: true,
hasRotation: true,
sizeMode: "width-height",
},
params: BOX_LIKE_MASK_PARAMS,
interaction: buildBoxMaskInteraction({
sizeMode: "width-height",
buildOverlayPath({ width, height }) {
return buildOverlayStarPath({ width, height });
},
}),
buildDefault(context) {
return {
type: "star",
params: getDefaultSquareMaskParams(context),
};
},
computeParamUpdate: computeBoxMaskParamUpdate,
renderer: {
buildPath({ resolvedParams, width, height }) {
const params = resolvedParams as RectangleMaskParams;
const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
getBoxLikeGeometry({ params, width, height });
return buildStarPath({
centerX,
centerY,
halfWidth: maskWidth / 2,
halfHeight: maskHeight / 2,
rotationRad,
});
},
buildStrokePath({ resolvedParams, width, height }) {
const params = resolvedParams as RectangleMaskParams;
const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
getBoxLikeGeometry({ params, width, height });
const offset = getStrokeOffset({
strokeAlign: params.strokeAlign,
strokeWidth: params.strokeWidth,
});
return buildStarPath({
centerX,
centerY,
halfWidth: Math.max(maskWidth / 2 + offset, 1),
halfHeight: Math.max(maskHeight / 2 + offset, 1),
rotationRad,
});
},
},
};