littlejsengine 1.14.16 → 1.14.19

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.
@@ -25,80 +25,97 @@ class PostProcessPlugin
25
25
  /** Create global post processing shader
26
26
  * @param {string} shaderCode
27
27
  * @param {boolean} [includeOverlay]
28
+ * @param {boolean} [includeMainCanvas]
28
29
  * @example
29
30
  * // create the post process plugin object
30
31
  * new PostProcessPlugin(shaderCode);
31
32
  */
32
- constructor(shaderCode, includeOverlay=false)
33
+ constructor(shaderCode, includeOverlay=false, includeMainCanvas=true)
33
34
  {
34
35
  ASSERT(!postProcess, 'Post process already initialized');
35
36
  postProcess = this;
36
37
 
37
- if (headlessMode) return;
38
-
39
- if (!glEnable)
40
- {
41
- console.warn('PostProcessPlugin: WebGL not enabled!');
42
- return;
43
- }
44
-
45
38
  if (!shaderCode) // default shader pass through
46
39
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
47
40
 
48
41
  /** @property {WebGLProgram} - Shader for post processing */
49
- this.shader = glCreateProgram(
50
- '#version 300 es\n' + // specify GLSL ES version
51
- 'precision highp float;'+ // use highp for better accuracy
52
- 'in vec2 p;'+ // position
53
- 'void main(){'+ // shader entry point
54
- 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
55
- '}' // end of shader
56
- ,
57
- '#version 300 es\n' + // specify GLSL ES version
58
- 'precision highp float;'+ // use highp for better accuracy
59
- 'uniform sampler2D iChannel0;'+ // input texture
60
- 'uniform vec3 iResolution;'+ // size of output texture
61
- 'uniform float iTime;'+ // time
62
- 'out vec4 c;'+ // out color
63
- '\n' + shaderCode + '\n'+ // insert custom shader code
64
- 'void main(){'+ // shader entry point
65
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
66
- 'c.a=1.;'+ // always use full alpha
67
- '}' // end of shader
68
- );
42
+ this.shader = undefined;
69
43
 
70
44
  /** @property {WebGLTexture} - Texture for post processing */
71
- this.texture = glCreateTexture();
45
+ this.texture = undefined;
72
46
 
73
- /** @property {boolean} - Should overlay canvas be included in post processing */
74
- this.includeOverlay = includeOverlay;
47
+ // setup the post processing plugin
48
+ initPostProcess();
49
+ engineAddPlugin(undefined, postProcessRender, postProcessContextLost, postProcessContextRestored);
75
50
 
76
- // Render the post processing shader, called automatically by the engine
77
- engineAddPlugin(undefined, postProcessRender);
78
- function postProcessRender()
51
+ function initPostProcess()
79
52
  {
80
53
  if (headlessMode) return;
81
-
82
- // prepare to render post process shader
83
- if (glEnable)
84
- {
85
- glFlush(); // clear out the buffer
86
- mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
87
- }
88
- else
54
+
55
+ if (!glEnable)
89
56
  {
90
- // set the viewport
91
- glContext.viewport(0, 0, glCanvas.width = drawCanvas.width, glCanvas.height = drawCanvas.height);
57
+ console.warn('PostProcessPlugin: WebGL not enabled!');
58
+ return;
92
59
  }
93
60
 
94
- if (postProcess.includeOverlay)
61
+ // create resources
62
+ postProcess.texture = glCreateTexture();
63
+ postProcess.shader = glCreateProgram(
64
+ '#version 300 es\n' + // specify GLSL ES version
65
+ 'precision highp float;'+ // use highp for better accuracy
66
+ 'in vec2 p;'+ // position
67
+ 'void main(){'+ // shader entry point
68
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
69
+ '}' // end of shader
70
+ ,
71
+ '#version 300 es\n' + // specify GLSL ES version
72
+ 'precision highp float;'+ // use highp for better accuracy
73
+ 'uniform sampler2D iChannel0;'+ // input texture
74
+ 'uniform vec3 iResolution;'+ // size of output texture
75
+ 'uniform float iTime;'+ // time
76
+ 'out vec4 c;'+ // out color
77
+ '\n' + shaderCode + '\n'+ // insert custom shader code
78
+ 'void main(){'+ // shader entry point
79
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
80
+ 'c.a=1.;'+ // always use full alpha
81
+ '}' // end of shader
82
+ );
83
+ }
84
+ function postProcessContextLost()
85
+ {
86
+ postProcess.shader = undefined;
87
+ postProcess.texture = undefined;
88
+ LOG('PostProcessPlugin: WebGL context lost');
89
+ }
90
+ function postProcessContextRestored()
91
+ {
92
+ initPostProcess();
93
+ LOG('PostProcessPlugin: WebGL context restored');
94
+ }
95
+ function postProcessRender()
96
+ {
97
+ if (headlessMode) return;
98
+
99
+ if (!glEnable)
100
+ return;
101
+
102
+ // clear out the buffer
103
+ glFlush();
104
+
105
+ if (includeMainCanvas || includeOverlay)
95
106
  {
96
- // copy overlay canvas so it will be included in post processing
97
- mainContext.drawImage(overlayCanvas, 0, 0);
98
- overlayCanvas.width |= 0;
107
+ // copy WebGL to the main canvas
108
+ mainContext.drawImage(glCanvas, 0, 0);
109
+
110
+ if (includeOverlay)
111
+ {
112
+ // copy overlay canvas so it will be included in post processing
113
+ mainContext.drawImage(overlayCanvas, 0, 0);
114
+ overlayCanvas.width |= 0; // clear overlay canvas
115
+ }
99
116
  }
100
117
 
101
- // setup shader program to draw one triangle
118
+ // setup shader program to draw a quad
102
119
  glContext.useProgram(postProcess.shader);
103
120
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
104
121
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
@@ -107,7 +124,10 @@ class PostProcessPlugin
107
124
  // set textures, pass in the 2d canvas and gl canvas in separate texture channels
108
125
  glContext.activeTexture(glContext.TEXTURE0);
109
126
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
110
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
127
+ if (includeMainCanvas || includeOverlay)
128
+ {
129
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
130
+ }
111
131
 
112
132
  // set vertex position attribute
113
133
  const vertexByteStride = 8;
@@ -48,6 +48,8 @@ class UISystemPlugin
48
48
  this.defaultHoverColor = hsl(0,0,.9);
49
49
  /** @property {Color} - Default color for disabled UI elements */
50
50
  this.defaultDisabledColor = hsl(0,0,.3);
51
+ /** @property {Color} - Uses a gradient fill combined with color */
52
+ this.defaultGradientColor = undefined;
51
53
  /** @property {number} - Default line width for UI elements */
52
54
  this.defaultLineWidth = 4;
53
55
  /** @property {number} - Default rounded rect corner radius for UI elements */
@@ -55,7 +57,7 @@ class UISystemPlugin
55
57
  /** @property {number} - Default scale to use for fitting text to object */
56
58
  this.defaultTextScale = .8;
57
59
  /** @property {string} - Default font for UI elements */
58
- this.defaultFont = 'arial';
60
+ this.defaultFont = fontDefault;
59
61
  /** @property {Sound} - Default sound when interactive UI element is pressed */
60
62
  this.defaultSoundPress = undefined;
61
63
  /** @property {Sound} - Default sound when interactive UI element is released */
@@ -72,7 +74,9 @@ class UISystemPlugin
72
74
  this.hoverObject = undefined;
73
75
  /** @property {UIObject} - Hover object at start of update */
74
76
  this.lastHoverObject = undefined;
75
-
77
+ /** @property {number} - If set ui coords will be renormalized to this canvas height */
78
+ this.nativeHeight = 0;
79
+
76
80
  engineAddPlugin(uiUpdate, uiRender);
77
81
 
78
82
  // setup recursive update and render
@@ -112,6 +116,17 @@ class UISystemPlugin
112
116
  }
113
117
  function uiRender()
114
118
  {
119
+ const context = uiSystem.uiContext;
120
+ context.save();
121
+ if (uiSystem.nativeHeight)
122
+ {
123
+ // convert to native height
124
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
125
+ context.translate(-s*mainCanvasSize.x/2,0);
126
+ context.scale(s,s);
127
+ context.translate(mainCanvasSize.x/2/s,0);
128
+ }
129
+
115
130
  function renderObject(o)
116
131
  {
117
132
  if (!o.visible)
@@ -123,6 +138,7 @@ class UISystemPlugin
123
138
  renderObject(c);
124
139
  }
125
140
  uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
141
+ context.restore();
126
142
  }
127
143
  }
128
144
 
@@ -132,8 +148,9 @@ class UISystemPlugin
132
148
  * @param {Color} [color=uiSystem.defaultColor]
133
149
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
134
150
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
135
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
136
- drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
151
+ * @param {number} [cornerRadius=uiSystem.defaultCornerRadius]
152
+ * @param {Color} [gradientColor=uiSystem.defaultGradientColor] */
153
+ drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius, gradientColor=uiSystem.defaultGradientColor)
137
154
  {
138
155
  ASSERT(isVector2(pos), 'pos must be a vec2');
139
156
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -143,7 +160,18 @@ class UISystemPlugin
143
160
  ASSERT(isNumber(cornerRadius), 'cornerRadius must be a number');
144
161
 
145
162
  const context = uiSystem.uiContext;
146
- context.fillStyle = color.toString();
163
+ if (gradientColor)
164
+ {
165
+ const g = context.createLinearGradient(
166
+ pos.x, pos.y-size.y/2, pos.x, pos.y+size.y/2);
167
+ const c = color.toString();
168
+ g.addColorStop(0, c);
169
+ g.addColorStop(.5, gradientColor.toString());
170
+ g.addColorStop(1, c);
171
+ context.fillStyle = g;
172
+ }
173
+ else
174
+ context.fillStyle = color.toString();
147
175
  context.beginPath();
148
176
  if (cornerRadius && context['roundRect'])
149
177
  context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
@@ -200,10 +228,11 @@ class UISystemPlugin
200
228
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
201
229
  * @param {string} [align]
202
230
  * @param {string} [font=uiSystem.defaultFont]
231
+ * @param {string} [fontStyle]
203
232
  * @param {boolean} [applyMaxWidth=true] */
204
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, applyMaxWidth=true)
233
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true)
205
234
  {
206
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
235
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
207
236
  }
208
237
 
209
238
  /**
@@ -264,17 +293,21 @@ class UIObject
264
293
  /** @property {boolean} - Is this object disabled? */
265
294
  this.disabled = false;
266
295
  /** @property {Color} - Color for text */
267
- this.textColor = uiSystem.defaultTextColor.copy()
296
+ this.textColor = uiSystem.defaultTextColor.copy();
268
297
  /** @property {Color} - Color used when hovering over the object */
269
- this.hoverColor = uiSystem.defaultHoverColor.copy()
298
+ this.hoverColor = uiSystem.defaultHoverColor.copy();
270
299
  /** @property {Color} - Color for line drawing */
271
- this.lineColor = uiSystem.defaultLineColor.copy()
300
+ this.lineColor = uiSystem.defaultLineColor.copy();
301
+ /** @property {Color} - Uses a gradient fill combined with color */
302
+ this.gradientColor = uiSystem.defaultGradientColor ? uiSystem.defaultGradientColor.copy() : undefined;
272
303
  /** @property {number} - Width for line drawing */
273
304
  this.lineWidth = uiSystem.defaultLineWidth;
274
305
  /** @property {number} - Corner radius for rounded rects */
275
306
  this.cornerRadius = uiSystem.defaultCornerRadius;
276
307
  /** @property {string} - Font for this objecct */
277
308
  this.font = uiSystem.defaultFont;
309
+ /** @property {string} - Font style for this object or undefined */
310
+ this.fontStyle = undefined;
278
311
  /** @property {number} - Override for text width */
279
312
  this.textWidth = undefined;
280
313
  /** @property {number} - Override for text height */
@@ -299,6 +332,8 @@ class UIObject
299
332
  this.interactive = false;
300
333
  /** @property {boolean} - Activate when dragged over with mouse held down */
301
334
  this.dragActivate = false;
335
+ /** @property {boolean} - True if this can be a hover object */
336
+ this.canBeHover = true;
302
337
  uiSystem.uiObjects.push(this);
303
338
  }
304
339
 
@@ -322,6 +357,26 @@ class UIObject
322
357
  child.parent = undefined;
323
358
  }
324
359
 
360
+ /** Check if the mouse is overlapping a box in screen space
361
+ * @return {boolean} - True if overlapping
362
+ */
363
+ isMouseOverlapping()
364
+ {
365
+ const size = !isTouchDevice ? this.size :
366
+ this.size.add(vec2(this.extraTouchSize || 0));
367
+ if (!uiSystem.nativeHeight)
368
+ return isOverlapping(this.pos, size, mousePosScreen);
369
+
370
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
371
+ const sInv = 1/s;
372
+ let pos = mousePosScreen.copy();
373
+ pos.x += s*mainCanvasSize.x/2;
374
+ pos.x *= sInv;
375
+ pos.y *= sInv;
376
+ pos.x -= sInv*mainCanvasSize.x/2;
377
+ return isOverlapping(this.pos, size, pos);
378
+ }
379
+
325
380
  /** Update the object, called automatically by plugin once each frame */
326
381
  update()
327
382
  {
@@ -329,13 +384,10 @@ class UIObject
329
384
  const isActive = this.isActiveObject();
330
385
  const mouseDown = mouseIsDown(0);
331
386
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
332
- if (!uiSystem.hoverObject)
387
+ if (this.canBeHover)
333
388
  if (mousePress || isActive || (!mouseDown && !isTouchDevice))
334
- {
335
- const size = this.size.add(vec2(isTouchDevice && this.extraTouchSize || 0));
336
- if (isOverlapping(this.pos, size, mousePosScreen))
337
- uiSystem.hoverObject = this;
338
- }
389
+ if (!uiSystem.hoverObject && this.isMouseOverlapping())
390
+ uiSystem.hoverObject = this;
339
391
  if (this.isHoverObject())
340
392
  {
341
393
  if (!this.disabled)
@@ -460,11 +512,13 @@ class UIText extends UIObject
460
512
 
461
513
  // make text not outlined by default
462
514
  this.lineWidth = 0;
515
+ // text can not be a hover object by default
516
+ this.canBeHover = false;
463
517
  }
464
518
  render()
465
519
  {
466
520
  const textSize = this.getTextSize();
467
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
521
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
468
522
  }
469
523
  }
470
524
 
@@ -530,7 +584,7 @@ class UIButton extends UIObject
530
584
 
531
585
  // set properties
532
586
  this.text = text;
533
- this.color = color.copy()
587
+ this.color = color.copy();
534
588
  this.interactive = true;
535
589
  }
536
590
  render()
@@ -540,7 +594,7 @@ class UIButton extends UIObject
540
594
  // draw the text scaled to fit
541
595
  const textSize = this.getTextSize();
542
596
  uiSystem.drawText(this.text, this.pos, textSize,
543
- this.textColor, 0, undefined, this.align, this.font);
597
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
544
598
  }
545
599
  }
546
600
 
@@ -594,7 +648,7 @@ class UICheckbox extends UIObject
594
648
  const textSize = this.getTextSize();
595
649
  const pos = this.pos.add(vec2(this.size.x,0));
596
650
  uiSystem.drawText(this.text, pos, textSize,
597
- this.textColor, 0, undefined, 'left', this.font, false);
651
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
598
652
  }
599
653
  }
600
654
 
@@ -672,13 +726,12 @@ class UIScrollbar extends UIObject
672
726
  const handlePos = isHorizontal ?
673
727
  vec2(lerp(p1, p2, this.value), this.pos.y) :
674
728
  vec2(this.pos.x, lerp(p2, p1, this.value))
675
- const handleColor = this.disabled ? this.disabledColor :
676
- this.interactive && this.isActiveObject() ? this.color : this.handleColor;
729
+ const handleColor = this.disabled ? this.disabledColor : this.handleColor;
677
730
  uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
678
731
 
679
732
  // draw the text scaled to fit on the scrollbar
680
733
  const textSize = this.getTextSize();
681
734
  uiSystem.drawText(this.text, this.pos, textSize,
682
- this.textColor, 0, undefined, this.align, this.font);
735
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
683
736
  }
684
737
  }
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.14.16';
33
+ const engineVersion = '1.14.19';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {number}
@@ -91,7 +91,17 @@ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
91
91
  ///////////////////////////////////////////////////////////////////////////////
92
92
  // plugin hooks
93
93
 
94
- const pluginUpdateList = [], pluginRenderList = [];
94
+ const pluginList = [];
95
+ class EnginePlugin
96
+ {
97
+ constructor(update, render, glContextLost, glContextRestored)
98
+ {
99
+ this.update = update;
100
+ this.render = render;
101
+ this.glContextLost = glContextLost;
102
+ this.glContextRestored = glContextRestored;
103
+ }
104
+ }
95
105
 
96
106
  /**
97
107
  * @callback PluginCallback - Update or render function for a plugin
@@ -99,15 +109,21 @@ const pluginUpdateList = [], pluginRenderList = [];
99
109
  */
100
110
 
101
111
  /** Add a new update function for a plugin
102
- * @param {PluginCallback} [updateFunction]
103
- * @param {PluginCallback} [renderFunction]
112
+ * @param {PluginCallback} [update]
113
+ * @param {PluginCallback} [render]
114
+ * @param {PluginCallback} [glContextLost]
115
+ * @param {PluginCallback} [glContextRestored]
104
116
  * @memberof Engine */
105
- function engineAddPlugin(updateFunction, renderFunction)
117
+ function engineAddPlugin(update, render, glContextLost, glContextRestored)
106
118
  {
107
- ASSERT(!pluginUpdateList.includes(updateFunction));
108
- ASSERT(!pluginRenderList.includes(renderFunction));
109
- updateFunction && pluginUpdateList.push(updateFunction);
110
- renderFunction && pluginRenderList.push(renderFunction);
119
+ // make sure plugin functions are unique
120
+ ASSERT(!pluginList.find(p=>
121
+ p.update === update && p.render === render &&
122
+ p.glContextLost === glContextLost &&
123
+ p.glContextRestored === glContextRestored));
124
+
125
+ const plugin = new EnginePlugin(update, render, glContextLost, glContextRestored);
126
+ pluginList.push(plugin);
111
127
  }
112
128
 
113
129
  ///////////////////////////////////////////////////////////////////////////////
@@ -192,7 +208,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
192
208
  wasUpdated = true;
193
209
  updateCanvas();
194
210
  inputUpdate();
195
- pluginUpdateList.forEach(f=>f());
211
+ pluginList.forEach(plugin=>plugin.update?.());
196
212
 
197
213
  // update object transforms even when paused
198
214
  for (const o of engineObjects)
@@ -225,7 +241,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
225
241
  updateCanvas();
226
242
  inputUpdate();
227
243
  gameUpdate();
228
- pluginUpdateList.forEach(f=>f());
244
+ pluginList.forEach(plugin=>plugin.update?.());
229
245
  engineObjectsUpdate();
230
246
 
231
247
  // do post update
@@ -259,7 +275,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
259
275
  for (const o of engineObjects)
260
276
  o.destroyed || o.render();
261
277
  gameRenderPost();
262
- pluginRenderList.forEach(f=>f());
278
+ pluginList.forEach(plugin=>plugin.render?.());
263
279
  touchGamepadRender();
264
280
  debugRender();
265
281
  glFlush();
@@ -386,7 +402,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
386
402
  image.onerror = image.onload = ()=>
387
403
  {
388
404
  const textureInfo = new TextureInfo(image);
389
- textureInfo.createWebGLTexture();
390
405
  textureInfos[textureIndex] = textureInfo;
391
406
  resolve();
392
407
  }
@@ -402,7 +417,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
402
417
  {
403
418
  const textureInfo = new TextureInfo(new Image);
404
419
  textureInfos[0] = textureInfo;
405
- textureInfo.createWebGLTexture();
406
420
  resolve();
407
421
  }));
408
422
  }
@@ -324,8 +324,16 @@ class SoundInstance
324
324
  this.stop();
325
325
  this.gainNode = audioContext.createGain();
326
326
  this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
327
- this.startTime = audioContext.currentTime - offset;
328
- this.pausedTime = undefined;
327
+ if (this.source)
328
+ {
329
+ this.startTime = audioContext.currentTime - offset;
330
+ this.pausedTime = undefined;
331
+ }
332
+ else
333
+ {
334
+ this.startTime = undefined;
335
+ this.pausedTime = 0;
336
+ }
329
337
  }
330
338
 
331
339
  /** Set the volume of this sound instance
@@ -474,12 +482,19 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
474
482
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
475
483
  * @param {number} [offset] - Offset in seconds to start playback from
476
484
  * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
477
- * @return {AudioBufferSourceNode} - The audio node of the sound played
485
+ * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
478
486
  * @memberof Audio */
479
487
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
480
488
  {
481
489
  if (!soundEnable || headlessMode) return;
482
490
 
491
+ if (!audioIsRunning())
492
+ {
493
+ // fix stalled audio, this sound won't be able to play
494
+ audioContext.resume();
495
+ return;
496
+ }
497
+
483
498
  // create buffer and source
484
499
  const channelCount = sampleChannels.length;
485
500
  const sampleLength = sampleChannels[0].length;
@@ -505,14 +520,9 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
505
520
  if (onended)
506
521
  source.addEventListener('ended', ()=> onended(source));
507
522
 
523
+ // play and return sound
508
524
  const startOffset = offset * rate;
509
- if (!audioIsRunning())
510
- {
511
- // fix stalled audio and start
512
- audioContext.resume().then(()=>source.start(0, startOffset));
513
- }
514
- else
515
- source.start(0, startOffset);
525
+ source.start(0, startOffset);
516
526
  return source;
517
527
  }
518
528