littlejsengine 1.9.6 → 1.9.8

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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * LittleJS Newgrounds API
3
+ * - NewgroundsMedal extends Medal with Newgrounds API functionality
4
+ * - Call newgroundsInit to enable Newgrounds functionality
5
+ * - Uses CryptoJS for encryption if optional cipher is provided
6
+ * - Keeps connection alive and logs views
7
+ * - Functions to interact with scoreboards
8
+ * - Functions to unlock medals
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ ///////////////////////////////////////////////////////////////////////////////
14
+
15
+ /** Newgrounds medal auto unlocks in newgronds API */
16
+ class NewgroundsMedal extends Medal
17
+ {
18
+ /** Create a medal object and adds it to the list of medals */
19
+ constructor(id, name, description, icon, src)
20
+ { super(id, name, description, icon, src); }
21
+
22
+ /** Unlocks a medal if not already unlocked */
23
+ unlock()
24
+ {
25
+ super.unlock();
26
+ newgrounds && newgrounds.unlockMedal(this.id);
27
+ }
28
+ }
29
+
30
+ ///////////////////////////////////////////////////////////////////////////////
31
+
32
+ /** Global Newgrounds object */
33
+ let newgrounds;
34
+
35
+ /** This can used to enable Newgrounds functionality
36
+ * @param {Number} app_id - The newgrounds App ID
37
+ * @param {String} [cipher] - The encryption Key (AES-128/Base64)
38
+ * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher
39
+ * @memberof Medals */
40
+ function newgroundsInit(app_id, cipher, cryptoJS)
41
+ { newgrounds = new Newgrounds(app_id, cipher, cryptoJS); }
42
+
43
+ /**
44
+ * Newgrounds API wrapper object
45
+ * @example
46
+ * // create a newgrounds object, replace the app id with your own
47
+ * const app_id = '52123:1ZuSTQ7l';
48
+ * newgrounds = new Newgrounds(app_id);
49
+ */
50
+ class Newgrounds
51
+ {
52
+ /** Create a newgrounds object
53
+ * @param {String} app_id - The newgrounds App ID
54
+ * @param {String} [cipher] - The encryption Key (AES-128/Base64)
55
+ * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher */
56
+ constructor(app_id, cipher, cryptoJS)
57
+ {
58
+ ASSERT(!newgrounds, 'there can only be one newgrounds object');
59
+ ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
60
+
61
+ this.app_id = app_id;
62
+ this.cipher = cipher;
63
+ this.cryptoJS = cryptoJS;
64
+ this.host = location ? location.hostname : '';
65
+
66
+ // get session id from url search params
67
+ const url = new URL(location.href);
68
+ this.session_id = url.searchParams.get('ngio_session_id');
69
+
70
+ if (!this.session_id)
71
+ return; // only use newgrounds when logged in
72
+
73
+ // get medals
74
+ const medalsResult = this.call('Medal.getList');
75
+ this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
76
+ debugMedals && console.log(this.medals);
77
+ for (const newgroundsMedal of this.medals)
78
+ {
79
+ const medal = medals[newgroundsMedal['id']];
80
+ if (medal)
81
+ {
82
+ // copy newgrounds medal data
83
+ medal.image = new Image;
84
+ medal.image.src = newgroundsMedal['icon'];
85
+ medal.name = newgroundsMedal['name'];
86
+ medal.description = newgroundsMedal['description'];
87
+ medal.unlocked = newgroundsMedal['unlocked'];
88
+ medal.difficulty = newgroundsMedal['difficulty'];
89
+ medal.value = newgroundsMedal['value'];
90
+
91
+ if (medal.value) // add value to description
92
+ medal.description = medal.description + ` (${ medal.value })`;
93
+ }
94
+ }
95
+
96
+ // get scoreboards
97
+ const scoreboardResult = this.call('ScoreBoard.getBoards');
98
+ this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
99
+ debugMedals && console.log(this.scoreboards);
100
+
101
+ // keep the session alive with a ping every 5 minutes
102
+ const keepAliveMS = 5 * 60 * 1e3;
103
+ setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
104
+ }
105
+
106
+ /** Send message to unlock a medal by id
107
+ * @param {Number} id - The medal id */
108
+ unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
109
+
110
+ /** Send message to post score
111
+ * @param {Number} id - The scoreboard id
112
+ * @param {Number} value - The score value */
113
+ postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
114
+
115
+ /** Get scores from a scoreboard
116
+ * @param {Number} id - The scoreboard id
117
+ * @param {String} [user] - A user's id or name
118
+ * @param {Number} [social] - If true, only social scores will be loaded
119
+ * @param {Number} [skip] - Number of scores to skip before start
120
+ * @param {Number} [limit] - Number of scores to include in the list
121
+ * @return {Object} - The response JSON object
122
+ */
123
+ getScores(id, user, social=0, skip=0, limit=10)
124
+ { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
125
+
126
+ /** Send message to log a view */
127
+ logView() { return this.call('App.logView', {'host':this.host}, true); }
128
+
129
+ /** Send a message to call a component of the Newgrounds API
130
+ * @param {String} component - Name of the component
131
+ * @param {Object} [parameters] - Parameters to use for call
132
+ * @param {Boolean} [async] - If true, don't wait for response before continuing
133
+ * @return {Object} - The response JSON object
134
+ */
135
+ call(component, parameters, async=false)
136
+ {
137
+ const call = {'component':component, 'parameters':parameters};
138
+ if (this.cipher)
139
+ {
140
+ // encrypt using AES-128 Base64 with cryptoJS
141
+ const cryptoJS = this.cryptoJS;
142
+ const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
143
+ const iv = cryptoJS['lib']['WordArray']['random'](16);
144
+ const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
145
+ call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
146
+ call['parameters'] = 0;
147
+ }
148
+
149
+ // build the input object
150
+ const input =
151
+ {
152
+ 'app_id': this.app_id,
153
+ 'session_id': this.session_id,
154
+ 'call': call
155
+ };
156
+
157
+ // build post data
158
+ const formData = new FormData();
159
+ formData.append('input', JSON.stringify(input));
160
+
161
+ // send post data
162
+ const xmlHttp = new XMLHttpRequest();
163
+ const url = 'https://newgrounds.io/gateway_v3.php';
164
+ xmlHttp.open('POST', url, !debugMedals && async);
165
+ xmlHttp.send(formData);
166
+ debugMedals && console.log(xmlHttp.responseText);
167
+ return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
168
+ }
169
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * LittleJS Post Processing Plugin
3
+ * - Supports shadertoy style post processing shaders
4
+ * - call glInitPostProcess
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ ///////////////////////////////////////////////////////////////////////////////
10
+ // post processing - can be enabled to pass other canvases through a final shader
11
+
12
+ let glPostShader, glPostTexture, glPostIncludeOverlay;
13
+
14
+ /** Set up a post processing shader
15
+ * @param {String} shaderCode
16
+ * @param {Boolean} includeOverlay
17
+ * @memberof WebGL */
18
+ function initPostProcess(shaderCode, includeOverlay=false)
19
+ {
20
+ ASSERT(!glPostShader, 'can only have 1 post effects shader');
21
+ if (headlessMode) return;
22
+
23
+ if (!shaderCode) // default shader pass through
24
+ shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
25
+
26
+ // create the shader
27
+ glPostShader = glCreateProgram(
28
+ '#version 300 es\n' + // specify GLSL ES version
29
+ 'precision highp float;'+ // use highp for better accuracy
30
+ 'in vec2 p;'+ // position
31
+ 'void main(){'+ // shader entry point
32
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
33
+ '}' // end of shader
34
+ ,
35
+ '#version 300 es\n' + // specify GLSL ES version
36
+ 'precision highp float;'+ // use highp for better accuracy
37
+ 'uniform sampler2D iChannel0;'+ // input texture
38
+ 'uniform vec3 iResolution;'+ // size of output texture
39
+ 'uniform float iTime;'+ // time
40
+ 'out vec4 c;'+ // out color
41
+ '\n' + shaderCode + '\n'+ // insert custom shader code
42
+ 'void main(){'+ // shader entry point
43
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
44
+ 'c.a=1.;'+ // always use full alpha
45
+ '}' // end of shader
46
+ );
47
+
48
+ // create buffer and texture
49
+ glPostTexture = glCreateTexture(undefined);
50
+ glPostIncludeOverlay = includeOverlay;
51
+
52
+ // hide the original 2d canvas
53
+ mainCanvas.style.visibility = 'hidden';
54
+ if (glPostIncludeOverlay)
55
+ overlayCanvas.style.visibility = 'hidden';
56
+
57
+ // Render the post processing shader, called automatically by the engine
58
+ addPluginRender(function()
59
+ {
60
+ if (headlessMode) return;
61
+
62
+ // prepare to render post process shader
63
+ if (glEnable)
64
+ {
65
+ glFlush(); // clear out the buffer
66
+ mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
67
+ }
68
+ else
69
+ {
70
+ // set the viewport
71
+ glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
72
+ }
73
+
74
+ // copy overlay canvas so it will be included in post processing
75
+ glPostIncludeOverlay && mainContext.drawImage(overlayCanvas, 0, 0);
76
+
77
+ // setup shader program to draw one triangle
78
+ glContext.useProgram(glPostShader);
79
+ glContext.bindBuffer(gl_ARRAY_BUFFER, glGeometryBuffer);
80
+ glContext.pixelStorei(gl_UNPACK_FLIP_Y_WEBGL, 1);
81
+ glContext.disable(gl_BLEND);
82
+
83
+ // set textures, pass in the 2d canvas and gl canvas in separate texture channels
84
+ glContext.activeTexture(gl_TEXTURE0);
85
+ glContext.bindTexture(gl_TEXTURE_2D, glPostTexture);
86
+ glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, mainCanvas);
87
+
88
+ // set vertex position attribute
89
+ const vertexByteStride = 8;
90
+ const pLocation = glContext.getAttribLocation(glPostShader, 'p');
91
+ glContext.enableVertexAttribArray(pLocation);
92
+ glContext.vertexAttribPointer(pLocation, 2, gl_FLOAT, false, vertexByteStride, 0);
93
+
94
+ // set uniforms and draw
95
+ const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
96
+ glContext.uniform1i(uniformLocation('iChannel0'), 0);
97
+ glContext.uniform1f(uniformLocation('iTime'), time);
98
+ glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
99
+ glContext.drawArrays(gl_TRIANGLE_STRIP, 0, 4);
100
+ });
101
+ }
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.8';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {Number}
@@ -84,6 +84,22 @@ function setPaused(isPaused) { paused = isPaused; }
84
84
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
85
85
 
86
86
  ///////////////////////////////////////////////////////////////////////////////
87
+ // plugin hooks
88
+
89
+ const pluginUpdateList = [], pluginRenderList = [];
90
+
91
+ /** Add a new update function for a plugin
92
+ * @param {Function} updateFunction
93
+ * @memberof Engine */
94
+ function addPluginUpdate(updateFunction) { pluginUpdateList.push(updateFunction); }
95
+
96
+ /** Add a new render function for a plugin
97
+ * @param {Function} renderFunction
98
+ * @memberof Engine */
99
+ function addPluginRender(renderFunction) { pluginRenderList.push(renderFunction); }
100
+
101
+ ///////////////////////////////////////////////////////////////////////////////
102
+ // Main engine functions
87
103
 
88
104
  /** Startup LittleJS engine with your callback functions
89
105
  * @param {Function} gameInit - Called once after the engine starts up, setup the game
@@ -159,6 +175,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
159
175
  // update game and objects
160
176
  inputUpdate();
161
177
  gameUpdate();
178
+ pluginUpdateList.forEach(f=>f());
162
179
  engineObjectsUpdate();
163
180
 
164
181
  // do post update
@@ -180,8 +197,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
180
197
  for (const o of engineObjects)
181
198
  o.destroyed || o.render();
182
199
  gameRenderPost();
183
- glRenderPostProcess();
184
- medalsRender();
200
+ pluginRenderList.forEach(f=>f());
185
201
  touchGamepadRender();
186
202
  debugRender();
187
203
  glCopyToContext(mainContext);
@@ -263,6 +279,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
263
279
 
264
280
  // init stuff and start engine
265
281
  inputInit();
282
+ audioInit();
266
283
  debugInit();
267
284
  glInit();
268
285
 
@@ -470,21 +487,23 @@ function drawEngineSplashScreen(t)
470
487
  x.setLineDash([99*p2,99]);
471
488
 
472
489
  // cab top
473
- rect(7,17,18,-8,color(2,2));
474
- rect(7,9,18,4,color(2,3));
475
- rect(25,9,8,8,color(2,1));
476
- rect(25,9,-18,8);
477
- rect(25,9,8,8);
490
+ rect(7,16,18,-8,color(2,2));
491
+ rect(7,8,18,4,color(2,3));
492
+ rect(25,8,8,8,color(2,1));
493
+ rect(25,8,-18,8);
494
+ rect(25,8,8,8);
478
495
 
479
496
  // cab
480
- rect(25,17,7,22,color());
481
- rect(11,40,14,-23,color(1,1));
482
- rect(11,17,14,17,color(1,2));
483
- rect(11,17,14,9,color(1,3));
484
- rect(15,31,6,-9,color(2,2));
485
- circle(15,23,5,0,PI/2,color(2,4),1);
486
- rect(25,17,-14,23);
487
- rect(21,22,-6,9);
497
+ rect(25,16,7,23,color());
498
+ rect(11,39,14,-23,color(1,1));
499
+ rect(11,16,14,18,color(1,2));
500
+ rect(11,16,14,8,color(1,3));
501
+ rect(25,16,-14,24);
502
+
503
+ // cab window
504
+ rect(15,29,6,-9,color(2,2));
505
+ circle(15,21,5,0,PI/2,color(2,4),1);
506
+ rect(21,21,-6,9);
488
507
 
489
508
  // little stack
490
509
  rect(37,14,9,6,color(3,2));
@@ -492,16 +511,16 @@ function drawEngineSplashScreen(t)
492
511
  rect(37,14,9,6);
493
512
 
494
513
  // big stack
495
- rect(50,20,10,-8,color(0,1))
496
- rect(50,20,6.5,-8,color(0,2))
497
- rect(50,20,3.5,-8,color(0,3))
498
- rect(50,20,10,-8)
499
- circle(55,2,11.4,.5,PI-.5,color(3,3))
500
- circle(55,2,11.4,.5,PI/2,color(3,2),1)
501
- circle(55,2,11.4,.5,PI-.5)
502
- rect(45,7,20,-7,color(0,2))
503
- rect(45,-1,20,4,color(0,3))
504
- rect(45,-1,20,8)
514
+ rect(50,20,10,-8,color(0,1));
515
+ rect(50,20,6.5,-8,color(0,2));
516
+ rect(50,20,3.5,-8,color(0,3));
517
+ rect(50,20,10,-8);
518
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
519
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
520
+ circle(55,2,11.4,.5,PI-.5);
521
+ rect(45,7,20,-7,color(0,2));
522
+ rect(45,-1,20,4,color(0,3));
523
+ rect(45,-1,20,8);
505
524
 
506
525
  // engine
507
526
  for (let i=5; i--;)
@@ -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)
@@ -85,7 +112,17 @@ class Sound
85
112
 
86
113
  // play the sound
87
114
  const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
88
- return this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate);
115
+ this.gainNode = audioContext.createGain();
116
+ return this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
117
+ }
118
+
119
+ /** Set the sound volume
120
+ * @param {Number} [volume] - How much to scale volume by
121
+ */
122
+ setVolume(volume=1)
123
+ {
124
+ if (this.gainNode)
125
+ this.gainNode.gain.value = volume;
89
126
  }
90
127
 
91
128
  /** Stop the last instance of this sound that was played */
@@ -139,16 +176,14 @@ class SoundWave extends Sound
139
176
  * @param {Number} [randomness] - How much to randomize frequency each time sound plays
140
177
  * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
141
178
  * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
179
+ * @param {Function} [onloadCallback] - callback function to call when sound is loaded
142
180
  */
143
- constructor(filename, randomness=0, range, taper)
181
+ constructor(filename, randomness=0, range, taper, onloadCallback)
144
182
  {
145
183
  super(undefined, range, taper);
146
- this.randomness = randomness;
147
-
148
184
  if (!soundEnable || headlessMode) return;
149
- if (!audioContext)
150
- audioContext = new AudioContext; // create audio context
151
185
 
186
+ this.randomness = randomness;
152
187
  fetch(filename)
153
188
  .then(response => response.arrayBuffer())
154
189
  .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
@@ -158,10 +193,23 @@ class SoundWave extends Sound
158
193
  for (let i = audioBuffer.numberOfChannels; i--;)
159
194
  this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
160
195
  this.sampleRate = audioBuffer.sampleRate;
161
- });
196
+ }).then(() => onloadCallback && onloadCallback(this));
162
197
  }
163
198
  }
164
199
 
200
+ /** Play an mp3, ogg, or wav audio from a local file or url
201
+ * @param {String} filename - Location of sound file to play
202
+ * @param {Number} [volume] - How much to scale volume by
203
+ * @param {Boolean} [loop] - True if the music should loop
204
+ * @return {SoundWave} - The sound object for this file
205
+ * @memberof Audio */
206
+ function playAudioFile(filename, volume=1, loop=false)
207
+ {
208
+ if (!soundEnable || headlessMode) return;
209
+
210
+ return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
211
+ }
212
+
165
213
  /**
166
214
  * Music Object - Stores a zzfx music track for later use
167
215
  *
@@ -216,23 +264,6 @@ class Music extends Sound
216
264
  { return super.play(undefined, volume, 1, 1, loop); }
217
265
  }
218
266
 
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
267
  /** Speak text with passed in settings
237
268
  * @param {String} text - The text to speak
238
269
  * @param {String} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
@@ -243,7 +274,8 @@ function playAudioFile(filename, volume=1, loop=false)
243
274
  * @memberof Audio */
244
275
  function speak(text, language='', volume=1, rate=1, pitch=1)
245
276
  {
246
- if (!soundEnable || !speechSynthesis || headlessMode) return;
277
+ if (!soundEnable || headlessMode) return;
278
+ if (!speechSynthesis) return;
247
279
 
248
280
  // common languages (not supported by all browsers)
249
281
  // en - english, it - italian, fr - french, de - german, es - spanish
@@ -273,30 +305,23 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
273
305
 
274
306
  ///////////////////////////////////////////////////////////////////////////////
275
307
 
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 */
308
+ // internal tracking if audio was suspended when last sound was played
309
+ // allows first suspended sound to play when audio is resumed
284
310
  let audioSuspended = false;
285
311
 
286
312
  /** Play cached audio samples with given settings
287
- * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
288
- * @param {Number} [volume] - How much to scale volume by
289
- * @param {Number} [rate] - The playback rate to use
290
- * @param {Number} [pan] - How much to apply stereo panning
291
- * @param {Boolean} [loop] - True if the sound should loop when it reaches the end
292
- * @param {Number} [sampleRate=44100] - Sample rate for the sound
313
+ * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
314
+ * @param {Number} [volume] - How much to scale volume by
315
+ * @param {Number} [rate] - The playback rate to use
316
+ * @param {Number} [pan] - How much to apply stereo panning
317
+ * @param {Boolean} [loop] - True if the sound should loop when it reaches the end
318
+ * @param {Number} [sampleRate=44100] - Sample rate for the sound
319
+ * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
293
320
  * @return {AudioBufferSourceNode} - The audio node of the sound played
294
321
  * @memberof Audio */
295
- function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
322
+ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR, gainNode)
296
323
  {
297
324
  if (!soundEnable || headlessMode) return;
298
- if (!audioContext)
299
- audioContext = new AudioContext; // create audio context
300
325
 
301
326
  // prevent sounds from building up if they can't be played
302
327
  const audioWasSuspended = audioSuspended;
@@ -320,10 +345,10 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
320
345
  source.playbackRate.value = rate;
321
346
  source.loop = loop;
322
347
 
323
- // create and connect gain node (createGain is more widely spported then GainNode construtor)
324
- const gainNode = audioContext.createGain();
325
- gainNode.gain.value = soundVolume*volume;
326
- gainNode.connect(audioContext.destination);
348
+ // create and connect gain node
349
+ gainNode = gainNode || audioContext.createGain();
350
+ gainNode.gain.value = volume;
351
+ gainNode.connect(audioGainNode);
327
352
 
328
353
  // connect source to stereo panner and gain
329
354
  source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);