littlejsengine 1.18.8 → 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 +30 -11
- package/dist/littlejs.d.ts +166 -74
- package/dist/littlejs.esm.js +610 -234
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +597 -229
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +590 -220
- package/package.json +4 -1
- package/plugins/box2d.js +17 -6
- package/plugins/medalSystem.js +2 -1
- package/plugins/newgrounds.js +22 -3
- package/plugins/pathFinder.js +40 -9
- package/plugins/pluginExport.js +4 -3
- package/plugins/postProcess.js +8 -1
- package/plugins/tweenSystem.js +33 -20
- package/plugins/uiSystem.js +21 -3
- package/src/engine.js +16 -4
- package/src/engineAudio.js +30 -15
- package/src/engineBuild.mjs +1 -1
- package/src/engineDebug.js +7 -9
- package/src/engineDraw.js +144 -33
- package/src/engineExport.js +9 -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 +67 -11
- package/src/engineWebGL.js +37 -7
package/src/engineDebug.js
CHANGED
|
@@ -83,12 +83,8 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, s
|
|
|
83
83
|
ASSERT(isNumber(time), 'time must be a number');
|
|
84
84
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
85
85
|
|
|
86
|
-
if (typeof size === 'number')
|
|
87
|
-
size = vec2(size); // allow passing in floats
|
|
88
86
|
if (isColor(color))
|
|
89
87
|
color = color.toString();
|
|
90
|
-
pos = pos.copy();
|
|
91
|
-
size = size.copy();
|
|
92
88
|
const timer = new Timer(time);
|
|
93
89
|
debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill, screenSpace});
|
|
94
90
|
}
|
|
@@ -158,7 +154,7 @@ function debugPoint(pos, color, time, angle, screenSpace=false)
|
|
|
158
154
|
* @param {number} [time]
|
|
159
155
|
* @param {boolean} [screenSpace]
|
|
160
156
|
* @memberof Debug */
|
|
161
|
-
function debugLine(posA, posB, color, width=.1, time, screenSpace=false)
|
|
157
|
+
function debugLine(posA, posB, color, width=.1, time=0, screenSpace=false)
|
|
162
158
|
{
|
|
163
159
|
ASSERT(isVector2(posA), 'posA must be a vec2');
|
|
164
160
|
ASSERT(isVector2(posB), 'posB must be a vec2');
|
|
@@ -196,7 +192,7 @@ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
|
|
|
196
192
|
debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color, time, 0, false, screenSpace);
|
|
197
193
|
}
|
|
198
194
|
|
|
199
|
-
/** Draw
|
|
195
|
+
/** Draw debug text in world space
|
|
200
196
|
* @param {string|number} text
|
|
201
197
|
* @param {Vector2} pos
|
|
202
198
|
* @param {number} [size]
|
|
@@ -502,7 +498,8 @@ function debugRender()
|
|
|
502
498
|
debugContext.fillText('FPS: ' + averageFPS.toFixed(1) + (glEnable?' WebGL':' Canvas2D'),
|
|
503
499
|
x, y += h);
|
|
504
500
|
debugContext.fillText('Objects: ' + engineObjects.length, x, y += h);
|
|
505
|
-
debugContext.fillText('Draw
|
|
501
|
+
debugContext.fillText('Draw Calls: ' + drawCount, x, y += h);
|
|
502
|
+
debugContext.fillText('Primitives: ' + primitiveCount, x, y += h);
|
|
506
503
|
debugContext.fillText('---------', x, y += h);
|
|
507
504
|
debugContext.fillStyle = '#f00';
|
|
508
505
|
debugContext.fillText('ESC: Debug Overlay', x, y += h);
|
|
@@ -526,7 +523,7 @@ function debugRender()
|
|
|
526
523
|
continue;
|
|
527
524
|
if (parseInt(i) < 3)
|
|
528
525
|
mousePressed += i + ' ' ;
|
|
529
|
-
else
|
|
526
|
+
else
|
|
530
527
|
keysPressed += i + ' ' ;
|
|
531
528
|
}
|
|
532
529
|
mousePressed && debugContext.fillText('Mouse: ' + mousePressed, x, y += h);
|
|
@@ -573,7 +570,8 @@ function debugRenderPost()
|
|
|
573
570
|
mainContext.font = '1em monospace';
|
|
574
571
|
mainContext.fillStyle = '#000';
|
|
575
572
|
const text = engineName + ' v' + engineVersion + ' / '
|
|
576
|
-
+ drawCount + ' / ' +
|
|
573
|
+
+ drawCount + ' / ' + primitiveCount + ' / '
|
|
574
|
+
+ engineObjects.length + ' / ' + averageFPS.toFixed(1)
|
|
577
575
|
+ (glEnable ? ' GL' : ' 2D') ;
|
|
578
576
|
mainContext.fillText(text, mainCanvas.width-3, 3);
|
|
579
577
|
mainContext.fillStyle = '#fff';
|
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
|
|
@@ -72,6 +72,12 @@ let textureInfos = [];
|
|
|
72
72
|
* @memberof Draw */
|
|
73
73
|
let drawCount;
|
|
74
74
|
|
|
75
|
+
/** Keeps track of how many primitives were drawn each frame for debugging
|
|
76
|
+
* A single draw call can render many primitives (e.g. a WebGL sprite batch).
|
|
77
|
+
* @type {number}
|
|
78
|
+
* @memberof Draw */
|
|
79
|
+
let primitiveCount;
|
|
80
|
+
|
|
75
81
|
// internal predicates for tint short-circuiting in canvas2D draw paths
|
|
76
82
|
// isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
|
|
77
83
|
// isBlack includes alpha so additive colors that only contribute alpha are not skipped
|
|
@@ -96,7 +102,7 @@ let drawCount;
|
|
|
96
102
|
* tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
|
|
97
103
|
* tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
|
|
98
104
|
* @memberof Draw */
|
|
99
|
-
function tile(index=
|
|
105
|
+
function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
100
106
|
{
|
|
101
107
|
ASSERT(isVector2(index) || typeof index === 'number', 'index must be a vec2 or number');
|
|
102
108
|
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
@@ -147,8 +153,8 @@ class TileInfo
|
|
|
147
153
|
* @param {Vector2} [pos=vec2()] - Top left corner of tile in pixels
|
|
148
154
|
* @param {Vector2} [size] - Size of tile in pixels
|
|
149
155
|
* @param {TextureInfo} [textureInfo] - Texture info to use
|
|
150
|
-
* @param {number} [padding] - How many pixels padding around
|
|
151
|
-
* @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)
|
|
152
158
|
*/
|
|
153
159
|
constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
154
160
|
{
|
|
@@ -180,7 +186,7 @@ class TileInfo
|
|
|
180
186
|
ASSERT(typeof frame === 'number');
|
|
181
187
|
const w = this.size.x + this.padding*2;
|
|
182
188
|
const x = frame*w;
|
|
183
|
-
ASSERT(x
|
|
189
|
+
ASSERT(x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
|
|
184
190
|
return this.offset(new Vector2(x));
|
|
185
191
|
}
|
|
186
192
|
|
|
@@ -269,7 +275,7 @@ class TextureInfo
|
|
|
269
275
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
270
276
|
* @memberof Draw */
|
|
271
277
|
function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
272
|
-
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
|
|
278
|
+
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace=false, context)
|
|
273
279
|
{
|
|
274
280
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
275
281
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -312,19 +318,17 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
312
318
|
}
|
|
313
319
|
else
|
|
314
320
|
{
|
|
315
|
-
//
|
|
316
|
-
// texture is bound doesn't leak in) and folding color+additive
|
|
317
|
-
// into the additive slot — matches the Canvas2D path's
|
|
321
|
+
// untextured: fold color+additive to match the Canvas2D path's
|
|
318
322
|
// color.add(additiveColor) on line ~337.
|
|
319
323
|
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
320
|
-
|
|
321
|
-
0, combined.rgbaInt());
|
|
324
|
+
glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
|
|
322
325
|
}
|
|
323
326
|
}
|
|
324
327
|
else
|
|
325
328
|
{
|
|
326
329
|
// normal canvas 2D rendering method (slower)
|
|
327
330
|
++drawCount;
|
|
331
|
+
++primitiveCount;
|
|
328
332
|
size = new Vector2(size.x, -size.y); // flip upside down sprites
|
|
329
333
|
drawCanvas2D(pos, size, angle, mirror, (context)=>
|
|
330
334
|
{
|
|
@@ -364,13 +368,13 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
364
368
|
* @param {Vector2} pos
|
|
365
369
|
* @param {Vector2} [size=vec2(1)]
|
|
366
370
|
* @param {Color} [colorTop=WHITE]
|
|
367
|
-
* @param {Color} [colorBottom=
|
|
371
|
+
* @param {Color} [colorBottom=CLEAR_WHITE]
|
|
368
372
|
* @param {number} [angle]
|
|
369
373
|
* @param {boolean} [useWebGL=glEnable]
|
|
370
374
|
* @param {boolean} [screenSpace]
|
|
371
375
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
372
376
|
* @memberof Draw */
|
|
373
|
-
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=
|
|
377
|
+
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
374
378
|
{
|
|
375
379
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
376
380
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -410,6 +414,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
410
414
|
{
|
|
411
415
|
// normal canvas 2D rendering method (slower)
|
|
412
416
|
++drawCount;
|
|
417
|
+
++primitiveCount;
|
|
413
418
|
size = new Vector2(size.x, -size.y); // fix upside down sprites
|
|
414
419
|
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
415
420
|
{
|
|
@@ -472,8 +477,9 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
472
477
|
return;
|
|
473
478
|
}
|
|
474
479
|
|
|
475
|
-
// Canvas2D path — increment
|
|
480
|
+
// Canvas2D path — increment counts here (WebGL counts via glFlush)
|
|
476
481
|
++drawCount;
|
|
482
|
+
++primitiveCount;
|
|
477
483
|
|
|
478
484
|
if (!screenSpace)
|
|
479
485
|
{
|
|
@@ -526,7 +532,7 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
526
532
|
* @param {boolean} [screenSpace]
|
|
527
533
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
528
534
|
* @memberof Draw */
|
|
529
|
-
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)
|
|
530
536
|
{
|
|
531
537
|
ASSERT(isArray(points), 'points must be an array');
|
|
532
538
|
ASSERT(isNumber(width), 'width must be a number');
|
|
@@ -547,6 +553,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
547
553
|
{
|
|
548
554
|
// normal canvas 2D rendering method (slower)
|
|
549
555
|
++drawCount;
|
|
556
|
+
++primitiveCount;
|
|
550
557
|
drawCanvas2D(pos, vec2(1), angle, false, (context)=>
|
|
551
558
|
{
|
|
552
559
|
context.strokeStyle = color.toString();
|
|
@@ -574,7 +581,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
574
581
|
* @param {boolean} [screenSpace]
|
|
575
582
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
576
583
|
* @memberof Draw */
|
|
577
|
-
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)
|
|
578
585
|
{
|
|
579
586
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
580
587
|
const size = vec2(width, halfDelta.length()*2);
|
|
@@ -590,9 +597,9 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
|
|
|
590
597
|
* @param {Vector2} [size=vec2(1)]
|
|
591
598
|
* @param {number} [sides]
|
|
592
599
|
* @param {Color} [color=WHITE]
|
|
593
|
-
* @param {number} [angle]
|
|
594
600
|
* @param {number} [lineWidth]
|
|
595
601
|
* @param {Color} [lineColor=BLACK]
|
|
602
|
+
* @param {number} [angle]
|
|
596
603
|
* @param {boolean} [useWebGL=glEnable]
|
|
597
604
|
* @param {boolean} [screenSpace]
|
|
598
605
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -685,7 +692,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
685
692
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
686
693
|
|
|
687
694
|
// clamp line width to prevent artifacts
|
|
688
|
-
lineWidth = clamp(lineWidth, 0,
|
|
695
|
+
lineWidth = clamp(lineWidth, 0, min(size.x, size.y));
|
|
689
696
|
|
|
690
697
|
if (useWebGL && glEnable)
|
|
691
698
|
{
|
|
@@ -727,6 +734,104 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
|
|
|
727
734
|
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
728
735
|
}
|
|
729
736
|
|
|
737
|
+
/** Draw an ellipse filled with a radial gradient from the center to the rim
|
|
738
|
+
* - Best when batched with other untextured polys
|
|
739
|
+
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
740
|
+
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
741
|
+
* @param {Vector2} pos
|
|
742
|
+
* @param {Vector2} [size=vec2(1)] - Width and height diameter
|
|
743
|
+
* @param {Color} [colorInner=WHITE]
|
|
744
|
+
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
745
|
+
* @param {number} [angle]
|
|
746
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
747
|
+
* @param {boolean} [screenSpace]
|
|
748
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
749
|
+
* @memberof Draw */
|
|
750
|
+
let drawEllipseGradientOffset = 0;
|
|
751
|
+
function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
752
|
+
{
|
|
753
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
754
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
755
|
+
ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
|
|
756
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
757
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
758
|
+
|
|
759
|
+
if (headlessMode) return;
|
|
760
|
+
|
|
761
|
+
if (useWebGL && glEnable)
|
|
762
|
+
{
|
|
763
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
764
|
+
if (screenSpace)
|
|
765
|
+
{
|
|
766
|
+
// convert to world space
|
|
767
|
+
pos = screenToWorld(pos);
|
|
768
|
+
size = size.scale(1/cameraScale);
|
|
769
|
+
angle += cameraAngle;
|
|
770
|
+
}
|
|
771
|
+
// fan as tristrip; rotate the boundary vertex by one slice per call
|
|
772
|
+
// so back-to-back gradients at the same position have their hole
|
|
773
|
+
// (from gpu edge-rule on the boundary line-degen) at different rim
|
|
774
|
+
// verts and don't visibly stack
|
|
775
|
+
const sides = glCircleSides;
|
|
776
|
+
const radiusX = size.x/2, radiusY = size.y/2;
|
|
777
|
+
const innerInt = colorInner.rgbaInt();
|
|
778
|
+
const outerInt = colorOuter.rgbaInt();
|
|
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
|
+
};
|
|
786
|
+
const startA = (offset%sides)/sides*PI*2;
|
|
787
|
+
const points = [rim(startA)];
|
|
788
|
+
const colors = [outerInt];
|
|
789
|
+
for (let i=sides; i--;)
|
|
790
|
+
{
|
|
791
|
+
const a = ((i+offset)%sides)/sides*PI*2;
|
|
792
|
+
points.push(pos);
|
|
793
|
+
colors.push(innerInt);
|
|
794
|
+
points.push(rim(a));
|
|
795
|
+
colors.push(outerInt);
|
|
796
|
+
}
|
|
797
|
+
glDrawColoredPoints(points, colors);
|
|
798
|
+
}
|
|
799
|
+
else
|
|
800
|
+
{
|
|
801
|
+
// normal canvas 2D rendering method (slower)
|
|
802
|
+
++drawCount;
|
|
803
|
+
++primitiveCount;
|
|
804
|
+
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
805
|
+
{
|
|
806
|
+
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
|
|
807
|
+
gradient.addColorStop(0, colorInner.toString());
|
|
808
|
+
gradient.addColorStop(1, colorOuter.toString());
|
|
809
|
+
context.fillStyle = gradient;
|
|
810
|
+
context.beginPath();
|
|
811
|
+
context.ellipse(0, 0, .5, .5, 0, 0, 9);
|
|
812
|
+
context.fill();
|
|
813
|
+
}, screenSpace, context);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
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
|
+
|
|
730
835
|
/**
|
|
731
836
|
* @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
|
|
732
837
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
@@ -781,7 +886,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
781
886
|
* @param {number} [angle]
|
|
782
887
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
783
888
|
* @memberof Draw */
|
|
784
|
-
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)
|
|
785
890
|
{
|
|
786
891
|
// convert to screen space
|
|
787
892
|
pos = worldToScreen(pos);
|
|
@@ -821,16 +926,16 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
|
|
|
821
926
|
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
822
927
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
823
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();
|
|
824
933
|
context.fillStyle = color.toString();
|
|
825
934
|
context.strokeStyle = lineColor.toString();
|
|
826
935
|
context.lineWidth = lineWidth;
|
|
827
936
|
context.textAlign = textAlign;
|
|
828
937
|
context.font = fontStyle + ' ' + size + 'px '+ font;
|
|
829
938
|
context.textBaseline = 'middle';
|
|
830
|
-
|
|
831
|
-
const lines = (text+'').split('\n');
|
|
832
|
-
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
833
|
-
context.save();
|
|
834
939
|
context.translate(pos.x, posY);
|
|
835
940
|
context.rotate(-angle);
|
|
836
941
|
let yOffset = 0;
|
|
@@ -991,6 +1096,9 @@ function isOnScreen(pos, size=0)
|
|
|
991
1096
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
992
1097
|
ASSERT(isVector2(size) || isNumber(size), 'size must be a vec2 or number');
|
|
993
1098
|
|
|
1099
|
+
// cameraScale of 0 collapses world coords; nothing is visible
|
|
1100
|
+
if (!cameraScale) return false;
|
|
1101
|
+
|
|
994
1102
|
// optimized circle on screen test
|
|
995
1103
|
// pos = worldToScreen(pos);
|
|
996
1104
|
let x = pos.x - cameraPos.x;
|
|
@@ -1032,7 +1140,10 @@ function combineCanvases()
|
|
|
1032
1140
|
const w = mainCanvasSize.x, h = mainCanvasSize.y;
|
|
1033
1141
|
workCanvas.width = w;
|
|
1034
1142
|
workCanvas.height = h;
|
|
1035
|
-
|
|
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);
|
|
1036
1147
|
glCopyToContext(workContext);
|
|
1037
1148
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
1038
1149
|
mainContext.drawImage(workCanvas, 0, 0);
|
|
@@ -1175,33 +1286,33 @@ function setCursor(cursorStyle = 'auto')
|
|
|
1175
1286
|
///////////////////////////////////////////////////////////////////////////////
|
|
1176
1287
|
|
|
1177
1288
|
/** Engine font image, 8x8 font provided by the engine
|
|
1178
|
-
* @type {
|
|
1289
|
+
* @type {ImageFont}
|
|
1179
1290
|
* @memberof Draw */
|
|
1180
|
-
let
|
|
1291
|
+
let engineImageFont;
|
|
1181
1292
|
|
|
1182
1293
|
/**
|
|
1183
|
-
* Font
|
|
1294
|
+
* Image Font Object - Draw text by using tiles in an image
|
|
1184
1295
|
* - 96 characters (from space to tilde) are stored in an image
|
|
1185
1296
|
* - A 8x8 default engine font is supplied for general use
|
|
1186
1297
|
* - This system is WebGL enabled for fast text rendering
|
|
1187
1298
|
* - Fonts can also be colored and scaled along each axis
|
|
1188
|
-
*
|
|
1299
|
+
*
|
|
1189
1300
|
* @memberof Draw
|
|
1190
1301
|
* @example
|
|
1191
1302
|
* // use built in font
|
|
1192
|
-
* const font =
|
|
1303
|
+
* const font = engineImageFont;
|
|
1193
1304
|
*
|
|
1194
1305
|
* // draw text
|
|
1195
1306
|
* font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
|
|
1196
1307
|
*/
|
|
1197
|
-
class
|
|
1308
|
+
class ImageFont
|
|
1198
1309
|
{
|
|
1199
1310
|
/** Create an image font
|
|
1200
1311
|
* @param {TileInfo} tileInfo - Tile info of first character in font
|
|
1201
1312
|
*/
|
|
1202
1313
|
constructor(tileInfo)
|
|
1203
1314
|
{
|
|
1204
|
-
ASSERT(!!tileInfo, 'tileInfo is required for
|
|
1315
|
+
ASSERT(!!tileInfo, 'tileInfo is required for ImageFont');
|
|
1205
1316
|
|
|
1206
1317
|
/** @property {TileInfo} - Tile info for the font */
|
|
1207
1318
|
this.tileInfo = tileInfo.frame(0);
|
|
@@ -1286,7 +1397,7 @@ class FontImage
|
|
|
1286
1397
|
}
|
|
1287
1398
|
|
|
1288
1399
|
// load engine font, called automatically on startup
|
|
1289
|
-
async function
|
|
1400
|
+
async function imageFontInit()
|
|
1290
1401
|
{
|
|
1291
1402
|
const image = new Image;
|
|
1292
1403
|
await new Promise(resolve =>
|
|
@@ -1299,5 +1410,5 @@ async function fontImageInit()
|
|
|
1299
1410
|
const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
|
|
1300
1411
|
const textureInfo = new TextureInfo(image);
|
|
1301
1412
|
const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
|
|
1302
|
-
|
|
1413
|
+
engineImageFont = new ImageFont(tileInfo);
|
|
1303
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,
|
|
@@ -187,6 +188,8 @@ export
|
|
|
187
188
|
shareURL,
|
|
188
189
|
readSaveData,
|
|
189
190
|
writeSaveData,
|
|
191
|
+
noise1D,
|
|
192
|
+
noise2D,
|
|
190
193
|
|
|
191
194
|
// Random
|
|
192
195
|
rand,
|
|
@@ -240,6 +243,7 @@ export
|
|
|
240
243
|
mainCanvasSize,
|
|
241
244
|
textureInfos,
|
|
242
245
|
drawCount,
|
|
246
|
+
primitiveCount,
|
|
243
247
|
screenToWorld,
|
|
244
248
|
worldToScreen,
|
|
245
249
|
screenToWorldDelta,
|
|
@@ -255,13 +259,15 @@ export
|
|
|
255
259
|
drawRegularPoly,
|
|
256
260
|
drawEllipse,
|
|
257
261
|
drawCircle,
|
|
262
|
+
drawEllipseGradient,
|
|
263
|
+
drawCircleGradient,
|
|
258
264
|
drawCanvas2D,
|
|
259
265
|
drawText,
|
|
260
266
|
drawTextScreen,
|
|
261
267
|
setBlendMode,
|
|
262
268
|
combineCanvases,
|
|
263
|
-
|
|
264
|
-
|
|
269
|
+
engineImageFont,
|
|
270
|
+
ImageFont,
|
|
265
271
|
isFullscreen,
|
|
266
272
|
toggleFullscreen,
|
|
267
273
|
setCursor,
|
|
@@ -284,6 +290,7 @@ export
|
|
|
284
290
|
glCopyToContext,
|
|
285
291
|
glSetAntialias,
|
|
286
292
|
glDraw,
|
|
293
|
+
glDrawUntextured,
|
|
287
294
|
glDrawPointsTransform,
|
|
288
295
|
glDrawOutlineTransform,
|
|
289
296
|
glDrawPoints,
|
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} */
|