littlejsengine 1.13.4 → 1.14.2
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/README.md +2 -1
- package/copyToGithub.bat +28 -0
- package/dist/littlejs.d.ts +346 -240
- package/dist/littlejs.esm.js +1370 -723
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +1350 -709
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +1333 -692
- package/examples/box2d/game.js +1 -1
- package/examples/box2d/gameObjects.js +1 -1
- package/examples/breakout/game.js +30 -25
- package/examples/electron/index.html +2 -2
- package/examples/index.html +207 -96
- package/examples/platformer/gameEffects.js +19 -18
- package/examples/platformer/gameLevel.js +6 -1
- package/examples/shorts/base.html +1 -1
- package/examples/shorts/flappyGame.js +2 -1
- package/examples/shorts/helloWorld.js +1 -1
- package/examples/shorts/music.js +106 -0
- package/examples/shorts/parallax.js +71 -0
- package/examples/shorts/piano.js +7 -8
- package/examples/shorts/postProcess.js +25 -8
- package/examples/shorts/shapes.js +1 -9
- package/examples/shorts/song.mp3 +0 -0
- package/examples/shorts/uiSystem.js +15 -8
- package/examples/starter/index.html +2 -2
- package/examples/stress/index.html +8 -3
- package/examples/style.css +14 -4
- package/examples/uiSystem/game.js +11 -11
- package/package.json +1 -1
- package/plugins/box2d.js +10 -8
- package/plugins/drawUtilities.js +5 -5
- package/plugins/newgrounds.js +6 -6
- package/plugins/pluginExport.js +1 -1
- package/plugins/uiSystem.js +103 -79
- package/plugins/zzfxm.js +10 -10
- package/reference.md +90 -54
- package/src/engine.js +22 -22
- package/src/engineAudio.js +284 -109
- package/src/engineBuild.js +9 -9
- package/src/engineDebug.js +26 -26
- package/src/engineDraw.js +148 -97
- package/src/engineExport.js +22 -16
- package/src/engineInput.js +57 -67
- package/src/engineMedals.js +25 -25
- package/src/engineObject.js +25 -22
- package/src/engineParticles.js +59 -59
- package/src/engineRelease.js +1 -1
- package/src/engineSettings.js +20 -13
- package/src/engineTileLayer.js +34 -34
- package/src/engineUtilities.js +62 -55
- package/src/engineWebGL.js +447 -65
- /package/examples/shorts/{playSound.js → sound.js} +0 -0
package/dist/littlejs.esm.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
7
|
* LittleJS - The Tiny Fast JavaScript Game Engine
|
|
8
8
|
* MIT License - Copyright 2021 Frank Force
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
10
|
* Engine Features
|
|
11
11
|
* - Object oriented system with base class engine object
|
|
12
12
|
* - Base class object handles update, physics, collision, rendering, etc
|
|
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
|
|
|
33
33
|
* @type {string}
|
|
34
34
|
* @default
|
|
35
35
|
* @memberof Engine */
|
|
36
|
-
const engineVersion = '1.
|
|
36
|
+
const engineVersion = '1.14.2';
|
|
37
37
|
|
|
38
38
|
/** Frames per second to update
|
|
39
39
|
* @type {number}
|
|
@@ -123,7 +123,7 @@ function engineAddPlugin(updateFunction, renderFunction)
|
|
|
123
123
|
* // Basic engine startup
|
|
124
124
|
* engineInit(
|
|
125
125
|
* () => { console.log('Game initialized!'); }, // gameInit
|
|
126
|
-
* () => {
|
|
126
|
+
* () => { updateGameLogic(); }, // gameUpdate
|
|
127
127
|
* () => { updateUI(); }, // gameUpdatePost
|
|
128
128
|
* () => { drawBackground(); }, // gameRender
|
|
129
129
|
* () => { drawHUD(); }, // gameRenderPost
|
|
@@ -149,7 +149,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
149
149
|
mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
|
|
150
150
|
|
|
151
151
|
// disable smoothing for pixel art
|
|
152
|
-
overlayContext.imageSmoothingEnabled =
|
|
152
|
+
overlayContext.imageSmoothingEnabled =
|
|
153
153
|
mainContext.imageSmoothingEnabled = !tilesPixelated;
|
|
154
154
|
|
|
155
155
|
// setup gl rendering if enabled
|
|
@@ -197,7 +197,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
197
197
|
deltaSmooth = frameTimeBufferMS;
|
|
198
198
|
frameTimeBufferMS = 0;
|
|
199
199
|
}
|
|
200
|
-
|
|
200
|
+
|
|
201
201
|
// update multiple frames if necessary in case of slow framerate
|
|
202
202
|
for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
|
|
203
203
|
{
|
|
@@ -242,7 +242,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
242
242
|
overlayContext.textBaseline = 'top';
|
|
243
243
|
overlayContext.font = '1em monospace';
|
|
244
244
|
overlayContext.fillStyle = '#000';
|
|
245
|
-
const text = engineName + ' ' + 'v' + engineVersion + ' / '
|
|
245
|
+
const text = engineName + ' ' + 'v' + engineVersion + ' / '
|
|
246
246
|
+ drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
|
|
247
247
|
+ (glEnable ? ' GL' : ' 2D') ;
|
|
248
248
|
overlayContext.fillText(text, mainCanvas.width-3, 3);
|
|
@@ -258,13 +258,13 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
258
258
|
function updateCanvas()
|
|
259
259
|
{
|
|
260
260
|
if (headlessMode) return;
|
|
261
|
-
|
|
261
|
+
|
|
262
262
|
if (canvasFixedSize.x)
|
|
263
263
|
{
|
|
264
264
|
// clear canvas and set fixed size
|
|
265
265
|
mainCanvas.width = canvasFixedSize.x;
|
|
266
266
|
mainCanvas.height = canvasFixedSize.y;
|
|
267
|
-
|
|
267
|
+
|
|
268
268
|
// fit to window by adding space on top or bottom if necessary
|
|
269
269
|
const aspect = innerWidth / innerHeight;
|
|
270
270
|
const fixedAspect = mainCanvas.width / mainCanvas.height;
|
|
@@ -274,10 +274,10 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
274
274
|
else
|
|
275
275
|
{
|
|
276
276
|
// clear canvas and set size to same as window
|
|
277
|
-
mainCanvas.width
|
|
277
|
+
mainCanvas.width = min(innerWidth, canvasMaxSize.x);
|
|
278
278
|
mainCanvas.height = min(innerHeight, canvasMaxSize.y);
|
|
279
279
|
}
|
|
280
|
-
|
|
280
|
+
|
|
281
281
|
// clear overlay canvas and set size
|
|
282
282
|
overlayCanvas.width = mainCanvas.width;
|
|
283
283
|
overlayCanvas.height = mainCanvas.height;
|
|
@@ -296,15 +296,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
296
296
|
return startEngine();
|
|
297
297
|
|
|
298
298
|
// setup html
|
|
299
|
-
const styleRoot =
|
|
299
|
+
const styleRoot =
|
|
300
300
|
'margin:0;' + // fill the window
|
|
301
301
|
'overflow:hidden;' + // no scroll bars
|
|
302
302
|
'background:#000;' + // set background color
|
|
303
303
|
'user-select:none;' + // prevent hold to select
|
|
304
304
|
'-webkit-user-select:none;' + // compatibility for ios
|
|
305
|
-
(!touchInputEnable ? '' : // no touch css settings
|
|
306
305
|
'touch-action:none;' + // prevent mobile pinch to resize
|
|
307
|
-
'-webkit-touch-callout:none'
|
|
306
|
+
'-webkit-touch-callout:none';// compatibility for ios
|
|
308
307
|
rootElement.style.cssText = styleRoot;
|
|
309
308
|
drawCanvas = mainCanvas = document.createElement('canvas');
|
|
310
309
|
rootElement.appendChild(mainCanvas);
|
|
@@ -321,7 +320,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
321
320
|
rootElement.appendChild(overlayCanvas);
|
|
322
321
|
overlayContext = overlayCanvas.getContext('2d');
|
|
323
322
|
|
|
324
|
-
// set
|
|
323
|
+
// set canvases
|
|
325
324
|
const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
|
|
326
325
|
'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
|
|
327
326
|
mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
|
|
@@ -330,17 +329,18 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
330
329
|
setCanvasPixelated(canvasPixelated);
|
|
331
330
|
setOverlayCanvasPixelated(overlayCanvasPixelated);
|
|
332
331
|
updateCanvas();
|
|
332
|
+
glPreRender();
|
|
333
333
|
|
|
334
334
|
// create offscreen canvas for image processing
|
|
335
335
|
workCanvas = new OffscreenCanvas(256, 256);
|
|
336
336
|
workContext = workCanvas.getContext('2d', { willReadFrequently: true });
|
|
337
|
-
|
|
337
|
+
|
|
338
338
|
// create promises for loading images
|
|
339
339
|
const promises = imageSources.map((src, textureIndex)=>
|
|
340
|
-
new Promise(resolve =>
|
|
340
|
+
new Promise(resolve =>
|
|
341
341
|
{
|
|
342
342
|
const image = new Image;
|
|
343
|
-
image.onerror = image.onload = ()=>
|
|
343
|
+
image.onerror = image.onload = ()=>
|
|
344
344
|
{
|
|
345
345
|
const textureInfo = new TextureInfo(image);
|
|
346
346
|
textureInfo.createWebGLTexture();
|
|
@@ -355,7 +355,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
355
355
|
if (!imageSources.length)
|
|
356
356
|
{
|
|
357
357
|
// no images to load
|
|
358
|
-
promises.push(new Promise(resolve =>
|
|
358
|
+
promises.push(new Promise(resolve =>
|
|
359
359
|
{
|
|
360
360
|
const textureInfo = new TextureInfo(new Image);
|
|
361
361
|
textureInfos[0] = textureInfo;
|
|
@@ -367,7 +367,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
367
367
|
if (showSplashScreen)
|
|
368
368
|
{
|
|
369
369
|
// draw splash screen
|
|
370
|
-
promises.push(new Promise(resolve =>
|
|
370
|
+
promises.push(new Promise(resolve =>
|
|
371
371
|
{
|
|
372
372
|
let t = 0;
|
|
373
373
|
console.log(`${engineName} Engine v${engineVersion}`);
|
|
@@ -599,7 +599,7 @@ function drawEngineSplashScreen(t)
|
|
|
599
599
|
line(36,20,60,20);
|
|
600
600
|
|
|
601
601
|
// engine front light
|
|
602
|
-
circle(60,30,4,PI,3*PI,color(3,2));
|
|
602
|
+
circle(60,30,4,PI,3*PI,color(3,2));
|
|
603
603
|
circle(60,30,4,PI,2*PI,color(3,3));
|
|
604
604
|
circle(60,30,4,PI,3*PI);
|
|
605
605
|
|
|
@@ -648,10 +648,10 @@ function drawEngineSplashScreen(t)
|
|
|
648
648
|
x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
|
|
649
649
|
X += w;
|
|
650
650
|
}
|
|
651
|
-
|
|
651
|
+
|
|
652
652
|
x.restore();
|
|
653
653
|
}
|
|
654
|
-
/**
|
|
654
|
+
/**
|
|
655
655
|
* LittleJS Debug System
|
|
656
656
|
* - Press Esc to show debug overlay with mouse pick
|
|
657
657
|
* - Number keys toggle debug functions
|
|
@@ -704,10 +704,10 @@ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParti
|
|
|
704
704
|
// Debug helper functions
|
|
705
705
|
|
|
706
706
|
/** Asserts if the expression is false, does not do anything in release builds
|
|
707
|
-
* @param {boolean} assert
|
|
707
|
+
* @param {boolean} assert
|
|
708
708
|
* @param {...Object} [output] - error message output
|
|
709
709
|
* @memberof Debug */
|
|
710
|
-
function ASSERT(assert, ...output)
|
|
710
|
+
function ASSERT(assert, ...output)
|
|
711
711
|
{
|
|
712
712
|
if (enableAsserts)
|
|
713
713
|
console.assert(assert, ...output);
|
|
@@ -723,9 +723,9 @@ function ASSERT(assert, ...output)
|
|
|
723
723
|
* @memberof Debug */
|
|
724
724
|
function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
|
|
725
725
|
{
|
|
726
|
-
if (typeof size
|
|
726
|
+
if (typeof size === 'number')
|
|
727
727
|
size = vec2(size); // allow passing in floats
|
|
728
|
-
ASSERT(typeof color
|
|
728
|
+
ASSERT(typeof color === 'string', 'pass in css color strings');
|
|
729
729
|
debugPrimitives.push({pos, size, color, time:new Timer(time), angle, fill});
|
|
730
730
|
}
|
|
731
731
|
|
|
@@ -739,7 +739,7 @@ function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
|
|
|
739
739
|
* @memberof Debug */
|
|
740
740
|
function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
|
|
741
741
|
{
|
|
742
|
-
ASSERT(typeof color
|
|
742
|
+
ASSERT(typeof color === 'string', 'pass in css color strings');
|
|
743
743
|
debugPrimitives.push({pos, points, color, time:new Timer(time), angle, fill});
|
|
744
744
|
}
|
|
745
745
|
|
|
@@ -752,7 +752,7 @@ function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
|
|
|
752
752
|
* @memberof Debug */
|
|
753
753
|
function debugCircle(pos, radius=0, color='#fff', time=0, fill=false)
|
|
754
754
|
{
|
|
755
|
-
ASSERT(typeof color
|
|
755
|
+
ASSERT(typeof color === 'string', 'pass in css color strings');
|
|
756
756
|
debugPrimitives.push({pos, size:radius, color, time:new Timer(time), angle:0, fill});
|
|
757
757
|
}
|
|
758
758
|
|
|
@@ -764,7 +764,7 @@ function debugCircle(pos, radius=0, color='#fff', time=0, fill=false)
|
|
|
764
764
|
* @memberof Debug */
|
|
765
765
|
function debugPoint(pos, color, time, angle)
|
|
766
766
|
{
|
|
767
|
-
ASSERT(typeof color
|
|
767
|
+
ASSERT(typeof color === 'string', 'pass in css color strings');
|
|
768
768
|
debugRect(pos, undefined, color, time, angle);
|
|
769
769
|
}
|
|
770
770
|
|
|
@@ -792,11 +792,11 @@ function debugLine(posA, posB, color, thickness=.1, time)
|
|
|
792
792
|
function debugOverlap(posA, sizeA, posB, sizeB, color)
|
|
793
793
|
{
|
|
794
794
|
const minPos = vec2(
|
|
795
|
-
min(posA.x - sizeA.x/2, posB.x - sizeB.x/2),
|
|
795
|
+
min(posA.x - sizeA.x/2, posB.x - sizeB.x/2),
|
|
796
796
|
min(posA.y - sizeA.y/2, posB.y - sizeB.y/2)
|
|
797
797
|
);
|
|
798
798
|
const maxPos = vec2(
|
|
799
|
-
max(posA.x + sizeA.x/2, posB.x + sizeB.x/2),
|
|
799
|
+
max(posA.x + sizeA.x/2, posB.x + sizeB.x/2),
|
|
800
800
|
max(posA.y + sizeA.y/2, posB.y + sizeB.y/2)
|
|
801
801
|
);
|
|
802
802
|
debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color);
|
|
@@ -813,7 +813,7 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
|
|
|
813
813
|
* @memberof Debug */
|
|
814
814
|
function debugText(text, pos, size=1, color='#fff', time=0, angle=0, font='monospace')
|
|
815
815
|
{
|
|
816
|
-
ASSERT(typeof color
|
|
816
|
+
ASSERT(typeof color === 'string', 'pass in css color strings');
|
|
817
817
|
debugPrimitives.push({text, pos, size, color, time:new Timer(time), angle, font});
|
|
818
818
|
}
|
|
819
819
|
|
|
@@ -825,7 +825,7 @@ function debugClear() { debugPrimitives = []; }
|
|
|
825
825
|
* @memberof Debug */
|
|
826
826
|
function debugScreenshot() { debugTakeScreenshot = 1; }
|
|
827
827
|
|
|
828
|
-
/** Save a canvas to disk
|
|
828
|
+
/** Save a canvas to disk
|
|
829
829
|
* @param {HTMLCanvasElement|OffscreenCanvas} canvas
|
|
830
830
|
* @param {string} [filename]
|
|
831
831
|
* @param {string} [type]
|
|
@@ -846,7 +846,7 @@ function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
|
|
|
846
846
|
debugSaveDataURL(canvas.toDataURL(type), filename);
|
|
847
847
|
}
|
|
848
848
|
|
|
849
|
-
/** Save a text file to disk
|
|
849
|
+
/** Save a text file to disk
|
|
850
850
|
* @param {string} text
|
|
851
851
|
* @param {string} [filename]
|
|
852
852
|
* @param {string} [type]
|
|
@@ -854,7 +854,7 @@ function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
|
|
|
854
854
|
function debugSaveText(text, filename='text', type='text/plain')
|
|
855
855
|
{ debugSaveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
|
|
856
856
|
|
|
857
|
-
/** Save a data url to disk
|
|
857
|
+
/** Save a data url to disk
|
|
858
858
|
* @param {string} dataURL
|
|
859
859
|
* @param {string} filename
|
|
860
860
|
* @memberof Debug */
|
|
@@ -973,7 +973,7 @@ function debugRender()
|
|
|
973
973
|
{
|
|
974
974
|
const saveContext = mainContext;
|
|
975
975
|
mainContext = overlayContext;
|
|
976
|
-
|
|
976
|
+
|
|
977
977
|
// draw red rectangle around screen
|
|
978
978
|
const cameraSize = getCameraSize();
|
|
979
979
|
debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008');
|
|
@@ -1023,14 +1023,14 @@ function debugRender()
|
|
|
1023
1023
|
overlayContext.scale(1, p.text ? 1 : -1);
|
|
1024
1024
|
overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
|
|
1025
1025
|
|
|
1026
|
-
if (p.text
|
|
1026
|
+
if (p.text !== undefined)
|
|
1027
1027
|
{
|
|
1028
1028
|
overlayContext.font = p.size*cameraScale + 'px '+ p.font;
|
|
1029
1029
|
overlayContext.textAlign = 'center';
|
|
1030
1030
|
overlayContext.textBaseline = 'middle';
|
|
1031
1031
|
overlayContext.fillText(p.text, 0, 0);
|
|
1032
1032
|
}
|
|
1033
|
-
else if (p.points
|
|
1033
|
+
else if (p.points !== undefined)
|
|
1034
1034
|
{
|
|
1035
1035
|
// poly
|
|
1036
1036
|
overlayContext.beginPath();
|
|
@@ -1043,13 +1043,13 @@ function debugRender()
|
|
|
1043
1043
|
p.fill && overlayContext.fill();
|
|
1044
1044
|
overlayContext.stroke();
|
|
1045
1045
|
}
|
|
1046
|
-
else if (p.size
|
|
1046
|
+
else if (p.size === 0 || p.size.x === 0 && p.size.y === 0)
|
|
1047
1047
|
{
|
|
1048
1048
|
// point
|
|
1049
1049
|
overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
|
|
1050
1050
|
overlayContext.fillRect(-1, -pointSize/2, 3, pointSize);
|
|
1051
1051
|
}
|
|
1052
|
-
else if (p.size.x
|
|
1052
|
+
else if (p.size.x !== undefined)
|
|
1053
1053
|
{
|
|
1054
1054
|
// rect
|
|
1055
1055
|
const s = p.size.scale(cameraScale).floor();
|
|
@@ -1065,14 +1065,14 @@ function debugRender()
|
|
|
1065
1065
|
p.fill && overlayContext.fill();
|
|
1066
1066
|
overlayContext.stroke();
|
|
1067
1067
|
}
|
|
1068
|
-
|
|
1068
|
+
|
|
1069
1069
|
overlayContext.restore();
|
|
1070
1070
|
});
|
|
1071
1071
|
|
|
1072
1072
|
// remove expired primitives
|
|
1073
1073
|
debugPrimitives = debugPrimitives.filter(r=>r.time<0);
|
|
1074
1074
|
}
|
|
1075
|
-
|
|
1075
|
+
|
|
1076
1076
|
if (debugObject)
|
|
1077
1077
|
{
|
|
1078
1078
|
const saveContext = mainContext;
|
|
@@ -1081,8 +1081,8 @@ function debugRender()
|
|
|
1081
1081
|
raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3));
|
|
1082
1082
|
drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5));
|
|
1083
1083
|
|
|
1084
|
-
const debugText = 'mouse pos = ' + mousePos +
|
|
1085
|
-
'\nmouse collision = ' + tileCollisionGetData(mousePos) +
|
|
1084
|
+
const debugText = 'mouse pos = ' + mousePos +
|
|
1085
|
+
'\nmouse collision = ' + tileCollisionGetData(mousePos) +
|
|
1086
1086
|
'\n\n--- object info ---\n' +
|
|
1087
1087
|
debugObject.toString();
|
|
1088
1088
|
drawTextScreen(debugText, mousePosScreen, 24, rgb(), .05, undefined, 'center', 'monospace');
|
|
@@ -1104,7 +1104,7 @@ function debugRender()
|
|
|
1104
1104
|
let x = 9, y = 0, h = lineHeight;
|
|
1105
1105
|
if (debugOverlay)
|
|
1106
1106
|
{
|
|
1107
|
-
overlayContext.fillText(engineName
|
|
1107
|
+
overlayContext.fillText(`${engineName} v${engineVersion}`, x, y += h/2 );
|
|
1108
1108
|
overlayContext.fillText('Time: ' + formatTime(time), x, y += h);
|
|
1109
1109
|
overlayContext.fillText('FPS: ' + averageFPS.toFixed(1), x, y += h);
|
|
1110
1110
|
overlayContext.fillText('Objects: ' + engineObjects.length, x, y += h);
|
|
@@ -1148,7 +1148,7 @@ function debugRender()
|
|
|
1148
1148
|
overlayContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
|
|
1149
1149
|
overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
|
|
1150
1150
|
}
|
|
1151
|
-
|
|
1151
|
+
|
|
1152
1152
|
overlayContext.restore();
|
|
1153
1153
|
}
|
|
1154
1154
|
}
|
|
@@ -1236,7 +1236,7 @@ function debugVideoCaptureUpdate()
|
|
|
1236
1236
|
{
|
|
1237
1237
|
if (!debugVideoCaptureIsActive())
|
|
1238
1238
|
return; // not recording
|
|
1239
|
-
|
|
1239
|
+
|
|
1240
1240
|
// save the video frame
|
|
1241
1241
|
combineCanvases();
|
|
1242
1242
|
debugVideoCaptureTrack.requestFrame();
|
|
@@ -1317,7 +1317,19 @@ function lerp(valueA, valueB, percent)
|
|
|
1317
1317
|
if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
|
|
1318
1318
|
console.warn('lerp() parameter order changed! use lerp(start, end, p)');
|
|
1319
1319
|
return valueA + clamp(percent) * (valueB-valueA);
|
|
1320
|
-
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
|
|
1323
|
+
* A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
|
|
1324
|
+
* @param {number} value
|
|
1325
|
+
* @param {number} percentA
|
|
1326
|
+
* @param {number} percentB
|
|
1327
|
+
* @param {number} lerpA
|
|
1328
|
+
* @param {number} lerpB
|
|
1329
|
+
* @return {number}
|
|
1330
|
+
* @memberof Utilities */
|
|
1331
|
+
function percentLerp(value, percentA, percentB, lerpA, lerpB)
|
|
1332
|
+
{ return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
|
|
1321
1333
|
|
|
1322
1334
|
/** Returns signed wrapped distance between the two values passed in
|
|
1323
1335
|
* @param {number} valueA
|
|
@@ -1369,7 +1381,7 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
|
|
|
1369
1381
|
* @memberof Utilities */
|
|
1370
1382
|
function isPowerOfTwo(value) { return !(value & (value - 1)); }
|
|
1371
1383
|
|
|
1372
|
-
/** Returns the nearest power of two not less
|
|
1384
|
+
/** Returns the nearest power of two not less than the value
|
|
1373
1385
|
* @param {number} value
|
|
1374
1386
|
* @return {number}
|
|
1375
1387
|
* @memberof Utilities */
|
|
@@ -1384,8 +1396,8 @@ function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
|
|
|
1384
1396
|
* @return {boolean} - True if overlapping
|
|
1385
1397
|
* @memberof Utilities */
|
|
1386
1398
|
function isOverlapping(posA, sizeA, posB, sizeB=vec2())
|
|
1387
|
-
{
|
|
1388
|
-
return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
|
|
1399
|
+
{
|
|
1400
|
+
return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
|
|
1389
1401
|
&& abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
|
|
1390
1402
|
}
|
|
1391
1403
|
|
|
@@ -1440,7 +1452,7 @@ function isIntersecting(start, end, pos, size)
|
|
|
1440
1452
|
function wave(frequency=1, amplitude=1, t=time, offset=0)
|
|
1441
1453
|
{ return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
|
|
1442
1454
|
|
|
1443
|
-
/** Formats seconds to mm:ss style for display purposes
|
|
1455
|
+
/** Formats seconds to mm:ss style for display purposes
|
|
1444
1456
|
* @param {number} t - time in seconds
|
|
1445
1457
|
* @return {string}
|
|
1446
1458
|
* @memberof Utilities */
|
|
@@ -1456,13 +1468,12 @@ async function fetchJSON(url)
|
|
|
1456
1468
|
return response.json();
|
|
1457
1469
|
}
|
|
1458
1470
|
|
|
1459
|
-
/**
|
|
1471
|
+
/**
|
|
1460
1472
|
* Check if object is a valid number, not NaN or undefined, but it may be infinite
|
|
1461
1473
|
* @param {any} n
|
|
1462
1474
|
* @return {boolean}
|
|
1463
|
-
* @memberof Utilities
|
|
1464
|
-
|
|
1465
|
-
function isNumber(n) { return typeof n == 'number' && !isNaN(n); }
|
|
1475
|
+
* @memberof Utilities */
|
|
1476
|
+
function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
|
|
1466
1477
|
|
|
1467
1478
|
///////////////////////////////////////////////////////////////////////////////
|
|
1468
1479
|
|
|
@@ -1517,13 +1528,13 @@ function randInCircle(radius=1, minRadius=0)
|
|
|
1517
1528
|
* @memberof Random */
|
|
1518
1529
|
function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
|
|
1519
1530
|
{
|
|
1520
|
-
return linear ? colorA.lerp(colorB, rand()) :
|
|
1531
|
+
return linear ? colorA.lerp(colorB, rand()) :
|
|
1521
1532
|
new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
|
|
1522
1533
|
}
|
|
1523
1534
|
|
|
1524
1535
|
///////////////////////////////////////////////////////////////////////////////
|
|
1525
1536
|
|
|
1526
|
-
/**
|
|
1537
|
+
/**
|
|
1527
1538
|
* Seeded random number generator
|
|
1528
1539
|
* - Can be used to create a deterministic random number sequence
|
|
1529
1540
|
* @example
|
|
@@ -1550,8 +1561,8 @@ class RandomGenerator
|
|
|
1550
1561
|
float(valueA=1, valueB=0)
|
|
1551
1562
|
{
|
|
1552
1563
|
// xorshift algorithm
|
|
1553
|
-
this.seed ^= this.seed << 13;
|
|
1554
|
-
this.seed ^= this.seed >>> 17;
|
|
1564
|
+
this.seed ^= this.seed << 13;
|
|
1565
|
+
this.seed ^= this.seed >>> 17;
|
|
1555
1566
|
this.seed ^= this.seed << 5;
|
|
1556
1567
|
return valueB + (valueA - valueB) * ((this.seed >>> 0) / 2**32);
|
|
1557
1568
|
}
|
|
@@ -1600,16 +1611,14 @@ class RandomGenerator
|
|
|
1600
1611
|
* let a = vec2(0, 1); // vector with coordinates (0, 1)
|
|
1601
1612
|
* a = vec2(5); // set a to (5, 5)
|
|
1602
1613
|
* b = vec2(); // set b to (0, 0)
|
|
1603
|
-
* @memberof Utilities
|
|
1604
|
-
*/
|
|
1614
|
+
* @memberof Utilities */
|
|
1605
1615
|
function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
|
|
1606
1616
|
|
|
1607
|
-
/**
|
|
1617
|
+
/**
|
|
1608
1618
|
* Check if object is a valid Vector2
|
|
1609
1619
|
* @param {any} v
|
|
1610
1620
|
* @return {boolean}
|
|
1611
|
-
* @memberof Utilities
|
|
1612
|
-
*/
|
|
1621
|
+
* @memberof Utilities */
|
|
1613
1622
|
function isVector2(v) { return v instanceof Vector2; }
|
|
1614
1623
|
|
|
1615
1624
|
// vector2 asserts
|
|
@@ -1618,10 +1627,10 @@ function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n);
|
|
|
1618
1627
|
function ASSERT_VECTOR2_NORMAL(v)
|
|
1619
1628
|
{
|
|
1620
1629
|
ASSERT_VECTOR2_VALID(v);
|
|
1621
|
-
ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
|
|
1630
|
+
ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
|
|
1622
1631
|
}
|
|
1623
1632
|
|
|
1624
|
-
/**
|
|
1633
|
+
/**
|
|
1625
1634
|
* 2D Vector object with vector math library
|
|
1626
1635
|
* - Functions do not change this so they can be chained together
|
|
1627
1636
|
* @example
|
|
@@ -1745,7 +1754,7 @@ class Vector2
|
|
|
1745
1754
|
* @param {number} [angle]
|
|
1746
1755
|
* @param {number} [length]
|
|
1747
1756
|
* @return {Vector2} */
|
|
1748
|
-
setAngle(angle=0, length=1)
|
|
1757
|
+
setAngle(angle=0, length=1)
|
|
1749
1758
|
{
|
|
1750
1759
|
ASSERT_NUMBER_VALID(angle);
|
|
1751
1760
|
ASSERT_NUMBER_VALID(length);
|
|
@@ -1760,7 +1769,7 @@ class Vector2
|
|
|
1760
1769
|
rotate(angle)
|
|
1761
1770
|
{
|
|
1762
1771
|
ASSERT_NUMBER_VALID(angle);
|
|
1763
|
-
const c = Math.cos(-angle), s = Math.sin(-angle);
|
|
1772
|
+
const c = Math.cos(-angle), s = Math.sin(-angle);
|
|
1764
1773
|
return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
|
|
1765
1774
|
}
|
|
1766
1775
|
|
|
@@ -1772,9 +1781,9 @@ class Vector2
|
|
|
1772
1781
|
ASSERT_NUMBER_VALID(direction);
|
|
1773
1782
|
ASSERT_NUMBER_VALID(length);
|
|
1774
1783
|
direction = mod(direction, 4);
|
|
1775
|
-
ASSERT(direction
|
|
1784
|
+
ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
|
|
1776
1785
|
'Vector2.setDirection() direction must be an integer between 0 and 3.');
|
|
1777
|
-
return vec2(direction%2 ? direction-1 ? -length : length : 0,
|
|
1786
|
+
return vec2(direction%2 ? direction-1 ? -length : length : 0,
|
|
1778
1787
|
direction%2 ? 0 : direction ? -length : length);
|
|
1779
1788
|
}
|
|
1780
1789
|
|
|
@@ -1826,7 +1835,7 @@ class Vector2
|
|
|
1826
1835
|
/** Returns this vector expressed as a string
|
|
1827
1836
|
* @param {number} digits - precision to display
|
|
1828
1837
|
* @return {string} */
|
|
1829
|
-
toString(digits=3)
|
|
1838
|
+
toString(digits=3)
|
|
1830
1839
|
{
|
|
1831
1840
|
ASSERT_NUMBER_VALID(digits);
|
|
1832
1841
|
if (debug)
|
|
@@ -1845,7 +1854,7 @@ class Vector2
|
|
|
1845
1854
|
|
|
1846
1855
|
///////////////////////////////////////////////////////////////////////////////
|
|
1847
1856
|
|
|
1848
|
-
/**
|
|
1857
|
+
/**
|
|
1849
1858
|
* Create a color object with RGBA values, white by default
|
|
1850
1859
|
* @param {number} [r=1] - red
|
|
1851
1860
|
* @param {number} [g=1] - green
|
|
@@ -1856,29 +1865,27 @@ class Vector2
|
|
|
1856
1865
|
*/
|
|
1857
1866
|
function rgb(r, g, b, a) { return new Color(r, g, b, a); }
|
|
1858
1867
|
|
|
1859
|
-
/**
|
|
1868
|
+
/**
|
|
1860
1869
|
* Create a color object with HSLA values, white by default
|
|
1861
1870
|
* @param {number} [h=0] - hue
|
|
1862
1871
|
* @param {number} [s=0] - saturation
|
|
1863
1872
|
* @param {number} [l=1] - lightness
|
|
1864
1873
|
* @param {number} [a=1] - alpha
|
|
1865
1874
|
* @return {Color}
|
|
1866
|
-
* @memberof Utilities
|
|
1867
|
-
*/
|
|
1875
|
+
* @memberof Utilities */
|
|
1868
1876
|
function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
|
|
1869
1877
|
|
|
1870
|
-
/**
|
|
1878
|
+
/**
|
|
1871
1879
|
* Check if object is a valid Color
|
|
1872
1880
|
* @param {any} c
|
|
1873
1881
|
* @return {boolean}
|
|
1874
|
-
* @memberof Utilities
|
|
1875
|
-
*/
|
|
1882
|
+
* @memberof Utilities */
|
|
1876
1883
|
function isColor(c) { return c instanceof Color; }
|
|
1877
1884
|
|
|
1878
1885
|
// color asserts
|
|
1879
1886
|
function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c) && c.isValid(), 'Color is invalid.', c); }
|
|
1880
1887
|
|
|
1881
|
-
/**
|
|
1888
|
+
/**
|
|
1882
1889
|
* Color object (red, green, blue, alpha) with some helpful functions
|
|
1883
1890
|
* @example
|
|
1884
1891
|
* let a = new Color; // white
|
|
@@ -1950,7 +1957,7 @@ class Color
|
|
|
1950
1957
|
* @param {number} scale
|
|
1951
1958
|
* @param {number} [alphaScale=scale]
|
|
1952
1959
|
* @return {Color} */
|
|
1953
|
-
scale(scale, alphaScale=scale)
|
|
1960
|
+
scale(scale, alphaScale=scale)
|
|
1954
1961
|
{ return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
|
|
1955
1962
|
|
|
1956
1963
|
/** Returns a copy of this color clamped to the valid range between 0 and 1
|
|
@@ -1967,9 +1974,9 @@ class Color
|
|
|
1967
1974
|
ASSERT_NUMBER_VALID(percent);
|
|
1968
1975
|
const p = clamp(percent);
|
|
1969
1976
|
return new Color(
|
|
1970
|
-
c.r*p + this.r*(1-p),
|
|
1971
|
-
c.g*p + this.g*(1-p),
|
|
1972
|
-
c.b*p + this.b*(1-p),
|
|
1977
|
+
c.r*p + this.r*(1-p),
|
|
1978
|
+
c.g*p + this.g*(1-p),
|
|
1979
|
+
c.b*p + this.b*(1-p),
|
|
1973
1980
|
c.a*p + this.a*(1-p));
|
|
1974
1981
|
}
|
|
1975
1982
|
|
|
@@ -2009,15 +2016,15 @@ class Color
|
|
|
2009
2016
|
const minC = min(r, g, b);
|
|
2010
2017
|
const l = (maxC + minC) / 2;
|
|
2011
2018
|
let h = 0, s = 0;
|
|
2012
|
-
if (maxC
|
|
2019
|
+
if (maxC !== minC)
|
|
2013
2020
|
{
|
|
2014
2021
|
let d = maxC - minC;
|
|
2015
2022
|
s = l > .5 ? d / (2 - maxC - minC) : d / (maxC + minC);
|
|
2016
|
-
if (r
|
|
2023
|
+
if (r === maxC)
|
|
2017
2024
|
h = (g - b) / d + (g < b ? 6 : 0);
|
|
2018
|
-
else if (g
|
|
2025
|
+
else if (g === maxC)
|
|
2019
2026
|
h = (b - r) / d + 2;
|
|
2020
|
-
else if (b
|
|
2027
|
+
else if (b === maxC)
|
|
2021
2028
|
h = (r - g) / d + 4;
|
|
2022
2029
|
}
|
|
2023
2030
|
return [h / 6, s, l, a];
|
|
@@ -2027,7 +2034,7 @@ class Color
|
|
|
2027
2034
|
* @param {number} [amount]
|
|
2028
2035
|
* @param {number} [alphaAmount]
|
|
2029
2036
|
* @return {Color} */
|
|
2030
|
-
mutate(amount=.05, alphaAmount=0)
|
|
2037
|
+
mutate(amount=.05, alphaAmount=0)
|
|
2031
2038
|
{
|
|
2032
2039
|
ASSERT_NUMBER_VALID(amount);
|
|
2033
2040
|
ASSERT_NUMBER_VALID(alphaAmount);
|
|
@@ -2043,47 +2050,47 @@ class Color
|
|
|
2043
2050
|
/** Returns this color expressed as a hex color code
|
|
2044
2051
|
* @param {boolean} [useAlpha] - if alpha should be included in result
|
|
2045
2052
|
* @return {string} */
|
|
2046
|
-
toString(useAlpha = true)
|
|
2053
|
+
toString(useAlpha = true)
|
|
2047
2054
|
{
|
|
2048
|
-
ASSERT(typeof useAlpha
|
|
2055
|
+
ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
|
|
2049
2056
|
if (debug && !this.isValid())
|
|
2050
2057
|
return `#000`;
|
|
2051
2058
|
const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
|
|
2052
2059
|
return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
|
|
2053
2060
|
}
|
|
2054
|
-
|
|
2061
|
+
|
|
2055
2062
|
/** Set this color from a hex code
|
|
2056
2063
|
* @param {string} hex - html hex code
|
|
2057
2064
|
* @return {Color} */
|
|
2058
2065
|
setHex(hex)
|
|
2059
2066
|
{
|
|
2060
|
-
ASSERT(typeof hex
|
|
2067
|
+
ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
|
|
2061
2068
|
ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
|
|
2062
2069
|
|
|
2063
2070
|
if (hex.length < 6)
|
|
2064
2071
|
{
|
|
2065
2072
|
const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
|
|
2066
2073
|
this.r = fromHex(1);
|
|
2067
|
-
this.g = fromHex(2)
|
|
2074
|
+
this.g = fromHex(2);
|
|
2068
2075
|
this.b = fromHex(3);
|
|
2069
|
-
this.a = hex.length
|
|
2076
|
+
this.a = hex.length === 5 ? fromHex(4) : 1;
|
|
2070
2077
|
}
|
|
2071
2078
|
else
|
|
2072
2079
|
{
|
|
2073
2080
|
const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
|
|
2074
2081
|
this.r = fromHex(1);
|
|
2075
|
-
this.g = fromHex(3)
|
|
2082
|
+
this.g = fromHex(3);
|
|
2076
2083
|
this.b = fromHex(5);
|
|
2077
|
-
this.a = hex.length
|
|
2084
|
+
this.a = hex.length === 9 ? fromHex(7) : 1;
|
|
2078
2085
|
}
|
|
2079
2086
|
|
|
2080
2087
|
ASSERT_COLOR_VALID(this);
|
|
2081
2088
|
return this;
|
|
2082
2089
|
}
|
|
2083
|
-
|
|
2090
|
+
|
|
2084
2091
|
/** Returns this color expressed as 32 bit RGBA value
|
|
2085
2092
|
* @return {number} */
|
|
2086
|
-
rgbaInt()
|
|
2093
|
+
rgbaInt()
|
|
2087
2094
|
{
|
|
2088
2095
|
const r = clamp(this.r)*255|0;
|
|
2089
2096
|
const g = clamp(this.g)*255<<8;
|
|
@@ -2104,7 +2111,7 @@ class Color
|
|
|
2104
2111
|
/** Color - White #ffffff
|
|
2105
2112
|
* @type {Color}
|
|
2106
2113
|
* @memberof Utilities */
|
|
2107
|
-
const WHITE = rgb();
|
|
2114
|
+
const WHITE = rgb();
|
|
2108
2115
|
|
|
2109
2116
|
/** Color - Clear White #ffffff with 0 alpha
|
|
2110
2117
|
* @type {Color}
|
|
@@ -2219,11 +2226,11 @@ class Timer
|
|
|
2219
2226
|
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
2220
2227
|
* @return {number} */
|
|
2221
2228
|
getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
|
|
2222
|
-
|
|
2229
|
+
|
|
2223
2230
|
/** Returns this timer expressed as a string
|
|
2224
2231
|
* @return {string} */
|
|
2225
2232
|
toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
|
|
2226
|
-
|
|
2233
|
+
|
|
2227
2234
|
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
2228
2235
|
* @return {number} */
|
|
2229
2236
|
valueOf() { return this.get(); }
|
|
@@ -2259,7 +2266,7 @@ let cameraScale = 32;
|
|
|
2259
2266
|
// Display settings
|
|
2260
2267
|
|
|
2261
2268
|
/** Enable applying color to tiles when using canvas2d
|
|
2262
|
-
* - This is slower but should be the same as
|
|
2269
|
+
* - This is slower but should be the same as WebGL rendering
|
|
2263
2270
|
* @type {boolean}
|
|
2264
2271
|
* @default
|
|
2265
2272
|
* @memberof Settings */
|
|
@@ -2305,14 +2312,13 @@ let tilesPixelated = true;
|
|
|
2305
2312
|
* @memberof Settings */
|
|
2306
2313
|
let fontDefault = 'arial';
|
|
2307
2314
|
|
|
2308
|
-
/** Enable to show the LittleJS splash screen
|
|
2315
|
+
/** Enable to show the LittleJS splash screen on startup
|
|
2309
2316
|
* @type {boolean}
|
|
2310
2317
|
* @default
|
|
2311
2318
|
* @memberof Settings */
|
|
2312
2319
|
let showSplashScreen = false;
|
|
2313
2320
|
|
|
2314
2321
|
/** Disables all rendering, audio, and input for servers
|
|
2315
|
-
* - Must be set before startup to take effect
|
|
2316
2322
|
* @type {boolean}
|
|
2317
2323
|
* @default
|
|
2318
2324
|
* @memberof Settings */
|
|
@@ -2321,13 +2327,18 @@ let headlessMode = false;
|
|
|
2321
2327
|
///////////////////////////////////////////////////////////////////////////////
|
|
2322
2328
|
// WebGL settings
|
|
2323
2329
|
|
|
2324
|
-
/** Enable
|
|
2325
|
-
* - Must be set before startup to take effect
|
|
2330
|
+
/** Enable WebGL accelerated rendering
|
|
2326
2331
|
* @type {boolean}
|
|
2327
2332
|
* @default
|
|
2328
2333
|
* @memberof Settings */
|
|
2329
2334
|
let glEnable = true;
|
|
2330
2335
|
|
|
2336
|
+
/** How many sided poly to use when drawing circles and ellipses with WebGL
|
|
2337
|
+
* @type {number}
|
|
2338
|
+
* @default
|
|
2339
|
+
* @memberof Settings */
|
|
2340
|
+
let glCircleSides = 32;
|
|
2341
|
+
|
|
2331
2342
|
///////////////////////////////////////////////////////////////////////////////
|
|
2332
2343
|
// Tile sheet settings
|
|
2333
2344
|
|
|
@@ -2409,13 +2420,13 @@ let particleEmitRateScale = 1;
|
|
|
2409
2420
|
* @memberof Settings */
|
|
2410
2421
|
let gamepadsEnable = true;
|
|
2411
2422
|
|
|
2412
|
-
/** If true, the dpad input is also routed to the left analog stick (for better
|
|
2423
|
+
/** If true, the dpad input is also routed to the left analog stick (for better accessibility)
|
|
2413
2424
|
* @type {boolean}
|
|
2414
2425
|
* @default
|
|
2415
2426
|
* @memberof Settings */
|
|
2416
2427
|
let gamepadDirectionEmulateStick = true;
|
|
2417
2428
|
|
|
2418
|
-
/** If true the WASD keys are also routed to the direction keys (for better
|
|
2429
|
+
/** If true the WASD keys are also routed to the direction keys (for better accessibility)
|
|
2419
2430
|
* @type {boolean}
|
|
2420
2431
|
* @default
|
|
2421
2432
|
* @memberof Settings */
|
|
@@ -2423,7 +2434,6 @@ let inputWASDEmulateDirection = true;
|
|
|
2423
2434
|
|
|
2424
2435
|
/** True if touch input is enabled for mobile devices
|
|
2425
2436
|
* - Touch events will be routed to mouse events
|
|
2426
|
-
* - Must be set before startup to take effect
|
|
2427
2437
|
* @type {boolean}
|
|
2428
2438
|
* @default
|
|
2429
2439
|
* @memberof Settings */
|
|
@@ -2431,7 +2441,6 @@ let touchInputEnable = true;
|
|
|
2431
2441
|
|
|
2432
2442
|
/** True if touch gamepad should appear on mobile devices
|
|
2433
2443
|
* - Supports left analog stick, 4 face buttons and start button (button 9)
|
|
2434
|
-
* - Must be set before startup to take effect
|
|
2435
2444
|
* @type {boolean}
|
|
2436
2445
|
* @default
|
|
2437
2446
|
* @memberof Settings */
|
|
@@ -2534,7 +2543,7 @@ function setCameraAngle(angle) { cameraAngle = angle; }
|
|
|
2534
2543
|
function setCameraScale(scale) { cameraScale = scale; }
|
|
2535
2544
|
|
|
2536
2545
|
/** Set if tiles should be colorized when using canvas2d
|
|
2537
|
-
* This can be slower but results should look nearly identical to
|
|
2546
|
+
* This can be slower but results should look nearly identical to WebGL rendering
|
|
2538
2547
|
* It can be enabled/disabled at any time
|
|
2539
2548
|
* Optimized for performance, and will use faster method if color is white or untextured
|
|
2540
2549
|
* @param {boolean} colorTiles
|
|
@@ -2570,8 +2579,8 @@ function setCanvasPixelated(pixelated)
|
|
|
2570
2579
|
* @param {boolean} pixelated
|
|
2571
2580
|
* @memberof Settings */
|
|
2572
2581
|
function setOverlayCanvasPixelated(pixelated)
|
|
2573
|
-
{
|
|
2574
|
-
overlayCanvasPixelated = pixelated;
|
|
2582
|
+
{
|
|
2583
|
+
overlayCanvasPixelated = pixelated;
|
|
2575
2584
|
if (overlayCanvas)
|
|
2576
2585
|
overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
|
|
2577
2586
|
}
|
|
@@ -2586,7 +2595,7 @@ function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
|
|
|
2586
2595
|
* @memberof Settings */
|
|
2587
2596
|
function setFontDefault(font) { fontDefault = font; }
|
|
2588
2597
|
|
|
2589
|
-
/** Set if the LittleJS splash screen be shown on startup
|
|
2598
|
+
/** Set if the LittleJS splash screen should be shown on startup
|
|
2590
2599
|
* @param {boolean} show
|
|
2591
2600
|
* @memberof Settings */
|
|
2592
2601
|
function setShowSplashScreen(show) { showSplashScreen = show; }
|
|
@@ -2596,16 +2605,21 @@ function setShowSplashScreen(show) { showSplashScreen = show; }
|
|
|
2596
2605
|
* @memberof Settings */
|
|
2597
2606
|
function setHeadlessMode(headless) { headlessMode = headless; }
|
|
2598
2607
|
|
|
2599
|
-
/** Set if
|
|
2608
|
+
/** Set if WebGL rendering is enabled
|
|
2600
2609
|
* @param {boolean} enable
|
|
2601
2610
|
* @memberof Settings */
|
|
2602
2611
|
function setGLEnable(enable)
|
|
2603
2612
|
{
|
|
2604
2613
|
glEnable = enable;
|
|
2605
|
-
if (glCanvas) // hide glCanvas if
|
|
2614
|
+
if (glCanvas) // hide glCanvas if WebGL is disabled
|
|
2606
2615
|
glCanvas.style.visibility = enable ? 'visible' : 'hidden';
|
|
2607
2616
|
}
|
|
2608
2617
|
|
|
2618
|
+
/** Set how many sided polygons to use when drawing circles and elipses with WebGL
|
|
2619
|
+
* @param {number} sides
|
|
2620
|
+
* @memberof Settings */
|
|
2621
|
+
function setGLCircleSides(sides) { glCircleSides = sides; }
|
|
2622
|
+
|
|
2609
2623
|
/** Set default size of tiles in pixels
|
|
2610
2624
|
* @param {Vector2} size
|
|
2611
2625
|
* @memberof Settings */
|
|
@@ -2636,7 +2650,7 @@ function setObjectDefaultDamping(damp) { objectDefaultDamping = damp; }
|
|
|
2636
2650
|
* @memberof Settings */
|
|
2637
2651
|
function setObjectDefaultAngleDamping(damp) { objectDefaultAngleDamping = damp; }
|
|
2638
2652
|
|
|
2639
|
-
/** Set how much to bounce when a collision
|
|
2653
|
+
/** Set how much to bounce when a collision occurs
|
|
2640
2654
|
* @param {number} restitution
|
|
2641
2655
|
* @memberof Settings */
|
|
2642
2656
|
function setObjectDefaultRestitution(restitution) { objectDefaultRestitution = restitution; }
|
|
@@ -2760,11 +2774,11 @@ function setShowWatermark(show) { showWatermark = show; }
|
|
|
2760
2774
|
* @param {string} key
|
|
2761
2775
|
* @memberof Debug */
|
|
2762
2776
|
function setDebugKey(key) { debugKey = key; }
|
|
2763
|
-
/**
|
|
2777
|
+
/**
|
|
2764
2778
|
* LittleJS Object System
|
|
2765
2779
|
*/
|
|
2766
2780
|
|
|
2767
|
-
/**
|
|
2781
|
+
/**
|
|
2768
2782
|
* LittleJS Object Base Object Class
|
|
2769
2783
|
* - Top level object class used by the engine
|
|
2770
2784
|
* - Automatically adds self to object list
|
|
@@ -2787,7 +2801,7 @@ function setDebugKey(key) { debugKey = key; }
|
|
|
2787
2801
|
* @example
|
|
2788
2802
|
* // create an engine object, normally you would first extend the class with your own
|
|
2789
2803
|
* const pos = vec2(2,3);
|
|
2790
|
-
* const object = new EngineObject(pos);
|
|
2804
|
+
* const object = new EngineObject(pos);
|
|
2791
2805
|
*/
|
|
2792
2806
|
class EngineObject
|
|
2793
2807
|
{
|
|
@@ -2805,9 +2819,9 @@ class EngineObject
|
|
|
2805
2819
|
ASSERT(isVector2(pos) && pos.isValid(), 'object pos should be a vec2');
|
|
2806
2820
|
ASSERT(isVector2(size) && size.isValid(), 'object size should be a vec2');
|
|
2807
2821
|
ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
|
|
2808
|
-
ASSERT(typeof angle
|
|
2822
|
+
ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
|
|
2809
2823
|
ASSERT(isColor(color) && color.isValid(), 'object color should be a valid rgba color');
|
|
2810
|
-
ASSERT(typeof renderOrder
|
|
2824
|
+
ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
|
|
2811
2825
|
|
|
2812
2826
|
/** @property {Vector2} - World space position of the object */
|
|
2813
2827
|
this.pos = pos.copy();
|
|
@@ -2875,7 +2889,7 @@ class EngineObject
|
|
|
2875
2889
|
// add to list of objects
|
|
2876
2890
|
engineObjects.push(this);
|
|
2877
2891
|
}
|
|
2878
|
-
|
|
2892
|
+
|
|
2879
2893
|
/** Update the object transform, called automatically by engine even when paused */
|
|
2880
2894
|
updateTransforms()
|
|
2881
2895
|
{
|
|
@@ -2944,7 +2958,7 @@ class EngineObject
|
|
|
2944
2958
|
for (const o of engineObjectsCollide)
|
|
2945
2959
|
{
|
|
2946
2960
|
// non solid objects don't collide with each other
|
|
2947
|
-
if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o
|
|
2961
|
+
if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o === this)
|
|
2948
2962
|
continue;
|
|
2949
2963
|
|
|
2950
2964
|
// check collision
|
|
@@ -2967,7 +2981,7 @@ class EngineObject
|
|
|
2967
2981
|
this.velocity = this.velocity.add(velocity);
|
|
2968
2982
|
if (o.mass) // push away if not fixed
|
|
2969
2983
|
o.velocity = o.velocity.subtract(velocity);
|
|
2970
|
-
|
|
2984
|
+
|
|
2971
2985
|
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
|
|
2972
2986
|
continue;
|
|
2973
2987
|
}
|
|
@@ -2978,7 +2992,7 @@ class EngineObject
|
|
|
2978
2992
|
const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
|
|
2979
2993
|
const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
|
|
2980
2994
|
const restitution = max(this.restitution, o.restitution);
|
|
2981
|
-
|
|
2995
|
+
|
|
2982
2996
|
if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
|
|
2983
2997
|
{
|
|
2984
2998
|
// push outside object collision
|
|
@@ -3066,7 +3080,7 @@ class EngineObject
|
|
|
3066
3080
|
{
|
|
3067
3081
|
// move to previous position
|
|
3068
3082
|
this.pos.y = oldPos.y;
|
|
3069
|
-
this.groundObject = undefined;
|
|
3083
|
+
this.groundObject = undefined;
|
|
3070
3084
|
}
|
|
3071
3085
|
}
|
|
3072
3086
|
if (blockedLayerX)
|
|
@@ -3080,20 +3094,20 @@ class EngineObject
|
|
|
3080
3094
|
}
|
|
3081
3095
|
}
|
|
3082
3096
|
}
|
|
3083
|
-
|
|
3097
|
+
|
|
3084
3098
|
/** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
|
|
3085
3099
|
render()
|
|
3086
3100
|
{
|
|
3087
3101
|
// default object render
|
|
3088
3102
|
drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
|
|
3089
3103
|
}
|
|
3090
|
-
|
|
3104
|
+
|
|
3091
3105
|
/** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
|
|
3092
3106
|
destroy()
|
|
3093
|
-
{
|
|
3107
|
+
{
|
|
3094
3108
|
if (this.destroyed)
|
|
3095
3109
|
return;
|
|
3096
|
-
|
|
3110
|
+
|
|
3097
3111
|
// disconnect from parent and destroy children
|
|
3098
3112
|
this.destroyed = 1;
|
|
3099
3113
|
this.parent && this.parent.removeChild(this);
|
|
@@ -3119,7 +3133,7 @@ class EngineObject
|
|
|
3119
3133
|
/** Convert from world space to local space for a vector (rotation only)
|
|
3120
3134
|
* @param {Vector2} vec - world space vector */
|
|
3121
3135
|
worldToLocalVector(vec) { return vec.rotate(-this.angle); }
|
|
3122
|
-
|
|
3136
|
+
|
|
3123
3137
|
/** Called to check if a tile collision should be resolved
|
|
3124
3138
|
* @param {number} tileData - the value of the tile at the position
|
|
3125
3139
|
* @param {Vector2} pos - tile where the collision occurred
|
|
@@ -3138,16 +3152,19 @@ class EngineObject
|
|
|
3138
3152
|
|
|
3139
3153
|
/** Apply acceleration to this object (adjust velocity, not affected by mass)
|
|
3140
3154
|
* @param {Vector2} acceleration */
|
|
3141
|
-
applyAcceleration(acceleration)
|
|
3155
|
+
applyAcceleration(acceleration)
|
|
3156
|
+
{ if (this.mass) this.velocity = this.velocity.add(acceleration); }
|
|
3142
3157
|
|
|
3143
|
-
/** Apply angular acceleration to this object
|
|
3158
|
+
/** Apply angular acceleration to this object
|
|
3144
3159
|
* @param {number} acceleration */
|
|
3145
|
-
applyAngularAcceleration(acceleration)
|
|
3160
|
+
applyAngularAcceleration(acceleration)
|
|
3161
|
+
{ if (this.mass) this.angleVelocity += acceleration; }
|
|
3146
3162
|
|
|
3147
3163
|
/** Apply force to this object (adjust velocity, affected by mass)
|
|
3148
3164
|
* @param {Vector2} force */
|
|
3149
|
-
applyForce(force)
|
|
3150
|
-
|
|
3165
|
+
applyForce(force)
|
|
3166
|
+
{ if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
|
|
3167
|
+
|
|
3151
3168
|
/** Get the direction of the mirror
|
|
3152
3169
|
* @return {number} -1 if this.mirror is true, or 1 if not mirrored */
|
|
3153
3170
|
getMirrorSign() { return this.mirror ? -1 : 1; }
|
|
@@ -3169,7 +3186,7 @@ class EngineObject
|
|
|
3169
3186
|
* @param {EngineObject} child */
|
|
3170
3187
|
removeChild(child)
|
|
3171
3188
|
{
|
|
3172
|
-
ASSERT(child.parent
|
|
3189
|
+
ASSERT(child.parent === this && this.children.includes(child));
|
|
3173
3190
|
this.children.splice(this.children.indexOf(child), 1);
|
|
3174
3191
|
child.parent = 0;
|
|
3175
3192
|
}
|
|
@@ -3215,7 +3232,7 @@ class EngineObject
|
|
|
3215
3232
|
{
|
|
3216
3233
|
if (!debug)
|
|
3217
3234
|
return;
|
|
3218
|
-
|
|
3235
|
+
|
|
3219
3236
|
// show object info for debugging
|
|
3220
3237
|
const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
|
|
3221
3238
|
const color = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
|
|
@@ -3225,24 +3242,24 @@ class EngineObject
|
|
|
3225
3242
|
this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(1,1,1,.5));
|
|
3226
3243
|
}
|
|
3227
3244
|
}
|
|
3228
|
-
/**
|
|
3245
|
+
/**
|
|
3229
3246
|
* LittleJS Drawing System
|
|
3230
3247
|
* - Hybrid system with both Canvas2D and WebGL available
|
|
3231
3248
|
* - Super fast tile sheet rendering with WebGL
|
|
3232
3249
|
* - Can apply rotation, mirror, color and additive color
|
|
3233
3250
|
* - Font rendering system with built in engine font
|
|
3234
3251
|
* - Many useful utility functions
|
|
3235
|
-
*
|
|
3252
|
+
*
|
|
3236
3253
|
* LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
|
|
3237
3254
|
* There are 3 canvas/contexts available to draw to...
|
|
3238
3255
|
* mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
|
|
3239
3256
|
* glCanvas - Used by the accelerated WebGL batch rendering system.
|
|
3240
3257
|
* overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
|
|
3241
|
-
*
|
|
3258
|
+
*
|
|
3242
3259
|
* The WebGL rendering system is very fast with some caveats...
|
|
3243
3260
|
* - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
|
|
3244
3261
|
* - Group additive rendering together using renderOrder to mitigate this issue
|
|
3245
|
-
*
|
|
3262
|
+
*
|
|
3246
3263
|
* The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
|
|
3247
3264
|
* @namespace Draw
|
|
3248
3265
|
*/
|
|
@@ -3287,7 +3304,7 @@ let workCanvas;
|
|
|
3287
3304
|
* @memberof Draw */
|
|
3288
3305
|
let workContext;
|
|
3289
3306
|
|
|
3290
|
-
/** The size of the main canvas (and other secondary canvases)
|
|
3307
|
+
/** The size of the main canvas (and other secondary canvases)
|
|
3291
3308
|
* @type {Vector2}
|
|
3292
3309
|
* @memberof Draw */
|
|
3293
3310
|
let mainCanvasSize = vec2();
|
|
@@ -3302,7 +3319,7 @@ let drawCount;
|
|
|
3302
3319
|
|
|
3303
3320
|
///////////////////////////////////////////////////////////////////////////////
|
|
3304
3321
|
|
|
3305
|
-
/**
|
|
3322
|
+
/**
|
|
3306
3323
|
* Create a tile info object using a grid based system
|
|
3307
3324
|
* - This can take vecs or floats for easier use and conversion
|
|
3308
3325
|
* - If an index is passed in, the tile size and index will determine the position
|
|
@@ -3316,15 +3333,14 @@ let drawCount;
|
|
|
3316
3333
|
* tile(5, 8) // a tile at index 5 using a tile size of 8
|
|
3317
3334
|
* tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
|
|
3318
3335
|
* tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
|
|
3319
|
-
* @memberof Draw
|
|
3320
|
-
*/
|
|
3336
|
+
* @memberof Draw */
|
|
3321
3337
|
function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
|
|
3322
3338
|
{
|
|
3323
3339
|
if (headlessMode)
|
|
3324
3340
|
return new TileInfo;
|
|
3325
3341
|
|
|
3326
3342
|
// if size is a number, make it a vector
|
|
3327
|
-
if (typeof size
|
|
3343
|
+
if (typeof size === 'number')
|
|
3328
3344
|
{
|
|
3329
3345
|
ASSERT(size > 0);
|
|
3330
3346
|
size = new Vector2(size, size);
|
|
@@ -3333,24 +3349,24 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
|
|
|
3333
3349
|
// create tile info object
|
|
3334
3350
|
const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
|
|
3335
3351
|
|
|
3336
|
-
//
|
|
3352
|
+
// get the position of the tile
|
|
3337
3353
|
const textureInfo = textureInfos[textureIndex];
|
|
3338
3354
|
ASSERT(!!textureInfo, 'Texture not loaded');
|
|
3339
3355
|
const sizePaddedX = size.x + padding*2;
|
|
3340
3356
|
const sizePaddedY = size.y + padding*2;
|
|
3341
|
-
if (typeof pos
|
|
3357
|
+
if (typeof pos === 'number')
|
|
3342
3358
|
{
|
|
3343
3359
|
const cols = textureInfo.size.x / sizePaddedX |0;
|
|
3344
|
-
ASSERT(cols>0, 'Tile size is too big for texture');
|
|
3360
|
+
ASSERT(cols > 0, 'Tile size is too big for texture');
|
|
3345
3361
|
const posX = pos % cols, posY = (pos / cols) |0;
|
|
3346
3362
|
tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
|
|
3347
3363
|
}
|
|
3348
3364
|
else
|
|
3349
3365
|
tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
|
|
3350
|
-
return tileInfo;
|
|
3366
|
+
return tileInfo;
|
|
3351
3367
|
}
|
|
3352
3368
|
|
|
3353
|
-
/**
|
|
3369
|
+
/**
|
|
3354
3370
|
* Tile Info - Stores info about how to draw a tile
|
|
3355
3371
|
*/
|
|
3356
3372
|
class TileInfo
|
|
@@ -3388,14 +3404,14 @@ class TileInfo
|
|
|
3388
3404
|
*/
|
|
3389
3405
|
frame(frame)
|
|
3390
3406
|
{
|
|
3391
|
-
ASSERT(typeof frame
|
|
3407
|
+
ASSERT(typeof frame === 'number');
|
|
3392
3408
|
return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
|
|
3393
3409
|
}
|
|
3394
3410
|
|
|
3395
3411
|
/**
|
|
3396
3412
|
* Set this tile to use a full image
|
|
3397
3413
|
* @param {HTMLImageElement|OffscreenCanvas} image
|
|
3398
|
-
* @param {WebGLTexture} [glTexture] -
|
|
3414
|
+
* @param {WebGLTexture} [glTexture] - WebGL texture
|
|
3399
3415
|
* @return {TileInfo}
|
|
3400
3416
|
*/
|
|
3401
3417
|
setFullImage(image, glTexture)
|
|
@@ -3413,7 +3429,7 @@ class TextureInfo
|
|
|
3413
3429
|
/**
|
|
3414
3430
|
* Create a TextureInfo, called automatically by the engine
|
|
3415
3431
|
* @param {HTMLImageElement|OffscreenCanvas} image
|
|
3416
|
-
* @param {WebGLTexture} [glTexture] -
|
|
3432
|
+
* @param {WebGLTexture} [glTexture] - WebGL texture
|
|
3417
3433
|
*/
|
|
3418
3434
|
constructor(image, glTexture)
|
|
3419
3435
|
{
|
|
@@ -3423,7 +3439,7 @@ class TextureInfo
|
|
|
3423
3439
|
this.size = vec2(image.width, image.height);
|
|
3424
3440
|
/** @property {Vector2} - inverse of the size, cached for rendering */
|
|
3425
3441
|
this.sizeInverse = vec2(1/image.width, 1/image.height);
|
|
3426
|
-
/** @property {WebGLTexture} -
|
|
3442
|
+
/** @property {WebGLTexture} - WebGL texture */
|
|
3427
3443
|
this.glTexture = glTexture;
|
|
3428
3444
|
}
|
|
3429
3445
|
|
|
@@ -3439,28 +3455,25 @@ class TextureInfo
|
|
|
3439
3455
|
// Drawing functions
|
|
3440
3456
|
|
|
3441
3457
|
/** Draw textured tile centered in world space, with color applied if using WebGL
|
|
3442
|
-
* @param {Vector2}
|
|
3443
|
-
* @param {Vector2}
|
|
3444
|
-
* @param {TileInfo}[tileInfo]
|
|
3445
|
-
* @param {Color}
|
|
3446
|
-
* @param {number}
|
|
3447
|
-
* @param {boolean}
|
|
3448
|
-
* @param {Color}
|
|
3449
|
-
* @param {boolean}
|
|
3450
|
-
* @param {boolean}
|
|
3458
|
+
* @param {Vector2} pos - Center of the tile in world space
|
|
3459
|
+
* @param {Vector2} [size=(1,1)] - Size of the tile in world space
|
|
3460
|
+
* @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
|
|
3461
|
+
* @param {Color} [color=(1,1,1,1)] - Color to modulate with
|
|
3462
|
+
* @param {number} [angle] - Angle to rotate by
|
|
3463
|
+
* @param {boolean} [mirror] - Is image flipped along the Y axis?
|
|
3464
|
+
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
3465
|
+
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
3466
|
+
* @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
|
|
3451
3467
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
3452
3468
|
* @memberof Draw */
|
|
3453
|
-
function drawTile(pos, size=new Vector2(1), tileInfo, color=
|
|
3469
|
+
function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
|
|
3454
3470
|
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
|
|
3455
3471
|
{
|
|
3456
|
-
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3457
3472
|
ASSERT(isVector2(pos) && pos.isValid(), 'drawTile pos should be a vec2');
|
|
3458
3473
|
ASSERT(isVector2(size) && size.isValid(), 'drawTile size should be a vec2');
|
|
3459
3474
|
ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
|
|
3460
3475
|
ASSERT(isNumber(angle), 'drawTile angle should be a number');
|
|
3461
|
-
|
|
3462
|
-
if (color.a <= 0 && (!additiveColor || additiveColor.a <= 0) || !size.x || !size.y)
|
|
3463
|
-
return; // completely invisible, skip render
|
|
3476
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3464
3477
|
|
|
3465
3478
|
const textureInfo = tileInfo && tileInfo.textureInfo;
|
|
3466
3479
|
if (useWebGL)
|
|
@@ -3484,22 +3497,22 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
|
|
|
3484
3497
|
{
|
|
3485
3498
|
const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
|
|
3486
3499
|
const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
|
|
3487
|
-
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
3488
|
-
x + tileImageFixBleedX, y + tileImageFixBleedY,
|
|
3489
|
-
x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
|
|
3490
|
-
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
3500
|
+
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
3501
|
+
x + tileImageFixBleedX, y + tileImageFixBleedY,
|
|
3502
|
+
x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
|
|
3503
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
3491
3504
|
}
|
|
3492
3505
|
else
|
|
3493
3506
|
{
|
|
3494
|
-
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
3495
|
-
x, y, x + w, y + h,
|
|
3496
|
-
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
3507
|
+
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
3508
|
+
x, y, x + w, y + h,
|
|
3509
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
3497
3510
|
}
|
|
3498
3511
|
}
|
|
3499
3512
|
else
|
|
3500
3513
|
{
|
|
3501
3514
|
// if no tile info, force untextured
|
|
3502
|
-
glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
|
|
3515
|
+
glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
|
|
3503
3516
|
}
|
|
3504
3517
|
}
|
|
3505
3518
|
else
|
|
@@ -3537,8 +3550,8 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
|
|
|
3537
3550
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3538
3551
|
* @memberof Draw */
|
|
3539
3552
|
function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
3540
|
-
{
|
|
3541
|
-
drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
|
|
3553
|
+
{
|
|
3554
|
+
drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
|
|
3542
3555
|
}
|
|
3543
3556
|
|
|
3544
3557
|
/** Draw colored line between two points
|
|
@@ -3561,6 +3574,30 @@ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL
|
|
|
3561
3574
|
drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
|
|
3562
3575
|
}
|
|
3563
3576
|
|
|
3577
|
+
/** Draw colored regular polygon using passed in number of sides
|
|
3578
|
+
* @param {Vector2} pos
|
|
3579
|
+
* @param {Vector2} [size=(1,1)]
|
|
3580
|
+
* @param {number} [sides]
|
|
3581
|
+
* @param {Color} [color=(1,1,1,1)]
|
|
3582
|
+
* @param {number} [angle]
|
|
3583
|
+
* @param {number} [lineWidth]
|
|
3584
|
+
* @param {Color} [lineColor=(0,0,0,1)]
|
|
3585
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3586
|
+
* @param {boolean} [screenSpace]
|
|
3587
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3588
|
+
* @memberof Draw */
|
|
3589
|
+
function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
3590
|
+
{
|
|
3591
|
+
// build regular polygon points
|
|
3592
|
+
const points = [];
|
|
3593
|
+
for (let i=sides; i--;)
|
|
3594
|
+
{
|
|
3595
|
+
const a = (i/sides)*PI*2;
|
|
3596
|
+
points.push(vec2(Math.sin(a)*size.x, Math.cos(a)*size.y));
|
|
3597
|
+
}
|
|
3598
|
+
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3564
3601
|
/** Draw colored polygon using passed in points
|
|
3565
3602
|
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
3566
3603
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -3568,33 +3605,49 @@ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL
|
|
|
3568
3605
|
* @param {Color} [lineColor=(0,0,0,1)]
|
|
3569
3606
|
* @param {Vector2} [pos=(0,0)] - Offset to apply
|
|
3570
3607
|
* @param {number} [angle] - Angle to rotate by
|
|
3571
|
-
* @param {boolean} [useWebGL]
|
|
3608
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3572
3609
|
* @param {boolean} [screenSpace]
|
|
3573
|
-
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context
|
|
3610
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3574
3611
|
* @memberof Draw */
|
|
3575
|
-
function drawPoly(points, color=
|
|
3612
|
+
function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
|
|
3576
3613
|
{
|
|
3577
3614
|
ASSERT(isVector2(pos) && pos.isValid(), 'drawPoly pos should be a vec2');
|
|
3578
3615
|
ASSERT(Array.isArray(points), 'drawPoly points should be an array');
|
|
3579
3616
|
ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
|
|
3580
3617
|
ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
|
|
3581
3618
|
ASSERT(isNumber(angle), 'drawPoly angle should be a number');
|
|
3582
|
-
ASSERT(!useWebGL, '
|
|
3583
|
-
|
|
3619
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3620
|
+
if (useWebGL)
|
|
3584
3621
|
{
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
context.lineTo(point.x, point.y);
|
|
3588
|
-
context.closePath();
|
|
3589
|
-
context.fillStyle = color.toString();
|
|
3590
|
-
context.fill();
|
|
3591
|
-
if (lineWidth)
|
|
3622
|
+
let scale = 1;
|
|
3623
|
+
if (screenSpace)
|
|
3592
3624
|
{
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3625
|
+
// convert to world space
|
|
3626
|
+
pos = screenToWorld(pos);
|
|
3627
|
+
scale = 1/cameraScale;
|
|
3596
3628
|
}
|
|
3597
|
-
|
|
3629
|
+
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
|
|
3630
|
+
if (lineWidth > 0)
|
|
3631
|
+
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, scale, scale, angle);
|
|
3632
|
+
}
|
|
3633
|
+
else
|
|
3634
|
+
{
|
|
3635
|
+
drawCanvas2D(pos, vec2(1), angle, false, context=>
|
|
3636
|
+
{
|
|
3637
|
+
context.fillStyle = color.toString();
|
|
3638
|
+
context.beginPath();
|
|
3639
|
+
for (const point of points)
|
|
3640
|
+
context.lineTo(point.x, point.y);
|
|
3641
|
+
context.closePath();
|
|
3642
|
+
context.fill();
|
|
3643
|
+
if (lineWidth)
|
|
3644
|
+
{
|
|
3645
|
+
context.strokeStyle = lineColor.toString();
|
|
3646
|
+
context.lineWidth = lineWidth;
|
|
3647
|
+
context.stroke();
|
|
3648
|
+
}
|
|
3649
|
+
}, screenSpace, context);
|
|
3650
|
+
}
|
|
3598
3651
|
}
|
|
3599
3652
|
|
|
3600
3653
|
/** Draw colored ellipse using passed in point
|
|
@@ -3604,32 +3657,41 @@ function drawPoly(points, color=new Color, lineWidth=0, lineColor=BLACK, pos=vec
|
|
|
3604
3657
|
* @param {number} [angle]
|
|
3605
3658
|
* @param {number} [lineWidth]
|
|
3606
3659
|
* @param {Color} [lineColor=(0,0,0,1)]
|
|
3607
|
-
* @param {boolean} [useWebGL]
|
|
3660
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3608
3661
|
* @param {boolean} [screenSpace]
|
|
3609
|
-
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context
|
|
3662
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3610
3663
|
* @memberof Draw */
|
|
3611
|
-
function drawEllipse(pos, size=vec2(1), color=
|
|
3664
|
+
function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
|
|
3612
3665
|
{
|
|
3613
3666
|
ASSERT(isVector2(pos) && pos.isValid(), 'drawEllipse pos should be a vec2');
|
|
3614
3667
|
ASSERT(isVector2(size) && size.isValid(), 'drawEllipse size should be a vec2');
|
|
3615
3668
|
ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
|
|
3616
3669
|
ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
|
|
3617
3670
|
ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
|
|
3618
|
-
ASSERT(lineWidth>=0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
|
|
3619
|
-
ASSERT(!useWebGL, '
|
|
3620
|
-
|
|
3671
|
+
ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
|
|
3672
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3673
|
+
if (useWebGL)
|
|
3621
3674
|
{
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3675
|
+
// draw as a regular polygon
|
|
3676
|
+
const sides = glCircleSides;
|
|
3677
|
+
drawRegularPoly(pos, size, sides, color, lineWidth, lineColor, angle, useWebGL, screenSpace, context);
|
|
3678
|
+
}
|
|
3679
|
+
else
|
|
3680
|
+
{
|
|
3681
|
+
drawCanvas2D(pos, vec2(1), angle, false, context=>
|
|
3627
3682
|
{
|
|
3628
|
-
context.
|
|
3629
|
-
context.
|
|
3630
|
-
context.
|
|
3631
|
-
|
|
3632
|
-
|
|
3683
|
+
context.fillStyle = color.toString();
|
|
3684
|
+
context.beginPath();
|
|
3685
|
+
context.ellipse(0, 0, size.x, size.y, 0, 0, 9);
|
|
3686
|
+
context.fill();
|
|
3687
|
+
if (lineWidth)
|
|
3688
|
+
{
|
|
3689
|
+
context.strokeStyle = lineColor.toString();
|
|
3690
|
+
context.lineWidth = lineWidth;
|
|
3691
|
+
context.stroke();
|
|
3692
|
+
}
|
|
3693
|
+
}, screenSpace, context);
|
|
3694
|
+
}
|
|
3633
3695
|
}
|
|
3634
3696
|
|
|
3635
3697
|
/** Draw colored circle using passed in point
|
|
@@ -3638,11 +3700,11 @@ function drawEllipse(pos, size=vec2(1), color=new Color, angle=0, lineWidth=0, l
|
|
|
3638
3700
|
* @param {Color} [color=(1,1,1,1)]
|
|
3639
3701
|
* @param {number} [lineWidth=0]
|
|
3640
3702
|
* @param {Color} [lineColor=(0,0,0,1)]
|
|
3641
|
-
* @param {boolean} [useWebGL]
|
|
3642
|
-
* @param {boolean} [screenSpace
|
|
3643
|
-
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context
|
|
3703
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3704
|
+
* @param {boolean} [screenSpace]
|
|
3705
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3644
3706
|
* @memberof Draw */
|
|
3645
|
-
function drawCircle(pos, radius=1, color=
|
|
3707
|
+
function drawCircle(pos, radius=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
|
|
3646
3708
|
{ drawEllipse(pos, vec2(radius), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context); }
|
|
3647
3709
|
|
|
3648
3710
|
/** Draw directly to a 2d canvas context in world space
|
|
@@ -3651,7 +3713,7 @@ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=BLACK
|
|
|
3651
3713
|
* @param {number} angle
|
|
3652
3714
|
* @param {boolean} [mirror]
|
|
3653
3715
|
* @param {Function} [drawFunction]
|
|
3654
|
-
* @param {boolean}
|
|
3716
|
+
* @param {boolean} [screenSpace=false]
|
|
3655
3717
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
3656
3718
|
* @memberof Draw */
|
|
3657
3719
|
function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
|
|
@@ -3721,7 +3783,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
3721
3783
|
* @param {number} [maxWidth]
|
|
3722
3784
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
|
|
3723
3785
|
* @memberof Draw */
|
|
3724
|
-
function drawTextScreen(text, pos, size=1, color=
|
|
3786
|
+
function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
|
|
3725
3787
|
{
|
|
3726
3788
|
context.fillStyle = color.toString();
|
|
3727
3789
|
context.strokeStyle = lineColor.toString();
|
|
@@ -3753,7 +3815,7 @@ function screenToWorld(screenPos)
|
|
|
3753
3815
|
{
|
|
3754
3816
|
let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
|
|
3755
3817
|
let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
|
|
3756
|
-
if (cameraAngle)
|
|
3818
|
+
if (cameraAngle)
|
|
3757
3819
|
{
|
|
3758
3820
|
// apply camera rotation
|
|
3759
3821
|
const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
|
|
@@ -3846,7 +3908,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
3846
3908
|
{
|
|
3847
3909
|
// white texture with no additive alpha, no need to tint
|
|
3848
3910
|
context.globalAlpha = color.a;
|
|
3849
|
-
context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3911
|
+
context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3850
3912
|
context.globalAlpha = 1;
|
|
3851
3913
|
}
|
|
3852
3914
|
else
|
|
@@ -3867,7 +3929,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
3867
3929
|
for (let i = 0; i < data.length; ++i)
|
|
3868
3930
|
data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
|
|
3869
3931
|
workContext.putImageData(imageData, 0, 0);
|
|
3870
|
-
context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3932
|
+
context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3871
3933
|
}
|
|
3872
3934
|
else
|
|
3873
3935
|
{
|
|
@@ -3880,7 +3942,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
3880
3942
|
}
|
|
3881
3943
|
workContext.putImageData(imageData, 0, 0);
|
|
3882
3944
|
context.globalAlpha = color.a;
|
|
3883
|
-
context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3945
|
+
context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3884
3946
|
context.globalAlpha = 1;
|
|
3885
3947
|
}
|
|
3886
3948
|
}
|
|
@@ -3907,7 +3969,7 @@ function toggleFullscreen()
|
|
|
3907
3969
|
}
|
|
3908
3970
|
|
|
3909
3971
|
/** Set the cursor style
|
|
3910
|
-
* @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
|
|
3972
|
+
* @param {string} [cursorStyle] - CSS cursor style (auto, none, crosshair, etc)
|
|
3911
3973
|
* @memberof Draw */
|
|
3912
3974
|
function setCursor(cursorStyle = 'auto')
|
|
3913
3975
|
{
|
|
@@ -3919,7 +3981,7 @@ function setCursor(cursorStyle = 'auto')
|
|
|
3919
3981
|
|
|
3920
3982
|
let engineFontImage;
|
|
3921
3983
|
|
|
3922
|
-
/**
|
|
3984
|
+
/**
|
|
3923
3985
|
* Font Image Object - Draw text on a 2D canvas by using characters in an image
|
|
3924
3986
|
* - 96 characters (from space to tilde) are stored in an image
|
|
3925
3987
|
* - Uses a default 8x8 font if none is supplied
|
|
@@ -3927,7 +3989,7 @@ let engineFontImage;
|
|
|
3927
3989
|
* @example
|
|
3928
3990
|
* // use built in font
|
|
3929
3991
|
* const font = new FontImage;
|
|
3930
|
-
*
|
|
3992
|
+
*
|
|
3931
3993
|
* // draw text
|
|
3932
3994
|
* font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
|
|
3933
3995
|
*/
|
|
@@ -3937,7 +3999,6 @@ class FontImage
|
|
|
3937
3999
|
* @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
|
|
3938
4000
|
* @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
|
|
3939
4001
|
* @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
|
|
3940
|
-
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
|
|
3941
4002
|
*/
|
|
3942
4003
|
constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
|
|
3943
4004
|
{
|
|
@@ -3951,7 +4012,6 @@ class FontImage
|
|
|
3951
4012
|
this.image = image || engineFontImage;
|
|
3952
4013
|
this.tileSize = tileSize;
|
|
3953
4014
|
this.paddingSize = paddingSize;
|
|
3954
|
-
this.context = context;
|
|
3955
4015
|
}
|
|
3956
4016
|
|
|
3957
4017
|
/** Draw text in world space using the image font
|
|
@@ -3959,23 +4019,32 @@ class FontImage
|
|
|
3959
4019
|
* @param {Vector2} pos
|
|
3960
4020
|
* @param {number} [scale=.25]
|
|
3961
4021
|
* @param {boolean} [center]
|
|
4022
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}[context=drawContext]
|
|
3962
4023
|
*/
|
|
3963
|
-
drawText(text, pos, scale=1, center)
|
|
4024
|
+
drawText(text, pos, scale=1, center, context=drawContext)
|
|
3964
4025
|
{
|
|
3965
|
-
this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
|
|
4026
|
+
this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
|
|
3966
4027
|
}
|
|
3967
4028
|
|
|
3968
|
-
/** Draw text in
|
|
4029
|
+
/** Draw text on overlay canvas in world space using the image font
|
|
4030
|
+
* @param {string} text
|
|
4031
|
+
* @param {Vector2} pos
|
|
4032
|
+
* @param {number} [scale]
|
|
4033
|
+
* @param {boolean} [center]
|
|
4034
|
+
*/
|
|
4035
|
+
drawTextOverlay(text, pos, scale=4, center)
|
|
4036
|
+
{ this.drawText(text, pos, scale, center, overlayContext); }
|
|
4037
|
+
|
|
4038
|
+
/** Draw text on overlay canvas in screen space using the image font
|
|
3969
4039
|
* @param {string} text
|
|
3970
4040
|
* @param {Vector2} pos
|
|
3971
4041
|
* @param {number} [scale]
|
|
3972
4042
|
* @param {boolean} [center]
|
|
4043
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
3973
4044
|
*/
|
|
3974
|
-
drawTextScreen(text, pos, scale=4, center)
|
|
4045
|
+
drawTextScreen(text, pos, scale=4, center, context=overlayContext)
|
|
3975
4046
|
{
|
|
3976
|
-
const context = this.context;
|
|
3977
4047
|
context.save();
|
|
3978
|
-
|
|
3979
4048
|
const size = this.tileSize;
|
|
3980
4049
|
const drawSize = size.add(this.paddingSize).scale(scale);
|
|
3981
4050
|
const cols = this.image.width / this.tileSize.x |0;
|
|
@@ -3994,15 +4063,14 @@ class FontImage
|
|
|
3994
4063
|
const x = tile % cols;
|
|
3995
4064
|
const y = tile / cols |0;
|
|
3996
4065
|
const drawPos = pos.add(vec2(j,i).multiply(drawSize));
|
|
3997
|
-
context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
|
|
4066
|
+
context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
|
|
3998
4067
|
drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
|
|
3999
4068
|
}
|
|
4000
4069
|
});
|
|
4001
|
-
|
|
4002
4070
|
context.restore();
|
|
4003
4071
|
}
|
|
4004
4072
|
}
|
|
4005
|
-
/**
|
|
4073
|
+
/**
|
|
4006
4074
|
* LittleJS Input System
|
|
4007
4075
|
* - Tracks keyboard down, pressed, and released
|
|
4008
4076
|
* - Tracks mouse buttons, position, and wheel
|
|
@@ -4018,10 +4086,10 @@ class FontImage
|
|
|
4018
4086
|
* @return {boolean}
|
|
4019
4087
|
* @memberof Input */
|
|
4020
4088
|
function keyIsDown(key, device=0)
|
|
4021
|
-
{
|
|
4089
|
+
{
|
|
4022
4090
|
ASSERT(key !== undefined, 'key is undefined');
|
|
4023
|
-
ASSERT(device > 0 || typeof key
|
|
4024
|
-
return inputData[device] && !!(inputData[device][key] & 1);
|
|
4091
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4092
|
+
return inputData[device] && !!(inputData[device][key] & 1);
|
|
4025
4093
|
}
|
|
4026
4094
|
|
|
4027
4095
|
/** Returns true if device key was pressed this frame
|
|
@@ -4030,10 +4098,10 @@ function keyIsDown(key, device=0)
|
|
|
4030
4098
|
* @return {boolean}
|
|
4031
4099
|
* @memberof Input */
|
|
4032
4100
|
function keyWasPressed(key, device=0)
|
|
4033
|
-
{
|
|
4101
|
+
{
|
|
4034
4102
|
ASSERT(key !== undefined, 'key is undefined');
|
|
4035
|
-
ASSERT(device > 0 || typeof key
|
|
4036
|
-
return inputData[device] && !!(inputData[device][key] & 2);
|
|
4103
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4104
|
+
return inputData[device] && !!(inputData[device][key] & 2);
|
|
4037
4105
|
}
|
|
4038
4106
|
|
|
4039
4107
|
/** Returns true if device key was released this frame
|
|
@@ -4042,9 +4110,9 @@ function keyWasPressed(key, device=0)
|
|
|
4042
4110
|
* @return {boolean}
|
|
4043
4111
|
* @memberof Input */
|
|
4044
4112
|
function keyWasReleased(key, device=0)
|
|
4045
|
-
{
|
|
4113
|
+
{
|
|
4046
4114
|
ASSERT(key !== undefined, 'key is undefined');
|
|
4047
|
-
ASSERT(device > 0 || typeof key
|
|
4115
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4048
4116
|
return inputData[device] && !!(inputData[device][key] & 4);
|
|
4049
4117
|
}
|
|
4050
4118
|
|
|
@@ -4166,7 +4234,7 @@ function gamepadWasReleased(button, gamepad=0)
|
|
|
4166
4234
|
* @param {number} [gamepad]
|
|
4167
4235
|
* @return {Vector2}
|
|
4168
4236
|
* @memberof Input */
|
|
4169
|
-
function gamepadStick(stick,
|
|
4237
|
+
function gamepadStick(stick, gamepad=0)
|
|
4170
4238
|
{ return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
|
|
4171
4239
|
|
|
4172
4240
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -4243,10 +4311,10 @@ function inputInit()
|
|
|
4243
4311
|
{
|
|
4244
4312
|
// handle remapping wasd keys to directions
|
|
4245
4313
|
return inputWASDEmulateDirection ?
|
|
4246
|
-
c
|
|
4247
|
-
c
|
|
4248
|
-
c
|
|
4249
|
-
c
|
|
4314
|
+
c === 'KeyW' ? 'ArrowUp' :
|
|
4315
|
+
c === 'KeyS' ? 'ArrowDown' :
|
|
4316
|
+
c === 'KeyA' ? 'ArrowLeft' :
|
|
4317
|
+
c === 'KeyD' ? 'ArrowRight' : c : c;
|
|
4250
4318
|
}
|
|
4251
4319
|
function onMouseDown(e)
|
|
4252
4320
|
{
|
|
@@ -4254,9 +4322,9 @@ function inputInit()
|
|
|
4254
4322
|
return;
|
|
4255
4323
|
|
|
4256
4324
|
// fix stalled audio requiring user interaction
|
|
4257
|
-
if (soundEnable && !headlessMode && audioContext &&
|
|
4325
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
4258
4326
|
audioContext.resume();
|
|
4259
|
-
|
|
4327
|
+
|
|
4260
4328
|
isUsingGamepad = false;
|
|
4261
4329
|
inputData[0][e.button] = 3;
|
|
4262
4330
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
@@ -4299,8 +4367,8 @@ function gamepadsUpdate()
|
|
|
4299
4367
|
const applyDeadZones = (v)=>
|
|
4300
4368
|
{
|
|
4301
4369
|
const min=.3, max=.8;
|
|
4302
|
-
const deadZone = (v)=>
|
|
4303
|
-
v >
|
|
4370
|
+
const deadZone = (v)=>
|
|
4371
|
+
v > min ? percent(v, min, max) :
|
|
4304
4372
|
v < -min ? -percent(-v, min, max) : 0;
|
|
4305
4373
|
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
4306
4374
|
}
|
|
@@ -4308,30 +4376,29 @@ function gamepadsUpdate()
|
|
|
4308
4376
|
// update touch gamepad if enabled
|
|
4309
4377
|
if (touchGamepadEnable && isTouchDevice)
|
|
4310
4378
|
{
|
|
4311
|
-
|
|
4312
|
-
|
|
4379
|
+
if (!touchGamepadTimer.isSet())
|
|
4380
|
+
return;
|
|
4381
|
+
|
|
4382
|
+
// read virtual analog stick
|
|
4383
|
+
const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
|
|
4384
|
+
sticks[0] = vec2();
|
|
4385
|
+
if (touchGamepadAnalog)
|
|
4386
|
+
sticks[0] = applyDeadZones(touchGamepadStick);
|
|
4387
|
+
else if (touchGamepadStick.lengthSquared() > .3)
|
|
4313
4388
|
{
|
|
4314
|
-
//
|
|
4315
|
-
|
|
4316
|
-
sticks[0] =
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
else if (touchGamepadStick.lengthSquared() > .3)
|
|
4320
|
-
{
|
|
4321
|
-
// convert to 8 way dpad
|
|
4322
|
-
sticks[0].x = Math.round(touchGamepadStick.x);
|
|
4323
|
-
sticks[0].y = -Math.round(touchGamepadStick.y);
|
|
4324
|
-
sticks[0] = sticks[0].clampLength();
|
|
4325
|
-
}
|
|
4389
|
+
// convert to 8 way dpad
|
|
4390
|
+
sticks[0].x = Math.round(touchGamepadStick.x);
|
|
4391
|
+
sticks[0].y = -Math.round(touchGamepadStick.y);
|
|
4392
|
+
sticks[0] = sticks[0].clampLength();
|
|
4393
|
+
}
|
|
4326
4394
|
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
}
|
|
4395
|
+
// read virtual gamepad buttons
|
|
4396
|
+
const data = inputData[1] || (inputData[1] = []);
|
|
4397
|
+
for (let i=10; i--;)
|
|
4398
|
+
{
|
|
4399
|
+
const j = i === 3 ? 2 : i === 2 ? 3 : i; // fix button locations
|
|
4400
|
+
const wasDown = gamepadIsDown(j,0);
|
|
4401
|
+
data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
4335
4402
|
}
|
|
4336
4403
|
}
|
|
4337
4404
|
|
|
@@ -4357,7 +4424,7 @@ function gamepadsUpdate()
|
|
|
4357
4424
|
// read analog sticks
|
|
4358
4425
|
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
4359
4426
|
sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
|
|
4360
|
-
|
|
4427
|
+
|
|
4361
4428
|
// read buttons
|
|
4362
4429
|
for (let j = gamepad.buttons.length; j--;)
|
|
4363
4430
|
{
|
|
@@ -4373,14 +4440,14 @@ function gamepadsUpdate()
|
|
|
4373
4440
|
{
|
|
4374
4441
|
// copy dpad to left analog stick when pressed
|
|
4375
4442
|
const dpad = vec2(
|
|
4376
|
-
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
4443
|
+
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
4377
4444
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
4378
4445
|
if (dpad.lengthSquared())
|
|
4379
4446
|
sticks[0] = dpad.clampLength();
|
|
4380
4447
|
}
|
|
4381
4448
|
|
|
4382
4449
|
// disable touch gamepad if using real gamepad
|
|
4383
|
-
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
4450
|
+
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
4384
4451
|
}
|
|
4385
4452
|
}
|
|
4386
4453
|
}
|
|
@@ -4405,20 +4472,13 @@ function vibrateStop() { vibrate(0); }
|
|
|
4405
4472
|
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
4406
4473
|
|
|
4407
4474
|
// touch gamepad internal variables
|
|
4408
|
-
let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
|
|
4475
|
+
let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
|
|
4409
4476
|
|
|
4410
4477
|
// enable touch input mouse passthrough
|
|
4411
4478
|
function touchInputInit()
|
|
4412
4479
|
{
|
|
4413
4480
|
// add non passive touch event listeners
|
|
4414
4481
|
let handleTouch = handleTouchDefault;
|
|
4415
|
-
if (touchGamepadEnable)
|
|
4416
|
-
{
|
|
4417
|
-
// touch input internal variables
|
|
4418
|
-
handleTouch = handleTouchGamepad;
|
|
4419
|
-
touchGamepadButtons = [];
|
|
4420
|
-
touchGamepadStick = vec2();
|
|
4421
|
-
}
|
|
4422
4482
|
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
4423
4483
|
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
4424
4484
|
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
@@ -4427,8 +4487,15 @@ function touchInputInit()
|
|
|
4427
4487
|
let wasTouching;
|
|
4428
4488
|
function handleTouchDefault(e)
|
|
4429
4489
|
{
|
|
4490
|
+
if (!touchInputEnable)
|
|
4491
|
+
return;
|
|
4492
|
+
|
|
4493
|
+
// route touch to gamepad
|
|
4494
|
+
if (touchGamepadEnable)
|
|
4495
|
+
handleTouchGamepad(e);
|
|
4496
|
+
|
|
4430
4497
|
// fix stalled audio requiring user interaction
|
|
4431
|
-
if (soundEnable && !headlessMode && audioContext &&
|
|
4498
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
4432
4499
|
audioContext.resume();
|
|
4433
4500
|
|
|
4434
4501
|
// check if touching and pass to mouse events
|
|
@@ -4457,7 +4524,7 @@ function touchInputInit()
|
|
|
4457
4524
|
// prevent default handling like copy and magnifier lens
|
|
4458
4525
|
if (inputPreventDefault && document.hasFocus()) // allow document to get focus
|
|
4459
4526
|
e.preventDefault();
|
|
4460
|
-
|
|
4527
|
+
|
|
4461
4528
|
// must return true so the document will get focus
|
|
4462
4529
|
return true;
|
|
4463
4530
|
}
|
|
@@ -4469,7 +4536,7 @@ function touchInputInit()
|
|
|
4469
4536
|
touchGamepadStick = vec2();
|
|
4470
4537
|
touchGamepadButtons = [];
|
|
4471
4538
|
isUsingGamepad = true;
|
|
4472
|
-
|
|
4539
|
+
|
|
4473
4540
|
const touching = e.touches.length;
|
|
4474
4541
|
if (touching)
|
|
4475
4542
|
{
|
|
@@ -4478,9 +4545,6 @@ function touchInputInit()
|
|
|
4478
4545
|
{
|
|
4479
4546
|
// touch anywhere to press start when paused
|
|
4480
4547
|
touchGamepadButtons[9] = 1;
|
|
4481
|
-
|
|
4482
|
-
// call default touch handler so normal touch events still work
|
|
4483
|
-
handleTouchDefault(e);
|
|
4484
4548
|
return;
|
|
4485
4549
|
}
|
|
4486
4550
|
}
|
|
@@ -4511,12 +4575,6 @@ function touchInputInit()
|
|
|
4511
4575
|
touchGamepadButtons[9] = 1;
|
|
4512
4576
|
}
|
|
4513
4577
|
}
|
|
4514
|
-
|
|
4515
|
-
// call default touch handler so normal touch events still work
|
|
4516
|
-
handleTouchDefault(e);
|
|
4517
|
-
|
|
4518
|
-
// must return true so the document will get focus
|
|
4519
|
-
return true;
|
|
4520
4578
|
}
|
|
4521
4579
|
}
|
|
4522
4580
|
|
|
@@ -4526,7 +4584,7 @@ function touchGamepadRender()
|
|
|
4526
4584
|
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
4527
4585
|
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
4528
4586
|
return;
|
|
4529
|
-
|
|
4587
|
+
|
|
4530
4588
|
// fade off when not touching or paused
|
|
4531
4589
|
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
4532
4590
|
if (!alpha || paused)
|
|
@@ -4557,11 +4615,11 @@ function touchGamepadRender()
|
|
|
4557
4615
|
const angle = i*PI/4;
|
|
4558
4616
|
context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
|
|
4559
4617
|
i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
|
|
4560
|
-
i
|
|
4618
|
+
i===1 && context.fill();
|
|
4561
4619
|
}
|
|
4562
4620
|
context.stroke();
|
|
4563
4621
|
}
|
|
4564
|
-
|
|
4622
|
+
|
|
4565
4623
|
// draw right face buttons
|
|
4566
4624
|
const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4567
4625
|
for (let i=4; i--;)
|
|
@@ -4596,8 +4654,8 @@ function pointerLockExit() { document.exitPointerLock && document.exitPointerLoc
|
|
|
4596
4654
|
/** Check if pointer is locked (true if locked)
|
|
4597
4655
|
* @return {boolean}
|
|
4598
4656
|
* @memberof Input */
|
|
4599
|
-
function pointerLockIsActive() { return document.pointerLockElement
|
|
4600
|
-
/**
|
|
4657
|
+
function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
|
|
4658
|
+
/**
|
|
4601
4659
|
* LittleJS Audio System
|
|
4602
4660
|
* - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
|
|
4603
4661
|
* - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
|
|
@@ -4618,10 +4676,21 @@ let audioContext = new AudioContext;
|
|
|
4618
4676
|
* @memberof Audio */
|
|
4619
4677
|
let audioMasterGain;
|
|
4620
4678
|
|
|
4679
|
+
/** Default sample rate used for sounds
|
|
4680
|
+
* @default 44100
|
|
4681
|
+
* @memberof Audio */
|
|
4682
|
+
const audioDefaultSampleRate = 44100;
|
|
4683
|
+
|
|
4684
|
+
/** Check if the audio context is running and available for playback
|
|
4685
|
+
* @return {boolean} - True if the audio context is running
|
|
4686
|
+
* @memberof Audio */
|
|
4687
|
+
function audioIsRunning()
|
|
4688
|
+
{ return audioContext.state === 'running'; }
|
|
4689
|
+
|
|
4621
4690
|
function audioInit()
|
|
4622
4691
|
{
|
|
4623
4692
|
if (!soundEnable || headlessMode) return;
|
|
4624
|
-
|
|
4693
|
+
|
|
4625
4694
|
audioMasterGain = audioContext.createGain();
|
|
4626
4695
|
audioMasterGain.connect(audioContext.destination);
|
|
4627
4696
|
audioMasterGain.gain.value = soundVolume; // set starting value
|
|
@@ -4629,14 +4698,14 @@ function audioInit()
|
|
|
4629
4698
|
|
|
4630
4699
|
///////////////////////////////////////////////////////////////////////////////
|
|
4631
4700
|
|
|
4632
|
-
/**
|
|
4701
|
+
/**
|
|
4633
4702
|
* Sound Object - Stores a sound for later use and can be played positionally
|
|
4634
|
-
*
|
|
4703
|
+
*
|
|
4635
4704
|
* <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
|
|
4636
4705
|
* @example
|
|
4637
4706
|
* // create a sound
|
|
4638
4707
|
* const sound_example = new Sound([.5,.5]);
|
|
4639
|
-
*
|
|
4708
|
+
*
|
|
4640
4709
|
* // play the sound
|
|
4641
4710
|
* sound_example.play();
|
|
4642
4711
|
*/
|
|
@@ -4653,33 +4722,41 @@ class Sound
|
|
|
4653
4722
|
|
|
4654
4723
|
/** @property {number} - World space max range of sound */
|
|
4655
4724
|
this.range = range;
|
|
4656
|
-
|
|
4657
4725
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
4658
4726
|
this.taper = taper;
|
|
4659
|
-
|
|
4660
4727
|
/** @property {number} - How much to randomize frequency each time sound plays */
|
|
4661
4728
|
this.randomness = 0;
|
|
4729
|
+
/** @property {number} - Sample rate for this sound */
|
|
4730
|
+
this.sampleRate = audioDefaultSampleRate;
|
|
4731
|
+
/** @property {number} - Percentage of this sound currently loaded */
|
|
4732
|
+
this.loadedPercent = 0;
|
|
4662
4733
|
|
|
4734
|
+
// generate zzfx sound now for fast playback
|
|
4663
4735
|
if (zzfxSound)
|
|
4664
4736
|
{
|
|
4665
|
-
//
|
|
4666
|
-
const defaultRandomness = .05;
|
|
4667
|
-
this.randomness = zzfxSound[
|
|
4668
|
-
|
|
4737
|
+
// remove randomness so it can be applied on playback
|
|
4738
|
+
const randomnessIndex = 1, defaultRandomness = .05;
|
|
4739
|
+
this.randomness = zzfxSound[randomnessIndex] !== undefined ?
|
|
4740
|
+
zzfxSound[randomnessIndex] : defaultRandomness;
|
|
4741
|
+
zzfxSound[randomnessIndex] = 0;
|
|
4742
|
+
|
|
4743
|
+
// generate the zzfx samples
|
|
4669
4744
|
this.sampleChannels = [zzfxG(...zzfxSound)];
|
|
4670
|
-
this.
|
|
4745
|
+
this.loadedPercent = 1;
|
|
4671
4746
|
}
|
|
4672
4747
|
}
|
|
4673
4748
|
|
|
4674
4749
|
/** Play the sound
|
|
4675
|
-
*
|
|
4676
|
-
* @param {
|
|
4677
|
-
* @param {number} [
|
|
4678
|
-
* @param {number} [
|
|
4679
|
-
* @param {
|
|
4680
|
-
* @
|
|
4750
|
+
* Sounds may not play until a user interaction occurs
|
|
4751
|
+
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
4752
|
+
* @param {number} [volume] - How much to scale volume by
|
|
4753
|
+
* @param {number} [pitch] - How much to scale pitch by
|
|
4754
|
+
* @param {number} [randomnessScale] - How much to scale pitch randomness
|
|
4755
|
+
* @param {boolean} [loop] - Should the sound loop?
|
|
4756
|
+
* @param {boolean} [paused] - Should the sound start paused
|
|
4757
|
+
* @return {SoundInstance} - The audio source node
|
|
4681
4758
|
*/
|
|
4682
|
-
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
|
|
4759
|
+
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
|
|
4683
4760
|
{
|
|
4684
4761
|
if (!soundEnable || headlessMode) return;
|
|
4685
4762
|
if (!this.sampleChannels) return;
|
|
@@ -4702,75 +4779,55 @@ class Sound
|
|
|
4702
4779
|
// get pan from screen space coords
|
|
4703
4780
|
pan = worldToScreen(pos).x * 2/mainCanvas.width - 1;
|
|
4704
4781
|
}
|
|
4705
|
-
|
|
4706
|
-
// play the sound
|
|
4707
|
-
const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
|
|
4708
|
-
this.gainNode = audioContext.createGain();
|
|
4709
|
-
this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
|
|
4710
|
-
return this.source;
|
|
4711
|
-
}
|
|
4712
|
-
|
|
4713
|
-
/** Set the sound volume of the most recently played instance of this sound
|
|
4714
|
-
* @param {number} [volume] - How much to scale volume by
|
|
4715
|
-
*/
|
|
4716
|
-
setVolume(volume=1)
|
|
4717
|
-
{
|
|
4718
|
-
if (this.gainNode)
|
|
4719
|
-
this.gainNode.gain.value = volume;
|
|
4720
|
-
}
|
|
4721
|
-
|
|
4722
|
-
/** Stop the last instance of this sound that was played
|
|
4723
|
-
* @param {number} [fadeTime] - How long to fade out (seconds)
|
|
4724
|
-
*/
|
|
4725
|
-
stop(fadeTime=0)
|
|
4726
|
-
{
|
|
4727
|
-
if (!this.source)
|
|
4728
|
-
return;
|
|
4729
4782
|
|
|
4730
|
-
//
|
|
4731
|
-
const
|
|
4732
|
-
|
|
4733
|
-
this.gainNode.gain.linearRampToValueAtTime(1, startFade);
|
|
4734
|
-
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
4735
|
-
this.source.stop(endFade);
|
|
4736
|
-
this.source = undefined;
|
|
4783
|
+
// Create and return sound instance
|
|
4784
|
+
const rate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
|
|
4785
|
+
return new SoundInstance(this, volume, rate, pan, loop, paused);
|
|
4737
4786
|
}
|
|
4738
4787
|
|
|
4739
|
-
/**
|
|
4740
|
-
* @
|
|
4788
|
+
/** Play a music track that loops by default
|
|
4789
|
+
* @param {number} [volume] - Volume to play the music at
|
|
4790
|
+
* @param {boolean} [loop] - Should the music loop?
|
|
4791
|
+
* @param {boolean} [paused] - Should the music start paused
|
|
4792
|
+
* @return {SoundInstance} - The audio source node
|
|
4741
4793
|
*/
|
|
4742
|
-
|
|
4794
|
+
playMusic(volume=1, loop=true, paused=false)
|
|
4795
|
+
{ return this.play(undefined, volume, 1, 0, loop, paused); }
|
|
4743
4796
|
|
|
4744
|
-
/** Play the sound as a note with a semitone offset
|
|
4797
|
+
/** Play the sound as a musical note with a semitone offset
|
|
4798
|
+
* This can be used to play music with chromatic scales
|
|
4745
4799
|
* @param {number} semitoneOffset - How many semitones to offset pitch
|
|
4746
|
-
* @param {Vector2} [pos] - World space position to play the sound
|
|
4747
|
-
* @param {number} [volume=1] - How much to scale volume by
|
|
4748
|
-
* @return {
|
|
4800
|
+
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
4801
|
+
* @param {number} [volume=1] - How much to scale volume by
|
|
4802
|
+
* @return {SoundInstance} - The audio source node
|
|
4749
4803
|
*/
|
|
4750
4804
|
playNote(semitoneOffset, pos, volume)
|
|
4751
|
-
{
|
|
4805
|
+
{
|
|
4806
|
+
const pitch = getNoteFrequency(semitoneOffset, 1);
|
|
4807
|
+
return this.play(pos, volume, pitch, 0);
|
|
4808
|
+
}
|
|
4752
4809
|
|
|
4753
4810
|
/** Get how long this sound is in seconds
|
|
4754
4811
|
* @return {number} - How long the sound is in seconds (undefined if loading)
|
|
4755
4812
|
*/
|
|
4756
|
-
getDuration()
|
|
4813
|
+
getDuration()
|
|
4757
4814
|
{ return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
|
|
4758
|
-
|
|
4759
|
-
/** Check if sound is
|
|
4760
|
-
* @return {boolean} - True if sound is
|
|
4815
|
+
|
|
4816
|
+
/** Check if sound is loaded, for sounds fetched from a url
|
|
4817
|
+
* @return {boolean} - True if sound is loaded and ready to play
|
|
4761
4818
|
*/
|
|
4762
|
-
|
|
4819
|
+
isLoaded() { return this.loadedPercent === 1; }
|
|
4763
4820
|
}
|
|
4764
4821
|
|
|
4765
4822
|
///////////////////////////////////////////////////////////////////////////////
|
|
4766
4823
|
|
|
4767
|
-
/**
|
|
4824
|
+
/**
|
|
4768
4825
|
* Sound Wave Object - Stores a wave sound for later use and can be played positionally
|
|
4769
4826
|
* - this can be used to play wave, mp3, and ogg files
|
|
4770
4827
|
* @example
|
|
4771
4828
|
* // create a sound
|
|
4772
4829
|
* const sound_example = new SoundWave('sound.mp3');
|
|
4773
|
-
*
|
|
4830
|
+
*
|
|
4774
4831
|
* // play the sound
|
|
4775
4832
|
* sound_example.play();
|
|
4776
4833
|
*/
|
|
@@ -4802,26 +4859,202 @@ class SoundWave extends Sound
|
|
|
4802
4859
|
const response = await fetch(filename);
|
|
4803
4860
|
const arrayBuffer = await response.arrayBuffer();
|
|
4804
4861
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4862
|
+
|
|
4863
|
+
// convert audio buffer to sample channels across multiple frames
|
|
4864
|
+
const channelCount = audioBuffer.numberOfChannels;
|
|
4865
|
+
const samplesPerFrame = 1e5;
|
|
4866
|
+
const sampleChannels = [];
|
|
4867
|
+
for (let channel = 0; channel < channelCount; channel++)
|
|
4868
|
+
{
|
|
4869
|
+
const channelData = audioBuffer.getChannelData(channel);
|
|
4870
|
+
const channelLength = channelData.length;
|
|
4871
|
+
sampleChannels[channel] = new Array(channelLength);
|
|
4872
|
+
let sampleIndex = 0;
|
|
4873
|
+
while (sampleIndex < channelLength)
|
|
4874
|
+
{
|
|
4875
|
+
// yield to next frame
|
|
4876
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
4877
|
+
|
|
4878
|
+
// copy chunk of samples
|
|
4879
|
+
const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
|
|
4880
|
+
for (; sampleIndex < endIndex; sampleIndex++)
|
|
4881
|
+
sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
|
|
4882
|
+
|
|
4883
|
+
// update loaded percent
|
|
4884
|
+
const samplesTotal = channelCount * channelLength;
|
|
4885
|
+
const samplesProcessed = channel * channelLength + sampleIndex;
|
|
4886
|
+
this.loadedPercent = samplesProcessed / samplesTotal;
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
// setup the sound to be played
|
|
4808
4891
|
this.sampleRate = audioBuffer.sampleRate;
|
|
4892
|
+
this.sampleChannels = sampleChannels;
|
|
4893
|
+
this.loadedPercent = 1;
|
|
4809
4894
|
if (this.onloadCallback)
|
|
4810
4895
|
this.onloadCallback();
|
|
4811
4896
|
}
|
|
4812
4897
|
}
|
|
4813
4898
|
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
*
|
|
4818
|
-
*
|
|
4819
|
-
*
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4899
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4900
|
+
|
|
4901
|
+
/**
|
|
4902
|
+
* Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
|
|
4903
|
+
* Represents a single playing instance of a sound with pause/resume capabilities
|
|
4904
|
+
* @example
|
|
4905
|
+
* // Play a sound and get an instance for control
|
|
4906
|
+
* const jumpSound = new Sound([.5,.5,220]);
|
|
4907
|
+
* const instance = jumpSound.play();
|
|
4908
|
+
*
|
|
4909
|
+
* // Control the individual instance
|
|
4910
|
+
* instance.setVolume(.5);
|
|
4911
|
+
* instance.pause();
|
|
4912
|
+
* instance.unpause();
|
|
4913
|
+
* instance.stop();
|
|
4914
|
+
*/
|
|
4915
|
+
class SoundInstance
|
|
4916
|
+
{
|
|
4917
|
+
/** Create a sound instance
|
|
4918
|
+
* @param {Sound} sound - The sound object
|
|
4919
|
+
* @param {number} [volume] - How much to scale volume by
|
|
4920
|
+
* @param {number} [rate] - The playback rate to use
|
|
4921
|
+
* @param {number} [pan] - How much to apply stereo panning
|
|
4922
|
+
* @param {boolean} [loop] - Should the sound loop?
|
|
4923
|
+
* @param {boolean} [paused] - Should the sound start paused? */
|
|
4924
|
+
constructor(sound, volume=1, rate=1, pan=0, loop=false, paused=false)
|
|
4925
|
+
{
|
|
4926
|
+
ASSERT(sound instanceof Sound, 'SoundInstance requires a valid Sound object');
|
|
4927
|
+
ASSERT(volume >= 0, 'Sound volume must be positive or zero');
|
|
4928
|
+
ASSERT(rate >= 0, 'Sound rate must be positive or zero');
|
|
4929
|
+
ASSERT(isNumber(pan), 'Sound pan must be a number');
|
|
4930
|
+
|
|
4931
|
+
/** @property {Sound} - The sound object */
|
|
4932
|
+
this.sound = sound;
|
|
4933
|
+
/** @property {number} - How much to scale volume by */
|
|
4934
|
+
this.volume = volume;
|
|
4935
|
+
/** @property {number} - The playback rate to use */
|
|
4936
|
+
this.rate = rate;
|
|
4937
|
+
/** @property {number} - How much to apply stereo panning */
|
|
4938
|
+
this.pan = pan;
|
|
4939
|
+
/** @property {boolean} - Should the sound loop */
|
|
4940
|
+
this.loop = loop;
|
|
4941
|
+
/** @property {number} - Timestamp for audio context when paused */
|
|
4942
|
+
this.pausedTime = 0;
|
|
4943
|
+
/** @property {number} - Timestamp for audio context when started */
|
|
4944
|
+
this.startTime = undefined;
|
|
4945
|
+
/** @property {GainNode} - Gain node for the sound */
|
|
4946
|
+
this.gainNode = undefined;
|
|
4947
|
+
/** @property {AudioBufferSourceNode} - Source node of the audio */
|
|
4948
|
+
this.source = undefined;
|
|
4949
|
+
// setup end callback and start sound
|
|
4950
|
+
this.onendedCallback = (source)=>
|
|
4951
|
+
{
|
|
4952
|
+
if (source === this.source)
|
|
4953
|
+
this.source = undefined;
|
|
4954
|
+
};
|
|
4955
|
+
if (!paused)
|
|
4956
|
+
this.start();
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
/** Start playing the sound instance from the offset time
|
|
4960
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
4961
|
+
*/
|
|
4962
|
+
start(offset=0)
|
|
4963
|
+
{
|
|
4964
|
+
ASSERT(offset >= 0, 'Sound start offset must be positive or zero');
|
|
4965
|
+
if (this.isPlaying())
|
|
4966
|
+
this.stop();
|
|
4967
|
+
this.gainNode = audioContext.createGain();
|
|
4968
|
+
this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
|
|
4969
|
+
this.startTime = audioContext.currentTime - offset;
|
|
4970
|
+
this.pausedTime = undefined;
|
|
4971
|
+
}
|
|
4972
|
+
|
|
4973
|
+
/** Set the volume of this sound instance
|
|
4974
|
+
* @param {number} volume */
|
|
4975
|
+
setVolume(volume)
|
|
4976
|
+
{
|
|
4977
|
+
ASSERT(volume >= 0, 'Sound volume must be positive or zero');
|
|
4978
|
+
this.volume = volume;
|
|
4979
|
+
if (this.gainNode)
|
|
4980
|
+
this.gainNode.gain.value = volume;
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4983
|
+
/** Stop this sound instance and reset position to the start */
|
|
4984
|
+
stop(fadeTime=0)
|
|
4985
|
+
{
|
|
4986
|
+
ASSERT(fadeTime >= 0, 'Sound fade time must be positive or zero');
|
|
4987
|
+
if (this.isPlaying())
|
|
4988
|
+
{
|
|
4989
|
+
if (fadeTime)
|
|
4990
|
+
{
|
|
4991
|
+
// ramp off gain
|
|
4992
|
+
const startFade = audioContext.currentTime;
|
|
4993
|
+
const endFade = startFade + fadeTime;
|
|
4994
|
+
this.gainNode.gain.linearRampToValueAtTime(1, startFade);
|
|
4995
|
+
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
4996
|
+
this.source.stop(endFade);
|
|
4997
|
+
}
|
|
4998
|
+
else
|
|
4999
|
+
this.source.stop();
|
|
5000
|
+
}
|
|
5001
|
+
this.pausedTime = 0;
|
|
5002
|
+
this.source = undefined;
|
|
5003
|
+
this.startTime = undefined;
|
|
5004
|
+
}
|
|
5005
|
+
|
|
5006
|
+
/** Pause this sound instance */
|
|
5007
|
+
pause()
|
|
5008
|
+
{
|
|
5009
|
+
if (this.isPaused())
|
|
5010
|
+
return;
|
|
5011
|
+
|
|
5012
|
+
// save current time and stop sound
|
|
5013
|
+
this.pausedTime = this.getCurrentTime();
|
|
5014
|
+
this.source.stop();
|
|
5015
|
+
this.source = undefined;
|
|
5016
|
+
this.startTime = undefined;
|
|
5017
|
+
}
|
|
5018
|
+
|
|
5019
|
+
/** Unpauses this sound instance */
|
|
5020
|
+
resume()
|
|
5021
|
+
{
|
|
5022
|
+
if (!this.isPaused())
|
|
5023
|
+
return;
|
|
5024
|
+
|
|
5025
|
+
// restart sound from paused time
|
|
5026
|
+
this.start(this.pausedTime);
|
|
5027
|
+
}
|
|
5028
|
+
|
|
5029
|
+
/** Check if this instance is currently playing
|
|
5030
|
+
* @return {boolean} - True if playing
|
|
5031
|
+
*/
|
|
5032
|
+
isPlaying() { return !!this.source; }
|
|
5033
|
+
|
|
5034
|
+
/** Check if this instance is paused and was not stopped
|
|
5035
|
+
* @return {boolean} - True if paused
|
|
5036
|
+
*/
|
|
5037
|
+
isPaused() { return !this.isPlaying(); }
|
|
5038
|
+
|
|
5039
|
+
/** Get the current playback time in seconds
|
|
5040
|
+
* @return {number} - Current playback time
|
|
5041
|
+
*/
|
|
5042
|
+
getCurrentTime()
|
|
5043
|
+
{
|
|
5044
|
+
const deltaTime = mod(audioContext.currentTime - this.startTime,
|
|
5045
|
+
this.getDuration());
|
|
5046
|
+
return this.isPlaying() ? deltaTime : this.pausedTime;
|
|
5047
|
+
}
|
|
4823
5048
|
|
|
4824
|
-
|
|
5049
|
+
/** Get the total duration of this sound
|
|
5050
|
+
* @return {number} - Total duration in seconds
|
|
5051
|
+
*/
|
|
5052
|
+
getDuration() { return this.sound.getDuration() / this.rate; }
|
|
5053
|
+
|
|
5054
|
+
/** Get source of this sound instance
|
|
5055
|
+
* @return {AudioBufferSourceNode}
|
|
5056
|
+
*/
|
|
5057
|
+
getSource() { return this.source; }
|
|
4825
5058
|
}
|
|
4826
5059
|
|
|
4827
5060
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -4875,9 +5108,11 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
|
|
|
4875
5108
|
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
4876
5109
|
* @param {number} [sampleRate=44100] - Sample rate for the sound
|
|
4877
5110
|
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing
|
|
5111
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
5112
|
+
* @param {Function} [onended] - Callback for when the sound ends
|
|
4878
5113
|
* @return {AudioBufferSourceNode} - The audio node of the sound played
|
|
4879
5114
|
* @memberof Audio */
|
|
4880
|
-
function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=
|
|
5115
|
+
function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
|
|
4881
5116
|
{
|
|
4882
5117
|
if (!soundEnable || headlessMode) return;
|
|
4883
5118
|
|
|
@@ -4902,16 +5137,20 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
4902
5137
|
const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
|
|
4903
5138
|
source.connect(pannerNode).connect(gainNode);
|
|
4904
5139
|
|
|
4905
|
-
//
|
|
4906
|
-
if (
|
|
5140
|
+
// callback when the sound ends
|
|
5141
|
+
if (onended)
|
|
5142
|
+
source.addEventListener('ended', ()=> onended(source));
|
|
5143
|
+
|
|
5144
|
+
if (!audioIsRunning())
|
|
4907
5145
|
{
|
|
4908
|
-
// fix stalled audio
|
|
4909
|
-
audioContext.resume()
|
|
5146
|
+
// fix stalled audio, this sound won't be able to play
|
|
5147
|
+
audioContext.resume();
|
|
5148
|
+
return;
|
|
4910
5149
|
}
|
|
4911
|
-
else
|
|
4912
|
-
source.start();
|
|
4913
5150
|
|
|
4914
|
-
// return sound
|
|
5151
|
+
// play and return sound
|
|
5152
|
+
const startOffset = offset * rate;
|
|
5153
|
+
source.start(0, startOffset);
|
|
4915
5154
|
return source;
|
|
4916
5155
|
}
|
|
4917
5156
|
|
|
@@ -4919,18 +5158,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
4919
5158
|
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
|
|
4920
5159
|
|
|
4921
5160
|
/** Generate and play a ZzFX sound
|
|
4922
|
-
*
|
|
5161
|
+
*
|
|
4923
5162
|
* <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
|
|
4924
5163
|
* @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
|
|
4925
5164
|
* @return {AudioBufferSourceNode} - The audio node of the sound played
|
|
4926
5165
|
* @memberof Audio */
|
|
4927
5166
|
function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
|
|
4928
5167
|
|
|
4929
|
-
/** Sample rate used for all ZzFX sounds
|
|
4930
|
-
* @default 44100
|
|
4931
|
-
* @memberof Audio */
|
|
4932
|
-
const zzfxR = 44100;
|
|
4933
|
-
|
|
4934
5168
|
/** Generate samples for a ZzFX sound
|
|
4935
5169
|
* @param {number} [volume] - Volume scale (percent)
|
|
4936
5170
|
* @param {number} [randomness] - How much to randomize frequency (percent Hz)
|
|
@@ -4954,11 +5188,10 @@ const zzfxR = 44100;
|
|
|
4954
5188
|
* @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
|
|
4955
5189
|
* @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
|
|
4956
5190
|
* @return {Array} - Array of audio samples
|
|
4957
|
-
* @memberof Audio
|
|
4958
|
-
*/
|
|
5191
|
+
* @memberof Audio */
|
|
4959
5192
|
function zzfxG
|
|
4960
5193
|
(
|
|
4961
|
-
volume = 1,
|
|
5194
|
+
volume = 1,
|
|
4962
5195
|
randomness = .05,
|
|
4963
5196
|
frequency = 220,
|
|
4964
5197
|
attack = 0,
|
|
@@ -4966,11 +5199,11 @@ function zzfxG
|
|
|
4966
5199
|
release = .1,
|
|
4967
5200
|
shape = 0,
|
|
4968
5201
|
shapeCurve = 1,
|
|
4969
|
-
slide = 0,
|
|
4970
|
-
deltaSlide = 0,
|
|
4971
|
-
pitchJump = 0,
|
|
4972
|
-
pitchJumpTime = 0,
|
|
4973
|
-
repeatTime = 0,
|
|
5202
|
+
slide = 0,
|
|
5203
|
+
deltaSlide = 0,
|
|
5204
|
+
pitchJump = 0,
|
|
5205
|
+
pitchJumpTime = 0,
|
|
5206
|
+
repeatTime = 0,
|
|
4974
5207
|
noise = 0,
|
|
4975
5208
|
modulation = 0,
|
|
4976
5209
|
bitCrush = 0,
|
|
@@ -4982,19 +5215,19 @@ function zzfxG
|
|
|
4982
5215
|
)
|
|
4983
5216
|
{
|
|
4984
5217
|
// init parameters
|
|
4985
|
-
let sampleRate =
|
|
4986
|
-
PI2 = PI*2,
|
|
5218
|
+
let sampleRate = audioDefaultSampleRate,
|
|
5219
|
+
PI2 = PI*2,
|
|
4987
5220
|
startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
|
|
4988
|
-
startFrequency = frequency *=
|
|
5221
|
+
startFrequency = frequency *=
|
|
4989
5222
|
(1 + rand(randomness,-randomness)) * PI2 / sampleRate,
|
|
4990
|
-
modOffset = 0, // modulation offset
|
|
5223
|
+
modOffset = 0, // modulation offset
|
|
4991
5224
|
repeat = 0, // repeat offset
|
|
4992
5225
|
crush = 0, // bit crush offset
|
|
4993
5226
|
jump = 1, // pitch jump timer
|
|
4994
5227
|
length, // sample length
|
|
4995
5228
|
b = [], // sample buffer
|
|
4996
5229
|
t = 0, // sample time
|
|
4997
|
-
i = 0, // sample index
|
|
5230
|
+
i = 0, // sample index
|
|
4998
5231
|
s = 0, // sample value
|
|
4999
5232
|
f, // wave frequency
|
|
5000
5233
|
|
|
@@ -5002,7 +5235,7 @@ function zzfxG
|
|
|
5002
5235
|
quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
|
|
5003
5236
|
cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
|
|
5004
5237
|
a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
|
|
5005
|
-
b0 = (1 + sign(filter) * cos) / 2 / a0,
|
|
5238
|
+
b0 = (1 + sign(filter) * cos) / 2 / a0,
|
|
5006
5239
|
b1 = -(sign(filter) + cos) / a0, b2 = b0,
|
|
5007
5240
|
x2 = 0, x1 = 0, y2 = 0, y1 = 0;
|
|
5008
5241
|
|
|
@@ -5048,7 +5281,7 @@ function zzfxG
|
|
|
5048
5281
|
0); // post release
|
|
5049
5282
|
|
|
5050
5283
|
s = delay ? s/2 + (delay > i ? 0 : // delay
|
|
5051
|
-
(i<length-delay? 1 : (length-i)/delay) * // release delay
|
|
5284
|
+
(i<length-delay? 1 : (length-i)/delay) * // release delay
|
|
5052
5285
|
b[i-delay|0]/2/volume) : s; // sample delay
|
|
5053
5286
|
|
|
5054
5287
|
if (filter) // apply filter
|
|
@@ -5060,14 +5293,14 @@ function zzfxG
|
|
|
5060
5293
|
t += f + f*noise*Math.sin(i**5); // noise
|
|
5061
5294
|
|
|
5062
5295
|
if (jump && ++jump > pitchJumpTime) // pitch jump
|
|
5063
|
-
{
|
|
5296
|
+
{
|
|
5064
5297
|
frequency += pitchJump; // apply pitch jump
|
|
5065
5298
|
startFrequency += pitchJump; // also apply to start
|
|
5066
5299
|
jump = 0; // stop pitch jump time
|
|
5067
|
-
}
|
|
5300
|
+
}
|
|
5068
5301
|
|
|
5069
5302
|
if (repeatTime && !(++repeat % repeatTime)) // repeat
|
|
5070
|
-
{
|
|
5303
|
+
{
|
|
5071
5304
|
frequency = startFrequency; // reset frequency
|
|
5072
5305
|
slide = startSlide; // reset slide
|
|
5073
5306
|
jump ||= 1; // reset pitch jump time
|
|
@@ -5076,7 +5309,7 @@ function zzfxG
|
|
|
5076
5309
|
|
|
5077
5310
|
return b; // return sample buffer
|
|
5078
5311
|
}
|
|
5079
|
-
/**
|
|
5312
|
+
/**
|
|
5080
5313
|
* LittleJS Tile Layer System
|
|
5081
5314
|
* - Caches arrays of tiles to off screen canvas for fast rendering
|
|
5082
5315
|
* - Unlimited numbers of layers, allocates canvases as needed
|
|
@@ -5090,9 +5323,9 @@ function zzfxG
|
|
|
5090
5323
|
// Tile Layer System
|
|
5091
5324
|
|
|
5092
5325
|
/** Keep track of all tile layers with collision
|
|
5093
|
-
* @type {Array<TileCollisionLayer>}
|
|
5326
|
+
* @type {Array<TileCollisionLayer>}
|
|
5094
5327
|
* @memberof TileCollision */
|
|
5095
|
-
|
|
5328
|
+
const tileCollisionLayers = [];
|
|
5096
5329
|
|
|
5097
5330
|
/** Get tile collision data for a given cell in the grid
|
|
5098
5331
|
* @param {Vector2} pos
|
|
@@ -5146,7 +5379,7 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
|
|
|
5146
5379
|
}
|
|
5147
5380
|
|
|
5148
5381
|
///////////////////////////////////////////////////////////////////////////////
|
|
5149
|
-
/**
|
|
5382
|
+
/**
|
|
5150
5383
|
* Load tile layers from exported data
|
|
5151
5384
|
* @param {Object} tileMapData - Level data from exported data
|
|
5152
5385
|
* @param {TileInfo} [tileInfo] - Default tile info (used for size and texture)
|
|
@@ -5179,13 +5412,13 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
|
|
|
5179
5412
|
{
|
|
5180
5413
|
const dataLayer = tileMapData.layers[layerIndex];
|
|
5181
5414
|
ASSERT(dataLayer.data && dataLayer.data.length);
|
|
5182
|
-
ASSERT(levelSize.area()
|
|
5415
|
+
ASSERT(levelSize.area() === dataLayer.data.length);
|
|
5183
5416
|
|
|
5184
5417
|
const layerRenderOrder = renderOrder - (layerCount - 1 - layerIndex);
|
|
5185
5418
|
const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
|
|
5186
5419
|
tileLayers[layerIndex] = tileLayer;
|
|
5187
5420
|
|
|
5188
|
-
for (let x=levelSize.x; x--;)
|
|
5421
|
+
for (let x=levelSize.x; x--;)
|
|
5189
5422
|
for (let y=levelSize.y; y--;)
|
|
5190
5423
|
{
|
|
5191
5424
|
const pos = vec2(x, levelSize.y-1-y);
|
|
@@ -5244,7 +5477,7 @@ class TileLayerData
|
|
|
5244
5477
|
/**
|
|
5245
5478
|
* Canvas Layer - cached off screen rendering system
|
|
5246
5479
|
* - Contains an offscreen canvas that can be rendered to
|
|
5247
|
-
* -
|
|
5480
|
+
* - WebGL rendering is optional, call useWebGL to enable
|
|
5248
5481
|
* @extends EngineObject
|
|
5249
5482
|
* @example
|
|
5250
5483
|
* const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
|
|
@@ -5266,18 +5499,18 @@ class CanvasLayer extends EngineObject
|
|
|
5266
5499
|
this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
|
|
5267
5500
|
/** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
|
|
5268
5501
|
this.context = headlessMode ? undefined : this.canvas.getContext('2d');
|
|
5269
|
-
/** @property {WebGLTexture} - Texture if using
|
|
5502
|
+
/** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
|
|
5270
5503
|
this.glTexture = undefined;
|
|
5271
5504
|
this.gravityScale = 0; // disable gravity by default for canvas layers
|
|
5272
5505
|
}
|
|
5273
|
-
|
|
5506
|
+
|
|
5274
5507
|
/** Destroy this canvas layer */
|
|
5275
5508
|
destroy()
|
|
5276
5509
|
{
|
|
5277
5510
|
if (this.destroyed)
|
|
5278
5511
|
return;
|
|
5279
5512
|
|
|
5280
|
-
// free up the
|
|
5513
|
+
// free up the WebGL texture
|
|
5281
5514
|
if (this.glTexture)
|
|
5282
5515
|
glDeleteTexture(this.glTexture);
|
|
5283
5516
|
super.destroy();
|
|
@@ -5302,12 +5535,12 @@ class CanvasLayer extends EngineObject
|
|
|
5302
5535
|
draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
|
|
5303
5536
|
{
|
|
5304
5537
|
// draw the canvas layer as a single tile that uses the whole texture
|
|
5305
|
-
const
|
|
5538
|
+
const useWebGL = glEnable && this.glTexture !== undefined;
|
|
5306
5539
|
const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
|
|
5307
|
-
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor,
|
|
5540
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
|
|
5308
5541
|
}
|
|
5309
5542
|
|
|
5310
|
-
/** Draw onto the layer canvas in world space (bypass
|
|
5543
|
+
/** Draw onto the layer canvas in world space (bypass WebGL)
|
|
5311
5544
|
* @param {Vector2} pos
|
|
5312
5545
|
* @param {Vector2} size
|
|
5313
5546
|
* @param {number} angle
|
|
@@ -5341,8 +5574,8 @@ class CanvasLayer extends EngineObject
|
|
|
5341
5574
|
if (textureInfo)
|
|
5342
5575
|
{
|
|
5343
5576
|
context.globalAlpha = color.a; // only alpha is supported
|
|
5344
|
-
context.drawImage(textureInfo.image,
|
|
5345
|
-
tileInfo.pos.x, tileInfo.pos.y,
|
|
5577
|
+
context.drawImage(textureInfo.image,
|
|
5578
|
+
tileInfo.pos.x, tileInfo.pos.y,
|
|
5346
5579
|
tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
|
|
5347
5580
|
context.globalAlpha = 1;
|
|
5348
5581
|
}
|
|
@@ -5360,11 +5593,11 @@ class CanvasLayer extends EngineObject
|
|
|
5360
5593
|
* @param {Vector2} [size=(1,1)]
|
|
5361
5594
|
* @param {Color} [color=(1,1,1,1)]
|
|
5362
5595
|
* @param {number} [angle=0] */
|
|
5363
|
-
drawRect(pos, size, color, angle)
|
|
5596
|
+
drawRect(pos, size, color, angle)
|
|
5364
5597
|
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
5365
5598
|
|
|
5366
|
-
/** Create or update the
|
|
5367
|
-
* @param {boolean} [enable] - enable
|
|
5599
|
+
/** Create or update the WebGL texture for this layer
|
|
5600
|
+
* @param {boolean} [enable] - enable WebGL rendering and update the texture */
|
|
5368
5601
|
useWebGL(enable=true)
|
|
5369
5602
|
{
|
|
5370
5603
|
if (glEnable && enable)
|
|
@@ -5410,7 +5643,7 @@ class TileLayer extends CanvasLayer
|
|
|
5410
5643
|
this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
|
|
5411
5644
|
/** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
|
|
5412
5645
|
this.context = this.canvas.getContext('2d');
|
|
5413
|
-
/** @property {WebGLTexture} - Texture if using
|
|
5646
|
+
/** @property {WebGLTexture} - Texture if using WebGL for this layer */
|
|
5414
5647
|
this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
|
|
5415
5648
|
// set no friction by default, applied friction is max of both objects
|
|
5416
5649
|
this.friction = 0;
|
|
@@ -5435,7 +5668,7 @@ class TileLayer extends CanvasLayer
|
|
|
5435
5668
|
}
|
|
5436
5669
|
}
|
|
5437
5670
|
|
|
5438
|
-
/** Set data at a given position in the array
|
|
5671
|
+
/** Set data at a given position in the array
|
|
5439
5672
|
* @param {Vector2} layerPos - Local position in array
|
|
5440
5673
|
* @param {TileLayerData} data - Data to set
|
|
5441
5674
|
* @param {boolean} [redraw] - Force the tile to redraw if true */
|
|
@@ -5447,26 +5680,26 @@ class TileLayer extends CanvasLayer
|
|
|
5447
5680
|
redraw && this.drawTileData(layerPos);
|
|
5448
5681
|
}
|
|
5449
5682
|
}
|
|
5450
|
-
|
|
5451
|
-
/** Get data at a given position in the array
|
|
5683
|
+
|
|
5684
|
+
/** Get data at a given position in the array
|
|
5452
5685
|
* @param {Vector2} layerPos - Local position in array
|
|
5453
5686
|
* @return {TileLayerData} */
|
|
5454
5687
|
getData(layerPos)
|
|
5455
5688
|
{ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
|
|
5456
|
-
|
|
5689
|
+
|
|
5457
5690
|
// Render the tile layer, called automatically by the engine
|
|
5458
5691
|
render()
|
|
5459
5692
|
{
|
|
5460
|
-
ASSERT(drawContext
|
|
5461
|
-
|
|
5693
|
+
ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
|
|
5694
|
+
|
|
5462
5695
|
// draw the tile layer as a single tile
|
|
5463
5696
|
const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
|
|
5464
5697
|
const pos = this.pos.add(this.size.scale(.5));
|
|
5465
|
-
const
|
|
5466
|
-
drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK,
|
|
5698
|
+
const useWebGL = glEnable && this.glTexture !== undefined;
|
|
5699
|
+
drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
|
|
5467
5700
|
}
|
|
5468
5701
|
|
|
5469
|
-
/** Draw all the tile data to an offscreen canvas
|
|
5702
|
+
/** Draw all the tile data to an offscreen canvas
|
|
5470
5703
|
* - This may be slow in some browsers but only needs to be done once */
|
|
5471
5704
|
redraw()
|
|
5472
5705
|
{
|
|
@@ -5476,7 +5709,7 @@ class TileLayer extends CanvasLayer
|
|
|
5476
5709
|
this.drawTileData(vec2(x,y), false);
|
|
5477
5710
|
this.redrawEnd();
|
|
5478
5711
|
if (this.glTexture)
|
|
5479
|
-
this.useWebGL(); // update
|
|
5712
|
+
this.useWebGL(); // update WebGL texture
|
|
5480
5713
|
}
|
|
5481
5714
|
|
|
5482
5715
|
/** Call to start the redraw process
|
|
@@ -5512,7 +5745,7 @@ class TileLayer extends CanvasLayer
|
|
|
5512
5745
|
/** Call to end the redraw process */
|
|
5513
5746
|
redrawEnd()
|
|
5514
5747
|
{
|
|
5515
|
-
ASSERT(drawContext
|
|
5748
|
+
ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
|
|
5516
5749
|
glCopyToContext(drawContext);
|
|
5517
5750
|
//debugSaveCanvas(this.canvas);
|
|
5518
5751
|
|
|
@@ -5523,7 +5756,7 @@ class TileLayer extends CanvasLayer
|
|
|
5523
5756
|
/** Draw the tile at a given position in the tile grid
|
|
5524
5757
|
* This can be used to clear out tiles when they are destroyed
|
|
5525
5758
|
* Tiles can also be redrawn if inside a redrawStart/End block
|
|
5526
|
-
* @param {Vector2} layerPos
|
|
5759
|
+
* @param {Vector2} layerPos
|
|
5527
5760
|
* @param {boolean} [clear] - should the old tile be cleared out
|
|
5528
5761
|
*/
|
|
5529
5762
|
drawTileData(layerPos, clear=true)
|
|
@@ -5538,9 +5771,9 @@ class TileLayer extends CanvasLayer
|
|
|
5538
5771
|
|
|
5539
5772
|
// draw the tile if it has layer data
|
|
5540
5773
|
const d = this.getData(layerPos);
|
|
5541
|
-
if (d.tile
|
|
5774
|
+
if (d.tile !== undefined)
|
|
5542
5775
|
{
|
|
5543
|
-
ASSERT(drawContext
|
|
5776
|
+
ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
|
|
5544
5777
|
const pos = layerPos.add(vec2(.5));
|
|
5545
5778
|
const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
|
|
5546
5779
|
drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
@@ -5553,7 +5786,7 @@ class TileLayer extends CanvasLayer
|
|
|
5553
5786
|
* Tile Collision Layer - a tile layer with collision
|
|
5554
5787
|
* - adds collision data and functions to TileLayer
|
|
5555
5788
|
* - there can be multiple tile collision layers
|
|
5556
|
-
* - tile
|
|
5789
|
+
* - tile collision layers should not overlap each other
|
|
5557
5790
|
* @extends TileLayer
|
|
5558
5791
|
*/
|
|
5559
5792
|
class TileCollisionLayer extends TileLayer
|
|
@@ -5704,7 +5937,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
5704
5937
|
debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
|
|
5705
5938
|
}
|
|
5706
5939
|
}
|
|
5707
|
-
/**
|
|
5940
|
+
/**
|
|
5708
5941
|
* LittleJS Particle System
|
|
5709
5942
|
*/
|
|
5710
5943
|
|
|
@@ -5721,7 +5954,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
5721
5954
|
* rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
|
|
5722
5955
|
* rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
|
|
5723
5956
|
* 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
|
|
5724
|
-
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
|
|
5957
|
+
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
|
|
5725
5958
|
* .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
|
|
5726
5959
|
* );
|
|
5727
5960
|
*/
|
|
@@ -5729,35 +5962,35 @@ class ParticleEmitter extends EngineObject
|
|
|
5729
5962
|
{
|
|
5730
5963
|
/** Create a particle system with the given settings
|
|
5731
5964
|
* @param {Vector2} position - World space position of the emitter
|
|
5732
|
-
* @param {
|
|
5733
|
-
* @param {
|
|
5734
|
-
* @param {
|
|
5735
|
-
* @param {
|
|
5736
|
-
* @param {
|
|
5965
|
+
* @param {number} [angle] - Angle to emit the particles
|
|
5966
|
+
* @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
|
|
5967
|
+
* @param {number} [emitTime] - How long to stay alive (0 is forever)
|
|
5968
|
+
* @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
|
|
5969
|
+
* @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
|
|
5737
5970
|
* @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
|
|
5738
5971
|
* @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
|
|
5739
5972
|
* @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
|
|
5740
5973
|
* @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
|
|
5741
5974
|
* @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
|
|
5742
|
-
* @param {
|
|
5743
|
-
* @param {
|
|
5744
|
-
* @param {
|
|
5745
|
-
* @param {
|
|
5746
|
-
* @param {
|
|
5747
|
-
* @param {
|
|
5748
|
-
* @param {
|
|
5749
|
-
* @param {
|
|
5750
|
-
* @param {
|
|
5751
|
-
* @param {
|
|
5752
|
-
* @param {
|
|
5975
|
+
* @param {number} [particleTime] - How long particles live
|
|
5976
|
+
* @param {number} [sizeStart] - How big are particles at start
|
|
5977
|
+
* @param {number} [sizeEnd] - How big are particles at end
|
|
5978
|
+
* @param {number} [speed] - How fast are particles when spawned
|
|
5979
|
+
* @param {number} [angleSpeed] - How fast are particles rotating
|
|
5980
|
+
* @param {number} [damping] - How much to dampen particle speed
|
|
5981
|
+
* @param {number} [angleDamping] - How much to dampen particle angular speed
|
|
5982
|
+
* @param {number} [gravityScale] - How much gravity effect particles
|
|
5983
|
+
* @param {number} [particleConeAngle] - Cone for start particle angle
|
|
5984
|
+
* @param {number} [fadeRate] - How quick to fade particles at start/end in percent of life
|
|
5985
|
+
* @param {number} [randomness] - Apply extra randomness percent
|
|
5753
5986
|
* @param {boolean} [collideTiles] - Do particles collide against tiles
|
|
5754
5987
|
* @param {boolean} [additive] - Should particles use additive blend
|
|
5755
5988
|
* @param {boolean} [randomColorLinear] - Should color be randomized linearly or across each component
|
|
5756
|
-
* @param {
|
|
5989
|
+
* @param {number} [renderOrder] - Render order for particles (additive is above other stuff by default)
|
|
5757
5990
|
* @param {boolean} [localSpace] - Should it be in local space of emitter (world space is default)
|
|
5758
5991
|
*/
|
|
5759
5992
|
constructor
|
|
5760
|
-
(
|
|
5993
|
+
(
|
|
5761
5994
|
position,
|
|
5762
5995
|
angle,
|
|
5763
5996
|
emitSize = 0,
|
|
@@ -5779,7 +6012,7 @@ class ParticleEmitter extends EngineObject
|
|
|
5779
6012
|
gravityScale = 0,
|
|
5780
6013
|
particleConeAngle = PI,
|
|
5781
6014
|
fadeRate = .1,
|
|
5782
|
-
randomness = .2,
|
|
6015
|
+
randomness = .2,
|
|
5783
6016
|
collideTiles = false,
|
|
5784
6017
|
additive = false,
|
|
5785
6018
|
randomColorLinear = true,
|
|
@@ -5790,13 +6023,13 @@ class ParticleEmitter extends EngineObject
|
|
|
5790
6023
|
super(position, vec2(), tileInfo, angle, undefined, renderOrder);
|
|
5791
6024
|
|
|
5792
6025
|
// emitter settings
|
|
5793
|
-
/** @property {
|
|
6026
|
+
/** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
|
|
5794
6027
|
this.emitSize = emitSize
|
|
5795
|
-
/** @property {
|
|
6028
|
+
/** @property {number} - How long to stay alive (0 is forever) */
|
|
5796
6029
|
this.emitTime = emitTime;
|
|
5797
|
-
/** @property {
|
|
6030
|
+
/** @property {number} - How many particles per second to spawn, does not emit if 0 */
|
|
5798
6031
|
this.emitRate = emitRate;
|
|
5799
|
-
/** @property {
|
|
6032
|
+
/** @property {number} - Local angle to apply velocity to particles from emitter */
|
|
5800
6033
|
this.emitConeAngle = emitConeAngle;
|
|
5801
6034
|
|
|
5802
6035
|
// color settings
|
|
@@ -5812,27 +6045,27 @@ class ParticleEmitter extends EngineObject
|
|
|
5812
6045
|
this.randomColorLinear = randomColorLinear;
|
|
5813
6046
|
|
|
5814
6047
|
// particle settings
|
|
5815
|
-
/** @property {
|
|
6048
|
+
/** @property {number} - How long particles live */
|
|
5816
6049
|
this.particleTime = particleTime;
|
|
5817
|
-
/** @property {
|
|
6050
|
+
/** @property {number} - How big are particles at start */
|
|
5818
6051
|
this.sizeStart = sizeStart;
|
|
5819
|
-
/** @property {
|
|
6052
|
+
/** @property {number} - How big are particles at end */
|
|
5820
6053
|
this.sizeEnd = sizeEnd;
|
|
5821
|
-
/** @property {
|
|
6054
|
+
/** @property {number} - How fast are particles when spawned */
|
|
5822
6055
|
this.speed = speed;
|
|
5823
|
-
/** @property {
|
|
6056
|
+
/** @property {number} - How fast are particles rotating */
|
|
5824
6057
|
this.angleSpeed = angleSpeed;
|
|
5825
|
-
/** @property {
|
|
6058
|
+
/** @property {number} - How much to dampen particle speed */
|
|
5826
6059
|
this.damping = damping;
|
|
5827
|
-
/** @property {
|
|
6060
|
+
/** @property {number} - How much to dampen particle angular speed */
|
|
5828
6061
|
this.angleDamping = angleDamping;
|
|
5829
|
-
/** @property {
|
|
6062
|
+
/** @property {number} - How much gravity affects particles */
|
|
5830
6063
|
this.gravityScale = gravityScale;
|
|
5831
|
-
/** @property {
|
|
6064
|
+
/** @property {number} - Cone for start particle angle */
|
|
5832
6065
|
this.particleConeAngle = particleConeAngle;
|
|
5833
|
-
/** @property {
|
|
6066
|
+
/** @property {number} - How quick to fade in particles at start/end in percent of life */
|
|
5834
6067
|
this.fadeRate = fadeRate;
|
|
5835
|
-
/** @property {
|
|
6068
|
+
/** @property {number} - Apply extra randomness percent */
|
|
5836
6069
|
this.randomness = randomness;
|
|
5837
6070
|
/** @property {boolean} - Do particles collide against tiles */
|
|
5838
6071
|
this.collideTiles = collideTiles;
|
|
@@ -5840,16 +6073,16 @@ class ParticleEmitter extends EngineObject
|
|
|
5840
6073
|
this.additive = additive;
|
|
5841
6074
|
/** @property {boolean} - Should it be in local space of emitter */
|
|
5842
6075
|
this.localSpace = localSpace;
|
|
5843
|
-
/** @property {
|
|
6076
|
+
/** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
|
|
5844
6077
|
this.trailScale = 0;
|
|
5845
6078
|
/** @property {Function} - Callback when particle is destroyed */
|
|
5846
6079
|
this.particleDestroyCallback = undefined;
|
|
5847
6080
|
/** @property {Function} - Callback when particle is created */
|
|
5848
6081
|
this.particleCreateCallback = undefined;
|
|
5849
|
-
/** @property {
|
|
6082
|
+
/** @property {number} - Track particle emit time */
|
|
5850
6083
|
this.emitTimeBuffer = 0;
|
|
5851
6084
|
}
|
|
5852
|
-
|
|
6085
|
+
|
|
5853
6086
|
/** Update the emitter to spawn particles, called automatically by engine once each frame */
|
|
5854
6087
|
update()
|
|
5855
6088
|
{
|
|
@@ -5873,7 +6106,7 @@ class ParticleEmitter extends EngineObject
|
|
|
5873
6106
|
if (debugParticles)
|
|
5874
6107
|
{
|
|
5875
6108
|
// show emitter bounds
|
|
5876
|
-
const emitSize = typeof this.emitSize
|
|
6109
|
+
const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
|
|
5877
6110
|
debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
|
|
5878
6111
|
}
|
|
5879
6112
|
}
|
|
@@ -5883,7 +6116,7 @@ class ParticleEmitter extends EngineObject
|
|
|
5883
6116
|
emitParticle()
|
|
5884
6117
|
{
|
|
5885
6118
|
// spawn a particle
|
|
5886
|
-
let pos = typeof this.emitSize
|
|
6119
|
+
let pos = typeof this.emitSize === 'number' ? // check if number was used
|
|
5887
6120
|
randInCircle(this.emitSize/2) // circle emitter
|
|
5888
6121
|
: vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
|
|
5889
6122
|
.multiply(this.emitSize).rotate(this.angle)
|
|
@@ -5908,7 +6141,7 @@ class ParticleEmitter extends EngineObject
|
|
|
5908
6141
|
const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
|
|
5909
6142
|
const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
|
|
5910
6143
|
const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
|
|
5911
|
-
|
|
6144
|
+
|
|
5912
6145
|
// build particle
|
|
5913
6146
|
const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
|
|
5914
6147
|
particle.velocity = vec2().setAngle(velocityAngle, speed);
|
|
@@ -5953,38 +6186,38 @@ class Particle extends EngineObject
|
|
|
5953
6186
|
* Typically this is created automatically by a ParticleEmitter
|
|
5954
6187
|
* @param {Vector2} position - World space position of the particle
|
|
5955
6188
|
* @param {TileInfo} tileInfo - Tile info to render particles
|
|
5956
|
-
* @param {
|
|
6189
|
+
* @param {number} angle - Angle to rotate the particle
|
|
5957
6190
|
* @param {Color} colorStart - Color at start of life
|
|
5958
6191
|
* @param {Color} colorEnd - Color at end of life
|
|
5959
|
-
* @param {
|
|
5960
|
-
* @param {
|
|
5961
|
-
* @param {
|
|
5962
|
-
* @param {
|
|
6192
|
+
* @param {number} lifeTime - How long to live for
|
|
6193
|
+
* @param {number} sizeStart - Size at start of life
|
|
6194
|
+
* @param {number} sizeEnd - Size at end of life
|
|
6195
|
+
* @param {number} fadeRate - How quick to fade in/out
|
|
5963
6196
|
* @param {boolean} additive - Does it use additive blend mode
|
|
5964
|
-
* @param {
|
|
6197
|
+
* @param {number} trailScale - If a trail, how long to make it
|
|
5965
6198
|
* @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
|
|
5966
6199
|
* @param {Function} [destroyCallback] - Callback when particle dies
|
|
5967
6200
|
*/
|
|
5968
6201
|
constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
|
|
5969
6202
|
)
|
|
5970
|
-
{
|
|
5971
|
-
super(position, vec2(), tileInfo, angle);
|
|
5972
|
-
|
|
6203
|
+
{
|
|
6204
|
+
super(position, vec2(), tileInfo, angle);
|
|
6205
|
+
|
|
5973
6206
|
/** @property {Color} - Color at start of life */
|
|
5974
6207
|
this.colorStart = colorStart;
|
|
5975
6208
|
/** @property {Color} - Calculated change in color */
|
|
5976
6209
|
this.colorEndDelta = colorEnd.subtract(colorStart);
|
|
5977
|
-
/** @property {
|
|
6210
|
+
/** @property {number} - How long to live for */
|
|
5978
6211
|
this.lifeTime = lifeTime;
|
|
5979
|
-
/** @property {
|
|
6212
|
+
/** @property {number} - Size at start of life */
|
|
5980
6213
|
this.sizeStart = sizeStart;
|
|
5981
|
-
/** @property {
|
|
6214
|
+
/** @property {number} - Calculated change in size */
|
|
5982
6215
|
this.sizeEndDelta = sizeEnd - sizeStart;
|
|
5983
|
-
/** @property {
|
|
6216
|
+
/** @property {number} - How quick to fade in/out */
|
|
5984
6217
|
this.fadeRate = fadeRate;
|
|
5985
6218
|
/** @property {boolean} - Is it additive */
|
|
5986
6219
|
this.additive = additive;
|
|
5987
|
-
/** @property {
|
|
6220
|
+
/** @property {number} - If a trail, how long to make it */
|
|
5988
6221
|
this.trailScale = trailScale;
|
|
5989
6222
|
/** @property {ParticleEmitter} - Parent emitter if local space */
|
|
5990
6223
|
this.localSpaceEmitter = localSpaceEmitter;
|
|
@@ -6024,7 +6257,7 @@ class Particle extends EngineObject
|
|
|
6024
6257
|
this.colorStart.r + p * this.colorEndDelta.r,
|
|
6025
6258
|
this.colorStart.g + p * this.colorEndDelta.g,
|
|
6026
6259
|
this.colorStart.b + p * this.colorEndDelta.b,
|
|
6027
|
-
(this.colorStart.a + p * this.colorEndDelta.a) *
|
|
6260
|
+
(this.colorStart.a + p * this.colorEndDelta.a) *
|
|
6028
6261
|
(p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
|
|
6029
6262
|
|
|
6030
6263
|
// draw the particle
|
|
@@ -6034,7 +6267,7 @@ class Particle extends EngineObject
|
|
|
6034
6267
|
if (this.localSpaceEmitter)
|
|
6035
6268
|
{
|
|
6036
6269
|
// in local space of emitter
|
|
6037
|
-
pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
|
|
6270
|
+
pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
|
|
6038
6271
|
angle += this.localSpaceEmitter.angle;
|
|
6039
6272
|
}
|
|
6040
6273
|
if (this.trailScale)
|
|
@@ -6058,7 +6291,7 @@ class Particle extends EngineObject
|
|
|
6058
6291
|
this.additive && setBlendMode();
|
|
6059
6292
|
debugParticles && debugRect(pos, size, '#f005', 0, angle);
|
|
6060
6293
|
|
|
6061
|
-
if (p
|
|
6294
|
+
if (p === 1)
|
|
6062
6295
|
{
|
|
6063
6296
|
// destroy particle when it's time runs out
|
|
6064
6297
|
this.color = color;
|
|
@@ -6068,7 +6301,7 @@ class Particle extends EngineObject
|
|
|
6068
6301
|
}
|
|
6069
6302
|
}
|
|
6070
6303
|
}
|
|
6071
|
-
/**
|
|
6304
|
+
/**
|
|
6072
6305
|
* LittleJS Medal System
|
|
6073
6306
|
* - Tracks and displays medals
|
|
6074
6307
|
* - Saves medals to local storage
|
|
@@ -6089,7 +6322,7 @@ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
|
|
|
6089
6322
|
/** Initialize medals with a save name used for storage
|
|
6090
6323
|
* - Call this after creating all medals
|
|
6091
6324
|
* - Checks if medals are unlocked
|
|
6092
|
-
* @param {
|
|
6325
|
+
* @param {string} saveName
|
|
6093
6326
|
* @memberof Medals */
|
|
6094
6327
|
function medalsInit(saveName)
|
|
6095
6328
|
{
|
|
@@ -6104,7 +6337,7 @@ function medalsInit(saveName)
|
|
|
6104
6337
|
{
|
|
6105
6338
|
if (!medalsDisplayQueue.length)
|
|
6106
6339
|
return;
|
|
6107
|
-
|
|
6340
|
+
|
|
6108
6341
|
// update first medal in queue
|
|
6109
6342
|
const medal = medalsDisplayQueue[0];
|
|
6110
6343
|
const time = timeReal - medalsDisplayTimeLast;
|
|
@@ -6119,7 +6352,7 @@ function medalsInit(saveName)
|
|
|
6119
6352
|
{
|
|
6120
6353
|
// slide on/off medals
|
|
6121
6354
|
const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
|
|
6122
|
-
const hidePercent =
|
|
6355
|
+
const hidePercent =
|
|
6123
6356
|
time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
|
|
6124
6357
|
time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
|
|
6125
6358
|
medal.render(hidePercent);
|
|
@@ -6135,43 +6368,43 @@ function medalsForEach(callback)
|
|
|
6135
6368
|
|
|
6136
6369
|
///////////////////////////////////////////////////////////////////////////////
|
|
6137
6370
|
|
|
6138
|
-
/**
|
|
6139
|
-
* Medal - Tracks an unlockable medal
|
|
6371
|
+
/**
|
|
6372
|
+
* Medal - Tracks an unlockable medal
|
|
6140
6373
|
* @example
|
|
6141
6374
|
* // create a medal
|
|
6142
6375
|
* const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
|
|
6143
|
-
*
|
|
6376
|
+
*
|
|
6144
6377
|
* // initialize medals
|
|
6145
6378
|
* medalsInit('Example Game');
|
|
6146
|
-
*
|
|
6379
|
+
*
|
|
6147
6380
|
* // unlock the medal
|
|
6148
6381
|
* medal_example.unlock();
|
|
6149
6382
|
*/
|
|
6150
6383
|
class Medal
|
|
6151
6384
|
{
|
|
6152
6385
|
/** Create a medal object and adds it to the list of medals
|
|
6153
|
-
* @param {
|
|
6154
|
-
* @param {
|
|
6155
|
-
* @param {
|
|
6156
|
-
* @param {
|
|
6157
|
-
* @param {
|
|
6386
|
+
* @param {number} id - The unique identifier of the medal
|
|
6387
|
+
* @param {string} name - Name of the medal
|
|
6388
|
+
* @param {string} [description] - Description of the medal
|
|
6389
|
+
* @param {string} [icon] - Icon for the medal
|
|
6390
|
+
* @param {string} [src] - Image location for the medal
|
|
6158
6391
|
*/
|
|
6159
6392
|
constructor(id, name, description='', icon='🏆', src)
|
|
6160
6393
|
{
|
|
6161
6394
|
ASSERT(id >= 0 && !medals[id]);
|
|
6162
|
-
|
|
6163
|
-
/** @property {
|
|
6395
|
+
|
|
6396
|
+
/** @property {number} - The unique identifier of the medal */
|
|
6164
6397
|
this.id = id;
|
|
6165
|
-
|
|
6166
|
-
/** @property {
|
|
6398
|
+
|
|
6399
|
+
/** @property {string} - Name of the medal */
|
|
6167
6400
|
this.name = name;
|
|
6168
|
-
|
|
6169
|
-
/** @property {
|
|
6401
|
+
|
|
6402
|
+
/** @property {string} - Description of the medal */
|
|
6170
6403
|
this.description = description;
|
|
6171
|
-
|
|
6172
|
-
/** @property {
|
|
6404
|
+
|
|
6405
|
+
/** @property {string} - Icon for the medal */
|
|
6173
6406
|
this.icon = icon;
|
|
6174
|
-
|
|
6407
|
+
|
|
6175
6408
|
/** @property {boolean} - Is the medal unlocked? */
|
|
6176
6409
|
this.unlocked = false;
|
|
6177
6410
|
|
|
@@ -6196,7 +6429,7 @@ class Medal
|
|
|
6196
6429
|
}
|
|
6197
6430
|
|
|
6198
6431
|
/** Render a medal
|
|
6199
|
-
* @param {
|
|
6432
|
+
* @param {number} [hidePercent] - How much to slide the medal off screen
|
|
6200
6433
|
*/
|
|
6201
6434
|
render(hidePercent=0)
|
|
6202
6435
|
{
|
|
@@ -6237,7 +6470,7 @@ class Medal
|
|
|
6237
6470
|
|
|
6238
6471
|
/** Render the icon for a medal
|
|
6239
6472
|
* @param {Vector2} pos - Screen space position
|
|
6240
|
-
* @param {
|
|
6473
|
+
* @param {number} size - Screen space size
|
|
6241
6474
|
*/
|
|
6242
6475
|
renderIcon(pos, size)
|
|
6243
6476
|
{
|
|
@@ -6247,14 +6480,14 @@ class Medal
|
|
|
6247
6480
|
else
|
|
6248
6481
|
drawTextScreen(this.icon, pos, size*.7, BLACK);
|
|
6249
6482
|
}
|
|
6250
|
-
|
|
6483
|
+
|
|
6251
6484
|
// Get local storage key used by the medal
|
|
6252
6485
|
storageKey() { return medalsSaveName + '_' + this.id; }
|
|
6253
6486
|
}
|
|
6254
6487
|
/**
|
|
6255
6488
|
* LittleJS WebGL Interface
|
|
6256
|
-
* - All
|
|
6257
|
-
* - Will fall back to 2D canvas rendering if
|
|
6489
|
+
* - All WebGL used by the engine is wrapped up here
|
|
6490
|
+
* - Will fall back to 2D canvas rendering if WebGL is not supported
|
|
6258
6491
|
* - For normal stuff you won't need to see or call anything in this file
|
|
6259
6492
|
* - For advanced stuff there are helper functions to create shaders, textures, etc
|
|
6260
6493
|
* - Can be disabled with glEnable to revert to 2D canvas rendering
|
|
@@ -6269,24 +6502,27 @@ class Medal
|
|
|
6269
6502
|
* @memberof WebGL */
|
|
6270
6503
|
let glCanvas;
|
|
6271
6504
|
|
|
6272
|
-
/**
|
|
6505
|
+
/** WebGL2 context for `glCanvas`
|
|
6273
6506
|
* @type {WebGL2RenderingContext}
|
|
6274
6507
|
* @memberof WebGL */
|
|
6275
6508
|
let glContext;
|
|
6276
6509
|
|
|
6277
|
-
/** Should
|
|
6510
|
+
/** Should WebGL be setup with anti-aliasing? must be set before calling engineInit
|
|
6278
6511
|
* @type {boolean}
|
|
6279
6512
|
* @memberof WebGL */
|
|
6280
6513
|
let glAntialias = true;
|
|
6281
6514
|
|
|
6282
6515
|
// WebGL internal variables not exposed to documentation
|
|
6283
|
-
let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData,
|
|
6516
|
+
let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
|
|
6284
6517
|
|
|
6285
|
-
// WebGL internal constants
|
|
6286
|
-
const
|
|
6518
|
+
// WebGL internal constants
|
|
6519
|
+
const gl_ARRAY_BUFFER_SIZE = 4e5;
|
|
6287
6520
|
const gl_INDICES_PER_INSTANCE = 11;
|
|
6288
6521
|
const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
|
|
6289
|
-
const
|
|
6522
|
+
const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
|
|
6523
|
+
const gl_INDICES_PER_POLY_VERTEX = 3;
|
|
6524
|
+
const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
|
|
6525
|
+
const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
|
|
6290
6526
|
|
|
6291
6527
|
///////////////////////////////////////////////////////////////////////////////
|
|
6292
6528
|
|
|
@@ -6307,11 +6543,11 @@ function glInit()
|
|
|
6307
6543
|
return;
|
|
6308
6544
|
}
|
|
6309
6545
|
|
|
6310
|
-
// create the
|
|
6546
|
+
// create the WebGL canvas
|
|
6311
6547
|
const rootElement = mainCanvas.parentElement;
|
|
6312
6548
|
rootElement.appendChild(glCanvas);
|
|
6313
6549
|
|
|
6314
|
-
// setup
|
|
6550
|
+
// setup instanced rendering shader program
|
|
6315
6551
|
glShader = glCreateProgram(
|
|
6316
6552
|
'#version 300 es\n' + // specify GLSL ES version
|
|
6317
6553
|
'precision highp float;'+ // use highp for better accuracy
|
|
@@ -6339,40 +6575,59 @@ function glInit()
|
|
|
6339
6575
|
'}' // end of shader
|
|
6340
6576
|
);
|
|
6341
6577
|
|
|
6578
|
+
// setup poly rendering shaders
|
|
6579
|
+
glPolyShader = glCreateProgram(
|
|
6580
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
6581
|
+
'precision highp float;'+ // use highp for better accuracy
|
|
6582
|
+
'uniform mat4 m;'+ // transform matrix
|
|
6583
|
+
'in vec2 p;'+ // in: position
|
|
6584
|
+
'in vec4 c;'+ // in: color
|
|
6585
|
+
'out vec4 d;'+ // out: color
|
|
6586
|
+
'void main(){'+ // shader entry point
|
|
6587
|
+
'gl_Position=m*vec4(p,1,1);'+ // transform position
|
|
6588
|
+
'd=c;'+ // pass color to fragment shader
|
|
6589
|
+
'}' // end of shader
|
|
6590
|
+
,
|
|
6591
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
6592
|
+
'precision highp float;'+ // use highp for better accuracy
|
|
6593
|
+
'in vec4 d;'+ // in: color
|
|
6594
|
+
'out vec4 c;'+ // out: color
|
|
6595
|
+
'void main(){'+ // shader entry point
|
|
6596
|
+
'c=d;'+ // set color
|
|
6597
|
+
'}' // end of shader
|
|
6598
|
+
);
|
|
6599
|
+
|
|
6342
6600
|
// init buffers
|
|
6343
|
-
const glInstanceData = new ArrayBuffer(
|
|
6601
|
+
const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
|
|
6344
6602
|
glPositionData = new Float32Array(glInstanceData);
|
|
6345
6603
|
glColorData = new Uint32Array(glInstanceData);
|
|
6346
6604
|
glArrayBuffer = glContext.createBuffer();
|
|
6347
6605
|
glGeometryBuffer = glContext.createBuffer();
|
|
6348
6606
|
|
|
6349
6607
|
// create the geometry buffer, triangle strip square
|
|
6350
|
-
const geometry = new Float32Array([
|
|
6608
|
+
const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
|
|
6351
6609
|
glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
|
|
6352
6610
|
glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
|
|
6353
6611
|
}
|
|
6354
6612
|
|
|
6355
|
-
|
|
6356
|
-
// Also used by tile layer rendering when redrawing tiles
|
|
6357
|
-
function glPreRender()
|
|
6613
|
+
function glSetInstancedMode(force=false)
|
|
6358
6614
|
{
|
|
6359
|
-
if (!
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6615
|
+
if (!glPolyMode && !force)
|
|
6616
|
+
return;
|
|
6617
|
+
|
|
6618
|
+
// setup instanced mode
|
|
6619
|
+
glFlush();
|
|
6620
|
+
glPolyMode = false;
|
|
6363
6621
|
glContext.useProgram(glShader);
|
|
6364
|
-
glContext.activeTexture(glContext.TEXTURE0);
|
|
6365
|
-
if (textureInfos[0])
|
|
6366
|
-
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
|
|
6367
6622
|
|
|
6368
6623
|
// set vertex attributes
|
|
6369
|
-
let offset =
|
|
6624
|
+
let offset = 0;
|
|
6370
6625
|
const initVertexAttribArray = (name, type, typeSize, size)=>
|
|
6371
6626
|
{
|
|
6372
6627
|
const location = glContext.getAttribLocation(glShader, name);
|
|
6373
6628
|
const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
|
|
6374
6629
|
const divisor = typeSize && 1; // only if not geometry
|
|
6375
|
-
const normalize = typeSize
|
|
6630
|
+
const normalize = typeSize === 1; // only if color
|
|
6376
6631
|
glContext.enableVertexAttribArray(location);
|
|
6377
6632
|
glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
|
|
6378
6633
|
glContext.vertexAttribDivisor(location, divisor);
|
|
@@ -6381,26 +6636,84 @@ function glPreRender()
|
|
|
6381
6636
|
glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
|
|
6382
6637
|
initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
|
|
6383
6638
|
glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
|
|
6384
|
-
glContext.bufferData(glContext.ARRAY_BUFFER,
|
|
6639
|
+
glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
|
|
6385
6640
|
initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
|
|
6386
6641
|
initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
|
|
6387
6642
|
initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
|
|
6388
6643
|
initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
|
|
6389
6644
|
initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
|
|
6645
|
+
}
|
|
6646
|
+
|
|
6647
|
+
function glSetPolyMode()
|
|
6648
|
+
{
|
|
6649
|
+
if (glPolyMode)
|
|
6650
|
+
return;
|
|
6390
6651
|
|
|
6652
|
+
// setup poly mode
|
|
6653
|
+
glFlush();
|
|
6654
|
+
glPolyMode = true;
|
|
6655
|
+
glContext.useProgram(glPolyShader);
|
|
6656
|
+
|
|
6657
|
+
// set vertex attributes
|
|
6658
|
+
let offset = 0;
|
|
6659
|
+
const initVertexAttribArray = (name, type, typeSize, size)=>
|
|
6660
|
+
{
|
|
6661
|
+
const location = glContext.getAttribLocation(glPolyShader, name);
|
|
6662
|
+
const normalize = typeSize === 1; // only normalize if color
|
|
6663
|
+
const stride = gl_POLY_VERTEX_BYTE_STRIDE;
|
|
6664
|
+
glContext.enableVertexAttribArray(location);
|
|
6665
|
+
glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
|
|
6666
|
+
glContext.vertexAttribDivisor(location, 0);
|
|
6667
|
+
offset += size*typeSize;
|
|
6668
|
+
}
|
|
6669
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
|
|
6670
|
+
glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
|
|
6671
|
+
initVertexAttribArray('p', glContext.FLOAT, 4, 2); // position
|
|
6672
|
+
initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
|
|
6673
|
+
}
|
|
6674
|
+
|
|
6675
|
+
// Setup WebGL render each frame, called automatically by engine
|
|
6676
|
+
// Also used by tile layer rendering when redrawing tiles
|
|
6677
|
+
function glPreRender()
|
|
6678
|
+
{
|
|
6679
|
+
if (!glEnable || !glContext) return;
|
|
6680
|
+
|
|
6681
|
+
// clear the canvas
|
|
6682
|
+
glClearCanvas();
|
|
6683
|
+
|
|
6391
6684
|
// build the transform matrix
|
|
6392
6685
|
const s = vec2(2*cameraScale).divide(mainCanvasSize);
|
|
6393
6686
|
const rotatedCam = cameraPos.rotate(-cameraAngle);
|
|
6394
6687
|
const p = vec2(-1).subtract(rotatedCam.multiply(s));
|
|
6395
6688
|
const ca = Math.cos(cameraAngle);
|
|
6396
6689
|
const sa = Math.sin(cameraAngle);
|
|
6397
|
-
|
|
6398
|
-
[
|
|
6690
|
+
const transform = [
|
|
6399
6691
|
s.x * ca, s.y * sa, 0, 0,
|
|
6400
6692
|
-s.x * sa, s.y * ca, 0, 0,
|
|
6401
6693
|
1, 1, 1, 0,
|
|
6402
|
-
p.x, p.y, 0, 1
|
|
6403
|
-
|
|
6694
|
+
p.x, p.y, 0, 1];
|
|
6695
|
+
|
|
6696
|
+
// set the same matrix for both shaders
|
|
6697
|
+
const initUniform = (program, uniform, value) =>
|
|
6698
|
+
{
|
|
6699
|
+
glContext.useProgram(program);
|
|
6700
|
+
const location = glContext.getUniformLocation(program, uniform);
|
|
6701
|
+
glContext.uniformMatrix4fv(location, false, value);
|
|
6702
|
+
}
|
|
6703
|
+
initUniform(glPolyShader, 'm', transform);
|
|
6704
|
+
initUniform(glShader, 'm', transform);
|
|
6705
|
+
|
|
6706
|
+
// set the active texture
|
|
6707
|
+
glContext.activeTexture(glContext.TEXTURE0);
|
|
6708
|
+
if (textureInfos[0])
|
|
6709
|
+
{
|
|
6710
|
+
glActiveTexture = textureInfos[0].glTexture;
|
|
6711
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
6712
|
+
}
|
|
6713
|
+
|
|
6714
|
+
// start in instanced rendering mode with additive blending off
|
|
6715
|
+
glAdditive = glBatchAdditive = glPolyMode = false;
|
|
6716
|
+
glSetInstancedMode(true);
|
|
6404
6717
|
}
|
|
6405
6718
|
|
|
6406
6719
|
/** Clear the canvas and setup the viewport
|
|
@@ -6408,21 +6721,21 @@ function glPreRender()
|
|
|
6408
6721
|
function glClearCanvas()
|
|
6409
6722
|
{
|
|
6410
6723
|
if (!glContext) return;
|
|
6411
|
-
|
|
6724
|
+
|
|
6412
6725
|
// clear and set to same size as main canvas
|
|
6413
6726
|
glContext.viewport(0, 0, glCanvas.width=drawCanvas.width, glCanvas.height=drawCanvas.height);
|
|
6414
6727
|
glContext.clear(glContext.COLOR_BUFFER_BIT);
|
|
6415
6728
|
}
|
|
6416
6729
|
|
|
6417
|
-
/** Set the
|
|
6730
|
+
/** Set the WebGL texture, called automatically if using multiple textures
|
|
6418
6731
|
* - This may also flush the gl buffer resulting in more draw calls and worse performance
|
|
6419
6732
|
* @param {WebGLTexture} texture
|
|
6420
|
-
* @param {boolean} wrap - Should the texture wrap or clamp
|
|
6733
|
+
* @param {boolean} [wrap] - Should the texture wrap or clamp
|
|
6421
6734
|
* @memberof WebGL */
|
|
6422
6735
|
function glSetTexture(texture, wrap=false)
|
|
6423
6736
|
{
|
|
6424
6737
|
// must flush cache with the old texture to set a new one
|
|
6425
|
-
if (!glContext || texture
|
|
6738
|
+
if (!glContext || texture === glActiveTexture)
|
|
6426
6739
|
return;
|
|
6427
6740
|
|
|
6428
6741
|
glFlush();
|
|
@@ -6435,8 +6748,8 @@ function glSetTexture(texture, wrap=false)
|
|
|
6435
6748
|
}
|
|
6436
6749
|
|
|
6437
6750
|
/** Compile WebGL shader of the given type, will throw errors if in debug mode
|
|
6438
|
-
* @param {
|
|
6439
|
-
* @param {
|
|
6751
|
+
* @param {string} source
|
|
6752
|
+
* @param {number} type
|
|
6440
6753
|
* @return {WebGLShader}
|
|
6441
6754
|
* @memberof WebGL */
|
|
6442
6755
|
function glCompileShader(source, type)
|
|
@@ -6455,8 +6768,8 @@ function glCompileShader(source, type)
|
|
|
6455
6768
|
}
|
|
6456
6769
|
|
|
6457
6770
|
/** Create WebGL program with given shaders
|
|
6458
|
-
* @param {
|
|
6459
|
-
* @param {
|
|
6771
|
+
* @param {string} vsSource
|
|
6772
|
+
* @param {string} fsSource
|
|
6460
6773
|
* @return {WebGLProgram}
|
|
6461
6774
|
* @memberof WebGL */
|
|
6462
6775
|
function glCreateProgram(vsSource, fsSource)
|
|
@@ -6504,7 +6817,7 @@ function glCreateTexture(image)
|
|
|
6504
6817
|
const whitePixel = new Uint8Array([255, 255, 255, 255]);
|
|
6505
6818
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
|
|
6506
6819
|
}
|
|
6507
|
-
|
|
6820
|
+
|
|
6508
6821
|
// set texture filtering
|
|
6509
6822
|
const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
|
|
6510
6823
|
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
|
|
@@ -6512,7 +6825,6 @@ function glCreateTexture(image)
|
|
|
6512
6825
|
return texture;
|
|
6513
6826
|
}
|
|
6514
6827
|
|
|
6515
|
-
|
|
6516
6828
|
/** Deletes a WebGL texture
|
|
6517
6829
|
* @param {WebGLTexture} [texture]
|
|
6518
6830
|
* @memberof WebGL */
|
|
@@ -6540,18 +6852,23 @@ function glSetTextureData(texture, image)
|
|
|
6540
6852
|
* @memberof WebGL */
|
|
6541
6853
|
function glFlush()
|
|
6542
6854
|
{
|
|
6543
|
-
if (
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6855
|
+
if (glEnable && glContext && glBatchCount)
|
|
6856
|
+
{
|
|
6857
|
+
// set bend mode
|
|
6858
|
+
const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
|
|
6859
|
+
glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
|
|
6860
|
+
glContext.enable(glContext.BLEND);
|
|
6861
|
+
glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData);
|
|
6862
|
+
|
|
6863
|
+
// draw the batch
|
|
6864
|
+
if (glPolyMode)
|
|
6865
|
+
glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
|
|
6866
|
+
else
|
|
6867
|
+
glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
|
|
6868
|
+
if (debug || showWatermark)
|
|
6869
|
+
drawCount += glBatchCount;
|
|
6870
|
+
glBatchCount = 0;
|
|
6871
|
+
}
|
|
6555
6872
|
glBatchAdditive = glAdditive;
|
|
6556
6873
|
}
|
|
6557
6874
|
|
|
@@ -6567,7 +6884,8 @@ function glCopyToContext(context)
|
|
|
6567
6884
|
context.drawImage(glCanvas, 0, 0);
|
|
6568
6885
|
}
|
|
6569
6886
|
|
|
6570
|
-
/** Set anti-aliasing for
|
|
6887
|
+
/** Set anti-aliasing for WebGL canvas
|
|
6888
|
+
* Must be called before engineInit
|
|
6571
6889
|
* @param {boolean} [antialias]
|
|
6572
6890
|
* @memberof WebGL */
|
|
6573
6891
|
function glSetAntialias(antialias=true)
|
|
@@ -6577,25 +6895,27 @@ function glSetAntialias(antialias=true)
|
|
|
6577
6895
|
}
|
|
6578
6896
|
|
|
6579
6897
|
/** Add a sprite to the gl draw list, used by all gl draw functions
|
|
6580
|
-
* @param {
|
|
6581
|
-
* @param {
|
|
6582
|
-
* @param {
|
|
6583
|
-
* @param {
|
|
6584
|
-
* @param {
|
|
6585
|
-
* @param {
|
|
6586
|
-
* @param {
|
|
6587
|
-
* @param {
|
|
6588
|
-
* @param {
|
|
6589
|
-
* @param {
|
|
6590
|
-
* @param {
|
|
6898
|
+
* @param {number} x
|
|
6899
|
+
* @param {number} y
|
|
6900
|
+
* @param {number} sizeX
|
|
6901
|
+
* @param {number} sizeY
|
|
6902
|
+
* @param {number} [angle]
|
|
6903
|
+
* @param {number} [uv0X]
|
|
6904
|
+
* @param {number} [uv0Y]
|
|
6905
|
+
* @param {number} [uv1X]
|
|
6906
|
+
* @param {number} [uv1Y]
|
|
6907
|
+
* @param {number} [rgba=-1] - white is -1
|
|
6908
|
+
* @param {number} [rgbaAdditive=0] - black is 0
|
|
6591
6909
|
* @memberof WebGL */
|
|
6592
6910
|
function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
|
|
6593
6911
|
{
|
|
6594
6912
|
// flush if there is not enough room or if different blend mode
|
|
6595
|
-
if (
|
|
6913
|
+
if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
|
|
6596
6914
|
glFlush();
|
|
6915
|
+
glSetInstancedMode();
|
|
6597
6916
|
|
|
6598
|
-
|
|
6917
|
+
glPolyMode = false;
|
|
6918
|
+
let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
|
|
6599
6919
|
glPositionData[offset++] = x;
|
|
6600
6920
|
glPositionData[offset++] = y;
|
|
6601
6921
|
glPositionData[offset++] = sizeX;
|
|
@@ -6607,6 +6927,301 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
6607
6927
|
glColorData[offset++] = rgba;
|
|
6608
6928
|
glColorData[offset++] = rgbaAdditive;
|
|
6609
6929
|
glPositionData[offset++] = angle;
|
|
6930
|
+
}
|
|
6931
|
+
|
|
6932
|
+
/** Transform and add a polygon to the gl draw list
|
|
6933
|
+
* @param {Array} points - Array of Vector2 points
|
|
6934
|
+
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
6935
|
+
* @param {number} x
|
|
6936
|
+
* @param {number} y
|
|
6937
|
+
* @param {number} sx
|
|
6938
|
+
* @param {number} sy
|
|
6939
|
+
* @param {number} angle
|
|
6940
|
+
* @param {boolean} [tristrip] - should tristrip algorithm be used
|
|
6941
|
+
* @memberof WebGL */
|
|
6942
|
+
function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
6943
|
+
{
|
|
6944
|
+
const pointsOut = [];
|
|
6945
|
+
for (const p of points)
|
|
6946
|
+
{
|
|
6947
|
+
// transform the point
|
|
6948
|
+
const px = p.x*sx;
|
|
6949
|
+
const py = p.y*sy;
|
|
6950
|
+
const sa = Math.sin(-angle);
|
|
6951
|
+
const ca = Math.cos(-angle);
|
|
6952
|
+
pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
|
|
6953
|
+
}
|
|
6954
|
+
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
6955
|
+
glDrawPoints(drawPoints, rgba);
|
|
6956
|
+
}
|
|
6957
|
+
|
|
6958
|
+
/** Transform and add a polygon to the gl draw list
|
|
6959
|
+
* @param {Array} points - Array of Vector2 points
|
|
6960
|
+
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
6961
|
+
* @param {number} lineWidth - Width of the outline
|
|
6962
|
+
* @param {number} x
|
|
6963
|
+
* @param {number} y
|
|
6964
|
+
* @param {number} sx
|
|
6965
|
+
* @param {number} sy
|
|
6966
|
+
* @param {number} angle
|
|
6967
|
+
* @memberof WebGL */
|
|
6968
|
+
function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle)
|
|
6969
|
+
{
|
|
6970
|
+
const outlinePoints = glMakeOutline(points, lineWidth);
|
|
6971
|
+
glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
|
|
6972
|
+
}
|
|
6973
|
+
|
|
6974
|
+
/** Add a polygon to the gl draw list
|
|
6975
|
+
* @param {Array} points - Array of Vector2 points in triangle strip order
|
|
6976
|
+
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
6977
|
+
* @memberof WebGL */
|
|
6978
|
+
function glDrawPoints(points, rgba)
|
|
6979
|
+
{
|
|
6980
|
+
if (!glEnable || points.length < 3)
|
|
6981
|
+
return; // needs at least 3 points to have area
|
|
6982
|
+
|
|
6983
|
+
// add 2 degenerate verts if batching with existing polys to separate them
|
|
6984
|
+
const needsBridge = glPolyMode && glBatchCount > 0;
|
|
6985
|
+
const bridgeVerts = needsBridge ? 2 : 0;
|
|
6986
|
+
const vertCount = points.length + bridgeVerts;
|
|
6987
|
+
|
|
6988
|
+
// flush if there is not enough room or if different blend mode
|
|
6989
|
+
if (!glPolyMode || glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
6990
|
+
glFlush();
|
|
6991
|
+
glSetPolyMode();
|
|
6992
|
+
|
|
6993
|
+
let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
|
|
6994
|
+
|
|
6995
|
+
// add degenerate bridge if needed (repeat last vertex of previous poly, then first of new poly)
|
|
6996
|
+
if (needsBridge)
|
|
6997
|
+
{
|
|
6998
|
+
// repeat last vertex from previous batch (it's at offset - 3)
|
|
6999
|
+
const prevOffset = offset - 3;
|
|
7000
|
+
glPositionData[offset++] = glPositionData[prevOffset];
|
|
7001
|
+
glPositionData[offset++] = glPositionData[prevOffset + 1];
|
|
7002
|
+
glColorData[offset++] = glColorData[prevOffset + 2];
|
|
7003
|
+
|
|
7004
|
+
// repeat first vertex of new poly
|
|
7005
|
+
glPositionData[offset++] = points[0].x;
|
|
7006
|
+
glPositionData[offset++] = points[0].y;
|
|
7007
|
+
glColorData[offset++] = rgba;
|
|
7008
|
+
}
|
|
7009
|
+
|
|
7010
|
+
// write vertices - they're already in triangle strip order
|
|
7011
|
+
for (const point of points)
|
|
7012
|
+
{
|
|
7013
|
+
glPositionData[offset++] = point.x;
|
|
7014
|
+
glPositionData[offset++] = point.y;
|
|
7015
|
+
glColorData[offset++] = rgba;
|
|
7016
|
+
}
|
|
7017
|
+
glBatchCount += vertCount;
|
|
7018
|
+
}
|
|
7019
|
+
|
|
7020
|
+
// WebGL internal function to convert polygon to outline triangle strip
|
|
7021
|
+
function glMakeOutline(points, width)
|
|
7022
|
+
{
|
|
7023
|
+
if (points.length < 2)
|
|
7024
|
+
return [];
|
|
7025
|
+
|
|
7026
|
+
const halfWidth = width / 2;
|
|
7027
|
+
const strip = [];
|
|
7028
|
+
const n = points.length;
|
|
7029
|
+
const e = 1e-6;
|
|
7030
|
+
for (let i = 0; i < n; i++)
|
|
7031
|
+
{
|
|
7032
|
+
// for each vertex, calculate normal based on adjacent edges
|
|
7033
|
+
const prev = points[(i - 1 + n) % n];
|
|
7034
|
+
const curr = points[i];
|
|
7035
|
+
const next = points[(i + 1) % n];
|
|
7036
|
+
|
|
7037
|
+
// direction from previous to current
|
|
7038
|
+
const dx1 = curr.x - prev.x;
|
|
7039
|
+
const dy1 = curr.y - prev.y;
|
|
7040
|
+
const len1 = (dx1*dx1 + dy1*dy1)**.5;
|
|
7041
|
+
|
|
7042
|
+
// direction from current to next
|
|
7043
|
+
const dx2 = next.x - curr.x;
|
|
7044
|
+
const dy2 = next.y - curr.y;
|
|
7045
|
+
const len2 = (dx2*dx2 + dy2*dy2)**.5;
|
|
7046
|
+
|
|
7047
|
+
if (len1 < e && len2 < e)
|
|
7048
|
+
continue; // skip degenerate point
|
|
7049
|
+
|
|
7050
|
+
// calculate perpendicular normals for each edge
|
|
7051
|
+
const nx1 = len1 > e ? -dy1 / len1 : 0;
|
|
7052
|
+
const ny1 = len1 > e ? dx1 / len1 : 0;
|
|
7053
|
+
const nx2 = len2 > e ? -dy2 / len2 : 0;
|
|
7054
|
+
const ny2 = len2 > e ? dx2 / len2 : 0;
|
|
7055
|
+
|
|
7056
|
+
// average the normals for miter
|
|
7057
|
+
let nx = nx1 + nx2;
|
|
7058
|
+
let ny = ny1 + ny2;
|
|
7059
|
+
const nlen = (nx*nx + ny*ny)**.5;
|
|
7060
|
+
if (nlen < e)
|
|
7061
|
+
{
|
|
7062
|
+
// 180 degree turn - use perpendicular
|
|
7063
|
+
nx = nx1;
|
|
7064
|
+
ny = ny1;
|
|
7065
|
+
}
|
|
7066
|
+
else
|
|
7067
|
+
{
|
|
7068
|
+
// calculate miter length
|
|
7069
|
+
nx /= nlen;
|
|
7070
|
+
ny /= nlen;
|
|
7071
|
+
const dot = nx1 * nx + ny1 * ny;
|
|
7072
|
+
if (dot > e)
|
|
7073
|
+
{
|
|
7074
|
+
// scale normal by miter length
|
|
7075
|
+
const miterLength = 1 / dot;
|
|
7076
|
+
nx *= miterLength;
|
|
7077
|
+
ny *= miterLength;
|
|
7078
|
+
}
|
|
7079
|
+
}
|
|
7080
|
+
|
|
7081
|
+
// create inner and outer points along the normal
|
|
7082
|
+
const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
|
|
7083
|
+
const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
|
|
7084
|
+
strip.push(inner);
|
|
7085
|
+
strip.push(outer);
|
|
7086
|
+
}
|
|
7087
|
+
if (strip.length > 1)
|
|
7088
|
+
{
|
|
7089
|
+
// close the loop
|
|
7090
|
+
strip.push(strip[0]);
|
|
7091
|
+
strip.push(strip[1]);
|
|
7092
|
+
}
|
|
7093
|
+
return strip;
|
|
7094
|
+
}
|
|
7095
|
+
|
|
7096
|
+
// WebGL internal function to convert polys to tri strips
|
|
7097
|
+
function glPolyStrip(points)
|
|
7098
|
+
{
|
|
7099
|
+
// validate input
|
|
7100
|
+
if (points.length < 3)
|
|
7101
|
+
return [];
|
|
7102
|
+
|
|
7103
|
+
// cross product helper: (b-a) x (c-a)
|
|
7104
|
+
const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
7105
|
+
|
|
7106
|
+
// calculate signed area of polygon
|
|
7107
|
+
const signedArea = (poly)=>
|
|
7108
|
+
{
|
|
7109
|
+
let area = 0;
|
|
7110
|
+
for (let i = poly.length; i--;)
|
|
7111
|
+
{
|
|
7112
|
+
const j = (i+1) % poly.length;
|
|
7113
|
+
area += poly[i].cross(poly[j]);
|
|
7114
|
+
}
|
|
7115
|
+
return area;
|
|
7116
|
+
}
|
|
7117
|
+
|
|
7118
|
+
// ensure counter-clockwise winding
|
|
7119
|
+
if (signedArea(points) < 0)
|
|
7120
|
+
points = points.reverse();
|
|
7121
|
+
|
|
7122
|
+
// tolerance constants
|
|
7123
|
+
const e = 1e-10;
|
|
7124
|
+
|
|
7125
|
+
// check if point is inside triangle
|
|
7126
|
+
const pointInTriangle = (p, a, b, c)=>
|
|
7127
|
+
{
|
|
7128
|
+
const c1 = cross(a, b, p);
|
|
7129
|
+
const c2 = cross(b, c, p);
|
|
7130
|
+
const c3 = cross(c, a, p);
|
|
7131
|
+
const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
|
|
7132
|
+
const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
|
|
7133
|
+
return !(negative && positive);
|
|
7134
|
+
};
|
|
7135
|
+
|
|
7136
|
+
// ear clipping triangulation
|
|
7137
|
+
const indices = [];
|
|
7138
|
+
for (let i = 0; i < points.length; ++i)
|
|
7139
|
+
indices[i] = i;
|
|
7140
|
+
const triangles = [];
|
|
7141
|
+
let attempts = 0;
|
|
7142
|
+
const maxAttempts = points.length ** 2 + 100;
|
|
7143
|
+
while (indices.length > 3 && attempts++ < maxAttempts)
|
|
7144
|
+
{
|
|
7145
|
+
let foundEar = false;
|
|
7146
|
+
for (let i = indices.length; --i;)
|
|
7147
|
+
{
|
|
7148
|
+
const i0 = indices[(i + indices.length - 1) % indices.length];
|
|
7149
|
+
const i1 = indices[i];
|
|
7150
|
+
const i2 = indices[(i + 1) % indices.length];
|
|
7151
|
+
const a = points[i0], b = points[i1], c = points[i2];
|
|
7152
|
+
|
|
7153
|
+
// check if convex
|
|
7154
|
+
if (cross(a, b, c) < e)
|
|
7155
|
+
continue;
|
|
7156
|
+
|
|
7157
|
+
// check if any other point is inside
|
|
7158
|
+
let hasInside = false;
|
|
7159
|
+
for (let j = 0; j < indices.length; j++)
|
|
7160
|
+
{
|
|
7161
|
+
const k = indices[j];
|
|
7162
|
+
if (k === i0 || k === i1 || k === i2)
|
|
7163
|
+
continue;
|
|
7164
|
+
const p = points[k];
|
|
7165
|
+
hasInside = pointInTriangle(p, a, b, c);
|
|
7166
|
+
if (hasInside)
|
|
7167
|
+
break;
|
|
7168
|
+
}
|
|
7169
|
+
if (hasInside)
|
|
7170
|
+
continue;
|
|
7171
|
+
|
|
7172
|
+
// found valid ear
|
|
7173
|
+
triangles.push([i0, i1, i2]);
|
|
7174
|
+
indices.splice(i, 1);
|
|
7175
|
+
foundEar = true;
|
|
7176
|
+
break;
|
|
7177
|
+
}
|
|
7178
|
+
|
|
7179
|
+
// fallback for degenerate cases
|
|
7180
|
+
if (!foundEar)
|
|
7181
|
+
{
|
|
7182
|
+
let worstIndex = -1, worstValue = Infinity;
|
|
7183
|
+
for (let i = indices.length; --i;)
|
|
7184
|
+
{
|
|
7185
|
+
const i0 = indices[(i + indices.length - 1) % indices.length];
|
|
7186
|
+
const i1 = indices[i];
|
|
7187
|
+
const i2 = indices[(i + 1) % indices.length];
|
|
7188
|
+
const value = abs(cross(points[i0], points[i1], points[i2]));
|
|
7189
|
+
if (value < worstValue)
|
|
7190
|
+
{
|
|
7191
|
+
worstValue = value;
|
|
7192
|
+
worstIndex = i;
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
if (worstIndex < 0)
|
|
7196
|
+
break;
|
|
7197
|
+
|
|
7198
|
+
const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
|
|
7199
|
+
const i1 = indices[worstIndex];
|
|
7200
|
+
const i2 = indices[(worstIndex + 1) % indices.length];
|
|
7201
|
+
triangles.push([i0, i1, i2]);
|
|
7202
|
+
indices.splice(worstIndex, 1);
|
|
7203
|
+
}
|
|
7204
|
+
}
|
|
7205
|
+
|
|
7206
|
+
// add final triangle
|
|
7207
|
+
if (indices.length === 3)
|
|
7208
|
+
triangles.push([indices[0], indices[1], indices[2]]);
|
|
7209
|
+
if (!triangles.length)
|
|
7210
|
+
return [];
|
|
7211
|
+
|
|
7212
|
+
// convert triangles to triangle strip with degenerate connectors
|
|
7213
|
+
const strip = [];
|
|
7214
|
+
let [a0, b0, c0] = triangles[0];
|
|
7215
|
+
strip.push(points[a0], points[b0], points[c0]);
|
|
7216
|
+
for (let i = 1; i < triangles.length; i++)
|
|
7217
|
+
{
|
|
7218
|
+
// add degenerate bridge from last vertex to first of new triangle
|
|
7219
|
+
const [a, b, c] = triangles[i];
|
|
7220
|
+
strip.push(points[c0], points[a]);
|
|
7221
|
+
strip.push(points[a], points[b], points[c]);
|
|
7222
|
+
c0 = c;
|
|
7223
|
+
}
|
|
7224
|
+
return strip;
|
|
6610
7225
|
}
|
|
6611
7226
|
/**
|
|
6612
7227
|
* LittleJS Newgrounds API
|
|
@@ -6631,11 +7246,11 @@ let newgrounds;
|
|
|
6631
7246
|
class NewgroundsMedal extends Medal
|
|
6632
7247
|
{
|
|
6633
7248
|
/** Create a newgrounds medal object and adds it to the list of medals
|
|
6634
|
-
* @param {
|
|
6635
|
-
* @param {
|
|
6636
|
-
* @param {
|
|
6637
|
-
* @param {
|
|
6638
|
-
* @param {
|
|
7249
|
+
* @param {number} id - The unique identifier of the medal
|
|
7250
|
+
* @param {string} name - Name of the medal
|
|
7251
|
+
* @param {string} [description] - Description of the medal
|
|
7252
|
+
* @param {string} [icon] - Icon for the medal
|
|
7253
|
+
* @param {string} [src] - Image location for the medal
|
|
6639
7254
|
*/
|
|
6640
7255
|
constructor(id, name, description, icon, src)
|
|
6641
7256
|
{ super(id, name, description, icon, src); }
|
|
@@ -6727,7 +7342,7 @@ class NewgroundsPlugin
|
|
|
6727
7342
|
* @param {number} id - The scoreboard id
|
|
6728
7343
|
* @param {string} [user] - A user's id or name
|
|
6729
7344
|
* @param {number} [social] - If true, only social scores will be loaded
|
|
6730
|
-
* @param {number} [skip] - Number of scores to skip
|
|
7345
|
+
* @param {number} [skip] - Number of scores to skip over
|
|
6731
7346
|
* @param {number} [limit] - Number of scores to include in the list
|
|
6732
7347
|
* @return {Object} - The response JSON object
|
|
6733
7348
|
*/
|
|
@@ -6950,16 +7565,16 @@ class ZzFXMusic extends Sound
|
|
|
6950
7565
|
if (!soundEnable || headlessMode) return;
|
|
6951
7566
|
this.randomness = 0;
|
|
6952
7567
|
this.sampleChannels = zzfxM(...zzfxMusic);
|
|
6953
|
-
this.sampleRate =
|
|
7568
|
+
this.sampleRate = audioDefaultSampleRate;
|
|
6954
7569
|
}
|
|
6955
7570
|
|
|
6956
|
-
/** Play the music
|
|
6957
|
-
* @param {number} [volume
|
|
6958
|
-
* @param {boolean} [loop] -
|
|
7571
|
+
/** Play the music that loops by default
|
|
7572
|
+
* @param {number} [volume] - Volume to play the music at
|
|
7573
|
+
* @param {boolean} [loop] - Should the music loop?
|
|
6959
7574
|
* @return {AudioBufferSourceNode} - The audio source node
|
|
6960
7575
|
*/
|
|
6961
|
-
playMusic(volume, loop=
|
|
6962
|
-
{ return super.play(undefined, volume, 1,
|
|
7576
|
+
playMusic(volume=1, loop=true)
|
|
7577
|
+
{ return super.play(undefined, volume, 1, 0, loop); }
|
|
6963
7578
|
}
|
|
6964
7579
|
|
|
6965
7580
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -6993,7 +7608,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
6993
7608
|
let panning = 0;
|
|
6994
7609
|
let hasMore = 1;
|
|
6995
7610
|
let sampleCache = {};
|
|
6996
|
-
let beatLength =
|
|
7611
|
+
let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
|
|
6997
7612
|
|
|
6998
7613
|
// for each channel in order until there are no more
|
|
6999
7614
|
for (; hasMore; channelIndex++) {
|
|
@@ -7012,15 +7627,15 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
7012
7627
|
// get next offset, use the length of first channel
|
|
7013
7628
|
nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
|
|
7014
7629
|
// for each beat in pattern, plus one extra if end of sequence
|
|
7015
|
-
isSequenceEnd = sequenceIndex
|
|
7630
|
+
isSequenceEnd = sequenceIndex === sequence.length - 1;
|
|
7016
7631
|
for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
|
|
7017
7632
|
|
|
7018
7633
|
// <channel-note>
|
|
7019
7634
|
note = patternChannel[i];
|
|
7020
7635
|
|
|
7021
7636
|
// stop if end, different instrument or new note
|
|
7022
|
-
stop = i
|
|
7023
|
-
instrument
|
|
7637
|
+
stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
|
|
7638
|
+
instrument !== (patternChannel[0] || 0) || note | 0;
|
|
7024
7639
|
|
|
7025
7640
|
// fill buffer with samples for previous beat, most cpu intensive part
|
|
7026
7641
|
for (j = 0; j < beatLength && notFirstBeat;
|
|
@@ -7105,11 +7720,11 @@ class UISystemPlugin
|
|
|
7105
7720
|
/** @property {Color} - Default text color for UI elements */
|
|
7106
7721
|
this.defaultTextColor = BLACK;
|
|
7107
7722
|
/** @property {Color} - Default button color for UI elements */
|
|
7108
|
-
this.defaultButtonColor = hsl(0,0,.
|
|
7723
|
+
this.defaultButtonColor = hsl(0,0,.7);
|
|
7109
7724
|
/** @property {Color} - Default hover color for UI elements */
|
|
7110
|
-
this.defaultHoverColor = hsl(0,0,.
|
|
7725
|
+
this.defaultHoverColor = hsl(0,0,.9);
|
|
7111
7726
|
/** @property {Color} - Default color for disabled UI elements */
|
|
7112
|
-
this.defaultDisabledColor = hsl(0,0,.
|
|
7727
|
+
this.defaultDisabledColor = hsl(0,0,.3);
|
|
7113
7728
|
/** @property {number} - Default line width for UI elements */
|
|
7114
7729
|
this.defaultLineWidth = 4;
|
|
7115
7730
|
/** @property {number} - Default rounded rect corner radius for UI elements */
|
|
@@ -7129,16 +7744,16 @@ class UISystemPlugin
|
|
|
7129
7744
|
|
|
7130
7745
|
engineAddPlugin(uiUpdate, uiRender);
|
|
7131
7746
|
|
|
7132
|
-
function updateInvisible(o)
|
|
7133
|
-
{
|
|
7134
|
-
for (const c of o.children)
|
|
7135
|
-
updateInvisible(c);
|
|
7136
|
-
o.updateInvisible();
|
|
7137
|
-
}
|
|
7138
|
-
|
|
7139
7747
|
// setup recursive update and render
|
|
7140
7748
|
function uiUpdate()
|
|
7141
7749
|
{
|
|
7750
|
+
function updateInvisibleObject(o)
|
|
7751
|
+
{
|
|
7752
|
+
// update invisible objects
|
|
7753
|
+
for (const c of o.children)
|
|
7754
|
+
updateInvisibleObject(c);
|
|
7755
|
+
o.updateInvisible();
|
|
7756
|
+
}
|
|
7142
7757
|
function updateObject(o)
|
|
7143
7758
|
{
|
|
7144
7759
|
if (o.visible)
|
|
@@ -7152,7 +7767,7 @@ class UISystemPlugin
|
|
|
7152
7767
|
o.update();
|
|
7153
7768
|
}
|
|
7154
7769
|
else
|
|
7155
|
-
|
|
7770
|
+
updateInvisibleObject(o);
|
|
7156
7771
|
}
|
|
7157
7772
|
uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
|
|
7158
7773
|
}
|
|
@@ -7178,7 +7793,7 @@ class UISystemPlugin
|
|
|
7178
7793
|
* @param {Color} [color=uiSystem.defaultColor]
|
|
7179
7794
|
* @param {number} [lineWidth=uiSystem.defaultLineWidth]
|
|
7180
7795
|
* @param {Color} [lineColor=uiSystem.defaultLineColor]
|
|
7181
|
-
* @param {number} [
|
|
7796
|
+
* @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
|
|
7182
7797
|
drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
|
|
7183
7798
|
{
|
|
7184
7799
|
const context = uiSystem.uiContext;
|
|
@@ -7253,32 +7868,40 @@ class UIObject
|
|
|
7253
7868
|
constructor(pos=vec2(), size=vec2())
|
|
7254
7869
|
{
|
|
7255
7870
|
/** @property {Vector2} - Local position of the object */
|
|
7256
|
-
this.localPos
|
|
7871
|
+
this.localPos = pos.copy();
|
|
7257
7872
|
/** @property {Vector2} - Screen space position of the object */
|
|
7258
|
-
this.pos
|
|
7873
|
+
this.pos = pos.copy();
|
|
7259
7874
|
/** @property {Vector2} - Screen space size of the object */
|
|
7260
|
-
this.size
|
|
7261
|
-
/** @property {Color} -
|
|
7262
|
-
this.color
|
|
7263
|
-
/** @property {
|
|
7264
|
-
this.
|
|
7265
|
-
/** @property {Color} -
|
|
7875
|
+
this.size = size.copy();
|
|
7876
|
+
/** @property {Color} - Color of the object */
|
|
7877
|
+
this.color = uiSystem.defaultColor;
|
|
7878
|
+
/** @property {string} - Text for this ui object */
|
|
7879
|
+
this.text = undefined;
|
|
7880
|
+
/** @property {Color} - Color when disabled */
|
|
7881
|
+
this.disabledColor = uiSystem.defaultDisabledColor;
|
|
7882
|
+
/** @property {boolean} - Is this object disabled? */
|
|
7883
|
+
this.disabled = false;
|
|
7884
|
+
/** @property {Color} - Color for text */
|
|
7885
|
+
this.textColor = uiSystem.defaultTextColor;
|
|
7886
|
+
/** @property {Color} - Color used when hovering over the object */
|
|
7266
7887
|
this.hoverColor = uiSystem.defaultHoverColor;
|
|
7267
|
-
/** @property {Color} -
|
|
7268
|
-
this.lineColor
|
|
7269
|
-
/** @property {number} -
|
|
7270
|
-
this.lineWidth
|
|
7271
|
-
/** @property {
|
|
7272
|
-
this.
|
|
7273
|
-
/** @property {
|
|
7274
|
-
this.
|
|
7275
|
-
/** @property {
|
|
7276
|
-
this.
|
|
7277
|
-
/** @property {
|
|
7278
|
-
this.
|
|
7279
|
-
/** @property {UIObject} - this object's
|
|
7280
|
-
this.
|
|
7281
|
-
/** @property {
|
|
7888
|
+
/** @property {Color} - Color for line drawing */
|
|
7889
|
+
this.lineColor = uiSystem.defaultLineColor;
|
|
7890
|
+
/** @property {number} - Width for line drawing */
|
|
7891
|
+
this.lineWidth = uiSystem.defaultLineWidth;
|
|
7892
|
+
/** @property {number} - Corner radius for rounded rects */
|
|
7893
|
+
this.cornerRadius = uiSystem.defaultCornerRadius;
|
|
7894
|
+
/** @property {string} - Font for this objecct */
|
|
7895
|
+
this.font = uiSystem.defaultFont;
|
|
7896
|
+
/** @property {number} - Override for text height */
|
|
7897
|
+
this.textHeight = undefined;
|
|
7898
|
+
/** @property {boolean} - Should this object be drawn */
|
|
7899
|
+
this.visible = true;
|
|
7900
|
+
/** @property {Array<UIObject>} - A list of this object's children */
|
|
7901
|
+
this.children = [];
|
|
7902
|
+
/** @property {UIObject} - This object's parent, position is in parent space */
|
|
7903
|
+
this.parent = undefined;
|
|
7904
|
+
/** @property {number} - Extra size added to make small buttons easier to touch on mobile devices */
|
|
7282
7905
|
this.extraTouchSize = 0;
|
|
7283
7906
|
/** @property {Sound} - Sound when interactive element is pressed */
|
|
7284
7907
|
this.soundPress = uiSystem.defaultSoundPress;
|
|
@@ -7310,7 +7933,7 @@ class UIObject
|
|
|
7310
7933
|
*/
|
|
7311
7934
|
removeChild(child)
|
|
7312
7935
|
{
|
|
7313
|
-
ASSERT(child.parent
|
|
7936
|
+
ASSERT(child.parent === this && this.children.includes(child));
|
|
7314
7937
|
this.children.splice(this.children.indexOf(child), 1);
|
|
7315
7938
|
child.parent = undefined;
|
|
7316
7939
|
}
|
|
@@ -7363,7 +7986,7 @@ class UIObject
|
|
|
7363
7986
|
this.mouseIsHeld = false;
|
|
7364
7987
|
}
|
|
7365
7988
|
|
|
7366
|
-
if (this.mouseIsOver
|
|
7989
|
+
if (this.mouseIsOver !== mouseWasOver)
|
|
7367
7990
|
this.mouseIsOver ? this.onEnter() : this.onLeave();
|
|
7368
7991
|
}
|
|
7369
7992
|
|
|
@@ -7374,7 +7997,7 @@ class UIObject
|
|
|
7374
7997
|
uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor, this.cornerRadius);
|
|
7375
7998
|
}
|
|
7376
7999
|
|
|
7377
|
-
/** Special update
|
|
8000
|
+
/** Special update when object is not visible */
|
|
7378
8001
|
updateInvisible()
|
|
7379
8002
|
{
|
|
7380
8003
|
// reset input state when not visible
|
|
@@ -7382,28 +8005,22 @@ class UIObject
|
|
|
7382
8005
|
}
|
|
7383
8006
|
|
|
7384
8007
|
/** Called when the mouse enters the object */
|
|
7385
|
-
onEnter()
|
|
7386
|
-
{}
|
|
8008
|
+
onEnter() {}
|
|
7387
8009
|
|
|
7388
8010
|
/** Called when the mouse leaves the object */
|
|
7389
|
-
onLeave()
|
|
7390
|
-
{}
|
|
8011
|
+
onLeave() {}
|
|
7391
8012
|
|
|
7392
8013
|
/** Called when the mouse is pressed while over the object */
|
|
7393
|
-
onPress()
|
|
7394
|
-
{}
|
|
8014
|
+
onPress() {}
|
|
7395
8015
|
|
|
7396
8016
|
/** Called when the mouse is released while over the object */
|
|
7397
|
-
onRelease()
|
|
7398
|
-
{}
|
|
8017
|
+
onRelease() {}
|
|
7399
8018
|
|
|
7400
8019
|
/** Called when user clicks on this object */
|
|
7401
|
-
onClick()
|
|
7402
|
-
{}
|
|
8020
|
+
onClick() {}
|
|
7403
8021
|
|
|
7404
8022
|
/** Called when the state of this object changes */
|
|
7405
|
-
onChange()
|
|
7406
|
-
{}
|
|
8023
|
+
onChange() {}
|
|
7407
8024
|
};
|
|
7408
8025
|
|
|
7409
8026
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -7424,13 +8041,13 @@ class UIText extends UIObject
|
|
|
7424
8041
|
{
|
|
7425
8042
|
super(pos, size);
|
|
7426
8043
|
|
|
7427
|
-
|
|
8044
|
+
// set properties
|
|
7428
8045
|
this.text = text;
|
|
7429
|
-
/** @property {string} */
|
|
7430
8046
|
this.align = align;
|
|
8047
|
+
this.font = font;
|
|
7431
8048
|
|
|
7432
|
-
|
|
7433
|
-
this.lineWidth = 0;
|
|
8049
|
+
// make text not outlined by default
|
|
8050
|
+
this.lineWidth = 0;
|
|
7434
8051
|
}
|
|
7435
8052
|
render()
|
|
7436
8053
|
{
|
|
@@ -7457,13 +8074,14 @@ class UITile extends UIObject
|
|
|
7457
8074
|
constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
|
|
7458
8075
|
{
|
|
7459
8076
|
super(pos, size);
|
|
7460
|
-
|
|
7461
8077
|
/** @property {TileInfo} - Tile image to use */
|
|
7462
8078
|
this.tileInfo = tileInfo;
|
|
7463
8079
|
/** @property {number} - Angle to rotate in radians */
|
|
7464
8080
|
this.angle = angle;
|
|
7465
8081
|
/** @property {boolean} - Should it be mirrored? */
|
|
7466
8082
|
this.mirror = mirror;
|
|
8083
|
+
|
|
8084
|
+
// set properties
|
|
7467
8085
|
this.color = color;
|
|
7468
8086
|
}
|
|
7469
8087
|
render()
|
|
@@ -7489,22 +8107,20 @@ class UIButton extends UIObject
|
|
|
7489
8107
|
{
|
|
7490
8108
|
super(pos, size);
|
|
7491
8109
|
|
|
7492
|
-
|
|
8110
|
+
// set properties
|
|
7493
8111
|
this.text = text;
|
|
7494
|
-
/** @property {Color} */
|
|
7495
|
-
this.disabledColor = uiSystem.defaultDisabledColor;
|
|
7496
|
-
/** @property {boolean} */
|
|
7497
|
-
this.disabled = false;
|
|
7498
|
-
this.interactive = true;
|
|
7499
8112
|
this.color = color;
|
|
8113
|
+
this.interactive = true;
|
|
7500
8114
|
}
|
|
7501
8115
|
render()
|
|
7502
8116
|
{
|
|
8117
|
+
// draw the button
|
|
7503
8118
|
const lineColor = this.mouseIsHeld && !this.disabled ? this.color : this.lineColor;
|
|
7504
8119
|
const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
|
|
7505
8120
|
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
|
|
7506
8121
|
|
|
7507
|
-
|
|
8122
|
+
// draw the text
|
|
8123
|
+
const textScale = .8; // scale text to fit
|
|
7508
8124
|
const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
|
|
7509
8125
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
7510
8126
|
this.textColor, 0, undefined, this.align, this.font);
|
|
@@ -7522,13 +8138,18 @@ class UICheckbox extends UIObject
|
|
|
7522
8138
|
* @param {Vector2} [pos]
|
|
7523
8139
|
* @param {Vector2} [size]
|
|
7524
8140
|
* @param {boolean} [checked]
|
|
8141
|
+
* @param {string} [text]
|
|
8142
|
+
* @param {Color} [color=uiSystem.defaultButtonColor]
|
|
7525
8143
|
*/
|
|
7526
|
-
constructor(pos, size, checked=false)
|
|
8144
|
+
constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
|
|
7527
8145
|
{
|
|
7528
8146
|
super(pos, size);
|
|
7529
|
-
|
|
7530
|
-
/** @property {boolean} */
|
|
8147
|
+
/** @property {boolean} - Current percentage value of this scrollbar 0-1 */
|
|
7531
8148
|
this.checked = checked;
|
|
8149
|
+
|
|
8150
|
+
// set properties
|
|
8151
|
+
this.text = text;
|
|
8152
|
+
this.color = color;
|
|
7532
8153
|
this.interactive = true;
|
|
7533
8154
|
}
|
|
7534
8155
|
onClick()
|
|
@@ -7538,14 +8159,24 @@ class UICheckbox extends UIObject
|
|
|
7538
8159
|
}
|
|
7539
8160
|
render()
|
|
7540
8161
|
{
|
|
7541
|
-
const color = this.mouseIsOver? this.hoverColor : this.color;
|
|
8162
|
+
const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
|
|
7542
8163
|
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor, this.cornerRadius);
|
|
7543
8164
|
if (this.checked)
|
|
7544
8165
|
{
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
8166
|
+
const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
|
|
8167
|
+
const length = lerp(1, 2**.5/2, p) / 2;
|
|
8168
|
+
let s = this.size.scale(length);
|
|
8169
|
+
uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))), this.pos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
|
|
8170
|
+
uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
|
|
7548
8171
|
}
|
|
8172
|
+
|
|
8173
|
+
// draw the text to the right side of the checkbox
|
|
8174
|
+
const textScale = .8; // scale text to fit
|
|
8175
|
+
const gapScale = .55;
|
|
8176
|
+
const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
|
|
8177
|
+
const pos = this.pos.add(vec2(this.size.x*gapScale,0));
|
|
8178
|
+
uiSystem.drawText(this.text, pos, textSize,
|
|
8179
|
+
this.textColor, 0, undefined, 'left', this.font);
|
|
7549
8180
|
}
|
|
7550
8181
|
}
|
|
7551
8182
|
|
|
@@ -7568,43 +8199,51 @@ class UIScrollbar extends UIObject
|
|
|
7568
8199
|
{
|
|
7569
8200
|
super(pos, size);
|
|
7570
8201
|
|
|
7571
|
-
/** @property {number} */
|
|
8202
|
+
/** @property {number} - Current percentage value of this scrollbar 0-1 */
|
|
7572
8203
|
this.value = value;
|
|
7573
|
-
/** @property {
|
|
7574
|
-
this.text = text;
|
|
7575
|
-
/** @property {Color} */
|
|
8204
|
+
/** @property {Color} - Color for the handle part of the scrollbar */
|
|
7576
8205
|
this.handleColor = handleColor;
|
|
8206
|
+
|
|
8207
|
+
// set properties
|
|
8208
|
+
this.text = text;
|
|
7577
8209
|
this.color = color;
|
|
7578
8210
|
this.interactive = true;
|
|
7579
8211
|
}
|
|
7580
8212
|
update()
|
|
7581
8213
|
{
|
|
7582
8214
|
super.update();
|
|
7583
|
-
if (this.mouseIsHeld)
|
|
8215
|
+
if (this.mouseIsHeld && this.interactive)
|
|
7584
8216
|
{
|
|
8217
|
+
// check if value changed
|
|
7585
8218
|
const handleSize = vec2(this.size.y);
|
|
7586
8219
|
const handleWidth = this.size.x - handleSize.x;
|
|
7587
8220
|
const p1 = this.pos.x - handleWidth/2;
|
|
7588
8221
|
const p2 = this.pos.x + handleWidth/2;
|
|
7589
8222
|
const oldValue = this.value;
|
|
7590
8223
|
this.value = percent(mousePosScreen.x, p1, p2);
|
|
7591
|
-
this.value
|
|
8224
|
+
this.value === oldValue || this.onChange();
|
|
7592
8225
|
}
|
|
7593
8226
|
}
|
|
7594
8227
|
render()
|
|
7595
8228
|
{
|
|
7596
|
-
|
|
7597
|
-
const
|
|
8229
|
+
// draw the scrollbar background
|
|
8230
|
+
const lineColor = this.interactive && this.mouseIsHeld && !this.disabled ?
|
|
8231
|
+
this.color : this.lineColor;
|
|
8232
|
+
const color = this.disabled ? this.disabledColor :
|
|
8233
|
+
this.interactive && this.mouseIsHeld ? this.hoverColor : this.color;
|
|
7598
8234
|
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
|
|
7599
8235
|
|
|
8236
|
+
// draw the scrollbar handle
|
|
7600
8237
|
const handleSize = vec2(this.size.y);
|
|
7601
8238
|
const handleWidth = this.size.x - handleSize.x;
|
|
7602
8239
|
const p1 = this.pos.x - handleWidth/2;
|
|
7603
8240
|
const p2 = this.pos.x + handleWidth/2;
|
|
7604
8241
|
const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
|
|
7605
|
-
const
|
|
7606
|
-
|
|
8242
|
+
const handleColor = this.disabled ? this.disabledColor :
|
|
8243
|
+
this.interactive && this.mouseIsHeld ? this.color : this.handleColor;
|
|
8244
|
+
uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
|
|
7607
8245
|
|
|
8246
|
+
// draw the text on the scrollbar
|
|
7608
8247
|
const textScale = .8; // scale text to fit in scrollbar
|
|
7609
8248
|
const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
|
|
7610
8249
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
@@ -7699,14 +8338,14 @@ class Box2dObject extends EngineObject
|
|
|
7699
8338
|
if (this.tileInfo)
|
|
7700
8339
|
super.render();
|
|
7701
8340
|
else
|
|
7702
|
-
this.drawFixtures(this.color, this.lineColor, this.lineWidth
|
|
8341
|
+
this.drawFixtures(this.color, this.lineColor, this.lineWidth);
|
|
7703
8342
|
}
|
|
7704
8343
|
|
|
7705
8344
|
/** Render debug info */
|
|
7706
8345
|
renderDebugInfo()
|
|
7707
8346
|
{
|
|
7708
8347
|
const isAsleep = !this.getIsAwake();
|
|
7709
|
-
const isStatic = this.getBodyType()
|
|
8348
|
+
const isStatic = this.getBodyType() === box2d.bodyTypeStatic;
|
|
7710
8349
|
const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
|
|
7711
8350
|
this.drawFixtures(color);
|
|
7712
8351
|
}
|
|
@@ -9174,7 +9813,7 @@ class Box2dPlugin
|
|
|
9174
9813
|
queryCallback.ReportFixture = function(fixturePointer)
|
|
9175
9814
|
{
|
|
9176
9815
|
const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
|
|
9177
|
-
if (dynamicOnly && fixture.GetBody().GetType()
|
|
9816
|
+
if (dynamicOnly && fixture.GetBody().GetType() !== box2d.instance.b2_dynamicBody)
|
|
9178
9817
|
return true; // continue getting results
|
|
9179
9818
|
if (!fixture.TestPoint(box2d.vec2dTo(pos)))
|
|
9180
9819
|
return true; // continue getting results
|
|
@@ -9203,7 +9842,7 @@ class Box2dPlugin
|
|
|
9203
9842
|
* @param {Color} [lineColor]
|
|
9204
9843
|
* @param {number} [lineWidth]
|
|
9205
9844
|
* @param {CanvasRenderingContext2D} [context] */
|
|
9206
|
-
drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context
|
|
9845
|
+
drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
|
|
9207
9846
|
{
|
|
9208
9847
|
const shape = box2d.castObjectType(fixture.GetShape());
|
|
9209
9848
|
switch (shape.GetType())
|
|
@@ -9213,20 +9852,20 @@ class Box2dPlugin
|
|
|
9213
9852
|
let points = [];
|
|
9214
9853
|
for (let i=shape.GetVertexCount(); i--;)
|
|
9215
9854
|
points.push(box2d.vec2From(shape.GetVertex(i)));
|
|
9216
|
-
drawPoly(points, color, lineWidth, lineColor, pos, angle
|
|
9855
|
+
drawPoly(points, color, lineWidth, lineColor, pos, angle);
|
|
9217
9856
|
break;
|
|
9218
9857
|
}
|
|
9219
9858
|
case box2d.instance.b2Shape.e_circle:
|
|
9220
9859
|
{
|
|
9221
9860
|
const radius = shape.get_m_radius();
|
|
9222
|
-
drawCircle(pos, radius, color, lineWidth, lineColor
|
|
9861
|
+
drawCircle(pos, radius, color, lineWidth, lineColor);
|
|
9223
9862
|
break;
|
|
9224
9863
|
}
|
|
9225
9864
|
case box2d.instance.b2Shape.e_edge:
|
|
9226
9865
|
{
|
|
9227
9866
|
const v1 = box2d.vec2From(shape.get_m_vertex1());
|
|
9228
9867
|
const v2 = box2d.vec2From(shape.get_m_vertex2());
|
|
9229
|
-
drawLine(v1, v2, lineWidth, lineColor, pos, angle
|
|
9868
|
+
drawLine(v1, v2, lineWidth, lineColor, pos, angle);
|
|
9230
9869
|
break;
|
|
9231
9870
|
}
|
|
9232
9871
|
}
|
|
@@ -9305,7 +9944,9 @@ class Box2dPlugin
|
|
|
9305
9944
|
}
|
|
9306
9945
|
|
|
9307
9946
|
///////////////////////////////////////////////////////////////////////////////
|
|
9308
|
-
/** Box2d Init - Call with await
|
|
9947
|
+
/** Box2d Init - Call with await to init box2d
|
|
9948
|
+
* @example
|
|
9949
|
+
* await box2dInit();
|
|
9309
9950
|
* @return {Promise<Box2dPlugin>}
|
|
9310
9951
|
* @memberof Box2D */
|
|
9311
9952
|
async function box2dInit()
|
|
@@ -9421,7 +10062,7 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
|
|
|
9421
10062
|
}
|
|
9422
10063
|
|
|
9423
10064
|
/** Draw a scalable nine-slice UI element in world space
|
|
9424
|
-
* This function can apply color and additive color if
|
|
10065
|
+
* This function can apply color and additive color if WebGL is enabled
|
|
9425
10066
|
* @param {Vector2} pos - World space position
|
|
9426
10067
|
* @param {Vector2} size - World space size
|
|
9427
10068
|
* @param {TileInfo} startTile - Starting tile for the nine-slice pattern
|
|
@@ -9450,9 +10091,9 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
|
|
|
9450
10091
|
{
|
|
9451
10092
|
// sides
|
|
9452
10093
|
const horizontal = i%2;
|
|
9453
|
-
const sidePos = cornerOffset.multiply(vec2(horizontal?i
|
|
10094
|
+
const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-1:1));
|
|
9454
10095
|
const sideSize = vec2(horizontal ? borderSize : centerSize.x, horizontal ? centerSize.y : borderSize);
|
|
9455
|
-
const sideTile = centerTile.offset(startTile.size.multiply(vec2(i
|
|
10096
|
+
const sideTile = centerTile.offset(startTile.size.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)))
|
|
9456
10097
|
drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
|
|
9457
10098
|
}
|
|
9458
10099
|
for (let i=4; i--;)
|
|
@@ -9481,7 +10122,7 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
|
|
|
9481
10122
|
}
|
|
9482
10123
|
|
|
9483
10124
|
/** Draw a scalable three-slice UI element in world space
|
|
9484
|
-
* This function can apply color and additive color if
|
|
10125
|
+
* This function can apply color and additive color if WebGL is enabled
|
|
9485
10126
|
* @param {Vector2} pos - World space position
|
|
9486
10127
|
* @param {Vector2} size - World space size
|
|
9487
10128
|
* @param {TileInfo} startTile - Starting tile for the three-slice pattern
|
|
@@ -9513,7 +10154,7 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
|
|
|
9513
10154
|
// sides
|
|
9514
10155
|
const a = angle + i*PI/2;
|
|
9515
10156
|
const horizontal = i%2;
|
|
9516
|
-
const sidePos = cornerOffset.multiply(vec2(horizontal?i
|
|
10157
|
+
const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-flip:flip));
|
|
9517
10158
|
const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize);
|
|
9518
10159
|
drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
|
|
9519
10160
|
}
|
|
@@ -9755,27 +10396,32 @@ export
|
|
|
9755
10396
|
// WebGL
|
|
9756
10397
|
glCanvas,
|
|
9757
10398
|
glContext,
|
|
10399
|
+
glClearCanvas,
|
|
10400
|
+
glSetTexture,
|
|
9758
10401
|
glCompileShader,
|
|
9759
|
-
glCopyToContext,
|
|
9760
10402
|
glCreateProgram,
|
|
9761
10403
|
glCreateTexture,
|
|
9762
10404
|
glDeleteTexture,
|
|
9763
10405
|
glSetTextureData,
|
|
9764
|
-
glDraw,
|
|
9765
10406
|
glFlush,
|
|
9766
|
-
|
|
10407
|
+
glCopyToContext,
|
|
9767
10408
|
glSetAntialias,
|
|
9768
|
-
|
|
10409
|
+
glDraw,
|
|
10410
|
+
glDrawPointsTransform,
|
|
10411
|
+
glDrawOutlineTransform,
|
|
10412
|
+
glDrawPoints,
|
|
9769
10413
|
glAntialias,
|
|
9770
|
-
glShader,
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
glPositionData,
|
|
9775
|
-
glColorData,
|
|
9776
|
-
glInstanceCount,
|
|
9777
|
-
glAdditive,
|
|
10414
|
+
glShader,
|
|
10415
|
+
glPolyShader,
|
|
10416
|
+
glPolyMode,
|
|
10417
|
+
glAdditive,
|
|
9778
10418
|
glBatchAdditive,
|
|
10419
|
+
glActiveTexture,
|
|
10420
|
+
glArrayBuffer,
|
|
10421
|
+
glGeometryBuffer,
|
|
10422
|
+
glPositionData,
|
|
10423
|
+
glColorData,
|
|
10424
|
+
glBatchCount,
|
|
9779
10425
|
|
|
9780
10426
|
// Input
|
|
9781
10427
|
keyIsDown,
|
|
@@ -9808,17 +10454,18 @@ export
|
|
|
9808
10454
|
pointerLockIsActive,
|
|
9809
10455
|
|
|
9810
10456
|
// Audio
|
|
10457
|
+
audioContext,
|
|
10458
|
+
audioMasterGain,
|
|
10459
|
+
audioDefaultSampleRate,
|
|
9811
10460
|
Sound,
|
|
9812
10461
|
SoundWave,
|
|
9813
|
-
|
|
10462
|
+
SoundInstance,
|
|
9814
10463
|
speak,
|
|
9815
10464
|
speakStop,
|
|
9816
10465
|
getNoteFrequency,
|
|
9817
10466
|
playSamples,
|
|
9818
10467
|
zzfx,
|
|
9819
10468
|
zzfxG,
|
|
9820
|
-
zzfxR,
|
|
9821
|
-
audioContext,
|
|
9822
10469
|
|
|
9823
10470
|
// Base Object
|
|
9824
10471
|
EngineObject,
|
|
@@ -9843,7 +10490,7 @@ export
|
|
|
9843
10490
|
medalsPreventUnlock,
|
|
9844
10491
|
medalsInit,
|
|
9845
10492
|
Medal,
|
|
9846
|
-
}
|
|
10493
|
+
}
|
|
9847
10494
|
/**
|
|
9848
10495
|
* LittleJS Module Plugins Export
|
|
9849
10496
|
*/
|
|
@@ -9899,4 +10546,4 @@ export
|
|
|
9899
10546
|
drawNineSliceScreen,
|
|
9900
10547
|
drawThreeSlice,
|
|
9901
10548
|
drawThreeSliceScreen,
|
|
9902
|
-
}
|
|
10549
|
+
}
|