physics-animator 0.1.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.
@@ -0,0 +1,450 @@
1
+ import { Euler, Matrix4, Quaternion, Vector2, Vector3, Vector4 } from "three";
2
+ import { Animator, Tween } from "../Animator.js";
3
+ import { Spring } from "../Spring.js";
4
+ export var QuaternionSpringMode;
5
+ (function (QuaternionSpringMode) {
6
+ QuaternionSpringMode[QuaternionSpringMode["DirectionRollCartesian"] = 0] = "DirectionRollCartesian";
7
+ QuaternionSpringMode[QuaternionSpringMode["YawPitchRoll"] = 1] = "YawPitchRoll";
8
+ })(QuaternionSpringMode || (QuaternionSpringMode = {}));
9
+ /**
10
+ * Extends Animator to add support for animating vectors and quaternions
11
+ */
12
+ export class ThreeAnimator {
13
+ animator;
14
+ get onAfterStep() {
15
+ return this.animator.onAfterStep;
16
+ }
17
+ get onBeforeStep() {
18
+ return this.animator.onBeforeStep;
19
+ }
20
+ quaternionSprings = new Map();
21
+ constructor(animator = new Animator()) {
22
+ this.animator = animator;
23
+ this.animator.onBeforeStep.on(e => this.stepQuaternionSprings(e.dt_s));
24
+ }
25
+ setTo(object, field, target) {
26
+ if (target instanceof Vector4) {
27
+ let v = object[field];
28
+ this.animator.setTo(v, 'x', target.x);
29
+ this.animator.setTo(v, 'y', target.y);
30
+ this.animator.setTo(v, 'z', target.z);
31
+ this.animator.setTo(v, 'w', target.w);
32
+ }
33
+ else if (target instanceof Vector3) {
34
+ let v = object[field];
35
+ this.animator.setTo(v, 'x', target.x);
36
+ this.animator.setTo(v, 'y', target.y);
37
+ this.animator.setTo(v, 'z', target.z);
38
+ }
39
+ else if (target instanceof Vector2) {
40
+ let v = object[field];
41
+ this.animator.setTo(v, 'x', target.x);
42
+ this.animator.setTo(v, 'y', target.y);
43
+ }
44
+ else if (target instanceof Quaternion) {
45
+ let q = object[field];
46
+ this.animator.setTo(q, 'x', target.x);
47
+ this.animator.setTo(q, 'y', target.y);
48
+ this.animator.setTo(q, 'z', target.z);
49
+ this.animator.setTo(q, 'w', target.w);
50
+ }
51
+ else if (target instanceof Euler) {
52
+ let e = object[field];
53
+ this.animator.setTo(e, 'x', target.x);
54
+ this.animator.setTo(e, 'y', target.y);
55
+ this.animator.setTo(e, 'z', target.z);
56
+ e.order = target.order;
57
+ }
58
+ else { // number
59
+ this.animator.setTo(object, field, target);
60
+ }
61
+ }
62
+ springTo(object, field, target, params = { duration_s: 0.5 }, mode) {
63
+ if (target instanceof Vector4) {
64
+ let v = object[field];
65
+ this.animator.springTo(v, 'x', target.x, params);
66
+ this.animator.springTo(v, 'y', target.y, params);
67
+ this.animator.springTo(v, 'z', target.z, params);
68
+ this.animator.springTo(v, 'w', target.w, params);
69
+ }
70
+ else if (target instanceof Vector3) {
71
+ let v = object[field];
72
+ this.animator.springTo(v, 'x', target.x, params);
73
+ this.animator.springTo(v, 'y', target.y, params);
74
+ this.animator.springTo(v, 'z', target.z, params);
75
+ }
76
+ else if (target instanceof Vector2) {
77
+ let v = object[field];
78
+ this.animator.springTo(v, 'x', target.x, params);
79
+ this.animator.springTo(v, 'y', target.y, params);
80
+ }
81
+ else if (target instanceof Quaternion) {
82
+ let q = object[field];
83
+ let spring = this.getQuaternionSpring(q);
84
+ // update
85
+ spring.target.copy(target).normalize();
86
+ spring.params = Spring.getPhysicsParameters(params);
87
+ spring.mode = mode ?? QuaternionSpringMode.DirectionRollCartesian;
88
+ }
89
+ else if (target instanceof Euler) {
90
+ let e = object[field];
91
+ this.animator.springTo(e, 'x', target.x, params);
92
+ this.animator.springTo(e, 'y', target.y, params);
93
+ this.animator.springTo(e, 'z', target.z, params);
94
+ e.order = target.order;
95
+ }
96
+ else { // number
97
+ this.animator.springTo(object, field, target, params);
98
+ }
99
+ }
100
+ customTweenTo(object, field, target, duration_s, step) {
101
+ if (target instanceof Vector4) {
102
+ let v = object[field];
103
+ this.animator.customTweenTo(v, 'x', target.x, duration_s, step);
104
+ this.animator.customTweenTo(v, 'y', target.y, duration_s, step);
105
+ this.animator.customTweenTo(v, 'z', target.z, duration_s, step);
106
+ this.animator.customTweenTo(v, 'w', target.w, duration_s, step);
107
+ }
108
+ else if (target instanceof Vector3) {
109
+ let v = object[field];
110
+ this.animator.customTweenTo(v, 'x', target.x, duration_s, step);
111
+ this.animator.customTweenTo(v, 'y', target.y, duration_s, step);
112
+ this.animator.customTweenTo(v, 'z', target.z, duration_s, step);
113
+ }
114
+ else if (target instanceof Vector2) {
115
+ let v = object[field];
116
+ this.animator.customTweenTo(v, 'x', target.x, duration_s, step);
117
+ this.animator.customTweenTo(v, 'y', target.y, duration_s, step);
118
+ }
119
+ else if (target instanceof Quaternion) {
120
+ throw new Error('Quaternion customTweenTo not yet supported, try springTo or use Euler');
121
+ }
122
+ else if (target instanceof Euler) {
123
+ let e = object[field];
124
+ this.animator.customTweenTo(e, 'x', target.x, duration_s, step);
125
+ this.animator.customTweenTo(e, 'y', target.y, duration_s, step);
126
+ this.animator.customTweenTo(e, 'z', target.z, duration_s, step);
127
+ e.order = target.order;
128
+ }
129
+ else { // number
130
+ this.animator.customTweenTo(object, field, target, duration_s, step);
131
+ }
132
+ }
133
+ linearTo(object, field, target, duration_s) {
134
+ this.customTweenTo(object, field, target, duration_s, Tween.linearStep);
135
+ }
136
+ easeInOutTo(object, field, target, duration_s) {
137
+ this.customTweenTo(object, field, target, duration_s, Tween.easeInOutStep);
138
+ }
139
+ easeInTo(object, field, target, duration_s) {
140
+ this.customTweenTo(object, field, target, duration_s, Tween.easeInStep);
141
+ }
142
+ easeOutTo(object, field, target, duration_s) {
143
+ this.customTweenTo(object, field, target, duration_s, Tween.easeOutStep);
144
+ }
145
+ step(dt_s) {
146
+ this.animator.step(dt_s);
147
+ }
148
+ tick() {
149
+ this.animator.tick();
150
+ }
151
+ remove(object, field) {
152
+ let v = object[field];
153
+ if (v instanceof Vector4) {
154
+ this.animator.remove(v, 'x');
155
+ this.animator.remove(v, 'y');
156
+ this.animator.remove(v, 'z');
157
+ this.animator.remove(v, 'w');
158
+ }
159
+ else if (v instanceof Vector3) {
160
+ this.animator.remove(v, 'x');
161
+ this.animator.remove(v, 'y');
162
+ this.animator.remove(v, 'z');
163
+ }
164
+ else if (v instanceof Vector2) {
165
+ this.animator.remove(v, 'x');
166
+ this.animator.remove(v, 'y');
167
+ }
168
+ else if (v instanceof Quaternion) {
169
+ this.quaternionSprings.delete(v);
170
+ }
171
+ else if (v instanceof Euler) {
172
+ this.animator.remove(v, 'x');
173
+ this.animator.remove(v, 'y');
174
+ this.animator.remove(v, 'z');
175
+ }
176
+ else { // number
177
+ this.animator.remove(object, field);
178
+ }
179
+ }
180
+ removeAll() {
181
+ this.animator.removeAll();
182
+ this.quaternionSprings.clear();
183
+ }
184
+ getVelocity(object, field, into) {
185
+ let target = object[field];
186
+ if (target instanceof Vector4) {
187
+ let i = into ?? new Vector4();
188
+ i.x = this.animator.getVelocity(target, 'x');
189
+ i.y = this.animator.getVelocity(target, 'y');
190
+ i.z = this.animator.getVelocity(target, 'z');
191
+ i.w = this.animator.getVelocity(target, 'w');
192
+ return i;
193
+ }
194
+ else if (target instanceof Vector3) {
195
+ let i = into ?? new Vector3();
196
+ i.x = this.animator.getVelocity(target, 'x');
197
+ i.y = this.animator.getVelocity(target, 'y');
198
+ i.z = this.animator.getVelocity(target, 'z');
199
+ return i;
200
+ }
201
+ else if (target instanceof Vector2) {
202
+ let i = into ?? new Vector2();
203
+ i.x = this.animator.getVelocity(target, 'x');
204
+ i.y = this.animator.getVelocity(target, 'y');
205
+ return i;
206
+ }
207
+ else if (target instanceof Quaternion) {
208
+ let spring = this.quaternionSprings.get(target);
209
+ return {
210
+ directionVelocity: spring?.directionVelocity ?? new Vector3(),
211
+ rollVelocity: spring?.rollVelocity ?? 0
212
+ };
213
+ }
214
+ else if (target instanceof Euler) {
215
+ let i = into ?? new Euler();
216
+ i.x = this.animator.getVelocity(target, 'x');
217
+ i.y = this.animator.getVelocity(target, 'y');
218
+ i.z = this.animator.getVelocity(target, 'z');
219
+ i.order = target.order;
220
+ return i;
221
+ }
222
+ else { // number
223
+ return this.animator.getVelocity(object, field);
224
+ }
225
+ }
226
+ startAnimationFrameLoop() {
227
+ return this.animator.startAnimationFrameLoop();
228
+ }
229
+ startIntervalLoop(interval_ms) {
230
+ return this.animator.startIntervalLoop(interval_ms);
231
+ }
232
+ stop() {
233
+ this.animator.stop();
234
+ }
235
+ stepQuaternionSprings(dt_s) {
236
+ // step quaternion springs
237
+ this.quaternionSprings.forEach((spring, q) => {
238
+ if (spring.params) {
239
+ if (spring.mode === QuaternionSpringMode.DirectionRollCartesian) {
240
+ stepSpringQuaternion(dt_s, spring, spring.params);
241
+ }
242
+ else {
243
+ stepSpringQuaternionSpherical(dt_s, spring, spring.params);
244
+ }
245
+ }
246
+ else {
247
+ // copy target
248
+ q.copy(spring.target);
249
+ // zero velocity
250
+ spring.directionVelocity.set(0, 0, 0);
251
+ spring.rollVelocity = 0;
252
+ }
253
+ // if quaternions match and velocity close to zero, remove spring
254
+ if (Math.abs(q.dot(spring.target)) > 0.999 && spring.directionVelocity.lengthSq() < 0.0001 && Math.abs(spring.rollVelocity) < 0.0001) {
255
+ this.quaternionSprings.delete(q);
256
+ }
257
+ });
258
+ }
259
+ getQuaternionSpring(q) {
260
+ let spring = this.quaternionSprings.get(q);
261
+ if (!spring) {
262
+ _m.makeRotationFromQuaternion(q);
263
+ let direction = new Vector3();
264
+ _m.extractBasis(new Vector3(), new Vector3(), direction);
265
+ spring = {
266
+ q: q,
267
+ target: new Quaternion(),
268
+ direction: direction,
269
+ directionVelocity: new Vector3(),
270
+ rollVelocity: 0,
271
+ params: null,
272
+ mode: QuaternionSpringMode.DirectionRollCartesian,
273
+ };
274
+ this.quaternionSprings.set(q, spring);
275
+ }
276
+ return spring;
277
+ }
278
+ }
279
+ /**
280
+ * Analytic quaternion spring
281
+ *
282
+ * Todo:
283
+ * - for cameras we want to prefer rotations in xz plane rather than z
284
+ * - animate direction in spherical space rather than cartesian
285
+ */
286
+ // working variables to avoid allocations
287
+ const _m = new Matrix4();
288
+ const _x = new Vector3();
289
+ const _y = new Vector3();
290
+ const _z = new Vector3();
291
+ const _qMatrix = new Matrix4();
292
+ const _springState = { x: 0, v: 0, targetX: 0 };
293
+ function stepSpringQuaternion(dt_s, state, parameters) {
294
+ // step direction spring in cartesian space
295
+ // we should do this in spherical in the future
296
+ let targetDirection = new Vector3();
297
+ let targetYBasis = new Vector3();
298
+ _m.makeRotationFromQuaternion(state.target);
299
+ _m.extractBasis(_x, targetYBasis, targetDirection);
300
+ let directionBefore = state.direction.clone();
301
+ // step spring direction
302
+ _springState.x = state.direction.x;
303
+ _springState.v = state.directionVelocity.x;
304
+ _springState.targetX = targetDirection.x;
305
+ Spring.stepSpring(dt_s, _springState, parameters);
306
+ state.direction.x = _springState.x;
307
+ state.directionVelocity.x = _springState.v;
308
+ _springState.x = state.direction.y;
309
+ _springState.v = state.directionVelocity.y;
310
+ _springState.targetX = targetDirection.y;
311
+ Spring.stepSpring(dt_s, _springState, parameters);
312
+ state.direction.y = _springState.x;
313
+ state.directionVelocity.y = _springState.v;
314
+ _springState.x = state.direction.z;
315
+ _springState.v = state.directionVelocity.z;
316
+ _springState.targetX = targetDirection.z;
317
+ Spring.stepSpring(dt_s, _springState, parameters);
318
+ state.direction.z = _springState.x;
319
+ state.directionVelocity.z = _springState.v;
320
+ // update quaternion
321
+ let directionDeltaQuaternion = new Quaternion().setFromUnitVectors(_x.copy(directionBefore).normalize(), _y.copy(state.direction).normalize());
322
+ state.q.premultiply(directionDeltaQuaternion);
323
+ // this forces synchronization of direction and quaternion
324
+ alignQuaternion(state.q, state.direction, _qMatrix);
325
+ // determine roll required to align yBasis with targetYBasis
326
+ let directionToTargetQuaternion = new Quaternion().setFromUnitVectors(state.direction.clone().normalize(), targetDirection);
327
+ _m.makeRotationFromQuaternion(state.q);
328
+ _m.extractBasis(_x, _y, _z);
329
+ let newYBasis = _y.applyQuaternion(directionToTargetQuaternion).clone();
330
+ let rollQuaternion = new Quaternion().setFromUnitVectors(newYBasis, targetYBasis);
331
+ // to axis angle (clamp w)
332
+ let rollAngle = Math.acos(Math.min(1, Math.max(-1, rollQuaternion.w))) * 2;
333
+ let rollSign = _x.crossVectors(newYBasis, targetYBasis).dot(targetDirection) < 0 ? -1 : 1;
334
+ rollAngle = -rollSign * rollAngle;
335
+ // step roll spring
336
+ _springState.x = rollAngle;
337
+ _springState.v = state.rollVelocity;
338
+ _springState.targetX = 0;
339
+ Spring.stepSpring(dt_s, _springState, parameters);
340
+ state.rollVelocity = _springState.v;
341
+ let rollAfter = _springState.x;
342
+ let rollDelta = rollAfter - rollAngle;
343
+ // apply roll correction
344
+ let rollDeltaQuaternion = new Quaternion().setFromAxisAngle(state.direction.clone().normalize(), rollDelta /*-rollAngle * 0.1*/);
345
+ state.q.premultiply(rollDeltaQuaternion);
346
+ state.q.normalize();
347
+ }
348
+ function stepSpringQuaternionSpherical(dt_s, state, parameters) {
349
+ let azimuthVelocity = state.directionVelocity.x;
350
+ let elevationVelocity = state.directionVelocity.y;
351
+ // get quaternion in spherical coordinates
352
+ let elAzRoll = quaternionToPitchYawRoll(state.q);
353
+ // get target quaternion in spherical coordinates
354
+ let targetElAzRoll = quaternionToPitchYawRoll(state.target);
355
+ // step springs
356
+ _springState.x = elAzRoll.x;
357
+ _springState.v = elevationVelocity;
358
+ _springState.targetX = getAngleContinuous(targetElAzRoll.x, elAzRoll.x);
359
+ Spring.stepSpring(dt_s, _springState, parameters);
360
+ elevationVelocity = _springState.v;
361
+ let elevationAfter = _springState.x;
362
+ _springState.x = elAzRoll.y;
363
+ _springState.v = azimuthVelocity;
364
+ _springState.targetX = getAngleContinuous(targetElAzRoll.y, elAzRoll.y);
365
+ Spring.stepSpring(dt_s, _springState, parameters);
366
+ azimuthVelocity = _springState.v;
367
+ let azimuthAfter = _springState.x;
368
+ // update directionVelocity
369
+ state.directionVelocity.x = azimuthVelocity;
370
+ state.directionVelocity.y = elevationVelocity;
371
+ // compose quaternion from spherical coordinates
372
+ // direction from azimuth and elevation
373
+ let direction = new Vector3(Math.cos(azimuthAfter) * Math.cos(elevationAfter), Math.sin(elevationAfter), Math.sin(azimuthAfter) * Math.cos(elevationAfter)).normalize();
374
+ // roll alignment spring
375
+ _springState.x = elAzRoll.z;
376
+ _springState.v = state.rollVelocity;
377
+ _springState.targetX = getAngleContinuous(targetElAzRoll.z, elAzRoll.z);
378
+ Spring.stepSpring(dt_s, _springState, parameters);
379
+ state.rollVelocity = _springState.v;
380
+ let rollAfter = _springState.x;
381
+ // compose quaternion from direction and roll
382
+ alignQuaternion(state.q, direction, _qMatrix);
383
+ setRoll(state.q, rollAfter);
384
+ }
385
+ function quaternionToPitchYawRoll(q, out = new Vector3()) {
386
+ // // get quaternion in spherical coordinates
387
+ _m.makeRotationFromQuaternion(q);
388
+ _m.extractBasis(_x, _y, _z);
389
+ // // azimuth and elevation are found from z direction
390
+ let azimuth = Math.atan2(_z.z, _z.x);
391
+ let elevation = Math.atan2(_z.y, Math.sqrt(_z.x * _z.x + _z.z * _z.z));
392
+ let roll = getRoll(q);
393
+ out.set(elevation, azimuth, roll);
394
+ return out;
395
+ }
396
+ function setRoll(q, roll) {
397
+ let currentRoll_rad = getRoll(q);
398
+ let deltaRoll = roll - currentRoll_rad;
399
+ let objectForward = new Vector3(0, 0, -1).applyQuaternion(q);
400
+ let rotation = new Quaternion().setFromAxisAngle(objectForward, deltaRoll);
401
+ q.premultiply(rotation);
402
+ }
403
+ /**
404
+ * Aligns quaternion
405
+ */
406
+ const _zBasis = new Vector3();
407
+ function alignQuaternion(q, direction, outMatrix = new Matrix4()) {
408
+ outMatrix.makeRotationFromQuaternion(q);
409
+ outMatrix.extractBasis(_x, _y, _z);
410
+ // must ensure _x and _y are orthogonal to zBasis
411
+ _zBasis.copy(direction).normalize();
412
+ _x.crossVectors(_y, _zBasis).normalize();
413
+ _y.crossVectors(_zBasis, _x).normalize();
414
+ outMatrix.makeBasis(_x, _y, _zBasis);
415
+ q.setFromRotationMatrix(outMatrix);
416
+ return outMatrix;
417
+ }
418
+ function getAngleContinuous(a, lastAngle) {
419
+ const tau = 2 * Math.PI;
420
+ let u = a / tau + 0.5;
421
+ let uLast = fract(lastAngle / tau + 0.5);
422
+ let du = u - uLast;
423
+ let angle;
424
+ if (Math.abs(du) < 0.5) {
425
+ angle = lastAngle + du * tau;
426
+ }
427
+ else {
428
+ // passed through 0
429
+ let duSmall = 1 - Math.abs(du);
430
+ angle = lastAngle + -Math.sign(du) * duSmall * tau;
431
+ }
432
+ return angle;
433
+ }
434
+ function fract(x) {
435
+ return x - Math.floor(x);
436
+ }
437
+ function getRoll(quaternion) {
438
+ return getQuaternionPlaneAngle(quaternion, new Vector3(0, 1, 0), new Vector3(0, 0, -1));
439
+ }
440
+ function getQuaternionPlaneAngle(quaternion, basisDirection, basisPlane) {
441
+ let objectDirection = basisDirection.clone().applyQuaternion(quaternion);
442
+ let objectPlane = basisPlane.clone().applyQuaternion(quaternion);
443
+ let objectDirectionProjected = objectDirection.projectOnPlane(objectPlane);
444
+ let worldZeroProjected = basisDirection.clone().projectOnPlane(objectPlane);
445
+ let angle = worldZeroProjected.angleTo(objectDirectionProjected);
446
+ // sign of angle
447
+ let sign = Math.sign(worldZeroProjected.cross(objectDirectionProjected).dot(objectPlane));
448
+ angle *= sign;
449
+ return angle;
450
+ }
@@ -0,0 +1 @@
1
+ export * from './ThreeAnimator.js';
@@ -0,0 +1 @@
1
+ export * from './ThreeAnimator.js';
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "physics-animator",
3
+ "version": "0.1.0",
4
+ "author": "haxiomic (George Corney)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "description": "A TypeScript animation system grounded in physics with three.js and react support.",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./*": {
16
+ "types": "./dist/*/index.d.ts",
17
+ "import": "./dist/*/index.js"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "typecheck": "tsc --noEmit",
22
+ "prepack": "rm -rf dist && npm run build",
23
+ "build": "tsc",
24
+ "dev": "tsc --watch"
25
+ },
26
+ "devDependencies": {
27
+ "@types/react": "^19.0.12",
28
+ "typescript": "^5.0.0"
29
+ },
30
+ "dependencies": {
31
+ "@haxiomic/event-signal": "^1.0.0",
32
+ "use-initializer": "^1.0.2"
33
+ },
34
+ "peerDependencies": {
35
+ "@types/three": "x.x.x",
36
+ "react": ">=18.2.0",
37
+ "three": "x.x.x"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@types/three": {
41
+ "optional": true
42
+ },
43
+ "three": {
44
+ "optional": true
45
+ },
46
+ "react": {
47
+ "optional": true
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,193 @@
1
+ import { EventSignal } from "@haxiomic/event-signal";
2
+
3
+ export type Sequence = {
4
+ stop: () => void,
5
+ events: {
6
+ onStep: EventSignal<number>,
7
+ onComplete: EventSignal<void>,
8
+ onFinally: EventSignal<{ complete: boolean, stepIndex: number }>,
9
+ },
10
+ promise: Promise<void>,
11
+ };
12
+
13
+ export class AnimationSequencer {
14
+
15
+ timeoutHandles: Array<any> = [];
16
+ intervalHandles: Array<any> = [];
17
+ sequences: Array<Sequence> = [];
18
+
19
+ constructor() {}
20
+
21
+ /**
22
+ * Execute a serial sequence of steps, firing a callback at each step. We either wait for the onCompleteEvent to be fired, or we wait for maxWait milliseconds before moving on to the next step.
23
+ *
24
+ * @returns Sequence - a function that can be called to stop the sequence
25
+ */
26
+ runSequence = (steps: Array<{
27
+ callback: () => (Promise<void> | void),
28
+ maxWait_ms?: number,
29
+ onCompleteEvent?: EventSignal<any>
30
+ }>): Sequence => {
31
+ let sequenceEvents = {
32
+ onStep: new EventSignal<number>(),
33
+ onComplete: new EventSignal<void>(),
34
+ onFinally: new EventSignal<{ complete: boolean, stepIndex: number }>(),
35
+ onError: new EventSignal<any>(),
36
+ }
37
+
38
+ let openListeners = new Set<{ remove: () => void }>();
39
+ let timeoutHandles = new Array<any>();
40
+
41
+ let stepIndex = 0;
42
+
43
+ const executeStep = (index: number) => {
44
+ try {
45
+ // check if we've reached the end of the sequence
46
+ if (index >= steps.length) {
47
+ sequenceEvents.onComplete.dispatch();
48
+ sequenceEvents.onFinally.dispatch({
49
+ complete: true,
50
+ stepIndex: steps.length - 1,
51
+ });
52
+ return;
53
+ }
54
+
55
+ let step = steps[index];
56
+ if (!step) {
57
+ throw new Error(`Step at index ${index} is undefined`);
58
+ }
59
+
60
+ sequenceEvents.onStep.dispatch(index);
61
+
62
+ let timeoutHandle: any = null;
63
+ let completeListener: { remove: () => void } | null = null;
64
+
65
+ if (step.onCompleteEvent) {
66
+ let listener = step.onCompleteEvent.once(() => {
67
+ openListeners.delete(listener);
68
+ });
69
+ }
70
+
71
+ if (step.maxWait_ms) {
72
+ timeoutHandle = this.setTimeout(() => next(), step.maxWait_ms);
73
+ timeoutHandles.push(timeoutHandle);
74
+ }
75
+
76
+ if (!step.onCompleteEvent && !step.maxWait_ms) {
77
+ next();
78
+ }
79
+
80
+ let hasFinished = false;
81
+ function next() {
82
+ if (hasFinished) return;
83
+
84
+ clearTimeout(timeoutHandle);
85
+ completeListener?.remove();
86
+
87
+ stepIndex++;
88
+ hasFinished = true;
89
+ executeStep(stepIndex);
90
+ }
91
+
92
+ let result = step.callback();
93
+ if ((result as any)['then']) {
94
+ (result as Promise<any>).then(() => {
95
+ // if no onCompleteEvent, then we can move on to the next step
96
+ if (!step.onCompleteEvent) {
97
+ next();
98
+ }
99
+ }).catch((error) => {
100
+ sequenceEvents.onError.dispatch(error);
101
+ stop();
102
+ });
103
+ }
104
+ } catch (error) {
105
+ sequenceEvents.onError.dispatch(error);
106
+ stop();
107
+ }
108
+ }
109
+
110
+ let stopped = false;
111
+ function stop() {
112
+ if (stopped) return;
113
+ for (let listener of openListeners) {
114
+ listener.remove();
115
+ }
116
+ for (let handle of timeoutHandles) {
117
+ clearTimeout(handle);
118
+ }
119
+ sequenceEvents.onFinally.dispatch({
120
+ complete: false,
121
+ stepIndex,
122
+ });
123
+ stopped = true;
124
+ }
125
+
126
+ // promise interface
127
+ let promise = new Promise<void>((resolve, reject) => {
128
+ sequenceEvents.onComplete.once(() => resolve());
129
+ sequenceEvents.onFinally.once(() => resolve());
130
+ sequenceEvents.onError.once((error) => reject(error));
131
+ });
132
+
133
+ let sequence = {
134
+ stop,
135
+ events: sequenceEvents,
136
+ promise,
137
+ }
138
+
139
+ // track sequence
140
+ this.sequences.push(sequence);
141
+ sequenceEvents.onFinally.once(() => {
142
+ let index = this.sequences.indexOf(sequence);
143
+ this.sequences.splice(index, 1);
144
+ });
145
+
146
+ // start
147
+ executeStep(stepIndex);
148
+
149
+ return sequence;
150
+ }
151
+
152
+ registerSequence = (sequence: Sequence) => {
153
+ this.sequences.push(sequence);
154
+ sequence.events.onFinally.once(() => {
155
+ let index = this.sequences.indexOf(sequence);
156
+ this.sequences.splice(index, 1);
157
+ });
158
+ }
159
+
160
+ setTimeout = (callback: Function, delay: number) => {
161
+ let handle = window.setTimeout(callback, delay);
162
+ this.timeoutHandles.push(handle);
163
+ return handle;
164
+ }
165
+
166
+ setInterval = (callback: Function, delay: number) => {
167
+ let handle = window.setInterval(callback, delay);
168
+ this.intervalHandles.push(handle);
169
+ return handle;
170
+ }
171
+
172
+ stopAllTimeouts = () => {
173
+ this.timeoutHandles.forEach(handle => clearTimeout(handle));
174
+ this.timeoutHandles = [];
175
+ }
176
+
177
+ stopAllIntervals = () => {
178
+ this.intervalHandles.forEach(handle => clearInterval(handle));
179
+ this.intervalHandles = [];
180
+ }
181
+
182
+ stopAllSequences = () => {
183
+ this.sequences.forEach(sequence => sequence.stop());
184
+ this.sequences = [];
185
+ }
186
+
187
+ stopAll = () => {
188
+ this.stopAllTimeouts();
189
+ this.stopAllIntervals();
190
+ this.stopAllSequences();
191
+ }
192
+
193
+ }