littlejsengine 1.18.29 → 1.20.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 (51) hide show
  1. package/FAQ.md +841 -815
  2. package/README.md +211 -199
  3. package/dist/littlejs.d.ts +4383 -812
  4. package/dist/littlejs.esm.js +31848 -16680
  5. package/dist/littlejs.esm.min.js +14 -1
  6. package/dist/littlejs.js +31230 -16166
  7. package/dist/littlejs.min.js +13 -1
  8. package/dist/littlejs.release.js +27419 -15457
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +375 -0
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +740 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -330
  16. package/plugins/math3d.js +911 -0
  17. package/plugins/medalSystem.js +118 -72
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +196 -115
  21. package/plugins/postProcess.js +284 -166
  22. package/plugins/render3d.js +4072 -0
  23. package/plugins/render3dDebug.js +109 -0
  24. package/plugins/render3dExtras.js +1754 -0
  25. package/plugins/render3dVoxels.js +502 -0
  26. package/plugins/textureSheet.js +86 -48
  27. package/plugins/threejs.js +183 -158
  28. package/plugins/tweakables.js +411 -0
  29. package/plugins/tweenSystem.js +646 -526
  30. package/plugins/uiSystem.js +2024 -1687
  31. package/plugins/zzfxm.js +8 -15
  32. package/src/engine.js +345 -194
  33. package/src/engineAudio.js +1022 -777
  34. package/src/engineBuild.mjs +289 -253
  35. package/src/engineDebug.js +912 -735
  36. package/src/engineDraw.js +1774 -1510
  37. package/src/engineEditor.js +2431 -0
  38. package/src/engineExport.js +419 -396
  39. package/src/engineInput.js +1462 -1293
  40. package/src/engineLogo.js +4 -3
  41. package/src/engineMath.js +1274 -1183
  42. package/src/engineObject.js +674 -551
  43. package/src/engineParticles.js +583 -532
  44. package/src/engineRelease.js +67 -45
  45. package/src/engineSettings.js +808 -726
  46. package/src/engineTileLayer.js +995 -741
  47. package/src/engineUtilities.js +32 -13
  48. package/src/engineWebGL.js +1162 -941
  49. package/src/jsconfig.json +17 -13
  50. package/plugins/box2d.wasm.js +0 -630
  51. package/plugins/box2d.wasm.wasm +0 -0
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.18.29",
3
+ "version": "1.20.0",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
7
7
  "exports": {
8
- "types": "./dist/littlejs.d.ts",
9
- "production": "./dist/littlejs.esm.min.js",
10
- "default": "./dist/littlejs.esm.js"
8
+ ".": {
9
+ "types": "./dist/littlejs.d.ts",
10
+ "production": "./dist/littlejs.esm.min.js",
11
+ "default": "./dist/littlejs.esm.js"
12
+ },
13
+ "./dist/*": "./dist/*",
14
+ "./package.json": "./package.json"
11
15
  },
12
16
  "repository": {
13
17
  "type": "git",
@@ -33,6 +37,7 @@
33
37
  "dist/",
34
38
  "src/",
35
39
  "plugins/",
40
+ "!plugins/box2d.wasm.*",
36
41
  "COPYRIGHT.txt",
37
42
  "FAQ.md"
38
43
  ],
@@ -43,7 +48,7 @@
43
48
  "test": "node --test --import ./test/setup.mjs \"test/**/*.test.mjs\""
44
49
  },
45
50
  "engines": {
46
- "node": ">=18"
51
+ "node": "^20.19.0 || >=22.12.0"
47
52
  },
48
53
  "devDependencies": {
49
54
  "bestzip": "^2.2.1",
@@ -0,0 +1,375 @@
1
+ /**
2
+ * LittleJS Audio Effects Plugin
3
+ * - Web Audio effects with a wet/dry mix: filter, reverb, delay, distortion, compressor
4
+ * - Route a sound through one with sound.output = effect
5
+ * - Route everything with setAudioMasterEffect(effect)
6
+ * - Chain effects with effect.connect(nextEffect)
7
+ * @namespace AudioEffects
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ ///////////////////////////////////////////////////////////////////////////////
13
+ // audioParamRamp lives in engineAudio.js, core loads before plugins in the bundle
14
+ /**
15
+ * Base class for audio effects, an input and output with a wet/dry mix between them
16
+ * - Sounds connect to input, output goes to the master gain until connect() moves it
17
+ * - Subclasses put their nodes between input and the wet gain with connectEffect
18
+ * @memberof AudioEffects
19
+ * @example
20
+ * const cave = new AudioReverb(3, 2);
21
+ * footstep.output = cave; // every play of this sound is in the cave
22
+ */
23
+ class AudioEffect
24
+ {
25
+ /** Create an audio effect
26
+ * @param {number} [mix] - Wet/dry balance, 0 is fully dry and 1 is fully wet */
27
+ constructor(mix=1)
28
+ {
29
+ ASSERT(isNumber(mix), 'mix must be a number');
30
+
31
+ /** @property {number} - Wet/dry balance, 0 is fully dry and 1 is fully wet */
32
+ this.mix = clamp(mix);
33
+ if (!audioContext) return; // no audio outside a browser, where the engine runs headless
34
+
35
+ /** @property {GainNode} - Connect sounds to this node */
36
+ this.input = audioContext.createGain();
37
+ /** @property {GainNode} - This node carries the mixed result, send it somewhere with connect(), never by assigning here
38
+ * - Unlike sound.output, which is where a sound's audio goes and can be set to an effect */
39
+ this.output = audioContext.createGain();
40
+ /** @property {GainNode} - Level of the unprocessed signal */
41
+ this.dryGain = audioContext.createGain();
42
+ /** @property {GainNode} - Level of the processed signal */
43
+ this.wetGain = audioContext.createGain();
44
+
45
+ this.input.connect(this.dryGain).connect(this.output);
46
+ this.wetGain.connect(this.output);
47
+ this.setMix(mix);
48
+
49
+ // send the result to the speakers, connect() moves it into a chain instead
50
+ this.output.connect(audioMasterGain);
51
+ }
52
+
53
+ /** Set the wet/dry balance
54
+ * @param {number} mix - 0 is fully dry and 1 is fully wet
55
+ * @param {number} [fadeTime] - Seconds to ramp over so the change doesn't click */
56
+ setMix(mix, fadeTime=0)
57
+ {
58
+ ASSERT(isNumber(mix), 'mix must be a number');
59
+ this.mix = mix = clamp(mix);
60
+ if (!audioContext) return;
61
+ this.rampParam(this.dryGain.gain, 1-mix, fadeTime);
62
+ this.rampParam(this.wetGain.gain, mix, fadeTime);
63
+ }
64
+
65
+ /** Ramp one of this effect's params, keeping the effect running until the ramp is done
66
+ * - The browser drops an effect from rendering while nothing plays through it, which
67
+ * would freeze a ramp partway, so a silent source feeds the input for the ramp's length
68
+ * @param {AudioParam} param - The param to ramp
69
+ * @param {number} value - Where to ramp to
70
+ * @param {number} [fadeTime] - Seconds to ramp over, 0 sets the value at once
71
+ * @protected */
72
+ rampParam(param, value, fadeTime=0)
73
+ {
74
+ audioParamRamp(param, value, fadeTime);
75
+ if (!fadeTime) return;
76
+ const keepAlive = new ConstantSourceNode(audioContext, { offset: 0 });
77
+ keepAlive.connect(this.input);
78
+ keepAlive.onended = ()=> keepAlive.disconnect();
79
+ keepAlive.start();
80
+ keepAlive.stop(audioContext.currentTime + fadeTime);
81
+ }
82
+
83
+ /** Send this effect's output into another effect or audio node instead of the speakers
84
+ * @template {AudioEffect|AudioNode} T
85
+ * @param {T} target - The next effect in the chain, or any audio node
86
+ * @return {T} - The target, so chains read left to right */
87
+ connect(target)
88
+ {
89
+ if (!audioContext) return target; // no audio outside a browser, where the engine runs headless
90
+
91
+ // an effect stands in for its input node, the same rule as sound.output
92
+ const node = /** @type {AudioNode} */ (target && 'input' in target ? target.input : target);
93
+ ASSERT(node && typeof node.connect === 'function', 'target must be an AudioEffect or AudioNode');
94
+ this.output.disconnect();
95
+ this.output.connect(node);
96
+ return target;
97
+ }
98
+
99
+ /** Stop sending this effect's output anywhere */
100
+ disconnect() { this.output?.disconnect(); }
101
+
102
+ /** Wire nodes between the input and the wet gain, for subclasses
103
+ * @param {AudioNode} first - Node the input connects to
104
+ * @param {AudioNode} [last=first] - Node that connects to the wet gain
105
+ * @protected */
106
+ connectEffect(first, last=first)
107
+ {
108
+ this.input.connect(first);
109
+ last.connect(this.wetGain);
110
+ }
111
+ }
112
+
113
+ ///////////////////////////////////////////////////////////////////////////////
114
+ /**
115
+ * Filter effect, muffle sounds underwater or behind a wall
116
+ * @extends AudioEffect
117
+ * @memberof AudioEffects
118
+ * @example
119
+ * const muffle = new AudioFilter('lowpass', 400);
120
+ * setAudioMasterEffect(muffle);
121
+ * muffle.setFrequency(20000, .5); // sweep back to clear
122
+ */
123
+ class AudioFilter extends AudioEffect
124
+ {
125
+ /** Create a filter effect
126
+ * @param {BiquadFilterType} [type] - lowpass, highpass, bandpass, notch, etc.
127
+ * @param {number} [frequency] - Cutoff or center frequency in Hz
128
+ * @param {number} [q] - Resonance at the cutoff, higher is sharper
129
+ * @param {number} [mix] - Wet/dry balance, 0 is fully dry and 1 is fully wet */
130
+ constructor(type='lowpass', frequency=1000, q=1, mix=1)
131
+ {
132
+ super(mix);
133
+ ASSERT(isNumber(frequency) && frequency >= 0, 'frequency must be positive or zero');
134
+ ASSERT(isNumber(q), 'q must be a number');
135
+ if (!audioContext) return; // no audio outside a browser, where the engine runs headless
136
+
137
+ /** @property {BiquadFilterNode} - The filter node */
138
+ this.node = audioContext.createBiquadFilter();
139
+ this.node.type = type;
140
+ this.node.frequency.value = frequency;
141
+ this.node.Q.value = q;
142
+ this.connectEffect(this.node);
143
+ }
144
+
145
+ /** Set the cutoff or center frequency
146
+ * @param {number} frequency - Frequency in Hz
147
+ * @param {number} [fadeTime] - Seconds to sweep over */
148
+ setFrequency(frequency, fadeTime=0)
149
+ {
150
+ ASSERT(isNumber(frequency) && frequency >= 0, 'frequency must be positive or zero');
151
+ if (!audioContext) return;
152
+ this.rampParam(this.node.frequency, frequency, fadeTime);
153
+ }
154
+
155
+ /** Set the resonance at the cutoff
156
+ * @param {number} q - Higher is sharper
157
+ * @param {number} [fadeTime] - Seconds to ramp over */
158
+ setQ(q, fadeTime=0)
159
+ {
160
+ ASSERT(isNumber(q), 'q must be a number');
161
+ if (!audioContext) return;
162
+ this.rampParam(this.node.Q, q, fadeTime);
163
+ }
164
+ }
165
+
166
+ ///////////////////////////////////////////////////////////////////////////////
167
+ /**
168
+ * Reverb effect, puts sounds in a room, cave, or hall
169
+ * - The impulse response is generated, no audio file needed
170
+ * @extends AudioEffect
171
+ * @memberof AudioEffects
172
+ * @example
173
+ * const hall = new AudioReverb(4, 1.5, .4);
174
+ * footstep.output = hall;
175
+ */
176
+ class AudioReverb extends AudioEffect
177
+ {
178
+ /** Create a reverb effect
179
+ * @param {number} [duration] - Seconds until the reverb tail is silent
180
+ * @param {number} [decay] - How quickly the tail fades, higher is faster
181
+ * @param {number} [mix] - Wet/dry balance, 0 is fully dry and 1 is fully wet */
182
+ constructor(duration=2, decay=2, mix=.5)
183
+ {
184
+ super(mix);
185
+ if (!audioContext) return; // no audio outside a browser, where the engine runs headless
186
+
187
+ /** @property {ConvolverNode} - The convolver node */
188
+ this.node = audioContext.createConvolver();
189
+ this.setRoom(duration, decay);
190
+ this.connectEffect(this.node);
191
+ }
192
+
193
+ /** Change the room by rebuilding the impulse response
194
+ * @param {number} duration - Seconds until the reverb tail is silent
195
+ * @param {number} [decay] - How quickly the tail fades, higher is faster */
196
+ setRoom(duration, decay=2)
197
+ {
198
+ ASSERT(isNumber(duration) && duration > 0, 'duration must be positive');
199
+ ASSERT(isNumber(decay) && decay > 0, 'decay must be positive');
200
+ if (!audioContext) return;
201
+ this.node.buffer = this.createImpulse(duration, decay);
202
+ }
203
+
204
+ /** Build a stereo impulse response of decaying noise
205
+ * @param {number} duration - Seconds until silence
206
+ * @param {number} decay - How quickly it fades, higher is faster
207
+ * @return {AudioBuffer} */
208
+ createImpulse(duration, decay)
209
+ {
210
+ const sampleRate = audioContext.sampleRate;
211
+ const length = max(1, sampleRate * duration | 0);
212
+ const buffer = audioContext.createBuffer(2, length, sampleRate);
213
+ for (let channel = 2; channel--;)
214
+ {
215
+ const samples = buffer.getChannelData(channel);
216
+ for (let i = length; i--;)
217
+ samples[i] = rand(-1, 1) * (1 - i/length) ** decay;
218
+ }
219
+ return buffer;
220
+ }
221
+ }
222
+
223
+ ///////////////////////////////////////////////////////////////////////////////
224
+ /**
225
+ * Delay effect, echoes that repeat and fade
226
+ * @extends AudioEffect
227
+ * @memberof AudioEffects
228
+ * @example
229
+ * const canyon = new AudioDelay(.4, .5);
230
+ * shout.output = canyon;
231
+ */
232
+ class AudioDelay extends AudioEffect
233
+ {
234
+ /** Create a delay effect
235
+ * @param {number} [time] - Seconds between echoes, up to 5
236
+ * @param {number} [feedback] - How much of each echo repeats, 0 to .95
237
+ * @param {number} [mix] - Wet/dry balance, 0 is fully dry and 1 is fully wet */
238
+ constructor(time=.3, feedback=.4, mix=.5)
239
+ {
240
+ super(mix);
241
+ if (!audioContext) return; // no audio outside a browser, where the engine runs headless
242
+
243
+ /** @property {DelayNode} - The delay node */
244
+ this.node = audioContext.createDelay(5);
245
+ /** @property {GainNode} - How much of the delayed signal feeds back in */
246
+ this.feedbackGain = audioContext.createGain();
247
+ this.node.connect(this.feedbackGain).connect(this.node);
248
+ this.connectEffect(this.node);
249
+ this.setTime(time);
250
+ this.setFeedback(feedback);
251
+ }
252
+
253
+ /** Set the time between echoes
254
+ * - Browsers hold a delay in a feedback loop to at least one render quantum, so 0 is not a bypass
255
+ * @param {number} time - Seconds, up to 5
256
+ * @param {number} [fadeTime] - Seconds to ramp over, pitch bends while it moves */
257
+ setTime(time, fadeTime=0)
258
+ {
259
+ ASSERT(isNumber(time) && time >= 0 && time <= 5, 'time must be between 0 and 5');
260
+ if (!audioContext) return;
261
+ this.rampParam(this.node.delayTime, time, fadeTime);
262
+ }
263
+
264
+ /** Set how much of each echo repeats, clamped below 1 so it always dies out
265
+ * @param {number} feedback - 0 to .95
266
+ * @param {number} [fadeTime] - Seconds to ramp over */
267
+ setFeedback(feedback, fadeTime=0)
268
+ {
269
+ ASSERT(isNumber(feedback), 'feedback must be a number');
270
+ if (!audioContext) return;
271
+ this.rampParam(this.feedbackGain.gain, clamp(feedback, 0, .95), fadeTime);
272
+ }
273
+ }
274
+
275
+ ///////////////////////////////////////////////////////////////////////////////
276
+ /**
277
+ * Distortion effect, overdrive for radios, damaged robots, and engines
278
+ * @extends AudioEffect
279
+ * @memberof AudioEffects
280
+ * @example
281
+ * const radio = new AudioDistortion(.8);
282
+ * voice.output = radio;
283
+ */
284
+ class AudioDistortion extends AudioEffect
285
+ {
286
+ /** Create a distortion effect
287
+ * @param {number} [amount] - How hard to drive the signal, 0 is clean and 1 is crushed
288
+ * @param {number} [mix] - Wet/dry balance, 0 is fully dry and 1 is fully wet */
289
+ constructor(amount=.5, mix=1)
290
+ {
291
+ super(mix);
292
+
293
+ /** @property {number} - How hard the signal is driven, 0 is clean and 1 is crushed */
294
+ this.amount = clamp(amount);
295
+ if (!audioContext) return; // no audio outside a browser, where the engine runs headless
296
+
297
+ /** @property {WaveShaperNode} - The wave shaper node */
298
+ this.node = audioContext.createWaveShaper();
299
+ this.node.oversample = '2x';
300
+ this.setAmount(amount);
301
+ this.connectEffect(this.node);
302
+ }
303
+
304
+ /** Set how hard to drive the signal, rebuilds the shaping curve
305
+ * @param {number} amount - 0 is clean and 1 is crushed */
306
+ setAmount(amount)
307
+ {
308
+ ASSERT(isNumber(amount), 'amount must be a number');
309
+ this.amount = amount = clamp(amount);
310
+ if (!audioContext) return;
311
+
312
+ // soft clip curve, drive grows with the square of amount so low values stay subtle
313
+ // enough points that quiet signals are still shaped at high drive, where the curve is steep near 0
314
+ const drive = 100 * amount * amount;
315
+ const samples = 1024;
316
+ const curve = new Float32Array(samples);
317
+ for (let i = samples; i--;)
318
+ {
319
+ const x = i * 2 / (samples - 1) - 1;
320
+ curve[i] = (1 + drive) * x / (1 + drive * abs(x));
321
+ }
322
+ this.node.curve = curve;
323
+ }
324
+ }
325
+
326
+ ///////////////////////////////////////////////////////////////////////////////
327
+ /**
328
+ * Compressor effect, evens out loud and quiet so many sounds at once don't clip
329
+ * - Meant for the master bus, it is not on by default
330
+ * @extends AudioEffect
331
+ * @memberof AudioEffects
332
+ * @example
333
+ * const compressor = new AudioCompressor;
334
+ * setAudioMasterEffect(compressor);
335
+ */
336
+ class AudioCompressor extends AudioEffect
337
+ {
338
+ /** Create a compressor effect
339
+ * @param {number} [threshold] - Level in dB above which the signal is reduced
340
+ * @param {number} [ratio] - How much to reduce it, 12 means 12 dB in becomes 1 dB out
341
+ * @param {number} [mix] - Wet/dry balance, 0 is fully dry and 1 is fully wet */
342
+ constructor(threshold=-24, ratio=12, mix=1)
343
+ {
344
+ super(mix);
345
+ ASSERT(isNumber(threshold), 'threshold must be a number');
346
+ ASSERT(isNumber(ratio) && ratio >= 1, 'ratio must be 1 or more');
347
+ if (!audioContext) return; // no audio outside a browser, where the engine runs headless
348
+
349
+ /** @property {DynamicsCompressorNode} - The compressor node */
350
+ this.node = audioContext.createDynamicsCompressor();
351
+ this.node.threshold.value = threshold;
352
+ this.node.ratio.value = ratio;
353
+ this.connectEffect(this.node);
354
+ }
355
+
356
+ /** Set the level above which the signal is reduced
357
+ * @param {number} threshold - Level in dB
358
+ * @param {number} [fadeTime] - Seconds to ramp over */
359
+ setThreshold(threshold, fadeTime=0)
360
+ {
361
+ ASSERT(isNumber(threshold), 'threshold must be a number');
362
+ if (!audioContext) return;
363
+ this.rampParam(this.node.threshold, threshold, fadeTime);
364
+ }
365
+
366
+ /** Set how much the signal is reduced above the threshold
367
+ * @param {number} ratio - 1 is no reduction, 20 is a hard limit
368
+ * @param {number} [fadeTime] - Seconds to ramp over */
369
+ setRatio(ratio, fadeTime=0)
370
+ {
371
+ ASSERT(isNumber(ratio) && ratio >= 1, 'ratio must be 1 or more');
372
+ if (!audioContext) return;
373
+ this.rampParam(this.node.ratio, ratio, fadeTime);
374
+ }
375
+ }