littlejsengine 1.18.12 → 1.18.17

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/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
@@ -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=new Vector2, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
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 tiles
157
- * @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)
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 < this.textureInfo.size.x, 'frame extends beyond texture width!');
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: glDrawUntextured picks the optimal path (poly
322
- // tristrip if already in poly mode, otherwise instanced with
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
  }
@@ -331,11 +329,12 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
331
329
  // normal canvas 2D rendering method (slower)
332
330
  ++drawCount;
333
331
  ++primitiveCount;
334
- size = new Vector2(size.x, -size.y); // flip upside down sprites
335
332
  drawCanvas2D(pos, size, angle, mirror, (context)=>
336
333
  {
337
334
  if (textureInfo)
338
335
  {
336
+ // un-flip Y so the image renders right-side up under drawCanvas2D's Y flip
337
+ context.scale(1, -1);
339
338
  // calculate uvs and render
340
339
  const x = tileInfo.pos.x, y = tileInfo.pos.y;
341
340
  const w = tileInfo.size.x, h = tileInfo.size.y;
@@ -343,7 +342,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
343
342
  }
344
343
  else
345
344
  {
346
- // if no tile info, use untextured rect
345
+ // if no tile info, use untextured rect (Y-symmetric, no compensation needed)
347
346
  const c = additiveColor ? color.add(additiveColor) : color;
348
347
  context.fillStyle = c.toString();
349
348
  context.fillRect(-.5, -.5, 1, 1);
@@ -417,11 +416,10 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, an
417
416
  // normal canvas 2D rendering method (slower)
418
417
  ++drawCount;
419
418
  ++primitiveCount;
420
- size = new Vector2(size.x, -size.y); // fix upside down sprites
421
419
  drawCanvas2D(pos, size, angle, false, (context)=>
422
420
  {
423
- // if no tile info, use untextured rect
424
- const gradient = context.createLinearGradient(0, -.5, 0, .5);
421
+ // gradient endpoints are flipped to match the Y flip inside drawCanvas2D
422
+ const gradient = context.createLinearGradient(0, .5, 0, -.5);
425
423
  gradient.addColorStop(0, colorTop.toString());
426
424
  gradient.addColorStop(1, colorBottom.toString());
427
425
  context.fillStyle = gradient;
@@ -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, Math.min(size.x, size.y));
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 a circle filled with a radial gradient from the center to the rim
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 {number} [size=1] - Diameter
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 drawCircleGradientOffset = 0;
752
- function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
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(isNumber(size), 'size must be a number');
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 /= cameraScale;
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 radius = size/2;
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 = drawCircleGradientOffset++;
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 = [vec2(pos.x + sin(startA)*radius, pos.y + cos(startA)*radius)];
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(vec2(pos.x + sin(a)*radius, pos.y + cos(a)*radius));
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, vec2(size), 0, false, (context)=>
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,13 +814,34 @@ 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
813
838
  * @memberof Draw
814
839
  */
815
840
 
816
- /** Draw directly to a 2d canvas context in world space
841
+ /** Draw directly to a 2d canvas context in world space.
842
+ * The Y axis is flipped so world-Y-up coordinates render right-side up
843
+ * (matches the WebGL path). Callers whose drawing depends on Y direction
844
+ * (e.g. linear gradients) should flip their own Y endpoints accordingly.
817
845
  * @param {Vector2} pos
818
846
  * @param {Vector2} size
819
847
  * @param {number} angle
@@ -861,7 +889,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
861
889
  * @param {number} [angle]
862
890
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
863
891
  * @memberof Draw */
864
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
892
+ function drawText(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
865
893
  {
866
894
  // convert to screen space
867
895
  pos = worldToScreen(pos);
@@ -901,16 +929,16 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
901
929
  ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
902
930
  ASSERT(isNumber(angle), 'angle must be a number');
903
931
 
932
+ const lines = (text+'').split('\n');
933
+ const posY = pos.y - (lines.length-1) * size/2; // center vertically
934
+ // save before style mutations so caller's context state is preserved
935
+ context.save();
904
936
  context.fillStyle = color.toString();
905
937
  context.strokeStyle = lineColor.toString();
906
938
  context.lineWidth = lineWidth;
907
939
  context.textAlign = textAlign;
908
940
  context.font = fontStyle + ' ' + size + 'px '+ font;
909
941
  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
942
  context.translate(pos.x, posY);
915
943
  context.rotate(-angle);
916
944
  let yOffset = 0;
@@ -1071,6 +1099,9 @@ function isOnScreen(pos, size=0)
1071
1099
  ASSERT(isVector2(pos), 'pos must be a vec2');
1072
1100
  ASSERT(isVector2(size) || isNumber(size), 'size must be a vec2 or number');
1073
1101
 
1102
+ // cameraScale of 0 collapses world coords; nothing is visible
1103
+ if (!cameraScale) return false;
1104
+
1074
1105
  // optimized circle on screen test
1075
1106
  // pos = worldToScreen(pos);
1076
1107
  let x = pos.x - cameraPos.x;
@@ -1112,7 +1143,10 @@ function combineCanvases()
1112
1143
  const w = mainCanvasSize.x, h = mainCanvasSize.y;
1113
1144
  workCanvas.width = w;
1114
1145
  workCanvas.height = h;
1115
- workContext.fillRect(0,0,w,h); // remove background alpha
1146
+ // remove background alpha — explicit fillStyle so a previous caller
1147
+ // leaving workContext.fillStyle transparent can't silently no-op this
1148
+ workContext.fillStyle = '#000';
1149
+ workContext.fillRect(0,0,w,h);
1116
1150
  glCopyToContext(workContext);
1117
1151
  workContext.drawImage(mainCanvas, 0, 0);
1118
1152
  mainContext.drawImage(workCanvas, 0, 0);
@@ -1255,33 +1289,33 @@ function setCursor(cursorStyle = 'auto')
1255
1289
  ///////////////////////////////////////////////////////////////////////////////
1256
1290
 
1257
1291
  /** Engine font image, 8x8 font provided by the engine
1258
- * @type {FontImage}
1292
+ * @type {ImageFont}
1259
1293
  * @memberof Draw */
1260
- let engineFontImage;
1294
+ let engineImageFont;
1261
1295
 
1262
1296
  /**
1263
- * Font Image Object - Draw text by using tiles in an image
1297
+ * Image Font Object - Draw text by using tiles in an image
1264
1298
  * - 96 characters (from space to tilde) are stored in an image
1265
1299
  * - A 8x8 default engine font is supplied for general use
1266
1300
  * - This system is WebGL enabled for fast text rendering
1267
1301
  * - Fonts can also be colored and scaled along each axis
1268
- *
1302
+ *
1269
1303
  * @memberof Draw
1270
1304
  * @example
1271
1305
  * // use built in font
1272
- * const font = engineFontImage;
1306
+ * const font = engineImageFont;
1273
1307
  *
1274
1308
  * // draw text
1275
1309
  * font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
1276
1310
  */
1277
- class FontImage
1311
+ class ImageFont
1278
1312
  {
1279
1313
  /** Create an image font
1280
1314
  * @param {TileInfo} tileInfo - Tile info of first character in font
1281
1315
  */
1282
1316
  constructor(tileInfo)
1283
1317
  {
1284
- ASSERT(!!tileInfo, 'tileInfo is required for FontImage');
1318
+ ASSERT(!!tileInfo, 'tileInfo is required for ImageFont');
1285
1319
 
1286
1320
  /** @property {TileInfo} - Tile info for the font */
1287
1321
  this.tileInfo = tileInfo.frame(0);
@@ -1366,7 +1400,7 @@ class FontImage
1366
1400
  }
1367
1401
 
1368
1402
  // load engine font, called automatically on startup
1369
- async function fontImageInit()
1403
+ async function imageFontInit()
1370
1404
  {
1371
1405
  const image = new Image;
1372
1406
  await new Promise(resolve =>
@@ -1379,5 +1413,5 @@ async function fontImageInit()
1379
1413
  const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
1380
1414
  const textureInfo = new TextureInfo(image);
1381
1415
  const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
1382
- engineFontImage = new FontImage(tileInfo);
1416
+ engineImageFont = new ImageFont(tileInfo);
1383
1417
  }
@@ -85,12 +85,18 @@ export
85
85
  inputWASDEmulateDirection,
86
86
  touchInputEnable,
87
87
  touchGamepadEnable,
88
+ touchGamepadPassthrough,
88
89
  touchGamepadCenterButtonSize,
89
90
  touchGamepadButtonCount,
91
+ touchGamepadLeftStick,
92
+ touchGamepadLeftButtonCount,
93
+ touchGamepadRightStick,
90
94
  touchGamepadAnalog,
95
+ touchGamepadFloating,
91
96
  touchGamepadSize,
92
97
  touchGamepadAlpha,
93
98
  touchGamepadDisplayTime,
99
+ touchGamepadVibration,
94
100
  vibrateEnable,
95
101
  soundEnable,
96
102
  soundVolume,
@@ -133,11 +139,18 @@ export
133
139
  setGamepadDirectionEmulateStick,
134
140
  setInputWASDEmulateDirection,
135
141
  setTouchGamepadEnable,
142
+ setTouchGamepadPassthrough,
136
143
  setTouchGamepadCenterButtonSize,
137
144
  setTouchGamepadButtonCount,
145
+ setTouchGamepadLeftStick,
146
+ setTouchGamepadLeftButtonCount,
147
+ setTouchGamepadRightStick,
138
148
  setTouchGamepadAnalog,
149
+ setTouchGamepadFloating,
139
150
  setTouchGamepadSize,
140
151
  setTouchGamepadAlpha,
152
+ setTouchGamepadDisplayTime,
153
+ setTouchGamepadVibration,
141
154
  setVibrateEnable,
142
155
  setSoundEnable,
143
156
  setSoundVolume,
@@ -258,14 +271,15 @@ export
258
271
  drawRegularPoly,
259
272
  drawEllipse,
260
273
  drawCircle,
274
+ drawEllipseGradient,
261
275
  drawCircleGradient,
262
276
  drawCanvas2D,
263
277
  drawText,
264
278
  drawTextScreen,
265
279
  setBlendMode,
266
280
  combineCanvases,
267
- engineFontImage,
268
- FontImage,
281
+ engineImageFont,
282
+ ImageFont,
269
283
  isFullscreen,
270
284
  toggleFullscreen,
271
285
  setCursor,