littlejsengine 1.14.19 → 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 (51) hide show
  1. package/dist/littlejs.d.ts +118 -18
  2. package/dist/littlejs.esm.js +506 -199
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +493 -199
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +468 -200
  7. package/examples/electron/game.js +1 -41
  8. package/examples/electron/index.html +2 -2
  9. package/examples/htmlMenu/game.js +1 -1
  10. package/examples/index.html +12 -6
  11. package/examples/module/game.js +5 -7
  12. package/examples/particles/index.html +6 -9
  13. package/examples/platformer/gameCharacter.js +1 -1
  14. package/examples/platformer/gameEffects.js +7 -2
  15. package/examples/platformer/gameObjects.js +3 -3
  16. package/examples/shorts/animation.js +2 -2
  17. package/examples/shorts/base.html +4 -1
  18. package/examples/shorts/cameraDrag.js +22 -0
  19. package/examples/shorts/debugDraw.js +38 -0
  20. package/examples/shorts/fps.js +90 -0
  21. package/examples/shorts/helloWorld.js +1 -1
  22. package/examples/shorts/hillGlideGame.js +1 -1
  23. package/examples/shorts/landerGame.js +1 -1
  24. package/examples/shorts/postProcess.js +1 -1
  25. package/examples/shorts/sequencer.js +3 -3
  26. package/examples/shorts/spaceGame.js +1 -1
  27. package/examples/shorts/spriteAtlas.js +6 -6
  28. package/examples/shorts/starfield.js +1 -1
  29. package/examples/shorts/tileRaycast.js +39 -0
  30. package/examples/shorts/tiles.png +0 -0
  31. package/examples/shorts/tiltedView.js +1 -1
  32. package/examples/starter/game.js +5 -7
  33. package/examples/starter/index.html +2 -2
  34. package/examples/typescript/game.js +3 -2
  35. package/examples/typescript/game.ts +5 -7
  36. package/examples/uiSystem/game.js +2 -2
  37. package/package.json +1 -1
  38. package/src/engine.js +3 -3
  39. package/src/engineAudio.js +13 -13
  40. package/src/engineDebug.js +27 -0
  41. package/src/engineDraw.js +68 -20
  42. package/src/engineExport.js +13 -0
  43. package/src/engineInput.js +14 -16
  44. package/src/engineObject.js +31 -6
  45. package/src/engineParticles.js +36 -15
  46. package/src/engineRelease.js +2 -1
  47. package/src/engineSettings.js +20 -1
  48. package/src/engineTileLayer.js +62 -59
  49. package/src/engineUtilities.js +206 -58
  50. package/src/engineWebGL.js +13 -8
  51. package/examples/shorts/raycasting.js +0 -79
@@ -65,16 +65,17 @@ function gameInit()
65
65
  // create particle emitter
66
66
  particleEmitter = new ParticleEmitter(
67
67
  vec2(16,9), 0, // emitPos, emitAngle
68
- 0, 0, 500, PI, // emitSize, emitTime, rate, cone
68
+ 0, 0, 500, 3.14, // emitSize, emitTime, rate, cone
69
69
  tile(0, 16), // tileIndex, tileSize
70
70
  hsl(1,1,1), hsl(0,0,0), // colorStartA, colorStartB
71
71
  hsl(0,0,0,0), hsl(0,0,0,0), // colorEndA, colorEndB
72
- 1, .2, .2, .1, .05, // time, sizeStart, sizeEnd, speed, angleSpeed
73
- .99, 1, 1, PI, // damping, angleDamping, gravityScale, cone
74
- .05, .5, true, true // fadeRate, randomness, collide, additive
72
+ 1, .2, .2, .1, .05, // time, sizeStart, sizeEnd, speed, angleSpeed
73
+ .99, 1, 1, 3.14, // damping, angleDamping, gravityScale, cone
74
+ .05, .5, true, true // fadeRate, randomness, collide, additive
75
75
  );
76
76
  particleEmitter.restitution = .3; // bounce when it collides
77
77
  particleEmitter.trailScale = 2; // stretch as it moves
78
+ particleEmitter.velocityInheritance = .3; // inherit emitter velocity
78
79
  }
79
80
 
80
81
  ///////////////////////////////////////////////////////////////////////////////
@@ -104,10 +105,7 @@ function gameUpdate()
104
105
 
105
106
  // move particles to mouse location if on screen
106
107
  if (mousePosScreen.x)
107
- {
108
108
  particleEmitter.pos = mousePos;
109
- particleEmitter.velocity = mouseDelta.scale(.3*timeDelta);
110
- }
111
109
  }
112
110
 
113
111
  ///////////////////////////////////////////////////////////////////////////////
@@ -7,7 +7,7 @@
7
7
  </head><body>
8
8
 
9
9
  <!-- LittleJS Engine -->
10
- <script src=../../dist/littlejs.js?11417></script>
10
+ <script src=../../dist/littlejs.js?1.14.24></script>
11
11
 
12
12
  <!-- LittleJS Engine Source
13
13
  <script src=../../src/engineDebug.js></script>
@@ -31,4 +31,4 @@
31
31
  -->
32
32
 
33
33
  <!-- Add your game scripts here -->
34
- <script src=game.js?11417></script>
34
+ <script src=game.js?1.14.24></script>
@@ -52,16 +52,17 @@ function gameInit() {
52
52
  LJS.setGravity(vec2(0, -.01));
53
53
  // create particle emitter
54
54
  particleEmitter = new LJS.ParticleEmitter(vec2(16, 9), 0, // emitPos, emitAngle
55
- 1, 0, 500, Math.PI, // emitSize, emitTime, rate, cone
55
+ 0, 0, 500, 3.14, // emitSize, emitTime, rate, cone
56
56
  tile(0, 16), // tileIndex, tileSize
57
57
  hsl(1, 1, 1), hsl(0, 0, 0), // colorStartA, colorStartB
58
58
  hsl(0, 0, 0, 0), hsl(0, 0, 0, 0), // colorEndA, colorEndB
59
59
  1, .2, .2, .1, .05, // time, sizeStart, sizeEnd, speed, angleSpeed
60
- .99, 1, 1, Math.PI, // damping, angleDamping, gravityScale, cone
60
+ .99, 1, 1, 3.14, // damping, angleDamping, gravityScale, cone
61
61
  .05, .5, true, true // fadeRate, randomness, collide, additive
62
62
  );
63
63
  particleEmitter.restitution = .3; // bounce when it collides
64
64
  particleEmitter.trailScale = 2; // stretch as it moves
65
+ particleEmitter.velocityInheritance = .3; // inherit emitter velocity
65
66
  }
66
67
  ///////////////////////////////////////////////////////////////////////////////
67
68
  function gameUpdate() {
@@ -69,16 +69,17 @@ function gameInit()
69
69
  // create particle emitter
70
70
  particleEmitter = new LJS.ParticleEmitter(
71
71
  vec2(16,9), 0, // emitPos, emitAngle
72
- 1, 0, 500, Math.PI, // emitSize, emitTime, rate, cone
72
+ 0, 0, 500, 3.14, // emitSize, emitTime, rate, cone
73
73
  tile(0, 16), // tileIndex, tileSize
74
74
  hsl(1,1,1), hsl(0,0,0), // colorStartA, colorStartB
75
75
  hsl(0,0,0,0), hsl(0,0,0,0), // colorEndA, colorEndB
76
- 1, .2, .2, .1, .05, // time, sizeStart, sizeEnd, speed, angleSpeed
77
- .99, 1, 1, Math.PI, // damping, angleDamping, gravityScale, cone
78
- .05, .5, true, true // fadeRate, randomness, collide, additive
76
+ 1, .2, .2, .1, .05, // time, sizeStart, sizeEnd, speed, angleSpeed
77
+ .99, 1, 1, 3.14, // damping, angleDamping, gravityScale, cone
78
+ .05, .5, true, true // fadeRate, randomness, collide, additive
79
79
  );
80
80
  particleEmitter.restitution = .3; // bounce when it collides
81
81
  particleEmitter.trailScale = 2; // stretch stretch as it moves
82
+ particleEmitter.velocityInheritance = .3; // inherit emitter velocity
82
83
  }
83
84
 
84
85
  ///////////////////////////////////////////////////////////////////////////////
@@ -101,10 +102,7 @@ function gameUpdate()
101
102
 
102
103
  // move particles to mouse location if on screen
103
104
  if (LJS.mousePosScreen.x)
104
- {
105
105
  particleEmitter.pos = LJS.mousePos;
106
- particleEmitter.velocity = LJS.mouseDelta.scale(.3*LJS.timeDelta);
107
- }
108
106
  }
109
107
 
110
108
  ///////////////////////////////////////////////////////////////////////////////
@@ -64,7 +64,7 @@ function createUI()
64
64
  // example button
65
65
  const button1 = new LJS.UIButton(vec2(0,140), vec2(350, 50), 'Test Button');
66
66
  uiMenu.addChild(button1);
67
- button1.onClick = ()=> uiBackground.color = hsl(Math.random(),1,.7);
67
+ button1.onClick = ()=> uiBackground.color = hsl(LJS.rand(),1,.7);
68
68
 
69
69
  // example checkbox
70
70
  const checkbox = new LJS.UICheckbox(vec2(-140,-20), vec2(40));
@@ -114,7 +114,7 @@ function gameRender()
114
114
  // test game rendering
115
115
  for (let i=0; i<1e3; ++i)
116
116
  {
117
- const pos = vec2(30*Math.sin(i+LJS.time/9),20*Math.sin(i*i+LJS.time/9));
117
+ const pos = vec2(30*LJS.sin(i+LJS.time/9),20*LJS.sin(i*i+LJS.time/9));
118
118
  LJS.drawTile(pos, vec2(2), tile(3,128), hsl(i/9,1,.4), LJS.time+i, !(i%2), hsl(i/9,1,.1,0));
119
119
  }
120
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.14.19",
3
+ "version": "1.14.23",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
package/src/engine.js CHANGED
@@ -30,7 +30,7 @@ const engineName = 'LittleJS';
30
30
  * @type {string}
31
31
  * @default
32
32
  * @memberof Engine */
33
- const engineVersion = '1.14.19';
33
+ const engineVersion = '1.14.24';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {number}
@@ -513,7 +513,7 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
513
513
 
514
514
  /**
515
515
  * @callback ObjectCallbackFunction - Function that processes an object
516
- * @param {EngineObject} uiObjects
516
+ * @param {EngineObject} object
517
517
  * @memberof Engine
518
518
  */
519
519
 
@@ -561,7 +561,7 @@ function drawEngineSplashScreen(t)
561
561
  // background
562
562
  const p3 = percent(t, 1, .8);
563
563
  const p4 = percent(t, 0, .5);
564
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,Math.hypot(w,h)*.7);
564
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
565
565
  g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
566
566
  g.addColorStop(1,hsl(0,0,0,p3).toString());
567
567
  x.save();
@@ -605,10 +605,10 @@ function zzfxG
605
605
 
606
606
  // biquad LP/HP filter
607
607
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
608
- cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
609
- a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
610
- b0 = (1 + sign(filter) * cos) / 2 / a0,
611
- b1 = -(sign(filter) + cos) / a0, b2 = b0,
608
+ cosw = cos(w), alpha = sin(w) / 2 / quality,
609
+ a0 = 1 + alpha, a1 = -2*cosw / a0, a2 = (1 - alpha) / a0,
610
+ b0 = (1 + sign(filter) * cosw) / 2 / a0,
611
+ b1 = -(sign(filter) + cosw) / a0, b2 = b0,
612
612
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
613
613
 
614
614
  // scale by sample rate
@@ -631,15 +631,15 @@ function zzfxG
631
631
  if (!(++crush%(bitCrush*100|0))) // bit crush
632
632
  {
633
633
  s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
634
- (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
635
- Math.sin(t**3) : // 4 noise
636
- Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
637
- 1-(2*t/PI2%2+2)%2: // 2 saw
638
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
639
- Math.sin(t); // 0 sin
634
+ (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
635
+ sin(t**3) : // 4 noise
636
+ max(min(tan(t),1),-1): // 3 tan
637
+ 1-(2*t/PI2%2+2)%2: // 2 saw
638
+ 1-4*abs(round(t/PI2)-t/PI2): // 1 triangle
639
+ sin(t); // 0 sin
640
640
 
641
641
  s = (repeatTime ?
642
- 1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
642
+ 1 - tremolo + tremolo*sin(PI2*i/repeatTime) // tremolo
643
643
  : 1) *
644
644
  (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
645
645
  (i < attack ? i/attack : // attack
@@ -661,8 +661,8 @@ function zzfxG
661
661
  }
662
662
 
663
663
  f = (frequency += slide += deltaSlide) *// frequency
664
- Math.cos(modulation*modOffset++); // modulation
665
- t += f + f*noise*Math.sin(i**5); // noise
664
+ cos(modulation*modOffset++); // modulation
665
+ t += f + f*noise*sin(i**5); // noise
666
666
 
667
667
  if (jump && ++jump > pitchJumpTime) // pitch jump
668
668
  {
@@ -622,4 +622,31 @@ function debugVideoCaptureUpdate()
622
622
  combineCanvases();
623
623
  debugVideoCaptureTrack.requestFrame();
624
624
  debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
625
+ }
626
+
627
+ // make color constants immutable with debug assertions
628
+ function debugProtectConstant(obj)
629
+ {
630
+ if (debug)
631
+ {
632
+ // get properties and store original values
633
+ const props = Object.keys(obj), values = {};
634
+ props.forEach(prop => values[prop] = obj[prop]);
635
+
636
+ // replace with getters/setters that assert
637
+ props.forEach(prop =>
638
+ {
639
+ Object.defineProperty(obj, prop, {
640
+ get: () => values[prop],
641
+ set: (value) =>
642
+ {
643
+ ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
644
+ },
645
+ enumerable: true
646
+ });
647
+ });
648
+ }
649
+
650
+ // freeze the object to prevent adding new properties
651
+ return Object.freeze(obj);
625
652
  }
package/src/engineDraw.js CHANGED
@@ -252,6 +252,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
252
252
  const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
253
253
  if (useWebGL)
254
254
  {
255
+ ASSERT(!!glContext, 'WebGL is not enabled!');
255
256
  if (screenSpace)
256
257
  {
257
258
  // convert to world space
@@ -347,6 +348,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
347
348
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
348
349
  if (useWebGL)
349
350
  {
351
+ ASSERT(!!glContext, 'WebGL is not enabled!');
350
352
  if (screenSpace)
351
353
  {
352
354
  // convert to world space
@@ -358,7 +360,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
358
360
  const halfSizeX = size.x/2, halfSizeY = size.y/2;
359
361
  const colorTopInt = colorTop.rgbaInt();
360
362
  const colorBottomInt = colorBottom.rgbaInt();
361
- const c = Math.cos(-angle), s = Math.sin(-angle);
363
+ const c = cos(-angle), s = sin(-angle);
362
364
  for (let i=4; i--;)
363
365
  {
364
366
  const x = i & 1 ? halfSizeX : -halfSizeX;
@@ -409,6 +411,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
409
411
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
410
412
  if (useWebGL)
411
413
  {
414
+ ASSERT(!!glContext, 'WebGL is not enabled!');
412
415
  let scale = 1;
413
416
  if (screenSpace)
414
417
  {
@@ -458,6 +461,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
458
461
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
459
462
  const size = vec2(width, halfDelta.length()*2);
460
463
  pos = pos.add(posA.add(halfDelta));
464
+ if (screenSpace)
465
+ halfDelta.y *= -1; // flip angle Y if screen space
461
466
  angle += halfDelta.angle();
462
467
  drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
463
468
  }
@@ -485,7 +490,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
485
490
  for (let i=sides; i--;)
486
491
  {
487
492
  const a = (i/sides)*PI*2;
488
- points.push(vec2(Math.sin(a)*sizeX, Math.cos(a)*sizeY));
493
+ points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
489
494
  }
490
495
  drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
491
496
  }
@@ -512,6 +517,7 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
512
517
 
513
518
  if (useWebGL)
514
519
  {
520
+ ASSERT(!!glContext, 'WebGL is not enabled!');
515
521
  let scale = 1;
516
522
  if (screenSpace)
517
523
  {
@@ -733,18 +739,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
733
739
  * @memberof Draw */
734
740
  function screenToWorld(screenPos)
735
741
  {
736
- let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
737
- 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;
738
744
  if (cameraAngle)
739
745
  {
740
746
  // apply camera rotation
741
- const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
742
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
743
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
744
- cameraPosRelativeX = rotatedX;
745
- 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;
746
752
  }
747
- return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
753
+ return new Vector2(x + cameraPos.x, y + cameraPos.y);
748
754
  }
749
755
 
750
756
  /** Convert from world to screen space coordinates
@@ -753,24 +759,64 @@ function screenToWorld(screenPos)
753
759
  * @memberof Draw */
754
760
  function worldToScreen(worldPos)
755
761
  {
756
- let cameraPosRelativeX = worldPos.x - cameraPos.x;
757
- let cameraPosRelativeY = worldPos.y - cameraPos.y;
762
+ let x = worldPos.x - cameraPos.x;
763
+ let y = worldPos.y - cameraPos.y;
758
764
  if (cameraAngle)
759
765
  {
760
766
  // apply inverse camera rotation
761
- const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
762
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
763
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
764
- cameraPosRelativeX = rotatedX;
765
- 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;
766
772
  }
767
773
  return new Vector2
768
774
  (
769
- cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
770
- cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
775
+ x * cameraScale + mainCanvasSize.x/2 - .5,
776
+ y * -cameraScale + mainCanvasSize.y/2 - .5
771
777
  );
772
778
  }
773
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
+
774
820
  /** Get the camera's visible area in world space
775
821
  * @return {Vector2}
776
822
  * @memberof Draw */
@@ -823,6 +869,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
823
869
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
824
870
  const sx2 = bleedScale;
825
871
  const sy2 = bleedScale;
872
+ sWidth = max(1,sWidth|0);
873
+ sHeight = max(1,sHeight|0);
826
874
  const sWidth2 = sWidth - 2*bleedScale;
827
875
  const sHeight2 = sHeight - 2*bleedScale;
828
876
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
@@ -837,7 +885,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
837
885
  // copy to offscreen canvas
838
886
  workCanvas.width = sWidth;
839
887
  workCanvas.height = sHeight;
840
- 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);
841
889
 
842
890
  // tint image using offscreen work context
843
891
  const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
@@ -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; }