littlejsengine 1.13.1 → 1.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/copyToGithub.bat +28 -0
- package/dist/littlejs.d.ts +430 -304
- package/dist/littlejs.esm.js +1745 -982
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +1716 -965
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +1673 -922
- package/examples/box2d/game.js +2 -2
- package/examples/box2d/gameObjects.js +2 -2
- package/examples/box2d/scenes.js +1 -1
- package/examples/breakout/game.js +30 -25
- package/examples/breakoutTutorial/README.md +44 -41
- package/examples/breakoutTutorial/game.js +2 -2
- package/examples/electron/build.bat +7 -0
- package/examples/electron/build.js +124 -0
- package/examples/electron/electron.js +35 -0
- package/examples/electron/game.js +140 -0
- package/examples/electron/index.html +13 -0
- package/examples/electron/package.json +12 -0
- package/examples/electron/tiles.png +0 -0
- package/examples/empty/game.js +1 -0
- package/examples/htmlMenu/game.js +1 -1
- package/examples/index.html +332 -160
- package/examples/module/game.js +5 -2
- package/examples/particles/index.html +12 -3
- package/examples/platformer/gameEffects.js +20 -19
- package/examples/platformer/gameLevel.js +6 -1
- package/examples/shorts/base.html +1 -1
- package/examples/shorts/blending.js +9 -5
- package/examples/shorts/box2d.js +1 -1
- package/examples/shorts/box2dCar.js +1 -1
- package/examples/shorts/clock.js +1 -1
- package/examples/shorts/colors.js +2 -2
- package/examples/shorts/flappyGame.js +2 -1
- package/examples/shorts/helloWorld.js +1 -1
- package/examples/shorts/maze.js +1 -1
- package/examples/shorts/music.js +106 -0
- package/examples/shorts/nineSlice.js +9 -9
- package/examples/shorts/parallax.js +71 -0
- package/examples/shorts/piano.js +9 -10
- package/examples/shorts/postProcess.js +25 -8
- package/examples/shorts/raycasting.js +2 -2
- package/examples/shorts/shapes.js +7 -15
- package/examples/shorts/slidingPuzzle.js +2 -2
- package/examples/shorts/song.mp3 +0 -0
- package/examples/shorts/starfield.js +5 -7
- package/examples/shorts/systemFont.js +1 -1
- package/examples/shorts/uiSystem.js +16 -8
- package/examples/starter/build.js +9 -8
- package/examples/starter/game.js +5 -2
- package/examples/starter/index.html +2 -2
- package/examples/stress/index.html +13 -8
- package/examples/style.css +19 -6
- package/examples/typescript/build.js +1 -1
- package/examples/typescript/game.js +2 -2
- package/examples/typescript/game.ts +5 -2
- package/examples/uiSystem/game.js +13 -12
- package/package.json +2 -2
- package/plugins/box2d.js +24 -97
- package/plugins/drawUtilities.js +29 -23
- package/plugins/newgrounds.js +6 -6
- package/plugins/pluginExport.js +1 -1
- package/plugins/postProcess.js +7 -0
- package/plugins/uiSystem.js +106 -82
- package/plugins/zzfxm.js +10 -10
- package/reference.md +90 -54
- package/src/engine.js +28 -23
- package/src/engineAudio.js +285 -110
- package/src/engineBuild.js +9 -9
- package/src/engineDebug.js +28 -28
- package/src/engineDraw.js +346 -202
- package/src/engineExport.js +28 -16
- package/src/engineInput.js +58 -68
- package/src/engineMedals.js +29 -29
- package/src/engineObject.js +28 -25
- package/src/engineParticles.js +67 -60
- package/src/engineRelease.js +1 -1
- package/src/engineSettings.js +70 -16
- package/src/engineTileLayer.js +34 -34
- package/src/engineUtilities.js +69 -62
- package/src/engineWebGL.js +467 -65
- /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.2';
|
|
37
37
|
|
|
38
38
|
/** Frames per second to update
|
|
39
39
|
* @type {number}
|
|
@@ -123,7 +123,7 @@ function engineAddPlugin(updateFunction, renderFunction)
|
|
|
123
123
|
* // Basic engine startup
|
|
124
124
|
* engineInit(
|
|
125
125
|
* () => { console.log('Game initialized!'); }, // gameInit
|
|
126
|
-
* () => {
|
|
126
|
+
* () => { updateGameLogic(); }, // gameUpdate
|
|
127
127
|
* () => { updateUI(); }, // gameUpdatePost
|
|
128
128
|
* () => { drawBackground(); }, // gameRender
|
|
129
129
|
* () => { drawHUD(); }, // gameRenderPost
|
|
@@ -149,7 +149,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
149
149
|
mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
|
|
150
150
|
|
|
151
151
|
// disable smoothing for pixel art
|
|
152
|
-
overlayContext.imageSmoothingEnabled =
|
|
152
|
+
overlayContext.imageSmoothingEnabled =
|
|
153
153
|
mainContext.imageSmoothingEnabled = !tilesPixelated;
|
|
154
154
|
|
|
155
155
|
// setup gl rendering if enabled
|
|
@@ -197,7 +197,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
197
197
|
deltaSmooth = frameTimeBufferMS;
|
|
198
198
|
frameTimeBufferMS = 0;
|
|
199
199
|
}
|
|
200
|
-
|
|
200
|
+
|
|
201
201
|
// update multiple frames if necessary in case of slow framerate
|
|
202
202
|
for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
|
|
203
203
|
{
|
|
@@ -242,7 +242,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
242
242
|
overlayContext.textBaseline = 'top';
|
|
243
243
|
overlayContext.font = '1em monospace';
|
|
244
244
|
overlayContext.fillStyle = '#000';
|
|
245
|
-
const text = engineName + ' ' + 'v' + engineVersion + ' / '
|
|
245
|
+
const text = engineName + ' ' + 'v' + engineVersion + ' / '
|
|
246
246
|
+ drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
|
|
247
247
|
+ (glEnable ? ' GL' : ' 2D') ;
|
|
248
248
|
overlayContext.fillText(text, mainCanvas.width-3, 3);
|
|
@@ -258,13 +258,13 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
258
258
|
function updateCanvas()
|
|
259
259
|
{
|
|
260
260
|
if (headlessMode) return;
|
|
261
|
-
|
|
261
|
+
|
|
262
262
|
if (canvasFixedSize.x)
|
|
263
263
|
{
|
|
264
264
|
// clear canvas and set fixed size
|
|
265
265
|
mainCanvas.width = canvasFixedSize.x;
|
|
266
266
|
mainCanvas.height = canvasFixedSize.y;
|
|
267
|
-
|
|
267
|
+
|
|
268
268
|
// fit to window by adding space on top or bottom if necessary
|
|
269
269
|
const aspect = innerWidth / innerHeight;
|
|
270
270
|
const fixedAspect = mainCanvas.width / mainCanvas.height;
|
|
@@ -274,10 +274,10 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
274
274
|
else
|
|
275
275
|
{
|
|
276
276
|
// clear canvas and set size to same as window
|
|
277
|
-
mainCanvas.width
|
|
277
|
+
mainCanvas.width = min(innerWidth, canvasMaxSize.x);
|
|
278
278
|
mainCanvas.height = min(innerHeight, canvasMaxSize.y);
|
|
279
279
|
}
|
|
280
|
-
|
|
280
|
+
|
|
281
281
|
// clear overlay canvas and set size
|
|
282
282
|
overlayCanvas.width = mainCanvas.width;
|
|
283
283
|
overlayCanvas.height = mainCanvas.height;
|
|
@@ -296,16 +296,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
296
296
|
return startEngine();
|
|
297
297
|
|
|
298
298
|
// setup html
|
|
299
|
-
const styleRoot =
|
|
299
|
+
const styleRoot =
|
|
300
300
|
'margin:0;' + // fill the window
|
|
301
301
|
'overflow:hidden;' + // no scroll bars
|
|
302
302
|
'background:#000;' + // set background color
|
|
303
|
-
(canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
|
|
304
303
|
'user-select:none;' + // prevent hold to select
|
|
305
304
|
'-webkit-user-select:none;' + // compatibility for ios
|
|
306
|
-
(!touchInputEnable ? '' : // no touch css settings
|
|
307
305
|
'touch-action:none;' + // prevent mobile pinch to resize
|
|
308
|
-
'-webkit-touch-callout:none'
|
|
306
|
+
'-webkit-touch-callout:none';// compatibility for ios
|
|
309
307
|
rootElement.style.cssText = styleRoot;
|
|
310
308
|
drawCanvas = mainCanvas = document.createElement('canvas');
|
|
311
309
|
rootElement.appendChild(mainCanvas);
|
|
@@ -322,20 +320,27 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
322
320
|
rootElement.appendChild(overlayCanvas);
|
|
323
321
|
overlayContext = overlayCanvas.getContext('2d');
|
|
324
322
|
|
|
325
|
-
// set
|
|
323
|
+
// set canvases
|
|
326
324
|
const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
|
|
327
325
|
'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
|
|
328
326
|
mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
|
|
329
327
|
if (glCanvas)
|
|
330
328
|
glCanvas.style.cssText = styleCanvas;
|
|
329
|
+
setCanvasPixelated(canvasPixelated);
|
|
330
|
+
setOverlayCanvasPixelated(overlayCanvasPixelated);
|
|
331
331
|
updateCanvas();
|
|
332
|
-
|
|
332
|
+
glPreRender();
|
|
333
|
+
|
|
334
|
+
// create offscreen canvas for image processing
|
|
335
|
+
workCanvas = new OffscreenCanvas(256, 256);
|
|
336
|
+
workContext = workCanvas.getContext('2d', { willReadFrequently: true });
|
|
337
|
+
|
|
333
338
|
// create promises for loading images
|
|
334
339
|
const promises = imageSources.map((src, textureIndex)=>
|
|
335
|
-
new Promise(resolve =>
|
|
340
|
+
new Promise(resolve =>
|
|
336
341
|
{
|
|
337
342
|
const image = new Image;
|
|
338
|
-
image.onerror = image.onload = ()=>
|
|
343
|
+
image.onerror = image.onload = ()=>
|
|
339
344
|
{
|
|
340
345
|
const textureInfo = new TextureInfo(image);
|
|
341
346
|
textureInfo.createWebGLTexture();
|
|
@@ -350,7 +355,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
350
355
|
if (!imageSources.length)
|
|
351
356
|
{
|
|
352
357
|
// no images to load
|
|
353
|
-
promises.push(new Promise(resolve =>
|
|
358
|
+
promises.push(new Promise(resolve =>
|
|
354
359
|
{
|
|
355
360
|
const textureInfo = new TextureInfo(new Image);
|
|
356
361
|
textureInfos[0] = textureInfo;
|
|
@@ -362,7 +367,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
362
367
|
if (showSplashScreen)
|
|
363
368
|
{
|
|
364
369
|
// draw splash screen
|
|
365
|
-
promises.push(new Promise(resolve =>
|
|
370
|
+
promises.push(new Promise(resolve =>
|
|
366
371
|
{
|
|
367
372
|
let t = 0;
|
|
368
373
|
console.log(`${engineName} Engine v${engineVersion}`);
|
|
@@ -594,7 +599,7 @@ function drawEngineSplashScreen(t)
|
|
|
594
599
|
line(36,20,60,20);
|
|
595
600
|
|
|
596
601
|
// engine front light
|
|
597
|
-
circle(60,30,4,PI,3*PI,color(3,2));
|
|
602
|
+
circle(60,30,4,PI,3*PI,color(3,2));
|
|
598
603
|
circle(60,30,4,PI,2*PI,color(3,3));
|
|
599
604
|
circle(60,30,4,PI,3*PI);
|
|
600
605
|
|
|
@@ -643,10 +648,10 @@ function drawEngineSplashScreen(t)
|
|
|
643
648
|
x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
|
|
644
649
|
X += w;
|
|
645
650
|
}
|
|
646
|
-
|
|
651
|
+
|
|
647
652
|
x.restore();
|
|
648
653
|
}
|
|
649
|
-
/**
|
|
654
|
+
/**
|
|
650
655
|
* LittleJS - Release Mode
|
|
651
656
|
* - This file is used for release builds in place of engineDebug.js
|
|
652
657
|
* - Debug functionality is disabled to reduce size and increase performance
|
|
@@ -759,7 +764,19 @@ function lerp(valueA, valueB, percent)
|
|
|
759
764
|
if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
|
|
760
765
|
console.warn('lerp() parameter order changed! use lerp(start, end, p)');
|
|
761
766
|
return valueA + clamp(percent) * (valueB-valueA);
|
|
762
|
-
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
|
|
770
|
+
* A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
|
|
771
|
+
* @param {number} value
|
|
772
|
+
* @param {number} percentA
|
|
773
|
+
* @param {number} percentB
|
|
774
|
+
* @param {number} lerpA
|
|
775
|
+
* @param {number} lerpB
|
|
776
|
+
* @return {number}
|
|
777
|
+
* @memberof Utilities */
|
|
778
|
+
function percentLerp(value, percentA, percentB, lerpA, lerpB)
|
|
779
|
+
{ return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
|
|
763
780
|
|
|
764
781
|
/** Returns signed wrapped distance between the two values passed in
|
|
765
782
|
* @param {number} valueA
|
|
@@ -811,22 +828,23 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
|
|
|
811
828
|
* @memberof Utilities */
|
|
812
829
|
function isPowerOfTwo(value) { return !(value & (value - 1)); }
|
|
813
830
|
|
|
814
|
-
/** Returns the nearest power of two not less
|
|
831
|
+
/** Returns the nearest power of two not less than the value
|
|
815
832
|
* @param {number} value
|
|
816
833
|
* @return {number}
|
|
817
834
|
* @memberof Utilities */
|
|
818
835
|
function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
|
|
819
836
|
|
|
820
|
-
/** Returns true if two axis aligned bounding boxes are overlapping
|
|
837
|
+
/** Returns true if two axis aligned bounding boxes are overlapping
|
|
838
|
+
* this can be used for simple collision detection between objects
|
|
821
839
|
* @param {Vector2} posA - Center of box A
|
|
822
840
|
* @param {Vector2} sizeA - Size of box A
|
|
823
841
|
* @param {Vector2} posB - Center of box B
|
|
824
|
-
* @param {Vector2} [sizeB=(0,0)] - Size of box B, a point if undefined
|
|
842
|
+
* @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
|
|
825
843
|
* @return {boolean} - True if overlapping
|
|
826
844
|
* @memberof Utilities */
|
|
827
845
|
function isOverlapping(posA, sizeA, posB, sizeB=vec2())
|
|
828
|
-
{
|
|
829
|
-
return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
|
|
846
|
+
{
|
|
847
|
+
return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
|
|
830
848
|
&& abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
|
|
831
849
|
}
|
|
832
850
|
|
|
@@ -875,12 +893,13 @@ function isIntersecting(start, end, pos, size)
|
|
|
875
893
|
* @param {number} [frequency] - Frequency of the wave in Hz
|
|
876
894
|
* @param {number} [amplitude] - Amplitude (max height) of the wave
|
|
877
895
|
* @param {number} [t=time] - Value to use for time of the wave
|
|
896
|
+
* @param {number} [offset] - Value to use for time offset of the wave
|
|
878
897
|
* @return {number} - Value waving between 0 and amplitude
|
|
879
898
|
* @memberof Utilities */
|
|
880
|
-
function wave(frequency=1, amplitude=1, t=time)
|
|
881
|
-
{ return amplitude/2 * (1 - Math.cos(t*frequency*2*PI)); }
|
|
899
|
+
function wave(frequency=1, amplitude=1, t=time, offset=0)
|
|
900
|
+
{ return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
|
|
882
901
|
|
|
883
|
-
/** Formats seconds to mm:ss style for display purposes
|
|
902
|
+
/** Formats seconds to mm:ss style for display purposes
|
|
884
903
|
* @param {number} t - time in seconds
|
|
885
904
|
* @return {string}
|
|
886
905
|
* @memberof Utilities */
|
|
@@ -896,13 +915,12 @@ async function fetchJSON(url)
|
|
|
896
915
|
return response.json();
|
|
897
916
|
}
|
|
898
917
|
|
|
899
|
-
/**
|
|
918
|
+
/**
|
|
900
919
|
* Check if object is a valid number, not NaN or undefined, but it may be infinite
|
|
901
920
|
* @param {any} n
|
|
902
921
|
* @return {boolean}
|
|
903
|
-
* @memberof Utilities
|
|
904
|
-
|
|
905
|
-
function isNumber(n) { return typeof n == 'number' && !isNaN(n); }
|
|
922
|
+
* @memberof Utilities */
|
|
923
|
+
function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
|
|
906
924
|
|
|
907
925
|
///////////////////////////////////////////////////////////////////////////////
|
|
908
926
|
|
|
@@ -957,13 +975,13 @@ function randInCircle(radius=1, minRadius=0)
|
|
|
957
975
|
* @memberof Random */
|
|
958
976
|
function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
|
|
959
977
|
{
|
|
960
|
-
return linear ? colorA.lerp(colorB, rand()) :
|
|
978
|
+
return linear ? colorA.lerp(colorB, rand()) :
|
|
961
979
|
new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
|
|
962
980
|
}
|
|
963
981
|
|
|
964
982
|
///////////////////////////////////////////////////////////////////////////////
|
|
965
983
|
|
|
966
|
-
/**
|
|
984
|
+
/**
|
|
967
985
|
* Seeded random number generator
|
|
968
986
|
* - Can be used to create a deterministic random number sequence
|
|
969
987
|
* @example
|
|
@@ -990,8 +1008,8 @@ class RandomGenerator
|
|
|
990
1008
|
float(valueA=1, valueB=0)
|
|
991
1009
|
{
|
|
992
1010
|
// xorshift algorithm
|
|
993
|
-
this.seed ^= this.seed << 13;
|
|
994
|
-
this.seed ^= this.seed >>> 17;
|
|
1011
|
+
this.seed ^= this.seed << 13;
|
|
1012
|
+
this.seed ^= this.seed >>> 17;
|
|
995
1013
|
this.seed ^= this.seed << 5;
|
|
996
1014
|
return valueB + (valueA - valueB) * ((this.seed >>> 0) / 2**32);
|
|
997
1015
|
}
|
|
@@ -1040,16 +1058,14 @@ class RandomGenerator
|
|
|
1040
1058
|
* let a = vec2(0, 1); // vector with coordinates (0, 1)
|
|
1041
1059
|
* a = vec2(5); // set a to (5, 5)
|
|
1042
1060
|
* b = vec2(); // set b to (0, 0)
|
|
1043
|
-
* @memberof Utilities
|
|
1044
|
-
*/
|
|
1061
|
+
* @memberof Utilities */
|
|
1045
1062
|
function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
|
|
1046
1063
|
|
|
1047
|
-
/**
|
|
1064
|
+
/**
|
|
1048
1065
|
* Check if object is a valid Vector2
|
|
1049
1066
|
* @param {any} v
|
|
1050
1067
|
* @return {boolean}
|
|
1051
|
-
* @memberof Utilities
|
|
1052
|
-
*/
|
|
1068
|
+
* @memberof Utilities */
|
|
1053
1069
|
function isVector2(v) { return v instanceof Vector2; }
|
|
1054
1070
|
|
|
1055
1071
|
// vector2 asserts
|
|
@@ -1058,10 +1074,10 @@ function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n);
|
|
|
1058
1074
|
function ASSERT_VECTOR2_NORMAL(v)
|
|
1059
1075
|
{
|
|
1060
1076
|
ASSERT_VECTOR2_VALID(v);
|
|
1061
|
-
ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
|
|
1077
|
+
ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
|
|
1062
1078
|
}
|
|
1063
1079
|
|
|
1064
|
-
/**
|
|
1080
|
+
/**
|
|
1065
1081
|
* 2D Vector object with vector math library
|
|
1066
1082
|
* - Functions do not change this so they can be chained together
|
|
1067
1083
|
* @example
|
|
@@ -1185,7 +1201,7 @@ class Vector2
|
|
|
1185
1201
|
* @param {number} [angle]
|
|
1186
1202
|
* @param {number} [length]
|
|
1187
1203
|
* @return {Vector2} */
|
|
1188
|
-
setAngle(angle=0, length=1)
|
|
1204
|
+
setAngle(angle=0, length=1)
|
|
1189
1205
|
{
|
|
1190
1206
|
ASSERT_NUMBER_VALID(angle);
|
|
1191
1207
|
ASSERT_NUMBER_VALID(length);
|
|
@@ -1200,7 +1216,7 @@ class Vector2
|
|
|
1200
1216
|
rotate(angle)
|
|
1201
1217
|
{
|
|
1202
1218
|
ASSERT_NUMBER_VALID(angle);
|
|
1203
|
-
const c = Math.cos(-angle), s = Math.sin(-angle);
|
|
1219
|
+
const c = Math.cos(-angle), s = Math.sin(-angle);
|
|
1204
1220
|
return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
|
|
1205
1221
|
}
|
|
1206
1222
|
|
|
@@ -1212,9 +1228,9 @@ class Vector2
|
|
|
1212
1228
|
ASSERT_NUMBER_VALID(direction);
|
|
1213
1229
|
ASSERT_NUMBER_VALID(length);
|
|
1214
1230
|
direction = mod(direction, 4);
|
|
1215
|
-
ASSERT(direction
|
|
1231
|
+
ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
|
|
1216
1232
|
'Vector2.setDirection() direction must be an integer between 0 and 3.');
|
|
1217
|
-
return vec2(direction%2 ? direction-1 ? -length : length : 0,
|
|
1233
|
+
return vec2(direction%2 ? direction-1 ? -length : length : 0,
|
|
1218
1234
|
direction%2 ? 0 : direction ? -length : length);
|
|
1219
1235
|
}
|
|
1220
1236
|
|
|
@@ -1266,7 +1282,7 @@ class Vector2
|
|
|
1266
1282
|
/** Returns this vector expressed as a string
|
|
1267
1283
|
* @param {number} digits - precision to display
|
|
1268
1284
|
* @return {string} */
|
|
1269
|
-
toString(digits=3)
|
|
1285
|
+
toString(digits=3)
|
|
1270
1286
|
{
|
|
1271
1287
|
ASSERT_NUMBER_VALID(digits);
|
|
1272
1288
|
if (debug)
|
|
@@ -1285,7 +1301,7 @@ class Vector2
|
|
|
1285
1301
|
|
|
1286
1302
|
///////////////////////////////////////////////////////////////////////////////
|
|
1287
1303
|
|
|
1288
|
-
/**
|
|
1304
|
+
/**
|
|
1289
1305
|
* Create a color object with RGBA values, white by default
|
|
1290
1306
|
* @param {number} [r=1] - red
|
|
1291
1307
|
* @param {number} [g=1] - green
|
|
@@ -1296,29 +1312,27 @@ class Vector2
|
|
|
1296
1312
|
*/
|
|
1297
1313
|
function rgb(r, g, b, a) { return new Color(r, g, b, a); }
|
|
1298
1314
|
|
|
1299
|
-
/**
|
|
1315
|
+
/**
|
|
1300
1316
|
* Create a color object with HSLA values, white by default
|
|
1301
1317
|
* @param {number} [h=0] - hue
|
|
1302
1318
|
* @param {number} [s=0] - saturation
|
|
1303
1319
|
* @param {number} [l=1] - lightness
|
|
1304
1320
|
* @param {number} [a=1] - alpha
|
|
1305
1321
|
* @return {Color}
|
|
1306
|
-
* @memberof Utilities
|
|
1307
|
-
*/
|
|
1322
|
+
* @memberof Utilities */
|
|
1308
1323
|
function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
|
|
1309
1324
|
|
|
1310
|
-
/**
|
|
1325
|
+
/**
|
|
1311
1326
|
* Check if object is a valid Color
|
|
1312
1327
|
* @param {any} c
|
|
1313
1328
|
* @return {boolean}
|
|
1314
|
-
* @memberof Utilities
|
|
1315
|
-
*/
|
|
1329
|
+
* @memberof Utilities */
|
|
1316
1330
|
function isColor(c) { return c instanceof Color; }
|
|
1317
1331
|
|
|
1318
1332
|
// color asserts
|
|
1319
1333
|
function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c) && c.isValid(), 'Color is invalid.', c); }
|
|
1320
1334
|
|
|
1321
|
-
/**
|
|
1335
|
+
/**
|
|
1322
1336
|
* Color object (red, green, blue, alpha) with some helpful functions
|
|
1323
1337
|
* @example
|
|
1324
1338
|
* let a = new Color; // white
|
|
@@ -1390,7 +1404,7 @@ class Color
|
|
|
1390
1404
|
* @param {number} scale
|
|
1391
1405
|
* @param {number} [alphaScale=scale]
|
|
1392
1406
|
* @return {Color} */
|
|
1393
|
-
scale(scale, alphaScale=scale)
|
|
1407
|
+
scale(scale, alphaScale=scale)
|
|
1394
1408
|
{ return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
|
|
1395
1409
|
|
|
1396
1410
|
/** Returns a copy of this color clamped to the valid range between 0 and 1
|
|
@@ -1407,9 +1421,9 @@ class Color
|
|
|
1407
1421
|
ASSERT_NUMBER_VALID(percent);
|
|
1408
1422
|
const p = clamp(percent);
|
|
1409
1423
|
return new Color(
|
|
1410
|
-
c.r*p + this.r*(1-p),
|
|
1411
|
-
c.g*p + this.g*(1-p),
|
|
1412
|
-
c.b*p + this.b*(1-p),
|
|
1424
|
+
c.r*p + this.r*(1-p),
|
|
1425
|
+
c.g*p + this.g*(1-p),
|
|
1426
|
+
c.b*p + this.b*(1-p),
|
|
1413
1427
|
c.a*p + this.a*(1-p));
|
|
1414
1428
|
}
|
|
1415
1429
|
|
|
@@ -1449,15 +1463,15 @@ class Color
|
|
|
1449
1463
|
const minC = min(r, g, b);
|
|
1450
1464
|
const l = (maxC + minC) / 2;
|
|
1451
1465
|
let h = 0, s = 0;
|
|
1452
|
-
if (maxC
|
|
1466
|
+
if (maxC !== minC)
|
|
1453
1467
|
{
|
|
1454
1468
|
let d = maxC - minC;
|
|
1455
1469
|
s = l > .5 ? d / (2 - maxC - minC) : d / (maxC + minC);
|
|
1456
|
-
if (r
|
|
1470
|
+
if (r === maxC)
|
|
1457
1471
|
h = (g - b) / d + (g < b ? 6 : 0);
|
|
1458
|
-
else if (g
|
|
1472
|
+
else if (g === maxC)
|
|
1459
1473
|
h = (b - r) / d + 2;
|
|
1460
|
-
else if (b
|
|
1474
|
+
else if (b === maxC)
|
|
1461
1475
|
h = (r - g) / d + 4;
|
|
1462
1476
|
}
|
|
1463
1477
|
return [h / 6, s, l, a];
|
|
@@ -1467,7 +1481,7 @@ class Color
|
|
|
1467
1481
|
* @param {number} [amount]
|
|
1468
1482
|
* @param {number} [alphaAmount]
|
|
1469
1483
|
* @return {Color} */
|
|
1470
|
-
mutate(amount=.05, alphaAmount=0)
|
|
1484
|
+
mutate(amount=.05, alphaAmount=0)
|
|
1471
1485
|
{
|
|
1472
1486
|
ASSERT_NUMBER_VALID(amount);
|
|
1473
1487
|
ASSERT_NUMBER_VALID(alphaAmount);
|
|
@@ -1483,47 +1497,47 @@ class Color
|
|
|
1483
1497
|
/** Returns this color expressed as a hex color code
|
|
1484
1498
|
* @param {boolean} [useAlpha] - if alpha should be included in result
|
|
1485
1499
|
* @return {string} */
|
|
1486
|
-
toString(useAlpha = true)
|
|
1500
|
+
toString(useAlpha = true)
|
|
1487
1501
|
{
|
|
1488
|
-
ASSERT(typeof useAlpha
|
|
1502
|
+
ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
|
|
1489
1503
|
if (debug && !this.isValid())
|
|
1490
1504
|
return `#000`;
|
|
1491
1505
|
const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
|
|
1492
1506
|
return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
|
|
1493
1507
|
}
|
|
1494
|
-
|
|
1508
|
+
|
|
1495
1509
|
/** Set this color from a hex code
|
|
1496
1510
|
* @param {string} hex - html hex code
|
|
1497
1511
|
* @return {Color} */
|
|
1498
1512
|
setHex(hex)
|
|
1499
1513
|
{
|
|
1500
|
-
ASSERT(typeof hex
|
|
1514
|
+
ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
|
|
1501
1515
|
ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
|
|
1502
1516
|
|
|
1503
1517
|
if (hex.length < 6)
|
|
1504
1518
|
{
|
|
1505
1519
|
const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
|
|
1506
1520
|
this.r = fromHex(1);
|
|
1507
|
-
this.g = fromHex(2)
|
|
1521
|
+
this.g = fromHex(2);
|
|
1508
1522
|
this.b = fromHex(3);
|
|
1509
|
-
this.a = hex.length
|
|
1523
|
+
this.a = hex.length === 5 ? fromHex(4) : 1;
|
|
1510
1524
|
}
|
|
1511
1525
|
else
|
|
1512
1526
|
{
|
|
1513
1527
|
const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
|
|
1514
1528
|
this.r = fromHex(1);
|
|
1515
|
-
this.g = fromHex(3)
|
|
1529
|
+
this.g = fromHex(3);
|
|
1516
1530
|
this.b = fromHex(5);
|
|
1517
|
-
this.a = hex.length
|
|
1531
|
+
this.a = hex.length === 9 ? fromHex(7) : 1;
|
|
1518
1532
|
}
|
|
1519
1533
|
|
|
1520
1534
|
ASSERT_COLOR_VALID(this);
|
|
1521
1535
|
return this;
|
|
1522
1536
|
}
|
|
1523
|
-
|
|
1537
|
+
|
|
1524
1538
|
/** Returns this color expressed as 32 bit RGBA value
|
|
1525
1539
|
* @return {number} */
|
|
1526
|
-
rgbaInt()
|
|
1540
|
+
rgbaInt()
|
|
1527
1541
|
{
|
|
1528
1542
|
const r = clamp(this.r)*255|0;
|
|
1529
1543
|
const g = clamp(this.g)*255<<8;
|
|
@@ -1535,9 +1549,7 @@ class Color
|
|
|
1535
1549
|
/** Checks if this is a valid color
|
|
1536
1550
|
* @return {boolean} */
|
|
1537
1551
|
isValid()
|
|
1538
|
-
{
|
|
1539
|
-
return isNumber(this.r) && isNumber(this.g) && isNumber(this.b) && isNumber(this.a);
|
|
1540
|
-
}
|
|
1552
|
+
{ return isNumber(this.r) && isNumber(this.g) && isNumber(this.b) && isNumber(this.a); }
|
|
1541
1553
|
}
|
|
1542
1554
|
|
|
1543
1555
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -1546,7 +1558,7 @@ class Color
|
|
|
1546
1558
|
/** Color - White #ffffff
|
|
1547
1559
|
* @type {Color}
|
|
1548
1560
|
* @memberof Utilities */
|
|
1549
|
-
const WHITE = rgb();
|
|
1561
|
+
const WHITE = rgb();
|
|
1550
1562
|
|
|
1551
1563
|
/** Color - Clear White #ffffff with 0 alpha
|
|
1552
1564
|
* @type {Color}
|
|
@@ -1661,11 +1673,11 @@ class Timer
|
|
|
1661
1673
|
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
1662
1674
|
* @return {number} */
|
|
1663
1675
|
getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
|
|
1664
|
-
|
|
1676
|
+
|
|
1665
1677
|
/** Returns this timer expressed as a string
|
|
1666
1678
|
* @return {string} */
|
|
1667
1679
|
toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
|
|
1668
|
-
|
|
1680
|
+
|
|
1669
1681
|
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
1670
1682
|
* @return {number} */
|
|
1671
1683
|
valueOf() { return this.get(); }
|
|
@@ -1700,6 +1712,13 @@ let cameraScale = 32;
|
|
|
1700
1712
|
///////////////////////////////////////////////////////////////////////////////
|
|
1701
1713
|
// Display settings
|
|
1702
1714
|
|
|
1715
|
+
/** Enable applying color to tiles when using canvas2d
|
|
1716
|
+
* - This is slower but should be the same as WebGL rendering
|
|
1717
|
+
* @type {boolean}
|
|
1718
|
+
* @default
|
|
1719
|
+
* @memberof Settings */
|
|
1720
|
+
let canvasColorTiles = true;
|
|
1721
|
+
|
|
1703
1722
|
/** The max size of the canvas, centered if window is larger
|
|
1704
1723
|
* @type {Vector2}
|
|
1705
1724
|
* @default Vector2(1920,1080)
|
|
@@ -1713,14 +1732,21 @@ let canvasMaxSize = vec2(1920, 1080);
|
|
|
1713
1732
|
* @memberof Settings */
|
|
1714
1733
|
let canvasFixedSize = vec2();
|
|
1715
1734
|
|
|
1716
|
-
/** Use nearest
|
|
1717
|
-
* - Must be set before startup to take effect
|
|
1735
|
+
/** Use nearest canvas scaling for more pixelated look
|
|
1718
1736
|
* - If enabled sets css image-rendering:pixelated
|
|
1719
1737
|
* @type {boolean}
|
|
1720
1738
|
* @default
|
|
1721
1739
|
* @memberof Settings */
|
|
1722
1740
|
let canvasPixelated = true;
|
|
1723
1741
|
|
|
1742
|
+
/** Use nearest canvas scaling for more pixelated look
|
|
1743
|
+
* - If enabled sets css image-rendering:pixelated
|
|
1744
|
+
* - This defaults to false because text looks better with smoothing
|
|
1745
|
+
* @type {boolean}
|
|
1746
|
+
* @default
|
|
1747
|
+
* @memberof Settings */
|
|
1748
|
+
let overlayCanvasPixelated = false;
|
|
1749
|
+
|
|
1724
1750
|
/** Disables texture filtering for crisper pixel art
|
|
1725
1751
|
* @type {boolean}
|
|
1726
1752
|
* @default
|
|
@@ -1733,14 +1759,13 @@ let tilesPixelated = true;
|
|
|
1733
1759
|
* @memberof Settings */
|
|
1734
1760
|
let fontDefault = 'arial';
|
|
1735
1761
|
|
|
1736
|
-
/** Enable to show the LittleJS splash screen
|
|
1762
|
+
/** Enable to show the LittleJS splash screen on startup
|
|
1737
1763
|
* @type {boolean}
|
|
1738
1764
|
* @default
|
|
1739
1765
|
* @memberof Settings */
|
|
1740
1766
|
let showSplashScreen = false;
|
|
1741
1767
|
|
|
1742
1768
|
/** Disables all rendering, audio, and input for servers
|
|
1743
|
-
* - Must be set before startup to take effect
|
|
1744
1769
|
* @type {boolean}
|
|
1745
1770
|
* @default
|
|
1746
1771
|
* @memberof Settings */
|
|
@@ -1749,13 +1774,18 @@ let headlessMode = false;
|
|
|
1749
1774
|
///////////////////////////////////////////////////////////////////////////////
|
|
1750
1775
|
// WebGL settings
|
|
1751
1776
|
|
|
1752
|
-
/** Enable
|
|
1753
|
-
* - Must be set before startup to take effect
|
|
1777
|
+
/** Enable WebGL accelerated rendering
|
|
1754
1778
|
* @type {boolean}
|
|
1755
1779
|
* @default
|
|
1756
1780
|
* @memberof Settings */
|
|
1757
1781
|
let glEnable = true;
|
|
1758
1782
|
|
|
1783
|
+
/** How many sided poly to use when drawing circles and ellipses with WebGL
|
|
1784
|
+
* @type {number}
|
|
1785
|
+
* @default
|
|
1786
|
+
* @memberof Settings */
|
|
1787
|
+
let glCircleSides = 32;
|
|
1788
|
+
|
|
1759
1789
|
///////////////////////////////////////////////////////////////////////////////
|
|
1760
1790
|
// Tile sheet settings
|
|
1761
1791
|
|
|
@@ -1837,13 +1867,13 @@ let particleEmitRateScale = 1;
|
|
|
1837
1867
|
* @memberof Settings */
|
|
1838
1868
|
let gamepadsEnable = true;
|
|
1839
1869
|
|
|
1840
|
-
/** If true, the dpad input is also routed to the left analog stick (for better
|
|
1870
|
+
/** If true, the dpad input is also routed to the left analog stick (for better accessibility)
|
|
1841
1871
|
* @type {boolean}
|
|
1842
1872
|
* @default
|
|
1843
1873
|
* @memberof Settings */
|
|
1844
1874
|
let gamepadDirectionEmulateStick = true;
|
|
1845
1875
|
|
|
1846
|
-
/** If true the WASD keys are also routed to the direction keys (for better
|
|
1876
|
+
/** If true the WASD keys are also routed to the direction keys (for better accessibility)
|
|
1847
1877
|
* @type {boolean}
|
|
1848
1878
|
* @default
|
|
1849
1879
|
* @memberof Settings */
|
|
@@ -1851,7 +1881,6 @@ let inputWASDEmulateDirection = true;
|
|
|
1851
1881
|
|
|
1852
1882
|
/** True if touch input is enabled for mobile devices
|
|
1853
1883
|
* - Touch events will be routed to mouse events
|
|
1854
|
-
* - Must be set before startup to take effect
|
|
1855
1884
|
* @type {boolean}
|
|
1856
1885
|
* @default
|
|
1857
1886
|
* @memberof Settings */
|
|
@@ -1859,7 +1888,6 @@ let touchInputEnable = true;
|
|
|
1859
1888
|
|
|
1860
1889
|
/** True if touch gamepad should appear on mobile devices
|
|
1861
1890
|
* - Supports left analog stick, 4 face buttons and start button (button 9)
|
|
1862
|
-
* - Must be set before startup to take effect
|
|
1863
1891
|
* @type {boolean}
|
|
1864
1892
|
* @default
|
|
1865
1893
|
* @memberof Settings */
|
|
@@ -1961,6 +1989,14 @@ function setCameraAngle(angle) { cameraAngle = angle; }
|
|
|
1961
1989
|
* @memberof Settings */
|
|
1962
1990
|
function setCameraScale(scale) { cameraScale = scale; }
|
|
1963
1991
|
|
|
1992
|
+
/** Set if tiles should be colorized when using canvas2d
|
|
1993
|
+
* This can be slower but results should look nearly identical to WebGL rendering
|
|
1994
|
+
* It can be enabled/disabled at any time
|
|
1995
|
+
* Optimized for performance, and will use faster method if color is white or untextured
|
|
1996
|
+
* @param {boolean} colorTiles
|
|
1997
|
+
* @memberof Settings */
|
|
1998
|
+
function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
|
|
1999
|
+
|
|
1964
2000
|
/** Set max size of the canvas
|
|
1965
2001
|
* @param {Vector2} size
|
|
1966
2002
|
* @memberof Settings */
|
|
@@ -1971,10 +2007,30 @@ function setCanvasMaxSize(size) { canvasMaxSize = size; }
|
|
|
1971
2007
|
* @memberof Settings */
|
|
1972
2008
|
function setCanvasFixedSize(size) { canvasFixedSize = size; }
|
|
1973
2009
|
|
|
1974
|
-
/** Use nearest
|
|
2010
|
+
/** Use nearest scaling algorithm for canvas for more pixelated look
|
|
2011
|
+
* - If enabled sets css image-rendering:pixelated
|
|
2012
|
+
* @param {boolean} pixelated
|
|
2013
|
+
* @memberof Settings */
|
|
2014
|
+
function setCanvasPixelated(pixelated)
|
|
2015
|
+
{
|
|
2016
|
+
canvasPixelated = pixelated;
|
|
2017
|
+
if (mainCanvas)
|
|
2018
|
+
mainCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
|
|
2019
|
+
if (glCanvas)
|
|
2020
|
+
glCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
/** Use nearest scaling algorithm for canvas for more pixelated look
|
|
2024
|
+
* - If enabled sets css image-rendering:pixelated
|
|
2025
|
+
* - This defaults to false because text looks better with smoothing
|
|
1975
2026
|
* @param {boolean} pixelated
|
|
1976
2027
|
* @memberof Settings */
|
|
1977
|
-
function
|
|
2028
|
+
function setOverlayCanvasPixelated(pixelated)
|
|
2029
|
+
{
|
|
2030
|
+
overlayCanvasPixelated = pixelated;
|
|
2031
|
+
if (overlayCanvas)
|
|
2032
|
+
overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
|
|
2033
|
+
}
|
|
1978
2034
|
|
|
1979
2035
|
/** Disables texture filtering for crisper pixel art
|
|
1980
2036
|
* @param {boolean} pixelated
|
|
@@ -1986,7 +2042,7 @@ function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
|
|
|
1986
2042
|
* @memberof Settings */
|
|
1987
2043
|
function setFontDefault(font) { fontDefault = font; }
|
|
1988
2044
|
|
|
1989
|
-
/** Set if the LittleJS splash screen be shown on startup
|
|
2045
|
+
/** Set if the LittleJS splash screen should be shown on startup
|
|
1990
2046
|
* @param {boolean} show
|
|
1991
2047
|
* @memberof Settings */
|
|
1992
2048
|
function setShowSplashScreen(show) { showSplashScreen = show; }
|
|
@@ -1996,10 +2052,20 @@ function setShowSplashScreen(show) { showSplashScreen = show; }
|
|
|
1996
2052
|
* @memberof Settings */
|
|
1997
2053
|
function setHeadlessMode(headless) { headlessMode = headless; }
|
|
1998
2054
|
|
|
1999
|
-
/** Set if
|
|
2055
|
+
/** Set if WebGL rendering is enabled
|
|
2000
2056
|
* @param {boolean} enable
|
|
2001
2057
|
* @memberof Settings */
|
|
2002
|
-
function setGLEnable(enable)
|
|
2058
|
+
function setGLEnable(enable)
|
|
2059
|
+
{
|
|
2060
|
+
glEnable = enable;
|
|
2061
|
+
if (glCanvas) // hide glCanvas if WebGL is disabled
|
|
2062
|
+
glCanvas.style.visibility = enable ? 'visible' : 'hidden';
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/** Set how many sided polygons to use when drawing circles and elipses with WebGL
|
|
2066
|
+
* @param {number} sides
|
|
2067
|
+
* @memberof Settings */
|
|
2068
|
+
function setGLCircleSides(sides) { glCircleSides = sides; }
|
|
2003
2069
|
|
|
2004
2070
|
/** Set default size of tiles in pixels
|
|
2005
2071
|
* @param {Vector2} size
|
|
@@ -2031,7 +2097,7 @@ function setObjectDefaultDamping(damp) { objectDefaultDamping = damp; }
|
|
|
2031
2097
|
* @memberof Settings */
|
|
2032
2098
|
function setObjectDefaultAngleDamping(damp) { objectDefaultAngleDamping = damp; }
|
|
2033
2099
|
|
|
2034
|
-
/** Set how much to bounce when a collision
|
|
2100
|
+
/** Set how much to bounce when a collision occurs
|
|
2035
2101
|
* @param {number} restitution
|
|
2036
2102
|
* @memberof Settings */
|
|
2037
2103
|
function setObjectDefaultRestitution(restitution) { objectDefaultRestitution = restitution; }
|
|
@@ -2155,11 +2221,11 @@ function setShowWatermark(show) { showWatermark = show; }
|
|
|
2155
2221
|
* @param {string} key
|
|
2156
2222
|
* @memberof Debug */
|
|
2157
2223
|
function setDebugKey(key) { debugKey = key; }
|
|
2158
|
-
/**
|
|
2224
|
+
/**
|
|
2159
2225
|
* LittleJS Object System
|
|
2160
2226
|
*/
|
|
2161
2227
|
|
|
2162
|
-
/**
|
|
2228
|
+
/**
|
|
2163
2229
|
* LittleJS Object Base Object Class
|
|
2164
2230
|
* - Top level object class used by the engine
|
|
2165
2231
|
* - Automatically adds self to object list
|
|
@@ -2182,7 +2248,7 @@ function setDebugKey(key) { debugKey = key; }
|
|
|
2182
2248
|
* @example
|
|
2183
2249
|
* // create an engine object, normally you would first extend the class with your own
|
|
2184
2250
|
* const pos = vec2(2,3);
|
|
2185
|
-
* const object = new EngineObject(pos);
|
|
2251
|
+
* const object = new EngineObject(pos);
|
|
2186
2252
|
*/
|
|
2187
2253
|
class EngineObject
|
|
2188
2254
|
{
|
|
@@ -2200,9 +2266,9 @@ class EngineObject
|
|
|
2200
2266
|
ASSERT(isVector2(pos) && pos.isValid(), 'object pos should be a vec2');
|
|
2201
2267
|
ASSERT(isVector2(size) && size.isValid(), 'object size should be a vec2');
|
|
2202
2268
|
ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
|
|
2203
|
-
ASSERT(typeof angle
|
|
2269
|
+
ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
|
|
2204
2270
|
ASSERT(isColor(color) && color.isValid(), 'object color should be a valid rgba color');
|
|
2205
|
-
ASSERT(typeof renderOrder
|
|
2271
|
+
ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
|
|
2206
2272
|
|
|
2207
2273
|
/** @property {Vector2} - World space position of the object */
|
|
2208
2274
|
this.pos = pos.copy();
|
|
@@ -2270,7 +2336,7 @@ class EngineObject
|
|
|
2270
2336
|
// add to list of objects
|
|
2271
2337
|
engineObjects.push(this);
|
|
2272
2338
|
}
|
|
2273
|
-
|
|
2339
|
+
|
|
2274
2340
|
/** Update the object transform, called automatically by engine even when paused */
|
|
2275
2341
|
updateTransforms()
|
|
2276
2342
|
{
|
|
@@ -2339,7 +2405,7 @@ class EngineObject
|
|
|
2339
2405
|
for (const o of engineObjectsCollide)
|
|
2340
2406
|
{
|
|
2341
2407
|
// non solid objects don't collide with each other
|
|
2342
|
-
if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o
|
|
2408
|
+
if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o === this)
|
|
2343
2409
|
continue;
|
|
2344
2410
|
|
|
2345
2411
|
// check collision
|
|
@@ -2362,8 +2428,8 @@ class EngineObject
|
|
|
2362
2428
|
this.velocity = this.velocity.add(velocity);
|
|
2363
2429
|
if (o.mass) // push away if not fixed
|
|
2364
2430
|
o.velocity = o.velocity.subtract(velocity);
|
|
2365
|
-
|
|
2366
|
-
|
|
2431
|
+
|
|
2432
|
+
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
|
|
2367
2433
|
continue;
|
|
2368
2434
|
}
|
|
2369
2435
|
|
|
@@ -2373,7 +2439,7 @@ class EngineObject
|
|
|
2373
2439
|
const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
|
|
2374
2440
|
const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
|
|
2375
2441
|
const restitution = max(this.restitution, o.restitution);
|
|
2376
|
-
|
|
2442
|
+
|
|
2377
2443
|
if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
|
|
2378
2444
|
{
|
|
2379
2445
|
// push outside object collision
|
|
@@ -2425,7 +2491,7 @@ class EngineObject
|
|
|
2425
2491
|
else // bounce if other object is fixed
|
|
2426
2492
|
this.velocity.x *= -restitution;
|
|
2427
2493
|
}
|
|
2428
|
-
|
|
2494
|
+
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
|
|
2429
2495
|
}
|
|
2430
2496
|
}
|
|
2431
2497
|
if (this.collideTiles)
|
|
@@ -2461,7 +2527,7 @@ class EngineObject
|
|
|
2461
2527
|
{
|
|
2462
2528
|
// move to previous position
|
|
2463
2529
|
this.pos.y = oldPos.y;
|
|
2464
|
-
this.groundObject = undefined;
|
|
2530
|
+
this.groundObject = undefined;
|
|
2465
2531
|
}
|
|
2466
2532
|
}
|
|
2467
2533
|
if (blockedLayerX)
|
|
@@ -2470,25 +2536,25 @@ class EngineObject
|
|
|
2470
2536
|
this.pos.x = oldPos.x;
|
|
2471
2537
|
this.velocity.x *= -this.restitution;
|
|
2472
2538
|
}
|
|
2473
|
-
|
|
2539
|
+
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
2474
2540
|
}
|
|
2475
2541
|
}
|
|
2476
2542
|
}
|
|
2477
2543
|
}
|
|
2478
|
-
|
|
2544
|
+
|
|
2479
2545
|
/** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
|
|
2480
2546
|
render()
|
|
2481
2547
|
{
|
|
2482
2548
|
// default object render
|
|
2483
2549
|
drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
|
|
2484
2550
|
}
|
|
2485
|
-
|
|
2551
|
+
|
|
2486
2552
|
/** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
|
|
2487
2553
|
destroy()
|
|
2488
|
-
{
|
|
2554
|
+
{
|
|
2489
2555
|
if (this.destroyed)
|
|
2490
2556
|
return;
|
|
2491
|
-
|
|
2557
|
+
|
|
2492
2558
|
// disconnect from parent and destroy children
|
|
2493
2559
|
this.destroyed = 1;
|
|
2494
2560
|
this.parent && this.parent.removeChild(this);
|
|
@@ -2514,7 +2580,7 @@ class EngineObject
|
|
|
2514
2580
|
/** Convert from world space to local space for a vector (rotation only)
|
|
2515
2581
|
* @param {Vector2} vec - world space vector */
|
|
2516
2582
|
worldToLocalVector(vec) { return vec.rotate(-this.angle); }
|
|
2517
|
-
|
|
2583
|
+
|
|
2518
2584
|
/** Called to check if a tile collision should be resolved
|
|
2519
2585
|
* @param {number} tileData - the value of the tile at the position
|
|
2520
2586
|
* @param {Vector2} pos - tile where the collision occurred
|
|
@@ -2533,16 +2599,19 @@ class EngineObject
|
|
|
2533
2599
|
|
|
2534
2600
|
/** Apply acceleration to this object (adjust velocity, not affected by mass)
|
|
2535
2601
|
* @param {Vector2} acceleration */
|
|
2536
|
-
applyAcceleration(acceleration)
|
|
2602
|
+
applyAcceleration(acceleration)
|
|
2603
|
+
{ if (this.mass) this.velocity = this.velocity.add(acceleration); }
|
|
2537
2604
|
|
|
2538
|
-
/** Apply angular acceleration to this object
|
|
2605
|
+
/** Apply angular acceleration to this object
|
|
2539
2606
|
* @param {number} acceleration */
|
|
2540
|
-
applyAngularAcceleration(acceleration)
|
|
2607
|
+
applyAngularAcceleration(acceleration)
|
|
2608
|
+
{ if (this.mass) this.angleVelocity += acceleration; }
|
|
2541
2609
|
|
|
2542
2610
|
/** Apply force to this object (adjust velocity, affected by mass)
|
|
2543
2611
|
* @param {Vector2} force */
|
|
2544
|
-
applyForce(force)
|
|
2545
|
-
|
|
2612
|
+
applyForce(force)
|
|
2613
|
+
{ if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
|
|
2614
|
+
|
|
2546
2615
|
/** Get the direction of the mirror
|
|
2547
2616
|
* @return {number} -1 if this.mirror is true, or 1 if not mirrored */
|
|
2548
2617
|
getMirrorSign() { return this.mirror ? -1 : 1; }
|
|
@@ -2564,7 +2633,7 @@ class EngineObject
|
|
|
2564
2633
|
* @param {EngineObject} child */
|
|
2565
2634
|
removeChild(child)
|
|
2566
2635
|
{
|
|
2567
|
-
ASSERT(child.parent
|
|
2636
|
+
ASSERT(child.parent === this && this.children.includes(child));
|
|
2568
2637
|
this.children.splice(this.children.indexOf(child), 1);
|
|
2569
2638
|
child.parent = 0;
|
|
2570
2639
|
}
|
|
@@ -2610,7 +2679,7 @@ class EngineObject
|
|
|
2610
2679
|
{
|
|
2611
2680
|
if (!debug)
|
|
2612
2681
|
return;
|
|
2613
|
-
|
|
2682
|
+
|
|
2614
2683
|
// show object info for debugging
|
|
2615
2684
|
const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
|
|
2616
2685
|
const color = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
|
|
@@ -2620,24 +2689,24 @@ class EngineObject
|
|
|
2620
2689
|
this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(1,1,1,.5));
|
|
2621
2690
|
}
|
|
2622
2691
|
}
|
|
2623
|
-
/**
|
|
2692
|
+
/**
|
|
2624
2693
|
* LittleJS Drawing System
|
|
2625
2694
|
* - Hybrid system with both Canvas2D and WebGL available
|
|
2626
2695
|
* - Super fast tile sheet rendering with WebGL
|
|
2627
2696
|
* - Can apply rotation, mirror, color and additive color
|
|
2628
2697
|
* - Font rendering system with built in engine font
|
|
2629
2698
|
* - Many useful utility functions
|
|
2630
|
-
*
|
|
2699
|
+
*
|
|
2631
2700
|
* LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
|
|
2632
2701
|
* There are 3 canvas/contexts available to draw to...
|
|
2633
2702
|
* mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
|
|
2634
2703
|
* glCanvas - Used by the accelerated WebGL batch rendering system.
|
|
2635
2704
|
* overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
|
|
2636
|
-
*
|
|
2705
|
+
*
|
|
2637
2706
|
* The WebGL rendering system is very fast with some caveats...
|
|
2638
2707
|
* - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
|
|
2639
2708
|
* - Group additive rendering together using renderOrder to mitigate this issue
|
|
2640
|
-
*
|
|
2709
|
+
*
|
|
2641
2710
|
* The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
|
|
2642
2711
|
* @namespace Draw
|
|
2643
2712
|
*/
|
|
@@ -2672,7 +2741,17 @@ let drawCanvas;
|
|
|
2672
2741
|
* @memberof Draw */
|
|
2673
2742
|
let drawContext;
|
|
2674
2743
|
|
|
2675
|
-
/**
|
|
2744
|
+
/** Offscreen canvas that can be used for image processing
|
|
2745
|
+
* @type {OffscreenCanvas}
|
|
2746
|
+
* @memberof Draw */
|
|
2747
|
+
let workCanvas;
|
|
2748
|
+
|
|
2749
|
+
/** Offscreen canvas that can be used for image processing
|
|
2750
|
+
* @type {OffscreenCanvasRenderingContext2D}
|
|
2751
|
+
* @memberof Draw */
|
|
2752
|
+
let workContext;
|
|
2753
|
+
|
|
2754
|
+
/** The size of the main canvas (and other secondary canvases)
|
|
2676
2755
|
* @type {Vector2}
|
|
2677
2756
|
* @memberof Draw */
|
|
2678
2757
|
let mainCanvasSize = vec2();
|
|
@@ -2687,29 +2766,28 @@ let drawCount;
|
|
|
2687
2766
|
|
|
2688
2767
|
///////////////////////////////////////////////////////////////////////////////
|
|
2689
2768
|
|
|
2690
|
-
/**
|
|
2769
|
+
/**
|
|
2691
2770
|
* Create a tile info object using a grid based system
|
|
2692
2771
|
* - This can take vecs or floats for easier use and conversion
|
|
2693
2772
|
* - If an index is passed in, the tile size and index will determine the position
|
|
2694
|
-
* @param {Vector2|number} [pos=0]
|
|
2773
|
+
* @param {Vector2|number} [pos=0] - Index of tile in sheet
|
|
2695
2774
|
* @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
|
|
2696
|
-
* @param {number} [textureIndex]
|
|
2697
|
-
* @param {number} [padding]
|
|
2775
|
+
* @param {number} [textureIndex] - Texture index to use
|
|
2776
|
+
* @param {number} [padding] - How many pixels padding around tiles
|
|
2698
2777
|
* @return {TileInfo}
|
|
2699
2778
|
* @example
|
|
2700
2779
|
* tile(2) // a tile at index 2 using the default tile size of 16
|
|
2701
2780
|
* tile(5, 8) // a tile at index 5 using a tile size of 8
|
|
2702
2781
|
* tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
|
|
2703
2782
|
* tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
|
|
2704
|
-
* @memberof Draw
|
|
2705
|
-
*/
|
|
2783
|
+
* @memberof Draw */
|
|
2706
2784
|
function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
|
|
2707
2785
|
{
|
|
2708
2786
|
if (headlessMode)
|
|
2709
2787
|
return new TileInfo;
|
|
2710
2788
|
|
|
2711
2789
|
// if size is a number, make it a vector
|
|
2712
|
-
if (typeof size
|
|
2790
|
+
if (typeof size === 'number')
|
|
2713
2791
|
{
|
|
2714
2792
|
ASSERT(size > 0);
|
|
2715
2793
|
size = new Vector2(size, size);
|
|
@@ -2718,24 +2796,24 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
|
|
|
2718
2796
|
// create tile info object
|
|
2719
2797
|
const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
|
|
2720
2798
|
|
|
2721
|
-
//
|
|
2799
|
+
// get the position of the tile
|
|
2722
2800
|
const textureInfo = textureInfos[textureIndex];
|
|
2723
2801
|
ASSERT(!!textureInfo, 'Texture not loaded');
|
|
2724
2802
|
const sizePaddedX = size.x + padding*2;
|
|
2725
2803
|
const sizePaddedY = size.y + padding*2;
|
|
2726
|
-
if (typeof pos
|
|
2804
|
+
if (typeof pos === 'number')
|
|
2727
2805
|
{
|
|
2728
2806
|
const cols = textureInfo.size.x / sizePaddedX |0;
|
|
2729
|
-
ASSERT(cols>0, 'Tile size is too big for texture');
|
|
2807
|
+
ASSERT(cols > 0, 'Tile size is too big for texture');
|
|
2730
2808
|
const posX = pos % cols, posY = (pos / cols) |0;
|
|
2731
2809
|
tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
|
|
2732
2810
|
}
|
|
2733
2811
|
else
|
|
2734
2812
|
tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
|
|
2735
|
-
return tileInfo;
|
|
2813
|
+
return tileInfo;
|
|
2736
2814
|
}
|
|
2737
2815
|
|
|
2738
|
-
/**
|
|
2816
|
+
/**
|
|
2739
2817
|
* Tile Info - Stores info about how to draw a tile
|
|
2740
2818
|
*/
|
|
2741
2819
|
class TileInfo
|
|
@@ -2773,14 +2851,14 @@ class TileInfo
|
|
|
2773
2851
|
*/
|
|
2774
2852
|
frame(frame)
|
|
2775
2853
|
{
|
|
2776
|
-
ASSERT(typeof frame
|
|
2854
|
+
ASSERT(typeof frame === 'number');
|
|
2777
2855
|
return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
|
|
2778
2856
|
}
|
|
2779
2857
|
|
|
2780
2858
|
/**
|
|
2781
2859
|
* Set this tile to use a full image
|
|
2782
2860
|
* @param {HTMLImageElement|OffscreenCanvas} image
|
|
2783
|
-
* @param {WebGLTexture} [glTexture] -
|
|
2861
|
+
* @param {WebGLTexture} [glTexture] - WebGL texture
|
|
2784
2862
|
* @return {TileInfo}
|
|
2785
2863
|
*/
|
|
2786
2864
|
setFullImage(image, glTexture)
|
|
@@ -2798,7 +2876,7 @@ class TextureInfo
|
|
|
2798
2876
|
/**
|
|
2799
2877
|
* Create a TextureInfo, called automatically by the engine
|
|
2800
2878
|
* @param {HTMLImageElement|OffscreenCanvas} image
|
|
2801
|
-
* @param {WebGLTexture} [glTexture] -
|
|
2879
|
+
* @param {WebGLTexture} [glTexture] - WebGL texture
|
|
2802
2880
|
*/
|
|
2803
2881
|
constructor(image, glTexture)
|
|
2804
2882
|
{
|
|
@@ -2808,7 +2886,7 @@ class TextureInfo
|
|
|
2808
2886
|
this.size = vec2(image.width, image.height);
|
|
2809
2887
|
/** @property {Vector2} - inverse of the size, cached for rendering */
|
|
2810
2888
|
this.sizeInverse = vec2(1/image.width, 1/image.height);
|
|
2811
|
-
/** @property {WebGLTexture} -
|
|
2889
|
+
/** @property {WebGLTexture} - WebGL texture */
|
|
2812
2890
|
this.glTexture = glTexture;
|
|
2813
2891
|
}
|
|
2814
2892
|
|
|
@@ -2821,76 +2899,28 @@ class TextureInfo
|
|
|
2821
2899
|
}
|
|
2822
2900
|
|
|
2823
2901
|
///////////////////////////////////////////////////////////////////////////////
|
|
2824
|
-
|
|
2825
|
-
/** Convert from screen to world space coordinates
|
|
2826
|
-
* @param {Vector2} screenPos
|
|
2827
|
-
* @return {Vector2}
|
|
2828
|
-
* @memberof Draw */
|
|
2829
|
-
function screenToWorld(screenPos)
|
|
2830
|
-
{
|
|
2831
|
-
let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
|
|
2832
|
-
let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
|
|
2833
|
-
if (cameraAngle)
|
|
2834
|
-
{
|
|
2835
|
-
// apply camera rotation
|
|
2836
|
-
const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
|
|
2837
|
-
const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
|
|
2838
|
-
const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
|
|
2839
|
-
cameraPosRelativeX = rotatedX;
|
|
2840
|
-
cameraPosRelativeY = rotatedY;
|
|
2841
|
-
}
|
|
2842
|
-
return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
/** Convert from world to screen space coordinates
|
|
2846
|
-
* @param {Vector2} worldPos
|
|
2847
|
-
* @return {Vector2}
|
|
2848
|
-
* @memberof Draw */
|
|
2849
|
-
function worldToScreen(worldPos)
|
|
2850
|
-
{
|
|
2851
|
-
let cameraPosRelativeX = worldPos.x - cameraPos.x;
|
|
2852
|
-
let cameraPosRelativeY = worldPos.y - cameraPos.y;
|
|
2853
|
-
if (cameraAngle)
|
|
2854
|
-
{
|
|
2855
|
-
// apply inverse camera rotation
|
|
2856
|
-
const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
|
|
2857
|
-
const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
|
|
2858
|
-
const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
|
|
2859
|
-
cameraPosRelativeX = rotatedX;
|
|
2860
|
-
cameraPosRelativeY = rotatedY;
|
|
2861
|
-
}
|
|
2862
|
-
return new Vector2
|
|
2863
|
-
(
|
|
2864
|
-
cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
|
|
2865
|
-
cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
|
|
2866
|
-
);
|
|
2867
|
-
}
|
|
2868
|
-
|
|
2869
|
-
/** Get the camera's visible area in world space
|
|
2870
|
-
* @return {Vector2}
|
|
2871
|
-
* @memberof Draw */
|
|
2872
|
-
function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
2902
|
+
// Drawing functions
|
|
2873
2903
|
|
|
2874
2904
|
/** Draw textured tile centered in world space, with color applied if using WebGL
|
|
2875
|
-
* @param {Vector2}
|
|
2876
|
-
* @param {Vector2}
|
|
2877
|
-
* @param {TileInfo}[tileInfo]
|
|
2878
|
-
* @param {Color}
|
|
2879
|
-
* @param {number}
|
|
2880
|
-
* @param {boolean}
|
|
2881
|
-
* @param {Color}
|
|
2882
|
-
* @param {boolean}
|
|
2883
|
-
* @param {boolean}
|
|
2905
|
+
* @param {Vector2} pos - Center of the tile in world space
|
|
2906
|
+
* @param {Vector2} [size=(1,1)] - Size of the tile in world space
|
|
2907
|
+
* @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
|
|
2908
|
+
* @param {Color} [color=(1,1,1,1)] - Color to modulate with
|
|
2909
|
+
* @param {number} [angle] - Angle to rotate by
|
|
2910
|
+
* @param {boolean} [mirror] - Is image flipped along the Y axis?
|
|
2911
|
+
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
2912
|
+
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
2913
|
+
* @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
|
|
2884
2914
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
2885
2915
|
* @memberof Draw */
|
|
2886
|
-
function drawTile(pos, size=new Vector2(1), tileInfo, color=
|
|
2916
|
+
function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
|
|
2887
2917
|
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
|
|
2888
2918
|
{
|
|
2889
|
-
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
2890
2919
|
ASSERT(isVector2(pos) && pos.isValid(), 'drawTile pos should be a vec2');
|
|
2891
2920
|
ASSERT(isVector2(size) && size.isValid(), 'drawTile size should be a vec2');
|
|
2892
2921
|
ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
|
|
2893
2922
|
ASSERT(isNumber(angle), 'drawTile angle should be a number');
|
|
2923
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
2894
2924
|
|
|
2895
2925
|
const textureInfo = tileInfo && tileInfo.textureInfo;
|
|
2896
2926
|
if (useWebGL)
|
|
@@ -2914,22 +2944,22 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
|
|
|
2914
2944
|
{
|
|
2915
2945
|
const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
|
|
2916
2946
|
const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
|
|
2917
|
-
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
2918
|
-
x + tileImageFixBleedX, y + tileImageFixBleedY,
|
|
2919
|
-
x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
|
|
2920
|
-
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
2947
|
+
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
2948
|
+
x + tileImageFixBleedX, y + tileImageFixBleedY,
|
|
2949
|
+
x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
|
|
2950
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
2921
2951
|
}
|
|
2922
2952
|
else
|
|
2923
2953
|
{
|
|
2924
|
-
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
2925
|
-
x, y, x + w, y + h,
|
|
2926
|
-
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
2954
|
+
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
2955
|
+
x, y, x + w, y + h,
|
|
2956
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
2927
2957
|
}
|
|
2928
2958
|
}
|
|
2929
2959
|
else
|
|
2930
2960
|
{
|
|
2931
2961
|
// if no tile info, force untextured
|
|
2932
|
-
glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
|
|
2962
|
+
glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
|
|
2933
2963
|
}
|
|
2934
2964
|
}
|
|
2935
2965
|
else
|
|
@@ -2942,18 +2972,15 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
|
|
|
2942
2972
|
if (textureInfo)
|
|
2943
2973
|
{
|
|
2944
2974
|
// calculate uvs and render
|
|
2945
|
-
const x = tileInfo.pos.x
|
|
2946
|
-
const
|
|
2947
|
-
|
|
2948
|
-
const h = tileInfo.size.y - 2*tileFixBleedScale;
|
|
2949
|
-
context.globalAlpha = color.a; // only alpha is supported
|
|
2950
|
-
context.drawImage(textureInfo.image, x, y, w, h, -.5, -.5, 1, 1);
|
|
2951
|
-
context.globalAlpha = 1; // set back to full alpha
|
|
2975
|
+
const x = tileInfo.pos.x, y = tileInfo.pos.y;
|
|
2976
|
+
const w = tileInfo.size.x, h = tileInfo.size.y;
|
|
2977
|
+
drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor);
|
|
2952
2978
|
}
|
|
2953
2979
|
else
|
|
2954
2980
|
{
|
|
2955
|
-
// if no tile info,
|
|
2956
|
-
|
|
2981
|
+
// if no tile info, use untextured rect
|
|
2982
|
+
const c = additiveColor ? color.add(additiveColor) : color;
|
|
2983
|
+
context.fillStyle = c.toString();
|
|
2957
2984
|
context.fillRect(-.5, -.5, 1, 1);
|
|
2958
2985
|
}
|
|
2959
2986
|
}, screenSpace, context);
|
|
@@ -2966,12 +2993,12 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
|
|
|
2966
2993
|
* @param {Color} [color=(1,1,1,1)]
|
|
2967
2994
|
* @param {number} [angle]
|
|
2968
2995
|
* @param {boolean} [useWebGL=glEnable]
|
|
2969
|
-
* @param {boolean} [screenSpace
|
|
2996
|
+
* @param {boolean} [screenSpace]
|
|
2970
2997
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
2971
2998
|
* @memberof Draw */
|
|
2972
2999
|
function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
2973
|
-
{
|
|
2974
|
-
drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
|
|
3000
|
+
{
|
|
3001
|
+
drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
|
|
2975
3002
|
}
|
|
2976
3003
|
|
|
2977
3004
|
/** Draw colored line between two points
|
|
@@ -2979,72 +3006,138 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
2979
3006
|
* @param {Vector2} posB
|
|
2980
3007
|
* @param {number} [thickness]
|
|
2981
3008
|
* @param {Color} [color=(1,1,1,1)]
|
|
3009
|
+
* @param {Vector2} [pos=(0,0)] - Offset to apply
|
|
3010
|
+
* @param {number} [angle] - Angle to rotate by
|
|
2982
3011
|
* @param {boolean} [useWebGL=glEnable]
|
|
2983
|
-
* @param {boolean} [screenSpace
|
|
3012
|
+
* @param {boolean} [screenSpace]
|
|
2984
3013
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
2985
3014
|
* @memberof Draw */
|
|
2986
|
-
function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
|
|
3015
|
+
function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
|
|
2987
3016
|
{
|
|
2988
3017
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
2989
3018
|
const size = vec2(thickness, halfDelta.length()*2);
|
|
2990
|
-
|
|
3019
|
+
pos = pos.add(posA.add(halfDelta));
|
|
3020
|
+
angle += halfDelta.angle();
|
|
3021
|
+
drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
/** Draw colored regular polygon using passed in number of sides
|
|
3025
|
+
* @param {Vector2} pos
|
|
3026
|
+
* @param {Vector2} [size=(1,1)]
|
|
3027
|
+
* @param {number} [sides]
|
|
3028
|
+
* @param {Color} [color=(1,1,1,1)]
|
|
3029
|
+
* @param {number} [angle]
|
|
3030
|
+
* @param {number} [lineWidth]
|
|
3031
|
+
* @param {Color} [lineColor=(0,0,0,1)]
|
|
3032
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3033
|
+
* @param {boolean} [screenSpace]
|
|
3034
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3035
|
+
* @memberof Draw */
|
|
3036
|
+
function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
3037
|
+
{
|
|
3038
|
+
// build regular polygon points
|
|
3039
|
+
const points = [];
|
|
3040
|
+
for (let i=sides; i--;)
|
|
3041
|
+
{
|
|
3042
|
+
const a = (i/sides)*PI*2;
|
|
3043
|
+
points.push(vec2(Math.sin(a)*size.x, Math.cos(a)*size.y));
|
|
3044
|
+
}
|
|
3045
|
+
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
|
|
2991
3046
|
}
|
|
2992
3047
|
|
|
2993
3048
|
/** Draw colored polygon using passed in points
|
|
2994
|
-
* @param {Array<Vector2>}
|
|
3049
|
+
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
2995
3050
|
* @param {Color} [color=(1,1,1,1)]
|
|
2996
|
-
* @param {number} [lineWidth
|
|
3051
|
+
* @param {number} [lineWidth]
|
|
2997
3052
|
* @param {Color} [lineColor=(0,0,0,1)]
|
|
2998
|
-
* @param {
|
|
2999
|
-
* @param {
|
|
3053
|
+
* @param {Vector2} [pos=(0,0)] - Offset to apply
|
|
3054
|
+
* @param {number} [angle] - Angle to rotate by
|
|
3055
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3056
|
+
* @param {boolean} [screenSpace]
|
|
3057
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3000
3058
|
* @memberof Draw */
|
|
3001
|
-
function drawPoly(points, color=
|
|
3059
|
+
function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
|
|
3002
3060
|
{
|
|
3003
|
-
ASSERT(
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
context
|
|
3009
|
-
|
|
3010
|
-
if (lineWidth)
|
|
3061
|
+
ASSERT(isVector2(pos) && pos.isValid(), 'drawPoly pos should be a vec2');
|
|
3062
|
+
ASSERT(Array.isArray(points), 'drawPoly points should be an array');
|
|
3063
|
+
ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
|
|
3064
|
+
ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
|
|
3065
|
+
ASSERT(isNumber(angle), 'drawPoly angle should be a number');
|
|
3066
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3067
|
+
if (useWebGL)
|
|
3011
3068
|
{
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3069
|
+
let scale = 1;
|
|
3070
|
+
if (screenSpace)
|
|
3071
|
+
{
|
|
3072
|
+
// convert to world space
|
|
3073
|
+
pos = screenToWorld(pos);
|
|
3074
|
+
scale = 1/cameraScale;
|
|
3075
|
+
}
|
|
3076
|
+
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
|
|
3077
|
+
if (lineWidth > 0)
|
|
3078
|
+
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, scale, scale, angle);
|
|
3079
|
+
}
|
|
3080
|
+
else
|
|
3081
|
+
{
|
|
3082
|
+
drawCanvas2D(pos, vec2(1), angle, false, context=>
|
|
3083
|
+
{
|
|
3084
|
+
context.fillStyle = color.toString();
|
|
3085
|
+
context.beginPath();
|
|
3086
|
+
for (const point of points)
|
|
3087
|
+
context.lineTo(point.x, point.y);
|
|
3088
|
+
context.closePath();
|
|
3089
|
+
context.fill();
|
|
3090
|
+
if (lineWidth)
|
|
3091
|
+
{
|
|
3092
|
+
context.strokeStyle = lineColor.toString();
|
|
3093
|
+
context.lineWidth = lineWidth;
|
|
3094
|
+
context.stroke();
|
|
3095
|
+
}
|
|
3096
|
+
}, screenSpace, context);
|
|
3015
3097
|
}
|
|
3016
3098
|
}
|
|
3017
3099
|
|
|
3018
3100
|
/** Draw colored ellipse using passed in point
|
|
3019
3101
|
* @param {Vector2} pos
|
|
3020
|
-
* @param {
|
|
3021
|
-
* @param {number} [height=1]
|
|
3022
|
-
* @param {number} [angle=0]
|
|
3102
|
+
* @param {Vector2} [size=(1,1)]
|
|
3023
3103
|
* @param {Color} [color=(1,1,1,1)]
|
|
3024
|
-
* @param {number} [
|
|
3104
|
+
* @param {number} [angle]
|
|
3105
|
+
* @param {number} [lineWidth]
|
|
3025
3106
|
* @param {Color} [lineColor=(0,0,0,1)]
|
|
3026
|
-
* @param {boolean} [
|
|
3027
|
-
* @param {
|
|
3107
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3108
|
+
* @param {boolean} [screenSpace]
|
|
3109
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3028
3110
|
* @memberof Draw */
|
|
3029
|
-
function drawEllipse(pos,
|
|
3111
|
+
function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
|
|
3030
3112
|
{
|
|
3031
|
-
ASSERT(
|
|
3032
|
-
|
|
3113
|
+
ASSERT(isVector2(pos) && pos.isValid(), 'drawEllipse pos should be a vec2');
|
|
3114
|
+
ASSERT(isVector2(size) && size.isValid(), 'drawEllipse size should be a vec2');
|
|
3115
|
+
ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
|
|
3116
|
+
ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
|
|
3117
|
+
ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
|
|
3118
|
+
ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
|
|
3119
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3120
|
+
if (useWebGL)
|
|
3033
3121
|
{
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
lineWidth *= cameraScale;
|
|
3122
|
+
// draw as a regular polygon
|
|
3123
|
+
const sides = glCircleSides;
|
|
3124
|
+
drawRegularPoly(pos, size, sides, color, lineWidth, lineColor, angle, useWebGL, screenSpace, context);
|
|
3038
3125
|
}
|
|
3039
|
-
|
|
3040
|
-
context.beginPath();
|
|
3041
|
-
context.ellipse(pos.x, pos.y, width, height, angle, 0, 9);
|
|
3042
|
-
context.fill();
|
|
3043
|
-
if (lineWidth)
|
|
3126
|
+
else
|
|
3044
3127
|
{
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3128
|
+
drawCanvas2D(pos, vec2(1), angle, false, context=>
|
|
3129
|
+
{
|
|
3130
|
+
context.fillStyle = color.toString();
|
|
3131
|
+
context.beginPath();
|
|
3132
|
+
context.ellipse(0, 0, size.x, size.y, 0, 0, 9);
|
|
3133
|
+
context.fill();
|
|
3134
|
+
if (lineWidth)
|
|
3135
|
+
{
|
|
3136
|
+
context.strokeStyle = lineColor.toString();
|
|
3137
|
+
context.lineWidth = lineWidth;
|
|
3138
|
+
context.stroke();
|
|
3139
|
+
}
|
|
3140
|
+
}, screenSpace, context);
|
|
3048
3141
|
}
|
|
3049
3142
|
}
|
|
3050
3143
|
|
|
@@ -3054,11 +3147,12 @@ function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth
|
|
|
3054
3147
|
* @param {Color} [color=(1,1,1,1)]
|
|
3055
3148
|
* @param {number} [lineWidth=0]
|
|
3056
3149
|
* @param {Color} [lineColor=(0,0,0,1)]
|
|
3057
|
-
* @param {boolean} [
|
|
3058
|
-
* @param {
|
|
3150
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3151
|
+
* @param {boolean} [screenSpace]
|
|
3152
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3059
3153
|
* @memberof Draw */
|
|
3060
|
-
function drawCircle(pos, radius=1, color=
|
|
3061
|
-
{ drawEllipse(pos, radius,
|
|
3154
|
+
function drawCircle(pos, radius=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
|
|
3155
|
+
{ drawEllipse(pos, vec2(radius), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context); }
|
|
3062
3156
|
|
|
3063
3157
|
/** Draw directly to a 2d canvas context in world space
|
|
3064
3158
|
* @param {Vector2} pos
|
|
@@ -3066,7 +3160,7 @@ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new C
|
|
|
3066
3160
|
* @param {number} angle
|
|
3067
3161
|
* @param {boolean} [mirror]
|
|
3068
3162
|
* @param {Function} [drawFunction]
|
|
3069
|
-
* @param {boolean}
|
|
3163
|
+
* @param {boolean} [screenSpace=false]
|
|
3070
3164
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
3071
3165
|
* @memberof Draw */
|
|
3072
3166
|
function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
|
|
@@ -3085,6 +3179,9 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
3085
3179
|
context.restore();
|
|
3086
3180
|
}
|
|
3087
3181
|
|
|
3182
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3183
|
+
// Text Drawing Functions
|
|
3184
|
+
|
|
3088
3185
|
/** Draw text on main canvas in world space
|
|
3089
3186
|
* Automatically splits new lines into rows
|
|
3090
3187
|
* @param {string} text
|
|
@@ -3133,7 +3230,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
3133
3230
|
* @param {number} [maxWidth]
|
|
3134
3231
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
|
|
3135
3232
|
* @memberof Draw */
|
|
3136
|
-
function drawTextScreen(text, pos, size=1, color=
|
|
3233
|
+
function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
|
|
3137
3234
|
{
|
|
3138
3235
|
context.fillStyle = color.toString();
|
|
3139
3236
|
context.strokeStyle = lineColor.toString();
|
|
@@ -3154,22 +3251,67 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
|
|
|
3154
3251
|
});
|
|
3155
3252
|
}
|
|
3156
3253
|
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3254
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3255
|
+
// Drawing utilities
|
|
3256
|
+
|
|
3257
|
+
/** Convert from screen to world space coordinates
|
|
3258
|
+
* @param {Vector2} screenPos
|
|
3259
|
+
* @return {Vector2}
|
|
3161
3260
|
* @memberof Draw */
|
|
3162
|
-
function
|
|
3261
|
+
function screenToWorld(screenPos)
|
|
3163
3262
|
{
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
else
|
|
3263
|
+
let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
|
|
3264
|
+
let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
|
|
3265
|
+
if (cameraAngle)
|
|
3168
3266
|
{
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3267
|
+
// apply camera rotation
|
|
3268
|
+
const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
|
|
3269
|
+
const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
|
|
3270
|
+
const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
|
|
3271
|
+
cameraPosRelativeX = rotatedX;
|
|
3272
|
+
cameraPosRelativeY = rotatedY;
|
|
3172
3273
|
}
|
|
3274
|
+
return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
/** Convert from world to screen space coordinates
|
|
3278
|
+
* @param {Vector2} worldPos
|
|
3279
|
+
* @return {Vector2}
|
|
3280
|
+
* @memberof Draw */
|
|
3281
|
+
function worldToScreen(worldPos)
|
|
3282
|
+
{
|
|
3283
|
+
let cameraPosRelativeX = worldPos.x - cameraPos.x;
|
|
3284
|
+
let cameraPosRelativeY = worldPos.y - cameraPos.y;
|
|
3285
|
+
if (cameraAngle)
|
|
3286
|
+
{
|
|
3287
|
+
// apply inverse camera rotation
|
|
3288
|
+
const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
|
|
3289
|
+
const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
|
|
3290
|
+
const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
|
|
3291
|
+
cameraPosRelativeX = rotatedX;
|
|
3292
|
+
cameraPosRelativeY = rotatedY;
|
|
3293
|
+
}
|
|
3294
|
+
return new Vector2
|
|
3295
|
+
(
|
|
3296
|
+
cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
|
|
3297
|
+
cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
|
|
3298
|
+
);
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
/** Get the camera's visible area in world space
|
|
3302
|
+
* @return {Vector2}
|
|
3303
|
+
* @memberof Draw */
|
|
3304
|
+
function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
3305
|
+
|
|
3306
|
+
/** Enable normal or additive blend mode
|
|
3307
|
+
* @param {boolean} [additive]
|
|
3308
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3309
|
+
* @memberof Draw */
|
|
3310
|
+
function setBlendMode(additive=false, context)
|
|
3311
|
+
{
|
|
3312
|
+
glAdditive = additive;
|
|
3313
|
+
context ||= drawContext;
|
|
3314
|
+
context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
|
|
3173
3315
|
}
|
|
3174
3316
|
|
|
3175
3317
|
/** Combines all LittleJS canvases onto the main canvas and clears them
|
|
@@ -3186,11 +3328,107 @@ function combineCanvases()
|
|
|
3186
3328
|
overlayCanvas.width |= 0;
|
|
3187
3329
|
}
|
|
3188
3330
|
|
|
3331
|
+
/** Helper function to draw an image with color and additive color applied
|
|
3332
|
+
* This is slower then normal drawImage when color is applied
|
|
3333
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
3334
|
+
* @param {HTMLImageElement|OffscreenCanvas} image
|
|
3335
|
+
* @param {number} sx
|
|
3336
|
+
* @param {number} sy
|
|
3337
|
+
* @param {number} sWidth
|
|
3338
|
+
* @param {number} sHeight
|
|
3339
|
+
* @param {number} dx
|
|
3340
|
+
* @param {number} dy
|
|
3341
|
+
* @param {number} dWidth
|
|
3342
|
+
* @param {number} dHeight
|
|
3343
|
+
* @param {Color} color
|
|
3344
|
+
* @param {Color} [additiveColor]
|
|
3345
|
+
* @memberof Draw */
|
|
3346
|
+
function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor)
|
|
3347
|
+
{
|
|
3348
|
+
function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
|
|
3349
|
+
function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
|
|
3350
|
+
const sx2 = tileFixBleedScale;
|
|
3351
|
+
const sy2 = tileFixBleedScale;
|
|
3352
|
+
const sWidth2 = sWidth - 2*tileFixBleedScale;
|
|
3353
|
+
const sHeight2 = sHeight - 2*tileFixBleedScale;
|
|
3354
|
+
if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
|
|
3355
|
+
{
|
|
3356
|
+
// white texture with no additive alpha, no need to tint
|
|
3357
|
+
context.globalAlpha = color.a;
|
|
3358
|
+
context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3359
|
+
context.globalAlpha = 1;
|
|
3360
|
+
}
|
|
3361
|
+
else
|
|
3362
|
+
{
|
|
3363
|
+
// copy to offscreen canvas
|
|
3364
|
+
workCanvas.width = sWidth;
|
|
3365
|
+
workCanvas.height = sHeight;
|
|
3366
|
+
workContext.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
|
|
3367
|
+
|
|
3368
|
+
// tint image using offscreen work context
|
|
3369
|
+
const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
|
|
3370
|
+
const data = imageData.data;
|
|
3371
|
+
if (additiveColor && !isBlack(additiveColor))
|
|
3372
|
+
{
|
|
3373
|
+
// slower path with additive color
|
|
3374
|
+
const colorMultiply = [color.r, color.g, color.b, color.a];
|
|
3375
|
+
const colorAdd = [additiveColor.r * 255, additiveColor.g * 255, additiveColor.b * 255, additiveColor.a * 255];
|
|
3376
|
+
for (let i = 0; i < data.length; ++i)
|
|
3377
|
+
data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
|
|
3378
|
+
workContext.putImageData(imageData, 0, 0);
|
|
3379
|
+
context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3380
|
+
}
|
|
3381
|
+
else
|
|
3382
|
+
{
|
|
3383
|
+
// faster path with no additive color
|
|
3384
|
+
for (let i = 0; i < data.length; i+=4)
|
|
3385
|
+
{
|
|
3386
|
+
data[i ] *= color.r;
|
|
3387
|
+
data[i+1] *= color.g;
|
|
3388
|
+
data[i+2] *= color.b;
|
|
3389
|
+
}
|
|
3390
|
+
workContext.putImageData(imageData, 0, 0);
|
|
3391
|
+
context.globalAlpha = color.a;
|
|
3392
|
+
context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
3393
|
+
context.globalAlpha = 1;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
|
|
3399
|
+
/** Returns true if fullscreen mode is active
|
|
3400
|
+
* @return {boolean}
|
|
3401
|
+
* @memberof Draw */
|
|
3402
|
+
function isFullscreen() { return !!document.fullscreenElement; }
|
|
3403
|
+
|
|
3404
|
+
/** Toggle fullscreen mode
|
|
3405
|
+
* @memberof Draw */
|
|
3406
|
+
function toggleFullscreen()
|
|
3407
|
+
{
|
|
3408
|
+
const rootElement = mainCanvas.parentElement;
|
|
3409
|
+
if (isFullscreen())
|
|
3410
|
+
{
|
|
3411
|
+
if (document.exitFullscreen)
|
|
3412
|
+
document.exitFullscreen();
|
|
3413
|
+
}
|
|
3414
|
+
else if (rootElement.requestFullscreen)
|
|
3415
|
+
rootElement.requestFullscreen();
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
/** Set the cursor style
|
|
3419
|
+
* @param {string} [cursorStyle] - CSS cursor style (auto, none, crosshair, etc)
|
|
3420
|
+
* @memberof Draw */
|
|
3421
|
+
function setCursor(cursorStyle = 'auto')
|
|
3422
|
+
{
|
|
3423
|
+
const rootElement = mainCanvas.parentElement;
|
|
3424
|
+
rootElement.style.cursor = cursorStyle;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3189
3427
|
///////////////////////////////////////////////////////////////////////////////
|
|
3190
3428
|
|
|
3191
3429
|
let engineFontImage;
|
|
3192
3430
|
|
|
3193
|
-
/**
|
|
3431
|
+
/**
|
|
3194
3432
|
* Font Image Object - Draw text on a 2D canvas by using characters in an image
|
|
3195
3433
|
* - 96 characters (from space to tilde) are stored in an image
|
|
3196
3434
|
* - Uses a default 8x8 font if none is supplied
|
|
@@ -3198,7 +3436,7 @@ let engineFontImage;
|
|
|
3198
3436
|
* @example
|
|
3199
3437
|
* // use built in font
|
|
3200
3438
|
* const font = new FontImage;
|
|
3201
|
-
*
|
|
3439
|
+
*
|
|
3202
3440
|
* // draw text
|
|
3203
3441
|
* font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
|
|
3204
3442
|
*/
|
|
@@ -3208,7 +3446,6 @@ class FontImage
|
|
|
3208
3446
|
* @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
|
|
3209
3447
|
* @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
|
|
3210
3448
|
* @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
|
|
3211
|
-
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
|
|
3212
3449
|
*/
|
|
3213
3450
|
constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
|
|
3214
3451
|
{
|
|
@@ -3222,7 +3459,6 @@ class FontImage
|
|
|
3222
3459
|
this.image = image || engineFontImage;
|
|
3223
3460
|
this.tileSize = tileSize;
|
|
3224
3461
|
this.paddingSize = paddingSize;
|
|
3225
|
-
this.context = context;
|
|
3226
3462
|
}
|
|
3227
3463
|
|
|
3228
3464
|
/** Draw text in world space using the image font
|
|
@@ -3230,30 +3466,39 @@ class FontImage
|
|
|
3230
3466
|
* @param {Vector2} pos
|
|
3231
3467
|
* @param {number} [scale=.25]
|
|
3232
3468
|
* @param {boolean} [center]
|
|
3469
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}[context=drawContext]
|
|
3233
3470
|
*/
|
|
3234
|
-
drawText(text, pos, scale=1, center)
|
|
3471
|
+
drawText(text, pos, scale=1, center, context=drawContext)
|
|
3235
3472
|
{
|
|
3236
|
-
this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
|
|
3473
|
+
this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
|
|
3237
3474
|
}
|
|
3238
3475
|
|
|
3239
|
-
/** Draw text in
|
|
3476
|
+
/** Draw text on overlay canvas in world space using the image font
|
|
3240
3477
|
* @param {string} text
|
|
3241
3478
|
* @param {Vector2} pos
|
|
3242
3479
|
* @param {number} [scale]
|
|
3243
3480
|
* @param {boolean} [center]
|
|
3244
3481
|
*/
|
|
3245
|
-
|
|
3482
|
+
drawTextOverlay(text, pos, scale=4, center)
|
|
3483
|
+
{ this.drawText(text, pos, scale, center, overlayContext); }
|
|
3484
|
+
|
|
3485
|
+
/** Draw text on overlay canvas in screen space using the image font
|
|
3486
|
+
* @param {string} text
|
|
3487
|
+
* @param {Vector2} pos
|
|
3488
|
+
* @param {number} [scale]
|
|
3489
|
+
* @param {boolean} [center]
|
|
3490
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
3491
|
+
*/
|
|
3492
|
+
drawTextScreen(text, pos, scale=4, center, context=overlayContext)
|
|
3246
3493
|
{
|
|
3247
|
-
const context = this.context;
|
|
3248
3494
|
context.save();
|
|
3249
|
-
|
|
3250
3495
|
const size = this.tileSize;
|
|
3251
3496
|
const drawSize = size.add(this.paddingSize).scale(scale);
|
|
3252
3497
|
const cols = this.image.width / this.tileSize.x |0;
|
|
3253
3498
|
(text+'').split('\n').forEach((line, i)=>
|
|
3254
3499
|
{
|
|
3255
3500
|
const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
|
|
3256
|
-
for(let j=line.length; j--;)
|
|
3501
|
+
for (let j=line.length; j--;)
|
|
3257
3502
|
{
|
|
3258
3503
|
// draw each character
|
|
3259
3504
|
let charCode = line[j].charCodeAt(0);
|
|
@@ -3265,46 +3510,14 @@ class FontImage
|
|
|
3265
3510
|
const x = tile % cols;
|
|
3266
3511
|
const y = tile / cols |0;
|
|
3267
3512
|
const drawPos = pos.add(vec2(j,i).multiply(drawSize));
|
|
3268
|
-
context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
|
|
3513
|
+
context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
|
|
3269
3514
|
drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
|
|
3270
3515
|
}
|
|
3271
3516
|
});
|
|
3272
|
-
|
|
3273
3517
|
context.restore();
|
|
3274
3518
|
}
|
|
3275
|
-
}
|
|
3276
|
-
|
|
3277
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
3278
|
-
// Display functions
|
|
3279
|
-
|
|
3280
|
-
/** Returns true if fullscreen mode is active
|
|
3281
|
-
* @return {boolean}
|
|
3282
|
-
* @memberof Draw */
|
|
3283
|
-
function isFullscreen() { return !!document.fullscreenElement; }
|
|
3284
|
-
|
|
3285
|
-
/** Toggle fullscreen mode
|
|
3286
|
-
* @memberof Draw */
|
|
3287
|
-
function toggleFullscreen()
|
|
3288
|
-
{
|
|
3289
|
-
const rootElement = mainCanvas.parentElement;
|
|
3290
|
-
if (isFullscreen())
|
|
3291
|
-
{
|
|
3292
|
-
if (document.exitFullscreen)
|
|
3293
|
-
document.exitFullscreen();
|
|
3294
|
-
}
|
|
3295
|
-
else if (rootElement.requestFullscreen)
|
|
3296
|
-
rootElement.requestFullscreen();
|
|
3297
|
-
}
|
|
3298
|
-
|
|
3299
|
-
/** Set the cursor style
|
|
3300
|
-
* @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
|
|
3301
|
-
* @memberof Draw */
|
|
3302
|
-
function setCursor(cursorStyle = 'auto')
|
|
3303
|
-
{
|
|
3304
|
-
const rootElement = mainCanvas.parentElement;
|
|
3305
|
-
rootElement.style.cursor = cursorStyle;
|
|
3306
3519
|
}
|
|
3307
|
-
/**
|
|
3520
|
+
/**
|
|
3308
3521
|
* LittleJS Input System
|
|
3309
3522
|
* - Tracks keyboard down, pressed, and released
|
|
3310
3523
|
* - Tracks mouse buttons, position, and wheel
|
|
@@ -3320,10 +3533,10 @@ function setCursor(cursorStyle = 'auto')
|
|
|
3320
3533
|
* @return {boolean}
|
|
3321
3534
|
* @memberof Input */
|
|
3322
3535
|
function keyIsDown(key, device=0)
|
|
3323
|
-
{
|
|
3536
|
+
{
|
|
3324
3537
|
ASSERT(key !== undefined, 'key is undefined');
|
|
3325
|
-
ASSERT(device > 0 || typeof key
|
|
3326
|
-
return inputData[device] && !!(inputData[device][key] & 1);
|
|
3538
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
3539
|
+
return inputData[device] && !!(inputData[device][key] & 1);
|
|
3327
3540
|
}
|
|
3328
3541
|
|
|
3329
3542
|
/** Returns true if device key was pressed this frame
|
|
@@ -3332,10 +3545,10 @@ function keyIsDown(key, device=0)
|
|
|
3332
3545
|
* @return {boolean}
|
|
3333
3546
|
* @memberof Input */
|
|
3334
3547
|
function keyWasPressed(key, device=0)
|
|
3335
|
-
{
|
|
3548
|
+
{
|
|
3336
3549
|
ASSERT(key !== undefined, 'key is undefined');
|
|
3337
|
-
ASSERT(device > 0 || typeof key
|
|
3338
|
-
return inputData[device] && !!(inputData[device][key] & 2);
|
|
3550
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
3551
|
+
return inputData[device] && !!(inputData[device][key] & 2);
|
|
3339
3552
|
}
|
|
3340
3553
|
|
|
3341
3554
|
/** Returns true if device key was released this frame
|
|
@@ -3344,9 +3557,9 @@ function keyWasPressed(key, device=0)
|
|
|
3344
3557
|
* @return {boolean}
|
|
3345
3558
|
* @memberof Input */
|
|
3346
3559
|
function keyWasReleased(key, device=0)
|
|
3347
|
-
{
|
|
3560
|
+
{
|
|
3348
3561
|
ASSERT(key !== undefined, 'key is undefined');
|
|
3349
|
-
ASSERT(device > 0 || typeof key
|
|
3562
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
3350
3563
|
return inputData[device] && !!(inputData[device][key] & 4);
|
|
3351
3564
|
}
|
|
3352
3565
|
|
|
@@ -3468,7 +3681,7 @@ function gamepadWasReleased(button, gamepad=0)
|
|
|
3468
3681
|
* @param {number} [gamepad]
|
|
3469
3682
|
* @return {Vector2}
|
|
3470
3683
|
* @memberof Input */
|
|
3471
|
-
function gamepadStick(stick,
|
|
3684
|
+
function gamepadStick(stick, gamepad=0)
|
|
3472
3685
|
{ return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
|
|
3473
3686
|
|
|
3474
3687
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -3545,10 +3758,10 @@ function inputInit()
|
|
|
3545
3758
|
{
|
|
3546
3759
|
// handle remapping wasd keys to directions
|
|
3547
3760
|
return inputWASDEmulateDirection ?
|
|
3548
|
-
c
|
|
3549
|
-
c
|
|
3550
|
-
c
|
|
3551
|
-
c
|
|
3761
|
+
c === 'KeyW' ? 'ArrowUp' :
|
|
3762
|
+
c === 'KeyS' ? 'ArrowDown' :
|
|
3763
|
+
c === 'KeyA' ? 'ArrowLeft' :
|
|
3764
|
+
c === 'KeyD' ? 'ArrowRight' : c : c;
|
|
3552
3765
|
}
|
|
3553
3766
|
function onMouseDown(e)
|
|
3554
3767
|
{
|
|
@@ -3556,9 +3769,9 @@ function inputInit()
|
|
|
3556
3769
|
return;
|
|
3557
3770
|
|
|
3558
3771
|
// fix stalled audio requiring user interaction
|
|
3559
|
-
if (soundEnable && !headlessMode && audioContext &&
|
|
3772
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
3560
3773
|
audioContext.resume();
|
|
3561
|
-
|
|
3774
|
+
|
|
3562
3775
|
isUsingGamepad = false;
|
|
3563
3776
|
inputData[0][e.button] = 3;
|
|
3564
3777
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
@@ -3601,8 +3814,8 @@ function gamepadsUpdate()
|
|
|
3601
3814
|
const applyDeadZones = (v)=>
|
|
3602
3815
|
{
|
|
3603
3816
|
const min=.3, max=.8;
|
|
3604
|
-
const deadZone = (v)=>
|
|
3605
|
-
v >
|
|
3817
|
+
const deadZone = (v)=>
|
|
3818
|
+
v > min ? percent(v, min, max) :
|
|
3606
3819
|
v < -min ? -percent(-v, min, max) : 0;
|
|
3607
3820
|
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
3608
3821
|
}
|
|
@@ -3610,30 +3823,29 @@ function gamepadsUpdate()
|
|
|
3610
3823
|
// update touch gamepad if enabled
|
|
3611
3824
|
if (touchGamepadEnable && isTouchDevice)
|
|
3612
3825
|
{
|
|
3613
|
-
|
|
3614
|
-
|
|
3826
|
+
if (!touchGamepadTimer.isSet())
|
|
3827
|
+
return;
|
|
3828
|
+
|
|
3829
|
+
// read virtual analog stick
|
|
3830
|
+
const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
|
|
3831
|
+
sticks[0] = vec2();
|
|
3832
|
+
if (touchGamepadAnalog)
|
|
3833
|
+
sticks[0] = applyDeadZones(touchGamepadStick);
|
|
3834
|
+
else if (touchGamepadStick.lengthSquared() > .3)
|
|
3615
3835
|
{
|
|
3616
|
-
//
|
|
3617
|
-
|
|
3618
|
-
sticks[0] =
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
else if (touchGamepadStick.lengthSquared() > .3)
|
|
3622
|
-
{
|
|
3623
|
-
// convert to 8 way dpad
|
|
3624
|
-
sticks[0].x = Math.round(touchGamepadStick.x);
|
|
3625
|
-
sticks[0].y = -Math.round(touchGamepadStick.y);
|
|
3626
|
-
sticks[0] = sticks[0].clampLength();
|
|
3627
|
-
}
|
|
3836
|
+
// convert to 8 way dpad
|
|
3837
|
+
sticks[0].x = Math.round(touchGamepadStick.x);
|
|
3838
|
+
sticks[0].y = -Math.round(touchGamepadStick.y);
|
|
3839
|
+
sticks[0] = sticks[0].clampLength();
|
|
3840
|
+
}
|
|
3628
3841
|
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
}
|
|
3842
|
+
// read virtual gamepad buttons
|
|
3843
|
+
const data = inputData[1] || (inputData[1] = []);
|
|
3844
|
+
for (let i=10; i--;)
|
|
3845
|
+
{
|
|
3846
|
+
const j = i === 3 ? 2 : i === 2 ? 3 : i; // fix button locations
|
|
3847
|
+
const wasDown = gamepadIsDown(j,0);
|
|
3848
|
+
data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
3637
3849
|
}
|
|
3638
3850
|
}
|
|
3639
3851
|
|
|
@@ -3659,7 +3871,7 @@ function gamepadsUpdate()
|
|
|
3659
3871
|
// read analog sticks
|
|
3660
3872
|
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
3661
3873
|
sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
|
|
3662
|
-
|
|
3874
|
+
|
|
3663
3875
|
// read buttons
|
|
3664
3876
|
for (let j = gamepad.buttons.length; j--;)
|
|
3665
3877
|
{
|
|
@@ -3675,14 +3887,14 @@ function gamepadsUpdate()
|
|
|
3675
3887
|
{
|
|
3676
3888
|
// copy dpad to left analog stick when pressed
|
|
3677
3889
|
const dpad = vec2(
|
|
3678
|
-
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
3890
|
+
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
3679
3891
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
3680
3892
|
if (dpad.lengthSquared())
|
|
3681
3893
|
sticks[0] = dpad.clampLength();
|
|
3682
3894
|
}
|
|
3683
3895
|
|
|
3684
3896
|
// disable touch gamepad if using real gamepad
|
|
3685
|
-
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
3897
|
+
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
3686
3898
|
}
|
|
3687
3899
|
}
|
|
3688
3900
|
}
|
|
@@ -3707,20 +3919,13 @@ function vibrateStop() { vibrate(0); }
|
|
|
3707
3919
|
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
3708
3920
|
|
|
3709
3921
|
// touch gamepad internal variables
|
|
3710
|
-
let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
|
|
3922
|
+
let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
|
|
3711
3923
|
|
|
3712
3924
|
// enable touch input mouse passthrough
|
|
3713
3925
|
function touchInputInit()
|
|
3714
3926
|
{
|
|
3715
3927
|
// add non passive touch event listeners
|
|
3716
3928
|
let handleTouch = handleTouchDefault;
|
|
3717
|
-
if (touchGamepadEnable)
|
|
3718
|
-
{
|
|
3719
|
-
// touch input internal variables
|
|
3720
|
-
handleTouch = handleTouchGamepad;
|
|
3721
|
-
touchGamepadButtons = [];
|
|
3722
|
-
touchGamepadStick = vec2();
|
|
3723
|
-
}
|
|
3724
3929
|
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
3725
3930
|
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
3726
3931
|
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
@@ -3729,8 +3934,15 @@ function touchInputInit()
|
|
|
3729
3934
|
let wasTouching;
|
|
3730
3935
|
function handleTouchDefault(e)
|
|
3731
3936
|
{
|
|
3937
|
+
if (!touchInputEnable)
|
|
3938
|
+
return;
|
|
3939
|
+
|
|
3940
|
+
// route touch to gamepad
|
|
3941
|
+
if (touchGamepadEnable)
|
|
3942
|
+
handleTouchGamepad(e);
|
|
3943
|
+
|
|
3732
3944
|
// fix stalled audio requiring user interaction
|
|
3733
|
-
if (soundEnable && !headlessMode && audioContext &&
|
|
3945
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
3734
3946
|
audioContext.resume();
|
|
3735
3947
|
|
|
3736
3948
|
// check if touching and pass to mouse events
|
|
@@ -3759,7 +3971,7 @@ function touchInputInit()
|
|
|
3759
3971
|
// prevent default handling like copy and magnifier lens
|
|
3760
3972
|
if (inputPreventDefault && document.hasFocus()) // allow document to get focus
|
|
3761
3973
|
e.preventDefault();
|
|
3762
|
-
|
|
3974
|
+
|
|
3763
3975
|
// must return true so the document will get focus
|
|
3764
3976
|
return true;
|
|
3765
3977
|
}
|
|
@@ -3771,7 +3983,7 @@ function touchInputInit()
|
|
|
3771
3983
|
touchGamepadStick = vec2();
|
|
3772
3984
|
touchGamepadButtons = [];
|
|
3773
3985
|
isUsingGamepad = true;
|
|
3774
|
-
|
|
3986
|
+
|
|
3775
3987
|
const touching = e.touches.length;
|
|
3776
3988
|
if (touching)
|
|
3777
3989
|
{
|
|
@@ -3780,9 +3992,6 @@ function touchInputInit()
|
|
|
3780
3992
|
{
|
|
3781
3993
|
// touch anywhere to press start when paused
|
|
3782
3994
|
touchGamepadButtons[9] = 1;
|
|
3783
|
-
|
|
3784
|
-
// call default touch handler so normal touch events still work
|
|
3785
|
-
handleTouchDefault(e);
|
|
3786
3995
|
return;
|
|
3787
3996
|
}
|
|
3788
3997
|
}
|
|
@@ -3813,12 +4022,6 @@ function touchInputInit()
|
|
|
3813
4022
|
touchGamepadButtons[9] = 1;
|
|
3814
4023
|
}
|
|
3815
4024
|
}
|
|
3816
|
-
|
|
3817
|
-
// call default touch handler so normal touch events still work
|
|
3818
|
-
handleTouchDefault(e);
|
|
3819
|
-
|
|
3820
|
-
// must return true so the document will get focus
|
|
3821
|
-
return true;
|
|
3822
4025
|
}
|
|
3823
4026
|
}
|
|
3824
4027
|
|
|
@@ -3828,7 +4031,7 @@ function touchGamepadRender()
|
|
|
3828
4031
|
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
3829
4032
|
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
3830
4033
|
return;
|
|
3831
|
-
|
|
4034
|
+
|
|
3832
4035
|
// fade off when not touching or paused
|
|
3833
4036
|
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
3834
4037
|
if (!alpha || paused)
|
|
@@ -3854,16 +4057,16 @@ function touchGamepadRender()
|
|
|
3854
4057
|
}
|
|
3855
4058
|
else // draw cross shaped gamepad
|
|
3856
4059
|
{
|
|
3857
|
-
for(let i=10; i--;)
|
|
4060
|
+
for (let i=10; i--;)
|
|
3858
4061
|
{
|
|
3859
4062
|
const angle = i*PI/4;
|
|
3860
4063
|
context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
|
|
3861
4064
|
i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
|
|
3862
|
-
i
|
|
4065
|
+
i===1 && context.fill();
|
|
3863
4066
|
}
|
|
3864
4067
|
context.stroke();
|
|
3865
4068
|
}
|
|
3866
|
-
|
|
4069
|
+
|
|
3867
4070
|
// draw right face buttons
|
|
3868
4071
|
const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
3869
4072
|
for (let i=4; i--;)
|
|
@@ -3898,8 +4101,8 @@ function pointerLockExit() { document.exitPointerLock && document.exitPointerLoc
|
|
|
3898
4101
|
/** Check if pointer is locked (true if locked)
|
|
3899
4102
|
* @return {boolean}
|
|
3900
4103
|
* @memberof Input */
|
|
3901
|
-
function pointerLockIsActive() { return document.pointerLockElement
|
|
3902
|
-
/**
|
|
4104
|
+
function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
|
|
4105
|
+
/**
|
|
3903
4106
|
* LittleJS Audio System
|
|
3904
4107
|
* - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
|
|
3905
4108
|
* - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
|
|
@@ -3920,10 +4123,21 @@ let audioContext = new AudioContext;
|
|
|
3920
4123
|
* @memberof Audio */
|
|
3921
4124
|
let audioMasterGain;
|
|
3922
4125
|
|
|
4126
|
+
/** Default sample rate used for sounds
|
|
4127
|
+
* @default 44100
|
|
4128
|
+
* @memberof Audio */
|
|
4129
|
+
const audioDefaultSampleRate = 44100;
|
|
4130
|
+
|
|
4131
|
+
/** Check if the audio context is running and available for playback
|
|
4132
|
+
* @return {boolean} - True if the audio context is running
|
|
4133
|
+
* @memberof Audio */
|
|
4134
|
+
function audioIsRunning()
|
|
4135
|
+
{ return audioContext.state === 'running'; }
|
|
4136
|
+
|
|
3923
4137
|
function audioInit()
|
|
3924
4138
|
{
|
|
3925
4139
|
if (!soundEnable || headlessMode) return;
|
|
3926
|
-
|
|
4140
|
+
|
|
3927
4141
|
audioMasterGain = audioContext.createGain();
|
|
3928
4142
|
audioMasterGain.connect(audioContext.destination);
|
|
3929
4143
|
audioMasterGain.gain.value = soundVolume; // set starting value
|
|
@@ -3931,14 +4145,14 @@ function audioInit()
|
|
|
3931
4145
|
|
|
3932
4146
|
///////////////////////////////////////////////////////////////////////////////
|
|
3933
4147
|
|
|
3934
|
-
/**
|
|
4148
|
+
/**
|
|
3935
4149
|
* Sound Object - Stores a sound for later use and can be played positionally
|
|
3936
|
-
*
|
|
4150
|
+
*
|
|
3937
4151
|
* <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
|
|
3938
4152
|
* @example
|
|
3939
4153
|
* // create a sound
|
|
3940
4154
|
* const sound_example = new Sound([.5,.5]);
|
|
3941
|
-
*
|
|
4155
|
+
*
|
|
3942
4156
|
* // play the sound
|
|
3943
4157
|
* sound_example.play();
|
|
3944
4158
|
*/
|
|
@@ -3955,33 +4169,41 @@ class Sound
|
|
|
3955
4169
|
|
|
3956
4170
|
/** @property {number} - World space max range of sound */
|
|
3957
4171
|
this.range = range;
|
|
3958
|
-
|
|
3959
4172
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
3960
4173
|
this.taper = taper;
|
|
3961
|
-
|
|
3962
4174
|
/** @property {number} - How much to randomize frequency each time sound plays */
|
|
3963
4175
|
this.randomness = 0;
|
|
4176
|
+
/** @property {number} - Sample rate for this sound */
|
|
4177
|
+
this.sampleRate = audioDefaultSampleRate;
|
|
4178
|
+
/** @property {number} - Percentage of this sound currently loaded */
|
|
4179
|
+
this.loadedPercent = 0;
|
|
3964
4180
|
|
|
4181
|
+
// generate zzfx sound now for fast playback
|
|
3965
4182
|
if (zzfxSound)
|
|
3966
4183
|
{
|
|
3967
|
-
//
|
|
3968
|
-
const defaultRandomness = .05;
|
|
3969
|
-
this.randomness = zzfxSound[
|
|
3970
|
-
|
|
4184
|
+
// remove randomness so it can be applied on playback
|
|
4185
|
+
const randomnessIndex = 1, defaultRandomness = .05;
|
|
4186
|
+
this.randomness = zzfxSound[randomnessIndex] !== undefined ?
|
|
4187
|
+
zzfxSound[randomnessIndex] : defaultRandomness;
|
|
4188
|
+
zzfxSound[randomnessIndex] = 0;
|
|
4189
|
+
|
|
4190
|
+
// generate the zzfx samples
|
|
3971
4191
|
this.sampleChannels = [zzfxG(...zzfxSound)];
|
|
3972
|
-
this.
|
|
4192
|
+
this.loadedPercent = 1;
|
|
3973
4193
|
}
|
|
3974
4194
|
}
|
|
3975
4195
|
|
|
3976
4196
|
/** Play the sound
|
|
3977
|
-
*
|
|
3978
|
-
* @param {
|
|
3979
|
-
* @param {number} [
|
|
3980
|
-
* @param {number} [
|
|
3981
|
-
* @param {
|
|
3982
|
-
* @
|
|
4197
|
+
* Sounds may not play until a user interaction occurs
|
|
4198
|
+
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
4199
|
+
* @param {number} [volume] - How much to scale volume by
|
|
4200
|
+
* @param {number} [pitch] - How much to scale pitch by
|
|
4201
|
+
* @param {number} [randomnessScale] - How much to scale pitch randomness
|
|
4202
|
+
* @param {boolean} [loop] - Should the sound loop?
|
|
4203
|
+
* @param {boolean} [paused] - Should the sound start paused
|
|
4204
|
+
* @return {SoundInstance} - The audio source node
|
|
3983
4205
|
*/
|
|
3984
|
-
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
|
|
4206
|
+
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
|
|
3985
4207
|
{
|
|
3986
4208
|
if (!soundEnable || headlessMode) return;
|
|
3987
4209
|
if (!this.sampleChannels) return;
|
|
@@ -4004,75 +4226,55 @@ class Sound
|
|
|
4004
4226
|
// get pan from screen space coords
|
|
4005
4227
|
pan = worldToScreen(pos).x * 2/mainCanvas.width - 1;
|
|
4006
4228
|
}
|
|
4007
|
-
|
|
4008
|
-
// play the sound
|
|
4009
|
-
const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
|
|
4010
|
-
this.gainNode = audioContext.createGain();
|
|
4011
|
-
this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
|
|
4012
|
-
return this.source;
|
|
4013
|
-
}
|
|
4014
|
-
|
|
4015
|
-
/** Set the sound volume of the most recently played instance of this sound
|
|
4016
|
-
* @param {number} [volume] - How much to scale volume by
|
|
4017
|
-
*/
|
|
4018
|
-
setVolume(volume=1)
|
|
4019
|
-
{
|
|
4020
|
-
if (this.gainNode)
|
|
4021
|
-
this.gainNode.gain.value = volume;
|
|
4022
|
-
}
|
|
4023
|
-
|
|
4024
|
-
/** Stop the last instance of this sound that was played
|
|
4025
|
-
* @param {number} [fadeTime] - How long to fade out (seconds)
|
|
4026
|
-
*/
|
|
4027
|
-
stop(fadeTime=0)
|
|
4028
|
-
{
|
|
4029
|
-
if (!this.source)
|
|
4030
|
-
return;
|
|
4031
4229
|
|
|
4032
|
-
//
|
|
4033
|
-
const
|
|
4034
|
-
|
|
4035
|
-
this.gainNode.gain.linearRampToValueAtTime(1, startFade);
|
|
4036
|
-
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
4037
|
-
this.source.stop(endFade);
|
|
4038
|
-
this.source = undefined;
|
|
4230
|
+
// Create and return sound instance
|
|
4231
|
+
const rate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
|
|
4232
|
+
return new SoundInstance(this, volume, rate, pan, loop, paused);
|
|
4039
4233
|
}
|
|
4040
4234
|
|
|
4041
|
-
/**
|
|
4042
|
-
* @
|
|
4235
|
+
/** Play a music track that loops by default
|
|
4236
|
+
* @param {number} [volume] - Volume to play the music at
|
|
4237
|
+
* @param {boolean} [loop] - Should the music loop?
|
|
4238
|
+
* @param {boolean} [paused] - Should the music start paused
|
|
4239
|
+
* @return {SoundInstance} - The audio source node
|
|
4043
4240
|
*/
|
|
4044
|
-
|
|
4241
|
+
playMusic(volume=1, loop=true, paused=false)
|
|
4242
|
+
{ return this.play(undefined, volume, 1, 0, loop, paused); }
|
|
4045
4243
|
|
|
4046
|
-
/** Play the sound as a note with a semitone offset
|
|
4244
|
+
/** Play the sound as a musical note with a semitone offset
|
|
4245
|
+
* This can be used to play music with chromatic scales
|
|
4047
4246
|
* @param {number} semitoneOffset - How many semitones to offset pitch
|
|
4048
|
-
* @param {Vector2} [pos] - World space position to play the sound
|
|
4049
|
-
* @param {number} [volume=1] - How much to scale volume by
|
|
4050
|
-
* @return {
|
|
4247
|
+
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
4248
|
+
* @param {number} [volume=1] - How much to scale volume by
|
|
4249
|
+
* @return {SoundInstance} - The audio source node
|
|
4051
4250
|
*/
|
|
4052
4251
|
playNote(semitoneOffset, pos, volume)
|
|
4053
|
-
{
|
|
4252
|
+
{
|
|
4253
|
+
const pitch = getNoteFrequency(semitoneOffset, 1);
|
|
4254
|
+
return this.play(pos, volume, pitch, 0);
|
|
4255
|
+
}
|
|
4054
4256
|
|
|
4055
4257
|
/** Get how long this sound is in seconds
|
|
4056
4258
|
* @return {number} - How long the sound is in seconds (undefined if loading)
|
|
4057
4259
|
*/
|
|
4058
|
-
getDuration()
|
|
4260
|
+
getDuration()
|
|
4059
4261
|
{ return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
|
|
4060
|
-
|
|
4061
|
-
/** Check if sound is
|
|
4062
|
-
* @return {boolean} - True if sound is
|
|
4262
|
+
|
|
4263
|
+
/** Check if sound is loaded, for sounds fetched from a url
|
|
4264
|
+
* @return {boolean} - True if sound is loaded and ready to play
|
|
4063
4265
|
*/
|
|
4064
|
-
|
|
4266
|
+
isLoaded() { return this.loadedPercent === 1; }
|
|
4065
4267
|
}
|
|
4066
4268
|
|
|
4067
4269
|
///////////////////////////////////////////////////////////////////////////////
|
|
4068
4270
|
|
|
4069
|
-
/**
|
|
4271
|
+
/**
|
|
4070
4272
|
* Sound Wave Object - Stores a wave sound for later use and can be played positionally
|
|
4071
4273
|
* - this can be used to play wave, mp3, and ogg files
|
|
4072
4274
|
* @example
|
|
4073
4275
|
* // create a sound
|
|
4074
4276
|
* const sound_example = new SoundWave('sound.mp3');
|
|
4075
|
-
*
|
|
4277
|
+
*
|
|
4076
4278
|
* // play the sound
|
|
4077
4279
|
* sound_example.play();
|
|
4078
4280
|
*/
|
|
@@ -4104,26 +4306,202 @@ class SoundWave extends Sound
|
|
|
4104
4306
|
const response = await fetch(filename);
|
|
4105
4307
|
const arrayBuffer = await response.arrayBuffer();
|
|
4106
4308
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4309
|
+
|
|
4310
|
+
// convert audio buffer to sample channels across multiple frames
|
|
4311
|
+
const channelCount = audioBuffer.numberOfChannels;
|
|
4312
|
+
const samplesPerFrame = 1e5;
|
|
4313
|
+
const sampleChannels = [];
|
|
4314
|
+
for (let channel = 0; channel < channelCount; channel++)
|
|
4315
|
+
{
|
|
4316
|
+
const channelData = audioBuffer.getChannelData(channel);
|
|
4317
|
+
const channelLength = channelData.length;
|
|
4318
|
+
sampleChannels[channel] = new Array(channelLength);
|
|
4319
|
+
let sampleIndex = 0;
|
|
4320
|
+
while (sampleIndex < channelLength)
|
|
4321
|
+
{
|
|
4322
|
+
// yield to next frame
|
|
4323
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
4324
|
+
|
|
4325
|
+
// copy chunk of samples
|
|
4326
|
+
const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
|
|
4327
|
+
for (; sampleIndex < endIndex; sampleIndex++)
|
|
4328
|
+
sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
|
|
4329
|
+
|
|
4330
|
+
// update loaded percent
|
|
4331
|
+
const samplesTotal = channelCount * channelLength;
|
|
4332
|
+
const samplesProcessed = channel * channelLength + sampleIndex;
|
|
4333
|
+
this.loadedPercent = samplesProcessed / samplesTotal;
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
// setup the sound to be played
|
|
4110
4338
|
this.sampleRate = audioBuffer.sampleRate;
|
|
4339
|
+
this.sampleChannels = sampleChannels;
|
|
4340
|
+
this.loadedPercent = 1;
|
|
4111
4341
|
if (this.onloadCallback)
|
|
4112
4342
|
this.onloadCallback();
|
|
4113
4343
|
}
|
|
4114
4344
|
}
|
|
4115
4345
|
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
*
|
|
4120
|
-
*
|
|
4121
|
-
*
|
|
4122
|
-
|
|
4346
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4347
|
+
|
|
4348
|
+
/**
|
|
4349
|
+
* Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
|
|
4350
|
+
* Represents a single playing instance of a sound with pause/resume capabilities
|
|
4351
|
+
* @example
|
|
4352
|
+
* // Play a sound and get an instance for control
|
|
4353
|
+
* const jumpSound = new Sound([.5,.5,220]);
|
|
4354
|
+
* const instance = jumpSound.play();
|
|
4355
|
+
*
|
|
4356
|
+
* // Control the individual instance
|
|
4357
|
+
* instance.setVolume(.5);
|
|
4358
|
+
* instance.pause();
|
|
4359
|
+
* instance.unpause();
|
|
4360
|
+
* instance.stop();
|
|
4361
|
+
*/
|
|
4362
|
+
class SoundInstance
|
|
4123
4363
|
{
|
|
4124
|
-
|
|
4364
|
+
/** Create a sound instance
|
|
4365
|
+
* @param {Sound} sound - The sound object
|
|
4366
|
+
* @param {number} [volume] - How much to scale volume by
|
|
4367
|
+
* @param {number} [rate] - The playback rate to use
|
|
4368
|
+
* @param {number} [pan] - How much to apply stereo panning
|
|
4369
|
+
* @param {boolean} [loop] - Should the sound loop?
|
|
4370
|
+
* @param {boolean} [paused] - Should the sound start paused? */
|
|
4371
|
+
constructor(sound, volume=1, rate=1, pan=0, loop=false, paused=false)
|
|
4372
|
+
{
|
|
4373
|
+
ASSERT(sound instanceof Sound, 'SoundInstance requires a valid Sound object');
|
|
4374
|
+
ASSERT(volume >= 0, 'Sound volume must be positive or zero');
|
|
4375
|
+
ASSERT(rate >= 0, 'Sound rate must be positive or zero');
|
|
4376
|
+
ASSERT(isNumber(pan), 'Sound pan must be a number');
|
|
4377
|
+
|
|
4378
|
+
/** @property {Sound} - The sound object */
|
|
4379
|
+
this.sound = sound;
|
|
4380
|
+
/** @property {number} - How much to scale volume by */
|
|
4381
|
+
this.volume = volume;
|
|
4382
|
+
/** @property {number} - The playback rate to use */
|
|
4383
|
+
this.rate = rate;
|
|
4384
|
+
/** @property {number} - How much to apply stereo panning */
|
|
4385
|
+
this.pan = pan;
|
|
4386
|
+
/** @property {boolean} - Should the sound loop */
|
|
4387
|
+
this.loop = loop;
|
|
4388
|
+
/** @property {number} - Timestamp for audio context when paused */
|
|
4389
|
+
this.pausedTime = 0;
|
|
4390
|
+
/** @property {number} - Timestamp for audio context when started */
|
|
4391
|
+
this.startTime = undefined;
|
|
4392
|
+
/** @property {GainNode} - Gain node for the sound */
|
|
4393
|
+
this.gainNode = undefined;
|
|
4394
|
+
/** @property {AudioBufferSourceNode} - Source node of the audio */
|
|
4395
|
+
this.source = undefined;
|
|
4396
|
+
// setup end callback and start sound
|
|
4397
|
+
this.onendedCallback = (source)=>
|
|
4398
|
+
{
|
|
4399
|
+
if (source === this.source)
|
|
4400
|
+
this.source = undefined;
|
|
4401
|
+
};
|
|
4402
|
+
if (!paused)
|
|
4403
|
+
this.start();
|
|
4404
|
+
}
|
|
4125
4405
|
|
|
4126
|
-
|
|
4406
|
+
/** Start playing the sound instance from the offset time
|
|
4407
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
4408
|
+
*/
|
|
4409
|
+
start(offset=0)
|
|
4410
|
+
{
|
|
4411
|
+
ASSERT(offset >= 0, 'Sound start offset must be positive or zero');
|
|
4412
|
+
if (this.isPlaying())
|
|
4413
|
+
this.stop();
|
|
4414
|
+
this.gainNode = audioContext.createGain();
|
|
4415
|
+
this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
|
|
4416
|
+
this.startTime = audioContext.currentTime - offset;
|
|
4417
|
+
this.pausedTime = undefined;
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
/** Set the volume of this sound instance
|
|
4421
|
+
* @param {number} volume */
|
|
4422
|
+
setVolume(volume)
|
|
4423
|
+
{
|
|
4424
|
+
ASSERT(volume >= 0, 'Sound volume must be positive or zero');
|
|
4425
|
+
this.volume = volume;
|
|
4426
|
+
if (this.gainNode)
|
|
4427
|
+
this.gainNode.gain.value = volume;
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
/** Stop this sound instance and reset position to the start */
|
|
4431
|
+
stop(fadeTime=0)
|
|
4432
|
+
{
|
|
4433
|
+
ASSERT(fadeTime >= 0, 'Sound fade time must be positive or zero');
|
|
4434
|
+
if (this.isPlaying())
|
|
4435
|
+
{
|
|
4436
|
+
if (fadeTime)
|
|
4437
|
+
{
|
|
4438
|
+
// ramp off gain
|
|
4439
|
+
const startFade = audioContext.currentTime;
|
|
4440
|
+
const endFade = startFade + fadeTime;
|
|
4441
|
+
this.gainNode.gain.linearRampToValueAtTime(1, startFade);
|
|
4442
|
+
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
4443
|
+
this.source.stop(endFade);
|
|
4444
|
+
}
|
|
4445
|
+
else
|
|
4446
|
+
this.source.stop();
|
|
4447
|
+
}
|
|
4448
|
+
this.pausedTime = 0;
|
|
4449
|
+
this.source = undefined;
|
|
4450
|
+
this.startTime = undefined;
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4453
|
+
/** Pause this sound instance */
|
|
4454
|
+
pause()
|
|
4455
|
+
{
|
|
4456
|
+
if (this.isPaused())
|
|
4457
|
+
return;
|
|
4458
|
+
|
|
4459
|
+
// save current time and stop sound
|
|
4460
|
+
this.pausedTime = this.getCurrentTime();
|
|
4461
|
+
this.source.stop();
|
|
4462
|
+
this.source = undefined;
|
|
4463
|
+
this.startTime = undefined;
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
/** Unpauses this sound instance */
|
|
4467
|
+
resume()
|
|
4468
|
+
{
|
|
4469
|
+
if (!this.isPaused())
|
|
4470
|
+
return;
|
|
4471
|
+
|
|
4472
|
+
// restart sound from paused time
|
|
4473
|
+
this.start(this.pausedTime);
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
/** Check if this instance is currently playing
|
|
4477
|
+
* @return {boolean} - True if playing
|
|
4478
|
+
*/
|
|
4479
|
+
isPlaying() { return !!this.source; }
|
|
4480
|
+
|
|
4481
|
+
/** Check if this instance is paused and was not stopped
|
|
4482
|
+
* @return {boolean} - True if paused
|
|
4483
|
+
*/
|
|
4484
|
+
isPaused() { return !this.isPlaying(); }
|
|
4485
|
+
|
|
4486
|
+
/** Get the current playback time in seconds
|
|
4487
|
+
* @return {number} - Current playback time
|
|
4488
|
+
*/
|
|
4489
|
+
getCurrentTime()
|
|
4490
|
+
{
|
|
4491
|
+
const deltaTime = mod(audioContext.currentTime - this.startTime,
|
|
4492
|
+
this.getDuration());
|
|
4493
|
+
return this.isPlaying() ? deltaTime : this.pausedTime;
|
|
4494
|
+
}
|
|
4495
|
+
|
|
4496
|
+
/** Get the total duration of this sound
|
|
4497
|
+
* @return {number} - Total duration in seconds
|
|
4498
|
+
*/
|
|
4499
|
+
getDuration() { return this.sound.getDuration() / this.rate; }
|
|
4500
|
+
|
|
4501
|
+
/** Get source of this sound instance
|
|
4502
|
+
* @return {AudioBufferSourceNode}
|
|
4503
|
+
*/
|
|
4504
|
+
getSource() { return this.source; }
|
|
4127
4505
|
}
|
|
4128
4506
|
|
|
4129
4507
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -4177,9 +4555,11 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
|
|
|
4177
4555
|
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
4178
4556
|
* @param {number} [sampleRate=44100] - Sample rate for the sound
|
|
4179
4557
|
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing
|
|
4558
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
4559
|
+
* @param {Function} [onended] - Callback for when the sound ends
|
|
4180
4560
|
* @return {AudioBufferSourceNode} - The audio node of the sound played
|
|
4181
4561
|
* @memberof Audio */
|
|
4182
|
-
function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=
|
|
4562
|
+
function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
|
|
4183
4563
|
{
|
|
4184
4564
|
if (!soundEnable || headlessMode) return;
|
|
4185
4565
|
|
|
@@ -4204,16 +4584,20 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
4204
4584
|
const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
|
|
4205
4585
|
source.connect(pannerNode).connect(gainNode);
|
|
4206
4586
|
|
|
4207
|
-
//
|
|
4208
|
-
if (
|
|
4587
|
+
// callback when the sound ends
|
|
4588
|
+
if (onended)
|
|
4589
|
+
source.addEventListener('ended', ()=> onended(source));
|
|
4590
|
+
|
|
4591
|
+
if (!audioIsRunning())
|
|
4209
4592
|
{
|
|
4210
|
-
// fix stalled audio
|
|
4211
|
-
audioContext.resume()
|
|
4593
|
+
// fix stalled audio, this sound won't be able to play
|
|
4594
|
+
audioContext.resume();
|
|
4595
|
+
return;
|
|
4212
4596
|
}
|
|
4213
|
-
else
|
|
4214
|
-
source.start();
|
|
4215
4597
|
|
|
4216
|
-
// return sound
|
|
4598
|
+
// play and return sound
|
|
4599
|
+
const startOffset = offset * rate;
|
|
4600
|
+
source.start(0, startOffset);
|
|
4217
4601
|
return source;
|
|
4218
4602
|
}
|
|
4219
4603
|
|
|
@@ -4221,18 +4605,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
4221
4605
|
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
|
|
4222
4606
|
|
|
4223
4607
|
/** Generate and play a ZzFX sound
|
|
4224
|
-
*
|
|
4608
|
+
*
|
|
4225
4609
|
* <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
|
|
4226
4610
|
* @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
|
|
4227
4611
|
* @return {AudioBufferSourceNode} - The audio node of the sound played
|
|
4228
4612
|
* @memberof Audio */
|
|
4229
4613
|
function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
|
|
4230
4614
|
|
|
4231
|
-
/** Sample rate used for all ZzFX sounds
|
|
4232
|
-
* @default 44100
|
|
4233
|
-
* @memberof Audio */
|
|
4234
|
-
const zzfxR = 44100;
|
|
4235
|
-
|
|
4236
4615
|
/** Generate samples for a ZzFX sound
|
|
4237
4616
|
* @param {number} [volume] - Volume scale (percent)
|
|
4238
4617
|
* @param {number} [randomness] - How much to randomize frequency (percent Hz)
|
|
@@ -4256,11 +4635,10 @@ const zzfxR = 44100;
|
|
|
4256
4635
|
* @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
|
|
4257
4636
|
* @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
|
|
4258
4637
|
* @return {Array} - Array of audio samples
|
|
4259
|
-
* @memberof Audio
|
|
4260
|
-
*/
|
|
4638
|
+
* @memberof Audio */
|
|
4261
4639
|
function zzfxG
|
|
4262
4640
|
(
|
|
4263
|
-
volume = 1,
|
|
4641
|
+
volume = 1,
|
|
4264
4642
|
randomness = .05,
|
|
4265
4643
|
frequency = 220,
|
|
4266
4644
|
attack = 0,
|
|
@@ -4268,11 +4646,11 @@ function zzfxG
|
|
|
4268
4646
|
release = .1,
|
|
4269
4647
|
shape = 0,
|
|
4270
4648
|
shapeCurve = 1,
|
|
4271
|
-
slide = 0,
|
|
4272
|
-
deltaSlide = 0,
|
|
4273
|
-
pitchJump = 0,
|
|
4274
|
-
pitchJumpTime = 0,
|
|
4275
|
-
repeatTime = 0,
|
|
4649
|
+
slide = 0,
|
|
4650
|
+
deltaSlide = 0,
|
|
4651
|
+
pitchJump = 0,
|
|
4652
|
+
pitchJumpTime = 0,
|
|
4653
|
+
repeatTime = 0,
|
|
4276
4654
|
noise = 0,
|
|
4277
4655
|
modulation = 0,
|
|
4278
4656
|
bitCrush = 0,
|
|
@@ -4284,19 +4662,19 @@ function zzfxG
|
|
|
4284
4662
|
)
|
|
4285
4663
|
{
|
|
4286
4664
|
// init parameters
|
|
4287
|
-
let sampleRate =
|
|
4288
|
-
PI2 = PI*2,
|
|
4665
|
+
let sampleRate = audioDefaultSampleRate,
|
|
4666
|
+
PI2 = PI*2,
|
|
4289
4667
|
startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
|
|
4290
|
-
startFrequency = frequency *=
|
|
4668
|
+
startFrequency = frequency *=
|
|
4291
4669
|
(1 + rand(randomness,-randomness)) * PI2 / sampleRate,
|
|
4292
|
-
modOffset = 0, // modulation offset
|
|
4670
|
+
modOffset = 0, // modulation offset
|
|
4293
4671
|
repeat = 0, // repeat offset
|
|
4294
4672
|
crush = 0, // bit crush offset
|
|
4295
4673
|
jump = 1, // pitch jump timer
|
|
4296
4674
|
length, // sample length
|
|
4297
4675
|
b = [], // sample buffer
|
|
4298
4676
|
t = 0, // sample time
|
|
4299
|
-
i = 0, // sample index
|
|
4677
|
+
i = 0, // sample index
|
|
4300
4678
|
s = 0, // sample value
|
|
4301
4679
|
f, // wave frequency
|
|
4302
4680
|
|
|
@@ -4304,7 +4682,7 @@ function zzfxG
|
|
|
4304
4682
|
quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
|
|
4305
4683
|
cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
|
|
4306
4684
|
a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
|
|
4307
|
-
b0 = (1 + sign(filter) * cos) / 2 / a0,
|
|
4685
|
+
b0 = (1 + sign(filter) * cos) / 2 / a0,
|
|
4308
4686
|
b1 = -(sign(filter) + cos) / a0, b2 = b0,
|
|
4309
4687
|
x2 = 0, x1 = 0, y2 = 0, y1 = 0;
|
|
4310
4688
|
|
|
@@ -4322,7 +4700,7 @@ function zzfxG
|
|
|
4322
4700
|
repeatTime = repeatTime * sampleRate | 0;
|
|
4323
4701
|
|
|
4324
4702
|
// generate waveform
|
|
4325
|
-
for(length = attack + decay + sustain + release + delay | 0;
|
|
4703
|
+
for (length = attack + decay + sustain + release + delay | 0;
|
|
4326
4704
|
i < length; b[i++] = s * volume) // sample
|
|
4327
4705
|
{
|
|
4328
4706
|
if (!(++crush%(bitCrush*100|0))) // bit crush
|
|
@@ -4350,7 +4728,7 @@ function zzfxG
|
|
|
4350
4728
|
0); // post release
|
|
4351
4729
|
|
|
4352
4730
|
s = delay ? s/2 + (delay > i ? 0 : // delay
|
|
4353
|
-
(i<length-delay? 1 : (length-i)/delay) * // release delay
|
|
4731
|
+
(i<length-delay? 1 : (length-i)/delay) * // release delay
|
|
4354
4732
|
b[i-delay|0]/2/volume) : s; // sample delay
|
|
4355
4733
|
|
|
4356
4734
|
if (filter) // apply filter
|
|
@@ -4362,14 +4740,14 @@ function zzfxG
|
|
|
4362
4740
|
t += f + f*noise*Math.sin(i**5); // noise
|
|
4363
4741
|
|
|
4364
4742
|
if (jump && ++jump > pitchJumpTime) // pitch jump
|
|
4365
|
-
{
|
|
4743
|
+
{
|
|
4366
4744
|
frequency += pitchJump; // apply pitch jump
|
|
4367
4745
|
startFrequency += pitchJump; // also apply to start
|
|
4368
4746
|
jump = 0; // stop pitch jump time
|
|
4369
|
-
}
|
|
4747
|
+
}
|
|
4370
4748
|
|
|
4371
4749
|
if (repeatTime && !(++repeat % repeatTime)) // repeat
|
|
4372
|
-
{
|
|
4750
|
+
{
|
|
4373
4751
|
frequency = startFrequency; // reset frequency
|
|
4374
4752
|
slide = startSlide; // reset slide
|
|
4375
4753
|
jump ||= 1; // reset pitch jump time
|
|
@@ -4378,7 +4756,7 @@ function zzfxG
|
|
|
4378
4756
|
|
|
4379
4757
|
return b; // return sample buffer
|
|
4380
4758
|
}
|
|
4381
|
-
/**
|
|
4759
|
+
/**
|
|
4382
4760
|
* LittleJS Tile Layer System
|
|
4383
4761
|
* - Caches arrays of tiles to off screen canvas for fast rendering
|
|
4384
4762
|
* - Unlimited numbers of layers, allocates canvases as needed
|
|
@@ -4392,9 +4770,9 @@ function zzfxG
|
|
|
4392
4770
|
// Tile Layer System
|
|
4393
4771
|
|
|
4394
4772
|
/** Keep track of all tile layers with collision
|
|
4395
|
-
* @type {Array<TileCollisionLayer>}
|
|
4773
|
+
* @type {Array<TileCollisionLayer>}
|
|
4396
4774
|
* @memberof TileCollision */
|
|
4397
|
-
|
|
4775
|
+
const tileCollisionLayers = [];
|
|
4398
4776
|
|
|
4399
4777
|
/** Get tile collision data for a given cell in the grid
|
|
4400
4778
|
* @param {Vector2} pos
|
|
@@ -4448,7 +4826,7 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
|
|
|
4448
4826
|
}
|
|
4449
4827
|
|
|
4450
4828
|
///////////////////////////////////////////////////////////////////////////////
|
|
4451
|
-
/**
|
|
4829
|
+
/**
|
|
4452
4830
|
* Load tile layers from exported data
|
|
4453
4831
|
* @param {Object} tileMapData - Level data from exported data
|
|
4454
4832
|
* @param {TileInfo} [tileInfo] - Default tile info (used for size and texture)
|
|
@@ -4481,13 +4859,13 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
|
|
|
4481
4859
|
{
|
|
4482
4860
|
const dataLayer = tileMapData.layers[layerIndex];
|
|
4483
4861
|
ASSERT(dataLayer.data && dataLayer.data.length);
|
|
4484
|
-
ASSERT(levelSize.area()
|
|
4862
|
+
ASSERT(levelSize.area() === dataLayer.data.length);
|
|
4485
4863
|
|
|
4486
4864
|
const layerRenderOrder = renderOrder - (layerCount - 1 - layerIndex);
|
|
4487
4865
|
const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
|
|
4488
4866
|
tileLayers[layerIndex] = tileLayer;
|
|
4489
4867
|
|
|
4490
|
-
for (let x=levelSize.x; x--;)
|
|
4868
|
+
for (let x=levelSize.x; x--;)
|
|
4491
4869
|
for (let y=levelSize.y; y--;)
|
|
4492
4870
|
{
|
|
4493
4871
|
const pos = vec2(x, levelSize.y-1-y);
|
|
@@ -4546,7 +4924,7 @@ class TileLayerData
|
|
|
4546
4924
|
/**
|
|
4547
4925
|
* Canvas Layer - cached off screen rendering system
|
|
4548
4926
|
* - Contains an offscreen canvas that can be rendered to
|
|
4549
|
-
* -
|
|
4927
|
+
* - WebGL rendering is optional, call useWebGL to enable
|
|
4550
4928
|
* @extends EngineObject
|
|
4551
4929
|
* @example
|
|
4552
4930
|
* const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
|
|
@@ -4568,18 +4946,18 @@ class CanvasLayer extends EngineObject
|
|
|
4568
4946
|
this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
|
|
4569
4947
|
/** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
|
|
4570
4948
|
this.context = headlessMode ? undefined : this.canvas.getContext('2d');
|
|
4571
|
-
/** @property {WebGLTexture} - Texture if using
|
|
4949
|
+
/** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
|
|
4572
4950
|
this.glTexture = undefined;
|
|
4573
4951
|
this.gravityScale = 0; // disable gravity by default for canvas layers
|
|
4574
4952
|
}
|
|
4575
|
-
|
|
4953
|
+
|
|
4576
4954
|
/** Destroy this canvas layer */
|
|
4577
4955
|
destroy()
|
|
4578
4956
|
{
|
|
4579
4957
|
if (this.destroyed)
|
|
4580
4958
|
return;
|
|
4581
4959
|
|
|
4582
|
-
// free up the
|
|
4960
|
+
// free up the WebGL texture
|
|
4583
4961
|
if (this.glTexture)
|
|
4584
4962
|
glDeleteTexture(this.glTexture);
|
|
4585
4963
|
super.destroy();
|
|
@@ -4604,12 +4982,12 @@ class CanvasLayer extends EngineObject
|
|
|
4604
4982
|
draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
|
|
4605
4983
|
{
|
|
4606
4984
|
// draw the canvas layer as a single tile that uses the whole texture
|
|
4607
|
-
const
|
|
4985
|
+
const useWebGL = glEnable && this.glTexture !== undefined;
|
|
4608
4986
|
const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
|
|
4609
|
-
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor,
|
|
4987
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
|
|
4610
4988
|
}
|
|
4611
4989
|
|
|
4612
|
-
/** Draw onto the layer canvas in world space (bypass
|
|
4990
|
+
/** Draw onto the layer canvas in world space (bypass WebGL)
|
|
4613
4991
|
* @param {Vector2} pos
|
|
4614
4992
|
* @param {Vector2} size
|
|
4615
4993
|
* @param {number} angle
|
|
@@ -4643,8 +5021,8 @@ class CanvasLayer extends EngineObject
|
|
|
4643
5021
|
if (textureInfo)
|
|
4644
5022
|
{
|
|
4645
5023
|
context.globalAlpha = color.a; // only alpha is supported
|
|
4646
|
-
context.drawImage(textureInfo.image,
|
|
4647
|
-
tileInfo.pos.x, tileInfo.pos.y,
|
|
5024
|
+
context.drawImage(textureInfo.image,
|
|
5025
|
+
tileInfo.pos.x, tileInfo.pos.y,
|
|
4648
5026
|
tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
|
|
4649
5027
|
context.globalAlpha = 1;
|
|
4650
5028
|
}
|
|
@@ -4662,11 +5040,11 @@ class CanvasLayer extends EngineObject
|
|
|
4662
5040
|
* @param {Vector2} [size=(1,1)]
|
|
4663
5041
|
* @param {Color} [color=(1,1,1,1)]
|
|
4664
5042
|
* @param {number} [angle=0] */
|
|
4665
|
-
drawRect(pos, size, color, angle)
|
|
5043
|
+
drawRect(pos, size, color, angle)
|
|
4666
5044
|
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
4667
5045
|
|
|
4668
|
-
/** Create or update the
|
|
4669
|
-
* @param {boolean} [enable] - enable
|
|
5046
|
+
/** Create or update the WebGL texture for this layer
|
|
5047
|
+
* @param {boolean} [enable] - enable WebGL rendering and update the texture */
|
|
4670
5048
|
useWebGL(enable=true)
|
|
4671
5049
|
{
|
|
4672
5050
|
if (glEnable && enable)
|
|
@@ -4712,7 +5090,7 @@ class TileLayer extends CanvasLayer
|
|
|
4712
5090
|
this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
|
|
4713
5091
|
/** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
|
|
4714
5092
|
this.context = this.canvas.getContext('2d');
|
|
4715
|
-
/** @property {WebGLTexture} - Texture if using
|
|
5093
|
+
/** @property {WebGLTexture} - Texture if using WebGL for this layer */
|
|
4716
5094
|
this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
|
|
4717
5095
|
// set no friction by default, applied friction is max of both objects
|
|
4718
5096
|
this.friction = 0;
|
|
@@ -4737,7 +5115,7 @@ class TileLayer extends CanvasLayer
|
|
|
4737
5115
|
}
|
|
4738
5116
|
}
|
|
4739
5117
|
|
|
4740
|
-
/** Set data at a given position in the array
|
|
5118
|
+
/** Set data at a given position in the array
|
|
4741
5119
|
* @param {Vector2} layerPos - Local position in array
|
|
4742
5120
|
* @param {TileLayerData} data - Data to set
|
|
4743
5121
|
* @param {boolean} [redraw] - Force the tile to redraw if true */
|
|
@@ -4749,26 +5127,26 @@ class TileLayer extends CanvasLayer
|
|
|
4749
5127
|
redraw && this.drawTileData(layerPos);
|
|
4750
5128
|
}
|
|
4751
5129
|
}
|
|
4752
|
-
|
|
4753
|
-
/** Get data at a given position in the array
|
|
5130
|
+
|
|
5131
|
+
/** Get data at a given position in the array
|
|
4754
5132
|
* @param {Vector2} layerPos - Local position in array
|
|
4755
5133
|
* @return {TileLayerData} */
|
|
4756
5134
|
getData(layerPos)
|
|
4757
5135
|
{ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
|
|
4758
|
-
|
|
5136
|
+
|
|
4759
5137
|
// Render the tile layer, called automatically by the engine
|
|
4760
5138
|
render()
|
|
4761
5139
|
{
|
|
4762
|
-
ASSERT(drawContext
|
|
4763
|
-
|
|
5140
|
+
ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
|
|
5141
|
+
|
|
4764
5142
|
// draw the tile layer as a single tile
|
|
4765
5143
|
const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
|
|
4766
5144
|
const pos = this.pos.add(this.size.scale(.5));
|
|
4767
|
-
const
|
|
4768
|
-
drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK,
|
|
5145
|
+
const useWebGL = glEnable && this.glTexture !== undefined;
|
|
5146
|
+
drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
|
|
4769
5147
|
}
|
|
4770
5148
|
|
|
4771
|
-
/** Draw all the tile data to an offscreen canvas
|
|
5149
|
+
/** Draw all the tile data to an offscreen canvas
|
|
4772
5150
|
* - This may be slow in some browsers but only needs to be done once */
|
|
4773
5151
|
redraw()
|
|
4774
5152
|
{
|
|
@@ -4778,7 +5156,7 @@ class TileLayer extends CanvasLayer
|
|
|
4778
5156
|
this.drawTileData(vec2(x,y), false);
|
|
4779
5157
|
this.redrawEnd();
|
|
4780
5158
|
if (this.glTexture)
|
|
4781
|
-
this.useWebGL(); // update
|
|
5159
|
+
this.useWebGL(); // update WebGL texture
|
|
4782
5160
|
}
|
|
4783
5161
|
|
|
4784
5162
|
/** Call to start the redraw process
|
|
@@ -4814,7 +5192,7 @@ class TileLayer extends CanvasLayer
|
|
|
4814
5192
|
/** Call to end the redraw process */
|
|
4815
5193
|
redrawEnd()
|
|
4816
5194
|
{
|
|
4817
|
-
ASSERT(drawContext
|
|
5195
|
+
ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
|
|
4818
5196
|
glCopyToContext(drawContext);
|
|
4819
5197
|
//debugSaveCanvas(this.canvas);
|
|
4820
5198
|
|
|
@@ -4825,7 +5203,7 @@ class TileLayer extends CanvasLayer
|
|
|
4825
5203
|
/** Draw the tile at a given position in the tile grid
|
|
4826
5204
|
* This can be used to clear out tiles when they are destroyed
|
|
4827
5205
|
* Tiles can also be redrawn if inside a redrawStart/End block
|
|
4828
|
-
* @param {Vector2} layerPos
|
|
5206
|
+
* @param {Vector2} layerPos
|
|
4829
5207
|
* @param {boolean} [clear] - should the old tile be cleared out
|
|
4830
5208
|
*/
|
|
4831
5209
|
drawTileData(layerPos, clear=true)
|
|
@@ -4840,9 +5218,9 @@ class TileLayer extends CanvasLayer
|
|
|
4840
5218
|
|
|
4841
5219
|
// draw the tile if it has layer data
|
|
4842
5220
|
const d = this.getData(layerPos);
|
|
4843
|
-
if (d.tile
|
|
5221
|
+
if (d.tile !== undefined)
|
|
4844
5222
|
{
|
|
4845
|
-
ASSERT(drawContext
|
|
5223
|
+
ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
|
|
4846
5224
|
const pos = layerPos.add(vec2(.5));
|
|
4847
5225
|
const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
|
|
4848
5226
|
drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
@@ -4855,7 +5233,7 @@ class TileLayer extends CanvasLayer
|
|
|
4855
5233
|
* Tile Collision Layer - a tile layer with collision
|
|
4856
5234
|
* - adds collision data and functions to TileLayer
|
|
4857
5235
|
* - there can be multiple tile collision layers
|
|
4858
|
-
* - tile
|
|
5236
|
+
* - tile collision layers should not overlap each other
|
|
4859
5237
|
* @extends TileLayer
|
|
4860
5238
|
*/
|
|
4861
5239
|
class TileCollisionLayer extends TileLayer
|
|
@@ -5006,7 +5384,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
5006
5384
|
debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
|
|
5007
5385
|
}
|
|
5008
5386
|
}
|
|
5009
|
-
/**
|
|
5387
|
+
/**
|
|
5010
5388
|
* LittleJS Particle System
|
|
5011
5389
|
*/
|
|
5012
5390
|
|
|
@@ -5023,7 +5401,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
5023
5401
|
* rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
|
|
5024
5402
|
* rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
|
|
5025
5403
|
* 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
|
|
5026
|
-
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
|
|
5404
|
+
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
|
|
5027
5405
|
* .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
|
|
5028
5406
|
* );
|
|
5029
5407
|
*/
|
|
@@ -5031,35 +5409,35 @@ class ParticleEmitter extends EngineObject
|
|
|
5031
5409
|
{
|
|
5032
5410
|
/** Create a particle system with the given settings
|
|
5033
5411
|
* @param {Vector2} position - World space position of the emitter
|
|
5034
|
-
* @param {
|
|
5035
|
-
* @param {
|
|
5036
|
-
* @param {
|
|
5037
|
-
* @param {
|
|
5038
|
-
* @param {
|
|
5412
|
+
* @param {number} [angle] - Angle to emit the particles
|
|
5413
|
+
* @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
|
|
5414
|
+
* @param {number} [emitTime] - How long to stay alive (0 is forever)
|
|
5415
|
+
* @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
|
|
5416
|
+
* @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
|
|
5039
5417
|
* @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
|
|
5040
5418
|
* @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
|
|
5041
5419
|
* @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
|
|
5042
5420
|
* @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
|
|
5043
5421
|
* @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
|
|
5044
|
-
* @param {
|
|
5045
|
-
* @param {
|
|
5046
|
-
* @param {
|
|
5047
|
-
* @param {
|
|
5048
|
-
* @param {
|
|
5049
|
-
* @param {
|
|
5050
|
-
* @param {
|
|
5051
|
-
* @param {
|
|
5052
|
-
* @param {
|
|
5053
|
-
* @param {
|
|
5054
|
-
* @param {
|
|
5422
|
+
* @param {number} [particleTime] - How long particles live
|
|
5423
|
+
* @param {number} [sizeStart] - How big are particles at start
|
|
5424
|
+
* @param {number} [sizeEnd] - How big are particles at end
|
|
5425
|
+
* @param {number} [speed] - How fast are particles when spawned
|
|
5426
|
+
* @param {number} [angleSpeed] - How fast are particles rotating
|
|
5427
|
+
* @param {number} [damping] - How much to dampen particle speed
|
|
5428
|
+
* @param {number} [angleDamping] - How much to dampen particle angular speed
|
|
5429
|
+
* @param {number} [gravityScale] - How much gravity effect particles
|
|
5430
|
+
* @param {number} [particleConeAngle] - Cone for start particle angle
|
|
5431
|
+
* @param {number} [fadeRate] - How quick to fade particles at start/end in percent of life
|
|
5432
|
+
* @param {number} [randomness] - Apply extra randomness percent
|
|
5055
5433
|
* @param {boolean} [collideTiles] - Do particles collide against tiles
|
|
5056
5434
|
* @param {boolean} [additive] - Should particles use additive blend
|
|
5057
5435
|
* @param {boolean} [randomColorLinear] - Should color be randomized linearly or across each component
|
|
5058
|
-
* @param {
|
|
5436
|
+
* @param {number} [renderOrder] - Render order for particles (additive is above other stuff by default)
|
|
5059
5437
|
* @param {boolean} [localSpace] - Should it be in local space of emitter (world space is default)
|
|
5060
5438
|
*/
|
|
5061
5439
|
constructor
|
|
5062
|
-
(
|
|
5440
|
+
(
|
|
5063
5441
|
position,
|
|
5064
5442
|
angle,
|
|
5065
5443
|
emitSize = 0,
|
|
@@ -5081,7 +5459,7 @@ class ParticleEmitter extends EngineObject
|
|
|
5081
5459
|
gravityScale = 0,
|
|
5082
5460
|
particleConeAngle = PI,
|
|
5083
5461
|
fadeRate = .1,
|
|
5084
|
-
randomness = .2,
|
|
5462
|
+
randomness = .2,
|
|
5085
5463
|
collideTiles = false,
|
|
5086
5464
|
additive = false,
|
|
5087
5465
|
randomColorLinear = true,
|
|
@@ -5092,13 +5470,13 @@ class ParticleEmitter extends EngineObject
|
|
|
5092
5470
|
super(position, vec2(), tileInfo, angle, undefined, renderOrder);
|
|
5093
5471
|
|
|
5094
5472
|
// emitter settings
|
|
5095
|
-
/** @property {
|
|
5473
|
+
/** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
|
|
5096
5474
|
this.emitSize = emitSize
|
|
5097
|
-
/** @property {
|
|
5475
|
+
/** @property {number} - How long to stay alive (0 is forever) */
|
|
5098
5476
|
this.emitTime = emitTime;
|
|
5099
|
-
/** @property {
|
|
5477
|
+
/** @property {number} - How many particles per second to spawn, does not emit if 0 */
|
|
5100
5478
|
this.emitRate = emitRate;
|
|
5101
|
-
/** @property {
|
|
5479
|
+
/** @property {number} - Local angle to apply velocity to particles from emitter */
|
|
5102
5480
|
this.emitConeAngle = emitConeAngle;
|
|
5103
5481
|
|
|
5104
5482
|
// color settings
|
|
@@ -5114,27 +5492,27 @@ class ParticleEmitter extends EngineObject
|
|
|
5114
5492
|
this.randomColorLinear = randomColorLinear;
|
|
5115
5493
|
|
|
5116
5494
|
// particle settings
|
|
5117
|
-
/** @property {
|
|
5495
|
+
/** @property {number} - How long particles live */
|
|
5118
5496
|
this.particleTime = particleTime;
|
|
5119
|
-
/** @property {
|
|
5497
|
+
/** @property {number} - How big are particles at start */
|
|
5120
5498
|
this.sizeStart = sizeStart;
|
|
5121
|
-
/** @property {
|
|
5499
|
+
/** @property {number} - How big are particles at end */
|
|
5122
5500
|
this.sizeEnd = sizeEnd;
|
|
5123
|
-
/** @property {
|
|
5501
|
+
/** @property {number} - How fast are particles when spawned */
|
|
5124
5502
|
this.speed = speed;
|
|
5125
|
-
/** @property {
|
|
5503
|
+
/** @property {number} - How fast are particles rotating */
|
|
5126
5504
|
this.angleSpeed = angleSpeed;
|
|
5127
|
-
/** @property {
|
|
5505
|
+
/** @property {number} - How much to dampen particle speed */
|
|
5128
5506
|
this.damping = damping;
|
|
5129
|
-
/** @property {
|
|
5507
|
+
/** @property {number} - How much to dampen particle angular speed */
|
|
5130
5508
|
this.angleDamping = angleDamping;
|
|
5131
|
-
/** @property {
|
|
5509
|
+
/** @property {number} - How much gravity affects particles */
|
|
5132
5510
|
this.gravityScale = gravityScale;
|
|
5133
|
-
/** @property {
|
|
5511
|
+
/** @property {number} - Cone for start particle angle */
|
|
5134
5512
|
this.particleConeAngle = particleConeAngle;
|
|
5135
|
-
/** @property {
|
|
5513
|
+
/** @property {number} - How quick to fade in particles at start/end in percent of life */
|
|
5136
5514
|
this.fadeRate = fadeRate;
|
|
5137
|
-
/** @property {
|
|
5515
|
+
/** @property {number} - Apply extra randomness percent */
|
|
5138
5516
|
this.randomness = randomness;
|
|
5139
5517
|
/** @property {boolean} - Do particles collide against tiles */
|
|
5140
5518
|
this.collideTiles = collideTiles;
|
|
@@ -5142,16 +5520,16 @@ class ParticleEmitter extends EngineObject
|
|
|
5142
5520
|
this.additive = additive;
|
|
5143
5521
|
/** @property {boolean} - Should it be in local space of emitter */
|
|
5144
5522
|
this.localSpace = localSpace;
|
|
5145
|
-
/** @property {
|
|
5523
|
+
/** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
|
|
5146
5524
|
this.trailScale = 0;
|
|
5147
5525
|
/** @property {Function} - Callback when particle is destroyed */
|
|
5148
5526
|
this.particleDestroyCallback = undefined;
|
|
5149
5527
|
/** @property {Function} - Callback when particle is created */
|
|
5150
5528
|
this.particleCreateCallback = undefined;
|
|
5151
|
-
/** @property {
|
|
5529
|
+
/** @property {number} - Track particle emit time */
|
|
5152
5530
|
this.emitTimeBuffer = 0;
|
|
5153
5531
|
}
|
|
5154
|
-
|
|
5532
|
+
|
|
5155
5533
|
/** Update the emitter to spawn particles, called automatically by engine once each frame */
|
|
5156
5534
|
update()
|
|
5157
5535
|
{
|
|
@@ -5175,7 +5553,7 @@ class ParticleEmitter extends EngineObject
|
|
|
5175
5553
|
if (debugParticles)
|
|
5176
5554
|
{
|
|
5177
5555
|
// show emitter bounds
|
|
5178
|
-
const emitSize = typeof this.emitSize
|
|
5556
|
+
const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
|
|
5179
5557
|
debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
|
|
5180
5558
|
}
|
|
5181
5559
|
}
|
|
@@ -5185,7 +5563,7 @@ class ParticleEmitter extends EngineObject
|
|
|
5185
5563
|
emitParticle()
|
|
5186
5564
|
{
|
|
5187
5565
|
// spawn a particle
|
|
5188
|
-
let pos = typeof this.emitSize
|
|
5566
|
+
let pos = typeof this.emitSize === 'number' ? // check if number was used
|
|
5189
5567
|
randInCircle(this.emitSize/2) // circle emitter
|
|
5190
5568
|
: vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
|
|
5191
5569
|
.multiply(this.emitSize).rotate(this.angle)
|
|
@@ -5210,15 +5588,22 @@ class ParticleEmitter extends EngineObject
|
|
|
5210
5588
|
const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
|
|
5211
5589
|
const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
|
|
5212
5590
|
const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
|
|
5213
|
-
|
|
5591
|
+
|
|
5214
5592
|
// build particle
|
|
5215
5593
|
const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
|
|
5216
5594
|
particle.velocity = vec2().setAngle(velocityAngle, speed);
|
|
5217
5595
|
particle.angleVelocity = angleSpeed;
|
|
5596
|
+
if (!this.localSpace)
|
|
5597
|
+
{
|
|
5598
|
+
// apply emitter velocity to particle
|
|
5599
|
+
particle.velocity.x += this.velocity.x;
|
|
5600
|
+
particle.velocity.y += this.velocity.y;
|
|
5601
|
+
particle.angleVelocity += this.angleVelocity;
|
|
5602
|
+
}
|
|
5218
5603
|
particle.fadeRate = this.fadeRate;
|
|
5219
5604
|
particle.damping = this.damping;
|
|
5220
5605
|
particle.angleDamping = this.angleDamping;
|
|
5221
|
-
particle.restitution
|
|
5606
|
+
particle.restitution = this.restitution;
|
|
5222
5607
|
particle.friction = this.friction;
|
|
5223
5608
|
particle.gravityScale = this.gravityScale;
|
|
5224
5609
|
particle.collideTiles = this.collideTiles;
|
|
@@ -5248,38 +5633,38 @@ class Particle extends EngineObject
|
|
|
5248
5633
|
* Typically this is created automatically by a ParticleEmitter
|
|
5249
5634
|
* @param {Vector2} position - World space position of the particle
|
|
5250
5635
|
* @param {TileInfo} tileInfo - Tile info to render particles
|
|
5251
|
-
* @param {
|
|
5636
|
+
* @param {number} angle - Angle to rotate the particle
|
|
5252
5637
|
* @param {Color} colorStart - Color at start of life
|
|
5253
5638
|
* @param {Color} colorEnd - Color at end of life
|
|
5254
|
-
* @param {
|
|
5255
|
-
* @param {
|
|
5256
|
-
* @param {
|
|
5257
|
-
* @param {
|
|
5639
|
+
* @param {number} lifeTime - How long to live for
|
|
5640
|
+
* @param {number} sizeStart - Size at start of life
|
|
5641
|
+
* @param {number} sizeEnd - Size at end of life
|
|
5642
|
+
* @param {number} fadeRate - How quick to fade in/out
|
|
5258
5643
|
* @param {boolean} additive - Does it use additive blend mode
|
|
5259
|
-
* @param {
|
|
5644
|
+
* @param {number} trailScale - If a trail, how long to make it
|
|
5260
5645
|
* @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
|
|
5261
5646
|
* @param {Function} [destroyCallback] - Callback when particle dies
|
|
5262
5647
|
*/
|
|
5263
5648
|
constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
|
|
5264
5649
|
)
|
|
5265
|
-
{
|
|
5266
|
-
super(position, vec2(), tileInfo, angle);
|
|
5267
|
-
|
|
5650
|
+
{
|
|
5651
|
+
super(position, vec2(), tileInfo, angle);
|
|
5652
|
+
|
|
5268
5653
|
/** @property {Color} - Color at start of life */
|
|
5269
5654
|
this.colorStart = colorStart;
|
|
5270
5655
|
/** @property {Color} - Calculated change in color */
|
|
5271
5656
|
this.colorEndDelta = colorEnd.subtract(colorStart);
|
|
5272
|
-
/** @property {
|
|
5657
|
+
/** @property {number} - How long to live for */
|
|
5273
5658
|
this.lifeTime = lifeTime;
|
|
5274
|
-
/** @property {
|
|
5659
|
+
/** @property {number} - Size at start of life */
|
|
5275
5660
|
this.sizeStart = sizeStart;
|
|
5276
|
-
/** @property {
|
|
5661
|
+
/** @property {number} - Calculated change in size */
|
|
5277
5662
|
this.sizeEndDelta = sizeEnd - sizeStart;
|
|
5278
|
-
/** @property {
|
|
5663
|
+
/** @property {number} - How quick to fade in/out */
|
|
5279
5664
|
this.fadeRate = fadeRate;
|
|
5280
5665
|
/** @property {boolean} - Is it additive */
|
|
5281
5666
|
this.additive = additive;
|
|
5282
|
-
/** @property {
|
|
5667
|
+
/** @property {number} - If a trail, how long to make it */
|
|
5283
5668
|
this.trailScale = trailScale;
|
|
5284
5669
|
/** @property {ParticleEmitter} - Parent emitter if local space */
|
|
5285
5670
|
this.localSpaceEmitter = localSpaceEmitter;
|
|
@@ -5319,7 +5704,7 @@ class Particle extends EngineObject
|
|
|
5319
5704
|
this.colorStart.r + p * this.colorEndDelta.r,
|
|
5320
5705
|
this.colorStart.g + p * this.colorEndDelta.g,
|
|
5321
5706
|
this.colorStart.b + p * this.colorEndDelta.b,
|
|
5322
|
-
(this.colorStart.a + p * this.colorEndDelta.a) *
|
|
5707
|
+
(this.colorStart.a + p * this.colorEndDelta.a) *
|
|
5323
5708
|
(p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
|
|
5324
5709
|
|
|
5325
5710
|
// draw the particle
|
|
@@ -5329,7 +5714,7 @@ class Particle extends EngineObject
|
|
|
5329
5714
|
if (this.localSpaceEmitter)
|
|
5330
5715
|
{
|
|
5331
5716
|
// in local space of emitter
|
|
5332
|
-
pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
|
|
5717
|
+
pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
|
|
5333
5718
|
angle += this.localSpaceEmitter.angle;
|
|
5334
5719
|
}
|
|
5335
5720
|
if (this.trailScale)
|
|
@@ -5353,7 +5738,7 @@ class Particle extends EngineObject
|
|
|
5353
5738
|
this.additive && setBlendMode();
|
|
5354
5739
|
debugParticles && debugRect(pos, size, '#f005', 0, angle);
|
|
5355
5740
|
|
|
5356
|
-
if (p
|
|
5741
|
+
if (p === 1)
|
|
5357
5742
|
{
|
|
5358
5743
|
// destroy particle when it's time runs out
|
|
5359
5744
|
this.color = color;
|
|
@@ -5363,7 +5748,7 @@ class Particle extends EngineObject
|
|
|
5363
5748
|
}
|
|
5364
5749
|
}
|
|
5365
5750
|
}
|
|
5366
|
-
/**
|
|
5751
|
+
/**
|
|
5367
5752
|
* LittleJS Medal System
|
|
5368
5753
|
* - Tracks and displays medals
|
|
5369
5754
|
* - Saves medals to local storage
|
|
@@ -5384,7 +5769,7 @@ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
|
|
|
5384
5769
|
/** Initialize medals with a save name used for storage
|
|
5385
5770
|
* - Call this after creating all medals
|
|
5386
5771
|
* - Checks if medals are unlocked
|
|
5387
|
-
* @param {
|
|
5772
|
+
* @param {string} saveName
|
|
5388
5773
|
* @memberof Medals */
|
|
5389
5774
|
function medalsInit(saveName)
|
|
5390
5775
|
{
|
|
@@ -5399,7 +5784,7 @@ function medalsInit(saveName)
|
|
|
5399
5784
|
{
|
|
5400
5785
|
if (!medalsDisplayQueue.length)
|
|
5401
5786
|
return;
|
|
5402
|
-
|
|
5787
|
+
|
|
5403
5788
|
// update first medal in queue
|
|
5404
5789
|
const medal = medalsDisplayQueue[0];
|
|
5405
5790
|
const time = timeReal - medalsDisplayTimeLast;
|
|
@@ -5414,7 +5799,7 @@ function medalsInit(saveName)
|
|
|
5414
5799
|
{
|
|
5415
5800
|
// slide on/off medals
|
|
5416
5801
|
const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
|
|
5417
|
-
const hidePercent =
|
|
5802
|
+
const hidePercent =
|
|
5418
5803
|
time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
|
|
5419
5804
|
time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
|
|
5420
5805
|
medal.render(hidePercent);
|
|
@@ -5430,43 +5815,43 @@ function medalsForEach(callback)
|
|
|
5430
5815
|
|
|
5431
5816
|
///////////////////////////////////////////////////////////////////////////////
|
|
5432
5817
|
|
|
5433
|
-
/**
|
|
5434
|
-
* Medal - Tracks an unlockable medal
|
|
5818
|
+
/**
|
|
5819
|
+
* Medal - Tracks an unlockable medal
|
|
5435
5820
|
* @example
|
|
5436
5821
|
* // create a medal
|
|
5437
5822
|
* const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
|
|
5438
|
-
*
|
|
5823
|
+
*
|
|
5439
5824
|
* // initialize medals
|
|
5440
5825
|
* medalsInit('Example Game');
|
|
5441
|
-
*
|
|
5826
|
+
*
|
|
5442
5827
|
* // unlock the medal
|
|
5443
5828
|
* medal_example.unlock();
|
|
5444
5829
|
*/
|
|
5445
5830
|
class Medal
|
|
5446
5831
|
{
|
|
5447
5832
|
/** Create a medal object and adds it to the list of medals
|
|
5448
|
-
* @param {
|
|
5449
|
-
* @param {
|
|
5450
|
-
* @param {
|
|
5451
|
-
* @param {
|
|
5452
|
-
* @param {
|
|
5833
|
+
* @param {number} id - The unique identifier of the medal
|
|
5834
|
+
* @param {string} name - Name of the medal
|
|
5835
|
+
* @param {string} [description] - Description of the medal
|
|
5836
|
+
* @param {string} [icon] - Icon for the medal
|
|
5837
|
+
* @param {string} [src] - Image location for the medal
|
|
5453
5838
|
*/
|
|
5454
5839
|
constructor(id, name, description='', icon='🏆', src)
|
|
5455
5840
|
{
|
|
5456
5841
|
ASSERT(id >= 0 && !medals[id]);
|
|
5457
|
-
|
|
5458
|
-
/** @property {
|
|
5842
|
+
|
|
5843
|
+
/** @property {number} - The unique identifier of the medal */
|
|
5459
5844
|
this.id = id;
|
|
5460
|
-
|
|
5461
|
-
/** @property {
|
|
5845
|
+
|
|
5846
|
+
/** @property {string} - Name of the medal */
|
|
5462
5847
|
this.name = name;
|
|
5463
|
-
|
|
5464
|
-
/** @property {
|
|
5848
|
+
|
|
5849
|
+
/** @property {string} - Description of the medal */
|
|
5465
5850
|
this.description = description;
|
|
5466
|
-
|
|
5467
|
-
/** @property {
|
|
5851
|
+
|
|
5852
|
+
/** @property {string} - Icon for the medal */
|
|
5468
5853
|
this.icon = icon;
|
|
5469
|
-
|
|
5854
|
+
|
|
5470
5855
|
/** @property {boolean} - Is the medal unlocked? */
|
|
5471
5856
|
this.unlocked = false;
|
|
5472
5857
|
|
|
@@ -5491,7 +5876,7 @@ class Medal
|
|
|
5491
5876
|
}
|
|
5492
5877
|
|
|
5493
5878
|
/** Render a medal
|
|
5494
|
-
* @param {
|
|
5879
|
+
* @param {number} [hidePercent] - How much to slide the medal off screen
|
|
5495
5880
|
*/
|
|
5496
5881
|
render(hidePercent=0)
|
|
5497
5882
|
{
|
|
@@ -5505,7 +5890,7 @@ class Medal
|
|
|
5505
5890
|
context.save();
|
|
5506
5891
|
context.beginPath();
|
|
5507
5892
|
context.fillStyle = new Color(.9,.9,.9).toString();
|
|
5508
|
-
context.strokeStyle =
|
|
5893
|
+
context.strokeStyle = BLACK.toString();
|
|
5509
5894
|
context.lineWidth = 3;
|
|
5510
5895
|
context.rect(x, y, width, height);
|
|
5511
5896
|
context.fill();
|
|
@@ -5522,17 +5907,17 @@ class Medal
|
|
|
5522
5907
|
const descriptionSize = height*.3;
|
|
5523
5908
|
const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
|
|
5524
5909
|
const textWidth = width - medalDisplayIconSize - 3*gap.x;
|
|
5525
|
-
drawTextScreen(this.name, pos, nameSize,
|
|
5910
|
+
drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
|
|
5526
5911
|
|
|
5527
5912
|
// draw the description
|
|
5528
5913
|
pos.y = y + height - gap.y*2 - descriptionSize/2;
|
|
5529
|
-
drawTextScreen(this.description, pos, descriptionSize,
|
|
5914
|
+
drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
|
|
5530
5915
|
context.restore();
|
|
5531
5916
|
}
|
|
5532
5917
|
|
|
5533
5918
|
/** Render the icon for a medal
|
|
5534
5919
|
* @param {Vector2} pos - Screen space position
|
|
5535
|
-
* @param {
|
|
5920
|
+
* @param {number} size - Screen space size
|
|
5536
5921
|
*/
|
|
5537
5922
|
renderIcon(pos, size)
|
|
5538
5923
|
{
|
|
@@ -5540,21 +5925,22 @@ class Medal
|
|
|
5540
5925
|
if (this.image)
|
|
5541
5926
|
overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
|
|
5542
5927
|
else
|
|
5543
|
-
drawTextScreen(this.icon, pos, size*.7,
|
|
5928
|
+
drawTextScreen(this.icon, pos, size*.7, BLACK);
|
|
5544
5929
|
}
|
|
5545
|
-
|
|
5930
|
+
|
|
5546
5931
|
// Get local storage key used by the medal
|
|
5547
5932
|
storageKey() { return medalsSaveName + '_' + this.id; }
|
|
5548
5933
|
}
|
|
5549
5934
|
/**
|
|
5550
5935
|
* LittleJS WebGL Interface
|
|
5551
|
-
* - All
|
|
5936
|
+
* - All WebGL used by the engine is wrapped up here
|
|
5937
|
+
* - Will fall back to 2D canvas rendering if WebGL is not supported
|
|
5552
5938
|
* - For normal stuff you won't need to see or call anything in this file
|
|
5553
5939
|
* - For advanced stuff there are helper functions to create shaders, textures, etc
|
|
5554
5940
|
* - Can be disabled with glEnable to revert to 2D canvas rendering
|
|
5555
5941
|
* - Batches sprite rendering on GPU for incredibly fast performance
|
|
5556
5942
|
* - Sprite transform math is done in the shader where possible
|
|
5557
|
-
* - Supports shadertoy style post processing shaders
|
|
5943
|
+
* - Supports shadertoy style post processing shaders via plugin
|
|
5558
5944
|
* @namespace WebGL
|
|
5559
5945
|
*/
|
|
5560
5946
|
|
|
@@ -5563,24 +5949,27 @@ class Medal
|
|
|
5563
5949
|
* @memberof WebGL */
|
|
5564
5950
|
let glCanvas;
|
|
5565
5951
|
|
|
5566
|
-
/**
|
|
5952
|
+
/** WebGL2 context for `glCanvas`
|
|
5567
5953
|
* @type {WebGL2RenderingContext}
|
|
5568
5954
|
* @memberof WebGL */
|
|
5569
5955
|
let glContext;
|
|
5570
5956
|
|
|
5571
|
-
/** Should
|
|
5957
|
+
/** Should WebGL be setup with anti-aliasing? must be set before calling engineInit
|
|
5572
5958
|
* @type {boolean}
|
|
5573
5959
|
* @memberof WebGL */
|
|
5574
5960
|
let glAntialias = true;
|
|
5575
5961
|
|
|
5576
5962
|
// WebGL internal variables not exposed to documentation
|
|
5577
|
-
let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData,
|
|
5963
|
+
let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
|
|
5578
5964
|
|
|
5579
|
-
// WebGL internal constants
|
|
5580
|
-
const
|
|
5965
|
+
// WebGL internal constants
|
|
5966
|
+
const gl_ARRAY_BUFFER_SIZE = 4e5;
|
|
5581
5967
|
const gl_INDICES_PER_INSTANCE = 11;
|
|
5582
5968
|
const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
|
|
5583
|
-
const
|
|
5969
|
+
const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
|
|
5970
|
+
const gl_INDICES_PER_POLY_VERTEX = 3;
|
|
5971
|
+
const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
|
|
5972
|
+
const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
|
|
5584
5973
|
|
|
5585
5974
|
///////////////////////////////////////////////////////////////////////////////
|
|
5586
5975
|
|
|
@@ -5593,11 +5982,19 @@ function glInit()
|
|
|
5593
5982
|
glCanvas = document.createElement('canvas');
|
|
5594
5983
|
glContext = glCanvas.getContext('webgl2', {antialias:glAntialias});
|
|
5595
5984
|
|
|
5596
|
-
|
|
5985
|
+
if (!glContext)
|
|
5986
|
+
{
|
|
5987
|
+
console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
|
|
5988
|
+
glCanvas = glContext = undefined;
|
|
5989
|
+
glEnable = false;
|
|
5990
|
+
return;
|
|
5991
|
+
}
|
|
5992
|
+
|
|
5993
|
+
// create the WebGL canvas
|
|
5597
5994
|
const rootElement = mainCanvas.parentElement;
|
|
5598
5995
|
rootElement.appendChild(glCanvas);
|
|
5599
5996
|
|
|
5600
|
-
// setup
|
|
5997
|
+
// setup instanced rendering shader program
|
|
5601
5998
|
glShader = glCreateProgram(
|
|
5602
5999
|
'#version 300 es\n' + // specify GLSL ES version
|
|
5603
6000
|
'precision highp float;'+ // use highp for better accuracy
|
|
@@ -5625,40 +6022,59 @@ function glInit()
|
|
|
5625
6022
|
'}' // end of shader
|
|
5626
6023
|
);
|
|
5627
6024
|
|
|
6025
|
+
// setup poly rendering shaders
|
|
6026
|
+
glPolyShader = glCreateProgram(
|
|
6027
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
6028
|
+
'precision highp float;'+ // use highp for better accuracy
|
|
6029
|
+
'uniform mat4 m;'+ // transform matrix
|
|
6030
|
+
'in vec2 p;'+ // in: position
|
|
6031
|
+
'in vec4 c;'+ // in: color
|
|
6032
|
+
'out vec4 d;'+ // out: color
|
|
6033
|
+
'void main(){'+ // shader entry point
|
|
6034
|
+
'gl_Position=m*vec4(p,1,1);'+ // transform position
|
|
6035
|
+
'd=c;'+ // pass color to fragment shader
|
|
6036
|
+
'}' // end of shader
|
|
6037
|
+
,
|
|
6038
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
6039
|
+
'precision highp float;'+ // use highp for better accuracy
|
|
6040
|
+
'in vec4 d;'+ // in: color
|
|
6041
|
+
'out vec4 c;'+ // out: color
|
|
6042
|
+
'void main(){'+ // shader entry point
|
|
6043
|
+
'c=d;'+ // set color
|
|
6044
|
+
'}' // end of shader
|
|
6045
|
+
);
|
|
6046
|
+
|
|
5628
6047
|
// init buffers
|
|
5629
|
-
const glInstanceData = new ArrayBuffer(
|
|
6048
|
+
const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
|
|
5630
6049
|
glPositionData = new Float32Array(glInstanceData);
|
|
5631
6050
|
glColorData = new Uint32Array(glInstanceData);
|
|
5632
6051
|
glArrayBuffer = glContext.createBuffer();
|
|
5633
6052
|
glGeometryBuffer = glContext.createBuffer();
|
|
5634
6053
|
|
|
5635
6054
|
// create the geometry buffer, triangle strip square
|
|
5636
|
-
const geometry = new Float32Array([
|
|
6055
|
+
const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
|
|
5637
6056
|
glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
|
|
5638
6057
|
glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
|
|
5639
6058
|
}
|
|
5640
6059
|
|
|
5641
|
-
|
|
5642
|
-
// Also used by tile layer rendering when redrawing tiles
|
|
5643
|
-
function glPreRender()
|
|
6060
|
+
function glSetInstancedMode(force=false)
|
|
5644
6061
|
{
|
|
5645
|
-
if (!
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
6062
|
+
if (!glPolyMode && !force)
|
|
6063
|
+
return;
|
|
6064
|
+
|
|
6065
|
+
// setup instanced mode
|
|
6066
|
+
glFlush();
|
|
6067
|
+
glPolyMode = false;
|
|
5649
6068
|
glContext.useProgram(glShader);
|
|
5650
|
-
glContext.activeTexture(glContext.TEXTURE0);
|
|
5651
|
-
if (textureInfos[0])
|
|
5652
|
-
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
|
|
5653
6069
|
|
|
5654
6070
|
// set vertex attributes
|
|
5655
|
-
let offset =
|
|
6071
|
+
let offset = 0;
|
|
5656
6072
|
const initVertexAttribArray = (name, type, typeSize, size)=>
|
|
5657
6073
|
{
|
|
5658
6074
|
const location = glContext.getAttribLocation(glShader, name);
|
|
5659
6075
|
const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
|
|
5660
6076
|
const divisor = typeSize && 1; // only if not geometry
|
|
5661
|
-
const normalize = typeSize
|
|
6077
|
+
const normalize = typeSize === 1; // only if color
|
|
5662
6078
|
glContext.enableVertexAttribArray(location);
|
|
5663
6079
|
glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
|
|
5664
6080
|
glContext.vertexAttribDivisor(location, divisor);
|
|
@@ -5667,46 +6083,106 @@ function glPreRender()
|
|
|
5667
6083
|
glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
|
|
5668
6084
|
initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
|
|
5669
6085
|
glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
|
|
5670
|
-
glContext.bufferData(glContext.ARRAY_BUFFER,
|
|
6086
|
+
glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
|
|
5671
6087
|
initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
|
|
5672
6088
|
initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
|
|
5673
6089
|
initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
|
|
5674
6090
|
initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
|
|
5675
6091
|
initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
|
|
6092
|
+
}
|
|
6093
|
+
|
|
6094
|
+
function glSetPolyMode()
|
|
6095
|
+
{
|
|
6096
|
+
if (glPolyMode)
|
|
6097
|
+
return;
|
|
5676
6098
|
|
|
6099
|
+
// setup poly mode
|
|
6100
|
+
glFlush();
|
|
6101
|
+
glPolyMode = true;
|
|
6102
|
+
glContext.useProgram(glPolyShader);
|
|
6103
|
+
|
|
6104
|
+
// set vertex attributes
|
|
6105
|
+
let offset = 0;
|
|
6106
|
+
const initVertexAttribArray = (name, type, typeSize, size)=>
|
|
6107
|
+
{
|
|
6108
|
+
const location = glContext.getAttribLocation(glPolyShader, name);
|
|
6109
|
+
const normalize = typeSize === 1; // only normalize if color
|
|
6110
|
+
const stride = gl_POLY_VERTEX_BYTE_STRIDE;
|
|
6111
|
+
glContext.enableVertexAttribArray(location);
|
|
6112
|
+
glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
|
|
6113
|
+
glContext.vertexAttribDivisor(location, 0);
|
|
6114
|
+
offset += size*typeSize;
|
|
6115
|
+
}
|
|
6116
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
|
|
6117
|
+
glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
|
|
6118
|
+
initVertexAttribArray('p', glContext.FLOAT, 4, 2); // position
|
|
6119
|
+
initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
|
|
6120
|
+
}
|
|
6121
|
+
|
|
6122
|
+
// Setup WebGL render each frame, called automatically by engine
|
|
6123
|
+
// Also used by tile layer rendering when redrawing tiles
|
|
6124
|
+
function glPreRender()
|
|
6125
|
+
{
|
|
6126
|
+
if (!glEnable || !glContext) return;
|
|
6127
|
+
|
|
6128
|
+
// clear the canvas
|
|
6129
|
+
glClearCanvas();
|
|
6130
|
+
|
|
5677
6131
|
// build the transform matrix
|
|
5678
6132
|
const s = vec2(2*cameraScale).divide(mainCanvasSize);
|
|
5679
6133
|
const rotatedCam = cameraPos.rotate(-cameraAngle);
|
|
5680
6134
|
const p = vec2(-1).subtract(rotatedCam.multiply(s));
|
|
5681
6135
|
const ca = Math.cos(cameraAngle);
|
|
5682
6136
|
const sa = Math.sin(cameraAngle);
|
|
5683
|
-
|
|
5684
|
-
[
|
|
6137
|
+
const transform = [
|
|
5685
6138
|
s.x * ca, s.y * sa, 0, 0,
|
|
5686
6139
|
-s.x * sa, s.y * ca, 0, 0,
|
|
5687
6140
|
1, 1, 1, 0,
|
|
5688
|
-
p.x, p.y, 0, 1
|
|
5689
|
-
|
|
6141
|
+
p.x, p.y, 0, 1];
|
|
6142
|
+
|
|
6143
|
+
// set the same matrix for both shaders
|
|
6144
|
+
const initUniform = (program, uniform, value) =>
|
|
6145
|
+
{
|
|
6146
|
+
glContext.useProgram(program);
|
|
6147
|
+
const location = glContext.getUniformLocation(program, uniform);
|
|
6148
|
+
glContext.uniformMatrix4fv(location, false, value);
|
|
6149
|
+
}
|
|
6150
|
+
initUniform(glPolyShader, 'm', transform);
|
|
6151
|
+
initUniform(glShader, 'm', transform);
|
|
6152
|
+
|
|
6153
|
+
// set the active texture
|
|
6154
|
+
glContext.activeTexture(glContext.TEXTURE0);
|
|
6155
|
+
if (textureInfos[0])
|
|
6156
|
+
{
|
|
6157
|
+
glActiveTexture = textureInfos[0].glTexture;
|
|
6158
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
6159
|
+
}
|
|
6160
|
+
|
|
6161
|
+
// start in instanced rendering mode with additive blending off
|
|
6162
|
+
glAdditive = glBatchAdditive = glPolyMode = false;
|
|
6163
|
+
glSetInstancedMode(true);
|
|
5690
6164
|
}
|
|
5691
6165
|
|
|
5692
6166
|
/** Clear the canvas and setup the viewport
|
|
5693
6167
|
* @memberof WebGL */
|
|
5694
6168
|
function glClearCanvas()
|
|
5695
6169
|
{
|
|
6170
|
+
if (!glContext) return;
|
|
6171
|
+
|
|
5696
6172
|
// clear and set to same size as main canvas
|
|
5697
6173
|
glContext.viewport(0, 0, glCanvas.width=drawCanvas.width, glCanvas.height=drawCanvas.height);
|
|
5698
6174
|
glContext.clear(glContext.COLOR_BUFFER_BIT);
|
|
5699
6175
|
}
|
|
5700
6176
|
|
|
5701
|
-
/** Set the
|
|
6177
|
+
/** Set the WebGL texture, called automatically if using multiple textures
|
|
5702
6178
|
* - This may also flush the gl buffer resulting in more draw calls and worse performance
|
|
5703
6179
|
* @param {WebGLTexture} texture
|
|
5704
|
-
* @param {boolean} wrap - Should the texture wrap or clamp
|
|
6180
|
+
* @param {boolean} [wrap] - Should the texture wrap or clamp
|
|
5705
6181
|
* @memberof WebGL */
|
|
5706
6182
|
function glSetTexture(texture, wrap=false)
|
|
5707
6183
|
{
|
|
5708
6184
|
// must flush cache with the old texture to set a new one
|
|
5709
|
-
if (
|
|
6185
|
+
if (!glContext || texture === glActiveTexture)
|
|
5710
6186
|
return;
|
|
5711
6187
|
|
|
5712
6188
|
glFlush();
|
|
@@ -5719,12 +6195,14 @@ function glSetTexture(texture, wrap=false)
|
|
|
5719
6195
|
}
|
|
5720
6196
|
|
|
5721
6197
|
/** Compile WebGL shader of the given type, will throw errors if in debug mode
|
|
5722
|
-
* @param {
|
|
5723
|
-
* @param {
|
|
6198
|
+
* @param {string} source
|
|
6199
|
+
* @param {number} type
|
|
5724
6200
|
* @return {WebGLShader}
|
|
5725
6201
|
* @memberof WebGL */
|
|
5726
6202
|
function glCompileShader(source, type)
|
|
5727
6203
|
{
|
|
6204
|
+
if (!glContext) return;
|
|
6205
|
+
|
|
5728
6206
|
// build the shader
|
|
5729
6207
|
const shader = glContext.createShader(type);
|
|
5730
6208
|
glContext.shaderSource(shader, source);
|
|
@@ -5737,12 +6215,14 @@ function glCompileShader(source, type)
|
|
|
5737
6215
|
}
|
|
5738
6216
|
|
|
5739
6217
|
/** Create WebGL program with given shaders
|
|
5740
|
-
* @param {
|
|
5741
|
-
* @param {
|
|
6218
|
+
* @param {string} vsSource
|
|
6219
|
+
* @param {string} fsSource
|
|
5742
6220
|
* @return {WebGLProgram}
|
|
5743
6221
|
* @memberof WebGL */
|
|
5744
6222
|
function glCreateProgram(vsSource, fsSource)
|
|
5745
6223
|
{
|
|
6224
|
+
if (!glContext) return;
|
|
6225
|
+
|
|
5746
6226
|
// build the program
|
|
5747
6227
|
const program = glContext.createProgram();
|
|
5748
6228
|
glContext.attachShader(program, glCompileShader(vsSource, glContext.VERTEX_SHADER));
|
|
@@ -5761,6 +6241,8 @@ function glCreateProgram(vsSource, fsSource)
|
|
|
5761
6241
|
* @memberof WebGL */
|
|
5762
6242
|
function glCreateTexture(image)
|
|
5763
6243
|
{
|
|
6244
|
+
if (!glContext) return;
|
|
6245
|
+
|
|
5764
6246
|
// build the texture
|
|
5765
6247
|
const texture = glContext.createTexture();
|
|
5766
6248
|
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
@@ -5782,7 +6264,7 @@ function glCreateTexture(image)
|
|
|
5782
6264
|
const whitePixel = new Uint8Array([255, 255, 255, 255]);
|
|
5783
6265
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
|
|
5784
6266
|
}
|
|
5785
|
-
|
|
6267
|
+
|
|
5786
6268
|
// set texture filtering
|
|
5787
6269
|
const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
|
|
5788
6270
|
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
|
|
@@ -5790,12 +6272,12 @@ function glCreateTexture(image)
|
|
|
5790
6272
|
return texture;
|
|
5791
6273
|
}
|
|
5792
6274
|
|
|
5793
|
-
|
|
5794
6275
|
/** Deletes a WebGL texture
|
|
5795
6276
|
* @param {WebGLTexture} [texture]
|
|
5796
6277
|
* @memberof WebGL */
|
|
5797
6278
|
function glDeleteTexture(texture)
|
|
5798
6279
|
{
|
|
6280
|
+
if (!glContext) return;
|
|
5799
6281
|
glContext.deleteTexture(texture);
|
|
5800
6282
|
}
|
|
5801
6283
|
|
|
@@ -5805,6 +6287,8 @@ function glDeleteTexture(texture)
|
|
|
5805
6287
|
* @memberof WebGL */
|
|
5806
6288
|
function glSetTextureData(texture, image)
|
|
5807
6289
|
{
|
|
6290
|
+
if (!glContext) return;
|
|
6291
|
+
|
|
5808
6292
|
// build the texture
|
|
5809
6293
|
ASSERT(!!image && image.width > 0, 'Invalid image data.');
|
|
5810
6294
|
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
@@ -5815,18 +6299,23 @@ function glSetTextureData(texture, image)
|
|
|
5815
6299
|
* @memberof WebGL */
|
|
5816
6300
|
function glFlush()
|
|
5817
6301
|
{
|
|
5818
|
-
if (
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
6302
|
+
if (glEnable && glContext && glBatchCount)
|
|
6303
|
+
{
|
|
6304
|
+
// set bend mode
|
|
6305
|
+
const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
|
|
6306
|
+
glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
|
|
6307
|
+
glContext.enable(glContext.BLEND);
|
|
6308
|
+
glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData);
|
|
6309
|
+
|
|
6310
|
+
// draw the batch
|
|
6311
|
+
if (glPolyMode)
|
|
6312
|
+
glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
|
|
6313
|
+
else
|
|
6314
|
+
glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
|
|
6315
|
+
if (debug || showWatermark)
|
|
6316
|
+
drawCount += glBatchCount;
|
|
6317
|
+
glBatchCount = 0;
|
|
6318
|
+
}
|
|
5830
6319
|
glBatchAdditive = glAdditive;
|
|
5831
6320
|
}
|
|
5832
6321
|
|
|
@@ -5835,14 +6324,15 @@ function glFlush()
|
|
|
5835
6324
|
* @memberof WebGL */
|
|
5836
6325
|
function glCopyToContext(context)
|
|
5837
6326
|
{
|
|
5838
|
-
if (!glEnable)
|
|
6327
|
+
if (!glEnable || !glContext)
|
|
5839
6328
|
return;
|
|
5840
6329
|
|
|
5841
6330
|
glFlush();
|
|
5842
6331
|
context.drawImage(glCanvas, 0, 0);
|
|
5843
6332
|
}
|
|
5844
6333
|
|
|
5845
|
-
/** Set anti-aliasing for
|
|
6334
|
+
/** Set anti-aliasing for WebGL canvas
|
|
6335
|
+
* Must be called before engineInit
|
|
5846
6336
|
* @param {boolean} [antialias]
|
|
5847
6337
|
* @memberof WebGL */
|
|
5848
6338
|
function glSetAntialias(antialias=true)
|
|
@@ -5852,25 +6342,27 @@ function glSetAntialias(antialias=true)
|
|
|
5852
6342
|
}
|
|
5853
6343
|
|
|
5854
6344
|
/** Add a sprite to the gl draw list, used by all gl draw functions
|
|
5855
|
-
* @param {
|
|
5856
|
-
* @param {
|
|
5857
|
-
* @param {
|
|
5858
|
-
* @param {
|
|
5859
|
-
* @param {
|
|
5860
|
-
* @param {
|
|
5861
|
-
* @param {
|
|
5862
|
-
* @param {
|
|
5863
|
-
* @param {
|
|
5864
|
-
* @param {
|
|
5865
|
-
* @param {
|
|
6345
|
+
* @param {number} x
|
|
6346
|
+
* @param {number} y
|
|
6347
|
+
* @param {number} sizeX
|
|
6348
|
+
* @param {number} sizeY
|
|
6349
|
+
* @param {number} [angle]
|
|
6350
|
+
* @param {number} [uv0X]
|
|
6351
|
+
* @param {number} [uv0Y]
|
|
6352
|
+
* @param {number} [uv1X]
|
|
6353
|
+
* @param {number} [uv1Y]
|
|
6354
|
+
* @param {number} [rgba=-1] - white is -1
|
|
6355
|
+
* @param {number} [rgbaAdditive=0] - black is 0
|
|
5866
6356
|
* @memberof WebGL */
|
|
5867
6357
|
function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
|
|
5868
6358
|
{
|
|
5869
6359
|
// flush if there is not enough room or if different blend mode
|
|
5870
|
-
if (
|
|
6360
|
+
if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
|
|
5871
6361
|
glFlush();
|
|
6362
|
+
glSetInstancedMode();
|
|
5872
6363
|
|
|
5873
|
-
|
|
6364
|
+
glPolyMode = false;
|
|
6365
|
+
let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
|
|
5874
6366
|
glPositionData[offset++] = x;
|
|
5875
6367
|
glPositionData[offset++] = y;
|
|
5876
6368
|
glPositionData[offset++] = sizeX;
|
|
@@ -5882,6 +6374,301 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
5882
6374
|
glColorData[offset++] = rgba;
|
|
5883
6375
|
glColorData[offset++] = rgbaAdditive;
|
|
5884
6376
|
glPositionData[offset++] = angle;
|
|
6377
|
+
}
|
|
6378
|
+
|
|
6379
|
+
/** Transform and add a polygon to the gl draw list
|
|
6380
|
+
* @param {Array} points - Array of Vector2 points
|
|
6381
|
+
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
6382
|
+
* @param {number} x
|
|
6383
|
+
* @param {number} y
|
|
6384
|
+
* @param {number} sx
|
|
6385
|
+
* @param {number} sy
|
|
6386
|
+
* @param {number} angle
|
|
6387
|
+
* @param {boolean} [tristrip] - should tristrip algorithm be used
|
|
6388
|
+
* @memberof WebGL */
|
|
6389
|
+
function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
6390
|
+
{
|
|
6391
|
+
const pointsOut = [];
|
|
6392
|
+
for (const p of points)
|
|
6393
|
+
{
|
|
6394
|
+
// transform the point
|
|
6395
|
+
const px = p.x*sx;
|
|
6396
|
+
const py = p.y*sy;
|
|
6397
|
+
const sa = Math.sin(-angle);
|
|
6398
|
+
const ca = Math.cos(-angle);
|
|
6399
|
+
pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
|
|
6400
|
+
}
|
|
6401
|
+
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
6402
|
+
glDrawPoints(drawPoints, rgba);
|
|
6403
|
+
}
|
|
6404
|
+
|
|
6405
|
+
/** Transform and add a polygon to the gl draw list
|
|
6406
|
+
* @param {Array} points - Array of Vector2 points
|
|
6407
|
+
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
6408
|
+
* @param {number} lineWidth - Width of the outline
|
|
6409
|
+
* @param {number} x
|
|
6410
|
+
* @param {number} y
|
|
6411
|
+
* @param {number} sx
|
|
6412
|
+
* @param {number} sy
|
|
6413
|
+
* @param {number} angle
|
|
6414
|
+
* @memberof WebGL */
|
|
6415
|
+
function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle)
|
|
6416
|
+
{
|
|
6417
|
+
const outlinePoints = glMakeOutline(points, lineWidth);
|
|
6418
|
+
glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
|
|
6419
|
+
}
|
|
6420
|
+
|
|
6421
|
+
/** Add a polygon to the gl draw list
|
|
6422
|
+
* @param {Array} points - Array of Vector2 points in triangle strip order
|
|
6423
|
+
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
6424
|
+
* @memberof WebGL */
|
|
6425
|
+
function glDrawPoints(points, rgba)
|
|
6426
|
+
{
|
|
6427
|
+
if (!glEnable || points.length < 3)
|
|
6428
|
+
return; // needs at least 3 points to have area
|
|
6429
|
+
|
|
6430
|
+
// add 2 degenerate verts if batching with existing polys to separate them
|
|
6431
|
+
const needsBridge = glPolyMode && glBatchCount > 0;
|
|
6432
|
+
const bridgeVerts = needsBridge ? 2 : 0;
|
|
6433
|
+
const vertCount = points.length + bridgeVerts;
|
|
6434
|
+
|
|
6435
|
+
// flush if there is not enough room or if different blend mode
|
|
6436
|
+
if (!glPolyMode || glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
6437
|
+
glFlush();
|
|
6438
|
+
glSetPolyMode();
|
|
6439
|
+
|
|
6440
|
+
let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
|
|
6441
|
+
|
|
6442
|
+
// add degenerate bridge if needed (repeat last vertex of previous poly, then first of new poly)
|
|
6443
|
+
if (needsBridge)
|
|
6444
|
+
{
|
|
6445
|
+
// repeat last vertex from previous batch (it's at offset - 3)
|
|
6446
|
+
const prevOffset = offset - 3;
|
|
6447
|
+
glPositionData[offset++] = glPositionData[prevOffset];
|
|
6448
|
+
glPositionData[offset++] = glPositionData[prevOffset + 1];
|
|
6449
|
+
glColorData[offset++] = glColorData[prevOffset + 2];
|
|
6450
|
+
|
|
6451
|
+
// repeat first vertex of new poly
|
|
6452
|
+
glPositionData[offset++] = points[0].x;
|
|
6453
|
+
glPositionData[offset++] = points[0].y;
|
|
6454
|
+
glColorData[offset++] = rgba;
|
|
6455
|
+
}
|
|
6456
|
+
|
|
6457
|
+
// write vertices - they're already in triangle strip order
|
|
6458
|
+
for (const point of points)
|
|
6459
|
+
{
|
|
6460
|
+
glPositionData[offset++] = point.x;
|
|
6461
|
+
glPositionData[offset++] = point.y;
|
|
6462
|
+
glColorData[offset++] = rgba;
|
|
6463
|
+
}
|
|
6464
|
+
glBatchCount += vertCount;
|
|
6465
|
+
}
|
|
6466
|
+
|
|
6467
|
+
// WebGL internal function to convert polygon to outline triangle strip
|
|
6468
|
+
function glMakeOutline(points, width)
|
|
6469
|
+
{
|
|
6470
|
+
if (points.length < 2)
|
|
6471
|
+
return [];
|
|
6472
|
+
|
|
6473
|
+
const halfWidth = width / 2;
|
|
6474
|
+
const strip = [];
|
|
6475
|
+
const n = points.length;
|
|
6476
|
+
const e = 1e-6;
|
|
6477
|
+
for (let i = 0; i < n; i++)
|
|
6478
|
+
{
|
|
6479
|
+
// for each vertex, calculate normal based on adjacent edges
|
|
6480
|
+
const prev = points[(i - 1 + n) % n];
|
|
6481
|
+
const curr = points[i];
|
|
6482
|
+
const next = points[(i + 1) % n];
|
|
6483
|
+
|
|
6484
|
+
// direction from previous to current
|
|
6485
|
+
const dx1 = curr.x - prev.x;
|
|
6486
|
+
const dy1 = curr.y - prev.y;
|
|
6487
|
+
const len1 = (dx1*dx1 + dy1*dy1)**.5;
|
|
6488
|
+
|
|
6489
|
+
// direction from current to next
|
|
6490
|
+
const dx2 = next.x - curr.x;
|
|
6491
|
+
const dy2 = next.y - curr.y;
|
|
6492
|
+
const len2 = (dx2*dx2 + dy2*dy2)**.5;
|
|
6493
|
+
|
|
6494
|
+
if (len1 < e && len2 < e)
|
|
6495
|
+
continue; // skip degenerate point
|
|
6496
|
+
|
|
6497
|
+
// calculate perpendicular normals for each edge
|
|
6498
|
+
const nx1 = len1 > e ? -dy1 / len1 : 0;
|
|
6499
|
+
const ny1 = len1 > e ? dx1 / len1 : 0;
|
|
6500
|
+
const nx2 = len2 > e ? -dy2 / len2 : 0;
|
|
6501
|
+
const ny2 = len2 > e ? dx2 / len2 : 0;
|
|
6502
|
+
|
|
6503
|
+
// average the normals for miter
|
|
6504
|
+
let nx = nx1 + nx2;
|
|
6505
|
+
let ny = ny1 + ny2;
|
|
6506
|
+
const nlen = (nx*nx + ny*ny)**.5;
|
|
6507
|
+
if (nlen < e)
|
|
6508
|
+
{
|
|
6509
|
+
// 180 degree turn - use perpendicular
|
|
6510
|
+
nx = nx1;
|
|
6511
|
+
ny = ny1;
|
|
6512
|
+
}
|
|
6513
|
+
else
|
|
6514
|
+
{
|
|
6515
|
+
// calculate miter length
|
|
6516
|
+
nx /= nlen;
|
|
6517
|
+
ny /= nlen;
|
|
6518
|
+
const dot = nx1 * nx + ny1 * ny;
|
|
6519
|
+
if (dot > e)
|
|
6520
|
+
{
|
|
6521
|
+
// scale normal by miter length
|
|
6522
|
+
const miterLength = 1 / dot;
|
|
6523
|
+
nx *= miterLength;
|
|
6524
|
+
ny *= miterLength;
|
|
6525
|
+
}
|
|
6526
|
+
}
|
|
6527
|
+
|
|
6528
|
+
// create inner and outer points along the normal
|
|
6529
|
+
const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
|
|
6530
|
+
const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
|
|
6531
|
+
strip.push(inner);
|
|
6532
|
+
strip.push(outer);
|
|
6533
|
+
}
|
|
6534
|
+
if (strip.length > 1)
|
|
6535
|
+
{
|
|
6536
|
+
// close the loop
|
|
6537
|
+
strip.push(strip[0]);
|
|
6538
|
+
strip.push(strip[1]);
|
|
6539
|
+
}
|
|
6540
|
+
return strip;
|
|
6541
|
+
}
|
|
6542
|
+
|
|
6543
|
+
// WebGL internal function to convert polys to tri strips
|
|
6544
|
+
function glPolyStrip(points)
|
|
6545
|
+
{
|
|
6546
|
+
// validate input
|
|
6547
|
+
if (points.length < 3)
|
|
6548
|
+
return [];
|
|
6549
|
+
|
|
6550
|
+
// cross product helper: (b-a) x (c-a)
|
|
6551
|
+
const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
6552
|
+
|
|
6553
|
+
// calculate signed area of polygon
|
|
6554
|
+
const signedArea = (poly)=>
|
|
6555
|
+
{
|
|
6556
|
+
let area = 0;
|
|
6557
|
+
for (let i = poly.length; i--;)
|
|
6558
|
+
{
|
|
6559
|
+
const j = (i+1) % poly.length;
|
|
6560
|
+
area += poly[i].cross(poly[j]);
|
|
6561
|
+
}
|
|
6562
|
+
return area;
|
|
6563
|
+
}
|
|
6564
|
+
|
|
6565
|
+
// ensure counter-clockwise winding
|
|
6566
|
+
if (signedArea(points) < 0)
|
|
6567
|
+
points = points.reverse();
|
|
6568
|
+
|
|
6569
|
+
// tolerance constants
|
|
6570
|
+
const e = 1e-10;
|
|
6571
|
+
|
|
6572
|
+
// check if point is inside triangle
|
|
6573
|
+
const pointInTriangle = (p, a, b, c)=>
|
|
6574
|
+
{
|
|
6575
|
+
const c1 = cross(a, b, p);
|
|
6576
|
+
const c2 = cross(b, c, p);
|
|
6577
|
+
const c3 = cross(c, a, p);
|
|
6578
|
+
const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
|
|
6579
|
+
const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
|
|
6580
|
+
return !(negative && positive);
|
|
6581
|
+
};
|
|
6582
|
+
|
|
6583
|
+
// ear clipping triangulation
|
|
6584
|
+
const indices = [];
|
|
6585
|
+
for (let i = 0; i < points.length; ++i)
|
|
6586
|
+
indices[i] = i;
|
|
6587
|
+
const triangles = [];
|
|
6588
|
+
let attempts = 0;
|
|
6589
|
+
const maxAttempts = points.length ** 2 + 100;
|
|
6590
|
+
while (indices.length > 3 && attempts++ < maxAttempts)
|
|
6591
|
+
{
|
|
6592
|
+
let foundEar = false;
|
|
6593
|
+
for (let i = indices.length; --i;)
|
|
6594
|
+
{
|
|
6595
|
+
const i0 = indices[(i + indices.length - 1) % indices.length];
|
|
6596
|
+
const i1 = indices[i];
|
|
6597
|
+
const i2 = indices[(i + 1) % indices.length];
|
|
6598
|
+
const a = points[i0], b = points[i1], c = points[i2];
|
|
6599
|
+
|
|
6600
|
+
// check if convex
|
|
6601
|
+
if (cross(a, b, c) < e)
|
|
6602
|
+
continue;
|
|
6603
|
+
|
|
6604
|
+
// check if any other point is inside
|
|
6605
|
+
let hasInside = false;
|
|
6606
|
+
for (let j = 0; j < indices.length; j++)
|
|
6607
|
+
{
|
|
6608
|
+
const k = indices[j];
|
|
6609
|
+
if (k === i0 || k === i1 || k === i2)
|
|
6610
|
+
continue;
|
|
6611
|
+
const p = points[k];
|
|
6612
|
+
hasInside = pointInTriangle(p, a, b, c);
|
|
6613
|
+
if (hasInside)
|
|
6614
|
+
break;
|
|
6615
|
+
}
|
|
6616
|
+
if (hasInside)
|
|
6617
|
+
continue;
|
|
6618
|
+
|
|
6619
|
+
// found valid ear
|
|
6620
|
+
triangles.push([i0, i1, i2]);
|
|
6621
|
+
indices.splice(i, 1);
|
|
6622
|
+
foundEar = true;
|
|
6623
|
+
break;
|
|
6624
|
+
}
|
|
6625
|
+
|
|
6626
|
+
// fallback for degenerate cases
|
|
6627
|
+
if (!foundEar)
|
|
6628
|
+
{
|
|
6629
|
+
let worstIndex = -1, worstValue = Infinity;
|
|
6630
|
+
for (let i = indices.length; --i;)
|
|
6631
|
+
{
|
|
6632
|
+
const i0 = indices[(i + indices.length - 1) % indices.length];
|
|
6633
|
+
const i1 = indices[i];
|
|
6634
|
+
const i2 = indices[(i + 1) % indices.length];
|
|
6635
|
+
const value = abs(cross(points[i0], points[i1], points[i2]));
|
|
6636
|
+
if (value < worstValue)
|
|
6637
|
+
{
|
|
6638
|
+
worstValue = value;
|
|
6639
|
+
worstIndex = i;
|
|
6640
|
+
}
|
|
6641
|
+
}
|
|
6642
|
+
if (worstIndex < 0)
|
|
6643
|
+
break;
|
|
6644
|
+
|
|
6645
|
+
const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
|
|
6646
|
+
const i1 = indices[worstIndex];
|
|
6647
|
+
const i2 = indices[(worstIndex + 1) % indices.length];
|
|
6648
|
+
triangles.push([i0, i1, i2]);
|
|
6649
|
+
indices.splice(worstIndex, 1);
|
|
6650
|
+
}
|
|
6651
|
+
}
|
|
6652
|
+
|
|
6653
|
+
// add final triangle
|
|
6654
|
+
if (indices.length === 3)
|
|
6655
|
+
triangles.push([indices[0], indices[1], indices[2]]);
|
|
6656
|
+
if (!triangles.length)
|
|
6657
|
+
return [];
|
|
6658
|
+
|
|
6659
|
+
// convert triangles to triangle strip with degenerate connectors
|
|
6660
|
+
const strip = [];
|
|
6661
|
+
let [a0, b0, c0] = triangles[0];
|
|
6662
|
+
strip.push(points[a0], points[b0], points[c0]);
|
|
6663
|
+
for (let i = 1; i < triangles.length; i++)
|
|
6664
|
+
{
|
|
6665
|
+
// add degenerate bridge from last vertex to first of new triangle
|
|
6666
|
+
const [a, b, c] = triangles[i];
|
|
6667
|
+
strip.push(points[c0], points[a]);
|
|
6668
|
+
strip.push(points[a], points[b], points[c]);
|
|
6669
|
+
c0 = c;
|
|
6670
|
+
}
|
|
6671
|
+
return strip;
|
|
5885
6672
|
}
|
|
5886
6673
|
/**
|
|
5887
6674
|
* LittleJS Newgrounds API
|
|
@@ -5906,11 +6693,11 @@ let newgrounds;
|
|
|
5906
6693
|
class NewgroundsMedal extends Medal
|
|
5907
6694
|
{
|
|
5908
6695
|
/** Create a newgrounds medal object and adds it to the list of medals
|
|
5909
|
-
* @param {
|
|
5910
|
-
* @param {
|
|
5911
|
-
* @param {
|
|
5912
|
-
* @param {
|
|
5913
|
-
* @param {
|
|
6696
|
+
* @param {number} id - The unique identifier of the medal
|
|
6697
|
+
* @param {string} name - Name of the medal
|
|
6698
|
+
* @param {string} [description] - Description of the medal
|
|
6699
|
+
* @param {string} [icon] - Icon for the medal
|
|
6700
|
+
* @param {string} [src] - Image location for the medal
|
|
5914
6701
|
*/
|
|
5915
6702
|
constructor(id, name, description, icon, src)
|
|
5916
6703
|
{ super(id, name, description, icon, src); }
|
|
@@ -6002,7 +6789,7 @@ class NewgroundsPlugin
|
|
|
6002
6789
|
* @param {number} id - The scoreboard id
|
|
6003
6790
|
* @param {string} [user] - A user's id or name
|
|
6004
6791
|
* @param {number} [social] - If true, only social scores will be loaded
|
|
6005
|
-
* @param {number} [skip] - Number of scores to skip
|
|
6792
|
+
* @param {number} [skip] - Number of scores to skip over
|
|
6006
6793
|
* @param {number} [limit] - Number of scores to include in the list
|
|
6007
6794
|
* @return {Object} - The response JSON object
|
|
6008
6795
|
*/
|
|
@@ -6090,6 +6877,13 @@ class PostProcessPlugin
|
|
|
6090
6877
|
postProcess = this;
|
|
6091
6878
|
|
|
6092
6879
|
if (headlessMode) return;
|
|
6880
|
+
|
|
6881
|
+
if (!glEnable)
|
|
6882
|
+
{
|
|
6883
|
+
console.warn('PostProcessPlugin: WebGL not enabled!');
|
|
6884
|
+
return;
|
|
6885
|
+
}
|
|
6886
|
+
|
|
6093
6887
|
if (!shaderCode) // default shader pass through
|
|
6094
6888
|
shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
|
|
6095
6889
|
|
|
@@ -6218,16 +7012,16 @@ class ZzFXMusic extends Sound
|
|
|
6218
7012
|
if (!soundEnable || headlessMode) return;
|
|
6219
7013
|
this.randomness = 0;
|
|
6220
7014
|
this.sampleChannels = zzfxM(...zzfxMusic);
|
|
6221
|
-
this.sampleRate =
|
|
7015
|
+
this.sampleRate = audioDefaultSampleRate;
|
|
6222
7016
|
}
|
|
6223
7017
|
|
|
6224
|
-
/** Play the music
|
|
6225
|
-
* @param {number} [volume
|
|
6226
|
-
* @param {boolean} [loop] -
|
|
7018
|
+
/** Play the music that loops by default
|
|
7019
|
+
* @param {number} [volume] - Volume to play the music at
|
|
7020
|
+
* @param {boolean} [loop] - Should the music loop?
|
|
6227
7021
|
* @return {AudioBufferSourceNode} - The audio source node
|
|
6228
7022
|
*/
|
|
6229
|
-
playMusic(volume, loop=
|
|
6230
|
-
{ return super.play(undefined, volume, 1,
|
|
7023
|
+
playMusic(volume=1, loop=true)
|
|
7024
|
+
{ return super.play(undefined, volume, 1, 0, loop); }
|
|
6231
7025
|
}
|
|
6232
7026
|
|
|
6233
7027
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -6261,7 +7055,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
6261
7055
|
let panning = 0;
|
|
6262
7056
|
let hasMore = 1;
|
|
6263
7057
|
let sampleCache = {};
|
|
6264
|
-
let beatLength =
|
|
7058
|
+
let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
|
|
6265
7059
|
|
|
6266
7060
|
// for each channel in order until there are no more
|
|
6267
7061
|
for (; hasMore; channelIndex++) {
|
|
@@ -6280,15 +7074,15 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
6280
7074
|
// get next offset, use the length of first channel
|
|
6281
7075
|
nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
|
|
6282
7076
|
// for each beat in pattern, plus one extra if end of sequence
|
|
6283
|
-
isSequenceEnd = sequenceIndex
|
|
7077
|
+
isSequenceEnd = sequenceIndex === sequence.length - 1;
|
|
6284
7078
|
for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
|
|
6285
7079
|
|
|
6286
7080
|
// <channel-note>
|
|
6287
7081
|
note = patternChannel[i];
|
|
6288
7082
|
|
|
6289
7083
|
// stop if end, different instrument or new note
|
|
6290
|
-
stop = i
|
|
6291
|
-
instrument
|
|
7084
|
+
stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
|
|
7085
|
+
instrument !== (patternChannel[0] || 0) || note | 0;
|
|
6292
7086
|
|
|
6293
7087
|
// fill buffer with samples for previous beat, most cpu intensive part
|
|
6294
7088
|
for (j = 0; j < beatLength && notFirstBeat;
|
|
@@ -6373,11 +7167,11 @@ class UISystemPlugin
|
|
|
6373
7167
|
/** @property {Color} - Default text color for UI elements */
|
|
6374
7168
|
this.defaultTextColor = BLACK;
|
|
6375
7169
|
/** @property {Color} - Default button color for UI elements */
|
|
6376
|
-
this.defaultButtonColor = hsl(0,0,.
|
|
7170
|
+
this.defaultButtonColor = hsl(0,0,.7);
|
|
6377
7171
|
/** @property {Color} - Default hover color for UI elements */
|
|
6378
|
-
this.defaultHoverColor = hsl(0,0,.
|
|
7172
|
+
this.defaultHoverColor = hsl(0,0,.9);
|
|
6379
7173
|
/** @property {Color} - Default color for disabled UI elements */
|
|
6380
|
-
this.defaultDisabledColor = hsl(0,0,.
|
|
7174
|
+
this.defaultDisabledColor = hsl(0,0,.3);
|
|
6381
7175
|
/** @property {number} - Default line width for UI elements */
|
|
6382
7176
|
this.defaultLineWidth = 4;
|
|
6383
7177
|
/** @property {number} - Default rounded rect corner radius for UI elements */
|
|
@@ -6397,16 +7191,16 @@ class UISystemPlugin
|
|
|
6397
7191
|
|
|
6398
7192
|
engineAddPlugin(uiUpdate, uiRender);
|
|
6399
7193
|
|
|
6400
|
-
function updateInvisible(o)
|
|
6401
|
-
{
|
|
6402
|
-
for(const c of o.children)
|
|
6403
|
-
updateInvisible(c);
|
|
6404
|
-
o.updateInvisible();
|
|
6405
|
-
}
|
|
6406
|
-
|
|
6407
7194
|
// setup recursive update and render
|
|
6408
7195
|
function uiUpdate()
|
|
6409
7196
|
{
|
|
7197
|
+
function updateInvisibleObject(o)
|
|
7198
|
+
{
|
|
7199
|
+
// update invisible objects
|
|
7200
|
+
for (const c of o.children)
|
|
7201
|
+
updateInvisibleObject(c);
|
|
7202
|
+
o.updateInvisible();
|
|
7203
|
+
}
|
|
6410
7204
|
function updateObject(o)
|
|
6411
7205
|
{
|
|
6412
7206
|
if (o.visible)
|
|
@@ -6415,12 +7209,12 @@ class UISystemPlugin
|
|
|
6415
7209
|
if (o.parent)
|
|
6416
7210
|
o.pos = o.localPos.add(o.parent.pos);
|
|
6417
7211
|
// update in reverse order to detect mouse enter/leave
|
|
6418
|
-
for(let i=o.children.length; i--;)
|
|
7212
|
+
for (let i=o.children.length; i--;)
|
|
6419
7213
|
updateObject(o.children[i]);
|
|
6420
7214
|
o.update();
|
|
6421
7215
|
}
|
|
6422
7216
|
else
|
|
6423
|
-
|
|
7217
|
+
updateInvisibleObject(o);
|
|
6424
7218
|
}
|
|
6425
7219
|
uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
|
|
6426
7220
|
}
|
|
@@ -6433,7 +7227,7 @@ class UISystemPlugin
|
|
|
6433
7227
|
if (o.parent)
|
|
6434
7228
|
o.pos = o.localPos.add(o.parent.pos);
|
|
6435
7229
|
o.render();
|
|
6436
|
-
for(const c of o.children)
|
|
7230
|
+
for (const c of o.children)
|
|
6437
7231
|
renderObject(c);
|
|
6438
7232
|
}
|
|
6439
7233
|
uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
|
|
@@ -6446,7 +7240,7 @@ class UISystemPlugin
|
|
|
6446
7240
|
* @param {Color} [color=uiSystem.defaultColor]
|
|
6447
7241
|
* @param {number} [lineWidth=uiSystem.defaultLineWidth]
|
|
6448
7242
|
* @param {Color} [lineColor=uiSystem.defaultLineColor]
|
|
6449
|
-
* @param {number} [
|
|
7243
|
+
* @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
|
|
6450
7244
|
drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
|
|
6451
7245
|
{
|
|
6452
7246
|
const context = uiSystem.uiContext;
|
|
@@ -6490,7 +7284,7 @@ class UISystemPlugin
|
|
|
6490
7284
|
* @param {boolean} [mirror] */
|
|
6491
7285
|
drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false)
|
|
6492
7286
|
{
|
|
6493
|
-
drawTile(pos, size, tileInfo, color, angle, mirror,
|
|
7287
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, CLEAR_BLACK, false, true, uiSystem.uiContext);
|
|
6494
7288
|
}
|
|
6495
7289
|
|
|
6496
7290
|
/** Draw text to the UI context
|
|
@@ -6521,32 +7315,40 @@ class UIObject
|
|
|
6521
7315
|
constructor(pos=vec2(), size=vec2())
|
|
6522
7316
|
{
|
|
6523
7317
|
/** @property {Vector2} - Local position of the object */
|
|
6524
|
-
this.localPos
|
|
7318
|
+
this.localPos = pos.copy();
|
|
6525
7319
|
/** @property {Vector2} - Screen space position of the object */
|
|
6526
|
-
this.pos
|
|
7320
|
+
this.pos = pos.copy();
|
|
6527
7321
|
/** @property {Vector2} - Screen space size of the object */
|
|
6528
|
-
this.size
|
|
6529
|
-
/** @property {Color} -
|
|
6530
|
-
this.color
|
|
6531
|
-
/** @property {
|
|
6532
|
-
this.
|
|
6533
|
-
/** @property {Color} -
|
|
7322
|
+
this.size = size.copy();
|
|
7323
|
+
/** @property {Color} - Color of the object */
|
|
7324
|
+
this.color = uiSystem.defaultColor;
|
|
7325
|
+
/** @property {string} - Text for this ui object */
|
|
7326
|
+
this.text = undefined;
|
|
7327
|
+
/** @property {Color} - Color when disabled */
|
|
7328
|
+
this.disabledColor = uiSystem.defaultDisabledColor;
|
|
7329
|
+
/** @property {boolean} - Is this object disabled? */
|
|
7330
|
+
this.disabled = false;
|
|
7331
|
+
/** @property {Color} - Color for text */
|
|
7332
|
+
this.textColor = uiSystem.defaultTextColor;
|
|
7333
|
+
/** @property {Color} - Color used when hovering over the object */
|
|
6534
7334
|
this.hoverColor = uiSystem.defaultHoverColor;
|
|
6535
|
-
/** @property {Color} -
|
|
6536
|
-
this.lineColor
|
|
6537
|
-
/** @property {number} -
|
|
6538
|
-
this.lineWidth
|
|
6539
|
-
/** @property {
|
|
6540
|
-
this.
|
|
6541
|
-
/** @property {
|
|
6542
|
-
this.
|
|
6543
|
-
/** @property {
|
|
6544
|
-
this.
|
|
6545
|
-
/** @property {
|
|
6546
|
-
this.
|
|
6547
|
-
/** @property {UIObject} - this object's
|
|
6548
|
-
this.
|
|
6549
|
-
/** @property {
|
|
7335
|
+
/** @property {Color} - Color for line drawing */
|
|
7336
|
+
this.lineColor = uiSystem.defaultLineColor;
|
|
7337
|
+
/** @property {number} - Width for line drawing */
|
|
7338
|
+
this.lineWidth = uiSystem.defaultLineWidth;
|
|
7339
|
+
/** @property {number} - Corner radius for rounded rects */
|
|
7340
|
+
this.cornerRadius = uiSystem.defaultCornerRadius;
|
|
7341
|
+
/** @property {string} - Font for this objecct */
|
|
7342
|
+
this.font = uiSystem.defaultFont;
|
|
7343
|
+
/** @property {number} - Override for text height */
|
|
7344
|
+
this.textHeight = undefined;
|
|
7345
|
+
/** @property {boolean} - Should this object be drawn */
|
|
7346
|
+
this.visible = true;
|
|
7347
|
+
/** @property {Array<UIObject>} - A list of this object's children */
|
|
7348
|
+
this.children = [];
|
|
7349
|
+
/** @property {UIObject} - This object's parent, position is in parent space */
|
|
7350
|
+
this.parent = undefined;
|
|
7351
|
+
/** @property {number} - Extra size added to make small buttons easier to touch on mobile devices */
|
|
6550
7352
|
this.extraTouchSize = 0;
|
|
6551
7353
|
/** @property {Sound} - Sound when interactive element is pressed */
|
|
6552
7354
|
this.soundPress = uiSystem.defaultSoundPress;
|
|
@@ -6578,7 +7380,7 @@ class UIObject
|
|
|
6578
7380
|
*/
|
|
6579
7381
|
removeChild(child)
|
|
6580
7382
|
{
|
|
6581
|
-
ASSERT(child.parent
|
|
7383
|
+
ASSERT(child.parent === this && this.children.includes(child));
|
|
6582
7384
|
this.children.splice(this.children.indexOf(child), 1);
|
|
6583
7385
|
child.parent = undefined;
|
|
6584
7386
|
}
|
|
@@ -6631,7 +7433,7 @@ class UIObject
|
|
|
6631
7433
|
this.mouseIsHeld = false;
|
|
6632
7434
|
}
|
|
6633
7435
|
|
|
6634
|
-
if (this.mouseIsOver
|
|
7436
|
+
if (this.mouseIsOver !== mouseWasOver)
|
|
6635
7437
|
this.mouseIsOver ? this.onEnter() : this.onLeave();
|
|
6636
7438
|
}
|
|
6637
7439
|
|
|
@@ -6642,7 +7444,7 @@ class UIObject
|
|
|
6642
7444
|
uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor, this.cornerRadius);
|
|
6643
7445
|
}
|
|
6644
7446
|
|
|
6645
|
-
/** Special update
|
|
7447
|
+
/** Special update when object is not visible */
|
|
6646
7448
|
updateInvisible()
|
|
6647
7449
|
{
|
|
6648
7450
|
// reset input state when not visible
|
|
@@ -6650,28 +7452,22 @@ class UIObject
|
|
|
6650
7452
|
}
|
|
6651
7453
|
|
|
6652
7454
|
/** Called when the mouse enters the object */
|
|
6653
|
-
onEnter()
|
|
6654
|
-
{}
|
|
7455
|
+
onEnter() {}
|
|
6655
7456
|
|
|
6656
7457
|
/** Called when the mouse leaves the object */
|
|
6657
|
-
onLeave()
|
|
6658
|
-
{}
|
|
7458
|
+
onLeave() {}
|
|
6659
7459
|
|
|
6660
7460
|
/** Called when the mouse is pressed while over the object */
|
|
6661
|
-
onPress()
|
|
6662
|
-
{}
|
|
7461
|
+
onPress() {}
|
|
6663
7462
|
|
|
6664
7463
|
/** Called when the mouse is released while over the object */
|
|
6665
|
-
onRelease()
|
|
6666
|
-
{}
|
|
7464
|
+
onRelease() {}
|
|
6667
7465
|
|
|
6668
7466
|
/** Called when user clicks on this object */
|
|
6669
|
-
onClick()
|
|
6670
|
-
{}
|
|
7467
|
+
onClick() {}
|
|
6671
7468
|
|
|
6672
7469
|
/** Called when the state of this object changes */
|
|
6673
|
-
onChange()
|
|
6674
|
-
{}
|
|
7470
|
+
onChange() {}
|
|
6675
7471
|
};
|
|
6676
7472
|
|
|
6677
7473
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -6692,13 +7488,13 @@ class UIText extends UIObject
|
|
|
6692
7488
|
{
|
|
6693
7489
|
super(pos, size);
|
|
6694
7490
|
|
|
6695
|
-
|
|
7491
|
+
// set properties
|
|
6696
7492
|
this.text = text;
|
|
6697
|
-
/** @property {string} */
|
|
6698
7493
|
this.align = align;
|
|
7494
|
+
this.font = font;
|
|
6699
7495
|
|
|
6700
|
-
|
|
6701
|
-
this.lineWidth = 0;
|
|
7496
|
+
// make text not outlined by default
|
|
7497
|
+
this.lineWidth = 0;
|
|
6702
7498
|
}
|
|
6703
7499
|
render()
|
|
6704
7500
|
{
|
|
@@ -6725,13 +7521,14 @@ class UITile extends UIObject
|
|
|
6725
7521
|
constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
|
|
6726
7522
|
{
|
|
6727
7523
|
super(pos, size);
|
|
6728
|
-
|
|
6729
7524
|
/** @property {TileInfo} - Tile image to use */
|
|
6730
7525
|
this.tileInfo = tileInfo;
|
|
6731
7526
|
/** @property {number} - Angle to rotate in radians */
|
|
6732
7527
|
this.angle = angle;
|
|
6733
7528
|
/** @property {boolean} - Should it be mirrored? */
|
|
6734
7529
|
this.mirror = mirror;
|
|
7530
|
+
|
|
7531
|
+
// set properties
|
|
6735
7532
|
this.color = color;
|
|
6736
7533
|
}
|
|
6737
7534
|
render()
|
|
@@ -6757,22 +7554,20 @@ class UIButton extends UIObject
|
|
|
6757
7554
|
{
|
|
6758
7555
|
super(pos, size);
|
|
6759
7556
|
|
|
6760
|
-
|
|
7557
|
+
// set properties
|
|
6761
7558
|
this.text = text;
|
|
6762
|
-
/** @property {Color} */
|
|
6763
|
-
this.disabledColor = uiSystem.defaultDisabledColor;
|
|
6764
|
-
/** @property {boolean} */
|
|
6765
|
-
this.disabled = false;
|
|
6766
|
-
this.interactive = true;
|
|
6767
7559
|
this.color = color;
|
|
7560
|
+
this.interactive = true;
|
|
6768
7561
|
}
|
|
6769
7562
|
render()
|
|
6770
7563
|
{
|
|
7564
|
+
// draw the button
|
|
6771
7565
|
const lineColor = this.mouseIsHeld && !this.disabled ? this.color : this.lineColor;
|
|
6772
7566
|
const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
|
|
6773
7567
|
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
|
|
6774
7568
|
|
|
6775
|
-
|
|
7569
|
+
// draw the text
|
|
7570
|
+
const textScale = .8; // scale text to fit
|
|
6776
7571
|
const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
|
|
6777
7572
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
6778
7573
|
this.textColor, 0, undefined, this.align, this.font);
|
|
@@ -6790,13 +7585,18 @@ class UICheckbox extends UIObject
|
|
|
6790
7585
|
* @param {Vector2} [pos]
|
|
6791
7586
|
* @param {Vector2} [size]
|
|
6792
7587
|
* @param {boolean} [checked]
|
|
7588
|
+
* @param {string} [text]
|
|
7589
|
+
* @param {Color} [color=uiSystem.defaultButtonColor]
|
|
6793
7590
|
*/
|
|
6794
|
-
constructor(pos, size, checked=false)
|
|
7591
|
+
constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
|
|
6795
7592
|
{
|
|
6796
7593
|
super(pos, size);
|
|
6797
|
-
|
|
6798
|
-
/** @property {boolean} */
|
|
7594
|
+
/** @property {boolean} - Current percentage value of this scrollbar 0-1 */
|
|
6799
7595
|
this.checked = checked;
|
|
7596
|
+
|
|
7597
|
+
// set properties
|
|
7598
|
+
this.text = text;
|
|
7599
|
+
this.color = color;
|
|
6800
7600
|
this.interactive = true;
|
|
6801
7601
|
}
|
|
6802
7602
|
onClick()
|
|
@@ -6806,14 +7606,24 @@ class UICheckbox extends UIObject
|
|
|
6806
7606
|
}
|
|
6807
7607
|
render()
|
|
6808
7608
|
{
|
|
6809
|
-
const color = this.mouseIsOver? this.hoverColor : this.color;
|
|
7609
|
+
const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
|
|
6810
7610
|
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor, this.cornerRadius);
|
|
6811
7611
|
if (this.checked)
|
|
6812
7612
|
{
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
7613
|
+
const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
|
|
7614
|
+
const length = lerp(1, 2**.5/2, p) / 2;
|
|
7615
|
+
let s = this.size.scale(length);
|
|
7616
|
+
uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))), this.pos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
|
|
7617
|
+
uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
|
|
6816
7618
|
}
|
|
7619
|
+
|
|
7620
|
+
// draw the text to the right side of the checkbox
|
|
7621
|
+
const textScale = .8; // scale text to fit
|
|
7622
|
+
const gapScale = .55;
|
|
7623
|
+
const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
|
|
7624
|
+
const pos = this.pos.add(vec2(this.size.x*gapScale,0));
|
|
7625
|
+
uiSystem.drawText(this.text, pos, textSize,
|
|
7626
|
+
this.textColor, 0, undefined, 'left', this.font);
|
|
6817
7627
|
}
|
|
6818
7628
|
}
|
|
6819
7629
|
|
|
@@ -6836,43 +7646,51 @@ class UIScrollbar extends UIObject
|
|
|
6836
7646
|
{
|
|
6837
7647
|
super(pos, size);
|
|
6838
7648
|
|
|
6839
|
-
/** @property {number} */
|
|
7649
|
+
/** @property {number} - Current percentage value of this scrollbar 0-1 */
|
|
6840
7650
|
this.value = value;
|
|
6841
|
-
/** @property {
|
|
6842
|
-
this.text = text;
|
|
6843
|
-
/** @property {Color} */
|
|
7651
|
+
/** @property {Color} - Color for the handle part of the scrollbar */
|
|
6844
7652
|
this.handleColor = handleColor;
|
|
7653
|
+
|
|
7654
|
+
// set properties
|
|
7655
|
+
this.text = text;
|
|
6845
7656
|
this.color = color;
|
|
6846
7657
|
this.interactive = true;
|
|
6847
7658
|
}
|
|
6848
7659
|
update()
|
|
6849
7660
|
{
|
|
6850
7661
|
super.update();
|
|
6851
|
-
if (this.mouseIsHeld)
|
|
7662
|
+
if (this.mouseIsHeld && this.interactive)
|
|
6852
7663
|
{
|
|
7664
|
+
// check if value changed
|
|
6853
7665
|
const handleSize = vec2(this.size.y);
|
|
6854
7666
|
const handleWidth = this.size.x - handleSize.x;
|
|
6855
7667
|
const p1 = this.pos.x - handleWidth/2;
|
|
6856
7668
|
const p2 = this.pos.x + handleWidth/2;
|
|
6857
7669
|
const oldValue = this.value;
|
|
6858
7670
|
this.value = percent(mousePosScreen.x, p1, p2);
|
|
6859
|
-
this.value
|
|
7671
|
+
this.value === oldValue || this.onChange();
|
|
6860
7672
|
}
|
|
6861
7673
|
}
|
|
6862
7674
|
render()
|
|
6863
7675
|
{
|
|
6864
|
-
|
|
6865
|
-
const
|
|
7676
|
+
// draw the scrollbar background
|
|
7677
|
+
const lineColor = this.interactive && this.mouseIsHeld && !this.disabled ?
|
|
7678
|
+
this.color : this.lineColor;
|
|
7679
|
+
const color = this.disabled ? this.disabledColor :
|
|
7680
|
+
this.interactive && this.mouseIsHeld ? this.hoverColor : this.color;
|
|
6866
7681
|
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
|
|
6867
7682
|
|
|
7683
|
+
// draw the scrollbar handle
|
|
6868
7684
|
const handleSize = vec2(this.size.y);
|
|
6869
7685
|
const handleWidth = this.size.x - handleSize.x;
|
|
6870
7686
|
const p1 = this.pos.x - handleWidth/2;
|
|
6871
7687
|
const p2 = this.pos.x + handleWidth/2;
|
|
6872
7688
|
const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
|
|
6873
|
-
const
|
|
6874
|
-
|
|
7689
|
+
const handleColor = this.disabled ? this.disabledColor :
|
|
7690
|
+
this.interactive && this.mouseIsHeld ? this.color : this.handleColor;
|
|
7691
|
+
uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
|
|
6875
7692
|
|
|
7693
|
+
// draw the text on the scrollbar
|
|
6876
7694
|
const textScale = .8; // scale text to fit in scrollbar
|
|
6877
7695
|
const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
|
|
6878
7696
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
@@ -6940,7 +7758,7 @@ class Box2dObject extends EngineObject
|
|
|
6940
7758
|
bodyDef.set_angle(-angle);
|
|
6941
7759
|
this.body = box2d.world.CreateBody(bodyDef);
|
|
6942
7760
|
this.body.object = this;
|
|
6943
|
-
this.
|
|
7761
|
+
this.lineColor = BLACK;
|
|
6944
7762
|
}
|
|
6945
7763
|
|
|
6946
7764
|
/** Destroy this object and it's physics body */
|
|
@@ -6967,27 +7785,27 @@ class Box2dObject extends EngineObject
|
|
|
6967
7785
|
if (this.tileInfo)
|
|
6968
7786
|
super.render();
|
|
6969
7787
|
else
|
|
6970
|
-
this.drawFixtures(this.color, this.
|
|
7788
|
+
this.drawFixtures(this.color, this.lineColor, this.lineWidth);
|
|
6971
7789
|
}
|
|
6972
7790
|
|
|
6973
7791
|
/** Render debug info */
|
|
6974
7792
|
renderDebugInfo()
|
|
6975
7793
|
{
|
|
6976
7794
|
const isAsleep = !this.getIsAwake();
|
|
6977
|
-
const isStatic = this.getBodyType()
|
|
7795
|
+
const isStatic = this.getBodyType() === box2d.bodyTypeStatic;
|
|
6978
7796
|
const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
|
|
6979
7797
|
this.drawFixtures(color);
|
|
6980
7798
|
}
|
|
6981
7799
|
|
|
6982
7800
|
/** Draws all this object's fixtures
|
|
6983
7801
|
* @param {Color} [color]
|
|
6984
|
-
* @param {Color} [
|
|
7802
|
+
* @param {Color} [lineColor]
|
|
6985
7803
|
* @param {number} [lineWidth]
|
|
6986
7804
|
* @param {CanvasRenderingContext2D} [context] */
|
|
6987
|
-
drawFixtures(color=WHITE,
|
|
7805
|
+
drawFixtures(color=WHITE, lineColor, lineWidth=.1, context)
|
|
6988
7806
|
{
|
|
6989
7807
|
this.getFixtureList().forEach(fixture=>
|
|
6990
|
-
box2d.drawFixture(fixture, this.pos, this.angle, color,
|
|
7808
|
+
box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context));
|
|
6991
7809
|
}
|
|
6992
7810
|
|
|
6993
7811
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -8442,7 +9260,7 @@ class Box2dPlugin
|
|
|
8442
9260
|
queryCallback.ReportFixture = function(fixturePointer)
|
|
8443
9261
|
{
|
|
8444
9262
|
const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
|
|
8445
|
-
if (dynamicOnly && fixture.GetBody().GetType()
|
|
9263
|
+
if (dynamicOnly && fixture.GetBody().GetType() !== box2d.instance.b2_dynamicBody)
|
|
8446
9264
|
return true; // continue getting results
|
|
8447
9265
|
if (!fixture.TestPoint(box2d.vec2dTo(pos)))
|
|
8448
9266
|
return true; // continue getting results
|
|
@@ -8468,10 +9286,10 @@ class Box2dPlugin
|
|
|
8468
9286
|
* @param {Vector2} pos
|
|
8469
9287
|
* @param {number} angle
|
|
8470
9288
|
* @param {Color} [color]
|
|
8471
|
-
* @param {Color} [
|
|
9289
|
+
* @param {Color} [lineColor]
|
|
8472
9290
|
* @param {number} [lineWidth]
|
|
8473
9291
|
* @param {CanvasRenderingContext2D} [context] */
|
|
8474
|
-
drawFixture(fixture, pos, angle, color=WHITE,
|
|
9292
|
+
drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
|
|
8475
9293
|
{
|
|
8476
9294
|
const shape = box2d.castObjectType(fixture.GetShape());
|
|
8477
9295
|
switch (shape.GetType())
|
|
@@ -8481,101 +9299,25 @@ class Box2dPlugin
|
|
|
8481
9299
|
let points = [];
|
|
8482
9300
|
for (let i=shape.GetVertexCount(); i--;)
|
|
8483
9301
|
points.push(box2d.vec2From(shape.GetVertex(i)));
|
|
8484
|
-
|
|
9302
|
+
drawPoly(points, color, lineWidth, lineColor, pos, angle);
|
|
8485
9303
|
break;
|
|
8486
9304
|
}
|
|
8487
9305
|
case box2d.instance.b2Shape.e_circle:
|
|
8488
9306
|
{
|
|
8489
9307
|
const radius = shape.get_m_radius();
|
|
8490
|
-
|
|
9308
|
+
drawCircle(pos, radius, color, lineWidth, lineColor);
|
|
8491
9309
|
break;
|
|
8492
9310
|
}
|
|
8493
9311
|
case box2d.instance.b2Shape.e_edge:
|
|
8494
9312
|
{
|
|
8495
9313
|
const v1 = box2d.vec2From(shape.get_m_vertex1());
|
|
8496
9314
|
const v2 = box2d.vec2From(shape.get_m_vertex2());
|
|
8497
|
-
|
|
9315
|
+
drawLine(v1, v2, lineWidth, lineColor, pos, angle);
|
|
8498
9316
|
break;
|
|
8499
9317
|
}
|
|
8500
9318
|
}
|
|
8501
9319
|
}
|
|
8502
9320
|
|
|
8503
|
-
/** draws a circle
|
|
8504
|
-
* @param {Vector2} pos
|
|
8505
|
-
* @param {number} radius
|
|
8506
|
-
* @param {Color} [color]
|
|
8507
|
-
* @param {Color} [outlineColor]
|
|
8508
|
-
* @param {number} [lineWidth]
|
|
8509
|
-
* @param {CanvasRenderingContext2D} [context] */
|
|
8510
|
-
drawCircle(pos, radius, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=drawContext)
|
|
8511
|
-
{
|
|
8512
|
-
drawCanvas2D(pos, vec2(1), 0, 0, context=>
|
|
8513
|
-
{
|
|
8514
|
-
context.beginPath();
|
|
8515
|
-
context.arc(0, 0, radius, 0, 9);
|
|
8516
|
-
box2d.drawFillStroke(color, outlineColor, lineWidth, context);
|
|
8517
|
-
}, 0, context);
|
|
8518
|
-
}
|
|
8519
|
-
|
|
8520
|
-
/** draws a polygon
|
|
8521
|
-
* @param {Vector2} pos
|
|
8522
|
-
* @param {number} angle
|
|
8523
|
-
* @param {Array<Vector2>} points
|
|
8524
|
-
* @param {Color} [color]
|
|
8525
|
-
* @param {Color} [outlineColor]
|
|
8526
|
-
* @param {number} [lineWidth]
|
|
8527
|
-
* @param {CanvasRenderingContext2D} [context] */
|
|
8528
|
-
drawPoly(pos, angle, points, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=drawContext)
|
|
8529
|
-
{
|
|
8530
|
-
drawCanvas2D(pos, vec2(1), angle, 0, context=>
|
|
8531
|
-
{
|
|
8532
|
-
context.beginPath();
|
|
8533
|
-
points.forEach(p=>context.lineTo(p.x, p.y));
|
|
8534
|
-
context.closePath();
|
|
8535
|
-
box2d.drawFillStroke(color, outlineColor, lineWidth, context);
|
|
8536
|
-
}, 0, context);
|
|
8537
|
-
}
|
|
8538
|
-
|
|
8539
|
-
/** draws a line
|
|
8540
|
-
* @param {Vector2} pos
|
|
8541
|
-
* @param {number} angle
|
|
8542
|
-
* @param {Vector2} posA
|
|
8543
|
-
* @param {Vector2} posB
|
|
8544
|
-
* @param {Color} [color]
|
|
8545
|
-
* @param {number} [lineWidth]
|
|
8546
|
-
* @param {CanvasRenderingContext2D} [context] */
|
|
8547
|
-
drawLine(pos, angle, posA, posB, color=WHITE, lineWidth=.1, context=drawContext)
|
|
8548
|
-
{
|
|
8549
|
-
drawCanvas2D(pos, vec2(1), angle, 0, context=>
|
|
8550
|
-
{
|
|
8551
|
-
context.beginPath();
|
|
8552
|
-
context.lineTo(posA.x, posA.y);
|
|
8553
|
-
context.lineTo(posB.x, posB.y);
|
|
8554
|
-
box2d.drawFillStroke(0, color, lineWidth, context);
|
|
8555
|
-
}, 0, context);
|
|
8556
|
-
}
|
|
8557
|
-
|
|
8558
|
-
/** performs a fill or stroke as a helper to the other draw functions
|
|
8559
|
-
* @param {Color} [color]
|
|
8560
|
-
* @param {Color} [outlineColor]
|
|
8561
|
-
* @param {number} [lineWidth]
|
|
8562
|
-
* @param {CanvasRenderingContext2D} [context] */
|
|
8563
|
-
drawFillStroke(color=WHITE, outlineColor=BLACK, lineWidth=.1, context=drawContext)
|
|
8564
|
-
{
|
|
8565
|
-
if (color)
|
|
8566
|
-
{
|
|
8567
|
-
context.fillStyle = color.toString();
|
|
8568
|
-
context.fill();
|
|
8569
|
-
}
|
|
8570
|
-
if (outlineColor && lineWidth)
|
|
8571
|
-
{
|
|
8572
|
-
context.lineWidth = lineWidth;
|
|
8573
|
-
context.lineJoin = context.lineCap = 'round';
|
|
8574
|
-
context.strokeStyle = outlineColor.toString();
|
|
8575
|
-
context.stroke();
|
|
8576
|
-
}
|
|
8577
|
-
}
|
|
8578
|
-
|
|
8579
9321
|
///////////////////////////////////////////////////////////////////////////////
|
|
8580
9322
|
// helper functions
|
|
8581
9323
|
|
|
@@ -8649,7 +9391,9 @@ class Box2dPlugin
|
|
|
8649
9391
|
}
|
|
8650
9392
|
|
|
8651
9393
|
///////////////////////////////////////////////////////////////////////////////
|
|
8652
|
-
/** Box2d Init - Call with await
|
|
9394
|
+
/** Box2d Init - Call with await to init box2d
|
|
9395
|
+
* @example
|
|
9396
|
+
* await box2dInit();
|
|
8653
9397
|
* @return {Promise<Box2dPlugin>}
|
|
8654
9398
|
* @memberof Box2D */
|
|
8655
9399
|
async function box2dInit()
|
|
@@ -8676,6 +9420,7 @@ async function box2dInit()
|
|
|
8676
9420
|
function setupDebugDraw()
|
|
8677
9421
|
{
|
|
8678
9422
|
// setup debug draw
|
|
9423
|
+
const debugLineWidth = .1;
|
|
8679
9424
|
const debugDraw = new box2d.instance.JSDraw();
|
|
8680
9425
|
const box2dColor = (c)=> new Color(c.get_r(), c.get_g(), c.get_b());
|
|
8681
9426
|
const box2dColorPointer = (c)=>
|
|
@@ -8693,33 +9438,33 @@ async function box2dInit()
|
|
|
8693
9438
|
color = getDebugColor(color);
|
|
8694
9439
|
point1 = box2d.vec2FromPointer(point1);
|
|
8695
9440
|
point2 = box2d.vec2FromPointer(point2);
|
|
8696
|
-
|
|
9441
|
+
drawLine(point1, point2, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
|
|
8697
9442
|
};
|
|
8698
9443
|
debugDraw.DrawPolygon = function(vertices, vertexCount, color)
|
|
8699
9444
|
{
|
|
8700
9445
|
color = getDebugColor(color);
|
|
8701
9446
|
const points = getPointsList(vertices, vertexCount);
|
|
8702
|
-
|
|
9447
|
+
drawPoly(points, CLEAR_WHITE, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
|
|
8703
9448
|
};
|
|
8704
9449
|
debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
|
|
8705
9450
|
{
|
|
8706
9451
|
color = getDebugColor(color);
|
|
8707
9452
|
const points = getPointsList(vertices, vertexCount);
|
|
8708
|
-
|
|
9453
|
+
drawPoly(points, color, 0, color, vec2(), 0, false, false, overlayContext);
|
|
8709
9454
|
};
|
|
8710
9455
|
debugDraw.DrawCircle = function(center, radius, color)
|
|
8711
9456
|
{
|
|
8712
9457
|
color = getDebugColor(color);
|
|
8713
9458
|
center = box2d.vec2FromPointer(center);
|
|
8714
|
-
|
|
9459
|
+
drawCircle(center, radius, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
|
|
8715
9460
|
};
|
|
8716
9461
|
debugDraw.DrawSolidCircle = function(center, radius, axis, color)
|
|
8717
9462
|
{
|
|
8718
9463
|
color = getDebugColor(color);
|
|
8719
9464
|
center = box2d.vec2FromPointer(center);
|
|
8720
9465
|
axis = box2d.vec2FromPointer(axis).scale(radius);
|
|
8721
|
-
|
|
8722
|
-
|
|
9466
|
+
drawCircle(center, radius, color, debugLineWidth, color, false, false, overlayContext);
|
|
9467
|
+
drawLine(vec2(), axis, debugLineWidth, color, center, 0, false, false, overlayContext);
|
|
8723
9468
|
};
|
|
8724
9469
|
debugDraw.DrawTransform = function(transform)
|
|
8725
9470
|
{
|
|
@@ -8728,8 +9473,8 @@ async function box2dInit()
|
|
|
8728
9473
|
const angle = -transform.get_q().GetAngle();
|
|
8729
9474
|
const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
|
|
8730
9475
|
const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
|
|
8731
|
-
|
|
8732
|
-
|
|
9476
|
+
drawLine(vec2(), p1, debugLineWidth, c1, pos, angle, false, false, overlayContext);
|
|
9477
|
+
drawLine(vec2(), p2, debugLineWidth, c2, pos, angle, false, false, overlayContext);
|
|
8733
9478
|
}
|
|
8734
9479
|
|
|
8735
9480
|
debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
|
|
@@ -8754,28 +9499,30 @@ async function box2dInit()
|
|
|
8754
9499
|
* @param {Vector2} pos - Screen space position
|
|
8755
9500
|
* @param {Vector2} size - Screen space size
|
|
8756
9501
|
* @param {TileInfo} startTile - Starting tile for the nine-slice pattern
|
|
8757
|
-
* @param {number} [borderSize
|
|
8758
|
-
* @param {number} [extraSpace
|
|
9502
|
+
* @param {number} [borderSize] - Width of the border sections
|
|
9503
|
+
* @param {number} [extraSpace] - Extra spacing adjustment
|
|
9504
|
+
* @param {number} [angle] - Angle to rotate by
|
|
8759
9505
|
* @memberof DrawUtilities */
|
|
8760
|
-
function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=
|
|
9506
|
+
function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
|
|
8761
9507
|
{
|
|
8762
|
-
drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, false, true, overlayContext);
|
|
9508
|
+
drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
|
|
8763
9509
|
}
|
|
8764
9510
|
|
|
8765
9511
|
/** Draw a scalable nine-slice UI element in world space
|
|
8766
|
-
* This function can apply color and additive color if
|
|
9512
|
+
* This function can apply color and additive color if WebGL is enabled
|
|
8767
9513
|
* @param {Vector2} pos - World space position
|
|
8768
9514
|
* @param {Vector2} size - World space size
|
|
8769
9515
|
* @param {TileInfo} startTile - Starting tile for the nine-slice pattern
|
|
8770
9516
|
* @param {Color} [color] - Color to modulate with
|
|
8771
|
-
* @param {number} [borderSize
|
|
9517
|
+
* @param {number} [borderSize] - Width of the border sections
|
|
8772
9518
|
* @param {Color} [additiveColor] - Additive color
|
|
8773
|
-
* @param {number} [extraSpace
|
|
9519
|
+
* @param {number} [extraSpace] - Extra spacing adjustment
|
|
9520
|
+
* @param {number} [angle] - Angle to rotate by
|
|
8774
9521
|
* @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
|
|
8775
9522
|
* @param {boolean} [screenSpace] - Use screen space coordinates
|
|
8776
9523
|
* @param {CanvasRenderingContext2D} [context] - Canvas context to use
|
|
8777
9524
|
* @memberof DrawUtilities */
|
|
8778
|
-
function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.
|
|
9525
|
+
function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
|
|
8779
9526
|
{
|
|
8780
9527
|
// setup nine slice tiles
|
|
8781
9528
|
const centerTile = startTile.offset(startTile.size);
|
|
@@ -8783,26 +9530,27 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
|
|
|
8783
9530
|
const cornerSize = vec2(borderSize);
|
|
8784
9531
|
const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5));
|
|
8785
9532
|
const flip = screenSpace ? -1 : 1;
|
|
9533
|
+
const rotateAngle = screenSpace ? -angle : angle;
|
|
8786
9534
|
|
|
8787
9535
|
// center
|
|
8788
|
-
drawTile(pos, centerSize, centerTile, color,
|
|
8789
|
-
for(let i=4; i--;)
|
|
9536
|
+
drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
|
|
9537
|
+
for (let i=4; i--;)
|
|
8790
9538
|
{
|
|
8791
9539
|
// sides
|
|
8792
9540
|
const horizontal = i%2;
|
|
8793
|
-
const sidePos = cornerOffset.multiply(vec2(horizontal?i
|
|
9541
|
+
const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-1:1));
|
|
8794
9542
|
const sideSize = vec2(horizontal ? borderSize : centerSize.x, horizontal ? centerSize.y : borderSize);
|
|
8795
|
-
const sideTile = centerTile.offset(startTile.size.multiply(vec2(i
|
|
8796
|
-
drawTile(pos.add(sidePos), sideSize, sideTile, color,
|
|
9543
|
+
const sideTile = centerTile.offset(startTile.size.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)))
|
|
9544
|
+
drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
|
|
8797
9545
|
}
|
|
8798
|
-
for(let i=4; i--;)
|
|
9546
|
+
for (let i=4; i--;)
|
|
8799
9547
|
{
|
|
8800
9548
|
// corners
|
|
8801
9549
|
const flipX = i>1;
|
|
8802
9550
|
const flipY = i && i<3;
|
|
8803
9551
|
const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-1:1));
|
|
8804
9552
|
const cornerTile = centerTile.offset(startTile.size.multiply(vec2(flipX?-1:1,flipY?flip:-flip)));
|
|
8805
|
-
drawTile(pos.add(cornerPos), cornerSize, cornerTile, color,
|
|
9553
|
+
drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
|
|
8806
9554
|
}
|
|
8807
9555
|
}
|
|
8808
9556
|
|
|
@@ -8811,28 +9559,30 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
|
|
|
8811
9559
|
* @param {Vector2} pos - Screen space position
|
|
8812
9560
|
* @param {Vector2} size - Screen space size
|
|
8813
9561
|
* @param {TileInfo} startTile - Starting tile for the three-slice pattern
|
|
8814
|
-
* @param {number} [borderSize
|
|
8815
|
-
* @param {number} [extraSpace
|
|
9562
|
+
* @param {number} [borderSize] - Width of the border sections
|
|
9563
|
+
* @param {number} [extraSpace] - Extra spacing adjustment
|
|
9564
|
+
* @param {number} [angle] - Angle to rotate by
|
|
8816
9565
|
* @memberof DrawUtilities */
|
|
8817
|
-
function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=
|
|
9566
|
+
function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
|
|
8818
9567
|
{
|
|
8819
|
-
drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, false, true, overlayContext);
|
|
9568
|
+
drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
|
|
8820
9569
|
}
|
|
8821
9570
|
|
|
8822
9571
|
/** Draw a scalable three-slice UI element in world space
|
|
8823
|
-
* This function can apply color and additive color if
|
|
9572
|
+
* This function can apply color and additive color if WebGL is enabled
|
|
8824
9573
|
* @param {Vector2} pos - World space position
|
|
8825
9574
|
* @param {Vector2} size - World space size
|
|
8826
9575
|
* @param {TileInfo} startTile - Starting tile for the three-slice pattern
|
|
8827
9576
|
* @param {Color} [color] - Color to modulate with
|
|
8828
|
-
* @param {number} [borderSize
|
|
9577
|
+
* @param {number} [borderSize] - Width of the border sections
|
|
8829
9578
|
* @param {Color} [additiveColor] - Additive color
|
|
8830
|
-
* @param {number} [extraSpace
|
|
9579
|
+
* @param {number} [extraSpace] - Extra spacing adjustment
|
|
9580
|
+
* @param {number} [angle] - Angle to rotate by
|
|
8831
9581
|
* @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
|
|
8832
9582
|
* @param {boolean} [screenSpace] - Use screen space coordinates
|
|
8833
9583
|
* @param {CanvasRenderingContext2D} [context] - Canvas context to use
|
|
8834
9584
|
* @memberof DrawUtilities */
|
|
8835
|
-
function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.
|
|
9585
|
+
function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
|
|
8836
9586
|
{
|
|
8837
9587
|
// setup three slice tiles
|
|
8838
9588
|
const cornerTile = startTile.frame(0);
|
|
@@ -8842,25 +9592,26 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
|
|
|
8842
9592
|
const cornerSize = vec2(borderSize);
|
|
8843
9593
|
const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5));
|
|
8844
9594
|
const flip = screenSpace ? -1 : 1;
|
|
9595
|
+
const rotateAngle = screenSpace ? -angle : angle;
|
|
8845
9596
|
|
|
8846
9597
|
// center
|
|
8847
|
-
drawTile(pos, centerSize, centerTile, color,
|
|
8848
|
-
for(let i=4; i--;)
|
|
9598
|
+
drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
|
|
9599
|
+
for (let i=4; i--;)
|
|
8849
9600
|
{
|
|
8850
9601
|
// sides
|
|
8851
|
-
const
|
|
9602
|
+
const a = angle + i*PI/2;
|
|
8852
9603
|
const horizontal = i%2;
|
|
8853
|
-
const sidePos = cornerOffset.multiply(vec2(horizontal?i
|
|
9604
|
+
const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-flip:flip));
|
|
8854
9605
|
const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize);
|
|
8855
|
-
drawTile(pos.add(sidePos), sideSize, sideTile, color,
|
|
9606
|
+
drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
|
|
8856
9607
|
}
|
|
8857
|
-
for(let i=4; i--;)
|
|
9608
|
+
for (let i=4; i--;)
|
|
8858
9609
|
{
|
|
8859
9610
|
// corners
|
|
8860
|
-
const
|
|
9611
|
+
const a = angle + i*PI/2;
|
|
8861
9612
|
const flipX = !i || i>2;
|
|
8862
9613
|
const flipY = i>1;
|
|
8863
9614
|
const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-flip:flip));
|
|
8864
|
-
drawTile(pos.add(cornerPos), cornerSize, cornerTile, color,
|
|
9615
|
+
drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
|
|
8865
9616
|
}
|
|
8866
9617
|
}
|