@vune-ui/animation 0.1.20
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/ARCHITECTURE.md +470 -0
- package/CHANGELOG.md +88 -0
- package/LICENSE +21 -0
- package/PERFORMANCE.md +151 -0
- package/README.md +630 -0
- package/dist/index.d.ts +474 -0
- package/dist/src/canvas/index.d.ts +15 -0
- package/dist/src/canvas/index.js +67 -0
- package/dist/src/constraints/index.d.ts +33 -0
- package/dist/src/constraints/index.js +346 -0
- package/dist/src/core/bezier.js +51 -0
- package/dist/src/core/composition.js +17 -0
- package/dist/src/core/controls.js +22 -0
- package/dist/src/core/default-engine.js +20 -0
- package/dist/src/core/easing.js +58 -0
- package/dist/src/core/engine.js +1031 -0
- package/dist/src/core/frame-budget.js +30 -0
- package/dist/src/core/index.d.ts +43 -0
- package/dist/src/core/index.js +17 -0
- package/dist/src/core/js-spring-batch.js +57 -0
- package/dist/src/core/kinetics.js +140 -0
- package/dist/src/core/math.js +20 -0
- package/dist/src/core/motion-value.js +53 -0
- package/dist/src/core/planner.js +72 -0
- package/dist/src/core/specs.js +70 -0
- package/dist/src/dom/index.d.ts +41 -0
- package/dist/src/dom/index.js +364 -0
- package/dist/src/gesture/index.d.ts +66 -0
- package/dist/src/gesture/index.js +376 -0
- package/dist/src/index.js +53 -0
- package/dist/src/interpolate/color.js +223 -0
- package/dist/src/interpolate/css.d.ts +13 -0
- package/dist/src/interpolate/css.js +34 -0
- package/dist/src/interpolate/index.d.ts +13 -0
- package/dist/src/interpolate/index.js +55 -0
- package/dist/src/interpolate/transform.js +247 -0
- package/dist/src/layout/index.d.ts +56 -0
- package/dist/src/layout/index.js +485 -0
- package/dist/src/material/index.d.ts +9 -0
- package/dist/src/material/index.js +70 -0
- package/dist/src/path/index.d.ts +37 -0
- package/dist/src/path/index.js +527 -0
- package/dist/src/render/frame-batcher.js +52 -0
- package/dist/src/scroll/index.d.ts +55 -0
- package/dist/src/scroll/index.js +233 -0
- package/dist/src/timeline/index.d.ts +147 -0
- package/dist/src/timeline/index.js +849 -0
- package/dist/src/transition/index.d.ts +88 -0
- package/dist/src/transition/index.js +369 -0
- package/dist/src/wasm/index.d.ts +29 -0
- package/dist/src/wasm/index.js +8 -0
- package/dist/src/wasm/loader.js +55 -0
- package/dist/src/wasm/shared-wasm-spring-batch.js +52 -0
- package/dist/src/wasm/wasm-spring-batch.js +52 -0
- package/dist/src/webgl/index.d.ts +22 -0
- package/dist/src/webgl/index.js +94 -0
- package/dist/src/webgpu/index.d.ts +35 -0
- package/dist/src/webgpu/index.js +73 -0
- package/dist/src/webgpu/spring-batch.js +218 -0
- package/dist/src/worker/index.d.ts +17 -0
- package/dist/src/worker/index.js +1 -0
- package/dist/src/worker/shared-spring-worker.js +218 -0
- package/dist/src/worker/shared-worker.js +75 -0
- package/dist/wasm/kernel-scalar.wasm +0 -0
- package/dist/wasm/kernel-shared-scalar.wasm +0 -0
- package/dist/wasm/kernel-shared-simd.wasm +0 -0
- package/dist/wasm/kernel-simd.wasm +0 -0
- package/package.json +113 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
const COMMAND_PARAMS = Object.freeze({
|
|
2
|
+
M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2, A: 7, Z: 0,
|
|
3
|
+
});
|
|
4
|
+
|
|
5
|
+
const TOKEN_RE = /[a-zA-Z]|[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
|
|
6
|
+
const EPSILON = 1e-9;
|
|
7
|
+
|
|
8
|
+
function point(x, y) { return { x, y }; }
|
|
9
|
+
const lerp = (a, b, t) => a + (b - a) * t;
|
|
10
|
+
const midpoint = (a, b) => point((a.x + b.x) * 0.5, (a.y + b.y) * 0.5);
|
|
11
|
+
const distance = (a, b) => Math.hypot(b.x - a.x, b.y - a.y);
|
|
12
|
+
const samePoint = (a, b) => Math.abs(a.x - b.x) <= EPSILON && Math.abs(a.y - b.y) <= EPSILON;
|
|
13
|
+
|
|
14
|
+
function lineAsCubic(p0, p3) {
|
|
15
|
+
return {
|
|
16
|
+
p0: point(p0.x, p0.y),
|
|
17
|
+
p1: point(lerp(p0.x, p3.x, 1 / 3), lerp(p0.y, p3.y, 1 / 3)),
|
|
18
|
+
p2: point(lerp(p0.x, p3.x, 2 / 3), lerp(p0.y, p3.y, 2 / 3)),
|
|
19
|
+
p3: point(p3.x, p3.y),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function quadraticAsCubic(p0, q, p3) {
|
|
24
|
+
return {
|
|
25
|
+
p0: point(p0.x, p0.y),
|
|
26
|
+
p1: point(p0.x + (q.x - p0.x) * (2 / 3), p0.y + (q.y - p0.y) * (2 / 3)),
|
|
27
|
+
p2: point(p3.x + (q.x - p3.x) * (2 / 3), p3.y + (q.y - p3.y) * (2 / 3)),
|
|
28
|
+
p3: point(p3.x, p3.y),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function vectorAngle(ux, uy, vx, vy) {
|
|
33
|
+
const dot = ux * vx + uy * vy;
|
|
34
|
+
const len = Math.hypot(ux, uy) * Math.hypot(vx, vy);
|
|
35
|
+
if (len <= EPSILON) return 0;
|
|
36
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, dot / len)));
|
|
37
|
+
return (ux * vy - uy * vx < 0 ? -1 : 1) * angle;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function arcAsCubics(p0, rxInput, ryInput, rotationDeg, largeArcFlag, sweepFlag, p3) {
|
|
41
|
+
let rx = Math.abs(rxInput);
|
|
42
|
+
let ry = Math.abs(ryInput);
|
|
43
|
+
if (rx <= EPSILON || ry <= EPSILON || samePoint(p0, p3)) {
|
|
44
|
+
return samePoint(p0, p3) ? [] : [lineAsCubic(p0, p3)];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const phi = rotationDeg * Math.PI / 180;
|
|
48
|
+
const cosPhi = Math.cos(phi);
|
|
49
|
+
const sinPhi = Math.sin(phi);
|
|
50
|
+
const dx2 = (p0.x - p3.x) * 0.5;
|
|
51
|
+
const dy2 = (p0.y - p3.y) * 0.5;
|
|
52
|
+
const x1p = cosPhi * dx2 + sinPhi * dy2;
|
|
53
|
+
const y1p = -sinPhi * dx2 + cosPhi * dy2;
|
|
54
|
+
|
|
55
|
+
const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
|
|
56
|
+
if (lambda > 1) {
|
|
57
|
+
const scale = Math.sqrt(lambda);
|
|
58
|
+
rx *= scale;
|
|
59
|
+
ry *= scale;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rx2 = rx * rx;
|
|
63
|
+
const ry2 = ry * ry;
|
|
64
|
+
const x1p2 = x1p * x1p;
|
|
65
|
+
const y1p2 = y1p * y1p;
|
|
66
|
+
const numerator = Math.max(0, rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2);
|
|
67
|
+
const denominator = Math.max(EPSILON, rx2 * y1p2 + ry2 * x1p2);
|
|
68
|
+
const sign = Boolean(largeArcFlag) === Boolean(sweepFlag) ? -1 : 1;
|
|
69
|
+
const coefficient = sign * Math.sqrt(numerator / denominator);
|
|
70
|
+
const cxp = coefficient * (rx * y1p / ry);
|
|
71
|
+
const cyp = coefficient * (-ry * x1p / rx);
|
|
72
|
+
|
|
73
|
+
const cx = cosPhi * cxp - sinPhi * cyp + (p0.x + p3.x) * 0.5;
|
|
74
|
+
const cy = sinPhi * cxp + cosPhi * cyp + (p0.y + p3.y) * 0.5;
|
|
75
|
+
|
|
76
|
+
const ux = (x1p - cxp) / rx;
|
|
77
|
+
const uy = (y1p - cyp) / ry;
|
|
78
|
+
const vx = (-x1p - cxp) / rx;
|
|
79
|
+
const vy = (-y1p - cyp) / ry;
|
|
80
|
+
let theta1 = vectorAngle(1, 0, ux, uy);
|
|
81
|
+
let delta = vectorAngle(ux, uy, vx, vy);
|
|
82
|
+
if (!sweepFlag && delta > 0) delta -= Math.PI * 2;
|
|
83
|
+
if (sweepFlag && delta < 0) delta += Math.PI * 2;
|
|
84
|
+
|
|
85
|
+
const segmentCount = Math.max(1, Math.ceil(Math.abs(delta) / (Math.PI / 2)));
|
|
86
|
+
const step = delta / segmentCount;
|
|
87
|
+
const result = [];
|
|
88
|
+
|
|
89
|
+
const ellipsePoint = (theta) => point(
|
|
90
|
+
cx + rx * cosPhi * Math.cos(theta) - ry * sinPhi * Math.sin(theta),
|
|
91
|
+
cy + rx * sinPhi * Math.cos(theta) + ry * cosPhi * Math.sin(theta),
|
|
92
|
+
);
|
|
93
|
+
const derivative = (theta) => point(
|
|
94
|
+
-rx * cosPhi * Math.sin(theta) - ry * sinPhi * Math.cos(theta),
|
|
95
|
+
-rx * sinPhi * Math.sin(theta) + ry * cosPhi * Math.cos(theta),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < segmentCount; i += 1) {
|
|
99
|
+
const a = theta1 + i * step;
|
|
100
|
+
const b = a + step;
|
|
101
|
+
const k = (4 / 3) * Math.tan((b - a) / 4);
|
|
102
|
+
const start = i === 0 ? p0 : ellipsePoint(a);
|
|
103
|
+
const end = i === segmentCount - 1 ? p3 : ellipsePoint(b);
|
|
104
|
+
const da = derivative(a);
|
|
105
|
+
const db = derivative(b);
|
|
106
|
+
result.push({
|
|
107
|
+
p0: point(start.x, start.y),
|
|
108
|
+
p1: point(start.x + k * da.x, start.y + k * da.y),
|
|
109
|
+
p2: point(end.x - k * db.x, end.y - k * db.y),
|
|
110
|
+
p3: point(end.x, end.y),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function tokenizePath(path) {
|
|
117
|
+
if (typeof path !== 'string') throw new TypeError('SVG path must be a string.');
|
|
118
|
+
const tokens = path.match(TOKEN_RE) ?? [];
|
|
119
|
+
if (tokens.length === 0) throw new TypeError('SVG path is empty or invalid.');
|
|
120
|
+
return tokens;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isCommand(token) { return /^[a-zA-Z]$/.test(token); }
|
|
124
|
+
|
|
125
|
+
function takeNumericToken(tokens, cursor, command) {
|
|
126
|
+
if (cursor.index >= tokens.length || isCommand(tokens[cursor.index])) {
|
|
127
|
+
throw new TypeError(`Not enough parameters for SVG command ${command}.`);
|
|
128
|
+
}
|
|
129
|
+
const raw = tokens[cursor.index++];
|
|
130
|
+
const value = Number(raw);
|
|
131
|
+
if (!Number.isFinite(value)) throw new TypeError(`Invalid numeric value in SVG command ${command}.`);
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* SVG arc flags are single-character grammar productions, not ordinary
|
|
137
|
+
* numbers. The path grammar therefore permits compact forms such as
|
|
138
|
+
* `a2 2 0 00-2-2`, where the large-arc and sweep flags are adjacent. A
|
|
139
|
+
* number-only tokenizer sees `00` as one token, so consume the flag one byte at
|
|
140
|
+
* a time and leave any remainder for the next flag/coordinate.
|
|
141
|
+
*/
|
|
142
|
+
function takeArcFlag(tokens, cursor, command) {
|
|
143
|
+
if (cursor.index >= tokens.length || isCommand(tokens[cursor.index])) {
|
|
144
|
+
throw new TypeError(`Not enough parameters for SVG command ${command}.`);
|
|
145
|
+
}
|
|
146
|
+
const raw = tokens[cursor.index];
|
|
147
|
+
const flag = raw[0];
|
|
148
|
+
if (flag !== '0' && flag !== '1') throw new TypeError(`Invalid arc flag in SVG command ${command}.`);
|
|
149
|
+
const remainder = raw.slice(1);
|
|
150
|
+
if (remainder.length > 0) tokens[cursor.index] = remainder;
|
|
151
|
+
else cursor.index += 1;
|
|
152
|
+
return Number(flag);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function takeCommandArgs(tokens, cursor, upper, command) {
|
|
156
|
+
if (upper === 'A') {
|
|
157
|
+
return [
|
|
158
|
+
takeNumericToken(tokens, cursor, command),
|
|
159
|
+
takeNumericToken(tokens, cursor, command),
|
|
160
|
+
takeNumericToken(tokens, cursor, command),
|
|
161
|
+
takeArcFlag(tokens, cursor, command),
|
|
162
|
+
takeArcFlag(tokens, cursor, command),
|
|
163
|
+
takeNumericToken(tokens, cursor, command),
|
|
164
|
+
takeNumericToken(tokens, cursor, command),
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
const count = COMMAND_PARAMS[upper];
|
|
168
|
+
const args = new Array(count);
|
|
169
|
+
for (let index = 0; index < count; index += 1) args[index] = takeNumericToken(tokens, cursor, command);
|
|
170
|
+
return args;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function parsePath(path) {
|
|
174
|
+
const tokens = tokenizePath(path);
|
|
175
|
+
const subpaths = [];
|
|
176
|
+
let currentSubpath = null;
|
|
177
|
+
let i = 0;
|
|
178
|
+
let command = null;
|
|
179
|
+
let current = point(0, 0);
|
|
180
|
+
let subpathStart = point(0, 0);
|
|
181
|
+
let previousCubicControl = null;
|
|
182
|
+
let previousQuadraticControl = null;
|
|
183
|
+
|
|
184
|
+
const ensureSubpath = () => {
|
|
185
|
+
if (!currentSubpath) {
|
|
186
|
+
currentSubpath = { segments: [], closed: false };
|
|
187
|
+
subpaths.push(currentSubpath);
|
|
188
|
+
subpathStart = point(current.x, current.y);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const addSegment = (segment) => {
|
|
193
|
+
ensureSubpath();
|
|
194
|
+
currentSubpath.segments.push(segment);
|
|
195
|
+
current = point(segment.p3.x, segment.p3.y);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
while (i < tokens.length) {
|
|
199
|
+
if (isCommand(tokens[i])) command = tokens[i++];
|
|
200
|
+
if (!command) throw new TypeError('SVG path must begin with a command.');
|
|
201
|
+
const upper = command.toUpperCase();
|
|
202
|
+
const relative = command !== upper;
|
|
203
|
+
const paramCount = COMMAND_PARAMS[upper];
|
|
204
|
+
if (paramCount == null) throw new TypeError(`Unsupported SVG path command: ${command}`);
|
|
205
|
+
|
|
206
|
+
if (upper === 'Z') {
|
|
207
|
+
if (currentSubpath) {
|
|
208
|
+
if (!samePoint(current, subpathStart)) addSegment(lineAsCubic(current, subpathStart));
|
|
209
|
+
currentSubpath.closed = true;
|
|
210
|
+
current = point(subpathStart.x, subpathStart.y);
|
|
211
|
+
}
|
|
212
|
+
previousCubicControl = null;
|
|
213
|
+
previousQuadraticControl = null;
|
|
214
|
+
command = null;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let firstSet = true;
|
|
219
|
+
while (i < tokens.length && !isCommand(tokens[i])) {
|
|
220
|
+
const cursor = { index: i };
|
|
221
|
+
const args = takeCommandArgs(tokens, cursor, upper, command);
|
|
222
|
+
i = cursor.index;
|
|
223
|
+
|
|
224
|
+
const absX = (value) => relative ? current.x + value : value;
|
|
225
|
+
const absY = (value) => relative ? current.y + value : value;
|
|
226
|
+
|
|
227
|
+
if (upper === 'M') {
|
|
228
|
+
const next = point(absX(args[0]), absY(args[1]));
|
|
229
|
+
if (firstSet) {
|
|
230
|
+
current = next;
|
|
231
|
+
subpathStart = point(next.x, next.y);
|
|
232
|
+
currentSubpath = { segments: [], closed: false };
|
|
233
|
+
subpaths.push(currentSubpath);
|
|
234
|
+
} else {
|
|
235
|
+
addSegment(lineAsCubic(current, next));
|
|
236
|
+
}
|
|
237
|
+
previousCubicControl = null;
|
|
238
|
+
previousQuadraticControl = null;
|
|
239
|
+
} else if (upper === 'L') {
|
|
240
|
+
addSegment(lineAsCubic(current, point(absX(args[0]), absY(args[1]))));
|
|
241
|
+
previousCubicControl = null;
|
|
242
|
+
previousQuadraticControl = null;
|
|
243
|
+
} else if (upper === 'H') {
|
|
244
|
+
addSegment(lineAsCubic(current, point(absX(args[0]), current.y)));
|
|
245
|
+
previousCubicControl = null;
|
|
246
|
+
previousQuadraticControl = null;
|
|
247
|
+
} else if (upper === 'V') {
|
|
248
|
+
addSegment(lineAsCubic(current, point(current.x, absY(args[0]))));
|
|
249
|
+
previousCubicControl = null;
|
|
250
|
+
previousQuadraticControl = null;
|
|
251
|
+
} else if (upper === 'C') {
|
|
252
|
+
const p1 = point(absX(args[0]), absY(args[1]));
|
|
253
|
+
const p2 = point(absX(args[2]), absY(args[3]));
|
|
254
|
+
const p3 = point(absX(args[4]), absY(args[5]));
|
|
255
|
+
addSegment({ p0: point(current.x, current.y), p1, p2, p3 });
|
|
256
|
+
previousCubicControl = point(p2.x, p2.y);
|
|
257
|
+
previousQuadraticControl = null;
|
|
258
|
+
} else if (upper === 'S') {
|
|
259
|
+
const p1 = previousCubicControl
|
|
260
|
+
? point(current.x * 2 - previousCubicControl.x, current.y * 2 - previousCubicControl.y)
|
|
261
|
+
: point(current.x, current.y);
|
|
262
|
+
const p2 = point(absX(args[0]), absY(args[1]));
|
|
263
|
+
const p3 = point(absX(args[2]), absY(args[3]));
|
|
264
|
+
addSegment({ p0: point(current.x, current.y), p1, p2, p3 });
|
|
265
|
+
previousCubicControl = point(p2.x, p2.y);
|
|
266
|
+
previousQuadraticControl = null;
|
|
267
|
+
} else if (upper === 'Q') {
|
|
268
|
+
const q = point(absX(args[0]), absY(args[1]));
|
|
269
|
+
const p3 = point(absX(args[2]), absY(args[3]));
|
|
270
|
+
addSegment(quadraticAsCubic(current, q, p3));
|
|
271
|
+
previousQuadraticControl = point(q.x, q.y);
|
|
272
|
+
previousCubicControl = null;
|
|
273
|
+
} else if (upper === 'T') {
|
|
274
|
+
const q = previousQuadraticControl
|
|
275
|
+
? point(current.x * 2 - previousQuadraticControl.x, current.y * 2 - previousQuadraticControl.y)
|
|
276
|
+
: point(current.x, current.y);
|
|
277
|
+
const p3 = point(absX(args[0]), absY(args[1]));
|
|
278
|
+
addSegment(quadraticAsCubic(current, q, p3));
|
|
279
|
+
previousQuadraticControl = point(q.x, q.y);
|
|
280
|
+
previousCubicControl = null;
|
|
281
|
+
} else if (upper === 'A') {
|
|
282
|
+
const p3 = point(absX(args[5]), absY(args[6]));
|
|
283
|
+
const cubics = arcAsCubics(current, args[0], args[1], args[2], args[3] !== 0, args[4] !== 0, p3);
|
|
284
|
+
for (const segment of cubics) addSegment(segment);
|
|
285
|
+
previousCubicControl = cubics.length ? point(cubics.at(-1).p2.x, cubics.at(-1).p2.y) : null;
|
|
286
|
+
previousQuadraticControl = null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
firstSet = false;
|
|
290
|
+
if (upper === 'M') command = relative ? 'l' : 'L';
|
|
291
|
+
if (i >= tokens.length || isCommand(tokens[i])) break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { subpaths: subpaths.filter((subpath) => subpath.segments.length > 0) };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function cloneSegment(segment) {
|
|
299
|
+
return {
|
|
300
|
+
p0: point(segment.p0.x, segment.p0.y),
|
|
301
|
+
p1: point(segment.p1.x, segment.p1.y),
|
|
302
|
+
p2: point(segment.p2.x, segment.p2.y),
|
|
303
|
+
p3: point(segment.p3.x, segment.p3.y),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function splitCubic(segment) {
|
|
308
|
+
const p01 = midpoint(segment.p0, segment.p1);
|
|
309
|
+
const p12 = midpoint(segment.p1, segment.p2);
|
|
310
|
+
const p23 = midpoint(segment.p2, segment.p3);
|
|
311
|
+
const p012 = midpoint(p01, p12);
|
|
312
|
+
const p123 = midpoint(p12, p23);
|
|
313
|
+
const p = midpoint(p012, p123);
|
|
314
|
+
return [
|
|
315
|
+
{ p0: cloneSegment(segment).p0, p1: p01, p2: p012, p3: p },
|
|
316
|
+
{ p0: p, p1: p123, p2: p23, p3: cloneSegment(segment).p3 },
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function approximateCubicLength(segment) {
|
|
321
|
+
return distance(segment.p0, segment.p1) + distance(segment.p1, segment.p2) + distance(segment.p2, segment.p3);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function equalizeSegments(segments, targetCount) {
|
|
325
|
+
const result = segments.map(cloneSegment);
|
|
326
|
+
while (result.length < targetCount) {
|
|
327
|
+
let longestIndex = 0;
|
|
328
|
+
let longestLength = -1;
|
|
329
|
+
for (let i = 0; i < result.length; i += 1) {
|
|
330
|
+
const length = approximateCubicLength(result[i]);
|
|
331
|
+
if (length > longestLength) {
|
|
332
|
+
longestLength = length;
|
|
333
|
+
longestIndex = i;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const [a, b] = splitCubic(result[longestIndex]);
|
|
337
|
+
result.splice(longestIndex, 1, a, b);
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function reverseSegments(segments) {
|
|
343
|
+
return [...segments].reverse().map((segment) => ({
|
|
344
|
+
p0: point(segment.p3.x, segment.p3.y),
|
|
345
|
+
p1: point(segment.p2.x, segment.p2.y),
|
|
346
|
+
p2: point(segment.p1.x, segment.p1.y),
|
|
347
|
+
p3: point(segment.p0.x, segment.p0.y),
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function rotateSegments(segments, shift) {
|
|
352
|
+
if (shift === 0) return segments.map(cloneSegment);
|
|
353
|
+
const n = segments.length;
|
|
354
|
+
const result = [];
|
|
355
|
+
for (let i = 0; i < n; i += 1) result.push(cloneSegment(segments[(i + shift) % n]));
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function segmentScore(a, b) {
|
|
360
|
+
const pairs = [
|
|
361
|
+
[a.p0, b.p0], [a.p1, b.p1], [a.p2, b.p2], [a.p3, b.p3],
|
|
362
|
+
];
|
|
363
|
+
let score = 0;
|
|
364
|
+
for (const [p, q] of pairs) {
|
|
365
|
+
const dx = p.x - q.x;
|
|
366
|
+
const dy = p.y - q.y;
|
|
367
|
+
score += dx * dx + dy * dy;
|
|
368
|
+
}
|
|
369
|
+
return score;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function alignmentScore(from, to) {
|
|
373
|
+
let score = 0;
|
|
374
|
+
for (let i = 0; i < from.length; i += 1) score += segmentScore(from[i], to[i]);
|
|
375
|
+
return score;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function alignmentCandidateShifts(from, candidate, maxCandidates) {
|
|
379
|
+
const n = candidate.length;
|
|
380
|
+
if (n <= maxCandidates) return Array.from({ length: n }, (_, index) => index);
|
|
381
|
+
const anchor = from[0].p0;
|
|
382
|
+
const ranked = candidate.map((segment, index) => {
|
|
383
|
+
const dx = segment.p0.x - anchor.x;
|
|
384
|
+
const dy = segment.p0.y - anchor.y;
|
|
385
|
+
return { index, score: dx * dx + dy * dy };
|
|
386
|
+
});
|
|
387
|
+
ranked.sort((a, b) => a.score - b.score);
|
|
388
|
+
const result = new Set();
|
|
389
|
+
const seedCount = Math.max(1, Math.floor(maxCandidates / 3));
|
|
390
|
+
for (let i = 0; i < Math.min(seedCount, ranked.length); i += 1) {
|
|
391
|
+
const index = ranked[i].index;
|
|
392
|
+
result.add(index);
|
|
393
|
+
result.add((index + n - 1) % n);
|
|
394
|
+
result.add((index + 1) % n);
|
|
395
|
+
}
|
|
396
|
+
return [...result].slice(0, maxCandidates);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function alignClosedSegments(from, to, allowReverse, maxCandidates = 18) {
|
|
400
|
+
let best = to.map(cloneSegment);
|
|
401
|
+
let bestScore = Infinity;
|
|
402
|
+
const candidates = allowReverse ? [to, reverseSegments(to)] : [to];
|
|
403
|
+
for (const candidate of candidates) {
|
|
404
|
+
const shifts = alignmentCandidateShifts(from, candidate, Math.max(3, maxCandidates));
|
|
405
|
+
for (const shift of shifts) {
|
|
406
|
+
const rotated = rotateSegments(candidate, shift);
|
|
407
|
+
const score = alignmentScore(from, rotated);
|
|
408
|
+
if (score < bestScore) {
|
|
409
|
+
bestScore = score;
|
|
410
|
+
best = rotated;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return best;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function maybeReverseOpen(from, to, allowReverse) {
|
|
418
|
+
if (!allowReverse || from.length === 0 || to.length === 0) return to;
|
|
419
|
+
const direct = distance(from[0].p0, to[0].p0) + distance(from.at(-1).p3, to.at(-1).p3);
|
|
420
|
+
const reversed = distance(from[0].p0, to.at(-1).p3) + distance(from.at(-1).p3, to[0].p0);
|
|
421
|
+
return reversed < direct ? reverseSegments(to) : to;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function flattenSubpaths(parsed, options) {
|
|
425
|
+
const meta = [];
|
|
426
|
+
const coords = [];
|
|
427
|
+
for (const subpath of parsed.subpaths) {
|
|
428
|
+
const offset = coords.length;
|
|
429
|
+
for (const segment of subpath.segments) {
|
|
430
|
+
coords.push(
|
|
431
|
+
segment.p0.x, segment.p0.y,
|
|
432
|
+
segment.p1.x, segment.p1.y,
|
|
433
|
+
segment.p2.x, segment.p2.y,
|
|
434
|
+
segment.p3.x, segment.p3.y,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
meta.push({ offset, count: subpath.segments.length, closed: subpath.closed });
|
|
438
|
+
}
|
|
439
|
+
return { coords: new Float64Array(coords), subpaths: meta, options };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function normalizePathPair(fromPath, toPath, { align = true, allowReverse = true, alignmentCandidates = 18 } = {}) {
|
|
443
|
+
const from = parsePath(fromPath);
|
|
444
|
+
const to = parsePath(toPath);
|
|
445
|
+
if (from.subpaths.length !== to.subpaths.length) {
|
|
446
|
+
throw new TypeError(`Path morph requires the same number of subpaths (${from.subpaths.length} !== ${to.subpaths.length}).`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const normalizedFrom = { subpaths: [] };
|
|
450
|
+
const normalizedTo = { subpaths: [] };
|
|
451
|
+
for (let i = 0; i < from.subpaths.length; i += 1) {
|
|
452
|
+
const a = from.subpaths[i];
|
|
453
|
+
const b = to.subpaths[i];
|
|
454
|
+
const count = Math.max(a.segments.length, b.segments.length);
|
|
455
|
+
let aSegments = equalizeSegments(a.segments, count);
|
|
456
|
+
let bSegments = equalizeSegments(b.segments, count);
|
|
457
|
+
|
|
458
|
+
if (align && a.closed && b.closed) bSegments = alignClosedSegments(aSegments, bSegments, allowReverse, alignmentCandidates);
|
|
459
|
+
else if (align && !a.closed && !b.closed) bSegments = maybeReverseOpen(aSegments, bSegments, allowReverse);
|
|
460
|
+
|
|
461
|
+
normalizedFrom.subpaths.push({ segments: aSegments, closed: a.closed });
|
|
462
|
+
normalizedTo.subpaths.push({ segments: bSegments, closed: b.closed });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const flatFrom = flattenSubpaths(normalizedFrom, { align, allowReverse });
|
|
466
|
+
const flatTo = flattenSubpaths(normalizedTo, { align, allowReverse });
|
|
467
|
+
return { from: flatFrom, to: flatTo };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function formatNumber(value, precision) {
|
|
471
|
+
if (Math.abs(value) < 10 ** (-(precision + 1))) return '0';
|
|
472
|
+
const rounded = Number(value.toFixed(precision));
|
|
473
|
+
return String(rounded);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export class PathMorpher {
|
|
477
|
+
constructor(fromPath, toPath, options = {}) {
|
|
478
|
+
const normalized = normalizePathPair(fromPath, toPath, options);
|
|
479
|
+
this.from = normalized.from.coords;
|
|
480
|
+
this.to = normalized.to.coords;
|
|
481
|
+
this.subpaths = normalized.from.subpaths;
|
|
482
|
+
this.precision = Math.max(0, Math.min(8, Math.floor(options.precision ?? 3)));
|
|
483
|
+
this.buffer = new Float64Array(this.from.length);
|
|
484
|
+
this.fromPath = fromPath;
|
|
485
|
+
this.toPath = toPath;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
get coordinateCount() { return this.from.length; }
|
|
489
|
+
get segmentCount() { return this.from.length / 8; }
|
|
490
|
+
|
|
491
|
+
sampleInto(progress, output = this.buffer) {
|
|
492
|
+
if (!output || output.length < this.from.length) throw new RangeError('Path morph output buffer is too small.');
|
|
493
|
+
const t = Math.max(0, Math.min(1, Number(progress) || 0));
|
|
494
|
+
for (let i = 0; i < this.from.length; i += 1) output[i] = this.from[i] + (this.to[i] - this.from[i]) * t;
|
|
495
|
+
return output;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
format(buffer = this.buffer) {
|
|
499
|
+
const parts = [];
|
|
500
|
+
for (const subpath of this.subpaths) {
|
|
501
|
+
const start = subpath.offset;
|
|
502
|
+
const f = (index) => formatNumber(buffer[index], this.precision);
|
|
503
|
+
parts.push(`M${f(start)} ${f(start + 1)}`);
|
|
504
|
+
for (let i = 0; i < subpath.count; i += 1) {
|
|
505
|
+
const offset = start + i * 8;
|
|
506
|
+
parts.push(`C${f(offset + 2)} ${f(offset + 3)} ${f(offset + 4)} ${f(offset + 5)} ${f(offset + 6)} ${f(offset + 7)}`);
|
|
507
|
+
}
|
|
508
|
+
if (subpath.closed) parts.push('Z');
|
|
509
|
+
}
|
|
510
|
+
return parts.join(' ');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
sample(progress) {
|
|
514
|
+
if (progress <= 0) return this.fromPath;
|
|
515
|
+
if (progress >= 1) return this.toPath;
|
|
516
|
+
return this.format(this.sampleInto(progress));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function createPathMorpher(fromPath, toPath, options = {}) {
|
|
521
|
+
return new PathMorpher(fromPath, toPath, options);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function interpolatePath(fromPath, toPath, options = {}) {
|
|
525
|
+
const morpher = new PathMorpher(fromPath, toPath, options);
|
|
526
|
+
return (progress) => morpher.sample(progress);
|
|
527
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function defaultRequestFrame(callback) {
|
|
2
|
+
if (typeof globalThis.requestAnimationFrame === 'function') return globalThis.requestAnimationFrame(callback);
|
|
3
|
+
return setTimeout(() => callback(globalThis.performance?.now?.() ?? Date.now()), 16);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function defaultCancelFrame(id) {
|
|
7
|
+
if (typeof globalThis.cancelAnimationFrame === 'function') globalThis.cancelAnimationFrame(id);
|
|
8
|
+
else clearTimeout(id);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class FrameBatcher {
|
|
12
|
+
constructor(flush, {
|
|
13
|
+
requestFrame = defaultRequestFrame,
|
|
14
|
+
cancelFrame = defaultCancelFrame,
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (typeof flush !== 'function') throw new TypeError('FrameBatcher requires a flush callback.');
|
|
17
|
+
this.flushCallback = flush;
|
|
18
|
+
this.requestFrame = requestFrame;
|
|
19
|
+
this.cancelFrame = cancelFrame;
|
|
20
|
+
this.pending = false;
|
|
21
|
+
this.frameId = null;
|
|
22
|
+
this.disposed = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
invalidate() {
|
|
26
|
+
if (this.disposed || this.pending) return false;
|
|
27
|
+
this.pending = true;
|
|
28
|
+
this.frameId = this.requestFrame((time) => {
|
|
29
|
+
this.pending = false;
|
|
30
|
+
this.frameId = null;
|
|
31
|
+
if (!this.disposed) this.flushCallback(time);
|
|
32
|
+
});
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
flushNow(time = globalThis.performance?.now?.() ?? Date.now()) {
|
|
37
|
+
if (this.disposed) return false;
|
|
38
|
+
if (this.pending && this.frameId != null) this.cancelFrame(this.frameId);
|
|
39
|
+
this.pending = false;
|
|
40
|
+
this.frameId = null;
|
|
41
|
+
this.flushCallback(time);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
dispose() {
|
|
46
|
+
if (this.disposed) return;
|
|
47
|
+
this.disposed = true;
|
|
48
|
+
if (this.pending && this.frameId != null) this.cancelFrame(this.frameId);
|
|
49
|
+
this.pending = false;
|
|
50
|
+
this.frameId = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { MotionValue } from '../../index.js';
|
|
2
|
+
import type { TimelinePlayer } from '../timeline/index.js';
|
|
3
|
+
|
|
4
|
+
export type ScrollMetrics = { offset: number; viewport: number; extent: number; max: number };
|
|
5
|
+
export type ScrollRangeContext = { offset?: number; metrics?: ScrollMetrics; target?: unknown; axis?: 'x' | 'y' };
|
|
6
|
+
export type ScrollBound = number | ((context: ScrollRangeContext | ScrollMetrics) => number);
|
|
7
|
+
|
|
8
|
+
export class ScrollTracker {
|
|
9
|
+
constructor(options?: {
|
|
10
|
+
start?: ScrollBound;
|
|
11
|
+
end?: ScrollBound;
|
|
12
|
+
clamp?: boolean;
|
|
13
|
+
velocity?: { windowMs?: number; maxSamples?: number; maxVelocity?: number };
|
|
14
|
+
initialOffset?: number;
|
|
15
|
+
});
|
|
16
|
+
readonly offset: MotionValue;
|
|
17
|
+
readonly progress: MotionValue;
|
|
18
|
+
readonly lastRange: { start: number; end: number; span: number };
|
|
19
|
+
setRange(start: ScrollBound, end: ScrollBound): this;
|
|
20
|
+
resolveRange(context?: ScrollRangeContext): { start: number; end: number; span: number };
|
|
21
|
+
sample(offset: number, time?: number, options?: { context?: ScrollRangeContext; resetVelocity?: boolean }): number;
|
|
22
|
+
reset(offset?: number, time?: number, context?: ScrollRangeContext): number;
|
|
23
|
+
getState(): { offset: number; velocity: number; progress: number; progressVelocity: number; start: number; end: number };
|
|
24
|
+
}
|
|
25
|
+
export function createScrollTracker(options?: ConstructorParameters<typeof ScrollTracker>[0]): ScrollTracker;
|
|
26
|
+
export function readScrollMetrics(target: Window | Element | object, axis?: 'x' | 'y'): ScrollMetrics;
|
|
27
|
+
|
|
28
|
+
export class ScrollObserver {
|
|
29
|
+
constructor(target: EventTarget & object, options?: {
|
|
30
|
+
tracker?: ScrollTracker;
|
|
31
|
+
axis?: 'x' | 'y';
|
|
32
|
+
start?: ScrollBound;
|
|
33
|
+
end?: ScrollBound;
|
|
34
|
+
clamp?: boolean;
|
|
35
|
+
requestFrame?: (callback: FrameRequestCallback) => number;
|
|
36
|
+
cancelFrame?: (id: number) => void;
|
|
37
|
+
passive?: boolean;
|
|
38
|
+
velocity?: { windowMs?: number; maxSamples?: number; maxVelocity?: number };
|
|
39
|
+
autoStart?: boolean;
|
|
40
|
+
});
|
|
41
|
+
readonly tracker: ScrollTracker;
|
|
42
|
+
readonly offset: MotionValue;
|
|
43
|
+
readonly progress: MotionValue;
|
|
44
|
+
metrics: ScrollMetrics;
|
|
45
|
+
schedule(): void;
|
|
46
|
+
update(time?: number, options?: { resetVelocity?: boolean }): ReturnType<ScrollTracker['getState']>;
|
|
47
|
+
dispose(): void;
|
|
48
|
+
}
|
|
49
|
+
export function observeScroll(target: EventTarget & object, options?: ConstructorParameters<typeof ScrollObserver>[1]): ScrollObserver;
|
|
50
|
+
|
|
51
|
+
export class ScrollTimelineLink {
|
|
52
|
+
constructor(player: TimelinePlayer, source: ScrollTracker | ScrollObserver | MotionValue, options?: { pause?: boolean });
|
|
53
|
+
dispose(): void;
|
|
54
|
+
}
|
|
55
|
+
export function bindScrollTimeline(player: TimelinePlayer, source: ScrollTracker | ScrollObserver | MotionValue, options?: { pause?: boolean }): ScrollTimelineLink;
|