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.
@@ -344,7 +344,13 @@ class Vector2
344
344
  /** Returns true if this vector is within the bounds of an array size passed in
345
345
  * @param {Vector2} arraySize
346
346
  * @return {Boolean} */
347
- arrayCheck(arraySize) { return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; }
347
+ arrayCheck(arraySize) { return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; }
348
+
349
+ /** Returns this vector expressed as a string
350
+ * @param {float} digits - precision to display
351
+ * @return {String} */
352
+ toString(digits=3)
353
+ { return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`; }
348
354
  }
349
355
 
350
356
  ///////////////////////////////////////////////////////////////////////////////
@@ -415,7 +421,7 @@ class Color
415
421
  * @return {Color} */
416
422
  lerp(c, p) { return this.add(c.subtract(this).scale(clamp(p))); }
417
423
 
418
- /** Sets this color given a hue, saturation, lightness , and alpha
424
+ /** Sets this color given a hue, saturation, lightness, and alpha
419
425
  * @param {Number} [hue=0]
420
426
  * @param {Number} [saturation=0]
421
427
  * @param {Number} [lightness=1]
@@ -451,21 +457,43 @@ class Color
451
457
  ).clamp();
452
458
  }
453
459
 
454
- /** Returns this color expressed as an rgba string
460
+ /** Returns this color expressed as an CSS color value
455
461
  * @return {String} */
456
- rgba()
462
+ toString()
457
463
  {
458
464
  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);
459
465
  return `rgb(${this.r*255|0},${this.g*255|0},${this.b*255|0},${this.a})`;
460
466
  }
461
467
 
462
- /** Returns this color expressed as 32 bit integer value
468
+ /** Returns this color expressed as 32 bit integer RGBA value
463
469
  * @return {Number} */
464
470
  rgbaInt()
465
471
  {
466
472
  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);
467
473
  return (this.r*255|0) + (this.g*255<<8) + (this.b*255<<16) + (this.a*255<<24);
468
474
  }
475
+
476
+ /** Returns this color expressed as a hex code
477
+ * @return {String} */
478
+ hex()
479
+ {
480
+ const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
481
+ return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b);
482
+ }
483
+
484
+ /** Set this color from a hex code
485
+ * @param {String} hex - html hex code
486
+ * @return {Color} */
487
+ setHex(hex)
488
+ {
489
+ const fromHex = (a)=> parseInt(hex.slice(a,a+2), 16)/255;
490
+ this.r = fromHex(1);
491
+ this.g = fromHex(3),
492
+ this.b = fromHex(5);
493
+ this.a = 1;
494
+ ASSERT(this.r>=0 && this.r<=1 && this.g>=0 && this.g<=1 && this.b>=0 && this.b<=1);
495
+ return this;
496
+ }
469
497
  }
470
498
 
471
499
  ///////////////////////////////////////////////////////////////////////////////
@@ -502,7 +530,7 @@ class Timer
502
530
 
503
531
  /** Returns true if set and elapsed
504
532
  * @return {Boolean} */
505
- elapsed() { return time > this.time; }
533
+ elapsed() { return time > this.time; }
506
534
 
507
535
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
508
536
  * @return {Number} */
@@ -511,6 +539,10 @@ class Timer
511
539
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
512
540
  * @return {Number} */
513
541
  getPercent() { return this.isSet()? percent(this.time - time, this.setTime, 0) : 0; }
542
+
543
+ /** Returns this timer expressed as a string
544
+ * @return {String} */
545
+ toString() { return this.unset() ? 'unset' : Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ); }
514
546
  }
515
547
  /**
516
548
  * LittleJS Engine Settings
@@ -659,6 +691,11 @@ let inputWASDEmulateDirection = 1;
659
691
  * @memberof Settings */
660
692
  let touchGamepadEnable = 0;
661
693
 
694
+ /** True if touch gamepad should be analog stick or false to use if 8 way dpad
695
+ * @default
696
+ * @memberof Settings */
697
+ let touchGamepadAnalog = 1;
698
+
662
699
  /** Size of virutal gamepad for touch devices in pixels
663
700
  * @default
664
701
  * @memberof Settings */
@@ -748,7 +785,7 @@ let medalDisplayIconSize = 50;
748
785
  const engineName = 'LittleJS';
749
786
 
750
787
  /** Version of engine */
751
- const engineVersion = '1.2.0';
788
+ const engineVersion = '1.3.0';
752
789
 
753
790
  /** Frames per second to update objects
754
791
  * @default */
@@ -785,7 +822,7 @@ let averageFPS, drawCount;
785
822
  // css text used for elements created by engine
786
823
  const styleBody = 'margin:0;overflow:hidden;background:#000' +
787
824
  ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
788
- const styleCanvas = 'position:absolute';
825
+ const styleCanvas = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
789
826
 
790
827
  ///////////////////////////////////////////////////////////////////////////////
791
828
 
@@ -829,8 +866,6 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
829
866
  // main update loop
830
867
  const engineUpdate = (frameTimeMS=0)=>
831
868
  {
832
- requestAnimationFrame(engineUpdate);
833
-
834
869
  // update time keeping
835
870
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
836
871
  frameTimeLastMS = frameTimeMS;
@@ -886,24 +921,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
886
921
 
887
922
  // fit to window by adding space on top or bottom if necessary
888
923
  const aspect = innerWidth / innerHeight;
889
- const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
890
-
891
- // clear css
892
- mainCanvas.style.width = overlayCanvas.style.width =
893
- mainCanvas.style.height = overlayCanvas.style.height =
894
- mainCanvas.style.top = overlayCanvas.style.top =
895
- mainCanvas.style.left = overlayCanvas.style.left = '';
896
-
897
- // letterbox center canvas
898
- if (aspect < fixedAspect)
899
- {
900
- mainCanvas.style.width = overlayCanvas.style.width = innerWidth;
901
- mainCanvas.style.top = overlayCanvas.style.top = (innerHeight - innerWidth/fixedAspect)/2;
902
- }
903
- else
924
+ const fixedAspect = mainCanvas.width / mainCanvas.height;
925
+ mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
926
+ mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
927
+ if (glCanvas)
904
928
  {
905
- mainCanvas.style.height = overlayCanvas.style.height = innerHeight;
906
- mainCanvas.style.left = overlayCanvas.style.left = (innerWidth - innerHeight*fixedAspect)/2;
929
+ glCanvas.style.width = mainCanvas.style.width;
930
+ glCanvas.style.height = mainCanvas.style.height;
907
931
  }
908
932
  }
909
933
  else
@@ -911,18 +935,6 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
911
935
  // clear and set size to same as window
912
936
  mainCanvas.width = min(innerWidth, canvasMaxSize.x);
913
937
  mainCanvas.height = min(innerHeight, canvasMaxSize.y);
914
-
915
- // center canvas
916
- overlayCanvas.style.top = mainCanvas.style.top = (innerHeight - mainCanvas.height)/2;
917
- overlayCanvas.style.left = mainCanvas.style.left = (innerWidth - mainCanvas.width)/2;
918
- }
919
-
920
- if (glEnable)
921
- {
922
- glCanvas.style.width = mainCanvas.style.width;
923
- glCanvas.style.height = mainCanvas.style.height;
924
- glCanvas.style.top = mainCanvas.style.top;
925
- glCanvas.style.left = mainCanvas.style.left;
926
938
  }
927
939
 
928
940
  // render sort then render while removing destroyed objects
@@ -952,6 +964,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
952
964
  overlayContext.fillText(text, mainCanvas.width-2, 2);
953
965
  drawCount = 0;
954
966
  }
967
+
968
+ requestAnimationFrame(engineUpdate);
955
969
  }
956
970
 
957
971
  // set tile image source to load the image and start the engine
@@ -1069,7 +1083,7 @@ class EngineObject
1069
1083
  * @param {Vector2} [size=objectDefaultSize] - World space size of the object
1070
1084
  * @param {Number} [tileIndex=-1] - Tile to use to render object, untextured if -1
1071
1085
  * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
1072
- * @param {Number} [angle=0] - Angle to rotate the object
1086
+ * @param {Number} [angle=0] - Angle the object is rotated by
1073
1087
  * @param {Color} [color] - Color to apply to tile when rendered
1074
1088
  * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
1075
1089
  */
@@ -1385,6 +1399,22 @@ class EngineObject
1385
1399
  this.isSolid = isSolid;
1386
1400
  this.collideTiles = collideTiles;
1387
1401
  }
1402
+
1403
+ toString()
1404
+ {
1405
+ let text = 'type = ' + this.constructor.name;
1406
+ if (this.pos.x || this.pos.y)
1407
+ text += '\npos = ' + this.pos;
1408
+ if (this.velocity.x || this.velocity.y)
1409
+ text += '\nvelocity = ' + this.velocity;
1410
+ if (this.size.x || this.size.y)
1411
+ text += '\nsize = ' + this.size;
1412
+ if (this.angle)
1413
+ text += '\nangle = ' + this.angle.toFixed(3);
1414
+ if (this.color)
1415
+ text += '\ncolor = ' + this.color;
1416
+ return text;
1417
+ }
1388
1418
  }
1389
1419
  /**
1390
1420
  * LittleJS Drawing System
@@ -1498,7 +1528,7 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1498
1528
  if (tileIndex < 0)
1499
1529
  {
1500
1530
  // if negative tile index, force untextured
1501
- context.fillStyle = color.rgba();
1531
+ context.fillStyle = color;
1502
1532
  context.fillRect(-.5, -.5, 1, 1);
1503
1533
  }
1504
1534
  else
@@ -1603,7 +1633,8 @@ function setBlendMode(additive, useWebGL=glEnable)
1603
1633
  mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1604
1634
  }
1605
1635
 
1606
- /** Draw text on overlay canvas in world space
1636
+ /** Draw text on overlay canvas in screen space
1637
+ * Automatically splits new lines into rows
1607
1638
  * @param {String} text
1608
1639
  * @param {Vector2} pos
1609
1640
  * @param {Number} [size=1]
@@ -1612,20 +1643,37 @@ function setBlendMode(additive, useWebGL=glEnable)
1612
1643
  * @param {Color} [lineColor=new Color(0,0,0)]
1613
1644
  * @param {String} [textAlign='center']
1614
1645
  * @memberof Draw */
1615
- function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault)
1646
+ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault)
1616
1647
  {
1617
- pos = worldToScreen(pos);
1618
- overlayContext.font = size*cameraScale + 'px '+ font;
1648
+ overlayContext.fillStyle = color;
1649
+ overlayContext.lineWidth = lineWidth *= cameraScale;
1650
+ overlayContext.strokeStyle = lineColor;
1619
1651
  overlayContext.textAlign = textAlign;
1652
+ overlayContext.font = size + 'px '+ font;
1620
1653
  overlayContext.textBaseline = 'middle';
1621
- if (lineWidth)
1654
+
1655
+ pos = pos.copy();
1656
+ text.split('\n').forEach(line=>
1622
1657
  {
1623
- overlayContext.lineWidth = lineWidth*cameraScale;
1624
- overlayContext.strokeStyle = lineColor.rgba();
1625
- overlayContext.strokeText(text, pos.x, pos.y);
1626
- }
1627
- overlayContext.fillStyle = color.rgba();
1628
- overlayContext.fillText(text, pos.x, pos.y);
1658
+ lineWidth && overlayContext.strokeText(line, pos.x, pos.y);
1659
+ overlayContext.fillText(line, pos.x, pos.y);
1660
+ pos.y += size;
1661
+ });
1662
+ }
1663
+
1664
+ /** Draw text on overlay canvas in world space
1665
+ * Automatically splits new lines into rows
1666
+ * @param {String} text
1667
+ * @param {Vector2} pos
1668
+ * @param {Number} [size=1]
1669
+ * @param {Color} [color=new Color(1,1,1)]
1670
+ * @param {Number} [lineWidth=0]
1671
+ * @param {Color} [lineColor=new Color(0,0,0)]
1672
+ * @param {String} [textAlign='center']
1673
+ * @memberof Draw */
1674
+ function drawText(text, pos, size=1, color, lineWidth, lineColor, textAlign, font)
1675
+ {
1676
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth, lineColor, textAlign, font);
1629
1677
  }
1630
1678
 
1631
1679
  ///////////////////////////////////////////////////////////////////////////////
@@ -1642,6 +1690,9 @@ function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new
1642
1690
  * // draw text
1643
1691
  * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
1644
1692
  */
1693
+
1694
+ let engineFontImage;
1695
+
1645
1696
  class FontImage
1646
1697
  {
1647
1698
  /** Create an image font
@@ -1653,14 +1704,14 @@ class FontImage
1653
1704
  */
1654
1705
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), startTileIndex=0, context=overlayContext)
1655
1706
  {
1656
- if (!image)
1707
+ if (!image && !engineFontImage)
1657
1708
  {
1658
- // use default font image
1659
- image = new Image();
1660
- 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';
1709
+ // load default font image
1710
+ engineFontImage = new Image();
1711
+ engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
1661
1712
  }
1662
1713
 
1663
- this.image = image;
1714
+ this.image = image || engineFontImage;
1664
1715
  this.tileSize = tileSize;
1665
1716
  this.paddingSize = paddingSize;
1666
1717
  this.startTileIndex = startTileIndex;
@@ -2027,7 +2078,7 @@ if (isTouchDevice)
2027
2078
  // touch gamepad, virtual on screen gamepad emulator for touch devices
2028
2079
 
2029
2080
  // touch input internal variables
2030
- let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2(), touchGamepadAnalog = 1;
2081
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
2031
2082
 
2032
2083
  // create the touch gamepad, called automatically by the engine
2033
2084
  function touchGamepadCreate()
@@ -2074,7 +2125,7 @@ function touchGamepadCreate()
2074
2125
  {
2075
2126
  // virtual analog stick
2076
2127
  if (touchGamepadAnalog)
2077
- touchGamepadStick = touchPos.subtract(stickCenter).clampLength(touchGamepadSize/2).scale(2/touchGamepadSize);
2128
+ touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
2078
2129
  else
2079
2130
  {
2080
2131
  // 8 way dpad
@@ -2115,12 +2166,28 @@ function touchGamepadRender()
2115
2166
  overlayContext.lineWidth = 3;
2116
2167
 
2117
2168
  // draw left analog stick
2118
- overlayContext.fillStyle = gamepadStick(0).length() > 0 ? '#fff' : '#000';
2169
+ overlayContext.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
2119
2170
  overlayContext.beginPath();
2120
- overlayContext.arc(touchGamepadSize, mainCanvasSize.y-touchGamepadSize, touchGamepadSize/2, 0,9);
2121
- overlayContext.fill();
2122
- overlayContext.stroke();
2123
2171
 
2172
+ const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2173
+ if (touchGamepadAnalog)
2174
+ {
2175
+ overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
2176
+ overlayContext.fill();
2177
+ overlayContext.stroke();
2178
+ }
2179
+ else // draw cross shaped gamepad
2180
+ {
2181
+ for(let i=10; i--;)
2182
+ {
2183
+ const angle = i*PI/4;
2184
+ overlayContext.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
2185
+ i%2 && overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
2186
+ i==1 && overlayContext.fill();
2187
+ }
2188
+ overlayContext.stroke();
2189
+ }
2190
+
2124
2191
  // draw right face buttons
2125
2192
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2126
2193
  for (let i=4; i--;)
@@ -2761,14 +2828,14 @@ class TileLayerData
2761
2828
  */
2762
2829
  class TileLayer extends EngineObject
2763
2830
  {
2764
- /** Create a tile layer object
2765
- * @param {Vector2} [position=new Vector2()] - World space position
2766
- * @param {Vector2} [size=objectDefaultSize] - World space size
2767
- * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2768
- * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2769
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2770
- */
2771
- constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2831
+ /** Create a tile layer object
2832
+ * @param {Vector2} [position=new Vector2()] - World space position
2833
+ * @param {Vector2} [size=tileCollisionSize] - World space size
2834
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2835
+ * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2836
+ * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2837
+ */
2838
+ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2772
2839
  {
2773
2840
  super(pos, size, -1, tileSize, 0, undefined, renderOrder);
2774
2841
 
@@ -2928,7 +2995,7 @@ class TileLayer extends EngineObject
2928
2995
  if (tileIndex < 0)
2929
2996
  {
2930
2997
  // untextured
2931
- context.fillStyle = color.rgba();
2998
+ context.fillStyle = color;
2932
2999
  context.fillRect(-.5, -.5, 1, 1);
2933
3000
  }
2934
3001
  else
@@ -2947,7 +3014,8 @@ class TileLayer extends EngineObject
2947
3014
  * @param {Vector2} [size=new Vector2(1,1)]
2948
3015
  * @param {Color} [color=new Color(1,1,1)]
2949
3016
  * @param {Number} [angle=0] */
2950
- drawRect(pos, size, color, angle) { this.drawTile(pos, size, -1, 0, color, angle); }
3017
+ drawRect(pos, size, color, angle)
3018
+ { this.drawTile(pos, size, -1, 0, color, angle); }
2951
3019
  }
2952
3020
  /*
2953
3021
  LittleJS Particle System
@@ -2966,9 +3034,9 @@ class TileLayer extends EngineObject
2966
3034
  * let pos = vec2(2,3);
2967
3035
  * let particleEmiter = new ParticleEmitter
2968
3036
  * (
2969
- * pos, 1, 0, 500, PI, // pos, emitSize, emitTime, emitRate, emiteCone
3037
+ * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emiteCone
2970
3038
  * 0, vec2(16), // tileIndex, tileSize
2971
- * new Color, new Color(0,0,0), // colorStartA, colorStartB
3039
+ * new Color(1,1,1), new Color(0,0,0), // colorStartA, colorStartB
2972
3040
  * new Color(1,1,1,0), new Color(0,0,0,0), // colorEndA, colorEndB
2973
3041
  * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
2974
3042
  * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
@@ -2979,6 +3047,7 @@ class ParticleEmitter extends EngineObject
2979
3047
  {
2980
3048
  /** Create a particle system with the given settings
2981
3049
  * @param {Vector2} position - World space position of the emitter
3050
+ * @param {Number} [angle=0] - Angle to emit the particles
2982
3051
  * @param {Number} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
2983
3052
  * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
2984
3053
  * @param {Number} [emitRate=100] - How many particles per second to spawn, does not emit if 0
@@ -3008,6 +3077,7 @@ class ParticleEmitter extends EngineObject
3008
3077
  constructor
3009
3078
  (
3010
3079
  pos,
3080
+ angle,
3011
3081
  emitSize = 0,
3012
3082
  emitTime = 0,
3013
3083
  emitRate = 100,
@@ -3035,7 +3105,7 @@ class ParticleEmitter extends EngineObject
3035
3105
  renderOrder = additive ? 1e9 : 0
3036
3106
  )
3037
3107
  {
3038
- super(pos, new Vector2, tileIndex, tileSize, 0, undefined, renderOrder);
3108
+ super(pos, new Vector2, tileIndex, tileSize, angle, undefined, renderOrder);
3039
3109
 
3040
3110
  // emitter settings
3041
3111
  /** @property {Number} - World space size of the emitter (float for circle diameter, vec2 for rect) */
@@ -3268,7 +3338,7 @@ function medalsInit(saveName)
3268
3338
  {
3269
3339
  // check if medals are unlocked
3270
3340
  medalsSaveName = saveName;
3271
- debugMedals || medals.forEach(medal=> medal.unlocked = 1 || localStorage[medal.storageKey()]);
3341
+ debugMedals || medals.forEach(medal=> localStorage[medal.storageKey()]);
3272
3342
  }
3273
3343
 
3274
3344
  /**
@@ -3457,6 +3527,9 @@ class Newgrounds
3457
3527
  const scoreboardResult = this.call('ScoreBoard.getBoards');
3458
3528
  this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
3459
3529
  debugMedals && console.log(this.scoreboards);
3530
+
3531
+ const keepAliveMS = 5 * 60 * 1e3;
3532
+ setInterval(()=>this.call('Gateway.ping', 0, 1), keepAliveMS);
3460
3533
  }
3461
3534
 
3462
3535
  /** Send message to unlock a medal by id
@@ -3468,9 +3541,6 @@ class Newgrounds
3468
3541
  * @param {Number} value - The score value */
3469
3542
  postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, 1); }
3470
3543
 
3471
- /** Send message to log a view */
3472
- logView() { return this.call('App.logView', {'host':this.host}, 1); }
3473
-
3474
3544
  /** Get scores from a scoreboard
3475
3545
  * @param {Number} id - The scoreboard id
3476
3546
  * @param {String} [user=0] - A user's id or name
@@ -3482,6 +3552,9 @@ class Newgrounds
3482
3552
  getScores(id, user=0, social=0, skip=0, limit=10)
3483
3553
  { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
3484
3554
 
3555
+ /** Send message to log a view */
3556
+ logView() { return this.call('App.logView', {'host':this.host}, 1); }
3557
+
3485
3558
  /** Send a message to call a component of the Newgrounds API
3486
3559
  * @param {String} component - Name of the component
3487
3560
  * @param {Object} [parameters=0] - Parameters to use for call
@@ -3578,24 +3651,24 @@ function glInit()
3578
3651
 
3579
3652
  // setup vertex and fragment shaders
3580
3653
  glShader = glCreateProgram(
3581
- 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3582
- 'uniform mat4 m;'+ // transform matrix
3583
- 'attribute vec2 p,t;'+ // position, uv
3584
- 'attribute vec4 c,a;'+ // color, additiveColor
3585
- 'varying vec2 v;'+ // return uv
3586
- 'varying vec4 d,e;'+ // return color, additiveColor
3587
- 'void main(){'+ // shader entry point
3588
- 'gl_Position=m*vec4(p,1,1);'+// transform position
3589
- 'v=t;d=c;e=a;'+ // pass stuff to fragment shader
3590
- '}' // end of shader
3654
+ 'precision highp float;'+ // use highp for better accuracy
3655
+ 'uniform mat4 m;'+ // transform matrix
3656
+ 'attribute vec2 p,t;'+ // position, uv
3657
+ 'attribute vec4 c,a;'+ // color, additiveColor
3658
+ 'varying vec2 v;'+ // return uv
3659
+ 'varying vec4 d,e;'+ // return color, additiveColor
3660
+ 'void main(){'+ // shader entry point
3661
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
3662
+ 'v=t;d=c;e=a;'+ // pass stuff to fragment shader
3663
+ '}' // end of shader
3591
3664
  ,
3592
- 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3593
- 'varying vec2 v;'+ // uv
3594
- 'varying vec4 d,e;'+ // color, additiveColor
3595
- 'uniform sampler2D j;'+ // texture
3596
- 'void main(){'+ // shader entry point
3597
- 'gl_FragColor=texture2D(j,v)*d+e;'+ // modulate texture by color plus additive
3598
- '}' // end of shader
3665
+ 'precision highp float;'+ // use highp for better accuracy
3666
+ 'varying vec2 v;'+ // uv
3667
+ 'varying vec4 d,e;'+ // color, additiveColor
3668
+ 'uniform sampler2D s;'+ // texture
3669
+ 'void main(){'+ // shader entry point
3670
+ 'gl_FragColor=texture2D(s,v)*d+e;'+ // modulate texture by color plus additive
3671
+ '}' // end of shader
3599
3672
  );
3600
3673
 
3601
3674
  // init buffers
@@ -3807,44 +3880,44 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffff
3807
3880
 
3808
3881
  // prepare to create the verts from size and angle
3809
3882
  const c = Math.cos(angle)/2, s = Math.sin(angle)/2;
3810
- const cX = c*sizeX, cY = c*sizeY, sX = s*sizeX, sY = s*sizeY;
3883
+ const cx = c*sizeX, cy = c*sizeY, sx = s*sizeX, sy = s*sizeY;
3811
3884
 
3812
3885
  // setup 2 triangles to form a quad
3813
3886
  let offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT;
3814
3887
 
3815
3888
  // vertex 0
3816
- glPositionData[offset++] = x - cX - sY;
3817
- glPositionData[offset++] = y - cY + sX;
3889
+ glPositionData[offset++] = x - cx - sy;
3890
+ glPositionData[offset++] = y - cy + sx;
3818
3891
  glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3819
3892
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3820
3893
 
3821
3894
  // vertex 1
3822
- glPositionData[offset++] = x + cX + sY;
3823
- glPositionData[offset++] = y + cY - sX;
3895
+ glPositionData[offset++] = x + cx + sy;
3896
+ glPositionData[offset++] = y + cy - sx;
3824
3897
  glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3825
3898
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3826
3899
 
3827
3900
  // vertex 2
3828
- glPositionData[offset++] = x - cX + sY;
3829
- glPositionData[offset++] = y + cY + sX;
3901
+ glPositionData[offset++] = x - cx + sy;
3902
+ glPositionData[offset++] = y + cy + sx;
3830
3903
  glPositionData[offset++] = uv0X; glPositionData[offset++] = uv0Y;
3831
3904
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3832
3905
 
3833
3906
  // vertex 0
3834
- glPositionData[offset++] = x - cX - sY;
3835
- glPositionData[offset++] = y - cY + sX;
3907
+ glPositionData[offset++] = x - cx - sy;
3908
+ glPositionData[offset++] = y - cy + sx;
3836
3909
  glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3837
3910
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3838
3911
 
3839
3912
  // vertex 3
3840
- glPositionData[offset++] = x + cX - sY;
3841
- glPositionData[offset++] = y - cY - sX;
3913
+ glPositionData[offset++] = x + cx - sy;
3914
+ glPositionData[offset++] = y - cy - sx;
3842
3915
  glPositionData[offset++] = uv1X; glPositionData[offset++] = uv1Y;
3843
3916
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3844
3917
 
3845
3918
  // vertex 1
3846
- glPositionData[offset++] = x + cX + sY;
3847
- glPositionData[offset++] = y + cY - sX;
3919
+ glPositionData[offset++] = x + cx + sy;
3920
+ glPositionData[offset++] = y + cy - sx;
3848
3921
  glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3849
3922
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3850
3923
  }
package/engine/engine.js CHANGED
@@ -22,7 +22,7 @@
22
22
  const engineName = 'LittleJS';
23
23
 
24
24
  /** Version of engine */
25
- const engineVersion = '1.2.0';
25
+ const engineVersion = '1.3.0';
26
26
 
27
27
  /** Frames per second to update objects
28
28
  * @default */
@@ -59,7 +59,7 @@ let averageFPS, drawCount;
59
59
  // css text used for elements created by engine
60
60
  const styleBody = 'margin:0;overflow:hidden;background:#000' +
61
61
  ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
62
- const styleCanvas = 'position:absolute';
62
+ const styleCanvas = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
63
63
 
64
64
  ///////////////////////////////////////////////////////////////////////////////
65
65
 
@@ -103,8 +103,6 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
103
103
  // main update loop
104
104
  const engineUpdate = (frameTimeMS=0)=>
105
105
  {
106
- requestAnimationFrame(engineUpdate);
107
-
108
106
  // update time keeping
109
107
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
110
108
  frameTimeLastMS = frameTimeMS;
@@ -160,24 +158,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
160
158
 
161
159
  // fit to window by adding space on top or bottom if necessary
162
160
  const aspect = innerWidth / innerHeight;
163
- const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
164
-
165
- // clear css
166
- mainCanvas.style.width = overlayCanvas.style.width =
167
- mainCanvas.style.height = overlayCanvas.style.height =
168
- mainCanvas.style.top = overlayCanvas.style.top =
169
- mainCanvas.style.left = overlayCanvas.style.left = '';
170
-
171
- // letterbox center canvas
172
- if (aspect < fixedAspect)
173
- {
174
- mainCanvas.style.width = overlayCanvas.style.width = innerWidth;
175
- mainCanvas.style.top = overlayCanvas.style.top = (innerHeight - innerWidth/fixedAspect)/2;
176
- }
177
- else
161
+ const fixedAspect = mainCanvas.width / mainCanvas.height;
162
+ mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
163
+ mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
164
+ if (glCanvas)
178
165
  {
179
- mainCanvas.style.height = overlayCanvas.style.height = innerHeight;
180
- mainCanvas.style.left = overlayCanvas.style.left = (innerWidth - innerHeight*fixedAspect)/2;
166
+ glCanvas.style.width = mainCanvas.style.width;
167
+ glCanvas.style.height = mainCanvas.style.height;
181
168
  }
182
169
  }
183
170
  else
@@ -185,18 +172,6 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
185
172
  // clear and set size to same as window
186
173
  mainCanvas.width = min(innerWidth, canvasMaxSize.x);
187
174
  mainCanvas.height = min(innerHeight, canvasMaxSize.y);
188
-
189
- // center canvas
190
- overlayCanvas.style.top = mainCanvas.style.top = (innerHeight - mainCanvas.height)/2;
191
- overlayCanvas.style.left = mainCanvas.style.left = (innerWidth - mainCanvas.width)/2;
192
- }
193
-
194
- if (glEnable)
195
- {
196
- glCanvas.style.width = mainCanvas.style.width;
197
- glCanvas.style.height = mainCanvas.style.height;
198
- glCanvas.style.top = mainCanvas.style.top;
199
- glCanvas.style.left = mainCanvas.style.left;
200
175
  }
201
176
 
202
177
  // render sort then render while removing destroyed objects
@@ -226,6 +201,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
226
201
  overlayContext.fillText(text, mainCanvas.width-2, 2);
227
202
  drawCount = 0;
228
203
  }
204
+
205
+ requestAnimationFrame(engineUpdate);
229
206
  }
230
207
 
231
208
  // set tile image source to load the image and start the engine