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.
- package/LICENSE +22 -0
- package/README.md +13 -8
- package/dist/littlejs.d.ts +85 -84
- package/dist/littlejs.esm.js +370 -390
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +365 -386
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +302 -355
- package/examples/box2d/game.js +138 -0
- package/examples/box2d/index.html +25 -0
- package/examples/box2d/scenes.js +412 -0
- package/examples/box2d/tiles.png +0 -0
- package/examples/breakout/game.js +3 -3
- package/examples/breakout/index.html +1 -0
- package/examples/platformer/gameEffects.js +2 -2
- package/examples/platformer/gameLevel.js +0 -1
- package/package.json +1 -1
- package/plugins/Box2D_v2.3.1_min.wasm.js +630 -0
- package/plugins/Box2D_v2.3.1_min.wasm.wasm +0 -0
- package/plugins/box2d.js +873 -0
- package/plugins/newgrounds.js +169 -0
- package/plugins/postProcess.js +101 -0
- package/src/engine.js +45 -26
- package/src/engineAudio.js +72 -47
- package/src/engineDebug.js +68 -33
- package/src/engineDraw.js +1 -1
- package/src/engineExport.js +5 -4
- package/src/engineMedals.js +39 -171
- package/src/engineObject.js +47 -4
- package/src/engineParticles.js +3 -0
- package/src/engineRelease.js +5 -2
- package/src/engineSettings.js +8 -3
- package/src/engineUtilities.js +81 -7
- package/src/engineWebGL.js +1 -94
|
@@ -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.
|
|
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
|
-
|
|
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,
|
|
474
|
-
rect(7,
|
|
475
|
-
rect(25,
|
|
476
|
-
rect(25,
|
|
477
|
-
rect(25,
|
|
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,
|
|
481
|
-
rect(11,
|
|
482
|
-
rect(11,
|
|
483
|
-
rect(11,
|
|
484
|
-
rect(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
rect(
|
|
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--;)
|
package/src/engineAudio.js
CHANGED
|
@@ -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 ||
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
277
|
-
|
|
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}
|
|
288
|
-
* @param {Number}
|
|
289
|
-
* @param {Number}
|
|
290
|
-
* @param {Number}
|
|
291
|
-
* @param {Boolean}
|
|
292
|
-
* @param {Number}
|
|
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
|
|
324
|
-
|
|
325
|
-
gainNode.gain.value =
|
|
326
|
-
gainNode.connect(
|
|
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);
|