Skip to content

packages/core/src/lib/geometry/circular-sweep.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import { finiteNumber, nonnegativeNumber } from "./validation.js";
2 
3 const fullTurn = Math.PI * 2;
4 
5 export interface CircularSweep {
6   readonly startAngle: number;
7   readonly endAngle: number;
8   readonly sweep: number;
9   readonly counterclockwise: boolean;
10   readonly fullCircle: boolean;
11 }
12 
13 /**
14  * Geometry and construction options for a arc path.
15  *
16  * @see {@link createArcPath}
17  * @see {@link WedgePathOptions}
18  */
19 export interface ArcPathOptions {
20   /** Horizontal coordinate of the center. See {@link ArcPathOptions}. */
21   cx?: number;
22   /** Vertical coordinate of the center. See {@link ArcPathOptions}. */
23   cy?: number;
24   /** Radius in the coordinate system of this geometry or effect. See {@link ArcPathOptions}. */
25   radius: number;
26   /** Angle at which the arc or slice begins. See {@link ArcPathOptions}. */
27   startAngle: number;
28   /** Angle at which the arc or slice ends. See {@link ArcPathOptions}. */
29   endAngle: number;
30   /** Whether the arc follows the counterclockwise direction. See {@link ArcPathOptions}. */
31   counterclockwise?: boolean;
32 }
33 
34 /**
35  * Geometry and construction options for a wedge path.
36  *
37  * @see {@link ArcPathOptions}
38  * @see {@link createWedgePath}
39  */
40 export type WedgePathOptions = ArcPathOptions;
41 
42 export function resolveCircularSweep(
43   startAngle: number,
44   endAngle: number,
45   counterclockwise: boolean,
46 ): CircularSweep {
47   const fullCircle = counterclockwise
48     ? startAngle - endAngle >= fullTurn
49     : endAngle - startAngle >= fullTurn;
50   let sweep: number;
51   if (fullCircle) {
52     sweep = counterclockwise ? -fullTurn : fullTurn;
53   } else if (counterclockwise) {
54     const distance = positiveModulo(startAngle - endAngle, fullTurn);
55     sweep = distance === 0 ? 0 : -distance;
56   } else {
57     sweep = positiveModulo(endAngle - startAngle, fullTurn);
58   }
59 
60   return {
61     startAngle,
62     endAngle: startAngle + sweep,
63     sweep,
64     counterclockwise,
65     fullCircle,
66   };
67 }
68 
69 /**
70  * Creates a Canvas Path2D for arc geometry from validated options.
71  *
72  * @param options - Circle center, radius, angles, and sweep direction. See {@link ArcPathOptions}.
73  * @returns A native Canvas path containing the arc.
74  *
75  * @see {@link ArcPathOptions}
76  */
77 export function createArcPath(options: Readonly<ArcPathOptions>): Path2D {
78   const normalized = validateArcOptions(options);
79   const sweep = resolveCircularSweep(
80     normalized.startAngle,
81     normalized.endAngle,
82     normalized.counterclockwise,
83   );
84   return buildArcPath(normalized.cx, normalized.cy, normalized.radius, sweep);
85 }
86 
87 /**
88  * Creates a Canvas Path2D for wedge geometry from validated options.
89  *
90  * @param options - Circle center, radius, angles, and sweep direction. See
91  * {@link WedgePathOptions} .
92  * @returns A native Canvas path containing the closed wedge.
93  *
94  * @see {@link WedgePathOptions}
95  */
96 export function createWedgePath(options: Readonly<WedgePathOptions>): Path2D {
97   const normalized = validateArcOptions(options);
98   const sweep = resolveCircularSweep(
99     normalized.startAngle,
100     normalized.endAngle,
101     normalized.counterclockwise,
102   );
103   return buildWedgePath(normalized.cx, normalized.cy, normalized.radius, sweep);
104 }
105 
106 function buildArcPath(
107   cx: number,
108   cy: number,
109   radius: number,
110   sweep: Readonly<CircularSweep>,
111 ): Path2D {
112   const path = new Path2D();
113   if (radius > 0 && sweep.sweep !== 0) {
114     path.arc(
115       cx,
116       cy,
117       radius,
118       sweep.startAngle,
119       sweep.endAngle,
120       sweep.counterclockwise,
121     );
122   }
123   return path;
124 }
125 
126 function buildWedgePath(
127   cx: number,
128   cy: number,
129   radius: number,
130   sweep: Readonly<CircularSweep>,
131 ): Path2D {
132   const path = new Path2D();
133   if (radius <= 0 || sweep.sweep === 0) return path;
134 
135   if (!sweep.fullCircle) {
136     path.moveTo(cx, cy);
137     path.lineTo(
138       cx + radius * Math.cos(sweep.startAngle),
139       cy + radius * Math.sin(sweep.startAngle),
140     );
141   }
142   path.arc(
143     cx,
144     cy,
145     radius,
146     sweep.startAngle,
147     sweep.endAngle,
148     sweep.counterclockwise,
149   );
150   path.closePath();
151   return path;
152 }
153 
154 function validateArcOptions(options: Readonly<ArcPathOptions>): {
155   cx: number;
156   cy: number;
157   radius: number;
158   startAngle: number;
159   endAngle: number;
160   counterclockwise: boolean;
161 } {
162   return {
163     cx: finiteNumber(options.cx ?? 0, "cx"),
164     cy: finiteNumber(options.cy ?? 0, "cy"),
165     radius: nonnegativeNumber(options.radius, "radius"),
166     startAngle: finiteNumber(options.startAngle, "startAngle"),
167     endAngle: finiteNumber(options.endAngle, "endAngle"),
168     counterclockwise: options.counterclockwise ?? false,
169   };
170 }
171 
172 function positiveModulo(value: number, divisor: number): number {
173   const result = value % divisor;
174   if (result === 0) return 0;
175   return result < 0 ? result + divisor : result;
176 }
177 

Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.