summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/graphics/definitions
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-27 00:55:15 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-29 15:56:48 +0200
commit4d127a038bf768eded68aae3cfa5c1b12241bdac (patch)
tree16bbb801caf6770546cff4fdb37b6408ec9ead98 /apps/web/src/lib/graphics/definitions
parent82817cb50cb0487350573d6129130aec705f924b (diff)
downloadopencut-4d127a038bf768eded68aae3cfa5c1b12241bdac.tar.gz
chore: normalize line endings
Diffstat (limited to 'apps/web/src/lib/graphics/definitions')
-rw-r--r--apps/web/src/lib/graphics/definitions/ellipse.ts146
-rw-r--r--apps/web/src/lib/graphics/definitions/index.ts58
-rw-r--r--apps/web/src/lib/graphics/definitions/polygon.ts422
-rw-r--r--apps/web/src/lib/graphics/definitions/rectangle.ts170
-rw-r--r--apps/web/src/lib/graphics/definitions/shared.ts32
-rw-r--r--apps/web/src/lib/graphics/definitions/star.ts218
6 files changed, 523 insertions, 523 deletions
diff --git a/apps/web/src/lib/graphics/definitions/ellipse.ts b/apps/web/src/lib/graphics/definitions/ellipse.ts
index 3fd76608..04b93c27 100644
--- a/apps/web/src/lib/graphics/definitions/ellipse.ts
+++ b/apps/web/src/lib/graphics/definitions/ellipse.ts
@@ -1,73 +1,73 @@
-import type { ParamDefinition } from "@/lib/params";
-import { applyAlignedStroke } from "../stroke";
-import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
-import type { GraphicDefinition } from "../types";
-
-interface EllipseParams {
- fill: string;
- stroke: string;
- strokeWidth: number;
- strokeAlign: GraphicStrokeAlign;
-}
-
-const ELLIPSE_PARAMS: ParamDefinition<keyof EllipseParams & 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,
-];
-
-export const ellipseGraphicDefinition: GraphicDefinition = {
- id: "ellipse",
- name: "Ellipse",
- keywords: ["ellipse", "circle", "oval"],
- params: ELLIPSE_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 inset = strokeAlign === "center" ? strokeWidth / 2 : 0;
- const centerX = width / 2;
- const centerY = height / 2;
- const radiusX = Math.max(1, width / 2 - inset);
- const radiusY = Math.max(1, height / 2 - inset);
-
- ctx.clearRect(0, 0, width, height);
- const path = new Path2D();
- path.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
- ctx.fillStyle = fill;
- ctx.fill(path);
-
- if (strokeWidth > 0) {
- applyAlignedStroke({
- ctx,
- path,
- strokeWidth,
- strokeAlign,
- strokeColor: stroke,
- });
- }
- },
-};
+import type { ParamDefinition } from "@/lib/params";
+import { applyAlignedStroke } from "../stroke";
+import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
+import type { GraphicDefinition } from "../types";
+
+interface EllipseParams {
+ fill: string;
+ stroke: string;
+ strokeWidth: number;
+ strokeAlign: GraphicStrokeAlign;
+}
+
+const ELLIPSE_PARAMS: ParamDefinition<keyof EllipseParams & 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,
+];
+
+export const ellipseGraphicDefinition: GraphicDefinition = {
+ id: "ellipse",
+ name: "Ellipse",
+ keywords: ["ellipse", "circle", "oval"],
+ params: ELLIPSE_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 inset = strokeAlign === "center" ? strokeWidth / 2 : 0;
+ const centerX = width / 2;
+ const centerY = height / 2;
+ const radiusX = Math.max(1, width / 2 - inset);
+ const radiusY = Math.max(1, height / 2 - inset);
+
+ ctx.clearRect(0, 0, width, height);
+ const path = new Path2D();
+ path.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
+ ctx.fillStyle = fill;
+ ctx.fill(path);
+
+ if (strokeWidth > 0) {
+ applyAlignedStroke({
+ ctx,
+ path,
+ strokeWidth,
+ strokeAlign,
+ strokeColor: stroke,
+ });
+ }
+ },
+};
diff --git a/apps/web/src/lib/graphics/definitions/index.ts b/apps/web/src/lib/graphics/definitions/index.ts
index e791f917..6fd9e446 100644
--- a/apps/web/src/lib/graphics/definitions/index.ts
+++ b/apps/web/src/lib/graphics/definitions/index.ts
@@ -1,29 +1,29 @@
-import { graphicsRegistry } from "../registry";
-import { ellipseGraphicDefinition } from "./ellipse";
-import { polygonGraphicDefinition } from "./polygon";
-import { rectangleGraphicDefinition } from "./rectangle";
-import { starGraphicDefinition } from "./star";
-
-const defaultGraphicDefinitions = [
- rectangleGraphicDefinition,
- ellipseGraphicDefinition,
- polygonGraphicDefinition,
- starGraphicDefinition,
-];
-
-export function registerDefaultGraphics(): void {
- for (const definition of defaultGraphicDefinitions) {
- if (graphicsRegistry.has(definition.id)) {
- continue;
- }
- graphicsRegistry.register(definition.id, definition);
- }
-}
-
-export {
- ellipseGraphicDefinition,
- polygonGraphicDefinition,
- rectangleGraphicDefinition,
- starGraphicDefinition,
-};
-export { STROKE_ALIGN_PARAM } from "./shared";
+import { graphicsRegistry } from "../registry";
+import { ellipseGraphicDefinition } from "./ellipse";
+import { polygonGraphicDefinition } from "./polygon";
+import { rectangleGraphicDefinition } from "./rectangle";
+import { starGraphicDefinition } from "./star";
+
+const defaultGraphicDefinitions = [
+ rectangleGraphicDefinition,
+ ellipseGraphicDefinition,
+ polygonGraphicDefinition,
+ starGraphicDefinition,
+];
+
+export function registerDefaultGraphics(): void {
+ for (const definition of defaultGraphicDefinitions) {
+ if (graphicsRegistry.has(definition.id)) {
+ continue;
+ }
+ graphicsRegistry.register(definition.id, definition);
+ }
+}
+
+export {
+ ellipseGraphicDefinition,
+ polygonGraphicDefinition,
+ rectangleGraphicDefinition,
+ starGraphicDefinition,
+};
+export { STROKE_ALIGN_PARAM } from "./shared";
diff --git a/apps/web/src/lib/graphics/definitions/polygon.ts b/apps/web/src/lib/graphics/definitions/polygon.ts
index 780c974c..deecb6ff 100644
--- a/apps/web/src/lib/graphics/definitions/polygon.ts
+++ b/apps/web/src/lib/graphics/definitions/polygon.ts
@@ -1,211 +1,211 @@
-import type { ParamDefinition } from "@/lib/params";
-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,
- });
- }
- },
-};
+import type { ParamDefinition } from "@/lib/params";
+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,
+ });
+ }
+ },
+};
diff --git a/apps/web/src/lib/graphics/definitions/rectangle.ts b/apps/web/src/lib/graphics/definitions/rectangle.ts
index 4e7f78f6..fb52c03a 100644
--- a/apps/web/src/lib/graphics/definitions/rectangle.ts
+++ b/apps/web/src/lib/graphics/definitions/rectangle.ts
@@ -1,85 +1,85 @@
-import type { ParamDefinition } from "@/lib/params";
-import { applyAlignedStroke } from "../stroke";
-import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
-import type { GraphicDefinition } from "../types";
-
-interface RectangleParams {
- fill: string;
- stroke: string;
- strokeWidth: number;
- strokeAlign: GraphicStrokeAlign;
- cornerRadius: number;
-}
-
-const RECTANGLE_PARAMS: ParamDefinition<keyof RectangleParams & 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: "cornerRadius",
- label: "Corner radius",
- type: "number",
- default: 0,
- min: 0,
- max: 50,
- step: 1,
- shortLabel: "R",
- },
-];
-
-export const rectangleGraphicDefinition: GraphicDefinition = {
- id: "rectangle",
- name: "Rectangle",
- keywords: ["rectangle", "square", "box"],
- params: RECTANGLE_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 inset = strokeAlign === "center" ? strokeWidth / 2 : 0;
- const drawWidth = Math.max(1, width - inset * 2);
- const drawHeight = Math.max(1, height - inset * 2);
- const radiusPercent = Math.max(0, Number(params.cornerRadius ?? 0));
- const radius =
- (Math.min(drawWidth, drawHeight) / 2) * Math.min(radiusPercent, 50) / 50;
-
- ctx.clearRect(0, 0, width, height);
- const path = new Path2D();
- path.roundRect(inset, inset, drawWidth, drawHeight, radius);
- ctx.fillStyle = fill;
- ctx.fill(path);
-
- if (strokeWidth > 0) {
- applyAlignedStroke({
- ctx,
- path,
- strokeWidth,
- strokeAlign,
- strokeColor: stroke,
- });
- }
- },
-};
+import type { ParamDefinition } from "@/lib/params";
+import { applyAlignedStroke } from "../stroke";
+import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
+import type { GraphicDefinition } from "../types";
+
+interface RectangleParams {
+ fill: string;
+ stroke: string;
+ strokeWidth: number;
+ strokeAlign: GraphicStrokeAlign;
+ cornerRadius: number;
+}
+
+const RECTANGLE_PARAMS: ParamDefinition<keyof RectangleParams & 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: "cornerRadius",
+ label: "Corner radius",
+ type: "number",
+ default: 0,
+ min: 0,
+ max: 50,
+ step: 1,
+ shortLabel: "R",
+ },
+];
+
+export const rectangleGraphicDefinition: GraphicDefinition = {
+ id: "rectangle",
+ name: "Rectangle",
+ keywords: ["rectangle", "square", "box"],
+ params: RECTANGLE_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 inset = strokeAlign === "center" ? strokeWidth / 2 : 0;
+ const drawWidth = Math.max(1, width - inset * 2);
+ const drawHeight = Math.max(1, height - inset * 2);
+ const radiusPercent = Math.max(0, Number(params.cornerRadius ?? 0));
+ const radius =
+ (Math.min(drawWidth, drawHeight) / 2) * Math.min(radiusPercent, 50) / 50;
+
+ ctx.clearRect(0, 0, width, height);
+ const path = new Path2D();
+ path.roundRect(inset, inset, drawWidth, drawHeight, radius);
+ ctx.fillStyle = fill;
+ ctx.fill(path);
+
+ if (strokeWidth > 0) {
+ applyAlignedStroke({
+ ctx,
+ path,
+ strokeWidth,
+ strokeAlign,
+ strokeColor: stroke,
+ });
+ }
+ },
+};
diff --git a/apps/web/src/lib/graphics/definitions/shared.ts b/apps/web/src/lib/graphics/definitions/shared.ts
index d2833f51..4f56c77c 100644
--- a/apps/web/src/lib/graphics/definitions/shared.ts
+++ b/apps/web/src/lib/graphics/definitions/shared.ts
@@ -1,16 +1,16 @@
-import type { ParamDefinition } from "@/lib/params";
-
-export type GraphicStrokeAlign = "inside" | "center" | "outside";
-
-export const STROKE_ALIGN_PARAM: ParamDefinition<"strokeAlign"> = {
- key: "strokeAlign",
- label: "Stroke align",
- type: "select",
- default: "center",
- group: "stroke",
- options: [
- { value: "inside", label: "Inside" },
- { value: "center", label: "Center" },
- { value: "outside", label: "Outside" },
- ],
-};
+import type { ParamDefinition } from "@/lib/params";
+
+export type GraphicStrokeAlign = "inside" | "center" | "outside";
+
+export const STROKE_ALIGN_PARAM: ParamDefinition<"strokeAlign"> = {
+ key: "strokeAlign",
+ label: "Stroke align",
+ type: "select",
+ default: "center",
+ group: "stroke",
+ options: [
+ { value: "inside", label: "Inside" },
+ { value: "center", label: "Center" },
+ { value: "outside", label: "Outside" },
+ ],
+};
diff --git a/apps/web/src/lib/graphics/definitions/star.ts b/apps/web/src/lib/graphics/definitions/star.ts
index c7f9ef56..2160c125 100644
--- a/apps/web/src/lib/graphics/definitions/star.ts
+++ b/apps/web/src/lib/graphics/definitions/star.ts
@@ -1,109 +1,109 @@
-import type { ParamDefinition } from "@/lib/params";
-import { applyAlignedStroke } from "../stroke";
-import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
-import type { GraphicDefinition } from "../types";
-
-interface StarParams {
- fill: string;
- stroke: string;
- strokeWidth: number;
- strokeAlign: GraphicStrokeAlign;
- points: number;
- depth: number;
-}
-
-const STAR_PARAMS: ParamDefinition<keyof StarParams & 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: "points",
- label: "Points",
- type: "number",
- default: 5,
- min: 3,
- max: 12,
- step: 1,
- shortLabel: "P",
- },
- {
- key: "depth",
- label: "Depth",
- type: "number",
- default: 45,
- min: 1,
- max: 99,
- step: 1,
- shortLabel: "D",
- },
-];
-
-export const starGraphicDefinition: GraphicDefinition = {
- id: "star",
- name: "Star",
- keywords: ["star", "sparkle", "burst"],
- params: STAR_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 points = Math.max(3, Math.min(12, Math.round(Number(params.points ?? 5))));
- const depth = Math.max(1, Math.min(99, Number(params.depth ?? 45))) / 100;
- const inset = strokeAlign === "center" ? strokeWidth / 2 : 0;
- const outerRadius = Math.max(1, Math.min(width, height) / 2 - inset);
- const innerRadius = outerRadius * depth;
- const centerX = width / 2;
- const centerY = height / 2;
-
- ctx.clearRect(0, 0, width, height);
- const path = new Path2D();
- for (let index = 0; index < points * 2; index++) {
- const radius = index % 2 === 0 ? outerRadius : innerRadius;
- const angle = -Math.PI / 2 + (index * Math.PI) / points;
- const x = centerX + Math.cos(angle) * radius;
- const y = centerY + Math.sin(angle) * radius;
-
- if (index === 0) {
- path.moveTo(x, y);
- } else {
- path.lineTo(x, y);
- }
- }
- path.closePath();
- ctx.fillStyle = fill;
- ctx.fill(path);
-
- if (strokeWidth > 0) {
- applyAlignedStroke({
- ctx,
- path,
- strokeWidth,
- strokeAlign,
- strokeColor: stroke,
- });
- }
- },
-};
+import type { ParamDefinition } from "@/lib/params";
+import { applyAlignedStroke } from "../stroke";
+import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
+import type { GraphicDefinition } from "../types";
+
+interface StarParams {
+ fill: string;
+ stroke: string;
+ strokeWidth: number;
+ strokeAlign: GraphicStrokeAlign;
+ points: number;
+ depth: number;
+}
+
+const STAR_PARAMS: ParamDefinition<keyof StarParams & 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: "points",
+ label: "Points",
+ type: "number",
+ default: 5,
+ min: 3,
+ max: 12,
+ step: 1,
+ shortLabel: "P",
+ },
+ {
+ key: "depth",
+ label: "Depth",
+ type: "number",
+ default: 45,
+ min: 1,
+ max: 99,
+ step: 1,
+ shortLabel: "D",
+ },
+];
+
+export const starGraphicDefinition: GraphicDefinition = {
+ id: "star",
+ name: "Star",
+ keywords: ["star", "sparkle", "burst"],
+ params: STAR_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 points = Math.max(3, Math.min(12, Math.round(Number(params.points ?? 5))));
+ const depth = Math.max(1, Math.min(99, Number(params.depth ?? 45))) / 100;
+ const inset = strokeAlign === "center" ? strokeWidth / 2 : 0;
+ const outerRadius = Math.max(1, Math.min(width, height) / 2 - inset);
+ const innerRadius = outerRadius * depth;
+ const centerX = width / 2;
+ const centerY = height / 2;
+
+ ctx.clearRect(0, 0, width, height);
+ const path = new Path2D();
+ for (let index = 0; index < points * 2; index++) {
+ const radius = index % 2 === 0 ? outerRadius : innerRadius;
+ const angle = -Math.PI / 2 + (index * Math.PI) / points;
+ const x = centerX + Math.cos(angle) * radius;
+ const y = centerY + Math.sin(angle) * radius;
+
+ if (index === 0) {
+ path.moveTo(x, y);
+ } else {
+ path.lineTo(x, y);
+ }
+ }
+ path.closePath();
+ ctx.fillStyle = fill;
+ ctx.fill(path);
+
+ if (strokeWidth > 0) {
+ applyAlignedStroke({
+ ctx,
+ path,
+ strokeWidth,
+ strokeAlign,
+ strokeColor: stroke,
+ });
+ }
+ },
+};