littlejsengine 1.18.12 → 1.18.15
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/FAQ.md +197 -15
- package/README.md +28 -7
- package/dist/littlejs.d.ts +105 -77
- package/dist/littlejs.esm.js +414 -277
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +406 -272
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +403 -265
- package/package.json +4 -1
- package/plugins/box2d.js +17 -6
- package/plugins/newgrounds.js +15 -3
- package/plugins/pathFinder.js +5 -3
- package/plugins/pluginExport.js +4 -3
- package/plugins/postProcess.js +8 -1
- package/plugins/tweenSystem.js +25 -20
- package/plugins/uiSystem.js +21 -3
- package/src/engine.js +14 -3
- package/src/engineAudio.js +27 -13
- package/src/engineBuild.mjs +1 -1
- package/src/engineDebug.js +3 -7
- package/src/engineDraw.js +71 -40
- package/src/engineExport.js +4 -2
- package/src/engineInput.js +17 -12
- package/src/engineLogo.js +1 -1
- package/src/engineMath.js +21 -7
- package/src/engineObject.js +19 -11
- package/src/engineParticles.js +26 -27
- package/src/engineSettings.js +3 -2
- package/src/engineTileLayer.js +62 -43
- package/src/engineUtilities.js +24 -11
- package/src/engineWebGL.js +23 -55
package/src/engineDraw.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* - Optimized tile sheet sprite rendering using WebGL batching
|
|
5
5
|
* - Primitive drawing for polygons, ellipses, and lines
|
|
6
6
|
* - Tile-based rendering with TileInfo and TextureInfo classes
|
|
7
|
-
* - Text rendering with custom fonts and
|
|
7
|
+
* - Text rendering with custom fonts and ImageFont support
|
|
8
8
|
* - Color and additive color blending for effects
|
|
9
9
|
* - Rotation, mirroring, and scaling transformations
|
|
10
10
|
* - Camera system with position, scale, and rotation
|
|
@@ -102,7 +102,7 @@ let primitiveCount;
|
|
|
102
102
|
* tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
|
|
103
103
|
* tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
|
|
104
104
|
* @memberof Draw */
|
|
105
|
-
function tile(index=
|
|
105
|
+
function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
106
106
|
{
|
|
107
107
|
ASSERT(isVector2(index) || typeof index === 'number', 'index must be a vec2 or number');
|
|
108
108
|
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
@@ -153,8 +153,8 @@ class TileInfo
|
|
|
153
153
|
* @param {Vector2} [pos=vec2()] - Top left corner of tile in pixels
|
|
154
154
|
* @param {Vector2} [size] - Size of tile in pixels
|
|
155
155
|
* @param {TextureInfo} [textureInfo] - Texture info to use
|
|
156
|
-
* @param {number} [padding] - How many pixels padding around
|
|
157
|
-
* @param {number} [bleed] - How many pixels smaller to
|
|
156
|
+
* @param {number} [padding] - How many pixels padding around all sides of each tile (increases grid size, does not affect tile size)
|
|
157
|
+
* @param {number} [bleed] - How many pixels smaller to shrink UVS of tiles (does not affect grid size, only UVs)
|
|
158
158
|
*/
|
|
159
159
|
constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
160
160
|
{
|
|
@@ -186,7 +186,7 @@ class TileInfo
|
|
|
186
186
|
ASSERT(typeof frame === 'number');
|
|
187
187
|
const w = this.size.x + this.padding*2;
|
|
188
188
|
const x = frame*w;
|
|
189
|
-
ASSERT(x
|
|
189
|
+
ASSERT(x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
|
|
190
190
|
return this.offset(new Vector2(x));
|
|
191
191
|
}
|
|
192
192
|
|
|
@@ -275,7 +275,7 @@ class TextureInfo
|
|
|
275
275
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
276
276
|
* @memberof Draw */
|
|
277
277
|
function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
278
|
-
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
|
|
278
|
+
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace=false, context)
|
|
279
279
|
{
|
|
280
280
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
281
281
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -318,10 +318,8 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
318
318
|
}
|
|
319
319
|
else
|
|
320
320
|
{
|
|
321
|
-
// untextured:
|
|
322
|
-
//
|
|
323
|
-
// uvs/rgba zeroed). Color+additive are folded together to match
|
|
324
|
-
// the Canvas2D path's color.add(additiveColor) on line ~337.
|
|
321
|
+
// untextured: fold color+additive to match the Canvas2D path's
|
|
322
|
+
// color.add(additiveColor) on line ~337.
|
|
325
323
|
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
326
324
|
glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
|
|
327
325
|
}
|
|
@@ -534,7 +532,7 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
534
532
|
* @param {boolean} [screenSpace]
|
|
535
533
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
536
534
|
* @memberof Draw */
|
|
537
|
-
function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
|
|
535
|
+
function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
538
536
|
{
|
|
539
537
|
ASSERT(isArray(points), 'points must be an array');
|
|
540
538
|
ASSERT(isNumber(width), 'width must be a number');
|
|
@@ -583,7 +581,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
583
581
|
* @param {boolean} [screenSpace]
|
|
584
582
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
585
583
|
* @memberof Draw */
|
|
586
|
-
function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
|
|
584
|
+
function drawLine(posA, posB, width=.1, color=WHITE, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
587
585
|
{
|
|
588
586
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
589
587
|
const size = vec2(width, halfDelta.length()*2);
|
|
@@ -599,9 +597,9 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
|
|
|
599
597
|
* @param {Vector2} [size=vec2(1)]
|
|
600
598
|
* @param {number} [sides]
|
|
601
599
|
* @param {Color} [color=WHITE]
|
|
602
|
-
* @param {number} [angle]
|
|
603
600
|
* @param {number} [lineWidth]
|
|
604
601
|
* @param {Color} [lineColor=BLACK]
|
|
602
|
+
* @param {number} [angle]
|
|
605
603
|
* @param {boolean} [useWebGL=glEnable]
|
|
606
604
|
* @param {boolean} [screenSpace]
|
|
607
605
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -694,7 +692,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
694
692
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
695
693
|
|
|
696
694
|
// clamp line width to prevent artifacts
|
|
697
|
-
lineWidth = clamp(lineWidth, 0,
|
|
695
|
+
lineWidth = clamp(lineWidth, 0, min(size.x, size.y));
|
|
698
696
|
|
|
699
697
|
if (useWebGL && glEnable)
|
|
700
698
|
{
|
|
@@ -736,24 +734,26 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
|
|
|
736
734
|
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
737
735
|
}
|
|
738
736
|
|
|
739
|
-
/** Draw
|
|
737
|
+
/** Draw an ellipse filled with a radial gradient from the center to the rim
|
|
740
738
|
* - Best when batched with other untextured polys
|
|
741
739
|
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
742
740
|
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
743
741
|
* @param {Vector2} pos
|
|
744
|
-
* @param {
|
|
742
|
+
* @param {Vector2} [size=vec2(1)] - Width and height diameter
|
|
745
743
|
* @param {Color} [colorInner=WHITE]
|
|
746
744
|
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
745
|
+
* @param {number} [angle]
|
|
747
746
|
* @param {boolean} [useWebGL=glEnable]
|
|
748
747
|
* @param {boolean} [screenSpace]
|
|
749
748
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
750
749
|
* @memberof Draw */
|
|
751
|
-
let
|
|
752
|
-
function
|
|
750
|
+
let drawEllipseGradientOffset = 0;
|
|
751
|
+
function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
753
752
|
{
|
|
754
753
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
755
|
-
ASSERT(
|
|
754
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
756
755
|
ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
|
|
756
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
757
757
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
758
758
|
|
|
759
759
|
if (headlessMode) return;
|
|
@@ -765,26 +765,33 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
|
|
|
765
765
|
{
|
|
766
766
|
// convert to world space
|
|
767
767
|
pos = screenToWorld(pos);
|
|
768
|
-
size
|
|
768
|
+
size = size.scale(1/cameraScale);
|
|
769
|
+
angle += cameraAngle;
|
|
769
770
|
}
|
|
770
771
|
// fan as tristrip; rotate the boundary vertex by one slice per call
|
|
771
772
|
// so back-to-back gradients at the same position have their hole
|
|
772
773
|
// (from gpu edge-rule on the boundary line-degen) at different rim
|
|
773
774
|
// verts and don't visibly stack
|
|
774
775
|
const sides = glCircleSides;
|
|
775
|
-
const
|
|
776
|
+
const radiusX = size.x/2, radiusY = size.y/2;
|
|
776
777
|
const innerInt = colorInner.rgbaInt();
|
|
777
778
|
const outerInt = colorOuter.rgbaInt();
|
|
778
|
-
const offset =
|
|
779
|
+
const offset = drawEllipseGradientOffset++;
|
|
780
|
+
const c = cos(-angle), s = sin(-angle);
|
|
781
|
+
const rim = (a) =>
|
|
782
|
+
{
|
|
783
|
+
const lx = sin(a)*radiusX, ly = cos(a)*radiusY;
|
|
784
|
+
return vec2(pos.x + lx*c - ly*s, pos.y + lx*s + ly*c);
|
|
785
|
+
};
|
|
779
786
|
const startA = (offset%sides)/sides*PI*2;
|
|
780
|
-
const points = [
|
|
787
|
+
const points = [rim(startA)];
|
|
781
788
|
const colors = [outerInt];
|
|
782
789
|
for (let i=sides; i--;)
|
|
783
790
|
{
|
|
784
791
|
const a = ((i+offset)%sides)/sides*PI*2;
|
|
785
792
|
points.push(pos);
|
|
786
793
|
colors.push(innerInt);
|
|
787
|
-
points.push(
|
|
794
|
+
points.push(rim(a));
|
|
788
795
|
colors.push(outerInt);
|
|
789
796
|
}
|
|
790
797
|
glDrawColoredPoints(points, colors);
|
|
@@ -794,7 +801,7 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
|
|
|
794
801
|
// normal canvas 2D rendering method (slower)
|
|
795
802
|
++drawCount;
|
|
796
803
|
++primitiveCount;
|
|
797
|
-
drawCanvas2D(pos,
|
|
804
|
+
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
798
805
|
{
|
|
799
806
|
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
|
|
800
807
|
gradient.addColorStop(0, colorInner.toString());
|
|
@@ -807,6 +814,24 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
|
|
|
807
814
|
}
|
|
808
815
|
}
|
|
809
816
|
|
|
817
|
+
/** Draw a circle filled with a radial gradient from the center to the rim
|
|
818
|
+
* - Best when batched with other untextured polys
|
|
819
|
+
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
820
|
+
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
821
|
+
* @param {Vector2} pos
|
|
822
|
+
* @param {number} [size=1] - Diameter
|
|
823
|
+
* @param {Color} [colorInner=WHITE]
|
|
824
|
+
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
825
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
826
|
+
* @param {boolean} [screenSpace]
|
|
827
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
828
|
+
* @memberof Draw */
|
|
829
|
+
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
|
|
830
|
+
{
|
|
831
|
+
ASSERT(isNumber(size), 'size must be a number');
|
|
832
|
+
drawEllipseGradient(pos, vec2(size), colorInner, colorOuter, 0, useWebGL, screenSpace, context);
|
|
833
|
+
}
|
|
834
|
+
|
|
810
835
|
/**
|
|
811
836
|
* @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
|
|
812
837
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
@@ -861,7 +886,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
861
886
|
* @param {number} [angle]
|
|
862
887
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
863
888
|
* @memberof Draw */
|
|
864
|
-
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
|
|
889
|
+
function drawText(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
|
|
865
890
|
{
|
|
866
891
|
// convert to screen space
|
|
867
892
|
pos = worldToScreen(pos);
|
|
@@ -901,16 +926,16 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
|
|
|
901
926
|
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
902
927
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
903
928
|
|
|
929
|
+
const lines = (text+'').split('\n');
|
|
930
|
+
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
931
|
+
// save before style mutations so caller's context state is preserved
|
|
932
|
+
context.save();
|
|
904
933
|
context.fillStyle = color.toString();
|
|
905
934
|
context.strokeStyle = lineColor.toString();
|
|
906
935
|
context.lineWidth = lineWidth;
|
|
907
936
|
context.textAlign = textAlign;
|
|
908
937
|
context.font = fontStyle + ' ' + size + 'px '+ font;
|
|
909
938
|
context.textBaseline = 'middle';
|
|
910
|
-
|
|
911
|
-
const lines = (text+'').split('\n');
|
|
912
|
-
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
913
|
-
context.save();
|
|
914
939
|
context.translate(pos.x, posY);
|
|
915
940
|
context.rotate(-angle);
|
|
916
941
|
let yOffset = 0;
|
|
@@ -1071,6 +1096,9 @@ function isOnScreen(pos, size=0)
|
|
|
1071
1096
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
1072
1097
|
ASSERT(isVector2(size) || isNumber(size), 'size must be a vec2 or number');
|
|
1073
1098
|
|
|
1099
|
+
// cameraScale of 0 collapses world coords; nothing is visible
|
|
1100
|
+
if (!cameraScale) return false;
|
|
1101
|
+
|
|
1074
1102
|
// optimized circle on screen test
|
|
1075
1103
|
// pos = worldToScreen(pos);
|
|
1076
1104
|
let x = pos.x - cameraPos.x;
|
|
@@ -1112,7 +1140,10 @@ function combineCanvases()
|
|
|
1112
1140
|
const w = mainCanvasSize.x, h = mainCanvasSize.y;
|
|
1113
1141
|
workCanvas.width = w;
|
|
1114
1142
|
workCanvas.height = h;
|
|
1115
|
-
|
|
1143
|
+
// remove background alpha — explicit fillStyle so a previous caller
|
|
1144
|
+
// leaving workContext.fillStyle transparent can't silently no-op this
|
|
1145
|
+
workContext.fillStyle = '#000';
|
|
1146
|
+
workContext.fillRect(0,0,w,h);
|
|
1116
1147
|
glCopyToContext(workContext);
|
|
1117
1148
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
1118
1149
|
mainContext.drawImage(workCanvas, 0, 0);
|
|
@@ -1255,33 +1286,33 @@ function setCursor(cursorStyle = 'auto')
|
|
|
1255
1286
|
///////////////////////////////////////////////////////////////////////////////
|
|
1256
1287
|
|
|
1257
1288
|
/** Engine font image, 8x8 font provided by the engine
|
|
1258
|
-
* @type {
|
|
1289
|
+
* @type {ImageFont}
|
|
1259
1290
|
* @memberof Draw */
|
|
1260
|
-
let
|
|
1291
|
+
let engineImageFont;
|
|
1261
1292
|
|
|
1262
1293
|
/**
|
|
1263
|
-
* Font
|
|
1294
|
+
* Image Font Object - Draw text by using tiles in an image
|
|
1264
1295
|
* - 96 characters (from space to tilde) are stored in an image
|
|
1265
1296
|
* - A 8x8 default engine font is supplied for general use
|
|
1266
1297
|
* - This system is WebGL enabled for fast text rendering
|
|
1267
1298
|
* - Fonts can also be colored and scaled along each axis
|
|
1268
|
-
*
|
|
1299
|
+
*
|
|
1269
1300
|
* @memberof Draw
|
|
1270
1301
|
* @example
|
|
1271
1302
|
* // use built in font
|
|
1272
|
-
* const font =
|
|
1303
|
+
* const font = engineImageFont;
|
|
1273
1304
|
*
|
|
1274
1305
|
* // draw text
|
|
1275
1306
|
* font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
|
|
1276
1307
|
*/
|
|
1277
|
-
class
|
|
1308
|
+
class ImageFont
|
|
1278
1309
|
{
|
|
1279
1310
|
/** Create an image font
|
|
1280
1311
|
* @param {TileInfo} tileInfo - Tile info of first character in font
|
|
1281
1312
|
*/
|
|
1282
1313
|
constructor(tileInfo)
|
|
1283
1314
|
{
|
|
1284
|
-
ASSERT(!!tileInfo, 'tileInfo is required for
|
|
1315
|
+
ASSERT(!!tileInfo, 'tileInfo is required for ImageFont');
|
|
1285
1316
|
|
|
1286
1317
|
/** @property {TileInfo} - Tile info for the font */
|
|
1287
1318
|
this.tileInfo = tileInfo.frame(0);
|
|
@@ -1366,7 +1397,7 @@ class FontImage
|
|
|
1366
1397
|
}
|
|
1367
1398
|
|
|
1368
1399
|
// load engine font, called automatically on startup
|
|
1369
|
-
async function
|
|
1400
|
+
async function imageFontInit()
|
|
1370
1401
|
{
|
|
1371
1402
|
const image = new Image;
|
|
1372
1403
|
await new Promise(resolve =>
|
|
@@ -1379,5 +1410,5 @@ async function fontImageInit()
|
|
|
1379
1410
|
const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
|
|
1380
1411
|
const textureInfo = new TextureInfo(image);
|
|
1381
1412
|
const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
|
|
1382
|
-
|
|
1413
|
+
engineImageFont = new ImageFont(tileInfo);
|
|
1383
1414
|
}
|
package/src/engineExport.js
CHANGED
|
@@ -138,6 +138,7 @@ export
|
|
|
138
138
|
setTouchGamepadAnalog,
|
|
139
139
|
setTouchGamepadSize,
|
|
140
140
|
setTouchGamepadAlpha,
|
|
141
|
+
setTouchGamepadDisplayTime,
|
|
141
142
|
setVibrateEnable,
|
|
142
143
|
setSoundEnable,
|
|
143
144
|
setSoundVolume,
|
|
@@ -258,14 +259,15 @@ export
|
|
|
258
259
|
drawRegularPoly,
|
|
259
260
|
drawEllipse,
|
|
260
261
|
drawCircle,
|
|
262
|
+
drawEllipseGradient,
|
|
261
263
|
drawCircleGradient,
|
|
262
264
|
drawCanvas2D,
|
|
263
265
|
drawText,
|
|
264
266
|
drawTextScreen,
|
|
265
267
|
setBlendMode,
|
|
266
268
|
combineCanvases,
|
|
267
|
-
|
|
268
|
-
|
|
269
|
+
engineImageFont,
|
|
270
|
+
ImageFont,
|
|
269
271
|
isFullscreen,
|
|
270
272
|
toggleFullscreen,
|
|
271
273
|
setCursor,
|
package/src/engineInput.js
CHANGED
|
@@ -447,9 +447,11 @@ function inputInit()
|
|
|
447
447
|
mouseDeltaScreen = mouseDeltaScreen.add(movement);
|
|
448
448
|
}
|
|
449
449
|
function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
|
|
450
|
-
function onMouseWheel(e)
|
|
451
|
-
{
|
|
452
|
-
|
|
450
|
+
function onMouseWheel(e)
|
|
451
|
+
{
|
|
452
|
+
// accumulate so multiple wheel events in one frame are not lost
|
|
453
|
+
if (!e.ctrlKey)
|
|
454
|
+
mouseWheel += sign(e.deltaY);
|
|
453
455
|
if (inputPreventDefault && e.cancelable && document.hasFocus())
|
|
454
456
|
e.preventDefault(); // prevent page scrolling
|
|
455
457
|
}
|
|
@@ -574,9 +576,14 @@ function inputInit()
|
|
|
574
576
|
if (button < touchGamepadButtonCount)
|
|
575
577
|
touchGamepadButtons[button] = 1;
|
|
576
578
|
}
|
|
577
|
-
else if (startCenter.distance(touchPos)
|
|
579
|
+
else if (startCenter.distance(touchPos) < touchGamepadCenterButtonSize &&
|
|
580
|
+
stickCenter.distance(touchPos) >= 2 * touchGamepadSize &&
|
|
581
|
+
buttonCenter.distance(touchPos) >= 2 * touchGamepadSize)
|
|
578
582
|
{
|
|
579
583
|
// virtual start button in center
|
|
584
|
+
// require a fat-finger buffer of touchGamepadSize beyond the
|
|
585
|
+
// edge of the stick/buttons so drift off those controls can't
|
|
586
|
+
// accidentally fire start
|
|
580
587
|
touchGamepadButtons[9] = 1;
|
|
581
588
|
}
|
|
582
589
|
}
|
|
@@ -618,7 +625,7 @@ function inputUpdate()
|
|
|
618
625
|
v > min ? percent(v, min, max) :
|
|
619
626
|
v < -min ? -percent(-v, min, max) : 0;
|
|
620
627
|
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
621
|
-
}
|
|
628
|
+
};
|
|
622
629
|
|
|
623
630
|
// update touch gamepad if enabled
|
|
624
631
|
if (touchGamepadEnable && isTouchDevice)
|
|
@@ -632,7 +639,12 @@ function inputUpdate()
|
|
|
632
639
|
debugCircle(stickCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
|
|
633
640
|
debugCircle(buttonCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
|
|
634
641
|
if (touchGamepadCenterButtonSize)
|
|
642
|
+
{
|
|
635
643
|
debugCircle(startCenter, 2*touchGamepadCenterButtonSize, 'cyan', 0, false, true);
|
|
644
|
+
// exclusion bubbles around controls (where start is blocked)
|
|
645
|
+
debugCircle(stickCenter, 4*touchGamepadSize, 'magenta', 0, false, true);
|
|
646
|
+
debugCircle(buttonCenter, 4*touchGamepadSize, 'magenta', 0, false, true);
|
|
647
|
+
}
|
|
636
648
|
}
|
|
637
649
|
|
|
638
650
|
if (!touchGamepadTimer.isSet()) return;
|
|
@@ -739,13 +751,6 @@ function inputUpdate()
|
|
|
739
751
|
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
740
752
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
741
753
|
}
|
|
742
|
-
else if (gamepad.axes && gamepad.axes.length >= 2)
|
|
743
|
-
{
|
|
744
|
-
// digital style dpad from axes
|
|
745
|
-
const x = clamp(round(gamepad.axes[0]), -1, 1);
|
|
746
|
-
const y = clamp(round(gamepad.axes[1]), -1, 1);
|
|
747
|
-
dpad.set(x, -y);
|
|
748
|
-
}
|
|
749
754
|
|
|
750
755
|
// copy dpad to left analog stick when pressed
|
|
751
756
|
if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
|
package/src/engineLogo.js
CHANGED
|
@@ -65,7 +65,7 @@ function drawEngineLogo(t)
|
|
|
65
65
|
x.closePath();
|
|
66
66
|
gradient(0, Y, 0, Y+H,C);
|
|
67
67
|
}
|
|
68
|
-
const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}
|
|
68
|
+
const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%)`:'#000';
|
|
69
69
|
|
|
70
70
|
// center and fit to screen
|
|
71
71
|
const alpha = oscillate(1,1,t);
|
package/src/engineMath.js
CHANGED
|
@@ -62,19 +62,19 @@ const max = Math.max;
|
|
|
62
62
|
* @param {number} x
|
|
63
63
|
* @return {number}
|
|
64
64
|
* @memberof Math */
|
|
65
|
-
const sign = Math.sign;
|
|
65
|
+
const sign = (x) => Math.sign(x);
|
|
66
66
|
|
|
67
67
|
/** Returns hypotenuse of values passed in
|
|
68
68
|
* @param {...number} values
|
|
69
69
|
* @return {number}
|
|
70
70
|
* @memberof Math */
|
|
71
|
-
const hypot = Math.hypot;
|
|
71
|
+
const hypot = (...values) => Math.hypot(...values);
|
|
72
72
|
|
|
73
73
|
/** Returns log2 of value passed in
|
|
74
74
|
* @param {number} x
|
|
75
75
|
* @return {number}
|
|
76
76
|
* @memberof Math */
|
|
77
|
-
const log2 = Math.log2;
|
|
77
|
+
const log2 = (x) => Math.log2(x);
|
|
78
78
|
|
|
79
79
|
/** Returns sin of value passed in
|
|
80
80
|
* @param {number} x
|
|
@@ -216,7 +216,8 @@ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
|
|
|
216
216
|
const dy = (posA.y - posB.y)*2;
|
|
217
217
|
const sx = sizeA.x + sizeB.x;
|
|
218
218
|
const sy = sizeA.y + sizeB.y;
|
|
219
|
-
|
|
219
|
+
// symmetric so isOverlapping(A,B) === isOverlapping(B,A) at touching edges
|
|
220
|
+
return abs(dx) < sx && abs(dy) < sy;
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
/** Returns true if a line segment is intersecting an axis aligned box
|
|
@@ -305,7 +306,7 @@ function isStringLike(s) { return s != null && typeof s?.toString() === 'string'
|
|
|
305
306
|
/**
|
|
306
307
|
* Check if object is an array
|
|
307
308
|
* @param {any} a
|
|
308
|
-
* @return {
|
|
309
|
+
* @return {a is Array<any>}
|
|
309
310
|
* @memberof Math */
|
|
310
311
|
function isArray(a) { return Array.isArray(a); }
|
|
311
312
|
|
|
@@ -443,7 +444,13 @@ function randVec2(length=1) { return new Vector2().setAngle(rand(2*PI), length);
|
|
|
443
444
|
* @return {Vector2}
|
|
444
445
|
* @memberof Random */
|
|
445
446
|
function randInCircle(radius=1, minRadius=0)
|
|
446
|
-
{
|
|
447
|
+
{
|
|
448
|
+
// r is uniform in area ⇒ r² uniform in [minRadius², radius²]
|
|
449
|
+
// (the squared inner bound is what makes minRadius the actual exclusion edge)
|
|
450
|
+
if (radius <= 0) return new Vector2;
|
|
451
|
+
const ratio = clamp(minRadius / radius);
|
|
452
|
+
return randVec2(radius * rand(ratio*ratio, 1)**.5);
|
|
453
|
+
}
|
|
447
454
|
|
|
448
455
|
/** Returns a random color between the two passed in colors, combine components if linear
|
|
449
456
|
* @param {Color} [colorA=WHITE]
|
|
@@ -513,7 +520,14 @@ class RandomGenerator
|
|
|
513
520
|
* @param {number} [valueA]
|
|
514
521
|
* @param {number} [valueB]
|
|
515
522
|
* @return {number} */
|
|
516
|
-
floatSign(valueA=1, valueB=0)
|
|
523
|
+
floatSign(valueA=1, valueB=0)
|
|
524
|
+
{
|
|
525
|
+
const lo = min(valueA, valueB);
|
|
526
|
+
const hi = max(valueA, valueB);
|
|
527
|
+
const d = hi - lo;
|
|
528
|
+
const e = this.float(d*2);
|
|
529
|
+
return e < d ? lo + e : d - lo - e;
|
|
530
|
+
}
|
|
517
531
|
|
|
518
532
|
/** Returns a random angle between -PI and PI
|
|
519
533
|
* @return {number} */
|
package/src/engineObject.js
CHANGED
|
@@ -71,7 +71,7 @@ class EngineObject
|
|
|
71
71
|
this.color = color.copy();
|
|
72
72
|
/** @property {Color} - Additive color to apply when rendered */
|
|
73
73
|
this.additiveColor = undefined;
|
|
74
|
-
/** @property {boolean} - Should
|
|
74
|
+
/** @property {boolean} - Should the rendered tile flip along the y axis. Affects rendering and the local→world transform of attached children (a mirrored parent flips its children's localPos.x and localAngle). Does not affect this object's own physics, collision, or localToWorld/worldToLocal. */
|
|
75
75
|
this.mirror = false;
|
|
76
76
|
/** @property {boolean} - Has object been destroyed? */
|
|
77
77
|
this.destroyed = false;
|
|
@@ -157,6 +157,9 @@ class EngineObject
|
|
|
157
157
|
// child objects do not have physics
|
|
158
158
|
ASSERT(!this.parent);
|
|
159
159
|
|
|
160
|
+
// bail if a collision callback destroyed us mid-frame
|
|
161
|
+
if (this.destroyed) return;
|
|
162
|
+
|
|
160
163
|
if (this.clampSpeed)
|
|
161
164
|
{
|
|
162
165
|
// limit max speed to prevent missing collisions
|
|
@@ -212,6 +215,8 @@ class EngineObject
|
|
|
212
215
|
|
|
213
216
|
// notify objects of collision and check if should be resolved
|
|
214
217
|
const collide1 = this.collideWithObject(o);
|
|
218
|
+
// callback may have destroyed us; stop resolving against more objects
|
|
219
|
+
if (this.destroyed) return;
|
|
215
220
|
const collide2 = o.collideWithObject(this);
|
|
216
221
|
if (!collide1 || !collide2) continue;
|
|
217
222
|
|
|
@@ -307,13 +312,17 @@ class EngineObject
|
|
|
307
312
|
const restitution = max(this.restitution, hitLayer.restitution);
|
|
308
313
|
if (isBlockedX)
|
|
309
314
|
{
|
|
310
|
-
// try to
|
|
315
|
+
// try to step over a 1-tile bump (direction follows gravity sign
|
|
316
|
+
// so inverted gravity steps down off a ceiling bump instead of up;
|
|
317
|
+
// zero gravity defaults to the normal-gravity step-up direction)
|
|
311
318
|
const epsilon = 1e-3;
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
319
|
+
const maxMove = .1;
|
|
320
|
+
const gravitySign = gravity.y > 0 ? -1 : 1;
|
|
321
|
+
const y = gravitySign > 0 ?
|
|
322
|
+
floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon :
|
|
323
|
+
ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon;
|
|
324
|
+
const delta = abs(y - this.pos.y);
|
|
325
|
+
if (delta < maxMove)
|
|
317
326
|
if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
|
|
318
327
|
{
|
|
319
328
|
this.pos.y = y;
|
|
@@ -453,6 +462,8 @@ class EngineObject
|
|
|
453
462
|
* @return {EngineObject} The child object added */
|
|
454
463
|
addChild(child, localPos=vec2(), localAngle=0)
|
|
455
464
|
{
|
|
465
|
+
ASSERT(!this.destroyed, 'cannot add child to destroyed object');
|
|
466
|
+
if (this.destroyed) return child;
|
|
456
467
|
ASSERT(!child.parent && !this.children.includes(child));
|
|
457
468
|
ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
458
469
|
ASSERT(child !== this, 'cannot add self as child');
|
|
@@ -469,10 +480,7 @@ class EngineObject
|
|
|
469
480
|
removeChild(child)
|
|
470
481
|
{
|
|
471
482
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
472
|
-
|
|
473
|
-
const index = this.children.indexOf(child);
|
|
474
|
-
ASSERT(index >= 0, 'child not found in children array');
|
|
475
|
-
index >= 0 && this.children.splice(index, 1);
|
|
483
|
+
this.children.splice(this.children.indexOf(child), 1);
|
|
476
484
|
child.parent = undefined;
|
|
477
485
|
}
|
|
478
486
|
|
package/src/engineParticles.js
CHANGED
|
@@ -68,7 +68,7 @@ class ParticleEmitter extends EngineObject
|
|
|
68
68
|
* @param {number} [angleDamping] - How much to dampen particle angular speed
|
|
69
69
|
* @param {number} [gravityScale] - How much gravity effect particles
|
|
70
70
|
* @param {number} [particleConeAngle] - Cone for start particle angle
|
|
71
|
-
* @param {number} [fadeRate] -
|
|
71
|
+
* @param {number} [fadeRate] - Fraction of life spent fading: half at fade-in (start), half at fade-out (end). e.g. .2 = 10% fade-in, 80% full opacity, 10% fade-out
|
|
72
72
|
* @param {number} [randomness] - Apply extra randomness percent
|
|
73
73
|
* @param {boolean} [collideTiles] - Do particles collide against tiles
|
|
74
74
|
* @param {boolean} [additive] - Should particles use additive blend
|
|
@@ -152,7 +152,7 @@ class ParticleEmitter extends EngineObject
|
|
|
152
152
|
this.gravityScale = gravityScale;
|
|
153
153
|
/** @property {number} - Cone for start particle angle */
|
|
154
154
|
this.particleConeAngle = particleConeAngle;
|
|
155
|
-
/** @property {number} -
|
|
155
|
+
/** @property {number} - Fraction of life spent fading, split half at start and half at end (e.g. .2 = 10% fade-in + 10% fade-out) */
|
|
156
156
|
this.fadeRate = fadeRate;
|
|
157
157
|
/** @property {number} - Apply extra randomness percent */
|
|
158
158
|
this.randomness = randomness;
|
|
@@ -430,33 +430,32 @@ class Particle
|
|
|
430
430
|
const hitLayer = tileCollisionTest(this.pos);
|
|
431
431
|
if (!testCollision(oldPos))
|
|
432
432
|
{
|
|
433
|
-
|
|
433
|
+
// testCollision already invoked collideCallback with the
|
|
434
|
+
// correct (this, data, pos) args; no need to re-check here.
|
|
435
|
+
// test which side we bounced off (or both if a corner)
|
|
436
|
+
const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
|
|
437
|
+
const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
|
|
438
|
+
const hitRestitution = max(restitution, hitLayer.restitution);
|
|
439
|
+
const hitFriction = max(friction, hitLayer.friction);
|
|
440
|
+
if (isBlockedX)
|
|
434
441
|
{
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const hitFriction = max(friction, hitLayer.friction);
|
|
440
|
-
if (isBlockedX)
|
|
441
|
-
{
|
|
442
|
-
// move to previous X position and bounce
|
|
443
|
-
this.pos.x = oldPos.x;
|
|
444
|
-
this.velocity.x *= -hitRestitution;
|
|
445
|
-
this.velocity.y *= hitFriction;
|
|
446
|
-
}
|
|
447
|
-
if (isBlockedY || !isBlockedX)
|
|
448
|
-
{
|
|
449
|
-
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
450
|
-
if (wasFalling)
|
|
451
|
-
this.groundObject = hitLayer;
|
|
452
|
-
|
|
453
|
-
// move to previous Y position and bounce
|
|
454
|
-
this.pos.y = oldPos.y;
|
|
455
|
-
this.velocity.y *= -hitRestitution;
|
|
456
|
-
this.velocity.x *= hitFriction;
|
|
457
|
-
}
|
|
458
|
-
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
442
|
+
// move to previous X position and bounce
|
|
443
|
+
this.pos.x = oldPos.x;
|
|
444
|
+
this.velocity.x *= -hitRestitution;
|
|
445
|
+
this.velocity.y *= hitFriction;
|
|
459
446
|
}
|
|
447
|
+
if (isBlockedY || !isBlockedX)
|
|
448
|
+
{
|
|
449
|
+
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
450
|
+
if (wasFalling)
|
|
451
|
+
this.groundObject = hitLayer;
|
|
452
|
+
|
|
453
|
+
// move to previous Y position and bounce
|
|
454
|
+
this.pos.y = oldPos.y;
|
|
455
|
+
this.velocity.y *= -hitRestitution;
|
|
456
|
+
this.velocity.x *= hitFriction;
|
|
457
|
+
}
|
|
458
|
+
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
460
459
|
}
|
|
461
460
|
}
|
|
462
461
|
}
|
package/src/engineSettings.js
CHANGED
|
@@ -51,7 +51,7 @@ let canvasColorTiles = true;
|
|
|
51
51
|
|
|
52
52
|
/** Color to clear the canvas to before render, does not clear if alpha is 0
|
|
53
53
|
* @type {Color}
|
|
54
|
-
* @memberof
|
|
54
|
+
* @memberof Settings */
|
|
55
55
|
let canvasClearColor = CLEAR_BLACK;
|
|
56
56
|
|
|
57
57
|
/** The max size of the canvas, centered if window is larger
|
|
@@ -251,7 +251,8 @@ let touchInputEnable = true;
|
|
|
251
251
|
let touchGamepadEnable = false;
|
|
252
252
|
|
|
253
253
|
/** True if touch gamepad should have start button in the center
|
|
254
|
-
* - Prevents activating
|
|
254
|
+
* - Prevents activating within 2*touchGamepadSize of the virtual stick or face buttons
|
|
255
|
+
* (one radius for the visible control + one radius of buffer beyond its edge)
|
|
255
256
|
* - When the game is paused, any touch will press the button
|
|
256
257
|
* - Set size to enable the center button
|
|
257
258
|
* @type {number}
|