shape-morph 0.1.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 +21 -0
- package/README.md +142 -0
- package/dist/index.cjs +1758 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +219 -0
- package/dist/index.d.ts +219 -0
- package/dist/index.js +1735 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +1808 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +216 -0
- package/dist/react/index.d.ts +216 -0
- package/dist/react/index.js +1804 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +73 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1758 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/utils.ts
|
|
4
|
+
var distanceEpsilon = 1e-4;
|
|
5
|
+
var angleEpsilon = 1e-6;
|
|
6
|
+
var relaxedDistanceEpsilon = 5e-3;
|
|
7
|
+
var floatPi = Math.PI;
|
|
8
|
+
var twoPi = 2 * Math.PI;
|
|
9
|
+
function pt(x, y) {
|
|
10
|
+
return { x, y };
|
|
11
|
+
}
|
|
12
|
+
function ptDistance(p) {
|
|
13
|
+
return Math.sqrt(p.x * p.x + p.y * p.y);
|
|
14
|
+
}
|
|
15
|
+
function ptDirection(p) {
|
|
16
|
+
const d = ptDistance(p);
|
|
17
|
+
return { x: p.x / d, y: p.y / d };
|
|
18
|
+
}
|
|
19
|
+
function ptDot(a, b) {
|
|
20
|
+
return a.x * b.x + a.y * b.y;
|
|
21
|
+
}
|
|
22
|
+
function ptClockwise(a, b) {
|
|
23
|
+
return a.x * b.y - a.y * b.x > 0;
|
|
24
|
+
}
|
|
25
|
+
function ptRotate90(p) {
|
|
26
|
+
return { x: -p.y, y: p.x };
|
|
27
|
+
}
|
|
28
|
+
function ptSub(a, b) {
|
|
29
|
+
return { x: a.x - b.x, y: a.y - b.y };
|
|
30
|
+
}
|
|
31
|
+
function ptAdd(a, b) {
|
|
32
|
+
return { x: a.x + b.x, y: a.y + b.y };
|
|
33
|
+
}
|
|
34
|
+
function ptMul(p, s) {
|
|
35
|
+
return { x: p.x * s, y: p.y * s };
|
|
36
|
+
}
|
|
37
|
+
function ptDiv(p, s) {
|
|
38
|
+
return { x: p.x / s, y: p.y / s };
|
|
39
|
+
}
|
|
40
|
+
function distance(x, y) {
|
|
41
|
+
return Math.sqrt(x * x + y * y);
|
|
42
|
+
}
|
|
43
|
+
function distanceSquared(x, y) {
|
|
44
|
+
return x * x + y * y;
|
|
45
|
+
}
|
|
46
|
+
function directionVector(x, y) {
|
|
47
|
+
const d = distance(x, y);
|
|
48
|
+
return { x: x / d, y: y / d };
|
|
49
|
+
}
|
|
50
|
+
function directionVectorAngle(angleRadians) {
|
|
51
|
+
return { x: Math.cos(angleRadians), y: Math.sin(angleRadians) };
|
|
52
|
+
}
|
|
53
|
+
function radialToCartesian(radius, angleRadians, center = { x: 0, y: 0 }) {
|
|
54
|
+
const d = directionVectorAngle(angleRadians);
|
|
55
|
+
return { x: d.x * radius + center.x, y: d.y * radius + center.y };
|
|
56
|
+
}
|
|
57
|
+
function interpolate(start, stop, fraction) {
|
|
58
|
+
return (1 - fraction) * start + fraction * stop;
|
|
59
|
+
}
|
|
60
|
+
function interpolatePoint(start, stop, fraction) {
|
|
61
|
+
return {
|
|
62
|
+
x: interpolate(start.x, stop.x, fraction),
|
|
63
|
+
y: interpolate(start.y, stop.y, fraction)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function positiveModulo(num, mod) {
|
|
67
|
+
return (num % mod + mod) % mod;
|
|
68
|
+
}
|
|
69
|
+
function convex(previous, current, next) {
|
|
70
|
+
return ptClockwise(ptSub(current, previous), ptSub(next, current));
|
|
71
|
+
}
|
|
72
|
+
function square(x) {
|
|
73
|
+
return x * x;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/core/cubic.ts
|
|
77
|
+
function axisExtremaTs(a, b, c) {
|
|
78
|
+
const zeroIsh = Math.abs(a) < distanceEpsilon;
|
|
79
|
+
if (zeroIsh) {
|
|
80
|
+
if (b !== 0) {
|
|
81
|
+
const t = 2 * c / (-2 * b);
|
|
82
|
+
if (t >= 0 && t <= 1) {
|
|
83
|
+
return [t];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const discriminant = b * b - 4 * a * c;
|
|
89
|
+
if (discriminant < 0) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const sqrtD = Math.sqrt(discriminant);
|
|
93
|
+
const results = [];
|
|
94
|
+
for (const t of [(-b + sqrtD) / (2 * a), (-b - sqrtD) / (2 * a)]) {
|
|
95
|
+
if (t >= 0 && t <= 1) {
|
|
96
|
+
results.push(t);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
var Cubic = class _Cubic {
|
|
102
|
+
points;
|
|
103
|
+
constructor(a, b, c, d, e, f, g, h) {
|
|
104
|
+
if (a instanceof Float64Array) {
|
|
105
|
+
this.points = a;
|
|
106
|
+
} else {
|
|
107
|
+
this.points = new Float64Array([
|
|
108
|
+
a,
|
|
109
|
+
b,
|
|
110
|
+
c,
|
|
111
|
+
d,
|
|
112
|
+
e,
|
|
113
|
+
f,
|
|
114
|
+
g,
|
|
115
|
+
h
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
get anchor0X() {
|
|
120
|
+
return this.points[0];
|
|
121
|
+
}
|
|
122
|
+
get anchor0Y() {
|
|
123
|
+
return this.points[1];
|
|
124
|
+
}
|
|
125
|
+
get control0X() {
|
|
126
|
+
return this.points[2];
|
|
127
|
+
}
|
|
128
|
+
get control0Y() {
|
|
129
|
+
return this.points[3];
|
|
130
|
+
}
|
|
131
|
+
get control1X() {
|
|
132
|
+
return this.points[4];
|
|
133
|
+
}
|
|
134
|
+
get control1Y() {
|
|
135
|
+
return this.points[5];
|
|
136
|
+
}
|
|
137
|
+
get anchor1X() {
|
|
138
|
+
return this.points[6];
|
|
139
|
+
}
|
|
140
|
+
get anchor1Y() {
|
|
141
|
+
return this.points[7];
|
|
142
|
+
}
|
|
143
|
+
pointOnCurve(t) {
|
|
144
|
+
const u = 1 - t;
|
|
145
|
+
return pt(
|
|
146
|
+
this.anchor0X * (u * u * u) + this.control0X * (3 * t * u * u) + this.control1X * (3 * t * t * u) + this.anchor1X * (t * t * t),
|
|
147
|
+
this.anchor0Y * (u * u * u) + this.control0Y * (3 * t * u * u) + this.control1Y * (3 * t * t * u) + this.anchor1Y * (t * t * t)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
zeroLength() {
|
|
151
|
+
return Math.abs(this.anchor0X - this.anchor1X) < distanceEpsilon && Math.abs(this.anchor0Y - this.anchor1Y) < distanceEpsilon;
|
|
152
|
+
}
|
|
153
|
+
convexTo(next) {
|
|
154
|
+
const prevVertex = pt(this.anchor0X, this.anchor0Y);
|
|
155
|
+
const currVertex = pt(this.anchor1X, this.anchor1Y);
|
|
156
|
+
const nextVertex = pt(next.anchor1X, next.anchor1Y);
|
|
157
|
+
return convex(prevVertex, currVertex, nextVertex);
|
|
158
|
+
}
|
|
159
|
+
split(t) {
|
|
160
|
+
const u = 1 - t;
|
|
161
|
+
const poc = this.pointOnCurve(t);
|
|
162
|
+
return [
|
|
163
|
+
new _Cubic(
|
|
164
|
+
this.anchor0X,
|
|
165
|
+
this.anchor0Y,
|
|
166
|
+
this.anchor0X * u + this.control0X * t,
|
|
167
|
+
this.anchor0Y * u + this.control0Y * t,
|
|
168
|
+
this.anchor0X * (u * u) + this.control0X * (2 * u * t) + this.control1X * (t * t),
|
|
169
|
+
this.anchor0Y * (u * u) + this.control0Y * (2 * u * t) + this.control1Y * (t * t),
|
|
170
|
+
poc.x,
|
|
171
|
+
poc.y
|
|
172
|
+
),
|
|
173
|
+
new _Cubic(
|
|
174
|
+
poc.x,
|
|
175
|
+
poc.y,
|
|
176
|
+
this.control0X * (u * u) + this.control1X * (2 * u * t) + this.anchor1X * (t * t),
|
|
177
|
+
this.control0Y * (u * u) + this.control1Y * (2 * u * t) + this.anchor1Y * (t * t),
|
|
178
|
+
this.control1X * u + this.anchor1X * t,
|
|
179
|
+
this.control1Y * u + this.anchor1Y * t,
|
|
180
|
+
this.anchor1X,
|
|
181
|
+
this.anchor1Y
|
|
182
|
+
)
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
reverse() {
|
|
186
|
+
return new _Cubic(
|
|
187
|
+
this.anchor1X,
|
|
188
|
+
this.anchor1Y,
|
|
189
|
+
this.control1X,
|
|
190
|
+
this.control1Y,
|
|
191
|
+
this.control0X,
|
|
192
|
+
this.control0Y,
|
|
193
|
+
this.anchor0X,
|
|
194
|
+
this.anchor0Y
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
transformed(f) {
|
|
198
|
+
const newPoints = new Float64Array(8);
|
|
199
|
+
for (let i = 0; i < 8; i += 2) {
|
|
200
|
+
const r = f(this.points[i], this.points[i + 1]);
|
|
201
|
+
newPoints[i] = r.x;
|
|
202
|
+
newPoints[i + 1] = r.y;
|
|
203
|
+
}
|
|
204
|
+
return new _Cubic(newPoints);
|
|
205
|
+
}
|
|
206
|
+
calculateBounds(approximate = true) {
|
|
207
|
+
if (this.zeroLength()) {
|
|
208
|
+
return [this.anchor0X, this.anchor0Y, this.anchor0X, this.anchor0Y];
|
|
209
|
+
}
|
|
210
|
+
let minX = Math.min(this.anchor0X, this.anchor1X);
|
|
211
|
+
let minY = Math.min(this.anchor0Y, this.anchor1Y);
|
|
212
|
+
let maxX = Math.max(this.anchor0X, this.anchor1X);
|
|
213
|
+
let maxY = Math.max(this.anchor0Y, this.anchor1Y);
|
|
214
|
+
if (approximate) {
|
|
215
|
+
return [
|
|
216
|
+
Math.min(minX, Math.min(this.control0X, this.control1X)),
|
|
217
|
+
Math.min(minY, Math.min(this.control0Y, this.control1Y)),
|
|
218
|
+
Math.max(maxX, Math.max(this.control0X, this.control1X)),
|
|
219
|
+
Math.max(maxY, Math.max(this.control0Y, this.control1Y))
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
const xCoeffs = [
|
|
223
|
+
-this.anchor0X + 3 * this.control0X - 3 * this.control1X + this.anchor1X,
|
|
224
|
+
2 * this.anchor0X - 4 * this.control0X + 2 * this.control1X,
|
|
225
|
+
-this.anchor0X + this.control0X
|
|
226
|
+
];
|
|
227
|
+
for (const t of axisExtremaTs(...xCoeffs)) {
|
|
228
|
+
const v = this.pointOnCurve(t).x;
|
|
229
|
+
minX = Math.min(minX, v);
|
|
230
|
+
maxX = Math.max(maxX, v);
|
|
231
|
+
}
|
|
232
|
+
const yCoeffs = [
|
|
233
|
+
-this.anchor0Y + 3 * this.control0Y - 3 * this.control1Y + this.anchor1Y,
|
|
234
|
+
2 * this.anchor0Y - 4 * this.control0Y + 2 * this.control1Y,
|
|
235
|
+
-this.anchor0Y + this.control0Y
|
|
236
|
+
];
|
|
237
|
+
for (const t of axisExtremaTs(...yCoeffs)) {
|
|
238
|
+
const v = this.pointOnCurve(t).y;
|
|
239
|
+
minY = Math.min(minY, v);
|
|
240
|
+
maxY = Math.max(maxY, v);
|
|
241
|
+
}
|
|
242
|
+
return [minX, minY, maxX, maxY];
|
|
243
|
+
}
|
|
244
|
+
static straightLine(x0, y0, x1, y1) {
|
|
245
|
+
return new _Cubic(
|
|
246
|
+
x0,
|
|
247
|
+
y0,
|
|
248
|
+
interpolate(x0, x1, 1 / 3),
|
|
249
|
+
interpolate(y0, y1, 1 / 3),
|
|
250
|
+
interpolate(x0, x1, 2 / 3),
|
|
251
|
+
interpolate(y0, y1, 2 / 3),
|
|
252
|
+
x1,
|
|
253
|
+
y1
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
static circularArc(centerX, centerY, x0, y0, x1, y1) {
|
|
257
|
+
const p0d = directionVector(x0 - centerX, y0 - centerY);
|
|
258
|
+
const p1d = directionVector(x1 - centerX, y1 - centerY);
|
|
259
|
+
const rotatedP0 = ptRotate90(p0d);
|
|
260
|
+
const rotatedP1 = ptRotate90(p1d);
|
|
261
|
+
const clockwise = rotatedP0.x * (x1 - centerX) + rotatedP0.y * (y1 - centerY) >= 0;
|
|
262
|
+
const cosa = p0d.x * p1d.x + p0d.y * p1d.y;
|
|
263
|
+
if (cosa > 0.999) {
|
|
264
|
+
return _Cubic.straightLine(x0, y0, x1, y1);
|
|
265
|
+
}
|
|
266
|
+
const k = distance(x0 - centerX, y0 - centerY) * 4 * (Math.sqrt(2 * (1 - cosa)) - Math.sqrt(1 - cosa * cosa)) / (3 * (1 - cosa)) * (clockwise ? 1 : -1);
|
|
267
|
+
return new _Cubic(
|
|
268
|
+
x0,
|
|
269
|
+
y0,
|
|
270
|
+
x0 + rotatedP0.x * k,
|
|
271
|
+
y0 + rotatedP0.y * k,
|
|
272
|
+
x1 - rotatedP1.x * k,
|
|
273
|
+
y1 - rotatedP1.y * k,
|
|
274
|
+
x1,
|
|
275
|
+
y1
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
static empty(x0, y0) {
|
|
279
|
+
return new _Cubic(x0, y0, x0, y0, x0, y0, x0, y0);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
function featureTransformed(f, transform) {
|
|
283
|
+
const newCubics = f.cubics.map((c) => c.transformed(transform));
|
|
284
|
+
if (f.type === "edge") {
|
|
285
|
+
return { type: "edge", cubics: newCubics };
|
|
286
|
+
}
|
|
287
|
+
return { type: "corner", cubics: newCubics, convex: f.convex };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/core/polygon.ts
|
|
291
|
+
var unrounded = { radius: 0, smoothing: 0 };
|
|
292
|
+
function cornerRounding(radius, smoothing = 0) {
|
|
293
|
+
return { radius, smoothing };
|
|
294
|
+
}
|
|
295
|
+
var RoundedPolygon = class _RoundedPolygon {
|
|
296
|
+
features;
|
|
297
|
+
center;
|
|
298
|
+
cubics;
|
|
299
|
+
constructor(features, center) {
|
|
300
|
+
this.features = features;
|
|
301
|
+
this.center = center;
|
|
302
|
+
this.cubics = buildCubicList(features, center);
|
|
303
|
+
}
|
|
304
|
+
get centerX() {
|
|
305
|
+
return this.center.x;
|
|
306
|
+
}
|
|
307
|
+
get centerY() {
|
|
308
|
+
return this.center.y;
|
|
309
|
+
}
|
|
310
|
+
transformed(f) {
|
|
311
|
+
const newCenter = f(this.center.x, this.center.y);
|
|
312
|
+
const newFeatures = this.features.map(
|
|
313
|
+
(feat) => featureTransformed(feat, f)
|
|
314
|
+
);
|
|
315
|
+
return new _RoundedPolygon(newFeatures, newCenter);
|
|
316
|
+
}
|
|
317
|
+
normalized() {
|
|
318
|
+
const bounds = this.calculateBounds();
|
|
319
|
+
const width = bounds[2] - bounds[0];
|
|
320
|
+
const height = bounds[3] - bounds[1];
|
|
321
|
+
const side = Math.max(width, height);
|
|
322
|
+
const offsetX = (side - width) / 2 - bounds[0];
|
|
323
|
+
const offsetY = (side - height) / 2 - bounds[1];
|
|
324
|
+
return this.transformed(
|
|
325
|
+
(x, y) => pt((x + offsetX) / side, (y + offsetY) / side)
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
calculateBounds(approximate = true) {
|
|
329
|
+
let minX = Number.MAX_VALUE;
|
|
330
|
+
let minY = Number.MAX_VALUE;
|
|
331
|
+
let maxX = -Number.MAX_VALUE;
|
|
332
|
+
let maxY = -Number.MAX_VALUE;
|
|
333
|
+
for (const cubic of this.cubics) {
|
|
334
|
+
const b = cubic.calculateBounds(approximate);
|
|
335
|
+
minX = Math.min(minX, b[0]);
|
|
336
|
+
minY = Math.min(minY, b[1]);
|
|
337
|
+
maxX = Math.max(maxX, b[2]);
|
|
338
|
+
maxY = Math.max(maxY, b[3]);
|
|
339
|
+
}
|
|
340
|
+
return [minX, minY, maxX, maxY];
|
|
341
|
+
}
|
|
342
|
+
calculateMaxBounds() {
|
|
343
|
+
let maxDistSq = 0;
|
|
344
|
+
for (const cubic of this.cubics) {
|
|
345
|
+
const anchorDist = distanceSquared(
|
|
346
|
+
cubic.anchor0X - this.centerX,
|
|
347
|
+
cubic.anchor0Y - this.centerY
|
|
348
|
+
);
|
|
349
|
+
const mid = cubic.pointOnCurve(0.5);
|
|
350
|
+
const midDist = distanceSquared(
|
|
351
|
+
mid.x - this.centerX,
|
|
352
|
+
mid.y - this.centerY
|
|
353
|
+
);
|
|
354
|
+
maxDistSq = Math.max(maxDistSq, Math.max(anchorDist, midDist));
|
|
355
|
+
}
|
|
356
|
+
const d = Math.sqrt(maxDistSq);
|
|
357
|
+
return [
|
|
358
|
+
this.centerX - d,
|
|
359
|
+
this.centerY - d,
|
|
360
|
+
this.centerX + d,
|
|
361
|
+
this.centerY + d
|
|
362
|
+
];
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
function resolveFeatureCubics(features, index, splitStart, splitEnd) {
|
|
366
|
+
if (index === 0 && splitEnd) {
|
|
367
|
+
return splitEnd;
|
|
368
|
+
}
|
|
369
|
+
if (index === features.length) {
|
|
370
|
+
return splitStart;
|
|
371
|
+
}
|
|
372
|
+
return features[index].cubics;
|
|
373
|
+
}
|
|
374
|
+
function processCubic(cubic, state) {
|
|
375
|
+
if (cubic.zeroLength()) {
|
|
376
|
+
if (state.last) {
|
|
377
|
+
const newPoints = new Float64Array(state.last.points);
|
|
378
|
+
newPoints[6] = cubic.anchor1X;
|
|
379
|
+
newPoints[7] = cubic.anchor1Y;
|
|
380
|
+
state.last = new Cubic(newPoints);
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (state.last) {
|
|
385
|
+
state.result.push(state.last);
|
|
386
|
+
}
|
|
387
|
+
state.last = cubic;
|
|
388
|
+
if (!state.first) {
|
|
389
|
+
state.first = cubic;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function buildCubicList(features, center) {
|
|
393
|
+
let splitStart = null;
|
|
394
|
+
let splitEnd = null;
|
|
395
|
+
if (features.length > 0 && features[0].cubics.length === 3) {
|
|
396
|
+
const centerCubic = features[0].cubics[1];
|
|
397
|
+
const [start, end] = centerCubic.split(0.5);
|
|
398
|
+
splitStart = [features[0].cubics[0], start];
|
|
399
|
+
splitEnd = [end, features[0].cubics[2]];
|
|
400
|
+
}
|
|
401
|
+
const state = {
|
|
402
|
+
first: null,
|
|
403
|
+
last: null,
|
|
404
|
+
result: []
|
|
405
|
+
};
|
|
406
|
+
for (let i = 0; i <= features.length; i++) {
|
|
407
|
+
const featureCubics = resolveFeatureCubics(
|
|
408
|
+
features,
|
|
409
|
+
i,
|
|
410
|
+
splitStart,
|
|
411
|
+
splitEnd
|
|
412
|
+
);
|
|
413
|
+
if (!featureCubics) {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
for (const cubic of featureCubics) {
|
|
417
|
+
processCubic(cubic, state);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (state.last && state.first) {
|
|
421
|
+
state.result.push(
|
|
422
|
+
new Cubic(
|
|
423
|
+
state.last.anchor0X,
|
|
424
|
+
state.last.anchor0Y,
|
|
425
|
+
state.last.control0X,
|
|
426
|
+
state.last.control0Y,
|
|
427
|
+
state.last.control1X,
|
|
428
|
+
state.last.control1Y,
|
|
429
|
+
state.first.anchor0X,
|
|
430
|
+
state.first.anchor0Y
|
|
431
|
+
)
|
|
432
|
+
);
|
|
433
|
+
} else {
|
|
434
|
+
state.result.push(Cubic.empty(center.x, center.y));
|
|
435
|
+
}
|
|
436
|
+
return state.result;
|
|
437
|
+
}
|
|
438
|
+
var RoundedCorner = class {
|
|
439
|
+
d1;
|
|
440
|
+
d2;
|
|
441
|
+
cornerRadius;
|
|
442
|
+
smoothing;
|
|
443
|
+
cosAngle;
|
|
444
|
+
sinAngle;
|
|
445
|
+
expectedRoundCut;
|
|
446
|
+
center = pt(0, 0);
|
|
447
|
+
p0;
|
|
448
|
+
p1;
|
|
449
|
+
p2;
|
|
450
|
+
constructor(p0, p1, p2, rounding) {
|
|
451
|
+
this.p0 = p0;
|
|
452
|
+
this.p1 = p1;
|
|
453
|
+
this.p2 = p2;
|
|
454
|
+
const v01 = ptSub(p0, p1);
|
|
455
|
+
const v21 = ptSub(p2, p1);
|
|
456
|
+
const d01 = ptDistance(v01);
|
|
457
|
+
const d21 = ptDistance(v21);
|
|
458
|
+
if (d01 > 0 && d21 > 0) {
|
|
459
|
+
this.d1 = ptDiv(v01, d01);
|
|
460
|
+
this.d2 = ptDiv(v21, d21);
|
|
461
|
+
this.cornerRadius = rounding.radius;
|
|
462
|
+
this.smoothing = rounding.smoothing;
|
|
463
|
+
this.cosAngle = ptDot(this.d1, this.d2);
|
|
464
|
+
this.sinAngle = Math.sqrt(1 - square(this.cosAngle));
|
|
465
|
+
this.expectedRoundCut = this.sinAngle > 1e-3 ? this.cornerRadius * (this.cosAngle + 1) / this.sinAngle : 0;
|
|
466
|
+
} else {
|
|
467
|
+
this.d1 = pt(0, 0);
|
|
468
|
+
this.d2 = pt(0, 0);
|
|
469
|
+
this.cornerRadius = 0;
|
|
470
|
+
this.smoothing = 0;
|
|
471
|
+
this.cosAngle = 0;
|
|
472
|
+
this.sinAngle = 0;
|
|
473
|
+
this.expectedRoundCut = 0;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
get expectedCut() {
|
|
477
|
+
return (1 + this.smoothing) * this.expectedRoundCut;
|
|
478
|
+
}
|
|
479
|
+
calculateActualSmoothingValue(allowedCut) {
|
|
480
|
+
if (allowedCut > this.expectedCut) {
|
|
481
|
+
return this.smoothing;
|
|
482
|
+
}
|
|
483
|
+
if (allowedCut > this.expectedRoundCut) {
|
|
484
|
+
return this.smoothing * (allowedCut - this.expectedRoundCut) / (this.expectedCut - this.expectedRoundCut);
|
|
485
|
+
}
|
|
486
|
+
return 0;
|
|
487
|
+
}
|
|
488
|
+
lineIntersection(p0, d0, p1, d1) {
|
|
489
|
+
const rotatedD1 = ptRotate90(d1);
|
|
490
|
+
const den = ptDot(d0, rotatedD1);
|
|
491
|
+
if (Math.abs(den) < distanceEpsilon) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
const diff = ptSub(p1, p0);
|
|
495
|
+
const num = ptDot(diff, rotatedD1);
|
|
496
|
+
if (Math.abs(den) < distanceEpsilon * Math.abs(num)) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
const k = num / den;
|
|
500
|
+
return ptAdd(p0, ptMul(d0, k));
|
|
501
|
+
}
|
|
502
|
+
computeFlankingCurve(actualRoundCut, actualSmoothingValues, corner, sideStart, circleSegmentIntersection, otherCircleSegmentIntersection, circleCenter, actualR) {
|
|
503
|
+
const sideDirection = ptDirection(ptSub(sideStart, corner));
|
|
504
|
+
const curveStart = ptAdd(
|
|
505
|
+
corner,
|
|
506
|
+
ptMul(sideDirection, actualRoundCut * (1 + actualSmoothingValues))
|
|
507
|
+
);
|
|
508
|
+
const p = interpolatePoint(
|
|
509
|
+
circleSegmentIntersection,
|
|
510
|
+
ptDiv(
|
|
511
|
+
ptAdd(circleSegmentIntersection, otherCircleSegmentIntersection),
|
|
512
|
+
2
|
|
513
|
+
),
|
|
514
|
+
actualSmoothingValues
|
|
515
|
+
);
|
|
516
|
+
const curveEnd = ptAdd(
|
|
517
|
+
circleCenter,
|
|
518
|
+
ptMul(
|
|
519
|
+
directionVector(p.x - circleCenter.x, p.y - circleCenter.y),
|
|
520
|
+
actualR
|
|
521
|
+
)
|
|
522
|
+
);
|
|
523
|
+
const circleTangent = ptRotate90(ptSub(curveEnd, circleCenter));
|
|
524
|
+
const anchorEnd = this.lineIntersection(
|
|
525
|
+
sideStart,
|
|
526
|
+
sideDirection,
|
|
527
|
+
curveEnd,
|
|
528
|
+
circleTangent
|
|
529
|
+
) ?? circleSegmentIntersection;
|
|
530
|
+
const anchorStart = ptDiv(ptAdd(curveStart, ptMul(anchorEnd, 2)), 3);
|
|
531
|
+
return new Cubic(
|
|
532
|
+
curveStart.x,
|
|
533
|
+
curveStart.y,
|
|
534
|
+
anchorStart.x,
|
|
535
|
+
anchorStart.y,
|
|
536
|
+
anchorEnd.x,
|
|
537
|
+
anchorEnd.y,
|
|
538
|
+
curveEnd.x,
|
|
539
|
+
curveEnd.y
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
getCubics(allowedCut0, allowedCut1 = allowedCut0) {
|
|
543
|
+
const allowedCut = Math.min(allowedCut0, allowedCut1);
|
|
544
|
+
if (this.expectedRoundCut < distanceEpsilon || allowedCut < distanceEpsilon || this.cornerRadius < distanceEpsilon) {
|
|
545
|
+
this.center = this.p1;
|
|
546
|
+
return [Cubic.straightLine(this.p1.x, this.p1.y, this.p1.x, this.p1.y)];
|
|
547
|
+
}
|
|
548
|
+
const actualRoundCut = Math.min(allowedCut, this.expectedRoundCut);
|
|
549
|
+
const actualSmoothing0 = this.calculateActualSmoothingValue(allowedCut0);
|
|
550
|
+
const actualSmoothing1 = this.calculateActualSmoothingValue(allowedCut1);
|
|
551
|
+
const actualR = this.cornerRadius * (actualRoundCut / this.expectedRoundCut);
|
|
552
|
+
const centerDistance = Math.sqrt(square(actualR) + square(actualRoundCut));
|
|
553
|
+
const halfDir = ptDiv(ptAdd(this.d1, this.d2), 2);
|
|
554
|
+
const halfDirNorm = ptDirection(halfDir);
|
|
555
|
+
this.center = ptAdd(this.p1, ptMul(halfDirNorm, centerDistance));
|
|
556
|
+
const circleIntersection0 = ptAdd(this.p1, ptMul(this.d1, actualRoundCut));
|
|
557
|
+
const circleIntersection2 = ptAdd(this.p1, ptMul(this.d2, actualRoundCut));
|
|
558
|
+
const flanking0 = this.computeFlankingCurve(
|
|
559
|
+
actualRoundCut,
|
|
560
|
+
actualSmoothing0,
|
|
561
|
+
this.p1,
|
|
562
|
+
this.p0,
|
|
563
|
+
circleIntersection0,
|
|
564
|
+
circleIntersection2,
|
|
565
|
+
this.center,
|
|
566
|
+
actualR
|
|
567
|
+
);
|
|
568
|
+
const flanking2 = this.computeFlankingCurve(
|
|
569
|
+
actualRoundCut,
|
|
570
|
+
actualSmoothing1,
|
|
571
|
+
this.p1,
|
|
572
|
+
this.p2,
|
|
573
|
+
circleIntersection2,
|
|
574
|
+
circleIntersection0,
|
|
575
|
+
this.center,
|
|
576
|
+
actualR
|
|
577
|
+
).reverse();
|
|
578
|
+
return [
|
|
579
|
+
flanking0,
|
|
580
|
+
Cubic.circularArc(
|
|
581
|
+
this.center.x,
|
|
582
|
+
this.center.y,
|
|
583
|
+
flanking0.anchor1X,
|
|
584
|
+
flanking0.anchor1Y,
|
|
585
|
+
flanking2.anchor0X,
|
|
586
|
+
flanking2.anchor0Y
|
|
587
|
+
),
|
|
588
|
+
flanking2
|
|
589
|
+
];
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
function calculateCenter(vertices) {
|
|
593
|
+
let cx = 0;
|
|
594
|
+
let cy = 0;
|
|
595
|
+
for (let i = 0; i < vertices.length; i += 2) {
|
|
596
|
+
cx += vertices[i];
|
|
597
|
+
cy += vertices[i + 1];
|
|
598
|
+
}
|
|
599
|
+
const n = vertices.length / 2;
|
|
600
|
+
return pt(cx / n, cy / n);
|
|
601
|
+
}
|
|
602
|
+
function verticesFromNumVerts(numVertices, radius, centerX, centerY) {
|
|
603
|
+
const result = [];
|
|
604
|
+
for (let i = 0; i < numVertices; i++) {
|
|
605
|
+
const v = radialToCartesian(
|
|
606
|
+
radius,
|
|
607
|
+
floatPi / numVertices * 2 * i,
|
|
608
|
+
pt(centerX, centerY)
|
|
609
|
+
);
|
|
610
|
+
result.push(v.x, v.y);
|
|
611
|
+
}
|
|
612
|
+
return result;
|
|
613
|
+
}
|
|
614
|
+
function createPolygonFromVertices(vertices, rounding = unrounded, perVertexRounding = null, centerX = Number.MIN_VALUE, centerY = Number.MIN_VALUE) {
|
|
615
|
+
const n = vertices.length / 2;
|
|
616
|
+
const roundedCorners = [];
|
|
617
|
+
for (let i = 0; i < n; i++) {
|
|
618
|
+
const vtxRounding = perVertexRounding?.[i] ?? rounding;
|
|
619
|
+
const prevIndex = (i + n - 1) % n * 2;
|
|
620
|
+
const nextIndex = (i + 1) % n * 2;
|
|
621
|
+
roundedCorners.push(
|
|
622
|
+
new RoundedCorner(
|
|
623
|
+
pt(vertices[prevIndex], vertices[prevIndex + 1]),
|
|
624
|
+
pt(vertices[i * 2], vertices[i * 2 + 1]),
|
|
625
|
+
pt(vertices[nextIndex], vertices[nextIndex + 1]),
|
|
626
|
+
vtxRounding
|
|
627
|
+
)
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
const cutAdjusts = [];
|
|
631
|
+
for (let ix = 0; ix < n; ix++) {
|
|
632
|
+
const expectedRoundCut = roundedCorners[ix].expectedRoundCut + roundedCorners[(ix + 1) % n].expectedRoundCut;
|
|
633
|
+
const expectedCut = roundedCorners[ix].expectedCut + roundedCorners[(ix + 1) % n].expectedCut;
|
|
634
|
+
const vtxX = vertices[ix * 2];
|
|
635
|
+
const vtxY = vertices[ix * 2 + 1];
|
|
636
|
+
const nextVtxX = vertices[(ix + 1) % n * 2];
|
|
637
|
+
const nextVtxY = vertices[(ix + 1) % n * 2 + 1];
|
|
638
|
+
const sideSize = distance(vtxX - nextVtxX, vtxY - nextVtxY);
|
|
639
|
+
if (expectedRoundCut > sideSize) {
|
|
640
|
+
cutAdjusts.push([sideSize / expectedRoundCut, 0]);
|
|
641
|
+
} else if (expectedCut > sideSize) {
|
|
642
|
+
cutAdjusts.push([
|
|
643
|
+
1,
|
|
644
|
+
(sideSize - expectedRoundCut) / (expectedCut - expectedRoundCut)
|
|
645
|
+
]);
|
|
646
|
+
} else {
|
|
647
|
+
cutAdjusts.push([1, 1]);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const corners = [];
|
|
651
|
+
for (let i = 0; i < n; i++) {
|
|
652
|
+
const allowedCuts = [];
|
|
653
|
+
for (let delta = 0; delta <= 1; delta++) {
|
|
654
|
+
const [roundCutRatio, cutRatio] = cutAdjusts[(i + n - 1 + delta) % n];
|
|
655
|
+
allowedCuts.push(
|
|
656
|
+
roundedCorners[i].expectedRoundCut * roundCutRatio + (roundedCorners[i].expectedCut - roundedCorners[i].expectedRoundCut) * cutRatio
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
corners.push(roundedCorners[i].getCubics(allowedCuts[0], allowedCuts[1]));
|
|
660
|
+
}
|
|
661
|
+
const tempFeatures = [];
|
|
662
|
+
for (let i = 0; i < n; i++) {
|
|
663
|
+
const prevVtxIndex = (i + n - 1) % n;
|
|
664
|
+
const nextVtxIndex = (i + 1) % n;
|
|
665
|
+
const currVertex = pt(vertices[i * 2], vertices[i * 2 + 1]);
|
|
666
|
+
const prevVertex = pt(
|
|
667
|
+
vertices[prevVtxIndex * 2],
|
|
668
|
+
vertices[prevVtxIndex * 2 + 1]
|
|
669
|
+
);
|
|
670
|
+
const nextVertex = pt(
|
|
671
|
+
vertices[nextVtxIndex * 2],
|
|
672
|
+
vertices[nextVtxIndex * 2 + 1]
|
|
673
|
+
);
|
|
674
|
+
const isConvex = convexFn(prevVertex, currVertex, nextVertex);
|
|
675
|
+
tempFeatures.push({ type: "corner", cubics: corners[i], convex: isConvex });
|
|
676
|
+
tempFeatures.push({
|
|
677
|
+
type: "edge",
|
|
678
|
+
cubics: [
|
|
679
|
+
Cubic.straightLine(
|
|
680
|
+
corners[i][corners[i].length - 1].anchor1X,
|
|
681
|
+
corners[i][corners[i].length - 1].anchor1Y,
|
|
682
|
+
corners[(i + 1) % n][0].anchor0X,
|
|
683
|
+
corners[(i + 1) % n][0].anchor0Y
|
|
684
|
+
)
|
|
685
|
+
]
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
const center = centerX === Number.MIN_VALUE || centerY === Number.MIN_VALUE ? calculateCenter(vertices) : pt(centerX, centerY);
|
|
689
|
+
return new RoundedPolygon(tempFeatures, center);
|
|
690
|
+
}
|
|
691
|
+
function convexFn(prev, curr, next) {
|
|
692
|
+
const v1 = ptSub(curr, prev);
|
|
693
|
+
const v2 = ptSub(next, curr);
|
|
694
|
+
return v1.x * v2.y - v1.y * v2.x > 0;
|
|
695
|
+
}
|
|
696
|
+
function createPolygon(numVertices, radius = 1, centerX = 0, centerY = 0, rounding = unrounded, perVertexRounding = null) {
|
|
697
|
+
const vertices = verticesFromNumVerts(numVertices, radius, centerX, centerY);
|
|
698
|
+
return createPolygonFromVertices(
|
|
699
|
+
vertices,
|
|
700
|
+
rounding,
|
|
701
|
+
perVertexRounding,
|
|
702
|
+
centerX,
|
|
703
|
+
centerY
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
function createCircle(numVertices = 8, radius = 1, centerX = 0, centerY = 0) {
|
|
707
|
+
const theta = floatPi / numVertices;
|
|
708
|
+
const polygonRadius = radius / Math.cos(theta);
|
|
709
|
+
return createPolygon(
|
|
710
|
+
numVertices,
|
|
711
|
+
polygonRadius,
|
|
712
|
+
centerX,
|
|
713
|
+
centerY,
|
|
714
|
+
cornerRounding(radius)
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
function createRectangle(width = 2, height = 2, rounding = unrounded, perVertexRounding = null, centerX = 0, centerY = 0) {
|
|
718
|
+
const left = centerX - width / 2;
|
|
719
|
+
const top = centerY - height / 2;
|
|
720
|
+
const right = centerX + width / 2;
|
|
721
|
+
const bottom = centerY + height / 2;
|
|
722
|
+
return createPolygonFromVertices(
|
|
723
|
+
[right, bottom, left, bottom, left, top, right, top],
|
|
724
|
+
rounding,
|
|
725
|
+
perVertexRounding,
|
|
726
|
+
centerX,
|
|
727
|
+
centerY
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
function starVerticesFromNumVerts(numVerticesPerRadius, radius, innerRadius, centerX, centerY) {
|
|
731
|
+
const result = [];
|
|
732
|
+
for (let i = 0; i < numVerticesPerRadius; i++) {
|
|
733
|
+
let v = radialToCartesian(radius, floatPi / numVerticesPerRadius * 2 * i);
|
|
734
|
+
result.push(v.x + centerX, v.y + centerY);
|
|
735
|
+
v = radialToCartesian(
|
|
736
|
+
innerRadius,
|
|
737
|
+
floatPi / numVerticesPerRadius * (2 * i + 1)
|
|
738
|
+
);
|
|
739
|
+
result.push(v.x + centerX, v.y + centerY);
|
|
740
|
+
}
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
function createStar(numVerticesPerRadius, radius = 1, innerRadius = 0.5, rounding = unrounded, innerRounding = null, perVertexRounding = null, centerX = 0, centerY = 0) {
|
|
744
|
+
let pvRounding = perVertexRounding;
|
|
745
|
+
if (!pvRounding && innerRounding) {
|
|
746
|
+
pvRounding = [];
|
|
747
|
+
for (let i = 0; i < numVerticesPerRadius; i++) {
|
|
748
|
+
pvRounding.push(rounding, innerRounding);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const vertices = starVerticesFromNumVerts(
|
|
752
|
+
numVerticesPerRadius,
|
|
753
|
+
radius,
|
|
754
|
+
innerRadius,
|
|
755
|
+
centerX,
|
|
756
|
+
centerY
|
|
757
|
+
);
|
|
758
|
+
return createPolygonFromVertices(
|
|
759
|
+
vertices,
|
|
760
|
+
rounding,
|
|
761
|
+
pvRounding,
|
|
762
|
+
centerX,
|
|
763
|
+
centerY
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/core/material-shapes.ts
|
|
768
|
+
function pnr(x, y, r = unrounded) {
|
|
769
|
+
return { x, y, r };
|
|
770
|
+
}
|
|
771
|
+
function angleDegrees(x, y) {
|
|
772
|
+
return Math.atan2(y, x) * 180 / floatPi;
|
|
773
|
+
}
|
|
774
|
+
function toRadians(degrees) {
|
|
775
|
+
return degrees / 360 * 2 * floatPi;
|
|
776
|
+
}
|
|
777
|
+
function distancePt(x, y) {
|
|
778
|
+
return Math.sqrt(x * x + y * y);
|
|
779
|
+
}
|
|
780
|
+
function rotateDegrees(px, py, angle, cx = 0, cy = 0) {
|
|
781
|
+
const a = toRadians(angle);
|
|
782
|
+
const ox = px - cx;
|
|
783
|
+
const oy = py - cy;
|
|
784
|
+
return [
|
|
785
|
+
ox * Math.cos(a) - oy * Math.sin(a) + cx,
|
|
786
|
+
ox * Math.sin(a) + oy * Math.cos(a) + cy
|
|
787
|
+
];
|
|
788
|
+
}
|
|
789
|
+
function doRepeatMirrored(points, reps, centerX, centerY) {
|
|
790
|
+
const angles = points.map((p) => angleDegrees(p.x - centerX, p.y - centerY));
|
|
791
|
+
const distances = points.map((p) => distancePt(p.x - centerX, p.y - centerY));
|
|
792
|
+
const actualReps = reps * 2;
|
|
793
|
+
const sectionAngle = 360 / actualReps;
|
|
794
|
+
const result = [];
|
|
795
|
+
for (let it = 0; it < actualReps; it++) {
|
|
796
|
+
const mirrored = it % 2 !== 0;
|
|
797
|
+
for (let index = 0; index < points.length; index++) {
|
|
798
|
+
const i = mirrored ? points.length - 1 - index : index;
|
|
799
|
+
if (i > 0 || !mirrored) {
|
|
800
|
+
const angleOffset = mirrored ? sectionAngle - angles[i] + 2 * angles[0] : angles[i];
|
|
801
|
+
const a = toRadians(sectionAngle * it + angleOffset);
|
|
802
|
+
const finalX = Math.cos(a) * distances[i] + centerX;
|
|
803
|
+
const finalY = Math.sin(a) * distances[i] + centerY;
|
|
804
|
+
result.push(pnr(finalX, finalY, points[i].r));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return result;
|
|
809
|
+
}
|
|
810
|
+
function doRepeatRotated(points, reps, centerX, centerY) {
|
|
811
|
+
const np = points.length;
|
|
812
|
+
const result = [];
|
|
813
|
+
for (let it = 0; it < np * reps; it++) {
|
|
814
|
+
const srcPoint = points[it % np];
|
|
815
|
+
const [rx, ry] = rotateDegrees(
|
|
816
|
+
srcPoint.x,
|
|
817
|
+
srcPoint.y,
|
|
818
|
+
Math.floor(it / np) * (360 / reps),
|
|
819
|
+
centerX,
|
|
820
|
+
centerY
|
|
821
|
+
);
|
|
822
|
+
result.push(pnr(rx, ry, srcPoint.r));
|
|
823
|
+
}
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
function doRepeat(points, reps, centerX, centerY, mirroring) {
|
|
827
|
+
if (mirroring) {
|
|
828
|
+
return doRepeatMirrored(points, reps, centerX, centerY);
|
|
829
|
+
}
|
|
830
|
+
return doRepeatRotated(points, reps, centerX, centerY);
|
|
831
|
+
}
|
|
832
|
+
function customPolygon(points, reps, centerX = 0.5, centerY = 0.5, mirroring = false) {
|
|
833
|
+
const actualPoints = doRepeat(points, reps, centerX, centerY, mirroring);
|
|
834
|
+
const vertices = [];
|
|
835
|
+
const pvRounding = [];
|
|
836
|
+
for (const p of actualPoints) {
|
|
837
|
+
vertices.push(p.x, p.y);
|
|
838
|
+
pvRounding.push(p.r);
|
|
839
|
+
}
|
|
840
|
+
return createPolygonFromVertices(
|
|
841
|
+
vertices,
|
|
842
|
+
unrounded,
|
|
843
|
+
pvRounding,
|
|
844
|
+
centerX,
|
|
845
|
+
centerY
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
function rotatePolygon(polygon, degrees) {
|
|
849
|
+
const rad = toRadians(degrees);
|
|
850
|
+
const cos = Math.cos(rad);
|
|
851
|
+
const sin = Math.sin(rad);
|
|
852
|
+
return polygon.transformed((x, y) => ({
|
|
853
|
+
x: x * cos - y * sin,
|
|
854
|
+
y: x * sin + y * cos
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
857
|
+
var r15 = cornerRounding(0.15);
|
|
858
|
+
var r20 = cornerRounding(0.2);
|
|
859
|
+
var r30 = cornerRounding(0.3);
|
|
860
|
+
var r50 = cornerRounding(0.5);
|
|
861
|
+
var r100 = cornerRounding(1);
|
|
862
|
+
function circle() {
|
|
863
|
+
return createCircle(10);
|
|
864
|
+
}
|
|
865
|
+
function square2() {
|
|
866
|
+
return createRectangle(1, 1, r30);
|
|
867
|
+
}
|
|
868
|
+
function slanted() {
|
|
869
|
+
return customPolygon(
|
|
870
|
+
[
|
|
871
|
+
pnr(0.926, 0.97, cornerRounding(0.189, 0.811)),
|
|
872
|
+
pnr(-0.021, 0.967, cornerRounding(0.187, 0.057))
|
|
873
|
+
],
|
|
874
|
+
2
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
function arch() {
|
|
878
|
+
return rotatePolygon(
|
|
879
|
+
createPolygonFromVertices(
|
|
880
|
+
// 4 vertices for a square, manually positioning
|
|
881
|
+
(() => {
|
|
882
|
+
const r = 1;
|
|
883
|
+
const verts = [];
|
|
884
|
+
for (let i = 0; i < 4; i++) {
|
|
885
|
+
const angle = floatPi / 4 * 2 * i;
|
|
886
|
+
verts.push(Math.cos(angle) * r, Math.sin(angle) * r);
|
|
887
|
+
}
|
|
888
|
+
return verts;
|
|
889
|
+
})(),
|
|
890
|
+
unrounded,
|
|
891
|
+
[r100, r100, r20, r20],
|
|
892
|
+
0,
|
|
893
|
+
0
|
|
894
|
+
),
|
|
895
|
+
-135
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
function fan() {
|
|
899
|
+
return customPolygon(
|
|
900
|
+
[
|
|
901
|
+
pnr(1.004, 1, cornerRounding(0.148, 0.417)),
|
|
902
|
+
pnr(0, 1, cornerRounding(0.151)),
|
|
903
|
+
pnr(0, -3e-3, cornerRounding(0.148)),
|
|
904
|
+
pnr(0.978, 0.02, cornerRounding(0.803))
|
|
905
|
+
],
|
|
906
|
+
1
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
function arrow() {
|
|
910
|
+
return customPolygon(
|
|
911
|
+
[
|
|
912
|
+
pnr(0.5, 0.892, cornerRounding(0.313)),
|
|
913
|
+
pnr(-0.216, 1.05, cornerRounding(0.207)),
|
|
914
|
+
pnr(0.499, -0.16, cornerRounding(0.215, 1)),
|
|
915
|
+
pnr(1.225, 1.06, cornerRounding(0.211))
|
|
916
|
+
],
|
|
917
|
+
1
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
function semiCircle() {
|
|
921
|
+
return createRectangle(1.6, 1, unrounded, [r20, r20, r100, r100]);
|
|
922
|
+
}
|
|
923
|
+
function oval() {
|
|
924
|
+
const scaled = createCircle().transformed((x, y) => ({ x, y: y * 0.64 }));
|
|
925
|
+
return rotatePolygon(scaled, -45);
|
|
926
|
+
}
|
|
927
|
+
function pill() {
|
|
928
|
+
return customPolygon(
|
|
929
|
+
[
|
|
930
|
+
pnr(0.961, 0.039, cornerRounding(0.426)),
|
|
931
|
+
pnr(1.001, 0.428),
|
|
932
|
+
pnr(1, 0.609, cornerRounding(1))
|
|
933
|
+
],
|
|
934
|
+
2,
|
|
935
|
+
0.5,
|
|
936
|
+
0.5,
|
|
937
|
+
true
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
function triangle() {
|
|
941
|
+
return rotatePolygon(
|
|
942
|
+
createPolygonFromVertices(
|
|
943
|
+
(() => {
|
|
944
|
+
const verts = [];
|
|
945
|
+
for (let i = 0; i < 3; i++) {
|
|
946
|
+
verts.push(
|
|
947
|
+
Math.cos(floatPi / 3 * 2 * i),
|
|
948
|
+
Math.sin(floatPi / 3 * 2 * i)
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
return verts;
|
|
952
|
+
})(),
|
|
953
|
+
r20,
|
|
954
|
+
null,
|
|
955
|
+
0,
|
|
956
|
+
0
|
|
957
|
+
),
|
|
958
|
+
-90
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
function diamond() {
|
|
962
|
+
return customPolygon(
|
|
963
|
+
[
|
|
964
|
+
pnr(0.5, 1.096, cornerRounding(0.151, 0.524)),
|
|
965
|
+
pnr(0.04, 0.5, cornerRounding(0.159))
|
|
966
|
+
],
|
|
967
|
+
2
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
function clamShell() {
|
|
971
|
+
return customPolygon(
|
|
972
|
+
[
|
|
973
|
+
pnr(0.171, 0.841, cornerRounding(0.159)),
|
|
974
|
+
pnr(-0.02, 0.5, cornerRounding(0.14)),
|
|
975
|
+
pnr(0.17, 0.159, cornerRounding(0.159))
|
|
976
|
+
],
|
|
977
|
+
2
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
function pentagon() {
|
|
981
|
+
return customPolygon(
|
|
982
|
+
[
|
|
983
|
+
pnr(0.5, -9e-3, cornerRounding(0.172)),
|
|
984
|
+
pnr(1.03, 0.365, cornerRounding(0.164)),
|
|
985
|
+
pnr(0.828, 0.97, cornerRounding(0.169))
|
|
986
|
+
],
|
|
987
|
+
1,
|
|
988
|
+
0.5,
|
|
989
|
+
0.5,
|
|
990
|
+
true
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
function gem() {
|
|
994
|
+
return customPolygon(
|
|
995
|
+
[
|
|
996
|
+
pnr(0.499, 1.023, cornerRounding(0.241, 0.778)),
|
|
997
|
+
pnr(-5e-3, 0.792, cornerRounding(0.208)),
|
|
998
|
+
pnr(0.073, 0.258, cornerRounding(0.228)),
|
|
999
|
+
pnr(0.433, 0, cornerRounding(0.491))
|
|
1000
|
+
],
|
|
1001
|
+
1,
|
|
1002
|
+
0.5,
|
|
1003
|
+
0.5,
|
|
1004
|
+
true
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
function sunny() {
|
|
1008
|
+
return createStar(8, 1, 0.8, r15);
|
|
1009
|
+
}
|
|
1010
|
+
function verySunny() {
|
|
1011
|
+
return customPolygon(
|
|
1012
|
+
[
|
|
1013
|
+
pnr(0.5, 1.08, cornerRounding(0.085)),
|
|
1014
|
+
pnr(0.358, 0.843, cornerRounding(0.085))
|
|
1015
|
+
],
|
|
1016
|
+
8
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
function cookie4() {
|
|
1020
|
+
return customPolygon(
|
|
1021
|
+
[
|
|
1022
|
+
pnr(1.237, 1.236, cornerRounding(0.258)),
|
|
1023
|
+
pnr(0.5, 0.918, cornerRounding(0.233))
|
|
1024
|
+
],
|
|
1025
|
+
4
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
function cookie6() {
|
|
1029
|
+
return customPolygon(
|
|
1030
|
+
[
|
|
1031
|
+
pnr(0.723, 0.884, cornerRounding(0.394)),
|
|
1032
|
+
pnr(0.5, 1.099, cornerRounding(0.398))
|
|
1033
|
+
],
|
|
1034
|
+
6
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
function cookie7() {
|
|
1038
|
+
return rotatePolygon(createStar(7, 1, 0.75, r50), -90);
|
|
1039
|
+
}
|
|
1040
|
+
function cookie9() {
|
|
1041
|
+
return rotatePolygon(createStar(9, 1, 0.8, r50), -90);
|
|
1042
|
+
}
|
|
1043
|
+
function cookie12() {
|
|
1044
|
+
return rotatePolygon(createStar(12, 1, 0.8, r50), -90);
|
|
1045
|
+
}
|
|
1046
|
+
function ghostish() {
|
|
1047
|
+
return customPolygon(
|
|
1048
|
+
[
|
|
1049
|
+
pnr(0.5, 0, cornerRounding(1)),
|
|
1050
|
+
pnr(1, 0, cornerRounding(1)),
|
|
1051
|
+
pnr(1, 1.14, cornerRounding(0.254, 0.106)),
|
|
1052
|
+
pnr(0.575, 0.906, cornerRounding(0.253))
|
|
1053
|
+
],
|
|
1054
|
+
1,
|
|
1055
|
+
0.5,
|
|
1056
|
+
0.5,
|
|
1057
|
+
true
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
function clover4() {
|
|
1061
|
+
return customPolygon(
|
|
1062
|
+
[pnr(0.5, 0.074), pnr(0.725, -0.099, cornerRounding(0.476))],
|
|
1063
|
+
4,
|
|
1064
|
+
0.5,
|
|
1065
|
+
0.5,
|
|
1066
|
+
true
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
function clover8() {
|
|
1070
|
+
return customPolygon(
|
|
1071
|
+
[pnr(0.5, 0.036), pnr(0.758, -0.101, cornerRounding(0.209))],
|
|
1072
|
+
8
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
function burst() {
|
|
1076
|
+
return customPolygon(
|
|
1077
|
+
[
|
|
1078
|
+
pnr(0.5, -6e-3, cornerRounding(6e-3)),
|
|
1079
|
+
pnr(0.592, 0.158, cornerRounding(6e-3))
|
|
1080
|
+
],
|
|
1081
|
+
12
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
function softBurst() {
|
|
1085
|
+
return customPolygon(
|
|
1086
|
+
[
|
|
1087
|
+
pnr(0.193, 0.277, cornerRounding(0.053)),
|
|
1088
|
+
pnr(0.176, 0.055, cornerRounding(0.053))
|
|
1089
|
+
],
|
|
1090
|
+
10
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
function boom() {
|
|
1094
|
+
return customPolygon(
|
|
1095
|
+
[
|
|
1096
|
+
pnr(0.457, 0.296, cornerRounding(7e-3)),
|
|
1097
|
+
pnr(0.5, -0.051, cornerRounding(7e-3))
|
|
1098
|
+
],
|
|
1099
|
+
15
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
function softBoom() {
|
|
1103
|
+
return customPolygon(
|
|
1104
|
+
[
|
|
1105
|
+
pnr(0.733, 0.454),
|
|
1106
|
+
pnr(0.839, 0.437, cornerRounding(0.532)),
|
|
1107
|
+
pnr(0.949, 0.449, cornerRounding(0.439, 1)),
|
|
1108
|
+
pnr(0.998, 0.478, cornerRounding(0.174))
|
|
1109
|
+
],
|
|
1110
|
+
16,
|
|
1111
|
+
0.5,
|
|
1112
|
+
0.5,
|
|
1113
|
+
true
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
function flower() {
|
|
1117
|
+
return customPolygon(
|
|
1118
|
+
[
|
|
1119
|
+
pnr(0.37, 0.187),
|
|
1120
|
+
pnr(0.416, 0.049, cornerRounding(0.381)),
|
|
1121
|
+
pnr(0.479, 1e-3, cornerRounding(0.095))
|
|
1122
|
+
],
|
|
1123
|
+
8,
|
|
1124
|
+
0.5,
|
|
1125
|
+
0.5,
|
|
1126
|
+
true
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
function puffy() {
|
|
1130
|
+
const base = customPolygon(
|
|
1131
|
+
[
|
|
1132
|
+
pnr(0.5, 0.053),
|
|
1133
|
+
pnr(0.545, -0.04, cornerRounding(0.405)),
|
|
1134
|
+
pnr(0.67, -0.035, cornerRounding(0.426)),
|
|
1135
|
+
pnr(0.717, 0.066, cornerRounding(0.574)),
|
|
1136
|
+
pnr(0.722, 0.128),
|
|
1137
|
+
pnr(0.777, 2e-3, cornerRounding(0.36)),
|
|
1138
|
+
pnr(0.914, 0.149, cornerRounding(0.66)),
|
|
1139
|
+
pnr(0.926, 0.289, cornerRounding(0.66)),
|
|
1140
|
+
pnr(0.881, 0.346),
|
|
1141
|
+
pnr(0.94, 0.344, cornerRounding(0.126)),
|
|
1142
|
+
pnr(1.003, 0.437, cornerRounding(0.255))
|
|
1143
|
+
],
|
|
1144
|
+
2,
|
|
1145
|
+
0.5,
|
|
1146
|
+
0.5,
|
|
1147
|
+
true
|
|
1148
|
+
);
|
|
1149
|
+
return base.transformed((x, y) => ({ x, y: y * 0.742 }));
|
|
1150
|
+
}
|
|
1151
|
+
function puffyDiamond() {
|
|
1152
|
+
return customPolygon(
|
|
1153
|
+
[
|
|
1154
|
+
pnr(0.87, 0.13, cornerRounding(0.146)),
|
|
1155
|
+
pnr(0.818, 0.357),
|
|
1156
|
+
pnr(1, 0.332, cornerRounding(0.853))
|
|
1157
|
+
],
|
|
1158
|
+
4,
|
|
1159
|
+
0.5,
|
|
1160
|
+
0.5,
|
|
1161
|
+
true
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
function pixelCircle() {
|
|
1165
|
+
return customPolygon(
|
|
1166
|
+
[
|
|
1167
|
+
pnr(0.5, 0),
|
|
1168
|
+
pnr(0.704, 0),
|
|
1169
|
+
pnr(0.704, 0.065),
|
|
1170
|
+
pnr(0.843, 0.065),
|
|
1171
|
+
pnr(0.843, 0.148),
|
|
1172
|
+
pnr(0.926, 0.148),
|
|
1173
|
+
pnr(0.926, 0.296),
|
|
1174
|
+
pnr(1, 0.296)
|
|
1175
|
+
],
|
|
1176
|
+
2,
|
|
1177
|
+
0.5,
|
|
1178
|
+
0.5,
|
|
1179
|
+
true
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
function pixelTriangle() {
|
|
1183
|
+
return customPolygon(
|
|
1184
|
+
[
|
|
1185
|
+
pnr(0.11, 0.5),
|
|
1186
|
+
pnr(0.113, 0),
|
|
1187
|
+
pnr(0.287, 0),
|
|
1188
|
+
pnr(0.287, 0.087),
|
|
1189
|
+
pnr(0.421, 0.087),
|
|
1190
|
+
pnr(0.421, 0.17),
|
|
1191
|
+
pnr(0.56, 0.17),
|
|
1192
|
+
pnr(0.56, 0.265),
|
|
1193
|
+
pnr(0.674, 0.265),
|
|
1194
|
+
pnr(0.675, 0.344),
|
|
1195
|
+
pnr(0.789, 0.344),
|
|
1196
|
+
pnr(0.789, 0.439),
|
|
1197
|
+
pnr(0.888, 0.439)
|
|
1198
|
+
],
|
|
1199
|
+
1,
|
|
1200
|
+
0.5,
|
|
1201
|
+
0.5,
|
|
1202
|
+
true
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
function bun() {
|
|
1206
|
+
return customPolygon(
|
|
1207
|
+
[
|
|
1208
|
+
pnr(0.796, 0.5),
|
|
1209
|
+
pnr(0.853, 0.518, cornerRounding(1)),
|
|
1210
|
+
pnr(0.992, 0.631, cornerRounding(1)),
|
|
1211
|
+
pnr(0.968, 1, cornerRounding(1))
|
|
1212
|
+
],
|
|
1213
|
+
2,
|
|
1214
|
+
0.5,
|
|
1215
|
+
0.5,
|
|
1216
|
+
true
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
function heart() {
|
|
1220
|
+
return customPolygon(
|
|
1221
|
+
[
|
|
1222
|
+
pnr(0.5, 0.268, cornerRounding(0.016)),
|
|
1223
|
+
pnr(0.792, -0.066, cornerRounding(0.958)),
|
|
1224
|
+
pnr(1.064, 0.276, cornerRounding(1)),
|
|
1225
|
+
pnr(0.501, 0.946, cornerRounding(0.129))
|
|
1226
|
+
],
|
|
1227
|
+
1,
|
|
1228
|
+
0.5,
|
|
1229
|
+
0.5,
|
|
1230
|
+
true
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
var shapeFactories = {
|
|
1234
|
+
Circle: circle,
|
|
1235
|
+
Square: square2,
|
|
1236
|
+
Slanted: slanted,
|
|
1237
|
+
Arch: arch,
|
|
1238
|
+
Fan: fan,
|
|
1239
|
+
Arrow: arrow,
|
|
1240
|
+
SemiCircle: semiCircle,
|
|
1241
|
+
Oval: oval,
|
|
1242
|
+
Pill: pill,
|
|
1243
|
+
Triangle: triangle,
|
|
1244
|
+
Diamond: diamond,
|
|
1245
|
+
ClamShell: clamShell,
|
|
1246
|
+
Pentagon: pentagon,
|
|
1247
|
+
Gem: gem,
|
|
1248
|
+
Sunny: sunny,
|
|
1249
|
+
VerySunny: verySunny,
|
|
1250
|
+
Cookie4Sided: cookie4,
|
|
1251
|
+
Cookie6Sided: cookie6,
|
|
1252
|
+
Cookie7Sided: cookie7,
|
|
1253
|
+
Cookie9Sided: cookie9,
|
|
1254
|
+
Cookie12Sided: cookie12,
|
|
1255
|
+
Ghostish: ghostish,
|
|
1256
|
+
Clover4Leaf: clover4,
|
|
1257
|
+
Clover8Leaf: clover8,
|
|
1258
|
+
Burst: burst,
|
|
1259
|
+
SoftBurst: softBurst,
|
|
1260
|
+
Boom: boom,
|
|
1261
|
+
SoftBoom: softBoom,
|
|
1262
|
+
Flower: flower,
|
|
1263
|
+
Puffy: puffy,
|
|
1264
|
+
PuffyDiamond: puffyDiamond,
|
|
1265
|
+
PixelCircle: pixelCircle,
|
|
1266
|
+
PixelTriangle: pixelTriangle,
|
|
1267
|
+
Bun: bun,
|
|
1268
|
+
Heart: heart
|
|
1269
|
+
};
|
|
1270
|
+
var shapeNames = Object.keys(
|
|
1271
|
+
shapeFactories
|
|
1272
|
+
);
|
|
1273
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1274
|
+
function getShape(name) {
|
|
1275
|
+
let shape = cache.get(name);
|
|
1276
|
+
if (!shape) {
|
|
1277
|
+
const factory = shapeFactories[name];
|
|
1278
|
+
shape = factory().normalized();
|
|
1279
|
+
cache.set(name, shape);
|
|
1280
|
+
}
|
|
1281
|
+
return shape;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/core/morph.ts
|
|
1285
|
+
var LengthMeasurer = class {
|
|
1286
|
+
segments = 3;
|
|
1287
|
+
measureCubic(c) {
|
|
1288
|
+
return this.closestProgressTo(c, Number.POSITIVE_INFINITY)[1];
|
|
1289
|
+
}
|
|
1290
|
+
findCubicCutPoint(c, m) {
|
|
1291
|
+
return this.closestProgressTo(c, m)[0];
|
|
1292
|
+
}
|
|
1293
|
+
closestProgressTo(cubic, threshold) {
|
|
1294
|
+
let total = 0;
|
|
1295
|
+
let remainder = threshold;
|
|
1296
|
+
let prev = pt(cubic.anchor0X, cubic.anchor0Y);
|
|
1297
|
+
for (let i = 1; i <= this.segments; i++) {
|
|
1298
|
+
const progress = i / this.segments;
|
|
1299
|
+
const point = cubic.pointOnCurve(progress);
|
|
1300
|
+
const segment = ptDistance(ptSub(point, prev));
|
|
1301
|
+
if (segment >= remainder) {
|
|
1302
|
+
return [
|
|
1303
|
+
progress - (1 - remainder / segment) / this.segments,
|
|
1304
|
+
threshold
|
|
1305
|
+
];
|
|
1306
|
+
}
|
|
1307
|
+
remainder -= segment;
|
|
1308
|
+
total += segment;
|
|
1309
|
+
prev = point;
|
|
1310
|
+
}
|
|
1311
|
+
return [1, total];
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
function cutMeasuredCubicAtProgress(mc, cutOutlineProgress, measurer) {
|
|
1315
|
+
const bounded = Math.max(
|
|
1316
|
+
mc.startOutlineProgress,
|
|
1317
|
+
Math.min(mc.endOutlineProgress, cutOutlineProgress)
|
|
1318
|
+
);
|
|
1319
|
+
const outlineProgressSize = mc.endOutlineProgress - mc.startOutlineProgress;
|
|
1320
|
+
const progressFromStart = bounded - mc.startOutlineProgress;
|
|
1321
|
+
const relativeProgress = progressFromStart / outlineProgressSize;
|
|
1322
|
+
const t = measurer.findCubicCutPoint(
|
|
1323
|
+
mc.cubic,
|
|
1324
|
+
relativeProgress * mc.measuredSize
|
|
1325
|
+
);
|
|
1326
|
+
const [c1, c2] = mc.cubic.split(t);
|
|
1327
|
+
return [
|
|
1328
|
+
{
|
|
1329
|
+
cubic: c1,
|
|
1330
|
+
startOutlineProgress: mc.startOutlineProgress,
|
|
1331
|
+
endOutlineProgress: bounded,
|
|
1332
|
+
measuredSize: measurer.measureCubic(c1)
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
cubic: c2,
|
|
1336
|
+
startOutlineProgress: bounded,
|
|
1337
|
+
endOutlineProgress: mc.endOutlineProgress,
|
|
1338
|
+
measuredSize: measurer.measureCubic(c2)
|
|
1339
|
+
}
|
|
1340
|
+
];
|
|
1341
|
+
}
|
|
1342
|
+
function measurePolygon(measurer, polygon) {
|
|
1343
|
+
const cubics = [];
|
|
1344
|
+
const featureToCubic = [];
|
|
1345
|
+
for (const feature of polygon.features) {
|
|
1346
|
+
for (let cubicIndex = 0; cubicIndex < feature.cubics.length; cubicIndex++) {
|
|
1347
|
+
if (feature.type === "corner" && cubicIndex === Math.floor(feature.cubics.length / 2)) {
|
|
1348
|
+
featureToCubic.push([feature, cubics.length]);
|
|
1349
|
+
}
|
|
1350
|
+
cubics.push(feature.cubics[cubicIndex]);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
const measures = [0];
|
|
1354
|
+
let cumulative = 0;
|
|
1355
|
+
for (const cubic of cubics) {
|
|
1356
|
+
const m = measurer.measureCubic(cubic);
|
|
1357
|
+
cumulative += m;
|
|
1358
|
+
measures.push(cumulative);
|
|
1359
|
+
}
|
|
1360
|
+
const totalMeasure = cumulative;
|
|
1361
|
+
const outlineProgress = measures.map((m) => m / totalMeasure);
|
|
1362
|
+
const features = featureToCubic.map(
|
|
1363
|
+
([feature, ix]) => ({
|
|
1364
|
+
progress: positiveModulo(
|
|
1365
|
+
(outlineProgress[ix] + outlineProgress[ix + 1]) / 2,
|
|
1366
|
+
1
|
|
1367
|
+
),
|
|
1368
|
+
feature
|
|
1369
|
+
})
|
|
1370
|
+
);
|
|
1371
|
+
const measuredCubics = [];
|
|
1372
|
+
let startProgress = 0;
|
|
1373
|
+
for (let i = 0; i < cubics.length; i++) {
|
|
1374
|
+
if (outlineProgress[i + 1] - outlineProgress[i] > distanceEpsilon) {
|
|
1375
|
+
measuredCubics.push({
|
|
1376
|
+
cubic: cubics[i],
|
|
1377
|
+
startOutlineProgress: startProgress,
|
|
1378
|
+
endOutlineProgress: outlineProgress[i + 1],
|
|
1379
|
+
measuredSize: measurer.measureCubic(cubics[i])
|
|
1380
|
+
});
|
|
1381
|
+
startProgress = outlineProgress[i + 1];
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const lastMeasured = measuredCubics.at(-1);
|
|
1385
|
+
if (lastMeasured) {
|
|
1386
|
+
lastMeasured.endOutlineProgress = 1;
|
|
1387
|
+
}
|
|
1388
|
+
return { cubics: measuredCubics, features, measurer };
|
|
1389
|
+
}
|
|
1390
|
+
function cutAndShift(mp, cuttingPoint) {
|
|
1391
|
+
if (cuttingPoint < distanceEpsilon) {
|
|
1392
|
+
return mp;
|
|
1393
|
+
}
|
|
1394
|
+
const cubics = mp.cubics;
|
|
1395
|
+
let targetIndex = cubics.findIndex(
|
|
1396
|
+
(c) => cuttingPoint >= c.startOutlineProgress && cuttingPoint <= c.endOutlineProgress
|
|
1397
|
+
);
|
|
1398
|
+
if (targetIndex === -1) {
|
|
1399
|
+
targetIndex = cubics.length - 1;
|
|
1400
|
+
}
|
|
1401
|
+
const [b1, b2] = cutMeasuredCubicAtProgress(
|
|
1402
|
+
cubics[targetIndex],
|
|
1403
|
+
cuttingPoint,
|
|
1404
|
+
mp.measurer
|
|
1405
|
+
);
|
|
1406
|
+
const retCubics = [b2.cubic];
|
|
1407
|
+
for (let i = 1; i < cubics.length; i++) {
|
|
1408
|
+
retCubics.push(cubics[(i + targetIndex) % cubics.length].cubic);
|
|
1409
|
+
}
|
|
1410
|
+
retCubics.push(b1.cubic);
|
|
1411
|
+
const retOutlineProgress = [];
|
|
1412
|
+
for (let index = 0; index < cubics.length + 2; index++) {
|
|
1413
|
+
if (index === 0) {
|
|
1414
|
+
retOutlineProgress.push(0);
|
|
1415
|
+
} else if (index === cubics.length + 1) {
|
|
1416
|
+
retOutlineProgress.push(1);
|
|
1417
|
+
} else {
|
|
1418
|
+
const cubicIndex = (targetIndex + index - 1) % cubics.length;
|
|
1419
|
+
retOutlineProgress.push(
|
|
1420
|
+
positiveModulo(cubics[cubicIndex].endOutlineProgress - cuttingPoint, 1)
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
const newFeatures = mp.features.map((f) => ({
|
|
1425
|
+
progress: positiveModulo(f.progress - cuttingPoint, 1),
|
|
1426
|
+
feature: f.feature
|
|
1427
|
+
}));
|
|
1428
|
+
const measuredCubics = [];
|
|
1429
|
+
let startProgress = 0;
|
|
1430
|
+
for (let i = 0; i < retCubics.length; i++) {
|
|
1431
|
+
if (retOutlineProgress[i + 1] - retOutlineProgress[i] > distanceEpsilon) {
|
|
1432
|
+
measuredCubics.push({
|
|
1433
|
+
cubic: retCubics[i],
|
|
1434
|
+
startOutlineProgress: startProgress,
|
|
1435
|
+
endOutlineProgress: retOutlineProgress[i + 1],
|
|
1436
|
+
measuredSize: mp.measurer.measureCubic(retCubics[i])
|
|
1437
|
+
});
|
|
1438
|
+
startProgress = retOutlineProgress[i + 1];
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
const lastCut = measuredCubics.at(-1);
|
|
1442
|
+
if (lastCut) {
|
|
1443
|
+
lastCut.endOutlineProgress = 1;
|
|
1444
|
+
}
|
|
1445
|
+
return {
|
|
1446
|
+
cubics: measuredCubics,
|
|
1447
|
+
features: newFeatures,
|
|
1448
|
+
measurer: mp.measurer
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
function progressInRange(progress, from, to) {
|
|
1452
|
+
if (to >= from) {
|
|
1453
|
+
return progress >= from && progress <= to;
|
|
1454
|
+
}
|
|
1455
|
+
return progress >= from || progress <= to;
|
|
1456
|
+
}
|
|
1457
|
+
function progressDistance(p1, p2) {
|
|
1458
|
+
const d = Math.abs(p1 - p2);
|
|
1459
|
+
return Math.min(d, 1 - d);
|
|
1460
|
+
}
|
|
1461
|
+
function linearMap(xValues, yValues, x) {
|
|
1462
|
+
let segStartIndex = 0;
|
|
1463
|
+
for (let i = 0; i < xValues.length; i++) {
|
|
1464
|
+
if (progressInRange(x, xValues[i], xValues[(i + 1) % xValues.length])) {
|
|
1465
|
+
segStartIndex = i;
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
const segEndIndex = (segStartIndex + 1) % xValues.length;
|
|
1470
|
+
const segSizeX = positiveModulo(
|
|
1471
|
+
xValues[segEndIndex] - xValues[segStartIndex],
|
|
1472
|
+
1
|
|
1473
|
+
);
|
|
1474
|
+
const segSizeY = positiveModulo(
|
|
1475
|
+
yValues[segEndIndex] - yValues[segStartIndex],
|
|
1476
|
+
1
|
|
1477
|
+
);
|
|
1478
|
+
const posInSeg = segSizeX < 1e-3 ? 0.5 : positiveModulo(x - xValues[segStartIndex], 1) / segSizeX;
|
|
1479
|
+
return positiveModulo(yValues[segStartIndex] + segSizeY * posInSeg, 1);
|
|
1480
|
+
}
|
|
1481
|
+
var DoubleMapper = class _DoubleMapper {
|
|
1482
|
+
sourceValues;
|
|
1483
|
+
targetValues;
|
|
1484
|
+
constructor(mappings) {
|
|
1485
|
+
this.sourceValues = mappings.map((m) => m[0]);
|
|
1486
|
+
this.targetValues = mappings.map((m) => m[1]);
|
|
1487
|
+
}
|
|
1488
|
+
map(x) {
|
|
1489
|
+
return linearMap(this.sourceValues, this.targetValues, x);
|
|
1490
|
+
}
|
|
1491
|
+
mapBack(x) {
|
|
1492
|
+
return linearMap(this.targetValues, this.sourceValues, x);
|
|
1493
|
+
}
|
|
1494
|
+
static Identity = new _DoubleMapper([
|
|
1495
|
+
[0, 0],
|
|
1496
|
+
[0.5, 0.5]
|
|
1497
|
+
]);
|
|
1498
|
+
};
|
|
1499
|
+
function featureRepresentativePoint(f) {
|
|
1500
|
+
const first = f.cubics[0];
|
|
1501
|
+
const last = f.cubics.at(-1) ?? first;
|
|
1502
|
+
return pt(
|
|
1503
|
+
(first.anchor0X + last.anchor1X) / 2,
|
|
1504
|
+
(first.anchor0Y + last.anchor1Y) / 2
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
function featureDistSquared(f1, f2) {
|
|
1508
|
+
if (f1.type === "corner" && f2.type === "corner" && f1.convex !== f2.convex) {
|
|
1509
|
+
return Number.MAX_VALUE;
|
|
1510
|
+
}
|
|
1511
|
+
const p1 = featureRepresentativePoint(f1);
|
|
1512
|
+
const p2 = featureRepresentativePoint(f2);
|
|
1513
|
+
const d = ptSub(p1, p2);
|
|
1514
|
+
return d.x * d.x + d.y * d.y;
|
|
1515
|
+
}
|
|
1516
|
+
function buildCandidateList(features1, features2) {
|
|
1517
|
+
const result = [];
|
|
1518
|
+
for (const f1 of features1) {
|
|
1519
|
+
for (const f2 of features2) {
|
|
1520
|
+
const d = featureDistSquared(f1.feature, f2.feature);
|
|
1521
|
+
if (d !== Number.MAX_VALUE) {
|
|
1522
|
+
result.push({ distance: d, f1, f2 });
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
result.sort((a, b) => a.distance - b.distance);
|
|
1527
|
+
return result;
|
|
1528
|
+
}
|
|
1529
|
+
function canInsertMapping(mapping, insertionIndex, p1, p2) {
|
|
1530
|
+
const n = mapping.length;
|
|
1531
|
+
if (n === 0) {
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1534
|
+
const [before1, before2] = mapping[(insertionIndex + n - 1) % n];
|
|
1535
|
+
const [after1, after2] = mapping[insertionIndex % n];
|
|
1536
|
+
if (progressDistance(p1, before1) < distanceEpsilon || progressDistance(p1, after1) < distanceEpsilon || progressDistance(p2, before2) < distanceEpsilon || progressDistance(p2, after2) < distanceEpsilon) {
|
|
1537
|
+
return false;
|
|
1538
|
+
}
|
|
1539
|
+
return n <= 1 || progressInRange(p2, before2, after2);
|
|
1540
|
+
}
|
|
1541
|
+
function doMapping(features1, features2) {
|
|
1542
|
+
const candidates = buildCandidateList(features1, features2);
|
|
1543
|
+
if (candidates.length === 0) {
|
|
1544
|
+
return [
|
|
1545
|
+
[0, 0],
|
|
1546
|
+
[0.5, 0.5]
|
|
1547
|
+
];
|
|
1548
|
+
}
|
|
1549
|
+
if (candidates.length === 1) {
|
|
1550
|
+
const dv = candidates[0];
|
|
1551
|
+
return [
|
|
1552
|
+
[dv.f1.progress, dv.f2.progress],
|
|
1553
|
+
[(dv.f1.progress + 0.5) % 1, (dv.f2.progress + 0.5) % 1]
|
|
1554
|
+
];
|
|
1555
|
+
}
|
|
1556
|
+
const mapping = [];
|
|
1557
|
+
const usedF1 = /* @__PURE__ */ new Set();
|
|
1558
|
+
const usedF2 = /* @__PURE__ */ new Set();
|
|
1559
|
+
for (const dv of candidates) {
|
|
1560
|
+
if (usedF1.has(dv.f1) || usedF2.has(dv.f2)) {
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
let insertionIndex = 0;
|
|
1564
|
+
for (let i = 0; i < mapping.length; i++) {
|
|
1565
|
+
if (mapping[i][0] < dv.f1.progress) {
|
|
1566
|
+
insertionIndex = i + 1;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (!canInsertMapping(mapping, insertionIndex, dv.f1.progress, dv.f2.progress)) {
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
mapping.splice(insertionIndex, 0, [dv.f1.progress, dv.f2.progress]);
|
|
1573
|
+
usedF1.add(dv.f1);
|
|
1574
|
+
usedF2.add(dv.f2);
|
|
1575
|
+
}
|
|
1576
|
+
return mapping;
|
|
1577
|
+
}
|
|
1578
|
+
function featureMapper(features1, features2) {
|
|
1579
|
+
const filtered1 = features1.filter((f) => f.feature.type === "corner");
|
|
1580
|
+
const filtered2 = features2.filter((f) => f.feature.type === "corner");
|
|
1581
|
+
const mapping = doMapping(filtered1, filtered2);
|
|
1582
|
+
return new DoubleMapper(mapping);
|
|
1583
|
+
}
|
|
1584
|
+
var Morph = class {
|
|
1585
|
+
morphMatch;
|
|
1586
|
+
constructor(start, end) {
|
|
1587
|
+
this.morphMatch = matchPolygons(start, end);
|
|
1588
|
+
}
|
|
1589
|
+
asCubics(progress) {
|
|
1590
|
+
const result = [];
|
|
1591
|
+
let firstCubic = null;
|
|
1592
|
+
let lastCubic = null;
|
|
1593
|
+
for (const [a, b] of this.morphMatch) {
|
|
1594
|
+
const points = new Float64Array(8);
|
|
1595
|
+
for (let j = 0; j < 8; j++) {
|
|
1596
|
+
points[j] = interpolate(a.points[j], b.points[j], progress);
|
|
1597
|
+
}
|
|
1598
|
+
const cubic = new Cubic(points);
|
|
1599
|
+
if (!firstCubic) {
|
|
1600
|
+
firstCubic = cubic;
|
|
1601
|
+
}
|
|
1602
|
+
if (lastCubic) {
|
|
1603
|
+
result.push(lastCubic);
|
|
1604
|
+
}
|
|
1605
|
+
lastCubic = cubic;
|
|
1606
|
+
}
|
|
1607
|
+
if (lastCubic && firstCubic) {
|
|
1608
|
+
result.push(
|
|
1609
|
+
new Cubic(
|
|
1610
|
+
lastCubic.anchor0X,
|
|
1611
|
+
lastCubic.anchor0Y,
|
|
1612
|
+
lastCubic.control0X,
|
|
1613
|
+
lastCubic.control0Y,
|
|
1614
|
+
lastCubic.control1X,
|
|
1615
|
+
lastCubic.control1Y,
|
|
1616
|
+
firstCubic.anchor0X,
|
|
1617
|
+
firstCubic.anchor0Y
|
|
1618
|
+
)
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
return result;
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
function matchPolygons(p1, p2) {
|
|
1625
|
+
const measurer = new LengthMeasurer();
|
|
1626
|
+
const measuredPolygon1 = measurePolygon(measurer, p1);
|
|
1627
|
+
const measuredPolygon2 = measurePolygon(measurer, p2);
|
|
1628
|
+
const doubleMapper = featureMapper(
|
|
1629
|
+
measuredPolygon1.features,
|
|
1630
|
+
measuredPolygon2.features
|
|
1631
|
+
);
|
|
1632
|
+
const polygon2CutPoint = doubleMapper.map(0);
|
|
1633
|
+
const bs1 = measuredPolygon1;
|
|
1634
|
+
const bs2 = cutAndShift(measuredPolygon2, polygon2CutPoint);
|
|
1635
|
+
const ret = [];
|
|
1636
|
+
let i1 = 0;
|
|
1637
|
+
let i2 = 0;
|
|
1638
|
+
let b1 = bs1.cubics[i1++] ?? null;
|
|
1639
|
+
let b2 = bs2.cubics[i2++] ?? null;
|
|
1640
|
+
while (b1 !== null && b2 !== null) {
|
|
1641
|
+
const b1a = i1 === bs1.cubics.length ? 1 : b1.endOutlineProgress;
|
|
1642
|
+
const b2a = i2 === bs2.cubics.length ? 1 : doubleMapper.mapBack(
|
|
1643
|
+
positiveModulo(b2.endOutlineProgress + polygon2CutPoint, 1)
|
|
1644
|
+
);
|
|
1645
|
+
const minb = Math.min(b1a, b2a);
|
|
1646
|
+
let seg1;
|
|
1647
|
+
let newb1;
|
|
1648
|
+
if (b1a > minb + angleEpsilon) {
|
|
1649
|
+
const [s, n] = cutMeasuredCubicAtProgress(b1, minb, measurer);
|
|
1650
|
+
seg1 = s;
|
|
1651
|
+
newb1 = n;
|
|
1652
|
+
} else {
|
|
1653
|
+
seg1 = b1;
|
|
1654
|
+
newb1 = bs1.cubics[i1++] ?? null;
|
|
1655
|
+
}
|
|
1656
|
+
let seg2;
|
|
1657
|
+
let newb2;
|
|
1658
|
+
if (b2a > minb + angleEpsilon) {
|
|
1659
|
+
const [s, n] = cutMeasuredCubicAtProgress(
|
|
1660
|
+
b2,
|
|
1661
|
+
positiveModulo(doubleMapper.map(minb) - polygon2CutPoint, 1),
|
|
1662
|
+
measurer
|
|
1663
|
+
);
|
|
1664
|
+
seg2 = s;
|
|
1665
|
+
newb2 = n;
|
|
1666
|
+
} else {
|
|
1667
|
+
seg2 = b2;
|
|
1668
|
+
newb2 = bs2.cubics[i2++] ?? null;
|
|
1669
|
+
}
|
|
1670
|
+
ret.push([seg1.cubic, seg2.cubic]);
|
|
1671
|
+
b1 = newb1;
|
|
1672
|
+
b2 = newb2;
|
|
1673
|
+
}
|
|
1674
|
+
return ret;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// src/output/clip-path.ts
|
|
1678
|
+
function toClipPathPath(cubics, size = 100) {
|
|
1679
|
+
if (cubics.length === 0) {
|
|
1680
|
+
return 'path("")';
|
|
1681
|
+
}
|
|
1682
|
+
const s = size;
|
|
1683
|
+
let d = `M${(cubics[0].anchor0X * s).toFixed(2)},${(cubics[0].anchor0Y * s).toFixed(2)}`;
|
|
1684
|
+
for (const c of cubics) {
|
|
1685
|
+
d += `C${(c.control0X * s).toFixed(2)},${(c.control0Y * s).toFixed(2)} ${(c.control1X * s).toFixed(2)},${(c.control1Y * s).toFixed(2)} ${(c.anchor1X * s).toFixed(2)},${(c.anchor1Y * s).toFixed(2)}`;
|
|
1686
|
+
}
|
|
1687
|
+
d += "Z";
|
|
1688
|
+
return `path("${d}")`;
|
|
1689
|
+
}
|
|
1690
|
+
function toClipPathPolygon(cubics, samplesPerCubic = 4) {
|
|
1691
|
+
if (cubics.length === 0) {
|
|
1692
|
+
return "polygon(0% 0%)";
|
|
1693
|
+
}
|
|
1694
|
+
const points = [];
|
|
1695
|
+
for (const cubic of cubics) {
|
|
1696
|
+
for (let i = 0; i < samplesPerCubic; i++) {
|
|
1697
|
+
const t = i / samplesPerCubic;
|
|
1698
|
+
const point = cubic.pointOnCurve(t);
|
|
1699
|
+
points.push(
|
|
1700
|
+
`${(point.x * 100).toFixed(2)}% ${(point.y * 100).toFixed(2)}%`
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
return `polygon(${points.join(",")})`;
|
|
1705
|
+
}
|
|
1706
|
+
function toMorphPair(start, end, samplesPerCubic = 4) {
|
|
1707
|
+
const morph = new Morph(start, end);
|
|
1708
|
+
return [
|
|
1709
|
+
toClipPathPolygon(morph.asCubics(0), samplesPerCubic),
|
|
1710
|
+
toClipPathPolygon(morph.asCubics(1), samplesPerCubic)
|
|
1711
|
+
];
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/output/svg-path.ts
|
|
1715
|
+
function toPathD(cubics, size = 100) {
|
|
1716
|
+
if (cubics.length === 0) {
|
|
1717
|
+
return "";
|
|
1718
|
+
}
|
|
1719
|
+
const s = size;
|
|
1720
|
+
const parts = [
|
|
1721
|
+
`M${(cubics[0].anchor0X * s).toFixed(2)},${(cubics[0].anchor0Y * s).toFixed(2)}`
|
|
1722
|
+
];
|
|
1723
|
+
for (const c of cubics) {
|
|
1724
|
+
parts.push(
|
|
1725
|
+
`C${(c.control0X * s).toFixed(2)},${(c.control0Y * s).toFixed(2)} ${(c.control1X * s).toFixed(2)},${(c.control1Y * s).toFixed(2)} ${(c.anchor1X * s).toFixed(2)},${(c.anchor1Y * s).toFixed(2)}`
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
parts.push("Z");
|
|
1729
|
+
return parts.join("");
|
|
1730
|
+
}
|
|
1731
|
+
function toSvgPath(polygon, size = 100) {
|
|
1732
|
+
return toPathD(polygon.cubics, size);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
exports.Cubic = Cubic;
|
|
1736
|
+
exports.Morph = Morph;
|
|
1737
|
+
exports.RoundedPolygon = RoundedPolygon;
|
|
1738
|
+
exports.angleEpsilon = angleEpsilon;
|
|
1739
|
+
exports.cornerRounding = cornerRounding;
|
|
1740
|
+
exports.createCircle = createCircle;
|
|
1741
|
+
exports.createPolygon = createPolygon;
|
|
1742
|
+
exports.createPolygonFromVertices = createPolygonFromVertices;
|
|
1743
|
+
exports.createRectangle = createRectangle;
|
|
1744
|
+
exports.createStar = createStar;
|
|
1745
|
+
exports.distanceEpsilon = distanceEpsilon;
|
|
1746
|
+
exports.floatPi = floatPi;
|
|
1747
|
+
exports.getShape = getShape;
|
|
1748
|
+
exports.relaxedDistanceEpsilon = relaxedDistanceEpsilon;
|
|
1749
|
+
exports.shapeNames = shapeNames;
|
|
1750
|
+
exports.toClipPathPath = toClipPathPath;
|
|
1751
|
+
exports.toClipPathPolygon = toClipPathPolygon;
|
|
1752
|
+
exports.toMorphPair = toMorphPair;
|
|
1753
|
+
exports.toPathD = toPathD;
|
|
1754
|
+
exports.toSvgPath = toSvgPath;
|
|
1755
|
+
exports.twoPi = twoPi;
|
|
1756
|
+
exports.unrounded = unrounded;
|
|
1757
|
+
//# sourceMappingURL=index.cjs.map
|
|
1758
|
+
//# sourceMappingURL=index.cjs.map
|