littlejsengine 1.25.0 → 1.26.1

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/CHANGELOG.md +99 -0
  2. package/FAQ.md +49 -11
  3. package/README.md +6 -2
  4. package/REFERENCE.md +129 -21
  5. package/dist/littlejs.d.ts +487 -102
  6. package/dist/littlejs.esm.js +2898 -719
  7. package/dist/littlejs.esm.min.js +3 -3
  8. package/dist/littlejs.js +2888 -716
  9. package/dist/littlejs.min.js +2 -2
  10. package/dist/littlejs.release.js +2702 -527
  11. package/package.json +4 -4
  12. package/plugins/box2d.js +110 -34
  13. package/plugins/gltf.js +74 -22
  14. package/plugins/lightSystem.js +410 -28
  15. package/plugins/math3d.js +4 -2
  16. package/plugins/medalSystem.js +19 -7
  17. package/plugins/newgrounds.js +4 -2
  18. package/plugins/parallax.js +1 -0
  19. package/plugins/particleEffects.js +44 -10
  20. package/plugins/pathFinder.js +9 -11
  21. package/plugins/pluginExport.js +9 -0
  22. package/plugins/postProcess.js +9 -3
  23. package/plugins/render3d.js +499 -2389
  24. package/plugins/render3dDebug.js +5 -8
  25. package/plugins/render3dEditor.js +81 -6
  26. package/plugins/render3dExtras.js +103 -31
  27. package/plugins/render3dLevel.js +53 -5
  28. package/plugins/render3dMesh.js +1158 -0
  29. package/plugins/render3dObject.js +1451 -0
  30. package/plugins/render3dVoxels.js +15 -3
  31. package/plugins/textureSheet.js +9 -8
  32. package/plugins/threejs.js +1 -1
  33. package/plugins/tweenSystem.js +76 -51
  34. package/plugins/uiSystem.js +37 -8
  35. package/plugins/wavedash.js +229 -0
  36. package/src/engine.js +199 -40
  37. package/src/engineAudio.js +37 -21
  38. package/src/engineBuild.mjs +6 -1
  39. package/src/engineDebug.js +22 -162
  40. package/src/engineDraw.js +142 -34
  41. package/src/engineEditor.js +79 -18
  42. package/src/engineExport.js +1 -3
  43. package/src/engineInput.js +34 -24
  44. package/src/engineMath.js +35 -14
  45. package/src/engineObject.js +60 -41
  46. package/src/engineParticles.js +58 -11
  47. package/src/engineRelease.js +1 -5
  48. package/src/engineSettings.js +48 -9
  49. package/src/engineTileLayer.js +11 -4
  50. package/src/engineUtilities.js +41 -9
  51. package/src/engineWebGL.js +37 -13
@@ -38,7 +38,8 @@ if (audioMasterGain)
38
38
  // soundVolume can be set directly, so the master gain follows it each frame
39
39
  function audioUpdateVolume()
40
40
  {
41
- if (audioMasterGain && soundVolume !== audioMasterVolume)
41
+ // a volume that is not a number, from a slider's text say, is not handed on, where every frame it would throw
42
+ if (audioMasterGain && soundVolume !== audioMasterVolume && isFinite(soundVolume))
42
43
  audioMasterGain.gain.value = audioMasterVolume = soundVolume;
43
44
  }
44
45
 
@@ -126,7 +127,6 @@ function audioStateChange()
126
127
  * - The output node is disconnected from everything else first, so it only feeds the speakers
127
128
  * - The two ends of a chain must already be connected to each other, like effectA.connect(effectB)
128
129
  * - Call with no arguments to remove the effect, an effect that was the master goes back to feeding the master gain
129
- * - Debug video capture records the end of the master chain, but loses its tap if the effect changes mid-capture
130
130
  * @param {AudioNode|AudioEffectNodes} [input] - Node or effect the master gain connects to
131
131
  * @param {AudioNode|AudioEffectNodes} [output] - Node or effect that connects to the audio destination, defaults to the input's output
132
132
  * @memberof Audio */
@@ -181,11 +181,16 @@ function audioEffectNode(effectOrNode, key)
181
181
  return /** @type {AudioNode} */ (effectOrNode);
182
182
  }
183
183
 
184
+ // a value for an audio param, which throws on one that is not finite, so a release build, with no asserts to stop it,
185
+ // takes NaN or Infinity as the value given in its place: silent, centered or the normal rate
186
+ const audioFinite = (value, otherwise)=> isFinite(value) ? value : otherwise;
187
+
184
188
  // ramp an audio param to a value, cancelling anything already scheduled so stacked calls don't fight;
185
189
  // returns when the ramp ends
186
190
  function audioParamRamp(param, value, fadeTime=0)
187
191
  {
188
192
  ASSERT(fadeTime >= 0, 'fadeTime must be positive or zero');
193
+ value = audioFinite(value, 0), fadeTime = audioFinite(fadeTime, 0);
189
194
  const startTime = audioContext.currentTime;
190
195
  param.cancelScheduledValues(startTime);
191
196
  if (fadeTime)
@@ -201,7 +206,7 @@ function audioParamRamp(param, value, fadeTime=0)
201
206
  ///////////////////////////////////////////////////////////////////////////////
202
207
 
203
208
  /**
204
- * @callback SoundLoadCallback - Function called when sound is loaded
209
+ * @callback SoundLoadCallback - Function called once a sound has loaded, or failed to, which sound.isLoaded() tells
205
210
  * @param {Sound} sound
206
211
  * @memberof Audio
207
212
  */
@@ -232,7 +237,8 @@ class Sound
232
237
  * @param {number} [randomness] - How much to randomize frequency each time sound plays, for zzfx sounds it overrides the array's own randomness, which is used if undefined
233
238
  * @param {number} [range=soundDefaultRange] - World space max range of sound
234
239
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
235
- * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
240
+ * @param {SoundLoadCallback} [onloadCallback] - Called once the sound has loaded, or its file failed to, so a
241
+ * game counting its sounds goes on; isLoaded says which
236
242
  */
237
243
  constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
238
244
  {
@@ -291,7 +297,7 @@ class Sound
291
297
 
292
298
  // remove randomness so it can be applied on playback, a value passed in wins over the array's
293
299
  const randomnessIndex = 1;
294
- this.randomness = randomness ?? zzfxSound[randomnessIndex] ?? .05;
300
+ this.randomness = randomness ?? clamp(zzfxSound[randomnessIndex] ?? .05); // 0 to 1, as one passed in
295
301
  zzfxSound[randomnessIndex] = 0;
296
302
 
297
303
  // generate the zzfx samples, then hand them to an audio buffer so
@@ -307,7 +313,12 @@ class Sound
307
313
  // report failures rather than leaving an unhandled rejection, the sound just stays unloaded and silent
308
314
  const filename = asset + '';
309
315
  engineAddLoad(this.loadSound(filename).catch(e=> // startup waits for it, and a release build says why too
310
- console.warn('Sound load failed for', filename, '-', e?.message ?? e)));
316
+ {
317
+ console.warn('Sound load failed for', filename, '-', e?.message ?? e);
318
+ // failed, not loaded and its callback threw; one that throws here says so, a load is no place to stop
319
+ try { this.loadedPercent === 1 || this.onloadCallback?.(this); }
320
+ catch (callbackError) { console.warn('Sound onloadCallback failed for', filename, '-', callbackError); }
321
+ }));
311
322
  }
312
323
  }
313
324
 
@@ -390,8 +401,8 @@ class Sound
390
401
  {
391
402
  // apply range based fade
392
403
  const lengthSquared = cameraPos.distanceSquared(pos);
393
- if (lengthSquared > range*range)
394
- return; // out of range
404
+ if (lengthSquared >= range*range)
405
+ return; // out of range, at it too, where the fade is silent
395
406
 
396
407
  // attenuate volume by distance, full volume out to the taper and a fade past it,
397
408
  // so a taper of 1 plays at full volume right up to the range
@@ -467,14 +478,14 @@ class Sound
467
478
  */
468
479
  isLoaded() { return this.loadedPercent === 1; }
469
480
 
470
- /** Loads a sound from a URL and decodes it into sample data.
481
+ /** Loads a sound from a URL and decodes it into sample data
471
482
  * @param {string} filename
472
483
  * @return {Promise} */
473
484
  async loadSound(filename)
474
485
  {
475
- const response = await fetch(filename);
486
+ const response = await loadFetch(filename, 'loadSound'); // named, with the hint for a page opened from disk
476
487
  if (!response.ok)
477
- throw new Error(`Failed to load sound from ${filename}: ${response.status} ${response.statusText}`);
488
+ throw new Error(`loadSound: could not load ${filename}, HTTP ${response.status} ${response.statusText}`);
478
489
  const arrayBuffer = await response.arrayBuffer();
479
490
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
480
491
 
@@ -525,11 +536,11 @@ class SoundInstance
525
536
  /** @property {Sound} - The sound object */
526
537
  this.sound = sound;
527
538
  /** @property {number} - How much to scale volume by */
528
- this.volume = volume;
539
+ this.volume = audioFinite(volume, 0);
529
540
  /** @property {number} - The playback rate to use */
530
- this.rate = rate;
541
+ this.rate = audioFinite(rate, 1);
531
542
  /** @property {number} - How much to apply stereo panning */
532
- this.pan = pan;
543
+ this.pan = audioFinite(pan, 0);
533
544
  /** @property {boolean} - Should the sound loop */
534
545
  this.loop = loop;
535
546
  /** @property {number|undefined} - Where it is in the sound while not playing, in the sound's own seconds, undefined while playing
@@ -621,7 +632,7 @@ class SoundInstance
621
632
  {
622
633
  ASSERT(volume >= 0, 'Sound volume must be positive or zero');
623
634
  ASSERT(fadeTime >= 0, 'Sound fade time must be positive or zero');
624
- this.volume = volume;
635
+ this.volume = volume = audioFinite(volume, 0);
625
636
  this.gainNode && audioParamRamp(this.gainNode.gain, volume, fadeTime);
626
637
  }
627
638
 
@@ -631,7 +642,7 @@ class SoundInstance
631
642
  setPan(pan)
632
643
  {
633
644
  ASSERT(isNumber(pan), 'Sound pan must be a number');
634
- this.pan = pan;
645
+ this.pan = pan = audioFinite(pan, 0);
635
646
  if (this.pannerNode)
636
647
  this.pannerNode.pan.value = clamp(pan, -1, 1);
637
648
  }
@@ -649,7 +660,7 @@ class SoundInstance
649
660
  this.startOffset = this.getCurrentTime();
650
661
  this.startTime = audioContext.currentTime;
651
662
  }
652
- this.rate = rate;
663
+ this.rate = rate = audioFinite(rate, 1);
653
664
  if (this.source)
654
665
  this.source.playbackRate.value = rate;
655
666
  }
@@ -725,7 +736,9 @@ class SoundInstance
725
736
  if (!this.isPlaying()) return this.pausedTime;
726
737
  const duration = this.getDuration();
727
738
  const place = this.startOffset + (audioContext.currentTime - this.startTime) * this.rate;
728
- return duration ? mod(place, duration) : 0; // a sound still loading has no length yet
739
+ // a sound still loading has no length yet; a loop goes around, a one shot past its end, before its ended
740
+ // event, stays at its end, so a pause there does not play it again on resume
741
+ return !duration ? 0 : this.loop ? mod(place, duration) : min(place, duration);
729
742
  }
730
743
 
731
744
  /** Get the length of the sound in its own seconds, the same at any rate; divide by the rate for how long it takes to play
@@ -865,6 +878,8 @@ function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode,
865
878
  return;
866
879
  }
867
880
 
881
+ volume = audioFinite(volume, 0), rate = audioFinite(rate, 1), pan = audioFinite(pan, 0);
882
+
868
883
  // setup source, many sources can share one buffer
869
884
  const source = audioContext.createBufferSource();
870
885
  source.buffer = buffer;
@@ -920,8 +935,8 @@ function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
920
935
  * @param {number} [release] - Release time, how fast sound fades out (seconds)
921
936
  * @param {number} [shape] - Shape of the sound wave
922
937
  * @param {number} [shapeCurve] - Squareness of wave (0=square, 1=normal, 2=pointy)
923
- * @param {number} [slide] - How much to slide frequency (kHz/s)
924
- * @param {number} [deltaSlide] - How much to change slide (kHz/s/s)
938
+ * @param {number} [slide] - How much to slide frequency, 1 is about 500 Hz a second, as ZzFX has it
939
+ * @param {number} [deltaSlide] - How much to change slide each second, in the units of slide
925
940
  * @param {number} [pitchJump] - Frequency of pitch jump (Hz)
926
941
  * @param {number} [pitchJumpTime] - Time of pitch jump (seconds)
927
942
  * @param {number} [repeatTime] - Resets some parameters periodically (seconds)
@@ -932,7 +947,8 @@ function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
932
947
  * @param {number} [sustainVolume] - Volume level for sustain (percent)
933
948
  * @param {number} [decay] - Decay time, how long to reach sustain after attack (seconds)
934
949
  * @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
935
- * @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
950
+ * @param {number} [filter] - Filter cutoff, positive for HPF, negative for LPF; the cutoff is about twice this in
951
+ * Hz, as ZzFX has it, and its resonance can take the samples past 1
936
952
  * @return {Float32Array} - The audio samples
937
953
  * @memberof Audio */
938
954
  function zzfxG
@@ -42,6 +42,7 @@ const enginePluginFiles =
42
42
  [
43
43
  `${PLUGIN_FOLDER}/medalSystem.js`,
44
44
  `${PLUGIN_FOLDER}/newgrounds.js`,
45
+ `${PLUGIN_FOLDER}/wavedash.js`,
45
46
  `${PLUGIN_FOLDER}/postProcess.js`,
46
47
  `${PLUGIN_FOLDER}/lightSystem.js`,
47
48
  `${PLUGIN_FOLDER}/audioEffects.js`,
@@ -55,6 +56,8 @@ const enginePluginFiles =
55
56
  `${PLUGIN_FOLDER}/pathFinder.js`,
56
57
  `${PLUGIN_FOLDER}/math3d.js`,
57
58
  `${PLUGIN_FOLDER}/render3d.js`,
59
+ `${PLUGIN_FOLDER}/render3dMesh.js`,
60
+ `${PLUGIN_FOLDER}/render3dObject.js`,
58
61
  `${PLUGIN_FOLDER}/render3dExtras.js`,
59
62
  `${PLUGIN_FOLDER}/render3dVoxels.js`,
60
63
  `${PLUGIN_FOLDER}/render3dLevel.js`,
@@ -292,7 +295,9 @@ function typeScriptBuildStep(filename)
292
295
  // strictNullChecks keeps undefined in the types, where tsc would drop it from accessors and object types
293
296
  // whatever the JSDoc says, so a game in strict mode can set render3D.shader = undefined and is told a
294
297
  // flare may be missing; a game not in strict mode reads the same types as before
295
- execSync(`npx -p typescript tsc "${filename}" --declaration --allowJs --emitDeclarationOnly --strictNullChecks --outFile "${tsFilename}"`);
298
+ // the target names the library tsc types with: without it tsc takes ES5's, where Map and Set do not resolve
299
+ // and come out as any, which went unseen while a dependency brought @types/node, which declares them
300
+ execSync(`npx -p typescript tsc "${filename}" --declaration --allowJs --emitDeclarationOnly --strictNullChecks --target es2022 --outFile "${tsFilename}"`);
296
301
 
297
302
  // Make declare module part use the package name littlejsengine
298
303
  let fileContent = fs.readFileSync(tsFilename, 'utf8');
@@ -6,7 +6,7 @@
6
6
  * - setDebugKeysAlways lets those keys work with the overlay closed too
7
7
  * - ASSERT and LOG macros for development (removed in release builds)
8
8
  * - Debug primitive rendering (rectangles, circles, lines, points, text)
9
- * - Screenshot and video capture support
9
+ * - Screenshot support
10
10
  * - FPS counter and performance watermark
11
11
  * - Debug overlay shows mouse position and picked objects
12
12
  * @namespace Debug
@@ -84,7 +84,24 @@ function ASSERT(assert, ...output)
84
84
  {
85
85
  if (assert) return;
86
86
  console.assert(assert, ...output)
87
- throw new Error('Assert failed!'); // halt execution
87
+ // the error says what went wrong, so an error box or overlay that shows only the error shows the message too: a
88
+ // value with a toString of its own as that, a vec2 or a color, a plain object as JSON, an object of a class with
89
+ // none by its class's name, each cut at 200 characters, so a whole level is not the message
90
+ const text = output.map((value)=>
91
+ {
92
+ let text;
93
+ try
94
+ {
95
+ const prototype = value && typeof value === 'object' ? Object.getPrototypeOf(value) : undefined;
96
+ text = prototype === Object.prototype || prototype === null ? JSON.stringify(value) :
97
+ prototype && value.toString === Object.prototype.toString ?
98
+ '[' + (value.constructor?.name || 'object') + ']' : String(value);
99
+ }
100
+ catch { text = Object.prototype.toString.call(value); }
101
+ text = String(text); // JSON of nothing, as from a toJSON that returns nothing, is undefined
102
+ return text.length > 200 ? text.slice(0, 199) + '…' : text;
103
+ }).join(' ');
104
+ throw new Error(text ? 'Assert failed: ' + text : 'Assert failed!'); // halt execution
88
105
  }
89
106
 
90
107
  /** Log to console if debug is enabled, does nothing in release builds
@@ -329,19 +346,6 @@ function debugUpdate()
329
346
  if (keyWasPressed('Digit0'))
330
347
  levelEditor.isOpen ? levelEditor.close() : levelEditor.open();
331
348
  }
332
- if (debugVideoCaptureIsActive())
333
- {
334
- // control to stop video capture, a capture the overlay started also stops when the overlay closes,
335
- // one the game started from code runs until it calls debugVideoCaptureStop
336
- if (debugKeys ? keyWasPressed('Digit8') : debugVideoCapture.fromOverlay)
337
- debugVideoCaptureStop();
338
- }
339
- else if (debugKeys && keyWasPressed('Digit8'))
340
- {
341
- debugVideoCaptureStart();
342
- if (debugVideoCapture)
343
- debugVideoCapture.fromOverlay = true;
344
- }
345
349
  }
346
350
 
347
351
  // the text beside the mouse, with the same shadow as the overlay
@@ -440,20 +444,13 @@ function debugRender()
440
444
  {
441
445
  if (debugTakeScreenshot)
442
446
  {
443
- // combine canvases, remove alpha and save, before the capture check so it is taken during video capture too
447
+ // combine canvases, remove alpha and save
444
448
  glFlush();
445
449
  combineCanvases();
446
450
  saveCanvas(mainCanvas);
447
451
  debugTakeScreenshot = 0;
448
452
  }
449
453
 
450
- if (debugVideoCaptureIsActive())
451
- {
452
- // don't show debug info when capturing video, but still drop the expired primitives so they don't pile up
453
- debugPrimitives = debugPrimitives.filter(r=>r.timer<0);
454
- return;
455
- }
456
-
457
454
  // flush any gl sprites before drawing debug info
458
455
  glFlush();
459
456
 
@@ -682,7 +679,6 @@ function debugRender()
682
679
  debugContext.fillText('6: Debug Sound', x, y += h);
683
680
  debugContext.fillStyle = '#fff';
684
681
  debugContext.fillText('7: Save Screenshot', x, y += h);
685
- debugContext.fillText('8: Toggle Video Capture', x, y += h);
686
682
  debugContext.fillStyle = debugTweakables ? '#f00' : '#fff';
687
683
  debugContext.fillText('9: Tweakables', x, y += h);
688
684
  debugContext.fillStyle = levelEditor.isOpen ? '#f00' : '#fff';
@@ -757,143 +753,6 @@ function debugRender()
757
753
  }
758
754
  }
759
755
 
760
- function debugRenderPost() { debugVideoCaptureIsActive() && debugVideoCaptureUpdate(); }
761
-
762
- ///////////////////////////////////////////////////////////////////////////////
763
- // video capture - records video and audio at 60 fps using MediaRecorder API
764
-
765
- // internal variables used to capture video
766
- let debugVideoCapture, debugVideoCaptureIcon;
767
-
768
- /** Check if video capture is active
769
- * @memberof Debug
770
- * @return {boolean} */
771
- function debugVideoCaptureIsActive() { return !!debugVideoCapture; }
772
-
773
- /** Start capturing video
774
- * @memberof Debug */
775
- function debugVideoCaptureStart()
776
- {
777
- ASSERT(!debugVideoCaptureIsActive(), 'Already capturing video!');
778
-
779
- // everything that can fail where recording is unsupported is in the try, a missing captureStream
780
- // or a MediaRecorder that refuses webm throws here and is cleaned up rather than stopping the game
781
- const captureTimer = new Timer(0, true);
782
- const chunks = [];
783
- let videoTrack, mediaRecorder, audioStreamDestination, silentAudioSource, audioTapNode;
784
- try
785
- {
786
- // setup captureStream to capture manually by passing 0
787
- const stream = mainCanvas.captureStream(0);
788
- videoTrack = stream.getVideoTracks()[0];
789
- videoTrack.applyConstraints({frameRate:frameRate}).catch(()=>{});
790
-
791
- // set up the media recorder
792
- mediaRecorder = new MediaRecorder(stream,
793
- {mimeType:'video/webm;codecs=vp8'});
794
- mediaRecorder.ondataavailable = (e)=> chunks.push(e.data);
795
- mediaRecorder.onstop = ()=>
796
- {
797
- const blob = new Blob(chunks, {type: 'video/webm'});
798
- const url = URL.createObjectURL(blob);
799
- saveDataURL(url, 'capture.webm', 1e3);
800
- };
801
-
802
- if (soundEnable)
803
- {
804
- // create silent audio source
805
- // fixes issue where video can not start recording without audio
806
- silentAudioSource = new ConstantSourceNode(audioContext, { offset: 0 });
807
- silentAudioSource.connect(audioMasterGain);
808
- silentAudioSource.start();
809
-
810
- // tap the end of the master chain so a master effect is in the recording
811
- // (a master effect swapped mid-capture drops the tap, the rest records silent)
812
- audioTapNode = audioMasterEffectOutput || audioMasterGain;
813
- audioStreamDestination = audioContext.createMediaStreamDestination();
814
- audioTapNode.connect(audioStreamDestination);
815
- for (const track of audioStreamDestination.stream.getAudioTracks())
816
- stream.addTrack(track); // add audio tracks to capture stream
817
- }
818
-
819
- // start recording
820
- mediaRecorder.start();
821
- }
822
- catch(e)
823
- {
824
- LOG('Video capture not supported in this browser!');
825
- videoTrack?.stop();
826
- silentAudioSource?.stop();
827
- if (audioStreamDestination)
828
- {
829
- // the tap may not have been made before the failure
830
- try { audioTapNode.disconnect(audioStreamDestination); }
831
- catch { }
832
- }
833
- return;
834
- }
835
-
836
- if (!debugVideoCaptureIcon)
837
- {
838
- // create recording icon to show it is capturing video
839
- debugVideoCaptureIcon = document.createElement('div');
840
- debugVideoCaptureIcon.style.position = 'absolute';
841
- debugVideoCaptureIcon.style.padding = '9px';
842
- debugVideoCaptureIcon.style.color = '#f00';
843
- debugVideoCaptureIcon.style.font = '50px monospace';
844
- document.body.appendChild(debugVideoCaptureIcon);
845
- }
846
- // show recording icon, only once recording has started
847
- debugVideoCaptureIcon.textContent = '';
848
- debugVideoCaptureIcon.style.display = '';
849
-
850
- LOG('Video capture started.');
851
-
852
- // save debug video info
853
- debugVideoCapture =
854
- {
855
- mediaRecorder,
856
- captureTimer,
857
- videoTrack,
858
- silentAudioSource,
859
- audioStreamDestination,
860
- audioTapNode
861
- };
862
- }
863
-
864
- /** Stop capturing video and save to disk
865
- * @memberof Debug */
866
- function debugVideoCaptureStop()
867
- {
868
- ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
869
-
870
- // stop recording
871
- LOG(`Video capture ended. ${debugVideoCapture.captureTimer.get().toFixed(2)} seconds recorded.`);
872
- debugVideoCaptureIcon.style.display = 'none';
873
- debugVideoCapture.silentAudioSource?.stop();
874
- debugVideoCapture.mediaRecorder?.stop();
875
- debugVideoCapture.videoTrack?.stop();
876
- if (debugVideoCapture.audioStreamDestination)
877
- {
878
- // the tap is already gone if the master effect changed during the capture
879
- try { debugVideoCapture.audioTapNode.disconnect(debugVideoCapture.audioStreamDestination); }
880
- catch { }
881
- }
882
- debugVideoCapture = undefined;
883
- }
884
-
885
- // update video capture, called automatically by engine
886
- function debugVideoCaptureUpdate()
887
- {
888
- ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
889
-
890
- // save the video frame
891
- combineCanvases();
892
- debugVideoCapture.videoTrack.requestFrame();
893
- debugVideoCaptureIcon.textContent = '● REC '
894
- + formatTime(debugVideoCapture.captureTimer.get());
895
- }
896
-
897
756
  ///////////////////////////////////////////////////////////////////////////////
898
757
  // debug utility functions
899
758
 
@@ -913,7 +772,8 @@ function debugProtectConstant(obj)
913
772
  get: ()=> values[prop],
914
773
  set: (value)=>
915
774
  {
916
- ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
775
+ ASSERT(false, `engine constants like RED can not be changed, change a copy made with copy(); ` +
776
+ `tried to set ${prop} of ${obj} to ${value}`);
917
777
  },
918
778
  enumerable: true
919
779
  });