Files
OpenCut/apps/web/src/graphics/definitions/polygon.ts
T
Maze WintherandCursor ab982b888e refactor: extract decoration model types
Move animation, effect, mask, and param value shapes into model decorations so element data no longer depends on decoration domains.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 03:58:21 +02:00

212 lines
4.8 KiB
TypeScript

import type { ParamDefinition } from "@/model/decorations/param-values";
import { applyAlignedStroke } from "../stroke";
import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
import type { GraphicDefinition } from "../types";
interface Point {
x: number;
y: number;
}
interface PolygonParams {
fill: string;
stroke: string;
strokeWidth: number;
strokeAlign: GraphicStrokeAlign;
sides: number;
cornerRadius: number;
}
const POLYGON_PARAMS: ParamDefinition<keyof PolygonParams & string>[] = [
{
key: "fill",
label: "Fill",
type: "color",
default: "#ffffff",
},
{
key: "stroke",
label: "Color",
type: "color",
default: "#000000",
group: "stroke",
},
{
key: "strokeWidth",
label: "Width",
type: "number",
default: 0,
min: 0,
max: 64,
step: 1,
shortLabel: "W",
group: "stroke",
},
STROKE_ALIGN_PARAM,
{
key: "sides",
label: "Sides",
type: "number",
default: 5,
min: 3,
max: 12,
step: 1,
shortLabel: "S",
},
{
key: "cornerRadius",
label: "Corner radius",
type: "number",
default: 0,
min: 0,
max: 50,
step: 1,
shortLabel: "R",
},
];
function buildPolygonVertices({
centerX,
centerY,
radius,
sides,
}: {
centerX: number;
centerY: number;
radius: number;
sides: number;
}): Point[] {
return Array.from({ length: sides }, (_, index) => {
const angle = -Math.PI / 2 + (index * Math.PI * 2) / sides;
return {
x: centerX + Math.cos(angle) * radius,
y: centerY + Math.sin(angle) * radius,
};
});
}
function distance({ a, b }: { a: Point; b: Point }): number {
return Math.hypot(a.x - b.x, a.y - b.y);
}
function normalize(point: Point): Point {
const length = Math.hypot(point.x, point.y) || 1;
return {
x: point.x / length,
y: point.y / length,
};
}
function traceRoundedPolygonPath({
path,
vertices,
radius,
}: {
path: Path2D;
vertices: Point[];
radius: number;
}): void {
if (vertices.length < 3) {
return;
}
if (radius <= 0) {
path.moveTo(vertices[0].x, vertices[0].y);
for (let index = 1; index < vertices.length; index++) {
path.lineTo(vertices[index].x, vertices[index].y);
}
path.closePath();
return;
}
for (let index = 0; index < vertices.length; index++) {
const previous = vertices[(index - 1 + vertices.length) % vertices.length];
const current = vertices[index];
const next = vertices[(index + 1) % vertices.length];
const toPrevious = normalize({
x: previous.x - current.x,
y: previous.y - current.y,
});
const toNext = normalize({
x: next.x - current.x,
y: next.y - current.y,
});
const angle = Math.acos(
Math.max(-1, Math.min(1, toPrevious.x * toNext.x + toPrevious.y * toNext.y)),
);
const maxOffset =
Math.min(distance({ a: previous, b: current }), distance({ a: current, b: next })) / 2;
const tangentOffset = Math.min(radius / Math.tan(angle / 2), maxOffset);
const start = {
x: current.x + toPrevious.x * tangentOffset,
y: current.y + toPrevious.y * tangentOffset,
};
const end = {
x: current.x + toNext.x * tangentOffset,
y: current.y + toNext.y * tangentOffset,
};
if (index === 0) {
path.moveTo(start.x, start.y);
} else {
path.lineTo(start.x, start.y);
}
path.arcTo(
current.x,
current.y,
end.x,
end.y,
Math.min(radius, maxOffset),
);
}
path.closePath();
}
export const polygonGraphicDefinition: GraphicDefinition = {
id: "polygon",
name: "Polygon",
keywords: ["polygon", "triangle", "pentagon", "hexagon", "diamond"],
params: POLYGON_PARAMS,
render({ ctx, params, width, height }) {
const fill = String(params.fill ?? "#ffffff");
const stroke = String(params.stroke ?? "#000000");
const strokeWidth = Math.max(0, Number(params.strokeWidth ?? 0));
const strokeAlign = (params.strokeAlign ?? "center") as GraphicStrokeAlign;
const sides = Math.max(3, Math.min(12, Math.round(Number(params.sides ?? 5))));
const inset = strokeAlign === "center" ? strokeWidth / 2 : 0;
const radius = Math.max(1, Math.min(width, height) / 2 - inset);
const maxCornerRadius = radius * Math.sin(Math.PI / sides);
const cornerRadiusPercent = Math.max(0, Number(params.cornerRadius ?? 0));
const cornerRadius =
maxCornerRadius * Math.min(cornerRadiusPercent, 50) / 50;
const vertices = buildPolygonVertices({
centerX: width / 2,
centerY: height / 2,
radius,
sides,
});
ctx.clearRect(0, 0, width, height);
const path = new Path2D();
traceRoundedPolygonPath({
path,
vertices,
radius: cornerRadius,
});
ctx.fillStyle = fill;
ctx.fill(path);
if (strokeWidth > 0) {
applyAlignedStroke({
ctx,
path,
strokeWidth,
strokeAlign,
strokeColor: stroke,
});
}
},
};