@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,376 @@
|
|
|
1
|
+
import { defaultEngine } from '../core/default-engine.js';
|
|
2
|
+
import { inertia as inertiaSpec } from '../core/kinetics.js';
|
|
3
|
+
import { spring } from '../core/specs.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_WINDOW_MS = 120;
|
|
6
|
+
const DEFAULT_MAX_SAMPLES = 12;
|
|
7
|
+
|
|
8
|
+
function nowMs() {
|
|
9
|
+
return globalThis.performance?.now?.() ?? Date.now();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function finite(value, fallback = 0) {
|
|
13
|
+
return Number.isFinite(value) ? Number(value) : fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class VelocityTracker {
|
|
17
|
+
constructor({ windowMs = DEFAULT_WINDOW_MS, maxSamples = DEFAULT_MAX_SAMPLES, maxVelocity = 100000 } = {}) {
|
|
18
|
+
this.windowMs = Math.max(16, finite(windowMs, DEFAULT_WINDOW_MS));
|
|
19
|
+
this.maxSamples = Math.max(2, Math.floor(finite(maxSamples, DEFAULT_MAX_SAMPLES)));
|
|
20
|
+
this.maxVelocity = Math.max(1, finite(maxVelocity, 100000));
|
|
21
|
+
this.values = new Float64Array(this.maxSamples);
|
|
22
|
+
this.times = new Float64Array(this.maxSamples);
|
|
23
|
+
this.head = 0;
|
|
24
|
+
this.count = 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#index(logicalIndex) {
|
|
28
|
+
return (this.head + logicalIndex) % this.maxSamples;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
reset(value, time = nowMs()) {
|
|
32
|
+
this.head = 0;
|
|
33
|
+
this.count = 0;
|
|
34
|
+
this.add(value, time);
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
add(value, time = nowMs()) {
|
|
39
|
+
value = finite(value);
|
|
40
|
+
time = finite(time, nowMs());
|
|
41
|
+
if (this.count > 0) {
|
|
42
|
+
const lastIndex = this.#index(this.count - 1);
|
|
43
|
+
const lastTime = this.times[lastIndex];
|
|
44
|
+
if (time < lastTime) return this;
|
|
45
|
+
if (time === lastTime) {
|
|
46
|
+
this.values[lastIndex] = value;
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let index;
|
|
52
|
+
if (this.count < this.maxSamples) {
|
|
53
|
+
index = this.#index(this.count);
|
|
54
|
+
this.count += 1;
|
|
55
|
+
} else {
|
|
56
|
+
this.head = (this.head + 1) % this.maxSamples;
|
|
57
|
+
index = this.#index(this.count - 1);
|
|
58
|
+
}
|
|
59
|
+
this.values[index] = value;
|
|
60
|
+
this.times[index] = time;
|
|
61
|
+
|
|
62
|
+
const cutoff = time - this.windowMs;
|
|
63
|
+
while (this.count > 2 && this.times[this.head] < cutoff) {
|
|
64
|
+
this.head = (this.head + 1) % this.maxSamples;
|
|
65
|
+
this.count -= 1;
|
|
66
|
+
}
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get velocity() {
|
|
71
|
+
if (this.count < 2) return 0;
|
|
72
|
+
const latestIndex = this.#index(this.count - 1);
|
|
73
|
+
const latest = this.times[latestIndex];
|
|
74
|
+
const weightScale = Math.max(24, this.windowMs * 0.55);
|
|
75
|
+
let weightSum = 0;
|
|
76
|
+
let meanT = 0;
|
|
77
|
+
let meanX = 0;
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < this.count; i += 1) {
|
|
80
|
+
const index = this.#index(i);
|
|
81
|
+
const ageMs = latest - this.times[index];
|
|
82
|
+
const weight = Math.exp(-ageMs / weightScale);
|
|
83
|
+
const t = (this.times[index] - latest) / 1000;
|
|
84
|
+
weightSum += weight;
|
|
85
|
+
meanT += t * weight;
|
|
86
|
+
meanX += this.values[index] * weight;
|
|
87
|
+
}
|
|
88
|
+
if (weightSum <= 0) return 0;
|
|
89
|
+
meanT /= weightSum;
|
|
90
|
+
meanX /= weightSum;
|
|
91
|
+
|
|
92
|
+
let numerator = 0;
|
|
93
|
+
let denominator = 0;
|
|
94
|
+
for (let i = 0; i < this.count; i += 1) {
|
|
95
|
+
const index = this.#index(i);
|
|
96
|
+
const ageMs = latest - this.times[index];
|
|
97
|
+
const weight = Math.exp(-ageMs / weightScale);
|
|
98
|
+
const t = (this.times[index] - latest) / 1000 - meanT;
|
|
99
|
+
const x = this.values[index] - meanX;
|
|
100
|
+
numerator += weight * t * x;
|
|
101
|
+
denominator += weight * t * t;
|
|
102
|
+
}
|
|
103
|
+
if (denominator < 1e-9) return 0;
|
|
104
|
+
const velocity = numerator / denominator;
|
|
105
|
+
return Math.max(-this.maxVelocity, Math.min(this.maxVelocity, velocity));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function rubberBandDistance(distance, dimension = 320, constant = 0.55) {
|
|
110
|
+
const sign = Math.sign(distance);
|
|
111
|
+
const d = Math.abs(finite(distance));
|
|
112
|
+
const size = Math.max(1, Math.abs(finite(dimension, 320)));
|
|
113
|
+
const c = Math.max(0, finite(constant, 0.55));
|
|
114
|
+
if (d === 0 || c === 0) return 0;
|
|
115
|
+
return sign * ((d * c * size) / (size + c * d));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function constrainWithRubberBand(value, min = -Infinity, max = Infinity, {
|
|
119
|
+
enabled = true,
|
|
120
|
+
constant = 0.55,
|
|
121
|
+
dimension,
|
|
122
|
+
} = {}) {
|
|
123
|
+
if (min > max) throw new RangeError('min cannot be greater than max.');
|
|
124
|
+
if (value < min) {
|
|
125
|
+
if (!enabled) return min;
|
|
126
|
+
const size = dimension ?? (Number.isFinite(max - min) ? max - min : 320);
|
|
127
|
+
return min + rubberBandDistance(value - min, size, constant);
|
|
128
|
+
}
|
|
129
|
+
if (value > max) {
|
|
130
|
+
if (!enabled) return max;
|
|
131
|
+
const size = dimension ?? (Number.isFinite(max - min) ? max - min : 320);
|
|
132
|
+
return max + rubberBandDistance(value - max, size, constant);
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveBounds(bounds) {
|
|
138
|
+
const source = typeof bounds === 'function' ? bounds() : bounds;
|
|
139
|
+
return {
|
|
140
|
+
minX: Number.isFinite(source?.minX) ? source.minX : -Infinity,
|
|
141
|
+
maxX: Number.isFinite(source?.maxX) ? source.maxX : Infinity,
|
|
142
|
+
minY: Number.isFinite(source?.minY) ? source.minY : -Infinity,
|
|
143
|
+
maxY: Number.isFinite(source?.maxY) ? source.maxY : Infinity,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function nearestSnap(target, points) {
|
|
148
|
+
if (typeof points === 'function') {
|
|
149
|
+
const value = Number(points(target));
|
|
150
|
+
return Number.isFinite(value) ? value : target;
|
|
151
|
+
}
|
|
152
|
+
if (!Array.isArray(points) || points.length === 0) return target;
|
|
153
|
+
let best = target;
|
|
154
|
+
let bestDistance = Infinity;
|
|
155
|
+
for (const point of points) {
|
|
156
|
+
if (!Number.isFinite(point)) continue;
|
|
157
|
+
const distance = Math.abs(point - target);
|
|
158
|
+
if (distance < bestDistance) {
|
|
159
|
+
best = point;
|
|
160
|
+
bestDistance = distance;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return best;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function groupControls(controls) {
|
|
167
|
+
const active = controls.filter(Boolean);
|
|
168
|
+
return {
|
|
169
|
+
cancel() { for (const control of active) control.cancel(); },
|
|
170
|
+
finish() { for (const control of active) control.finish(); },
|
|
171
|
+
finished: Promise.all(active.map((control) => control.finished)),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export class DragController {
|
|
176
|
+
constructor({
|
|
177
|
+
x = null,
|
|
178
|
+
y = null,
|
|
179
|
+
axis = 'both',
|
|
180
|
+
engine = defaultEngine,
|
|
181
|
+
bounds = null,
|
|
182
|
+
momentum = true,
|
|
183
|
+
inertia = {},
|
|
184
|
+
rubberBand = true,
|
|
185
|
+
rubberBandConstant = 0.55,
|
|
186
|
+
rubberBandDimension,
|
|
187
|
+
directionLock = false,
|
|
188
|
+
directionLockThreshold = 8,
|
|
189
|
+
snapX = null,
|
|
190
|
+
snapY = null,
|
|
191
|
+
settle = {},
|
|
192
|
+
velocity = {},
|
|
193
|
+
onStart,
|
|
194
|
+
onMove,
|
|
195
|
+
onEnd,
|
|
196
|
+
} = {}) {
|
|
197
|
+
if (!['x', 'y', 'both'].includes(axis)) throw new TypeError("axis must be 'x', 'y', or 'both'.");
|
|
198
|
+
if ((axis === 'x' || axis === 'both') && !x?.set) throw new TypeError('DragController requires x MotionValue for the selected axis.');
|
|
199
|
+
if ((axis === 'y' || axis === 'both') && !y?.set) throw new TypeError('DragController requires y MotionValue for the selected axis.');
|
|
200
|
+
this.x = x;
|
|
201
|
+
this.y = y;
|
|
202
|
+
this.axis = axis;
|
|
203
|
+
this.engine = engine;
|
|
204
|
+
this.boundsSource = bounds;
|
|
205
|
+
this.momentum = momentum;
|
|
206
|
+
this.inertiaOptions = inertia;
|
|
207
|
+
this.rubberBand = rubberBand;
|
|
208
|
+
this.rubberBandConstant = rubberBandConstant;
|
|
209
|
+
this.rubberBandDimension = rubberBandDimension;
|
|
210
|
+
this.directionLock = directionLock;
|
|
211
|
+
this.directionLockThreshold = Math.max(0, directionLockThreshold);
|
|
212
|
+
this.snapX = snapX;
|
|
213
|
+
this.snapY = snapY;
|
|
214
|
+
this.settleOptions = settle;
|
|
215
|
+
this.onStart = onStart;
|
|
216
|
+
this.onMove = onMove;
|
|
217
|
+
this.onEnd = onEnd;
|
|
218
|
+
this.trackerX = new VelocityTracker(velocity);
|
|
219
|
+
this.trackerY = new VelocityTracker(velocity);
|
|
220
|
+
this.active = false;
|
|
221
|
+
this.lockedAxis = null;
|
|
222
|
+
this.startPoint = { x: 0, y: 0 };
|
|
223
|
+
this.startValue = { x: 0, y: 0 };
|
|
224
|
+
this.lastPoint = { x: 0, y: 0 };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#axisEnabled(axis) {
|
|
228
|
+
if (this.lockedAxis) return this.lockedAxis === axis;
|
|
229
|
+
return this.axis === 'both' || this.axis === axis;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#rubberOptions(axis, bounds) {
|
|
233
|
+
const enabled = this.rubberBand !== false;
|
|
234
|
+
const constant = typeof this.rubberBand === 'number' ? this.rubberBand : this.rubberBandConstant;
|
|
235
|
+
const dimension = typeof this.rubberBandDimension === 'object'
|
|
236
|
+
? this.rubberBandDimension?.[axis]
|
|
237
|
+
: this.rubberBandDimension;
|
|
238
|
+
return { enabled, constant, dimension };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
start(point, time = nowMs()) {
|
|
242
|
+
const px = finite(point?.x);
|
|
243
|
+
const py = finite(point?.y);
|
|
244
|
+
if (this.x) this.engine.stop(this.x, 'interrupted');
|
|
245
|
+
if (this.y) this.engine.stop(this.y, 'interrupted');
|
|
246
|
+
this.active = true;
|
|
247
|
+
this.lockedAxis = null;
|
|
248
|
+
this.startPoint = { x: px, y: py };
|
|
249
|
+
this.lastPoint = { x: px, y: py };
|
|
250
|
+
this.startValue = { x: this.x?.get?.() ?? 0, y: this.y?.get?.() ?? 0 };
|
|
251
|
+
this.trackerX.reset(px, time);
|
|
252
|
+
this.trackerY.reset(py, time);
|
|
253
|
+
const state = this.getState();
|
|
254
|
+
this.onStart?.(state);
|
|
255
|
+
return state;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
move(point, time = nowMs()) {
|
|
259
|
+
if (!this.active) return this.getState();
|
|
260
|
+
const px = finite(point?.x, this.lastPoint.x);
|
|
261
|
+
const py = finite(point?.y, this.lastPoint.y);
|
|
262
|
+
this.lastPoint = { x: px, y: py };
|
|
263
|
+
this.trackerX.add(px, time);
|
|
264
|
+
this.trackerY.add(py, time);
|
|
265
|
+
|
|
266
|
+
const dx = px - this.startPoint.x;
|
|
267
|
+
const dy = py - this.startPoint.y;
|
|
268
|
+
if (this.directionLock && !this.lockedAxis) {
|
|
269
|
+
if (Math.hypot(dx, dy) < this.directionLockThreshold) {
|
|
270
|
+
const state = this.getState();
|
|
271
|
+
this.onMove?.(state);
|
|
272
|
+
return state;
|
|
273
|
+
}
|
|
274
|
+
this.lockedAxis = this.axis === 'both'
|
|
275
|
+
? (Math.abs(dx) >= Math.abs(dy) ? 'x' : 'y')
|
|
276
|
+
: this.axis;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const bounds = resolveBounds(this.boundsSource);
|
|
280
|
+
if (this.x && this.#axisEnabled('x')) {
|
|
281
|
+
const raw = this.startValue.x + dx;
|
|
282
|
+
const next = constrainWithRubberBand(raw, bounds.minX, bounds.maxX, this.#rubberOptions('x', bounds));
|
|
283
|
+
this.x.set(next, this.trackerX.velocity);
|
|
284
|
+
}
|
|
285
|
+
if (this.y && this.#axisEnabled('y')) {
|
|
286
|
+
const raw = this.startValue.y + dy;
|
|
287
|
+
const next = constrainWithRubberBand(raw, bounds.minY, bounds.maxY, this.#rubberOptions('y', bounds));
|
|
288
|
+
this.y.set(next, this.trackerY.velocity);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const state = this.getState();
|
|
292
|
+
this.onMove?.(state);
|
|
293
|
+
return state;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#releaseAxis(axis, motion, velocity, min, max, snap) {
|
|
297
|
+
if (!motion || !this.#axisEnabled(axis)) return null;
|
|
298
|
+
const current = motion.get();
|
|
299
|
+
const outside = current < min || current > max;
|
|
300
|
+
if (!this.momentum && !outside) {
|
|
301
|
+
motion.set(current, 0);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!this.momentum && outside) {
|
|
306
|
+
const target = Math.max(min, Math.min(max, current));
|
|
307
|
+
return this.engine.animate(motion, target, spring({
|
|
308
|
+
response: this.settleOptions.response ?? 0.28,
|
|
309
|
+
dampingRatio: this.settleOptions.dampingRatio ?? 0.82,
|
|
310
|
+
initialVelocity: velocity,
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const userModify = this.inertiaOptions.modifyTarget;
|
|
315
|
+
const modifyTarget = (target) => {
|
|
316
|
+
const modified = typeof userModify === 'function' ? userModify(target, axis) : target;
|
|
317
|
+
return nearestSnap(modified, snap);
|
|
318
|
+
};
|
|
319
|
+
return this.engine.animateVelocity(motion, inertiaSpec({
|
|
320
|
+
...this.inertiaOptions,
|
|
321
|
+
velocity,
|
|
322
|
+
min,
|
|
323
|
+
max,
|
|
324
|
+
modifyTarget,
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
end(time = nowMs()) {
|
|
329
|
+
if (!this.active) return { ...this.getState(), controls: groupControls([]) };
|
|
330
|
+
this.trackerX.add(this.lastPoint.x, time);
|
|
331
|
+
this.trackerY.add(this.lastPoint.y, time);
|
|
332
|
+
this.active = false;
|
|
333
|
+
const velocity = { x: this.trackerX.velocity, y: this.trackerY.velocity };
|
|
334
|
+
const bounds = resolveBounds(this.boundsSource);
|
|
335
|
+
const controls = [
|
|
336
|
+
this.#releaseAxis('x', this.x, velocity.x, bounds.minX, bounds.maxX, this.snapX),
|
|
337
|
+
this.#releaseAxis('y', this.y, velocity.y, bounds.minY, bounds.maxY, this.snapY),
|
|
338
|
+
];
|
|
339
|
+
const result = { ...this.getState(), velocity, controls: groupControls(controls) };
|
|
340
|
+
this.onEnd?.(result);
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
cancel({ settle = true } = {}) {
|
|
345
|
+
if (!this.active) return this.getState();
|
|
346
|
+
this.active = false;
|
|
347
|
+
if (settle) {
|
|
348
|
+
const bounds = resolveBounds(this.boundsSource);
|
|
349
|
+
for (const [axis, motion, min, max] of [
|
|
350
|
+
['x', this.x, bounds.minX, bounds.maxX],
|
|
351
|
+
['y', this.y, bounds.minY, bounds.maxY],
|
|
352
|
+
]) {
|
|
353
|
+
if (!motion || !this.#axisEnabled(axis)) continue;
|
|
354
|
+
const target = Math.max(min, Math.min(max, motion.get()));
|
|
355
|
+
if (target !== motion.get()) this.engine.animate(motion, target, spring({ response: 0.25, dampingRatio: 0.9 }));
|
|
356
|
+
else motion.set(motion.get(), 0);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return this.getState();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
getState() {
|
|
363
|
+
return {
|
|
364
|
+
active: this.active,
|
|
365
|
+
axis: this.axis,
|
|
366
|
+
lockedAxis: this.lockedAxis,
|
|
367
|
+
point: { ...this.lastPoint },
|
|
368
|
+
value: { x: this.x?.get?.() ?? null, y: this.y?.get?.() ?? null },
|
|
369
|
+
velocity: { x: this.trackerX.velocity, y: this.trackerY.velocity },
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function createDragController(options) {
|
|
375
|
+
return new DragController(options);
|
|
376
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export { MotionValue, motionValue } from './core/motion-value.js';
|
|
2
|
+
export { MotionEngine } from './core/engine.js';
|
|
3
|
+
export { FrameBudgetGovernor } from './core/frame-budget.js';
|
|
4
|
+
export { compileEasing, evaluateCompiledEasing, derivativeCompiledEasing } from './core/easing.js';
|
|
5
|
+
export { AnimationControls } from './core/controls.js';
|
|
6
|
+
export { animate, animateVelocity, animateDecay, animateInertia, defaultEngine } from './core/default-engine.js';
|
|
7
|
+
export { compileMotionPlan, resolveMotionPlan, isMotionExecutionPlan } from './core/planner.js';
|
|
8
|
+
export {
|
|
9
|
+
spring,
|
|
10
|
+
timing,
|
|
11
|
+
cubicBezier,
|
|
12
|
+
curves,
|
|
13
|
+
smooth,
|
|
14
|
+
snappy,
|
|
15
|
+
bouncy,
|
|
16
|
+
gentle,
|
|
17
|
+
interactive,
|
|
18
|
+
resolveMotionSpec,
|
|
19
|
+
} from './core/specs.js';
|
|
20
|
+
export { delay, parallel, sequence } from './core/composition.js';
|
|
21
|
+
export {
|
|
22
|
+
decay,
|
|
23
|
+
inertia,
|
|
24
|
+
projectDecayTarget,
|
|
25
|
+
stepDecay,
|
|
26
|
+
stepDampedSpring,
|
|
27
|
+
} from './core/kinetics.js';
|
|
28
|
+
export {
|
|
29
|
+
animateInterpolated,
|
|
30
|
+
createInterpolator,
|
|
31
|
+
interpolateNumber,
|
|
32
|
+
interpolateColor,
|
|
33
|
+
mixColor,
|
|
34
|
+
parseColor,
|
|
35
|
+
formatColor,
|
|
36
|
+
interpolateTransform,
|
|
37
|
+
mixTransform,
|
|
38
|
+
parseTransform,
|
|
39
|
+
formatTransform,
|
|
40
|
+
} from './interpolate/index.js';
|
|
41
|
+
|
|
42
|
+
export { parsePath, normalizePathPair, PathMorpher, createPathMorpher, interpolatePath } from './path/index.js';
|
|
43
|
+
export { materials, resolveMaterial, mixMaterial, interpolateMaterial, materialToCss } from './material/index.js';
|
|
44
|
+
|
|
45
|
+
export { VelocityTracker, DragController, createDragController, rubberBandDistance, constrainWithRubberBand } from './gesture/index.js';
|
|
46
|
+
|
|
47
|
+
export { Timeline, TimelinePlayer, PhaseTimeline, TimelineScrubber, timeline, createPhaseTimeline, createTimelineScrubber, stagger } from './timeline/index.js';
|
|
48
|
+
|
|
49
|
+
export { StateTransitionGraph, TransitionController, PresenceController, createStateTransitionGraph, createTransition, createPresence } from './transition/index.js';
|
|
50
|
+
|
|
51
|
+
export { ScrollTracker, ScrollObserver, ScrollTimelineLink, createScrollTracker, observeScroll, readScrollMetrics, bindScrollTimeline } from './scroll/index.js';
|
|
52
|
+
export { ConstraintNode, ConstraintGraph, createConstraintGraph } from './constraints/index.js';
|
|
53
|
+
export { WebGPUSpringBatch } from './webgpu/index.js';
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
const NAMED = Object.freeze({
|
|
2
|
+
transparent: [0, 0, 0, 0],
|
|
3
|
+
black: [0, 0, 0, 1],
|
|
4
|
+
white: [1, 1, 1, 1],
|
|
5
|
+
red: [1, 0, 0, 1],
|
|
6
|
+
green: [0, 0.5019607843, 0, 1],
|
|
7
|
+
blue: [0, 0, 1, 1],
|
|
8
|
+
yellow: [1, 1, 0, 1],
|
|
9
|
+
cyan: [0, 1, 1, 1],
|
|
10
|
+
aqua: [0, 1, 1, 1],
|
|
11
|
+
magenta: [1, 0, 1, 1],
|
|
12
|
+
fuchsia: [1, 0, 1, 1],
|
|
13
|
+
gray: [0.5019607843, 0.5019607843, 0.5019607843, 1],
|
|
14
|
+
grey: [0.5019607843, 0.5019607843, 0.5019607843, 1],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const clamp01 = (value) => Math.min(1, Math.max(0, value));
|
|
18
|
+
const lerp = (a, b, t) => a + (b - a) * t;
|
|
19
|
+
|
|
20
|
+
function parseRgbChannel(token) {
|
|
21
|
+
const value = token.trim();
|
|
22
|
+
if (value.endsWith('%')) return clamp01(Number.parseFloat(value) / 100);
|
|
23
|
+
return clamp01(Number.parseFloat(value) / 255);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseAlpha(token = '1') {
|
|
27
|
+
const value = token.trim();
|
|
28
|
+
if (value.endsWith('%')) return clamp01(Number.parseFloat(value) / 100);
|
|
29
|
+
return clamp01(Number.parseFloat(value));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseAngle(token) {
|
|
33
|
+
const value = token.trim().toLowerCase();
|
|
34
|
+
if (value.endsWith('turn')) return Number.parseFloat(value) * 360;
|
|
35
|
+
if (value.endsWith('rad')) return Number.parseFloat(value) * 180 / Math.PI;
|
|
36
|
+
if (value.endsWith('grad')) return Number.parseFloat(value) * 0.9;
|
|
37
|
+
return Number.parseFloat(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hslToRgb(h, s, l) {
|
|
41
|
+
const hue = ((h % 360) + 360) % 360 / 360;
|
|
42
|
+
if (s === 0) return [l, l, l];
|
|
43
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
44
|
+
const p = 2 * l - q;
|
|
45
|
+
const channel = (offset) => {
|
|
46
|
+
let t = hue + offset;
|
|
47
|
+
if (t < 0) t += 1;
|
|
48
|
+
if (t > 1) t -= 1;
|
|
49
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
50
|
+
if (t < 1 / 2) return q;
|
|
51
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
52
|
+
return p;
|
|
53
|
+
};
|
|
54
|
+
return [channel(1 / 3), channel(0), channel(-1 / 3)];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function splitFunctionalBody(body) {
|
|
58
|
+
const slash = body.split('/');
|
|
59
|
+
const main = slash[0].trim();
|
|
60
|
+
const alpha = slash[1]?.trim();
|
|
61
|
+
const parts = main.includes(',')
|
|
62
|
+
? main.split(',').map((part) => part.trim()).filter(Boolean)
|
|
63
|
+
: main.split(/\s+/).filter(Boolean);
|
|
64
|
+
if (parts.length === 4 && alpha == null) return { parts: parts.slice(0, 3), alpha: parts[3] };
|
|
65
|
+
return { parts, alpha };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function parseColor(input) {
|
|
69
|
+
if (input && typeof input === 'object') {
|
|
70
|
+
if (Array.isArray(input) && input.length >= 3) {
|
|
71
|
+
return { r: clamp01(Number(input[0])), g: clamp01(Number(input[1])), b: clamp01(Number(input[2])), a: clamp01(Number(input[3] ?? 1)) };
|
|
72
|
+
}
|
|
73
|
+
if ('r' in input && 'g' in input && 'b' in input) {
|
|
74
|
+
return { r: clamp01(Number(input.r)), g: clamp01(Number(input.g)), b: clamp01(Number(input.b)), a: clamp01(Number(input.a ?? 1)) };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof input !== 'string') throw new TypeError('Color must be a CSS color string or {r,g,b,a}.');
|
|
79
|
+
const text = input.trim().toLowerCase();
|
|
80
|
+
if (NAMED[text]) {
|
|
81
|
+
const [r, g, b, a] = NAMED[text];
|
|
82
|
+
return { r, g, b, a };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (text.startsWith('#')) {
|
|
86
|
+
const hex = text.slice(1);
|
|
87
|
+
if (![3, 4, 6, 8].includes(hex.length) || !/^[0-9a-f]+$/i.test(hex)) throw new TypeError(`Unsupported hex color: ${input}`);
|
|
88
|
+
const expanded = hex.length <= 4 ? [...hex].map((char) => char + char).join('') : hex;
|
|
89
|
+
const value = Number.parseInt(expanded, 16);
|
|
90
|
+
if (expanded.length === 6) {
|
|
91
|
+
return { r: ((value >> 16) & 255) / 255, g: ((value >> 8) & 255) / 255, b: (value & 255) / 255, a: 1 };
|
|
92
|
+
}
|
|
93
|
+
return { r: ((value >> 24) & 255) / 255, g: ((value >> 16) & 255) / 255, b: ((value >> 8) & 255) / 255, a: (value & 255) / 255 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let match = text.match(/^rgba?\((.*)\)$/);
|
|
97
|
+
if (match) {
|
|
98
|
+
const { parts, alpha } = splitFunctionalBody(match[1]);
|
|
99
|
+
if (parts.length !== 3) throw new TypeError(`Invalid rgb() color: ${input}`);
|
|
100
|
+
return { r: parseRgbChannel(parts[0]), g: parseRgbChannel(parts[1]), b: parseRgbChannel(parts[2]), a: parseAlpha(alpha) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
match = text.match(/^hsla?\((.*)\)$/);
|
|
104
|
+
if (match) {
|
|
105
|
+
const { parts, alpha } = splitFunctionalBody(match[1]);
|
|
106
|
+
if (parts.length !== 3 || !parts[1].endsWith('%') || !parts[2].endsWith('%')) throw new TypeError(`Invalid hsl() color: ${input}`);
|
|
107
|
+
const [r, g, b] = hslToRgb(parseAngle(parts[0]), clamp01(Number.parseFloat(parts[1]) / 100), clamp01(Number.parseFloat(parts[2]) / 100));
|
|
108
|
+
return { r, g, b, a: parseAlpha(alpha) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new TypeError(`Unsupported color syntax: ${input}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function formatColor(color) {
|
|
115
|
+
const r = Math.round(clamp01(color.r) * 255);
|
|
116
|
+
const g = Math.round(clamp01(color.g) * 255);
|
|
117
|
+
const b = Math.round(clamp01(color.b) * 255);
|
|
118
|
+
const a = Math.round(clamp01(color.a ?? 1) * 10000) / 10000;
|
|
119
|
+
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function srgbToLinear(value) {
|
|
123
|
+
const c = clamp01(value);
|
|
124
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function linearToSrgb(value) {
|
|
128
|
+
const c = Math.max(0, value);
|
|
129
|
+
return clamp01(c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function linearRgbToOklab({ r, g, b }) {
|
|
133
|
+
const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
|
134
|
+
const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
|
135
|
+
const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
|
136
|
+
const l3 = Math.cbrt(l);
|
|
137
|
+
const m3 = Math.cbrt(m);
|
|
138
|
+
const s3 = Math.cbrt(s);
|
|
139
|
+
return {
|
|
140
|
+
l: 0.2104542553 * l3 + 0.793617785 * m3 - 0.0040720468 * s3,
|
|
141
|
+
a: 1.9779984951 * l3 - 2.428592205 * m3 + 0.4505937099 * s3,
|
|
142
|
+
b: 0.0259040371 * l3 + 0.7827717662 * m3 - 0.808675766 * s3,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function oklabToLinearRgb({ l, a, b }) {
|
|
147
|
+
const lp = l + 0.3963377774 * a + 0.2158037573 * b;
|
|
148
|
+
const mp = l - 0.1055613458 * a - 0.0638541728 * b;
|
|
149
|
+
const sp = l - 0.0894841775 * a - 1.291485548 * b;
|
|
150
|
+
const l3 = lp ** 3;
|
|
151
|
+
const m3 = mp ** 3;
|
|
152
|
+
const s3 = sp ** 3;
|
|
153
|
+
return {
|
|
154
|
+
r: 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3,
|
|
155
|
+
g: -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3,
|
|
156
|
+
b: -0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toLinear(color) {
|
|
161
|
+
return { r: srgbToLinear(color.r), g: srgbToLinear(color.g), b: srgbToLinear(color.b) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function fromLinear(color, alpha) {
|
|
165
|
+
return { r: linearToSrgb(color.r), g: linearToSrgb(color.g), b: linearToSrgb(color.b), a: alpha };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function shortestHue(from, to) {
|
|
169
|
+
let delta = (to - from) % 360;
|
|
170
|
+
if (delta > 180) delta -= 360;
|
|
171
|
+
if (delta < -180) delta += 360;
|
|
172
|
+
return delta;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function mixColor(fromInput, toInput, t, { space = 'oklab' } = {}) {
|
|
176
|
+
let from = parseColor(fromInput);
|
|
177
|
+
let to = parseColor(toInput);
|
|
178
|
+
const progress = clamp01(t);
|
|
179
|
+
const alpha = lerp(from.a, to.a, progress);
|
|
180
|
+
|
|
181
|
+
// CSS transparent is transparent black. Borrow the visible endpoint's chroma
|
|
182
|
+
// when one side is fully transparent so fades do not pass through a dark halo.
|
|
183
|
+
if (from.a <= 1e-8 && to.a > 1e-8) from = { ...from, r: to.r, g: to.g, b: to.b };
|
|
184
|
+
if (to.a <= 1e-8 && from.a > 1e-8) to = { ...to, r: from.r, g: from.g, b: from.b };
|
|
185
|
+
|
|
186
|
+
if (space === 'srgb') {
|
|
187
|
+
return { r: lerp(from.r, to.r, progress), g: lerp(from.g, to.g, progress), b: lerp(from.b, to.b, progress), a: alpha };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const fromLinearColor = toLinear(from);
|
|
191
|
+
const toLinearColor = toLinear(to);
|
|
192
|
+
if (space === 'linear-srgb') {
|
|
193
|
+
return fromLinear({
|
|
194
|
+
r: lerp(fromLinearColor.r, toLinearColor.r, progress),
|
|
195
|
+
g: lerp(fromLinearColor.g, toLinearColor.g, progress),
|
|
196
|
+
b: lerp(fromLinearColor.b, toLinearColor.b, progress),
|
|
197
|
+
}, alpha);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const a = linearRgbToOklab(fromLinearColor);
|
|
201
|
+
const b = linearRgbToOklab(toLinearColor);
|
|
202
|
+
let lab;
|
|
203
|
+
if (space === 'oklch') {
|
|
204
|
+
const c1 = Math.hypot(a.a, a.b);
|
|
205
|
+
const c2 = Math.hypot(b.a, b.b);
|
|
206
|
+
const h1 = Math.atan2(a.b, a.a) * 180 / Math.PI;
|
|
207
|
+
const h2 = Math.atan2(b.b, b.a) * 180 / Math.PI;
|
|
208
|
+
const c = lerp(c1, c2, progress);
|
|
209
|
+
const h = h1 + shortestHue(h1, h2) * progress;
|
|
210
|
+
lab = { l: lerp(a.l, b.l, progress), a: c * Math.cos(h * Math.PI / 180), b: c * Math.sin(h * Math.PI / 180) };
|
|
211
|
+
} else if (space === 'oklab') {
|
|
212
|
+
lab = { l: lerp(a.l, b.l, progress), a: lerp(a.a, b.a, progress), b: lerp(a.b, b.b, progress) };
|
|
213
|
+
} else {
|
|
214
|
+
throw new TypeError(`Unknown color interpolation space: ${space}`);
|
|
215
|
+
}
|
|
216
|
+
return fromLinear(oklabToLinearRgb(lab), alpha);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function interpolateColor(from, to, options) {
|
|
220
|
+
const start = parseColor(from);
|
|
221
|
+
const end = parseColor(to);
|
|
222
|
+
return (progress) => formatColor(mixColor(start, end, progress, options));
|
|
223
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createInterpolator,
|
|
3
|
+
interpolateNumber,
|
|
4
|
+
interpolateColor,
|
|
5
|
+
mixColor,
|
|
6
|
+
parseColor,
|
|
7
|
+
formatColor,
|
|
8
|
+
interpolateTransform,
|
|
9
|
+
mixTransform,
|
|
10
|
+
parseTransform,
|
|
11
|
+
formatTransform,
|
|
12
|
+
} from '../../index.js';
|
|
13
|
+
export type { InterpolatorOptions } from '../../index.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { interpolateColor } from './color.js';
|
|
2
|
+
import { interpolateTransform } from './transform.js';
|
|
3
|
+
|
|
4
|
+
export * from './color.js';
|
|
5
|
+
export * from './transform.js';
|
|
6
|
+
|
|
7
|
+
export function interpolateNumber(from, to) {
|
|
8
|
+
const a = Number(from);
|
|
9
|
+
const b = Number(to);
|
|
10
|
+
if (!Number.isFinite(a) || !Number.isFinite(b)) throw new TypeError('Numeric interpolation requires finite numbers.');
|
|
11
|
+
return (progress) => a + (b - a) * progress;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Lean CSS-oriented interpolator. Unlike the broad interpolate entry point,
|
|
16
|
+
* this module intentionally does not statically import SVG path or material
|
|
17
|
+
* interpolation, so DOM-only consumers do not pay for those feature families.
|
|
18
|
+
*/
|
|
19
|
+
export function createInterpolator(from, to, options = {}) {
|
|
20
|
+
if (typeof options.interpolate === 'function') return (progress) => options.interpolate(from, to, progress);
|
|
21
|
+
if (typeof from === 'number' && typeof to === 'number') return interpolateNumber(from, to);
|
|
22
|
+
if (options.type === 'transform' || (typeof from === 'object' && from !== null && typeof to === 'object' && to !== null && ('x' in from || 'scale' in from || 'rotate' in from))) {
|
|
23
|
+
return interpolateTransform(from, to, options.transform);
|
|
24
|
+
}
|
|
25
|
+
if (options.type === 'color') return interpolateColor(from, to, options.color);
|
|
26
|
+
if (options.type === 'path' || options.type === 'material') {
|
|
27
|
+
throw new TypeError(`The CSS interpolator does not include ${options.type} interpolation. Import from @vune-ui/animation/interpolate for that feature.`);
|
|
28
|
+
}
|
|
29
|
+
if (typeof from === 'string' && typeof to === 'string') {
|
|
30
|
+
try { return interpolateColor(from, to, options.color); } catch {}
|
|
31
|
+
try { return interpolateTransform(from, to, options.transform); } catch {}
|
|
32
|
+
}
|
|
33
|
+
throw new TypeError('No CSS interpolator is available for these values. Pass options.type or options.interpolate.');
|
|
34
|
+
}
|