packages/core/src/lib/geometry/circular-sweep.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.