littlejsengine 1.14.16 → 1.14.23

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.
Files changed (62) hide show
  1. package/dist/littlejs.d.ts +173 -50
  2. package/dist/littlejs.esm.js +892 -419
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +879 -419
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +854 -420
  7. package/examples/box2d/game.js +3 -2
  8. package/examples/box2d/tiles.png +0 -0
  9. package/examples/breakout/game.js +5 -5
  10. package/examples/electron/game.js +1 -41
  11. package/examples/electron/index.html +2 -2
  12. package/examples/htmlMenu/game.js +1 -1
  13. package/examples/index.html +69 -55
  14. package/examples/module/game.js +6 -8
  15. package/examples/particles/index.html +6 -9
  16. package/examples/platformer/gameCharacter.js +1 -1
  17. package/examples/platformer/gameEffects.js +8 -3
  18. package/examples/platformer/gameObjects.js +3 -3
  19. package/examples/shorts/animation.js +2 -2
  20. package/examples/shorts/base.html +4 -1
  21. package/examples/shorts/cameraDrag.js +22 -0
  22. package/examples/shorts/debugDraw.js +38 -0
  23. package/examples/shorts/fps.js +90 -0
  24. package/examples/shorts/helloWorld.js +1 -1
  25. package/examples/shorts/hillGlideGame.js +1 -1
  26. package/examples/shorts/landerGame.js +1 -1
  27. package/examples/shorts/musicPlayer.js +1 -0
  28. package/examples/shorts/parallax.js +1 -1
  29. package/examples/shorts/postProcess.js +1 -1
  30. package/examples/shorts/sequencer.js +5 -5
  31. package/examples/shorts/shader.js +29 -0
  32. package/examples/shorts/spaceGame.js +1 -1
  33. package/examples/shorts/spriteAtlas.js +6 -6
  34. package/examples/shorts/starfield.js +1 -1
  35. package/examples/shorts/tileLayer.js +1 -1
  36. package/examples/shorts/tileRaycast.js +39 -0
  37. package/examples/shorts/tiles.png +0 -0
  38. package/examples/shorts/tiltedView.js +14 -6
  39. package/examples/shorts/topDown.js +1 -1
  40. package/examples/starter/game.js +6 -8
  41. package/examples/starter/index.html +2 -2
  42. package/examples/style.css +1 -0
  43. package/examples/typescript/game.js +5 -4
  44. package/examples/typescript/game.ts +5 -7
  45. package/examples/uiSystem/game.js +3 -2
  46. package/package.json +1 -1
  47. package/plugins/postProcess.js +71 -51
  48. package/plugins/uiSystem.js +76 -23
  49. package/src/engine.js +30 -16
  50. package/src/engineAudio.js +33 -23
  51. package/src/engineDebug.js +27 -0
  52. package/src/engineDraw.js +103 -49
  53. package/src/engineExport.js +13 -0
  54. package/src/engineInput.js +14 -16
  55. package/src/engineObject.js +31 -6
  56. package/src/engineParticles.js +37 -16
  57. package/src/engineRelease.js +2 -1
  58. package/src/engineSettings.js +20 -1
  59. package/src/engineTileLayer.js +89 -94
  60. package/src/engineUtilities.js +206 -58
  61. package/src/engineWebGL.js +145 -69
  62. package/examples/shorts/raycasting.js +0 -79
package/src/engineDraw.js CHANGED
@@ -173,16 +173,15 @@ class TileInfo
173
173
  }
174
174
 
175
175
  /**
176
- * Set this tile to use a full image
177
- * @param {HTMLImageElement|OffscreenCanvas} image
178
- * @param {WebGLTexture} [glTexture] - WebGL texture
176
+ * Set this tile to use a full image in a texture info
177
+ * @param {TextureInfo} textureInfo
179
178
  * @return {TileInfo}
180
179
  */
181
- setFullImage(image, glTexture)
180
+ setFullImage(textureInfo)
182
181
  {
183
182
  this.pos = new Vector2;
184
- this.size = new Vector2(image.width, image.height);
185
- this.textureInfo = new TextureInfo(image, glTexture);
183
+ this.size = textureInfo.size.copy();
184
+ this.textureInfo = textureInfo;
186
185
  // do not use padding or bleed
187
186
  this.bleedScale = this.padding = 0;
188
187
  return this;
@@ -198,26 +197,30 @@ class TextureInfo
198
197
  /**
199
198
  * Create a TextureInfo, called automatically by the engine
200
199
  * @param {HTMLImageElement|OffscreenCanvas} image
201
- * @param {WebGLTexture} [glTexture] - WebGL texture
200
+ * @param {boolean} [useWebGL] - Should use WebGL if available?
202
201
  */
203
- constructor(image, glTexture)
202
+ constructor(image, useWebGL=true)
204
203
  {
205
- /** @property {HTMLImageElement} - image source */
204
+ /** @property {HTMLImageElement|OffscreenCanvas} - image source */
206
205
  this.image = image;
207
206
  /** @property {Vector2} - size of the image */
208
- this.size = vec2(image.width, image.height);
207
+ this.size = image ? vec2(image.width, image.height) : vec2();
209
208
  /** @property {Vector2} - inverse of the size, cached for rendering */
210
- this.sizeInverse = vec2(1/image.width, 1/image.height);
209
+ this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
211
210
  /** @property {WebGLTexture} - WebGL texture */
212
- this.glTexture = glTexture;
211
+ this.glTexture = undefined;
212
+ useWebGL && this.createWebGLTexture();
213
213
  }
214
214
 
215
- createWebGLTexture()
216
- {
217
- ASSERT(!this.glTexture);
218
- if (glEnable)
219
- this.glTexture = glCreateTexture(this.image);
220
- }
215
+ /** Creates the WebGL texture, updates if already created */
216
+ createWebGLTexture() { glRegisterTextureInfo(this); }
217
+
218
+ /** Destroys the WebGL texture */
219
+ destroyWebGLTexture() { glUnregisterTextureInfo(this); }
220
+
221
+ /** Check if the texture is webgl enabled
222
+ * @return {boolean} */
223
+ hasWebGL() { return !!this.glTexture; }
221
224
  }
222
225
 
223
226
  ///////////////////////////////////////////////////////////////////////////////
@@ -249,6 +252,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
249
252
  const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
250
253
  if (useWebGL)
251
254
  {
255
+ ASSERT(!!glContext, 'WebGL is not enabled!');
252
256
  if (screenSpace)
253
257
  {
254
258
  // convert to world space
@@ -344,6 +348,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
344
348
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
345
349
  if (useWebGL)
346
350
  {
351
+ ASSERT(!!glContext, 'WebGL is not enabled!');
347
352
  if (screenSpace)
348
353
  {
349
354
  // convert to world space
@@ -355,7 +360,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
355
360
  const halfSizeX = size.x/2, halfSizeY = size.y/2;
356
361
  const colorTopInt = colorTop.rgbaInt();
357
362
  const colorBottomInt = colorBottom.rgbaInt();
358
- const c = Math.cos(-angle), s = Math.sin(-angle);
363
+ const c = cos(-angle), s = sin(-angle);
359
364
  for (let i=4; i--;)
360
365
  {
361
366
  const x = i & 1 ? halfSizeX : -halfSizeX;
@@ -406,6 +411,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
406
411
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
407
412
  if (useWebGL)
408
413
  {
414
+ ASSERT(!!glContext, 'WebGL is not enabled!');
409
415
  let scale = 1;
410
416
  if (screenSpace)
411
417
  {
@@ -455,6 +461,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
455
461
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
456
462
  const size = vec2(width, halfDelta.length()*2);
457
463
  pos = pos.add(posA.add(halfDelta));
464
+ if (screenSpace)
465
+ halfDelta.y *= -1; // flip angle Y if screen space
458
466
  angle += halfDelta.angle();
459
467
  drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
460
468
  }
@@ -482,7 +490,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
482
490
  for (let i=sides; i--;)
483
491
  {
484
492
  const a = (i/sides)*PI*2;
485
- points.push(vec2(Math.sin(a)*sizeX, Math.cos(a)*sizeY));
493
+ points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
486
494
  }
487
495
  drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
488
496
  }
@@ -509,6 +517,7 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
509
517
 
510
518
  if (useWebGL)
511
519
  {
520
+ ASSERT(!!glContext, 'WebGL is not enabled!');
512
521
  let scale = 1;
513
522
  if (screenSpace)
514
523
  {
@@ -650,12 +659,13 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
650
659
  * @param {Color} [lineColor=(0,0,0,1)]
651
660
  * @param {CanvasTextAlign} [textAlign='center']
652
661
  * @param {string} [font=fontDefault]
662
+ * @param {string} [fontStyle]
653
663
  * @param {number} [maxWidth]
654
664
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
655
665
  * @memberof Draw */
656
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth, context=drawContext)
666
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
657
667
  {
658
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, maxWidth, context);
668
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
659
669
  }
660
670
 
661
671
  /** Draw text on overlay canvas in world space
@@ -668,11 +678,12 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
668
678
  * @param {Color} [lineColor=(0,0,0,1)]
669
679
  * @param {CanvasTextAlign} [textAlign='center']
670
680
  * @param {string} [font=fontDefault]
681
+ * @param {string} [fontStyle]
671
682
  * @param {number} [maxWidth]
672
683
  * @memberof Draw */
673
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth)
684
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
674
685
  {
675
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, maxWidth, overlayContext);
686
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
676
687
  }
677
688
 
678
689
  /** Draw text on overlay canvas in screen space
@@ -685,10 +696,11 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
685
696
  * @param {Color} [lineColor=(0,0,0,1)]
686
697
  * @param {CanvasTextAlign} [textAlign]
687
698
  * @param {string} [font=fontDefault]
699
+ * @param {string} [fontStyle]
688
700
  * @param {number} [maxWidth]
689
701
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
690
702
  * @memberof Draw */
691
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
703
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
692
704
  {
693
705
  ASSERT(isString(text), 'text must be a string');
694
706
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -696,15 +708,15 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
696
708
  ASSERT(isColor(color), 'color must be a color');
697
709
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
698
710
  ASSERT(isColor(lineColor), 'lineColor must be a color');
699
- ASSERT(isColor(lineColor), 'lineColor must be a color');
700
711
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
701
712
  ASSERT(isString(font), 'font must be a string');
713
+ ASSERT(isString(fontStyle), 'fontStyle must be a string');
702
714
 
703
715
  context.fillStyle = color.toString();
704
716
  context.strokeStyle = lineColor.toString();
705
717
  context.lineWidth = lineWidth;
706
718
  context.textAlign = textAlign;
707
- context.font = size + 'px '+ font;
719
+ context.font = fontStyle + ' ' + size + 'px '+ font;
708
720
  context.textBaseline = 'middle';
709
721
 
710
722
  const lines = (text+'').split('\n');
@@ -727,18 +739,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
727
739
  * @memberof Draw */
728
740
  function screenToWorld(screenPos)
729
741
  {
730
- let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
731
- let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
742
+ let x = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
743
+ let y = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
732
744
  if (cameraAngle)
733
745
  {
734
746
  // apply camera rotation
735
- const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
736
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
737
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
738
- cameraPosRelativeX = rotatedX;
739
- cameraPosRelativeY = rotatedY;
747
+ const c = cos(-cameraAngle), s = sin(-cameraAngle);
748
+ const rotatedX = x * c - y * s;
749
+ const rotatedY = x * s + y * c;
750
+ x = rotatedX;
751
+ y = rotatedY;
740
752
  }
741
- return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
753
+ return new Vector2(x + cameraPos.x, y + cameraPos.y);
742
754
  }
743
755
 
744
756
  /** Convert from world to screen space coordinates
@@ -747,24 +759,64 @@ function screenToWorld(screenPos)
747
759
  * @memberof Draw */
748
760
  function worldToScreen(worldPos)
749
761
  {
750
- let cameraPosRelativeX = worldPos.x - cameraPos.x;
751
- let cameraPosRelativeY = worldPos.y - cameraPos.y;
762
+ let x = worldPos.x - cameraPos.x;
763
+ let y = worldPos.y - cameraPos.y;
752
764
  if (cameraAngle)
753
765
  {
754
766
  // apply inverse camera rotation
755
- const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
756
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
757
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
758
- cameraPosRelativeX = rotatedX;
759
- cameraPosRelativeY = rotatedY;
767
+ const c = cos(cameraAngle), s = sin(cameraAngle);
768
+ const rotatedX = x * c - y * s;
769
+ const rotatedY = x * s + y * c;
770
+ x = rotatedX;
771
+ y = rotatedY;
760
772
  }
761
773
  return new Vector2
762
774
  (
763
- cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
764
- cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
775
+ x * cameraScale + mainCanvasSize.x/2 - .5,
776
+ y * -cameraScale + mainCanvasSize.y/2 - .5
765
777
  );
766
778
  }
767
779
 
780
+ /** Convert from screen to world space coordinates for a directional vector (no translation)
781
+ * @param {Vector2} screenDelta
782
+ * @return {Vector2}
783
+ * @memberof Draw */
784
+ function screenToWorldDelta(screenDelta)
785
+ {
786
+ let x = screenDelta.x / cameraScale;
787
+ let y = screenDelta.y / -cameraScale;
788
+ if (cameraAngle)
789
+ {
790
+ // apply camera rotation
791
+ const c = cos(-cameraAngle), s = sin(-cameraAngle);
792
+ const rotatedX = x * c - y * s;
793
+ const rotatedY = x * s + y * c;
794
+ x = rotatedX;
795
+ y = rotatedY;
796
+ }
797
+ return new Vector2(x, y);
798
+ }
799
+
800
+ /** Convert from screen to world space coordinates for a directional vector (no translation)
801
+ * @param {Vector2} worldDelta
802
+ * @return {Vector2}
803
+ * @memberof Draw */
804
+ function worldToScreenDelta(worldDelta)
805
+ {
806
+ let x = worldDelta.x;
807
+ let y = worldDelta.y;
808
+ if (cameraAngle)
809
+ {
810
+ // apply inverse camera rotation
811
+ const c = cos(cameraAngle), s = sin(cameraAngle);
812
+ const rotatedX = x * c - y * s;
813
+ const rotatedY = x * s + y * c;
814
+ x = rotatedX;
815
+ y = rotatedY;
816
+ }
817
+ return new Vector2(x * cameraScale, y * -cameraScale);
818
+ }
819
+
768
820
  /** Get the camera's visible area in world space
769
821
  * @return {Vector2}
770
822
  * @memberof Draw */
@@ -817,6 +869,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
817
869
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
818
870
  const sx2 = bleedScale;
819
871
  const sy2 = bleedScale;
872
+ sWidth = max(1,sWidth|0);
873
+ sHeight = max(1,sHeight|0);
820
874
  const sWidth2 = sWidth - 2*bleedScale;
821
875
  const sHeight2 = sHeight - 2*bleedScale;
822
876
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
@@ -831,7 +885,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
831
885
  // copy to offscreen canvas
832
886
  workCanvas.width = sWidth;
833
887
  workCanvas.height = sHeight;
834
- workContext.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
888
+ workContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
835
889
 
836
890
  // tint image using offscreen work context
837
891
  const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
@@ -912,11 +966,11 @@ let engineFontImage;
912
966
  class FontImage
913
967
  {
914
968
  /** Create an image font
915
- * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
916
- * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
917
- * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
969
+ * @param {HTMLImageElement} [image] - Image for the font, default if undefined
970
+ * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
971
+ * @param {Vector2} [paddingSize=(0,1)] - How much space between characters
918
972
  */
919
- constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
973
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
920
974
  {
921
975
  // load default font image
922
976
  if (!engineFontImage)
@@ -79,6 +79,7 @@ export
79
79
  gamepadDirectionEmulateStick,
80
80
  inputWASDEmulateDirection,
81
81
  touchGamepadEnable,
82
+ touchGamepadCenterButton,
82
83
  touchGamepadAnalog,
83
84
  touchGamepadSize,
84
85
  touchGamepadAlpha,
@@ -122,6 +123,7 @@ export
122
123
  setGamepadDirectionEmulateStick,
123
124
  setInputWASDEmulateDirection,
124
125
  setTouchGamepadEnable,
126
+ setTouchGamepadCenterButton,
125
127
  setTouchGamepadAnalog,
126
128
  setTouchGamepadSize,
127
129
  setTouchGamepadAlpha,
@@ -140,9 +142,18 @@ export
140
142
  // Utilities
141
143
  PI,
142
144
  abs,
145
+ floor,
146
+ ceil,
147
+ round,
143
148
  min,
144
149
  max,
145
150
  sign,
151
+ hypot,
152
+ log2,
153
+ sin,
154
+ cos,
155
+ tan,
156
+ atan2,
146
157
  mod,
147
158
  clamp,
148
159
  percent,
@@ -210,6 +221,8 @@ export
210
221
  drawCount,
211
222
  screenToWorld,
212
223
  worldToScreen,
224
+ screenToWorldDelta,
225
+ worldToScreenDelta,
213
226
  drawTile,
214
227
  drawRect,
215
228
  drawRectGradient,
@@ -182,9 +182,9 @@ function inputUpdate()
182
182
  if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
183
183
  inputClear();
184
184
 
185
- // update mouse world space position
185
+ // update mouse world space position and delta
186
186
  mousePos = screenToWorld(mousePosScreen);
187
- mouseDelta = mouseDeltaScreen.multiply(vec2(1,-1)).rotate(-cameraAngle);
187
+ mouseDelta = screenToWorldDelta(mouseDeltaScreen);
188
188
 
189
189
  // update gamepads if enabled
190
190
  gamepadsUpdate();
@@ -216,7 +216,6 @@ function inputInit()
216
216
  document.addEventListener('wheel', onMouseWheel);
217
217
  document.addEventListener('contextmenu', onContextMenu);
218
218
  document.addEventListener('blur', onBlur);
219
- document.addEventListener('mouseleave', onMouseLeave);
220
219
 
221
220
  // init touch input
222
221
  if (isTouchDevice && touchInputEnable)
@@ -258,7 +257,10 @@ function inputInit()
258
257
 
259
258
  isUsingGamepad = false;
260
259
  inputData[0][e.button] = 3;
260
+
261
+ let mousePosScreenLast = mousePosScreen;
261
262
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
263
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
262
264
  inputPreventDefault && e.button && e.preventDefault();
263
265
  }
264
266
  function onMouseUp(e)
@@ -269,18 +271,13 @@ function inputInit()
269
271
  }
270
272
  function onMouseMove(e)
271
273
  {
274
+ let mousePosScreenLast = mousePosScreen;
272
275
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
273
- mouseDeltaScreen = mouseDeltaScreen.add(vec2(e.movementX, e.movementY));
276
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
274
277
  }
275
278
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
276
279
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
277
280
  function onBlur() { inputClear(); } // reset input when focus is lost
278
- function onMouseLeave()
279
- {
280
- // set mouse position and delta when leaving canvas
281
- mousePosScreen = vec2(-1);
282
- mouseDeltaScreen = vec2(0);
283
- }
284
281
  }
285
282
 
286
283
  // convert a mouse or touch event position to screen space
@@ -324,8 +321,8 @@ function gamepadsUpdate()
324
321
  else if (touchGamepadStick.lengthSquared() > .3)
325
322
  {
326
323
  // convert to 8 way dpad
327
- sticks[0].x = Math.round(touchGamepadStick.x);
328
- sticks[0].y = -Math.round(touchGamepadStick.y);
324
+ sticks[0].x = round(touchGamepadStick.x);
325
+ sticks[0].y = -round(touchGamepadStick.y);
329
326
  sticks[0] = sticks[0].clampLength();
330
327
  }
331
328
 
@@ -453,11 +450,11 @@ function touchInputInit()
453
450
  {
454
451
  // set event pos and pass it along
455
452
  const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
456
- const lastMousePosScreen = mousePosScreen;
453
+ const mousePosScreenLast = mousePosScreen;
457
454
  mousePosScreen = mouseEventToScreen(pos);
458
455
  if (wasTouching)
459
456
  {
460
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(lastMousePosScreen));
457
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
461
458
  isUsingGamepad = touchGamepadEnable;
462
459
  }
463
460
  else
@@ -489,7 +486,7 @@ function touchInputInit()
489
486
  if (touching)
490
487
  {
491
488
  touchGamepadTimer.set();
492
- if (paused && !wasTouching)
489
+ if (touchGamepadCenterButton && !wasTouching && paused)
493
490
  {
494
491
  // touch anywhere to press start when paused
495
492
  touchGamepadButtons[9] = 1;
@@ -528,7 +525,8 @@ function touchInputInit()
528
525
  if (button < touchGamepadButtonCount)
529
526
  touchGamepadButtons[button] = 1;
530
527
  }
531
- else if (startCenter.distance(touchPos) < touchGamepadSize && !wasTouching)
528
+ else if (touchGamepadCenterButton && !wasTouching &&
529
+ startCenter.distance(touchPos) < touchGamepadSize)
532
530
  {
533
531
  // virtual start button in center
534
532
  touchGamepadButtons[9] = 1;
@@ -287,6 +287,27 @@ class EngineObject
287
287
  // test which side we bounced off (or both if a corner)
288
288
  const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
289
289
  const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
290
+
291
+ if (blockedLayerX)
292
+ {
293
+ // try to move up a tiny bit
294
+ const epsilon = 1e-3;
295
+ const maxMoveUp = .1;
296
+ const y = floor(oldPos.y-this.size.y/2+1) +
297
+ this.size.y/2 + epsilon;
298
+ const delta = y - this.pos.y;
299
+ if (delta < maxMoveUp)
300
+ if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
301
+ {
302
+ this.pos.y = y;
303
+ debugPhysics && debugRect(this.pos, this.size, '#ff0');
304
+ return;
305
+ }
306
+
307
+ // move to previous position and bounce
308
+ this.pos.x = oldPos.x;
309
+ this.velocity.x *= -this.restitution;
310
+ }
290
311
  if (blockedLayerY || !blockedLayerX)
291
312
  {
292
313
  // bounce velocity
@@ -310,12 +331,6 @@ class EngineObject
310
331
  this.groundObject = undefined;
311
332
  }
312
333
  }
313
- if (blockedLayerX)
314
- {
315
- // move to previous position and bounce
316
- this.pos.x = oldPos.x;
317
- this.velocity.x *= -this.restitution;
318
- }
319
334
  debugPhysics && debugRect(this.pos, this.size, '#f00');
320
335
  }
321
336
  }
@@ -373,6 +388,16 @@ class EngineObject
373
388
  */
374
389
  collideWithObject(object) { return true; }
375
390
 
391
+ /** Get this object's up vector
392
+ * @param {number} [scale] - length of the vector
393
+ * @return {Vector2} */
394
+ getUp(scale=1) { return vec2().setAngle(this.angle, scale); }
395
+
396
+ /** Get this object's right vector
397
+ * @param {number} [scale] - length of the vector
398
+ * @return {Vector2} */
399
+ getRight(scale=1) { return vec2().setAngle(this.angle+PI/2, scale); }
400
+
376
401
  /** How long since the object was created
377
402
  * @return {number} */
378
403
  getAliveTime() { return time - this.spawnTime; }
@@ -23,7 +23,7 @@
23
23
  * tile(0, 16), // tileInfo
24
24
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
25
25
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
26
- * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
26
+ * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
27
27
  * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
28
28
  * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
29
29
  * );
@@ -152,6 +152,12 @@ class ParticleEmitter extends EngineObject
152
152
  this.particleCreateCallback = undefined;
153
153
  /** @property {number} - Track particle emit time */
154
154
  this.emitTimeBuffer = 0;
155
+ /** @property {number} - Percentage of velocity to pass to particles (0-1) */
156
+ this.velocityInheritance = 0;
157
+
158
+ // track previous position and angle
159
+ this.previousAngle = this.angle;
160
+ this.previousPos = this.pos.copy();
155
161
  }
156
162
 
157
163
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
@@ -160,6 +166,18 @@ class ParticleEmitter extends EngineObject
160
166
  // only do default update to apply parent transforms
161
167
  this.parent && super.update();
162
168
 
169
+ if (this.velocityInheritance)
170
+ {
171
+ // pass emitter velocity to particles
172
+ const p = this.velocityInheritance;
173
+ this.velocity.x = p * (this.pos.x - this.previousPos.x);
174
+ this.velocity.y = p * (this.pos.y - this.previousPos.y);
175
+ this.angleVelocity = p * (this.angle - this.previousAngle);
176
+ this.previousAngle = this.angle;
177
+ this.previousPos.x = this.pos.x;
178
+ this.previousPos.y = this.pos.y;
179
+ }
180
+
163
181
  // update emitter
164
182
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
165
183
  {
@@ -217,7 +235,7 @@ class ParticleEmitter extends EngineObject
217
235
  const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
218
236
  particle.velocity = vec2().setAngle(velocityAngle, speed);
219
237
  particle.angleVelocity = angleSpeed;
220
- if (!this.localSpace)
238
+ if (!this.localSpace && this.velocityInheritance > 0)
221
239
  {
222
240
  // apply emitter velocity to particle
223
241
  particle.velocity.x += this.velocity.x;
@@ -315,6 +333,16 @@ class Particle extends EngineObject
315
333
  this.velocity.y *= s;
316
334
  }
317
335
  }
336
+
337
+ if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
338
+ {
339
+ // destroy particle when its time runs out
340
+ const c = this.colorEnd;
341
+ this.color.set(c.r, c.g, c.b, c.a);
342
+ this.size.set(this.sizeEnd, this.sizeEnd);
343
+ this.destroyCallback && this.destroyCallback(this);
344
+ this.destroyed = 1;
345
+ }
318
346
  }
319
347
 
320
348
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -323,7 +351,7 @@ class Particle extends EngineObject
323
351
  // lerp color and size
324
352
  const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
325
353
  const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
326
- this.size.x = this.size.y = radius;
354
+ const size = vec2(radius);
327
355
  this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
328
356
  this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
329
357
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
@@ -343,7 +371,7 @@ class Particle extends EngineObject
343
371
  {
344
372
  // in local space of emitter
345
373
  const a = this.localSpaceEmitter.angle;
346
- const c = Math.cos(a), s = Math.sin(a);
374
+ const c = cos(a), s = sin(a);
347
375
  pos = this.localSpaceEmitter.pos.add(
348
376
  new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
349
377
  angle += this.localSpaceEmitter.angle;
@@ -359,21 +387,14 @@ class Particle extends EngineObject
359
387
  {
360
388
  // stretch in direction of motion
361
389
  const trailLength = speed * this.trailScale;
362
- this.size.y = max(this.size.x, trailLength);
363
- angle = Math.atan2(direction.x, direction.y);
364
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
390
+ size.y = max(size.x, trailLength);
391
+ angle = atan2(direction.x, direction.y);
392
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
365
393
  }
366
394
  }
367
395
  else
368
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
396
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
369
397
  this.additive && setBlendMode();
370
- debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
371
-
372
- if (p1 === 1)
373
- {
374
- // destroy particle when its time runs out
375
- this.destroyCallback && this.destroyCallback(this);
376
- this.destroyed = 1;
377
- }
398
+ debugParticles && debugRect(pos, size, '#f005', 0, angle);
378
399
  }
379
400
  }
@@ -38,4 +38,5 @@ function debugShowErrors(){}
38
38
  function debugVideoCaptureIsActive(){ return false; }
39
39
  function debugVideoCaptureStart (){}
40
40
  function debugVideoCaptureStop (){}
41
- function debugVideoCaptureUpdate(){}
41
+ function debugVideoCaptureUpdate(){}
42
+ function debugProtectConstant(o){ return o; }
@@ -216,6 +216,14 @@ let touchInputEnable = true;
216
216
  * @memberof Settings */
217
217
  let touchGamepadEnable = false;
218
218
 
219
+ /** True if touch gamepad should have start button in the center
220
+ * - When the game is paused, any touch will press the button
221
+ * - This can function as a way to pause/unpause the game
222
+ * @type {boolean}
223
+ * @default
224
+ * @memberof Settings */
225
+ let touchGamepadCenterButton = true;
226
+
219
227
  /** True if touch gamepad should be analog stick or false to use if 8 way dpad
220
228
  * @type {boolean}
221
229
  * @default
@@ -391,9 +399,14 @@ function setHeadlessMode(headless) { headlessMode = headless; }
391
399
  * @memberof Settings */
392
400
  function setGLEnable(enable)
393
401
  {
402
+ if (enable && !glCanBeEnabled)
403
+ {
404
+ console.warn('Can not enable WebGL if it was disabled on start.');
405
+ return;
406
+ }
394
407
  glEnable = enable;
395
408
  if (glCanvas) // hide glCanvas if WebGL is disabled
396
- glCanvas.style.visibility = enable ? 'visible' : 'hidden';
409
+ glCanvas.style.display = enable ? '' : 'none';
397
410
  }
398
411
 
399
412
  /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
@@ -481,6 +494,12 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
481
494
  * @memberof Settings */
482
495
  function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
483
496
 
497
+ /** True if touch gamepad should have start button in the center
498
+ * - This can function as a way to pause/unpause the game
499
+ * @param {boolean} enable
500
+ * @memberof Settings */
501
+ function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
502
+
484
503
  /** Set if touch gamepad should be analog stick or 8 way dpad
485
504
  * @param {boolean} analog
486
505
  * @memberof Settings */