- 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
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
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 = [];
/* @ignore */
this.startPoint = pool.pull("Point");
/* @ignore */
this.isDirty = false;
}
/**
* begin a new path
*/
beginPath() {
// empty the cache and recycle all vectors
this.points.forEach((point) => {
pool.push(point);
});
this.isDirty = true;
this.points.length = 0;
this.startPoint.set(0, 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() {
let points = this.points;
if (points.length > 0) {
let firstPoint = points[0];
if (!firstPoint.equals(points[points.length-1])) {
this.lineTo(firstPoint.x, firstPoint.y);
}
this.isDirty = true;
}
}
/**
* triangulate the shape defined by this path into an array of triangles
* @returns {Point[]} an array of vertices representing the triangulated path or shape
*/
triangulatePath() {
let vertices = this.vertices;
if (this.isDirty) {
let points = this.points;
let indices = earcut(points.flatMap(p => [p.x, p.y]));
let indicesLength = indices.length;
// pre-allocate vertices if necessary
while (vertices.length < indicesLength) {
vertices.push(pool.pull("Point"));
}
// calculate all vertices
for (let i = 0; i < indicesLength; i++ ) {
let point = points[indices[i]];
vertices[i].set(point.x, point.y);
}
// recycle overhead from a previous triangulation
while (vertices.length > indicesLength) {
pool.push(vertices[vertices.length-1]);
vertices.length -= 1;
}
this.isDirty = false;
}
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.startPoint.set(x, y);
this.isDirty = true;
}
/**
* connects the last point in the current path 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) {
let points = this.points;
let startPoint = this.startPoint;
let lastPoint = points.length === 0 ? startPoint : points[points.length-1];
if (!startPoint.equals(lastPoint)) {
points.push(pool.pull("Point", startPoint.x, startPoint.y));
} else {
points.push(pool.pull("Point", lastPoint.x, lastPoint.y));
}
points.push(pool.pull("Point", x, y));
startPoint.x = x;
startPoint.y = y;
this.isDirty = true;
}
/**
* 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) {
// 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;
const 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;
}
let diff = endAngle - startAngle;
let direction = 1;
if (anticlockwise) {
direction = -1;
diff = TAU - diff;
}
if (fullCircle) diff = TAU;
const length = diff * radius;
const nr_of_interpolation_points = length / this.arcResolution;
const dangle = diff / nr_of_interpolation_points;
const angleStep = dangle * direction;
this.moveTo(x + radius * Math.cos(startAngle), y + radius * Math.sin(startAngle));
let angle = startAngle;
for (let j = 0; j < nr_of_interpolation_points; j++) {
this.lineTo(x + radius * Math.cos(angle), y + radius * Math.sin(angle));
angle += angleStep;
}
this.lineTo(x + radius * Math.cos(endAngle), y + radius * Math.sin(endAngle));
this.isDirty = true;
}
/**
* 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) {
let points = this.points;
// based on from https://github.com/karellodewijk/canvas-webgl/blob/master/canvas-webgl.js
let x0 = points[points.length-1].x, y0 = points[points.length-1].y;
//a = -incoming vector, b = outgoing vector to x1, y1
let a0 = x0 - x1, a1 = y0 - y1;
let b0 = x2 - x1, b1 = y2 - y1;
//normalize
let l_a = Math.sqrt(Math.pow(a0, 2) + Math.pow(a1, 2));
let l_b = Math.sqrt(Math.pow(b0, 2) + Math.pow(b1, 2));
a0 /= l_a; a1 /= l_a; b0 /= l_b; b1 /= l_b;
let angle = Math.atan2(a1, a0) - Math.atan2(b1, b0);
//work out tangent points using tan(θ) = opposite / adjacent; angle/2 because hypotenuse is the bisection of a,b
let tan_angle_div2 = Math.tan(angle/2);
let adj_l = (radius/tan_angle_div2);
let tangent1_pointx = x1 + a0 * adj_l, tangent1_pointy = y1 + a1 * adj_l;
let tangent2_pointx = x1 + b0 * adj_l, tangent2_pointy = y1 + b1 * adj_l;
this.moveTo(tangent1_pointx, tangent1_pointy);
let bisec0 = (a0 + b0) / 2.0, bisec1 = (a1 + b1) / 2.0;
let bisec_l = Math.sqrt(Math.pow(bisec0, 2) + Math.pow(bisec1, 2));
bisec0 /= bisec_l; bisec1 /= bisec_l;
let hyp_l = Math.sqrt(Math.pow(radius, 2) + Math.pow(adj_l, 2));
let centerx = x1 + hyp_l * bisec0, centery = y1 + hyp_l * bisec1;
let startAngle = Math.atan2(tangent1_pointy - centery, tangent1_pointx - centerx);
let endAngle = Math.atan2(tangent2_pointy - centery, tangent2_pointx - centerx);
this.arc(centerx, centery, 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) {
// based on from https://github.com/karellodewijk/canvas-webgl/blob/master/canvas-webgl.js
if (startAngle === endAngle) return;
let 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;
}
let diff = endAngle - startAngle;
let direction = 1;
if (anticlockwise) {
direction = -1;
diff = TAU - diff;
}
if (fullCircle) diff = TAU;
const length = (diff * radiusX + diff * radiusY) / 2;
const nr_of_interpolation_points = length / this.arcResolution;
const dangle = diff / nr_of_interpolation_points;
const angleStep = dangle * direction;
let angle = startAngle;
const cos_rotation = Math.cos(rotation);
const sin_rotation = Math.sin(rotation);
this.moveTo(x + radiusX * Math.cos(startAngle), y + radiusY * Math.sin(startAngle));
for (let j = 0; j < nr_of_interpolation_points; j++) {
const _x1 = radiusX * Math.cos(angle);
const _y1 = radiusY * Math.sin(angle);
const _x2 = x + _x1 * cos_rotation - _y1 * sin_rotation;
const _y2 = y + _x1 * sin_rotation + _y1 * cos_rotation;
this.lineTo( _x2, _y2);
angle += angleStep;
}
// close the ellipse
this.lineTo(x + radiusX * Math.cos(startAngle), y + radiusY * Math.sin(startAngle));
this.isDirty = true;
}
/**
* 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);
this.isDirty = true;
}
/**
* 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.moveTo(x + width, y + radius);
this.lineTo(x + width, y + height - radius);
this.arcTo(x + width, y + height, x + width - radius, y + height, radius);
this.moveTo(x + width - radius, y + height);
this.lineTo(x + radius, y + height);
this.arcTo(x, y + height, x, y + height - radius, radius);
this.moveTo(x, y + height - radius);
this.lineTo(x, y + radius);
this.arcTo(x, y, x + radius, y, radius);
this.isDirty = true;
}
}