littlejsengine 1.13.4 → 1.14.4
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 +4 -3
- package/dist/littlejs.d.ts +395 -249
- package/dist/littlejs.esm.js +1550 -754
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +1528 -744
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +1504 -720
- package/examples/box2d/game.js +4 -4
- package/examples/box2d/gameObjects.js +1 -1
- package/examples/breakout/game.js +30 -25
- package/examples/breakoutTutorial/README.md +1 -1
- package/examples/breakoutTutorial/game.js +1 -1
- package/examples/electron/build.js +1 -1
- package/examples/electron/index.html +2 -2
- package/examples/empty/game.js +1 -1
- package/examples/htmlMenu/index.html +7 -7
- package/examples/index.html +208 -97
- package/examples/platformer/gameEffects.js +20 -25
- 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 +12 -18
- 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 +14 -7
- package/examples/style.css +14 -4
- package/examples/typescript/build.js +1 -1
- package/examples/uiSystem/game.js +11 -11
- package/package.json +1 -1
- package/plugins/box2d.js +12 -10
- 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 +94 -56
- package/src/engine.js +28 -24
- package/src/engineAudio.js +284 -109
- package/src/engineBuild.js +11 -11
- package/src/engineDebug.js +29 -29
- package/src/engineDraw.js +285 -112
- package/src/engineExport.js +28 -16
- package/src/engineInput.js +57 -67
- package/src/engineMedals.js +25 -25
- package/src/engineObject.js +28 -25
- package/src/engineParticles.js +59 -59
- package/src/engineRelease.js +1 -1
- package/src/engineSettings.js +20 -13
- package/src/engineTileLayer.js +34 -35
- package/src/engineUtilities.js +66 -59
- package/src/engineWebGL.js +466 -66
- /package/examples/shorts/{playSound.js → sound.js} +0 -0
package/dist/littlejs.release.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.4';
|
|
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,15 +242,14 @@ 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);
|
|
249
249
|
overlayContext.fillStyle = '#fff';
|
|
250
250
|
overlayContext.fillText(text, mainCanvas.width-2, 2);
|
|
251
251
|
}
|
|
252
|
-
|
|
253
|
-
drawCount = 0;
|
|
252
|
+
drawCount = 0;
|
|
254
253
|
}
|
|
255
254
|
requestAnimationFrame(engineUpdate);
|
|
256
255
|
}
|
|
@@ -258,13 +257,13 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
258
257
|
function updateCanvas()
|
|
259
258
|
{
|
|
260
259
|
if (headlessMode) return;
|
|
261
|
-
|
|
260
|
+
|
|
262
261
|
if (canvasFixedSize.x)
|
|
263
262
|
{
|
|
264
263
|
// clear canvas and set fixed size
|
|
265
264
|
mainCanvas.width = canvasFixedSize.x;
|
|
266
265
|
mainCanvas.height = canvasFixedSize.y;
|
|
267
|
-
|
|
266
|
+
|
|
268
267
|
// fit to window by adding space on top or bottom if necessary
|
|
269
268
|
const aspect = innerWidth / innerHeight;
|
|
270
269
|
const fixedAspect = mainCanvas.width / mainCanvas.height;
|
|
@@ -274,16 +273,21 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
274
273
|
else
|
|
275
274
|
{
|
|
276
275
|
// clear canvas and set size to same as window
|
|
277
|
-
mainCanvas.width
|
|
276
|
+
mainCanvas.width = min(innerWidth, canvasMaxSize.x);
|
|
278
277
|
mainCanvas.height = min(innerHeight, canvasMaxSize.y);
|
|
279
278
|
}
|
|
280
|
-
|
|
279
|
+
|
|
281
280
|
// clear overlay canvas and set size
|
|
282
281
|
overlayCanvas.width = mainCanvas.width;
|
|
283
282
|
overlayCanvas.height = mainCanvas.height;
|
|
284
283
|
|
|
285
284
|
// save canvas size
|
|
286
285
|
mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
|
|
286
|
+
|
|
287
|
+
// set default line join and cap
|
|
288
|
+
const lineJoin = 'round', lineCap = 'round';
|
|
289
|
+
mainContext.lineJoin = overlayContext.lineJoin = lineJoin;
|
|
290
|
+
mainContext.lineCap = overlayContext.lineCap = lineCap;
|
|
287
291
|
}
|
|
288
292
|
|
|
289
293
|
// wait for gameInit to load
|
|
@@ -296,15 +300,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
296
300
|
return startEngine();
|
|
297
301
|
|
|
298
302
|
// setup html
|
|
299
|
-
const styleRoot =
|
|
303
|
+
const styleRoot =
|
|
300
304
|
'margin:0;' + // fill the window
|
|
301
305
|
'overflow:hidden;' + // no scroll bars
|
|
302
306
|
'background:#000;' + // set background color
|
|
303
307
|
'user-select:none;' + // prevent hold to select
|
|
304
308
|
'-webkit-user-select:none;' + // compatibility for ios
|
|
305
|
-
(!touchInputEnable ? '' : // no touch css settings
|
|
306
309
|
'touch-action:none;' + // prevent mobile pinch to resize
|
|
307
|
-
'-webkit-touch-callout:none'
|
|
310
|
+
'-webkit-touch-callout:none';// compatibility for ios
|
|
308
311
|
rootElement.style.cssText = styleRoot;
|
|
309
312
|
drawCanvas = mainCanvas = document.createElement('canvas');
|
|
310
313
|
rootElement.appendChild(mainCanvas);
|
|
@@ -321,7 +324,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
321
324
|
rootElement.appendChild(overlayCanvas);
|
|
322
325
|
overlayContext = overlayCanvas.getContext('2d');
|
|
323
326
|
|
|
324
|
-
// set
|
|
327
|
+
// set canvases
|
|
325
328
|
const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
|
|
326
329
|
'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
|
|
327
330
|
mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
|
|
@@ -330,17 +333,18 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
330
333
|
setCanvasPixelated(canvasPixelated);
|
|
331
334
|
setOverlayCanvasPixelated(overlayCanvasPixelated);
|
|
332
335
|
updateCanvas();
|
|
336
|
+
glPreRender();
|
|
333
337
|
|
|
334
338
|
// create offscreen canvas for image processing
|
|
335
339
|
workCanvas = new OffscreenCanvas(256, 256);
|
|
336
340
|
workContext = workCanvas.getContext('2d', { willReadFrequently: true });
|
|
337
|
-
|
|
341
|
+
|
|
338
342
|
// create promises for loading images
|
|
339
343
|
const promises = imageSources.map((src, textureIndex)=>
|
|
340
|
-
new Promise(resolve =>
|
|
344
|
+
new Promise(resolve =>
|
|
341
345
|
{
|
|
342
346
|
const image = new Image;
|
|
343
|
-
image.onerror = image.onload = ()=>
|
|
347
|
+
image.onerror = image.onload = ()=>
|
|
344
348
|
{
|
|
345
349
|
const textureInfo = new TextureInfo(image);
|
|
346
350
|
textureInfo.createWebGLTexture();
|
|
@@ -355,7 +359,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
355
359
|
if (!imageSources.length)
|
|
356
360
|
{
|
|
357
361
|
// no images to load
|
|
358
|
-
promises.push(new Promise(resolve =>
|
|
362
|
+
promises.push(new Promise(resolve =>
|
|
359
363
|
{
|
|
360
364
|
const textureInfo = new TextureInfo(new Image);
|
|
361
365
|
textureInfos[0] = textureInfo;
|
|
@@ -367,7 +371,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
367
371
|
if (showSplashScreen)
|
|
368
372
|
{
|
|
369
373
|
// draw splash screen
|
|
370
|
-
promises.push(new Promise(resolve =>
|
|
374
|
+
promises.push(new Promise(resolve =>
|
|
371
375
|
{
|
|
372
376
|
let t = 0;
|
|
373
377
|
console.log(`${engineName} Engine v${engineVersion}`);
|
|
@@ -599,7 +603,7 @@ function drawEngineSplashScreen(t)
|
|
|
599
603
|
line(36,20,60,20);
|
|
600
604
|
|
|
601
605
|
// engine front light
|
|
602
|
-
circle(60,30,4,PI,3*PI,color(3,2));
|
|
606
|
+
circle(60,30,4,PI,3*PI,color(3,2));
|
|
603
607
|
circle(60,30,4,PI,2*PI,color(3,3));
|
|
604
608
|
circle(60,30,4,PI,3*PI);
|
|
605
609
|
|
|
@@ -648,10 +652,10 @@ function drawEngineSplashScreen(t)
|
|
|
648
652
|
x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
|
|
649
653
|
X += w;
|
|
650
654
|
}
|
|
651
|
-
|
|
655
|
+
|
|
652
656
|
x.restore();
|
|
653
657
|
}
|
|
654
|
-
/**
|
|
658
|
+
/**
|
|
655
659
|
* LittleJS - Release Mode
|
|
656
660
|
* - This file is used for release builds in place of engineDebug.js
|
|
657
661
|
* - Debug functionality is disabled to reduce size and increase performance
|
|
@@ -764,7 +768,19 @@ function lerp(valueA, valueB, percent)
|
|
|
764
768
|
if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
|
|
765
769
|
console.warn('lerp() parameter order changed! use lerp(start, end, p)');
|
|
766
770
|
return valueA + clamp(percent) * (valueB-valueA);
|
|
767
|
-
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
|
|
774
|
+
* A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
|
|
775
|
+
* @param {number} value
|
|
776
|
+
* @param {number} percentA
|
|
777
|
+
* @param {number} percentB
|
|
778
|
+
* @param {number} lerpA
|
|
779
|
+
* @param {number} lerpB
|
|
780
|
+
* @return {number}
|
|
781
|
+
* @memberof Utilities */
|
|
782
|
+
function percentLerp(value, percentA, percentB, lerpA, lerpB)
|
|
783
|
+
{ return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
|
|
768
784
|
|
|
769
785
|
/** Returns signed wrapped distance between the two values passed in
|
|
770
786
|
* @param {number} valueA
|
|
@@ -816,7 +832,7 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
|
|
|
816
832
|
* @memberof Utilities */
|
|
817
833
|
function isPowerOfTwo(value) { return !(value & (value - 1)); }
|
|
818
834
|
|
|
819
|
-
/** Returns the nearest power of two not less
|
|
835
|
+
/** Returns the nearest power of two not less than the value
|
|
820
836
|
* @param {number} value
|
|
821
837
|
* @return {number}
|
|
822
838
|
* @memberof Utilities */
|
|
@@ -831,8 +847,8 @@ function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
|
|
|
831
847
|
* @return {boolean} - True if overlapping
|
|
832
848
|
* @memberof Utilities */
|
|
833
849
|
function isOverlapping(posA, sizeA, posB, sizeB=vec2())
|
|
834
|
-
{
|
|
835
|
-
return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
|
|
850
|
+
{
|
|
851
|
+
return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
|
|
836
852
|
&& abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
|
|
837
853
|
}
|
|
838
854
|
|
|
@@ -887,7 +903,7 @@ function isIntersecting(start, end, pos, size)
|
|
|
887
903
|
function wave(frequency=1, amplitude=1, t=time, offset=0)
|
|
888
904
|
{ return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
|
|
889
905
|
|
|
890
|
-
/** Formats seconds to mm:ss style for display purposes
|
|
906
|
+
/** Formats seconds to mm:ss style for display purposes
|
|
891
907
|
* @param {number} t - time in seconds
|
|
892
908
|
* @return {string}
|
|
893
909
|
* @memberof Utilities */
|
|
@@ -903,13 +919,12 @@ async function fetchJSON(url)
|
|
|
903
919
|
return response.json();
|
|
904
920
|
}
|
|
905
921
|
|
|
906
|
-
/**
|
|
922
|
+
/**
|
|
907
923
|
* Check if object is a valid number, not NaN or undefined, but it may be infinite
|
|
908
924
|
* @param {any} n
|
|
909
925
|
* @return {boolean}
|
|
910
|
-
* @memberof Utilities
|
|
911
|
-
|
|
912
|
-
function isNumber(n) { return typeof n == 'number' && !isNaN(n); }
|
|
926
|
+
* @memberof Utilities */
|
|
927
|
+
function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
|
|
913
928
|
|
|
914
929
|
///////////////////////////////////////////////////////////////////////////////
|
|
915
930
|
|
|
@@ -964,13 +979,13 @@ function randInCircle(radius=1, minRadius=0)
|
|
|
964
979
|
* @memberof Random */
|
|
965
980
|
function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
|
|
966
981
|
{
|
|
967
|
-
return linear ? colorA.lerp(colorB, rand()) :
|
|
982
|
+
return linear ? colorA.lerp(colorB, rand()) :
|
|
968
983
|
new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
|
|
969
984
|
}
|
|
970
985
|
|
|
971
986
|
///////////////////////////////////////////////////////////////////////////////
|
|
972
987
|
|
|
973
|
-
/**
|
|
988
|
+
/**
|
|
974
989
|
* Seeded random number generator
|
|
975
990
|
* - Can be used to create a deterministic random number sequence
|
|
976
991
|
* @example
|
|
@@ -997,8 +1012,8 @@ class RandomGenerator
|
|
|
997
1012
|
float(valueA=1, valueB=0)
|
|
998
1013
|
{
|
|
999
1014
|
// xorshift algorithm
|
|
1000
|
-
this.seed ^= this.seed << 13;
|
|
1001
|
-
this.seed ^= this.seed >>> 17;
|
|
1015
|
+
this.seed ^= this.seed << 13;
|
|
1016
|
+
this.seed ^= this.seed >>> 17;
|
|
1002
1017
|
this.seed ^= this.seed << 5;
|
|
1003
1018
|
return valueB + (valueA - valueB) * ((this.seed >>> 0) / 2**32);
|
|
1004
1019
|
}
|
|
@@ -1047,28 +1062,26 @@ class RandomGenerator
|
|
|
1047
1062
|
* let a = vec2(0, 1); // vector with coordinates (0, 1)
|
|
1048
1063
|
* a = vec2(5); // set a to (5, 5)
|
|
1049
1064
|
* b = vec2(); // set b to (0, 0)
|
|
1050
|
-
* @memberof Utilities
|
|
1051
|
-
*/
|
|
1065
|
+
* @memberof Utilities */
|
|
1052
1066
|
function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
|
|
1053
1067
|
|
|
1054
|
-
/**
|
|
1068
|
+
/**
|
|
1055
1069
|
* Check if object is a valid Vector2
|
|
1056
1070
|
* @param {any} v
|
|
1057
1071
|
* @return {boolean}
|
|
1058
|
-
* @memberof Utilities
|
|
1059
|
-
|
|
1060
|
-
function isVector2(v) { return v instanceof Vector2; }
|
|
1072
|
+
* @memberof Utilities */
|
|
1073
|
+
function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
|
|
1061
1074
|
|
|
1062
1075
|
// vector2 asserts
|
|
1063
|
-
function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v)
|
|
1076
|
+
function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v), 'Vector2 is invalid.', v); }
|
|
1064
1077
|
function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n); }
|
|
1065
1078
|
function ASSERT_VECTOR2_NORMAL(v)
|
|
1066
1079
|
{
|
|
1067
1080
|
ASSERT_VECTOR2_VALID(v);
|
|
1068
|
-
ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
|
|
1081
|
+
ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
|
|
1069
1082
|
}
|
|
1070
1083
|
|
|
1071
|
-
/**
|
|
1084
|
+
/**
|
|
1072
1085
|
* 2D Vector object with vector math library
|
|
1073
1086
|
* - Functions do not change this so they can be chained together
|
|
1074
1087
|
* @example
|
|
@@ -1192,7 +1205,7 @@ class Vector2
|
|
|
1192
1205
|
* @param {number} [angle]
|
|
1193
1206
|
* @param {number} [length]
|
|
1194
1207
|
* @return {Vector2} */
|
|
1195
|
-
setAngle(angle=0, length=1)
|
|
1208
|
+
setAngle(angle=0, length=1)
|
|
1196
1209
|
{
|
|
1197
1210
|
ASSERT_NUMBER_VALID(angle);
|
|
1198
1211
|
ASSERT_NUMBER_VALID(length);
|
|
@@ -1207,7 +1220,7 @@ class Vector2
|
|
|
1207
1220
|
rotate(angle)
|
|
1208
1221
|
{
|
|
1209
1222
|
ASSERT_NUMBER_VALID(angle);
|
|
1210
|
-
const c = Math.cos(-angle), s = Math.sin(-angle);
|
|
1223
|
+
const c = Math.cos(-angle), s = Math.sin(-angle);
|
|
1211
1224
|
return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
|
|
1212
1225
|
}
|
|
1213
1226
|
|
|
@@ -1219,9 +1232,9 @@ class Vector2
|
|
|
1219
1232
|
ASSERT_NUMBER_VALID(direction);
|
|
1220
1233
|
ASSERT_NUMBER_VALID(length);
|
|
1221
1234
|
direction = mod(direction, 4);
|
|
1222
|
-
ASSERT(direction
|
|
1235
|
+
ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
|
|
1223
1236
|
'Vector2.setDirection() direction must be an integer between 0 and 3.');
|
|
1224
|
-
return vec2(direction%2 ? direction-1 ? -length : length : 0,
|
|
1237
|
+
return vec2(direction%2 ? direction-1 ? -length : length : 0,
|
|
1225
1238
|
direction%2 ? 0 : direction ? -length : length);
|
|
1226
1239
|
}
|
|
1227
1240
|
|
|
@@ -1273,7 +1286,7 @@ class Vector2
|
|
|
1273
1286
|
/** Returns this vector expressed as a string
|
|
1274
1287
|
* @param {number} digits - precision to display
|
|
1275
1288
|
* @return {string} */
|
|
1276
|
-
toString(digits=3)
|
|
1289
|
+
toString(digits=3)
|
|
1277
1290
|
{
|
|
1278
1291
|
ASSERT_NUMBER_VALID(digits);
|
|
1279
1292
|
if (debug)
|
|
@@ -1292,7 +1305,7 @@ class Vector2
|
|
|
1292
1305
|
|
|
1293
1306
|
///////////////////////////////////////////////////////////////////////////////
|
|
1294
1307
|
|
|
1295
|
-
/**
|
|
1308
|
+
/**
|
|
1296
1309
|
* Create a color object with RGBA values, white by default
|
|
1297
1310
|
* @param {number} [r=1] - red
|
|
1298
1311
|
* @param {number} [g=1] - green
|
|
@@ -1303,29 +1316,27 @@ class Vector2
|
|
|
1303
1316
|
*/
|
|
1304
1317
|
function rgb(r, g, b, a) { return new Color(r, g, b, a); }
|
|
1305
1318
|
|
|
1306
|
-
/**
|
|
1319
|
+
/**
|
|
1307
1320
|
* Create a color object with HSLA values, white by default
|
|
1308
1321
|
* @param {number} [h=0] - hue
|
|
1309
1322
|
* @param {number} [s=0] - saturation
|
|
1310
1323
|
* @param {number} [l=1] - lightness
|
|
1311
1324
|
* @param {number} [a=1] - alpha
|
|
1312
1325
|
* @return {Color}
|
|
1313
|
-
* @memberof Utilities
|
|
1314
|
-
*/
|
|
1326
|
+
* @memberof Utilities */
|
|
1315
1327
|
function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
|
|
1316
1328
|
|
|
1317
|
-
/**
|
|
1329
|
+
/**
|
|
1318
1330
|
* Check if object is a valid Color
|
|
1319
1331
|
* @param {any} c
|
|
1320
1332
|
* @return {boolean}
|
|
1321
|
-
* @memberof Utilities
|
|
1322
|
-
|
|
1323
|
-
function isColor(c) { return c instanceof Color; }
|
|
1333
|
+
* @memberof Utilities */
|
|
1334
|
+
function isColor(c) { return c instanceof Color && c.isValid(); }
|
|
1324
1335
|
|
|
1325
1336
|
// color asserts
|
|
1326
|
-
function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c)
|
|
1337
|
+
function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'Color is invalid.', c); }
|
|
1327
1338
|
|
|
1328
|
-
/**
|
|
1339
|
+
/**
|
|
1329
1340
|
* Color object (red, green, blue, alpha) with some helpful functions
|
|
1330
1341
|
* @example
|
|
1331
1342
|
* let a = new Color; // white
|
|
@@ -1397,7 +1408,7 @@ class Color
|
|
|
1397
1408
|
* @param {number} scale
|
|
1398
1409
|
* @param {number} [alphaScale=scale]
|
|
1399
1410
|
* @return {Color} */
|
|
1400
|
-
scale(scale, alphaScale=scale)
|
|
1411
|
+
scale(scale, alphaScale=scale)
|
|
1401
1412
|
{ return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
|
|
1402
1413
|
|
|
1403
1414
|
/** Returns a copy of this color clamped to the valid range between 0 and 1
|
|
@@ -1414,9 +1425,9 @@ class Color
|
|
|
1414
1425
|
ASSERT_NUMBER_VALID(percent);
|
|
1415
1426
|
const p = clamp(percent);
|
|
1416
1427
|
return new Color(
|
|
1417
|
-
c.r*p + this.r*(1-p),
|
|
1418
|
-
c.g*p + this.g*(1-p),
|
|
1419
|
-
c.b*p + this.b*(1-p),
|
|
1428
|
+
c.r*p + this.r*(1-p),
|
|
1429
|
+
c.g*p + this.g*(1-p),
|
|
1430
|
+
c.b*p + this.b*(1-p),
|
|
1420
1431
|
c.a*p + this.a*(1-p));
|
|
1421
1432
|
}
|
|
1422
1433
|
|
|
@@ -1456,15 +1467,15 @@ class Color
|
|
|
1456
1467
|
const minC = min(r, g, b);
|
|
1457
1468
|
const l = (maxC + minC) / 2;
|
|
1458
1469
|
let h = 0, s = 0;
|
|
1459
|
-
if (maxC
|
|
1470
|
+
if (maxC !== minC)
|
|
1460
1471
|
{
|
|
1461
1472
|
let d = maxC - minC;
|
|
1462
1473
|
s = l > .5 ? d / (2 - maxC - minC) : d / (maxC + minC);
|
|
1463
|
-
if (r
|
|
1474
|
+
if (r === maxC)
|
|
1464
1475
|
h = (g - b) / d + (g < b ? 6 : 0);
|
|
1465
|
-
else if (g
|
|
1476
|
+
else if (g === maxC)
|
|
1466
1477
|
h = (b - r) / d + 2;
|
|
1467
|
-
else if (b
|
|
1478
|
+
else if (b === maxC)
|
|
1468
1479
|
h = (r - g) / d + 4;
|
|
1469
1480
|
}
|
|
1470
1481
|
return [h / 6, s, l, a];
|
|
@@ -1474,7 +1485,7 @@ class Color
|
|
|
1474
1485
|
* @param {number} [amount]
|
|
1475
1486
|
* @param {number} [alphaAmount]
|
|
1476
1487
|
* @return {Color} */
|
|
1477
|
-
mutate(amount=.05, alphaAmount=0)
|
|
1488
|
+
mutate(amount=.05, alphaAmount=0)
|
|
1478
1489
|
{
|
|
1479
1490
|
ASSERT_NUMBER_VALID(amount);
|
|
1480
1491
|
ASSERT_NUMBER_VALID(alphaAmount);
|
|
@@ -1490,47 +1501,47 @@ class Color
|
|
|
1490
1501
|
/** Returns this color expressed as a hex color code
|
|
1491
1502
|
* @param {boolean} [useAlpha] - if alpha should be included in result
|
|
1492
1503
|
* @return {string} */
|
|
1493
|
-
toString(useAlpha = true)
|
|
1504
|
+
toString(useAlpha = true)
|
|
1494
1505
|
{
|
|
1495
|
-
ASSERT(typeof useAlpha
|
|
1506
|
+
ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
|
|
1496
1507
|
if (debug && !this.isValid())
|
|
1497
1508
|
return `#000`;
|
|
1498
1509
|
const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
|
|
1499
1510
|
return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
|
|
1500
1511
|
}
|
|
1501
|
-
|
|
1512
|
+
|
|
1502
1513
|
/** Set this color from a hex code
|
|
1503
1514
|
* @param {string} hex - html hex code
|
|
1504
1515
|
* @return {Color} */
|
|
1505
1516
|
setHex(hex)
|
|
1506
1517
|
{
|
|
1507
|
-
ASSERT(typeof hex
|
|
1518
|
+
ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
|
|
1508
1519
|
ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
|
|
1509
1520
|
|
|
1510
1521
|
if (hex.length < 6)
|
|
1511
1522
|
{
|
|
1512
1523
|
const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
|
|
1513
1524
|
this.r = fromHex(1);
|
|
1514
|
-
this.g = fromHex(2)
|
|
1525
|
+
this.g = fromHex(2);
|
|
1515
1526
|
this.b = fromHex(3);
|
|
1516
|
-
this.a = hex.length
|
|
1527
|
+
this.a = hex.length === 5 ? fromHex(4) : 1;
|
|
1517
1528
|
}
|
|
1518
1529
|
else
|
|
1519
1530
|
{
|
|
1520
1531
|
const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
|
|
1521
1532
|
this.r = fromHex(1);
|
|
1522
|
-
this.g = fromHex(3)
|
|
1533
|
+
this.g = fromHex(3);
|
|
1523
1534
|
this.b = fromHex(5);
|
|
1524
|
-
this.a = hex.length
|
|
1535
|
+
this.a = hex.length === 9 ? fromHex(7) : 1;
|
|
1525
1536
|
}
|
|
1526
1537
|
|
|
1527
1538
|
ASSERT_COLOR_VALID(this);
|
|
1528
1539
|
return this;
|
|
1529
1540
|
}
|
|
1530
|
-
|
|
1541
|
+
|
|
1531
1542
|
/** Returns this color expressed as 32 bit RGBA value
|
|
1532
1543
|
* @return {number} */
|
|
1533
|
-
rgbaInt()
|
|
1544
|
+
rgbaInt()
|
|
1534
1545
|
{
|
|
1535
1546
|
const r = clamp(this.r)*255|0;
|
|
1536
1547
|
const g = clamp(this.g)*255<<8;
|
|
@@ -1551,7 +1562,7 @@ class Color
|
|
|
1551
1562
|
/** Color - White #ffffff
|
|
1552
1563
|
* @type {Color}
|
|
1553
1564
|
* @memberof Utilities */
|
|
1554
|
-
const WHITE = rgb();
|
|
1565
|
+
const WHITE = rgb();
|
|
1555
1566
|
|
|
1556
1567
|
/** Color - Clear White #ffffff with 0 alpha
|
|
1557
1568
|
* @type {Color}
|
|
@@ -1666,11 +1677,11 @@ class Timer
|
|
|
1666
1677
|
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
1667
1678
|
* @return {number} */
|
|
1668
1679
|
getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
|
|
1669
|
-
|
|
1680
|
+
|
|
1670
1681
|
/** Returns this timer expressed as a string
|
|
1671
1682
|
* @return {string} */
|
|
1672
1683
|
toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
|
|
1673
|
-
|
|
1684
|
+
|
|
1674
1685
|
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
1675
1686
|
* @return {number} */
|
|
1676
1687
|
valueOf() { return this.get(); }
|
|
@@ -1706,7 +1717,7 @@ let cameraScale = 32;
|
|
|
1706
1717
|
// Display settings
|
|
1707
1718
|
|
|
1708
1719
|
/** Enable applying color to tiles when using canvas2d
|
|
1709
|
-
* - This is slower but should be the same as
|
|
1720
|
+
* - This is slower but should be the same as WebGL rendering
|
|
1710
1721
|
* @type {boolean}
|
|
1711
1722
|
* @default
|
|
1712
1723
|
* @memberof Settings */
|
|
@@ -1752,14 +1763,13 @@ let tilesPixelated = true;
|
|
|
1752
1763
|
* @memberof Settings */
|
|
1753
1764
|
let fontDefault = 'arial';
|
|
1754
1765
|
|
|
1755
|
-
/** Enable to show the LittleJS splash screen
|
|
1766
|
+
/** Enable to show the LittleJS splash screen on startup
|
|
1756
1767
|
* @type {boolean}
|
|
1757
1768
|
* @default
|
|
1758
1769
|
* @memberof Settings */
|
|
1759
1770
|
let showSplashScreen = false;
|
|
1760
1771
|
|
|
1761
1772
|
/** Disables all rendering, audio, and input for servers
|
|
1762
|
-
* - Must be set before startup to take effect
|
|
1763
1773
|
* @type {boolean}
|
|
1764
1774
|
* @default
|
|
1765
1775
|
* @memberof Settings */
|
|
@@ -1768,13 +1778,18 @@ let headlessMode = false;
|
|
|
1768
1778
|
///////////////////////////////////////////////////////////////////////////////
|
|
1769
1779
|
// WebGL settings
|
|
1770
1780
|
|
|
1771
|
-
/** Enable
|
|
1772
|
-
* - Must be set before startup to take effect
|
|
1781
|
+
/** Enable WebGL accelerated rendering
|
|
1773
1782
|
* @type {boolean}
|
|
1774
1783
|
* @default
|
|
1775
1784
|
* @memberof Settings */
|
|
1776
1785
|
let glEnable = true;
|
|
1777
1786
|
|
|
1787
|
+
/** How many sided poly to use when drawing circles and ellipses with WebGL
|
|
1788
|
+
* @type {number}
|
|
1789
|
+
* @default
|
|
1790
|
+
* @memberof Settings */
|
|
1791
|
+
let glCircleSides = 32;
|
|
1792
|
+
|
|
1778
1793
|
///////////////////////////////////////////////////////////////////////////////
|
|
1779
1794
|
// Tile sheet settings
|
|
1780
1795
|
|
|
@@ -1856,13 +1871,13 @@ let particleEmitRateScale = 1;
|
|
|
1856
1871
|
* @memberof Settings */
|
|
1857
1872
|
let gamepadsEnable = true;
|
|
1858
1873
|
|
|
1859
|
-
/** If true, the dpad input is also routed to the left analog stick (for better
|
|
1874
|
+
/** If true, the dpad input is also routed to the left analog stick (for better accessibility)
|
|
1860
1875
|
* @type {boolean}
|
|
1861
1876
|
* @default
|
|
1862
1877
|
* @memberof Settings */
|
|
1863
1878
|
let gamepadDirectionEmulateStick = true;
|
|
1864
1879
|
|
|
1865
|
-
/** If true the WASD keys are also routed to the direction keys (for better
|
|
1880
|
+
/** If true the WASD keys are also routed to the direction keys (for better accessibility)
|
|
1866
1881
|
* @type {boolean}
|
|
1867
1882
|
* @default
|
|
1868
1883
|
* @memberof Settings */
|
|
@@ -1870,7 +1885,6 @@ let inputWASDEmulateDirection = true;
|
|
|
1870
1885
|
|
|
1871
1886
|
/** True if touch input is enabled for mobile devices
|
|
1872
1887
|
* - Touch events will be routed to mouse events
|
|
1873
|
-
* - Must be set before startup to take effect
|
|
1874
1888
|
* @type {boolean}
|
|
1875
1889
|
* @default
|
|
1876
1890
|
* @memberof Settings */
|
|
@@ -1878,7 +1892,6 @@ let touchInputEnable = true;
|
|
|
1878
1892
|
|
|
1879
1893
|
/** True if touch gamepad should appear on mobile devices
|
|
1880
1894
|
* - Supports left analog stick, 4 face buttons and start button (button 9)
|
|
1881
|
-
* - Must be set before startup to take effect
|
|
1882
1895
|
* @type {boolean}
|
|
1883
1896
|
* @default
|
|
1884
1897
|
* @memberof Settings */
|
|
@@ -1981,7 +1994,7 @@ function setCameraAngle(angle) { cameraAngle = angle; }
|
|
|
1981
1994
|
function setCameraScale(scale) { cameraScale = scale; }
|
|
1982
1995
|
|
|
1983
1996
|
/** Set if tiles should be colorized when using canvas2d
|
|
1984
|
-
* This can be slower but results should look nearly identical to
|
|
1997
|
+
* This can be slower but results should look nearly identical to WebGL rendering
|
|
1985
1998
|
* It can be enabled/disabled at any time
|
|
1986
1999
|
* Optimized for performance, and will use faster method if color is white or untextured
|
|
1987
2000
|
* @param {boolean} colorTiles
|
|
@@ -2017,8 +2030,8 @@ function setCanvasPixelated(pixelated)
|
|
|
2017
2030
|
* @param {boolean} pixelated
|
|
2018
2031
|
* @memberof Settings */
|
|
2019
2032
|
function setOverlayCanvasPixelated(pixelated)
|
|
2020
|
-
{
|
|
2021
|
-
overlayCanvasPixelated = pixelated;
|
|
2033
|
+
{
|
|
2034
|
+
overlayCanvasPixelated = pixelated;
|
|
2022
2035
|
if (overlayCanvas)
|
|
2023
2036
|
overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
|
|
2024
2037
|
}
|
|
@@ -2033,7 +2046,7 @@ function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
|
|
|
2033
2046
|
* @memberof Settings */
|
|
2034
2047
|
function setFontDefault(font) { fontDefault = font; }
|
|
2035
2048
|
|
|
2036
|
-
/** Set if the LittleJS splash screen be shown on startup
|
|
2049
|
+
/** Set if the LittleJS splash screen should be shown on startup
|
|
2037
2050
|
* @param {boolean} show
|
|
2038
2051
|
* @memberof Settings */
|
|
2039
2052
|
function setShowSplashScreen(show) { showSplashScreen = show; }
|
|
@@ -2043,16 +2056,21 @@ function setShowSplashScreen(show) { showSplashScreen = show; }
|
|
|
2043
2056
|
* @memberof Settings */
|
|
2044
2057
|
function setHeadlessMode(headless) { headlessMode = headless; }
|
|
2045
2058
|
|
|
2046
|
-
/** Set if
|
|
2059
|
+
/** Set if WebGL rendering is enabled
|
|
2047
2060
|
* @param {boolean} enable
|
|
2048
2061
|
* @memberof Settings */
|
|
2049
2062
|
function setGLEnable(enable)
|
|
2050
2063
|
{
|
|
2051
2064
|
glEnable = enable;
|
|
2052
|
-
if (glCanvas) // hide glCanvas if
|
|
2065
|
+
if (glCanvas) // hide glCanvas if WebGL is disabled
|
|
2053
2066
|
glCanvas.style.visibility = enable ? 'visible' : 'hidden';
|
|
2054
2067
|
}
|
|
2055
2068
|
|
|
2069
|
+
/** Set how many sided polygons to use when drawing circles and elipses with WebGL
|
|
2070
|
+
* @param {number} sides
|
|
2071
|
+
* @memberof Settings */
|
|
2072
|
+
function setGLCircleSides(sides) { glCircleSides = sides; }
|
|
2073
|
+
|
|
2056
2074
|
/** Set default size of tiles in pixels
|
|
2057
2075
|
* @param {Vector2} size
|
|
2058
2076
|
* @memberof Settings */
|
|
@@ -2083,7 +2101,7 @@ function setObjectDefaultDamping(damp) { objectDefaultDamping = damp; }
|
|
|
2083
2101
|
* @memberof Settings */
|
|
2084
2102
|
function setObjectDefaultAngleDamping(damp) { objectDefaultAngleDamping = damp; }
|
|
2085
2103
|
|
|
2086
|
-
/** Set how much to bounce when a collision
|
|
2104
|
+
/** Set how much to bounce when a collision occurs
|
|
2087
2105
|
* @param {number} restitution
|
|
2088
2106
|
* @memberof Settings */
|
|
2089
2107
|
function setObjectDefaultRestitution(restitution) { objectDefaultRestitution = restitution; }
|
|
@@ -2207,11 +2225,11 @@ function setShowWatermark(show) { showWatermark = show; }
|
|
|
2207
2225
|
* @param {string} key
|
|
2208
2226
|
* @memberof Debug */
|
|
2209
2227
|
function setDebugKey(key) { debugKey = key; }
|
|
2210
|
-
/**
|
|
2228
|
+
/**
|
|
2211
2229
|
* LittleJS Object System
|
|
2212
2230
|
*/
|
|
2213
2231
|
|
|
2214
|
-
/**
|
|
2232
|
+
/**
|
|
2215
2233
|
* LittleJS Object Base Object Class
|
|
2216
2234
|
* - Top level object class used by the engine
|
|
2217
2235
|
* - Automatically adds self to object list
|
|
@@ -2234,7 +2252,7 @@ function setDebugKey(key) { debugKey = key; }
|
|
|
2234
2252
|
* @example
|
|
2235
2253
|
* // create an engine object, normally you would first extend the class with your own
|
|
2236
2254
|
* const pos = vec2(2,3);
|
|
2237
|
-
* const object = new EngineObject(pos);
|
|
2255
|
+
* const object = new EngineObject(pos);
|
|
2238
2256
|
*/
|
|
2239
2257
|
class EngineObject
|
|
2240
2258
|
{
|
|
@@ -2249,12 +2267,12 @@ class EngineObject
|
|
|
2249
2267
|
constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=new Color, renderOrder=0)
|
|
2250
2268
|
{
|
|
2251
2269
|
// check passed in params
|
|
2252
|
-
ASSERT(isVector2(pos)
|
|
2253
|
-
ASSERT(isVector2(size)
|
|
2270
|
+
ASSERT(isVector2(pos), 'object pos should be a vec2');
|
|
2271
|
+
ASSERT(isVector2(size), 'object size should be a vec2');
|
|
2254
2272
|
ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
|
|
2255
|
-
ASSERT(typeof angle
|
|
2256
|
-
ASSERT(isColor(color)
|
|
2257
|
-
ASSERT(typeof renderOrder
|
|
2273
|
+
ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
|
|
2274
|
+
ASSERT(isColor(color), 'object color should be a valid rgba color');
|
|
2275
|
+
ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
|
|
2258
2276
|
|
|
2259
2277
|
/** @property {Vector2} - World space position of the object */
|
|
2260
2278
|
this.pos = pos.copy();
|
|
@@ -2322,7 +2340,7 @@ class EngineObject
|
|
|
2322
2340
|
// add to list of objects
|
|
2323
2341
|
engineObjects.push(this);
|
|
2324
2342
|
}
|
|
2325
|
-
|
|
2343
|
+
|
|
2326
2344
|
/** Update the object transform, called automatically by engine even when paused */
|
|
2327
2345
|
updateTransforms()
|
|
2328
2346
|
{
|
|
@@ -2391,7 +2409,7 @@ class EngineObject
|
|
|
2391
2409
|
for (const o of engineObjectsCollide)
|
|
2392
2410
|
{
|
|
2393
2411
|
// non solid objects don't collide with each other
|
|
2394
|
-
if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o
|
|
2412
|
+
if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o === this)
|
|
2395
2413
|
continue;
|
|
2396
2414
|
|
|
2397
2415
|
// check collision
|
|
@@ -2414,7 +2432,7 @@ class EngineObject
|
|
|
2414
2432
|
this.velocity = this.velocity.add(velocity);
|
|
2415
2433
|
if (o.mass) // push away if not fixed
|
|
2416
2434
|
o.velocity = o.velocity.subtract(velocity);
|
|
2417
|
-
|
|
2435
|
+
|
|
2418
2436
|
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
|
|
2419
2437
|
continue;
|
|
2420
2438
|
}
|
|
@@ -2425,7 +2443,7 @@ class EngineObject
|
|
|
2425
2443
|
const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
|
|
2426
2444
|
const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
|
|
2427
2445
|
const restitution = max(this.restitution, o.restitution);
|
|
2428
|
-
|
|
2446
|
+
|
|
2429
2447
|
if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
|
|
2430
2448
|
{
|
|
2431
2449
|
// push outside object collision
|
|
@@ -2513,7 +2531,7 @@ class EngineObject
|
|
|
2513
2531
|
{
|
|
2514
2532
|
// move to previous position
|
|
2515
2533
|
this.pos.y = oldPos.y;
|
|
2516
|
-
this.groundObject = undefined;
|
|
2534
|
+
this.groundObject = undefined;
|
|
2517
2535
|
}
|
|
2518
2536
|
}
|
|
2519
2537
|
if (blockedLayerX)
|
|
@@ -2527,20 +2545,20 @@ class EngineObject
|
|
|
2527
2545
|
}
|
|
2528
2546
|
}
|
|
2529
2547
|
}
|
|
2530
|
-
|
|
2548
|
+
|
|
2531
2549
|
/** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
|
|
2532
2550
|
render()
|
|
2533
2551
|
{
|
|
2534
2552
|
// default object render
|
|
2535
2553
|
drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
|
|
2536
2554
|
}
|
|
2537
|
-
|
|
2555
|
+
|
|
2538
2556
|
/** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
|
|
2539
2557
|
destroy()
|
|
2540
|
-
{
|
|
2558
|
+
{
|
|
2541
2559
|
if (this.destroyed)
|
|
2542
2560
|
return;
|
|
2543
|
-
|
|
2561
|
+
|
|
2544
2562
|
// disconnect from parent and destroy children
|
|
2545
2563
|
this.destroyed = 1;
|
|
2546
2564
|
this.parent && this.parent.removeChild(this);
|
|
@@ -2566,7 +2584,7 @@ class EngineObject
|
|
|
2566
2584
|
/** Convert from world space to local space for a vector (rotation only)
|
|
2567
2585
|
* @param {Vector2} vec - world space vector */
|
|
2568
2586
|
worldToLocalVector(vec) { return vec.rotate(-this.angle); }
|
|
2569
|
-
|
|
2587
|
+
|
|
2570
2588
|
/** Called to check if a tile collision should be resolved
|
|
2571
2589
|
* @param {number} tileData - the value of the tile at the position
|
|
2572
2590
|
* @param {Vector2} pos - tile where the collision occurred
|
|
@@ -2585,16 +2603,19 @@ class EngineObject
|
|
|
2585
2603
|
|
|
2586
2604
|
/** Apply acceleration to this object (adjust velocity, not affected by mass)
|
|
2587
2605
|
* @param {Vector2} acceleration */
|
|
2588
|
-
applyAcceleration(acceleration)
|
|
2606
|
+
applyAcceleration(acceleration)
|
|
2607
|
+
{ if (this.mass) this.velocity = this.velocity.add(acceleration); }
|
|
2589
2608
|
|
|
2590
|
-
/** Apply angular acceleration to this object
|
|
2609
|
+
/** Apply angular acceleration to this object
|
|
2591
2610
|
* @param {number} acceleration */
|
|
2592
|
-
applyAngularAcceleration(acceleration)
|
|
2611
|
+
applyAngularAcceleration(acceleration)
|
|
2612
|
+
{ if (this.mass) this.angleVelocity += acceleration; }
|
|
2593
2613
|
|
|
2594
2614
|
/** Apply force to this object (adjust velocity, affected by mass)
|
|
2595
2615
|
* @param {Vector2} force */
|
|
2596
|
-
applyForce(force)
|
|
2597
|
-
|
|
2616
|
+
applyForce(force)
|
|
2617
|
+
{ if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
|
|
2618
|
+
|
|
2598
2619
|
/** Get the direction of the mirror
|
|
2599
2620
|
* @return {number} -1 if this.mirror is true, or 1 if not mirrored */
|
|
2600
2621
|
getMirrorSign() { return this.mirror ? -1 : 1; }
|
|
@@ -2616,7 +2637,7 @@ class EngineObject
|
|
|
2616
2637
|
* @param {EngineObject} child */
|
|
2617
2638
|
removeChild(child)
|
|
2618
2639
|
{
|
|
2619
|
-
ASSERT(child.parent
|
|
2640
|
+
ASSERT(child.parent === this && this.children.includes(child));
|
|
2620
2641
|
this.children.splice(this.children.indexOf(child), 1);
|
|
2621
2642
|
child.parent = 0;
|
|
2622
2643
|
}
|
|
@@ -2662,7 +2683,7 @@ class EngineObject
|
|
|
2662
2683
|
{
|
|
2663
2684
|
if (!debug)
|
|
2664
2685
|
return;
|
|
2665
|
-
|
|
2686
|
+
|
|
2666
2687
|
// show object info for debugging
|
|
2667
2688
|
const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
|
|
2668
2689
|
const color = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
|
|
@@ -2672,24 +2693,24 @@ class EngineObject
|
|
|
2672
2693
|
this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(1,1,1,.5));
|
|
2673
2694
|
}
|
|
2674
2695
|
}
|
|
2675
|
-
/**
|
|
2696
|
+
/**
|
|
2676
2697
|
* LittleJS Drawing System
|
|
2677
2698
|
* - Hybrid system with both Canvas2D and WebGL available
|
|
2678
2699
|
* - Super fast tile sheet rendering with WebGL
|
|
2679
2700
|
* - Can apply rotation, mirror, color and additive color
|
|
2680
2701
|
* - Font rendering system with built in engine font
|
|
2681
2702
|
* - Many useful utility functions
|
|
2682
|
-
*
|
|
2703
|
+
*
|
|
2683
2704
|
* LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
|
|
2684
2705
|
* There are 3 canvas/contexts available to draw to...
|
|
2685
2706
|
* mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
|
|
2686
2707
|
* glCanvas - Used by the accelerated WebGL batch rendering system.
|
|
2687
2708
|
* overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
|
|
2688
|
-
*
|
|
2709
|
+
*
|
|
2689
2710
|
* The WebGL rendering system is very fast with some caveats...
|
|
2690
2711
|
* - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
|
|
2691
2712
|
* - Group additive rendering together using renderOrder to mitigate this issue
|
|
2692
|
-
*
|
|
2713
|
+
*
|
|
2693
2714
|
* The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
|
|
2694
2715
|
* @namespace Draw
|
|
2695
2716
|
*/
|
|
@@ -2734,7 +2755,7 @@ let workCanvas;
|
|
|
2734
2755
|
* @memberof Draw */
|
|
2735
2756
|
let workContext;
|
|
2736
2757
|
|
|
2737
|
-
/** The size of the main canvas (and other secondary canvases)
|
|
2758
|
+
/** The size of the main canvas (and other secondary canvases)
|
|
2738
2759
|
* @type {Vector2}
|
|
2739
2760
|
* @memberof Draw */
|
|
2740
2761
|
let mainCanvasSize = vec2();
|
|
@@ -2744,12 +2765,14 @@ let mainCanvasSize = vec2();
|
|
|
2744
2765
|
* @memberof Draw */
|
|
2745
2766
|
let textureInfos = [];
|
|
2746
2767
|
|
|
2747
|
-
|
|
2768
|
+
/** Keeps track of how many draw calls there were each frame for debugging
|
|
2769
|
+
* @type {number}
|
|
2770
|
+
* @memberof Draw */
|
|
2748
2771
|
let drawCount;
|
|
2749
2772
|
|
|
2750
2773
|
///////////////////////////////////////////////////////////////////////////////
|
|
2751
2774
|
|
|
2752
|
-
/**
|
|
2775
|
+
/**
|
|
2753
2776
|
* Create a tile info object using a grid based system
|
|
2754
2777
|
* - This can take vecs or floats for easier use and conversion
|
|
2755
2778
|
* - If an index is passed in, the tile size and index will determine the position
|
|
@@ -2763,15 +2786,14 @@ let drawCount;
|
|
|
2763
2786
|
* tile(5, 8) // a tile at index 5 using a tile size of 8
|
|
2764
2787
|
* tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
|
|
2765
2788
|
* tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
|
|
2766
|
-
* @memberof Draw
|
|
2767
|
-
*/
|
|
2789
|
+
* @memberof Draw */
|
|
2768
2790
|
function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
|
|
2769
2791
|
{
|
|
2770
2792
|
if (headlessMode)
|
|
2771
2793
|
return new TileInfo;
|
|
2772
2794
|
|
|
2773
2795
|
// if size is a number, make it a vector
|
|
2774
|
-
if (typeof size
|
|
2796
|
+
if (typeof size === 'number')
|
|
2775
2797
|
{
|
|
2776
2798
|
ASSERT(size > 0);
|
|
2777
2799
|
size = new Vector2(size, size);
|
|
@@ -2780,24 +2802,24 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
|
|
|
2780
2802
|
// create tile info object
|
|
2781
2803
|
const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
|
|
2782
2804
|
|
|
2783
|
-
//
|
|
2805
|
+
// get the position of the tile
|
|
2784
2806
|
const textureInfo = textureInfos[textureIndex];
|
|
2785
2807
|
ASSERT(!!textureInfo, 'Texture not loaded');
|
|
2786
2808
|
const sizePaddedX = size.x + padding*2;
|
|
2787
2809
|
const sizePaddedY = size.y + padding*2;
|
|
2788
|
-
if (typeof pos
|
|
2810
|
+
if (typeof pos === 'number')
|
|
2789
2811
|
{
|
|
2790
2812
|
const cols = textureInfo.size.x / sizePaddedX |0;
|
|
2791
|
-
ASSERT(cols>0, 'Tile size is too big for texture');
|
|
2813
|
+
ASSERT(cols > 0, 'Tile size is too big for texture');
|
|
2792
2814
|
const posX = pos % cols, posY = (pos / cols) |0;
|
|
2793
2815
|
tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
|
|
2794
2816
|
}
|
|
2795
2817
|
else
|
|
2796
2818
|
tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
|
|
2797
|
-
return tileInfo;
|
|
2819
|
+
return tileInfo;
|
|
2798
2820
|
}
|
|
2799
2821
|
|
|
2800
|
-
/**
|
|
2822
|
+
/**
|
|
2801
2823
|
* Tile Info - Stores info about how to draw a tile
|
|
2802
2824
|
*/
|
|
2803
2825
|
class TileInfo
|
|
@@ -2835,14 +2857,14 @@ class TileInfo
|
|
|
2835
2857
|
*/
|
|
2836
2858
|
frame(frame)
|
|
2837
2859
|
{
|
|
2838
|
-
ASSERT(typeof frame
|
|
2860
|
+
ASSERT(typeof frame === 'number');
|
|
2839
2861
|
return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
|
|
2840
2862
|
}
|
|
2841
2863
|
|
|
2842
2864
|
/**
|
|
2843
2865
|
* Set this tile to use a full image
|
|
2844
2866
|
* @param {HTMLImageElement|OffscreenCanvas} image
|
|
2845
|
-
* @param {WebGLTexture} [glTexture] -
|
|
2867
|
+
* @param {WebGLTexture} [glTexture] - WebGL texture
|
|
2846
2868
|
* @return {TileInfo}
|
|
2847
2869
|
*/
|
|
2848
2870
|
setFullImage(image, glTexture)
|
|
@@ -2860,7 +2882,7 @@ class TextureInfo
|
|
|
2860
2882
|
/**
|
|
2861
2883
|
* Create a TextureInfo, called automatically by the engine
|
|
2862
2884
|
* @param {HTMLImageElement|OffscreenCanvas} image
|
|
2863
|
-
* @param {WebGLTexture} [glTexture] -
|
|
2885
|
+
* @param {WebGLTexture} [glTexture] - WebGL texture
|
|
2864
2886
|
*/
|
|
2865
2887
|
constructor(image, glTexture)
|
|
2866
2888
|
{
|
|
@@ -2870,7 +2892,7 @@ class TextureInfo
|
|
|
2870
2892
|
this.size = vec2(image.width, image.height);
|
|
2871
2893
|
/** @property {Vector2} - inverse of the size, cached for rendering */
|
|
2872
2894
|
this.sizeInverse = vec2(1/image.width, 1/image.height);
|
|
2873
|
-
/** @property {WebGLTexture} -
|
|
2895
|
+
/** @property {WebGLTexture} - WebGL texture */
|
|
2874
2896
|
this.glTexture = glTexture;
|
|
2875
2897
|
}
|
|
2876
2898
|
|
|
@@ -2886,28 +2908,25 @@ class TextureInfo
|
|
|
2886
2908
|
// Drawing functions
|
|
2887
2909
|
|
|
2888
2910
|
/** Draw textured tile centered in world space, with color applied if using WebGL
|
|
2889
|
-
* @param {Vector2}
|
|
2890
|
-
* @param {Vector2}
|
|
2891
|
-
* @param {TileInfo}[tileInfo]
|
|
2892
|
-
* @param {Color}
|
|
2893
|
-
* @param {number}
|
|
2894
|
-
* @param {boolean}
|
|
2895
|
-
* @param {Color}
|
|
2896
|
-
* @param {boolean}
|
|
2897
|
-
* @param {boolean}
|
|
2911
|
+
* @param {Vector2} pos - Center of the tile in world space
|
|
2912
|
+
* @param {Vector2} [size=(1,1)] - Size of the tile in world space
|
|
2913
|
+
* @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
|
|
2914
|
+
* @param {Color} [color=(1,1,1,1)] - Color to modulate with
|
|
2915
|
+
* @param {number} [angle] - Angle to rotate by
|
|
2916
|
+
* @param {boolean} [mirror] - Is image flipped along the Y axis?
|
|
2917
|
+
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
2918
|
+
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
2919
|
+
* @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
|
|
2898
2920
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
2899
2921
|
* @memberof Draw */
|
|
2900
|
-
function drawTile(pos, size=new Vector2(1), tileInfo, color=
|
|
2922
|
+
function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
|
|
2901
2923
|
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
|
|
2902
2924
|
{
|
|
2903
|
-
ASSERT(
|
|
2904
|
-
ASSERT(isVector2(
|
|
2905
|
-
ASSERT(isVector2(size) && size.isValid(), 'drawTile size should be a vec2');
|
|
2925
|
+
ASSERT(isVector2(pos), 'drawTile pos should be a vec2');
|
|
2926
|
+
ASSERT(isVector2(size), 'drawTile size should be a vec2');
|
|
2906
2927
|
ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
|
|
2907
2928
|
ASSERT(isNumber(angle), 'drawTile angle should be a number');
|
|
2908
|
-
|
|
2909
|
-
if (color.a <= 0 && (!additiveColor || additiveColor.a <= 0) || !size.x || !size.y)
|
|
2910
|
-
return; // completely invisible, skip render
|
|
2929
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
2911
2930
|
|
|
2912
2931
|
const textureInfo = tileInfo && tileInfo.textureInfo;
|
|
2913
2932
|
if (useWebGL)
|
|
@@ -2931,28 +2950,28 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
|
|
|
2931
2950
|
{
|
|
2932
2951
|
const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
|
|
2933
2952
|
const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
|
|
2934
|
-
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
2935
|
-
x + tileImageFixBleedX, y + tileImageFixBleedY,
|
|
2936
|
-
x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
|
|
2937
|
-
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
2953
|
+
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
2954
|
+
x + tileImageFixBleedX, y + tileImageFixBleedY,
|
|
2955
|
+
x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
|
|
2956
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
2938
2957
|
}
|
|
2939
2958
|
else
|
|
2940
2959
|
{
|
|
2941
|
-
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
2942
|
-
x, y, x + w, y + h,
|
|
2943
|
-
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
2960
|
+
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
2961
|
+
x, y, x + w, y + h,
|
|
2962
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
2944
2963
|
}
|
|
2945
2964
|
}
|
|
2946
2965
|
else
|
|
2947
2966
|
{
|
|
2948
2967
|
// if no tile info, force untextured
|
|
2949
|
-
glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
|
|
2968
|
+
glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
|
|
2950
2969
|
}
|
|
2951
2970
|
}
|
|
2952
2971
|
else
|
|
2953
2972
|
{
|
|
2954
2973
|
// normal canvas 2D rendering method (slower)
|
|
2955
|
-
|
|
2974
|
+
++drawCount;
|
|
2956
2975
|
size = new Vector2(size.x, -size.y); // fix upside down sprites
|
|
2957
2976
|
drawCanvas2D(pos, size, angle, mirror, (context)=>
|
|
2958
2977
|
{
|
|
@@ -2984,14 +3003,128 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
|
|
|
2984
3003
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
2985
3004
|
* @memberof Draw */
|
|
2986
3005
|
function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
2987
|
-
{
|
|
2988
|
-
drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
|
|
3006
|
+
{
|
|
3007
|
+
drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
/** Draw a rect centered on pos with a gradient from top to bottom
|
|
3011
|
+
* @param {Vector2} pos
|
|
3012
|
+
* @param {Vector2} [size=(1,1)]
|
|
3013
|
+
* @param {Color} [colorTop=(1,1,1,1)]
|
|
3014
|
+
* @param {Color} [colorBottom=(0,0,0,1)]
|
|
3015
|
+
* @param {number} [angle]
|
|
3016
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3017
|
+
* @param {boolean} [screenSpace]
|
|
3018
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3019
|
+
* @memberof Draw */
|
|
3020
|
+
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
3021
|
+
{
|
|
3022
|
+
ASSERT(isVector2(pos), 'drawRectGradient pos should be a vec2');
|
|
3023
|
+
ASSERT(isVector2(size), 'drawRectGradient size should be a vec2');
|
|
3024
|
+
ASSERT(isColor(colorTop) && isColor(colorBottom), 'drawRectGradient color is invalid');
|
|
3025
|
+
ASSERT(isNumber(angle), 'drawRectGradient angle should be a number');
|
|
3026
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3027
|
+
if (useWebGL)
|
|
3028
|
+
{
|
|
3029
|
+
if (screenSpace)
|
|
3030
|
+
{
|
|
3031
|
+
// convert to world space
|
|
3032
|
+
pos = screenToWorld(pos);
|
|
3033
|
+
size = size.scale(1/cameraScale);
|
|
3034
|
+
}
|
|
3035
|
+
// build 4 corner points for the rectangle
|
|
3036
|
+
const points = [], colors = [];
|
|
3037
|
+
const halfSizeX = size.x/2, halfSizeY = size.y/2;
|
|
3038
|
+
const colorTopInt = colorTop.rgbaInt();
|
|
3039
|
+
const colorBottomInt = colorBottom.rgbaInt();
|
|
3040
|
+
const c = Math.cos(-angle), s = Math.sin(-angle);
|
|
3041
|
+
for (let i=4; i--;)
|
|
3042
|
+
{
|
|
3043
|
+
const x = i & 1 ? halfSizeX : -halfSizeX;
|
|
3044
|
+
const y = i & 2 ? halfSizeY : -halfSizeY;
|
|
3045
|
+
const rx = x * c - y * s;
|
|
3046
|
+
const ry = x * s + y * c;
|
|
3047
|
+
const color = i & 2 ? colorTopInt : colorBottomInt;
|
|
3048
|
+
points.push(vec2(pos.x + rx, pos.y + ry));
|
|
3049
|
+
colors.push(color);
|
|
3050
|
+
}
|
|
3051
|
+
glDrawColoredPoints(points, colors);
|
|
3052
|
+
}
|
|
3053
|
+
else
|
|
3054
|
+
{
|
|
3055
|
+
// normal canvas 2D rendering method (slower)
|
|
3056
|
+
++drawCount;
|
|
3057
|
+
size = new Vector2(size.x, -size.y); // fix upside down sprites
|
|
3058
|
+
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
3059
|
+
{
|
|
3060
|
+
// if no tile info, use untextured rect
|
|
3061
|
+
const gradient = context.createLinearGradient(0, -.5, 0, .5);
|
|
3062
|
+
gradient.addColorStop(0, colorTop.toString());
|
|
3063
|
+
gradient.addColorStop(1, colorBottom.toString());
|
|
3064
|
+
context.fillStyle = gradient;
|
|
3065
|
+
context.fillRect(-.5, -.5, 1, 1);
|
|
3066
|
+
}, screenSpace, context);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
/** Draw connected lines between a series of points
|
|
3071
|
+
* @param {Array<Vector2>} points
|
|
3072
|
+
* @param {number} [width]
|
|
3073
|
+
* @param {Color} [color=(1,1,1,1)]
|
|
3074
|
+
* @param {boolean} [wrap] - Should the last point connect to the first?
|
|
3075
|
+
* @param {Vector2} [pos=(0,0)] - Offset to apply
|
|
3076
|
+
* @param {number} [angle] - Angle to rotate by
|
|
3077
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3078
|
+
* @param {boolean} [screenSpace]
|
|
3079
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3080
|
+
* @memberof Draw */
|
|
3081
|
+
function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
|
|
3082
|
+
{
|
|
3083
|
+
ASSERT(Array.isArray(points), 'drawLineList points should be an array');
|
|
3084
|
+
ASSERT(isNumber(width), 'drawLineList width should be a number');
|
|
3085
|
+
ASSERT(isColor(color), 'drawLineList color is invalid');
|
|
3086
|
+
ASSERT(isVector2(pos), 'drawLineList pos should be a vec2');
|
|
3087
|
+
ASSERT(isNumber(angle), 'drawLineList angle should be a number');
|
|
3088
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3089
|
+
if (useWebGL)
|
|
3090
|
+
{
|
|
3091
|
+
let scale = 1;
|
|
3092
|
+
if (screenSpace)
|
|
3093
|
+
{
|
|
3094
|
+
// convert to world space
|
|
3095
|
+
pos = screenToWorld(pos);
|
|
3096
|
+
scale = 1/cameraScale;
|
|
3097
|
+
}
|
|
3098
|
+
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, scale, scale, angle, wrap);
|
|
3099
|
+
}
|
|
3100
|
+
else
|
|
3101
|
+
{
|
|
3102
|
+
// normal canvas 2D rendering method (slower)
|
|
3103
|
+
++drawCount;
|
|
3104
|
+
drawCanvas2D(pos, vec2(1), angle, false, (context)=>
|
|
3105
|
+
{
|
|
3106
|
+
context.strokeStyle = color.toString();
|
|
3107
|
+
context.lineWidth = width;
|
|
3108
|
+
context.beginPath();
|
|
3109
|
+
for (let i=0; i<points.length; ++i)
|
|
3110
|
+
{
|
|
3111
|
+
const point = points[i];
|
|
3112
|
+
if (i)
|
|
3113
|
+
context.lineTo(point.x, point.y);
|
|
3114
|
+
else
|
|
3115
|
+
context.moveTo(point.x, point.y);
|
|
3116
|
+
}
|
|
3117
|
+
if (wrap)
|
|
3118
|
+
context.closePath();
|
|
3119
|
+
context.stroke();
|
|
3120
|
+
}, screenSpace, context);
|
|
3121
|
+
}
|
|
2989
3122
|
}
|
|
2990
3123
|
|
|
2991
3124
|
/** Draw colored line between two points
|
|
2992
3125
|
* @param {Vector2} posA
|
|
2993
3126
|
* @param {Vector2} posB
|
|
2994
|
-
* @param {number} [
|
|
3127
|
+
* @param {number} [width]
|
|
2995
3128
|
* @param {Color} [color=(1,1,1,1)]
|
|
2996
3129
|
* @param {Vector2} [pos=(0,0)] - Offset to apply
|
|
2997
3130
|
* @param {number} [angle] - Angle to rotate by
|
|
@@ -2999,15 +3132,43 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
2999
3132
|
* @param {boolean} [screenSpace]
|
|
3000
3133
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3001
3134
|
* @memberof Draw */
|
|
3002
|
-
function drawLine(posA, posB,
|
|
3135
|
+
function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
|
|
3003
3136
|
{
|
|
3004
3137
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
3005
|
-
const size = vec2(
|
|
3138
|
+
const size = vec2(width, halfDelta.length()*2);
|
|
3006
3139
|
pos = pos.add(posA.add(halfDelta));
|
|
3007
3140
|
angle += halfDelta.angle();
|
|
3008
3141
|
drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
|
|
3009
3142
|
}
|
|
3010
3143
|
|
|
3144
|
+
/** Draw colored regular polygon using passed in number of sides
|
|
3145
|
+
* @param {Vector2} pos
|
|
3146
|
+
* @param {Vector2} [size=(1,1)]
|
|
3147
|
+
* @param {number} [sides]
|
|
3148
|
+
* @param {Color} [color=(1,1,1,1)]
|
|
3149
|
+
* @param {number} [angle]
|
|
3150
|
+
* @param {number} [lineWidth]
|
|
3151
|
+
* @param {Color} [lineColor=(0,0,0,1)]
|
|
3152
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3153
|
+
* @param {boolean} [screenSpace]
|
|
3154
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3155
|
+
* @memberof Draw */
|
|
3156
|
+
function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
3157
|
+
{
|
|
3158
|
+
ASSERT(isVector2(size), 'drawRegularPoly size should be a vec2');
|
|
3159
|
+
ASSERT(isNumber(sides), 'drawRegularPoly sides should be a number');
|
|
3160
|
+
|
|
3161
|
+
// build regular polygon points
|
|
3162
|
+
const points = [];
|
|
3163
|
+
const sizeX = size.x/2, sizeY = size.y/2;
|
|
3164
|
+
for (let i=sides; i--;)
|
|
3165
|
+
{
|
|
3166
|
+
const a = (i/sides)*PI*2;
|
|
3167
|
+
points.push(vec2(Math.sin(a)*sizeX, Math.cos(a)*sizeY));
|
|
3168
|
+
}
|
|
3169
|
+
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3011
3172
|
/** Draw colored polygon using passed in points
|
|
3012
3173
|
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
3013
3174
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -3015,82 +3176,110 @@ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL
|
|
|
3015
3176
|
* @param {Color} [lineColor=(0,0,0,1)]
|
|
3016
3177
|
* @param {Vector2} [pos=(0,0)] - Offset to apply
|
|
3017
3178
|
* @param {number} [angle] - Angle to rotate by
|
|
3018
|
-
* @param {boolean} [useWebGL]
|
|
3179
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3019
3180
|
* @param {boolean} [screenSpace]
|
|
3020
|
-
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context
|
|
3181
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3021
3182
|
* @memberof Draw */
|
|
3022
|
-
function drawPoly(points, color=
|
|
3183
|
+
function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
|
|
3023
3184
|
{
|
|
3024
|
-
ASSERT(isVector2(pos)
|
|
3185
|
+
ASSERT(isVector2(pos), 'drawPoly pos should be a vec2');
|
|
3025
3186
|
ASSERT(Array.isArray(points), 'drawPoly points should be an array');
|
|
3026
3187
|
ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
|
|
3027
3188
|
ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
|
|
3028
3189
|
ASSERT(isNumber(angle), 'drawPoly angle should be a number');
|
|
3029
|
-
ASSERT(!useWebGL, '
|
|
3030
|
-
|
|
3190
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3191
|
+
if (useWebGL)
|
|
3031
3192
|
{
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
context.lineTo(point.x, point.y);
|
|
3035
|
-
context.closePath();
|
|
3036
|
-
context.fillStyle = color.toString();
|
|
3037
|
-
context.fill();
|
|
3038
|
-
if (lineWidth)
|
|
3193
|
+
let scale = 1;
|
|
3194
|
+
if (screenSpace)
|
|
3039
3195
|
{
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3196
|
+
// convert to world space
|
|
3197
|
+
pos = screenToWorld(pos);
|
|
3198
|
+
scale = 1/cameraScale;
|
|
3043
3199
|
}
|
|
3044
|
-
|
|
3200
|
+
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
|
|
3201
|
+
if (lineWidth > 0)
|
|
3202
|
+
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, scale, scale, angle);
|
|
3203
|
+
}
|
|
3204
|
+
else
|
|
3205
|
+
{
|
|
3206
|
+
drawCanvas2D(pos, vec2(1), angle, false, context=>
|
|
3207
|
+
{
|
|
3208
|
+
context.fillStyle = color.toString();
|
|
3209
|
+
context.beginPath();
|
|
3210
|
+
for (const point of points)
|
|
3211
|
+
context.lineTo(point.x, point.y);
|
|
3212
|
+
context.closePath();
|
|
3213
|
+
context.fill();
|
|
3214
|
+
if (lineWidth)
|
|
3215
|
+
{
|
|
3216
|
+
context.strokeStyle = lineColor.toString();
|
|
3217
|
+
context.lineWidth = lineWidth;
|
|
3218
|
+
context.stroke();
|
|
3219
|
+
}
|
|
3220
|
+
}, screenSpace, context);
|
|
3221
|
+
}
|
|
3045
3222
|
}
|
|
3046
3223
|
|
|
3047
3224
|
/** Draw colored ellipse using passed in point
|
|
3048
3225
|
* @param {Vector2} pos
|
|
3049
|
-
* @param {Vector2} [size=(1,1)]
|
|
3226
|
+
* @param {Vector2} [size=(1,1)] - Width and height diameter
|
|
3050
3227
|
* @param {Color} [color=(1,1,1,1)]
|
|
3051
3228
|
* @param {number} [angle]
|
|
3052
3229
|
* @param {number} [lineWidth]
|
|
3053
3230
|
* @param {Color} [lineColor=(0,0,0,1)]
|
|
3054
|
-
* @param {boolean} [useWebGL]
|
|
3231
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3055
3232
|
* @param {boolean} [screenSpace]
|
|
3056
|
-
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context
|
|
3233
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3057
3234
|
* @memberof Draw */
|
|
3058
|
-
function drawEllipse(pos, size=vec2(1), color=
|
|
3235
|
+
function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
|
|
3059
3236
|
{
|
|
3060
|
-
ASSERT(isVector2(pos)
|
|
3061
|
-
ASSERT(isVector2(size)
|
|
3237
|
+
ASSERT(isVector2(pos), 'drawEllipse pos should be a vec2');
|
|
3238
|
+
ASSERT(isVector2(size), 'drawEllipse size should be a vec2');
|
|
3062
3239
|
ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
|
|
3063
3240
|
ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
|
|
3064
3241
|
ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
|
|
3065
|
-
ASSERT(lineWidth>=0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
|
|
3066
|
-
ASSERT(!useWebGL, '
|
|
3067
|
-
|
|
3242
|
+
ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
|
|
3243
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3244
|
+
if (useWebGL)
|
|
3068
3245
|
{
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3246
|
+
// draw as a regular polygon
|
|
3247
|
+
const sides = glCircleSides;
|
|
3248
|
+
drawRegularPoly(pos, size, sides, color, lineWidth, lineColor, angle, useWebGL, screenSpace, context);
|
|
3249
|
+
}
|
|
3250
|
+
else
|
|
3251
|
+
{
|
|
3252
|
+
drawCanvas2D(pos, vec2(1), angle, false, context=>
|
|
3074
3253
|
{
|
|
3075
|
-
context.
|
|
3076
|
-
context.
|
|
3077
|
-
context.
|
|
3078
|
-
|
|
3079
|
-
|
|
3254
|
+
context.fillStyle = color.toString();
|
|
3255
|
+
context.beginPath();
|
|
3256
|
+
context.ellipse(0, 0, size.x/2, size.y/2, 0, 0, 9);
|
|
3257
|
+
context.fill();
|
|
3258
|
+
if (lineWidth)
|
|
3259
|
+
{
|
|
3260
|
+
context.strokeStyle = lineColor.toString();
|
|
3261
|
+
context.lineWidth = lineWidth;
|
|
3262
|
+
context.stroke();
|
|
3263
|
+
}
|
|
3264
|
+
}, screenSpace, context);
|
|
3265
|
+
}
|
|
3080
3266
|
}
|
|
3081
3267
|
|
|
3082
3268
|
/** Draw colored circle using passed in point
|
|
3083
3269
|
* @param {Vector2} pos
|
|
3084
|
-
* @param {number} [
|
|
3270
|
+
* @param {number} [size=1] - Diameter
|
|
3085
3271
|
* @param {Color} [color=(1,1,1,1)]
|
|
3086
3272
|
* @param {number} [lineWidth=0]
|
|
3087
3273
|
* @param {Color} [lineColor=(0,0,0,1)]
|
|
3088
|
-
* @param {boolean} [useWebGL]
|
|
3089
|
-
* @param {boolean} [screenSpace
|
|
3090
|
-
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context
|
|
3274
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3275
|
+
* @param {boolean} [screenSpace]
|
|
3276
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3091
3277
|
* @memberof Draw */
|
|
3092
|
-
function drawCircle(pos,
|
|
3093
|
-
{
|
|
3278
|
+
function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
|
|
3279
|
+
{
|
|
3280
|
+
ASSERT(isNumber(size), 'drawCircle size should be a number');
|
|
3281
|
+
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
3282
|
+
}
|
|
3094
3283
|
|
|
3095
3284
|
/** Draw directly to a 2d canvas context in world space
|
|
3096
3285
|
* @param {Vector2} pos
|
|
@@ -3098,7 +3287,7 @@ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=BLACK
|
|
|
3098
3287
|
* @param {number} angle
|
|
3099
3288
|
* @param {boolean} [mirror]
|
|
3100
3289
|
* @param {Function} [drawFunction]
|
|
3101
|
-
* @param {boolean}
|
|
3290
|
+
* @param {boolean} [screenSpace=false]
|
|
3102
3291
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
3103
3292
|
* @memberof Draw */
|
|
3104
3293
|
function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
|
|
@@ -3168,7 +3357,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
3168
3357
|
* @param {number} [maxWidth]
|
|
3169
3358
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
|
|
3170
3359
|
* @memberof Draw */
|
|
3171
|
-
function drawTextScreen(text, pos, size=1, color=
|
|
3360
|
+
function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
|
|
3172
3361
|
{
|
|
3173
3362
|
context.fillStyle = color.toString();
|
|
3174
3363
|
context.strokeStyle = lineColor.toString();
|
|
@@ -3176,7 +3365,6 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
|
|
|
3176
3365
|
context.textAlign = textAlign;
|
|
3177
3366
|
context.font = size + 'px '+ font;
|
|
3178
3367
|
context.textBaseline = 'middle';
|
|
3179
|
-
context.lineJoin = 'round';
|
|
3180
3368
|
|
|
3181
3369
|
const lines = (text+'').split('\n');
|
|
3182
3370
|
let posY = pos.y;
|
|
@@ -3200,7 +3388,7 @@ function screenToWorld(screenPos)
|
|
|
3200
3388
|
{
|
|
3201
3389
|
let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
|
|
3202
3390
|
let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
|
|
3203
|
-
if (cameraAngle)
|
|
3391
|
+
if (cameraAngle)
|
|
3204
3392
|
{
|
|
3205
3393
|
// apply camera rotation
|
|
3206
3394
|
const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
|
|
@@ -3293,7 +3481,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
3293
3481
|
{
|
|
3294
3482
|
// white texture with no additive alpha, no need to tint
|
|
3295
3483
|
context.globalAlpha = color.a;
|
|
3296
|
-
context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3484
|
+
context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3297
3485
|
context.globalAlpha = 1;
|
|
3298
3486
|
}
|
|
3299
3487
|
else
|
|
@@ -3314,7 +3502,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
3314
3502
|
for (let i = 0; i < data.length; ++i)
|
|
3315
3503
|
data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
|
|
3316
3504
|
workContext.putImageData(imageData, 0, 0);
|
|
3317
|
-
context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3505
|
+
context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3318
3506
|
}
|
|
3319
3507
|
else
|
|
3320
3508
|
{
|
|
@@ -3327,7 +3515,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
3327
3515
|
}
|
|
3328
3516
|
workContext.putImageData(imageData, 0, 0);
|
|
3329
3517
|
context.globalAlpha = color.a;
|
|
3330
|
-
context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3518
|
+
context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3331
3519
|
context.globalAlpha = 1;
|
|
3332
3520
|
}
|
|
3333
3521
|
}
|
|
@@ -3354,7 +3542,7 @@ function toggleFullscreen()
|
|
|
3354
3542
|
}
|
|
3355
3543
|
|
|
3356
3544
|
/** Set the cursor style
|
|
3357
|
-
* @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
|
|
3545
|
+
* @param {string} [cursorStyle] - CSS cursor style (auto, none, crosshair, etc)
|
|
3358
3546
|
* @memberof Draw */
|
|
3359
3547
|
function setCursor(cursorStyle = 'auto')
|
|
3360
3548
|
{
|
|
@@ -3366,7 +3554,7 @@ function setCursor(cursorStyle = 'auto')
|
|
|
3366
3554
|
|
|
3367
3555
|
let engineFontImage;
|
|
3368
3556
|
|
|
3369
|
-
/**
|
|
3557
|
+
/**
|
|
3370
3558
|
* Font Image Object - Draw text on a 2D canvas by using characters in an image
|
|
3371
3559
|
* - 96 characters (from space to tilde) are stored in an image
|
|
3372
3560
|
* - Uses a default 8x8 font if none is supplied
|
|
@@ -3374,9 +3562,9 @@ let engineFontImage;
|
|
|
3374
3562
|
* @example
|
|
3375
3563
|
* // use built in font
|
|
3376
3564
|
* const font = new FontImage;
|
|
3377
|
-
*
|
|
3565
|
+
*
|
|
3378
3566
|
* // draw text
|
|
3379
|
-
* font.drawTextScreen(
|
|
3567
|
+
* font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
|
|
3380
3568
|
*/
|
|
3381
3569
|
class FontImage
|
|
3382
3570
|
{
|
|
@@ -3384,7 +3572,6 @@ class FontImage
|
|
|
3384
3572
|
* @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
|
|
3385
3573
|
* @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
|
|
3386
3574
|
* @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
|
|
3387
|
-
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
|
|
3388
3575
|
*/
|
|
3389
3576
|
constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
|
|
3390
3577
|
{
|
|
@@ -3398,7 +3585,6 @@ class FontImage
|
|
|
3398
3585
|
this.image = image || engineFontImage;
|
|
3399
3586
|
this.tileSize = tileSize;
|
|
3400
3587
|
this.paddingSize = paddingSize;
|
|
3401
|
-
this.context = context;
|
|
3402
3588
|
}
|
|
3403
3589
|
|
|
3404
3590
|
/** Draw text in world space using the image font
|
|
@@ -3406,23 +3592,32 @@ class FontImage
|
|
|
3406
3592
|
* @param {Vector2} pos
|
|
3407
3593
|
* @param {number} [scale=.25]
|
|
3408
3594
|
* @param {boolean} [center]
|
|
3595
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}[context=drawContext]
|
|
3409
3596
|
*/
|
|
3410
|
-
drawText(text, pos, scale=1, center)
|
|
3597
|
+
drawText(text, pos, scale=1, center, context=drawContext)
|
|
3411
3598
|
{
|
|
3412
|
-
this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
|
|
3599
|
+
this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
|
|
3413
3600
|
}
|
|
3414
3601
|
|
|
3415
|
-
/** Draw text in
|
|
3602
|
+
/** Draw text on overlay canvas in world space using the image font
|
|
3603
|
+
* @param {string} text
|
|
3604
|
+
* @param {Vector2} pos
|
|
3605
|
+
* @param {number} [scale]
|
|
3606
|
+
* @param {boolean} [center]
|
|
3607
|
+
*/
|
|
3608
|
+
drawTextOverlay(text, pos, scale=4, center)
|
|
3609
|
+
{ this.drawText(text, pos, scale, center, overlayContext); }
|
|
3610
|
+
|
|
3611
|
+
/** Draw text on overlay canvas in screen space using the image font
|
|
3416
3612
|
* @param {string} text
|
|
3417
3613
|
* @param {Vector2} pos
|
|
3418
3614
|
* @param {number} [scale]
|
|
3419
3615
|
* @param {boolean} [center]
|
|
3616
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
3420
3617
|
*/
|
|
3421
|
-
drawTextScreen(text, pos, scale=4, center)
|
|
3618
|
+
drawTextScreen(text, pos, scale=4, center, context=overlayContext)
|
|
3422
3619
|
{
|
|
3423
|
-
const context = this.context;
|
|
3424
3620
|
context.save();
|
|
3425
|
-
|
|
3426
3621
|
const size = this.tileSize;
|
|
3427
3622
|
const drawSize = size.add(this.paddingSize).scale(scale);
|
|
3428
3623
|
const cols = this.image.width / this.tileSize.x |0;
|
|
@@ -3441,15 +3636,14 @@ class FontImage
|
|
|
3441
3636
|
const x = tile % cols;
|
|
3442
3637
|
const y = tile / cols |0;
|
|
3443
3638
|
const drawPos = pos.add(vec2(j,i).multiply(drawSize));
|
|
3444
|
-
context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
|
|
3639
|
+
context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
|
|
3445
3640
|
drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
|
|
3446
3641
|
}
|
|
3447
3642
|
});
|
|
3448
|
-
|
|
3449
3643
|
context.restore();
|
|
3450
3644
|
}
|
|
3451
3645
|
}
|
|
3452
|
-
/**
|
|
3646
|
+
/**
|
|
3453
3647
|
* LittleJS Input System
|
|
3454
3648
|
* - Tracks keyboard down, pressed, and released
|
|
3455
3649
|
* - Tracks mouse buttons, position, and wheel
|
|
@@ -3465,10 +3659,10 @@ class FontImage
|
|
|
3465
3659
|
* @return {boolean}
|
|
3466
3660
|
* @memberof Input */
|
|
3467
3661
|
function keyIsDown(key, device=0)
|
|
3468
|
-
{
|
|
3662
|
+
{
|
|
3469
3663
|
ASSERT(key !== undefined, 'key is undefined');
|
|
3470
|
-
ASSERT(device > 0 || typeof key
|
|
3471
|
-
return inputData[device] && !!(inputData[device][key] & 1);
|
|
3664
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
3665
|
+
return inputData[device] && !!(inputData[device][key] & 1);
|
|
3472
3666
|
}
|
|
3473
3667
|
|
|
3474
3668
|
/** Returns true if device key was pressed this frame
|
|
@@ -3477,10 +3671,10 @@ function keyIsDown(key, device=0)
|
|
|
3477
3671
|
* @return {boolean}
|
|
3478
3672
|
* @memberof Input */
|
|
3479
3673
|
function keyWasPressed(key, device=0)
|
|
3480
|
-
{
|
|
3674
|
+
{
|
|
3481
3675
|
ASSERT(key !== undefined, 'key is undefined');
|
|
3482
|
-
ASSERT(device > 0 || typeof key
|
|
3483
|
-
return inputData[device] && !!(inputData[device][key] & 2);
|
|
3676
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
3677
|
+
return inputData[device] && !!(inputData[device][key] & 2);
|
|
3484
3678
|
}
|
|
3485
3679
|
|
|
3486
3680
|
/** Returns true if device key was released this frame
|
|
@@ -3489,9 +3683,9 @@ function keyWasPressed(key, device=0)
|
|
|
3489
3683
|
* @return {boolean}
|
|
3490
3684
|
* @memberof Input */
|
|
3491
3685
|
function keyWasReleased(key, device=0)
|
|
3492
|
-
{
|
|
3686
|
+
{
|
|
3493
3687
|
ASSERT(key !== undefined, 'key is undefined');
|
|
3494
|
-
ASSERT(device > 0 || typeof key
|
|
3688
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
3495
3689
|
return inputData[device] && !!(inputData[device][key] & 4);
|
|
3496
3690
|
}
|
|
3497
3691
|
|
|
@@ -3613,7 +3807,7 @@ function gamepadWasReleased(button, gamepad=0)
|
|
|
3613
3807
|
* @param {number} [gamepad]
|
|
3614
3808
|
* @return {Vector2}
|
|
3615
3809
|
* @memberof Input */
|
|
3616
|
-
function gamepadStick(stick,
|
|
3810
|
+
function gamepadStick(stick, gamepad=0)
|
|
3617
3811
|
{ return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
|
|
3618
3812
|
|
|
3619
3813
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -3690,10 +3884,10 @@ function inputInit()
|
|
|
3690
3884
|
{
|
|
3691
3885
|
// handle remapping wasd keys to directions
|
|
3692
3886
|
return inputWASDEmulateDirection ?
|
|
3693
|
-
c
|
|
3694
|
-
c
|
|
3695
|
-
c
|
|
3696
|
-
c
|
|
3887
|
+
c === 'KeyW' ? 'ArrowUp' :
|
|
3888
|
+
c === 'KeyS' ? 'ArrowDown' :
|
|
3889
|
+
c === 'KeyA' ? 'ArrowLeft' :
|
|
3890
|
+
c === 'KeyD' ? 'ArrowRight' : c : c;
|
|
3697
3891
|
}
|
|
3698
3892
|
function onMouseDown(e)
|
|
3699
3893
|
{
|
|
@@ -3701,9 +3895,9 @@ function inputInit()
|
|
|
3701
3895
|
return;
|
|
3702
3896
|
|
|
3703
3897
|
// fix stalled audio requiring user interaction
|
|
3704
|
-
if (soundEnable && !headlessMode && audioContext &&
|
|
3898
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
3705
3899
|
audioContext.resume();
|
|
3706
|
-
|
|
3900
|
+
|
|
3707
3901
|
isUsingGamepad = false;
|
|
3708
3902
|
inputData[0][e.button] = 3;
|
|
3709
3903
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
@@ -3746,8 +3940,8 @@ function gamepadsUpdate()
|
|
|
3746
3940
|
const applyDeadZones = (v)=>
|
|
3747
3941
|
{
|
|
3748
3942
|
const min=.3, max=.8;
|
|
3749
|
-
const deadZone = (v)=>
|
|
3750
|
-
v >
|
|
3943
|
+
const deadZone = (v)=>
|
|
3944
|
+
v > min ? percent(v, min, max) :
|
|
3751
3945
|
v < -min ? -percent(-v, min, max) : 0;
|
|
3752
3946
|
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
3753
3947
|
}
|
|
@@ -3755,30 +3949,29 @@ function gamepadsUpdate()
|
|
|
3755
3949
|
// update touch gamepad if enabled
|
|
3756
3950
|
if (touchGamepadEnable && isTouchDevice)
|
|
3757
3951
|
{
|
|
3758
|
-
|
|
3759
|
-
|
|
3952
|
+
if (!touchGamepadTimer.isSet())
|
|
3953
|
+
return;
|
|
3954
|
+
|
|
3955
|
+
// read virtual analog stick
|
|
3956
|
+
const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
|
|
3957
|
+
sticks[0] = vec2();
|
|
3958
|
+
if (touchGamepadAnalog)
|
|
3959
|
+
sticks[0] = applyDeadZones(touchGamepadStick);
|
|
3960
|
+
else if (touchGamepadStick.lengthSquared() > .3)
|
|
3760
3961
|
{
|
|
3761
|
-
//
|
|
3762
|
-
|
|
3763
|
-
sticks[0] =
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
else if (touchGamepadStick.lengthSquared() > .3)
|
|
3767
|
-
{
|
|
3768
|
-
// convert to 8 way dpad
|
|
3769
|
-
sticks[0].x = Math.round(touchGamepadStick.x);
|
|
3770
|
-
sticks[0].y = -Math.round(touchGamepadStick.y);
|
|
3771
|
-
sticks[0] = sticks[0].clampLength();
|
|
3772
|
-
}
|
|
3962
|
+
// convert to 8 way dpad
|
|
3963
|
+
sticks[0].x = Math.round(touchGamepadStick.x);
|
|
3964
|
+
sticks[0].y = -Math.round(touchGamepadStick.y);
|
|
3965
|
+
sticks[0] = sticks[0].clampLength();
|
|
3966
|
+
}
|
|
3773
3967
|
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
}
|
|
3968
|
+
// read virtual gamepad buttons
|
|
3969
|
+
const data = inputData[1] || (inputData[1] = []);
|
|
3970
|
+
for (let i=10; i--;)
|
|
3971
|
+
{
|
|
3972
|
+
const j = i === 3 ? 2 : i === 2 ? 3 : i; // fix button locations
|
|
3973
|
+
const wasDown = gamepadIsDown(j,0);
|
|
3974
|
+
data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
3782
3975
|
}
|
|
3783
3976
|
}
|
|
3784
3977
|
|
|
@@ -3804,7 +3997,7 @@ function gamepadsUpdate()
|
|
|
3804
3997
|
// read analog sticks
|
|
3805
3998
|
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
3806
3999
|
sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
|
|
3807
|
-
|
|
4000
|
+
|
|
3808
4001
|
// read buttons
|
|
3809
4002
|
for (let j = gamepad.buttons.length; j--;)
|
|
3810
4003
|
{
|
|
@@ -3820,14 +4013,14 @@ function gamepadsUpdate()
|
|
|
3820
4013
|
{
|
|
3821
4014
|
// copy dpad to left analog stick when pressed
|
|
3822
4015
|
const dpad = vec2(
|
|
3823
|
-
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
4016
|
+
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
3824
4017
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
3825
4018
|
if (dpad.lengthSquared())
|
|
3826
4019
|
sticks[0] = dpad.clampLength();
|
|
3827
4020
|
}
|
|
3828
4021
|
|
|
3829
4022
|
// disable touch gamepad if using real gamepad
|
|
3830
|
-
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
4023
|
+
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
3831
4024
|
}
|
|
3832
4025
|
}
|
|
3833
4026
|
}
|
|
@@ -3852,20 +4045,13 @@ function vibrateStop() { vibrate(0); }
|
|
|
3852
4045
|
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
3853
4046
|
|
|
3854
4047
|
// touch gamepad internal variables
|
|
3855
|
-
let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
|
|
4048
|
+
let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
|
|
3856
4049
|
|
|
3857
4050
|
// enable touch input mouse passthrough
|
|
3858
4051
|
function touchInputInit()
|
|
3859
4052
|
{
|
|
3860
4053
|
// add non passive touch event listeners
|
|
3861
4054
|
let handleTouch = handleTouchDefault;
|
|
3862
|
-
if (touchGamepadEnable)
|
|
3863
|
-
{
|
|
3864
|
-
// touch input internal variables
|
|
3865
|
-
handleTouch = handleTouchGamepad;
|
|
3866
|
-
touchGamepadButtons = [];
|
|
3867
|
-
touchGamepadStick = vec2();
|
|
3868
|
-
}
|
|
3869
4055
|
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
3870
4056
|
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
3871
4057
|
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
@@ -3874,8 +4060,15 @@ function touchInputInit()
|
|
|
3874
4060
|
let wasTouching;
|
|
3875
4061
|
function handleTouchDefault(e)
|
|
3876
4062
|
{
|
|
4063
|
+
if (!touchInputEnable)
|
|
4064
|
+
return;
|
|
4065
|
+
|
|
4066
|
+
// route touch to gamepad
|
|
4067
|
+
if (touchGamepadEnable)
|
|
4068
|
+
handleTouchGamepad(e);
|
|
4069
|
+
|
|
3877
4070
|
// fix stalled audio requiring user interaction
|
|
3878
|
-
if (soundEnable && !headlessMode && audioContext &&
|
|
4071
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
3879
4072
|
audioContext.resume();
|
|
3880
4073
|
|
|
3881
4074
|
// check if touching and pass to mouse events
|
|
@@ -3904,7 +4097,7 @@ function touchInputInit()
|
|
|
3904
4097
|
// prevent default handling like copy and magnifier lens
|
|
3905
4098
|
if (inputPreventDefault && document.hasFocus()) // allow document to get focus
|
|
3906
4099
|
e.preventDefault();
|
|
3907
|
-
|
|
4100
|
+
|
|
3908
4101
|
// must return true so the document will get focus
|
|
3909
4102
|
return true;
|
|
3910
4103
|
}
|
|
@@ -3916,7 +4109,7 @@ function touchInputInit()
|
|
|
3916
4109
|
touchGamepadStick = vec2();
|
|
3917
4110
|
touchGamepadButtons = [];
|
|
3918
4111
|
isUsingGamepad = true;
|
|
3919
|
-
|
|
4112
|
+
|
|
3920
4113
|
const touching = e.touches.length;
|
|
3921
4114
|
if (touching)
|
|
3922
4115
|
{
|
|
@@ -3925,9 +4118,6 @@ function touchInputInit()
|
|
|
3925
4118
|
{
|
|
3926
4119
|
// touch anywhere to press start when paused
|
|
3927
4120
|
touchGamepadButtons[9] = 1;
|
|
3928
|
-
|
|
3929
|
-
// call default touch handler so normal touch events still work
|
|
3930
|
-
handleTouchDefault(e);
|
|
3931
4121
|
return;
|
|
3932
4122
|
}
|
|
3933
4123
|
}
|
|
@@ -3958,12 +4148,6 @@ function touchInputInit()
|
|
|
3958
4148
|
touchGamepadButtons[9] = 1;
|
|
3959
4149
|
}
|
|
3960
4150
|
}
|
|
3961
|
-
|
|
3962
|
-
// call default touch handler so normal touch events still work
|
|
3963
|
-
handleTouchDefault(e);
|
|
3964
|
-
|
|
3965
|
-
// must return true so the document will get focus
|
|
3966
|
-
return true;
|
|
3967
4151
|
}
|
|
3968
4152
|
}
|
|
3969
4153
|
|
|
@@ -3973,7 +4157,7 @@ function touchGamepadRender()
|
|
|
3973
4157
|
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
3974
4158
|
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
3975
4159
|
return;
|
|
3976
|
-
|
|
4160
|
+
|
|
3977
4161
|
// fade off when not touching or paused
|
|
3978
4162
|
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
3979
4163
|
if (!alpha || paused)
|
|
@@ -4004,11 +4188,11 @@ function touchGamepadRender()
|
|
|
4004
4188
|
const angle = i*PI/4;
|
|
4005
4189
|
context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
|
|
4006
4190
|
i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
|
|
4007
|
-
i
|
|
4191
|
+
i===1 && context.fill();
|
|
4008
4192
|
}
|
|
4009
4193
|
context.stroke();
|
|
4010
4194
|
}
|
|
4011
|
-
|
|
4195
|
+
|
|
4012
4196
|
// draw right face buttons
|
|
4013
4197
|
const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4014
4198
|
for (let i=4; i--;)
|
|
@@ -4043,8 +4227,8 @@ function pointerLockExit() { document.exitPointerLock && document.exitPointerLoc
|
|
|
4043
4227
|
/** Check if pointer is locked (true if locked)
|
|
4044
4228
|
* @return {boolean}
|
|
4045
4229
|
* @memberof Input */
|
|
4046
|
-
function pointerLockIsActive() { return document.pointerLockElement
|
|
4047
|
-
/**
|
|
4230
|
+
function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
|
|
4231
|
+
/**
|
|
4048
4232
|
* LittleJS Audio System
|
|
4049
4233
|
* - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
|
|
4050
4234
|
* - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
|
|
@@ -4065,10 +4249,21 @@ let audioContext = new AudioContext;
|
|
|
4065
4249
|
* @memberof Audio */
|
|
4066
4250
|
let audioMasterGain;
|
|
4067
4251
|
|
|
4252
|
+
/** Default sample rate used for sounds
|
|
4253
|
+
* @default 44100
|
|
4254
|
+
* @memberof Audio */
|
|
4255
|
+
const audioDefaultSampleRate = 44100;
|
|
4256
|
+
|
|
4257
|
+
/** Check if the audio context is running and available for playback
|
|
4258
|
+
* @return {boolean} - True if the audio context is running
|
|
4259
|
+
* @memberof Audio */
|
|
4260
|
+
function audioIsRunning()
|
|
4261
|
+
{ return audioContext.state === 'running'; }
|
|
4262
|
+
|
|
4068
4263
|
function audioInit()
|
|
4069
4264
|
{
|
|
4070
4265
|
if (!soundEnable || headlessMode) return;
|
|
4071
|
-
|
|
4266
|
+
|
|
4072
4267
|
audioMasterGain = audioContext.createGain();
|
|
4073
4268
|
audioMasterGain.connect(audioContext.destination);
|
|
4074
4269
|
audioMasterGain.gain.value = soundVolume; // set starting value
|
|
@@ -4076,14 +4271,14 @@ function audioInit()
|
|
|
4076
4271
|
|
|
4077
4272
|
///////////////////////////////////////////////////////////////////////////////
|
|
4078
4273
|
|
|
4079
|
-
/**
|
|
4274
|
+
/**
|
|
4080
4275
|
* Sound Object - Stores a sound for later use and can be played positionally
|
|
4081
|
-
*
|
|
4276
|
+
*
|
|
4082
4277
|
* <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
|
|
4083
4278
|
* @example
|
|
4084
4279
|
* // create a sound
|
|
4085
4280
|
* const sound_example = new Sound([.5,.5]);
|
|
4086
|
-
*
|
|
4281
|
+
*
|
|
4087
4282
|
* // play the sound
|
|
4088
4283
|
* sound_example.play();
|
|
4089
4284
|
*/
|
|
@@ -4100,33 +4295,41 @@ class Sound
|
|
|
4100
4295
|
|
|
4101
4296
|
/** @property {number} - World space max range of sound */
|
|
4102
4297
|
this.range = range;
|
|
4103
|
-
|
|
4104
4298
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
4105
4299
|
this.taper = taper;
|
|
4106
|
-
|
|
4107
4300
|
/** @property {number} - How much to randomize frequency each time sound plays */
|
|
4108
4301
|
this.randomness = 0;
|
|
4302
|
+
/** @property {number} - Sample rate for this sound */
|
|
4303
|
+
this.sampleRate = audioDefaultSampleRate;
|
|
4304
|
+
/** @property {number} - Percentage of this sound currently loaded */
|
|
4305
|
+
this.loadedPercent = 0;
|
|
4109
4306
|
|
|
4307
|
+
// generate zzfx sound now for fast playback
|
|
4110
4308
|
if (zzfxSound)
|
|
4111
4309
|
{
|
|
4112
|
-
//
|
|
4113
|
-
const defaultRandomness = .05;
|
|
4114
|
-
this.randomness = zzfxSound[
|
|
4115
|
-
|
|
4310
|
+
// remove randomness so it can be applied on playback
|
|
4311
|
+
const randomnessIndex = 1, defaultRandomness = .05;
|
|
4312
|
+
this.randomness = zzfxSound[randomnessIndex] !== undefined ?
|
|
4313
|
+
zzfxSound[randomnessIndex] : defaultRandomness;
|
|
4314
|
+
zzfxSound[randomnessIndex] = 0;
|
|
4315
|
+
|
|
4316
|
+
// generate the zzfx samples
|
|
4116
4317
|
this.sampleChannels = [zzfxG(...zzfxSound)];
|
|
4117
|
-
this.
|
|
4318
|
+
this.loadedPercent = 1;
|
|
4118
4319
|
}
|
|
4119
4320
|
}
|
|
4120
4321
|
|
|
4121
4322
|
/** Play the sound
|
|
4122
|
-
*
|
|
4123
|
-
* @param {
|
|
4124
|
-
* @param {number} [
|
|
4125
|
-
* @param {number} [
|
|
4126
|
-
* @param {
|
|
4127
|
-
* @
|
|
4323
|
+
* Sounds may not play until a user interaction occurs
|
|
4324
|
+
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
4325
|
+
* @param {number} [volume] - How much to scale volume by
|
|
4326
|
+
* @param {number} [pitch] - How much to scale pitch by
|
|
4327
|
+
* @param {number} [randomnessScale] - How much to scale pitch randomness
|
|
4328
|
+
* @param {boolean} [loop] - Should the sound loop?
|
|
4329
|
+
* @param {boolean} [paused] - Should the sound start paused
|
|
4330
|
+
* @return {SoundInstance} - The audio source node
|
|
4128
4331
|
*/
|
|
4129
|
-
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
|
|
4332
|
+
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
|
|
4130
4333
|
{
|
|
4131
4334
|
if (!soundEnable || headlessMode) return;
|
|
4132
4335
|
if (!this.sampleChannels) return;
|
|
@@ -4149,75 +4352,55 @@ class Sound
|
|
|
4149
4352
|
// get pan from screen space coords
|
|
4150
4353
|
pan = worldToScreen(pos).x * 2/mainCanvas.width - 1;
|
|
4151
4354
|
}
|
|
4152
|
-
|
|
4153
|
-
// play the sound
|
|
4154
|
-
const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
|
|
4155
|
-
this.gainNode = audioContext.createGain();
|
|
4156
|
-
this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
|
|
4157
|
-
return this.source;
|
|
4158
|
-
}
|
|
4159
|
-
|
|
4160
|
-
/** Set the sound volume of the most recently played instance of this sound
|
|
4161
|
-
* @param {number} [volume] - How much to scale volume by
|
|
4162
|
-
*/
|
|
4163
|
-
setVolume(volume=1)
|
|
4164
|
-
{
|
|
4165
|
-
if (this.gainNode)
|
|
4166
|
-
this.gainNode.gain.value = volume;
|
|
4167
|
-
}
|
|
4168
|
-
|
|
4169
|
-
/** Stop the last instance of this sound that was played
|
|
4170
|
-
* @param {number} [fadeTime] - How long to fade out (seconds)
|
|
4171
|
-
*/
|
|
4172
|
-
stop(fadeTime=0)
|
|
4173
|
-
{
|
|
4174
|
-
if (!this.source)
|
|
4175
|
-
return;
|
|
4176
4355
|
|
|
4177
|
-
//
|
|
4178
|
-
const
|
|
4179
|
-
|
|
4180
|
-
this.gainNode.gain.linearRampToValueAtTime(1, startFade);
|
|
4181
|
-
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
4182
|
-
this.source.stop(endFade);
|
|
4183
|
-
this.source = undefined;
|
|
4356
|
+
// Create and return sound instance
|
|
4357
|
+
const rate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
|
|
4358
|
+
return new SoundInstance(this, volume, rate, pan, loop, paused);
|
|
4184
4359
|
}
|
|
4185
4360
|
|
|
4186
|
-
/**
|
|
4187
|
-
* @
|
|
4361
|
+
/** Play a music track that loops by default
|
|
4362
|
+
* @param {number} [volume] - Volume to play the music at
|
|
4363
|
+
* @param {boolean} [loop] - Should the music loop?
|
|
4364
|
+
* @param {boolean} [paused] - Should the music start paused
|
|
4365
|
+
* @return {SoundInstance} - The audio source node
|
|
4188
4366
|
*/
|
|
4189
|
-
|
|
4367
|
+
playMusic(volume=1, loop=true, paused=false)
|
|
4368
|
+
{ return this.play(undefined, volume, 1, 0, loop, paused); }
|
|
4190
4369
|
|
|
4191
|
-
/** Play the sound as a note with a semitone offset
|
|
4370
|
+
/** Play the sound as a musical note with a semitone offset
|
|
4371
|
+
* This can be used to play music with chromatic scales
|
|
4192
4372
|
* @param {number} semitoneOffset - How many semitones to offset pitch
|
|
4193
|
-
* @param {Vector2} [pos] - World space position to play the sound
|
|
4194
|
-
* @param {number} [volume=1] - How much to scale volume by
|
|
4195
|
-
* @return {
|
|
4373
|
+
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
4374
|
+
* @param {number} [volume=1] - How much to scale volume by
|
|
4375
|
+
* @return {SoundInstance} - The audio source node
|
|
4196
4376
|
*/
|
|
4197
4377
|
playNote(semitoneOffset, pos, volume)
|
|
4198
|
-
{
|
|
4378
|
+
{
|
|
4379
|
+
const pitch = getNoteFrequency(semitoneOffset, 1);
|
|
4380
|
+
return this.play(pos, volume, pitch, 0);
|
|
4381
|
+
}
|
|
4199
4382
|
|
|
4200
4383
|
/** Get how long this sound is in seconds
|
|
4201
4384
|
* @return {number} - How long the sound is in seconds (undefined if loading)
|
|
4202
4385
|
*/
|
|
4203
|
-
getDuration()
|
|
4386
|
+
getDuration()
|
|
4204
4387
|
{ return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
|
|
4205
|
-
|
|
4206
|
-
/** Check if sound is
|
|
4207
|
-
* @return {boolean} - True if sound is
|
|
4388
|
+
|
|
4389
|
+
/** Check if sound is loaded, for sounds fetched from a url
|
|
4390
|
+
* @return {boolean} - True if sound is loaded and ready to play
|
|
4208
4391
|
*/
|
|
4209
|
-
|
|
4392
|
+
isLoaded() { return this.loadedPercent === 1; }
|
|
4210
4393
|
}
|
|
4211
4394
|
|
|
4212
4395
|
///////////////////////////////////////////////////////////////////////////////
|
|
4213
4396
|
|
|
4214
|
-
/**
|
|
4397
|
+
/**
|
|
4215
4398
|
* Sound Wave Object - Stores a wave sound for later use and can be played positionally
|
|
4216
4399
|
* - this can be used to play wave, mp3, and ogg files
|
|
4217
4400
|
* @example
|
|
4218
4401
|
* // create a sound
|
|
4219
4402
|
* const sound_example = new SoundWave('sound.mp3');
|
|
4220
|
-
*
|
|
4403
|
+
*
|
|
4221
4404
|
* // play the sound
|
|
4222
4405
|
* sound_example.play();
|
|
4223
4406
|
*/
|
|
@@ -4241,34 +4424,210 @@ class SoundWave extends Sound
|
|
|
4241
4424
|
this.loadSound(filename);
|
|
4242
4425
|
}
|
|
4243
4426
|
|
|
4244
|
-
/** Loads a sound from a URL and decodes it into sample data. Must be used with await!
|
|
4245
|
-
* @param {string} filename
|
|
4246
|
-
* @return {Promise<void>} */
|
|
4247
|
-
async loadSound(filename)
|
|
4427
|
+
/** Loads a sound from a URL and decodes it into sample data. Must be used with await!
|
|
4428
|
+
* @param {string} filename
|
|
4429
|
+
* @return {Promise<void>} */
|
|
4430
|
+
async loadSound(filename)
|
|
4431
|
+
{
|
|
4432
|
+
const response = await fetch(filename);
|
|
4433
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
4434
|
+
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
4435
|
+
|
|
4436
|
+
// convert audio buffer to sample channels across multiple frames
|
|
4437
|
+
const channelCount = audioBuffer.numberOfChannels;
|
|
4438
|
+
const samplesPerFrame = 1e5;
|
|
4439
|
+
const sampleChannels = [];
|
|
4440
|
+
for (let channel = 0; channel < channelCount; channel++)
|
|
4441
|
+
{
|
|
4442
|
+
const channelData = audioBuffer.getChannelData(channel);
|
|
4443
|
+
const channelLength = channelData.length;
|
|
4444
|
+
sampleChannels[channel] = new Array(channelLength);
|
|
4445
|
+
let sampleIndex = 0;
|
|
4446
|
+
while (sampleIndex < channelLength)
|
|
4447
|
+
{
|
|
4448
|
+
// yield to next frame
|
|
4449
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
4450
|
+
|
|
4451
|
+
// copy chunk of samples
|
|
4452
|
+
const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
|
|
4453
|
+
for (; sampleIndex < endIndex; sampleIndex++)
|
|
4454
|
+
sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
|
|
4455
|
+
|
|
4456
|
+
// update loaded percent
|
|
4457
|
+
const samplesTotal = channelCount * channelLength;
|
|
4458
|
+
const samplesProcessed = channel * channelLength + sampleIndex;
|
|
4459
|
+
this.loadedPercent = samplesProcessed / samplesTotal;
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
// setup the sound to be played
|
|
4464
|
+
this.sampleRate = audioBuffer.sampleRate;
|
|
4465
|
+
this.sampleChannels = sampleChannels;
|
|
4466
|
+
this.loadedPercent = 1;
|
|
4467
|
+
if (this.onloadCallback)
|
|
4468
|
+
this.onloadCallback();
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4473
|
+
|
|
4474
|
+
/**
|
|
4475
|
+
* Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
|
|
4476
|
+
* Represents a single playing instance of a sound with pause/resume capabilities
|
|
4477
|
+
* @example
|
|
4478
|
+
* // Play a sound and get an instance for control
|
|
4479
|
+
* const jumpSound = new Sound([.5,.5,220]);
|
|
4480
|
+
* const instance = jumpSound.play();
|
|
4481
|
+
*
|
|
4482
|
+
* // Control the individual instance
|
|
4483
|
+
* instance.setVolume(.5);
|
|
4484
|
+
* instance.pause();
|
|
4485
|
+
* instance.unpause();
|
|
4486
|
+
* instance.stop();
|
|
4487
|
+
*/
|
|
4488
|
+
class SoundInstance
|
|
4489
|
+
{
|
|
4490
|
+
/** Create a sound instance
|
|
4491
|
+
* @param {Sound} sound - The sound object
|
|
4492
|
+
* @param {number} [volume] - How much to scale volume by
|
|
4493
|
+
* @param {number} [rate] - The playback rate to use
|
|
4494
|
+
* @param {number} [pan] - How much to apply stereo panning
|
|
4495
|
+
* @param {boolean} [loop] - Should the sound loop?
|
|
4496
|
+
* @param {boolean} [paused] - Should the sound start paused? */
|
|
4497
|
+
constructor(sound, volume=1, rate=1, pan=0, loop=false, paused=false)
|
|
4498
|
+
{
|
|
4499
|
+
ASSERT(sound instanceof Sound, 'SoundInstance requires a valid Sound object');
|
|
4500
|
+
ASSERT(volume >= 0, 'Sound volume must be positive or zero');
|
|
4501
|
+
ASSERT(rate >= 0, 'Sound rate must be positive or zero');
|
|
4502
|
+
ASSERT(isNumber(pan), 'Sound pan must be a number');
|
|
4503
|
+
|
|
4504
|
+
/** @property {Sound} - The sound object */
|
|
4505
|
+
this.sound = sound;
|
|
4506
|
+
/** @property {number} - How much to scale volume by */
|
|
4507
|
+
this.volume = volume;
|
|
4508
|
+
/** @property {number} - The playback rate to use */
|
|
4509
|
+
this.rate = rate;
|
|
4510
|
+
/** @property {number} - How much to apply stereo panning */
|
|
4511
|
+
this.pan = pan;
|
|
4512
|
+
/** @property {boolean} - Should the sound loop */
|
|
4513
|
+
this.loop = loop;
|
|
4514
|
+
/** @property {number} - Timestamp for audio context when paused */
|
|
4515
|
+
this.pausedTime = 0;
|
|
4516
|
+
/** @property {number} - Timestamp for audio context when started */
|
|
4517
|
+
this.startTime = undefined;
|
|
4518
|
+
/** @property {GainNode} - Gain node for the sound */
|
|
4519
|
+
this.gainNode = undefined;
|
|
4520
|
+
/** @property {AudioBufferSourceNode} - Source node of the audio */
|
|
4521
|
+
this.source = undefined;
|
|
4522
|
+
// setup end callback and start sound
|
|
4523
|
+
this.onendedCallback = (source)=>
|
|
4524
|
+
{
|
|
4525
|
+
if (source === this.source)
|
|
4526
|
+
this.source = undefined;
|
|
4527
|
+
};
|
|
4528
|
+
if (!paused)
|
|
4529
|
+
this.start();
|
|
4530
|
+
}
|
|
4531
|
+
|
|
4532
|
+
/** Start playing the sound instance from the offset time
|
|
4533
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
4534
|
+
*/
|
|
4535
|
+
start(offset=0)
|
|
4536
|
+
{
|
|
4537
|
+
ASSERT(offset >= 0, 'Sound start offset must be positive or zero');
|
|
4538
|
+
if (this.isPlaying())
|
|
4539
|
+
this.stop();
|
|
4540
|
+
this.gainNode = audioContext.createGain();
|
|
4541
|
+
this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
|
|
4542
|
+
this.startTime = audioContext.currentTime - offset;
|
|
4543
|
+
this.pausedTime = undefined;
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4546
|
+
/** Set the volume of this sound instance
|
|
4547
|
+
* @param {number} volume */
|
|
4548
|
+
setVolume(volume)
|
|
4549
|
+
{
|
|
4550
|
+
ASSERT(volume >= 0, 'Sound volume must be positive or zero');
|
|
4551
|
+
this.volume = volume;
|
|
4552
|
+
if (this.gainNode)
|
|
4553
|
+
this.gainNode.gain.value = volume;
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
/** Stop this sound instance and reset position to the start */
|
|
4557
|
+
stop(fadeTime=0)
|
|
4558
|
+
{
|
|
4559
|
+
ASSERT(fadeTime >= 0, 'Sound fade time must be positive or zero');
|
|
4560
|
+
if (this.isPlaying())
|
|
4561
|
+
{
|
|
4562
|
+
if (fadeTime)
|
|
4563
|
+
{
|
|
4564
|
+
// ramp off gain
|
|
4565
|
+
const startFade = audioContext.currentTime;
|
|
4566
|
+
const endFade = startFade + fadeTime;
|
|
4567
|
+
this.gainNode.gain.linearRampToValueAtTime(1, startFade);
|
|
4568
|
+
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
4569
|
+
this.source.stop(endFade);
|
|
4570
|
+
}
|
|
4571
|
+
else
|
|
4572
|
+
this.source.stop();
|
|
4573
|
+
}
|
|
4574
|
+
this.pausedTime = 0;
|
|
4575
|
+
this.source = undefined;
|
|
4576
|
+
this.startTime = undefined;
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
/** Pause this sound instance */
|
|
4580
|
+
pause()
|
|
4581
|
+
{
|
|
4582
|
+
if (this.isPaused())
|
|
4583
|
+
return;
|
|
4584
|
+
|
|
4585
|
+
// save current time and stop sound
|
|
4586
|
+
this.pausedTime = this.getCurrentTime();
|
|
4587
|
+
this.source.stop();
|
|
4588
|
+
this.source = undefined;
|
|
4589
|
+
this.startTime = undefined;
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4592
|
+
/** Unpauses this sound instance */
|
|
4593
|
+
resume()
|
|
4594
|
+
{
|
|
4595
|
+
if (!this.isPaused())
|
|
4596
|
+
return;
|
|
4597
|
+
|
|
4598
|
+
// restart sound from paused time
|
|
4599
|
+
this.start(this.pausedTime);
|
|
4600
|
+
}
|
|
4601
|
+
|
|
4602
|
+
/** Check if this instance is currently playing
|
|
4603
|
+
* @return {boolean} - True if playing
|
|
4604
|
+
*/
|
|
4605
|
+
isPlaying() { return !!this.source; }
|
|
4606
|
+
|
|
4607
|
+
/** Check if this instance is paused and was not stopped
|
|
4608
|
+
* @return {boolean} - True if paused
|
|
4609
|
+
*/
|
|
4610
|
+
isPaused() { return !this.isPlaying(); }
|
|
4611
|
+
|
|
4612
|
+
/** Get the current playback time in seconds
|
|
4613
|
+
* @return {number} - Current playback time
|
|
4614
|
+
*/
|
|
4615
|
+
getCurrentTime()
|
|
4248
4616
|
{
|
|
4249
|
-
const
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
this.sampleChannels = [];
|
|
4253
|
-
for (let i = audioBuffer.numberOfChannels; i--;)
|
|
4254
|
-
this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
|
|
4255
|
-
this.sampleRate = audioBuffer.sampleRate;
|
|
4256
|
-
if (this.onloadCallback)
|
|
4257
|
-
this.onloadCallback();
|
|
4617
|
+
const deltaTime = mod(audioContext.currentTime - this.startTime,
|
|
4618
|
+
this.getDuration());
|
|
4619
|
+
return this.isPlaying() ? deltaTime : this.pausedTime;
|
|
4258
4620
|
}
|
|
4259
|
-
}
|
|
4260
4621
|
|
|
4261
|
-
/**
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
* @return {SoundWave} - The sound object for this file
|
|
4266
|
-
* @memberof Audio */
|
|
4267
|
-
function playAudioFile(filename, volume=1, loop=false)
|
|
4268
|
-
{
|
|
4269
|
-
if (!soundEnable || headlessMode) return;
|
|
4622
|
+
/** Get the total duration of this sound
|
|
4623
|
+
* @return {number} - Total duration in seconds
|
|
4624
|
+
*/
|
|
4625
|
+
getDuration() { return this.sound.getDuration() / this.rate; }
|
|
4270
4626
|
|
|
4271
|
-
|
|
4627
|
+
/** Get source of this sound instance
|
|
4628
|
+
* @return {AudioBufferSourceNode}
|
|
4629
|
+
*/
|
|
4630
|
+
getSource() { return this.source; }
|
|
4272
4631
|
}
|
|
4273
4632
|
|
|
4274
4633
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -4322,9 +4681,11 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
|
|
|
4322
4681
|
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
4323
4682
|
* @param {number} [sampleRate=44100] - Sample rate for the sound
|
|
4324
4683
|
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing
|
|
4684
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
4685
|
+
* @param {Function} [onended] - Callback for when the sound ends
|
|
4325
4686
|
* @return {AudioBufferSourceNode} - The audio node of the sound played
|
|
4326
4687
|
* @memberof Audio */
|
|
4327
|
-
function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=
|
|
4688
|
+
function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
|
|
4328
4689
|
{
|
|
4329
4690
|
if (!soundEnable || headlessMode) return;
|
|
4330
4691
|
|
|
@@ -4349,16 +4710,20 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
4349
4710
|
const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
|
|
4350
4711
|
source.connect(pannerNode).connect(gainNode);
|
|
4351
4712
|
|
|
4352
|
-
//
|
|
4353
|
-
if (
|
|
4713
|
+
// callback when the sound ends
|
|
4714
|
+
if (onended)
|
|
4715
|
+
source.addEventListener('ended', ()=> onended(source));
|
|
4716
|
+
|
|
4717
|
+
if (!audioIsRunning())
|
|
4354
4718
|
{
|
|
4355
|
-
// fix stalled audio
|
|
4356
|
-
audioContext.resume()
|
|
4719
|
+
// fix stalled audio, this sound won't be able to play
|
|
4720
|
+
audioContext.resume();
|
|
4721
|
+
return;
|
|
4357
4722
|
}
|
|
4358
|
-
else
|
|
4359
|
-
source.start();
|
|
4360
4723
|
|
|
4361
|
-
// return sound
|
|
4724
|
+
// play and return sound
|
|
4725
|
+
const startOffset = offset * rate;
|
|
4726
|
+
source.start(0, startOffset);
|
|
4362
4727
|
return source;
|
|
4363
4728
|
}
|
|
4364
4729
|
|
|
@@ -4366,18 +4731,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
4366
4731
|
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
|
|
4367
4732
|
|
|
4368
4733
|
/** Generate and play a ZzFX sound
|
|
4369
|
-
*
|
|
4734
|
+
*
|
|
4370
4735
|
* <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
|
|
4371
4736
|
* @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
|
|
4372
4737
|
* @return {AudioBufferSourceNode} - The audio node of the sound played
|
|
4373
4738
|
* @memberof Audio */
|
|
4374
4739
|
function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
|
|
4375
4740
|
|
|
4376
|
-
/** Sample rate used for all ZzFX sounds
|
|
4377
|
-
* @default 44100
|
|
4378
|
-
* @memberof Audio */
|
|
4379
|
-
const zzfxR = 44100;
|
|
4380
|
-
|
|
4381
4741
|
/** Generate samples for a ZzFX sound
|
|
4382
4742
|
* @param {number} [volume] - Volume scale (percent)
|
|
4383
4743
|
* @param {number} [randomness] - How much to randomize frequency (percent Hz)
|
|
@@ -4401,11 +4761,10 @@ const zzfxR = 44100;
|
|
|
4401
4761
|
* @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
|
|
4402
4762
|
* @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
|
|
4403
4763
|
* @return {Array} - Array of audio samples
|
|
4404
|
-
* @memberof Audio
|
|
4405
|
-
*/
|
|
4764
|
+
* @memberof Audio */
|
|
4406
4765
|
function zzfxG
|
|
4407
4766
|
(
|
|
4408
|
-
volume = 1,
|
|
4767
|
+
volume = 1,
|
|
4409
4768
|
randomness = .05,
|
|
4410
4769
|
frequency = 220,
|
|
4411
4770
|
attack = 0,
|
|
@@ -4413,11 +4772,11 @@ function zzfxG
|
|
|
4413
4772
|
release = .1,
|
|
4414
4773
|
shape = 0,
|
|
4415
4774
|
shapeCurve = 1,
|
|
4416
|
-
slide = 0,
|
|
4417
|
-
deltaSlide = 0,
|
|
4418
|
-
pitchJump = 0,
|
|
4419
|
-
pitchJumpTime = 0,
|
|
4420
|
-
repeatTime = 0,
|
|
4775
|
+
slide = 0,
|
|
4776
|
+
deltaSlide = 0,
|
|
4777
|
+
pitchJump = 0,
|
|
4778
|
+
pitchJumpTime = 0,
|
|
4779
|
+
repeatTime = 0,
|
|
4421
4780
|
noise = 0,
|
|
4422
4781
|
modulation = 0,
|
|
4423
4782
|
bitCrush = 0,
|
|
@@ -4429,19 +4788,19 @@ function zzfxG
|
|
|
4429
4788
|
)
|
|
4430
4789
|
{
|
|
4431
4790
|
// init parameters
|
|
4432
|
-
let sampleRate =
|
|
4433
|
-
PI2 = PI*2,
|
|
4791
|
+
let sampleRate = audioDefaultSampleRate,
|
|
4792
|
+
PI2 = PI*2,
|
|
4434
4793
|
startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
|
|
4435
|
-
startFrequency = frequency *=
|
|
4794
|
+
startFrequency = frequency *=
|
|
4436
4795
|
(1 + rand(randomness,-randomness)) * PI2 / sampleRate,
|
|
4437
|
-
modOffset = 0, // modulation offset
|
|
4796
|
+
modOffset = 0, // modulation offset
|
|
4438
4797
|
repeat = 0, // repeat offset
|
|
4439
4798
|
crush = 0, // bit crush offset
|
|
4440
4799
|
jump = 1, // pitch jump timer
|
|
4441
4800
|
length, // sample length
|
|
4442
4801
|
b = [], // sample buffer
|
|
4443
4802
|
t = 0, // sample time
|
|
4444
|
-
i = 0, // sample index
|
|
4803
|
+
i = 0, // sample index
|
|
4445
4804
|
s = 0, // sample value
|
|
4446
4805
|
f, // wave frequency
|
|
4447
4806
|
|
|
@@ -4449,7 +4808,7 @@ function zzfxG
|
|
|
4449
4808
|
quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
|
|
4450
4809
|
cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
|
|
4451
4810
|
a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
|
|
4452
|
-
b0 = (1 + sign(filter) * cos) / 2 / a0,
|
|
4811
|
+
b0 = (1 + sign(filter) * cos) / 2 / a0,
|
|
4453
4812
|
b1 = -(sign(filter) + cos) / a0, b2 = b0,
|
|
4454
4813
|
x2 = 0, x1 = 0, y2 = 0, y1 = 0;
|
|
4455
4814
|
|
|
@@ -4495,7 +4854,7 @@ function zzfxG
|
|
|
4495
4854
|
0); // post release
|
|
4496
4855
|
|
|
4497
4856
|
s = delay ? s/2 + (delay > i ? 0 : // delay
|
|
4498
|
-
(i<length-delay? 1 : (length-i)/delay) * // release delay
|
|
4857
|
+
(i<length-delay? 1 : (length-i)/delay) * // release delay
|
|
4499
4858
|
b[i-delay|0]/2/volume) : s; // sample delay
|
|
4500
4859
|
|
|
4501
4860
|
if (filter) // apply filter
|
|
@@ -4507,14 +4866,14 @@ function zzfxG
|
|
|
4507
4866
|
t += f + f*noise*Math.sin(i**5); // noise
|
|
4508
4867
|
|
|
4509
4868
|
if (jump && ++jump > pitchJumpTime) // pitch jump
|
|
4510
|
-
{
|
|
4869
|
+
{
|
|
4511
4870
|
frequency += pitchJump; // apply pitch jump
|
|
4512
4871
|
startFrequency += pitchJump; // also apply to start
|
|
4513
4872
|
jump = 0; // stop pitch jump time
|
|
4514
|
-
}
|
|
4873
|
+
}
|
|
4515
4874
|
|
|
4516
4875
|
if (repeatTime && !(++repeat % repeatTime)) // repeat
|
|
4517
|
-
{
|
|
4876
|
+
{
|
|
4518
4877
|
frequency = startFrequency; // reset frequency
|
|
4519
4878
|
slide = startSlide; // reset slide
|
|
4520
4879
|
jump ||= 1; // reset pitch jump time
|
|
@@ -4523,12 +4882,11 @@ function zzfxG
|
|
|
4523
4882
|
|
|
4524
4883
|
return b; // return sample buffer
|
|
4525
4884
|
}
|
|
4526
|
-
/**
|
|
4885
|
+
/**
|
|
4527
4886
|
* LittleJS Tile Layer System
|
|
4528
4887
|
* - Caches arrays of tiles to off screen canvas for fast rendering
|
|
4529
4888
|
* - Unlimited numbers of layers, allocates canvases as needed
|
|
4530
4889
|
* - Tile layers can be drawn to using their context with canvas2d
|
|
4531
|
-
* - Drawn directly to the main canvas without using WebGL
|
|
4532
4890
|
* - Tile layers can also have collision with EngineObjects
|
|
4533
4891
|
* @namespace TileCollision
|
|
4534
4892
|
*/
|
|
@@ -4537,9 +4895,9 @@ function zzfxG
|
|
|
4537
4895
|
// Tile Layer System
|
|
4538
4896
|
|
|
4539
4897
|
/** Keep track of all tile layers with collision
|
|
4540
|
-
* @type {Array<TileCollisionLayer>}
|
|
4898
|
+
* @type {Array<TileCollisionLayer>}
|
|
4541
4899
|
* @memberof TileCollision */
|
|
4542
|
-
|
|
4900
|
+
const tileCollisionLayers = [];
|
|
4543
4901
|
|
|
4544
4902
|
/** Get tile collision data for a given cell in the grid
|
|
4545
4903
|
* @param {Vector2} pos
|
|
@@ -4593,7 +4951,7 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
|
|
|
4593
4951
|
}
|
|
4594
4952
|
|
|
4595
4953
|
///////////////////////////////////////////////////////////////////////////////
|
|
4596
|
-
/**
|
|
4954
|
+
/**
|
|
4597
4955
|
* Load tile layers from exported data
|
|
4598
4956
|
* @param {Object} tileMapData - Level data from exported data
|
|
4599
4957
|
* @param {TileInfo} [tileInfo] - Default tile info (used for size and texture)
|
|
@@ -4626,13 +4984,13 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
|
|
|
4626
4984
|
{
|
|
4627
4985
|
const dataLayer = tileMapData.layers[layerIndex];
|
|
4628
4986
|
ASSERT(dataLayer.data && dataLayer.data.length);
|
|
4629
|
-
ASSERT(levelSize.area()
|
|
4987
|
+
ASSERT(levelSize.area() === dataLayer.data.length);
|
|
4630
4988
|
|
|
4631
4989
|
const layerRenderOrder = renderOrder - (layerCount - 1 - layerIndex);
|
|
4632
4990
|
const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
|
|
4633
4991
|
tileLayers[layerIndex] = tileLayer;
|
|
4634
4992
|
|
|
4635
|
-
for (let x=levelSize.x; x--;)
|
|
4993
|
+
for (let x=levelSize.x; x--;)
|
|
4636
4994
|
for (let y=levelSize.y; y--;)
|
|
4637
4995
|
{
|
|
4638
4996
|
const pos = vec2(x, levelSize.y-1-y);
|
|
@@ -4691,7 +5049,7 @@ class TileLayerData
|
|
|
4691
5049
|
/**
|
|
4692
5050
|
* Canvas Layer - cached off screen rendering system
|
|
4693
5051
|
* - Contains an offscreen canvas that can be rendered to
|
|
4694
|
-
* -
|
|
5052
|
+
* - WebGL rendering is optional, call useWebGL to enable
|
|
4695
5053
|
* @extends EngineObject
|
|
4696
5054
|
* @example
|
|
4697
5055
|
* const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
|
|
@@ -4713,18 +5071,18 @@ class CanvasLayer extends EngineObject
|
|
|
4713
5071
|
this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
|
|
4714
5072
|
/** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
|
|
4715
5073
|
this.context = headlessMode ? undefined : this.canvas.getContext('2d');
|
|
4716
|
-
/** @property {WebGLTexture} - Texture if using
|
|
5074
|
+
/** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
|
|
4717
5075
|
this.glTexture = undefined;
|
|
4718
5076
|
this.gravityScale = 0; // disable gravity by default for canvas layers
|
|
4719
5077
|
}
|
|
4720
|
-
|
|
5078
|
+
|
|
4721
5079
|
/** Destroy this canvas layer */
|
|
4722
5080
|
destroy()
|
|
4723
5081
|
{
|
|
4724
5082
|
if (this.destroyed)
|
|
4725
5083
|
return;
|
|
4726
5084
|
|
|
4727
|
-
// free up the
|
|
5085
|
+
// free up the WebGL texture
|
|
4728
5086
|
if (this.glTexture)
|
|
4729
5087
|
glDeleteTexture(this.glTexture);
|
|
4730
5088
|
super.destroy();
|
|
@@ -4749,12 +5107,12 @@ class CanvasLayer extends EngineObject
|
|
|
4749
5107
|
draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
|
|
4750
5108
|
{
|
|
4751
5109
|
// draw the canvas layer as a single tile that uses the whole texture
|
|
4752
|
-
const
|
|
5110
|
+
const useWebGL = glEnable && this.glTexture !== undefined;
|
|
4753
5111
|
const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
|
|
4754
|
-
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor,
|
|
5112
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
|
|
4755
5113
|
}
|
|
4756
5114
|
|
|
4757
|
-
/** Draw onto the layer canvas in world space (bypass
|
|
5115
|
+
/** Draw onto the layer canvas in world space (bypass WebGL)
|
|
4758
5116
|
* @param {Vector2} pos
|
|
4759
5117
|
* @param {Vector2} size
|
|
4760
5118
|
* @param {number} angle
|
|
@@ -4788,8 +5146,8 @@ class CanvasLayer extends EngineObject
|
|
|
4788
5146
|
if (textureInfo)
|
|
4789
5147
|
{
|
|
4790
5148
|
context.globalAlpha = color.a; // only alpha is supported
|
|
4791
|
-
context.drawImage(textureInfo.image,
|
|
4792
|
-
tileInfo.pos.x, tileInfo.pos.y,
|
|
5149
|
+
context.drawImage(textureInfo.image,
|
|
5150
|
+
tileInfo.pos.x, tileInfo.pos.y,
|
|
4793
5151
|
tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
|
|
4794
5152
|
context.globalAlpha = 1;
|
|
4795
5153
|
}
|
|
@@ -4807,11 +5165,11 @@ class CanvasLayer extends EngineObject
|
|
|
4807
5165
|
* @param {Vector2} [size=(1,1)]
|
|
4808
5166
|
* @param {Color} [color=(1,1,1,1)]
|
|
4809
5167
|
* @param {number} [angle=0] */
|
|
4810
|
-
drawRect(pos, size, color, angle)
|
|
5168
|
+
drawRect(pos, size, color, angle)
|
|
4811
5169
|
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
4812
5170
|
|
|
4813
|
-
/** Create or update the
|
|
4814
|
-
* @param {boolean} [enable] - enable
|
|
5171
|
+
/** Create or update the WebGL texture for this layer
|
|
5172
|
+
* @param {boolean} [enable] - enable WebGL rendering and update the texture */
|
|
4815
5173
|
useWebGL(enable=true)
|
|
4816
5174
|
{
|
|
4817
5175
|
if (glEnable && enable)
|
|
@@ -4857,7 +5215,7 @@ class TileLayer extends CanvasLayer
|
|
|
4857
5215
|
this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
|
|
4858
5216
|
/** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
|
|
4859
5217
|
this.context = this.canvas.getContext('2d');
|
|
4860
|
-
/** @property {WebGLTexture} - Texture if using
|
|
5218
|
+
/** @property {WebGLTexture} - Texture if using WebGL for this layer */
|
|
4861
5219
|
this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
|
|
4862
5220
|
// set no friction by default, applied friction is max of both objects
|
|
4863
5221
|
this.friction = 0;
|
|
@@ -4882,7 +5240,7 @@ class TileLayer extends CanvasLayer
|
|
|
4882
5240
|
}
|
|
4883
5241
|
}
|
|
4884
5242
|
|
|
4885
|
-
/** Set data at a given position in the array
|
|
5243
|
+
/** Set data at a given position in the array
|
|
4886
5244
|
* @param {Vector2} layerPos - Local position in array
|
|
4887
5245
|
* @param {TileLayerData} data - Data to set
|
|
4888
5246
|
* @param {boolean} [redraw] - Force the tile to redraw if true */
|
|
@@ -4894,26 +5252,26 @@ class TileLayer extends CanvasLayer
|
|
|
4894
5252
|
redraw && this.drawTileData(layerPos);
|
|
4895
5253
|
}
|
|
4896
5254
|
}
|
|
4897
|
-
|
|
4898
|
-
/** Get data at a given position in the array
|
|
5255
|
+
|
|
5256
|
+
/** Get data at a given position in the array
|
|
4899
5257
|
* @param {Vector2} layerPos - Local position in array
|
|
4900
5258
|
* @return {TileLayerData} */
|
|
4901
5259
|
getData(layerPos)
|
|
4902
5260
|
{ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
|
|
4903
|
-
|
|
5261
|
+
|
|
4904
5262
|
// Render the tile layer, called automatically by the engine
|
|
4905
5263
|
render()
|
|
4906
5264
|
{
|
|
4907
|
-
ASSERT(drawContext
|
|
4908
|
-
|
|
5265
|
+
ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
|
|
5266
|
+
|
|
4909
5267
|
// draw the tile layer as a single tile
|
|
4910
5268
|
const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
|
|
4911
5269
|
const pos = this.pos.add(this.size.scale(.5));
|
|
4912
|
-
const
|
|
4913
|
-
drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK,
|
|
5270
|
+
const useWebGL = glEnable && this.glTexture !== undefined;
|
|
5271
|
+
drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
|
|
4914
5272
|
}
|
|
4915
5273
|
|
|
4916
|
-
/** Draw all the tile data to an offscreen canvas
|
|
5274
|
+
/** Draw all the tile data to an offscreen canvas
|
|
4917
5275
|
* - This may be slow in some browsers but only needs to be done once */
|
|
4918
5276
|
redraw()
|
|
4919
5277
|
{
|
|
@@ -4923,7 +5281,7 @@ class TileLayer extends CanvasLayer
|
|
|
4923
5281
|
this.drawTileData(vec2(x,y), false);
|
|
4924
5282
|
this.redrawEnd();
|
|
4925
5283
|
if (this.glTexture)
|
|
4926
|
-
this.useWebGL(); // update
|
|
5284
|
+
this.useWebGL(); // update WebGL texture
|
|
4927
5285
|
}
|
|
4928
5286
|
|
|
4929
5287
|
/** Call to start the redraw process
|
|
@@ -4959,7 +5317,7 @@ class TileLayer extends CanvasLayer
|
|
|
4959
5317
|
/** Call to end the redraw process */
|
|
4960
5318
|
redrawEnd()
|
|
4961
5319
|
{
|
|
4962
|
-
ASSERT(drawContext
|
|
5320
|
+
ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
|
|
4963
5321
|
glCopyToContext(drawContext);
|
|
4964
5322
|
//debugSaveCanvas(this.canvas);
|
|
4965
5323
|
|
|
@@ -4970,7 +5328,7 @@ class TileLayer extends CanvasLayer
|
|
|
4970
5328
|
/** Draw the tile at a given position in the tile grid
|
|
4971
5329
|
* This can be used to clear out tiles when they are destroyed
|
|
4972
5330
|
* Tiles can also be redrawn if inside a redrawStart/End block
|
|
4973
|
-
* @param {Vector2} layerPos
|
|
5331
|
+
* @param {Vector2} layerPos
|
|
4974
5332
|
* @param {boolean} [clear] - should the old tile be cleared out
|
|
4975
5333
|
*/
|
|
4976
5334
|
drawTileData(layerPos, clear=true)
|
|
@@ -4985,9 +5343,9 @@ class TileLayer extends CanvasLayer
|
|
|
4985
5343
|
|
|
4986
5344
|
// draw the tile if it has layer data
|
|
4987
5345
|
const d = this.getData(layerPos);
|
|
4988
|
-
if (d.tile
|
|
5346
|
+
if (d.tile !== undefined)
|
|
4989
5347
|
{
|
|
4990
|
-
ASSERT(drawContext
|
|
5348
|
+
ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
|
|
4991
5349
|
const pos = layerPos.add(vec2(.5));
|
|
4992
5350
|
const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
|
|
4993
5351
|
drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
@@ -5000,7 +5358,7 @@ class TileLayer extends CanvasLayer
|
|
|
5000
5358
|
* Tile Collision Layer - a tile layer with collision
|
|
5001
5359
|
* - adds collision data and functions to TileLayer
|
|
5002
5360
|
* - there can be multiple tile collision layers
|
|
5003
|
-
* - tile
|
|
5361
|
+
* - tile collision layers should not overlap each other
|
|
5004
5362
|
* @extends TileLayer
|
|
5005
5363
|
*/
|
|
5006
5364
|
class TileCollisionLayer extends TileLayer
|
|
@@ -5151,7 +5509,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
5151
5509
|
debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
|
|
5152
5510
|
}
|
|
5153
5511
|
}
|
|
5154
|
-
/**
|
|
5512
|
+
/**
|
|
5155
5513
|
* LittleJS Particle System
|
|
5156
5514
|
*/
|
|
5157
5515
|
|
|
@@ -5168,7 +5526,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
5168
5526
|
* rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
|
|
5169
5527
|
* rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
|
|
5170
5528
|
* 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
|
|
5171
|
-
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
|
|
5529
|
+
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
|
|
5172
5530
|
* .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
|
|
5173
5531
|
* );
|
|
5174
5532
|
*/
|
|
@@ -5176,35 +5534,35 @@ class ParticleEmitter extends EngineObject
|
|
|
5176
5534
|
{
|
|
5177
5535
|
/** Create a particle system with the given settings
|
|
5178
5536
|
* @param {Vector2} position - World space position of the emitter
|
|
5179
|
-
* @param {
|
|
5180
|
-
* @param {
|
|
5181
|
-
* @param {
|
|
5182
|
-
* @param {
|
|
5183
|
-
* @param {
|
|
5537
|
+
* @param {number} [angle] - Angle to emit the particles
|
|
5538
|
+
* @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
|
|
5539
|
+
* @param {number} [emitTime] - How long to stay alive (0 is forever)
|
|
5540
|
+
* @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
|
|
5541
|
+
* @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
|
|
5184
5542
|
* @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
|
|
5185
5543
|
* @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
|
|
5186
5544
|
* @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
|
|
5187
5545
|
* @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
|
|
5188
5546
|
* @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
|
|
5189
|
-
* @param {
|
|
5190
|
-
* @param {
|
|
5191
|
-
* @param {
|
|
5192
|
-
* @param {
|
|
5193
|
-
* @param {
|
|
5194
|
-
* @param {
|
|
5195
|
-
* @param {
|
|
5196
|
-
* @param {
|
|
5197
|
-
* @param {
|
|
5198
|
-
* @param {
|
|
5199
|
-
* @param {
|
|
5547
|
+
* @param {number} [particleTime] - How long particles live
|
|
5548
|
+
* @param {number} [sizeStart] - How big are particles at start
|
|
5549
|
+
* @param {number} [sizeEnd] - How big are particles at end
|
|
5550
|
+
* @param {number} [speed] - How fast are particles when spawned
|
|
5551
|
+
* @param {number} [angleSpeed] - How fast are particles rotating
|
|
5552
|
+
* @param {number} [damping] - How much to dampen particle speed
|
|
5553
|
+
* @param {number} [angleDamping] - How much to dampen particle angular speed
|
|
5554
|
+
* @param {number} [gravityScale] - How much gravity effect particles
|
|
5555
|
+
* @param {number} [particleConeAngle] - Cone for start particle angle
|
|
5556
|
+
* @param {number} [fadeRate] - How quick to fade particles at start/end in percent of life
|
|
5557
|
+
* @param {number} [randomness] - Apply extra randomness percent
|
|
5200
5558
|
* @param {boolean} [collideTiles] - Do particles collide against tiles
|
|
5201
5559
|
* @param {boolean} [additive] - Should particles use additive blend
|
|
5202
5560
|
* @param {boolean} [randomColorLinear] - Should color be randomized linearly or across each component
|
|
5203
|
-
* @param {
|
|
5561
|
+
* @param {number} [renderOrder] - Render order for particles (additive is above other stuff by default)
|
|
5204
5562
|
* @param {boolean} [localSpace] - Should it be in local space of emitter (world space is default)
|
|
5205
5563
|
*/
|
|
5206
5564
|
constructor
|
|
5207
|
-
(
|
|
5565
|
+
(
|
|
5208
5566
|
position,
|
|
5209
5567
|
angle,
|
|
5210
5568
|
emitSize = 0,
|
|
@@ -5226,7 +5584,7 @@ class ParticleEmitter extends EngineObject
|
|
|
5226
5584
|
gravityScale = 0,
|
|
5227
5585
|
particleConeAngle = PI,
|
|
5228
5586
|
fadeRate = .1,
|
|
5229
|
-
randomness = .2,
|
|
5587
|
+
randomness = .2,
|
|
5230
5588
|
collideTiles = false,
|
|
5231
5589
|
additive = false,
|
|
5232
5590
|
randomColorLinear = true,
|
|
@@ -5237,13 +5595,13 @@ class ParticleEmitter extends EngineObject
|
|
|
5237
5595
|
super(position, vec2(), tileInfo, angle, undefined, renderOrder);
|
|
5238
5596
|
|
|
5239
5597
|
// emitter settings
|
|
5240
|
-
/** @property {
|
|
5598
|
+
/** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
|
|
5241
5599
|
this.emitSize = emitSize
|
|
5242
|
-
/** @property {
|
|
5600
|
+
/** @property {number} - How long to stay alive (0 is forever) */
|
|
5243
5601
|
this.emitTime = emitTime;
|
|
5244
|
-
/** @property {
|
|
5602
|
+
/** @property {number} - How many particles per second to spawn, does not emit if 0 */
|
|
5245
5603
|
this.emitRate = emitRate;
|
|
5246
|
-
/** @property {
|
|
5604
|
+
/** @property {number} - Local angle to apply velocity to particles from emitter */
|
|
5247
5605
|
this.emitConeAngle = emitConeAngle;
|
|
5248
5606
|
|
|
5249
5607
|
// color settings
|
|
@@ -5259,27 +5617,27 @@ class ParticleEmitter extends EngineObject
|
|
|
5259
5617
|
this.randomColorLinear = randomColorLinear;
|
|
5260
5618
|
|
|
5261
5619
|
// particle settings
|
|
5262
|
-
/** @property {
|
|
5620
|
+
/** @property {number} - How long particles live */
|
|
5263
5621
|
this.particleTime = particleTime;
|
|
5264
|
-
/** @property {
|
|
5622
|
+
/** @property {number} - How big are particles at start */
|
|
5265
5623
|
this.sizeStart = sizeStart;
|
|
5266
|
-
/** @property {
|
|
5624
|
+
/** @property {number} - How big are particles at end */
|
|
5267
5625
|
this.sizeEnd = sizeEnd;
|
|
5268
|
-
/** @property {
|
|
5626
|
+
/** @property {number} - How fast are particles when spawned */
|
|
5269
5627
|
this.speed = speed;
|
|
5270
|
-
/** @property {
|
|
5628
|
+
/** @property {number} - How fast are particles rotating */
|
|
5271
5629
|
this.angleSpeed = angleSpeed;
|
|
5272
|
-
/** @property {
|
|
5630
|
+
/** @property {number} - How much to dampen particle speed */
|
|
5273
5631
|
this.damping = damping;
|
|
5274
|
-
/** @property {
|
|
5632
|
+
/** @property {number} - How much to dampen particle angular speed */
|
|
5275
5633
|
this.angleDamping = angleDamping;
|
|
5276
|
-
/** @property {
|
|
5634
|
+
/** @property {number} - How much gravity affects particles */
|
|
5277
5635
|
this.gravityScale = gravityScale;
|
|
5278
|
-
/** @property {
|
|
5636
|
+
/** @property {number} - Cone for start particle angle */
|
|
5279
5637
|
this.particleConeAngle = particleConeAngle;
|
|
5280
|
-
/** @property {
|
|
5638
|
+
/** @property {number} - How quick to fade in particles at start/end in percent of life */
|
|
5281
5639
|
this.fadeRate = fadeRate;
|
|
5282
|
-
/** @property {
|
|
5640
|
+
/** @property {number} - Apply extra randomness percent */
|
|
5283
5641
|
this.randomness = randomness;
|
|
5284
5642
|
/** @property {boolean} - Do particles collide against tiles */
|
|
5285
5643
|
this.collideTiles = collideTiles;
|
|
@@ -5287,16 +5645,16 @@ class ParticleEmitter extends EngineObject
|
|
|
5287
5645
|
this.additive = additive;
|
|
5288
5646
|
/** @property {boolean} - Should it be in local space of emitter */
|
|
5289
5647
|
this.localSpace = localSpace;
|
|
5290
|
-
/** @property {
|
|
5648
|
+
/** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
|
|
5291
5649
|
this.trailScale = 0;
|
|
5292
5650
|
/** @property {Function} - Callback when particle is destroyed */
|
|
5293
5651
|
this.particleDestroyCallback = undefined;
|
|
5294
5652
|
/** @property {Function} - Callback when particle is created */
|
|
5295
5653
|
this.particleCreateCallback = undefined;
|
|
5296
|
-
/** @property {
|
|
5654
|
+
/** @property {number} - Track particle emit time */
|
|
5297
5655
|
this.emitTimeBuffer = 0;
|
|
5298
5656
|
}
|
|
5299
|
-
|
|
5657
|
+
|
|
5300
5658
|
/** Update the emitter to spawn particles, called automatically by engine once each frame */
|
|
5301
5659
|
update()
|
|
5302
5660
|
{
|
|
@@ -5320,7 +5678,7 @@ class ParticleEmitter extends EngineObject
|
|
|
5320
5678
|
if (debugParticles)
|
|
5321
5679
|
{
|
|
5322
5680
|
// show emitter bounds
|
|
5323
|
-
const emitSize = typeof this.emitSize
|
|
5681
|
+
const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
|
|
5324
5682
|
debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
|
|
5325
5683
|
}
|
|
5326
5684
|
}
|
|
@@ -5330,7 +5688,7 @@ class ParticleEmitter extends EngineObject
|
|
|
5330
5688
|
emitParticle()
|
|
5331
5689
|
{
|
|
5332
5690
|
// spawn a particle
|
|
5333
|
-
let pos = typeof this.emitSize
|
|
5691
|
+
let pos = typeof this.emitSize === 'number' ? // check if number was used
|
|
5334
5692
|
randInCircle(this.emitSize/2) // circle emitter
|
|
5335
5693
|
: vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
|
|
5336
5694
|
.multiply(this.emitSize).rotate(this.angle)
|
|
@@ -5355,7 +5713,7 @@ class ParticleEmitter extends EngineObject
|
|
|
5355
5713
|
const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
|
|
5356
5714
|
const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
|
|
5357
5715
|
const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
|
|
5358
|
-
|
|
5716
|
+
|
|
5359
5717
|
// build particle
|
|
5360
5718
|
const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
|
|
5361
5719
|
particle.velocity = vec2().setAngle(velocityAngle, speed);
|
|
@@ -5400,38 +5758,38 @@ class Particle extends EngineObject
|
|
|
5400
5758
|
* Typically this is created automatically by a ParticleEmitter
|
|
5401
5759
|
* @param {Vector2} position - World space position of the particle
|
|
5402
5760
|
* @param {TileInfo} tileInfo - Tile info to render particles
|
|
5403
|
-
* @param {
|
|
5761
|
+
* @param {number} angle - Angle to rotate the particle
|
|
5404
5762
|
* @param {Color} colorStart - Color at start of life
|
|
5405
5763
|
* @param {Color} colorEnd - Color at end of life
|
|
5406
|
-
* @param {
|
|
5407
|
-
* @param {
|
|
5408
|
-
* @param {
|
|
5409
|
-
* @param {
|
|
5764
|
+
* @param {number} lifeTime - How long to live for
|
|
5765
|
+
* @param {number} sizeStart - Size at start of life
|
|
5766
|
+
* @param {number} sizeEnd - Size at end of life
|
|
5767
|
+
* @param {number} fadeRate - How quick to fade in/out
|
|
5410
5768
|
* @param {boolean} additive - Does it use additive blend mode
|
|
5411
|
-
* @param {
|
|
5769
|
+
* @param {number} trailScale - If a trail, how long to make it
|
|
5412
5770
|
* @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
|
|
5413
5771
|
* @param {Function} [destroyCallback] - Callback when particle dies
|
|
5414
5772
|
*/
|
|
5415
5773
|
constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
|
|
5416
5774
|
)
|
|
5417
|
-
{
|
|
5418
|
-
super(position, vec2(), tileInfo, angle);
|
|
5419
|
-
|
|
5775
|
+
{
|
|
5776
|
+
super(position, vec2(), tileInfo, angle);
|
|
5777
|
+
|
|
5420
5778
|
/** @property {Color} - Color at start of life */
|
|
5421
5779
|
this.colorStart = colorStart;
|
|
5422
5780
|
/** @property {Color} - Calculated change in color */
|
|
5423
5781
|
this.colorEndDelta = colorEnd.subtract(colorStart);
|
|
5424
|
-
/** @property {
|
|
5782
|
+
/** @property {number} - How long to live for */
|
|
5425
5783
|
this.lifeTime = lifeTime;
|
|
5426
|
-
/** @property {
|
|
5784
|
+
/** @property {number} - Size at start of life */
|
|
5427
5785
|
this.sizeStart = sizeStart;
|
|
5428
|
-
/** @property {
|
|
5786
|
+
/** @property {number} - Calculated change in size */
|
|
5429
5787
|
this.sizeEndDelta = sizeEnd - sizeStart;
|
|
5430
|
-
/** @property {
|
|
5788
|
+
/** @property {number} - How quick to fade in/out */
|
|
5431
5789
|
this.fadeRate = fadeRate;
|
|
5432
5790
|
/** @property {boolean} - Is it additive */
|
|
5433
5791
|
this.additive = additive;
|
|
5434
|
-
/** @property {
|
|
5792
|
+
/** @property {number} - If a trail, how long to make it */
|
|
5435
5793
|
this.trailScale = trailScale;
|
|
5436
5794
|
/** @property {ParticleEmitter} - Parent emitter if local space */
|
|
5437
5795
|
this.localSpaceEmitter = localSpaceEmitter;
|
|
@@ -5471,7 +5829,7 @@ class Particle extends EngineObject
|
|
|
5471
5829
|
this.colorStart.r + p * this.colorEndDelta.r,
|
|
5472
5830
|
this.colorStart.g + p * this.colorEndDelta.g,
|
|
5473
5831
|
this.colorStart.b + p * this.colorEndDelta.b,
|
|
5474
|
-
(this.colorStart.a + p * this.colorEndDelta.a) *
|
|
5832
|
+
(this.colorStart.a + p * this.colorEndDelta.a) *
|
|
5475
5833
|
(p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
|
|
5476
5834
|
|
|
5477
5835
|
// draw the particle
|
|
@@ -5481,7 +5839,7 @@ class Particle extends EngineObject
|
|
|
5481
5839
|
if (this.localSpaceEmitter)
|
|
5482
5840
|
{
|
|
5483
5841
|
// in local space of emitter
|
|
5484
|
-
pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
|
|
5842
|
+
pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
|
|
5485
5843
|
angle += this.localSpaceEmitter.angle;
|
|
5486
5844
|
}
|
|
5487
5845
|
if (this.trailScale)
|
|
@@ -5505,7 +5863,7 @@ class Particle extends EngineObject
|
|
|
5505
5863
|
this.additive && setBlendMode();
|
|
5506
5864
|
debugParticles && debugRect(pos, size, '#f005', 0, angle);
|
|
5507
5865
|
|
|
5508
|
-
if (p
|
|
5866
|
+
if (p === 1)
|
|
5509
5867
|
{
|
|
5510
5868
|
// destroy particle when it's time runs out
|
|
5511
5869
|
this.color = color;
|
|
@@ -5515,7 +5873,7 @@ class Particle extends EngineObject
|
|
|
5515
5873
|
}
|
|
5516
5874
|
}
|
|
5517
5875
|
}
|
|
5518
|
-
/**
|
|
5876
|
+
/**
|
|
5519
5877
|
* LittleJS Medal System
|
|
5520
5878
|
* - Tracks and displays medals
|
|
5521
5879
|
* - Saves medals to local storage
|
|
@@ -5536,7 +5894,7 @@ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
|
|
|
5536
5894
|
/** Initialize medals with a save name used for storage
|
|
5537
5895
|
* - Call this after creating all medals
|
|
5538
5896
|
* - Checks if medals are unlocked
|
|
5539
|
-
* @param {
|
|
5897
|
+
* @param {string} saveName
|
|
5540
5898
|
* @memberof Medals */
|
|
5541
5899
|
function medalsInit(saveName)
|
|
5542
5900
|
{
|
|
@@ -5551,7 +5909,7 @@ function medalsInit(saveName)
|
|
|
5551
5909
|
{
|
|
5552
5910
|
if (!medalsDisplayQueue.length)
|
|
5553
5911
|
return;
|
|
5554
|
-
|
|
5912
|
+
|
|
5555
5913
|
// update first medal in queue
|
|
5556
5914
|
const medal = medalsDisplayQueue[0];
|
|
5557
5915
|
const time = timeReal - medalsDisplayTimeLast;
|
|
@@ -5566,7 +5924,7 @@ function medalsInit(saveName)
|
|
|
5566
5924
|
{
|
|
5567
5925
|
// slide on/off medals
|
|
5568
5926
|
const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
|
|
5569
|
-
const hidePercent =
|
|
5927
|
+
const hidePercent =
|
|
5570
5928
|
time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
|
|
5571
5929
|
time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
|
|
5572
5930
|
medal.render(hidePercent);
|
|
@@ -5582,43 +5940,43 @@ function medalsForEach(callback)
|
|
|
5582
5940
|
|
|
5583
5941
|
///////////////////////////////////////////////////////////////////////////////
|
|
5584
5942
|
|
|
5585
|
-
/**
|
|
5586
|
-
* Medal - Tracks an unlockable medal
|
|
5943
|
+
/**
|
|
5944
|
+
* Medal - Tracks an unlockable medal
|
|
5587
5945
|
* @example
|
|
5588
5946
|
* // create a medal
|
|
5589
5947
|
* const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
|
|
5590
|
-
*
|
|
5948
|
+
*
|
|
5591
5949
|
* // initialize medals
|
|
5592
5950
|
* medalsInit('Example Game');
|
|
5593
|
-
*
|
|
5951
|
+
*
|
|
5594
5952
|
* // unlock the medal
|
|
5595
5953
|
* medal_example.unlock();
|
|
5596
5954
|
*/
|
|
5597
5955
|
class Medal
|
|
5598
5956
|
{
|
|
5599
5957
|
/** Create a medal object and adds it to the list of medals
|
|
5600
|
-
* @param {
|
|
5601
|
-
* @param {
|
|
5602
|
-
* @param {
|
|
5603
|
-
* @param {
|
|
5604
|
-
* @param {
|
|
5958
|
+
* @param {number} id - The unique identifier of the medal
|
|
5959
|
+
* @param {string} name - Name of the medal
|
|
5960
|
+
* @param {string} [description] - Description of the medal
|
|
5961
|
+
* @param {string} [icon] - Icon for the medal
|
|
5962
|
+
* @param {string} [src] - Image location for the medal
|
|
5605
5963
|
*/
|
|
5606
5964
|
constructor(id, name, description='', icon='🏆', src)
|
|
5607
5965
|
{
|
|
5608
5966
|
ASSERT(id >= 0 && !medals[id]);
|
|
5609
|
-
|
|
5610
|
-
/** @property {
|
|
5967
|
+
|
|
5968
|
+
/** @property {number} - The unique identifier of the medal */
|
|
5611
5969
|
this.id = id;
|
|
5612
|
-
|
|
5613
|
-
/** @property {
|
|
5970
|
+
|
|
5971
|
+
/** @property {string} - Name of the medal */
|
|
5614
5972
|
this.name = name;
|
|
5615
|
-
|
|
5616
|
-
/** @property {
|
|
5973
|
+
|
|
5974
|
+
/** @property {string} - Description of the medal */
|
|
5617
5975
|
this.description = description;
|
|
5618
|
-
|
|
5619
|
-
/** @property {
|
|
5976
|
+
|
|
5977
|
+
/** @property {string} - Icon for the medal */
|
|
5620
5978
|
this.icon = icon;
|
|
5621
|
-
|
|
5979
|
+
|
|
5622
5980
|
/** @property {boolean} - Is the medal unlocked? */
|
|
5623
5981
|
this.unlocked = false;
|
|
5624
5982
|
|
|
@@ -5643,7 +6001,7 @@ class Medal
|
|
|
5643
6001
|
}
|
|
5644
6002
|
|
|
5645
6003
|
/** Render a medal
|
|
5646
|
-
* @param {
|
|
6004
|
+
* @param {number} [hidePercent] - How much to slide the medal off screen
|
|
5647
6005
|
*/
|
|
5648
6006
|
render(hidePercent=0)
|
|
5649
6007
|
{
|
|
@@ -5684,7 +6042,7 @@ class Medal
|
|
|
5684
6042
|
|
|
5685
6043
|
/** Render the icon for a medal
|
|
5686
6044
|
* @param {Vector2} pos - Screen space position
|
|
5687
|
-
* @param {
|
|
6045
|
+
* @param {number} size - Screen space size
|
|
5688
6046
|
*/
|
|
5689
6047
|
renderIcon(pos, size)
|
|
5690
6048
|
{
|
|
@@ -5694,14 +6052,14 @@ class Medal
|
|
|
5694
6052
|
else
|
|
5695
6053
|
drawTextScreen(this.icon, pos, size*.7, BLACK);
|
|
5696
6054
|
}
|
|
5697
|
-
|
|
6055
|
+
|
|
5698
6056
|
// Get local storage key used by the medal
|
|
5699
6057
|
storageKey() { return medalsSaveName + '_' + this.id; }
|
|
5700
6058
|
}
|
|
5701
6059
|
/**
|
|
5702
6060
|
* LittleJS WebGL Interface
|
|
5703
|
-
* - All
|
|
5704
|
-
* - Will fall back to 2D canvas rendering if
|
|
6061
|
+
* - All WebGL used by the engine is wrapped up here
|
|
6062
|
+
* - Will fall back to 2D canvas rendering if WebGL is not supported
|
|
5705
6063
|
* - For normal stuff you won't need to see or call anything in this file
|
|
5706
6064
|
* - For advanced stuff there are helper functions to create shaders, textures, etc
|
|
5707
6065
|
* - Can be disabled with glEnable to revert to 2D canvas rendering
|
|
@@ -5716,24 +6074,27 @@ class Medal
|
|
|
5716
6074
|
* @memberof WebGL */
|
|
5717
6075
|
let glCanvas;
|
|
5718
6076
|
|
|
5719
|
-
/**
|
|
6077
|
+
/** WebGL2 context for `glCanvas`
|
|
5720
6078
|
* @type {WebGL2RenderingContext}
|
|
5721
6079
|
* @memberof WebGL */
|
|
5722
6080
|
let glContext;
|
|
5723
6081
|
|
|
5724
|
-
/** Should
|
|
6082
|
+
/** Should WebGL be setup with anti-aliasing? must be set before calling engineInit
|
|
5725
6083
|
* @type {boolean}
|
|
5726
6084
|
* @memberof WebGL */
|
|
5727
6085
|
let glAntialias = true;
|
|
5728
6086
|
|
|
5729
6087
|
// WebGL internal variables not exposed to documentation
|
|
5730
|
-
let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData,
|
|
6088
|
+
let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
|
|
5731
6089
|
|
|
5732
|
-
// WebGL internal constants
|
|
5733
|
-
const
|
|
6090
|
+
// WebGL internal constants
|
|
6091
|
+
const gl_ARRAY_BUFFER_SIZE = 5e5;
|
|
5734
6092
|
const gl_INDICES_PER_INSTANCE = 11;
|
|
5735
6093
|
const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
|
|
5736
|
-
const
|
|
6094
|
+
const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
|
|
6095
|
+
const gl_INDICES_PER_POLY_VERTEX = 3;
|
|
6096
|
+
const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
|
|
6097
|
+
const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
|
|
5737
6098
|
|
|
5738
6099
|
///////////////////////////////////////////////////////////////////////////////
|
|
5739
6100
|
|
|
@@ -5754,11 +6115,11 @@ function glInit()
|
|
|
5754
6115
|
return;
|
|
5755
6116
|
}
|
|
5756
6117
|
|
|
5757
|
-
// create the
|
|
6118
|
+
// create the WebGL canvas
|
|
5758
6119
|
const rootElement = mainCanvas.parentElement;
|
|
5759
6120
|
rootElement.appendChild(glCanvas);
|
|
5760
6121
|
|
|
5761
|
-
// setup
|
|
6122
|
+
// setup instanced rendering shader program
|
|
5762
6123
|
glShader = glCreateProgram(
|
|
5763
6124
|
'#version 300 es\n' + // specify GLSL ES version
|
|
5764
6125
|
'precision highp float;'+ // use highp for better accuracy
|
|
@@ -5786,40 +6147,59 @@ function glInit()
|
|
|
5786
6147
|
'}' // end of shader
|
|
5787
6148
|
);
|
|
5788
6149
|
|
|
6150
|
+
// setup poly rendering shaders
|
|
6151
|
+
glPolyShader = glCreateProgram(
|
|
6152
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
6153
|
+
'precision highp float;'+ // use highp for better accuracy
|
|
6154
|
+
'uniform mat4 m;'+ // transform matrix
|
|
6155
|
+
'in vec2 p;'+ // in: position
|
|
6156
|
+
'in vec4 c;'+ // in: color
|
|
6157
|
+
'out vec4 d;'+ // out: color
|
|
6158
|
+
'void main(){'+ // shader entry point
|
|
6159
|
+
'gl_Position=m*vec4(p,1,1);'+ // transform position
|
|
6160
|
+
'd=c;'+ // pass color to fragment shader
|
|
6161
|
+
'}' // end of shader
|
|
6162
|
+
,
|
|
6163
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
6164
|
+
'precision highp float;'+ // use highp for better accuracy
|
|
6165
|
+
'in vec4 d;'+ // in: color
|
|
6166
|
+
'out vec4 c;'+ // out: color
|
|
6167
|
+
'void main(){'+ // shader entry point
|
|
6168
|
+
'c=d;'+ // set color
|
|
6169
|
+
'}' // end of shader
|
|
6170
|
+
);
|
|
6171
|
+
|
|
5789
6172
|
// init buffers
|
|
5790
|
-
const glInstanceData = new ArrayBuffer(
|
|
6173
|
+
const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
|
|
5791
6174
|
glPositionData = new Float32Array(glInstanceData);
|
|
5792
6175
|
glColorData = new Uint32Array(glInstanceData);
|
|
5793
6176
|
glArrayBuffer = glContext.createBuffer();
|
|
5794
6177
|
glGeometryBuffer = glContext.createBuffer();
|
|
5795
6178
|
|
|
5796
6179
|
// create the geometry buffer, triangle strip square
|
|
5797
|
-
const geometry = new Float32Array([
|
|
6180
|
+
const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
|
|
5798
6181
|
glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
|
|
5799
6182
|
glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
|
|
5800
6183
|
}
|
|
5801
6184
|
|
|
5802
|
-
|
|
5803
|
-
// Also used by tile layer rendering when redrawing tiles
|
|
5804
|
-
function glPreRender()
|
|
6185
|
+
function glSetInstancedMode()
|
|
5805
6186
|
{
|
|
5806
|
-
if (!
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
6187
|
+
if (!glPolyMode)
|
|
6188
|
+
return;
|
|
6189
|
+
|
|
6190
|
+
// setup instanced mode
|
|
6191
|
+
glFlush();
|
|
6192
|
+
glPolyMode = false;
|
|
5810
6193
|
glContext.useProgram(glShader);
|
|
5811
|
-
glContext.activeTexture(glContext.TEXTURE0);
|
|
5812
|
-
if (textureInfos[0])
|
|
5813
|
-
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
|
|
5814
6194
|
|
|
5815
6195
|
// set vertex attributes
|
|
5816
|
-
let offset =
|
|
6196
|
+
let offset = 0;
|
|
5817
6197
|
const initVertexAttribArray = (name, type, typeSize, size)=>
|
|
5818
6198
|
{
|
|
5819
6199
|
const location = glContext.getAttribLocation(glShader, name);
|
|
5820
6200
|
const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
|
|
5821
6201
|
const divisor = typeSize && 1; // only if not geometry
|
|
5822
|
-
const normalize = typeSize
|
|
6202
|
+
const normalize = typeSize === 1; // only if color
|
|
5823
6203
|
glContext.enableVertexAttribArray(location);
|
|
5824
6204
|
glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
|
|
5825
6205
|
glContext.vertexAttribDivisor(location, divisor);
|
|
@@ -5828,26 +6208,87 @@ function glPreRender()
|
|
|
5828
6208
|
glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
|
|
5829
6209
|
initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
|
|
5830
6210
|
glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
|
|
5831
|
-
glContext.bufferData(glContext.ARRAY_BUFFER,
|
|
6211
|
+
glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
|
|
5832
6212
|
initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
|
|
5833
6213
|
initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
|
|
5834
6214
|
initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
|
|
5835
6215
|
initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
|
|
5836
6216
|
initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
|
|
6217
|
+
}
|
|
6218
|
+
|
|
6219
|
+
function glSetPolyMode()
|
|
6220
|
+
{
|
|
6221
|
+
if (glPolyMode)
|
|
6222
|
+
return;
|
|
5837
6223
|
|
|
6224
|
+
// setup poly mode
|
|
6225
|
+
glFlush();
|
|
6226
|
+
glPolyMode = true;
|
|
6227
|
+
glContext.useProgram(glPolyShader);
|
|
6228
|
+
|
|
6229
|
+
// set vertex attributes
|
|
6230
|
+
let offset = 0;
|
|
6231
|
+
const initVertexAttribArray = (name, type, typeSize, size)=>
|
|
6232
|
+
{
|
|
6233
|
+
const location = glContext.getAttribLocation(glPolyShader, name);
|
|
6234
|
+
const normalize = typeSize === 1; // only normalize if color
|
|
6235
|
+
const stride = gl_POLY_VERTEX_BYTE_STRIDE;
|
|
6236
|
+
glContext.enableVertexAttribArray(location);
|
|
6237
|
+
glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
|
|
6238
|
+
glContext.vertexAttribDivisor(location, 0);
|
|
6239
|
+
offset += size*typeSize;
|
|
6240
|
+
}
|
|
6241
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
|
|
6242
|
+
glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
|
|
6243
|
+
initVertexAttribArray('p', glContext.FLOAT, 4, 2); // position
|
|
6244
|
+
initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
|
|
6245
|
+
}
|
|
6246
|
+
|
|
6247
|
+
// Setup WebGL render each frame, called automatically by engine
|
|
6248
|
+
// Also used by tile layer rendering when redrawing tiles
|
|
6249
|
+
function glPreRender()
|
|
6250
|
+
{
|
|
6251
|
+
if (!glEnable || !glContext) return;
|
|
6252
|
+
|
|
6253
|
+
// clear the canvas
|
|
6254
|
+
glClearCanvas();
|
|
6255
|
+
|
|
5838
6256
|
// build the transform matrix
|
|
5839
6257
|
const s = vec2(2*cameraScale).divide(mainCanvasSize);
|
|
5840
6258
|
const rotatedCam = cameraPos.rotate(-cameraAngle);
|
|
5841
6259
|
const p = vec2(-1).subtract(rotatedCam.multiply(s));
|
|
5842
6260
|
const ca = Math.cos(cameraAngle);
|
|
5843
6261
|
const sa = Math.sin(cameraAngle);
|
|
5844
|
-
|
|
5845
|
-
[
|
|
6262
|
+
const transform = [
|
|
5846
6263
|
s.x * ca, s.y * sa, 0, 0,
|
|
5847
6264
|
-s.x * sa, s.y * ca, 0, 0,
|
|
5848
6265
|
1, 1, 1, 0,
|
|
5849
|
-
p.x, p.y, 0, 1
|
|
5850
|
-
|
|
6266
|
+
p.x, p.y, 0, 1];
|
|
6267
|
+
|
|
6268
|
+
// set the same transform matrix for both shaders
|
|
6269
|
+
const initUniform = (program, uniform, value) =>
|
|
6270
|
+
{
|
|
6271
|
+
glContext.useProgram(program);
|
|
6272
|
+
const location = glContext.getUniformLocation(program, uniform);
|
|
6273
|
+
glContext.uniformMatrix4fv(location, false, value);
|
|
6274
|
+
}
|
|
6275
|
+
initUniform(glPolyShader, 'm', transform);
|
|
6276
|
+
initUniform(glShader, 'm', transform);
|
|
6277
|
+
|
|
6278
|
+
// set the active texture
|
|
6279
|
+
glContext.activeTexture(glContext.TEXTURE0);
|
|
6280
|
+
if (textureInfos[0])
|
|
6281
|
+
{
|
|
6282
|
+
glActiveTexture = textureInfos[0].glTexture;
|
|
6283
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
6284
|
+
}
|
|
6285
|
+
|
|
6286
|
+
// start with additive blending off
|
|
6287
|
+
glAdditive = glBatchAdditive = false;
|
|
6288
|
+
|
|
6289
|
+
// force it to enter instanced mode
|
|
6290
|
+
glPolyMode = true;
|
|
6291
|
+
glSetInstancedMode();
|
|
5851
6292
|
}
|
|
5852
6293
|
|
|
5853
6294
|
/** Clear the canvas and setup the viewport
|
|
@@ -5855,21 +6296,23 @@ function glPreRender()
|
|
|
5855
6296
|
function glClearCanvas()
|
|
5856
6297
|
{
|
|
5857
6298
|
if (!glContext) return;
|
|
5858
|
-
|
|
6299
|
+
|
|
5859
6300
|
// clear and set to same size as main canvas
|
|
5860
|
-
|
|
6301
|
+
glCanvas.width = drawCanvas.width;
|
|
6302
|
+
glCanvas.height = drawCanvas.height;
|
|
6303
|
+
glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
|
|
5861
6304
|
glContext.clear(glContext.COLOR_BUFFER_BIT);
|
|
5862
6305
|
}
|
|
5863
6306
|
|
|
5864
|
-
/** Set the
|
|
6307
|
+
/** Set the WebGL texture, called automatically if using multiple textures
|
|
5865
6308
|
* - This may also flush the gl buffer resulting in more draw calls and worse performance
|
|
5866
6309
|
* @param {WebGLTexture} texture
|
|
5867
|
-
* @param {boolean} wrap - Should the texture wrap or clamp
|
|
6310
|
+
* @param {boolean} [wrap] - Should the texture wrap or clamp
|
|
5868
6311
|
* @memberof WebGL */
|
|
5869
6312
|
function glSetTexture(texture, wrap=false)
|
|
5870
6313
|
{
|
|
5871
6314
|
// must flush cache with the old texture to set a new one
|
|
5872
|
-
if (!glContext || texture
|
|
6315
|
+
if (!glContext || texture === glActiveTexture)
|
|
5873
6316
|
return;
|
|
5874
6317
|
|
|
5875
6318
|
glFlush();
|
|
@@ -5882,8 +6325,8 @@ function glSetTexture(texture, wrap=false)
|
|
|
5882
6325
|
}
|
|
5883
6326
|
|
|
5884
6327
|
/** Compile WebGL shader of the given type, will throw errors if in debug mode
|
|
5885
|
-
* @param {
|
|
5886
|
-
* @param {
|
|
6328
|
+
* @param {string} source
|
|
6329
|
+
* @param {number} type
|
|
5887
6330
|
* @return {WebGLShader}
|
|
5888
6331
|
* @memberof WebGL */
|
|
5889
6332
|
function glCompileShader(source, type)
|
|
@@ -5902,8 +6345,8 @@ function glCompileShader(source, type)
|
|
|
5902
6345
|
}
|
|
5903
6346
|
|
|
5904
6347
|
/** Create WebGL program with given shaders
|
|
5905
|
-
* @param {
|
|
5906
|
-
* @param {
|
|
6348
|
+
* @param {string} vsSource
|
|
6349
|
+
* @param {string} fsSource
|
|
5907
6350
|
* @return {WebGLProgram}
|
|
5908
6351
|
* @memberof WebGL */
|
|
5909
6352
|
function glCreateProgram(vsSource, fsSource)
|
|
@@ -5951,7 +6394,7 @@ function glCreateTexture(image)
|
|
|
5951
6394
|
const whitePixel = new Uint8Array([255, 255, 255, 255]);
|
|
5952
6395
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
|
|
5953
6396
|
}
|
|
5954
|
-
|
|
6397
|
+
|
|
5955
6398
|
// set texture filtering
|
|
5956
6399
|
const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
|
|
5957
6400
|
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
|
|
@@ -5959,7 +6402,6 @@ function glCreateTexture(image)
|
|
|
5959
6402
|
return texture;
|
|
5960
6403
|
}
|
|
5961
6404
|
|
|
5962
|
-
|
|
5963
6405
|
/** Deletes a WebGL texture
|
|
5964
6406
|
* @param {WebGLTexture} [texture]
|
|
5965
6407
|
* @memberof WebGL */
|
|
@@ -5987,18 +6429,25 @@ function glSetTextureData(texture, image)
|
|
|
5987
6429
|
* @memberof WebGL */
|
|
5988
6430
|
function glFlush()
|
|
5989
6431
|
{
|
|
5990
|
-
if (
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6432
|
+
if (glEnable && glContext && glBatchCount)
|
|
6433
|
+
{
|
|
6434
|
+
// set bend mode
|
|
6435
|
+
const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
|
|
6436
|
+
glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
|
|
6437
|
+
glContext.enable(glContext.BLEND);
|
|
6438
|
+
|
|
6439
|
+
const byteLength = glBatchCount *
|
|
6440
|
+
(glPolyMode ? gl_INDICES_PER_POLY_VERTEX : gl_INDICES_PER_INSTANCE);
|
|
6441
|
+
glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData, 0, byteLength);
|
|
6442
|
+
|
|
6443
|
+
// draw the batch
|
|
6444
|
+
if (glPolyMode)
|
|
6445
|
+
glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
|
|
6446
|
+
else
|
|
6447
|
+
glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
|
|
6448
|
+
drawCount += glBatchCount;
|
|
6449
|
+
glBatchCount = 0;
|
|
6450
|
+
}
|
|
6002
6451
|
glBatchAdditive = glAdditive;
|
|
6003
6452
|
}
|
|
6004
6453
|
|
|
@@ -6014,7 +6463,8 @@ function glCopyToContext(context)
|
|
|
6014
6463
|
context.drawImage(glCanvas, 0, 0);
|
|
6015
6464
|
}
|
|
6016
6465
|
|
|
6017
|
-
/** Set anti-aliasing for
|
|
6466
|
+
/** Set anti-aliasing for WebGL canvas
|
|
6467
|
+
* Must be called before engineInit
|
|
6018
6468
|
* @param {boolean} [antialias]
|
|
6019
6469
|
* @memberof WebGL */
|
|
6020
6470
|
function glSetAntialias(antialias=true)
|
|
@@ -6024,25 +6474,27 @@ function glSetAntialias(antialias=true)
|
|
|
6024
6474
|
}
|
|
6025
6475
|
|
|
6026
6476
|
/** Add a sprite to the gl draw list, used by all gl draw functions
|
|
6027
|
-
* @param {
|
|
6028
|
-
* @param {
|
|
6029
|
-
* @param {
|
|
6030
|
-
* @param {
|
|
6031
|
-
* @param {
|
|
6032
|
-
* @param {
|
|
6033
|
-
* @param {
|
|
6034
|
-
* @param {
|
|
6035
|
-
* @param {
|
|
6036
|
-
* @param {
|
|
6037
|
-
* @param {
|
|
6477
|
+
* @param {number} x
|
|
6478
|
+
* @param {number} y
|
|
6479
|
+
* @param {number} sizeX
|
|
6480
|
+
* @param {number} sizeY
|
|
6481
|
+
* @param {number} [angle]
|
|
6482
|
+
* @param {number} [uv0X]
|
|
6483
|
+
* @param {number} [uv0Y]
|
|
6484
|
+
* @param {number} [uv1X]
|
|
6485
|
+
* @param {number} [uv1Y]
|
|
6486
|
+
* @param {number} [rgba=-1] - white is -1
|
|
6487
|
+
* @param {number} [rgbaAdditive=0] - black is 0
|
|
6038
6488
|
* @memberof WebGL */
|
|
6039
6489
|
function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
|
|
6040
6490
|
{
|
|
6041
6491
|
// flush if there is not enough room or if different blend mode
|
|
6042
|
-
if (
|
|
6492
|
+
if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
|
|
6043
6493
|
glFlush();
|
|
6494
|
+
glSetInstancedMode();
|
|
6044
6495
|
|
|
6045
|
-
|
|
6496
|
+
glPolyMode = false;
|
|
6497
|
+
let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
|
|
6046
6498
|
glPositionData[offset++] = x;
|
|
6047
6499
|
glPositionData[offset++] = y;
|
|
6048
6500
|
glPositionData[offset++] = sizeX;
|
|
@@ -6054,6 +6506,312 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
6054
6506
|
glColorData[offset++] = rgba;
|
|
6055
6507
|
glColorData[offset++] = rgbaAdditive;
|
|
6056
6508
|
glPositionData[offset++] = angle;
|
|
6509
|
+
}
|
|
6510
|
+
|
|
6511
|
+
/** Transform and add a polygon to the gl draw list
|
|
6512
|
+
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
6513
|
+
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
6514
|
+
* @param {number} x
|
|
6515
|
+
* @param {number} y
|
|
6516
|
+
* @param {number} sx
|
|
6517
|
+
* @param {number} sy
|
|
6518
|
+
* @param {number} angle
|
|
6519
|
+
* @param {boolean} [tristrip] - should tristrip algorithm be used
|
|
6520
|
+
* @memberof WebGL */
|
|
6521
|
+
function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
6522
|
+
{
|
|
6523
|
+
const pointsOut = [];
|
|
6524
|
+
for (const p of points)
|
|
6525
|
+
{
|
|
6526
|
+
// transform the point
|
|
6527
|
+
const px = p.x*sx;
|
|
6528
|
+
const py = p.y*sy;
|
|
6529
|
+
const sa = Math.sin(-angle);
|
|
6530
|
+
const ca = Math.cos(-angle);
|
|
6531
|
+
pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
|
|
6532
|
+
}
|
|
6533
|
+
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
6534
|
+
glDrawPoints(drawPoints, rgba);
|
|
6535
|
+
}
|
|
6536
|
+
|
|
6537
|
+
/** Transform and add a polygon to the gl draw list
|
|
6538
|
+
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
6539
|
+
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
6540
|
+
* @param {number} lineWidth - Width of the outline
|
|
6541
|
+
* @param {number} x
|
|
6542
|
+
* @param {number} y
|
|
6543
|
+
* @param {number} sx
|
|
6544
|
+
* @param {number} sy
|
|
6545
|
+
* @param {number} angle
|
|
6546
|
+
* @param {boolean} [wrap] - Should the outline connect the first and last points
|
|
6547
|
+
* @memberof WebGL */
|
|
6548
|
+
function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle, wrap=true)
|
|
6549
|
+
{
|
|
6550
|
+
const outlinePoints = glMakeOutline(points, lineWidth, wrap);
|
|
6551
|
+
glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
|
|
6552
|
+
}
|
|
6553
|
+
|
|
6554
|
+
/** Add a list of points to the gl draw list
|
|
6555
|
+
* @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
|
|
6556
|
+
* @param {number} rgba - Color as a 32-bit integer
|
|
6557
|
+
* @memberof WebGL */
|
|
6558
|
+
function glDrawPoints(points, rgba)
|
|
6559
|
+
{
|
|
6560
|
+
if (!glEnable || points.length < 3)
|
|
6561
|
+
return; // needs at least 3 points to have area
|
|
6562
|
+
|
|
6563
|
+
// flush if there is not enough room or if different blend mode
|
|
6564
|
+
const vertCount = points.length + 2;
|
|
6565
|
+
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
6566
|
+
glFlush();
|
|
6567
|
+
glSetPolyMode();
|
|
6568
|
+
|
|
6569
|
+
// setup triangle strip with degenerate verts at start and end
|
|
6570
|
+
let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
|
|
6571
|
+
for(let i = vertCount; i--;)
|
|
6572
|
+
{
|
|
6573
|
+
const j = clamp(i-1, 0, vertCount-3);
|
|
6574
|
+
const point = points[j];
|
|
6575
|
+
glPositionData[offset++] = point.x;
|
|
6576
|
+
glPositionData[offset++] = point.y;
|
|
6577
|
+
glColorData[offset++] = rgba;
|
|
6578
|
+
}
|
|
6579
|
+
glBatchCount += vertCount;
|
|
6580
|
+
}
|
|
6581
|
+
|
|
6582
|
+
/** Add a list of colored points to the gl draw list
|
|
6583
|
+
* @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
|
|
6584
|
+
* @param {Array<number>} pointColors - Array of 32-bit integer colors
|
|
6585
|
+
* @memberof WebGL */
|
|
6586
|
+
function glDrawColoredPoints(points, pointColors)
|
|
6587
|
+
{
|
|
6588
|
+
if (!glEnable || points.length < 3)
|
|
6589
|
+
return; // needs at least 3 points to have area
|
|
6590
|
+
|
|
6591
|
+
// flush if there is not enough room or if different blend mode
|
|
6592
|
+
const vertCount = points.length + 2;
|
|
6593
|
+
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
6594
|
+
glFlush();
|
|
6595
|
+
glSetPolyMode();
|
|
6596
|
+
|
|
6597
|
+
// setup triangle strip with degenerate verts at start and end
|
|
6598
|
+
let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
|
|
6599
|
+
for(let i = vertCount; i--;)
|
|
6600
|
+
{
|
|
6601
|
+
const j = clamp(i-1, 0, vertCount-3);
|
|
6602
|
+
const point = points[j];
|
|
6603
|
+
const color = pointColors[j];
|
|
6604
|
+
glPositionData[offset++] = point.x;
|
|
6605
|
+
glPositionData[offset++] = point.y;
|
|
6606
|
+
glColorData[offset++] = color;
|
|
6607
|
+
}
|
|
6608
|
+
glBatchCount += vertCount;
|
|
6609
|
+
}
|
|
6610
|
+
|
|
6611
|
+
// WebGL internal function to convert polygon to outline triangle strip
|
|
6612
|
+
function glMakeOutline(points, width, wrap=true)
|
|
6613
|
+
{
|
|
6614
|
+
if (points.length < 2)
|
|
6615
|
+
return [];
|
|
6616
|
+
|
|
6617
|
+
const halfWidth = width / 2;
|
|
6618
|
+
const strip = [];
|
|
6619
|
+
const n = points.length;
|
|
6620
|
+
const e = 1e-6;
|
|
6621
|
+
const miterLimit = width*100;
|
|
6622
|
+
for (let i = 0; i < n; i++)
|
|
6623
|
+
{
|
|
6624
|
+
// for each vertex, calculate normal based on adjacent edges
|
|
6625
|
+
const prev = points[wrap ? (i - 1 + n) % n : max(i - 1, 0)];
|
|
6626
|
+
const curr = points[i];
|
|
6627
|
+
const next = points[wrap ? (i + 1) % n : min(i + 1, n - 1)];
|
|
6628
|
+
|
|
6629
|
+
// direction from previous to current
|
|
6630
|
+
const dx1 = curr.x - prev.x;
|
|
6631
|
+
const dy1 = curr.y - prev.y;
|
|
6632
|
+
const len1 = (dx1*dx1 + dy1*dy1)**.5;
|
|
6633
|
+
|
|
6634
|
+
// direction from current to next
|
|
6635
|
+
const dx2 = next.x - curr.x;
|
|
6636
|
+
const dy2 = next.y - curr.y;
|
|
6637
|
+
const len2 = (dx2*dx2 + dy2*dy2)**.5;
|
|
6638
|
+
|
|
6639
|
+
if (len1 < e && len2 < e)
|
|
6640
|
+
continue; // skip degenerate point
|
|
6641
|
+
|
|
6642
|
+
// calculate perpendicular normals for each edge
|
|
6643
|
+
const nx1 = len1 > e ? -dy1 / len1 : 0;
|
|
6644
|
+
const ny1 = len1 > e ? dx1 / len1 : 0;
|
|
6645
|
+
const nx2 = len2 > e ? -dy2 / len2 : 0;
|
|
6646
|
+
const ny2 = len2 > e ? dx2 / len2 : 0;
|
|
6647
|
+
|
|
6648
|
+
// average the normals for miter
|
|
6649
|
+
let nx = nx1 + nx2;
|
|
6650
|
+
let ny = ny1 + ny2;
|
|
6651
|
+
const nlen = (nx*nx + ny*ny)**.5;
|
|
6652
|
+
if (nlen < e)
|
|
6653
|
+
{
|
|
6654
|
+
// 180 degree turn - use perpendicular
|
|
6655
|
+
nx = nx1;
|
|
6656
|
+
ny = ny1;
|
|
6657
|
+
}
|
|
6658
|
+
else
|
|
6659
|
+
{
|
|
6660
|
+
// calculate miter length
|
|
6661
|
+
nx /= nlen;
|
|
6662
|
+
ny /= nlen;
|
|
6663
|
+
const dot = nx1 * nx + ny1 * ny;
|
|
6664
|
+
if (dot > e)
|
|
6665
|
+
{
|
|
6666
|
+
// scale normal by miter length, clamped to miterLimit
|
|
6667
|
+
const miterLength = min(1 / dot, miterLimit);
|
|
6668
|
+
nx *= miterLength;
|
|
6669
|
+
ny *= miterLength;
|
|
6670
|
+
}
|
|
6671
|
+
}
|
|
6672
|
+
|
|
6673
|
+
// create inner and outer points along the normal
|
|
6674
|
+
const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
|
|
6675
|
+
const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
|
|
6676
|
+
strip.push(inner);
|
|
6677
|
+
strip.push(outer);
|
|
6678
|
+
}
|
|
6679
|
+
if (strip.length > 1 && wrap)
|
|
6680
|
+
{
|
|
6681
|
+
// close the loop
|
|
6682
|
+
strip.push(strip[0]);
|
|
6683
|
+
strip.push(strip[1]);
|
|
6684
|
+
}
|
|
6685
|
+
return strip;
|
|
6686
|
+
}
|
|
6687
|
+
|
|
6688
|
+
// WebGL internal function to convert polys to tri strips
|
|
6689
|
+
function glPolyStrip(points)
|
|
6690
|
+
{
|
|
6691
|
+
// validate input
|
|
6692
|
+
if (points.length < 3)
|
|
6693
|
+
return [];
|
|
6694
|
+
|
|
6695
|
+
// cross product helper: (b-a) x (c-a)
|
|
6696
|
+
const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
6697
|
+
|
|
6698
|
+
// calculate signed area of polygon
|
|
6699
|
+
const signedArea = (poly)=>
|
|
6700
|
+
{
|
|
6701
|
+
let area = 0;
|
|
6702
|
+
for (let i = poly.length; i--;)
|
|
6703
|
+
{
|
|
6704
|
+
const j = (i+1) % poly.length;
|
|
6705
|
+
area += poly[i].cross(poly[j]);
|
|
6706
|
+
}
|
|
6707
|
+
return area;
|
|
6708
|
+
}
|
|
6709
|
+
|
|
6710
|
+
// ensure counter-clockwise winding
|
|
6711
|
+
if (signedArea(points) < 0)
|
|
6712
|
+
points = points.reverse();
|
|
6713
|
+
|
|
6714
|
+
// check if point is inside triangle
|
|
6715
|
+
const e = 1e-9;
|
|
6716
|
+
const pointInTriangle = (p, a, b, c)=>
|
|
6717
|
+
{
|
|
6718
|
+
const c1 = cross(a, b, p);
|
|
6719
|
+
const c2 = cross(b, c, p);
|
|
6720
|
+
const c3 = cross(c, a, p);
|
|
6721
|
+
const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
|
|
6722
|
+
const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
|
|
6723
|
+
return !(negative && positive);
|
|
6724
|
+
};
|
|
6725
|
+
|
|
6726
|
+
// ear clipping triangulation
|
|
6727
|
+
const indices = [];
|
|
6728
|
+
for (let i = 0; i < points.length; ++i)
|
|
6729
|
+
indices[i] = i;
|
|
6730
|
+
const triangles = [];
|
|
6731
|
+
let attempts = 0;
|
|
6732
|
+
const maxAttempts = points.length ** 2 + 100;
|
|
6733
|
+
while (indices.length > 3 && attempts++ < maxAttempts)
|
|
6734
|
+
{
|
|
6735
|
+
let foundEar = false;
|
|
6736
|
+
for (let i = indices.length; --i;)
|
|
6737
|
+
{
|
|
6738
|
+
const i0 = indices[(i + indices.length - 1) % indices.length];
|
|
6739
|
+
const i1 = indices[i];
|
|
6740
|
+
const i2 = indices[(i + 1) % indices.length];
|
|
6741
|
+
const a = points[i0], b = points[i1], c = points[i2];
|
|
6742
|
+
|
|
6743
|
+
// check if convex
|
|
6744
|
+
if (cross(a, b, c) < e)
|
|
6745
|
+
continue;
|
|
6746
|
+
|
|
6747
|
+
// check if any other point is inside
|
|
6748
|
+
let hasInside = false;
|
|
6749
|
+
for (let j = 0; j < indices.length; j++)
|
|
6750
|
+
{
|
|
6751
|
+
const k = indices[j];
|
|
6752
|
+
if (k === i0 || k === i1 || k === i2)
|
|
6753
|
+
continue;
|
|
6754
|
+
const p = points[k];
|
|
6755
|
+
hasInside = pointInTriangle(p, a, b, c);
|
|
6756
|
+
if (hasInside)
|
|
6757
|
+
break;
|
|
6758
|
+
}
|
|
6759
|
+
if (hasInside)
|
|
6760
|
+
continue;
|
|
6761
|
+
|
|
6762
|
+
// found valid ear
|
|
6763
|
+
triangles.push([i0, i1, i2]);
|
|
6764
|
+
indices.splice(i, 1);
|
|
6765
|
+
foundEar = true;
|
|
6766
|
+
break;
|
|
6767
|
+
}
|
|
6768
|
+
|
|
6769
|
+
// fallback for degenerate cases
|
|
6770
|
+
if (!foundEar)
|
|
6771
|
+
{
|
|
6772
|
+
let worstIndex = -1, worstValue = Infinity;
|
|
6773
|
+
for (let i = indices.length; --i;)
|
|
6774
|
+
{
|
|
6775
|
+
const i0 = indices[(i + indices.length - 1) % indices.length];
|
|
6776
|
+
const i1 = indices[i];
|
|
6777
|
+
const i2 = indices[(i + 1) % indices.length];
|
|
6778
|
+
const value = abs(cross(points[i0], points[i1], points[i2]));
|
|
6779
|
+
if (value < worstValue)
|
|
6780
|
+
{
|
|
6781
|
+
worstValue = value;
|
|
6782
|
+
worstIndex = i;
|
|
6783
|
+
}
|
|
6784
|
+
}
|
|
6785
|
+
if (worstIndex < 0)
|
|
6786
|
+
break;
|
|
6787
|
+
|
|
6788
|
+
const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
|
|
6789
|
+
const i1 = indices[worstIndex];
|
|
6790
|
+
const i2 = indices[(worstIndex + 1) % indices.length];
|
|
6791
|
+
triangles.push([i0, i1, i2]);
|
|
6792
|
+
indices.splice(worstIndex, 1);
|
|
6793
|
+
}
|
|
6794
|
+
}
|
|
6795
|
+
|
|
6796
|
+
// add final triangle
|
|
6797
|
+
if (indices.length === 3)
|
|
6798
|
+
triangles.push([indices[0], indices[1], indices[2]]);
|
|
6799
|
+
if (!triangles.length)
|
|
6800
|
+
return [];
|
|
6801
|
+
|
|
6802
|
+
// convert triangles to triangle strip with degenerate connectors
|
|
6803
|
+
const strip = [];
|
|
6804
|
+
let [a0, b0, c0] = triangles[0];
|
|
6805
|
+
strip.push(points[a0], points[b0], points[c0]);
|
|
6806
|
+
for (let i = 1; i < triangles.length; i++)
|
|
6807
|
+
{
|
|
6808
|
+
// add degenerate bridge from last vertex to first of new triangle
|
|
6809
|
+
const [a, b, c] = triangles[i];
|
|
6810
|
+
strip.push(points[c0], points[a]);
|
|
6811
|
+
strip.push(points[a], points[b], points[c]);
|
|
6812
|
+
c0 = c;
|
|
6813
|
+
}
|
|
6814
|
+
return strip;
|
|
6057
6815
|
}
|
|
6058
6816
|
/**
|
|
6059
6817
|
* LittleJS Newgrounds API
|
|
@@ -6078,11 +6836,11 @@ let newgrounds;
|
|
|
6078
6836
|
class NewgroundsMedal extends Medal
|
|
6079
6837
|
{
|
|
6080
6838
|
/** Create a newgrounds medal object and adds it to the list of medals
|
|
6081
|
-
* @param {
|
|
6082
|
-
* @param {
|
|
6083
|
-
* @param {
|
|
6084
|
-
* @param {
|
|
6085
|
-
* @param {
|
|
6839
|
+
* @param {number} id - The unique identifier of the medal
|
|
6840
|
+
* @param {string} name - Name of the medal
|
|
6841
|
+
* @param {string} [description] - Description of the medal
|
|
6842
|
+
* @param {string} [icon] - Icon for the medal
|
|
6843
|
+
* @param {string} [src] - Image location for the medal
|
|
6086
6844
|
*/
|
|
6087
6845
|
constructor(id, name, description, icon, src)
|
|
6088
6846
|
{ super(id, name, description, icon, src); }
|
|
@@ -6174,7 +6932,7 @@ class NewgroundsPlugin
|
|
|
6174
6932
|
* @param {number} id - The scoreboard id
|
|
6175
6933
|
* @param {string} [user] - A user's id or name
|
|
6176
6934
|
* @param {number} [social] - If true, only social scores will be loaded
|
|
6177
|
-
* @param {number} [skip] - Number of scores to skip
|
|
6935
|
+
* @param {number} [skip] - Number of scores to skip over
|
|
6178
6936
|
* @param {number} [limit] - Number of scores to include in the list
|
|
6179
6937
|
* @return {Object} - The response JSON object
|
|
6180
6938
|
*/
|
|
@@ -6397,16 +7155,16 @@ class ZzFXMusic extends Sound
|
|
|
6397
7155
|
if (!soundEnable || headlessMode) return;
|
|
6398
7156
|
this.randomness = 0;
|
|
6399
7157
|
this.sampleChannels = zzfxM(...zzfxMusic);
|
|
6400
|
-
this.sampleRate =
|
|
7158
|
+
this.sampleRate = audioDefaultSampleRate;
|
|
6401
7159
|
}
|
|
6402
7160
|
|
|
6403
|
-
/** Play the music
|
|
6404
|
-
* @param {number} [volume
|
|
6405
|
-
* @param {boolean} [loop] -
|
|
7161
|
+
/** Play the music that loops by default
|
|
7162
|
+
* @param {number} [volume] - Volume to play the music at
|
|
7163
|
+
* @param {boolean} [loop] - Should the music loop?
|
|
6406
7164
|
* @return {AudioBufferSourceNode} - The audio source node
|
|
6407
7165
|
*/
|
|
6408
|
-
playMusic(volume, loop=
|
|
6409
|
-
{ return super.play(undefined, volume, 1,
|
|
7166
|
+
playMusic(volume=1, loop=true)
|
|
7167
|
+
{ return super.play(undefined, volume, 1, 0, loop); }
|
|
6410
7168
|
}
|
|
6411
7169
|
|
|
6412
7170
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -6440,7 +7198,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
6440
7198
|
let panning = 0;
|
|
6441
7199
|
let hasMore = 1;
|
|
6442
7200
|
let sampleCache = {};
|
|
6443
|
-
let beatLength =
|
|
7201
|
+
let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
|
|
6444
7202
|
|
|
6445
7203
|
// for each channel in order until there are no more
|
|
6446
7204
|
for (; hasMore; channelIndex++) {
|
|
@@ -6459,15 +7217,15 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
6459
7217
|
// get next offset, use the length of first channel
|
|
6460
7218
|
nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
|
|
6461
7219
|
// for each beat in pattern, plus one extra if end of sequence
|
|
6462
|
-
isSequenceEnd = sequenceIndex
|
|
7220
|
+
isSequenceEnd = sequenceIndex === sequence.length - 1;
|
|
6463
7221
|
for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
|
|
6464
7222
|
|
|
6465
7223
|
// <channel-note>
|
|
6466
7224
|
note = patternChannel[i];
|
|
6467
7225
|
|
|
6468
7226
|
// stop if end, different instrument or new note
|
|
6469
|
-
stop = i
|
|
6470
|
-
instrument
|
|
7227
|
+
stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
|
|
7228
|
+
instrument !== (patternChannel[0] || 0) || note | 0;
|
|
6471
7229
|
|
|
6472
7230
|
// fill buffer with samples for previous beat, most cpu intensive part
|
|
6473
7231
|
for (j = 0; j < beatLength && notFirstBeat;
|
|
@@ -6552,11 +7310,11 @@ class UISystemPlugin
|
|
|
6552
7310
|
/** @property {Color} - Default text color for UI elements */
|
|
6553
7311
|
this.defaultTextColor = BLACK;
|
|
6554
7312
|
/** @property {Color} - Default button color for UI elements */
|
|
6555
|
-
this.defaultButtonColor = hsl(0,0,.
|
|
7313
|
+
this.defaultButtonColor = hsl(0,0,.7);
|
|
6556
7314
|
/** @property {Color} - Default hover color for UI elements */
|
|
6557
|
-
this.defaultHoverColor = hsl(0,0,.
|
|
7315
|
+
this.defaultHoverColor = hsl(0,0,.9);
|
|
6558
7316
|
/** @property {Color} - Default color for disabled UI elements */
|
|
6559
|
-
this.defaultDisabledColor = hsl(0,0,.
|
|
7317
|
+
this.defaultDisabledColor = hsl(0,0,.3);
|
|
6560
7318
|
/** @property {number} - Default line width for UI elements */
|
|
6561
7319
|
this.defaultLineWidth = 4;
|
|
6562
7320
|
/** @property {number} - Default rounded rect corner radius for UI elements */
|
|
@@ -6576,16 +7334,16 @@ class UISystemPlugin
|
|
|
6576
7334
|
|
|
6577
7335
|
engineAddPlugin(uiUpdate, uiRender);
|
|
6578
7336
|
|
|
6579
|
-
function updateInvisible(o)
|
|
6580
|
-
{
|
|
6581
|
-
for (const c of o.children)
|
|
6582
|
-
updateInvisible(c);
|
|
6583
|
-
o.updateInvisible();
|
|
6584
|
-
}
|
|
6585
|
-
|
|
6586
7337
|
// setup recursive update and render
|
|
6587
7338
|
function uiUpdate()
|
|
6588
7339
|
{
|
|
7340
|
+
function updateInvisibleObject(o)
|
|
7341
|
+
{
|
|
7342
|
+
// update invisible objects
|
|
7343
|
+
for (const c of o.children)
|
|
7344
|
+
updateInvisibleObject(c);
|
|
7345
|
+
o.updateInvisible();
|
|
7346
|
+
}
|
|
6589
7347
|
function updateObject(o)
|
|
6590
7348
|
{
|
|
6591
7349
|
if (o.visible)
|
|
@@ -6599,7 +7357,7 @@ class UISystemPlugin
|
|
|
6599
7357
|
o.update();
|
|
6600
7358
|
}
|
|
6601
7359
|
else
|
|
6602
|
-
|
|
7360
|
+
updateInvisibleObject(o);
|
|
6603
7361
|
}
|
|
6604
7362
|
uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
|
|
6605
7363
|
}
|
|
@@ -6625,7 +7383,7 @@ class UISystemPlugin
|
|
|
6625
7383
|
* @param {Color} [color=uiSystem.defaultColor]
|
|
6626
7384
|
* @param {number} [lineWidth=uiSystem.defaultLineWidth]
|
|
6627
7385
|
* @param {Color} [lineColor=uiSystem.defaultLineColor]
|
|
6628
|
-
* @param {number} [
|
|
7386
|
+
* @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
|
|
6629
7387
|
drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
|
|
6630
7388
|
{
|
|
6631
7389
|
const context = uiSystem.uiContext;
|
|
@@ -6700,32 +7458,40 @@ class UIObject
|
|
|
6700
7458
|
constructor(pos=vec2(), size=vec2())
|
|
6701
7459
|
{
|
|
6702
7460
|
/** @property {Vector2} - Local position of the object */
|
|
6703
|
-
this.localPos
|
|
7461
|
+
this.localPos = pos.copy();
|
|
6704
7462
|
/** @property {Vector2} - Screen space position of the object */
|
|
6705
|
-
this.pos
|
|
7463
|
+
this.pos = pos.copy();
|
|
6706
7464
|
/** @property {Vector2} - Screen space size of the object */
|
|
6707
|
-
this.size
|
|
6708
|
-
/** @property {Color} -
|
|
6709
|
-
this.color
|
|
6710
|
-
/** @property {
|
|
6711
|
-
this.
|
|
6712
|
-
/** @property {Color} -
|
|
7465
|
+
this.size = size.copy();
|
|
7466
|
+
/** @property {Color} - Color of the object */
|
|
7467
|
+
this.color = uiSystem.defaultColor;
|
|
7468
|
+
/** @property {string} - Text for this ui object */
|
|
7469
|
+
this.text = undefined;
|
|
7470
|
+
/** @property {Color} - Color when disabled */
|
|
7471
|
+
this.disabledColor = uiSystem.defaultDisabledColor;
|
|
7472
|
+
/** @property {boolean} - Is this object disabled? */
|
|
7473
|
+
this.disabled = false;
|
|
7474
|
+
/** @property {Color} - Color for text */
|
|
7475
|
+
this.textColor = uiSystem.defaultTextColor;
|
|
7476
|
+
/** @property {Color} - Color used when hovering over the object */
|
|
6713
7477
|
this.hoverColor = uiSystem.defaultHoverColor;
|
|
6714
|
-
/** @property {Color} -
|
|
6715
|
-
this.lineColor
|
|
6716
|
-
/** @property {number} -
|
|
6717
|
-
this.lineWidth
|
|
6718
|
-
/** @property {
|
|
6719
|
-
this.
|
|
6720
|
-
/** @property {
|
|
6721
|
-
this.
|
|
6722
|
-
/** @property {
|
|
6723
|
-
this.
|
|
6724
|
-
/** @property {
|
|
6725
|
-
this.
|
|
6726
|
-
/** @property {UIObject} - this object's
|
|
6727
|
-
this.
|
|
6728
|
-
/** @property {
|
|
7478
|
+
/** @property {Color} - Color for line drawing */
|
|
7479
|
+
this.lineColor = uiSystem.defaultLineColor;
|
|
7480
|
+
/** @property {number} - Width for line drawing */
|
|
7481
|
+
this.lineWidth = uiSystem.defaultLineWidth;
|
|
7482
|
+
/** @property {number} - Corner radius for rounded rects */
|
|
7483
|
+
this.cornerRadius = uiSystem.defaultCornerRadius;
|
|
7484
|
+
/** @property {string} - Font for this objecct */
|
|
7485
|
+
this.font = uiSystem.defaultFont;
|
|
7486
|
+
/** @property {number} - Override for text height */
|
|
7487
|
+
this.textHeight = undefined;
|
|
7488
|
+
/** @property {boolean} - Should this object be drawn */
|
|
7489
|
+
this.visible = true;
|
|
7490
|
+
/** @property {Array<UIObject>} - A list of this object's children */
|
|
7491
|
+
this.children = [];
|
|
7492
|
+
/** @property {UIObject} - This object's parent, position is in parent space */
|
|
7493
|
+
this.parent = undefined;
|
|
7494
|
+
/** @property {number} - Extra size added to make small buttons easier to touch on mobile devices */
|
|
6729
7495
|
this.extraTouchSize = 0;
|
|
6730
7496
|
/** @property {Sound} - Sound when interactive element is pressed */
|
|
6731
7497
|
this.soundPress = uiSystem.defaultSoundPress;
|
|
@@ -6757,7 +7523,7 @@ class UIObject
|
|
|
6757
7523
|
*/
|
|
6758
7524
|
removeChild(child)
|
|
6759
7525
|
{
|
|
6760
|
-
ASSERT(child.parent
|
|
7526
|
+
ASSERT(child.parent === this && this.children.includes(child));
|
|
6761
7527
|
this.children.splice(this.children.indexOf(child), 1);
|
|
6762
7528
|
child.parent = undefined;
|
|
6763
7529
|
}
|
|
@@ -6810,7 +7576,7 @@ class UIObject
|
|
|
6810
7576
|
this.mouseIsHeld = false;
|
|
6811
7577
|
}
|
|
6812
7578
|
|
|
6813
|
-
if (this.mouseIsOver
|
|
7579
|
+
if (this.mouseIsOver !== mouseWasOver)
|
|
6814
7580
|
this.mouseIsOver ? this.onEnter() : this.onLeave();
|
|
6815
7581
|
}
|
|
6816
7582
|
|
|
@@ -6821,7 +7587,7 @@ class UIObject
|
|
|
6821
7587
|
uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor, this.cornerRadius);
|
|
6822
7588
|
}
|
|
6823
7589
|
|
|
6824
|
-
/** Special update
|
|
7590
|
+
/** Special update when object is not visible */
|
|
6825
7591
|
updateInvisible()
|
|
6826
7592
|
{
|
|
6827
7593
|
// reset input state when not visible
|
|
@@ -6829,28 +7595,22 @@ class UIObject
|
|
|
6829
7595
|
}
|
|
6830
7596
|
|
|
6831
7597
|
/** Called when the mouse enters the object */
|
|
6832
|
-
onEnter()
|
|
6833
|
-
{}
|
|
7598
|
+
onEnter() {}
|
|
6834
7599
|
|
|
6835
7600
|
/** Called when the mouse leaves the object */
|
|
6836
|
-
onLeave()
|
|
6837
|
-
{}
|
|
7601
|
+
onLeave() {}
|
|
6838
7602
|
|
|
6839
7603
|
/** Called when the mouse is pressed while over the object */
|
|
6840
|
-
onPress()
|
|
6841
|
-
{}
|
|
7604
|
+
onPress() {}
|
|
6842
7605
|
|
|
6843
7606
|
/** Called when the mouse is released while over the object */
|
|
6844
|
-
onRelease()
|
|
6845
|
-
{}
|
|
7607
|
+
onRelease() {}
|
|
6846
7608
|
|
|
6847
7609
|
/** Called when user clicks on this object */
|
|
6848
|
-
onClick()
|
|
6849
|
-
{}
|
|
7610
|
+
onClick() {}
|
|
6850
7611
|
|
|
6851
7612
|
/** Called when the state of this object changes */
|
|
6852
|
-
onChange()
|
|
6853
|
-
{}
|
|
7613
|
+
onChange() {}
|
|
6854
7614
|
};
|
|
6855
7615
|
|
|
6856
7616
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -6871,13 +7631,13 @@ class UIText extends UIObject
|
|
|
6871
7631
|
{
|
|
6872
7632
|
super(pos, size);
|
|
6873
7633
|
|
|
6874
|
-
|
|
7634
|
+
// set properties
|
|
6875
7635
|
this.text = text;
|
|
6876
|
-
/** @property {string} */
|
|
6877
7636
|
this.align = align;
|
|
7637
|
+
this.font = font;
|
|
6878
7638
|
|
|
6879
|
-
|
|
6880
|
-
this.lineWidth = 0;
|
|
7639
|
+
// make text not outlined by default
|
|
7640
|
+
this.lineWidth = 0;
|
|
6881
7641
|
}
|
|
6882
7642
|
render()
|
|
6883
7643
|
{
|
|
@@ -6904,13 +7664,14 @@ class UITile extends UIObject
|
|
|
6904
7664
|
constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
|
|
6905
7665
|
{
|
|
6906
7666
|
super(pos, size);
|
|
6907
|
-
|
|
6908
7667
|
/** @property {TileInfo} - Tile image to use */
|
|
6909
7668
|
this.tileInfo = tileInfo;
|
|
6910
7669
|
/** @property {number} - Angle to rotate in radians */
|
|
6911
7670
|
this.angle = angle;
|
|
6912
7671
|
/** @property {boolean} - Should it be mirrored? */
|
|
6913
7672
|
this.mirror = mirror;
|
|
7673
|
+
|
|
7674
|
+
// set properties
|
|
6914
7675
|
this.color = color;
|
|
6915
7676
|
}
|
|
6916
7677
|
render()
|
|
@@ -6936,22 +7697,20 @@ class UIButton extends UIObject
|
|
|
6936
7697
|
{
|
|
6937
7698
|
super(pos, size);
|
|
6938
7699
|
|
|
6939
|
-
|
|
7700
|
+
// set properties
|
|
6940
7701
|
this.text = text;
|
|
6941
|
-
/** @property {Color} */
|
|
6942
|
-
this.disabledColor = uiSystem.defaultDisabledColor;
|
|
6943
|
-
/** @property {boolean} */
|
|
6944
|
-
this.disabled = false;
|
|
6945
|
-
this.interactive = true;
|
|
6946
7702
|
this.color = color;
|
|
7703
|
+
this.interactive = true;
|
|
6947
7704
|
}
|
|
6948
7705
|
render()
|
|
6949
7706
|
{
|
|
7707
|
+
// draw the button
|
|
6950
7708
|
const lineColor = this.mouseIsHeld && !this.disabled ? this.color : this.lineColor;
|
|
6951
7709
|
const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
|
|
6952
7710
|
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
|
|
6953
7711
|
|
|
6954
|
-
|
|
7712
|
+
// draw the text
|
|
7713
|
+
const textScale = .8; // scale text to fit
|
|
6955
7714
|
const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
|
|
6956
7715
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
6957
7716
|
this.textColor, 0, undefined, this.align, this.font);
|
|
@@ -6969,13 +7728,18 @@ class UICheckbox extends UIObject
|
|
|
6969
7728
|
* @param {Vector2} [pos]
|
|
6970
7729
|
* @param {Vector2} [size]
|
|
6971
7730
|
* @param {boolean} [checked]
|
|
7731
|
+
* @param {string} [text]
|
|
7732
|
+
* @param {Color} [color=uiSystem.defaultButtonColor]
|
|
6972
7733
|
*/
|
|
6973
|
-
constructor(pos, size, checked=false)
|
|
7734
|
+
constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
|
|
6974
7735
|
{
|
|
6975
7736
|
super(pos, size);
|
|
6976
|
-
|
|
6977
|
-
/** @property {boolean} */
|
|
7737
|
+
/** @property {boolean} - Current percentage value of this scrollbar 0-1 */
|
|
6978
7738
|
this.checked = checked;
|
|
7739
|
+
|
|
7740
|
+
// set properties
|
|
7741
|
+
this.text = text;
|
|
7742
|
+
this.color = color;
|
|
6979
7743
|
this.interactive = true;
|
|
6980
7744
|
}
|
|
6981
7745
|
onClick()
|
|
@@ -6985,14 +7749,24 @@ class UICheckbox extends UIObject
|
|
|
6985
7749
|
}
|
|
6986
7750
|
render()
|
|
6987
7751
|
{
|
|
6988
|
-
const color = this.mouseIsOver? this.hoverColor : this.color;
|
|
7752
|
+
const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
|
|
6989
7753
|
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor, this.cornerRadius);
|
|
6990
7754
|
if (this.checked)
|
|
6991
7755
|
{
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
7756
|
+
const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
|
|
7757
|
+
const length = lerp(1, 2**.5/2, p) / 2;
|
|
7758
|
+
let s = this.size.scale(length);
|
|
7759
|
+
uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))), this.pos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
|
|
7760
|
+
uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
|
|
6995
7761
|
}
|
|
7762
|
+
|
|
7763
|
+
// draw the text to the right side of the checkbox
|
|
7764
|
+
const textScale = .8; // scale text to fit
|
|
7765
|
+
const gapScale = .55;
|
|
7766
|
+
const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
|
|
7767
|
+
const pos = this.pos.add(vec2(this.size.x*gapScale,0));
|
|
7768
|
+
uiSystem.drawText(this.text, pos, textSize,
|
|
7769
|
+
this.textColor, 0, undefined, 'left', this.font);
|
|
6996
7770
|
}
|
|
6997
7771
|
}
|
|
6998
7772
|
|
|
@@ -7015,43 +7789,51 @@ class UIScrollbar extends UIObject
|
|
|
7015
7789
|
{
|
|
7016
7790
|
super(pos, size);
|
|
7017
7791
|
|
|
7018
|
-
/** @property {number} */
|
|
7792
|
+
/** @property {number} - Current percentage value of this scrollbar 0-1 */
|
|
7019
7793
|
this.value = value;
|
|
7020
|
-
/** @property {
|
|
7021
|
-
this.text = text;
|
|
7022
|
-
/** @property {Color} */
|
|
7794
|
+
/** @property {Color} - Color for the handle part of the scrollbar */
|
|
7023
7795
|
this.handleColor = handleColor;
|
|
7796
|
+
|
|
7797
|
+
// set properties
|
|
7798
|
+
this.text = text;
|
|
7024
7799
|
this.color = color;
|
|
7025
7800
|
this.interactive = true;
|
|
7026
7801
|
}
|
|
7027
7802
|
update()
|
|
7028
7803
|
{
|
|
7029
7804
|
super.update();
|
|
7030
|
-
if (this.mouseIsHeld)
|
|
7805
|
+
if (this.mouseIsHeld && this.interactive)
|
|
7031
7806
|
{
|
|
7807
|
+
// check if value changed
|
|
7032
7808
|
const handleSize = vec2(this.size.y);
|
|
7033
7809
|
const handleWidth = this.size.x - handleSize.x;
|
|
7034
7810
|
const p1 = this.pos.x - handleWidth/2;
|
|
7035
7811
|
const p2 = this.pos.x + handleWidth/2;
|
|
7036
7812
|
const oldValue = this.value;
|
|
7037
7813
|
this.value = percent(mousePosScreen.x, p1, p2);
|
|
7038
|
-
this.value
|
|
7814
|
+
this.value === oldValue || this.onChange();
|
|
7039
7815
|
}
|
|
7040
7816
|
}
|
|
7041
7817
|
render()
|
|
7042
7818
|
{
|
|
7043
|
-
|
|
7044
|
-
const
|
|
7819
|
+
// draw the scrollbar background
|
|
7820
|
+
const lineColor = this.interactive && this.mouseIsHeld && !this.disabled ?
|
|
7821
|
+
this.color : this.lineColor;
|
|
7822
|
+
const color = this.disabled ? this.disabledColor :
|
|
7823
|
+
this.interactive && this.mouseIsHeld ? this.hoverColor : this.color;
|
|
7045
7824
|
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
|
|
7046
7825
|
|
|
7826
|
+
// draw the scrollbar handle
|
|
7047
7827
|
const handleSize = vec2(this.size.y);
|
|
7048
7828
|
const handleWidth = this.size.x - handleSize.x;
|
|
7049
7829
|
const p1 = this.pos.x - handleWidth/2;
|
|
7050
7830
|
const p2 = this.pos.x + handleWidth/2;
|
|
7051
7831
|
const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
|
|
7052
|
-
const
|
|
7053
|
-
|
|
7832
|
+
const handleColor = this.disabled ? this.disabledColor :
|
|
7833
|
+
this.interactive && this.mouseIsHeld ? this.color : this.handleColor;
|
|
7834
|
+
uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
|
|
7054
7835
|
|
|
7836
|
+
// draw the text on the scrollbar
|
|
7055
7837
|
const textScale = .8; // scale text to fit in scrollbar
|
|
7056
7838
|
const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
|
|
7057
7839
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
@@ -7146,14 +7928,14 @@ class Box2dObject extends EngineObject
|
|
|
7146
7928
|
if (this.tileInfo)
|
|
7147
7929
|
super.render();
|
|
7148
7930
|
else
|
|
7149
|
-
this.drawFixtures(this.color, this.lineColor, this.lineWidth
|
|
7931
|
+
this.drawFixtures(this.color, this.lineColor, this.lineWidth);
|
|
7150
7932
|
}
|
|
7151
7933
|
|
|
7152
7934
|
/** Render debug info */
|
|
7153
7935
|
renderDebugInfo()
|
|
7154
7936
|
{
|
|
7155
7937
|
const isAsleep = !this.getIsAwake();
|
|
7156
|
-
const isStatic = this.getBodyType()
|
|
7938
|
+
const isStatic = this.getBodyType() === box2d.bodyTypeStatic;
|
|
7157
7939
|
const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
|
|
7158
7940
|
this.drawFixtures(color);
|
|
7159
7941
|
}
|
|
@@ -8621,7 +9403,7 @@ class Box2dPlugin
|
|
|
8621
9403
|
queryCallback.ReportFixture = function(fixturePointer)
|
|
8622
9404
|
{
|
|
8623
9405
|
const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
|
|
8624
|
-
if (dynamicOnly && fixture.GetBody().GetType()
|
|
9406
|
+
if (dynamicOnly && fixture.GetBody().GetType() !== box2d.instance.b2_dynamicBody)
|
|
8625
9407
|
return true; // continue getting results
|
|
8626
9408
|
if (!fixture.TestPoint(box2d.vec2dTo(pos)))
|
|
8627
9409
|
return true; // continue getting results
|
|
@@ -8650,7 +9432,7 @@ class Box2dPlugin
|
|
|
8650
9432
|
* @param {Color} [lineColor]
|
|
8651
9433
|
* @param {number} [lineWidth]
|
|
8652
9434
|
* @param {CanvasRenderingContext2D} [context] */
|
|
8653
|
-
drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context
|
|
9435
|
+
drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
|
|
8654
9436
|
{
|
|
8655
9437
|
const shape = box2d.castObjectType(fixture.GetShape());
|
|
8656
9438
|
switch (shape.GetType())
|
|
@@ -8660,20 +9442,20 @@ class Box2dPlugin
|
|
|
8660
9442
|
let points = [];
|
|
8661
9443
|
for (let i=shape.GetVertexCount(); i--;)
|
|
8662
9444
|
points.push(box2d.vec2From(shape.GetVertex(i)));
|
|
8663
|
-
drawPoly(points, color, lineWidth, lineColor, pos, angle
|
|
9445
|
+
drawPoly(points, color, lineWidth, lineColor, pos, angle);
|
|
8664
9446
|
break;
|
|
8665
9447
|
}
|
|
8666
9448
|
case box2d.instance.b2Shape.e_circle:
|
|
8667
9449
|
{
|
|
8668
9450
|
const radius = shape.get_m_radius();
|
|
8669
|
-
drawCircle(pos, radius, color, lineWidth, lineColor
|
|
9451
|
+
drawCircle(pos, radius*2, color, lineWidth, lineColor);
|
|
8670
9452
|
break;
|
|
8671
9453
|
}
|
|
8672
9454
|
case box2d.instance.b2Shape.e_edge:
|
|
8673
9455
|
{
|
|
8674
9456
|
const v1 = box2d.vec2From(shape.get_m_vertex1());
|
|
8675
9457
|
const v2 = box2d.vec2From(shape.get_m_vertex2());
|
|
8676
|
-
drawLine(v1, v2, lineWidth, lineColor, pos, angle
|
|
9458
|
+
drawLine(v1, v2, lineWidth, lineColor, pos, angle);
|
|
8677
9459
|
break;
|
|
8678
9460
|
}
|
|
8679
9461
|
}
|
|
@@ -8752,7 +9534,9 @@ class Box2dPlugin
|
|
|
8752
9534
|
}
|
|
8753
9535
|
|
|
8754
9536
|
///////////////////////////////////////////////////////////////////////////////
|
|
8755
|
-
/** Box2d Init - Call with await
|
|
9537
|
+
/** Box2d Init - Call with await to init box2d
|
|
9538
|
+
* @example
|
|
9539
|
+
* await box2dInit();
|
|
8756
9540
|
* @return {Promise<Box2dPlugin>}
|
|
8757
9541
|
* @memberof Box2D */
|
|
8758
9542
|
async function box2dInit()
|
|
@@ -8815,14 +9599,14 @@ async function box2dInit()
|
|
|
8815
9599
|
{
|
|
8816
9600
|
color = getDebugColor(color);
|
|
8817
9601
|
center = box2d.vec2FromPointer(center);
|
|
8818
|
-
drawCircle(center, radius, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
|
|
9602
|
+
drawCircle(center, radius*2, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
|
|
8819
9603
|
};
|
|
8820
9604
|
debugDraw.DrawSolidCircle = function(center, radius, axis, color)
|
|
8821
9605
|
{
|
|
8822
9606
|
color = getDebugColor(color);
|
|
8823
9607
|
center = box2d.vec2FromPointer(center);
|
|
8824
9608
|
axis = box2d.vec2FromPointer(axis).scale(radius);
|
|
8825
|
-
drawCircle(center, radius, color, debugLineWidth, color, false, false, overlayContext);
|
|
9609
|
+
drawCircle(center, radius*2, color, debugLineWidth, color, false, false, overlayContext);
|
|
8826
9610
|
drawLine(vec2(), axis, debugLineWidth, color, center, 0, false, false, overlayContext);
|
|
8827
9611
|
};
|
|
8828
9612
|
debugDraw.DrawTransform = function(transform)
|
|
@@ -8868,7 +9652,7 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
|
|
|
8868
9652
|
}
|
|
8869
9653
|
|
|
8870
9654
|
/** Draw a scalable nine-slice UI element in world space
|
|
8871
|
-
* This function can apply color and additive color if
|
|
9655
|
+
* This function can apply color and additive color if WebGL is enabled
|
|
8872
9656
|
* @param {Vector2} pos - World space position
|
|
8873
9657
|
* @param {Vector2} size - World space size
|
|
8874
9658
|
* @param {TileInfo} startTile - Starting tile for the nine-slice pattern
|
|
@@ -8897,9 +9681,9 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
|
|
|
8897
9681
|
{
|
|
8898
9682
|
// sides
|
|
8899
9683
|
const horizontal = i%2;
|
|
8900
|
-
const sidePos = cornerOffset.multiply(vec2(horizontal?i
|
|
9684
|
+
const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-1:1));
|
|
8901
9685
|
const sideSize = vec2(horizontal ? borderSize : centerSize.x, horizontal ? centerSize.y : borderSize);
|
|
8902
|
-
const sideTile = centerTile.offset(startTile.size.multiply(vec2(i
|
|
9686
|
+
const sideTile = centerTile.offset(startTile.size.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)))
|
|
8903
9687
|
drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
|
|
8904
9688
|
}
|
|
8905
9689
|
for (let i=4; i--;)
|
|
@@ -8928,7 +9712,7 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
|
|
|
8928
9712
|
}
|
|
8929
9713
|
|
|
8930
9714
|
/** Draw a scalable three-slice UI element in world space
|
|
8931
|
-
* This function can apply color and additive color if
|
|
9715
|
+
* This function can apply color and additive color if WebGL is enabled
|
|
8932
9716
|
* @param {Vector2} pos - World space position
|
|
8933
9717
|
* @param {Vector2} size - World space size
|
|
8934
9718
|
* @param {TileInfo} startTile - Starting tile for the three-slice pattern
|
|
@@ -8960,7 +9744,7 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
|
|
|
8960
9744
|
// sides
|
|
8961
9745
|
const a = angle + i*PI/2;
|
|
8962
9746
|
const horizontal = i%2;
|
|
8963
|
-
const sidePos = cornerOffset.multiply(vec2(horizontal?i
|
|
9747
|
+
const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-flip:flip));
|
|
8964
9748
|
const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize);
|
|
8965
9749
|
drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
|
|
8966
9750
|
}
|