summaryrefslogtreecommitdiff
path: root/apps/web/src/masks/definitions/star.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/masks/definitions/star.ts
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/masks/definitions/star.ts')
-rw-r--r--apps/web/src/masks/definitions/star.ts140
1 files changed, 140 insertions, 0 deletions
diff --git a/apps/web/src/masks/definitions/star.ts b/apps/web/src/masks/definitions/star.ts
new file mode 100644
index 00000000..a7f8819c
--- /dev/null
+++ b/apps/web/src/masks/definitions/star.ts
@@ -0,0 +1,140 @@
+import type { MaskDefinition, RectangleMaskParams } from "@/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,
+ });
+ },
+ },
+};