@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
package/README.md
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
# @vune-ui/animation
|
|
2
|
+
|
|
3
|
+
A renderer-agnostic motion runtime focused on interruptibility, low main-thread cost, smooth retargeting, and scalable execution. Vune can consume it through an adapter, but the core has no Vune, DOM, React, or Vue dependency.
|
|
4
|
+
|
|
5
|
+
## Current state (0.2.2)
|
|
6
|
+
|
|
7
|
+
### Numeric runtime
|
|
8
|
+
|
|
9
|
+
- Numeric `MotionValue` with velocity tracking, detailed subscriptions, and allocation-free `subscribeValue()` listeners for hot render bindings.
|
|
10
|
+
- Interruptible springs. Retargeting preserves current position and velocity.
|
|
11
|
+
- Response/damping springs and low-level mass/stiffness/damping springs.
|
|
12
|
+
- Distance-adaptive `smooth`, `snappy`, `bouncy`, `gentle`, and `interactive` profiles.
|
|
13
|
+
- Cubic-bezier timing animations.
|
|
14
|
+
- Analytic exponential decay and bounded inertia for release motion.
|
|
15
|
+
- Exact damped-spring settling for low-count interaction physics, avoiding frame-rate-dependent substeps.
|
|
16
|
+
- One scheduler for all active motion.
|
|
17
|
+
- Dense spring storage with swap-remove.
|
|
18
|
+
- Bounded solver substeps for bad frame-time spikes.
|
|
19
|
+
- Reduced-motion support.
|
|
20
|
+
|
|
21
|
+
### Adaptive execution backends
|
|
22
|
+
|
|
23
|
+
- Zero-startup-cost JS spring kernel.
|
|
24
|
+
- Lazy WebAssembly promotion for larger spring batches.
|
|
25
|
+
- WebAssembly SIMD (`f32x4`) with scalar WASM fallback.
|
|
26
|
+
- State remains in WASM linear memory after promotion; there is no per-frame full-buffer copy.
|
|
27
|
+
- WASM memory grows on demand instead of imposing a fixed allocator ceiling.
|
|
28
|
+
- Optional Worker + `SharedArrayBuffer` + shared `WebAssembly.Memory` path.
|
|
29
|
+
- `worker: 'auto'` is now the default. It only prepares a Worker when workload crosses the threshold and gracefully stays on JS/WASM when shared memory is unavailable.
|
|
30
|
+
- The normal auto scheduler can switch itself to asynchronous Worker frames after the shared backend is ready.
|
|
31
|
+
- When a Worker frame is running, retarget/cancel/replace mutations are buffered to the frame boundary so the main thread never races the Worker over an in-flight shared slot.
|
|
32
|
+
- Overlapping `stepAsync()` calls are serialized at the engine level as well as the backend level.
|
|
33
|
+
- Supported environments use `Atomics.waitAsync()` for completion, eliminating the per-frame Worker `done` message. A message fallback remains for environments without it.
|
|
34
|
+
- Worker failure restores authoritative `MotionValue` state before falling back to the local shared-WASM path.
|
|
35
|
+
- Optional WebGPU Compute promotion (`gpu: 'auto'`) runs dense spring batches in a compute shader with asynchronous readback and automatically falls back when WebGPU is unavailable.
|
|
36
|
+
|
|
37
|
+
### Frame-budget governor
|
|
38
|
+
|
|
39
|
+
`FrameBudgetGovernor` tracks an exponential moving average of main-thread motion cost. Under sustained pressure it can lower the promotion thresholds, moving medium workloads to WASM or the Worker earlier without changing public motion semantics.
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const engine = new MotionEngine({
|
|
43
|
+
frameBudgetMs: 7,
|
|
44
|
+
adaptiveBackends: true,
|
|
45
|
+
wasmThreshold: 256,
|
|
46
|
+
workerThreshold: 4096,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
console.log(engine.getBackendPlan());
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The governor does not skip frames or silently lower animation quality. It only changes where the same solver work runs.
|
|
53
|
+
|
|
54
|
+
### Direct manipulation / gestures
|
|
55
|
+
|
|
56
|
+
The gesture package is renderer-agnostic. It turns pointer-like coordinates into `MotionValue` updates, release velocity, inertia, snapping, rubber-band bounds, and spring settling without knowing about DOM elements.
|
|
57
|
+
|
|
58
|
+
- `VelocityTracker` uses a short recency-weighted regression window instead of a single event delta.
|
|
59
|
+
- Tracking storage is a fixed-size typed-array ring buffer, so steady-state input sampling allocates no sample objects.
|
|
60
|
+
- `DragController` supports x/y/both axes, direction locking, dynamic bounds, momentum, snapping, and rubber-band resistance.
|
|
61
|
+
- Release motion preserves measured velocity and changes from decay to an exact damped spring only when a bound is crossed.
|
|
62
|
+
- The DOM adapter consumes `PointerEvent.getCoalescedEvents()` when available, retaining high-frequency pen/touch samples that browsers may merge into one delivered event.
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { motionValue } from '@vune-ui/animation';
|
|
66
|
+
import { createDragController } from '@vune-ui/animation/gesture';
|
|
67
|
+
import { bindMotionStyles, bindPointerDrag } from '@vune-ui/animation/dom';
|
|
68
|
+
|
|
69
|
+
const x = motionValue(0);
|
|
70
|
+
const drag = createDragController({
|
|
71
|
+
x,
|
|
72
|
+
axis: 'x',
|
|
73
|
+
bounds: { minX: 0, maxX: 480 },
|
|
74
|
+
snapX: [0, 160, 320, 480],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
bindMotionStyles(card, { x });
|
|
78
|
+
const unbind = bindPointerDrag(card, drag);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The numeric core also exposes release motion directly:
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
import { animateInertia } from '@vune-ui/animation';
|
|
85
|
+
|
|
86
|
+
animateInertia(x, {
|
|
87
|
+
velocity: 1450, // units per second
|
|
88
|
+
min: 0,
|
|
89
|
+
max: 480,
|
|
90
|
+
timeConstant: 0.325,
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Keyframes, phases, and timeline graph
|
|
95
|
+
|
|
96
|
+
`Timeline` is a precompiled time-axis runtime rather than a Promise chain. Numeric keyframe times and values live in typed arrays, cubic-bezier curves are compiled once into shared lookup tables, and steady-state scalar sampling creates no timeline frame objects.
|
|
97
|
+
|
|
98
|
+
- Multiple tracks on one timeline.
|
|
99
|
+
- Nested timeline clips with offsets, playback speed, and fill policy.
|
|
100
|
+
- `play()`, `pause()`, `seek()`, `scrub()`, `reverse()`, playback-rate changes, finite/infinite repeats, and alternate direction.
|
|
101
|
+
- Exact numeric velocity propagation from keyframe tracks, so interrupting a timeline with a spring or inertia animation keeps motion continuity.
|
|
102
|
+
- Bidirectional ownership arbitration: a timeline interrupts an older spring/timeline on the same `MotionValue`, and starting an engine animation interrupts the owning timeline.
|
|
103
|
+
- Large frame gaps use wall-clock driver time while numerical spring integration keeps its safety clamp. Timelines therefore catch up after a suspended tab instead of playing in slow motion.
|
|
104
|
+
- `PhaseTimeline` compiles named multi-property choreography with transition durations and holds.
|
|
105
|
+
- `stagger()` supports first/last/center/index origins and optional easing.
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
import {
|
|
109
|
+
MotionEngine,
|
|
110
|
+
curves,
|
|
111
|
+
motionValue,
|
|
112
|
+
timeline,
|
|
113
|
+
} from '@vune-ui/animation';
|
|
114
|
+
|
|
115
|
+
const engine = new MotionEngine();
|
|
116
|
+
const x = motionValue(0);
|
|
117
|
+
const scale = motionValue(1);
|
|
118
|
+
|
|
119
|
+
const intro = timeline()
|
|
120
|
+
.fromTo(x, 0, 280, {
|
|
121
|
+
at: 0,
|
|
122
|
+
duration: 0.42,
|
|
123
|
+
easing: curves.smooth,
|
|
124
|
+
})
|
|
125
|
+
.fromTo(scale, 0.94, 1, {
|
|
126
|
+
at: 0.08,
|
|
127
|
+
duration: 0.28,
|
|
128
|
+
easing: curves.easeOut,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const player = intro.player({
|
|
132
|
+
engine,
|
|
133
|
+
iterations: 2,
|
|
134
|
+
direction: 'alternate',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
player.play();
|
|
138
|
+
player.scrub(0.5); // deterministic seek, useful for interactive scrubbing
|
|
139
|
+
player.reverse();
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Structured values can use the same timeline through the existing interpolation layer:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
const colorTimeline = timeline().fromTo(
|
|
146
|
+
(color) => panel.style.backgroundColor = color,
|
|
147
|
+
'#ff2d55',
|
|
148
|
+
'#5ac8fa',
|
|
149
|
+
{ duration: 0.4, type: 'color', color: { space: 'oklab' } },
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Named phases are convenient for reusable UI choreography:
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
import { createPhaseTimeline } from '@vune-ui/animation/timeline';
|
|
157
|
+
|
|
158
|
+
const press = createPhaseTimeline({ scale, opacity }, [
|
|
159
|
+
{ name: 'idle', values: { scale: 1, opacity: 1 } },
|
|
160
|
+
{ name: 'pressed', duration: 0.10, hold: 0.04, values: { scale: 0.96, opacity: 0.88 } },
|
|
161
|
+
{ name: 'release', duration: 0.18, values: { scale: 1, opacity: 1 } },
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
press.player({ engine }).play();
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
Timeline playback can also be driven by a numeric interaction domain instead of a clock:
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
import { createTimelineScrubber } from '@vune-ui/animation/timeline';
|
|
175
|
+
|
|
176
|
+
// 0..400 can be drag pixels while the timeline remains normalized internally.
|
|
177
|
+
const scrubber = createTimelineScrubber(player, {
|
|
178
|
+
min: 0,
|
|
179
|
+
max: 400,
|
|
180
|
+
snapPoints: [0, 400],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
scrubber.set(160, 900);
|
|
184
|
+
scrubber.release(); // bounded inertia -> nearest snap point
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The scrubber only depends on a `MotionValue`; a `DragController`, gamepad axis, scroll position, or custom input source can all drive it.
|
|
188
|
+
|
|
189
|
+
### State transitions and presence
|
|
190
|
+
|
|
191
|
+
The transition package adds named UI state motion without making the numeric core aware of components or renderers. Numeric `MotionValue` bindings are animated directly, so every property retains its own velocity during interruption. Structured bindings (colors, transforms, materials, paths, or custom interpolators) share one precompiled scalar progress animation per state change.
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
import { motionValue, spring } from '@vune-ui/animation';
|
|
195
|
+
import { createStateTransitionGraph } from '@vune-ui/animation/transition';
|
|
196
|
+
|
|
197
|
+
const x = motionValue(0);
|
|
198
|
+
const opacity = motionValue(0);
|
|
199
|
+
|
|
200
|
+
const panel = createStateTransitionGraph({ x, opacity }, {
|
|
201
|
+
hidden: { x: 24, opacity: 0 },
|
|
202
|
+
visible: { x: 0, opacity: 1 },
|
|
203
|
+
focused: { x: 0, opacity: 0.92 },
|
|
204
|
+
}, {
|
|
205
|
+
initial: 'hidden',
|
|
206
|
+
routes: {
|
|
207
|
+
'hidden->visible': spring({ response: 0.32, dampingRatio: 0.82 }),
|
|
208
|
+
'*->hidden': spring({ response: 0.24, dampingRatio: 0.9 }),
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
panel.to('visible');
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`TransitionController` is the two-state enter/exit convenience layer. Reversing an enter while it is still moving retargets the same numeric `MotionValue`s instead of restarting them from rest.
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
import { createPresence, createTransition } from '@vune-ui/animation/transition';
|
|
219
|
+
|
|
220
|
+
const transition = createTransition([
|
|
221
|
+
{ key: 'opacity', target: opacity, from: 0, to: 1 },
|
|
222
|
+
{ key: 'scale', target: scale, from: 0.96, to: 1 },
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
const presence = createPresence(transition);
|
|
226
|
+
presence.enter();
|
|
227
|
+
|
|
228
|
+
// `rendered` stays true until exit motion actually finishes.
|
|
229
|
+
const exit = presence.exit();
|
|
230
|
+
await exit.finished;
|
|
231
|
+
console.log(presence.rendered); // false
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
If enter is requested during exit, the pending unmount is invalidated and the same values reverse smoothly.
|
|
235
|
+
|
|
236
|
+
### Structured interpolation
|
|
237
|
+
|
|
238
|
+
- Numbers.
|
|
239
|
+
- CSS colors: hex, rgb/rgba, hsl/hsla and common basic named colors.
|
|
240
|
+
- `srgb`, linear-light `linear-srgb`, `oklab`, and `oklch` interpolation.
|
|
241
|
+
- OKLab is the default color path for visually even transitions.
|
|
242
|
+
- Transparent endpoint chroma borrowing avoids the usual transparent-black fade halo.
|
|
243
|
+
- 2D CSS transform matrix composition/decomposition.
|
|
244
|
+
- Common 3D transform components.
|
|
245
|
+
- Shortest-path angle interpolation by default.
|
|
246
|
+
- Materials: blur, saturation, brightness, contrast and perceptual tint interpolation driven by a single progress value.
|
|
247
|
+
- SVG path morphing with one-time command normalization to cubic curves.
|
|
248
|
+
- `animateInterpolated()` drives structured output through the same scalar motion runtime.
|
|
249
|
+
|
|
250
|
+
### SVG path morphing
|
|
251
|
+
|
|
252
|
+
Path parsing and topology work happen once. Per-frame numeric sampling only lerps a reusable typed buffer.
|
|
253
|
+
|
|
254
|
+
Supported input commands include `M/L/H/V/C/S/Q/T/A/Z`, absolute or relative. Lines, quadratics and arcs normalize to cubic segments. Paths with different segment counts are equalized by splitting cubic curves. Closed paths can align starting segments and reverse winding to avoid needlessly long morphs.
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
import { createPathMorpher } from '@vune-ui/animation/path';
|
|
258
|
+
|
|
259
|
+
const morph = createPathMorpher(
|
|
260
|
+
'M0 0 L100 0 L100 100 Z',
|
|
261
|
+
'M20 10 C80 -20 120 60 80 120 Z',
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Allocation-free numeric sampling for Canvas/WebGL/custom renderers.
|
|
265
|
+
const coordinates = morph.sampleInto(0.5);
|
|
266
|
+
|
|
267
|
+
// SVG DOM route. Formatting is done only when a path string is needed.
|
|
268
|
+
const d = morph.sample(0.5);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
For very large closed paths, alignment uses a bounded set of promising start-index candidates instead of exhaustive O(n^2) matching.
|
|
272
|
+
|
|
273
|
+
### Materials
|
|
274
|
+
|
|
275
|
+
The material model is renderer-agnostic. Presets are only convenient starting values.
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
import { materials, mixMaterial } from '@vune-ui/animation/material';
|
|
279
|
+
|
|
280
|
+
const material = mixMaterial(materials.ultraThin, materials.glass, 0.5);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The DOM adapter maps a resolved material to `backdrop-filter` plus a tint color:
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
import { animateMaterial } from '@vune-ui/animation/dom';
|
|
287
|
+
import { smooth } from '@vune-ui/animation';
|
|
288
|
+
|
|
289
|
+
animateMaterial(panel, 'ultraThin', 'glass', smooth());
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Layout / FLIP
|
|
293
|
+
|
|
294
|
+
- `LayoutTransition` captures first and last bounds in separate read phases.
|
|
295
|
+
- Writes begin only after all last bounds have been measured.
|
|
296
|
+
- Position and size projection.
|
|
297
|
+
- Existing transforms are preserved and restored.
|
|
298
|
+
- Page-scroll compensation.
|
|
299
|
+
- Nested projection correction: an animated child removes the selected ancestor's projection instead of double-transforming.
|
|
300
|
+
- Interrupting an older layout transition captures the current visual rectangle before restoring the previous projection.
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
Matched geometry across different element instances uses keyed snapshots:
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
import {
|
|
308
|
+
captureSharedLayout,
|
|
309
|
+
createSharedLayoutTransition,
|
|
310
|
+
} from '@vune-ui/animation/layout';
|
|
311
|
+
|
|
312
|
+
const before = captureSharedLayout(oldTree, {
|
|
313
|
+
key: (element) => element.dataset.motionKey,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// mutate / replace the tree here
|
|
317
|
+
|
|
318
|
+
const shared = createSharedLayoutTransition(before, newTree, {
|
|
319
|
+
key: (element) => element.dataset.motionKey,
|
|
320
|
+
fadeTarget: true,
|
|
321
|
+
});
|
|
322
|
+
shared.play();
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The source element does not have to remain mounted. All matched targets in one shared transition use a single progress animation; each target only evaluates its projection matrix during the write phase. Duplicate source or target keys are rejected instead of being matched ambiguously. If a matched transition is interrupted, the next transition captures the currently rendered target geometry before cancelling the old projection so it can continue without jumping back to the underlying layout box.
|
|
326
|
+
|
|
327
|
+
### DOM adapter
|
|
328
|
+
|
|
329
|
+
- x/y/z, scale3d, rotateX/Y/Z bindings.
|
|
330
|
+
- Direct numeric style bindings with units.
|
|
331
|
+
- Structured `animateStyle()` for colors/transforms.
|
|
332
|
+
- `animateMaterial()` and `applyMaterial()`.
|
|
333
|
+
- `animatePath()` / `animateAttribute()` for SVG and other attributes.
|
|
334
|
+
- WAAPI escape hatch for fixed compositor-friendly keyframes.
|
|
335
|
+
- One global dirty-element queue: updating thousands of bound elements does not enqueue one microtask per element.
|
|
336
|
+
- Dirty compositor writes: direct/attribute updates do not reserialize transforms, and `will-change` is limited to the bound transform/opacity properties.
|
|
337
|
+
- Optional RAF-based DOM commit mode.
|
|
338
|
+
- Pointer-drag binding with pointer capture, touch-action management, cancellation handling, and coalesced-event sampling.
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
import { configureDomBatching } from '@vune-ui/animation/dom';
|
|
342
|
+
|
|
343
|
+
configureDomBatching({ scheduler: 'raf' });
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Basic numeric API
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
import { motionValue, animate, smooth, spring } from '@vune-ui/animation';
|
|
350
|
+
|
|
351
|
+
const x = motionValue(0);
|
|
352
|
+
|
|
353
|
+
x.subscribe((value, { velocity }) => {
|
|
354
|
+
console.log(value, velocity);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Hot render bindings that only need the value avoid allocating metadata.
|
|
358
|
+
x.subscribeValue((value) => render(value));
|
|
359
|
+
|
|
360
|
+
animate(x, 600, smooth());
|
|
361
|
+
|
|
362
|
+
// Retarget while moving. Existing velocity is kept.
|
|
363
|
+
setTimeout(() => {
|
|
364
|
+
animate(x, -120, spring({
|
|
365
|
+
response: 0.34,
|
|
366
|
+
dampingRatio: 0.8,
|
|
367
|
+
}));
|
|
368
|
+
}, 120);
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
For deterministic/manual stepping:
|
|
372
|
+
|
|
373
|
+
```js
|
|
374
|
+
import { MotionEngine, motionValue, spring } from '@vune-ui/animation';
|
|
375
|
+
|
|
376
|
+
const engine = new MotionEngine({ autoStart: false, wasm: false, worker: false });
|
|
377
|
+
const x = motionValue(0);
|
|
378
|
+
|
|
379
|
+
engine.animate(x, 100, spring());
|
|
380
|
+
for (let frame = 0; frame < 120; frame += 1) {
|
|
381
|
+
engine.step(1000 / 60);
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Color, transform, path, and material interpolation
|
|
386
|
+
|
|
387
|
+
```js
|
|
388
|
+
import { animateInterpolated, smooth } from '@vune-ui/animation';
|
|
389
|
+
|
|
390
|
+
animateInterpolated(
|
|
391
|
+
'#ff2d55',
|
|
392
|
+
'#5ac8fa',
|
|
393
|
+
smooth(),
|
|
394
|
+
(color) => {
|
|
395
|
+
element.style.backgroundColor = color;
|
|
396
|
+
},
|
|
397
|
+
{ type: 'color', color: { space: 'oklab' } },
|
|
398
|
+
);
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Transforms can be strings or component objects. A `350deg -> 10deg` rotation takes the short 20-degree route by default.
|
|
402
|
+
|
|
403
|
+
Path and material values use the same progress-channel mechanism:
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
animateInterpolated(fromPath, toPath, smooth(), updatePath, {
|
|
407
|
+
type: 'path',
|
|
408
|
+
path: { precision: 2 },
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
animateInterpolated('clear', 'glass', smooth(), updateMaterial, {
|
|
412
|
+
type: 'material',
|
|
413
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## Layout animation
|
|
417
|
+
|
|
418
|
+
```js
|
|
419
|
+
import { smooth } from '@vune-ui/animation';
|
|
420
|
+
import { animateLayout } from '@vune-ui/animation/layout';
|
|
421
|
+
|
|
422
|
+
const { controls } = animateLayout(
|
|
423
|
+
[sidebar, content],
|
|
424
|
+
() => root.classList.toggle('expanded'),
|
|
425
|
+
{ spec: smooth() },
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
await controls?.finished;
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
The numeric core does not know that this is a layout animation.
|
|
432
|
+
|
|
433
|
+
## Worker + shared WASM
|
|
434
|
+
|
|
435
|
+
For normal browser use, `worker: 'auto'` lets the engine decide when to prepare and use the Worker. Explicit manual control is still available:
|
|
436
|
+
|
|
437
|
+
```js
|
|
438
|
+
const engine = new MotionEngine({
|
|
439
|
+
autoStart: false,
|
|
440
|
+
wasm: 'auto',
|
|
441
|
+
worker: true,
|
|
442
|
+
workerThreshold: 4096,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await engine.prepareWorker();
|
|
446
|
+
|
|
447
|
+
const values = Array.from({ length: 10000 }, () => motionValue(0));
|
|
448
|
+
for (const value of values) engine.animate(value, 200, spring());
|
|
449
|
+
|
|
450
|
+
await engine.stepAsync(1000 / 60);
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
The Worker and main thread instantiate the same kernel over the same shared `WebAssembly.Memory`. Spring state itself never crosses `postMessage`.
|
|
454
|
+
|
|
455
|
+
When `Atomics.waitAsync()` exists, the completion path is:
|
|
456
|
+
|
|
457
|
+
```text
|
|
458
|
+
main worker
|
|
459
|
+
| |
|
|
460
|
+
| count + dt + sequence |
|
|
461
|
+
| Atomics.notify -----------> |
|
|
462
|
+
| | step_springs(...)
|
|
463
|
+
| | store(doneSequence)
|
|
464
|
+
| <---------- Atomics.notify |
|
|
465
|
+
| resume |
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Without `Atomics.waitAsync()`, the backend uses the previous tiny `done` message as a fallback.
|
|
469
|
+
|
|
470
|
+
### Browser requirements for shared Worker execution
|
|
471
|
+
|
|
472
|
+
Shared WASM memory requires `SharedArrayBuffer`. On the web that normally means a secure, cross-origin-isolated page, for example:
|
|
473
|
+
|
|
474
|
+
```text
|
|
475
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
476
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
If shared memory is unavailable, `worker: 'auto'` keeps the regular JS/WASM execution path.
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
## Scroll-driven motion
|
|
483
|
+
|
|
484
|
+
`ScrollTracker` is renderer-independent: it maps an arbitrary scalar offset to a normalized `MotionValue`, while preserving offset and normalized velocity. The DOM/window `ScrollObserver` is a thin adapter that coalesces bursts of scroll events into one metric read per animation frame.
|
|
485
|
+
|
|
486
|
+
```js
|
|
487
|
+
import { observeScroll, bindScrollTimeline } from '@vune-ui/animation/scroll';
|
|
488
|
+
|
|
489
|
+
const scroll = observeScroll(scroller, {
|
|
490
|
+
axis: 'y',
|
|
491
|
+
start: 0,
|
|
492
|
+
end: (metrics) => metrics.max,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// player stays paused; scroll position deterministically seeks it.
|
|
496
|
+
const link = bindScrollTimeline(player, scroll);
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
The range may be dynamic, so resizes/content growth can be reflected on the next sampled frame without rebuilding the tracker. `ScrollTracker` itself can also be fed from a virtual scroller, game camera, native shell, or any other numeric source.
|
|
500
|
+
|
|
501
|
+
## Constraint graph
|
|
502
|
+
|
|
503
|
+
`ConstraintGraph` evaluates relationships between numeric values after source motion commits. The graph is compiled once into a topological operation order and typed arrays, rather than chaining one subscription callback per relationship.
|
|
504
|
+
|
|
505
|
+
```js
|
|
506
|
+
import { createConstraintGraph } from '@vune-ui/animation/constraints';
|
|
507
|
+
|
|
508
|
+
const graph = createConstraintGraph();
|
|
509
|
+
const leader = graph.node(x, { name: 'leader' });
|
|
510
|
+
const follower = graph.node(y, { name: 'follower' });
|
|
511
|
+
const opacityNode = graph.node(opacity);
|
|
512
|
+
|
|
513
|
+
graph
|
|
514
|
+
.affine(follower, leader, { scale: 0.5, offset: 24 })
|
|
515
|
+
.clamp(opacityNode, follower, { min: 0, max: 1 })
|
|
516
|
+
.attach(engine);
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Built-in relations include affine/follow, clamp, weighted sum, mix, and custom mapping. Numeric velocity is propagated analytically through the built-in relations. This means a derived value can be interrupted by spring/inertia motion without first losing its instantaneous velocity.
|
|
520
|
+
|
|
521
|
+
The topology is intentionally locked after the first compile/evaluate. Runtime work is then just typed-array reads, a compact operation switch, and writes to bound outputs. Cycles are rejected at compile time.
|
|
522
|
+
|
|
523
|
+
## Canvas / WebGL / WebGPU adapters
|
|
524
|
+
|
|
525
|
+
Renderer adapters remain outside the motion solver. They subscribe through the value-only hot path and coalesce changes so a burst of `MotionValue` commits does not create one render submission per value.
|
|
526
|
+
|
|
527
|
+
Canvas keeps one retained `Float64Array` snapshot and invokes one draw callback per dirty frame:
|
|
528
|
+
|
|
529
|
+
```js
|
|
530
|
+
import { createCanvasRenderer } from '@vune-ui/animation/canvas';
|
|
531
|
+
|
|
532
|
+
const renderer = createCanvasRenderer(ctx, [x, y, scale], (ctx, values) => {
|
|
533
|
+
const [x, y, scale] = values;
|
|
534
|
+
// draw using the retained numeric snapshot
|
|
535
|
+
});
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
WebGL resolves uniform locations once and batches dirty uniform uploads:
|
|
539
|
+
|
|
540
|
+
```js
|
|
541
|
+
import { createWebGLUniformBinder } from '@vune-ui/animation/webgl';
|
|
542
|
+
|
|
543
|
+
const uniforms = createWebGLUniformBinder(gl, program, [
|
|
544
|
+
{ name: 'uProgress', value: progress },
|
|
545
|
+
{ name: 'uPosition', values: [x, y] },
|
|
546
|
+
]);
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
WebGPU packs scalar values into one retained `Float32Array` and emits one `queue.writeBuffer()` per dirty frame:
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
import { createWebGPUBufferBinder } from '@vune-ui/animation/webgpu';
|
|
553
|
+
|
|
554
|
+
const gpuValues = createWebGPUBufferBinder(device, uniformBuffer, [
|
|
555
|
+
{ value: x, index: 0 },
|
|
556
|
+
{ value: y, index: 1 },
|
|
557
|
+
{ value: opacity, index: 4 }, // explicit padding/layout is allowed
|
|
558
|
+
], { floatCount: 8 });
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
`WebGPUSpringBatch` is also available for direct dense spring workloads. It uses one storage buffer, one compute dispatch per bounded solver substep, and one readback per frame. Use it for sufficiently large batches; small interactive animations should stay on JS/WASM to avoid GPU submission and readback overhead.
|
|
562
|
+
|
|
563
|
+
```js
|
|
564
|
+
import { MotionEngine, motionValue, spring } from '@vune-ui/animation';
|
|
565
|
+
|
|
566
|
+
const engine = new MotionEngine({ gpu: 'auto', gpuThreshold: 4096 });
|
|
567
|
+
const value = motionValue(0);
|
|
568
|
+
engine.animate(value, 1, spring());
|
|
569
|
+
await engine.stepAsync(16.6667);
|
|
570
|
+
console.log(engine.getBackendPlan().gpu);
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
## Package entry points
|
|
574
|
+
|
|
575
|
+
```text
|
|
576
|
+
@vune-ui/animation
|
|
577
|
+
@vune-ui/animation/dom
|
|
578
|
+
@vune-ui/animation/interpolate
|
|
579
|
+
@vune-ui/animation/layout
|
|
580
|
+
@vune-ui/animation/gesture
|
|
581
|
+
@vune-ui/animation/timeline
|
|
582
|
+
@vune-ui/animation/transition
|
|
583
|
+
@vune-ui/animation/scroll
|
|
584
|
+
@vune-ui/animation/constraints
|
|
585
|
+
@vune-ui/animation/canvas
|
|
586
|
+
@vune-ui/animation/webgl
|
|
587
|
+
@vune-ui/animation/webgpu
|
|
588
|
+
@vune-ui/animation/material
|
|
589
|
+
@vune-ui/animation/path
|
|
590
|
+
@vune-ui/animation/wasm
|
|
591
|
+
@vune-ui/animation/worker
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
## Build / test / benchmark
|
|
595
|
+
|
|
596
|
+
```sh
|
|
597
|
+
npm run build:wasm
|
|
598
|
+
npm test
|
|
599
|
+
npm run test:full
|
|
600
|
+
npm run typecheck
|
|
601
|
+
npm run bench -- 10000 600
|
|
602
|
+
npm run bench:worker -- 10000 300
|
|
603
|
+
npm run bench:path -- 256 10000
|
|
604
|
+
npm run bench:gestures -- 500000
|
|
605
|
+
npm run bench:timeline -- 10000 600
|
|
606
|
+
npm run bench:transitions -- 10000 1000
|
|
607
|
+
npm run bench:constraints -- 10000 1000
|
|
608
|
+
npm run bench:scroll -- 500000
|
|
609
|
+
npm run bench:renderers -- 1000 500
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
`npm test` runs the focused motion suite; `npm run test:full` runs every
|
|
613
|
+
non-browser test file.
|
|
614
|
+
|
|
615
|
+
`build:wasm` emits four binaries:
|
|
616
|
+
|
|
617
|
+
```text
|
|
618
|
+
kernel-scalar.wasm
|
|
619
|
+
kernel-simd.wasm
|
|
620
|
+
kernel-shared-scalar.wasm
|
|
621
|
+
kernel-shared-simd.wasm
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
The numeric benchmarks intentionally exclude browser layout, paint, compositing, input, and framework overhead.
|
|
625
|
+
|
|
626
|
+
## Deliberately outside the numeric core
|
|
627
|
+
|
|
628
|
+
Renderer-specific layout measurement, DOM style parsing, material rendering, path rendering, and framework bindings remain adapters or optional packages. Canvas/WebGL/WebGPU have lightweight value-to-renderer bridges, while dense WebGPU spring compute is an optional asynchronous backend selected by `MotionEngine`.
|
|
629
|
+
|
|
630
|
+
The next useful layers are a particle/mesh compute backend, richer rotated/skewed shared-layout geometry, declarative constraint presets, and framework bindings such as Vune.
|