-
-
Notifications
You must be signed in to change notification settings - Fork 639
/
path2d.js
284 lines (244 loc) · 11.7 KB
/
path2d.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
import pool from "./../system/pooling.js";
import { TAU } from "./../math/math.js";
import earcut from "earcut";
/**
* @classdesc
* a simplified path2d implementation, supporting only one path
*/
export default class Path2D {
constructor() {
/**
* the points defining the current path
* @type {Point[]}
*/
this.points = [];
/**
* space between interpolated points for quadratic and bezier curve approx. in pixels.
* @type {number}
* @default 5
*/
this.arcResolution = 5;
/* @ignore */
this.vertices = [];
}
/**
* begin a new path
*/
beginPath() {
// empty the cache and recycle all vectors
this.points.forEach((point) => {
pool.push(point);
});
this.points.length = 0;
}
/**
* causes the point of the pen to move back to the start of the current path.
* It tries to draw a straight line from the current point to the start.
* If the shape has already been closed or has only one point, this function does nothing.
*/
closePath() {
var points = this.points;
if (points.length > 1 && !points[points.length-1].equals(points[0])) {
points.push(pool.pull("Point", points[0].x, points[0].y));
}
}
/**
* triangulate the shape defined by this path into an array of triangles
* @returns {Point[]}
*/
triangulatePath() {
var i = 0;
var points = this.points;
var vertices = this.vertices;
var indices = earcut(points.flatMap(p => [p.x, p.y]));
// pre-allocate vertices if necessary
while (vertices.length < indices.length) {
vertices.push(pool.pull("Point"));
}
// calculate all vertices
for (i = 0; i < indices.length; i++ ) {
var point = points[indices[i]];
vertices[i].set(point.x, point.y);
}
// recycle overhead from a previous triangulation
while (vertices.length > indices.length) {
pool.push(vertices[vertices.length-1]);
vertices.length -= 1;
}
return vertices;
}
/**
* moves the starting point of the current path to the (x, y) coordinates.
* @param {number} x - the x-axis (horizontal) coordinate of the point.
* @param {number} y - the y-axis (vertical) coordinate of the point.
*/
moveTo(x, y) {
this.points.push(pool.pull("Point", x, y));
}
/**
* connects the last point in the current patch to the (x, y) coordinates with a straight line.
* @param {number} x - the x-axis coordinate of the line's end point.
* @param {number} y - the y-axis coordinate of the line's end point.
*/
lineTo(x, y) {
this.points.push(pool.pull("Point", x, y));
}
/**
* adds an arc to the current path which is centered at (x, y) position with the given radius,
* starting at startAngle and ending at endAngle going in the given direction by counterclockwise (defaulting to clockwise).
* @param {number} x - the horizontal coordinate of the arc's center.
* @param {number} y - the vertical coordinate of the arc's center.
* @param {number} radius - the arc's radius. Must be positive.
* @param {number} startAngle - the angle at which the arc starts in radians, measured from the positive x-axis.
* @param {number} endAngle - the angle at which the arc ends in radians, measured from the positive x-axis.
* @param {boolean} [anticlockwise=false] - an optional boolean value. If true, draws the arc counter-clockwise between the start and end angles.
*/
arc(x, y, radius, startAngle, endAngle, anticlockwise = false) {
var points = this.points;
// based on from https://github.com/karellodewijk/canvas-webgl/blob/master/canvas-webgl.js
//bring angles all in [0, 2*PI] range
if (startAngle === endAngle) return;
var fullCircle = anticlockwise ? Math.abs(startAngle-endAngle) >= (TAU) : Math.abs(endAngle-startAngle) >= (TAU);
startAngle = startAngle % (TAU);
endAngle = endAngle % (TAU);
if (startAngle < 0) startAngle += TAU;
if (endAngle < 0) endAngle += TAU;
if (startAngle >= endAngle) {
endAngle+= TAU;
}
var diff = endAngle - startAngle;
var direction = 1;
if (anticlockwise) {
direction = -1;
diff = TAU - diff;
}
if (fullCircle) diff = TAU;
var length = diff * radius;
var nr_of_interpolation_points = length / this.arcResolution;
var dangle = diff / nr_of_interpolation_points;
var angle = startAngle;
for (var j = 0; j < nr_of_interpolation_points; j++) {
points.push(pool.pull("Point", x + radius * Math.cos(angle), y + radius * Math.sin(angle)));
angle += direction * dangle;
}
points.push(pool.pull("Point", x + radius * Math.cos(endAngle), y + radius * Math.sin(endAngle)));
}
/**
* adds a circular arc to the path with the given control points and radius, connected to the previous point by a straight line.
* @param {number} x1 - the x-axis coordinate of the first control point.
* @param {number} y1 - the y-axis coordinate of the first control point.
* @param {number} x2 - the x-axis coordinate of the second control point.
* @param {number} y2 - the y-axis coordinate of the second control point.
* @param {number} radius - the arc's radius. Must be positive.
*/
arcTo(x1, y1, x2, y2, radius) {
var points = this.points;
// based on from https://github.com/karellodewijk/canvas-webgl/blob/master/canvas-webgl.js
var x0 = points[points.length-1].x, y0 = points[points.length-1].y;
//a = -incoming vector, b = outgoing vector to x1, y1
var a = [x0 - x1, y0 - y1];
var b = [x2 - x1, y2 - y1];
//normalize
var l_a = Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2));
var l_b = Math.sqrt(Math.pow(b[0], 2) + Math.pow(b[1], 2));
a[0] /= l_a; a[1] /= l_a; b[0] /= l_b; b[1] /= l_b;
var angle = Math.atan2(a[1], a[0]) - Math.atan2(b[1], b[0]);
//work out tangent points using tan(θ) = opposite / adjacent; angle/2 because hypotenuse is the bisection of a,b
var tan_angle_div2 = Math.tan(angle/2);
var adj_l = (radius/tan_angle_div2);
var tangent_point1 = [x1 + a[0] * adj_l, y1 + a[1] * adj_l];
var tangent_point2 = [x1 + b[0] * adj_l, y1 + b[1] * adj_l];
points.push(pool.pull("Point", tangent_point1[0], tangent_point1[1]));
var bisec = [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0];
var bisec_l = Math.sqrt(Math.pow(bisec[0], 2) + Math.pow(bisec[1], 2));
bisec[0] /= bisec_l; bisec[1] /= bisec_l;
var hyp_l = Math.sqrt(Math.pow(radius, 2) + Math.pow(adj_l, 2));
var center = [x1 + hyp_l * bisec[0], y1 + hyp_l * bisec[1]];
var startAngle = Math.atan2(tangent_point1[1] - center[1], tangent_point1[0] - center[0]);
var endAngle = Math.atan2(tangent_point2[1] - center[1], tangent_point2[0] - center[0]);
this.arc(center[0], center[1], radius, startAngle, endAngle);
}
/**
* adds an elliptical arc to the path which is centered at (x, y) position with the radii radiusX and radiusY
* starting at startAngle and ending at endAngle going in the given direction by counterclockwise.
* @param {number} x - the x-axis (horizontal) coordinate of the ellipse's center.
* @param {number} y - the y-axis (vertical) coordinate of the ellipse's center.
* @param {number} radiusX - the ellipse's major-axis radius. Must be non-negative.
* @param {number} radiusY - the ellipse's minor-axis radius. Must be non-negative.
* @param {number} rotation - the rotation of the ellipse, expressed in radians.
* @param {number} startAngle - the angle at which the ellipse starts, measured clockwise from the positive x-axis and expressed in radians.
* @param {number} endAngle - the angle at which the ellipse ends, measured clockwise from the positive x-axis and expressed in radians.
* @param {boolean} [anticlockwise=false] - an optional boolean value which, if true, draws the ellipse counterclockwise (anticlockwise).
*/
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise = false) {
var points = this.points;
// based on from https://github.com/karellodewijk/canvas-webgl/blob/master/canvas-webgl.js
if (startAngle === endAngle) return;
var fullCircle = anticlockwise ? Math.abs(startAngle-endAngle) >= (TAU) : Math.abs(endAngle-startAngle) >= (TAU);
//bring angles all in [0, 2*PI] range
startAngle = startAngle % (TAU);
endAngle = endAngle % (TAU);
if (startAngle < 0) startAngle += TAU;
if (endAngle < 0) endAngle += TAU;
if (startAngle>=endAngle) {
endAngle += TAU;
}
var diff = endAngle - startAngle;
var direction = 1;
if (anticlockwise) {
direction = -1;
diff = TAU - diff;
}
if (fullCircle) diff = TAU;
var length = (diff * radiusX + diff * radiusY) / 2;
var nr_of_interpolation_points = length / this.arcResolution;
var dangle = diff / nr_of_interpolation_points;
var angle = startAngle;
var cos_rotation = Math.cos(rotation);
var sin_rotation = Math.sin(rotation);
for (var j = 0; j < nr_of_interpolation_points; j++) {
var _x1 = radiusX * Math.cos(angle);
var _y1 = radiusY * Math.sin(angle);
var _x2 = x + _x1 * cos_rotation - _y1 * sin_rotation;
var _y2 = y + _x1 * sin_rotation + _y1 * cos_rotation;
points.push(pool.pull("Point", _x2, _y2));
angle += direction * dangle;
}
}
/**
* creates a path for a rectangle at position (x, y) with a size that is determined by width and height.
* @param {number} x - the x-axis coordinate of the rectangle's starting point.
* @param {number} y - the y-axis coordinate of the rectangle's starting point.
* @param {number} width - the rectangle's width. Positive values are to the right, and negative to the left.
* @param {number} height - the rectangle's height. Positive values are down, and negative are up.
*/
rect(x, y, width, height) {
this.moveTo(x, y);
this.lineTo(x + width, y);
this.moveTo(x + width, y);
this.lineTo(x + width, y + height);
this.moveTo(x + width, y + height);
this.lineTo(x, y + height);
this.moveTo(x, y + height);
this.lineTo(x, y);
}
/**
* adds an rounded rectangle to the current path.
* @param {number} x - the x-axis coordinate of the rectangle's starting point.
* @param {number} y - the y-axis coordinate of the rectangle's starting point.
* @param {number} width - the rectangle's width. Positive values are to the right, and negative to the left.
* @param {number} height - the rectangle's height. Positive values are down, and negative are up.
* @param {number} radius - the arc's radius to draw the borders. Must be positive.
*/
roundRect(x, y, width, height, radius) {
this.moveTo(x + radius, y);
this.lineTo(x + width - radius, y);
this.arcTo(x + width, y, x + width, y + radius, radius);
this.lineTo(x + width, y + height - radius);
this.arcTo(x + width, y + height, x + width - radius, y + height, radius);
this.lineTo(x + radius, y + height);
this.arcTo(x, y + height, x, y + height - radius, radius);
this.lineTo(x, y + radius);
this.arcTo(x, y, x + radius, y, radius);
}
}