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.
- package/dist/littlejs.d.ts +1306 -66
- package/dist/littlejs.esm.js +3097 -269
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +253 -265
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +155 -260
- package/examples/box2d/game.js +7 -10
- package/examples/box2d/gameObjects.js +33 -33
- package/examples/box2d/index.html +6 -6
- package/examples/box2d/scenes.js +28 -28
- package/examples/breakout/game.js +1 -4
- package/examples/breakout/index.html +4 -4
- package/examples/breakoutTutorial/index.html +2 -2
- package/examples/htmlMenu/game.js +3 -6
- package/examples/htmlMenu/index.html +2 -2
- package/examples/module/index.html +1 -1
- package/examples/particles/index.html +1 -1
- package/examples/platformer/game.js +0 -3
- package/examples/platformer/index.html +8 -8
- package/examples/puzzle/game.js +0 -3
- package/examples/puzzle/index.html +2 -2
- package/examples/shorts/base.html +1 -1
- package/examples/starter/build.js +4 -4
- package/examples/starter/index.html +13 -13
- package/examples/stress/index.html +3 -2
- package/examples/uiSystem/game.js +1 -7
- package/examples/uiSystem/index.html +3 -3
- package/package.json +3 -3
- package/plugins/box2d.js +1552 -640
- package/plugins/{Box2D_v2.3.1_min.wasm.js → box2d.wasm.js} +1 -1
- package/plugins/newgrounds.js +7 -8
- package/plugins/pluginExport.js +51 -0
- package/plugins/postProcess.js +94 -93
- package/plugins/uiSystem.js +145 -161
- package/plugins/zzfxm.js +163 -0
- package/src/engine.js +15 -20
- package/src/engineAudio.js +74 -204
- package/src/engineBuild.js +21 -3
- package/src/engineDebug.js +104 -6
- package/src/engineDraw.js +27 -14
- package/src/engineExport.js +12 -4
- package/src/engineParticles.js +6 -1
- package/src/engineRelease.js +6 -1
- package/src/engineSettings.js +2 -2
- package/src/engineUtilities.js +5 -11
- package/src/engineWebGL.js +19 -7
- package/examples/starter/build/index.html +0 -2
- package/examples/starter/build/index.js +0 -1
- package/examples/starter/build/tiles.png +0 -0
- package/examples/starter/game.zip +0 -0
- /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.
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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 ?
|
|
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
|
|
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;
|
|
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'
|
|
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
|
);
|
package/src/engineAudio.js
CHANGED
|
@@ -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
|
|
22
|
+
let audioMasterGain;
|
|
23
23
|
|
|
24
24
|
function audioInit()
|
|
25
25
|
{
|
|
26
26
|
if (!soundEnable || headlessMode) return;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
370
|
+
let sampleRate = zzfxR,
|
|
371
|
+
PI2 = PI*2,
|
|
408
372
|
startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
|
|
409
373
|
startFrequency = frequency *=
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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)
|
|
409
|
+
i < length; b[i++] = s * volume) // sample
|
|
436
410
|
{
|
|
437
|
-
if (!(++
|
|
411
|
+
if (!(++crush%(bitCrush*100|0))) // bit crush
|
|
438
412
|
{
|
|
439
|
-
s = shape? shape>1? shape>2? shape>3?
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
1-
|
|
444
|
-
Math.
|
|
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)*
|
|
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)
|
|
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*
|
|
444
|
+
Math.cos(modulation*modOffset++); // modulation
|
|
470
445
|
t += f + f*noise*Math.sin(i**5); // noise
|
|
471
446
|
|
|
472
|
-
if (
|
|
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
|
-
|
|
451
|
+
jump = 0; // stop pitch jump time
|
|
477
452
|
}
|
|
478
453
|
|
|
479
|
-
if (repeatTime && !(++
|
|
454
|
+
if (repeatTime && !(++repeat % repeatTime)) // repeat
|
|
480
455
|
{
|
|
481
|
-
frequency = startFrequency;
|
|
482
|
-
slide = startSlide;
|
|
483
|
-
|
|
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
|
-
}
|
package/src/engineBuild.js
CHANGED
|
@@ -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
|
-
[
|
|
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
|
-
[
|
|
91
|
-
|
|
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!`);
|
package/src/engineDebug.js
CHANGED
|
@@ -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
|
|
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
|
}
|