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.
@@ -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 a debug axis aligned bounding box in world space
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 Count: ' + drawCount, x, y += h);
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 if (keyIsDown(i, 0))
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 + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
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 FontImage support
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=new Vector2, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
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 tiles
151
- * @param {number} [bleed] - How many pixels smaller to draw tiles
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 < this.textureInfo.size.x, 'frame extends beyond texture width!');
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
- // if no tile info, force untextured by zeroing rgba (so whatever
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
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0,
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=BLACK]
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=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
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 drawCount here (WebGL batch counts via glBatchCount)
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, Math.min(size.x, size.y));
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
- workContext.fillRect(0,0,w,h); // remove background alpha
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 {FontImage}
1289
+ * @type {ImageFont}
1179
1290
  * @memberof Draw */
1180
- let engineFontImage;
1291
+ let engineImageFont;
1181
1292
 
1182
1293
  /**
1183
- * Font Image Object - Draw text by using tiles in an image
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 = engineFontImage;
1303
+ * const font = engineImageFont;
1193
1304
  *
1194
1305
  * // draw text
1195
1306
  * font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
1196
1307
  */
1197
- class FontImage
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 FontImage');
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 fontImageInit()
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
- engineFontImage = new FontImage(tileInfo);
1413
+ engineImageFont = new ImageFont(tileInfo);
1303
1414
  }
@@ -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
- engineFontImage,
264
- FontImage,
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,
@@ -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
- mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
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) < touchGamepadCenterButtonSize)
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]}%`:'#000';
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
- return dx >= -sx && dx < sx && dy >= -sy && dy < sy;
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 {boolean}
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
- { return radius > 0 ? randVec2(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
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) { return this.float(valueA, valueB) * this.sign(); }
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} */