@kyralabs/m3e-shapes 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +50 -0
- package/README.md +20 -2
- package/dist/index.d.ts +437 -0
- package/dist/index.js +2031 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,2031 @@
|
|
|
1
|
+
// src/graphics-shapes/corner-rounding.ts
|
|
2
|
+
var CornerRounding = class _CornerRounding {
|
|
3
|
+
/** [Unrounded] has a rounding radius of zero, producing a sharp corner at a vertex. */
|
|
4
|
+
static Unrounded = new _CornerRounding();
|
|
5
|
+
radius;
|
|
6
|
+
smoothing;
|
|
7
|
+
/** Both values are Kotlin `Float`s (rounded to float32). */
|
|
8
|
+
constructor(radius = 0, smoothing = 0) {
|
|
9
|
+
this.radius = Math.fround(radius);
|
|
10
|
+
this.smoothing = Math.fround(smoothing);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// src/graphics-shapes/point.ts
|
|
15
|
+
var f = Math.fround;
|
|
16
|
+
var point = (x, y) => ({ x: f(x), y: f(y) });
|
|
17
|
+
var getDistance = (p) => f(Math.sqrt(getDistanceSquared(p)));
|
|
18
|
+
var getDistanceSquared = (p) => f(f(p.x * p.x) + f(p.y * p.y));
|
|
19
|
+
var dotProduct = (p, other) => f(f(p.x * other.x) + f(p.y * other.y));
|
|
20
|
+
var dotProductXY = (p, otherX, otherY) => f(f(p.x * otherX) + f(p.y * otherY));
|
|
21
|
+
var clockwise = (p, other) => f(f(p.x * other.y) - f(p.y * other.x)) > 0;
|
|
22
|
+
function getDirection(p) {
|
|
23
|
+
const d = getDistance(p);
|
|
24
|
+
if (!(d > 0)) throw new Error("Can't get the direction of a 0-length vector");
|
|
25
|
+
return div(p, d);
|
|
26
|
+
}
|
|
27
|
+
var minus = (p, other) => point(p.x - other.x, p.y - other.y);
|
|
28
|
+
var plus = (p, other) => point(p.x + other.x, p.y + other.y);
|
|
29
|
+
var times = (p, operand) => point(p.x * operand, p.y * operand);
|
|
30
|
+
var div = (p, operand) => point(p.x / operand, p.y / operand);
|
|
31
|
+
var interpolatePoint = (start, stop, fraction) => point(
|
|
32
|
+
f(f(f(1 - fraction) * start.x) + f(fraction * stop.x)),
|
|
33
|
+
f(f(f(1 - fraction) * start.y) + f(fraction * stop.y))
|
|
34
|
+
);
|
|
35
|
+
function transformed(p, f4) {
|
|
36
|
+
const [x, y] = f4(p.x, p.y);
|
|
37
|
+
return point(x, y);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/graphics-shapes/utils.ts
|
|
41
|
+
var f2 = Math.fround;
|
|
42
|
+
var FLOAT_MIN_VALUE = 1401298464324817e-60;
|
|
43
|
+
var FLOAT_MAX_VALUE = 34028234663852886e22;
|
|
44
|
+
var distance = (x, y) => f2(Math.sqrt(f2(f2(x * x) + f2(y * y))));
|
|
45
|
+
var distanceSquared = (x, y) => f2(f2(x * x) + f2(y * y));
|
|
46
|
+
function directionVector(x, y) {
|
|
47
|
+
const d = distance(x, y);
|
|
48
|
+
if (!(d > 0)) throw new Error("Required distance greater than zero");
|
|
49
|
+
return point(f2(x / d), f2(y / d));
|
|
50
|
+
}
|
|
51
|
+
var directionVectorFromAngle = (angleRadians) => point(f2(Math.cos(angleRadians)), f2(Math.sin(angleRadians)));
|
|
52
|
+
var DistanceEpsilon = f2(1e-4);
|
|
53
|
+
var AngleEpsilon = f2(1e-6);
|
|
54
|
+
var Zero = point(0, 0);
|
|
55
|
+
var FloatPi = f2(Math.PI);
|
|
56
|
+
var TwoPi = f2(2 * FloatPi);
|
|
57
|
+
var angle = (x, y) => f2(f2(f2(Math.atan2(y, x)) + TwoPi) % TwoPi);
|
|
58
|
+
var radialToCartesian = (radius, angleRadians, center = Zero) => plus(times(directionVectorFromAngle(angleRadians), radius), center);
|
|
59
|
+
var rotate90 = (p) => point(-p.y, p.x);
|
|
60
|
+
var square = (x) => f2(x * x);
|
|
61
|
+
var interpolate = (start, stop, fraction) => f2(f2(f2(1 - fraction) * start) + f2(fraction * stop));
|
|
62
|
+
var positiveModulo = (num, mod) => f2(f2(f2(num % mod) + mod) % mod);
|
|
63
|
+
function findMinimum(v0, v1, tolerance, fn) {
|
|
64
|
+
let a = v0;
|
|
65
|
+
let b = v1;
|
|
66
|
+
while (f2(b - a) > tolerance) {
|
|
67
|
+
const c1 = f2(f2(f2(2 * a) + b) / 3);
|
|
68
|
+
const c2 = f2(f2(f2(2 * b) + a) / 3);
|
|
69
|
+
if (fn(c1) < fn(c2)) {
|
|
70
|
+
b = c2;
|
|
71
|
+
} else {
|
|
72
|
+
a = c1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return f2(f2(a + b) / 2);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/graphics-shapes/cubic.ts
|
|
79
|
+
var inUnitRange = (t) => t >= 0 && t <= 1;
|
|
80
|
+
var Cubic = class _Cubic {
|
|
81
|
+
/** Kotlin `FloatArray`: values are stored as float32. */
|
|
82
|
+
points;
|
|
83
|
+
constructor(points = new Float32Array(8)) {
|
|
84
|
+
if (points.length !== 8) throw new Error("Points array size should be 8");
|
|
85
|
+
this.points = Float32Array.from(points);
|
|
86
|
+
}
|
|
87
|
+
/** Creates a Cubic from its eight coordinates (the top-level Kotlin `Cubic(...)` factory). */
|
|
88
|
+
static of(anchor0X, anchor0Y, control0X, control0Y, control1X, control1Y, anchor1X, anchor1Y) {
|
|
89
|
+
return new _Cubic([
|
|
90
|
+
anchor0X,
|
|
91
|
+
anchor0Y,
|
|
92
|
+
control0X,
|
|
93
|
+
control0Y,
|
|
94
|
+
control1X,
|
|
95
|
+
control1Y,
|
|
96
|
+
anchor1X,
|
|
97
|
+
anchor1Y
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
static fromPoints(anchor0, control0, control1, anchor1) {
|
|
101
|
+
return _Cubic.of(
|
|
102
|
+
anchor0.x,
|
|
103
|
+
anchor0.y,
|
|
104
|
+
control0.x,
|
|
105
|
+
control0.y,
|
|
106
|
+
control1.x,
|
|
107
|
+
control1.y,
|
|
108
|
+
anchor1.x,
|
|
109
|
+
anchor1.y
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
get anchor0X() {
|
|
113
|
+
return this.points[0];
|
|
114
|
+
}
|
|
115
|
+
get anchor0Y() {
|
|
116
|
+
return this.points[1];
|
|
117
|
+
}
|
|
118
|
+
get control0X() {
|
|
119
|
+
return this.points[2];
|
|
120
|
+
}
|
|
121
|
+
get control0Y() {
|
|
122
|
+
return this.points[3];
|
|
123
|
+
}
|
|
124
|
+
get control1X() {
|
|
125
|
+
return this.points[4];
|
|
126
|
+
}
|
|
127
|
+
get control1Y() {
|
|
128
|
+
return this.points[5];
|
|
129
|
+
}
|
|
130
|
+
get anchor1X() {
|
|
131
|
+
return this.points[6];
|
|
132
|
+
}
|
|
133
|
+
get anchor1Y() {
|
|
134
|
+
return this.points[7];
|
|
135
|
+
}
|
|
136
|
+
pointOnCurve(t) {
|
|
137
|
+
const u = f2(1 - t);
|
|
138
|
+
const w0 = f2(f2(u * u) * u);
|
|
139
|
+
const w1 = f2(f2(f2(3 * t) * u) * u);
|
|
140
|
+
const w2 = f2(f2(f2(3 * t) * t) * u);
|
|
141
|
+
const w3 = f2(f2(t * t) * t);
|
|
142
|
+
return point(
|
|
143
|
+
f2(
|
|
144
|
+
f2(f2(f2(this.anchor0X * w0) + f2(this.control0X * w1)) + f2(this.control1X * w2)) + f2(this.anchor1X * w3)
|
|
145
|
+
),
|
|
146
|
+
f2(
|
|
147
|
+
f2(f2(f2(this.anchor0Y * w0) + f2(this.control0Y * w1)) + f2(this.control1Y * w2)) + f2(this.anchor1Y * w3)
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
zeroLength() {
|
|
152
|
+
return Math.abs(f2(this.anchor0X - this.anchor1X)) < DistanceEpsilon && Math.abs(f2(this.anchor0Y - this.anchor1Y)) < DistanceEpsilon;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Fills [bounds] with `[minX, minY, maxX, maxY]`. With [approximate], the bounding box of all
|
|
156
|
+
* anchors and controls; otherwise the exact extrema (solving the derivative quadratic).
|
|
157
|
+
*/
|
|
158
|
+
calculateBounds(bounds = [0, 0, 0, 0], approximate = false) {
|
|
159
|
+
if (this.zeroLength()) {
|
|
160
|
+
bounds[0] = this.anchor0X;
|
|
161
|
+
bounds[1] = this.anchor0Y;
|
|
162
|
+
bounds[2] = this.anchor0X;
|
|
163
|
+
bounds[3] = this.anchor0Y;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
let minX = Math.min(this.anchor0X, this.anchor1X);
|
|
167
|
+
let minY = Math.min(this.anchor0Y, this.anchor1Y);
|
|
168
|
+
let maxX = Math.max(this.anchor0X, this.anchor1X);
|
|
169
|
+
let maxY = Math.max(this.anchor0Y, this.anchor1Y);
|
|
170
|
+
if (approximate) {
|
|
171
|
+
bounds[0] = Math.min(minX, Math.min(this.control0X, this.control1X));
|
|
172
|
+
bounds[1] = Math.min(minY, Math.min(this.control0Y, this.control1Y));
|
|
173
|
+
bounds[2] = Math.max(maxX, Math.max(this.control0X, this.control1X));
|
|
174
|
+
bounds[3] = Math.max(maxY, Math.max(this.control0Y, this.control1Y));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const zeroIsh = (value) => Math.abs(value) < DistanceEpsilon;
|
|
178
|
+
const includeX = (t) => {
|
|
179
|
+
if (!inUnitRange(t)) return;
|
|
180
|
+
const it = this.pointOnCurve(t).x;
|
|
181
|
+
if (it < minX) minX = it;
|
|
182
|
+
if (it > maxX) maxX = it;
|
|
183
|
+
};
|
|
184
|
+
const includeY = (t) => {
|
|
185
|
+
if (!inUnitRange(t)) return;
|
|
186
|
+
const it = this.pointOnCurve(t).y;
|
|
187
|
+
if (it < minY) minY = it;
|
|
188
|
+
if (it > maxY) maxY = it;
|
|
189
|
+
};
|
|
190
|
+
const xa = f2(
|
|
191
|
+
f2(f2(-this.anchor0X + f2(3 * this.control0X)) - f2(3 * this.control1X)) + this.anchor1X
|
|
192
|
+
);
|
|
193
|
+
const xb = f2(f2(f2(2 * this.anchor0X) - f2(4 * this.control0X)) + f2(2 * this.control1X));
|
|
194
|
+
const xc = f2(-this.anchor0X + this.control0X);
|
|
195
|
+
if (zeroIsh(xa)) {
|
|
196
|
+
if (xb !== 0) includeX(f2(f2(2 * xc) / f2(-2 * xb)));
|
|
197
|
+
} else {
|
|
198
|
+
const xs = f2(f2(xb * xb) - f2(f2(4 * xa) * xc));
|
|
199
|
+
if (xs >= 0) {
|
|
200
|
+
includeX(f2(f2(-xb + f2(Math.sqrt(xs))) / f2(2 * xa)));
|
|
201
|
+
includeX(f2(f2(-xb - f2(Math.sqrt(xs))) / f2(2 * xa)));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const ya = f2(
|
|
205
|
+
f2(f2(-this.anchor0Y + f2(3 * this.control0Y)) - f2(3 * this.control1Y)) + this.anchor1Y
|
|
206
|
+
);
|
|
207
|
+
const yb = f2(f2(f2(2 * this.anchor0Y) - f2(4 * this.control0Y)) + f2(2 * this.control1Y));
|
|
208
|
+
const yc = f2(-this.anchor0Y + this.control0Y);
|
|
209
|
+
if (zeroIsh(ya)) {
|
|
210
|
+
if (yb !== 0) includeY(f2(f2(2 * yc) / f2(-2 * yb)));
|
|
211
|
+
} else {
|
|
212
|
+
const ys = f2(f2(yb * yb) - f2(f2(4 * ya) * yc));
|
|
213
|
+
if (ys >= 0) {
|
|
214
|
+
includeY(f2(f2(-yb + f2(Math.sqrt(ys))) / f2(2 * ya)));
|
|
215
|
+
includeY(f2(f2(-yb - f2(Math.sqrt(ys))) / f2(2 * ya)));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
bounds[0] = minX;
|
|
219
|
+
bounds[1] = minY;
|
|
220
|
+
bounds[2] = maxX;
|
|
221
|
+
bounds[3] = maxY;
|
|
222
|
+
}
|
|
223
|
+
/** Returns two Cubics, created by splitting this curve at the given distance of [t]. */
|
|
224
|
+
split(t) {
|
|
225
|
+
const u = f2(1 - t);
|
|
226
|
+
const p = this.pointOnCurve(t);
|
|
227
|
+
const uu = f2(u * u);
|
|
228
|
+
const ut2 = f2(f2(2 * u) * t);
|
|
229
|
+
const tt = f2(t * t);
|
|
230
|
+
const lerp = (a, b) => f2(f2(a * u) + f2(b * t));
|
|
231
|
+
const quad = (a, b, c) => f2(f2(f2(a * uu) + f2(b * ut2)) + f2(c * tt));
|
|
232
|
+
return [
|
|
233
|
+
_Cubic.of(
|
|
234
|
+
this.anchor0X,
|
|
235
|
+
this.anchor0Y,
|
|
236
|
+
lerp(this.anchor0X, this.control0X),
|
|
237
|
+
lerp(this.anchor0Y, this.control0Y),
|
|
238
|
+
quad(this.anchor0X, this.control0X, this.control1X),
|
|
239
|
+
quad(this.anchor0Y, this.control0Y, this.control1Y),
|
|
240
|
+
p.x,
|
|
241
|
+
p.y
|
|
242
|
+
),
|
|
243
|
+
_Cubic.of(
|
|
244
|
+
p.x,
|
|
245
|
+
p.y,
|
|
246
|
+
quad(this.control0X, this.control1X, this.anchor1X),
|
|
247
|
+
quad(this.control0Y, this.control1Y, this.anchor1Y),
|
|
248
|
+
lerp(this.control1X, this.anchor1X),
|
|
249
|
+
lerp(this.control1Y, this.anchor1Y),
|
|
250
|
+
this.anchor1X,
|
|
251
|
+
this.anchor1Y
|
|
252
|
+
)
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
/** Utility function to reverse the control/anchor points for this curve. */
|
|
256
|
+
reverse() {
|
|
257
|
+
return _Cubic.of(
|
|
258
|
+
this.anchor1X,
|
|
259
|
+
this.anchor1Y,
|
|
260
|
+
this.control1X,
|
|
261
|
+
this.control1Y,
|
|
262
|
+
this.control0X,
|
|
263
|
+
this.control0Y,
|
|
264
|
+
this.anchor0X,
|
|
265
|
+
this.anchor0Y
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
plus(o) {
|
|
269
|
+
return new _Cubic(this.points.map((v, i) => v + o.points[i]));
|
|
270
|
+
}
|
|
271
|
+
times(x) {
|
|
272
|
+
return new _Cubic(this.points.map((v) => v * x));
|
|
273
|
+
}
|
|
274
|
+
div(x) {
|
|
275
|
+
return this.times(f2(1 / x));
|
|
276
|
+
}
|
|
277
|
+
toString() {
|
|
278
|
+
const [a0x, a0y, c0x, c0y, c1x, c1y, a1x, a1y] = Array.from(this.points, String);
|
|
279
|
+
return `anchor0: (${a0x}, ${a0y}) control0: (${c0x}, ${c0y}), control1: (${c1x}, ${c1y}), anchor1: (${a1x}, ${a1y})`;
|
|
280
|
+
}
|
|
281
|
+
equals(other) {
|
|
282
|
+
if (this === other) return true;
|
|
283
|
+
return other instanceof _Cubic && this.points.every((v, i) => v === other.points[i]);
|
|
284
|
+
}
|
|
285
|
+
/** Transforms the points in this [Cubic] with the given [PointTransformer]. */
|
|
286
|
+
transformed(f4) {
|
|
287
|
+
const newCubic = new MutableCubic();
|
|
288
|
+
newCubic.points.set(this.points);
|
|
289
|
+
newCubic.transform(f4);
|
|
290
|
+
return newCubic;
|
|
291
|
+
}
|
|
292
|
+
/** Generates a bezier curve that is a straight line between the given anchor points. */
|
|
293
|
+
static straightLine(x0, y0, x1, y1) {
|
|
294
|
+
return _Cubic.of(
|
|
295
|
+
x0,
|
|
296
|
+
y0,
|
|
297
|
+
interpolate(x0, x1, ONE_THIRD),
|
|
298
|
+
interpolate(y0, y1, ONE_THIRD),
|
|
299
|
+
interpolate(x0, x1, TWO_THIRDS),
|
|
300
|
+
interpolate(y0, y1, TWO_THIRDS),
|
|
301
|
+
x1,
|
|
302
|
+
y1
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Generates a bezier curve that approximates a circular arc, with p0 and p1 as the starting and
|
|
307
|
+
* ending anchor points. The curve generated is the smallest of the two possible arcs around the
|
|
308
|
+
* entire 360-degree circle. Arcs of greater than 180 degrees should use more than one arc
|
|
309
|
+
* together. Note that p0 and p1 should be equidistant from the center.
|
|
310
|
+
*/
|
|
311
|
+
static circularArc(centerX, centerY, x0, y0, x1, y1) {
|
|
312
|
+
const p0d = directionVector(f2(x0 - centerX), f2(y0 - centerY));
|
|
313
|
+
const p1d = directionVector(f2(x1 - centerX), f2(y1 - centerY));
|
|
314
|
+
const rotatedP0 = rotate90(p0d);
|
|
315
|
+
const rotatedP1 = rotate90(p1d);
|
|
316
|
+
const clockwise2 = dotProductXY(rotatedP0, f2(x1 - centerX), f2(y1 - centerY)) >= 0;
|
|
317
|
+
const cosa = dotProduct(p0d, p1d);
|
|
318
|
+
if (cosa > COS_NEARLY_ZERO_ANGLE) return _Cubic.straightLine(x0, y0, x1, y1);
|
|
319
|
+
const oneMinusCosa = f2(1 - cosa);
|
|
320
|
+
const k = f2(
|
|
321
|
+
f2(
|
|
322
|
+
f2(
|
|
323
|
+
f2(f2(distance(f2(x0 - centerX), f2(y0 - centerY)) * 4) / 3) * f2(f2(Math.sqrt(f2(2 * oneMinusCosa))) - f2(Math.sqrt(f2(1 - f2(cosa * cosa)))))
|
|
324
|
+
) / oneMinusCosa
|
|
325
|
+
) * (clockwise2 ? 1 : -1)
|
|
326
|
+
);
|
|
327
|
+
return _Cubic.of(
|
|
328
|
+
x0,
|
|
329
|
+
y0,
|
|
330
|
+
f2(x0 + f2(rotatedP0.x * k)),
|
|
331
|
+
f2(y0 + f2(rotatedP0.y * k)),
|
|
332
|
+
f2(x1 - f2(rotatedP1.x * k)),
|
|
333
|
+
f2(y1 - f2(rotatedP1.y * k)),
|
|
334
|
+
x1,
|
|
335
|
+
y1
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
var COS_NEARLY_ZERO_ANGLE = f2(0.999);
|
|
340
|
+
var ONE_THIRD = f2(1 / 3);
|
|
341
|
+
var TWO_THIRDS = f2(2 / 3);
|
|
342
|
+
var MutableCubic = class extends Cubic {
|
|
343
|
+
transformOnePoint(f4, ix) {
|
|
344
|
+
const [x, y] = f4(this.points[ix], this.points[ix + 1]);
|
|
345
|
+
this.points[ix] = x;
|
|
346
|
+
this.points[ix + 1] = y;
|
|
347
|
+
}
|
|
348
|
+
transform(f4) {
|
|
349
|
+
this.transformOnePoint(f4, 0);
|
|
350
|
+
this.transformOnePoint(f4, 2);
|
|
351
|
+
this.transformOnePoint(f4, 4);
|
|
352
|
+
this.transformOnePoint(f4, 6);
|
|
353
|
+
}
|
|
354
|
+
interpolate(c1, c2, progress) {
|
|
355
|
+
for (let i = 0; i < 8; i++) {
|
|
356
|
+
this.points[i] = interpolate(c1.points[i], c2.points[i], progress);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// src/graphics-shapes/features.ts
|
|
362
|
+
var Feature = class {
|
|
363
|
+
constructor(cubics) {
|
|
364
|
+
this.cubics = cubics;
|
|
365
|
+
}
|
|
366
|
+
cubics;
|
|
367
|
+
};
|
|
368
|
+
var Edge = class _Edge extends Feature {
|
|
369
|
+
transformed(f4) {
|
|
370
|
+
return new _Edge(this.cubics.map((c) => c.transformed(f4)));
|
|
371
|
+
}
|
|
372
|
+
toString() {
|
|
373
|
+
return "Edge";
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
var Corner = class _Corner extends Feature {
|
|
377
|
+
constructor(cubics, vertex, roundedCenter, convex = true) {
|
|
378
|
+
super(cubics);
|
|
379
|
+
this.vertex = vertex;
|
|
380
|
+
this.roundedCenter = roundedCenter;
|
|
381
|
+
this.convex = convex;
|
|
382
|
+
}
|
|
383
|
+
vertex;
|
|
384
|
+
roundedCenter;
|
|
385
|
+
convex;
|
|
386
|
+
transformed(f4) {
|
|
387
|
+
return new _Corner(
|
|
388
|
+
this.cubics.map((c) => c.transformed(f4)),
|
|
389
|
+
transformed(this.vertex, f4),
|
|
390
|
+
transformed(this.roundedCenter, f4),
|
|
391
|
+
this.convex
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
toString() {
|
|
395
|
+
return `Corner: vertex=(${String(this.vertex.x)}, ${String(this.vertex.y)}), center=(${String(this.roundedCenter.x)}, ${String(this.roundedCenter.y)}), convex=${String(this.convex)}`;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// src/graphics-shapes/float-mapping.ts
|
|
400
|
+
function progressInRange(progress, progressFrom, progressTo) {
|
|
401
|
+
return progressTo >= progressFrom ? progress >= progressFrom && progress <= progressTo : progress >= progressFrom || progress <= progressTo;
|
|
402
|
+
}
|
|
403
|
+
function linearMap(xValues, yValues, x) {
|
|
404
|
+
if (!(x >= 0 && x <= 1)) throw new Error(`Invalid progress: ${String(x)}`);
|
|
405
|
+
const n = xValues.length;
|
|
406
|
+
const segmentStartIndex = xValues.findIndex(
|
|
407
|
+
(_, it) => progressInRange(x, xValues[it], xValues[(it + 1) % n])
|
|
408
|
+
);
|
|
409
|
+
if (segmentStartIndex === -1)
|
|
410
|
+
throw new Error("Collection contains no element matching the predicate.");
|
|
411
|
+
const segmentEndIndex = (segmentStartIndex + 1) % n;
|
|
412
|
+
const segmentSizeX = positiveModulo(
|
|
413
|
+
f2(xValues[segmentEndIndex] - xValues[segmentStartIndex]),
|
|
414
|
+
1
|
|
415
|
+
);
|
|
416
|
+
const segmentSizeY = positiveModulo(
|
|
417
|
+
f2(yValues[segmentEndIndex] - yValues[segmentStartIndex]),
|
|
418
|
+
1
|
|
419
|
+
);
|
|
420
|
+
const positionInSegment = segmentSizeX < f2(1e-3) ? 0.5 : f2(positiveModulo(f2(x - xValues[segmentStartIndex]), 1) / segmentSizeX);
|
|
421
|
+
return positiveModulo(f2(yValues[segmentStartIndex] + f2(segmentSizeY * positionInSegment)), 1);
|
|
422
|
+
}
|
|
423
|
+
var DoubleMapper = class _DoubleMapper {
|
|
424
|
+
sourceValues = [];
|
|
425
|
+
targetValues = [];
|
|
426
|
+
constructor(...mappings) {
|
|
427
|
+
for (const [source, target] of mappings) {
|
|
428
|
+
this.sourceValues.push(f2(source));
|
|
429
|
+
this.targetValues.push(f2(target));
|
|
430
|
+
}
|
|
431
|
+
validateProgress(this.sourceValues);
|
|
432
|
+
validateProgress(this.targetValues);
|
|
433
|
+
}
|
|
434
|
+
map(x) {
|
|
435
|
+
return linearMap(this.sourceValues, this.targetValues, x);
|
|
436
|
+
}
|
|
437
|
+
mapBack(x) {
|
|
438
|
+
return linearMap(this.targetValues, this.sourceValues, x);
|
|
439
|
+
}
|
|
440
|
+
static Identity = new _DoubleMapper(
|
|
441
|
+
// We need any 2 points in the (x, x) diagonal, with x in the [0, 1) range,
|
|
442
|
+
// We spread them as much as possible to minimize float errors.
|
|
443
|
+
[0, 0],
|
|
444
|
+
[0.5, 0.5]
|
|
445
|
+
);
|
|
446
|
+
};
|
|
447
|
+
function validateProgress(p) {
|
|
448
|
+
if (!p.every((curr) => curr >= 0 && curr <= 1)) {
|
|
449
|
+
throw new Error(`FloatMapping - Progress outside of range: ${p.join(", ")}`);
|
|
450
|
+
}
|
|
451
|
+
let wraps = 0;
|
|
452
|
+
for (let it = 1; it < p.length; it++) if (p[it] < p[it - 1]) wraps++;
|
|
453
|
+
if (wraps > 1) throw new Error(`FloatMapping - Progress wraps more than once: ${p.join(", ")}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/graphics-shapes/feature-mapping.ts
|
|
457
|
+
function minBy(from, toInclusive, key) {
|
|
458
|
+
let best = from;
|
|
459
|
+
let bestKey = key(from);
|
|
460
|
+
for (let i = from + 1; i <= toInclusive; i++) {
|
|
461
|
+
const k = key(i);
|
|
462
|
+
if (k < bestKey) {
|
|
463
|
+
best = i;
|
|
464
|
+
bestKey = k;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return best;
|
|
468
|
+
}
|
|
469
|
+
function featureMapper(features1, features2) {
|
|
470
|
+
const filteredFeatures1 = features1.filter((f4) => f4.feature instanceof Corner);
|
|
471
|
+
const filteredFeatures2 = features2.filter((f4) => f4.feature instanceof Corner);
|
|
472
|
+
const [m1, m2] = filteredFeatures1.length > filteredFeatures2.length ? [doMapping(filteredFeatures2, filteredFeatures1), filteredFeatures2] : [filteredFeatures1, doMapping(filteredFeatures1, filteredFeatures2)];
|
|
473
|
+
const mm = [];
|
|
474
|
+
for (let i = 0; i < m1.length; i++) {
|
|
475
|
+
if (i === m2.length) break;
|
|
476
|
+
mm.push([m1[i].progress, m2[i].progress]);
|
|
477
|
+
}
|
|
478
|
+
return new DoubleMapper(...mm);
|
|
479
|
+
}
|
|
480
|
+
function featureDistSquared(f1, f22) {
|
|
481
|
+
if (f1 instanceof Corner && f22 instanceof Corner && f1.convex !== f22.convex) {
|
|
482
|
+
return FLOAT_MAX_VALUE;
|
|
483
|
+
}
|
|
484
|
+
const first1 = f1.cubics[0];
|
|
485
|
+
const last1 = f1.cubics[f1.cubics.length - 1];
|
|
486
|
+
const first2 = f22.cubics[0];
|
|
487
|
+
const last2 = f22.cubics[f22.cubics.length - 1];
|
|
488
|
+
const c1x = f2(f2(first1.anchor0X + last1.anchor1X) / 2);
|
|
489
|
+
const c1y = f2(f2(first1.anchor0Y + last1.anchor1Y) / 2);
|
|
490
|
+
const c2x = f2(f2(first2.anchor0X + last2.anchor1X) / 2);
|
|
491
|
+
const c2y = f2(f2(first2.anchor0Y + last2.anchor1Y) / 2);
|
|
492
|
+
const dx = f2(c1x - c2x);
|
|
493
|
+
const dy = f2(c1y - c2y);
|
|
494
|
+
return f2(f2(dx * dx) + f2(dy * dy));
|
|
495
|
+
}
|
|
496
|
+
function doMapping(f1, f22) {
|
|
497
|
+
const ix = minBy(0, f22.length - 1, (it) => featureDistSquared(f1[0].feature, f22[it].feature));
|
|
498
|
+
const m = f1.length;
|
|
499
|
+
const n = f22.length;
|
|
500
|
+
const ret = [f22[ix]];
|
|
501
|
+
let lastPicked = ix;
|
|
502
|
+
for (let i = 1; i < m; i++) {
|
|
503
|
+
let last = ix - (m - i);
|
|
504
|
+
if (!(last > lastPicked)) last += n;
|
|
505
|
+
const best = minBy(
|
|
506
|
+
lastPicked + 1,
|
|
507
|
+
last,
|
|
508
|
+
(it) => featureDistSquared(f1[i].feature, f22[it % n].feature)
|
|
509
|
+
);
|
|
510
|
+
ret.push(f22[best % n]);
|
|
511
|
+
lastPicked = best;
|
|
512
|
+
}
|
|
513
|
+
return ret;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/graphics-shapes/polygon-measure.ts
|
|
517
|
+
var ProgressableFeature = class {
|
|
518
|
+
constructor(progress, feature) {
|
|
519
|
+
this.progress = progress;
|
|
520
|
+
this.feature = feature;
|
|
521
|
+
}
|
|
522
|
+
progress;
|
|
523
|
+
feature;
|
|
524
|
+
};
|
|
525
|
+
var AngleMeasurer = class {
|
|
526
|
+
constructor(centerX, centerY) {
|
|
527
|
+
this.centerX = centerX;
|
|
528
|
+
this.centerY = centerY;
|
|
529
|
+
}
|
|
530
|
+
centerX;
|
|
531
|
+
centerY;
|
|
532
|
+
measureCubic(c) {
|
|
533
|
+
const it = positiveModulo(
|
|
534
|
+
f2(
|
|
535
|
+
angle(f2(c.anchor1X - this.centerX), f2(c.anchor1Y - this.centerY)) - angle(f2(c.anchor0X - this.centerX), f2(c.anchor0Y - this.centerY))
|
|
536
|
+
),
|
|
537
|
+
TwoPi
|
|
538
|
+
);
|
|
539
|
+
return it > f2(TwoPi - DistanceEpsilon) ? 0 : it;
|
|
540
|
+
}
|
|
541
|
+
findCubicCutPoint(c, m) {
|
|
542
|
+
const angle0 = angle(f2(c.anchor0X - this.centerX), f2(c.anchor0Y - this.centerY));
|
|
543
|
+
return findMinimum(0, 1, f2(1e-5), (t) => {
|
|
544
|
+
const curvePoint = c.pointOnCurve(t);
|
|
545
|
+
const a = angle(f2(curvePoint.x - this.centerX), f2(curvePoint.y - this.centerY));
|
|
546
|
+
return Math.abs(f2(positiveModulo(f2(a - angle0), TwoPi) - m));
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
var MeasuredCubic = class _MeasuredCubic {
|
|
551
|
+
constructor(measurer, cubic, _startOutlineProgress, _endOutlineProgress) {
|
|
552
|
+
this.measurer = measurer;
|
|
553
|
+
this.cubic = cubic;
|
|
554
|
+
this._startOutlineProgress = _startOutlineProgress;
|
|
555
|
+
this._endOutlineProgress = _endOutlineProgress;
|
|
556
|
+
if (!(_endOutlineProgress >= _startOutlineProgress)) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
"endOutlineProgress is expected to be equal or greater than startOutlineProgress"
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
this.measuredSize = measurer.measureCubic(cubic);
|
|
562
|
+
}
|
|
563
|
+
measurer;
|
|
564
|
+
cubic;
|
|
565
|
+
_startOutlineProgress;
|
|
566
|
+
_endOutlineProgress;
|
|
567
|
+
measuredSize;
|
|
568
|
+
get startOutlineProgress() {
|
|
569
|
+
return this._startOutlineProgress;
|
|
570
|
+
}
|
|
571
|
+
get endOutlineProgress() {
|
|
572
|
+
return this._endOutlineProgress;
|
|
573
|
+
}
|
|
574
|
+
updateProgressRange(startOutlineProgress = this._startOutlineProgress, endOutlineProgress = this._endOutlineProgress) {
|
|
575
|
+
if (!(endOutlineProgress >= startOutlineProgress)) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
"endOutlineProgress is expected to be equal or greater than startOutlineProgress"
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
this._startOutlineProgress = startOutlineProgress;
|
|
581
|
+
this._endOutlineProgress = endOutlineProgress;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Cut this MeasuredCubic into two MeasuredCubics at the given outline progress value.
|
|
585
|
+
*/
|
|
586
|
+
cutAtProgress(cutOutlineProgress) {
|
|
587
|
+
const boundedCutOutlineProgress = Math.min(
|
|
588
|
+
Math.max(cutOutlineProgress, this._startOutlineProgress),
|
|
589
|
+
this._endOutlineProgress
|
|
590
|
+
);
|
|
591
|
+
const outlineProgressSize = f2(this._endOutlineProgress - this._startOutlineProgress);
|
|
592
|
+
const progressFromStart = f2(boundedCutOutlineProgress - this._startOutlineProgress);
|
|
593
|
+
const relativeProgress = f2(progressFromStart / outlineProgressSize);
|
|
594
|
+
const t = this.measurer.findCubicCutPoint(this.cubic, f2(relativeProgress * this.measuredSize));
|
|
595
|
+
if (!(t >= 0 && t <= 1)) throw new Error("Cubic cut point is expected to be between 0 and 1");
|
|
596
|
+
const [c1, c2] = this.cubic.split(t);
|
|
597
|
+
return [
|
|
598
|
+
new _MeasuredCubic(this.measurer, c1, this._startOutlineProgress, boundedCutOutlineProgress),
|
|
599
|
+
new _MeasuredCubic(this.measurer, c2, boundedCutOutlineProgress, this._endOutlineProgress)
|
|
600
|
+
];
|
|
601
|
+
}
|
|
602
|
+
toString() {
|
|
603
|
+
return `MeasuredCubic(outlineProgress=[${String(this._startOutlineProgress)} .. ${String(this._endOutlineProgress)}], size=${String(this.measuredSize)}, cubic=${String(this.cubic)})`;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
var MeasuredPolygon = class _MeasuredPolygon {
|
|
607
|
+
constructor(measurer, features, cubics, outlineProgress) {
|
|
608
|
+
this.measurer = measurer;
|
|
609
|
+
this.features = features;
|
|
610
|
+
if (outlineProgress.length !== cubics.length + 1) {
|
|
611
|
+
throw new Error("Outline progress size is expected to be the cubics size + 1");
|
|
612
|
+
}
|
|
613
|
+
if (outlineProgress[0] !== 0)
|
|
614
|
+
throw new Error("First outline progress value is expected to be zero");
|
|
615
|
+
if (outlineProgress[outlineProgress.length - 1] !== 1) {
|
|
616
|
+
throw new Error("Last outline progress value is expected to be one");
|
|
617
|
+
}
|
|
618
|
+
const measuredCubics = [];
|
|
619
|
+
let startOutlineProgress = 0;
|
|
620
|
+
for (let index = 0; index < cubics.length; index++) {
|
|
621
|
+
if (f2(outlineProgress[index + 1] - outlineProgress[index]) > DistanceEpsilon) {
|
|
622
|
+
measuredCubics.push(
|
|
623
|
+
new MeasuredCubic(
|
|
624
|
+
measurer,
|
|
625
|
+
cubics[index],
|
|
626
|
+
startOutlineProgress,
|
|
627
|
+
outlineProgress[index + 1]
|
|
628
|
+
)
|
|
629
|
+
);
|
|
630
|
+
startOutlineProgress = outlineProgress[index + 1];
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
measuredCubics[measuredCubics.length - 1].updateProgressRange(void 0, 1);
|
|
634
|
+
this.cubics = measuredCubics;
|
|
635
|
+
}
|
|
636
|
+
measurer;
|
|
637
|
+
features;
|
|
638
|
+
cubics;
|
|
639
|
+
get size() {
|
|
640
|
+
return this.cubics.length;
|
|
641
|
+
}
|
|
642
|
+
get(index) {
|
|
643
|
+
const cubic = this.cubics[index];
|
|
644
|
+
if (!cubic) throw new RangeError(`index ${String(index)} out of range`);
|
|
645
|
+
return cubic;
|
|
646
|
+
}
|
|
647
|
+
[Symbol.iterator]() {
|
|
648
|
+
return this.cubics[Symbol.iterator]();
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Finds the point in the input list of measured cubics that pass the given outline progress, and
|
|
652
|
+
* generates a new MeasuredPolygon (equivalent to this), that starts at that point. This usually
|
|
653
|
+
* means cutting the cubic that crosses the outline progress (unless the cut is at one of its
|
|
654
|
+
* ends) For example, given outline progress 0.4f and measured cubics on these outline progress
|
|
655
|
+
* ranges: c1 [0 -> 0.2] c2 [0.2 -> 0.5] c3 [0.5 -> 1.0] c2 will be cut in two, at the given
|
|
656
|
+
* outline progress, we can name these c2a [0.2 -> 0.4] and c2b [0.4 -> 0.5] The return then will
|
|
657
|
+
* have the following cubics, in this order: c2b, c3, c1, c2a
|
|
658
|
+
*/
|
|
659
|
+
cutAndShift(cuttingPoint) {
|
|
660
|
+
if (!(cuttingPoint >= 0 && cuttingPoint <= 1)) {
|
|
661
|
+
throw new Error("Cutting point is expected to be between 0 and 1");
|
|
662
|
+
}
|
|
663
|
+
if (cuttingPoint < DistanceEpsilon) return this;
|
|
664
|
+
const targetIndex = this.cubics.findIndex(
|
|
665
|
+
(it) => cuttingPoint >= it.startOutlineProgress && cuttingPoint <= it.endOutlineProgress
|
|
666
|
+
);
|
|
667
|
+
const target = this.get(targetIndex);
|
|
668
|
+
const [b1, b2] = target.cutAtProgress(cuttingPoint);
|
|
669
|
+
const retCubics = [b2.cubic];
|
|
670
|
+
for (let i = 1; i < this.cubics.length; i++) {
|
|
671
|
+
retCubics.push(this.get((i + targetIndex) % this.cubics.length).cubic);
|
|
672
|
+
}
|
|
673
|
+
retCubics.push(b1.cubic);
|
|
674
|
+
const retOutlineProgress = [];
|
|
675
|
+
for (let index = 0; index < this.cubics.length + 2; index++) {
|
|
676
|
+
if (index === 0) retOutlineProgress.push(0);
|
|
677
|
+
else if (index === this.cubics.length + 1) retOutlineProgress.push(1);
|
|
678
|
+
else {
|
|
679
|
+
const cubicIndex = (targetIndex + index - 1) % this.cubics.length;
|
|
680
|
+
retOutlineProgress.push(
|
|
681
|
+
positiveModulo(f2(this.get(cubicIndex).endOutlineProgress - cuttingPoint), 1)
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const newFeatures = this.features.map(
|
|
686
|
+
(it) => new ProgressableFeature(positiveModulo(f2(it.progress - cuttingPoint), 1), it.feature)
|
|
687
|
+
);
|
|
688
|
+
return new _MeasuredPolygon(this.measurer, newFeatures, retCubics, retOutlineProgress);
|
|
689
|
+
}
|
|
690
|
+
static measurePolygon(measurer, polygon) {
|
|
691
|
+
const cubics = [];
|
|
692
|
+
const featureToCubic = [];
|
|
693
|
+
for (const feature of polygon.features) {
|
|
694
|
+
for (let cubicIndex = 0; cubicIndex < feature.cubics.length; cubicIndex++) {
|
|
695
|
+
if (feature instanceof Corner && cubicIndex === Math.trunc(feature.cubics.length / 2)) {
|
|
696
|
+
featureToCubic.push([feature, cubics.length]);
|
|
697
|
+
}
|
|
698
|
+
cubics.push(feature.cubics[cubicIndex]);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const measures = [0];
|
|
702
|
+
for (const cubic of cubics) {
|
|
703
|
+
const m = measurer.measureCubic(cubic);
|
|
704
|
+
if (!(m >= 0)) throw new Error("Measured cubic is expected to be greater or equal to zero");
|
|
705
|
+
measures.push(f2(measures[measures.length - 1] + m));
|
|
706
|
+
}
|
|
707
|
+
const totalMeasure = measures[measures.length - 1];
|
|
708
|
+
const outlineProgress = measures.map((m) => f2(m / totalMeasure));
|
|
709
|
+
const features = featureToCubic.map(
|
|
710
|
+
([feature, ix]) => new ProgressableFeature(f2(f2(outlineProgress[ix] + outlineProgress[ix + 1]) / 2), feature)
|
|
711
|
+
);
|
|
712
|
+
return new _MeasuredPolygon(measurer, features, cubics, outlineProgress);
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// src/graphics-shapes/morph.ts
|
|
717
|
+
var Morph = class _Morph {
|
|
718
|
+
constructor(start, end) {
|
|
719
|
+
this.start = start;
|
|
720
|
+
this.end = end;
|
|
721
|
+
this.morphMatch = _Morph.match(start, end);
|
|
722
|
+
}
|
|
723
|
+
start;
|
|
724
|
+
end;
|
|
725
|
+
/** The structure which holds the actual shape being morphed: matched start/end cubics. */
|
|
726
|
+
morphMatch;
|
|
727
|
+
/** Axis-aligned bounds covering both start and end shapes (and so the whole morph). */
|
|
728
|
+
calculateBounds(bounds = [0, 0, 0, 0], approximate = true) {
|
|
729
|
+
this.start.calculateBounds(bounds, approximate);
|
|
730
|
+
const [minX, minY, maxX, maxY] = bounds;
|
|
731
|
+
this.end.calculateBounds(bounds, approximate);
|
|
732
|
+
bounds[0] = Math.min(minX, bounds[0]);
|
|
733
|
+
bounds[1] = Math.min(minY, bounds[1]);
|
|
734
|
+
bounds[2] = Math.max(maxX, bounds[2]);
|
|
735
|
+
bounds[3] = Math.max(maxY, bounds[3]);
|
|
736
|
+
return bounds;
|
|
737
|
+
}
|
|
738
|
+
/** A square that holds both shapes in any rotation (see RoundedPolygon.calculateMaxBounds). */
|
|
739
|
+
calculateMaxBounds(bounds = [0, 0, 0, 0]) {
|
|
740
|
+
this.start.calculateMaxBounds(bounds);
|
|
741
|
+
const [minX, minY, maxX, maxY] = bounds;
|
|
742
|
+
this.end.calculateMaxBounds(bounds);
|
|
743
|
+
bounds[0] = Math.min(minX, bounds[0]);
|
|
744
|
+
bounds[1] = Math.min(minY, bounds[1]);
|
|
745
|
+
bounds[2] = Math.max(maxX, bounds[2]);
|
|
746
|
+
bounds[3] = Math.max(maxY, bounds[3]);
|
|
747
|
+
return bounds;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Returns a representation of the morph object at a given [progress] value as a list of Cubics.
|
|
751
|
+
* Note that this function causes a new list to be created and populated, so there is some
|
|
752
|
+
* overhead.
|
|
753
|
+
*/
|
|
754
|
+
asCubics(progress) {
|
|
755
|
+
const out = [];
|
|
756
|
+
let firstCubic = null;
|
|
757
|
+
let lastCubic = null;
|
|
758
|
+
for (const [first, second] of this.morphMatch) {
|
|
759
|
+
const cubic = new Cubic(
|
|
760
|
+
first.points.map((v, it) => interpolate(v, second.points[it], progress))
|
|
761
|
+
);
|
|
762
|
+
firstCubic ??= cubic;
|
|
763
|
+
if (lastCubic !== null) out.push(lastCubic);
|
|
764
|
+
lastCubic = cubic;
|
|
765
|
+
}
|
|
766
|
+
if (lastCubic !== null && firstCubic !== null) {
|
|
767
|
+
out.push(
|
|
768
|
+
Cubic.of(
|
|
769
|
+
lastCubic.anchor0X,
|
|
770
|
+
lastCubic.anchor0Y,
|
|
771
|
+
lastCubic.control0X,
|
|
772
|
+
lastCubic.control0Y,
|
|
773
|
+
lastCubic.control1X,
|
|
774
|
+
lastCubic.control1Y,
|
|
775
|
+
firstCubic.anchor0X,
|
|
776
|
+
firstCubic.anchor0Y
|
|
777
|
+
)
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
return out;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Calls [callback] for each of the cubics of the morph at [progress], reusing [mutableCubic]
|
|
784
|
+
* (no per-frame allocation).
|
|
785
|
+
*/
|
|
786
|
+
forEachCubic(progress, callback, mutableCubic = new MutableCubic()) {
|
|
787
|
+
for (const [first, second] of this.morphMatch) {
|
|
788
|
+
mutableCubic.interpolate(first, second, progress);
|
|
789
|
+
callback(mutableCubic);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Matches the cubics of two polygons: measures both outlines, maps their corners, cuts and
|
|
794
|
+
* shifts the end polygon, then walks both, cutting curves so every pair spans the same progress.
|
|
795
|
+
*/
|
|
796
|
+
static match(p1, p2) {
|
|
797
|
+
const measuredPolygon1 = MeasuredPolygon.measurePolygon(
|
|
798
|
+
new AngleMeasurer(p1.centerX, p1.centerY),
|
|
799
|
+
p1
|
|
800
|
+
);
|
|
801
|
+
const measuredPolygon2 = MeasuredPolygon.measurePolygon(
|
|
802
|
+
new AngleMeasurer(p2.centerX, p2.centerY),
|
|
803
|
+
p2
|
|
804
|
+
);
|
|
805
|
+
const features1 = measuredPolygon1.features;
|
|
806
|
+
const features2 = measuredPolygon2.features;
|
|
807
|
+
const doubleMapper = featureMapper(features1, features2);
|
|
808
|
+
const polygon2CutPoint = doubleMapper.map(0);
|
|
809
|
+
const bs1 = measuredPolygon1;
|
|
810
|
+
const bs2 = measuredPolygon2.cutAndShift(polygon2CutPoint);
|
|
811
|
+
const ret = [];
|
|
812
|
+
const getOrNull = (p, i) => i < p.size ? p.get(i) : null;
|
|
813
|
+
let i1 = 0;
|
|
814
|
+
let i2 = 0;
|
|
815
|
+
let b1 = getOrNull(bs1, i1++);
|
|
816
|
+
let b2 = getOrNull(bs2, i2++);
|
|
817
|
+
while (b1 !== null && b2 !== null) {
|
|
818
|
+
const b1a = i1 === bs1.size ? 1 : b1.endOutlineProgress;
|
|
819
|
+
const b2a = i2 === bs2.size ? 1 : doubleMapper.mapBack(positiveModulo(f2(b2.endOutlineProgress + polygon2CutPoint), 1));
|
|
820
|
+
const minb = Math.min(b1a, b2a);
|
|
821
|
+
let seg1;
|
|
822
|
+
let newb1;
|
|
823
|
+
if (b1a > f2(minb + AngleEpsilon)) {
|
|
824
|
+
[seg1, newb1] = b1.cutAtProgress(minb);
|
|
825
|
+
} else {
|
|
826
|
+
seg1 = b1;
|
|
827
|
+
newb1 = getOrNull(bs1, i1++);
|
|
828
|
+
}
|
|
829
|
+
let seg2;
|
|
830
|
+
let newb2;
|
|
831
|
+
if (b2a > f2(minb + AngleEpsilon)) {
|
|
832
|
+
[seg2, newb2] = b2.cutAtProgress(
|
|
833
|
+
positiveModulo(f2(doubleMapper.map(minb) - polygon2CutPoint), 1)
|
|
834
|
+
);
|
|
835
|
+
} else {
|
|
836
|
+
seg2 = b2;
|
|
837
|
+
newb2 = getOrNull(bs2, i2++);
|
|
838
|
+
}
|
|
839
|
+
ret.push([seg1.cubic, seg2.cubic]);
|
|
840
|
+
b1 = newb1;
|
|
841
|
+
b2 = newb2;
|
|
842
|
+
}
|
|
843
|
+
if (!(b1 === null && b2 === null)) {
|
|
844
|
+
throw new Error("Expected both Polygon's Cubic to be fully matched");
|
|
845
|
+
}
|
|
846
|
+
return ret;
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
// src/graphics-shapes/rounded-polygon.ts
|
|
851
|
+
var RoundedPolygon = class _RoundedPolygon {
|
|
852
|
+
constructor(features, centerX, centerY) {
|
|
853
|
+
this.features = features;
|
|
854
|
+
this.centerX = f2(centerX);
|
|
855
|
+
this.centerY = f2(centerY);
|
|
856
|
+
this.cubics = buildCubics(features);
|
|
857
|
+
let prevCubic = this.cubics[this.cubics.length - 1];
|
|
858
|
+
for (const cubic of this.cubics) {
|
|
859
|
+
if (Math.abs(f2(cubic.anchor0X - prevCubic.anchor1X)) > DistanceEpsilon || Math.abs(f2(cubic.anchor0Y - prevCubic.anchor1Y)) > DistanceEpsilon) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
"RoundedPolygon must be contiguous, with the anchor points of all curves matching the anchor points of the preceding and succeeding cubics"
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
prevCubic = cubic;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
features;
|
|
868
|
+
cubics;
|
|
869
|
+
centerX;
|
|
870
|
+
centerY;
|
|
871
|
+
/** Transforms (scales/translates/etc.) this [RoundedPolygon] with the given [PointTransformer]. */
|
|
872
|
+
transformed(f4) {
|
|
873
|
+
const center = transformed(point(this.centerX, this.centerY), f4);
|
|
874
|
+
return new _RoundedPolygon(
|
|
875
|
+
this.features.map((feature) => feature.transformed(f4)),
|
|
876
|
+
center.x,
|
|
877
|
+
center.y
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Creates a new RoundedPolygon, moving and resizing this one, so it is completely inside the
|
|
882
|
+
* (0, 0) -> (1, 1) square, centered if there extra space in one direction
|
|
883
|
+
*/
|
|
884
|
+
normalized() {
|
|
885
|
+
const bounds = this.calculateBounds();
|
|
886
|
+
const width = f2(bounds[2] - bounds[0]);
|
|
887
|
+
const height = f2(bounds[3] - bounds[1]);
|
|
888
|
+
const side = Math.max(width, height);
|
|
889
|
+
const offsetX = f2(f2(f2(side - width) / 2) - bounds[0]);
|
|
890
|
+
const offsetY = f2(f2(f2(side - height) / 2) - bounds[1]);
|
|
891
|
+
return this.transformed((x, y) => [f2(f2(x + offsetX) / side), f2(f2(y + offsetY) / side)]);
|
|
892
|
+
}
|
|
893
|
+
toString() {
|
|
894
|
+
return `[RoundedPolygon. Cubics = ${this.cubics.join(", ")} || Features = ${this.features.join(", ")} || Center = (${String(this.centerX)}, ${String(this.centerY)})]`;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Like [calculateBounds], this function calculates the axis-aligned bounds of the object and
|
|
898
|
+
* returns that rectangle. But this function determines the max dimension of the shape (by
|
|
899
|
+
* calculating the distance from its center to the start and midpoint of each curve) and returns
|
|
900
|
+
* a square which can be used to hold the object in any rotation.
|
|
901
|
+
*/
|
|
902
|
+
calculateMaxBounds(bounds = [0, 0, 0, 0]) {
|
|
903
|
+
if (bounds.length < 4) throw new Error("Required bounds size of 4");
|
|
904
|
+
let maxDistSquared = 0;
|
|
905
|
+
for (const cubic of this.cubics) {
|
|
906
|
+
const anchorDistance = distanceSquared(
|
|
907
|
+
f2(cubic.anchor0X - this.centerX),
|
|
908
|
+
f2(cubic.anchor0Y - this.centerY)
|
|
909
|
+
);
|
|
910
|
+
const middlePoint = cubic.pointOnCurve(0.5);
|
|
911
|
+
const middleDistance = distanceSquared(
|
|
912
|
+
f2(middlePoint.x - this.centerX),
|
|
913
|
+
f2(middlePoint.y - this.centerY)
|
|
914
|
+
);
|
|
915
|
+
maxDistSquared = Math.max(maxDistSquared, Math.max(anchorDistance, middleDistance));
|
|
916
|
+
}
|
|
917
|
+
const d = f2(Math.sqrt(maxDistSquared));
|
|
918
|
+
bounds[0] = f2(this.centerX - d);
|
|
919
|
+
bounds[1] = f2(this.centerY - d);
|
|
920
|
+
bounds[2] = f2(this.centerX + d);
|
|
921
|
+
bounds[3] = f2(this.centerY + d);
|
|
922
|
+
return bounds;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Calculates the axis-aligned bounds of the object. With [approximate] (the default), uses the
|
|
926
|
+
* bounding box of every anchor and control point.
|
|
927
|
+
*/
|
|
928
|
+
calculateBounds(bounds = [0, 0, 0, 0], approximate = true) {
|
|
929
|
+
if (bounds.length < 4) throw new Error("Required bounds size of 4");
|
|
930
|
+
let minX = FLOAT_MAX_VALUE;
|
|
931
|
+
let minY = FLOAT_MAX_VALUE;
|
|
932
|
+
let maxX = FLOAT_MIN_VALUE;
|
|
933
|
+
let maxY = FLOAT_MIN_VALUE;
|
|
934
|
+
for (const cubic of this.cubics) {
|
|
935
|
+
cubic.calculateBounds(bounds, approximate);
|
|
936
|
+
minX = Math.min(minX, bounds[0]);
|
|
937
|
+
minY = Math.min(minY, bounds[1]);
|
|
938
|
+
maxX = Math.max(maxX, bounds[2]);
|
|
939
|
+
maxY = Math.max(maxY, bounds[3]);
|
|
940
|
+
}
|
|
941
|
+
bounds[0] = minX;
|
|
942
|
+
bounds[1] = minY;
|
|
943
|
+
bounds[2] = maxX;
|
|
944
|
+
bounds[3] = maxY;
|
|
945
|
+
return bounds;
|
|
946
|
+
}
|
|
947
|
+
/** Copy constructor (Kotlin `RoundedPolygon(source)`). */
|
|
948
|
+
static copy(source) {
|
|
949
|
+
return new _RoundedPolygon(source.features, source.centerX, source.centerY);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Regular polygon of [numVertices] vertices around ([centerX], [centerY]) at [radius]
|
|
953
|
+
* (Kotlin `RoundedPolygon(numVertices, …)`).
|
|
954
|
+
*/
|
|
955
|
+
static fromVertexCount(numVertices, options = {}) {
|
|
956
|
+
const { radius = 1, centerX = 0, centerY = 0, rounding = CornerRounding.Unrounded } = options;
|
|
957
|
+
return _RoundedPolygon.fromVertices(
|
|
958
|
+
verticesFromNumVerts(numVertices, radius, centerX, centerY),
|
|
959
|
+
{
|
|
960
|
+
rounding,
|
|
961
|
+
perVertexRounding: options.perVertexRounding ?? null,
|
|
962
|
+
centerX,
|
|
963
|
+
centerY
|
|
964
|
+
}
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Polygon from an ordered list of `[x0, y0, x1, y1, …]` vertices (Kotlin
|
|
969
|
+
* `RoundedPolygon(vertices, …)`). Center defaults to the (1.0.1) estimate from the vertices.
|
|
970
|
+
*/
|
|
971
|
+
static fromVertices(vertices, options = {}) {
|
|
972
|
+
const {
|
|
973
|
+
rounding = CornerRounding.Unrounded,
|
|
974
|
+
perVertexRounding = null,
|
|
975
|
+
centerX = FLOAT_MIN_VALUE,
|
|
976
|
+
centerY = FLOAT_MIN_VALUE
|
|
977
|
+
} = options;
|
|
978
|
+
if (vertices.length < 6) throw new Error("Polygons must have at least 3 vertices");
|
|
979
|
+
if (vertices.length % 2 === 1) throw new Error("The vertices array should have even size");
|
|
980
|
+
if (perVertexRounding !== null && perVertexRounding.length * 2 !== vertices.length) {
|
|
981
|
+
throw new Error(
|
|
982
|
+
"perVertexRounding list should be either null or the same size as the number of vertices (vertices.size / 2)"
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
const v = (i) => f2(vertices[i]);
|
|
986
|
+
const corners = [];
|
|
987
|
+
const n = vertices.length / 2;
|
|
988
|
+
const roundedCorners = [];
|
|
989
|
+
for (let i = 0; i < n; i++) {
|
|
990
|
+
const vtxRounding = perVertexRounding?.[i] ?? rounding;
|
|
991
|
+
const prevIndex = (i + n - 1) % n * 2;
|
|
992
|
+
const nextIndex = (i + 1) % n * 2;
|
|
993
|
+
roundedCorners.push(
|
|
994
|
+
new RoundedCorner(
|
|
995
|
+
point(v(prevIndex), v(prevIndex + 1)),
|
|
996
|
+
point(v(i * 2), v(i * 2 + 1)),
|
|
997
|
+
point(v(nextIndex), v(nextIndex + 1)),
|
|
998
|
+
vtxRounding
|
|
999
|
+
)
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
const cutAdjusts = [];
|
|
1003
|
+
for (let ix = 0; ix < n; ix++) {
|
|
1004
|
+
const here = roundedCorners[ix];
|
|
1005
|
+
const next = roundedCorners[(ix + 1) % n];
|
|
1006
|
+
const expectedRoundCut = f2(here.expectedRoundCut + next.expectedRoundCut);
|
|
1007
|
+
const expectedCut = f2(here.expectedCut + next.expectedCut);
|
|
1008
|
+
const vtxX = v(ix * 2);
|
|
1009
|
+
const vtxY = v(ix * 2 + 1);
|
|
1010
|
+
const nextVtxX = v((ix + 1) % n * 2);
|
|
1011
|
+
const nextVtxY = v((ix + 1) % n * 2 + 1);
|
|
1012
|
+
const sideSize = distance(f2(vtxX - nextVtxX), f2(vtxY - nextVtxY));
|
|
1013
|
+
if (expectedRoundCut > sideSize) {
|
|
1014
|
+
cutAdjusts.push([f2(sideSize / expectedRoundCut), 0]);
|
|
1015
|
+
} else if (expectedCut > sideSize) {
|
|
1016
|
+
cutAdjusts.push([1, f2(f2(sideSize - expectedRoundCut) / f2(expectedCut - expectedRoundCut))]);
|
|
1017
|
+
} else {
|
|
1018
|
+
cutAdjusts.push([1, 1]);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
for (let i = 0; i < n; i++) {
|
|
1022
|
+
const allowedCuts = [];
|
|
1023
|
+
const corner = roundedCorners[i];
|
|
1024
|
+
for (let delta = 0; delta <= 1; delta++) {
|
|
1025
|
+
const [roundCutRatio, cutRatio] = cutAdjusts[(i + n - 1 + delta) % n];
|
|
1026
|
+
allowedCuts.push(
|
|
1027
|
+
f2(
|
|
1028
|
+
f2(corner.expectedRoundCut * roundCutRatio) + f2(f2(corner.expectedCut - corner.expectedRoundCut) * cutRatio)
|
|
1029
|
+
)
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
corners.push(corner.getCubics(allowedCuts[0], allowedCuts[1]));
|
|
1033
|
+
}
|
|
1034
|
+
const tempFeatures = [];
|
|
1035
|
+
for (let i = 0; i < n; i++) {
|
|
1036
|
+
const prevVtxIndex = (i + n - 1) % n;
|
|
1037
|
+
const nextVtxIndex = (i + 1) % n;
|
|
1038
|
+
const currVertex = point(v(i * 2), v(i * 2 + 1));
|
|
1039
|
+
const prevVertex = point(v(prevVtxIndex * 2), v(prevVtxIndex * 2 + 1));
|
|
1040
|
+
const nextVertex = point(v(nextVtxIndex * 2), v(nextVtxIndex * 2 + 1));
|
|
1041
|
+
const convex = clockwise(minus(currVertex, prevVertex), minus(nextVertex, currVertex));
|
|
1042
|
+
tempFeatures.push(new Corner(corners[i], currVertex, roundedCorners[i].center, convex));
|
|
1043
|
+
const last = corners[i][corners[i].length - 1];
|
|
1044
|
+
const first = corners[(i + 1) % n][0];
|
|
1045
|
+
tempFeatures.push(
|
|
1046
|
+
new Edge([
|
|
1047
|
+
Cubic.straightLine(last.anchor1X, last.anchor1Y, first.anchor0X, first.anchor0Y)
|
|
1048
|
+
])
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
const { x: cx, y: cy } = centerX === FLOAT_MIN_VALUE || centerY === FLOAT_MIN_VALUE ? calculateCenter(vertices) : point(f2(centerX), f2(centerY));
|
|
1052
|
+
return new _RoundedPolygon(tempFeatures, cx, cy);
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
function buildCubics(features) {
|
|
1056
|
+
const out = [];
|
|
1057
|
+
let firstCubic = null;
|
|
1058
|
+
let lastCubic = null;
|
|
1059
|
+
let firstFeatureSplitStart = null;
|
|
1060
|
+
let firstFeatureSplitEnd = null;
|
|
1061
|
+
if (features.length > 0 && features[0].cubics.length === 3) {
|
|
1062
|
+
const centerCubic = features[0].cubics[1];
|
|
1063
|
+
const [start, end] = centerCubic.split(0.5);
|
|
1064
|
+
firstFeatureSplitStart = [features[0].cubics[0], start];
|
|
1065
|
+
firstFeatureSplitEnd = [end, features[0].cubics[2]];
|
|
1066
|
+
}
|
|
1067
|
+
for (let i = 0; i <= features.length; i++) {
|
|
1068
|
+
let featureCubics;
|
|
1069
|
+
if (i === 0 && firstFeatureSplitEnd !== null) featureCubics = firstFeatureSplitEnd;
|
|
1070
|
+
else if (i === features.length) {
|
|
1071
|
+
if (firstFeatureSplitStart !== null) featureCubics = firstFeatureSplitStart;
|
|
1072
|
+
else break;
|
|
1073
|
+
} else featureCubics = features[i].cubics;
|
|
1074
|
+
for (const cubic of featureCubics) {
|
|
1075
|
+
if (!cubic.zeroLength()) {
|
|
1076
|
+
if (lastCubic !== null) out.push(lastCubic);
|
|
1077
|
+
lastCubic = cubic;
|
|
1078
|
+
firstCubic ??= cubic;
|
|
1079
|
+
} else if (lastCubic !== null) {
|
|
1080
|
+
lastCubic.points[6] = cubic.anchor1X;
|
|
1081
|
+
lastCubic.points[7] = cubic.anchor1Y;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (lastCubic !== null && firstCubic !== null) {
|
|
1086
|
+
out.push(
|
|
1087
|
+
Cubic.of(
|
|
1088
|
+
lastCubic.anchor0X,
|
|
1089
|
+
lastCubic.anchor0Y,
|
|
1090
|
+
lastCubic.control0X,
|
|
1091
|
+
lastCubic.control0Y,
|
|
1092
|
+
lastCubic.control1X,
|
|
1093
|
+
lastCubic.control1Y,
|
|
1094
|
+
firstCubic.anchor0X,
|
|
1095
|
+
firstCubic.anchor0Y
|
|
1096
|
+
)
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
return out;
|
|
1100
|
+
}
|
|
1101
|
+
function calculateCenter(vertices) {
|
|
1102
|
+
let cumulativeX = 0;
|
|
1103
|
+
let cumulativeY = 0;
|
|
1104
|
+
let index = 0;
|
|
1105
|
+
while (index < vertices.length) {
|
|
1106
|
+
cumulativeX = f2(cumulativeX + f2(vertices[index++]));
|
|
1107
|
+
cumulativeY = f2(cumulativeY + f2(vertices[index++]));
|
|
1108
|
+
}
|
|
1109
|
+
return point(f2(cumulativeX / vertices.length) / 2, f2(cumulativeY / vertices.length) / 2);
|
|
1110
|
+
}
|
|
1111
|
+
var RoundedCorner = class {
|
|
1112
|
+
constructor(p0, p1, p2, rounding = null) {
|
|
1113
|
+
this.p0 = p0;
|
|
1114
|
+
this.p1 = p1;
|
|
1115
|
+
this.p2 = p2;
|
|
1116
|
+
this.rounding = rounding;
|
|
1117
|
+
this.d1 = getDirection(minus(p0, p1));
|
|
1118
|
+
this.d2 = getDirection(minus(p2, p1));
|
|
1119
|
+
this.cornerRadius = rounding?.radius ?? 0;
|
|
1120
|
+
this.smoothing = rounding?.smoothing ?? 0;
|
|
1121
|
+
this.cosAngle = dotProduct(this.d1, this.d2);
|
|
1122
|
+
this.sinAngle = f2(Math.sqrt(f2(1 - square(this.cosAngle))));
|
|
1123
|
+
this.expectedRoundCut = this.sinAngle > 1e-3 ? f2(f2(this.cornerRadius * f2(this.cosAngle + 1)) / this.sinAngle) : 0;
|
|
1124
|
+
}
|
|
1125
|
+
p0;
|
|
1126
|
+
p1;
|
|
1127
|
+
p2;
|
|
1128
|
+
rounding;
|
|
1129
|
+
d1;
|
|
1130
|
+
d2;
|
|
1131
|
+
cornerRadius;
|
|
1132
|
+
smoothing;
|
|
1133
|
+
cosAngle;
|
|
1134
|
+
sinAngle;
|
|
1135
|
+
expectedRoundCut;
|
|
1136
|
+
/**
|
|
1137
|
+
* the center of the circle approximated by the rounding curve (or the middle of the three
|
|
1138
|
+
* curves if smoothing is requested). The center is the same as p0 if there is no rounding.
|
|
1139
|
+
*/
|
|
1140
|
+
center = point(0, 0);
|
|
1141
|
+
/** smoothing changes the actual cut. 0 is same as expectedRoundCut, 1 doubles it */
|
|
1142
|
+
get expectedCut() {
|
|
1143
|
+
return f2(f2(1 + this.smoothing) * this.expectedRoundCut);
|
|
1144
|
+
}
|
|
1145
|
+
getCubics(allowedCut0, allowedCut1 = allowedCut0) {
|
|
1146
|
+
const allowedCut = Math.min(allowedCut0, allowedCut1);
|
|
1147
|
+
if (this.expectedRoundCut < DistanceEpsilon || allowedCut < DistanceEpsilon || this.cornerRadius < DistanceEpsilon) {
|
|
1148
|
+
this.center = this.p1;
|
|
1149
|
+
return [Cubic.straightLine(this.p1.x, this.p1.y, this.p1.x, this.p1.y)];
|
|
1150
|
+
}
|
|
1151
|
+
const actualRoundCut = Math.min(allowedCut, this.expectedRoundCut);
|
|
1152
|
+
const actualSmoothing0 = this.calculateActualSmoothingValue(allowedCut0);
|
|
1153
|
+
const actualSmoothing1 = this.calculateActualSmoothingValue(allowedCut1);
|
|
1154
|
+
const actualR = f2(f2(this.cornerRadius * actualRoundCut) / this.expectedRoundCut);
|
|
1155
|
+
const centerDistance = f2(Math.sqrt(f2(square(actualR) + square(actualRoundCut))));
|
|
1156
|
+
this.center = plus(
|
|
1157
|
+
this.p1,
|
|
1158
|
+
times(getDirection(div(plus(this.d1, this.d2), 2)), centerDistance)
|
|
1159
|
+
);
|
|
1160
|
+
const circleIntersection0 = plus(this.p1, times(this.d1, actualRoundCut));
|
|
1161
|
+
const circleIntersection2 = plus(this.p1, times(this.d2, actualRoundCut));
|
|
1162
|
+
const flanking0 = this.computeFlankingCurve(
|
|
1163
|
+
actualRoundCut,
|
|
1164
|
+
actualSmoothing0,
|
|
1165
|
+
this.p1,
|
|
1166
|
+
this.p0,
|
|
1167
|
+
circleIntersection0,
|
|
1168
|
+
circleIntersection2,
|
|
1169
|
+
this.center,
|
|
1170
|
+
actualR
|
|
1171
|
+
);
|
|
1172
|
+
const flanking2 = this.computeFlankingCurve(
|
|
1173
|
+
actualRoundCut,
|
|
1174
|
+
actualSmoothing1,
|
|
1175
|
+
this.p1,
|
|
1176
|
+
this.p2,
|
|
1177
|
+
circleIntersection2,
|
|
1178
|
+
circleIntersection0,
|
|
1179
|
+
this.center,
|
|
1180
|
+
actualR
|
|
1181
|
+
).reverse();
|
|
1182
|
+
return [
|
|
1183
|
+
flanking0,
|
|
1184
|
+
Cubic.circularArc(
|
|
1185
|
+
this.center.x,
|
|
1186
|
+
this.center.y,
|
|
1187
|
+
flanking0.anchor1X,
|
|
1188
|
+
flanking0.anchor1Y,
|
|
1189
|
+
flanking2.anchor0X,
|
|
1190
|
+
flanking2.anchor0Y
|
|
1191
|
+
),
|
|
1192
|
+
flanking2
|
|
1193
|
+
];
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* If allowedCut (the amount we are able to cut) is greater than the expected cut (without
|
|
1197
|
+
* smoothing applied yet), then there is room to apply smoothing and we calculate the actual
|
|
1198
|
+
* smoothing value here.
|
|
1199
|
+
*/
|
|
1200
|
+
calculateActualSmoothingValue(allowedCut) {
|
|
1201
|
+
if (allowedCut > this.expectedCut) return this.smoothing;
|
|
1202
|
+
if (allowedCut > this.expectedRoundCut) {
|
|
1203
|
+
return f2(
|
|
1204
|
+
f2(this.smoothing * f2(allowedCut - this.expectedRoundCut)) / f2(this.expectedCut - this.expectedRoundCut)
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
return 0;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Compute a single flanking curve for this corner: from the side start to the rounding circle,
|
|
1211
|
+
* smoothed by [actualSmoothingValues].
|
|
1212
|
+
*/
|
|
1213
|
+
computeFlankingCurve(actualRoundCut, actualSmoothingValues, corner, sideStart, circleSegmentIntersection, otherCircleSegmentIntersection, circleCenter, actualR) {
|
|
1214
|
+
const sideDirection = getDirection(minus(sideStart, corner));
|
|
1215
|
+
const curveStart = plus(
|
|
1216
|
+
corner,
|
|
1217
|
+
times(times(sideDirection, actualRoundCut), f2(1 + actualSmoothingValues))
|
|
1218
|
+
);
|
|
1219
|
+
const p = interpolatePoint(
|
|
1220
|
+
circleSegmentIntersection,
|
|
1221
|
+
div(plus(circleSegmentIntersection, otherCircleSegmentIntersection), 2),
|
|
1222
|
+
actualSmoothingValues
|
|
1223
|
+
);
|
|
1224
|
+
const curveEnd = plus(
|
|
1225
|
+
circleCenter,
|
|
1226
|
+
times(directionVector(f2(p.x - circleCenter.x), f2(p.y - circleCenter.y)), actualR)
|
|
1227
|
+
);
|
|
1228
|
+
const circleTangent = rotate90(minus(curveEnd, circleCenter));
|
|
1229
|
+
const anchorEnd = lineIntersection(sideStart, sideDirection, curveEnd, circleTangent) ?? circleSegmentIntersection;
|
|
1230
|
+
const anchorStart = div(plus(curveStart, times(anchorEnd, 2)), 3);
|
|
1231
|
+
return Cubic.fromPoints(curveStart, anchorStart, anchorEnd, curveEnd);
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
function lineIntersection(p0, d0, p1, d1) {
|
|
1235
|
+
const rotatedD1 = rotate90(d1);
|
|
1236
|
+
const den = dotProduct(d0, rotatedD1);
|
|
1237
|
+
if (Math.abs(den) < DistanceEpsilon) return null;
|
|
1238
|
+
const num = dotProduct(minus(p1, p0), rotatedD1);
|
|
1239
|
+
if (Math.abs(den) < f2(DistanceEpsilon * Math.abs(num))) return null;
|
|
1240
|
+
const k = f2(num / den);
|
|
1241
|
+
return plus(p0, times(d0, k));
|
|
1242
|
+
}
|
|
1243
|
+
function verticesFromNumVerts(numVertices, radius, centerX, centerY) {
|
|
1244
|
+
const result = [];
|
|
1245
|
+
for (let i = 0; i < numVertices; i++) {
|
|
1246
|
+
const vertex = plus(
|
|
1247
|
+
radialToCartesian(f2(radius), f2(f2(f2(FloatPi / numVertices) * 2) * i)),
|
|
1248
|
+
point(centerX, centerY)
|
|
1249
|
+
);
|
|
1250
|
+
result.push(vertex.x, vertex.y);
|
|
1251
|
+
}
|
|
1252
|
+
return result;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/graphics-shapes/shapes.ts
|
|
1256
|
+
function circle(options = {}) {
|
|
1257
|
+
const { numVertices = 8 } = options;
|
|
1258
|
+
const radius = f2(options.radius ?? 1);
|
|
1259
|
+
const centerX = f2(options.centerX ?? 0);
|
|
1260
|
+
const centerY = f2(options.centerY ?? 0);
|
|
1261
|
+
if (numVertices < 3) throw new Error("Circle must have at least three vertices");
|
|
1262
|
+
const theta = f2(FloatPi / numVertices);
|
|
1263
|
+
const polygonRadius = f2(radius / f2(Math.cos(theta)));
|
|
1264
|
+
return RoundedPolygon.fromVertexCount(numVertices, {
|
|
1265
|
+
rounding: new CornerRounding(radius),
|
|
1266
|
+
radius: polygonRadius,
|
|
1267
|
+
centerX,
|
|
1268
|
+
centerY
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
function rectangle(options = {}) {
|
|
1272
|
+
const { rounding = CornerRounding.Unrounded, perVertexRounding = null } = options;
|
|
1273
|
+
const width = f2(options.width ?? 2);
|
|
1274
|
+
const height = f2(options.height ?? 2);
|
|
1275
|
+
const centerX = f2(options.centerX ?? 0);
|
|
1276
|
+
const centerY = f2(options.centerY ?? 0);
|
|
1277
|
+
const left = f2(centerX - f2(width / 2));
|
|
1278
|
+
const top = f2(centerY - f2(height / 2));
|
|
1279
|
+
const right = f2(centerX + f2(width / 2));
|
|
1280
|
+
const bottom = f2(centerY + f2(height / 2));
|
|
1281
|
+
return RoundedPolygon.fromVertices([right, bottom, left, bottom, left, top, right, top], {
|
|
1282
|
+
rounding,
|
|
1283
|
+
perVertexRounding,
|
|
1284
|
+
centerX,
|
|
1285
|
+
centerY
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
function star(numVerticesPerRadius, options = {}) {
|
|
1289
|
+
const { rounding = CornerRounding.Unrounded, innerRounding = null } = options;
|
|
1290
|
+
const radius = f2(options.radius ?? 1);
|
|
1291
|
+
const innerRadius = f2(options.innerRadius ?? 0.5);
|
|
1292
|
+
const centerX = f2(options.centerX ?? 0);
|
|
1293
|
+
const centerY = f2(options.centerY ?? 0);
|
|
1294
|
+
if (radius <= 0 || innerRadius <= 0) throw new Error("Star radii must both be greater than 0");
|
|
1295
|
+
if (innerRadius >= radius) throw new Error("innerRadius must be less than radius");
|
|
1296
|
+
let pvRounding = options.perVertexRounding ?? null;
|
|
1297
|
+
if (pvRounding === null && innerRounding !== null) {
|
|
1298
|
+
pvRounding = Array.from({ length: numVerticesPerRadius }, () => [
|
|
1299
|
+
rounding,
|
|
1300
|
+
innerRounding
|
|
1301
|
+
]).flat();
|
|
1302
|
+
}
|
|
1303
|
+
return RoundedPolygon.fromVertices(
|
|
1304
|
+
starVerticesFromNumVerts(numVerticesPerRadius, radius, innerRadius, centerX, centerY),
|
|
1305
|
+
{ rounding, perVertexRounding: pvRounding, centerX, centerY }
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
function pill(options = {}) {
|
|
1309
|
+
const width = f2(options.width ?? 2);
|
|
1310
|
+
const height = f2(options.height ?? 1);
|
|
1311
|
+
const smoothing = f2(options.smoothing ?? 0);
|
|
1312
|
+
const centerX = f2(options.centerX ?? 0);
|
|
1313
|
+
const centerY = f2(options.centerY ?? 0);
|
|
1314
|
+
if (!(width > 0 && height > 0))
|
|
1315
|
+
throw new Error("Pill shapes must have positive width and height");
|
|
1316
|
+
const wHalf = f2(width / 2);
|
|
1317
|
+
const hHalf = f2(height / 2);
|
|
1318
|
+
return RoundedPolygon.fromVertices(
|
|
1319
|
+
[
|
|
1320
|
+
f2(wHalf + centerX),
|
|
1321
|
+
f2(hHalf + centerY),
|
|
1322
|
+
f2(-wHalf + centerX),
|
|
1323
|
+
f2(hHalf + centerY),
|
|
1324
|
+
f2(-wHalf + centerX),
|
|
1325
|
+
f2(-hHalf + centerY),
|
|
1326
|
+
f2(wHalf + centerX),
|
|
1327
|
+
f2(-hHalf + centerY)
|
|
1328
|
+
],
|
|
1329
|
+
{ rounding: new CornerRounding(Math.min(wHalf, hHalf), smoothing), centerX, centerY }
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
function pillStar(options = {}) {
|
|
1333
|
+
const {
|
|
1334
|
+
numVerticesPerRadius = 8,
|
|
1335
|
+
rounding = CornerRounding.Unrounded,
|
|
1336
|
+
innerRounding = null
|
|
1337
|
+
} = options;
|
|
1338
|
+
const width = f2(options.width ?? 2);
|
|
1339
|
+
const height = f2(options.height ?? 1);
|
|
1340
|
+
const innerRadiusRatio = f2(options.innerRadiusRatio ?? 0.5);
|
|
1341
|
+
const vertexSpacing = f2(options.vertexSpacing ?? 0.5);
|
|
1342
|
+
const startLocation = f2(options.startLocation ?? 0);
|
|
1343
|
+
const centerX = f2(options.centerX ?? 0);
|
|
1344
|
+
const centerY = f2(options.centerY ?? 0);
|
|
1345
|
+
if (!(width > 0 && height > 0))
|
|
1346
|
+
throw new Error("Pill shapes must have positive width and height");
|
|
1347
|
+
if (!(innerRadiusRatio > 0 && innerRadiusRatio <= 1)) {
|
|
1348
|
+
throw new Error("innerRadius must be between 0 and 1");
|
|
1349
|
+
}
|
|
1350
|
+
let pvRounding = options.perVertexRounding ?? null;
|
|
1351
|
+
if (pvRounding === null && innerRounding !== null) {
|
|
1352
|
+
pvRounding = Array.from({ length: numVerticesPerRadius }, () => [
|
|
1353
|
+
rounding,
|
|
1354
|
+
innerRounding
|
|
1355
|
+
]).flat();
|
|
1356
|
+
}
|
|
1357
|
+
return RoundedPolygon.fromVertices(
|
|
1358
|
+
pillStarVerticesFromNumVerts(
|
|
1359
|
+
numVerticesPerRadius,
|
|
1360
|
+
width,
|
|
1361
|
+
height,
|
|
1362
|
+
innerRadiusRatio,
|
|
1363
|
+
vertexSpacing,
|
|
1364
|
+
startLocation,
|
|
1365
|
+
centerX,
|
|
1366
|
+
centerY
|
|
1367
|
+
),
|
|
1368
|
+
{ rounding, perVertexRounding: pvRounding, centerX, centerY }
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
function pillStarVerticesFromNumVerts(numVerticesPerRadius, width, height, innerRadius, vertexSpacing, startLocation, centerX, centerY) {
|
|
1372
|
+
const endcapRadius = Math.min(width, height);
|
|
1373
|
+
const vSegLen = Math.max(f2(height - width), 0);
|
|
1374
|
+
const hSegLen = Math.max(f2(width - height), 0);
|
|
1375
|
+
const vSegHalf = f2(vSegLen / 2);
|
|
1376
|
+
const hSegHalf = f2(hSegLen / 2);
|
|
1377
|
+
const circlePerimeter = f2(f2(TwoPi * endcapRadius) * interpolate(innerRadius, 1, vertexSpacing));
|
|
1378
|
+
const perimeter = f2(f2(f2(2 * hSegLen) + f2(2 * vSegLen)) + circlePerimeter);
|
|
1379
|
+
const sections = new Float32Array(11);
|
|
1380
|
+
const quarter = f2(circlePerimeter / 4);
|
|
1381
|
+
sections[0] = 0;
|
|
1382
|
+
sections[1] = vSegLen / 2;
|
|
1383
|
+
sections[2] = sections[1] + quarter;
|
|
1384
|
+
sections[3] = sections[2] + hSegLen;
|
|
1385
|
+
sections[4] = sections[3] + quarter;
|
|
1386
|
+
sections[5] = sections[4] + vSegLen;
|
|
1387
|
+
sections[6] = sections[5] + quarter;
|
|
1388
|
+
sections[7] = sections[6] + hSegLen;
|
|
1389
|
+
sections[8] = sections[7] + quarter;
|
|
1390
|
+
sections[9] = sections[8] + f2(vSegLen / 2);
|
|
1391
|
+
sections[10] = perimeter;
|
|
1392
|
+
const tPerVertex = f2(perimeter / (2 * numVerticesPerRadius));
|
|
1393
|
+
let inner = false;
|
|
1394
|
+
let currSecIndex = 0;
|
|
1395
|
+
let secStart = 0;
|
|
1396
|
+
let secEnd = sections[1];
|
|
1397
|
+
let t = f2(startLocation * perimeter);
|
|
1398
|
+
const result = [];
|
|
1399
|
+
const rectBR = point(hSegHalf, vSegHalf);
|
|
1400
|
+
const rectBL = point(-hSegHalf, vSegHalf);
|
|
1401
|
+
const rectTL = point(-hSegHalf, -vSegHalf);
|
|
1402
|
+
const rectTR = point(hSegHalf, -vSegHalf);
|
|
1403
|
+
for (let i = 0; i < numVerticesPerRadius * 2; i++) {
|
|
1404
|
+
const boundedT = f2(t % perimeter);
|
|
1405
|
+
if (boundedT < secStart) currSecIndex = 0;
|
|
1406
|
+
while (boundedT >= sections[(currSecIndex + 1) % sections.length]) {
|
|
1407
|
+
currSecIndex = (currSecIndex + 1) % sections.length;
|
|
1408
|
+
secStart = sections[currSecIndex];
|
|
1409
|
+
secEnd = sections[(currSecIndex + 1) % sections.length];
|
|
1410
|
+
}
|
|
1411
|
+
const tInSection = f2(boundedT - secStart);
|
|
1412
|
+
const tProportion = f2(tInSection / f2(secEnd - secStart));
|
|
1413
|
+
const halfArc = f2(f2(tProportion * FloatPi) / 2);
|
|
1414
|
+
const currRadius = inner ? f2(endcapRadius * innerRadius) : endcapRadius;
|
|
1415
|
+
let vertex;
|
|
1416
|
+
switch (currSecIndex) {
|
|
1417
|
+
case 0:
|
|
1418
|
+
vertex = point(currRadius, f2(tProportion * vSegHalf));
|
|
1419
|
+
break;
|
|
1420
|
+
case 1:
|
|
1421
|
+
vertex = plus(radialToCartesian(currRadius, halfArc), rectBR);
|
|
1422
|
+
break;
|
|
1423
|
+
case 2:
|
|
1424
|
+
vertex = point(f2(hSegHalf - f2(tProportion * hSegLen)), currRadius);
|
|
1425
|
+
break;
|
|
1426
|
+
case 3:
|
|
1427
|
+
vertex = plus(radialToCartesian(currRadius, f2(f2(FloatPi / 2) + halfArc)), rectBL);
|
|
1428
|
+
break;
|
|
1429
|
+
case 4:
|
|
1430
|
+
vertex = point(-currRadius, f2(vSegHalf - f2(tProportion * vSegLen)));
|
|
1431
|
+
break;
|
|
1432
|
+
case 5:
|
|
1433
|
+
vertex = plus(radialToCartesian(currRadius, f2(FloatPi + halfArc)), rectTL);
|
|
1434
|
+
break;
|
|
1435
|
+
case 6:
|
|
1436
|
+
vertex = point(f2(-hSegHalf + f2(tProportion * hSegLen)), -currRadius);
|
|
1437
|
+
break;
|
|
1438
|
+
case 7:
|
|
1439
|
+
vertex = plus(radialToCartesian(currRadius, f2(f2(FloatPi * 1.5) + halfArc)), rectTR);
|
|
1440
|
+
break;
|
|
1441
|
+
default:
|
|
1442
|
+
vertex = point(currRadius, f2(-vSegHalf + f2(tProportion * vSegHalf)));
|
|
1443
|
+
}
|
|
1444
|
+
result.push(f2(vertex.x + centerX), f2(vertex.y + centerY));
|
|
1445
|
+
t = f2(t + tPerVertex);
|
|
1446
|
+
inner = !inner;
|
|
1447
|
+
}
|
|
1448
|
+
return result;
|
|
1449
|
+
}
|
|
1450
|
+
function starVerticesFromNumVerts(numVerticesPerRadius, radius, innerRadius, centerX, centerY) {
|
|
1451
|
+
const result = [];
|
|
1452
|
+
for (let i = 0; i < numVerticesPerRadius; i++) {
|
|
1453
|
+
let vertex = radialToCartesian(radius, f2(f2(f2(FloatPi / numVerticesPerRadius) * 2) * i));
|
|
1454
|
+
result.push(f2(vertex.x + centerX), f2(vertex.y + centerY));
|
|
1455
|
+
vertex = radialToCartesian(innerRadius, f2(f2(FloatPi / numVerticesPerRadius) * (2 * i + 1)));
|
|
1456
|
+
result.push(f2(vertex.x + centerX), f2(vertex.y + centerY));
|
|
1457
|
+
}
|
|
1458
|
+
return result;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/matrix.ts
|
|
1462
|
+
var f3 = Math.fround;
|
|
1463
|
+
var Matrix = class {
|
|
1464
|
+
values = new Float32Array([
|
|
1465
|
+
1,
|
|
1466
|
+
0,
|
|
1467
|
+
0,
|
|
1468
|
+
0,
|
|
1469
|
+
0,
|
|
1470
|
+
1,
|
|
1471
|
+
0,
|
|
1472
|
+
0,
|
|
1473
|
+
0,
|
|
1474
|
+
0,
|
|
1475
|
+
1,
|
|
1476
|
+
0,
|
|
1477
|
+
0,
|
|
1478
|
+
0,
|
|
1479
|
+
0,
|
|
1480
|
+
1
|
|
1481
|
+
]);
|
|
1482
|
+
get(row, column) {
|
|
1483
|
+
return this.values[row * 4 + column] ?? 0;
|
|
1484
|
+
}
|
|
1485
|
+
set(row, column, v) {
|
|
1486
|
+
this.values[row * 4 + column] = v;
|
|
1487
|
+
}
|
|
1488
|
+
/** Does the 3D transform on the point and returns its `x` and `y` values. */
|
|
1489
|
+
map(x, y) {
|
|
1490
|
+
const v00 = this.get(0, 0);
|
|
1491
|
+
const v01 = this.get(0, 1);
|
|
1492
|
+
const v03 = this.get(0, 3);
|
|
1493
|
+
const v10 = this.get(1, 0);
|
|
1494
|
+
const v11 = this.get(1, 1);
|
|
1495
|
+
const v13 = this.get(1, 3);
|
|
1496
|
+
const v30 = this.get(3, 0);
|
|
1497
|
+
const v31 = this.get(3, 1);
|
|
1498
|
+
const v33 = this.get(3, 3);
|
|
1499
|
+
const z = f3(f3(f3(v03 * x) + f3(v13 * y)) + v33);
|
|
1500
|
+
const inverseZ = f3(1 / z);
|
|
1501
|
+
const pZ = Number.isFinite(inverseZ) ? inverseZ : 0;
|
|
1502
|
+
return [
|
|
1503
|
+
f3(pZ * f3(f3(f3(v00 * x) + f3(v10 * y)) + v30)),
|
|
1504
|
+
f3(pZ * f3(f3(f3(v01 * x) + f3(v11 * y)) + v31))
|
|
1505
|
+
];
|
|
1506
|
+
}
|
|
1507
|
+
/** Applies a rotation of [degrees] around Z to `this`. */
|
|
1508
|
+
rotateZ(degrees) {
|
|
1509
|
+
const r = f3(degrees) * (Math.PI / 180);
|
|
1510
|
+
const s = Math.fround(Math.sin(r));
|
|
1511
|
+
const c = Math.fround(Math.cos(r));
|
|
1512
|
+
for (let column = 0; column < 4; column++) {
|
|
1513
|
+
const a0 = this.get(0, column);
|
|
1514
|
+
const a1 = this.get(1, column);
|
|
1515
|
+
this.set(0, column, f3(c * a0) + f3(s * a1));
|
|
1516
|
+
this.set(1, column, f3(-s * a0) + f3(c * a1));
|
|
1517
|
+
}
|
|
1518
|
+
return this;
|
|
1519
|
+
}
|
|
1520
|
+
/** Scales this matrix by [x], [y], [z]. */
|
|
1521
|
+
scale(x = 1, y = 1, z = 1) {
|
|
1522
|
+
for (let column = 0; column < 4; column++) {
|
|
1523
|
+
this.set(0, column, this.get(0, column) * f3(x));
|
|
1524
|
+
this.set(1, column, this.get(1, column) * f3(y));
|
|
1525
|
+
this.set(2, column, this.get(2, column) * f3(z));
|
|
1526
|
+
}
|
|
1527
|
+
return this;
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
// src/path.ts
|
|
1532
|
+
var mapPoints = (command, f4) => {
|
|
1533
|
+
switch (command[0]) {
|
|
1534
|
+
case "M":
|
|
1535
|
+
case "L":
|
|
1536
|
+
return [command[0], ...f4(command[1], command[2])];
|
|
1537
|
+
case "C":
|
|
1538
|
+
return [
|
|
1539
|
+
"C",
|
|
1540
|
+
...f4(command[1], command[2]),
|
|
1541
|
+
...f4(command[3], command[4]),
|
|
1542
|
+
...f4(command[5], command[6])
|
|
1543
|
+
];
|
|
1544
|
+
case "Z":
|
|
1545
|
+
return command;
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
var transformPath = (path, matrix) => path.map((command) => mapPoints(command, (x, y) => matrix.map(x, y)));
|
|
1549
|
+
var translatePath = (path, dx, dy) => path.map((command) => mapPoints(command, (x, y) => [x + dx, y + dy]));
|
|
1550
|
+
function pathBounds(path) {
|
|
1551
|
+
let left = Infinity;
|
|
1552
|
+
let top = Infinity;
|
|
1553
|
+
let right = -Infinity;
|
|
1554
|
+
let bottom = -Infinity;
|
|
1555
|
+
for (const command of path) {
|
|
1556
|
+
for (let i = 1; i + 1 < command.length; i += 2) {
|
|
1557
|
+
const x = command[i];
|
|
1558
|
+
const y = command[i + 1];
|
|
1559
|
+
left = Math.min(left, x);
|
|
1560
|
+
top = Math.min(top, y);
|
|
1561
|
+
right = Math.max(right, x);
|
|
1562
|
+
bottom = Math.max(bottom, y);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
return left === Infinity ? [0, 0, 0, 0] : [left, top, right, bottom];
|
|
1566
|
+
}
|
|
1567
|
+
var formatNumber = (value, fractionDigits) => {
|
|
1568
|
+
const fixed = value.toFixed(fractionDigits);
|
|
1569
|
+
const text = fixed.includes(".") ? fixed.replace(/\.?0+$/, "") : fixed;
|
|
1570
|
+
return text === "-0" ? "0" : text;
|
|
1571
|
+
};
|
|
1572
|
+
function toSvgPathData(path, fractionDigits = 4) {
|
|
1573
|
+
return path.map(
|
|
1574
|
+
([verb, ...values]) => values.length === 0 ? verb : `${verb}${values.map((v) => formatNumber(v, fractionDigits)).join(" ")}`
|
|
1575
|
+
).join("");
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// src/shape-util.ts
|
|
1579
|
+
var transformedByMatrix = (polygon, matrix) => polygon.transformed((x, y) => matrix.map(x, y));
|
|
1580
|
+
function polygonToPath(polygon, options = {}) {
|
|
1581
|
+
const { startAngle = 0, repeatPath = false, closePath = true } = options;
|
|
1582
|
+
return pathFromCubics(
|
|
1583
|
+
startAngle,
|
|
1584
|
+
repeatPath,
|
|
1585
|
+
closePath,
|
|
1586
|
+
polygon.cubics,
|
|
1587
|
+
polygon.centerX,
|
|
1588
|
+
polygon.centerY
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
function morphToPath(morph, progress, options = {}) {
|
|
1592
|
+
const {
|
|
1593
|
+
startAngle = 0,
|
|
1594
|
+
repeatPath = false,
|
|
1595
|
+
closePath = true,
|
|
1596
|
+
rotationPivotX = 0,
|
|
1597
|
+
rotationPivotY = 0
|
|
1598
|
+
} = options;
|
|
1599
|
+
return pathFromCubics(
|
|
1600
|
+
startAngle,
|
|
1601
|
+
repeatPath,
|
|
1602
|
+
closePath,
|
|
1603
|
+
morph.asCubics(progress),
|
|
1604
|
+
rotationPivotX,
|
|
1605
|
+
rotationPivotY
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
function pathFromCubics(startAngle, repeatPath, closePath, cubics, rotationPivotX, rotationPivotY) {
|
|
1609
|
+
const path = [];
|
|
1610
|
+
const first = cubics[0];
|
|
1611
|
+
if (first !== void 0) path.push(["M", first.anchor0X, first.anchor0Y]);
|
|
1612
|
+
const addCubics = () => {
|
|
1613
|
+
for (const it of cubics) {
|
|
1614
|
+
path.push([
|
|
1615
|
+
"C",
|
|
1616
|
+
it.control0X,
|
|
1617
|
+
it.control0Y,
|
|
1618
|
+
it.control1X,
|
|
1619
|
+
it.control1Y,
|
|
1620
|
+
it.anchor1X,
|
|
1621
|
+
it.anchor1Y
|
|
1622
|
+
]);
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
addCubics();
|
|
1626
|
+
if (repeatPath && first !== void 0) {
|
|
1627
|
+
path.push(["L", first.anchor0X, first.anchor0Y]);
|
|
1628
|
+
addCubics();
|
|
1629
|
+
}
|
|
1630
|
+
if (closePath) path.push(["Z"]);
|
|
1631
|
+
if (startAngle !== 0 && first !== void 0) {
|
|
1632
|
+
const f4 = Math.fround;
|
|
1633
|
+
const angleToFirstCubic = radiansToDegrees(
|
|
1634
|
+
f4(Math.atan2(f4(first.anchor0Y - rotationPivotY), f4(first.anchor0X - rotationPivotX)))
|
|
1635
|
+
);
|
|
1636
|
+
return transformPath(path, new Matrix().rotateZ(f4(-angleToFirstCubic + startAngle)));
|
|
1637
|
+
}
|
|
1638
|
+
return path;
|
|
1639
|
+
}
|
|
1640
|
+
var radiansToDegrees = (radians) => Math.fround(radians * 180 / Math.PI);
|
|
1641
|
+
|
|
1642
|
+
// src/material-shapes.ts
|
|
1643
|
+
function toShapeOutline(polygon, width, height, startAngle = 0) {
|
|
1644
|
+
const shapePath = polygonToPath(polygon, { startAngle });
|
|
1645
|
+
const path = transformPath(shapePath, new Matrix().scale(width, height));
|
|
1646
|
+
const [left, top, right, bottom] = pathBounds(path);
|
|
1647
|
+
return translatePath(path, width / 2 - (left + right) / 2, height / 2 - (top + bottom) / 2);
|
|
1648
|
+
}
|
|
1649
|
+
var cornerRound15 = new CornerRounding(f2(0.15));
|
|
1650
|
+
var cornerRound20 = new CornerRounding(f2(0.2));
|
|
1651
|
+
var cornerRound30 = new CornerRounding(f2(0.3));
|
|
1652
|
+
var cornerRound50 = new CornerRounding(f2(0.5));
|
|
1653
|
+
var cornerRound100 = new CornerRounding(1);
|
|
1654
|
+
var rotateNeg45 = new Matrix().rotateZ(-45);
|
|
1655
|
+
var rotateNeg90 = new Matrix().rotateZ(-90);
|
|
1656
|
+
var rotateNeg135 = new Matrix().rotateZ(-135);
|
|
1657
|
+
var pnr = (x, y, r = CornerRounding.Unrounded) => ({
|
|
1658
|
+
x: f2(x),
|
|
1659
|
+
y: f2(y),
|
|
1660
|
+
r
|
|
1661
|
+
});
|
|
1662
|
+
var cr = (radius, smoothing = 0) => new CornerRounding(radius, smoothing);
|
|
1663
|
+
var toRadians = (degrees) => f2(f2(f2(degrees / 360) * 2) * FloatPi);
|
|
1664
|
+
var angleDegrees = (x, y) => f2(f2(f2(Math.atan2(y, x)) * 180) / FloatPi);
|
|
1665
|
+
function rotateDegrees(p, angle2, centerX, centerY) {
|
|
1666
|
+
const a = toRadians(angle2);
|
|
1667
|
+
const cos = f2(Math.cos(a));
|
|
1668
|
+
const sin = f2(Math.sin(a));
|
|
1669
|
+
const offX = f2(p.x - centerX);
|
|
1670
|
+
const offY = f2(p.y - centerY);
|
|
1671
|
+
return {
|
|
1672
|
+
x: f2(f2(f2(offX * cos) - f2(offY * sin)) + centerX),
|
|
1673
|
+
y: f2(f2(f2(offX * sin) + f2(offY * cos)) + centerY),
|
|
1674
|
+
r: p.r
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
function doRepeat(points, reps, centerX, centerY, mirroring) {
|
|
1678
|
+
const out = [];
|
|
1679
|
+
if (mirroring) {
|
|
1680
|
+
const angles = points.map((it) => angleDegrees(f2(it.x - centerX), f2(it.y - centerY)));
|
|
1681
|
+
const distances = points.map((it) => {
|
|
1682
|
+
const x = f2(it.x - centerX);
|
|
1683
|
+
const y = f2(it.y - centerY);
|
|
1684
|
+
return f2(Math.sqrt(f2(f2(x * x) + f2(y * y))));
|
|
1685
|
+
});
|
|
1686
|
+
const actualReps = reps * 2;
|
|
1687
|
+
const sectionAngle = f2(360 / actualReps);
|
|
1688
|
+
for (let it = 0; it < actualReps; it++) {
|
|
1689
|
+
for (let index = 0; index < points.length; index++) {
|
|
1690
|
+
const i = it % 2 === 0 ? index : points.length - 1 - index;
|
|
1691
|
+
if (i > 0 || it % 2 === 0) {
|
|
1692
|
+
const angle2 = angles[i] ?? 0;
|
|
1693
|
+
const a = toRadians(
|
|
1694
|
+
f2(
|
|
1695
|
+
f2(sectionAngle * it) + (it % 2 === 0 ? angle2 : f2(f2(sectionAngle - angle2) + f2(2 * (angles[0] ?? 0))))
|
|
1696
|
+
)
|
|
1697
|
+
);
|
|
1698
|
+
const distance2 = distances[i] ?? 0;
|
|
1699
|
+
out.push({
|
|
1700
|
+
x: f2(f2(f2(Math.cos(a)) * distance2) + centerX),
|
|
1701
|
+
y: f2(f2(f2(Math.sin(a)) * distance2) + centerY),
|
|
1702
|
+
r: points[i]?.r ?? CornerRounding.Unrounded
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
} else {
|
|
1708
|
+
const np = points.length;
|
|
1709
|
+
for (let it = 0; it < np * reps; it++) {
|
|
1710
|
+
const p = points[it % np];
|
|
1711
|
+
if (p) out.push(rotateDegrees(p, f2(Math.trunc(it / np) * 360 / reps), centerX, centerY));
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
return out;
|
|
1715
|
+
}
|
|
1716
|
+
function customPolygon(points, reps, mirroring = false) {
|
|
1717
|
+
const centerX = 0.5;
|
|
1718
|
+
const centerY = 0.5;
|
|
1719
|
+
const actualPoints = doRepeat(points, reps, centerX, centerY, mirroring);
|
|
1720
|
+
return RoundedPolygon.fromVertices(
|
|
1721
|
+
actualPoints.flatMap((p) => [p.x, p.y]),
|
|
1722
|
+
{ perVertexRounding: actualPoints.map((p) => p.r), centerX, centerY }
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
var builders = {
|
|
1726
|
+
Circle: () => circle({ numVertices: 10 }),
|
|
1727
|
+
Square: () => rectangle({ width: 1, height: 1, rounding: cornerRound30 }),
|
|
1728
|
+
Slanted: () => customPolygon([pnr(0.926, 0.97, cr(0.189, 0.811)), pnr(-0.021, 0.967, cr(0.187, 0.057))], 2),
|
|
1729
|
+
Arch: () => transformedByMatrix(
|
|
1730
|
+
RoundedPolygon.fromVertexCount(4, {
|
|
1731
|
+
perVertexRounding: [cornerRound100, cornerRound100, cornerRound20, cornerRound20]
|
|
1732
|
+
}),
|
|
1733
|
+
rotateNeg135
|
|
1734
|
+
),
|
|
1735
|
+
Fan: () => customPolygon(
|
|
1736
|
+
[
|
|
1737
|
+
pnr(1.004, 1, cr(0.148, 0.417)),
|
|
1738
|
+
pnr(0, 1, cr(0.151)),
|
|
1739
|
+
pnr(0, -3e-3, cr(0.148)),
|
|
1740
|
+
pnr(0.978, 0.02, cr(0.803))
|
|
1741
|
+
],
|
|
1742
|
+
1
|
|
1743
|
+
),
|
|
1744
|
+
Arrow: () => customPolygon(
|
|
1745
|
+
[
|
|
1746
|
+
pnr(0.5, 0.892, cr(0.313)),
|
|
1747
|
+
pnr(-0.216, 1.05, cr(0.207)),
|
|
1748
|
+
pnr(0.499, -0.16, cr(0.215, 1)),
|
|
1749
|
+
pnr(1.225, 1.06, cr(0.211))
|
|
1750
|
+
],
|
|
1751
|
+
1
|
|
1752
|
+
),
|
|
1753
|
+
SemiCircle: () => rectangle({
|
|
1754
|
+
width: f2(1.6),
|
|
1755
|
+
height: 1,
|
|
1756
|
+
perVertexRounding: [cornerRound20, cornerRound20, cornerRound100, cornerRound100]
|
|
1757
|
+
}),
|
|
1758
|
+
Oval: () => transformedByMatrix(transformedByMatrix(circle(), new Matrix().scale(1, f2(0.64))), rotateNeg45),
|
|
1759
|
+
Pill: () => customPolygon(
|
|
1760
|
+
[pnr(0.961, 0.039, cr(0.426)), pnr(1.001, 0.428), pnr(1, 0.609, cr(1))],
|
|
1761
|
+
2,
|
|
1762
|
+
true
|
|
1763
|
+
),
|
|
1764
|
+
Triangle: () => transformedByMatrix(
|
|
1765
|
+
RoundedPolygon.fromVertexCount(3, { rounding: cornerRound20 }),
|
|
1766
|
+
rotateNeg90
|
|
1767
|
+
),
|
|
1768
|
+
Diamond: () => customPolygon([pnr(0.5, 1.096, cr(0.151, 0.524)), pnr(0.04, 0.5, cr(0.159))], 2),
|
|
1769
|
+
ClamShell: () => customPolygon(
|
|
1770
|
+
[pnr(0.171, 0.841, cr(0.159)), pnr(-0.02, 0.5, cr(0.14)), pnr(0.17, 0.159, cr(0.159))],
|
|
1771
|
+
2
|
|
1772
|
+
),
|
|
1773
|
+
Pentagon: () => customPolygon(
|
|
1774
|
+
[pnr(0.5, -9e-3, cr(0.172)), pnr(1.03, 0.365, cr(0.164)), pnr(0.828, 0.97, cr(0.169))],
|
|
1775
|
+
1,
|
|
1776
|
+
true
|
|
1777
|
+
),
|
|
1778
|
+
Gem: () => customPolygon(
|
|
1779
|
+
[
|
|
1780
|
+
pnr(0.499, 1.023, cr(0.241, 0.778)),
|
|
1781
|
+
pnr(-5e-3, 0.792, cr(0.208)),
|
|
1782
|
+
pnr(0.073, 0.258, cr(0.228)),
|
|
1783
|
+
pnr(0.433, -0, cr(0.491))
|
|
1784
|
+
],
|
|
1785
|
+
1,
|
|
1786
|
+
true
|
|
1787
|
+
),
|
|
1788
|
+
Sunny: () => star(8, { innerRadius: f2(0.8), rounding: cornerRound15 }),
|
|
1789
|
+
VerySunny: () => customPolygon([pnr(0.5, 1.08, cr(0.085)), pnr(0.358, 0.843, cr(0.085))], 8),
|
|
1790
|
+
Cookie4Sided: () => customPolygon([pnr(1.237, 1.236, cr(0.258)), pnr(0.5, 0.918, cr(0.233))], 4),
|
|
1791
|
+
// 6-point cookie
|
|
1792
|
+
Cookie6Sided: () => customPolygon([pnr(0.723, 0.884, cr(0.394)), pnr(0.5, 1.099, cr(0.398))], 6),
|
|
1793
|
+
// 7-point cookie
|
|
1794
|
+
Cookie7Sided: () => transformedByMatrix(star(7, { innerRadius: f2(0.75), rounding: cornerRound50 }), rotateNeg90),
|
|
1795
|
+
Cookie9Sided: () => transformedByMatrix(star(9, { innerRadius: f2(0.8), rounding: cornerRound50 }), rotateNeg90),
|
|
1796
|
+
Cookie12Sided: () => transformedByMatrix(star(12, { innerRadius: f2(0.8), rounding: cornerRound50 }), rotateNeg90),
|
|
1797
|
+
Ghostish: () => customPolygon(
|
|
1798
|
+
[
|
|
1799
|
+
pnr(0.5, 0, cr(1)),
|
|
1800
|
+
pnr(1, 0, cr(1)),
|
|
1801
|
+
pnr(1, 1.14, cr(0.254, 0.106)),
|
|
1802
|
+
pnr(0.575, 0.906, cr(0.253))
|
|
1803
|
+
],
|
|
1804
|
+
1,
|
|
1805
|
+
true
|
|
1806
|
+
),
|
|
1807
|
+
Clover4Leaf: () => customPolygon([pnr(0.5, 0.074), pnr(0.725, -0.099, cr(0.476))], 4, true),
|
|
1808
|
+
Clover8Leaf: () => customPolygon([pnr(0.5, 0.036), pnr(0.758, -0.101, cr(0.209))], 8),
|
|
1809
|
+
Burst: () => customPolygon([pnr(0.5, -6e-3, cr(6e-3)), pnr(0.592, 0.158, cr(6e-3))], 12),
|
|
1810
|
+
SoftBurst: () => customPolygon([pnr(0.193, 0.277, cr(0.053)), pnr(0.176, 0.055, cr(0.053))], 10),
|
|
1811
|
+
Boom: () => customPolygon([pnr(0.457, 0.296, cr(7e-3)), pnr(0.5, -0.051, cr(7e-3))], 15),
|
|
1812
|
+
SoftBoom: () => customPolygon(
|
|
1813
|
+
[
|
|
1814
|
+
pnr(0.733, 0.454),
|
|
1815
|
+
pnr(0.839, 0.437, cr(0.532)),
|
|
1816
|
+
pnr(0.949, 0.449, cr(0.439, 1)),
|
|
1817
|
+
pnr(0.998, 0.478, cr(0.174))
|
|
1818
|
+
],
|
|
1819
|
+
16,
|
|
1820
|
+
true
|
|
1821
|
+
),
|
|
1822
|
+
Flower: () => customPolygon(
|
|
1823
|
+
[pnr(0.37, 0.187), pnr(0.416, 0.049, cr(0.381)), pnr(0.479, 1e-3, cr(0.095))],
|
|
1824
|
+
8,
|
|
1825
|
+
true
|
|
1826
|
+
),
|
|
1827
|
+
Puffy: () => transformedByMatrix(
|
|
1828
|
+
customPolygon(
|
|
1829
|
+
[
|
|
1830
|
+
pnr(0.5, 0.053),
|
|
1831
|
+
pnr(0.545, -0.04, cr(0.405)),
|
|
1832
|
+
pnr(0.67, -0.035, cr(0.426)),
|
|
1833
|
+
pnr(0.717, 0.066, cr(0.574)),
|
|
1834
|
+
pnr(0.722, 0.128),
|
|
1835
|
+
pnr(0.777, 2e-3, cr(0.36)),
|
|
1836
|
+
pnr(0.914, 0.149, cr(0.66)),
|
|
1837
|
+
pnr(0.926, 0.289, cr(0.66)),
|
|
1838
|
+
pnr(0.881, 0.346),
|
|
1839
|
+
pnr(0.94, 0.344, cr(0.126)),
|
|
1840
|
+
pnr(1.003, 0.437, cr(0.255))
|
|
1841
|
+
],
|
|
1842
|
+
2,
|
|
1843
|
+
true
|
|
1844
|
+
),
|
|
1845
|
+
new Matrix().scale(1, f2(0.742))
|
|
1846
|
+
),
|
|
1847
|
+
PuffyDiamond: () => customPolygon(
|
|
1848
|
+
[pnr(0.87, 0.13, cr(0.146)), pnr(0.818, 0.357), pnr(1, 0.332, cr(0.853))],
|
|
1849
|
+
4,
|
|
1850
|
+
true
|
|
1851
|
+
),
|
|
1852
|
+
PixelCircle: () => customPolygon(
|
|
1853
|
+
[
|
|
1854
|
+
pnr(0.5, 0),
|
|
1855
|
+
pnr(0.704, 0),
|
|
1856
|
+
pnr(0.704, 0.065),
|
|
1857
|
+
pnr(0.843, 0.065),
|
|
1858
|
+
pnr(0.843, 0.148),
|
|
1859
|
+
pnr(0.926, 0.148),
|
|
1860
|
+
pnr(0.926, 0.296),
|
|
1861
|
+
pnr(1, 0.296)
|
|
1862
|
+
],
|
|
1863
|
+
2,
|
|
1864
|
+
true
|
|
1865
|
+
),
|
|
1866
|
+
PixelTriangle: () => customPolygon(
|
|
1867
|
+
[
|
|
1868
|
+
pnr(0.11, 0.5),
|
|
1869
|
+
pnr(0.113, 0),
|
|
1870
|
+
pnr(0.287, 0),
|
|
1871
|
+
pnr(0.287, 0.087),
|
|
1872
|
+
pnr(0.421, 0.087),
|
|
1873
|
+
pnr(0.421, 0.17),
|
|
1874
|
+
pnr(0.56, 0.17),
|
|
1875
|
+
pnr(0.56, 0.265),
|
|
1876
|
+
pnr(0.674, 0.265),
|
|
1877
|
+
pnr(0.675, 0.344),
|
|
1878
|
+
pnr(0.789, 0.344),
|
|
1879
|
+
pnr(0.789, 0.439),
|
|
1880
|
+
pnr(0.888, 0.439)
|
|
1881
|
+
],
|
|
1882
|
+
1,
|
|
1883
|
+
true
|
|
1884
|
+
),
|
|
1885
|
+
Bun: () => customPolygon(
|
|
1886
|
+
[pnr(0.796, 0.5), pnr(0.853, 0.518, cr(1)), pnr(0.992, 0.631, cr(1)), pnr(0.968, 1, cr(1))],
|
|
1887
|
+
2,
|
|
1888
|
+
true
|
|
1889
|
+
),
|
|
1890
|
+
Heart: () => customPolygon(
|
|
1891
|
+
[
|
|
1892
|
+
pnr(0.5, 0.268, cr(0.016)),
|
|
1893
|
+
pnr(0.792, -0.066, cr(0.958)),
|
|
1894
|
+
pnr(1.064, 0.276, cr(1)),
|
|
1895
|
+
pnr(0.501, 0.946, cr(0.129))
|
|
1896
|
+
],
|
|
1897
|
+
1,
|
|
1898
|
+
true
|
|
1899
|
+
)
|
|
1900
|
+
};
|
|
1901
|
+
var materialShapeNames = Object.keys(builders);
|
|
1902
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1903
|
+
function materialShape(name) {
|
|
1904
|
+
let polygon = cache.get(name);
|
|
1905
|
+
if (polygon === void 0) {
|
|
1906
|
+
polygon = builders[name]().normalized();
|
|
1907
|
+
cache.set(name, polygon);
|
|
1908
|
+
}
|
|
1909
|
+
return polygon;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// src/loading-indicator.ts
|
|
1913
|
+
var INDETERMINATE = [
|
|
1914
|
+
"SoftBurst",
|
|
1915
|
+
"Cookie9Sided",
|
|
1916
|
+
"Pentagon",
|
|
1917
|
+
"Pill",
|
|
1918
|
+
"Sunny",
|
|
1919
|
+
"Cookie4Sided",
|
|
1920
|
+
"Oval"
|
|
1921
|
+
];
|
|
1922
|
+
var indeterminateIndicatorPolygons = () => INDETERMINATE.map(materialShape);
|
|
1923
|
+
var determinateIndicatorPolygons = () => [
|
|
1924
|
+
transformedByMatrix(materialShape("Circle"), new Matrix().rotateZ(f2(360 / 20))),
|
|
1925
|
+
materialShape("SoftBurst")
|
|
1926
|
+
];
|
|
1927
|
+
function morphSequence(polygons, circularSequence) {
|
|
1928
|
+
const morphs = [];
|
|
1929
|
+
for (let i = 0; i < polygons.length; i++) {
|
|
1930
|
+
if (i + 1 < polygons.length) {
|
|
1931
|
+
morphs.push(new Morph(polygons[i].normalized(), polygons[i + 1].normalized()));
|
|
1932
|
+
} else if (circularSequence) {
|
|
1933
|
+
morphs.push(new Morph(polygons[i].normalized(), polygons[0].normalized()));
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
return morphs;
|
|
1937
|
+
}
|
|
1938
|
+
function calculateScaleFactor(polygons) {
|
|
1939
|
+
let scaleFactor = 1;
|
|
1940
|
+
const bounds = [0, 0, 0, 0];
|
|
1941
|
+
const maxBounds = [0, 0, 0, 0];
|
|
1942
|
+
for (const polygon of polygons) {
|
|
1943
|
+
polygon.calculateBounds(bounds);
|
|
1944
|
+
polygon.calculateMaxBounds(maxBounds);
|
|
1945
|
+
const scaleX = f2(f2(bounds[2] - bounds[0]) / f2(maxBounds[2] - maxBounds[0]));
|
|
1946
|
+
const scaleY = f2(f2(bounds[3] - bounds[1]) / f2(maxBounds[3] - maxBounds[1]));
|
|
1947
|
+
scaleFactor = Math.min(scaleFactor, Math.max(scaleX, scaleY));
|
|
1948
|
+
}
|
|
1949
|
+
return scaleFactor;
|
|
1950
|
+
}
|
|
1951
|
+
function processPath(path, width, height, scaleFactor) {
|
|
1952
|
+
const scaled = transformPath(
|
|
1953
|
+
path,
|
|
1954
|
+
new Matrix().scale(f2(width * scaleFactor), f2(height * scaleFactor))
|
|
1955
|
+
);
|
|
1956
|
+
const [left, top, right, bottom] = pathBounds(scaled);
|
|
1957
|
+
const dx = f2(f2(width / 2) - f2(left + f2(f2(right - left) / 2)));
|
|
1958
|
+
const dy = f2(f2(height / 2) - f2(top + f2(f2(bottom - top) / 2)));
|
|
1959
|
+
return scaled.map((command) => {
|
|
1960
|
+
switch (command[0]) {
|
|
1961
|
+
case "M":
|
|
1962
|
+
case "L":
|
|
1963
|
+
return [command[0], f2(command[1] + dx), f2(command[2] + dy)];
|
|
1964
|
+
case "C":
|
|
1965
|
+
return [
|
|
1966
|
+
"C",
|
|
1967
|
+
f2(command[1] + dx),
|
|
1968
|
+
f2(command[2] + dy),
|
|
1969
|
+
f2(command[3] + dx),
|
|
1970
|
+
f2(command[4] + dy),
|
|
1971
|
+
f2(command[5] + dx),
|
|
1972
|
+
f2(command[6] + dy)
|
|
1973
|
+
];
|
|
1974
|
+
case "Z":
|
|
1975
|
+
return command;
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
function determinateFrame(progress, morphCount) {
|
|
1980
|
+
const value = Number.isNaN(progress) ? 0 : Math.min(Math.max(f2(progress), 0), 1);
|
|
1981
|
+
const scaled = f2(morphCount * value);
|
|
1982
|
+
const morphIndex = Math.min(Math.trunc(scaled), morphCount - 1);
|
|
1983
|
+
const morphProgress = value === 1 && morphIndex === morphCount - 1 ? 1 : scaled % 1;
|
|
1984
|
+
return { morphIndex, morphProgress, rotation: f2(-value * 180) };
|
|
1985
|
+
}
|
|
1986
|
+
var LoadingIndicatorShapes = class {
|
|
1987
|
+
morphs;
|
|
1988
|
+
scaleFactor;
|
|
1989
|
+
constructor(polygons, circularSequence, activeIndicatorScale) {
|
|
1990
|
+
if (polygons.length < 2) {
|
|
1991
|
+
throw new Error("indicatorPolygons should have, at least, two RoundedPolygons");
|
|
1992
|
+
}
|
|
1993
|
+
this.morphs = morphSequence(polygons, circularSequence);
|
|
1994
|
+
this.scaleFactor = f2(calculateScaleFactor(polygons) * f2(activeIndicatorScale));
|
|
1995
|
+
}
|
|
1996
|
+
/** The processed path of morph [morphIndex] at [progress] in a [size]-sided square. */
|
|
1997
|
+
path(morphIndex, progress, size) {
|
|
1998
|
+
const morph = this.morphs[morphIndex];
|
|
1999
|
+
return processPath(
|
|
2000
|
+
morphToPath(morph, progress, { startAngle: 0 }),
|
|
2001
|
+
size,
|
|
2002
|
+
size,
|
|
2003
|
+
this.scaleFactor
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
/** {@link path} as an SVG `d` attribute. */
|
|
2007
|
+
pathData(morphIndex, progress, size) {
|
|
2008
|
+
return toSvgPathData(this.path(morphIndex, progress, size));
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
var defaultShapes = /* @__PURE__ */ new Map();
|
|
2012
|
+
var customShapes = /* @__PURE__ */ new WeakMap();
|
|
2013
|
+
function loadingIndicatorShapes(polygons, indeterminate, activeIndicatorScale) {
|
|
2014
|
+
const key = `${String(indeterminate)}:${String(activeIndicatorScale)}`;
|
|
2015
|
+
let cache2 = defaultShapes;
|
|
2016
|
+
if (polygons) {
|
|
2017
|
+
cache2 = customShapes.get(polygons) ?? /* @__PURE__ */ new Map();
|
|
2018
|
+
customShapes.set(polygons, cache2);
|
|
2019
|
+
}
|
|
2020
|
+
let shapes = cache2.get(key);
|
|
2021
|
+
if (!shapes) {
|
|
2022
|
+
const list = polygons ?? (indeterminate ? indeterminateIndicatorPolygons() : determinateIndicatorPolygons());
|
|
2023
|
+
shapes = new LoadingIndicatorShapes(list, indeterminate, activeIndicatorScale);
|
|
2024
|
+
cache2.set(key, shapes);
|
|
2025
|
+
}
|
|
2026
|
+
return shapes;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
export { Corner, CornerRounding, Cubic, Edge, Feature, LoadingIndicatorShapes, Morph, MutableCubic, RoundedPolygon, calculateScaleFactor, circle, determinateFrame, determinateIndicatorPolygons, indeterminateIndicatorPolygons, loadingIndicatorShapes, materialShape, materialShapeNames, morphSequence, morphToPath, pathBounds, pill, pillStar, polygonToPath, processPath, rectangle, star, toShapeOutline, toSvgPathData, translatePath };
|
|
2030
|
+
//# sourceMappingURL=index.js.map
|
|
2031
|
+
//# sourceMappingURL=index.js.map
|