littlejsengine 1.18.2 → 1.18.4
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/AI.md +1 -0
- package/LICENSE +5 -27
- package/dist/littlejs.d.ts +201 -3
- package/dist/littlejs.esm.js +713 -18
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +701 -18
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +701 -18
- package/examples/index.html +62 -800
- package/examples/shorts/base.html +1 -1
- package/examples/shorts/textureWrapped.js +12 -0
- package/examples/shorts/tween.js +24 -0
- package/examples/shorts.js +743 -0
- package/examples/starter/index.html +2 -2
- package/examples/tweenSystem/game.js +171 -0
- package/examples/tweenSystem/index.html +10 -0
- package/examples/tweenSystem/tiles.png +0 -0
- package/package.json +1 -1
- package/plugins/box2d.js +4 -2
- package/plugins/pluginExport.js +7 -0
- package/plugins/tween.js +509 -0
- package/plugins/uiSystem.js +1 -1
- package/reference.md +1 -0
- package/src/engine.js +9 -6
- package/src/engineBuild.mjs +1 -0
- package/src/engineDraw.js +133 -2
- package/src/engineExport.js +5 -0
- package/src/engineInput.js +26 -0
- package/src/engineSettings.js +17 -0
- package/src/engineWebGL.js +3 -7
- package/test/smoke.test.mjs +16 -0
- package/test/tween.test.mjs +576 -0
- package/.claude/settings.local.json +0 -7
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { Tween, tweenProperty, tweenStopAll, tweenUpdate, Ease, vec2, rgb, Vector2, Color } from '../dist/littlejs.esm.js';
|
|
4
|
+
|
|
5
|
+
test('Tween, tweenProperty, tweenStopAll, tweenUpdate, Ease are exported from the bundle', () =>
|
|
6
|
+
{
|
|
7
|
+
assert.equal(typeof Tween, 'function');
|
|
8
|
+
assert.equal(typeof tweenProperty, 'function');
|
|
9
|
+
assert.equal(typeof tweenStopAll, 'function');
|
|
10
|
+
assert.equal(typeof tweenUpdate, 'function');
|
|
11
|
+
assert.equal(typeof Ease, 'object'); // namespace object, not a class
|
|
12
|
+
assert.equal(typeof Ease.LINEAR, 'function');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const near = (a, b, eps=1e-9) => Math.abs(a - b) <= eps;
|
|
16
|
+
|
|
17
|
+
test('Ease.LINEAR is the identity on [0,1]', () =>
|
|
18
|
+
{
|
|
19
|
+
assert(near(Ease.LINEAR(0), 0));
|
|
20
|
+
assert(near(Ease.LINEAR(0.5), 0.5));
|
|
21
|
+
assert(near(Ease.LINEAR(1), 1));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('Ease.POWER(2) returns x squared', () =>
|
|
25
|
+
{
|
|
26
|
+
const f = Ease.POWER(2);
|
|
27
|
+
assert(near(f(0), 0));
|
|
28
|
+
assert(near(f(0.5), 0.25));
|
|
29
|
+
assert(near(f(1), 1));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('Ease.SINE / CIRC / BACK / SPRING anchor at both 0 and 1; EXPO and ELASTIC only at 1', () =>
|
|
33
|
+
{
|
|
34
|
+
for (const f of [Ease.SINE, Ease.CIRC, Ease.BACK])
|
|
35
|
+
{
|
|
36
|
+
assert(near(f(0), 0), `${f.name}(0) should be 0`);
|
|
37
|
+
assert(near(f(1), 1), `${f.name}(1) should be 1`);
|
|
38
|
+
}
|
|
39
|
+
// EXPO doesn't hit 0 exactly at x=0 by formula (2^-10 ≈ 0.001), but does hit 1 at x=1
|
|
40
|
+
assert(near(Ease.EXPO(1), 1));
|
|
41
|
+
// ELASTIC also doesn't start at 0 (formula gives -2^-10 * sin(37π/6) ≈ -0.0005),
|
|
42
|
+
// but does finish at exactly 1 at x=1.
|
|
43
|
+
assert(near(Ease.ELASTIC(1), 1));
|
|
44
|
+
// SPRING formula evaluates to 1 at x=1
|
|
45
|
+
assert(near(Ease.SPRING(1), 1));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('Ease.BOUNCE finishes at 1', () =>
|
|
49
|
+
{
|
|
50
|
+
assert(near(Ease.BOUNCE(1), 1, 1e-6));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('Ease.IN is the identity transformer: returns the curve unchanged', () =>
|
|
54
|
+
{
|
|
55
|
+
// Direction-modifier shape: takes a curve, returns a curve. Used for
|
|
56
|
+
// programmatic direction picking, e.g. (bouncy ? Ease.OUT : Ease.IN)(curve).
|
|
57
|
+
assert.equal(Ease.IN(Ease.SINE), Ease.SINE);
|
|
58
|
+
assert.equal(Ease.IN(Ease.BACK), Ease.BACK);
|
|
59
|
+
// The implementation is a true identity function, so passing a number
|
|
60
|
+
// also returns it unchanged — meaning Ease.IN doubles as a linear curve.
|
|
61
|
+
assert(near(Ease.IN(0), 0));
|
|
62
|
+
assert(near(Ease.IN(0.5), 0.5));
|
|
63
|
+
assert(near(Ease.IN(1), 1));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('Ease.OUT(LINEAR) is identity', () =>
|
|
67
|
+
{
|
|
68
|
+
const f = Ease.OUT(Ease.LINEAR);
|
|
69
|
+
assert(near(f(0), 0));
|
|
70
|
+
assert(near(f(0.5), 0.5));
|
|
71
|
+
assert(near(f(1), 1));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('Ease.OUT(POWER(2)) starts fast, ends slow', () =>
|
|
75
|
+
{
|
|
76
|
+
const f = Ease.OUT(Ease.POWER(2));
|
|
77
|
+
assert(near(f(0), 0));
|
|
78
|
+
assert(near(f(1), 1));
|
|
79
|
+
// f(x) = 1 - (1-x)^2; f(0.5) = 1 - 0.25 = 0.75
|
|
80
|
+
assert(near(f(0.5), 0.75));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('Ease.PIECEWISE splits range across the supplied curves', () =>
|
|
84
|
+
{
|
|
85
|
+
// PIECEWISE(LINEAR, LINEAR) is equivalent to LINEAR
|
|
86
|
+
const f = Ease.PIECEWISE(Ease.LINEAR, Ease.LINEAR);
|
|
87
|
+
assert(near(f(0), 0));
|
|
88
|
+
assert(near(f(0.25), 0.25));
|
|
89
|
+
assert(near(f(0.5), 0.5));
|
|
90
|
+
assert(near(f(0.75), 0.75));
|
|
91
|
+
assert(near(f(0.9999), 0.9999, 1e-3));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('Ease.IN_OUT(LINEAR) at 0.5 is 0.5 (regression: original referenced undefined Piecewise)', () =>
|
|
95
|
+
{
|
|
96
|
+
const f = Ease.IN_OUT(Ease.LINEAR);
|
|
97
|
+
assert(near(f(0), 0));
|
|
98
|
+
assert(near(f(0.5), 0.5));
|
|
99
|
+
assert(near(f(1), 1, 1e-3));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('Ease.BOUNCE is the ease-in form (slow ramp, bouncing impacts near the end)', () =>
|
|
103
|
+
{
|
|
104
|
+
// Mirror of easeOutBounce: where easeOutBounce peaks at 1 at x = 4/11
|
|
105
|
+
// (early), easeInBounce reaches 0 at x = 1 - 4/11 = 7/11 (late dip near
|
|
106
|
+
// the start). This direction matches the other ease-in base curves.
|
|
107
|
+
assert(near(Ease.BOUNCE(7/11), 0, 1e-9));
|
|
108
|
+
// At x=0.5 the ease-in form is 1 - 0.765625 = 0.234375.
|
|
109
|
+
assert(near(Ease.BOUNCE(0.5), 0.234375, 1e-9));
|
|
110
|
+
// Endpoints
|
|
111
|
+
assert(near(Ease.BOUNCE(0), 0, 1e-9));
|
|
112
|
+
assert(near(Ease.BOUNCE(1), 1, 1e-9));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('Ease.OUT(Ease.BOUNCE) is the canonical "ground impact" easeOutBounce', () =>
|
|
116
|
+
{
|
|
117
|
+
const f = Ease.OUT(Ease.BOUNCE);
|
|
118
|
+
// Now THIS is the form where the value peaks at 1 at x = 4/11 (early
|
|
119
|
+
// bounce) — the shape users expect when they want a bouncing landing.
|
|
120
|
+
assert(near(f(4/11), 1, 1e-9));
|
|
121
|
+
assert(near(f(0.5), 0.765625, 1e-9));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('Ease.BEZIER(0,0,1,1) is approximately linear', () =>
|
|
125
|
+
{
|
|
126
|
+
const f = Ease.BEZIER(0, 0, 1, 1);
|
|
127
|
+
assert(near(f(0), 0, 1e-3));
|
|
128
|
+
assert(near(f(0.25), 0.25, 1e-3));
|
|
129
|
+
assert(near(f(0.5), 0.5, 1e-3));
|
|
130
|
+
assert(near(f(0.75), 0.75, 1e-3));
|
|
131
|
+
assert(near(f(1), 1, 1e-3));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('Ease.BEZIER endpoints are (0,0) and (1,1) for symmetric ease-in-out', () =>
|
|
135
|
+
{
|
|
136
|
+
// ease-in-out style control points
|
|
137
|
+
const f = Ease.BEZIER(0.42, 0, 0.58, 1);
|
|
138
|
+
assert(near(f(0), 0, 1e-3));
|
|
139
|
+
assert(near(f(1), 1, 1e-3));
|
|
140
|
+
// crosses 0.5 at x=0.5 due to symmetry
|
|
141
|
+
assert(near(f(0.5), 0.5, 1e-2));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('Tween constructor calls callback once with the start value', () =>
|
|
145
|
+
{
|
|
146
|
+
const calls = [];
|
|
147
|
+
const t = new Tween((v) => calls.push(v), 5, 10, 1);
|
|
148
|
+
assert.deepEqual(calls, [5]);
|
|
149
|
+
t.stop(); // cleanup
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('Tween constructor stores duration, ease, useRealTime, paused options', () =>
|
|
153
|
+
{
|
|
154
|
+
const t = new Tween(() => {}, 0, 1, 2, { ease: Ease.SINE, useRealTime: true, paused: true });
|
|
155
|
+
assert.equal(t.duration, 2);
|
|
156
|
+
assert.equal(t.life, 2);
|
|
157
|
+
assert.equal(t.ease, Ease.SINE);
|
|
158
|
+
assert.equal(t.useRealTime, true);
|
|
159
|
+
assert.equal(t.paused, true);
|
|
160
|
+
t.stop();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('Tween defaults: start=0 end=1 duration=1 ease=LINEAR useRealTime=false paused=false', () =>
|
|
164
|
+
{
|
|
165
|
+
const t = new Tween(() => {});
|
|
166
|
+
assert.equal(t.start, 0);
|
|
167
|
+
assert.equal(t.end, 1);
|
|
168
|
+
assert.equal(t.duration, 1);
|
|
169
|
+
assert.equal(t.ease, Ease.LINEAR);
|
|
170
|
+
assert.equal(t.useRealTime, false);
|
|
171
|
+
assert.equal(t.paused, false);
|
|
172
|
+
t.stop();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('Tween.setEase returns this and updates ease', () =>
|
|
176
|
+
{
|
|
177
|
+
const t = new Tween(() => {});
|
|
178
|
+
const ret = t.setEase(Ease.SINE);
|
|
179
|
+
assert.equal(ret, t);
|
|
180
|
+
assert.equal(t.ease, Ease.SINE);
|
|
181
|
+
t.stop();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('Tween advances toward end as tweenUpdate is called', () =>
|
|
185
|
+
{
|
|
186
|
+
const calls = [];
|
|
187
|
+
const t = new Tween((v) => calls.push(v), 0, 10, 1); // 1 second, 0 → 10
|
|
188
|
+
// constructor pushed start
|
|
189
|
+
assert.deepEqual(calls, [0]);
|
|
190
|
+
tweenUpdate(0.5); // halfway
|
|
191
|
+
assert.equal(calls.length, 2);
|
|
192
|
+
assert(near(calls[1], 5));
|
|
193
|
+
t.stop();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('Tween completes after duration seconds and fires the end value', () =>
|
|
197
|
+
{
|
|
198
|
+
const calls = [];
|
|
199
|
+
new Tween((v) => calls.push(v), 0, 10, 1);
|
|
200
|
+
tweenUpdate(1.0); // exactly one duration
|
|
201
|
+
// expect [start=0, end=10] — final callback fires with the end value
|
|
202
|
+
assert.deepEqual(calls, [0, 10]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('Tween.then(fn) fires once at completion', () =>
|
|
206
|
+
{
|
|
207
|
+
let thenCalls = 0;
|
|
208
|
+
const t = new Tween(() => {}, 0, 1, 1).then(() => thenCalls++);
|
|
209
|
+
tweenUpdate(1.0);
|
|
210
|
+
assert.equal(thenCalls, 1);
|
|
211
|
+
// a second update must not re-fire it (tween is removed from active list)
|
|
212
|
+
tweenUpdate(1.0);
|
|
213
|
+
assert.equal(thenCalls, 1);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('Tween.then returns this for chaining', () =>
|
|
217
|
+
{
|
|
218
|
+
const t = new Tween(() => {});
|
|
219
|
+
assert.equal(t.then(() => {}), t);
|
|
220
|
+
t.stop();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('Tween that overshoots its remaining life still fires end value once', () =>
|
|
224
|
+
{
|
|
225
|
+
const calls = [];
|
|
226
|
+
new Tween((v) => calls.push(v), 0, 10, 1);
|
|
227
|
+
tweenUpdate(5.0); // way past duration
|
|
228
|
+
assert.deepEqual(calls, [0, 10]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('useRealTime: true tween advances on realDelta even when gameDelta is 0', () =>
|
|
232
|
+
{
|
|
233
|
+
const calls = [];
|
|
234
|
+
new Tween((v) => calls.push(v), 0, 10, 1, { useRealTime: true });
|
|
235
|
+
tweenUpdate(0, 1.0); // game frozen, real time advances 1s
|
|
236
|
+
assert.deepEqual(calls, [0, 10]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('Default tween (game time) does not advance when gameDelta is 0', () =>
|
|
240
|
+
{
|
|
241
|
+
const calls = [];
|
|
242
|
+
const t = new Tween((v) => calls.push(v), 0, 10, 1);
|
|
243
|
+
tweenUpdate(0, 1.0); // game frozen, only real advances; default tween ignores realDelta
|
|
244
|
+
assert.deepEqual(calls, [0]);
|
|
245
|
+
t.stop();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('Multiple tweens complete in a single tweenUpdate without skipping', () =>
|
|
249
|
+
{
|
|
250
|
+
const aCalls = [];
|
|
251
|
+
const bCalls = [];
|
|
252
|
+
let aThen = 0;
|
|
253
|
+
let bThen = 0;
|
|
254
|
+
new Tween((v) => aCalls.push(v), 0, 10, 1).then(() => aThen++);
|
|
255
|
+
new Tween((v) => bCalls.push(v), 0, 20, 1).then(() => bThen++);
|
|
256
|
+
tweenUpdate(1.0); // both should finish
|
|
257
|
+
assert.deepEqual(aCalls, [0, 10]);
|
|
258
|
+
assert.deepEqual(bCalls, [0, 20]);
|
|
259
|
+
assert.equal(aThen, 1);
|
|
260
|
+
assert.equal(bThen, 1);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('tweenUpdate skips a tween whose paused field is true', () =>
|
|
264
|
+
{
|
|
265
|
+
const calls = [];
|
|
266
|
+
const t = new Tween((v) => calls.push(v), 0, 10, 1, { paused: true });
|
|
267
|
+
tweenUpdate(2.0);
|
|
268
|
+
assert.deepEqual(calls, [0]); // only the constructor start snap
|
|
269
|
+
t.stop();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('Tween.pause prevents advancement', () =>
|
|
273
|
+
{
|
|
274
|
+
const calls = [];
|
|
275
|
+
const t = new Tween((v) => calls.push(v), 0, 10, 1);
|
|
276
|
+
t.pause();
|
|
277
|
+
tweenUpdate(0.5);
|
|
278
|
+
assert.deepEqual(calls, [0]); // no advance
|
|
279
|
+
t.stop();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('Tween.resume re-enables advancement', () =>
|
|
283
|
+
{
|
|
284
|
+
const calls = [];
|
|
285
|
+
const t = new Tween((v) => calls.push(v), 0, 10, 1, { paused: true });
|
|
286
|
+
tweenUpdate(0.5); // paused, no advance
|
|
287
|
+
assert.deepEqual(calls, [0]);
|
|
288
|
+
t.resume();
|
|
289
|
+
tweenUpdate(0.5);
|
|
290
|
+
assert.equal(calls.length, 2);
|
|
291
|
+
assert(near(calls[1], 5));
|
|
292
|
+
t.stop();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('Tween.stop prevents the then-callback from firing', () =>
|
|
296
|
+
{
|
|
297
|
+
let thenCalls = 0;
|
|
298
|
+
const t = new Tween(() => {}, 0, 1, 1).then(() => thenCalls++);
|
|
299
|
+
t.stop();
|
|
300
|
+
tweenUpdate(2.0);
|
|
301
|
+
assert.equal(thenCalls, 0);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('Tween.restart resets life, clears pause, re-snaps to start', () =>
|
|
305
|
+
{
|
|
306
|
+
const calls = [];
|
|
307
|
+
const t = new Tween((v) => calls.push(v), 0, 10, 1);
|
|
308
|
+
tweenUpdate(0.6); // partway: calls = [0, 6]
|
|
309
|
+
t.pause();
|
|
310
|
+
t.restart();
|
|
311
|
+
assert.equal(t.life, 1);
|
|
312
|
+
assert.equal(t.paused, false);
|
|
313
|
+
// restart fires callback with start again
|
|
314
|
+
assert.equal(calls[calls.length - 1], 0);
|
|
315
|
+
t.stop();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('Tween.restart re-adds a stopped tween to the active list', () =>
|
|
319
|
+
{
|
|
320
|
+
const calls = [];
|
|
321
|
+
const t = new Tween((v) => calls.push(v), 0, 10, 1);
|
|
322
|
+
t.stop();
|
|
323
|
+
assert.equal(t.isActive(), false);
|
|
324
|
+
t.restart();
|
|
325
|
+
assert.equal(t.isActive(), true);
|
|
326
|
+
t.stop();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('Tween.isActive reflects active list + pause', () =>
|
|
330
|
+
{
|
|
331
|
+
const t = new Tween(() => {}, 0, 1, 1);
|
|
332
|
+
assert.equal(t.isActive(), true);
|
|
333
|
+
t.pause();
|
|
334
|
+
assert.equal(t.isActive(), false);
|
|
335
|
+
t.resume();
|
|
336
|
+
assert.equal(t.isActive(), true);
|
|
337
|
+
t.stop();
|
|
338
|
+
assert.equal(t.isActive(), false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('Tween.loop(3) runs 3 total iterations', () =>
|
|
342
|
+
{
|
|
343
|
+
const calls = [];
|
|
344
|
+
new Tween((v) => calls.push(v), 0, 10, 1).loop(3);
|
|
345
|
+
// iteration 1: constructor pushes 0; tweenUpdate(1.0) pushes 10
|
|
346
|
+
tweenUpdate(1.0);
|
|
347
|
+
// then-callback fires, creates iteration 2; that constructor pushes 0
|
|
348
|
+
// iteration 2: tweenUpdate below pushes 10, etc
|
|
349
|
+
tweenUpdate(1.0);
|
|
350
|
+
tweenUpdate(1.0);
|
|
351
|
+
// 3 iterations × 2 callback fires (start, end) each = 6 calls
|
|
352
|
+
assert.deepEqual(calls, [0, 10, 0, 10, 0, 10]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('Tween.loop(1) is the same as no loop (chain ends after first iteration)', () =>
|
|
356
|
+
{
|
|
357
|
+
const calls = [];
|
|
358
|
+
new Tween((v) => calls.push(v), 0, 10, 1).loop(1);
|
|
359
|
+
tweenUpdate(1.0);
|
|
360
|
+
tweenUpdate(1.0); // no second iteration scheduled
|
|
361
|
+
assert.deepEqual(calls, [0, 10]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('Tween.loop returns this for chaining', () =>
|
|
365
|
+
{
|
|
366
|
+
const t = new Tween(() => {});
|
|
367
|
+
assert.equal(t.loop(2), t);
|
|
368
|
+
t.stop();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('Calling Tween.then after Tween.loop overrides the loop (last call wins)', () =>
|
|
372
|
+
{
|
|
373
|
+
let thenCalls = 0;
|
|
374
|
+
const calls = [];
|
|
375
|
+
new Tween((v) => { calls.push(v); }, 0, 10, 1)
|
|
376
|
+
.loop(5)
|
|
377
|
+
.then(() => thenCalls++);
|
|
378
|
+
tweenUpdate(1.0); // iteration 1 ends; then() fires its callback; loop chain is replaced
|
|
379
|
+
assert.equal(thenCalls, 1);
|
|
380
|
+
tweenUpdate(1.0); // no further iterations
|
|
381
|
+
assert.deepEqual(calls, [0, 10]);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('Tween.pingPong(3) swaps start/end between iterations', () =>
|
|
385
|
+
{
|
|
386
|
+
const calls = [];
|
|
387
|
+
new Tween((v) => calls.push(v), 0, 10, 1).pingPong(3);
|
|
388
|
+
tweenUpdate(1.0); // iter 1 (0→10) completes; iter 2 (10→0) starts (snaps to 10)
|
|
389
|
+
tweenUpdate(1.0); // iter 2 completes (→0); iter 3 (0→10) starts (snaps to 0)
|
|
390
|
+
tweenUpdate(1.0); // iter 3 completes (→10); chain ends
|
|
391
|
+
assert.deepEqual(calls, [0, 10, 10, 0, 0, 10]);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('Tween.pingPong returns this for chaining', () =>
|
|
395
|
+
{
|
|
396
|
+
const t = new Tween(() => {});
|
|
397
|
+
assert.equal(t.pingPong(2), t);
|
|
398
|
+
t.stop();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('Tween.pingPong(1) ends after one iteration', () =>
|
|
402
|
+
{
|
|
403
|
+
const calls = [];
|
|
404
|
+
new Tween((v) => calls.push(v), 0, 10, 1).pingPong(1);
|
|
405
|
+
tweenUpdate(1.0);
|
|
406
|
+
tweenUpdate(1.0);
|
|
407
|
+
assert.deepEqual(calls, [0, 10]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('tweenProperty drives a flat property by name', () =>
|
|
411
|
+
{
|
|
412
|
+
const obj = { x: 0 };
|
|
413
|
+
const t = tweenProperty(obj, 'x', 0, 10, 1);
|
|
414
|
+
assert.equal(obj.x, 0); // constructor snaps to start
|
|
415
|
+
tweenUpdate(0.5);
|
|
416
|
+
assert(near(obj.x, 5));
|
|
417
|
+
tweenUpdate(0.5);
|
|
418
|
+
assert(near(obj.x, 10));
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('tweenProperty walks dot paths into nested objects', () =>
|
|
422
|
+
{
|
|
423
|
+
const obj = { pos: { x: 0, y: 0 } };
|
|
424
|
+
tweenProperty(obj, 'pos.x', 0, 10, 1);
|
|
425
|
+
assert.equal(obj.pos.x, 0);
|
|
426
|
+
assert.equal(obj.pos.y, 0); // unaffected
|
|
427
|
+
tweenUpdate(1.0);
|
|
428
|
+
assert(near(obj.pos.x, 10));
|
|
429
|
+
assert.equal(obj.pos.y, 0);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('tweenProperty returns the underlying Tween for chaining', () =>
|
|
433
|
+
{
|
|
434
|
+
const obj = { x: 0 };
|
|
435
|
+
const t = tweenProperty(obj, 'x', 0, 10, 1);
|
|
436
|
+
assert(t instanceof Tween);
|
|
437
|
+
// chain methods work
|
|
438
|
+
assert.equal(t.setEase(Ease.SINE), t);
|
|
439
|
+
t.stop();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('tweenProperty supports options like useRealTime', () =>
|
|
443
|
+
{
|
|
444
|
+
const obj = { x: 0 };
|
|
445
|
+
const t = tweenProperty(obj, 'x', 0, 10, 1, { useRealTime: true });
|
|
446
|
+
assert.equal(t.useRealTime, true);
|
|
447
|
+
tweenUpdate(0, 1.0); // game frozen, real advances
|
|
448
|
+
assert(near(obj.x, 10));
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test('integration: chain setEase + then + tweenProperty + useRealTime', () =>
|
|
452
|
+
{
|
|
453
|
+
const obj = { value: 0 };
|
|
454
|
+
let chainFinished = false;
|
|
455
|
+
|
|
456
|
+
tweenProperty(obj, 'value', 0, 100, 2, { useRealTime: true })
|
|
457
|
+
.setEase(Ease.OUT(Ease.POWER(2)))
|
|
458
|
+
.then(() => { chainFinished = true; });
|
|
459
|
+
|
|
460
|
+
assert.equal(obj.value, 0);
|
|
461
|
+
tweenUpdate(0, 1.0); // half duration on real time
|
|
462
|
+
assert(obj.value > 0 && obj.value < 100, 'value should be partway');
|
|
463
|
+
tweenUpdate(0, 1.0); // finish
|
|
464
|
+
assert(near(obj.value, 100));
|
|
465
|
+
assert.equal(chainFinished, true);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test('Tween.getPercent: 0 at start, 0.5 at midpoint, 1 at completion', () =>
|
|
469
|
+
{
|
|
470
|
+
const t = new Tween(() => {}, 0, 10, 1);
|
|
471
|
+
assert.equal(t.getPercent(), 0);
|
|
472
|
+
tweenUpdate(0.5);
|
|
473
|
+
assert(near(t.getPercent(), 0.5));
|
|
474
|
+
tweenUpdate(0.5); // completes the tween
|
|
475
|
+
assert.equal(t.getPercent(), 1);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test('Tween.getPercent clamps overshoot to 1', () =>
|
|
479
|
+
{
|
|
480
|
+
const t = new Tween(() => {}, 0, 10, 1);
|
|
481
|
+
tweenUpdate(5.0); // overshoot
|
|
482
|
+
assert.equal(t.getPercent(), 1);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test('tweenStopAll stops every active tween and prevents then-callbacks from firing', () =>
|
|
486
|
+
{
|
|
487
|
+
let cbCalls = 0;
|
|
488
|
+
let thenCalls = 0;
|
|
489
|
+
new Tween(() => cbCalls++, 0, 1, 1).then(() => thenCalls += 100);
|
|
490
|
+
new Tween(() => cbCalls++, 0, 1, 1).then(() => thenCalls += 100);
|
|
491
|
+
new Tween(() => cbCalls++, 0, 1, 1);
|
|
492
|
+
// 3 callbacks fired from constructors snapping to start
|
|
493
|
+
assert.equal(cbCalls, 3);
|
|
494
|
+
tweenStopAll();
|
|
495
|
+
tweenUpdate(2.0); // would normally complete every tween, but they're stopped
|
|
496
|
+
assert.equal(cbCalls, 3); // no advancement — still just the snaps
|
|
497
|
+
assert.equal(thenCalls, 0); // no then-callbacks fired
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test('Tween accepts Vector2 start/end and interpolates each component', () =>
|
|
501
|
+
{
|
|
502
|
+
const calls = [];
|
|
503
|
+
new Tween((v) => calls.push(v), vec2(0, 0), vec2(10, 20), 1);
|
|
504
|
+
// Constructor snap fires the start value
|
|
505
|
+
assert.equal(calls.length, 1);
|
|
506
|
+
assert(calls[0] instanceof Vector2);
|
|
507
|
+
assert(near(calls[0].x, 0));
|
|
508
|
+
assert(near(calls[0].y, 0));
|
|
509
|
+
|
|
510
|
+
// Halfway: interpolated component-wise
|
|
511
|
+
tweenUpdate(0.5);
|
|
512
|
+
assert.equal(calls.length, 2);
|
|
513
|
+
assert(near(calls[1].x, 5));
|
|
514
|
+
assert(near(calls[1].y, 10));
|
|
515
|
+
|
|
516
|
+
// Completion: end value fires once
|
|
517
|
+
tweenUpdate(0.5);
|
|
518
|
+
assert.equal(calls.length, 3);
|
|
519
|
+
assert(near(calls[2].x, 10));
|
|
520
|
+
assert(near(calls[2].y, 20));
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('Tween accepts Color start/end and interpolates each channel', () =>
|
|
524
|
+
{
|
|
525
|
+
const calls = [];
|
|
526
|
+
new Tween((c) => calls.push(c), rgb(0, 0, 0, 1), rgb(1, 1, 1, 0), 1);
|
|
527
|
+
// Halfway should be (0.5, 0.5, 0.5, 0.5)
|
|
528
|
+
tweenUpdate(0.5);
|
|
529
|
+
const mid = calls[calls.length - 1];
|
|
530
|
+
assert(mid instanceof Color);
|
|
531
|
+
assert(near(mid.r, 0.5));
|
|
532
|
+
assert(near(mid.g, 0.5));
|
|
533
|
+
assert(near(mid.b, 0.5));
|
|
534
|
+
assert(near(mid.a, 0.5));
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test('tweenProperty walks dot-paths and assigns Vector2 values', () =>
|
|
538
|
+
{
|
|
539
|
+
const obj = { pos: vec2(0, 0) };
|
|
540
|
+
tweenProperty(obj, 'pos', vec2(0, 0), vec2(10, 20), 1);
|
|
541
|
+
// Constructor snap: obj.pos is replaced with a fresh Vector2 equal to start
|
|
542
|
+
assert(obj.pos instanceof Vector2);
|
|
543
|
+
assert(near(obj.pos.x, 0));
|
|
544
|
+
assert(near(obj.pos.y, 0));
|
|
545
|
+
|
|
546
|
+
// Completion
|
|
547
|
+
tweenUpdate(1.0);
|
|
548
|
+
assert(near(obj.pos.x, 10));
|
|
549
|
+
assert(near(obj.pos.y, 20));
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('Tween throws on type mismatch (Vector2 start, Color end)', () =>
|
|
553
|
+
{
|
|
554
|
+
assert.throws(() => new Tween(() => {}, vec2(0, 0), rgb(1, 0, 0, 1), 1));
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test('Tween.getValue returns the current interpolated value', () =>
|
|
558
|
+
{
|
|
559
|
+
const t = new Tween(() => {}, 0, 100, 1);
|
|
560
|
+
assert.equal(t.getValue(), 0); // at construction
|
|
561
|
+
tweenUpdate(0.25);
|
|
562
|
+
assert(near(t.getValue(), 25));
|
|
563
|
+
tweenUpdate(0.25);
|
|
564
|
+
assert(near(t.getValue(), 50));
|
|
565
|
+
t.stop();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test('Tween.getValue returns a Vector2 for Vector2 tweens', () =>
|
|
569
|
+
{
|
|
570
|
+
const t = new Tween(() => {}, vec2(0, 0), vec2(10, 20), 1);
|
|
571
|
+
const v = t.getValue();
|
|
572
|
+
assert(v instanceof Vector2);
|
|
573
|
+
assert(near(v.x, 0));
|
|
574
|
+
assert(near(v.y, 0));
|
|
575
|
+
t.stop();
|
|
576
|
+
});
|