@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,88 @@
|
|
|
1
|
+
import type { MotionEngine, MotionSpec, MotionValue, InterpolatorOptions, AnimationResult } from '../../index.js';
|
|
2
|
+
|
|
3
|
+
export type TransitionTarget<T = unknown> = MotionValue | ((value: T) => void) | { set(value: T, velocity?: number): void };
|
|
4
|
+
export type StateBinding<T = unknown> = TransitionTarget<T> | ({ target: TransitionTarget<T>; spec?: MotionSpec } & InterpolatorOptions);
|
|
5
|
+
export type TransitionGroupResult = { status: AnimationResult['status']; results?: unknown[] };
|
|
6
|
+
export type TransitionGroupControls = {
|
|
7
|
+
cancel(): void;
|
|
8
|
+
finish(): void;
|
|
9
|
+
readonly finished: Promise<TransitionGroupResult>;
|
|
10
|
+
};
|
|
11
|
+
export type TransitionRoutes = Record<string, MotionSpec> | ((from: string, to: string) => MotionSpec | undefined);
|
|
12
|
+
|
|
13
|
+
export class StateTransitionGraph {
|
|
14
|
+
constructor(
|
|
15
|
+
bindings: Record<string, StateBinding>,
|
|
16
|
+
states: Record<string, Record<string, unknown>>,
|
|
17
|
+
options?: {
|
|
18
|
+
initial?: string;
|
|
19
|
+
engine?: MotionEngine;
|
|
20
|
+
spec?: MotionSpec;
|
|
21
|
+
routes?: TransitionRoutes;
|
|
22
|
+
onStateChange?: (state: string, previous: string, info: { immediate: boolean; graph: StateTransitionGraph }) => void;
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
readonly engine: MotionEngine;
|
|
26
|
+
state: string;
|
|
27
|
+
targetState: string;
|
|
28
|
+
active: TransitionGroupControls | null;
|
|
29
|
+
set(state: string): this;
|
|
30
|
+
to(state: string, spec?: MotionSpec): TransitionGroupControls;
|
|
31
|
+
cancel(): void;
|
|
32
|
+
finish(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createStateTransitionGraph(
|
|
36
|
+
bindings: Record<string, StateBinding>,
|
|
37
|
+
states: Record<string, Record<string, unknown>>,
|
|
38
|
+
options?: ConstructorParameters<typeof StateTransitionGraph>[2],
|
|
39
|
+
): StateTransitionGraph;
|
|
40
|
+
|
|
41
|
+
export type TransitionBinding<T = unknown> = {
|
|
42
|
+
key?: string;
|
|
43
|
+
target: TransitionTarget<T>;
|
|
44
|
+
from: T;
|
|
45
|
+
to: T;
|
|
46
|
+
spec?: MotionSpec;
|
|
47
|
+
} & InterpolatorOptions;
|
|
48
|
+
|
|
49
|
+
export class TransitionController {
|
|
50
|
+
constructor(bindings: TransitionBinding[], options?: {
|
|
51
|
+
present?: boolean;
|
|
52
|
+
engine?: MotionEngine;
|
|
53
|
+
enter?: MotionSpec;
|
|
54
|
+
exit?: MotionSpec;
|
|
55
|
+
onEnter?: (controller: TransitionController) => void;
|
|
56
|
+
onExit?: (controller: TransitionController) => void;
|
|
57
|
+
onEntered?: (controller: TransitionController) => void;
|
|
58
|
+
onExited?: (controller: TransitionController) => void;
|
|
59
|
+
});
|
|
60
|
+
present: boolean;
|
|
61
|
+
readonly graph: StateTransitionGraph;
|
|
62
|
+
readonly state: 'entered' | 'exited' | 'entering' | 'exiting';
|
|
63
|
+
enter(spec?: MotionSpec): TransitionGroupControls;
|
|
64
|
+
exit(spec?: MotionSpec): TransitionGroupControls;
|
|
65
|
+
setPresent(present: boolean, spec?: MotionSpec): TransitionGroupControls;
|
|
66
|
+
cancel(): void;
|
|
67
|
+
finish(): void;
|
|
68
|
+
dispose(): void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createTransition(bindings: TransitionBinding[], options?: ConstructorParameters<typeof TransitionController>[1]): TransitionController;
|
|
72
|
+
|
|
73
|
+
export class PresenceController {
|
|
74
|
+
constructor(transition: TransitionController, options?: {
|
|
75
|
+
present?: boolean;
|
|
76
|
+
onRenderChange?: (rendered: boolean, controller: PresenceController) => void;
|
|
77
|
+
});
|
|
78
|
+
readonly transition: TransitionController;
|
|
79
|
+
present: boolean;
|
|
80
|
+
rendered: boolean;
|
|
81
|
+
setPresent(present: boolean, spec?: MotionSpec): TransitionGroupControls;
|
|
82
|
+
enter(spec?: MotionSpec): TransitionGroupControls;
|
|
83
|
+
exit(spec?: MotionSpec): TransitionGroupControls;
|
|
84
|
+
cancel(): void;
|
|
85
|
+
finish(): void;
|
|
86
|
+
dispose(): void;
|
|
87
|
+
}
|
|
88
|
+
export function createPresence(transition: TransitionController, options?: ConstructorParameters<typeof PresenceController>[1]): PresenceController;
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { defaultEngine } from '../core/default-engine.js';
|
|
2
|
+
import { motionValue } from '../core/motion-value.js';
|
|
3
|
+
import { smooth } from '../core/specs.js';
|
|
4
|
+
import { createInterpolator } from '../interpolate/index.js';
|
|
5
|
+
|
|
6
|
+
function isMotionValue(value) {
|
|
7
|
+
return value && typeof value.get === 'function' && typeof value.set === 'function' && typeof value.subscribe === 'function';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function writerFor(target) {
|
|
11
|
+
if (isMotionValue(target)) return (value, velocity = 0) => target.set(Number(value), velocity);
|
|
12
|
+
if (typeof target === 'function') return target;
|
|
13
|
+
if (target && typeof target.set === 'function') return (value) => target.set(value);
|
|
14
|
+
throw new TypeError('Transition binding target must be a MotionValue, callback, or settable object.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeBinding(binding) {
|
|
18
|
+
if (isMotionValue(binding) || typeof binding === 'function' || (binding && typeof binding.set === 'function' && !('target' in binding))) {
|
|
19
|
+
return { target: binding };
|
|
20
|
+
}
|
|
21
|
+
if (!binding || typeof binding !== 'object' || !('target' in binding)) {
|
|
22
|
+
throw new TypeError('Transition binding requires a target.');
|
|
23
|
+
}
|
|
24
|
+
const { target, ...options } = binding;
|
|
25
|
+
return { target, ...options };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function routeKey(from, to) { return `${String(from)}->${String(to)}`; }
|
|
29
|
+
|
|
30
|
+
function resolveRoute(routes, from, to, fallback) {
|
|
31
|
+
if (!routes) return fallback;
|
|
32
|
+
if (typeof routes === 'function') return routes(from, to) ?? fallback;
|
|
33
|
+
return routes[routeKey(from, to)]
|
|
34
|
+
?? routes[routeKey('*', to)]
|
|
35
|
+
?? routes[routeKey(from, '*')]
|
|
36
|
+
?? routes['*->*']
|
|
37
|
+
?? fallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function deferred() {
|
|
41
|
+
let resolve;
|
|
42
|
+
let settled = false;
|
|
43
|
+
const promise = new Promise((resolver) => { resolve = resolver; });
|
|
44
|
+
return {
|
|
45
|
+
promise,
|
|
46
|
+
settle(value) {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
resolve(value);
|
|
50
|
+
},
|
|
51
|
+
get settled() { return settled; },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function groupControls(controls, { onFinish } = {}) {
|
|
56
|
+
const d = deferred();
|
|
57
|
+
const live = controls.filter(Boolean);
|
|
58
|
+
if (live.length === 0) {
|
|
59
|
+
const result = { status: 'finished' };
|
|
60
|
+
onFinish?.(result);
|
|
61
|
+
d.settle(result);
|
|
62
|
+
} else {
|
|
63
|
+
Promise.all(live.map((control) => control.finished)).then((results) => {
|
|
64
|
+
const status = results.some((result) => result?.status === 'cancelled')
|
|
65
|
+
? 'cancelled'
|
|
66
|
+
: results.some((result) => result?.status === 'interrupted')
|
|
67
|
+
? 'interrupted'
|
|
68
|
+
: 'finished';
|
|
69
|
+
const result = { status, results };
|
|
70
|
+
onFinish?.(result);
|
|
71
|
+
d.settle(result);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
cancel() { for (const control of live) control.cancel?.(); },
|
|
76
|
+
finish() { for (const control of live) control.finish?.(); },
|
|
77
|
+
finished: d.promise,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stateValue(states, state, key) {
|
|
82
|
+
const values = states[state];
|
|
83
|
+
if (!values || typeof values !== 'object') throw new RangeError(`Unknown transition state: ${String(state)}`);
|
|
84
|
+
if (!(key in values)) throw new TypeError(`Transition state ${String(state)} is missing binding ${key}.`);
|
|
85
|
+
return values[key];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Named-state motion graph. Numeric MotionValues are animated directly so each
|
|
90
|
+
* property preserves its own physical velocity. Structured values share one
|
|
91
|
+
* scalar progress channel per transition and precompile their interpolators.
|
|
92
|
+
*/
|
|
93
|
+
export class StateTransitionGraph {
|
|
94
|
+
constructor(bindings, states, {
|
|
95
|
+
initial,
|
|
96
|
+
engine = defaultEngine,
|
|
97
|
+
spec = smooth(),
|
|
98
|
+
routes,
|
|
99
|
+
onStateChange,
|
|
100
|
+
} = {}) {
|
|
101
|
+
if (!bindings || typeof bindings !== 'object') throw new TypeError('StateTransitionGraph requires bindings.');
|
|
102
|
+
if (!states || typeof states !== 'object' || Object.keys(states).length === 0) throw new TypeError('StateTransitionGraph requires states.');
|
|
103
|
+
this.engine = engine;
|
|
104
|
+
this.defaultSpec = spec;
|
|
105
|
+
this.routes = routes;
|
|
106
|
+
this.onStateChange = typeof onStateChange === 'function' ? onStateChange : null;
|
|
107
|
+
this.bindings = new Map(Object.entries(bindings).map(([key, binding]) => [key, normalizeBinding(binding)]));
|
|
108
|
+
this.states = states;
|
|
109
|
+
this.state = initial ?? Object.keys(states)[0];
|
|
110
|
+
this.targetState = this.state;
|
|
111
|
+
this.generation = 0;
|
|
112
|
+
this.active = null;
|
|
113
|
+
this.currentStructured = new Map();
|
|
114
|
+
this.#applyImmediate(this.state);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#applyImmediate(state) {
|
|
118
|
+
for (const [key, binding] of this.bindings) {
|
|
119
|
+
const value = stateValue(this.states, state, key);
|
|
120
|
+
if (isMotionValue(binding.target) && typeof value === 'number' && binding.type == null && typeof binding.interpolate !== 'function') {
|
|
121
|
+
this.engine.stop(binding.target, 'interrupted');
|
|
122
|
+
binding.target.set(value, 0);
|
|
123
|
+
} else {
|
|
124
|
+
writerFor(binding.target)(value, 0);
|
|
125
|
+
this.currentStructured.set(key, value);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
set(state) {
|
|
131
|
+
if (!(state in this.states)) throw new RangeError(`Unknown transition state: ${String(state)}`);
|
|
132
|
+
this.generation += 1;
|
|
133
|
+
this.active?.cancel?.();
|
|
134
|
+
this.active = null;
|
|
135
|
+
this.#applyImmediate(state);
|
|
136
|
+
const previous = this.state;
|
|
137
|
+
this.state = state;
|
|
138
|
+
this.targetState = state;
|
|
139
|
+
this.onStateChange?.(state, previous, { immediate: true, graph: this });
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
to(state, specOverride) {
|
|
144
|
+
if (!(state in this.states)) throw new RangeError(`Unknown transition state: ${String(state)}`);
|
|
145
|
+
const fromState = this.targetState ?? this.state;
|
|
146
|
+
const spec = specOverride ?? resolveRoute(this.routes, fromState, state, this.defaultSpec);
|
|
147
|
+
const generation = ++this.generation;
|
|
148
|
+
this.targetState = state;
|
|
149
|
+
|
|
150
|
+
// Structured progress is private to each transition. Cancelling it is safe;
|
|
151
|
+
// numeric MotionValues are deliberately *not* cancelled here because the
|
|
152
|
+
// next engine.animate() call interrupts/retargets them while retaining velocity.
|
|
153
|
+
this.active?._cancelStructured?.();
|
|
154
|
+
|
|
155
|
+
const controls = [];
|
|
156
|
+
const structured = [];
|
|
157
|
+
for (const [key, binding] of this.bindings) {
|
|
158
|
+
const targetValue = stateValue(this.states, state, key);
|
|
159
|
+
if (isMotionValue(binding.target) && typeof targetValue === 'number' && binding.type == null && typeof binding.interpolate !== 'function') {
|
|
160
|
+
controls.push(this.engine.animate(binding.target, targetValue, binding.spec ?? spec));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const current = this.currentStructured.has(key)
|
|
164
|
+
? this.currentStructured.get(key)
|
|
165
|
+
: stateValue(this.states, this.state, key);
|
|
166
|
+
const fastNumber = typeof current === 'number'
|
|
167
|
+
&& typeof targetValue === 'number'
|
|
168
|
+
&& binding.type == null
|
|
169
|
+
&& typeof binding.interpolate !== 'function';
|
|
170
|
+
structured.push({
|
|
171
|
+
key,
|
|
172
|
+
binding,
|
|
173
|
+
writer: writerFor(binding.target),
|
|
174
|
+
mixer: fastNumber ? null : createInterpolator(current, targetValue, binding),
|
|
175
|
+
from: fastNumber ? current : 0,
|
|
176
|
+
delta: fastNumber ? targetValue - current : 0,
|
|
177
|
+
current,
|
|
178
|
+
targetValue,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let structuredControls = null;
|
|
183
|
+
let unsubscribe = null;
|
|
184
|
+
if (structured.length > 0) {
|
|
185
|
+
const progress = motionValue(0);
|
|
186
|
+
unsubscribe = progress.subscribeValue((value) => {
|
|
187
|
+
for (const item of structured) {
|
|
188
|
+
const mixed = item.mixer ? item.mixer(value) : item.from + item.delta * value;
|
|
189
|
+
item.writer(mixed);
|
|
190
|
+
item.current = mixed;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
structuredControls = this.engine.animate(progress, 1, spec);
|
|
194
|
+
controls.push(structuredControls);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const group = groupControls(controls, {
|
|
198
|
+
onFinish: (result) => {
|
|
199
|
+
unsubscribe?.();
|
|
200
|
+
for (const item of structured) {
|
|
201
|
+
this.currentStructured.set(item.key, result.status === 'finished' ? item.targetValue : item.current);
|
|
202
|
+
}
|
|
203
|
+
if (generation !== this.generation) return;
|
|
204
|
+
if (result.status === 'finished') {
|
|
205
|
+
const previous = this.state;
|
|
206
|
+
this.state = state;
|
|
207
|
+
this.targetState = state;
|
|
208
|
+
this.onStateChange?.(state, previous, { immediate: false, graph: this });
|
|
209
|
+
}
|
|
210
|
+
if (this.active === group) this.active = null;
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
group._cancelStructured = () => {
|
|
214
|
+
for (const item of structured) this.currentStructured.set(item.key, item.current);
|
|
215
|
+
if (structuredControls) structuredControls.cancel();
|
|
216
|
+
unsubscribe?.();
|
|
217
|
+
unsubscribe = null;
|
|
218
|
+
};
|
|
219
|
+
this.active = group;
|
|
220
|
+
return group;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
cancel() {
|
|
224
|
+
this.generation += 1;
|
|
225
|
+
this.active?._cancelStructured?.();
|
|
226
|
+
this.active?.cancel?.();
|
|
227
|
+
this.active = null;
|
|
228
|
+
this.targetState = this.state;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
finish() { this.active?.finish?.(); }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function createStateTransitionGraph(bindings, states, options) {
|
|
235
|
+
return new StateTransitionGraph(bindings, states, options);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Two-state enter/exit convenience wrapper. It is intentionally implemented on
|
|
240
|
+
* top of the same named-state graph so rapid enter -> exit reversals retarget
|
|
241
|
+
* numeric MotionValues instead of restarting them from rest.
|
|
242
|
+
*/
|
|
243
|
+
export class TransitionController {
|
|
244
|
+
constructor(bindings, {
|
|
245
|
+
present = false,
|
|
246
|
+
engine = defaultEngine,
|
|
247
|
+
enter = smooth(),
|
|
248
|
+
exit = enter,
|
|
249
|
+
onEnter,
|
|
250
|
+
onExit,
|
|
251
|
+
onEntered,
|
|
252
|
+
onExited,
|
|
253
|
+
} = {}) {
|
|
254
|
+
if (!Array.isArray(bindings)) throw new TypeError('TransitionController bindings must be an array.');
|
|
255
|
+
const graphBindings = {};
|
|
256
|
+
const exited = {};
|
|
257
|
+
const entered = {};
|
|
258
|
+
bindings.forEach((raw, index) => {
|
|
259
|
+
if (!raw || typeof raw !== 'object' || !('target' in raw) || !('from' in raw) || !('to' in raw)) {
|
|
260
|
+
throw new TypeError('Each transition binding requires target, from, and to.');
|
|
261
|
+
}
|
|
262
|
+
const key = raw.key ?? `binding${index}`;
|
|
263
|
+
const { from, to, key: _key, ...binding } = raw;
|
|
264
|
+
graphBindings[key] = binding;
|
|
265
|
+
exited[key] = from;
|
|
266
|
+
entered[key] = to;
|
|
267
|
+
});
|
|
268
|
+
this.onEnter = typeof onEnter === 'function' ? onEnter : null;
|
|
269
|
+
this.onExit = typeof onExit === 'function' ? onExit : null;
|
|
270
|
+
this.onEntered = typeof onEntered === 'function' ? onEntered : null;
|
|
271
|
+
this.onExited = typeof onExited === 'function' ? onExited : null;
|
|
272
|
+
this.graph = new StateTransitionGraph(graphBindings, { exited, entered }, {
|
|
273
|
+
initial: present ? 'entered' : 'exited',
|
|
274
|
+
engine,
|
|
275
|
+
spec: enter,
|
|
276
|
+
routes: {
|
|
277
|
+
'*->entered': enter,
|
|
278
|
+
'*->exited': exit,
|
|
279
|
+
},
|
|
280
|
+
onStateChange: (state, previous, info) => {
|
|
281
|
+
if (!info.immediate && state === 'entered') this.onEntered?.(this);
|
|
282
|
+
if (!info.immediate && state === 'exited') this.onExited?.(this);
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
this.present = Boolean(present);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
get state() {
|
|
289
|
+
const target = this.graph.targetState;
|
|
290
|
+
if (this.graph.active) return target === 'entered' ? 'entering' : 'exiting';
|
|
291
|
+
return this.graph.state;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
enter(spec) {
|
|
295
|
+
this.present = true;
|
|
296
|
+
this.onEnter?.(this);
|
|
297
|
+
return this.graph.to('entered', spec);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
exit(spec) {
|
|
301
|
+
this.present = false;
|
|
302
|
+
this.onExit?.(this);
|
|
303
|
+
return this.graph.to('exited', spec);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
setPresent(present, spec) { return present ? this.enter(spec) : this.exit(spec); }
|
|
307
|
+
cancel() { this.graph.cancel(); }
|
|
308
|
+
finish() { this.graph.finish(); }
|
|
309
|
+
dispose() { this.cancel(); }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function createTransition(bindings, options) {
|
|
313
|
+
return new TransitionController(bindings, options);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Lifecycle wrapper that keeps content logically rendered until its exit motion
|
|
318
|
+
* finishes. Framework adapters can observe `rendered` and unmount only after
|
|
319
|
+
* the promise resolves, while an enter during exit cancels that pending unmount.
|
|
320
|
+
*/
|
|
321
|
+
export class PresenceController {
|
|
322
|
+
constructor(transition, {
|
|
323
|
+
present = transition?.present ?? false,
|
|
324
|
+
onRenderChange,
|
|
325
|
+
} = {}) {
|
|
326
|
+
if (!(transition instanceof TransitionController)) throw new TypeError('PresenceController requires a TransitionController.');
|
|
327
|
+
this.transition = transition;
|
|
328
|
+
this.present = Boolean(present);
|
|
329
|
+
this.rendered = Boolean(present);
|
|
330
|
+
this.onRenderChange = typeof onRenderChange === 'function' ? onRenderChange : null;
|
|
331
|
+
this.generation = 0;
|
|
332
|
+
if (this.present !== transition.present) {
|
|
333
|
+
transition.graph.set(this.present ? 'entered' : 'exited');
|
|
334
|
+
transition.present = this.present;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#setRendered(value) {
|
|
339
|
+
if (this.rendered === value) return;
|
|
340
|
+
this.rendered = value;
|
|
341
|
+
this.onRenderChange?.(value, this);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
setPresent(present, spec) {
|
|
345
|
+
const next = Boolean(present);
|
|
346
|
+
const generation = ++this.generation;
|
|
347
|
+
this.present = next;
|
|
348
|
+
if (next) {
|
|
349
|
+
this.#setRendered(true);
|
|
350
|
+
return this.transition.enter(spec);
|
|
351
|
+
}
|
|
352
|
+
const controls = this.transition.exit(spec);
|
|
353
|
+
controls.finished.then((result) => {
|
|
354
|
+
if (generation !== this.generation || this.present) return;
|
|
355
|
+
if (result.status === 'finished') this.#setRendered(false);
|
|
356
|
+
});
|
|
357
|
+
return controls;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
enter(spec) { return this.setPresent(true, spec); }
|
|
361
|
+
exit(spec) { return this.setPresent(false, spec); }
|
|
362
|
+
cancel() { this.generation += 1; this.transition.cancel(); }
|
|
363
|
+
finish() { this.transition.finish(); }
|
|
364
|
+
dispose() { this.cancel(); }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function createPresence(transition, options) {
|
|
368
|
+
return new PresenceController(transition, options);
|
|
369
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export class WasmSpringBatch {
|
|
2
|
+
static create(capacity?: number): Promise<WasmSpringBatch>;
|
|
3
|
+
readonly kind: 'wasm';
|
|
4
|
+
readonly variant: 'simd' | 'scalar';
|
|
5
|
+
readonly capacity: number;
|
|
6
|
+
readonly positions: Float32Array;
|
|
7
|
+
readonly velocities: Float32Array;
|
|
8
|
+
readonly targets: Float32Array;
|
|
9
|
+
readonly omegas: Float32Array;
|
|
10
|
+
readonly dampingRatios: Float32Array;
|
|
11
|
+
step(count: number, dtSeconds: number): void;
|
|
12
|
+
}
|
|
13
|
+
export class SharedWasmSpringBatch {
|
|
14
|
+
static create(capacity?: number, options?: { memory?: WebAssembly.Memory }): Promise<SharedWasmSpringBatch>;
|
|
15
|
+
readonly kind: 'shared-wasm';
|
|
16
|
+
readonly variant: 'simd' | 'scalar';
|
|
17
|
+
readonly capacity: number;
|
|
18
|
+
readonly memory: WebAssembly.Memory;
|
|
19
|
+
readonly positions: Float32Array;
|
|
20
|
+
readonly velocities: Float32Array;
|
|
21
|
+
readonly targets: Float32Array;
|
|
22
|
+
readonly omegas: Float32Array;
|
|
23
|
+
readonly dampingRatios: Float32Array;
|
|
24
|
+
step(count: number, dtSeconds: number): void;
|
|
25
|
+
}
|
|
26
|
+
export function loadSpringKernel(): Promise<unknown>;
|
|
27
|
+
export function loadSharedSpringKernel(options?: { memory?: WebAssembly.Memory; preferSimd?: boolean }): Promise<unknown>;
|
|
28
|
+
export function instantiateSharedSpringKernel(memory: WebAssembly.Memory, variant?: 'simd' | 'scalar'): Promise<unknown>;
|
|
29
|
+
export function createSharedWasmMemory(options?: { initialPages?: number; maximumPages?: number }): WebAssembly.Memory;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export {
|
|
2
|
+
loadSpringKernel,
|
|
3
|
+
loadSharedSpringKernel,
|
|
4
|
+
instantiateSharedSpringKernel,
|
|
5
|
+
createSharedWasmMemory,
|
|
6
|
+
} from './loader.js';
|
|
7
|
+
export { WasmSpringBatch } from './wasm-spring-batch.js';
|
|
8
|
+
export { SharedWasmSpringBatch } from './shared-wasm-spring-batch.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
async function readWasm(url) {
|
|
2
|
+
if (url.protocol === 'file:' && typeof process !== 'undefined' && process.versions?.node) {
|
|
3
|
+
const nodeFs = ['node', 'fs/promises'].join(':');
|
|
4
|
+
const { readFile } = await import(/* @vite-ignore */ nodeFs);
|
|
5
|
+
return readFile(url);
|
|
6
|
+
}
|
|
7
|
+
const response = await fetch(url);
|
|
8
|
+
if (!response.ok) throw new Error(`Failed to load WASM: ${response.status}`);
|
|
9
|
+
return response.arrayBuffer();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function instantiate(url, imports = {}) {
|
|
13
|
+
const bytes = await readWasm(url);
|
|
14
|
+
return WebAssembly.instantiate(bytes, imports);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadSpringKernel() {
|
|
18
|
+
const simdUrl = new URL('../../wasm/kernel-simd.wasm', import.meta.url);
|
|
19
|
+
try {
|
|
20
|
+
const instance = await instantiate(simdUrl);
|
|
21
|
+
return { instance: instance.instance ?? instance, variant: 'simd' };
|
|
22
|
+
} catch (simdError) {
|
|
23
|
+
const scalarUrl = new URL('../../wasm/kernel-scalar.wasm', import.meta.url);
|
|
24
|
+
const instance = await instantiate(scalarUrl);
|
|
25
|
+
return { instance: instance.instance ?? instance, variant: 'scalar', simdError };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createSharedWasmMemory({ initialPages = 64, maximumPages = 1024 } = {}) {
|
|
30
|
+
if (typeof SharedArrayBuffer !== 'function') throw new Error('SharedArrayBuffer is unavailable.');
|
|
31
|
+
return new WebAssembly.Memory({ initial: initialPages, maximum: maximumPages, shared: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function instantiateSharedSpringKernel(memory, variant = 'simd') {
|
|
35
|
+
if (!(memory instanceof WebAssembly.Memory) || !(memory.buffer instanceof SharedArrayBuffer)) {
|
|
36
|
+
throw new TypeError('A shared WebAssembly.Memory is required.');
|
|
37
|
+
}
|
|
38
|
+
const url = new URL(`../../wasm/kernel-shared-${variant}.wasm`, import.meta.url);
|
|
39
|
+
const instance = await instantiate(url, { env: { memory } });
|
|
40
|
+
return { instance: instance.instance ?? instance, variant };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function loadSharedSpringKernel({ memory = createSharedWasmMemory(), preferSimd = true } = {}) {
|
|
44
|
+
if (preferSimd) {
|
|
45
|
+
try {
|
|
46
|
+
const loaded = await instantiateSharedSpringKernel(memory, 'simd');
|
|
47
|
+
return { ...loaded, memory };
|
|
48
|
+
} catch (simdError) {
|
|
49
|
+
const loaded = await instantiateSharedSpringKernel(memory, 'scalar');
|
|
50
|
+
return { ...loaded, memory, simdError };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const loaded = await instantiateSharedSpringKernel(memory, 'scalar');
|
|
54
|
+
return { ...loaded, memory };
|
|
55
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createSharedWasmMemory, loadSharedSpringKernel } from './loader.js';
|
|
2
|
+
|
|
3
|
+
export class SharedWasmSpringBatch {
|
|
4
|
+
static async create(capacity = 65536, { memory } = {}) {
|
|
5
|
+
const sharedMemory = memory ?? createSharedWasmMemory();
|
|
6
|
+
const { instance, variant } = await loadSharedSpringKernel({ memory: sharedMemory });
|
|
7
|
+
return new SharedWasmSpringBatch(instance.exports, variant, capacity, sharedMemory);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
constructor(exports, variant, capacity, memory) {
|
|
11
|
+
this.kind = 'shared-wasm';
|
|
12
|
+
this.variant = variant;
|
|
13
|
+
this.capacity = capacity;
|
|
14
|
+
this.exports = exports;
|
|
15
|
+
this.memory = memory;
|
|
16
|
+
const bytes = capacity * 4;
|
|
17
|
+
const alloc = (size) => exports.motion_alloc(size, 16);
|
|
18
|
+
this.ptrs = {
|
|
19
|
+
positions: alloc(bytes),
|
|
20
|
+
velocities: alloc(bytes),
|
|
21
|
+
targets: alloc(bytes),
|
|
22
|
+
omegas: alloc(bytes),
|
|
23
|
+
dampingRatios: alloc(bytes),
|
|
24
|
+
};
|
|
25
|
+
for (const [name, pointer] of Object.entries(this.ptrs)) {
|
|
26
|
+
if (!pointer) throw new Error(`Shared WASM allocation failed for ${name}.`);
|
|
27
|
+
this[name] = new Float32Array(memory.buffer, pointer, capacity);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ensureCapacity(required) {
|
|
32
|
+
if (required > this.capacity) throw new RangeError(`Shared WASM spring capacity exceeded (${required} > ${this.capacity}).`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
copyInto(other, count) {
|
|
36
|
+
for (const key of ['positions', 'velocities', 'targets', 'omegas', 'dampingRatios']) {
|
|
37
|
+
other[key].set(this[key].subarray(0, count));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
step(count, dtSeconds) {
|
|
42
|
+
this.exports.step_springs(
|
|
43
|
+
this.ptrs.positions,
|
|
44
|
+
this.ptrs.velocities,
|
|
45
|
+
this.ptrs.targets,
|
|
46
|
+
this.ptrs.omegas,
|
|
47
|
+
this.ptrs.dampingRatios,
|
|
48
|
+
count,
|
|
49
|
+
dtSeconds,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { loadSpringKernel } from './loader.js';
|
|
2
|
+
|
|
3
|
+
export class WasmSpringBatch {
|
|
4
|
+
static async create(capacity = 65536) {
|
|
5
|
+
const { instance, variant } = await loadSpringKernel();
|
|
6
|
+
return new WasmSpringBatch(instance.exports, variant, capacity);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
constructor(exports, variant, capacity) {
|
|
10
|
+
this.kind = 'wasm';
|
|
11
|
+
this.variant = variant;
|
|
12
|
+
this.capacity = capacity;
|
|
13
|
+
this.exports = exports;
|
|
14
|
+
const bytes = capacity * 4;
|
|
15
|
+
const alloc = (size) => exports.motion_alloc(size, 16);
|
|
16
|
+
this.ptrs = {
|
|
17
|
+
positions: alloc(bytes),
|
|
18
|
+
velocities: alloc(bytes),
|
|
19
|
+
targets: alloc(bytes),
|
|
20
|
+
omegas: alloc(bytes),
|
|
21
|
+
dampingRatios: alloc(bytes),
|
|
22
|
+
};
|
|
23
|
+
for (const [name, pointer] of Object.entries(this.ptrs)) {
|
|
24
|
+
if (!pointer) throw new Error(`WASM allocation failed for ${name}.`);
|
|
25
|
+
this[name] = new Float32Array(exports.memory.buffer, pointer, capacity);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ensureCapacity(required) {
|
|
30
|
+
if (required > this.capacity) throw new RangeError(`WASM spring capacity exceeded (${required} > ${this.capacity}).`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
copyInto(other, count) {
|
|
34
|
+
other.positions.set(this.positions.subarray(0, count));
|
|
35
|
+
other.velocities.set(this.velocities.subarray(0, count));
|
|
36
|
+
other.targets.set(this.targets.subarray(0, count));
|
|
37
|
+
other.omegas.set(this.omegas.subarray(0, count));
|
|
38
|
+
other.dampingRatios.set(this.dampingRatios.subarray(0, count));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
step(count, dtSeconds) {
|
|
42
|
+
this.exports.step_springs(
|
|
43
|
+
this.ptrs.positions,
|
|
44
|
+
this.ptrs.velocities,
|
|
45
|
+
this.ptrs.targets,
|
|
46
|
+
this.ptrs.omegas,
|
|
47
|
+
this.ptrs.dampingRatios,
|
|
48
|
+
count,
|
|
49
|
+
dtSeconds,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MotionValue } from '../../index.js';
|
|
2
|
+
export type WebGLUniformType = '1f' | '2fv' | '3fv' | '4fv' | 'matrix4fv';
|
|
3
|
+
export type WebGLUniformBinding = {
|
|
4
|
+
name?: string;
|
|
5
|
+
location?: WebGLUniformLocation | unknown;
|
|
6
|
+
value?: MotionValue;
|
|
7
|
+
values?: MotionValue[];
|
|
8
|
+
type?: WebGLUniformType;
|
|
9
|
+
};
|
|
10
|
+
export class WebGLUniformBinder {
|
|
11
|
+
constructor(gl: WebGLRenderingContext | WebGL2RenderingContext | any, program: WebGLProgram | unknown, bindings?: WebGLUniformBinding[], options?: {
|
|
12
|
+
autoUseProgram?: boolean;
|
|
13
|
+
requestFrame?: (callback: FrameRequestCallback) => any;
|
|
14
|
+
cancelFrame?: (id: any) => void;
|
|
15
|
+
flushInitial?: boolean;
|
|
16
|
+
});
|
|
17
|
+
add(binding: WebGLUniformBinding): this;
|
|
18
|
+
flush(): number;
|
|
19
|
+
flushNow(): this;
|
|
20
|
+
dispose(): void;
|
|
21
|
+
}
|
|
22
|
+
export function createWebGLUniformBinder(gl: any, program: unknown, bindings?: WebGLUniformBinding[], options?: ConstructorParameters<typeof WebGLUniformBinder>[3]): WebGLUniformBinder;
|