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.
- package/README.md +50 -0
- package/dist/AnimationSequencer.d.ts +36 -0
- package/dist/AnimationSequencer.js +151 -0
- package/dist/Animator.d.ts +125 -0
- package/dist/Animator.js +306 -0
- package/dist/Spring.d.ts +47 -0
- package/dist/Spring.js +117 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +3 -0
- package/dist/react/useAnimator.d.ts +12 -0
- package/dist/react/useAnimator.js +26 -0
- package/dist/react/useSpringState.d.ts +15 -0
- package/dist/react/useSpringState.js +13 -0
- package/dist/react/useSpringValue.d.ts +15 -0
- package/dist/react/useSpringValue.js +56 -0
- package/dist/three/ThreeAnimator.d.ts +56 -0
- package/dist/three/ThreeAnimator.js +450 -0
- package/dist/three/index.d.ts +1 -0
- package/dist/three/index.js +1 -0
- package/package.json +50 -0
- package/src/AnimationSequencer.ts +193 -0
- package/src/Animator.ts +377 -0
- package/src/Spring.ts +162 -0
- package/src/index.ts +3 -0
- package/src/react/index.ts +3 -0
- package/src/react/useAnimator.ts +45 -0
- package/src/react/useSpringState.ts +22 -0
- package/src/react/useSpringValue.ts +81 -0
- package/src/three/ThreeAnimator.ts +605 -0
- package/src/three/index.ts +1 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Physics Animator
|
|
2
|
+
|
|
3
|
+
A TypeScript animation system grounded in physics with three.js and react support.
|
|
4
|
+
|
|
5
|
+
Why use this over other animation systems?
|
|
6
|
+
|
|
7
|
+
This library focuses on simplicity and correctness – I've generally run into design troubles with popular animation libraries like framer motion: for example complex framer animations become convoluted and don't correctly handled interruptions without discontinuities in velocity
|
|
8
|
+
|
|
9
|
+
For example in react, to animate opacity we could do
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
useSpringValue(
|
|
13
|
+
{ initial: 0, target: 1, duration_s: 0.8 },
|
|
14
|
+
value => el.style.opacity = value
|
|
15
|
+
)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or via state
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
const opacity = useSpringState({ initial: 0, target: 1, duration_s: 0.8 })
|
|
22
|
+
|
|
23
|
+
return <div style={opacity} />
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
It works with arrays and objects
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
const rgb = useSpringState({ initial: [0, 0, 0], target: [1, 0, 0], duration_s: 0.8 })
|
|
30
|
+
const xy = useSpringState({ initial: { x: 0, y: 0 }, target: {x: mouse.x, y: mouse.y}, duration_s: 0.1 })
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Outside of react we use the animator object
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const animator = new Animator();
|
|
37
|
+
animator.startAnimationFrameLoop();
|
|
38
|
+
|
|
39
|
+
animator.springTo(character, 'opacity', 1, { duration_s: 0.8 })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
We can animate three objects like vectors:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
animator.springTo(character, 'rotation', new Quaternion(), { duration_s: 2 })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Velocity state is stored within the animator object
|
|
49
|
+
|
|
50
|
+
Tweens are also supported
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { EventSignal } from "@haxiomic/event-signal";
|
|
2
|
+
export type Sequence = {
|
|
3
|
+
stop: () => void;
|
|
4
|
+
events: {
|
|
5
|
+
onStep: EventSignal<number>;
|
|
6
|
+
onComplete: EventSignal<void>;
|
|
7
|
+
onFinally: EventSignal<{
|
|
8
|
+
complete: boolean;
|
|
9
|
+
stepIndex: number;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
promise: Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
export declare class AnimationSequencer {
|
|
15
|
+
timeoutHandles: Array<any>;
|
|
16
|
+
intervalHandles: Array<any>;
|
|
17
|
+
sequences: Array<Sequence>;
|
|
18
|
+
constructor();
|
|
19
|
+
/**
|
|
20
|
+
* 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.
|
|
21
|
+
*
|
|
22
|
+
* @returns Sequence - a function that can be called to stop the sequence
|
|
23
|
+
*/
|
|
24
|
+
runSequence: (steps: Array<{
|
|
25
|
+
callback: () => (Promise<void> | void);
|
|
26
|
+
maxWait_ms?: number;
|
|
27
|
+
onCompleteEvent?: EventSignal<any>;
|
|
28
|
+
}>) => Sequence;
|
|
29
|
+
registerSequence: (sequence: Sequence) => void;
|
|
30
|
+
setTimeout: (callback: Function, delay: number) => number;
|
|
31
|
+
setInterval: (callback: Function, delay: number) => number;
|
|
32
|
+
stopAllTimeouts: () => void;
|
|
33
|
+
stopAllIntervals: () => void;
|
|
34
|
+
stopAllSequences: () => void;
|
|
35
|
+
stopAll: () => void;
|
|
36
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { EventSignal } from "@haxiomic/event-signal";
|
|
2
|
+
export class AnimationSequencer {
|
|
3
|
+
timeoutHandles = [];
|
|
4
|
+
intervalHandles = [];
|
|
5
|
+
sequences = [];
|
|
6
|
+
constructor() { }
|
|
7
|
+
/**
|
|
8
|
+
* 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.
|
|
9
|
+
*
|
|
10
|
+
* @returns Sequence - a function that can be called to stop the sequence
|
|
11
|
+
*/
|
|
12
|
+
runSequence = (steps) => {
|
|
13
|
+
let sequenceEvents = {
|
|
14
|
+
onStep: new EventSignal(),
|
|
15
|
+
onComplete: new EventSignal(),
|
|
16
|
+
onFinally: new EventSignal(),
|
|
17
|
+
onError: new EventSignal(),
|
|
18
|
+
};
|
|
19
|
+
let openListeners = new Set();
|
|
20
|
+
let timeoutHandles = new Array();
|
|
21
|
+
let stepIndex = 0;
|
|
22
|
+
const executeStep = (index) => {
|
|
23
|
+
try {
|
|
24
|
+
// check if we've reached the end of the sequence
|
|
25
|
+
if (index >= steps.length) {
|
|
26
|
+
sequenceEvents.onComplete.dispatch();
|
|
27
|
+
sequenceEvents.onFinally.dispatch({
|
|
28
|
+
complete: true,
|
|
29
|
+
stepIndex: steps.length - 1,
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
let step = steps[index];
|
|
34
|
+
if (!step) {
|
|
35
|
+
throw new Error(`Step at index ${index} is undefined`);
|
|
36
|
+
}
|
|
37
|
+
sequenceEvents.onStep.dispatch(index);
|
|
38
|
+
let timeoutHandle = null;
|
|
39
|
+
let completeListener = null;
|
|
40
|
+
if (step.onCompleteEvent) {
|
|
41
|
+
let listener = step.onCompleteEvent.once(() => {
|
|
42
|
+
openListeners.delete(listener);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (step.maxWait_ms) {
|
|
46
|
+
timeoutHandle = this.setTimeout(() => next(), step.maxWait_ms);
|
|
47
|
+
timeoutHandles.push(timeoutHandle);
|
|
48
|
+
}
|
|
49
|
+
if (!step.onCompleteEvent && !step.maxWait_ms) {
|
|
50
|
+
next();
|
|
51
|
+
}
|
|
52
|
+
let hasFinished = false;
|
|
53
|
+
function next() {
|
|
54
|
+
if (hasFinished)
|
|
55
|
+
return;
|
|
56
|
+
clearTimeout(timeoutHandle);
|
|
57
|
+
completeListener?.remove();
|
|
58
|
+
stepIndex++;
|
|
59
|
+
hasFinished = true;
|
|
60
|
+
executeStep(stepIndex);
|
|
61
|
+
}
|
|
62
|
+
let result = step.callback();
|
|
63
|
+
if (result['then']) {
|
|
64
|
+
result.then(() => {
|
|
65
|
+
// if no onCompleteEvent, then we can move on to the next step
|
|
66
|
+
if (!step.onCompleteEvent) {
|
|
67
|
+
next();
|
|
68
|
+
}
|
|
69
|
+
}).catch((error) => {
|
|
70
|
+
sequenceEvents.onError.dispatch(error);
|
|
71
|
+
stop();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
sequenceEvents.onError.dispatch(error);
|
|
77
|
+
stop();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
let stopped = false;
|
|
81
|
+
function stop() {
|
|
82
|
+
if (stopped)
|
|
83
|
+
return;
|
|
84
|
+
for (let listener of openListeners) {
|
|
85
|
+
listener.remove();
|
|
86
|
+
}
|
|
87
|
+
for (let handle of timeoutHandles) {
|
|
88
|
+
clearTimeout(handle);
|
|
89
|
+
}
|
|
90
|
+
sequenceEvents.onFinally.dispatch({
|
|
91
|
+
complete: false,
|
|
92
|
+
stepIndex,
|
|
93
|
+
});
|
|
94
|
+
stopped = true;
|
|
95
|
+
}
|
|
96
|
+
// promise interface
|
|
97
|
+
let promise = new Promise((resolve, reject) => {
|
|
98
|
+
sequenceEvents.onComplete.once(() => resolve());
|
|
99
|
+
sequenceEvents.onFinally.once(() => resolve());
|
|
100
|
+
sequenceEvents.onError.once((error) => reject(error));
|
|
101
|
+
});
|
|
102
|
+
let sequence = {
|
|
103
|
+
stop,
|
|
104
|
+
events: sequenceEvents,
|
|
105
|
+
promise,
|
|
106
|
+
};
|
|
107
|
+
// track sequence
|
|
108
|
+
this.sequences.push(sequence);
|
|
109
|
+
sequenceEvents.onFinally.once(() => {
|
|
110
|
+
let index = this.sequences.indexOf(sequence);
|
|
111
|
+
this.sequences.splice(index, 1);
|
|
112
|
+
});
|
|
113
|
+
// start
|
|
114
|
+
executeStep(stepIndex);
|
|
115
|
+
return sequence;
|
|
116
|
+
};
|
|
117
|
+
registerSequence = (sequence) => {
|
|
118
|
+
this.sequences.push(sequence);
|
|
119
|
+
sequence.events.onFinally.once(() => {
|
|
120
|
+
let index = this.sequences.indexOf(sequence);
|
|
121
|
+
this.sequences.splice(index, 1);
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
setTimeout = (callback, delay) => {
|
|
125
|
+
let handle = window.setTimeout(callback, delay);
|
|
126
|
+
this.timeoutHandles.push(handle);
|
|
127
|
+
return handle;
|
|
128
|
+
};
|
|
129
|
+
setInterval = (callback, delay) => {
|
|
130
|
+
let handle = window.setInterval(callback, delay);
|
|
131
|
+
this.intervalHandles.push(handle);
|
|
132
|
+
return handle;
|
|
133
|
+
};
|
|
134
|
+
stopAllTimeouts = () => {
|
|
135
|
+
this.timeoutHandles.forEach(handle => clearTimeout(handle));
|
|
136
|
+
this.timeoutHandles = [];
|
|
137
|
+
};
|
|
138
|
+
stopAllIntervals = () => {
|
|
139
|
+
this.intervalHandles.forEach(handle => clearInterval(handle));
|
|
140
|
+
this.intervalHandles = [];
|
|
141
|
+
};
|
|
142
|
+
stopAllSequences = () => {
|
|
143
|
+
this.sequences.forEach(sequence => sequence.stop());
|
|
144
|
+
this.sequences = [];
|
|
145
|
+
};
|
|
146
|
+
stopAll = () => {
|
|
147
|
+
this.stopAllTimeouts();
|
|
148
|
+
this.stopAllIntervals();
|
|
149
|
+
this.stopAllSequences();
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { EventSignal } from "@haxiomic/event-signal";
|
|
2
|
+
import { Spring, SpringParameters } from "./Spring.js";
|
|
3
|
+
declare enum AnimationType {
|
|
4
|
+
Spring = 0,
|
|
5
|
+
Tween = 1
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Physically based animation of numeric properties of objects
|
|
9
|
+
*
|
|
10
|
+
* Designed to avoid discontinuities for smooth animation in all conditions
|
|
11
|
+
*/
|
|
12
|
+
export declare class Animator {
|
|
13
|
+
onBeforeStep: EventSignal<{
|
|
14
|
+
dt_s: number;
|
|
15
|
+
}, {
|
|
16
|
+
dt_s: number;
|
|
17
|
+
}>;
|
|
18
|
+
onAfterStep: EventSignal<{
|
|
19
|
+
dt_s: number;
|
|
20
|
+
}, {
|
|
21
|
+
dt_s: number;
|
|
22
|
+
}>;
|
|
23
|
+
protected _onAnimationComplete: EventSignal<{
|
|
24
|
+
object: any;
|
|
25
|
+
field: string | number | symbol;
|
|
26
|
+
}, {
|
|
27
|
+
object: any;
|
|
28
|
+
field: string | number | symbol;
|
|
29
|
+
}>;
|
|
30
|
+
protected _onObjectAnimationsComplete: EventSignal<{
|
|
31
|
+
object: any;
|
|
32
|
+
}, {
|
|
33
|
+
object: any;
|
|
34
|
+
}>;
|
|
35
|
+
animations: Map<any, Map<string | number | symbol, {
|
|
36
|
+
target: number;
|
|
37
|
+
type: AnimationType;
|
|
38
|
+
springParams: Spring.PhysicsParameters | null;
|
|
39
|
+
tweenParams: Tween.Parameters | null;
|
|
40
|
+
step: TweenStepFn | null;
|
|
41
|
+
velocity: number;
|
|
42
|
+
}>>;
|
|
43
|
+
constructor(onBeforeStep?: (dt_s: number) => void, onAfterStep?: (dt_s: number) => void);
|
|
44
|
+
springTo<Obj, Name extends keyof Obj>(object: Obj, field: Name, target: Obj[Name] & number, params?: SpringParameters | null): void;
|
|
45
|
+
customTweenTo<Obj, Name extends keyof Obj>(object: Obj, field: Name, target: Obj[Name] & number, duration_s: number, step: TweenStepFn): void;
|
|
46
|
+
linearTo<Obj, Name extends keyof Obj>(object: Obj, field: Name, target: Obj[Name] & number, duration_s: number): void;
|
|
47
|
+
easeInOutTo<Obj, Name extends keyof Obj>(object: Obj, field: Name, target: Obj[Name] & number, duration_s: number): void;
|
|
48
|
+
easeInTo<Obj, Name extends keyof Obj>(object: Obj, field: Name, target: Obj[Name] & number, duration_s: number): void;
|
|
49
|
+
easeOutTo<Obj, Name extends keyof Obj>(object: Obj, field: Name, target: Obj[Name] & number, duration_s: number): void;
|
|
50
|
+
/**
|
|
51
|
+
* Remove animation from the object and set the field to the target value
|
|
52
|
+
*/
|
|
53
|
+
setTo<Obj, Name extends keyof Obj, T extends Obj[Name]>(object: Obj, field: Name, target: T): void;
|
|
54
|
+
onComplete<Obj, Name extends keyof Obj>(object: Obj, field: Name, callback: (object: Obj, field: Name) => void): {
|
|
55
|
+
priority: number;
|
|
56
|
+
listener: (event: {
|
|
57
|
+
object: any;
|
|
58
|
+
field: string | number | symbol;
|
|
59
|
+
}) => void;
|
|
60
|
+
remove: () => void;
|
|
61
|
+
};
|
|
62
|
+
onAllComplete<Obj>(object: Obj, callback: (object: Obj) => void, once?: 'once'): {
|
|
63
|
+
priority: number;
|
|
64
|
+
listener: (event: {
|
|
65
|
+
object: any;
|
|
66
|
+
}) => void;
|
|
67
|
+
remove: () => void;
|
|
68
|
+
};
|
|
69
|
+
private _springState;
|
|
70
|
+
step(dt_s: number): void;
|
|
71
|
+
private t_last;
|
|
72
|
+
tick(): number;
|
|
73
|
+
protected _currentLoopControl: {
|
|
74
|
+
stop: () => void;
|
|
75
|
+
start: () => void;
|
|
76
|
+
} | null;
|
|
77
|
+
/**
|
|
78
|
+
* Start the animation loop using requestAnimationFrame
|
|
79
|
+
*
|
|
80
|
+
* This will stop any existing animation loop
|
|
81
|
+
*/
|
|
82
|
+
startAnimationFrameLoop(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Start the animation loop using setTimeout
|
|
85
|
+
*
|
|
86
|
+
* This will stop any existing animation loop
|
|
87
|
+
*/
|
|
88
|
+
startIntervalLoop(interval_ms?: number): void;
|
|
89
|
+
stop(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Remove animation for this object and field if it exists
|
|
92
|
+
* Does not change the value of the field
|
|
93
|
+
*/
|
|
94
|
+
remove<T>(object: T, field: keyof T): void;
|
|
95
|
+
/**
|
|
96
|
+
* Remove all animations for this object
|
|
97
|
+
*/
|
|
98
|
+
removeObject(object: any): void;
|
|
99
|
+
/**
|
|
100
|
+
* Remove all animations
|
|
101
|
+
*/
|
|
102
|
+
removeAll(): void;
|
|
103
|
+
getVelocity<Obj, Name extends keyof Obj>(object: Obj, field: Name): number;
|
|
104
|
+
/**
|
|
105
|
+
* Creates a new map if one doesn't already exist for the given object
|
|
106
|
+
*/
|
|
107
|
+
private getObjectAnimations;
|
|
108
|
+
/**
|
|
109
|
+
* Creates a new spring if one doesn't already exist for the given object and field
|
|
110
|
+
*/
|
|
111
|
+
private getAnimationOrCreate;
|
|
112
|
+
}
|
|
113
|
+
export type TweenStepFn = (object: any, field: string | number | symbol, target: number, params: Tween.Parameters, dt_s: number) => void;
|
|
114
|
+
export declare namespace Tween {
|
|
115
|
+
type Parameters = {
|
|
116
|
+
x0: number;
|
|
117
|
+
t0_ms: number;
|
|
118
|
+
duration_s: number;
|
|
119
|
+
};
|
|
120
|
+
function linearStep(object: any, field: string | number | symbol, target: number, params: Tween.Parameters, dt_s: number): void;
|
|
121
|
+
function easeInOutStep(object: any, field: string | number | symbol, target: number, params: Tween.Parameters, dt_s: number): void;
|
|
122
|
+
function easeInStep(object: any, field: string | number | symbol, target: number, params: Tween.Parameters, dt_s: number): void;
|
|
123
|
+
function easeOutStep(object: any, field: string | number | symbol, target: number, params: Tween.Parameters, dt_s: number): void;
|
|
124
|
+
}
|
|
125
|
+
export {};
|
package/dist/Animator.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { EventSignal } from "@haxiomic/event-signal";
|
|
2
|
+
import { Spring } from "./Spring.js";
|
|
3
|
+
var AnimationType;
|
|
4
|
+
(function (AnimationType) {
|
|
5
|
+
AnimationType[AnimationType["Spring"] = 0] = "Spring";
|
|
6
|
+
AnimationType[AnimationType["Tween"] = 1] = "Tween";
|
|
7
|
+
})(AnimationType || (AnimationType = {}));
|
|
8
|
+
/**
|
|
9
|
+
* Physically based animation of numeric properties of objects
|
|
10
|
+
*
|
|
11
|
+
* Designed to avoid discontinuities for smooth animation in all conditions
|
|
12
|
+
*/
|
|
13
|
+
export class Animator {
|
|
14
|
+
onBeforeStep = new EventSignal();
|
|
15
|
+
onAfterStep = new EventSignal();
|
|
16
|
+
_onAnimationComplete = new EventSignal();
|
|
17
|
+
_onObjectAnimationsComplete = new EventSignal();
|
|
18
|
+
animations = new Map();
|
|
19
|
+
constructor(onBeforeStep, onAfterStep) {
|
|
20
|
+
if (onBeforeStep) {
|
|
21
|
+
this.onBeforeStep.addListener(e => onBeforeStep(e.dt_s));
|
|
22
|
+
}
|
|
23
|
+
if (onAfterStep) {
|
|
24
|
+
this.onAfterStep.addListener(e => onAfterStep(e.dt_s));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
springTo(object, field, target, params = { duration_s: 0.5 }) {
|
|
28
|
+
if (params != null) {
|
|
29
|
+
let spring = this.getAnimationOrCreate(object, field, AnimationType.Spring);
|
|
30
|
+
// update the target and parameters
|
|
31
|
+
spring.type = AnimationType.Spring;
|
|
32
|
+
spring.target = target;
|
|
33
|
+
spring.springParams = Spring.getPhysicsParameters(params);
|
|
34
|
+
spring.step = null;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
this.setTo(object, field, target);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
customTweenTo(object, field, target, duration_s, step) {
|
|
41
|
+
let animation = this.getAnimationOrCreate(object, field, AnimationType.Tween);
|
|
42
|
+
animation.type = AnimationType.Tween;
|
|
43
|
+
animation.target = target;
|
|
44
|
+
animation.tweenParams = {
|
|
45
|
+
x0: object[field],
|
|
46
|
+
t0_ms: performance.now(),
|
|
47
|
+
duration_s: duration_s,
|
|
48
|
+
};
|
|
49
|
+
animation.step = step;
|
|
50
|
+
}
|
|
51
|
+
linearTo(object, field, target, duration_s) {
|
|
52
|
+
this.customTweenTo(object, field, target, duration_s, Tween.linearStep);
|
|
53
|
+
}
|
|
54
|
+
easeInOutTo(object, field, target, duration_s) {
|
|
55
|
+
this.customTweenTo(object, field, target, duration_s, Tween.easeInOutStep);
|
|
56
|
+
}
|
|
57
|
+
easeInTo(object, field, target, duration_s) {
|
|
58
|
+
this.customTweenTo(object, field, target, duration_s, Tween.easeInStep);
|
|
59
|
+
}
|
|
60
|
+
easeOutTo(object, field, target, duration_s) {
|
|
61
|
+
this.customTweenTo(object, field, target, duration_s, Tween.easeOutStep);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Remove animation from the object and set the field to the target value
|
|
65
|
+
*/
|
|
66
|
+
setTo(object, field, target) {
|
|
67
|
+
this.remove(object, field);
|
|
68
|
+
object[field] = target;
|
|
69
|
+
}
|
|
70
|
+
onComplete(object, field, callback) {
|
|
71
|
+
return this._onAnimationComplete.addListener(e => {
|
|
72
|
+
if (e.object === object && e.field === field) {
|
|
73
|
+
callback(object, field);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
onAllComplete(object, callback, once) {
|
|
78
|
+
let listener = this._onObjectAnimationsComplete.addListener(e => {
|
|
79
|
+
if (e.object === object) {
|
|
80
|
+
callback(object);
|
|
81
|
+
if (once) {
|
|
82
|
+
listener.remove();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return listener;
|
|
87
|
+
}
|
|
88
|
+
_springState = { x: 0, targetX: 0, v: 0 };
|
|
89
|
+
step(dt_s) {
|
|
90
|
+
if (this.onBeforeStep.hasListeners()) {
|
|
91
|
+
this.onBeforeStep.dispatch({ dt_s });
|
|
92
|
+
}
|
|
93
|
+
let springState = this._springState;
|
|
94
|
+
// step all animations
|
|
95
|
+
this.animations.forEach((objectAnims, object) => {
|
|
96
|
+
objectAnims.forEach((animation, field) => {
|
|
97
|
+
switch (animation.type) {
|
|
98
|
+
case AnimationType.Spring:
|
|
99
|
+
{
|
|
100
|
+
// step the spring
|
|
101
|
+
springState.x = object[field];
|
|
102
|
+
springState.targetX = animation.target;
|
|
103
|
+
springState.v = animation.velocity;
|
|
104
|
+
if (animation.springParams != null) {
|
|
105
|
+
Spring.stepSpring(dt_s, springState, animation.springParams);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// instant transition: set to the target
|
|
109
|
+
springState.x = springState.targetX;
|
|
110
|
+
springState.v = 0;
|
|
111
|
+
}
|
|
112
|
+
// update the object
|
|
113
|
+
object[field] = springState.x;
|
|
114
|
+
animation.velocity = springState.v;
|
|
115
|
+
// remove the spring if it's close enough to the target and velocity is close to 0
|
|
116
|
+
if (Math.abs(springState.x - springState.targetX) < 0.0001 && Math.abs(springState.v) < 0.0001) {
|
|
117
|
+
object[field] = animation.target;
|
|
118
|
+
objectAnims.delete(field);
|
|
119
|
+
this._onAnimationComplete.dispatch({ object, field });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
case AnimationType.Tween: {
|
|
124
|
+
// step the tween
|
|
125
|
+
let x = object[field];
|
|
126
|
+
animation.step(object, field, animation.target, animation.tweenParams, dt_s);
|
|
127
|
+
let x_new = object[field];
|
|
128
|
+
animation.velocity = (x_new - x) / dt_s;
|
|
129
|
+
// remove the tween if it's complete
|
|
130
|
+
let deltaTime_s = (performance.now() - animation.tweenParams.t0_ms) / 1000;
|
|
131
|
+
if (deltaTime_s >= animation.tweenParams.duration_s) {
|
|
132
|
+
object[field] = animation.target;
|
|
133
|
+
objectAnims.delete(field);
|
|
134
|
+
this._onAnimationComplete.dispatch({ object, field });
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// remove the object if it has no more springs
|
|
141
|
+
if (objectAnims.size == 0) {
|
|
142
|
+
this.animations.delete(object);
|
|
143
|
+
this._onObjectAnimationsComplete.dispatch({ object });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
this.onAfterStep.dispatch({ dt_s });
|
|
147
|
+
}
|
|
148
|
+
t_last = -1;
|
|
149
|
+
tick() {
|
|
150
|
+
let t_s = performance.now() / 1000;
|
|
151
|
+
let dt_s = this.t_last >= 0 ? t_s - this.t_last : 1 / 60;
|
|
152
|
+
this.t_last = t_s;
|
|
153
|
+
this.step(dt_s);
|
|
154
|
+
return dt_s;
|
|
155
|
+
}
|
|
156
|
+
_currentLoopControl = null;
|
|
157
|
+
/**
|
|
158
|
+
* Start the animation loop using requestAnimationFrame
|
|
159
|
+
*
|
|
160
|
+
* This will stop any existing animation loop
|
|
161
|
+
*/
|
|
162
|
+
startAnimationFrameLoop() {
|
|
163
|
+
this.stop();
|
|
164
|
+
let frameLoopHandle = -1;
|
|
165
|
+
let frameLoop = () => {
|
|
166
|
+
this.tick();
|
|
167
|
+
frameLoopHandle = window.requestAnimationFrame(frameLoop);
|
|
168
|
+
};
|
|
169
|
+
frameLoop();
|
|
170
|
+
this._currentLoopControl = {
|
|
171
|
+
stop: () => {
|
|
172
|
+
window.cancelAnimationFrame(frameLoopHandle);
|
|
173
|
+
},
|
|
174
|
+
start: () => {
|
|
175
|
+
frameLoop();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Start the animation loop using setTimeout
|
|
181
|
+
*
|
|
182
|
+
* This will stop any existing animation loop
|
|
183
|
+
*/
|
|
184
|
+
startIntervalLoop(interval_ms = 1000 / 240) {
|
|
185
|
+
this.stop();
|
|
186
|
+
let intervalHandle = -1;
|
|
187
|
+
let intervalLoop = () => {
|
|
188
|
+
this.tick();
|
|
189
|
+
intervalHandle = window.setTimeout(intervalLoop, interval_ms);
|
|
190
|
+
};
|
|
191
|
+
intervalLoop();
|
|
192
|
+
this._currentLoopControl = {
|
|
193
|
+
stop: () => {
|
|
194
|
+
window.clearTimeout(intervalHandle);
|
|
195
|
+
},
|
|
196
|
+
start: () => {
|
|
197
|
+
intervalLoop();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
stop() {
|
|
202
|
+
if (this._currentLoopControl != null) {
|
|
203
|
+
this._currentLoopControl.stop();
|
|
204
|
+
this._currentLoopControl = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Remove animation for this object and field if it exists
|
|
209
|
+
* Does not change the value of the field
|
|
210
|
+
*/
|
|
211
|
+
remove(object, field) {
|
|
212
|
+
let objectSprings = this.animations.get(object);
|
|
213
|
+
if (objectSprings != null) {
|
|
214
|
+
objectSprings.delete(field);
|
|
215
|
+
}
|
|
216
|
+
// if there are no more springs for this object, remove it from the map
|
|
217
|
+
if (objectSprings != null && objectSprings.size == 0) {
|
|
218
|
+
this.animations.delete(object);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Remove all animations for this object
|
|
223
|
+
*/
|
|
224
|
+
removeObject(object) {
|
|
225
|
+
this.animations.delete(object);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Remove all animations
|
|
229
|
+
*/
|
|
230
|
+
removeAll() {
|
|
231
|
+
this.animations.clear();
|
|
232
|
+
}
|
|
233
|
+
getVelocity(object, field) {
|
|
234
|
+
let spring = this.getObjectAnimations(object).get(field);
|
|
235
|
+
return spring?.velocity ?? 0;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Creates a new map if one doesn't already exist for the given object
|
|
239
|
+
*/
|
|
240
|
+
getObjectAnimations(object) {
|
|
241
|
+
let objectAnimations = this.animations.get(object);
|
|
242
|
+
if (objectAnimations == null) {
|
|
243
|
+
// create
|
|
244
|
+
objectAnimations = new Map();
|
|
245
|
+
this.animations.set(object, objectAnimations);
|
|
246
|
+
}
|
|
247
|
+
return objectAnimations;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Creates a new spring if one doesn't already exist for the given object and field
|
|
251
|
+
*/
|
|
252
|
+
getAnimationOrCreate(object, field, type) {
|
|
253
|
+
let objectAnimations = this.getObjectAnimations(object);
|
|
254
|
+
let animation = objectAnimations.get(field);
|
|
255
|
+
if (animation == null) {
|
|
256
|
+
// create
|
|
257
|
+
animation = {
|
|
258
|
+
target: 0,
|
|
259
|
+
type: type,
|
|
260
|
+
springParams: null,
|
|
261
|
+
tweenParams: null,
|
|
262
|
+
velocity: 0,
|
|
263
|
+
step: null
|
|
264
|
+
};
|
|
265
|
+
objectAnimations.set(field, animation);
|
|
266
|
+
}
|
|
267
|
+
animation.type = type;
|
|
268
|
+
return animation;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
export var Tween;
|
|
272
|
+
(function (Tween) {
|
|
273
|
+
function linearStep(object, field, target, params, dt_s) {
|
|
274
|
+
let dx = target - params.x0;
|
|
275
|
+
let t = (performance.now() - params.t0_ms) / 1000;
|
|
276
|
+
let u = t / params.duration_s;
|
|
277
|
+
let x_new = params.x0 + dx * u;
|
|
278
|
+
object[field] = x_new;
|
|
279
|
+
}
|
|
280
|
+
Tween.linearStep = linearStep;
|
|
281
|
+
// cubic ease in out
|
|
282
|
+
function easeInOutStep(object, field, target, params, dt_s) {
|
|
283
|
+
let dx = target - params.x0;
|
|
284
|
+
let t = (performance.now() - params.t0_ms) / 1000;
|
|
285
|
+
let u = t / params.duration_s;
|
|
286
|
+
let x_new = params.x0 + dx * u * u * (3 - 2 * u);
|
|
287
|
+
object[field] = x_new;
|
|
288
|
+
}
|
|
289
|
+
Tween.easeInOutStep = easeInOutStep;
|
|
290
|
+
function easeInStep(object, field, target, params, dt_s) {
|
|
291
|
+
let dx = target - params.x0;
|
|
292
|
+
let t = (performance.now() - params.t0_ms) / 1000;
|
|
293
|
+
let u = t / params.duration_s;
|
|
294
|
+
let x_new = params.x0 + dx * u * u * u;
|
|
295
|
+
object[field] = x_new;
|
|
296
|
+
}
|
|
297
|
+
Tween.easeInStep = easeInStep;
|
|
298
|
+
function easeOutStep(object, field, target, params, dt_s) {
|
|
299
|
+
let dx = target - params.x0;
|
|
300
|
+
let t = (performance.now() - params.t0_ms) / 1000;
|
|
301
|
+
let u = t / params.duration_s;
|
|
302
|
+
let x_new = params.x0 + dx * (1 - Math.pow(1 - u, 3));
|
|
303
|
+
object[field] = x_new;
|
|
304
|
+
}
|
|
305
|
+
Tween.easeOutStep = easeOutStep;
|
|
306
|
+
})(Tween || (Tween = {}));
|