littlejsengine 1.9.6 → 1.9.7

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.
@@ -750,14 +750,15 @@ class Color
750
750
  ).clamp();
751
751
  }
752
752
 
753
- /** Returns this color expressed as a rgb color code
753
+ /** Returns this color expressed as a hex color code
754
754
  * @param {Boolean} [useAlpha] - if alpha should be included in result
755
755
  * @return {String} */
756
- toString(useAlpha = true)
757
- {
758
- return `rgb(${this.r*255},${this.g*255},${this.b*255},${useAlpha ? this.a : 0})`;
756
+ toString(useAlpha = true)
757
+ {
758
+ const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
759
+ return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
759
760
  }
760
-
761
+
761
762
  /** Set this color from a hex code
762
763
  * @param {String} hex - html hex code
763
764
  * @return {Color} */
@@ -1254,7 +1255,12 @@ function setSoundEnable(enable) { soundEnable = enable; }
1254
1255
  /** Set volume scale to apply to all sound, music and speech
1255
1256
  * @param {Number} volume
1256
1257
  * @memberof Settings */
1257
- function setSoundVolume(volume) { soundVolume = volume; }
1258
+ function setSoundVolume(volume)
1259
+ {
1260
+ soundVolume = volume;
1261
+ if (soundEnable && !headlessMode && audioGainNode)
1262
+ audioGainNode.gain.value = volume; // update gain immediatly
1263
+ }
1258
1264
 
1259
1265
  /** Set default range where sound no longer plays
1260
1266
  * @param {Number} range
@@ -1387,6 +1393,8 @@ class EngineObject
1387
1393
  this.spawnTime = time;
1388
1394
  /** @property {Array} - List of children of this object */
1389
1395
  this.children = [];
1396
+ /** @property {Boolean} - Limit object speed using linear or circular math */
1397
+ this.clampSpeedLinear = true;
1390
1398
 
1391
1399
  // parent child system
1392
1400
  /** @property {EngineObject} - Parent of object if in local space */
@@ -1435,8 +1443,21 @@ class EngineObject
1435
1443
  return;
1436
1444
 
1437
1445
  // limit max speed to prevent missing collisions
1438
- this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1439
- this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1446
+ if (this.clampSpeedLinear)
1447
+ {
1448
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1449
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1450
+ }
1451
+ else
1452
+ {
1453
+ const length2 = this.velocity.lengthSquared();
1454
+ if (length2 > objectMaxSpeed*objectMaxSpeed)
1455
+ {
1456
+ const s = objectMaxSpeed / length2**.5;
1457
+ this.velocity.x *= s;
1458
+ this.velocity.y *= s;
1459
+ }
1460
+ }
1440
1461
 
1441
1462
  // apply physics
1442
1463
  const oldPos = this.pos.copy();
@@ -2719,6 +2740,32 @@ function touchGamepadRender()
2719
2740
 
2720
2741
 
2721
2742
 
2743
+ /** Audio context used by the engine
2744
+ * @type {AudioContext}
2745
+ * @memberof Audio */
2746
+ let audioContext;
2747
+
2748
+ /** Master gain node for all audio to pass through
2749
+ * @type {GainNode}
2750
+ * @memberof Audio */
2751
+ let audioGainNode;
2752
+
2753
+ function audioInit()
2754
+ {
2755
+ if (!soundEnable || headlessMode) return;
2756
+
2757
+ // create audio context
2758
+ audioContext = new AudioContext;
2759
+
2760
+ // create and connect gain node
2761
+ // (createGain is more widely spported then GainNode construtor)
2762
+ audioGainNode = audioContext.createGain();
2763
+ audioGainNode.connect(audioContext.destination);
2764
+ setSoundVolume(soundVolume); // update gain volume
2765
+ }
2766
+
2767
+ ///////////////////////////////////////////////////////////////////////////////
2768
+
2722
2769
  /**
2723
2770
  * Sound Object - Stores a sound for later use and can be played positionally
2724
2771
  *
@@ -2770,7 +2817,8 @@ class Sound
2770
2817
  */
2771
2818
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
2772
2819
  {
2773
- if (!soundEnable || !this.sampleChannels || headlessMode) return;
2820
+ if (!soundEnable || headlessMode) return;
2821
+ if (!this.sampleChannels) return;
2774
2822
 
2775
2823
  let pan;
2776
2824
  if (pos)
@@ -2847,16 +2895,14 @@ class SoundWave extends Sound
2847
2895
  * @param {Number} [randomness] - How much to randomize frequency each time sound plays
2848
2896
  * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
2849
2897
  * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
2898
+ * @param {Function} [onloadCallback] - callback function to call when sound is loaded
2850
2899
  */
2851
- constructor(filename, randomness=0, range, taper)
2900
+ constructor(filename, randomness=0, range, taper, onloadCallback)
2852
2901
  {
2853
2902
  super(undefined, range, taper);
2854
- this.randomness = randomness;
2855
-
2856
2903
  if (!soundEnable || headlessMode) return;
2857
- if (!audioContext)
2858
- audioContext = new AudioContext; // create audio context
2859
2904
 
2905
+ this.randomness = randomness;
2860
2906
  fetch(filename)
2861
2907
  .then(response => response.arrayBuffer())
2862
2908
  .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
@@ -2866,10 +2912,23 @@ class SoundWave extends Sound
2866
2912
  for (let i = audioBuffer.numberOfChannels; i--;)
2867
2913
  this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
2868
2914
  this.sampleRate = audioBuffer.sampleRate;
2869
- });
2915
+ }).then(() => onloadCallback && onloadCallback(this));
2870
2916
  }
2871
2917
  }
2872
2918
 
2919
+ /** Play an mp3, ogg, or wav audio from a local file or url
2920
+ * @param {String} filename - Location of sound file to play
2921
+ * @param {Number} [volume] - How much to scale volume by
2922
+ * @param {Boolean} [loop] - True if the music should loop
2923
+ * @return {SoundWave} - The sound object for this file
2924
+ * @memberof Audio */
2925
+ function playAudioFile(filename, volume=1, loop=false)
2926
+ {
2927
+ if (!soundEnable || headlessMode) return;
2928
+
2929
+ return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
2930
+ }
2931
+
2873
2932
  /**
2874
2933
  * Music Object - Stores a zzfx music track for later use
2875
2934
  *
@@ -2924,23 +2983,6 @@ class Music extends Sound
2924
2983
  { return super.play(undefined, volume, 1, 1, loop); }
2925
2984
  }
2926
2985
 
2927
- /** Play an mp3, ogg, or wav audio from a local file or url
2928
- * @param {String} filename - Location of sound file to play
2929
- * @param {Number} [volume] - How much to scale volume by
2930
- * @param {Boolean} [loop] - True if the music should loop
2931
- * @return {HTMLAudioElement} - The audio element for this sound
2932
- * @memberof Audio */
2933
- function playAudioFile(filename, volume=1, loop=false)
2934
- {
2935
- if (!soundEnable || headlessMode) return;
2936
-
2937
- const audio = new Audio(filename);
2938
- audio.volume = soundVolume * volume;
2939
- audio.loop = loop;
2940
- audio.play();
2941
- return audio;
2942
- }
2943
-
2944
2986
  /** Speak text with passed in settings
2945
2987
  * @param {String} text - The text to speak
2946
2988
  * @param {String} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
@@ -2951,7 +2993,8 @@ function playAudioFile(filename, volume=1, loop=false)
2951
2993
  * @memberof Audio */
2952
2994
  function speak(text, language='', volume=1, rate=1, pitch=1)
2953
2995
  {
2954
- if (!soundEnable || !speechSynthesis || headlessMode) return;
2996
+ if (!soundEnable || headlessMode) return;
2997
+ if (!speechSynthesis) return;
2955
2998
 
2956
2999
  // common languages (not supported by all browsers)
2957
3000
  // en - english, it - italian, fr - french, de - german, es - spanish
@@ -2981,14 +3024,8 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
2981
3024
 
2982
3025
  ///////////////////////////////////////////////////////////////////////////////
2983
3026
 
2984
- /** Audio context used by the engine
2985
- * @type {AudioContext}
2986
- * @memberof Audio */
2987
- let audioContext;
2988
-
2989
- /** Keep track if audio was suspended when last sound was played
2990
- * @type {Boolean}
2991
- * @memberof Audio */
3027
+ // internal tracking if audio was suspended when last sound was played
3028
+ // allows first suspended sound to play when audio is resumed
2992
3029
  let audioSuspended = false;
2993
3030
 
2994
3031
  /** Play cached audio samples with given settings
@@ -3003,8 +3040,6 @@ let audioSuspended = false;
3003
3040
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
3004
3041
  {
3005
3042
  if (!soundEnable || headlessMode) return;
3006
- if (!audioContext)
3007
- audioContext = new AudioContext; // create audio context
3008
3043
 
3009
3044
  // prevent sounds from building up if they can't be played
3010
3045
  const audioWasSuspended = audioSuspended;
@@ -3028,10 +3063,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3028
3063
  source.playbackRate.value = rate;
3029
3064
  source.loop = loop;
3030
3065
 
3031
- // create and connect gain node (createGain is more widely spported then GainNode construtor)
3066
+ // set master gain volume
3067
+ setSoundVolume(soundVolume);
3068
+
3069
+ // create and connect gain node
3032
3070
  const gainNode = audioContext.createGain();
3033
- gainNode.gain.value = soundVolume*volume;
3034
- gainNode.connect(audioContext.destination);
3071
+ gainNode.gain.value = volume;
3072
+ gainNode.connect(audioGainNode);
3035
3073
 
3036
3074
  // connect source to stereo panner and gain
3037
3075
  source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
@@ -3938,6 +3976,9 @@ class Particle extends EngineObject
3938
3976
  this.localSpaceEmitter = localSpaceEmitter;
3939
3977
  /** @property {Function} - Called when particle dies */
3940
3978
  this.destroyCallback = destroyCallback;
3979
+
3980
+ // particles use circular clamped speed
3981
+ this.clampSpeedLinear = false;
3941
3982
  }
3942
3983
 
3943
3984
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -4717,7 +4758,7 @@ const engineName = 'LittleJS';
4717
4758
  * @type {String}
4718
4759
  * @default
4719
4760
  * @memberof Engine */
4720
- const engineVersion = '1.9.6';
4761
+ const engineVersion = '1.9.7';
4721
4762
 
4722
4763
  /** Frames per second to update
4723
4764
  * @type {Number}
@@ -4950,6 +4991,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4950
4991
 
4951
4992
  // init stuff and start engine
4952
4993
  inputInit();
4994
+ audioInit();
4953
4995
  debugInit();
4954
4996
  glInit();
4955
4997
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.9.6",
3
+ "version": "1.9.7",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
package/src/engine.js CHANGED
@@ -30,7 +30,7 @@ const engineName = 'LittleJS';
30
30
  * @type {String}
31
31
  * @default
32
32
  * @memberof Engine */
33
- const engineVersion = '1.9.6';
33
+ const engineVersion = '1.9.7';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {Number}
@@ -263,6 +263,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
263
263
 
264
264
  // init stuff and start engine
265
265
  inputInit();
266
+ audioInit();
266
267
  debugInit();
267
268
  glInit();
268
269
 
@@ -11,6 +11,32 @@
11
11
 
12
12
  'use strict';
13
13
 
14
+ /** Audio context used by the engine
15
+ * @type {AudioContext}
16
+ * @memberof Audio */
17
+ let audioContext;
18
+
19
+ /** Master gain node for all audio to pass through
20
+ * @type {GainNode}
21
+ * @memberof Audio */
22
+ let audioGainNode;
23
+
24
+ function audioInit()
25
+ {
26
+ if (!soundEnable || headlessMode) return;
27
+
28
+ // create audio context
29
+ audioContext = new AudioContext;
30
+
31
+ // create and connect gain node
32
+ // (createGain is more widely spported then GainNode construtor)
33
+ audioGainNode = audioContext.createGain();
34
+ audioGainNode.connect(audioContext.destination);
35
+ setSoundVolume(soundVolume); // update gain volume
36
+ }
37
+
38
+ ///////////////////////////////////////////////////////////////////////////////
39
+
14
40
  /**
15
41
  * Sound Object - Stores a sound for later use and can be played positionally
16
42
  *
@@ -62,7 +88,8 @@ class Sound
62
88
  */
63
89
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
64
90
  {
65
- if (!soundEnable || !this.sampleChannels || headlessMode) return;
91
+ if (!soundEnable || headlessMode) return;
92
+ if (!this.sampleChannels) return;
66
93
 
67
94
  let pan;
68
95
  if (pos)
@@ -139,16 +166,14 @@ class SoundWave extends Sound
139
166
  * @param {Number} [randomness] - How much to randomize frequency each time sound plays
140
167
  * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
141
168
  * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
169
+ * @param {Function} [onloadCallback] - callback function to call when sound is loaded
142
170
  */
143
- constructor(filename, randomness=0, range, taper)
171
+ constructor(filename, randomness=0, range, taper, onloadCallback)
144
172
  {
145
173
  super(undefined, range, taper);
146
- this.randomness = randomness;
147
-
148
174
  if (!soundEnable || headlessMode) return;
149
- if (!audioContext)
150
- audioContext = new AudioContext; // create audio context
151
175
 
176
+ this.randomness = randomness;
152
177
  fetch(filename)
153
178
  .then(response => response.arrayBuffer())
154
179
  .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
@@ -158,10 +183,23 @@ class SoundWave extends Sound
158
183
  for (let i = audioBuffer.numberOfChannels; i--;)
159
184
  this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
160
185
  this.sampleRate = audioBuffer.sampleRate;
161
- });
186
+ }).then(() => onloadCallback && onloadCallback(this));
162
187
  }
163
188
  }
164
189
 
190
+ /** Play an mp3, ogg, or wav audio from a local file or url
191
+ * @param {String} filename - Location of sound file to play
192
+ * @param {Number} [volume] - How much to scale volume by
193
+ * @param {Boolean} [loop] - True if the music should loop
194
+ * @return {SoundWave} - The sound object for this file
195
+ * @memberof Audio */
196
+ function playAudioFile(filename, volume=1, loop=false)
197
+ {
198
+ if (!soundEnable || headlessMode) return;
199
+
200
+ return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
201
+ }
202
+
165
203
  /**
166
204
  * Music Object - Stores a zzfx music track for later use
167
205
  *
@@ -216,23 +254,6 @@ class Music extends Sound
216
254
  { return super.play(undefined, volume, 1, 1, loop); }
217
255
  }
218
256
 
219
- /** Play an mp3, ogg, or wav audio from a local file or url
220
- * @param {String} filename - Location of sound file to play
221
- * @param {Number} [volume] - How much to scale volume by
222
- * @param {Boolean} [loop] - True if the music should loop
223
- * @return {HTMLAudioElement} - The audio element for this sound
224
- * @memberof Audio */
225
- function playAudioFile(filename, volume=1, loop=false)
226
- {
227
- if (!soundEnable || headlessMode) return;
228
-
229
- const audio = new Audio(filename);
230
- audio.volume = soundVolume * volume;
231
- audio.loop = loop;
232
- audio.play();
233
- return audio;
234
- }
235
-
236
257
  /** Speak text with passed in settings
237
258
  * @param {String} text - The text to speak
238
259
  * @param {String} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
@@ -243,7 +264,8 @@ function playAudioFile(filename, volume=1, loop=false)
243
264
  * @memberof Audio */
244
265
  function speak(text, language='', volume=1, rate=1, pitch=1)
245
266
  {
246
- if (!soundEnable || !speechSynthesis || headlessMode) return;
267
+ if (!soundEnable || headlessMode) return;
268
+ if (!speechSynthesis) return;
247
269
 
248
270
  // common languages (not supported by all browsers)
249
271
  // en - english, it - italian, fr - french, de - german, es - spanish
@@ -273,14 +295,8 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
273
295
 
274
296
  ///////////////////////////////////////////////////////////////////////////////
275
297
 
276
- /** Audio context used by the engine
277
- * @type {AudioContext}
278
- * @memberof Audio */
279
- let audioContext;
280
-
281
- /** Keep track if audio was suspended when last sound was played
282
- * @type {Boolean}
283
- * @memberof Audio */
298
+ // internal tracking if audio was suspended when last sound was played
299
+ // allows first suspended sound to play when audio is resumed
284
300
  let audioSuspended = false;
285
301
 
286
302
  /** Play cached audio samples with given settings
@@ -295,8 +311,6 @@ let audioSuspended = false;
295
311
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
296
312
  {
297
313
  if (!soundEnable || headlessMode) return;
298
- if (!audioContext)
299
- audioContext = new AudioContext; // create audio context
300
314
 
301
315
  // prevent sounds from building up if they can't be played
302
316
  const audioWasSuspended = audioSuspended;
@@ -320,10 +334,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
320
334
  source.playbackRate.value = rate;
321
335
  source.loop = loop;
322
336
 
323
- // create and connect gain node (createGain is more widely spported then GainNode construtor)
337
+ // set master gain volume
338
+ setSoundVolume(soundVolume);
339
+
340
+ // create and connect gain node
324
341
  const gainNode = audioContext.createGain();
325
- gainNode.gain.value = soundVolume*volume;
326
- gainNode.connect(audioContext.destination);
342
+ gainNode.gain.value = volume;
343
+ gainNode.connect(audioGainNode);
327
344
 
328
345
  // connect source to stereo panner and gain
329
346
  source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
@@ -178,6 +178,7 @@ export {
178
178
  FontImage,
179
179
  isFullscreen,
180
180
  toggleFullscreen,
181
+ getCameraSize,
181
182
 
182
183
  // WebGL
183
184
  glCanvas,
@@ -85,6 +85,8 @@ class EngineObject
85
85
  this.spawnTime = time;
86
86
  /** @property {Array} - List of children of this object */
87
87
  this.children = [];
88
+ /** @property {Boolean} - Limit object speed using linear or circular math */
89
+ this.clampSpeedLinear = true;
88
90
 
89
91
  // parent child system
90
92
  /** @property {EngineObject} - Parent of object if in local space */
@@ -133,8 +135,21 @@ class EngineObject
133
135
  return;
134
136
 
135
137
  // limit max speed to prevent missing collisions
136
- this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
137
- this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
138
+ if (this.clampSpeedLinear)
139
+ {
140
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
141
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
142
+ }
143
+ else
144
+ {
145
+ const length2 = this.velocity.lengthSquared();
146
+ if (length2 > objectMaxSpeed*objectMaxSpeed)
147
+ {
148
+ const s = objectMaxSpeed / length2**.5;
149
+ this.velocity.x *= s;
150
+ this.velocity.y *= s;
151
+ }
152
+ }
138
153
 
139
154
  // apply physics
140
155
  const oldPos = this.pos.copy();
@@ -274,6 +274,9 @@ class Particle extends EngineObject
274
274
  this.localSpaceEmitter = localSpaceEmitter;
275
275
  /** @property {Function} - Called when particle dies */
276
276
  this.destroyCallback = destroyCallback;
277
+
278
+ // particles use circular clamped speed
279
+ this.clampSpeedLinear = false;
277
280
  }
278
281
 
279
282
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -417,7 +417,12 @@ function setSoundEnable(enable) { soundEnable = enable; }
417
417
  /** Set volume scale to apply to all sound, music and speech
418
418
  * @param {Number} volume
419
419
  * @memberof Settings */
420
- function setSoundVolume(volume) { soundVolume = volume; }
420
+ function setSoundVolume(volume)
421
+ {
422
+ soundVolume = volume;
423
+ if (soundEnable && !headlessMode && audioGainNode)
424
+ audioGainNode.gain.value = volume; // update gain immediatly
425
+ }
421
426
 
422
427
  /** Set default range where sound no longer plays
423
428
  * @param {Number} range
@@ -715,14 +715,15 @@ class Color
715
715
  ).clamp();
716
716
  }
717
717
 
718
- /** Returns this color expressed as a rgb color code
718
+ /** Returns this color expressed as a hex color code
719
719
  * @param {Boolean} [useAlpha] - if alpha should be included in result
720
720
  * @return {String} */
721
- toString(useAlpha = true)
722
- {
723
- return `rgb(${this.r*255},${this.g*255},${this.b*255},${useAlpha ? this.a : 0})`;
721
+ toString(useAlpha = true)
722
+ {
723
+ const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
724
+ return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
724
725
  }
725
-
726
+
726
727
  /** Set this color from a hex code
727
728
  * @param {String} hex - html hex code
728
729
  * @return {Color} */