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.
- package/CHANGELOG.md +99 -0
- package/FAQ.md +49 -11
- package/README.md +6 -2
- package/REFERENCE.md +129 -21
- package/dist/littlejs.d.ts +487 -102
- package/dist/littlejs.esm.js +2898 -719
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2888 -716
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2702 -527
- package/package.json +4 -4
- package/plugins/box2d.js +110 -34
- package/plugins/gltf.js +74 -22
- package/plugins/lightSystem.js +410 -28
- package/plugins/math3d.js +4 -2
- package/plugins/medalSystem.js +19 -7
- package/plugins/newgrounds.js +4 -2
- package/plugins/parallax.js +1 -0
- package/plugins/particleEffects.js +44 -10
- package/plugins/pathFinder.js +9 -11
- package/plugins/pluginExport.js +9 -0
- package/plugins/postProcess.js +9 -3
- package/plugins/render3d.js +499 -2389
- package/plugins/render3dDebug.js +5 -8
- package/plugins/render3dEditor.js +81 -6
- package/plugins/render3dExtras.js +103 -31
- package/plugins/render3dLevel.js +53 -5
- package/plugins/render3dMesh.js +1158 -0
- package/plugins/render3dObject.js +1451 -0
- package/plugins/render3dVoxels.js +15 -3
- package/plugins/textureSheet.js +9 -8
- package/plugins/threejs.js +1 -1
- package/plugins/tweenSystem.js +76 -51
- package/plugins/uiSystem.js +37 -8
- package/plugins/wavedash.js +229 -0
- package/src/engine.js +199 -40
- package/src/engineAudio.js +37 -21
- package/src/engineBuild.mjs +6 -1
- package/src/engineDebug.js +22 -162
- package/src/engineDraw.js +142 -34
- package/src/engineEditor.js +79 -18
- package/src/engineExport.js +1 -3
- package/src/engineInput.js +34 -24
- package/src/engineMath.js +35 -14
- package/src/engineObject.js +60 -41
- package/src/engineParticles.js +58 -11
- package/src/engineRelease.js +1 -5
- package/src/engineSettings.js +48 -9
- package/src/engineTileLayer.js +11 -4
- package/src/engineUtilities.js +41 -9
- package/src/engineWebGL.js +37 -13
package/src/engineAudio.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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] -
|
|
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
|
-
|
|
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
|
|
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
|
|
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(`
|
|
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
|
-
|
|
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
|
|
924
|
-
* @param {number} [deltaSlide] - How much to change slide
|
|
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
|
|
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
|
package/src/engineBuild.mjs
CHANGED
|
@@ -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
|
-
|
|
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');
|
package/src/engineDebug.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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, `
|
|
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
|
});
|