littlejsengine 1.11.10 → 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 +1311 -81
- package/dist/littlejs.esm.js +3135 -299
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +291 -293
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +193 -288
- 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 +14 -7
- package/examples/htmlMenu/index.html +3 -12
- 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 +15 -5
- 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 +13 -9
- 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 -6
- package/src/engineInput.js +10 -5
- package/src/engineMedals.js +20 -10
- package/src/engineObject.js +3 -1
- package/src/engineParticles.js +6 -1
- package/src/engineRelease.js +6 -1
- package/src/engineSettings.js +7 -14
- package/src/engineUtilities.js +5 -11
- package/src/engineWebGL.js +19 -7
- /package/plugins/{Box2D_v2.3.1_min.wasm.wasm → box2d.wasm.wasm} +0 -0
package/dist/littlejs.js
CHANGED
|
@@ -77,8 +77,10 @@ function ASSERT(assert, output)
|
|
|
77
77
|
* @memberof Debug */
|
|
78
78
|
function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
|
|
79
79
|
{
|
|
80
|
+
if (typeof size == 'number')
|
|
81
|
+
size = vec2(size); // allow passing in floats
|
|
80
82
|
ASSERT(typeof color == 'string', 'pass in css color strings');
|
|
81
|
-
debugPrimitives.push({pos, size
|
|
83
|
+
debugPrimitives.push({pos, size, color, time:new Timer(time), angle, fill});
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/** Draw a debug poly in world space
|
|
@@ -202,17 +204,17 @@ function debugSaveDataURL(dataURL, filename)
|
|
|
202
204
|
* @memberof Debug */
|
|
203
205
|
function debugShowErrors()
|
|
204
206
|
{
|
|
205
|
-
onunhandledrejection = (event)=>showError(event.reason);
|
|
206
|
-
onerror = (event, source, lineno, colno)=>
|
|
207
|
-
showError(`${event}\n${source}\nLn ${lineno}, Col ${colno}`);
|
|
208
|
-
|
|
209
207
|
const showError = (message)=>
|
|
210
208
|
{
|
|
211
209
|
// replace entire page with error message
|
|
212
210
|
document.body.style.display = '';
|
|
213
211
|
document.body.style.backgroundColor = '#111';
|
|
214
|
-
document.body.innerHTML = `<pre style=color:#f00;font-size:50px>` + message;
|
|
212
|
+
document.body.innerHTML = `<pre style=color:#f00;font-size:50px;white-space:pre-wrap>` + message;
|
|
215
213
|
}
|
|
214
|
+
onunhandledrejection = (event)=>
|
|
215
|
+
showError(event.reason.stack || event.reason);
|
|
216
|
+
onerror = (message, source, lineno, colno)=>
|
|
217
|
+
showError(`${message}\n${source}\nLn ${lineno}, Col ${colno}`);
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -245,11 +247,16 @@ function debugUpdate()
|
|
|
245
247
|
debugRaycast = !debugRaycast;
|
|
246
248
|
if (keyWasPressed('Digit5'))
|
|
247
249
|
debugScreenshot();
|
|
250
|
+
if (keyWasPressed('Digit6'))
|
|
251
|
+
debugVideoCaptureIsActive() ? debugVideoCaptureStop() : debugVideoCaptureStart();
|
|
248
252
|
}
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
function debugRender()
|
|
252
256
|
{
|
|
257
|
+
if (debugVideoCaptureIsActive())
|
|
258
|
+
return; // don't show debug info when capturing video
|
|
259
|
+
|
|
253
260
|
glCopyToContext(mainContext);
|
|
254
261
|
|
|
255
262
|
if (debugTakeScreenshot)
|
|
@@ -444,6 +451,7 @@ function debugRender()
|
|
|
444
451
|
overlayContext.fillText('4: Debug Raycasts', x, y += h);
|
|
445
452
|
overlayContext.fillStyle = '#fff';
|
|
446
453
|
overlayContext.fillText('5: Save Screenshot', x, y += h);
|
|
454
|
+
overlayContext.fillText('6: Capture Video', x, y += h);
|
|
447
455
|
|
|
448
456
|
let keysPressed = '';
|
|
449
457
|
for(const i in inputData[0])
|
|
@@ -472,6 +480,96 @@ function debugRender()
|
|
|
472
480
|
|
|
473
481
|
overlayContext.restore();
|
|
474
482
|
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
486
|
+
// video capture - records video and audio at 60 fps using MediaRecorder API
|
|
487
|
+
|
|
488
|
+
// internal variables used to capture video
|
|
489
|
+
let debugVideoCapture, debugVideoCaptureTrack, debugVideoCaptureIcon, debugVideoCaptureTimer;
|
|
490
|
+
|
|
491
|
+
/** Check if video capture is active
|
|
492
|
+
* @memberof Debug */
|
|
493
|
+
function debugVideoCaptureIsActive() { return !!debugVideoCapture; }
|
|
494
|
+
|
|
495
|
+
/** Start capturing video
|
|
496
|
+
* @memberof Debug */
|
|
497
|
+
function debugVideoCaptureStart()
|
|
498
|
+
{
|
|
499
|
+
if (debugVideoCaptureIsActive())
|
|
500
|
+
return; // already recording
|
|
501
|
+
|
|
502
|
+
// captureStream passing in 0 to only capture when requestFrame() is called
|
|
503
|
+
const stream = mainCanvas.captureStream(0);
|
|
504
|
+
const chunks = [];
|
|
505
|
+
debugVideoCaptureTrack = stream.getVideoTracks()[0];
|
|
506
|
+
if (debugVideoCaptureTrack.applyConstraints)
|
|
507
|
+
debugVideoCaptureTrack.applyConstraints({frameRate:frameRate}); // force 60 fps
|
|
508
|
+
debugVideoCapture = new MediaRecorder(stream, {mimeType:'video/webm;codecs=vp8'});
|
|
509
|
+
debugVideoCapture.ondataavailable = (e)=> chunks.push(e.data);
|
|
510
|
+
debugVideoCapture.onstop = ()=>
|
|
511
|
+
{
|
|
512
|
+
const blob = new Blob(chunks, {type: 'video/webm'});
|
|
513
|
+
const url = URL.createObjectURL(blob);
|
|
514
|
+
downloadLink.download = 'capture.webm';
|
|
515
|
+
downloadLink.href = url;
|
|
516
|
+
downloadLink.click();
|
|
517
|
+
URL.revokeObjectURL(url);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
if (audioMasterGain)
|
|
521
|
+
{
|
|
522
|
+
// connect to audio master gain node
|
|
523
|
+
const audioStreamDestination = audioContext.createMediaStreamDestination();
|
|
524
|
+
audioMasterGain.connect(audioStreamDestination);
|
|
525
|
+
for (const track of audioStreamDestination.stream.getAudioTracks())
|
|
526
|
+
stream.addTrack(track); // add audio tracks to capture stream
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// start recording
|
|
530
|
+
console.log('Video capture started.');
|
|
531
|
+
debugVideoCapture.start();
|
|
532
|
+
debugVideoCaptureTimer = new Timer(0);
|
|
533
|
+
|
|
534
|
+
if (!debugVideoCaptureIcon)
|
|
535
|
+
{
|
|
536
|
+
// create recording icon to show it is capturing video
|
|
537
|
+
debugVideoCaptureIcon = document.createElement('div');
|
|
538
|
+
debugVideoCaptureIcon.style.position = 'absolute';
|
|
539
|
+
debugVideoCaptureIcon.style.padding = '9px';
|
|
540
|
+
debugVideoCaptureIcon.style.color = '#f00';
|
|
541
|
+
debugVideoCaptureIcon.style.font = '50px monospace';
|
|
542
|
+
document.body.appendChild(debugVideoCaptureIcon);
|
|
543
|
+
}
|
|
544
|
+
// show recording icon
|
|
545
|
+
debugVideoCaptureIcon.textContent = '';
|
|
546
|
+
debugVideoCaptureIcon.style.display = '';
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Stop capturing video and save to disk
|
|
550
|
+
* @memberof Debug */
|
|
551
|
+
function debugVideoCaptureStop()
|
|
552
|
+
{
|
|
553
|
+
if (!debugVideoCaptureIsActive())
|
|
554
|
+
return; // not recording
|
|
555
|
+
|
|
556
|
+
// stop recording
|
|
557
|
+
console.log(`Video capture ended. ${debugVideoCaptureTimer.get().toFixed(2)} seconds recorded.`);
|
|
558
|
+
debugVideoCapture.stop();
|
|
559
|
+
debugVideoCapture = 0;
|
|
560
|
+
debugVideoCaptureIcon.style.display = 'none';
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// update video capture, called automatically by engine
|
|
564
|
+
function debugVideoCaptureUpdate()
|
|
565
|
+
{
|
|
566
|
+
if (!debugVideoCaptureIsActive())
|
|
567
|
+
return; // not recording
|
|
568
|
+
|
|
569
|
+
// save the video frame
|
|
570
|
+
combineCanvases();
|
|
571
|
+
debugVideoCaptureTrack.requestFrame();
|
|
572
|
+
debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
|
|
475
573
|
}
|
|
476
574
|
/**
|
|
477
575
|
* LittleJS Utility Classes and Functions
|
|
@@ -566,7 +664,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
|
|
|
566
664
|
* @returns {number}
|
|
567
665
|
* @memberof Utilities */
|
|
568
666
|
function lerpWrap(percent, valueA, valueB, wrapSize=1)
|
|
569
|
-
{ return
|
|
667
|
+
{ return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
|
|
570
668
|
|
|
571
669
|
/** Returns signed wrapped distance between the two angles passed in
|
|
572
670
|
* @param {number} angleA
|
|
@@ -764,23 +862,17 @@ class RandomGenerator
|
|
|
764
862
|
///////////////////////////////////////////////////////////////////////////////
|
|
765
863
|
|
|
766
864
|
/**
|
|
767
|
-
* Create a 2d vector, can take
|
|
768
|
-
* @param {
|
|
769
|
-
* @param {number} [y]
|
|
865
|
+
* Create a 2d vector, can take 1 or 2 scalar values
|
|
866
|
+
* @param {number} [x]
|
|
867
|
+
* @param {number} [y] - if y is undefined, x is used for both
|
|
770
868
|
* @return {Vector2}
|
|
771
869
|
* @example
|
|
772
870
|
* let a = vec2(0, 1); // vector with coordinates (0, 1)
|
|
773
|
-
* let b = vec2(a); // copy a into b
|
|
774
871
|
* a = vec2(5); // set a to (5, 5)
|
|
775
872
|
* b = vec2(); // set b to (0, 0)
|
|
776
873
|
* @memberof Utilities
|
|
777
874
|
*/
|
|
778
|
-
function vec2(x=0, y)
|
|
779
|
-
{
|
|
780
|
-
return typeof x == 'number' ?
|
|
781
|
-
new Vector2(x, y == undefined? x : y) :
|
|
782
|
-
new Vector2(x.x, x.y);
|
|
783
|
-
}
|
|
875
|
+
function vec2(x=0, y) { return new Vector2(x, y == undefined? x : y); }
|
|
784
876
|
|
|
785
877
|
/**
|
|
786
878
|
* Check if object is a valid Vector2
|
|
@@ -1465,6 +1557,7 @@ let fontDefault = 'arial';
|
|
|
1465
1557
|
let showSplashScreen = false;
|
|
1466
1558
|
|
|
1467
1559
|
/** Disables all rendering, audio, and input for servers
|
|
1560
|
+
* - Must be set before startup to take effect
|
|
1468
1561
|
* @type {boolean}
|
|
1469
1562
|
* @default
|
|
1470
1563
|
* @memberof Settings */
|
|
@@ -1474,12 +1567,14 @@ let headlessMode = false;
|
|
|
1474
1567
|
// WebGL settings
|
|
1475
1568
|
|
|
1476
1569
|
/** Enable webgl rendering, webgl can be disabled and removed from build (with some features disabled)
|
|
1570
|
+
* - Must be set before startup to take effect
|
|
1477
1571
|
* @type {boolean}
|
|
1478
1572
|
* @default
|
|
1479
1573
|
* @memberof Settings */
|
|
1480
1574
|
let glEnable = true;
|
|
1481
1575
|
|
|
1482
1576
|
/** Fixes slow rendering in some browsers by not compositing the WebGL canvas
|
|
1577
|
+
* - Must be set before startup to take effect
|
|
1483
1578
|
* @type {boolean}
|
|
1484
1579
|
* @default
|
|
1485
1580
|
* @memberof Settings */
|
|
@@ -1580,6 +1675,7 @@ let inputWASDEmulateDirection = true;
|
|
|
1580
1675
|
|
|
1581
1676
|
/** True if touch input is enabled for mobile devices
|
|
1582
1677
|
* - Touch events will be routed to mouse events
|
|
1678
|
+
* - Must be set before startup to take effect
|
|
1583
1679
|
* @type {boolean}
|
|
1584
1680
|
* @default
|
|
1585
1681
|
* @memberof Settings */
|
|
@@ -1587,7 +1683,7 @@ let touchInputEnable = true;
|
|
|
1587
1683
|
|
|
1588
1684
|
/** True if touch gamepad should appear on mobile devices
|
|
1589
1685
|
* - Supports left analog stick, 4 face buttons and start button (button 9)
|
|
1590
|
-
* - Must be set
|
|
1686
|
+
* - Must be set before startup to take effect
|
|
1591
1687
|
* @type {boolean}
|
|
1592
1688
|
* @default
|
|
1593
1689
|
* @memberof Settings */
|
|
@@ -1665,12 +1761,6 @@ let medalDisplaySlideTime = .5;
|
|
|
1665
1761
|
* @memberof Settings */
|
|
1666
1762
|
let medalDisplaySize = vec2(640, 80);
|
|
1667
1763
|
|
|
1668
|
-
/** Size of icon in medal display
|
|
1669
|
-
* @type {number}
|
|
1670
|
-
* @default
|
|
1671
|
-
* @memberof Settings */
|
|
1672
|
-
let medalDisplayIconSize = 50;
|
|
1673
|
-
|
|
1674
1764
|
/** Set to stop medals from being unlockable (like if cheats are enabled)
|
|
1675
1765
|
* @type {boolean}
|
|
1676
1766
|
* @default
|
|
@@ -1846,8 +1936,8 @@ function setSoundEnable(enable) { soundEnable = enable; }
|
|
|
1846
1936
|
function setSoundVolume(volume)
|
|
1847
1937
|
{
|
|
1848
1938
|
soundVolume = volume;
|
|
1849
|
-
if (soundEnable && !headlessMode &&
|
|
1850
|
-
|
|
1939
|
+
if (soundEnable && !headlessMode && audioMasterGain)
|
|
1940
|
+
audioMasterGain.gain.value = volume; // update gain immediately
|
|
1851
1941
|
}
|
|
1852
1942
|
|
|
1853
1943
|
/** Set default range where sound no longer plays
|
|
@@ -1875,11 +1965,6 @@ function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
|
|
|
1875
1965
|
* @memberof Settings */
|
|
1876
1966
|
function setMedalDisplaySize(size) { medalDisplaySize = size; }
|
|
1877
1967
|
|
|
1878
|
-
/** Set size of icon in medal display
|
|
1879
|
-
* @param {number} size
|
|
1880
|
-
* @memberof Settings */
|
|
1881
|
-
function setMedalDisplayIconSize(size) { medalDisplayIconSize = size; }
|
|
1882
|
-
|
|
1883
1968
|
/** Set to stop medals from being unlockable
|
|
1884
1969
|
* @param {boolean} preventUnlock
|
|
1885
1970
|
* @memberof Settings */
|
|
@@ -2069,7 +2154,8 @@ class EngineObject
|
|
|
2069
2154
|
if (this.groundObject)
|
|
2070
2155
|
{
|
|
2071
2156
|
// apply friction in local space of ground object
|
|
2072
|
-
const groundSpeed = this.groundObject
|
|
2157
|
+
const groundSpeed = this.groundObject != this && this.groundObject.velocity ?
|
|
2158
|
+
this.groundObject.velocity.x : 0;
|
|
2073
2159
|
this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
|
|
2074
2160
|
this.groundObject = undefined;
|
|
2075
2161
|
//debugOverlay && debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
|
|
@@ -2196,6 +2282,7 @@ class EngineObject
|
|
|
2196
2282
|
this.pos.y = (oldPos.y-this.size.y/2|0)+this.size.y/2+epsilon;
|
|
2197
2283
|
|
|
2198
2284
|
// set ground object to self for tile collision
|
|
2285
|
+
// TODO: rework system so tile collision is its own object
|
|
2199
2286
|
this.groundObject = this;
|
|
2200
2287
|
}
|
|
2201
2288
|
else
|
|
@@ -2516,6 +2603,8 @@ class TextureInfo
|
|
|
2516
2603
|
this.image = image;
|
|
2517
2604
|
/** @property {Vector2} - size of the image */
|
|
2518
2605
|
this.size = vec2(image.width, image.height);
|
|
2606
|
+
/** @property {Vector2} - inverse of the size, cached for rendering */
|
|
2607
|
+
this.sizeInverse = vec2(1/image.width, 1/image.height);
|
|
2519
2608
|
/** @property {WebGLTexture} - webgl texture */
|
|
2520
2609
|
this.glTexture = glEnable && glCreateTexture(image);
|
|
2521
2610
|
}
|
|
@@ -2561,19 +2650,19 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
|
2561
2650
|
* @param {Color} [color=(1,1,1,1)] - Color to modulate with
|
|
2562
2651
|
* @param {number} [angle] - Angle to rotate by
|
|
2563
2652
|
* @param {boolean} [mirror] - If true image is flipped along the Y axis
|
|
2564
|
-
* @param {Color} [additiveColor
|
|
2653
|
+
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
2565
2654
|
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
|
|
2566
2655
|
* @param {boolean} [screenSpace=false] - If true the pos and size are in screen space
|
|
2567
2656
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
2568
2657
|
* @memberof Draw */
|
|
2569
2658
|
function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
|
|
2570
|
-
angle=0, mirror, additiveColor
|
|
2659
|
+
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
|
|
2571
2660
|
{
|
|
2572
2661
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
2573
2662
|
ASSERT(typeof tileInfo !== 'number' || !tileInfo,
|
|
2574
2663
|
'this is an old style calls, to fix replace it with tile(tileIndex, tileSize)');
|
|
2575
2664
|
ASSERT(isVector2(pos) && isVector2(size));
|
|
2576
|
-
ASSERT(isColor(color) && isColor(additiveColor));
|
|
2665
|
+
ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)));
|
|
2577
2666
|
|
|
2578
2667
|
const textureInfo = tileInfo && tileInfo.getTextureInfo();
|
|
2579
2668
|
if (useWebGL)
|
|
@@ -2584,21 +2673,30 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
|
|
|
2584
2673
|
pos = screenToWorld(pos);
|
|
2585
2674
|
size = size.scale(1/cameraScale);
|
|
2586
2675
|
}
|
|
2587
|
-
|
|
2588
2676
|
if (textureInfo)
|
|
2589
2677
|
{
|
|
2590
2678
|
// calculate uvs and render
|
|
2591
|
-
const sizeInverse =
|
|
2679
|
+
const sizeInverse = textureInfo.sizeInverse;
|
|
2592
2680
|
const x = tileInfo.pos.x * sizeInverse.x;
|
|
2593
2681
|
const y = tileInfo.pos.y * sizeInverse.y;
|
|
2594
2682
|
const w = tileInfo.size.x * sizeInverse.x;
|
|
2595
2683
|
const h = tileInfo.size.y * sizeInverse.y;
|
|
2596
|
-
const tileImageFixBleed = sizeInverse.scale(tileFixBleedScale);
|
|
2597
2684
|
glSetTexture(textureInfo.glTexture);
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2685
|
+
if (tileFixBleedScale)
|
|
2686
|
+
{
|
|
2687
|
+
const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
|
|
2688
|
+
const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
|
|
2689
|
+
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
2690
|
+
x + tileImageFixBleedX, y + tileImageFixBleedY,
|
|
2691
|
+
x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
|
|
2692
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
2693
|
+
}
|
|
2694
|
+
else
|
|
2695
|
+
{
|
|
2696
|
+
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
2697
|
+
x, y, x + w, y + h,
|
|
2698
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
2699
|
+
}
|
|
2602
2700
|
}
|
|
2603
2701
|
else
|
|
2604
2702
|
{
|
|
@@ -2810,16 +2908,15 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
2810
2908
|
function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
|
|
2811
2909
|
{
|
|
2812
2910
|
context.fillStyle = color.toString();
|
|
2813
|
-
context.lineWidth = lineWidth;
|
|
2814
2911
|
context.strokeStyle = lineColor.toString();
|
|
2912
|
+
context.lineWidth = lineWidth;
|
|
2815
2913
|
context.textAlign = textAlign;
|
|
2816
2914
|
context.font = size + 'px '+ font;
|
|
2817
2915
|
context.textBaseline = 'middle';
|
|
2818
2916
|
context.lineJoin = 'round';
|
|
2819
2917
|
|
|
2820
|
-
pos = pos.copy();
|
|
2821
|
-
|
|
2822
2918
|
const lines = (text+'').split('\n');
|
|
2919
|
+
pos = pos.copy();
|
|
2823
2920
|
pos.y -= (lines.length-1) * size/2; // center text vertically
|
|
2824
2921
|
lines.forEach(line=>
|
|
2825
2922
|
{
|
|
@@ -2889,7 +2986,10 @@ class FontImage
|
|
|
2889
2986
|
{
|
|
2890
2987
|
// load default font image
|
|
2891
2988
|
if (!engineFontImage)
|
|
2892
|
-
|
|
2989
|
+
{
|
|
2990
|
+
engineFontImage = new Image;
|
|
2991
|
+
engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
|
|
2992
|
+
}
|
|
2893
2993
|
|
|
2894
2994
|
this.image = image || engineFontImage;
|
|
2895
2995
|
this.tileSize = tileSize;
|
|
@@ -3075,10 +3175,16 @@ let mouseWheel = 0;
|
|
|
3075
3175
|
* @memberof Input */
|
|
3076
3176
|
let isUsingGamepad = false;
|
|
3077
3177
|
|
|
3078
|
-
/** Prevents input continuing to the default browser handling (
|
|
3178
|
+
/** Prevents input continuing to the default browser handling (true by default)
|
|
3079
3179
|
* @type {boolean}
|
|
3080
3180
|
* @memberof Input */
|
|
3081
|
-
let
|
|
3181
|
+
let inputPreventDefault = true;
|
|
3182
|
+
|
|
3183
|
+
/** Prevents input continuing to the default browser handling
|
|
3184
|
+
* This is useful to disable for html menus so the browser can handle input normally
|
|
3185
|
+
* @param {boolean} preventDefault
|
|
3186
|
+
* @memberof Input */
|
|
3187
|
+
function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
|
|
3082
3188
|
|
|
3083
3189
|
/** Returns true if gamepad button is down
|
|
3084
3190
|
* @param {number} button
|
|
@@ -3158,7 +3264,6 @@ function inputInit()
|
|
|
3158
3264
|
if (inputWASDEmulateDirection)
|
|
3159
3265
|
inputData[0][remapKey(e.code)] = 3;
|
|
3160
3266
|
}
|
|
3161
|
-
preventDefaultInput && e.preventDefault();
|
|
3162
3267
|
}
|
|
3163
3268
|
|
|
3164
3269
|
onkeyup = (e)=>
|
|
@@ -3188,7 +3293,7 @@ function inputInit()
|
|
|
3188
3293
|
isUsingGamepad = false;
|
|
3189
3294
|
inputData[0][e.button] = 3;
|
|
3190
3295
|
mousePosScreen = mouseEventToScreen(e);
|
|
3191
|
-
e.button && e.preventDefault();
|
|
3296
|
+
inputPreventDefault && e.button && e.preventDefault();
|
|
3192
3297
|
}
|
|
3193
3298
|
onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
|
|
3194
3299
|
onmousemove = (e)=> mousePosScreen = mouseEventToScreen(e);
|
|
@@ -3372,7 +3477,7 @@ function touchInputInit()
|
|
|
3372
3477
|
wasTouching = touching;
|
|
3373
3478
|
|
|
3374
3479
|
// prevent default handling like copy and magnifier lens
|
|
3375
|
-
if (document.hasFocus()) // allow document to get focus
|
|
3480
|
+
if (inputPreventDefault && document.hasFocus()) // allow document to get focus
|
|
3376
3481
|
e.preventDefault();
|
|
3377
3482
|
|
|
3378
3483
|
// must return true so the document will get focus
|
|
@@ -3515,16 +3620,15 @@ let audioContext = new AudioContext;
|
|
|
3515
3620
|
/** Master gain node for all audio to pass through
|
|
3516
3621
|
* @type {GainNode}
|
|
3517
3622
|
* @memberof Audio */
|
|
3518
|
-
let
|
|
3623
|
+
let audioMasterGain;
|
|
3519
3624
|
|
|
3520
3625
|
function audioInit()
|
|
3521
3626
|
{
|
|
3522
3627
|
if (!soundEnable || headlessMode) return;
|
|
3523
3628
|
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
audioGainNode.gain.value = soundVolume; // set starting value
|
|
3629
|
+
audioMasterGain = audioContext.createGain();
|
|
3630
|
+
audioMasterGain.connect(audioContext.destination);
|
|
3631
|
+
audioMasterGain.gain.value = soundVolume; // set starting value
|
|
3528
3632
|
}
|
|
3529
3633
|
|
|
3530
3634
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -3653,6 +3757,8 @@ class Sound
|
|
|
3653
3757
|
isLoading() { return !this.sampleChannels; }
|
|
3654
3758
|
}
|
|
3655
3759
|
|
|
3760
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3761
|
+
|
|
3656
3762
|
/**
|
|
3657
3763
|
* Sound Wave Object - Stores a wave sound for later use and can be played positionally
|
|
3658
3764
|
* - this can be used to play wave, mp3, and ogg files
|
|
@@ -3704,59 +3810,7 @@ function playAudioFile(filename, volume=1, loop=false)
|
|
|
3704
3810
|
return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
|
|
3705
3811
|
}
|
|
3706
3812
|
|
|
3707
|
-
|
|
3708
|
-
* Music Object - Stores a zzfx music track for later use
|
|
3709
|
-
*
|
|
3710
|
-
* <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
|
|
3711
|
-
* @example
|
|
3712
|
-
* // create some music
|
|
3713
|
-
* const music_example = new Music(
|
|
3714
|
-
* [
|
|
3715
|
-
* [ // instruments
|
|
3716
|
-
* [,0,400] // simple note
|
|
3717
|
-
* ],
|
|
3718
|
-
* [ // patterns
|
|
3719
|
-
* [ // pattern 1
|
|
3720
|
-
* [ // channel 0
|
|
3721
|
-
* 0, -1, // instrument 0, left speaker
|
|
3722
|
-
* 1, 0, 9, 1 // channel notes
|
|
3723
|
-
* ],
|
|
3724
|
-
* [ // channel 1
|
|
3725
|
-
* 0, 1, // instrument 0, right speaker
|
|
3726
|
-
* 0, 12, 17, -1 // channel notes
|
|
3727
|
-
* ]
|
|
3728
|
-
* ],
|
|
3729
|
-
* ],
|
|
3730
|
-
* [0, 0, 0, 0], // sequence, play pattern 0 four times
|
|
3731
|
-
* 90 // BPM
|
|
3732
|
-
* ]);
|
|
3733
|
-
*
|
|
3734
|
-
* // play the music
|
|
3735
|
-
* music_example.play();
|
|
3736
|
-
*/
|
|
3737
|
-
class Music extends Sound
|
|
3738
|
-
{
|
|
3739
|
-
/** Create a music object and cache the zzfx music samples for later use
|
|
3740
|
-
* @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters
|
|
3741
|
-
*/
|
|
3742
|
-
constructor(zzfxMusic)
|
|
3743
|
-
{
|
|
3744
|
-
super(undefined);
|
|
3745
|
-
|
|
3746
|
-
if (!soundEnable || headlessMode) return;
|
|
3747
|
-
this.randomness = 0;
|
|
3748
|
-
this.sampleChannels = zzfxM(...zzfxMusic);
|
|
3749
|
-
this.sampleRate = zzfxR;
|
|
3750
|
-
}
|
|
3751
|
-
|
|
3752
|
-
/** Play the music
|
|
3753
|
-
* @param {number} [volume=1] - How much to scale volume by
|
|
3754
|
-
* @param {boolean} [loop] - True if the music should loop
|
|
3755
|
-
* @return {AudioBufferSourceNode} - The audio source node
|
|
3756
|
-
*/
|
|
3757
|
-
playMusic(volume, loop=false)
|
|
3758
|
-
{ return super.play(undefined, volume, 1, 1, loop); }
|
|
3759
|
-
}
|
|
3813
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3760
3814
|
|
|
3761
3815
|
/** Speak text with passed in settings
|
|
3762
3816
|
* @param {string} text - The text to speak
|
|
@@ -3828,7 +3882,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
3828
3882
|
// create and connect gain node
|
|
3829
3883
|
gainNode = gainNode || audioContext.createGain();
|
|
3830
3884
|
gainNode.gain.value = volume;
|
|
3831
|
-
gainNode.connect(
|
|
3885
|
+
gainNode.connect(audioMasterGain);
|
|
3832
3886
|
|
|
3833
3887
|
// connect source to stereo panner and gain
|
|
3834
3888
|
const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
|
|
@@ -3848,7 +3902,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
3848
3902
|
}
|
|
3849
3903
|
|
|
3850
3904
|
///////////////////////////////////////////////////////////////////////////////
|
|
3851
|
-
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.
|
|
3905
|
+
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
|
|
3852
3906
|
|
|
3853
3907
|
/** Generate and play a ZzFX sound
|
|
3854
3908
|
*
|
|
@@ -3890,21 +3944,45 @@ const zzfxR = 44100;
|
|
|
3890
3944
|
*/
|
|
3891
3945
|
function zzfxG
|
|
3892
3946
|
(
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3947
|
+
volume = 1,
|
|
3948
|
+
randomness = .05,
|
|
3949
|
+
frequency = 220,
|
|
3950
|
+
attack = 0,
|
|
3951
|
+
sustain = 0,
|
|
3952
|
+
release = .1,
|
|
3953
|
+
shape = 0,
|
|
3954
|
+
shapeCurve = 1,
|
|
3955
|
+
slide = 0,
|
|
3956
|
+
deltaSlide = 0,
|
|
3957
|
+
pitchJump = 0,
|
|
3958
|
+
pitchJumpTime = 0,
|
|
3959
|
+
repeatTime = 0,
|
|
3960
|
+
noise = 0,
|
|
3961
|
+
modulation = 0,
|
|
3962
|
+
bitCrush = 0,
|
|
3963
|
+
delay = 0,
|
|
3964
|
+
sustainVolume = 1,
|
|
3965
|
+
decay = 0,
|
|
3966
|
+
tremolo = 0,
|
|
3967
|
+
filter = 0
|
|
3898
3968
|
)
|
|
3899
3969
|
{
|
|
3900
|
-
// LJS Note: ZZFX modded so randomness is handled by Sound class
|
|
3901
|
-
|
|
3902
3970
|
// init parameters
|
|
3903
|
-
let
|
|
3971
|
+
let sampleRate = zzfxR,
|
|
3972
|
+
PI2 = PI*2,
|
|
3904
3973
|
startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
|
|
3905
3974
|
startFrequency = frequency *=
|
|
3906
|
-
|
|
3907
|
-
|
|
3975
|
+
(1 + rand(randomness,-randomness)) * PI2 / sampleRate,
|
|
3976
|
+
modOffset = 0, // modulation offset
|
|
3977
|
+
repeat = 0, // repeat offset
|
|
3978
|
+
crush = 0, // bit crush offset
|
|
3979
|
+
jump = 1, // pitch jump timer
|
|
3980
|
+
length, // sample length
|
|
3981
|
+
b = [], // sample buffer
|
|
3982
|
+
t = 0, // sample time
|
|
3983
|
+
i = 0, // sample index
|
|
3984
|
+
s = 0, // sample value
|
|
3985
|
+
f, // wave frequency
|
|
3908
3986
|
|
|
3909
3987
|
// biquad LP/HP filter
|
|
3910
3988
|
quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
|
|
@@ -3914,35 +3992,37 @@ function zzfxG
|
|
|
3914
3992
|
b1 = -(sign(filter) + cos) / a0, b2 = b0,
|
|
3915
3993
|
x2 = 0, x1 = 0, y2 = 0, y1 = 0;
|
|
3916
3994
|
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3995
|
+
// scale by sample rate
|
|
3996
|
+
const minAttack = 9; // prevent pop if attack is 0
|
|
3997
|
+
attack = attack * sampleRate || minAttack;
|
|
3998
|
+
decay *= sampleRate;
|
|
3999
|
+
sustain *= sampleRate;
|
|
4000
|
+
release *= sampleRate;
|
|
4001
|
+
delay *= sampleRate;
|
|
4002
|
+
deltaSlide *= 500 * PI2 / sampleRate**3;
|
|
4003
|
+
modulation *= PI2 / sampleRate;
|
|
4004
|
+
pitchJump *= PI2 / sampleRate;
|
|
4005
|
+
pitchJumpTime *= sampleRate;
|
|
4006
|
+
repeatTime = repeatTime * sampleRate | 0;
|
|
3928
4007
|
|
|
3929
4008
|
// generate waveform
|
|
3930
4009
|
for(length = attack + decay + sustain + release + delay | 0;
|
|
3931
|
-
i < length; b[i++] = s * volume)
|
|
4010
|
+
i < length; b[i++] = s * volume) // sample
|
|
3932
4011
|
{
|
|
3933
|
-
if (!(++
|
|
4012
|
+
if (!(++crush%(bitCrush*100|0))) // bit crush
|
|
3934
4013
|
{
|
|
3935
|
-
s = shape? shape>1? shape>2? shape>3?
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
1-
|
|
3940
|
-
Math.
|
|
4014
|
+
s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
|
|
4015
|
+
(t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
|
|
4016
|
+
Math.sin(t**3) : // 4 noise
|
|
4017
|
+
Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
|
|
4018
|
+
1-(2*t/PI2%2+2)%2: // 2 saw
|
|
4019
|
+
1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
|
|
4020
|
+
Math.sin(t); // 0 sin
|
|
3941
4021
|
|
|
3942
4022
|
s = (repeatTime ?
|
|
3943
4023
|
1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
|
|
3944
4024
|
: 1) *
|
|
3945
|
-
sign(s)*
|
|
4025
|
+
(shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
|
|
3946
4026
|
(i < attack ? i/attack : // attack
|
|
3947
4027
|
i < attack + decay ? // decay
|
|
3948
4028
|
1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
|
|
@@ -3957,136 +4037,32 @@ function zzfxG
|
|
|
3957
4037
|
(i<length-delay? 1 : (length-i)/delay) * // release delay
|
|
3958
4038
|
b[i-delay|0]/2/volume) : s; // sample delay
|
|
3959
4039
|
|
|
3960
|
-
if (filter)
|
|
4040
|
+
if (filter) // apply filter
|
|
3961
4041
|
s = y1 = b2*x2 + b1*(x2=x1) + b0*(x1=s) - a2*y2 - a1*(y2=y1);
|
|
3962
4042
|
}
|
|
3963
4043
|
|
|
3964
4044
|
f = (frequency += slide += deltaSlide) *// frequency
|
|
3965
|
-
Math.cos(modulation*
|
|
4045
|
+
Math.cos(modulation*modOffset++); // modulation
|
|
3966
4046
|
t += f + f*noise*Math.sin(i**5); // noise
|
|
3967
4047
|
|
|
3968
|
-
if (
|
|
4048
|
+
if (jump && ++jump > pitchJumpTime) // pitch jump
|
|
3969
4049
|
{
|
|
3970
4050
|
frequency += pitchJump; // apply pitch jump
|
|
3971
4051
|
startFrequency += pitchJump; // also apply to start
|
|
3972
|
-
|
|
4052
|
+
jump = 0; // stop pitch jump time
|
|
3973
4053
|
}
|
|
3974
4054
|
|
|
3975
|
-
if (repeatTime && !(++
|
|
4055
|
+
if (repeatTime && !(++repeat % repeatTime)) // repeat
|
|
3976
4056
|
{
|
|
3977
|
-
frequency = startFrequency;
|
|
3978
|
-
slide = startSlide;
|
|
3979
|
-
|
|
4057
|
+
frequency = startFrequency; // reset frequency
|
|
4058
|
+
slide = startSlide; // reset slide
|
|
4059
|
+
jump ||= 1; // reset pitch jump time
|
|
3980
4060
|
}
|
|
3981
4061
|
}
|
|
3982
4062
|
|
|
3983
|
-
return b;
|
|
4063
|
+
return b; // return sample buffer
|
|
3984
4064
|
}
|
|
3985
|
-
|
|
3986
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
3987
|
-
// ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
|
|
3988
|
-
|
|
3989
|
-
/** Generate samples for a ZzFM song with given parameters
|
|
3990
|
-
* @param {Array} instruments - Array of ZzFX sound parameters
|
|
3991
|
-
* @param {Array} patterns - Array of pattern data
|
|
3992
|
-
* @param {Array} sequence - Array of pattern indexes
|
|
3993
|
-
* @param {number} [BPM] - Playback speed of the song in BPM
|
|
3994
|
-
* @return {Array} - Left and right channel sample data
|
|
3995
|
-
* @memberof Audio */
|
|
3996
|
-
function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
3997
|
-
{
|
|
3998
|
-
let i, j, k;
|
|
3999
|
-
let instrumentParameters;
|
|
4000
|
-
let note;
|
|
4001
|
-
let sample;
|
|
4002
|
-
let patternChannel;
|
|
4003
|
-
let notFirstBeat;
|
|
4004
|
-
let stop;
|
|
4005
|
-
let instrument;
|
|
4006
|
-
let attenuation;
|
|
4007
|
-
let outSampleOffset;
|
|
4008
|
-
let isSequenceEnd;
|
|
4009
|
-
let sampleOffset = 0;
|
|
4010
|
-
let nextSampleOffset;
|
|
4011
|
-
let sampleBuffer = [];
|
|
4012
|
-
let leftChannelBuffer = [];
|
|
4013
|
-
let rightChannelBuffer = [];
|
|
4014
|
-
let channelIndex = 0;
|
|
4015
|
-
let panning = 0;
|
|
4016
|
-
let hasMore = 1;
|
|
4017
|
-
let sampleCache = {};
|
|
4018
|
-
let beatLength = zzfxR / BPM * 60 >> 2;
|
|
4019
|
-
|
|
4020
|
-
// for each channel in order until there are no more
|
|
4021
|
-
for (; hasMore; channelIndex++) {
|
|
4022
|
-
|
|
4023
|
-
// reset current values
|
|
4024
|
-
sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
|
|
4025
|
-
|
|
4026
|
-
// for each pattern in sequence
|
|
4027
|
-
sequence.forEach((patternIndex, sequenceIndex) => {
|
|
4028
|
-
// get pattern for current channel, use empty 1 note pattern if none found
|
|
4029
|
-
patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
|
|
4030
|
-
|
|
4031
|
-
// check if there are more channels
|
|
4032
|
-
hasMore |= patterns[patternIndex][channelIndex]&&1;
|
|
4033
|
-
|
|
4034
|
-
// get next offset, use the length of first channel
|
|
4035
|
-
nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
|
|
4036
|
-
// for each beat in pattern, plus one extra if end of sequence
|
|
4037
|
-
isSequenceEnd = sequenceIndex == sequence.length - 1;
|
|
4038
|
-
for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
|
|
4039
|
-
|
|
4040
|
-
// <channel-note>
|
|
4041
|
-
note = patternChannel[i];
|
|
4042
|
-
|
|
4043
|
-
// stop if end, different instrument or new note
|
|
4044
|
-
stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
|
|
4045
|
-
instrument != (patternChannel[0] || 0) || note | 0;
|
|
4046
|
-
|
|
4047
|
-
// fill buffer with samples for previous beat, most cpu intensive part
|
|
4048
|
-
for (j = 0; j < beatLength && notFirstBeat;
|
|
4049
|
-
|
|
4050
|
-
// fade off attenuation at end of beat if stopping note, prevents clicking
|
|
4051
|
-
j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
|
|
4052
|
-
) {
|
|
4053
|
-
// copy sample to stereo buffers with panning
|
|
4054
|
-
sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
|
|
4055
|
-
leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
|
|
4056
|
-
rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
|
|
4057
|
-
}
|
|
4058
|
-
|
|
4059
|
-
// set up for next note
|
|
4060
|
-
if (note) {
|
|
4061
|
-
// set attenuation
|
|
4062
|
-
attenuation = note % 1;
|
|
4063
|
-
panning = patternChannel[1] || 0;
|
|
4064
|
-
if (note |= 0) {
|
|
4065
|
-
// get cached sample
|
|
4066
|
-
sampleBuffer = sampleCache[
|
|
4067
|
-
[
|
|
4068
|
-
instrument = patternChannel[sampleOffset = 0] || 0,
|
|
4069
|
-
note
|
|
4070
|
-
]
|
|
4071
|
-
] = sampleCache[[instrument, note]] || (
|
|
4072
|
-
// add sample to cache
|
|
4073
|
-
instrumentParameters = [...instruments[instrument]],
|
|
4074
|
-
instrumentParameters[2] *= 2 ** ((note - 12) / 12),
|
|
4075
|
-
|
|
4076
|
-
// allow negative values to stop notes
|
|
4077
|
-
note > 0 ? zzfxG(...instrumentParameters) : []
|
|
4078
|
-
);
|
|
4079
|
-
}
|
|
4080
|
-
}
|
|
4081
|
-
}
|
|
4082
|
-
|
|
4083
|
-
// update the sample offset
|
|
4084
|
-
outSampleOffset = nextSampleOffset;
|
|
4085
|
-
});
|
|
4086
|
-
}
|
|
4087
|
-
|
|
4088
|
-
return [leftChannelBuffer, rightChannelBuffer];
|
|
4089
|
-
}
|
|
4065
|
+
|
|
4090
4066
|
/**
|
|
4091
4067
|
* LittleJS Tile Layer System
|
|
4092
4068
|
* - Caches arrays of tiles to off screen canvas for fast rendering
|
|
@@ -4643,7 +4619,12 @@ class ParticleEmitter extends EngineObject
|
|
|
4643
4619
|
else
|
|
4644
4620
|
this.destroy();
|
|
4645
4621
|
|
|
4646
|
-
|
|
4622
|
+
if (debugParticles)
|
|
4623
|
+
{
|
|
4624
|
+
// show emitter bounds
|
|
4625
|
+
const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
|
|
4626
|
+
debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
|
|
4627
|
+
}
|
|
4647
4628
|
}
|
|
4648
4629
|
|
|
4649
4630
|
/** Spawn one particle
|
|
@@ -4948,8 +4929,9 @@ class Medal
|
|
|
4948
4929
|
{
|
|
4949
4930
|
const context = overlayContext;
|
|
4950
4931
|
const width = min(medalDisplaySize.x, mainCanvas.width);
|
|
4932
|
+
const height = medalDisplaySize.y;
|
|
4951
4933
|
const x = overlayCanvas.width - width;
|
|
4952
|
-
const y = -
|
|
4934
|
+
const y = -height*hidePercent;
|
|
4953
4935
|
|
|
4954
4936
|
// draw containing rect and clip to that region
|
|
4955
4937
|
context.save();
|
|
@@ -4957,25 +4939,34 @@ class Medal
|
|
|
4957
4939
|
context.fillStyle = new Color(.9,.9,.9).toString();
|
|
4958
4940
|
context.strokeStyle = new Color(0,0,0).toString();
|
|
4959
4941
|
context.lineWidth = 3;
|
|
4960
|
-
context.rect(x, y, width,
|
|
4942
|
+
context.rect(x, y, width, height);
|
|
4961
4943
|
context.fill();
|
|
4962
4944
|
context.stroke();
|
|
4963
4945
|
context.clip();
|
|
4964
4946
|
|
|
4965
|
-
// draw the icon
|
|
4966
|
-
|
|
4967
|
-
const
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4947
|
+
// draw the icon
|
|
4948
|
+
const gap = vec2(.1, .05).scale(height);
|
|
4949
|
+
const medalDisplayIconSize = height - 2*gap.x;
|
|
4950
|
+
this.renderIcon(vec2(x + gap.x + medalDisplayIconSize/2, y + height/2), medalDisplayIconSize);
|
|
4951
|
+
|
|
4952
|
+
// draw the name
|
|
4953
|
+
const nameSize = height*.5;
|
|
4954
|
+
const descriptionSize = height*.3;
|
|
4955
|
+
const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
|
|
4956
|
+
const textWidth = width - medalDisplayIconSize - 3*gap.x;
|
|
4957
|
+
drawTextScreen(this.name, pos, nameSize, new Color(0,0,0), 0, undefined, 'left', undefined, textWidth);
|
|
4958
|
+
|
|
4959
|
+
// draw the description
|
|
4960
|
+
pos.y = y + height - gap.y*2 - descriptionSize/2;
|
|
4961
|
+
drawTextScreen(this.description, pos, descriptionSize, new Color(0,0,0), 0, undefined, 'left', undefined, textWidth);
|
|
4971
4962
|
context.restore();
|
|
4972
4963
|
}
|
|
4973
4964
|
|
|
4974
4965
|
/** Render the icon for a medal
|
|
4975
4966
|
* @param {Vector2} pos - Screen space position
|
|
4976
|
-
* @param {Number}
|
|
4967
|
+
* @param {Number} size - Screen space size
|
|
4977
4968
|
*/
|
|
4978
|
-
renderIcon(pos, size
|
|
4969
|
+
renderIcon(pos, size)
|
|
4979
4970
|
{
|
|
4980
4971
|
// draw the image or icon
|
|
4981
4972
|
if (this.image)
|
|
@@ -5095,12 +5086,12 @@ function glPreRender()
|
|
|
5095
5086
|
|
|
5096
5087
|
// set vertex attributes
|
|
5097
5088
|
let offset = glAdditive = glBatchAdditive = 0;
|
|
5098
|
-
|
|
5089
|
+
const initVertexAttribArray = (name, type, typeSize, size)=>
|
|
5099
5090
|
{
|
|
5100
5091
|
const location = glContext.getAttribLocation(glShader, name);
|
|
5101
5092
|
const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
|
|
5102
5093
|
const divisor = typeSize && 1; // only if not geometry
|
|
5103
|
-
const normalize = typeSize==1; // only if color
|
|
5094
|
+
const normalize = typeSize == 1; // only if color
|
|
5104
5095
|
glContext.enableVertexAttribArray(location);
|
|
5105
5096
|
glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
|
|
5106
5097
|
glContext.vertexAttribDivisor(location, divisor);
|
|
@@ -5199,14 +5190,14 @@ function glCreateTexture(image)
|
|
|
5199
5190
|
const texture = glContext.createTexture();
|
|
5200
5191
|
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
5201
5192
|
if (image && image.width)
|
|
5202
|
-
|
|
5193
|
+
glSetTextureData(texture, image);
|
|
5203
5194
|
else
|
|
5204
5195
|
{
|
|
5205
5196
|
// create a white texture
|
|
5206
5197
|
const whitePixel = new Uint8Array([255, 255, 255, 255]);
|
|
5207
5198
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
|
|
5208
5199
|
}
|
|
5209
|
-
|
|
5200
|
+
|
|
5210
5201
|
// use point filtering for pixelated rendering
|
|
5211
5202
|
const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
|
|
5212
5203
|
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
|
|
@@ -5214,6 +5205,18 @@ function glCreateTexture(image)
|
|
|
5214
5205
|
return texture;
|
|
5215
5206
|
}
|
|
5216
5207
|
|
|
5208
|
+
/** Set WebGL texture data from an image
|
|
5209
|
+
* @param {WebGLTexture} texture
|
|
5210
|
+
* @param {HTMLImageElement} image
|
|
5211
|
+
* @memberof WebGL */
|
|
5212
|
+
function glSetTextureData(texture, image)
|
|
5213
|
+
{
|
|
5214
|
+
// build the texture
|
|
5215
|
+
ASSERT(!!image && image.width > 0, 'Invalid image data.');
|
|
5216
|
+
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
5217
|
+
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
|
|
5218
|
+
}
|
|
5219
|
+
|
|
5217
5220
|
/** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
|
|
5218
5221
|
* @memberof WebGL */
|
|
5219
5222
|
function glFlush()
|
|
@@ -5267,10 +5270,10 @@ function glSetAntialias(antialias=true)
|
|
|
5267
5270
|
* @param {Number} uv0Y
|
|
5268
5271
|
* @param {Number} uv1X
|
|
5269
5272
|
* @param {Number} uv1Y
|
|
5270
|
-
* @param {Number} rgba
|
|
5271
|
-
* @param {Number} [rgbaAdditive=0]
|
|
5273
|
+
* @param {Number} [rgba=-1] - white is -1
|
|
5274
|
+
* @param {Number} [rgbaAdditive=0] - black is 0
|
|
5272
5275
|
* @memberof WebGL */
|
|
5273
|
-
function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
|
|
5276
|
+
function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=-1, rgbaAdditive=0)
|
|
5274
5277
|
{
|
|
5275
5278
|
ASSERT(typeof rgba == 'number' && typeof rgbaAdditive == 'number', 'invalid color');
|
|
5276
5279
|
|
|
@@ -5323,7 +5326,7 @@ const engineName = 'LittleJS';
|
|
|
5323
5326
|
* @type {string}
|
|
5324
5327
|
* @default
|
|
5325
5328
|
* @memberof Engine */
|
|
5326
|
-
const engineVersion = '1.11.
|
|
5329
|
+
const engineVersion = '1.11.17';
|
|
5327
5330
|
|
|
5328
5331
|
/** Frames per second to update
|
|
5329
5332
|
* @type {number}
|
|
@@ -5411,16 +5414,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5411
5414
|
ASSERT(Array.isArray(imageSources), 'pass in images as array');
|
|
5412
5415
|
|
|
5413
5416
|
// allow passing in empty functions
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
gameUpdatePost = ()=>{};
|
|
5420
|
-
if (!gameRender)
|
|
5421
|
-
gameRender = ()=>{};
|
|
5422
|
-
if (!gameRenderPost)
|
|
5423
|
-
gameRenderPost = ()=>{};
|
|
5417
|
+
gameInit ||= ()=>{};
|
|
5418
|
+
gameUpdate ||= ()=>{};
|
|
5419
|
+
gameUpdatePost ||= ()=>{};
|
|
5420
|
+
gameRender ||= ()=>{};
|
|
5421
|
+
gameRenderPost ||= ()=>{};
|
|
5424
5422
|
|
|
5425
5423
|
// Called automatically by engine to setup render system
|
|
5426
5424
|
function enginePreRender()
|
|
@@ -5447,11 +5445,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5447
5445
|
const debugSpeedUp = debug && keyIsDown('Equal'); // +
|
|
5448
5446
|
const debugSpeedDown = debug && keyIsDown('Minus'); // -
|
|
5449
5447
|
if (debug) // +/- to speed/slow time
|
|
5450
|
-
frameTimeDeltaMS *= debugSpeedUp ?
|
|
5448
|
+
frameTimeDeltaMS *= debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
|
|
5451
5449
|
timeReal += frameTimeDeltaMS / 1e3;
|
|
5452
5450
|
frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
|
|
5453
5451
|
if (!debugSpeedUp)
|
|
5454
|
-
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp
|
|
5452
|
+
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
|
|
5453
|
+
if (debug && debugVideoCaptureIsActive())
|
|
5454
|
+
frameTimeBufferMS = 0; // disable time smoothing when capturing video
|
|
5455
5455
|
|
|
5456
5456
|
updateCanvas();
|
|
5457
5457
|
|
|
@@ -5530,6 +5530,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5530
5530
|
}
|
|
5531
5531
|
}
|
|
5532
5532
|
|
|
5533
|
+
debugVideoCaptureUpdate();
|
|
5533
5534
|
requestAnimationFrame(engineUpdate);
|
|
5534
5535
|
}
|
|
5535
5536
|
|
|
@@ -5577,11 +5578,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5577
5578
|
|
|
5578
5579
|
// setup html
|
|
5579
5580
|
const styleRoot =
|
|
5580
|
-
'margin:0;
|
|
5581
|
-
'width:100vw;height:100vh;' + // fill the window
|
|
5582
|
-
'display:flex;' + // use flexbox
|
|
5583
|
-
'align-items:center;' + // horizontal center
|
|
5584
|
-
'justify-content:center;' + // vertical center
|
|
5581
|
+
'margin:0;' + // fill the window
|
|
5585
5582
|
'background:#000;' + // set background color
|
|
5586
5583
|
(canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
|
|
5587
5584
|
'user-select:none;' + // prevent hold to select
|
|
@@ -5604,7 +5601,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5604
5601
|
overlayContext = overlayCanvas.getContext('2d');
|
|
5605
5602
|
|
|
5606
5603
|
// set canvas style
|
|
5607
|
-
const styleCanvas = 'position:absolute'
|
|
5604
|
+
const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
|
|
5605
|
+
'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
|
|
5608
5606
|
mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
|
|
5609
5607
|
if (glCanvas)
|
|
5610
5608
|
glCanvas.style.cssText = styleCanvas;
|
|
@@ -5615,12 +5613,12 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5615
5613
|
new Promise(resolve =>
|
|
5616
5614
|
{
|
|
5617
5615
|
const image = new Image;
|
|
5618
|
-
image.crossOrigin = 'anonymous';
|
|
5619
5616
|
image.onerror = image.onload = ()=>
|
|
5620
5617
|
{
|
|
5621
5618
|
textureInfos[textureIndex] = new TextureInfo(image);
|
|
5622
5619
|
resolve();
|
|
5623
5620
|
}
|
|
5621
|
+
image.crossOrigin = 'anonymous';
|
|
5624
5622
|
image.src = src;
|
|
5625
5623
|
})
|
|
5626
5624
|
);
|