diff options
Diffstat (limited to 'apps/web/src/lib/graphics/definitions')
| -rw-r--r-- | apps/web/src/lib/graphics/definitions/ellipse.ts | 146 | ||||
| -rw-r--r-- | apps/web/src/lib/graphics/definitions/index.ts | 58 | ||||
| -rw-r--r-- | apps/web/src/lib/graphics/definitions/polygon.ts | 422 | ||||
| -rw-r--r-- | apps/web/src/lib/graphics/definitions/rectangle.ts | 170 | ||||
| -rw-r--r-- | apps/web/src/lib/graphics/definitions/shared.ts | 32 | ||||
| -rw-r--r-- | apps/web/src/lib/graphics/definitions/star.ts | 218 |
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,
+ });
+ }
+ },
+};
|
