physics-animator 0.2.1 → 0.9.1

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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  A TypeScript animation system grounded in physics with three.js and react support.
4
4
 
5
+ <div align="center">
6
+ <a href="https://haxiomic.github.io/physics-animator/">
7
+ <img width="759" alt="Screenshot 2025-06-19 at 10 56 58" src="https://github.com/user-attachments/assets/3c8c4684-75fd-491f-a4ab-ece9aca4c327" />
8
+ </a>
9
+ </div>
10
+
5
11
  Why use this over other animation systems?
6
12
 
7
13
  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
@@ -20,7 +26,7 @@ Or via state
20
26
  ```tsx
21
27
  const opacity = useSpringState({ initial: 0, target: 1, duration_s: 0.8 })
22
28
 
23
- return <div style={opacity} />
29
+ return <div style={{ opacity }} />
24
30
  ```
25
31
 
26
32
  It works with arrays and objects
@@ -36,15 +42,24 @@ Outside of react we use the animator object
36
42
  const animator = new Animator();
37
43
  animator.startAnimationFrameLoop();
38
44
 
39
- animator.springTo(character, 'opacity', 1, { duration_s: 0.8 })
45
+ animator.springTo(character, { opacity: 1 }, { duration_s: 0.8, bounce: 1 })
46
+ ```
47
+
48
+ We can animate nested fields
49
+
50
+ ```ts
51
+ animator.springTo(character, { position: { x, y, z } }, { duration_s: 2 })
40
52
  ```
41
53
 
42
- We can animate three objects like vectors:
54
+ And get notified of changes, even if they're nested within the object
43
55
 
44
56
  ```ts
45
- animator.springTo(character, 'rotation', new Quaternion(), { duration_s: 2 })
57
+ animator.springTo(character.color, { rgb: [1, 0, 0] }, { duration_s: 2 })
58
+
59
+ // will fire if any property within character is changed by the animator
60
+ animator.onChange(character, () => render());
46
61
  ```
47
62
 
48
63
  Velocity state is stored within the animator object
49
64
 
50
- Tweens are also supported
65
+ Tweens are also supported
@@ -1,13 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Tween = exports.Animator = void 0;
3
+ exports.Animator = void 0;
4
4
  const event_signal_1 = require("@haxiomic/event-signal");
5
- const Spring_js_1 = require("./Spring.js");
6
- var AnimationType;
7
- (function (AnimationType) {
8
- AnimationType[AnimationType["Spring"] = 0] = "Spring";
9
- AnimationType[AnimationType["Tween"] = 1] = "Tween";
10
- })(AnimationType || (AnimationType = {}));
5
+ const IFieldAnimator_js_1 = require("./IFieldAnimator.js");
6
+ const SpringAnimator_js_1 = require("./animators/SpringAnimator.js");
7
+ const TweenAnimator_js_1 = require("./animators/TweenAnimator.js");
11
8
  /**
12
9
  * Physically based animation of numeric properties of objects
13
10
  *
@@ -15,73 +12,74 @@ var AnimationType;
15
12
  */
16
13
  class Animator {
17
14
  constructor(onBeforeStep, onAfterStep) {
18
- this.onBeforeStep = new event_signal_1.EventSignal();
19
- this.onAfterStep = new event_signal_1.EventSignal();
20
- this._onAnimationComplete = new event_signal_1.EventSignal();
21
- this._onObjectAnimationsComplete = new event_signal_1.EventSignal();
22
15
  this.animations = new Map();
23
- this._springState = { x: 0, targetX: 0, v: 0 };
16
+ this.events = {
17
+ beforeStep: new event_signal_1.EventSignal(),
18
+ afterStep: new event_signal_1.EventSignal(),
19
+ completeField: new event_signal_1.EventSignal(),
20
+ completeObject: new event_signal_1.EventSignal(),
21
+ };
22
+ this.changeObjectEvents = new Map();
23
+ this.changeFieldEvents = new Map();
24
+ // we use these signals to coalesce object change events
25
+ this.beforeChange = new event_signal_1.EventSignal();
26
+ this.afterChange = new event_signal_1.EventSignal();
24
27
  this.t_last = -1;
25
28
  this._currentLoopControl = null;
26
29
  if (onBeforeStep) {
27
- this.onBeforeStep.addListener(e => onBeforeStep(e.dt_s));
30
+ this.events.beforeStep.addListener(e => onBeforeStep(e.dt_s));
28
31
  }
29
32
  if (onAfterStep) {
30
- this.onAfterStep.addListener(e => onAfterStep(e.dt_s));
33
+ this.events.afterStep.addListener(e => onAfterStep(e.dt_s));
31
34
  }
32
35
  }
33
- springTo(object, field, target, params = { duration_s: 0.5 }) {
34
- if (params != null) {
35
- let spring = this.getAnimationOrCreate(object, field, AnimationType.Spring);
36
- // update the target and parameters
37
- spring.type = AnimationType.Spring;
38
- spring.target = target;
39
- spring.springParams = Spring_js_1.Spring.getPhysicsParameters(params);
40
- spring.step = null;
41
- }
42
- else {
43
- this.setTo(object, field, target);
44
- }
36
+ setTo(object, target) {
37
+ this.beforeChange.dispatch();
38
+ forObjectFieldsRecursive(object, target, (obj, field, targetValue) => {
39
+ this.setFieldTo(obj, field, targetValue);
40
+ this.dispatchChangeObjectEvent(obj);
41
+ });
42
+ this.afterChange.dispatch();
45
43
  }
46
- customTweenTo(object, field, target, duration_s, step) {
47
- let animation = this.getAnimationOrCreate(object, field, AnimationType.Tween);
48
- animation.type = AnimationType.Tween;
49
- animation.target = target;
50
- animation.tweenParams = {
51
- x0: object[field],
52
- t0_ms: performance.now(),
53
- duration_s: duration_s,
54
- };
55
- animation.step = step;
44
+ animateTo(object, target, animator = SpringAnimator_js_1.SpringAnimator, params = null) {
45
+ forObjectFieldsRecursive(object, target, (subObj, field, targetValue) => {
46
+ this.syncAnimation(subObj, field, targetValue, animator, params);
47
+ });
56
48
  }
57
- linearTo(object, field, target, duration_s) {
58
- this.customTweenTo(object, field, target, duration_s, Tween.linearStep);
49
+ springTo(object, target, params) {
50
+ this.animateTo(object, target, SpringAnimator_js_1.SpringAnimator, params);
59
51
  }
60
- easeInOutTo(object, field, target, duration_s) {
61
- this.customTweenTo(object, field, target, duration_s, Tween.easeInOutStep);
52
+ customTweenTo(object, target, duration_s, easingFn) {
53
+ this.animateTo(object, target, TweenAnimator_js_1.TweenAnimator, {
54
+ duration_s,
55
+ easingFn,
56
+ });
62
57
  }
63
- easeInTo(object, field, target, duration_s) {
64
- this.customTweenTo(object, field, target, duration_s, Tween.easeInStep);
58
+ linearTo(object, target, duration_s) {
59
+ this.customTweenTo(object, target, duration_s, TweenAnimator_js_1.linearStep);
65
60
  }
66
- easeOutTo(object, field, target, duration_s) {
67
- this.customTweenTo(object, field, target, duration_s, Tween.easeOutStep);
61
+ easeInOutTo(object, target, duration_s) {
62
+ this.customTweenTo(object, target, duration_s, TweenAnimator_js_1.easeInOutStep);
68
63
  }
69
- /**
70
- * Remove animation from the object and set the field to the target value
71
- */
72
- setTo(object, field, target) {
73
- this.remove(object, field);
74
- object[field] = target;
64
+ easeInTo(object, target, duration_s) {
65
+ this.customTweenTo(object, target, duration_s, TweenAnimator_js_1.easeInStep);
75
66
  }
76
- onComplete(object, field, callback) {
77
- return this._onAnimationComplete.addListener(e => {
67
+ easeOutTo(object, target, duration_s) {
68
+ this.customTweenTo(object, target, duration_s, TweenAnimator_js_1.easeOutStep);
69
+ }
70
+ onCompleteField(object, field, callback, once) {
71
+ let listener = this.events.completeField.addListener(e => {
78
72
  if (e.object === object && e.field === field) {
79
73
  callback(object, field);
74
+ if (once) {
75
+ listener.remove();
76
+ }
80
77
  }
81
78
  });
79
+ return listener;
82
80
  }
83
- onAllComplete(object, callback, once) {
84
- let listener = this._onObjectAnimationsComplete.addListener(e => {
81
+ onComplete(object, callback, once) {
82
+ const listener = this.events.completeObject.addListener(e => {
85
83
  if (e.object === object) {
86
84
  callback(object);
87
85
  if (once) {
@@ -91,64 +89,92 @@ class Animator {
91
89
  });
92
90
  return listener;
93
91
  }
94
- step(dt_s) {
95
- if (this.onBeforeStep.hasListeners()) {
96
- this.onBeforeStep.dispatch({ dt_s });
92
+ onChangeField(object, field, callback) {
93
+ // check if field is an object
94
+ if (typeof object[field] === 'object' && object[field] !== null) {
95
+ return this.onChange(object[field], (subObject) => {
96
+ callback(object, field);
97
+ });
98
+ }
99
+ else {
100
+ // add a listener for this field
101
+ return this.addChangeFieldListener(object, field, callback);
97
102
  }
98
- let springState = this._springState;
103
+ }
104
+ onChange(object, callback) {
105
+ // add a listener for this object and every sub-object
106
+ const removeCallbacks = new Array();
107
+ // coalesce events within a single step
108
+ let objectChanged = false;
109
+ removeCallbacks.push(this.beforeChange.addListener(() => {
110
+ objectChanged = false;
111
+ }).remove, this.afterChange.addListener(() => {
112
+ if (objectChanged) {
113
+ callback(object);
114
+ }
115
+ }).remove);
116
+ const subObjectChangedCallback = (subObject) => {
117
+ objectChanged = true;
118
+ };
119
+ enumerateObjects(object, (subObject) => {
120
+ let signal = this.changeObjectEvents.get(object);
121
+ if (signal == null) {
122
+ signal = new event_signal_1.EventSignal();
123
+ this.changeObjectEvents.set(subObject, signal);
124
+ }
125
+ const subListener = signal.addListener(subObjectChangedCallback);
126
+ removeCallbacks.push(() => {
127
+ subListener.remove();
128
+ if (!signal.hasListeners()) {
129
+ this.changeObjectEvents.delete(object);
130
+ }
131
+ });
132
+ });
133
+ return {
134
+ remove: () => {
135
+ for (const remove of removeCallbacks) {
136
+ remove();
137
+ }
138
+ }
139
+ };
140
+ }
141
+ onBeforeStep(callback) {
142
+ return this.events.beforeStep.addListener(e => callback(e.dt_s));
143
+ }
144
+ onAfterStep(callback) {
145
+ return this.events.afterStep.addListener(e => callback(e.dt_s));
146
+ }
147
+ step(dt_s) {
148
+ this.events.beforeStep.dispatch({ dt_s });
149
+ this.beforeChange.dispatch();
99
150
  // step all animations
100
- this.animations.forEach((objectAnims, object) => {
101
- objectAnims.forEach((animation, field) => {
102
- switch (animation.type) {
103
- case AnimationType.Spring:
151
+ for (let [object, objectAnims] of this.animations.entries()) {
152
+ for (let [field, animation] of objectAnims.entries()) {
153
+ let result = animation.animator.step(animation.state, object, field, animation.params, dt_s);
154
+ // dispatch the field change event
155
+ this.dispatchChangeFieldEvent(object, field);
156
+ // handle animation completion
157
+ switch (result) {
158
+ case IFieldAnimator_js_1.StepResult.Complete:
104
159
  {
105
- // step the spring
106
- springState.x = object[field];
107
- springState.targetX = animation.target;
108
- springState.v = animation.velocity;
109
- if (animation.springParams != null) {
110
- Spring_js_1.Spring.stepSpring(dt_s, springState, animation.springParams);
111
- }
112
- else {
113
- // instant transition: set to the target
114
- springState.x = springState.targetX;
115
- springState.v = 0;
116
- }
117
- // update the object
118
- object[field] = springState.x;
119
- animation.velocity = springState.v;
120
- // remove the spring if it's close enough to the target and velocity is close to 0
121
- if (Math.abs(springState.x - springState.targetX) < 0.0001 && Math.abs(springState.v) < 0.0001) {
122
- object[field] = animation.target;
123
- objectAnims.delete(field);
124
- this._onAnimationComplete.dispatch({ object, field });
125
- }
126
- }
127
- break;
128
- case AnimationType.Tween: {
129
- // step the tween
130
- let x = object[field];
131
- animation.step(object, field, animation.target, animation.tweenParams, dt_s);
132
- let x_new = object[field];
133
- animation.velocity = (x_new - x) / dt_s;
134
- // remove the tween if it's complete
135
- let deltaTime_s = (performance.now() - animation.tweenParams.t0_ms) / 1000;
136
- if (deltaTime_s >= animation.tweenParams.duration_s) {
137
- object[field] = animation.target;
138
160
  objectAnims.delete(field);
139
- this._onAnimationComplete.dispatch({ object, field });
161
+ this.events.completeField.dispatch({ object, field });
140
162
  }
141
163
  break;
142
- }
143
164
  }
144
- });
165
+ }
166
+ ;
167
+ // dispatch the object change event
168
+ this.dispatchChangeObjectEvent(object);
145
169
  // remove the object if it has no more springs
146
170
  if (objectAnims.size == 0) {
147
171
  this.animations.delete(object);
148
- this._onObjectAnimationsComplete.dispatch({ object });
172
+ this.events.completeObject.dispatch({ object });
149
173
  }
150
- });
151
- this.onAfterStep.dispatch({ dt_s });
174
+ }
175
+ ;
176
+ this.afterChange.dispatch();
177
+ this.events.afterStep.dispatch({ dt_s });
152
178
  }
153
179
  tick() {
154
180
  let t_s = performance.now() / 1000;
@@ -211,31 +237,106 @@ class Animator {
211
237
  * Remove animation for this object and field if it exists
212
238
  * Does not change the value of the field
213
239
  */
214
- remove(object, field) {
215
- let objectSprings = this.animations.get(object);
216
- if (objectSprings != null) {
217
- objectSprings.delete(field);
240
+ remove(object, field, dispatchComplete = false) {
241
+ let objectAnimations = this.animations.get(object);
242
+ if (objectAnimations != null) {
243
+ objectAnimations.delete(field);
244
+ if (dispatchComplete) {
245
+ this.events.completeField.dispatch({ object, field });
246
+ }
218
247
  }
219
248
  // if there are no more springs for this object, remove it from the map
220
- if (objectSprings != null && objectSprings.size == 0) {
221
- this.animations.delete(object);
249
+ if (objectAnimations != null && objectAnimations.size == 0) {
250
+ this.removeObject(object, dispatchComplete);
222
251
  }
223
252
  }
224
253
  /**
225
254
  * Remove all animations for this object
226
255
  */
227
- removeObject(object) {
256
+ removeObject(object, dispatchComplete = false) {
228
257
  this.animations.delete(object);
258
+ if (dispatchComplete) {
259
+ this.events.completeObject.dispatch({ object });
260
+ }
229
261
  }
230
262
  /**
231
263
  * Remove all animations
232
264
  */
233
- removeAll() {
265
+ removeAll(dispatchComplete = false) {
266
+ for (let [object, objectAnimations] of this.animations.entries()) {
267
+ for (let field of objectAnimations.keys()) {
268
+ this.remove(object, field, dispatchComplete);
269
+ }
270
+ }
234
271
  this.animations.clear();
235
272
  }
236
- getVelocity(object, field) {
237
- let spring = this.getObjectAnimations(object).get(field);
238
- return spring?.velocity ?? 0;
273
+ getState(object, field) {
274
+ let animation = this.getObjectAnimations(object).get(field);
275
+ return animation?.state;
276
+ }
277
+ dispatchChangeObjectEvent(object) {
278
+ let signal = this.changeObjectEvents.get(object);
279
+ if (signal == null) {
280
+ return;
281
+ }
282
+ signal.dispatch(object);
283
+ }
284
+ dispatchChangeFieldEvent(object, field) {
285
+ const map = this.changeFieldEvents.get(object);
286
+ if (map == null) {
287
+ return;
288
+ }
289
+ let signal = map.get(field);
290
+ if (signal == null) {
291
+ return;
292
+ }
293
+ if (signal.hasListeners()) {
294
+ signal.dispatch({ object, field });
295
+ }
296
+ }
297
+ addChangeFieldListener(object, field, callback) {
298
+ const getOrCreateChangeFieldSignal = (object, field) => {
299
+ let map = this.changeFieldEvents.get(object);
300
+ if (map == null) {
301
+ map = new Map();
302
+ this.changeFieldEvents.set(object, map);
303
+ }
304
+ let signal = map.get(field);
305
+ if (signal == null) {
306
+ signal = new event_signal_1.EventSignal();
307
+ map.set(field, signal);
308
+ }
309
+ return signal;
310
+ };
311
+ const signal = getOrCreateChangeFieldSignal(object, field);
312
+ const listener = signal.addListener((e) => {
313
+ callback(e.object, e.field);
314
+ });
315
+ return {
316
+ remove: () => {
317
+ listener.remove();
318
+ // cleanup
319
+ if (!signal.hasListeners()) {
320
+ let map = this.changeFieldEvents.get(object);
321
+ map?.delete(field);
322
+ if (map != null && map.size === 0) {
323
+ this.changeFieldEvents.delete(object);
324
+ }
325
+ }
326
+ }
327
+ };
328
+ }
329
+ /**
330
+ * Remove animation from the object and set the field to the target value
331
+ *
332
+ * This completes the animation immediately and dispatches the onComplete event
333
+ */
334
+ setFieldTo(object, field, targetValue) {
335
+ const dispatchComplete = true;
336
+ this.remove(object, field, dispatchComplete);
337
+ object[field] = targetValue;
338
+ // dispatch the field change event
339
+ this.dispatchChangeFieldEvent(object, field);
239
340
  }
240
341
  /**
241
342
  * Creates a new map if one doesn't already exist for the given object
@@ -252,59 +353,56 @@ class Animator {
252
353
  /**
253
354
  * Creates a new spring if one doesn't already exist for the given object and field
254
355
  */
255
- getAnimationOrCreate(object, field, type) {
356
+ syncAnimation(object, field, targetValue, fieldAnimator, params = null) {
256
357
  let objectAnimations = this.getObjectAnimations(object);
257
358
  let animation = objectAnimations.get(field);
258
- if (animation == null) {
359
+ let animatorChanged = animation?.animator !== fieldAnimator;
360
+ if (animation == null || animatorChanged) {
259
361
  // create
260
362
  animation = {
261
- target: 0,
262
- type: type,
263
- springParams: null,
264
- tweenParams: null,
265
- velocity: 0,
266
- step: null
363
+ animator: fieldAnimator,
364
+ state: fieldAnimator.createState(object, field, targetValue, params),
365
+ params,
267
366
  };
268
367
  objectAnimations.set(field, animation);
269
368
  }
270
- animation.type = type;
369
+ else {
370
+ animation.params = params;
371
+ animation.animator.updateState(animation.state, object, field, targetValue, params);
372
+ }
271
373
  return animation;
272
374
  }
273
375
  }
274
376
  exports.Animator = Animator;
275
- var Tween;
276
- (function (Tween) {
277
- function linearStep(object, field, target, params, dt_s) {
278
- let dx = target - params.x0;
279
- let t = (performance.now() - params.t0_ms) / 1000;
280
- let u = t / params.duration_s;
281
- let x_new = params.x0 + dx * u;
282
- object[field] = x_new;
283
- }
284
- Tween.linearStep = linearStep;
285
- // cubic ease in out
286
- function easeInOutStep(object, field, target, params, dt_s) {
287
- let dx = target - params.x0;
288
- let t = (performance.now() - params.t0_ms) / 1000;
289
- let u = t / params.duration_s;
290
- let x_new = params.x0 + dx * u * u * (3 - 2 * u);
291
- object[field] = x_new;
292
- }
293
- Tween.easeInOutStep = easeInOutStep;
294
- function easeInStep(object, field, target, params, dt_s) {
295
- let dx = target - params.x0;
296
- let t = (performance.now() - params.t0_ms) / 1000;
297
- let u = t / params.duration_s;
298
- let x_new = params.x0 + dx * u * u * u;
299
- object[field] = x_new;
300
- }
301
- Tween.easeInStep = easeInStep;
302
- function easeOutStep(object, field, target, params, dt_s) {
303
- let dx = target - params.x0;
304
- let t = (performance.now() - params.t0_ms) / 1000;
305
- let u = t / params.duration_s;
306
- let x_new = params.x0 + dx * (1 - Math.pow(1 - u, 3));
307
- object[field] = x_new;
308
- }
309
- Tween.easeOutStep = easeOutStep;
310
- })(Tween || (exports.Tween = Tween = {}));
377
+ function forObjectFieldsRecursive(sourceObj, targetObj, callback) {
378
+ for (let field in targetObj) {
379
+ if (Object.prototype.hasOwnProperty.call(targetObj, field)) {
380
+ let targetValue = targetObj[field];
381
+ if (typeof targetValue === 'object') {
382
+ forObjectFieldsRecursive(sourceObj[field], targetValue, callback);
383
+ }
384
+ else {
385
+ callback(sourceObj, field, targetValue);
386
+ }
387
+ }
388
+ }
389
+ }
390
+ function enumerateObjects(input, callback) {
391
+ // Call callback for the current object if it's an object or array
392
+ if (typeof input === 'object' && input !== null) {
393
+ callback(input);
394
+ // Recursively enumerate nested objects
395
+ if (Array.isArray(input)) {
396
+ for (const item of input) {
397
+ enumerateObjects(item, callback);
398
+ }
399
+ }
400
+ else {
401
+ for (const key in input) {
402
+ if (input.hasOwnProperty(key)) {
403
+ enumerateObjects(input[key], callback);
404
+ }
405
+ }
406
+ }
407
+ }
408
+ }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StepResult = void 0;
4
+ var StepResult;
5
+ (function (StepResult) {
6
+ StepResult[StepResult["Continue"] = 0] = "Continue";
7
+ StepResult[StepResult["Complete"] = 1] = "Complete";
8
+ })(StepResult || (exports.StepResult = StepResult = {}));
@@ -1,11 +1,47 @@
1
1
  "use strict";
2
- /**
3
- * Spring
4
- *
5
- * @author George Corney (haxiomic)
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.Spring = void 0;
3
+ exports.Spring = exports.SpringAnimator = void 0;
4
+ const IFieldAnimator_js_1 = require("../IFieldAnimator.js");
5
+ const defaultSpringParameters = {
6
+ duration_s: 0.5,
7
+ };
8
+ exports.SpringAnimator = {
9
+ createState(obj, field, target, params) {
10
+ return {
11
+ x: obj[field],
12
+ targetX: target,
13
+ v: 0,
14
+ physicsParameters: Spring.getPhysicsParameters(params ?? defaultSpringParameters),
15
+ };
16
+ },
17
+ updateState(state, object, field, target, params) {
18
+ state.x = object[field];
19
+ state.targetX = target;
20
+ state.physicsParameters = Spring.getPhysicsParameters(params ?? defaultSpringParameters);
21
+ },
22
+ step(state, object, field, params, dt_s) {
23
+ let physicsParameters = state.physicsParameters;
24
+ // step the spring
25
+ if (physicsParameters != null && isFinite(physicsParameters.strength) && isFinite(physicsParameters.damping)) {
26
+ Spring.stepSpring(dt_s, state, physicsParameters);
27
+ }
28
+ else {
29
+ // instant transition: set to the target
30
+ state.x = state.targetX;
31
+ state.v = 0;
32
+ }
33
+ // update the object
34
+ object[field] = state.x;
35
+ // complete the animation if it's close enough to the target and velocity is close to 0
36
+ if (Math.abs(state.x - state.targetX) < 0.0001 && Math.abs(state.v) < 0.0001) {
37
+ object[field] = state.targetX;
38
+ return IFieldAnimator_js_1.StepResult.Complete;
39
+ }
40
+ else {
41
+ return IFieldAnimator_js_1.StepResult.Continue;
42
+ }
43
+ }
44
+ };
9
45
  var Spring;
10
46
  (function (Spring) {
11
47
  /**
@@ -16,7 +52,7 @@ var Spring;
16
52
  * `strength = damping * damping / 4`
17
53
  */
18
54
  function Exponential(options) {
19
- // solved numerically
55
+ // found numerically
20
56
  const halfLifeConstant = 3.356694; // from solve (1+u)*exp(-u)=0.5 for u, and constant = 2u
21
57
  const pointOnePercentConstant = 18.46682; // from solve (1+u)*exp(-u)=0.001 for u, and constant = 2u
22
58
  const damping = pointOnePercentConstant / options.duration_s;
@@ -29,11 +65,12 @@ var Spring;
29
65
  // -2ln(0.001) = b t
30
66
  const durationTarget = 0.001; // 0.1% of target
31
67
  let damping = -2 * Math.log(durationTarget) / duration_s;
32
- // 4k - b^2 > 0
33
- let bSq = damping * damping;
34
- const criticalStrength = bSq / 4;
35
- let strength = criticalStrength + (bounce * bounce + 1);
36
- return { damping, strength };
68
+ // see https://www.desmos.com/calculator/h43ylohte7
69
+ const strength = 0.25 * (((2 * bounce * Math.PI) / duration_s) ** 2 + damping ** 2);
70
+ return {
71
+ damping,
72
+ strength,
73
+ };
37
74
  }
38
75
  Spring.Underdamped = Underdamped;
39
76
  function getPhysicsParameters(parameters) {
@@ -56,6 +93,8 @@ var Spring;
56
93
  * @param dt_s
57
94
  * @param state
58
95
  * @param parameters
96
+ *
97
+ * If parameters are NaN or infinite, the spring will skip to the target
59
98
  */
60
99
  function stepSpring(dt_s, state, parameters) {
61
100
  // analytic integration (unconditionally stable)
@@ -74,6 +113,12 @@ var Spring;
74
113
  return;
75
114
  if (dt_s === 0)
76
115
  return;
116
+ if (!isFinite(k) || !isFinite(b) || !isFinite(v0) || !isFinite(dx0)) {
117
+ // skip to target
118
+ state.x = state.targetX;
119
+ state.v = 0;
120
+ return 0; // no energy
121
+ }
77
122
  let critical = k * 4 - b * b;
78
123
  if (critical > 0) {
79
124
  // under damped