@flighthq/tween 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.
Files changed (44) hide show
  1. package/dist/colorTween.d.ts +14 -0
  2. package/dist/colorTween.d.ts.map +1 -0
  3. package/dist/colorTween.js +28 -0
  4. package/dist/colorTween.js.map +1 -0
  5. package/dist/index.d.ts +8 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +8 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/internal.d.ts +3 -0
  10. package/dist/internal.d.ts.map +1 -0
  11. package/dist/internal.js +18 -0
  12. package/dist/internal.js.map +1 -0
  13. package/dist/timer.d.ts +3 -0
  14. package/dist/timer.d.ts.map +1 -0
  15. package/dist/timer.js +5 -0
  16. package/dist/timer.js.map +1 -0
  17. package/dist/tween.d.ts +20 -0
  18. package/dist/tween.d.ts.map +1 -0
  19. package/dist/tween.js +194 -0
  20. package/dist/tween.js.map +1 -0
  21. package/dist/tweenManager.d.ts +5 -0
  22. package/dist/tweenManager.d.ts.map +1 -0
  23. package/dist/tweenManager.js +10 -0
  24. package/dist/tweenManager.js.map +1 -0
  25. package/dist/tweenProgress.d.ts +33 -0
  26. package/dist/tweenProgress.d.ts.map +1 -0
  27. package/dist/tweenProgress.js +87 -0
  28. package/dist/tweenProgress.js.map +1 -0
  29. package/dist/tweenStagger.d.ts +27 -0
  30. package/dist/tweenStagger.d.ts.map +1 -0
  31. package/dist/tweenStagger.js +51 -0
  32. package/dist/tweenStagger.js.map +1 -0
  33. package/dist/updateTweens.d.ts +4 -0
  34. package/dist/updateTweens.d.ts.map +1 -0
  35. package/dist/updateTweens.js +78 -0
  36. package/dist/updateTweens.js.map +1 -0
  37. package/package.json +39 -0
  38. package/src/colorTween.test.ts +48 -0
  39. package/src/timer.test.ts +56 -0
  40. package/src/tween.test.ts +385 -0
  41. package/src/tweenManager.test.ts +52 -0
  42. package/src/tweenProgress.test.ts +197 -0
  43. package/src/tweenStagger.test.ts +94 -0
  44. package/src/updateTweens.test.ts +292 -0
@@ -0,0 +1,78 @@
1
+ import { emitSignal } from '@flighthq/signals';
2
+ import { initializeTween } from './internal';
3
+ export function completeTween(tween) {
4
+ if (tween.complete)
5
+ return;
6
+ if (!tween.initialized)
7
+ initializeTween(tween);
8
+ const effectiveT = tween.reverse ? 0 : 1;
9
+ const easedT = tween.ease(effectiveT);
10
+ const target = tween.target;
11
+ for (const detail of tween.properties) {
12
+ let value = detail.start + detail.change * easedT;
13
+ if (tween.snapping)
14
+ value = Math.round(value);
15
+ target[detail.key] = value;
16
+ }
17
+ tween.complete = true;
18
+ emitSignal(tween.onComplete);
19
+ }
20
+ function updateTween(tween, deltaTime) {
21
+ if (tween.paused || tween.complete)
22
+ return;
23
+ tween.elapsed += deltaTime;
24
+ const activeElapsed = tween.elapsed - tween.delay;
25
+ if (activeElapsed <= 0)
26
+ return;
27
+ if (!tween.initialized)
28
+ initializeTween(tween);
29
+ const t = Math.min(activeElapsed / tween.duration, 1);
30
+ const effectiveT = tween.reverse ? 1 - t : t;
31
+ const easedT = tween.ease(effectiveT);
32
+ const target = tween.target;
33
+ for (const detail of tween.properties) {
34
+ let value = detail.start + detail.change * easedT;
35
+ if (tween.snapping)
36
+ value = Math.round(value);
37
+ target[detail.key] = value;
38
+ }
39
+ emitSignal(tween.onUpdate);
40
+ if (t >= 1) {
41
+ if (tween.repeat === 0) {
42
+ tween.complete = true;
43
+ emitSignal(tween.onComplete);
44
+ }
45
+ else {
46
+ if (tween.reflect) {
47
+ tween.reverse = !tween.reverse;
48
+ emitSignal(tween.onYoyo);
49
+ }
50
+ tween.elapsed = tween.delay;
51
+ if (tween.repeat > 0)
52
+ tween.repeat--;
53
+ emitSignal(tween.onRepeat);
54
+ }
55
+ }
56
+ }
57
+ // Time is unit-agnostic. `deltaTime` here and every duration value (`duration`,
58
+ // `delay`, `repeatDelay`, `each`) pass through unchanged in whatever unit the
59
+ // caller supplies — there is no built-in seconds assumption. The only contract
60
+ // is consistency: feed `updateTweens` and the duration fields the same unit
61
+ // (all seconds, or all milliseconds, or all frames). The package never converts.
62
+ export function updateTweens(manager, deltaTime) {
63
+ for (const [target, list] of manager.tweens) {
64
+ let i = list.length - 1;
65
+ while (i >= 0) {
66
+ if (list[i].complete) {
67
+ list.splice(i, 1);
68
+ }
69
+ else {
70
+ updateTween(list[i], deltaTime);
71
+ }
72
+ i--;
73
+ }
74
+ if (list.length === 0)
75
+ manager.tweens.delete(target);
76
+ }
77
+ }
78
+ //# sourceMappingURL=updateTweens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updateTweens.js","sourceRoot":"","sources":["../src/updateTweens.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@flighthq/tween",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/easing": "0.1.0",
31
+ "@flighthq/signals": "0.1.0",
32
+ "@flighthq/types": "0.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "typescript": "^5.3.0"
36
+ },
37
+ "description": "Tween animation system with easing functions",
38
+ "sideEffects": false
39
+ }
@@ -0,0 +1,48 @@
1
+ import { createColorTween } from './colorTween';
2
+ import { createTweenManager } from './tweenManager';
3
+ import { updateTweens } from './updateTweens';
4
+
5
+ describe('createColorTween', () => {
6
+ it('reaches the target color after full duration', () => {
7
+ const manager = createTweenManager();
8
+ const target = { color: 0xff0000 }; // red
9
+ createColorTween(manager, target, 'color', 1000, 0x0000ff); // to blue
10
+ updateTweens(manager, 1000);
11
+ expect(target.color).toBe(0x0000ff);
12
+ });
13
+
14
+ it('starts from the current color value', () => {
15
+ const manager = createTweenManager();
16
+ const target = { color: 0x00ff00 }; // green
17
+ createColorTween(manager, target, 'color', 1000, 0xff0000, { ease: (t) => t });
18
+ updateTweens(manager, 1000);
19
+ expect(target.color).toBe(0xff0000);
20
+ });
21
+
22
+ it('writes an integer color on each update', () => {
23
+ const manager = createTweenManager();
24
+ const target = { color: 0x000000 };
25
+ createColorTween(manager, target, 'color', 1000, 0xffffff, { ease: (t) => t });
26
+ updateTweens(manager, 500);
27
+ expect(Number.isInteger(target.color)).toBe(true);
28
+ });
29
+
30
+ it('interpolates components independently at the midpoint', () => {
31
+ const manager = createTweenManager();
32
+ const target = { color: 0xff0000 }; // red: r=255 g=0 b=0
33
+ createColorTween(manager, target, 'color', 1000, 0x0000ff, { ease: (t) => t }); // blue: r=0 g=0 b=255
34
+ updateTweens(manager, 500);
35
+ // midpoint: r≈128, g=0, b≈128 → 0x800080
36
+ const r = (target.color >> 16) & 0xff;
37
+ const b = target.color & 0xff;
38
+ expect(r).toBeCloseTo(128, 0);
39
+ expect(b).toBeCloseTo(128, 0);
40
+ });
41
+
42
+ it('does not affect the target before any update', () => {
43
+ const manager = createTweenManager();
44
+ const target = { color: 0xabcdef };
45
+ createColorTween(manager, target, 'color', 1000, 0x000000);
46
+ expect(target.color).toBe(0xabcdef);
47
+ });
48
+ });
@@ -0,0 +1,56 @@
1
+ import { connectSignal } from '@flighthq/signals';
2
+
3
+ import { createTweenTimer } from './timer';
4
+ import { createTweenManager } from './tweenManager';
5
+ import { updateTweens } from './updateTweens';
6
+
7
+ describe('createTweenTimer', () => {
8
+ it('fires onComplete after the specified duration', () => {
9
+ const manager = createTweenManager();
10
+ const timer = createTweenTimer(manager, 1000);
11
+ let fired = 0;
12
+ connectSignal(timer.onComplete, () => fired++);
13
+ updateTweens(manager, 1000);
14
+ expect(fired).toBe(1);
15
+ });
16
+
17
+ it('does not fire onComplete before duration elapses', () => {
18
+ const manager = createTweenManager();
19
+ const timer = createTweenTimer(manager, 1000);
20
+ let fired = 0;
21
+ connectSignal(timer.onComplete, () => fired++);
22
+ updateTweens(manager, 500);
23
+ expect(fired).toBe(0);
24
+ });
25
+
26
+ it('respects the delay option', () => {
27
+ const manager = createTweenManager();
28
+ const timer = createTweenTimer(manager, 1000, { delay: 500 });
29
+ let fired = 0;
30
+ connectSignal(timer.onComplete, () => fired++);
31
+ updateTweens(manager, 1000);
32
+ expect(fired).toBe(0);
33
+ updateTweens(manager, 500);
34
+ expect(fired).toBe(1);
35
+ });
36
+
37
+ it('each call produces an independent sentinel target', () => {
38
+ const manager = createTweenManager();
39
+ const t1 = createTweenTimer(manager, 1000);
40
+ const t2 = createTweenTimer(manager, 1000);
41
+ expect(t1.target).not.toBe(t2.target);
42
+ expect(manager.tweens.size).toBe(2);
43
+ });
44
+
45
+ it('fires onRepeat and keeps running when repeat: -1', () => {
46
+ const manager = createTweenManager();
47
+ const timer = createTweenTimer(manager, 1000, { repeat: -1 });
48
+ let ticks = 0;
49
+ connectSignal(timer.onRepeat, () => ticks++);
50
+ updateTweens(manager, 1000);
51
+ updateTweens(manager, 1000);
52
+ updateTweens(manager, 1000);
53
+ expect(ticks).toBe(3);
54
+ expect(timer.complete).toBe(false);
55
+ });
56
+ });
@@ -0,0 +1,385 @@
1
+ import { connectSignal } from '@flighthq/signals';
2
+
3
+ import {
4
+ applyTween,
5
+ createTween,
6
+ getActiveTweenCount,
7
+ getTweensOf,
8
+ hasTweensOf,
9
+ killTweensOfProperty,
10
+ pauseAllTweens,
11
+ pauseTween,
12
+ pauseTweens,
13
+ resetAllTweens,
14
+ resumeAllTweens,
15
+ resumeTween,
16
+ resumeTweens,
17
+ stopAllTweens,
18
+ stopTween,
19
+ stopTweens,
20
+ } from './tween';
21
+ import { createTweenManager } from './tweenManager';
22
+ import { updateTweens } from './updateTweens';
23
+
24
+ describe('applyTween', () => {
25
+ it('immediately sets properties on target', () => {
26
+ const manager = createTweenManager();
27
+ const target = { x: 0 };
28
+ applyTween(manager, target, { x: 50 });
29
+ expect(target.x).toBe(50);
30
+ });
31
+
32
+ it('stops conflicting tweens before applying', () => {
33
+ const manager = createTweenManager();
34
+ const target = { x: 0 };
35
+ const tween = createTween(manager, target, 1000, { x: 100 });
36
+ applyTween(manager, target, { x: 50 });
37
+ expect(tween.complete).toBe(true);
38
+ expect(target.x).toBe(50);
39
+ });
40
+ });
41
+
42
+ describe('createTween', () => {
43
+ it('registers with manager', () => {
44
+ const manager = createTweenManager();
45
+ const target = { x: 0 };
46
+ createTween(manager, target, 1000, { x: 100 });
47
+ expect(manager.tweens.has(target)).toBe(true);
48
+ });
49
+
50
+ it('uses defaultManager when no manager provided', () => {
51
+ const target = { x: 0 };
52
+ const tween = createTween(target, 1000, { x: 100 });
53
+ expect(tween.target).toBe(target);
54
+ });
55
+
56
+ it('stops existing tween on same property when overwrite=true', () => {
57
+ const manager = createTweenManager();
58
+ const target = { x: 0 };
59
+ const first = createTween(manager, target, 1000, { x: 100 });
60
+ createTween(manager, target, 1000, { x: 200 });
61
+ expect(first.complete).toBe(true);
62
+ });
63
+
64
+ it('does not stop existing tween when overwrite=false', () => {
65
+ const manager = createTweenManager();
66
+ const target = { x: 0 };
67
+ const first = createTween(manager, target, 1000, { x: 100 });
68
+ createTween(manager, target, 1000, { x: 200 }, { overwrite: false });
69
+ expect(first.complete).toBe(false);
70
+ });
71
+
72
+ it('does not stop existing tween with non-overlapping properties', () => {
73
+ const manager = createTweenManager();
74
+ const target = { x: 0, y: 0 };
75
+ const first = createTween(manager, target, 1000, { x: 100 });
76
+ createTween(manager, target, 1000, { y: 200 });
77
+ expect(first.complete).toBe(false);
78
+ });
79
+
80
+ it('initializes with correct defaults', () => {
81
+ const manager = createTweenManager();
82
+ const tween = createTween(manager, { x: 0 }, 1000, { x: 100 });
83
+ expect(tween.complete).toBe(false);
84
+ expect(tween.elapsed).toBe(0);
85
+ expect(tween.paused).toBe(false);
86
+ expect(tween.repeat).toBe(0);
87
+ expect(tween.reflect).toBe(false);
88
+ expect(tween.reverse).toBe(false);
89
+ expect(tween.smartRotation).toBe(false);
90
+ expect(tween.snapping).toBe(false);
91
+ });
92
+
93
+ it('pre-allocates properties array from propertyMap at creation', () => {
94
+ const manager = createTweenManager();
95
+ const tween = createTween(manager, { x: 0, y: 0 }, 1000, { x: 100, y: 200 });
96
+ expect(tween.properties).toHaveLength(2);
97
+ expect(tween.properties.map((d) => d.key)).toEqual(expect.arrayContaining(['x', 'y']));
98
+ });
99
+ });
100
+
101
+ describe('getActiveTweenCount', () => {
102
+ it('returns 0 for an empty manager', () => {
103
+ const manager = createTweenManager();
104
+ expect(getActiveTweenCount(manager)).toBe(0);
105
+ });
106
+
107
+ it('counts total tweens across all targets', () => {
108
+ const manager = createTweenManager();
109
+ createTween(manager, { x: 0 }, 1000, { x: 100 });
110
+ createTween(manager, { y: 0 }, 1000, { y: 100 });
111
+ expect(getActiveTweenCount(manager)).toBe(2);
112
+ });
113
+
114
+ it('decreases after tweens complete and are cleaned up', () => {
115
+ const manager = createTweenManager();
116
+ createTween(manager, { x: 0 }, 1000, { x: 100 });
117
+ updateTweens(manager, 1000);
118
+ updateTweens(manager, 0); // cleanup pass
119
+ expect(getActiveTweenCount(manager)).toBe(0);
120
+ });
121
+ });
122
+
123
+ describe('getTweensOf', () => {
124
+ it('returns empty array when target has no tweens', () => {
125
+ const manager = createTweenManager();
126
+ expect(getTweensOf(manager, {})).toEqual([]);
127
+ });
128
+
129
+ it('returns the tweens registered for a target', () => {
130
+ const manager = createTweenManager();
131
+ const target = { x: 0, y: 0 };
132
+ const t1 = createTween(manager, target, 1000, { x: 100 });
133
+ const t2 = createTween(manager, target, 1000, { y: 100 }, { overwrite: false });
134
+ const result = getTweensOf(manager, target);
135
+ expect(result).toContain(t1);
136
+ expect(result).toContain(t2);
137
+ });
138
+ });
139
+
140
+ describe('hasTweensOf', () => {
141
+ it('returns false when target has no tweens', () => {
142
+ const manager = createTweenManager();
143
+ expect(hasTweensOf(manager, {})).toBe(false);
144
+ });
145
+
146
+ it('returns true when target has active tweens', () => {
147
+ const manager = createTweenManager();
148
+ const target = { x: 0 };
149
+ createTween(manager, target, 1000, { x: 100 });
150
+ expect(hasTweensOf(manager, target)).toBe(true);
151
+ });
152
+
153
+ it('returns false after all tweens for that target complete and are cleaned up', () => {
154
+ const manager = createTweenManager();
155
+ const target = { x: 0 };
156
+ createTween(manager, target, 1000, { x: 100 });
157
+ updateTweens(manager, 1000);
158
+ updateTweens(manager, 0); // cleanup pass
159
+ expect(hasTweensOf(manager, target)).toBe(false);
160
+ });
161
+ });
162
+
163
+ describe('killTweensOfProperty', () => {
164
+ it('marks all tweens with the given property as complete', () => {
165
+ const manager = createTweenManager();
166
+ const a = createTween(manager, { x: 0 }, 1000, { x: 100 });
167
+ const b = createTween(manager, { x: 0 }, 1000, { x: 200 }, { overwrite: false });
168
+ killTweensOfProperty(manager, 'x');
169
+ expect(a.complete).toBe(true);
170
+ expect(b.complete).toBe(true);
171
+ });
172
+
173
+ it('does not affect tweens without the given property', () => {
174
+ const manager = createTweenManager();
175
+ const target = { x: 0, y: 0 };
176
+ const a = createTween(manager, target, 1000, { x: 100 });
177
+ const b = createTween(manager, target, 1000, { y: 100 }, { overwrite: false });
178
+ killTweensOfProperty(manager, 'x');
179
+ expect(a.complete).toBe(true);
180
+ expect(b.complete).toBe(false);
181
+ });
182
+
183
+ it('is a no-op when no tweens match', () => {
184
+ const manager = createTweenManager();
185
+ createTween(manager, { x: 0 }, 1000, { x: 100 });
186
+ expect(() => killTweensOfProperty(manager, 'z')).not.toThrow();
187
+ });
188
+
189
+ it('kills matching tweens across multiple targets', () => {
190
+ const manager = createTweenManager();
191
+ const t1 = createTween(manager, { x: 0 }, 1000, { x: 100 });
192
+ const t2 = createTween(manager, { x: 0 }, 1000, { x: 200 });
193
+ killTweensOfProperty(manager, 'x');
194
+ expect(t1.complete).toBe(true);
195
+ expect(t2.complete).toBe(true);
196
+ });
197
+ });
198
+
199
+ describe('pauseAllTweens', () => {
200
+ it('sets paused=true on every tween in the manager', () => {
201
+ const manager = createTweenManager();
202
+ const a = createTween(manager, { x: 0 }, 1000, { x: 100 });
203
+ pauseAllTweens(manager);
204
+ expect(a.paused).toBe(true);
205
+ });
206
+ });
207
+
208
+ describe('pauseTween', () => {
209
+ it('sets paused to true on a single tween', () => {
210
+ const manager = createTweenManager();
211
+ const tween = createTween(manager, { x: 0 }, 1000, { x: 100 });
212
+ pauseTween(tween);
213
+ expect(tween.paused).toBe(true);
214
+ });
215
+ });
216
+
217
+ describe('pauseTweens', () => {
218
+ it('pauses only tweens for the given target', () => {
219
+ const manager = createTweenManager();
220
+ const t = createTween(manager, { x: 0 }, 1000, { x: 100 });
221
+ pauseTweens(manager, t.target);
222
+ expect(t.paused).toBe(true);
223
+ });
224
+ });
225
+
226
+ describe('resetAllTweens', () => {
227
+ it('removes all tweens from the manager without completing them', () => {
228
+ const manager = createTweenManager();
229
+ const a = createTween(manager, { x: 0 }, 1000, { x: 100 });
230
+ const b = createTween(manager, { y: 0 }, 1000, { y: 100 });
231
+ resetAllTweens(manager);
232
+ expect(manager.tweens.size).toBe(0);
233
+ expect(a.complete).toBe(false);
234
+ expect(b.complete).toBe(false);
235
+ });
236
+ });
237
+
238
+ describe('resumeAllTweens', () => {
239
+ it('sets paused=false on every tween in the manager', () => {
240
+ const manager = createTweenManager();
241
+ const a = createTween(manager, { x: 0 }, 1000, { x: 100 });
242
+ pauseAllTweens(manager);
243
+ resumeAllTweens(manager);
244
+ expect(a.paused).toBe(false);
245
+ });
246
+ });
247
+
248
+ describe('resumeTween', () => {
249
+ it('sets paused to false on a single tween', () => {
250
+ const manager = createTweenManager();
251
+ const tween = createTween(manager, { x: 0 }, 1000, { x: 100 });
252
+ pauseTween(tween);
253
+ resumeTween(tween);
254
+ expect(tween.paused).toBe(false);
255
+ });
256
+ });
257
+
258
+ describe('resumeTweens', () => {
259
+ it('resumes only tweens for the given target', () => {
260
+ const manager = createTweenManager();
261
+ const t = createTween(manager, { x: 0 }, 1000, { x: 100 });
262
+ pauseTweens(manager, t.target);
263
+ resumeTweens(manager, t.target);
264
+ expect(t.paused).toBe(false);
265
+ });
266
+ });
267
+
268
+ describe('stopAllTweens', () => {
269
+ it('marks every tween in the manager as complete', () => {
270
+ const manager = createTweenManager();
271
+ const a = createTween(manager, { x: 0 }, 1000, { x: 100 });
272
+ stopAllTweens(manager);
273
+ expect(a.complete).toBe(true);
274
+ });
275
+ });
276
+
277
+ describe('stopTween', () => {
278
+ it('marks a single tween as complete', () => {
279
+ const manager = createTweenManager();
280
+ const tween = createTween(manager, { x: 0 }, 1000, { x: 100 });
281
+ stopTween(tween);
282
+ expect(tween.complete).toBe(true);
283
+ });
284
+
285
+ it('leaves the target value at its current position, does not jump to end', () => {
286
+ const manager = createTweenManager();
287
+ const target = { x: 0 };
288
+ const tween = createTween(manager, target, 1000, { x: 100 }, { ease: (t) => t });
289
+ updateTweens(manager, 500);
290
+ expect(target.x).toBeCloseTo(50);
291
+ stopTween(tween);
292
+ expect(target.x).toBeCloseTo(50); // unchanged — pass { complete: true } to jump to end instead
293
+ });
294
+
295
+ it('complete: true jumps to end values before stopping', () => {
296
+ const manager = createTweenManager();
297
+ const target = { x: 0 };
298
+ const tween = createTween(manager, target, 1000, { x: 100 }, { ease: (t) => t });
299
+ updateTweens(manager, 500);
300
+ stopTween(tween, { complete: true });
301
+ expect(target.x).toBe(100);
302
+ });
303
+
304
+ it('complete: true fires onComplete by default', () => {
305
+ const manager = createTweenManager();
306
+ const tween = createTween(manager, { x: 0 }, 1000, { x: 100 });
307
+ let fired = 0;
308
+ connectSignal(tween.onComplete, () => fired++);
309
+ stopTween(tween, { complete: true });
310
+ expect(fired).toBe(1);
311
+ });
312
+
313
+ it('complete: true with sendEvent: false suppresses onComplete', () => {
314
+ const manager = createTweenManager();
315
+ const target = { x: 0 };
316
+ const tween = createTween(manager, target, 1000, { x: 100 });
317
+ let fired = 0;
318
+ connectSignal(tween.onComplete, () => fired++);
319
+ stopTween(tween, { complete: true, sendEvent: false });
320
+ expect(fired).toBe(0);
321
+ expect(target.x).toBe(100); // values still applied
322
+ });
323
+ });
324
+
325
+ describe('stopTweens', () => {
326
+ it('stops all tweens for a target', () => {
327
+ const manager = createTweenManager();
328
+ const target = { x: 0, y: 0 };
329
+ const a = createTween(manager, target, 1000, { x: 100 });
330
+ const b = createTween(manager, target, 1000, { y: 100 }, { overwrite: false });
331
+ stopTweens(manager, target);
332
+ expect(a.complete).toBe(true);
333
+ expect(b.complete).toBe(true);
334
+ });
335
+
336
+ it('stops only tweens with matching properties', () => {
337
+ const manager = createTweenManager();
338
+ const target = { x: 0, y: 0 };
339
+ const a = createTween(manager, target, 1000, { x: 100 });
340
+ const b = createTween(manager, target, 1000, { y: 100 }, { overwrite: false });
341
+ stopTweens(manager, target, { x: 0 });
342
+ expect(a.complete).toBe(true);
343
+ expect(b.complete).toBe(false);
344
+ });
345
+
346
+ it('leaves the target value at its current position, does not jump to end', () => {
347
+ const manager = createTweenManager();
348
+ const target = { x: 0 };
349
+ createTween(manager, target, 1000, { x: 100 }, { ease: (t) => t });
350
+ updateTweens(manager, 500);
351
+ expect(target.x).toBeCloseTo(50);
352
+ stopTweens(manager, target);
353
+ expect(target.x).toBeCloseTo(50); // unchanged — use completeTween to jump to end instead
354
+ });
355
+
356
+ it('complete: true jumps to end values before stopping', () => {
357
+ const manager = createTweenManager();
358
+ const target = { x: 0 };
359
+ createTween(manager, target, 1000, { x: 100 }, { ease: (t) => t });
360
+ updateTweens(manager, 500);
361
+ stopTweens(manager, target, undefined, { complete: true });
362
+ expect(target.x).toBe(100);
363
+ });
364
+
365
+ it('complete: true fires onComplete by default', () => {
366
+ const manager = createTweenManager();
367
+ const target = { x: 0 };
368
+ const tween = createTween(manager, target, 1000, { x: 100 });
369
+ let fired = 0;
370
+ connectSignal(tween.onComplete, () => fired++);
371
+ stopTweens(manager, target, undefined, { complete: true });
372
+ expect(fired).toBe(1);
373
+ });
374
+
375
+ it('complete: true with sendEvent: false suppresses onComplete', () => {
376
+ const manager = createTweenManager();
377
+ const target = { x: 0 };
378
+ const tween = createTween(manager, target, 1000, { x: 100 });
379
+ let fired = 0;
380
+ connectSignal(tween.onComplete, () => fired++);
381
+ stopTweens(manager, target, undefined, { complete: true, sendEvent: false });
382
+ expect(fired).toBe(0);
383
+ expect(target.x).toBe(100); // values still applied
384
+ });
385
+ });
@@ -0,0 +1,52 @@
1
+ import { createTween } from './tween';
2
+ import { createTweenManager, defaultManager } from './tweenManager';
3
+ import { updateTweens } from './updateTweens';
4
+
5
+ describe('createTweenManager', () => {
6
+ it('returns a TweenManager with empty tweens map', () => {
7
+ const manager = createTweenManager();
8
+ expect(manager.tweens).toBeInstanceOf(Map);
9
+ expect(manager.tweens.size).toBe(0);
10
+ });
11
+
12
+ it('each call returns a distinct instance', () => {
13
+ const a = createTweenManager();
14
+ const b = createTweenManager();
15
+ expect(a).not.toBe(b);
16
+ expect(a.tweens).not.toBe(b.tweens);
17
+ });
18
+ });
19
+
20
+ describe('defaultEase', () => {
21
+ it('defaults to easeOutExponential when no option provided', () => {
22
+ const manager = createTweenManager();
23
+ expect(typeof manager.defaultEase).toBe('function');
24
+ expect(manager.defaultEase(0)).toBe(0);
25
+ expect(manager.defaultEase(1)).toBeCloseTo(1);
26
+ });
27
+
28
+ it('uses provided defaultEase for tweens that omit ease option', () => {
29
+ const linearEase = (t: number) => t;
30
+ const manager = createTweenManager({ defaultEase: linearEase });
31
+ const target = { x: 0 };
32
+ createTween(manager, target, 1000, { x: 100 });
33
+ updateTweens(manager, 500);
34
+ expect(target.x).toBeCloseTo(50); // linear: exactly 50% at 50% time
35
+ });
36
+
37
+ it('explicit ease option overrides defaultEase', () => {
38
+ const linearEase = (t: number) => t;
39
+ const alwaysOne = () => 1;
40
+ const manager = createTweenManager({ defaultEase: linearEase });
41
+ const target = { x: 0 };
42
+ createTween(manager, target, 1000, { x: 100 }, { ease: alwaysOne });
43
+ updateTweens(manager, 1);
44
+ expect(target.x).toBe(100); // alwaysOne ease jumps straight to end
45
+ });
46
+ });
47
+
48
+ describe('defaultManager', () => {
49
+ it('is a TweenManager', () => {
50
+ expect(defaultManager.tweens).toBeInstanceOf(Map);
51
+ });
52
+ });