littlejsengine 1.11.13 → 1.11.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/littlejs.d.ts +1306 -66
  2. package/dist/littlejs.esm.js +3097 -269
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +253 -265
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +155 -260
  7. package/examples/box2d/game.js +7 -10
  8. package/examples/box2d/gameObjects.js +33 -33
  9. package/examples/box2d/index.html +6 -6
  10. package/examples/box2d/scenes.js +28 -28
  11. package/examples/breakout/game.js +1 -4
  12. package/examples/breakout/index.html +4 -4
  13. package/examples/breakoutTutorial/index.html +2 -2
  14. package/examples/htmlMenu/game.js +3 -6
  15. package/examples/htmlMenu/index.html +2 -2
  16. package/examples/module/index.html +1 -1
  17. package/examples/particles/index.html +1 -1
  18. package/examples/platformer/game.js +0 -3
  19. package/examples/platformer/index.html +8 -8
  20. package/examples/puzzle/game.js +0 -3
  21. package/examples/puzzle/index.html +2 -2
  22. package/examples/shorts/base.html +1 -1
  23. package/examples/starter/build.js +4 -4
  24. package/examples/starter/index.html +13 -13
  25. package/examples/stress/index.html +3 -2
  26. package/examples/uiSystem/game.js +1 -7
  27. package/examples/uiSystem/index.html +3 -3
  28. package/package.json +3 -3
  29. package/plugins/box2d.js +1552 -640
  30. package/plugins/{Box2D_v2.3.1_min.wasm.js → box2d.wasm.js} +1 -1
  31. package/plugins/newgrounds.js +7 -8
  32. package/plugins/pluginExport.js +51 -0
  33. package/plugins/postProcess.js +94 -93
  34. package/plugins/uiSystem.js +145 -161
  35. package/plugins/zzfxm.js +163 -0
  36. package/src/engine.js +15 -20
  37. package/src/engineAudio.js +74 -204
  38. package/src/engineBuild.js +21 -3
  39. package/src/engineDebug.js +104 -6
  40. package/src/engineDraw.js +27 -14
  41. package/src/engineExport.js +12 -4
  42. package/src/engineParticles.js +6 -1
  43. package/src/engineRelease.js +6 -1
  44. package/src/engineSettings.js +2 -2
  45. package/src/engineUtilities.js +5 -11
  46. package/src/engineWebGL.js +19 -7
  47. package/examples/starter/build/index.html +0 -2
  48. package/examples/starter/build/index.js +0 -1
  49. package/examples/starter/build/tiles.png +0 -0
  50. package/examples/starter/game.zip +0 -0
  51. /package/plugins/{Box2D_v2.3.1_min.wasm.wasm → box2d.wasm.wasm} +0 -0
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.11.13';
33
+ const engineVersion = '1.11.17';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {number}
@@ -118,16 +118,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
118
118
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
119
119
 
120
120
  // allow passing in empty functions
121
- if (!gameInit)
122
- gameInit = ()=>{};
123
- if (!gameUpdate)
124
- gameUpdate = ()=>{};
125
- if (!gameUpdatePost)
126
- gameUpdatePost = ()=>{};
127
- if (!gameRender)
128
- gameRender = ()=>{};
129
- if (!gameRenderPost)
130
- gameRenderPost = ()=>{};
121
+ gameInit ||= ()=>{};
122
+ gameUpdate ||= ()=>{};
123
+ gameUpdatePost ||= ()=>{};
124
+ gameRender ||= ()=>{};
125
+ gameRenderPost ||= ()=>{};
131
126
 
132
127
  // Called automatically by engine to setup render system
133
128
  function enginePreRender()
@@ -154,11 +149,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
154
149
  const debugSpeedUp = debug && keyIsDown('Equal'); // +
155
150
  const debugSpeedDown = debug && keyIsDown('Minus'); // -
156
151
  if (debug) // +/- to speed/slow time
157
- frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1;
152
+ frameTimeDeltaMS *= debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
158
153
  timeReal += frameTimeDeltaMS / 1e3;
159
154
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
160
155
  if (!debugSpeedUp)
161
- frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
156
+ frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
157
+ if (debug && debugVideoCaptureIsActive())
158
+ frameTimeBufferMS = 0; // disable time smoothing when capturing video
162
159
 
163
160
  updateCanvas();
164
161
 
@@ -237,6 +234,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
237
234
  }
238
235
  }
239
236
 
237
+ debugVideoCaptureUpdate();
240
238
  requestAnimationFrame(engineUpdate);
241
239
  }
242
240
 
@@ -284,11 +282,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
284
282
 
285
283
  // setup html
286
284
  const styleRoot =
287
- 'margin:0;overflow:hidden;' + // fill the window
288
- 'width:100vw;height:100vh;' + // fill the window
289
- 'display:flex;' + // use flexbox
290
- 'align-items:center;' + // horizontal center
291
- 'justify-content:center;' + // vertical center
285
+ 'margin:0;' + // fill the window
292
286
  'background:#000;' + // set background color
293
287
  (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
294
288
  'user-select:none;' + // prevent hold to select
@@ -311,7 +305,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
311
305
  overlayContext = overlayCanvas.getContext('2d');
312
306
 
313
307
  // set canvas style
314
- const styleCanvas = 'position:absolute'; // allow canvases to overlap
308
+ const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
309
+ 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
315
310
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
316
311
  if (glCanvas)
317
312
  glCanvas.style.cssText = styleCanvas;
@@ -322,12 +317,12 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
322
317
  new Promise(resolve =>
323
318
  {
324
319
  const image = new Image;
325
- image.crossOrigin = 'anonymous';
326
320
  image.onerror = image.onload = ()=>
327
321
  {
328
322
  textureInfos[textureIndex] = new TextureInfo(image);
329
323
  resolve();
330
324
  }
325
+ image.crossOrigin = 'anonymous';
331
326
  image.src = src;
332
327
  })
333
328
  );
@@ -19,16 +19,15 @@ let audioContext = new AudioContext;
19
19
  /** Master gain node for all audio to pass through
20
20
  * @type {GainNode}
21
21
  * @memberof Audio */
22
- let audioGainNode;
22
+ let audioMasterGain;
23
23
 
24
24
  function audioInit()
25
25
  {
26
26
  if (!soundEnable || headlessMode) return;
27
27
 
28
- // (createGain is more widely supported then GainNode constructor)
29
- audioGainNode = audioContext.createGain();
30
- audioGainNode.connect(audioContext.destination);
31
- audioGainNode.gain.value = soundVolume; // set starting value
28
+ audioMasterGain = audioContext.createGain();
29
+ audioMasterGain.connect(audioContext.destination);
30
+ audioMasterGain.gain.value = soundVolume; // set starting value
32
31
  }
33
32
 
34
33
  ///////////////////////////////////////////////////////////////////////////////
@@ -157,6 +156,8 @@ class Sound
157
156
  isLoading() { return !this.sampleChannels; }
158
157
  }
159
158
 
159
+ ///////////////////////////////////////////////////////////////////////////////
160
+
160
161
  /**
161
162
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
162
163
  * - this can be used to play wave, mp3, and ogg files
@@ -208,59 +209,7 @@ function playAudioFile(filename, volume=1, loop=false)
208
209
  return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
209
210
  }
210
211
 
211
- /**
212
- * Music Object - Stores a zzfx music track for later use
213
- *
214
- * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
215
- * @example
216
- * // create some music
217
- * const music_example = new Music(
218
- * [
219
- * [ // instruments
220
- * [,0,400] // simple note
221
- * ],
222
- * [ // patterns
223
- * [ // pattern 1
224
- * [ // channel 0
225
- * 0, -1, // instrument 0, left speaker
226
- * 1, 0, 9, 1 // channel notes
227
- * ],
228
- * [ // channel 1
229
- * 0, 1, // instrument 0, right speaker
230
- * 0, 12, 17, -1 // channel notes
231
- * ]
232
- * ],
233
- * ],
234
- * [0, 0, 0, 0], // sequence, play pattern 0 four times
235
- * 90 // BPM
236
- * ]);
237
- *
238
- * // play the music
239
- * music_example.play();
240
- */
241
- class Music extends Sound
242
- {
243
- /** Create a music object and cache the zzfx music samples for later use
244
- * @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters
245
- */
246
- constructor(zzfxMusic)
247
- {
248
- super(undefined);
249
-
250
- if (!soundEnable || headlessMode) return;
251
- this.randomness = 0;
252
- this.sampleChannels = zzfxM(...zzfxMusic);
253
- this.sampleRate = zzfxR;
254
- }
255
-
256
- /** Play the music
257
- * @param {number} [volume=1] - How much to scale volume by
258
- * @param {boolean} [loop] - True if the music should loop
259
- * @return {AudioBufferSourceNode} - The audio source node
260
- */
261
- playMusic(volume, loop=false)
262
- { return super.play(undefined, volume, 1, 1, loop); }
263
- }
212
+ ///////////////////////////////////////////////////////////////////////////////
264
213
 
265
214
  /** Speak text with passed in settings
266
215
  * @param {string} text - The text to speak
@@ -332,7 +281,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
332
281
  // create and connect gain node
333
282
  gainNode = gainNode || audioContext.createGain();
334
283
  gainNode.gain.value = volume;
335
- gainNode.connect(audioGainNode);
284
+ gainNode.connect(audioMasterGain);
336
285
 
337
286
  // connect source to stereo panner and gain
338
287
  const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
@@ -352,7 +301,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
352
301
  }
353
302
 
354
303
  ///////////////////////////////////////////////////////////////////////////////
355
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.1 by Frank Force
304
+ // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
356
305
 
357
306
  /** Generate and play a ZzFX sound
358
307
  *
@@ -394,21 +343,45 @@ const zzfxR = 44100;
394
343
  */
395
344
  function zzfxG
396
345
  (
397
- // parameters
398
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
399
- release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
400
- pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
401
- bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
346
+ volume = 1,
347
+ randomness = .05,
348
+ frequency = 220,
349
+ attack = 0,
350
+ sustain = 0,
351
+ release = .1,
352
+ shape = 0,
353
+ shapeCurve = 1,
354
+ slide = 0,
355
+ deltaSlide = 0,
356
+ pitchJump = 0,
357
+ pitchJumpTime = 0,
358
+ repeatTime = 0,
359
+ noise = 0,
360
+ modulation = 0,
361
+ bitCrush = 0,
362
+ delay = 0,
363
+ sustainVolume = 1,
364
+ decay = 0,
365
+ tremolo = 0,
366
+ filter = 0
402
367
  )
403
368
  {
404
- // LJS Note: ZZFX modded so randomness is handled by Sound class
405
-
406
369
  // init parameters
407
- let PI2 = PI*2, sampleRate = zzfxR,
370
+ let sampleRate = zzfxR,
371
+ PI2 = PI*2,
408
372
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
409
373
  startFrequency = frequency *=
410
- rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
411
- b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
374
+ (1 + rand(randomness,-randomness)) * PI2 / sampleRate,
375
+ modOffset = 0, // modulation offset
376
+ repeat = 0, // repeat offset
377
+ crush = 0, // bit crush offset
378
+ jump = 1, // pitch jump timer
379
+ length, // sample length
380
+ b = [], // sample buffer
381
+ t = 0, // sample time
382
+ i = 0, // sample index
383
+ s = 0, // sample value
384
+ f, // wave frequency
412
385
 
413
386
  // biquad LP/HP filter
414
387
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
@@ -418,35 +391,37 @@ function zzfxG
418
391
  b1 = -(sign(filter) + cos) / a0, b2 = b0,
419
392
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
420
393
 
421
- // scale by sample rate
422
- attack = attack * sampleRate + 9; // minimum attack to prevent pop
423
- decay *= sampleRate;
424
- sustain *= sampleRate;
425
- release *= sampleRate;
426
- delay *= sampleRate;
427
- deltaSlide *= 500 * PI2 / sampleRate**3;
428
- modulation *= PI2 / sampleRate;
429
- pitchJump *= PI2 / sampleRate;
430
- pitchJumpTime *= sampleRate;
431
- repeatTime = repeatTime * sampleRate | 0;
394
+ // scale by sample rate
395
+ const minAttack = 9; // prevent pop if attack is 0
396
+ attack = attack * sampleRate || minAttack;
397
+ decay *= sampleRate;
398
+ sustain *= sampleRate;
399
+ release *= sampleRate;
400
+ delay *= sampleRate;
401
+ deltaSlide *= 500 * PI2 / sampleRate**3;
402
+ modulation *= PI2 / sampleRate;
403
+ pitchJump *= PI2 / sampleRate;
404
+ pitchJumpTime *= sampleRate;
405
+ repeatTime = repeatTime * sampleRate | 0;
432
406
 
433
407
  // generate waveform
434
408
  for(length = attack + decay + sustain + release + delay | 0;
435
- i < length; b[i++] = s * volume) // sample
409
+ i < length; b[i++] = s * volume) // sample
436
410
  {
437
- if (!(++c%(bitCrush*100|0))) // bit crush
411
+ if (!(++crush%(bitCrush*100|0))) // bit crush
438
412
  {
439
- s = shape? shape>1? shape>2? shape>3? // wave shape
440
- Math.sin(t**3) : // 4 noise
441
- clamp(Math.tan(t),1,-1): // 3 tan
442
- 1-(2*t/PI2%2+2)%2: // 2 saw
443
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
444
- Math.sin(t); // 0 sin
413
+ s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
414
+ (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
415
+ Math.sin(t**3) : // 4 noise
416
+ Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
417
+ 1-(2*t/PI2%2+2)%2: // 2 saw
418
+ 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
419
+ Math.sin(t); // 0 sin
445
420
 
446
421
  s = (repeatTime ?
447
422
  1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
448
423
  : 1) *
449
- sign(s)*(abs(s)**shapeCurve) * // curve
424
+ (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
450
425
  (i < attack ? i/attack : // attack
451
426
  i < attack + decay ? // decay
452
427
  1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
@@ -461,133 +436,28 @@ function zzfxG
461
436
  (i<length-delay? 1 : (length-i)/delay) * // release delay
462
437
  b[i-delay|0]/2/volume) : s; // sample delay
463
438
 
464
- if (filter) // apply filter
439
+ if (filter) // apply filter
465
440
  s = y1 = b2*x2 + b1*(x2=x1) + b0*(x1=s) - a2*y2 - a1*(y2=y1);
466
441
  }
467
442
 
468
443
  f = (frequency += slide += deltaSlide) *// frequency
469
- Math.cos(modulation*tm++); // modulation
444
+ Math.cos(modulation*modOffset++); // modulation
470
445
  t += f + f*noise*Math.sin(i**5); // noise
471
446
 
472
- if (j && ++j > pitchJumpTime) // pitch jump
447
+ if (jump && ++jump > pitchJumpTime) // pitch jump
473
448
  {
474
449
  frequency += pitchJump; // apply pitch jump
475
450
  startFrequency += pitchJump; // also apply to start
476
- j = 0; // stop pitch jump time
451
+ jump = 0; // stop pitch jump time
477
452
  }
478
453
 
479
- if (repeatTime && !(++r % repeatTime)) // repeat
454
+ if (repeatTime && !(++repeat % repeatTime)) // repeat
480
455
  {
481
- frequency = startFrequency; // reset frequency
482
- slide = startSlide; // reset slide
483
- j = j || 1; // reset pitch jump time
456
+ frequency = startFrequency; // reset frequency
457
+ slide = startSlide; // reset slide
458
+ jump ||= 1; // reset pitch jump time
484
459
  }
485
460
  }
486
461
 
487
- return b;
462
+ return b; // return sample buffer
488
463
  }
489
-
490
- ///////////////////////////////////////////////////////////////////////////////
491
- // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
492
-
493
- /** Generate samples for a ZzFM song with given parameters
494
- * @param {Array} instruments - Array of ZzFX sound parameters
495
- * @param {Array} patterns - Array of pattern data
496
- * @param {Array} sequence - Array of pattern indexes
497
- * @param {number} [BPM] - Playback speed of the song in BPM
498
- * @return {Array} - Left and right channel sample data
499
- * @memberof Audio */
500
- function zzfxM(instruments, patterns, sequence, BPM = 125)
501
- {
502
- let i, j, k;
503
- let instrumentParameters;
504
- let note;
505
- let sample;
506
- let patternChannel;
507
- let notFirstBeat;
508
- let stop;
509
- let instrument;
510
- let attenuation;
511
- let outSampleOffset;
512
- let isSequenceEnd;
513
- let sampleOffset = 0;
514
- let nextSampleOffset;
515
- let sampleBuffer = [];
516
- let leftChannelBuffer = [];
517
- let rightChannelBuffer = [];
518
- let channelIndex = 0;
519
- let panning = 0;
520
- let hasMore = 1;
521
- let sampleCache = {};
522
- let beatLength = zzfxR / BPM * 60 >> 2;
523
-
524
- // for each channel in order until there are no more
525
- for (; hasMore; channelIndex++) {
526
-
527
- // reset current values
528
- sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
529
-
530
- // for each pattern in sequence
531
- sequence.forEach((patternIndex, sequenceIndex) => {
532
- // get pattern for current channel, use empty 1 note pattern if none found
533
- patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
534
-
535
- // check if there are more channels
536
- hasMore |= patterns[patternIndex][channelIndex]&&1;
537
-
538
- // get next offset, use the length of first channel
539
- nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
540
- // for each beat in pattern, plus one extra if end of sequence
541
- isSequenceEnd = sequenceIndex == sequence.length - 1;
542
- for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
543
-
544
- // <channel-note>
545
- note = patternChannel[i];
546
-
547
- // stop if end, different instrument or new note
548
- stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
549
- instrument != (patternChannel[0] || 0) || note | 0;
550
-
551
- // fill buffer with samples for previous beat, most cpu intensive part
552
- for (j = 0; j < beatLength && notFirstBeat;
553
-
554
- // fade off attenuation at end of beat if stopping note, prevents clicking
555
- j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
556
- ) {
557
- // copy sample to stereo buffers with panning
558
- sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
559
- leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
560
- rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
561
- }
562
-
563
- // set up for next note
564
- if (note) {
565
- // set attenuation
566
- attenuation = note % 1;
567
- panning = patternChannel[1] || 0;
568
- if (note |= 0) {
569
- // get cached sample
570
- sampleBuffer = sampleCache[
571
- [
572
- instrument = patternChannel[sampleOffset = 0] || 0,
573
- note
574
- ]
575
- ] = sampleCache[[instrument, note]] || (
576
- // add sample to cache
577
- instrumentParameters = [...instruments[instrument]],
578
- instrumentParameters[2] *= 2 ** ((note - 12) / 12),
579
-
580
- // allow negative values to stop notes
581
- note > 0 ? zzfxG(...instrumentParameters) : []
582
- );
583
- }
584
- }
585
- }
586
-
587
- // update the sample offset
588
- outSampleOffset = nextSampleOffset;
589
- });
590
- }
591
-
592
- return [leftChannelBuffer, rightChannelBuffer];
593
- }
@@ -13,6 +13,7 @@
13
13
  const ENGINE_NAME = 'littlejs';
14
14
  const BUILD_FOLDER = 'dist';
15
15
  const SOURCE_FOLDER = 'src';
16
+ const PLUGIN_FOLDER = 'plugins';
16
17
  const engineSourceFiles =
17
18
  [
18
19
  `${SOURCE_FOLDER}/engineUtilities.js`,
@@ -27,6 +28,15 @@ const engineSourceFiles =
27
28
  `${SOURCE_FOLDER}/engineWebGL.js`,
28
29
  `${SOURCE_FOLDER}/engine.js`,
29
30
  ];
31
+ const enginePluginFiles =
32
+ [
33
+ `${PLUGIN_FOLDER}/newgrounds.js`,
34
+ `${PLUGIN_FOLDER}/postProcess.js`,
35
+ `${PLUGIN_FOLDER}/zzfxm.js`,
36
+ `${PLUGIN_FOLDER}/uiSystem.js`,
37
+ `${PLUGIN_FOLDER}/box2d.js`,
38
+ `${PLUGIN_FOLDER}/pluginExport.js`,
39
+ ];
30
40
  const asciiArt =`
31
41
  ~~~~°°°°ooo°oOo°ooOooOooOo.
32
42
  __________ ________ ____'°oO.
@@ -79,7 +89,11 @@ Build
79
89
  (
80
90
  'Build Engine -- ESM',
81
91
  `${BUILD_FOLDER}/${ENGINE_NAME}.esm.js`,
82
- [`${BUILD_FOLDER}/${ENGINE_NAME}.js`, `${SOURCE_FOLDER}/engineExport.js`],
92
+ [
93
+ `${BUILD_FOLDER}/${ENGINE_NAME}.js`,
94
+ `${SOURCE_FOLDER}/engineExport.js`,
95
+ ...enginePluginFiles
96
+ ],
83
97
  [typeScriptBuildStep]
84
98
  );
85
99
 
@@ -87,8 +101,12 @@ Build
87
101
  (
88
102
  'Build Engine -- ESM minified release',
89
103
  `${BUILD_FOLDER}/${ENGINE_NAME}.esm.min.js`,
90
- [`${BUILD_FOLDER}/${ENGINE_NAME}.min.js`, `${SOURCE_FOLDER}/engineExport.js`],
91
- [uglifyBuildStep]
104
+ [
105
+ `${BUILD_FOLDER}/${ENGINE_NAME}.release.js`,
106
+ `${SOURCE_FOLDER}/engineExport.js`,
107
+ ...enginePluginFiles
108
+ ],
109
+ [closureCompilerStep, uglifyBuildStep]
92
110
  );
93
111
 
94
112
  console.log(`Engine built in ${((Date.now() - startTime)/1e3).toFixed(2)} seconds!`);
@@ -72,8 +72,10 @@ function ASSERT(assert, output)
72
72
  * @memberof Debug */
73
73
  function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
74
74
  {
75
+ if (typeof size == 'number')
76
+ size = vec2(size); // allow passing in floats
75
77
  ASSERT(typeof color == 'string', 'pass in css color strings');
76
- debugPrimitives.push({pos, size:vec2(size), color, time:new Timer(time), angle, fill});
78
+ debugPrimitives.push({pos, size, color, time:new Timer(time), angle, fill});
77
79
  }
78
80
 
79
81
  /** Draw a debug poly in world space
@@ -197,17 +199,17 @@ function debugSaveDataURL(dataURL, filename)
197
199
  * @memberof Debug */
198
200
  function debugShowErrors()
199
201
  {
200
- onunhandledrejection = (event)=>showError(event.reason);
201
- onerror = (event, source, lineno, colno)=>
202
- showError(`${event}\n${source}\nLn ${lineno}, Col ${colno}`);
203
-
204
202
  const showError = (message)=>
205
203
  {
206
204
  // replace entire page with error message
207
205
  document.body.style.display = '';
208
206
  document.body.style.backgroundColor = '#111';
209
- document.body.innerHTML = `<pre style=color:#f00;font-size:50px>` + message;
207
+ document.body.innerHTML = `<pre style=color:#f00;font-size:50px;white-space:pre-wrap>` + message;
210
208
  }
209
+ onunhandledrejection = (event)=>
210
+ showError(event.reason.stack || event.reason);
211
+ onerror = (message, source, lineno, colno)=>
212
+ showError(`${message}\n${source}\nLn ${lineno}, Col ${colno}`);
211
213
  }
212
214
 
213
215
  ///////////////////////////////////////////////////////////////////////////////
@@ -240,11 +242,16 @@ function debugUpdate()
240
242
  debugRaycast = !debugRaycast;
241
243
  if (keyWasPressed('Digit5'))
242
244
  debugScreenshot();
245
+ if (keyWasPressed('Digit6'))
246
+ debugVideoCaptureIsActive() ? debugVideoCaptureStop() : debugVideoCaptureStart();
243
247
  }
244
248
  }
245
249
 
246
250
  function debugRender()
247
251
  {
252
+ if (debugVideoCaptureIsActive())
253
+ return; // don't show debug info when capturing video
254
+
248
255
  glCopyToContext(mainContext);
249
256
 
250
257
  if (debugTakeScreenshot)
@@ -439,6 +446,7 @@ function debugRender()
439
446
  overlayContext.fillText('4: Debug Raycasts', x, y += h);
440
447
  overlayContext.fillStyle = '#fff';
441
448
  overlayContext.fillText('5: Save Screenshot', x, y += h);
449
+ overlayContext.fillText('6: Capture Video', x, y += h);
442
450
 
443
451
  let keysPressed = '';
444
452
  for(const i in inputData[0])
@@ -467,4 +475,94 @@ function debugRender()
467
475
 
468
476
  overlayContext.restore();
469
477
  }
478
+ }
479
+
480
+ ///////////////////////////////////////////////////////////////////////////////
481
+ // video capture - records video and audio at 60 fps using MediaRecorder API
482
+
483
+ // internal variables used to capture video
484
+ let debugVideoCapture, debugVideoCaptureTrack, debugVideoCaptureIcon, debugVideoCaptureTimer;
485
+
486
+ /** Check if video capture is active
487
+ * @memberof Debug */
488
+ function debugVideoCaptureIsActive() { return !!debugVideoCapture; }
489
+
490
+ /** Start capturing video
491
+ * @memberof Debug */
492
+ function debugVideoCaptureStart()
493
+ {
494
+ if (debugVideoCaptureIsActive())
495
+ return; // already recording
496
+
497
+ // captureStream passing in 0 to only capture when requestFrame() is called
498
+ const stream = mainCanvas.captureStream(0);
499
+ const chunks = [];
500
+ debugVideoCaptureTrack = stream.getVideoTracks()[0];
501
+ if (debugVideoCaptureTrack.applyConstraints)
502
+ debugVideoCaptureTrack.applyConstraints({frameRate:frameRate}); // force 60 fps
503
+ debugVideoCapture = new MediaRecorder(stream, {mimeType:'video/webm;codecs=vp8'});
504
+ debugVideoCapture.ondataavailable = (e)=> chunks.push(e.data);
505
+ debugVideoCapture.onstop = ()=>
506
+ {
507
+ const blob = new Blob(chunks, {type: 'video/webm'});
508
+ const url = URL.createObjectURL(blob);
509
+ downloadLink.download = 'capture.webm';
510
+ downloadLink.href = url;
511
+ downloadLink.click();
512
+ URL.revokeObjectURL(url);
513
+ };
514
+
515
+ if (audioMasterGain)
516
+ {
517
+ // connect to audio master gain node
518
+ const audioStreamDestination = audioContext.createMediaStreamDestination();
519
+ audioMasterGain.connect(audioStreamDestination);
520
+ for (const track of audioStreamDestination.stream.getAudioTracks())
521
+ stream.addTrack(track); // add audio tracks to capture stream
522
+ }
523
+
524
+ // start recording
525
+ console.log('Video capture started.');
526
+ debugVideoCapture.start();
527
+ debugVideoCaptureTimer = new Timer(0);
528
+
529
+ if (!debugVideoCaptureIcon)
530
+ {
531
+ // create recording icon to show it is capturing video
532
+ debugVideoCaptureIcon = document.createElement('div');
533
+ debugVideoCaptureIcon.style.position = 'absolute';
534
+ debugVideoCaptureIcon.style.padding = '9px';
535
+ debugVideoCaptureIcon.style.color = '#f00';
536
+ debugVideoCaptureIcon.style.font = '50px monospace';
537
+ document.body.appendChild(debugVideoCaptureIcon);
538
+ }
539
+ // show recording icon
540
+ debugVideoCaptureIcon.textContent = '';
541
+ debugVideoCaptureIcon.style.display = '';
542
+ }
543
+
544
+ /** Stop capturing video and save to disk
545
+ * @memberof Debug */
546
+ function debugVideoCaptureStop()
547
+ {
548
+ if (!debugVideoCaptureIsActive())
549
+ return; // not recording
550
+
551
+ // stop recording
552
+ console.log(`Video capture ended. ${debugVideoCaptureTimer.get().toFixed(2)} seconds recorded.`);
553
+ debugVideoCapture.stop();
554
+ debugVideoCapture = 0;
555
+ debugVideoCaptureIcon.style.display = 'none';
556
+ }
557
+
558
+ // update video capture, called automatically by engine
559
+ function debugVideoCaptureUpdate()
560
+ {
561
+ if (!debugVideoCaptureIsActive())
562
+ return; // not recording
563
+
564
+ // save the video frame
565
+ combineCanvases();
566
+ debugVideoCaptureTrack.requestFrame();
567
+ debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
470
568
  }