littlejsengine 1.2.0 → 1.3.0

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.
@@ -160,7 +160,7 @@ const debugUpdate = ()=>
160
160
  {
161
161
  if (!debug)
162
162
  return;
163
-
163
+
164
164
  if (keyWasPressed(192)) // ~
165
165
  debugOverlay = !debugOverlay;
166
166
  if (debugOverlay)
@@ -273,17 +273,11 @@ const debugRender = ()=>
273
273
  drawRect(mousePos.floor().add(vec2(.5)), vec2(1), new Color(0,0,1,.5));
274
274
  drawLine(mousePos, bestObject.pos, .1, !raycastHitPos ? new Color(0,1,0,.5) : new Color(1,0,0,.5));
275
275
 
276
- let pos = mousePos.copy(), height = vec2(0,.5);
277
- const printVec2 = (v)=> '(' + (v.x>0?' ':'') + (v.x).toFixed(2) + ',' + (v.y>0?' ':'') + (v.y).toFixed(2) + ')';
278
- const args = [.5, new Color, .05, undefined, undefined, 'monospace'];
279
-
280
- drawText('pos = ' + printVec2(bestObject.pos)
281
- + (bestObject.angle>0?' ':' ') + (bestObject.angle*180/PI).toFixed(1) + '°',
282
- pos = pos.add(height), ...args);
283
- drawText('vel = ' + printVec2(bestObject.velocity), pos = pos.add(height), ...args);
284
- drawText('size = ' + printVec2(bestObject.size), pos = pos.add(height), ...args);
285
- drawText('type = ' + ( bestObject.constructor.name), pos = mousePos.subtract(height), ...args);
286
- drawText('collision = ' + getTileCollisionData(mousePos), pos = mousePos.subtract(height.scale(2)), ...args);
276
+ const debugText = 'mouse pos = ' + mousePos +
277
+ '\nmouse collision = ' + getTileCollisionData(mousePos) +
278
+ '\n\n--- object info ---\n' +
279
+ bestObject.toString();
280
+ drawTextScreen(debugText, mousePosScreen, 24, new Color, .05, undefined, undefined, 'monospace');
287
281
  mainContext = saveContext;
288
282
  }
289
283
 
@@ -292,11 +286,12 @@ const debugRender = ()=>
292
286
 
293
287
  {
294
288
  // draw debug primitives
295
- overlayContext.save();
296
289
  overlayContext.lineWidth = 2;
297
290
  const pointSize = debugPointSize * cameraScale;
298
291
  debugPrimitives.forEach(p=>
299
292
  {
293
+ overlayContext.save();
294
+
300
295
  // create canvas transform from world space to screen space
301
296
  const pos = worldToScreen(p.pos);
302
297
 
@@ -332,12 +327,12 @@ const debugRender = ()=>
332
327
  p.fill && overlayContext.fill();
333
328
  overlayContext.stroke();
334
329
  }
330
+
331
+ overlayContext.restore();
335
332
  });
336
333
 
337
334
  // remove expired pritives
338
335
  debugPrimitives = debugPrimitives.filter(r=>r.time.get()<0);
339
-
340
- overlayContext.restore();
341
336
  }
342
337
 
343
338
  {
@@ -711,7 +706,13 @@ class Vector2
711
706
  /** Returns true if this vector is within the bounds of an array size passed in
712
707
  * @param {Vector2} arraySize
713
708
  * @return {Boolean} */
714
- arrayCheck(arraySize) { return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; }
709
+ arrayCheck(arraySize) { return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; }
710
+
711
+ /** Returns this vector expressed as a string
712
+ * @param {float} digits - precision to display
713
+ * @return {String} */
714
+ toString(digits=3)
715
+ { return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`; }
715
716
  }
716
717
 
717
718
  ///////////////////////////////////////////////////////////////////////////////
@@ -782,7 +783,7 @@ class Color
782
783
  * @return {Color} */
783
784
  lerp(c, p) { return this.add(c.subtract(this).scale(clamp(p))); }
784
785
 
785
- /** Sets this color given a hue, saturation, lightness , and alpha
786
+ /** Sets this color given a hue, saturation, lightness, and alpha
786
787
  * @param {Number} [hue=0]
787
788
  * @param {Number} [saturation=0]
788
789
  * @param {Number} [lightness=1]
@@ -818,21 +819,43 @@ class Color
818
819
  ).clamp();
819
820
  }
820
821
 
821
- /** Returns this color expressed as an rgba string
822
+ /** Returns this color expressed as an CSS color value
822
823
  * @return {String} */
823
- rgba()
824
+ toString()
824
825
  {
825
826
  ASSERT(this.r>=0 && this.r<=1 && this.g>=0 && this.g<=1 && this.b>=0 && this.b<=1 && this.a>=0 && this.a<=1);
826
827
  return `rgb(${this.r*255|0},${this.g*255|0},${this.b*255|0},${this.a})`;
827
828
  }
828
829
 
829
- /** Returns this color expressed as 32 bit integer value
830
+ /** Returns this color expressed as 32 bit integer RGBA value
830
831
  * @return {Number} */
831
832
  rgbaInt()
832
833
  {
833
834
  ASSERT(this.r>=0 && this.r<=1 && this.g>=0 && this.g<=1 && this.b>=0 && this.b<=1 && this.a>=0 && this.a<=1);
834
835
  return (this.r*255|0) + (this.g*255<<8) + (this.b*255<<16) + (this.a*255<<24);
835
836
  }
837
+
838
+ /** Returns this color expressed as a hex code
839
+ * @return {String} */
840
+ hex()
841
+ {
842
+ const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
843
+ return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b);
844
+ }
845
+
846
+ /** Set this color from a hex code
847
+ * @param {String} hex - html hex code
848
+ * @return {Color} */
849
+ setHex(hex)
850
+ {
851
+ const fromHex = (a)=> parseInt(hex.slice(a,a+2), 16)/255;
852
+ this.r = fromHex(1);
853
+ this.g = fromHex(3),
854
+ this.b = fromHex(5);
855
+ this.a = 1;
856
+ ASSERT(this.r>=0 && this.r<=1 && this.g>=0 && this.g<=1 && this.b>=0 && this.b<=1);
857
+ return this;
858
+ }
836
859
  }
837
860
 
838
861
  ///////////////////////////////////////////////////////////////////////////////
@@ -869,7 +892,7 @@ class Timer
869
892
 
870
893
  /** Returns true if set and elapsed
871
894
  * @return {Boolean} */
872
- elapsed() { return time > this.time; }
895
+ elapsed() { return time > this.time; }
873
896
 
874
897
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
875
898
  * @return {Number} */
@@ -878,6 +901,10 @@ class Timer
878
901
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
879
902
  * @return {Number} */
880
903
  getPercent() { return this.isSet()? percent(this.time - time, this.setTime, 0) : 0; }
904
+
905
+ /** Returns this timer expressed as a string
906
+ * @return {String} */
907
+ toString() { return this.unset() ? 'unset' : Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ); }
881
908
  }
882
909
  /**
883
910
  * LittleJS Engine Settings
@@ -1026,6 +1053,11 @@ let inputWASDEmulateDirection = 1;
1026
1053
  * @memberof Settings */
1027
1054
  let touchGamepadEnable = 0;
1028
1055
 
1056
+ /** True if touch gamepad should be analog stick or false to use if 8 way dpad
1057
+ * @default
1058
+ * @memberof Settings */
1059
+ let touchGamepadAnalog = 1;
1060
+
1029
1061
  /** Size of virutal gamepad for touch devices in pixels
1030
1062
  * @default
1031
1063
  * @memberof Settings */
@@ -1129,7 +1161,7 @@ class EngineObject
1129
1161
  * @param {Vector2} [size=objectDefaultSize] - World space size of the object
1130
1162
  * @param {Number} [tileIndex=-1] - Tile to use to render object, untextured if -1
1131
1163
  * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
1132
- * @param {Number} [angle=0] - Angle to rotate the object
1164
+ * @param {Number} [angle=0] - Angle the object is rotated by
1133
1165
  * @param {Color} [color] - Color to apply to tile when rendered
1134
1166
  * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
1135
1167
  */
@@ -1445,6 +1477,22 @@ class EngineObject
1445
1477
  this.isSolid = isSolid;
1446
1478
  this.collideTiles = collideTiles;
1447
1479
  }
1480
+
1481
+ toString()
1482
+ {
1483
+ let text = 'type = ' + this.constructor.name;
1484
+ if (this.pos.x || this.pos.y)
1485
+ text += '\npos = ' + this.pos;
1486
+ if (this.velocity.x || this.velocity.y)
1487
+ text += '\nvelocity = ' + this.velocity;
1488
+ if (this.size.x || this.size.y)
1489
+ text += '\nsize = ' + this.size;
1490
+ if (this.angle)
1491
+ text += '\nangle = ' + this.angle.toFixed(3);
1492
+ if (this.color)
1493
+ text += '\ncolor = ' + this.color;
1494
+ return text;
1495
+ }
1448
1496
  }
1449
1497
  /**
1450
1498
  * LittleJS Drawing System
@@ -1558,7 +1606,7 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1558
1606
  if (tileIndex < 0)
1559
1607
  {
1560
1608
  // if negative tile index, force untextured
1561
- context.fillStyle = color.rgba();
1609
+ context.fillStyle = color;
1562
1610
  context.fillRect(-.5, -.5, 1, 1);
1563
1611
  }
1564
1612
  else
@@ -1663,7 +1711,8 @@ function setBlendMode(additive, useWebGL=glEnable)
1663
1711
  mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1664
1712
  }
1665
1713
 
1666
- /** Draw text on overlay canvas in world space
1714
+ /** Draw text on overlay canvas in screen space
1715
+ * Automatically splits new lines into rows
1667
1716
  * @param {String} text
1668
1717
  * @param {Vector2} pos
1669
1718
  * @param {Number} [size=1]
@@ -1672,20 +1721,37 @@ function setBlendMode(additive, useWebGL=glEnable)
1672
1721
  * @param {Color} [lineColor=new Color(0,0,0)]
1673
1722
  * @param {String} [textAlign='center']
1674
1723
  * @memberof Draw */
1675
- function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault)
1724
+ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault)
1676
1725
  {
1677
- pos = worldToScreen(pos);
1678
- overlayContext.font = size*cameraScale + 'px '+ font;
1726
+ overlayContext.fillStyle = color;
1727
+ overlayContext.lineWidth = lineWidth *= cameraScale;
1728
+ overlayContext.strokeStyle = lineColor;
1679
1729
  overlayContext.textAlign = textAlign;
1730
+ overlayContext.font = size + 'px '+ font;
1680
1731
  overlayContext.textBaseline = 'middle';
1681
- if (lineWidth)
1732
+
1733
+ pos = pos.copy();
1734
+ text.split('\n').forEach(line=>
1682
1735
  {
1683
- overlayContext.lineWidth = lineWidth*cameraScale;
1684
- overlayContext.strokeStyle = lineColor.rgba();
1685
- overlayContext.strokeText(text, pos.x, pos.y);
1686
- }
1687
- overlayContext.fillStyle = color.rgba();
1688
- overlayContext.fillText(text, pos.x, pos.y);
1736
+ lineWidth && overlayContext.strokeText(line, pos.x, pos.y);
1737
+ overlayContext.fillText(line, pos.x, pos.y);
1738
+ pos.y += size;
1739
+ });
1740
+ }
1741
+
1742
+ /** Draw text on overlay canvas in world space
1743
+ * Automatically splits new lines into rows
1744
+ * @param {String} text
1745
+ * @param {Vector2} pos
1746
+ * @param {Number} [size=1]
1747
+ * @param {Color} [color=new Color(1,1,1)]
1748
+ * @param {Number} [lineWidth=0]
1749
+ * @param {Color} [lineColor=new Color(0,0,0)]
1750
+ * @param {String} [textAlign='center']
1751
+ * @memberof Draw */
1752
+ function drawText(text, pos, size=1, color, lineWidth, lineColor, textAlign, font)
1753
+ {
1754
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth, lineColor, textAlign, font);
1689
1755
  }
1690
1756
 
1691
1757
  ///////////////////////////////////////////////////////////////////////////////
@@ -1702,6 +1768,9 @@ function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new
1702
1768
  * // draw text
1703
1769
  * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
1704
1770
  */
1771
+
1772
+ let engineFontImage;
1773
+
1705
1774
  class FontImage
1706
1775
  {
1707
1776
  /** Create an image font
@@ -1713,14 +1782,14 @@ class FontImage
1713
1782
  */
1714
1783
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), startTileIndex=0, context=overlayContext)
1715
1784
  {
1716
- if (!image)
1785
+ if (!image && !engineFontImage)
1717
1786
  {
1718
- // use default font image
1719
- image = new Image();
1720
- image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6MCIBdFY+Q0PMNgf0yCgsSAaZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWw1hPWNRcEyWh2npdpBmSUBrRaN1WSlQAWXaWLF0kkwR7C/N7XUvIaQr4xpeqeQigjLwBRZ/6KkufAACcv5z7GYhvgWHII8xN/4C2hvLCkmZSVgIJMHNTxpYOsJOiitEjUQnIQAFFP+opoQfCbjWmfzWiNzCjEvxAPFARmw/Y5Te0sl2zEp4M+m7cToJjdJS7y1xcFlFT7EtWBZ8nl7uLSff7UgYewtlyY6yHM4RrjNrnsfn/W0DBK9xNZctZ7gsTeXTe5eqLqXFlKnAn+vkNXzH6TqPW10ksKoSlauKk1TcncZLSTxNnQ8Av/8RGPXOFIuMCZ/fpwIZx5vU4QIRQ44HwUyHx/QrZhm5VtXFex7ja2se0KBpnPT0DSwS4xd86uAy+SqnLBgkPY5JolhJ33ENyK6n0VyRzCnzebIkFdY/NUtM66lTdhix1KUss6q5snH5ftVJXG0QITb1gUDvzD3z/BbyOLMirwCtPzAAAAAElFTkSuQmCC';
1787
+ // load default font image
1788
+ engineFontImage = new Image();
1789
+ engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
1721
1790
  }
1722
1791
 
1723
- this.image = image;
1792
+ this.image = image || engineFontImage;
1724
1793
  this.tileSize = tileSize;
1725
1794
  this.paddingSize = paddingSize;
1726
1795
  this.startTileIndex = startTileIndex;
@@ -2087,7 +2156,7 @@ if (isTouchDevice)
2087
2156
  // touch gamepad, virtual on screen gamepad emulator for touch devices
2088
2157
 
2089
2158
  // touch input internal variables
2090
- let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2(), touchGamepadAnalog = 1;
2159
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
2091
2160
 
2092
2161
  // create the touch gamepad, called automatically by the engine
2093
2162
  function touchGamepadCreate()
@@ -2134,7 +2203,7 @@ function touchGamepadCreate()
2134
2203
  {
2135
2204
  // virtual analog stick
2136
2205
  if (touchGamepadAnalog)
2137
- touchGamepadStick = touchPos.subtract(stickCenter).clampLength(touchGamepadSize/2).scale(2/touchGamepadSize);
2206
+ touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
2138
2207
  else
2139
2208
  {
2140
2209
  // 8 way dpad
@@ -2175,12 +2244,28 @@ function touchGamepadRender()
2175
2244
  overlayContext.lineWidth = 3;
2176
2245
 
2177
2246
  // draw left analog stick
2178
- overlayContext.fillStyle = gamepadStick(0).length() > 0 ? '#fff' : '#000';
2247
+ overlayContext.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
2179
2248
  overlayContext.beginPath();
2180
- overlayContext.arc(touchGamepadSize, mainCanvasSize.y-touchGamepadSize, touchGamepadSize/2, 0,9);
2181
- overlayContext.fill();
2182
- overlayContext.stroke();
2183
2249
 
2250
+ const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2251
+ if (touchGamepadAnalog)
2252
+ {
2253
+ overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
2254
+ overlayContext.fill();
2255
+ overlayContext.stroke();
2256
+ }
2257
+ else // draw cross shaped gamepad
2258
+ {
2259
+ for(let i=10; i--;)
2260
+ {
2261
+ const angle = i*PI/4;
2262
+ overlayContext.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
2263
+ i%2 && overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
2264
+ i==1 && overlayContext.fill();
2265
+ }
2266
+ overlayContext.stroke();
2267
+ }
2268
+
2184
2269
  // draw right face buttons
2185
2270
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2186
2271
  for (let i=4; i--;)
@@ -2821,14 +2906,14 @@ class TileLayerData
2821
2906
  */
2822
2907
  class TileLayer extends EngineObject
2823
2908
  {
2824
- /** Create a tile layer object
2825
- * @param {Vector2} [position=new Vector2()] - World space position
2826
- * @param {Vector2} [size=objectDefaultSize] - World space size
2827
- * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2828
- * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2829
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2830
- */
2831
- constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2909
+ /** Create a tile layer object
2910
+ * @param {Vector2} [position=new Vector2()] - World space position
2911
+ * @param {Vector2} [size=tileCollisionSize] - World space size
2912
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2913
+ * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2914
+ * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2915
+ */
2916
+ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2832
2917
  {
2833
2918
  super(pos, size, -1, tileSize, 0, undefined, renderOrder);
2834
2919
 
@@ -2988,7 +3073,7 @@ class TileLayer extends EngineObject
2988
3073
  if (tileIndex < 0)
2989
3074
  {
2990
3075
  // untextured
2991
- context.fillStyle = color.rgba();
3076
+ context.fillStyle = color;
2992
3077
  context.fillRect(-.5, -.5, 1, 1);
2993
3078
  }
2994
3079
  else
@@ -3007,7 +3092,8 @@ class TileLayer extends EngineObject
3007
3092
  * @param {Vector2} [size=new Vector2(1,1)]
3008
3093
  * @param {Color} [color=new Color(1,1,1)]
3009
3094
  * @param {Number} [angle=0] */
3010
- drawRect(pos, size, color, angle) { this.drawTile(pos, size, -1, 0, color, angle); }
3095
+ drawRect(pos, size, color, angle)
3096
+ { this.drawTile(pos, size, -1, 0, color, angle); }
3011
3097
  }
3012
3098
  /*
3013
3099
  LittleJS Particle System
@@ -3026,9 +3112,9 @@ class TileLayer extends EngineObject
3026
3112
  * let pos = vec2(2,3);
3027
3113
  * let particleEmiter = new ParticleEmitter
3028
3114
  * (
3029
- * pos, 1, 0, 500, PI, // pos, emitSize, emitTime, emitRate, emiteCone
3115
+ * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emiteCone
3030
3116
  * 0, vec2(16), // tileIndex, tileSize
3031
- * new Color, new Color(0,0,0), // colorStartA, colorStartB
3117
+ * new Color(1,1,1), new Color(0,0,0), // colorStartA, colorStartB
3032
3118
  * new Color(1,1,1,0), new Color(0,0,0,0), // colorEndA, colorEndB
3033
3119
  * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
3034
3120
  * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
@@ -3039,6 +3125,7 @@ class ParticleEmitter extends EngineObject
3039
3125
  {
3040
3126
  /** Create a particle system with the given settings
3041
3127
  * @param {Vector2} position - World space position of the emitter
3128
+ * @param {Number} [angle=0] - Angle to emit the particles
3042
3129
  * @param {Number} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
3043
3130
  * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
3044
3131
  * @param {Number} [emitRate=100] - How many particles per second to spawn, does not emit if 0
@@ -3068,6 +3155,7 @@ class ParticleEmitter extends EngineObject
3068
3155
  constructor
3069
3156
  (
3070
3157
  pos,
3158
+ angle,
3071
3159
  emitSize = 0,
3072
3160
  emitTime = 0,
3073
3161
  emitRate = 100,
@@ -3095,7 +3183,7 @@ class ParticleEmitter extends EngineObject
3095
3183
  renderOrder = additive ? 1e9 : 0
3096
3184
  )
3097
3185
  {
3098
- super(pos, new Vector2, tileIndex, tileSize, 0, undefined, renderOrder);
3186
+ super(pos, new Vector2, tileIndex, tileSize, angle, undefined, renderOrder);
3099
3187
 
3100
3188
  // emitter settings
3101
3189
  /** @property {Number} - World space size of the emitter (float for circle diameter, vec2 for rect) */
@@ -3328,7 +3416,7 @@ function medalsInit(saveName)
3328
3416
  {
3329
3417
  // check if medals are unlocked
3330
3418
  medalsSaveName = saveName;
3331
- debugMedals || medals.forEach(medal=> medal.unlocked = 1 || localStorage[medal.storageKey()]);
3419
+ debugMedals || medals.forEach(medal=> localStorage[medal.storageKey()]);
3332
3420
  }
3333
3421
 
3334
3422
  /**
@@ -3517,6 +3605,9 @@ class Newgrounds
3517
3605
  const scoreboardResult = this.call('ScoreBoard.getBoards');
3518
3606
  this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
3519
3607
  debugMedals && console.log(this.scoreboards);
3608
+
3609
+ const keepAliveMS = 5 * 60 * 1e3;
3610
+ setInterval(()=>this.call('Gateway.ping', 0, 1), keepAliveMS);
3520
3611
  }
3521
3612
 
3522
3613
  /** Send message to unlock a medal by id
@@ -3528,9 +3619,6 @@ class Newgrounds
3528
3619
  * @param {Number} value - The score value */
3529
3620
  postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, 1); }
3530
3621
 
3531
- /** Send message to log a view */
3532
- logView() { return this.call('App.logView', {'host':this.host}, 1); }
3533
-
3534
3622
  /** Get scores from a scoreboard
3535
3623
  * @param {Number} id - The scoreboard id
3536
3624
  * @param {String} [user=0] - A user's id or name
@@ -3542,6 +3630,9 @@ class Newgrounds
3542
3630
  getScores(id, user=0, social=0, skip=0, limit=10)
3543
3631
  { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
3544
3632
 
3633
+ /** Send message to log a view */
3634
+ logView() { return this.call('App.logView', {'host':this.host}, 1); }
3635
+
3545
3636
  /** Send a message to call a component of the Newgrounds API
3546
3637
  * @param {String} component - Name of the component
3547
3638
  * @param {Object} [parameters=0] - Parameters to use for call
@@ -3638,24 +3729,24 @@ function glInit()
3638
3729
 
3639
3730
  // setup vertex and fragment shaders
3640
3731
  glShader = glCreateProgram(
3641
- 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3642
- 'uniform mat4 m;'+ // transform matrix
3643
- 'attribute vec2 p,t;'+ // position, uv
3644
- 'attribute vec4 c,a;'+ // color, additiveColor
3645
- 'varying vec2 v;'+ // return uv
3646
- 'varying vec4 d,e;'+ // return color, additiveColor
3647
- 'void main(){'+ // shader entry point
3648
- 'gl_Position=m*vec4(p,1,1);'+// transform position
3649
- 'v=t;d=c;e=a;'+ // pass stuff to fragment shader
3650
- '}' // end of shader
3732
+ 'precision highp float;'+ // use highp for better accuracy
3733
+ 'uniform mat4 m;'+ // transform matrix
3734
+ 'attribute vec2 p,t;'+ // position, uv
3735
+ 'attribute vec4 c,a;'+ // color, additiveColor
3736
+ 'varying vec2 v;'+ // return uv
3737
+ 'varying vec4 d,e;'+ // return color, additiveColor
3738
+ 'void main(){'+ // shader entry point
3739
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
3740
+ 'v=t;d=c;e=a;'+ // pass stuff to fragment shader
3741
+ '}' // end of shader
3651
3742
  ,
3652
- 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3653
- 'varying vec2 v;'+ // uv
3654
- 'varying vec4 d,e;'+ // color, additiveColor
3655
- 'uniform sampler2D j;'+ // texture
3656
- 'void main(){'+ // shader entry point
3657
- 'gl_FragColor=texture2D(j,v)*d+e;'+ // modulate texture by color plus additive
3658
- '}' // end of shader
3743
+ 'precision highp float;'+ // use highp for better accuracy
3744
+ 'varying vec2 v;'+ // uv
3745
+ 'varying vec4 d,e;'+ // color, additiveColor
3746
+ 'uniform sampler2D s;'+ // texture
3747
+ 'void main(){'+ // shader entry point
3748
+ 'gl_FragColor=texture2D(s,v)*d+e;'+ // modulate texture by color plus additive
3749
+ '}' // end of shader
3659
3750
  );
3660
3751
 
3661
3752
  // init buffers
@@ -3867,44 +3958,44 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffff
3867
3958
 
3868
3959
  // prepare to create the verts from size and angle
3869
3960
  const c = Math.cos(angle)/2, s = Math.sin(angle)/2;
3870
- const cX = c*sizeX, cY = c*sizeY, sX = s*sizeX, sY = s*sizeY;
3961
+ const cx = c*sizeX, cy = c*sizeY, sx = s*sizeX, sy = s*sizeY;
3871
3962
 
3872
3963
  // setup 2 triangles to form a quad
3873
3964
  let offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT;
3874
3965
 
3875
3966
  // vertex 0
3876
- glPositionData[offset++] = x - cX - sY;
3877
- glPositionData[offset++] = y - cY + sX;
3967
+ glPositionData[offset++] = x - cx - sy;
3968
+ glPositionData[offset++] = y - cy + sx;
3878
3969
  glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3879
3970
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3880
3971
 
3881
3972
  // vertex 1
3882
- glPositionData[offset++] = x + cX + sY;
3883
- glPositionData[offset++] = y + cY - sX;
3973
+ glPositionData[offset++] = x + cx + sy;
3974
+ glPositionData[offset++] = y + cy - sx;
3884
3975
  glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3885
3976
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3886
3977
 
3887
3978
  // vertex 2
3888
- glPositionData[offset++] = x - cX + sY;
3889
- glPositionData[offset++] = y + cY + sX;
3979
+ glPositionData[offset++] = x - cx + sy;
3980
+ glPositionData[offset++] = y + cy + sx;
3890
3981
  glPositionData[offset++] = uv0X; glPositionData[offset++] = uv0Y;
3891
3982
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3892
3983
 
3893
3984
  // vertex 0
3894
- glPositionData[offset++] = x - cX - sY;
3895
- glPositionData[offset++] = y - cY + sX;
3985
+ glPositionData[offset++] = x - cx - sy;
3986
+ glPositionData[offset++] = y - cy + sx;
3896
3987
  glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3897
3988
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3898
3989
 
3899
3990
  // vertex 3
3900
- glPositionData[offset++] = x + cX - sY;
3901
- glPositionData[offset++] = y - cY - sX;
3991
+ glPositionData[offset++] = x + cx - sy;
3992
+ glPositionData[offset++] = y - cy - sx;
3902
3993
  glPositionData[offset++] = uv1X; glPositionData[offset++] = uv1Y;
3903
3994
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3904
3995
 
3905
3996
  // vertex 1
3906
- glPositionData[offset++] = x + cX + sY;
3907
- glPositionData[offset++] = y + cY - sX;
3997
+ glPositionData[offset++] = x + cx + sy;
3998
+ glPositionData[offset++] = y + cy - sx;
3908
3999
  glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3909
4000
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3910
4001
  }
@@ -3965,7 +4056,7 @@ gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2; // vec2 * 2 + (char * 4) * 2
3965
4056
  const engineName = 'LittleJS';
3966
4057
 
3967
4058
  /** Version of engine */
3968
- const engineVersion = '1.2.0';
4059
+ const engineVersion = '1.3.0';
3969
4060
 
3970
4061
  /** Frames per second to update objects
3971
4062
  * @default */
@@ -4002,7 +4093,7 @@ let averageFPS, drawCount;
4002
4093
  // css text used for elements created by engine
4003
4094
  const styleBody = 'margin:0;overflow:hidden;background:#000' +
4004
4095
  ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
4005
- const styleCanvas = 'position:absolute';
4096
+ const styleCanvas = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
4006
4097
 
4007
4098
  ///////////////////////////////////////////////////////////////////////////////
4008
4099
 
@@ -4046,8 +4137,6 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4046
4137
  // main update loop
4047
4138
  const engineUpdate = (frameTimeMS=0)=>
4048
4139
  {
4049
- requestAnimationFrame(engineUpdate);
4050
-
4051
4140
  // update time keeping
4052
4141
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
4053
4142
  frameTimeLastMS = frameTimeMS;
@@ -4103,24 +4192,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4103
4192
 
4104
4193
  // fit to window by adding space on top or bottom if necessary
4105
4194
  const aspect = innerWidth / innerHeight;
4106
- const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
4107
-
4108
- // clear css
4109
- mainCanvas.style.width = overlayCanvas.style.width =
4110
- mainCanvas.style.height = overlayCanvas.style.height =
4111
- mainCanvas.style.top = overlayCanvas.style.top =
4112
- mainCanvas.style.left = overlayCanvas.style.left = '';
4113
-
4114
- // letterbox center canvas
4115
- if (aspect < fixedAspect)
4195
+ const fixedAspect = mainCanvas.width / mainCanvas.height;
4196
+ mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
4197
+ mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
4198
+ if (glCanvas)
4116
4199
  {
4117
- mainCanvas.style.width = overlayCanvas.style.width = innerWidth;
4118
- mainCanvas.style.top = overlayCanvas.style.top = (innerHeight - innerWidth/fixedAspect)/2;
4119
- }
4120
- else
4121
- {
4122
- mainCanvas.style.height = overlayCanvas.style.height = innerHeight;
4123
- mainCanvas.style.left = overlayCanvas.style.left = (innerWidth - innerHeight*fixedAspect)/2;
4200
+ glCanvas.style.width = mainCanvas.style.width;
4201
+ glCanvas.style.height = mainCanvas.style.height;
4124
4202
  }
4125
4203
  }
4126
4204
  else
@@ -4128,18 +4206,6 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4128
4206
  // clear and set size to same as window
4129
4207
  mainCanvas.width = min(innerWidth, canvasMaxSize.x);
4130
4208
  mainCanvas.height = min(innerHeight, canvasMaxSize.y);
4131
-
4132
- // center canvas
4133
- overlayCanvas.style.top = mainCanvas.style.top = (innerHeight - mainCanvas.height)/2;
4134
- overlayCanvas.style.left = mainCanvas.style.left = (innerWidth - mainCanvas.width)/2;
4135
- }
4136
-
4137
- if (glEnable)
4138
- {
4139
- glCanvas.style.width = mainCanvas.style.width;
4140
- glCanvas.style.height = mainCanvas.style.height;
4141
- glCanvas.style.top = mainCanvas.style.top;
4142
- glCanvas.style.left = mainCanvas.style.left;
4143
4209
  }
4144
4210
 
4145
4211
  // render sort then render while removing destroyed objects
@@ -4169,6 +4235,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4169
4235
  overlayContext.fillText(text, mainCanvas.width-2, 2);
4170
4236
  drawCount = 0;
4171
4237
  }
4238
+
4239
+ requestAnimationFrame(engineUpdate);
4172
4240
  }
4173
4241
 
4174
4242
  // set tile image source to load the image and start the engine