@mindees/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation/animation.d.ts +69 -0
- package/dist/animation/animation.d.ts.map +1 -0
- package/dist/animation/animation.js +294 -0
- package/dist/animation/animation.js.map +1 -0
- package/dist/animation/easing.d.ts +25 -0
- package/dist/animation/easing.d.ts.map +1 -0
- package/dist/animation/easing.js +61 -0
- package/dist/animation/easing.js.map +1 -0
- package/dist/gesture/animated.d.ts +23 -0
- package/dist/gesture/animated.d.ts.map +1 -0
- package/dist/gesture/animated.js +53 -0
- package/dist/gesture/animated.js.map +1 -0
- package/dist/gesture/recognizers.d.ts +133 -0
- package/dist/gesture/recognizers.d.ts.map +1 -0
- package/dist/gesture/recognizers.js +507 -0
- package/dist/gesture/recognizers.js.map +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/reactive/reactive.d.ts +38 -21
- package/dist/reactive/reactive.d.ts.map +1 -1
- package/dist/reactive/reactive.js +88 -25
- package/dist/reactive/reactive.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts.map +1 -1
- package/dist/scheduler/scheduler.js +13 -0
- package/dist/scheduler/scheduler.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Easing } from "./easing.js";
|
|
2
|
+
|
|
3
|
+
//#region src/animation/animation.d.ts
|
|
4
|
+
/** A frame source: subscribe with a per-frame `tick(nowMs)`, get back an unsubscribe. (`requestAnimationFrame` on web, vsync on native, a manual ticker in tests.) */
|
|
5
|
+
type FrameSource = (tick: (nowMs: number) => void) => () => void;
|
|
6
|
+
/** A running animation: stop it, or await natural completion. */
|
|
7
|
+
interface AnimationHandle {
|
|
8
|
+
/** Stop now (keeps the current value); `done` resolves `false`. Idempotent. */
|
|
9
|
+
stop(): void;
|
|
10
|
+
/** Resolves `true` on natural completion, `false` if interrupted/stopped. Settles exactly once. */
|
|
11
|
+
readonly done: Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
/** A reactive, animatable number. Call to read (tracks); `.set` jumps (untracked, stops any driver). */
|
|
14
|
+
interface AnimatedValue {
|
|
15
|
+
(): number;
|
|
16
|
+
/** Jump to `v` immediately, cancelling any running driver. */
|
|
17
|
+
set(v: number): void;
|
|
18
|
+
/** The current per-frame velocity (units/sec) — seeds spring-interrupts-spring. */
|
|
19
|
+
readonly velocity: () => number;
|
|
20
|
+
/** Stop any running driver, keeping the current value. */
|
|
21
|
+
stop(): void;
|
|
22
|
+
}
|
|
23
|
+
/** Inject (or clear) the frame source that drives animations. `null` (default) → jump-to-final. */
|
|
24
|
+
declare function setFrameSource(src: FrameSource | null): void;
|
|
25
|
+
/** The current frame source, or `null`. */
|
|
26
|
+
declare function getFrameSource(): FrameSource | null;
|
|
27
|
+
/** Create an {@link AnimatedValue} (a reactive number you can drive with {@link timing}/{@link spring}). */
|
|
28
|
+
declare function animate(initial: number): AnimatedValue;
|
|
29
|
+
/** Animate `av` to `to` over `duration` ms with `easing` (RN `Animated.timing`). */
|
|
30
|
+
declare function timing(av: AnimatedValue, opts: {
|
|
31
|
+
readonly to: number;
|
|
32
|
+
readonly duration?: number;
|
|
33
|
+
readonly easing?: Easing;
|
|
34
|
+
readonly delay?: number;
|
|
35
|
+
readonly onComplete?: (finished: boolean) => void;
|
|
36
|
+
}): AnimationHandle;
|
|
37
|
+
/** Animate `av` to `to` with spring physics (RN/Reanimated `withSpring`, Flutter `SpringSimulation`). */
|
|
38
|
+
declare function spring(av: AnimatedValue, opts: {
|
|
39
|
+
readonly to: number;
|
|
40
|
+
readonly stiffness?: number;
|
|
41
|
+
readonly damping?: number;
|
|
42
|
+
readonly mass?: number;
|
|
43
|
+
readonly velocity?: number;
|
|
44
|
+
readonly restDelta?: number;
|
|
45
|
+
readonly restVelocity?: number;
|
|
46
|
+
readonly onComplete?: (finished: boolean) => void;
|
|
47
|
+
}): AnimationHandle;
|
|
48
|
+
/**
|
|
49
|
+
* Map an accessor through a piecewise-linear range (RN `Animated.interpolate`). Returns a plain
|
|
50
|
+
* accessor, so it tracks `value` and re-reads inside the consuming style each frame — glitch-free
|
|
51
|
+
* for free. `inputRange` must be monotonically increasing and match `outputRange` length (≥2).
|
|
52
|
+
*/
|
|
53
|
+
declare function interpolate(value: () => number, inputRange: readonly number[], outputRange: readonly number[], opts?: {
|
|
54
|
+
readonly extrapolate?: 'clamp' | 'extend';
|
|
55
|
+
}): () => number;
|
|
56
|
+
/** A `requestAnimationFrame`-backed {@link FrameSource} for the web (the host wires this at startup). */
|
|
57
|
+
declare function rafFrameSource(): FrameSource;
|
|
58
|
+
/** A manually-driven {@link FrameSource} for deterministic tests: call `tick(nowMs)` to advance. */
|
|
59
|
+
declare function manualFrameSource(): {
|
|
60
|
+
source: FrameSource;
|
|
61
|
+
tick: (nowMs: number) => void;
|
|
62
|
+
};
|
|
63
|
+
/** @internal Test-only: number of running animations. */
|
|
64
|
+
declare function _activeAnimationCount(): number;
|
|
65
|
+
/** @internal Test-only: reset all engine state between tests. */
|
|
66
|
+
declare function _resetAnimation(): void;
|
|
67
|
+
//#endregion
|
|
68
|
+
export { AnimatedValue, AnimationHandle, FrameSource, _activeAnimationCount, _resetAnimation, animate, getFrameSource, interpolate, manualFrameSource, rafFrameSource, setFrameSource, spring, timing };
|
|
69
|
+
//# sourceMappingURL=animation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animation.d.ts","names":[],"sources":["../../src/animation/animation.ts"],"mappings":";;;;KAiBY,WAAA,IAAe,IAA6B,GAAtB,KAAA;;UAGjB,eAAA;EAQA;EANf,IAAA;;WAES,IAAA,EAAM,OAAO;AAAA;;UAIP,aAAA;EAAA;EAOX;EAJJ,GAAA,CAAI,CAAA;EAkCU;EAAA,SAhCL,QAAA;;EAET,IAAA;AAAA;AAyDF;AAAA,iBA3BgB,cAAA,CAAe,GAAuB,EAAlB,WAAW;;iBA2B/B,cAAA,IAAkB,WAAW;AAAA;AAAA,iBAqD7B,OAAA,CAAQ,OAAA,WAAkB,aAAa;;iBAyFvC,MAAA,CACd,EAAA,EAAI,aAAA,EACJ,IAAA;EAAA,SACW,EAAA;EAAA,SACA,QAAA;EAAA,SACA,MAAA,GAAS,MAAA;EAAA,SACT,KAAA;EAAA,SACA,UAAA,IAAc,QAAA;AAAA,IAExB,eAAA;;iBA+Ba,MAAA,CACd,EAAA,EAAI,aAAA,EACJ,IAAA;EAAA,SACW,EAAA;EAAA,SACA,SAAA;EAAA,SACA,OAAA;EAAA,SACA,IAAA;EAAA,SACA,QAAA;EAAA,SACA,SAAA;EAAA,SACA,YAAA;EAAA,SACA,UAAA,IAAc,QAAA;AAAA,IAExB,eAAe;;;;;;iBAiDF,WAAA,CACd,KAAA,gBACA,UAAA,qBACA,WAAA,qBACA,IAAA;EAAA,SAAkB,WAAA;AAAA;;iBAgCJ,cAAA,IAAkB,WAAW;;iBAe7B,iBAAA;EAAuB,MAAA,EAAQ,WAAW;EAAE,IAAA,GAAO,KAAA;AAAA;;iBAcnD,qBAAA;;iBAKA,eAAA"}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { batch, getOwner, onCleanup, signal, untrack } from "../reactive/reactive.js";
|
|
2
|
+
import { linear } from "./easing.js";
|
|
3
|
+
//#region src/animation/animation.ts
|
|
4
|
+
/**
|
|
5
|
+
* The animation engine — RN `Animated`/Reanimated + Flutter `AnimationController` parity, built
|
|
6
|
+
* entirely on the reactive core. An {@link AnimatedValue} **is a signal**, so reading it inside a
|
|
7
|
+
* `style` accessor re-renders only that node (no renderer surface). One injected {@link FrameSource}
|
|
8
|
+
* (mirroring `setReactiveScheduler`) drives a single loop that ticks every active driver inside one
|
|
9
|
+
* `batch()` per frame — so a style reading several animated values recomputes once (glitch-free).
|
|
10
|
+
*
|
|
11
|
+
* With no frame source (SSR / headless / tests until one is wired) animations **jump to their final
|
|
12
|
+
* value** synchronously: deterministic, never a hang, server output shows the end state.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
const MAX_DT = .064;
|
|
17
|
+
const SPRING_MAX_FRAMES = 600;
|
|
18
|
+
const DEFAULT_DURATION = 250;
|
|
19
|
+
const internals = /* @__PURE__ */ new WeakMap();
|
|
20
|
+
const active = /* @__PURE__ */ new Set();
|
|
21
|
+
let frameSource = null;
|
|
22
|
+
let unsubscribe = null;
|
|
23
|
+
let lastNow = -1;
|
|
24
|
+
/** Inject (or clear) the frame source that drives animations. `null` (default) → jump-to-final. */
|
|
25
|
+
function setFrameSource(src) {
|
|
26
|
+
if (src === frameSource) return;
|
|
27
|
+
if (unsubscribe) {
|
|
28
|
+
unsubscribe();
|
|
29
|
+
unsubscribe = null;
|
|
30
|
+
}
|
|
31
|
+
frameSource = src;
|
|
32
|
+
if (src === null) batch(() => {
|
|
33
|
+
for (const d of [...active]) {
|
|
34
|
+
writeValue(d.av, d.target);
|
|
35
|
+
active.delete(d);
|
|
36
|
+
const st = internals.get(d.av);
|
|
37
|
+
if (st) st.driver = null;
|
|
38
|
+
d.settle(true);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
else if (active.size > 0) ensureLoop();
|
|
42
|
+
}
|
|
43
|
+
/** The current frame source, or `null`. */
|
|
44
|
+
function getFrameSource() {
|
|
45
|
+
return frameSource;
|
|
46
|
+
}
|
|
47
|
+
function writeValue(av, v) {
|
|
48
|
+
untrack(() => internals.get(av)?.signal.set(v));
|
|
49
|
+
}
|
|
50
|
+
function maybeSleep() {
|
|
51
|
+
if (active.size === 0 && unsubscribe) {
|
|
52
|
+
unsubscribe();
|
|
53
|
+
unsubscribe = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function ensureLoop() {
|
|
57
|
+
if (unsubscribe === null && frameSource !== null) {
|
|
58
|
+
lastNow = -1;
|
|
59
|
+
unsubscribe = frameSource(onFrame);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function onFrame(now) {
|
|
63
|
+
if (lastNow < 0) lastNow = now;
|
|
64
|
+
const dt = Math.min(Math.max(0, (now - lastNow) / 1e3), MAX_DT);
|
|
65
|
+
batch(() => {
|
|
66
|
+
for (const d of [...active]) {
|
|
67
|
+
if (!active.has(d)) continue;
|
|
68
|
+
let running;
|
|
69
|
+
try {
|
|
70
|
+
running = d.tick(now, dt);
|
|
71
|
+
} catch {
|
|
72
|
+
running = false;
|
|
73
|
+
}
|
|
74
|
+
if (!running) {
|
|
75
|
+
active.delete(d);
|
|
76
|
+
const st = internals.get(d.av);
|
|
77
|
+
if (st) st.driver = null;
|
|
78
|
+
d.settle(true);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
lastNow = now;
|
|
83
|
+
maybeSleep();
|
|
84
|
+
}
|
|
85
|
+
/** Create an {@link AnimatedValue} (a reactive number you can drive with {@link timing}/{@link spring}). */
|
|
86
|
+
function animate(initial) {
|
|
87
|
+
const s = signal(initial);
|
|
88
|
+
const state = {
|
|
89
|
+
signal: s,
|
|
90
|
+
vel: { v: 0 },
|
|
91
|
+
driver: null
|
|
92
|
+
};
|
|
93
|
+
const av = Object.assign(() => s(), {
|
|
94
|
+
set(v) {
|
|
95
|
+
stopDriver(av);
|
|
96
|
+
untrack(() => s.set(v));
|
|
97
|
+
},
|
|
98
|
+
velocity: () => state.vel.v,
|
|
99
|
+
stop() {
|
|
100
|
+
stopDriver(av);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
internals.set(av, state);
|
|
104
|
+
return av;
|
|
105
|
+
}
|
|
106
|
+
function stopDriver(av) {
|
|
107
|
+
const st = internals.get(av);
|
|
108
|
+
if (st?.driver) {
|
|
109
|
+
const d = st.driver;
|
|
110
|
+
active.delete(d);
|
|
111
|
+
st.driver = null;
|
|
112
|
+
d.settle(false);
|
|
113
|
+
maybeSleep();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Begin a driver on `av`: settle any prior driver (last-write-wins), capture the current value as
|
|
118
|
+
* the start, and either jump-to-final (no frame source) or join the loop. Returns the handle.
|
|
119
|
+
*/
|
|
120
|
+
function start(av, opts, build) {
|
|
121
|
+
const st = internals.get(av);
|
|
122
|
+
if (!st) throw new TypeError("animation driver: value was not created with animate()");
|
|
123
|
+
stopDriver(av);
|
|
124
|
+
let settled = false;
|
|
125
|
+
let resolveDone;
|
|
126
|
+
const done = new Promise((r) => {
|
|
127
|
+
resolveDone = r;
|
|
128
|
+
});
|
|
129
|
+
const settle = (finished) => {
|
|
130
|
+
if (settled) return;
|
|
131
|
+
settled = true;
|
|
132
|
+
resolveDone(finished);
|
|
133
|
+
opts.onComplete?.(finished);
|
|
134
|
+
};
|
|
135
|
+
const from = untrack(av);
|
|
136
|
+
if (frameSource === null) {
|
|
137
|
+
writeValue(av, opts.to);
|
|
138
|
+
settle(true);
|
|
139
|
+
return {
|
|
140
|
+
stop: () => settle(false),
|
|
141
|
+
done
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const tick = build(from, settle);
|
|
145
|
+
const driver = {
|
|
146
|
+
av,
|
|
147
|
+
target: opts.to,
|
|
148
|
+
tick,
|
|
149
|
+
settle
|
|
150
|
+
};
|
|
151
|
+
st.driver = driver;
|
|
152
|
+
active.add(driver);
|
|
153
|
+
if (getOwner() !== null) onCleanup(() => {
|
|
154
|
+
if (st.driver === driver) stopDriver(av);
|
|
155
|
+
});
|
|
156
|
+
ensureLoop();
|
|
157
|
+
return {
|
|
158
|
+
stop: () => stopDriver(av),
|
|
159
|
+
done
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** Animate `av` to `to` over `duration` ms with `easing` (RN `Animated.timing`). */
|
|
163
|
+
function timing(av, opts) {
|
|
164
|
+
const duration = Number.isFinite(opts.duration) ? opts.duration : DEFAULT_DURATION;
|
|
165
|
+
const easing = opts.easing ?? linear;
|
|
166
|
+
const delay = Number.isFinite(opts.delay) ? opts.delay : 0;
|
|
167
|
+
return start(av, opts, (from, _settle) => {
|
|
168
|
+
let startTime = -1;
|
|
169
|
+
let prev = from;
|
|
170
|
+
const settleAt = () => {
|
|
171
|
+
internals.get(av).vel.v = 0;
|
|
172
|
+
writeValue(av, opts.to);
|
|
173
|
+
return false;
|
|
174
|
+
};
|
|
175
|
+
return (now, dt) => {
|
|
176
|
+
if (startTime < 0) startTime = now;
|
|
177
|
+
const elapsed = now - startTime - delay;
|
|
178
|
+
if (elapsed < 0) return true;
|
|
179
|
+
const t = duration <= 0 ? 1 : Math.min(elapsed / duration, 1);
|
|
180
|
+
const next = from + (opts.to - from) * easing(t);
|
|
181
|
+
if (!Number.isFinite(next)) return settleAt();
|
|
182
|
+
if (dt > 0) internals.get(av).vel.v = (next - prev) / dt;
|
|
183
|
+
prev = next;
|
|
184
|
+
if (t >= 1) return settleAt();
|
|
185
|
+
writeValue(av, next);
|
|
186
|
+
return true;
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/** Animate `av` to `to` with spring physics (RN/Reanimated `withSpring`, Flutter `SpringSimulation`). */
|
|
191
|
+
function spring(av, opts) {
|
|
192
|
+
const stiffness = Math.max(0, opts.stiffness ?? 170);
|
|
193
|
+
const damping = Math.max(0, opts.damping ?? 26);
|
|
194
|
+
const mass = Math.max(1e-4, opts.mass ?? 1);
|
|
195
|
+
const restDelta = opts.restDelta ?? .01;
|
|
196
|
+
const restVelocity = opts.restVelocity ?? .01;
|
|
197
|
+
const omega = Math.sqrt(stiffness / mass);
|
|
198
|
+
return start(av, opts, (from) => {
|
|
199
|
+
let x = from;
|
|
200
|
+
let v = opts.velocity ?? internals.get(av).vel.v;
|
|
201
|
+
let frames = 0;
|
|
202
|
+
return (_now, dt) => {
|
|
203
|
+
frames++;
|
|
204
|
+
const steps = Math.max(1, Math.ceil(dt * omega / 1.5));
|
|
205
|
+
const sub = dt / steps;
|
|
206
|
+
for (let i = 0; i < steps; i++) {
|
|
207
|
+
const a = (-stiffness * (x - opts.to) - damping * v) / mass;
|
|
208
|
+
v += a * sub;
|
|
209
|
+
x += v * sub;
|
|
210
|
+
}
|
|
211
|
+
if (!Number.isFinite(x)) {
|
|
212
|
+
internals.get(av).vel.v = 0;
|
|
213
|
+
writeValue(av, opts.to);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
internals.get(av).vel.v = v;
|
|
217
|
+
if (Math.abs(x - opts.to) < restDelta && Math.abs(v) < restVelocity || frames > SPRING_MAX_FRAMES) {
|
|
218
|
+
internals.get(av).vel.v = 0;
|
|
219
|
+
writeValue(av, opts.to);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
writeValue(av, x);
|
|
223
|
+
return true;
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Map an accessor through a piecewise-linear range (RN `Animated.interpolate`). Returns a plain
|
|
229
|
+
* accessor, so it tracks `value` and re-reads inside the consuming style each frame — glitch-free
|
|
230
|
+
* for free. `inputRange` must be monotonically increasing and match `outputRange` length (≥2).
|
|
231
|
+
*/
|
|
232
|
+
function interpolate(value, inputRange, outputRange, opts) {
|
|
233
|
+
if (inputRange.length !== outputRange.length || inputRange.length < 2) throw new RangeError("interpolate: inputRange and outputRange must be the same length (>= 2)");
|
|
234
|
+
const extrapolate = opts?.extrapolate ?? "clamp";
|
|
235
|
+
const n = inputRange.length;
|
|
236
|
+
return () => {
|
|
237
|
+
const x = value();
|
|
238
|
+
if (Number.isNaN(x)) return outputRange[0];
|
|
239
|
+
const lerp = (i) => {
|
|
240
|
+
const x0 = inputRange[i];
|
|
241
|
+
const x1 = inputRange[i + 1];
|
|
242
|
+
const y0 = outputRange[i];
|
|
243
|
+
const y1 = outputRange[i + 1];
|
|
244
|
+
if (x1 === x0) return y0;
|
|
245
|
+
return y0 + (y1 - y0) * (x - x0) / (x1 - x0);
|
|
246
|
+
};
|
|
247
|
+
if (x <= inputRange[0]) return extrapolate === "extend" ? lerp(0) : outputRange[0];
|
|
248
|
+
if (x >= inputRange[n - 1]) return extrapolate === "extend" ? lerp(n - 2) : outputRange[n - 1];
|
|
249
|
+
for (let i = 0; i < n - 1; i++) if (x >= inputRange[i] && x <= inputRange[i + 1]) return lerp(i);
|
|
250
|
+
return outputRange[n - 1];
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** A `requestAnimationFrame`-backed {@link FrameSource} for the web (the host wires this at startup). */
|
|
254
|
+
function rafFrameSource() {
|
|
255
|
+
return (tick) => {
|
|
256
|
+
const raf = globalThis.requestAnimationFrame;
|
|
257
|
+
const caf = globalThis.cancelAnimationFrame;
|
|
258
|
+
if (!raf) return () => {};
|
|
259
|
+
let id = raf(function loop(t) {
|
|
260
|
+
tick(t);
|
|
261
|
+
id = raf(loop);
|
|
262
|
+
});
|
|
263
|
+
return () => caf?.(id);
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** A manually-driven {@link FrameSource} for deterministic tests: call `tick(nowMs)` to advance. */
|
|
267
|
+
function manualFrameSource() {
|
|
268
|
+
let cb = null;
|
|
269
|
+
return {
|
|
270
|
+
source: (t) => {
|
|
271
|
+
cb = t;
|
|
272
|
+
return () => {
|
|
273
|
+
cb = null;
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
tick: (nowMs) => cb?.(nowMs)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/** @internal Test-only: number of running animations. */
|
|
280
|
+
function _activeAnimationCount() {
|
|
281
|
+
return active.size;
|
|
282
|
+
}
|
|
283
|
+
/** @internal Test-only: reset all engine state between tests. */
|
|
284
|
+
function _resetAnimation() {
|
|
285
|
+
if (unsubscribe) unsubscribe();
|
|
286
|
+
unsubscribe = null;
|
|
287
|
+
active.clear();
|
|
288
|
+
frameSource = null;
|
|
289
|
+
lastNow = -1;
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
export { _activeAnimationCount, _resetAnimation, animate, getFrameSource, interpolate, manualFrameSource, rafFrameSource, setFrameSource, spring, timing };
|
|
293
|
+
|
|
294
|
+
//# sourceMappingURL=animation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animation.js","names":[],"sources":["../../src/animation/animation.ts"],"sourcesContent":["/**\n * The animation engine — RN `Animated`/Reanimated + Flutter `AnimationController` parity, built\n * entirely on the reactive core. An {@link AnimatedValue} **is a signal**, so reading it inside a\n * `style` accessor re-renders only that node (no renderer surface). One injected {@link FrameSource}\n * (mirroring `setReactiveScheduler`) drives a single loop that ticks every active driver inside one\n * `batch()` per frame — so a style reading several animated values recomputes once (glitch-free).\n *\n * With no frame source (SSR / headless / tests until one is wired) animations **jump to their final\n * value** synchronously: deterministic, never a hang, server output shows the end state.\n *\n * @module\n */\n\nimport { batch, getOwner, onCleanup, type Signal, signal, untrack } from '../reactive'\nimport { type Easing, linear } from './easing'\n\n/** A frame source: subscribe with a per-frame `tick(nowMs)`, get back an unsubscribe. (`requestAnimationFrame` on web, vsync on native, a manual ticker in tests.) */\nexport type FrameSource = (tick: (nowMs: number) => void) => () => void\n\n/** A running animation: stop it, or await natural completion. */\nexport interface AnimationHandle {\n /** Stop now (keeps the current value); `done` resolves `false`. Idempotent. */\n stop(): void\n /** Resolves `true` on natural completion, `false` if interrupted/stopped. Settles exactly once. */\n readonly done: Promise<boolean>\n}\n\n/** A reactive, animatable number. Call to read (tracks); `.set` jumps (untracked, stops any driver). */\nexport interface AnimatedValue {\n (): number\n /** Jump to `v` immediately, cancelling any running driver. */\n set(v: number): void\n /** The current per-frame velocity (units/sec) — seeds spring-interrupts-spring. */\n readonly velocity: () => number\n /** Stop any running driver, keeping the current value. */\n stop(): void\n}\n\ninterface Driver {\n readonly av: AnimatedValue\n /** Target value, used to jump-to-final if the frame source detaches mid-flight. */\n readonly target: number\n /** Advance by this frame; return `true` while still running. */\n tick(nowMs: number, dt: number): boolean\n /** Resolve the handle + fire onComplete exactly once. */\n settle(finished: boolean): void\n}\n\ninterface Internal {\n readonly signal: Signal<number>\n readonly vel: { v: number }\n driver: Driver | null\n}\n\nconst MAX_DT = 0.064 // clamp huge gaps (backgrounded tab / GC / breakpoint) so springs can't explode\nconst SPRING_MAX_FRAMES = 600 // ~10s @60fps: a non-converging spring fails safely instead of forever\nconst DEFAULT_DURATION = 250 // matches Atlas tokens.duration.standard (kept literal to avoid an atlas dep)\n\nconst internals = new WeakMap<AnimatedValue, Internal>()\nconst active = new Set<Driver>()\nlet frameSource: FrameSource | null = null\nlet unsubscribe: (() => void) | null = null\nlet lastNow = -1 // -1 = uninitialized (a real frame can arrive at now=0)\n\n/** Inject (or clear) the frame source that drives animations. `null` (default) → jump-to-final. */\nexport function setFrameSource(src: FrameSource | null): void {\n if (src === frameSource) return\n // Detach the current loop FIRST so any change — including a non-null→non-null swap — can never\n // leak the old subscription (which would keep driving) nor block resubscribing under the new one.\n if (unsubscribe) {\n unsubscribe()\n unsubscribe = null\n }\n frameSource = src\n if (src === null) {\n // Detaching entirely: flush every active driver to its final value so nothing is left frozen\n // (symmetric with the start-time SSR fallback).\n batch(() => {\n for (const d of [...active]) {\n writeValue(d.av, d.target)\n active.delete(d)\n const st = internals.get(d.av)\n if (st) st.driver = null\n d.settle(true)\n }\n })\n } else if (active.size > 0) {\n ensureLoop() // resubscribe in-flight animations under the new source\n }\n}\n\n/** The current frame source, or `null`. */\nexport function getFrameSource(): FrameSource | null {\n return frameSource\n}\n\nfunction writeValue(av: AnimatedValue, v: number): void {\n untrack(() => internals.get(av)?.signal.set(v))\n}\n\nfunction maybeSleep(): void {\n if (active.size === 0 && unsubscribe) {\n unsubscribe()\n unsubscribe = null\n }\n}\n\nfunction ensureLoop(): void {\n if (unsubscribe === null && frameSource !== null) {\n lastNow = -1\n unsubscribe = frameSource(onFrame)\n }\n}\n\nfunction onFrame(now: number): void {\n if (lastNow < 0) lastNow = now // first frame establishes the baseline (dt = 0, no jump)\n // Clamp to [0, MAX_DT]: never integrate backward on a non-monotonic timestamp, never explode on a\n // huge gap (backgrounded tab / GC / breakpoint).\n const dt = Math.min(Math.max(0, (now - lastNow) / 1000), MAX_DT)\n // One batch per frame: every driver's write coalesces into a single flush, so a style reading\n // multiple animated values recomputes exactly once (glitch-free).\n batch(() => {\n for (const d of [...active]) {\n // A sibling's onComplete this frame may have stopped this driver — don't tick a removed one\n // (would violate stop()'s \"keeps current value\" contract with an extra write).\n if (!active.has(d)) continue\n let running: boolean\n try {\n running = d.tick(now, dt)\n } catch {\n running = false // isolate a throwing driver (mirrors flushEffects' error isolation)\n }\n if (!running) {\n active.delete(d)\n const st = internals.get(d.av)\n if (st) st.driver = null\n d.settle(true)\n }\n }\n })\n lastNow = now\n maybeSleep()\n}\n\n/** Create an {@link AnimatedValue} (a reactive number you can drive with {@link timing}/{@link spring}). */\nexport function animate(initial: number): AnimatedValue {\n const s = signal(initial)\n const state: Internal = { signal: s, vel: { v: 0 }, driver: null }\n const av: AnimatedValue = Object.assign(() => s(), {\n set(v: number): void {\n stopDriver(av) // a manual jump cancels any running animation (RN setValue semantics)\n untrack(() => s.set(v))\n },\n velocity: () => state.vel.v,\n stop(): void {\n stopDriver(av)\n },\n })\n internals.set(av, state)\n return av\n}\n\nfunction stopDriver(av: AnimatedValue): void {\n const st = internals.get(av)\n if (st?.driver) {\n const d = st.driver\n active.delete(d)\n st.driver = null\n d.settle(false)\n maybeSleep()\n }\n}\n\n/** Options shared by drivers. */\ninterface DriverOptions {\n readonly to: number\n readonly onComplete?: (finished: boolean) => void\n}\n\n/**\n * Begin a driver on `av`: settle any prior driver (last-write-wins), capture the current value as\n * the start, and either jump-to-final (no frame source) or join the loop. Returns the handle.\n */\nfunction start(\n av: AnimatedValue,\n opts: DriverOptions,\n build: (from: number, settle: (finished: boolean) => void) => Driver['tick'],\n): AnimationHandle {\n const st = internals.get(av)\n if (!st) throw new TypeError('animation driver: value was not created with animate()')\n stopDriver(av) // last-write-wins: at most one driver per value\n\n let settled = false\n let resolveDone!: (finished: boolean) => void\n const done = new Promise<boolean>((r) => {\n resolveDone = r\n })\n const settle = (finished: boolean): void => {\n if (settled) return\n settled = true\n resolveDone(finished)\n opts.onComplete?.(finished)\n }\n\n const from = untrack(av) // start from the CURRENT rendered value (continuous retarget)\n\n // No frame source → jump to the final value synchronously (SSR / headless / not-yet-wired).\n if (frameSource === null) {\n writeValue(av, opts.to)\n settle(true)\n return { stop: () => settle(false), done }\n }\n\n const tick = build(from, settle)\n const driver: Driver = { av, target: opts.to, tick, settle }\n st.driver = driver\n active.add(driver)\n\n // Auto-stop when the owner that started the animation is disposed (unmount), so the loop never\n // writes a dead signal and never leaks a frame subscription.\n if (getOwner() !== null) {\n onCleanup(() => {\n if (st.driver === driver) stopDriver(av)\n })\n }\n\n ensureLoop()\n return {\n stop: () => stopDriver(av),\n done,\n }\n}\n\n/** Animate `av` to `to` over `duration` ms with `easing` (RN `Animated.timing`). */\nexport function timing(\n av: AnimatedValue,\n opts: {\n readonly to: number\n readonly duration?: number\n readonly easing?: Easing\n readonly delay?: number\n readonly onComplete?: (finished: boolean) => void\n },\n): AnimationHandle {\n // Sanitize: `??` only catches null/undefined, so a NaN/Infinity duration would write NaN forever\n // (permanent under Object.is) — fall back to the default for any non-finite duration.\n const duration = Number.isFinite(opts.duration) ? (opts.duration as number) : DEFAULT_DURATION\n const easing = opts.easing ?? linear\n const delay = Number.isFinite(opts.delay) ? (opts.delay as number) : 0\n return start(av, opts, (from, _settle) => {\n let startTime = -1 // -1 = uninitialized (a real frame can arrive at now=0)\n let prev = from\n const settleAt = (): boolean => {\n internals.get(av)!.vel.v = 0 // at rest: velocity is 0 (so a following spring doesn't inherit phantom momentum)\n writeValue(av, opts.to)\n return false\n }\n return (now, dt) => {\n if (startTime < 0) startTime = now\n const elapsed = now - startTime - delay\n if (elapsed < 0) return true // still in the delay window\n const t = duration <= 0 ? 1 : Math.min(elapsed / duration, 1)\n const next = from + (opts.to - from) * easing(t)\n if (!Number.isFinite(next)) return settleAt() // defensive: never write NaN/Infinity\n if (dt > 0) internals.get(av)!.vel.v = (next - prev) / dt\n prev = next\n if (t >= 1) return settleAt()\n writeValue(av, next)\n return true\n }\n })\n}\n\n/** Animate `av` to `to` with spring physics (RN/Reanimated `withSpring`, Flutter `SpringSimulation`). */\nexport function spring(\n av: AnimatedValue,\n opts: {\n readonly to: number\n readonly stiffness?: number\n readonly damping?: number\n readonly mass?: number\n readonly velocity?: number\n readonly restDelta?: number\n readonly restVelocity?: number\n readonly onComplete?: (finished: boolean) => void\n },\n): AnimationHandle {\n const stiffness = Math.max(0, opts.stiffness ?? 170)\n const damping = Math.max(0, opts.damping ?? 26)\n const mass = Math.max(1e-4, opts.mass ?? 1)\n const restDelta = opts.restDelta ?? 0.01\n const restVelocity = opts.restVelocity ?? 0.01\n const omega = Math.sqrt(stiffness / mass) // natural frequency, for substep stability\n return start(av, opts, (from) => {\n let x = from\n let v = opts.velocity ?? internals.get(av)!.vel.v\n let frames = 0\n return (_now, dt) => {\n frames++\n // Semi-implicit (symplectic) Euler is only conditionally stable (omega·dt < ~2). Stiffness is\n // user-controlled, so SUBSTEP the frame's dt to keep each step well inside the stable region —\n // a stiff spring stays finite instead of diverging to Infinity/NaN.\n const steps = Math.max(1, Math.ceil((dt * omega) / 1.5))\n const sub = dt / steps\n for (let i = 0; i < steps; i++) {\n const a = (-stiffness * (x - opts.to) - damping * v) / mass\n v += a * sub\n x += v * sub\n }\n if (!Number.isFinite(x)) {\n // Defensive: never write NaN/Infinity (permanent under Object.is) — snap to the target.\n internals.get(av)!.vel.v = 0\n writeValue(av, opts.to)\n return false\n }\n internals.get(av)!.vel.v = v\n if (\n (Math.abs(x - opts.to) < restDelta && Math.abs(v) < restVelocity) ||\n frames > SPRING_MAX_FRAMES\n ) {\n internals.get(av)!.vel.v = 0\n writeValue(av, opts.to)\n return false\n }\n writeValue(av, x)\n return true\n }\n })\n}\n\n/**\n * Map an accessor through a piecewise-linear range (RN `Animated.interpolate`). Returns a plain\n * accessor, so it tracks `value` and re-reads inside the consuming style each frame — glitch-free\n * for free. `inputRange` must be monotonically increasing and match `outputRange` length (≥2).\n */\nexport function interpolate(\n value: () => number,\n inputRange: readonly number[],\n outputRange: readonly number[],\n opts?: { readonly extrapolate?: 'clamp' | 'extend' },\n): () => number {\n if (inputRange.length !== outputRange.length || inputRange.length < 2) {\n throw new RangeError('interpolate: inputRange and outputRange must be the same length (>= 2)')\n }\n const extrapolate = opts?.extrapolate ?? 'clamp'\n const n = inputRange.length\n return () => {\n const x = value()\n if (Number.isNaN(x)) return outputRange[0] as number // a NaN source maps to the first output, not the last\n const lerp = (i: number): number => {\n const x0 = inputRange[i] as number\n const x1 = inputRange[i + 1] as number\n const y0 = outputRange[i] as number\n const y1 = outputRange[i + 1] as number\n if (x1 === x0) return y0 // zero-width segment → avoid divide-by-zero\n return y0 + ((y1 - y0) * (x - x0)) / (x1 - x0)\n }\n if (x <= (inputRange[0] as number)) {\n return extrapolate === 'extend' ? lerp(0) : (outputRange[0] as number)\n }\n if (x >= (inputRange[n - 1] as number)) {\n return extrapolate === 'extend' ? lerp(n - 2) : (outputRange[n - 1] as number)\n }\n for (let i = 0; i < n - 1; i++) {\n if (x >= (inputRange[i] as number) && x <= (inputRange[i + 1] as number)) return lerp(i)\n }\n return outputRange[n - 1] as number // unreachable for monotonic input\n }\n}\n\n/** A `requestAnimationFrame`-backed {@link FrameSource} for the web (the host wires this at startup). */\nexport function rafFrameSource(): FrameSource {\n return (tick) => {\n const raf = (globalThis as { requestAnimationFrame?: (cb: (t: number) => void) => number })\n .requestAnimationFrame\n const caf = (globalThis as { cancelAnimationFrame?: (id: number) => void }).cancelAnimationFrame\n if (!raf) return () => {} // no rAF (non-browser) → caller already degrades to jump-to-final\n let id = raf(function loop(t: number) {\n tick(t)\n id = raf(loop)\n })\n return () => caf?.(id)\n }\n}\n\n/** A manually-driven {@link FrameSource} for deterministic tests: call `tick(nowMs)` to advance. */\nexport function manualFrameSource(): { source: FrameSource; tick: (nowMs: number) => void } {\n let cb: ((nowMs: number) => void) | null = null\n return {\n source: (t) => {\n cb = t\n return () => {\n cb = null\n }\n },\n tick: (nowMs) => cb?.(nowMs),\n }\n}\n\n/** @internal Test-only: number of running animations. */\nexport function _activeAnimationCount(): number {\n return active.size\n}\n\n/** @internal Test-only: reset all engine state between tests. */\nexport function _resetAnimation(): void {\n if (unsubscribe) unsubscribe()\n unsubscribe = null\n active.clear()\n frameSource = null\n lastNow = -1\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/animation/easing.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Easing curves for the animation system. A few named curves plus a {@link cubicBezier} factory
|
|
4
|
+
* that parses a CSS `cubic-bezier(x1,y1,x2,y2)` string — so Atlas's `easing` tokens (which are such
|
|
5
|
+
* strings) map straight onto the animation engine without a circular dependency.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
/** An easing function: maps normalized time `t` in `[0,1]` to an eased progress (usually `[0,1]`). */
|
|
10
|
+
type Easing = (t: number) => number;
|
|
11
|
+
declare const linear: Easing;
|
|
12
|
+
declare const easeInQuad: Easing;
|
|
13
|
+
declare const easeOutQuad: Easing;
|
|
14
|
+
declare const easeInOutQuad: Easing;
|
|
15
|
+
declare const easeOutCubic: Easing;
|
|
16
|
+
/**
|
|
17
|
+
* Build an {@link Easing} from a CSS `cubic-bezier(x1, y1, x2, y2)` string (spaces tolerated, as in
|
|
18
|
+
* Atlas's tokens). Solves `x(t) = input` via Newton-Raphson with a bisection fallback, then returns
|
|
19
|
+
* `y(t)`. A malformed string falls back to {@link linear} — it never returns `NaN` (a `NaN` written
|
|
20
|
+
* into a signal would be permanent under `Object.is` and freeze every binding).
|
|
21
|
+
*/
|
|
22
|
+
declare function cubicBezier(css: string): Easing;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { Easing, cubicBezier, easeInOutQuad, easeInQuad, easeOutCubic, easeOutQuad, linear };
|
|
25
|
+
//# sourceMappingURL=easing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"easing.d.ts","names":[],"sources":["../../src/animation/easing.ts"],"mappings":";;AASA;;;;AAA+B;AAE/B;;KAFY,MAAA,IAAU,CAAS;AAAA,cAElB,MAAA,EAAQ,MAAiB;AAAA,cACzB,UAAA,EAAY,MAAqB;AAAA,cACjC,WAAA,EAAa,MAA2B;AAAA,cACxC,aAAA,EAAe,MAA4D;AAAA,cAC3E,YAAA,EAAc,MAAgC;;AAHb;AAC9C;;;;iBA0BgB,WAAA,CAAY,GAAA,WAAc,MAAM"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/animation/easing.ts
|
|
2
|
+
const linear = (t) => t;
|
|
3
|
+
const easeInQuad = (t) => t * t;
|
|
4
|
+
const easeOutQuad = (t) => t * (2 - t);
|
|
5
|
+
const easeInOutQuad = (t) => t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
6
|
+
const easeOutCubic = (t) => 1 - (1 - t) ** 3;
|
|
7
|
+
/** Evaluate a 1-D cubic Bézier (control points `0, a, b, 1`) at parameter `t`. */
|
|
8
|
+
function bezier(t, a, b) {
|
|
9
|
+
const c = 3 * a;
|
|
10
|
+
const bb = 3 * (b - a) - c;
|
|
11
|
+
return (((1 - c - bb) * t + bb) * t + c) * t;
|
|
12
|
+
}
|
|
13
|
+
/** Derivative of {@link bezier} w.r.t. `t` (for Newton-Raphson). */
|
|
14
|
+
function bezierSlope(t, a, b) {
|
|
15
|
+
const c = 3 * a;
|
|
16
|
+
const bb = 3 * (b - a) - c;
|
|
17
|
+
return (3 * (1 - c - bb) * t + 2 * bb) * t + c;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build an {@link Easing} from a CSS `cubic-bezier(x1, y1, x2, y2)` string (spaces tolerated, as in
|
|
21
|
+
* Atlas's tokens). Solves `x(t) = input` via Newton-Raphson with a bisection fallback, then returns
|
|
22
|
+
* `y(t)`. A malformed string falls back to {@link linear} — it never returns `NaN` (a `NaN` written
|
|
23
|
+
* into a signal would be permanent under `Object.is` and freeze every binding).
|
|
24
|
+
*/
|
|
25
|
+
function cubicBezier(css) {
|
|
26
|
+
const match = /cubic-bezier\(([^)]+)\)/.exec(css);
|
|
27
|
+
if (!match?.[1]) return linear;
|
|
28
|
+
const parts = match[1].split(",").map((s) => Number.parseFloat(s.trim()));
|
|
29
|
+
if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return linear;
|
|
30
|
+
const [x1, y1, x2, y2] = parts;
|
|
31
|
+
const solveT = (x) => {
|
|
32
|
+
let t = x;
|
|
33
|
+
for (let i = 0; i < 8; i++) {
|
|
34
|
+
const xt = bezier(t, x1, x2) - x;
|
|
35
|
+
if (Math.abs(xt) < 1e-6) return t;
|
|
36
|
+
const slope = bezierSlope(t, x1, x2);
|
|
37
|
+
if (Math.abs(slope) < 1e-6) break;
|
|
38
|
+
t -= xt / slope;
|
|
39
|
+
}
|
|
40
|
+
let lo = 0;
|
|
41
|
+
let hi = 1;
|
|
42
|
+
let mid = x;
|
|
43
|
+
for (let i = 0; i < 30; i++) {
|
|
44
|
+
mid = (lo + hi) / 2;
|
|
45
|
+
const xt = bezier(mid, x1, x2);
|
|
46
|
+
if (Math.abs(xt - x) < 1e-6) break;
|
|
47
|
+
if (xt < x) lo = mid;
|
|
48
|
+
else hi = mid;
|
|
49
|
+
}
|
|
50
|
+
return mid;
|
|
51
|
+
};
|
|
52
|
+
return (t) => {
|
|
53
|
+
if (t <= 0) return 0;
|
|
54
|
+
if (t >= 1) return 1;
|
|
55
|
+
return bezier(solveT(t), y1, y2);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { cubicBezier, easeInOutQuad, easeInQuad, easeOutCubic, easeOutQuad, linear };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=easing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"easing.js","names":[],"sources":["../../src/animation/easing.ts"],"sourcesContent":["/**\n * Easing curves for the animation system. A few named curves plus a {@link cubicBezier} factory\n * that parses a CSS `cubic-bezier(x1,y1,x2,y2)` string — so Atlas's `easing` tokens (which are such\n * strings) map straight onto the animation engine without a circular dependency.\n *\n * @module\n */\n\n/** An easing function: maps normalized time `t` in `[0,1]` to an eased progress (usually `[0,1]`). */\nexport type Easing = (t: number) => number\n\nexport const linear: Easing = (t) => t\nexport const easeInQuad: Easing = (t) => t * t\nexport const easeOutQuad: Easing = (t) => t * (2 - t)\nexport const easeInOutQuad: Easing = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)\nexport const easeOutCubic: Easing = (t) => 1 - (1 - t) ** 3\n\n/** Evaluate a 1-D cubic Bézier (control points `0, a, b, 1`) at parameter `t`. */\nfunction bezier(t: number, a: number, b: number): number {\n const c = 3 * a\n const bb = 3 * (b - a) - c\n const aa = 1 - c - bb\n return ((aa * t + bb) * t + c) * t\n}\n\n/** Derivative of {@link bezier} w.r.t. `t` (for Newton-Raphson). */\nfunction bezierSlope(t: number, a: number, b: number): number {\n const c = 3 * a\n const bb = 3 * (b - a) - c\n const aa = 1 - c - bb\n return (3 * aa * t + 2 * bb) * t + c\n}\n\n/**\n * Build an {@link Easing} from a CSS `cubic-bezier(x1, y1, x2, y2)` string (spaces tolerated, as in\n * Atlas's tokens). Solves `x(t) = input` via Newton-Raphson with a bisection fallback, then returns\n * `y(t)`. A malformed string falls back to {@link linear} — it never returns `NaN` (a `NaN` written\n * into a signal would be permanent under `Object.is` and freeze every binding).\n */\nexport function cubicBezier(css: string): Easing {\n const match = /cubic-bezier\\(([^)]+)\\)/.exec(css)\n if (!match?.[1]) return linear\n const parts = match[1].split(',').map((s) => Number.parseFloat(s.trim()))\n if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return linear\n const [x1, y1, x2, y2] = parts as [number, number, number, number]\n\n const solveT = (x: number): number => {\n let t = x\n for (let i = 0; i < 8; i++) {\n const xt = bezier(t, x1, x2) - x\n if (Math.abs(xt) < 1e-6) return t\n const slope = bezierSlope(t, x1, x2)\n if (Math.abs(slope) < 1e-6) break\n t -= xt / slope\n }\n // Bisection fallback (always converges on the monotonic [0,1] domain).\n let lo = 0\n let hi = 1\n let mid = x\n for (let i = 0; i < 30; i++) {\n mid = (lo + hi) / 2\n const xt = bezier(mid, x1, x2)\n if (Math.abs(xt - x) < 1e-6) break\n if (xt < x) lo = mid\n else hi = mid\n }\n return mid\n }\n\n return (t) => {\n if (t <= 0) return 0\n if (t >= 1) return 1\n return bezier(solveT(t), y1, y2)\n }\n}\n"],"mappings":";AAWA,MAAa,UAAkB,MAAM;AACrC,MAAa,cAAsB,MAAM,IAAI;AAC7C,MAAa,eAAuB,MAAM,KAAK,IAAI;AACnD,MAAa,iBAAyB,MAAO,IAAI,KAAM,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,KAAK;AACtF,MAAa,gBAAwB,MAAM,KAAK,IAAI,MAAM;;AAG1D,SAAS,OAAO,GAAW,GAAW,GAAmB;CACvD,MAAM,IAAI,IAAI;CACd,MAAM,KAAK,KAAK,IAAI,KAAK;CAEzB,UADW,IAAI,IAAI,MACL,IAAI,MAAM,IAAI,KAAK;AACnC;;AAGA,SAAS,YAAY,GAAW,GAAW,GAAmB;CAC5D,MAAM,IAAI,IAAI;CACd,MAAM,KAAK,KAAK,IAAI,KAAK;CAEzB,QAAQ,KADG,IAAI,IAAI,MACF,IAAI,IAAI,MAAM,IAAI;AACrC;;;;;;;AAQA,SAAgB,YAAY,KAAqB;CAC/C,MAAM,QAAQ,0BAA0B,KAAK,GAAG;CAChD,IAAI,CAAC,QAAQ,IAAI,OAAO;CACxB,MAAM,QAAQ,MAAM,GAAG,MAAM,GAAG,EAAE,KAAK,MAAM,OAAO,WAAW,EAAE,KAAK,CAAC,CAAC;CACxE,IAAI,MAAM,WAAW,KAAK,MAAM,MAAM,MAAM,OAAO,MAAM,CAAC,CAAC,GAAG,OAAO;CACrE,MAAM,CAAC,IAAI,IAAI,IAAI,MAAM;CAEzB,MAAM,UAAU,MAAsB;EACpC,IAAI,IAAI;EACR,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK;GAC1B,MAAM,KAAK,OAAO,GAAG,IAAI,EAAE,IAAI;GAC/B,IAAI,KAAK,IAAI,EAAE,IAAI,MAAM,OAAO;GAChC,MAAM,QAAQ,YAAY,GAAG,IAAI,EAAE;GACnC,IAAI,KAAK,IAAI,KAAK,IAAI,MAAM;GAC5B,KAAK,KAAK;EACZ;EAEA,IAAI,KAAK;EACT,IAAI,KAAK;EACT,IAAI,MAAM;EACV,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK;GAC3B,OAAO,KAAK,MAAM;GAClB,MAAM,KAAK,OAAO,KAAK,IAAI,EAAE;GAC7B,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,MAAM;GAC7B,IAAI,KAAK,GAAG,KAAK;QACZ,KAAK;EACZ;EACA,OAAO;CACT;CAEA,QAAQ,MAAM;EACZ,IAAI,KAAK,GAAG,OAAO;EACnB,IAAI,KAAK,GAAG,OAAO;EACnB,OAAO,OAAO,OAAO,CAAC,GAAG,IAAI,EAAE;CACjC;AACF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AnimatedValue } from "../animation/animation.js";
|
|
2
|
+
import { PanState, Recognizer } from "./recognizers.js";
|
|
3
|
+
|
|
4
|
+
//#region src/gesture/animated.d.ts
|
|
5
|
+
/** Drag `x`/`y` with a pan; on release, spring each axis to `release()`'s target (default: stay put), seeded with the gesture velocity. */
|
|
6
|
+
declare function panAnimated(x: AnimatedValue, y: AnimatedValue, opts?: {
|
|
7
|
+
/** Choose the per-axis settle target on release (e.g. snap-back to 0). Returns `void` to stay put. */readonly release?: (e: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
velocityX: number;
|
|
11
|
+
velocityY: number;
|
|
12
|
+
}) => {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
} | void;
|
|
16
|
+
readonly spring?: {
|
|
17
|
+
stiffness?: number;
|
|
18
|
+
damping?: number;
|
|
19
|
+
};
|
|
20
|
+
}): Recognizer<PanState>;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { panAnimated };
|
|
23
|
+
//# sourceMappingURL=animated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animated.d.ts","names":[],"sources":["../../src/gesture/animated.ts"],"mappings":";;;;;iBAcgB,WAAA,CACd,CAAA,EAAG,aAAA,EACH,CAAA,EAAG,aAAA,EACH,IAAA;EAUW,+GARA,OAAA,IAAW,CAAA;IAClB,CAAA;IACA,CAAA;IACA,SAAA;IACA,SAAA;EAAA;IACM,CAAA;IAAW,CAAA;EAAA;EAAA,SACV,MAAA;IAAW,SAAA;IAAoB,OAAA;EAAA;AAAA,IAEzC,UAAA,CAAW,QAAA"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { untrack } from "../reactive/reactive.js";
|
|
2
|
+
import { spring } from "../animation/animation.js";
|
|
3
|
+
import { pan } from "./recognizers.js";
|
|
4
|
+
//#region src/gesture/animated.ts
|
|
5
|
+
/**
|
|
6
|
+
* Bridge a {@link pan} gesture to {@link AnimatedValue}s — the headline interaction: drag follows
|
|
7
|
+
* the finger 1:1, and on release each axis springs to a target **seeded with the gesture's own
|
|
8
|
+
* velocity** (so a flick keeps flowing). This explicit velocity handoff is required because
|
|
9
|
+
* `AnimatedValue.set` doesn't populate the value's velocity cell.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
/** Drag `x`/`y` with a pan; on release, spring each axis to `release()`'s target (default: stay put), seeded with the gesture velocity. */
|
|
14
|
+
function panAnimated(x, y, opts) {
|
|
15
|
+
let baseX = 0;
|
|
16
|
+
let baseY = 0;
|
|
17
|
+
return pan({
|
|
18
|
+
onBegin: () => {
|
|
19
|
+
baseX = untrack(x);
|
|
20
|
+
baseY = untrack(y);
|
|
21
|
+
x.set(baseX);
|
|
22
|
+
y.set(baseY);
|
|
23
|
+
},
|
|
24
|
+
onUpdate: (e) => {
|
|
25
|
+
x.set(baseX + e.translationX);
|
|
26
|
+
y.set(baseY + e.translationY);
|
|
27
|
+
},
|
|
28
|
+
onEnd: (e) => {
|
|
29
|
+
const target = opts?.release?.({
|
|
30
|
+
x: untrack(x),
|
|
31
|
+
y: untrack(y),
|
|
32
|
+
velocityX: e.velocityX,
|
|
33
|
+
velocityY: e.velocityY
|
|
34
|
+
});
|
|
35
|
+
if (!target) return;
|
|
36
|
+
const sp = opts?.spring ?? {};
|
|
37
|
+
spring(x, {
|
|
38
|
+
to: target.x,
|
|
39
|
+
velocity: e.velocityX * 1e3,
|
|
40
|
+
...sp
|
|
41
|
+
});
|
|
42
|
+
spring(y, {
|
|
43
|
+
to: target.y,
|
|
44
|
+
velocity: e.velocityY * 1e3,
|
|
45
|
+
...sp
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { panAnimated };
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=animated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animated.js","names":[],"sources":["../../src/gesture/animated.ts"],"sourcesContent":["/**\n * Bridge a {@link pan} gesture to {@link AnimatedValue}s — the headline interaction: drag follows\n * the finger 1:1, and on release each axis springs to a target **seeded with the gesture's own\n * velocity** (so a flick keeps flowing). This explicit velocity handoff is required because\n * `AnimatedValue.set` doesn't populate the value's velocity cell.\n *\n * @module\n */\n\nimport { type AnimatedValue, spring } from '../animation'\nimport { untrack } from '../reactive'\nimport { type PanState, pan, type Recognizer } from './recognizers'\n\n/** Drag `x`/`y` with a pan; on release, spring each axis to `release()`'s target (default: stay put), seeded with the gesture velocity. */\nexport function panAnimated(\n x: AnimatedValue,\n y: AnimatedValue,\n opts?: {\n /** Choose the per-axis settle target on release (e.g. snap-back to 0). Returns `void` to stay put. */\n readonly release?: (e: {\n x: number\n y: number\n velocityX: number\n velocityY: number\n }) => { x: number; y: number } | void\n readonly spring?: { stiffness?: number; damping?: number }\n },\n): Recognizer<PanState> {\n let baseX = 0\n let baseY = 0\n return pan({\n onBegin: () => {\n baseX = untrack(x)\n baseY = untrack(y)\n // Grab mid-fling: writing the values stops any in-flight release spring immediately, so the\n // drag takes over instead of fighting the animation (AnimatedValue.set cancels its driver).\n x.set(baseX)\n y.set(baseY)\n },\n onUpdate: (e) => {\n x.set(baseX + e.translationX)\n y.set(baseY + e.translationY)\n },\n onEnd: (e) => {\n const target = opts?.release?.({\n x: untrack(x),\n y: untrack(y),\n velocityX: e.velocityX,\n velocityY: e.velocityY,\n })\n if (!target) return // stay where released\n const sp = opts?.spring ?? {}\n // px/ms → px/s for the spring integrator; seed each axis with the finger's velocity.\n spring(x, { to: target.x, velocity: e.velocityX * 1000, ...sp })\n spring(y, { to: target.y, velocity: e.velocityY * 1000, ...sp })\n },\n })\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,SAAgB,YACd,GACA,GACA,MAUsB;CACtB,IAAI,QAAQ;CACZ,IAAI,QAAQ;CACZ,OAAO,IAAI;EACT,eAAe;GACb,QAAQ,QAAQ,CAAC;GACjB,QAAQ,QAAQ,CAAC;GAGjB,EAAE,IAAI,KAAK;GACX,EAAE,IAAI,KAAK;EACb;EACA,WAAW,MAAM;GACf,EAAE,IAAI,QAAQ,EAAE,YAAY;GAC5B,EAAE,IAAI,QAAQ,EAAE,YAAY;EAC9B;EACA,QAAQ,MAAM;GACZ,MAAM,SAAS,MAAM,UAAU;IAC7B,GAAG,QAAQ,CAAC;IACZ,GAAG,QAAQ,CAAC;IACZ,WAAW,EAAE;IACb,WAAW,EAAE;GACf,CAAC;GACD,IAAI,CAAC,QAAQ;GACb,MAAM,KAAK,MAAM,UAAU,CAAC;GAE5B,OAAO,GAAG;IAAE,IAAI,OAAO;IAAG,UAAU,EAAE,YAAY;IAAM,GAAG;GAAG,CAAC;GAC/D,OAAO,GAAG;IAAE,IAAI,OAAO;IAAG,UAAU,EAAE,YAAY;IAAM,GAAG;GAAG,CAAC;EACjE;CACF,CAAC;AACH"}
|