littlejsengine 1.1.17 → 1.2.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.
@@ -1,5 +1,7 @@
1
1
  /*
2
- LittleJS - Release build include file
2
+ LittleJS - Release Build
3
+ MIT License - Copyright 2021 Frank Force
4
+
3
5
  - This file is used for release builds in place of engineDebug.js
4
6
  - Debug functionality will be disabled to lower size and increase performance
5
7
  */
@@ -80,27 +82,27 @@ const mod = (a, b=1)=> ((a % b) + b) % b;
80
82
 
81
83
  /** Clamps the value beween max and min
82
84
  * @param {Number} value
83
- * @param {Number} [max=1]
84
85
  * @param {Number} [min=0]
86
+ * @param {Number} [max=1]
85
87
  * @return {Number}
86
88
  * @memberof Utilities */
87
- const clamp = (v, max=1, min=0)=> (ASSERT(max > min), v < min ? min : v > max ? max : v);
89
+ const clamp = (v, min=0, max=1)=> v < min ? min : v > max ? max : v;
88
90
 
89
91
  /** Returns what percentage the value is between max and min
90
92
  * @param {Number} value
91
- * @param {Number} [max=1]
92
93
  * @param {Number} [min=0]
94
+ * @param {Number} [max=1]
93
95
  * @return {Number}
94
96
  * @memberof Utilities */
95
- const percent = (v, max=1, min=0)=> max-min ? clamp((v-min) / (max-min)) : 0;
97
+ const percent = (v, min=0, max=1)=> max-min ? clamp((v-min) / (max-min)) : 0;
96
98
 
97
99
  /** Linearly interpolates the percent value between max and min
98
100
  * @param {Number} percent
99
- * @param {Number} [max=1]
100
101
  * @param {Number} [min=0]
102
+ * @param {Number} [max=1]
101
103
  * @return {Number}
102
104
  * @memberof Utilities */
103
- const lerp = (p, max=1, min=0)=> min + clamp(p) * (max-min);
105
+ const lerp = (p, min=0, max=1)=> min + clamp(p) * (max-min);
104
106
 
105
107
  /** Applies smoothstep function to the percentage value
106
108
  * @param {Number} value
@@ -115,11 +117,11 @@ const smoothStep = (p)=> p * p * (3 - 2 * p);
115
117
  const nearestPowerOfTwo = (v)=> 2**Math.ceil(Math.log2(v));
116
118
 
117
119
  /** Returns true if two axis aligned bounding boxes are overlapping
118
- * @param {Vector2} pointA - Center of box A
119
- * @param {Vector2} sizeA - Size of box A
120
- * @param {Vector2} pointB - Center of box B
121
- * @param {Vector2} sizeB - Size of box B
122
- * @return {Boolean} - True if overlapping
120
+ * @param {Vector2} pointA - Center of box A
121
+ * @param {Vector2} sizeA - Size of box A
122
+ * @param {Vector2} pointB - Center of box B
123
+ * @param {Vector2} [sizeB] - Size of box B
124
+ * @return {Boolean} - True if overlapping
123
125
  * @memberof Utilities */
124
126
  const isOverlapping = (pA, sA, pB, sB)=> abs(pA.x - pB.x)*2 < sA.x + sB.x & abs(pA.y - pB.y)*2 < sA.y + sB.y;
125
127
 
@@ -216,10 +218,12 @@ const vec2 = (x=0, y)=> x.x == undefined ? new Vector2(x, y == undefined? x : y)
216
218
 
217
219
  /**
218
220
  * 2D Vector object with vector math library
221
+ * <br> - Functions do not change this so they can be chained together
219
222
  * @example
220
223
  * let a = new Vector2(2, 3); // vector with coordinates (2, 3)
221
224
  * let b = new Vector2; // vector with coordinates (0, 0)
222
225
  * let c = vec2(4, 2); // use the vec2 function to make a Vector2
226
+ * let d = a.add(b).scale(5); // operators can be chained
223
227
  */
224
228
  class Vector2
225
229
  {
@@ -506,7 +510,7 @@ class Timer
506
510
 
507
511
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
508
512
  * @return {Number} */
509
- getPercent() { return this.isSet()? percent(this.time - time, 0, this.setTime) : 0; }
513
+ getPercent() { return this.isSet()? percent(this.time - time, this.setTime, 0) : 0; }
510
514
  }
511
515
  /**
512
516
  * LittleJS Engine Settings
@@ -522,7 +526,7 @@ class Timer
522
526
  * @type {Vector2}
523
527
  * @default
524
528
  * @memberof Settings */
525
- const canvasMaxSize = vec2(1920, 1200);
529
+ let canvasMaxSize = vec2(1920, 1200);
526
530
 
527
531
  /** Fixed size of the canvas, if enabled cavnvas size never changes
528
532
  * @type {Vector2}
@@ -547,12 +551,12 @@ let fontDefault = 'arial';
547
551
  * @type {Vector2}
548
552
  * @default
549
553
  * @memberof Settings */
550
- const tileSizeDefault = vec2(16);
554
+ let tileSizeDefault = vec2(16);
551
555
 
552
556
  /** Prevent tile bleeding from neighbors in pixels
553
557
  * @default
554
558
  * @memberof Settings */
555
- const tileBleedShrinkFix = .3;
559
+ let tileFixBleedScale = .3;
556
560
 
557
561
  ///////////////////////////////////////////////////////////////////////////////
558
562
  // Object settings
@@ -561,43 +565,48 @@ const tileBleedShrinkFix = .3;
561
565
  * @type {Vector2}
562
566
  * @default
563
567
  * @memberof Settings */
564
- const objectDefaultSize = vec2(1);
568
+ let objectDefaultSize = vec2(1);
565
569
 
566
570
  /** Default object mass for collison calcuations (how heavy objects are)
567
571
  * @default
568
572
  * @memberof Settings */
569
- const objectDefaultMass = 1;
573
+ let objectDefaultMass = 1;
570
574
 
571
575
  /** How much to slow velocity by each frame (0-1)
572
576
  * @default
573
577
  * @memberof Settings */
574
- const objectDefaultDamping = .99;
578
+ let objectDefaultDamping = .99;
575
579
 
576
580
  /** How much to slow angular velocity each frame (0-1)
577
581
  * @default
578
582
  * @memberof Settings */
579
- const objectDefaultAngleDamping = .99;
583
+ let objectDefaultAngleDamping = .99;
580
584
 
581
585
  /** How much to bounce when a collision occurs (0-1)
582
586
  * @default
583
587
  * @memberof Settings */
584
- const objectDefaultElasticity = 0;
588
+ let objectDefaultElasticity = 0;
585
589
 
586
590
  /** How much to slow when touching (0-1)
587
591
  * @default
588
592
  * @memberof Settings */
589
- const objectDefaultFriction = .8;
593
+ let objectDefaultFriction = .8;
590
594
 
591
595
  /** Clamp max speed to avoid fast objects missing collisions
592
596
  * @default
593
597
  * @memberof Settings */
594
- const objectMaxSpeed = 1;
598
+ let objectMaxSpeed = 1;
595
599
 
596
600
  /** How much gravity to apply to objects along the Y axis, negative is down
597
601
  * @default
598
602
  * @memberof Settings */
599
603
  let gravity = 0;
600
604
 
605
+ /** Scales emit rate of particles, useful for low graphics mode (0 disables particle emitters)
606
+ * @default
607
+ * @memberof Settings */
608
+ let particleEmitRateScale = 1;
609
+
601
610
  ///////////////////////////////////////////////////////////////////////////////
602
611
  // Camera settings
603
612
 
@@ -618,7 +627,7 @@ let cameraScale = max(tileSizeDefault.x, tileSizeDefault.y);
618
627
  /** Enable webgl rendering, webgl can be disabled and removed from build (with some features disabled)
619
628
  * @default
620
629
  * @memberof Settings */
621
- const glEnable = 1;
630
+ let glEnable = 1;
622
631
 
623
632
  /** Fixes slow rendering in some browsers by not compositing the WebGL canvas
624
633
  * @default
@@ -631,34 +640,39 @@ let glOverlay = 1;
631
640
  /** Should gamepads be allowed
632
641
  * @default
633
642
  * @memberof Settings */
634
- const gamepadsEnable = 1;
643
+ let gamepadsEnable = 1;
635
644
 
636
645
  /** If true, the dpad input is also routed to the left analog stick (for better accessability)
637
646
  * @default
638
647
  * @memberof Settings */
639
- const gamepadDirectionEmulateStick = 1;
648
+ let gamepadDirectionEmulateStick = 1;
640
649
 
641
650
  /** If true the WASD keys are also routed to the direction keys (for better accessability)
642
651
  * @default
643
652
  * @memberof Settings */
644
- const inputWASDEmulateDirection = 1;
653
+ let inputWASDEmulateDirection = 1;
645
654
 
646
- /** If true touch input is routed to mouse functions
655
+ /** True if touch gamepad should appear on mobile devices
656
+ * <br> - Supports left analog stick, 4 face buttons and start button (button 9)
657
+ * <br> - Must be set by end of gameInit to be activated
647
658
  * @default
648
659
  * @memberof Settings */
649
- const touchMouseEnable = 1;
660
+ let touchGamepadEnable = 0;
650
661
 
651
- /** Size of virutal gamepad for touch devices in pixels, 80 recommended
652
- * <br> - Supports left analog stick, 4 face buttons and start button (button 9)
653
- * <br> - Must be set by end of gameInit to be activated
662
+ /** Size of virutal gamepad for touch devices in pixels
654
663
  * @default
655
664
  * @memberof Settings */
656
- let touchGamepadSize = 0;
665
+ let touchGamepadSize = 80;
657
666
 
658
667
  /** Transparency of touch gamepad overlay
659
668
  * @default
660
669
  * @memberof Settings */
661
- let touchGamepadAlpha = .5;
670
+ let touchGamepadAlpha = .3;
671
+
672
+ /** Allow vibration hardware if it exists
673
+ * @default
674
+ * @memberof Settings */
675
+ let vibrateEnable = 1;
662
676
 
663
677
  ///////////////////////////////////////////////////////////////////////////////
664
678
  // Audio settings
@@ -671,17 +685,17 @@ let soundVolume = .5;
671
685
  /** All audio code can be disabled and removed from build
672
686
  * @default
673
687
  * @memberof Settings */
674
- const soundEnable = 1;
688
+ let soundEnable = 1;
675
689
 
676
690
  /** Default range where sound no longer plays
677
691
  * @default
678
692
  * @memberof Settings */
679
- const soundDefaultRange = 30;
693
+ let soundDefaultRange = 30;
680
694
 
681
695
  /** Default range percent to start tapering off sound (0-1)
682
696
  * @default
683
697
  * @memberof Settings */
684
- const soundDefaultTaper = .7;
698
+ let soundDefaultTaper = .7;
685
699
 
686
700
  ///////////////////////////////////////////////////////////////////////////////
687
701
  // Medals settings
@@ -689,27 +703,27 @@ const soundDefaultTaper = .7;
689
703
  /** How long to show medals for in seconds
690
704
  * @default
691
705
  * @memberof Settings */
692
- const medalDisplayTime = 5;
706
+ let medalDisplayTime = 5;
693
707
 
694
708
  /** How quickly to slide on/off medals in seconds
695
709
  * @default
696
710
  * @memberof Settings */
697
- const medalDisplaySlideTime = .5;
711
+ let medalDisplaySlideTime = .5;
698
712
 
699
713
  /** Width of medal display
700
714
  * @default
701
715
  * @memberof Settings */
702
- const medalDisplayWidth = 640;
716
+ let medalDisplayWidth = 640;
703
717
 
704
718
  /** Height of medal display
705
719
  * @default
706
720
  * @memberof Settings */
707
- const medalDisplayHeight = 99;
721
+ let medalDisplayHeight = 80;
708
722
 
709
723
  /** Size of icon in medal display
710
724
  * @default
711
725
  * @memberof Settings */
712
- const medalDisplayIconSize = 80;
726
+ let medalDisplayIconSize = 50;
713
727
  /*
714
728
  LittleJS - The Tiny JavaScript Game Engine That Can!
715
729
  MIT License - Copyright 2021 Frank Force
@@ -734,7 +748,7 @@ const medalDisplayIconSize = 80;
734
748
  const engineName = 'LittleJS';
735
749
 
736
750
  /** Version of engine */
737
- const engineVersion = '1.1.17';
751
+ const engineVersion = '1.2.0';
738
752
 
739
753
  /** Frames per second to update objects
740
754
  * @default */
@@ -747,7 +761,7 @@ const timeDelta = 1/frameRate;
747
761
  /** Array containing all engine objects */
748
762
  let engineObjects = [];
749
763
 
750
- /** Array containing only objects that are set to collide with other objects (for optimization) */
764
+ /** Array containing only objects that are set to collide with other objects this frame (for optimization) */
751
765
  let engineObjectsCollide = [];
752
766
 
753
767
  /** Current update frame, used to calculate time */
@@ -763,8 +777,15 @@ let timeReal = 0;
763
777
  let paused = 0;
764
778
 
765
779
  // Engine internal variables not exposed to documentation
766
- let frameTimeLastMS = 0, frameTimeBufferMS = 0, debugFPS = 0,
767
- shrinkTilesX, shrinkTilesY, drawCount, tileImageSize, tileImageSizeInverse;
780
+ let frameTimeLastMS = 0, frameTimeBufferMS = 0, tileImageSize, tileImageFixBleed;
781
+
782
+ // Engine stat tracking, if showWatermark is true
783
+ let averageFPS, drawCount;
784
+
785
+ // css text used for elements created by engine
786
+ const styleBody = 'margin:0;overflow:hidden;background:#000' +
787
+ ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
788
+ const styleCanvas = 'position:absolute';
768
789
 
769
790
  ///////////////////////////////////////////////////////////////////////////////
770
791
 
@@ -782,17 +803,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
782
803
  tileImage.onerror = tileImage.onload = ()=>
783
804
  {
784
805
  // save tile image info
785
- tileImageSizeInverse = vec2(1).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
806
+ tileImageFixBleed = vec2(tileFixBleedScale).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
786
807
  debug && (tileImage.onload=()=>ASSERT(1)); // tile sheet can not reloaded
787
- shrinkTilesX = tileBleedShrinkFix/tileImageSize.x;
788
- shrinkTilesY = tileBleedShrinkFix/tileImageSize.y;
789
808
 
790
809
  // setup html
810
+ document.body.style = styleBody;
791
811
  document.body.appendChild(mainCanvas = document.createElement('canvas'));
792
- document.body.style = 'margin:0;overflow:hidden;background:#000' +
793
- (touchMouseEnable?';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none':'');
794
- mainCanvas.style = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
795
812
  mainContext = mainCanvas.getContext('2d');
813
+ mainCanvas.style = styleCanvas;
796
814
 
797
815
  // init stuff and start engine
798
816
  debugInit();
@@ -800,8 +818,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
800
818
 
801
819
  // create overlay canvas for hud to appear above gl canvas
802
820
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
803
- overlayCanvas.style = mainCanvas.style.cssText;
804
821
  overlayContext = overlayCanvas.getContext('2d');
822
+ overlayCanvas.style = styleCanvas;
805
823
 
806
824
  gameInit();
807
825
  touchGamepadCreate();
@@ -817,7 +835,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
817
835
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
818
836
  frameTimeLastMS = frameTimeMS;
819
837
  if (debug || showWatermark)
820
- debugFPS = lerp(.05, 1e3/(frameTimeDeltaMS||1), debugFPS);
838
+ averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
821
839
  if (debug)
822
840
  frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
823
841
  timeReal += frameTimeDeltaMS / 1e3;
@@ -868,13 +886,24 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
868
886
 
869
887
  // fit to window by adding space on top or bottom if necessary
870
888
  const aspect = innerWidth / innerHeight;
871
- const fixedAspect = mainCanvas.width / mainCanvas.height;
872
- mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
873
- mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
874
- if (glCanvas)
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
875
904
  {
876
- glCanvas.style.width = mainCanvas.style.width;
877
- glCanvas.style.height = mainCanvas.style.height;
905
+ mainCanvas.style.height = overlayCanvas.style.height = innerHeight;
906
+ mainCanvas.style.left = overlayCanvas.style.left = (innerWidth - innerHeight*fixedAspect)/2;
878
907
  }
879
908
  }
880
909
  else
@@ -882,14 +911,22 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
882
911
  // clear and set size to same as window
883
912
  mainCanvas.width = min(innerWidth, canvasMaxSize.x);
884
913
  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;
885
926
  }
886
927
 
887
- // save canvas size and clear overlay canvas
888
- mainCanvasSize = vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height);
889
- mainContext.imageSmoothingEnabled = !cavasPixelated; // disable smoothing for pixel art
890
-
891
928
  // render sort then render while removing destroyed objects
892
- glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
929
+ enginePreRender();
893
930
  gameRender();
894
931
  engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
895
932
  for (const o of engineObjects)
@@ -908,7 +945,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
908
945
  overlayContext.font = '1em monospace';
909
946
  overlayContext.fillStyle = '#000';
910
947
  const text = engineName + ' ' + 'v' + engineVersion + ' / '
911
- + drawCount + ' / ' + engineObjects.length + ' / ' + debugFPS.toFixed(1);
948
+ + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
949
+ + ' ' + (glEnable ? 'GL' : '2D') ;
912
950
  overlayContext.fillText(text, mainCanvas.width-3, 3);
913
951
  overlayContext.fillStyle = '#fff';
914
952
  overlayContext.fillText(text, mainCanvas.width-2, 2);
@@ -920,12 +958,27 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
920
958
  tileImageSource ? tileImage.src = tileImageSource : tileImage.onload();
921
959
  }
922
960
 
961
+ // called by engine to setup render system
962
+ function enginePreRender()
963
+ {
964
+ // save canvas size and clear canvases
965
+ mainCanvasSize = vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height);
966
+
967
+ // disable smoothing for pixel art
968
+ mainContext.imageSmoothingEnabled = !cavasPixelated;
969
+
970
+ // setup gl rendering if enabled
971
+ glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
972
+ }
923
973
 
924
974
  ///////////////////////////////////////////////////////////////////////////////
925
975
 
926
976
  /** Calls update on each engine object (recursively if child), removes destroyed objects, and updated time */
927
977
  function engineObjectsUpdate()
928
978
  {
979
+ // get list of solid objects for physics optimzation
980
+ engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
981
+
929
982
  // recursive object update
930
983
  const updateObject = (o)=>
931
984
  {
@@ -941,17 +994,16 @@ function engineObjectsUpdate()
941
994
 
942
995
  // remove destroyed objects
943
996
  engineObjects = engineObjects.filter(o=>!o.destroyed);
944
- engineObjectsCollide = engineObjectsCollide.filter(o=>!o.destroyed);
945
997
 
946
998
  // increment frame and update time
947
999
  time = ++frame / frameRate;
948
1000
  }
949
1001
 
950
- /** Detroy and remove all objects that are not persistent or descendants of a persistent object */
1002
+ /** Destroy and remove all objects */
951
1003
  function engineObjectsDestroy()
952
1004
  {
953
1005
  for (const o of engineObjects)
954
- o.persistent || o.parent || o.destroy();
1006
+ o.parent || o.destroy();
955
1007
  engineObjects = engineObjects.filter(o=>!o.destroyed);
956
1008
  }
957
1009
 
@@ -962,21 +1014,18 @@ function engineObjectsDestroy()
962
1014
  * @param {Array} [objects=engineObjects] - List of objects to check */
963
1015
  function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
964
1016
  {
965
- if (!pos)
1017
+ if (!pos) // all objects
966
1018
  {
967
- // all objects
968
1019
  for (const o of objects)
969
1020
  callbackFunction(o);
970
1021
  }
971
- else if (size.x != undefined)
1022
+ else if (size.x != undefined) // bounding box test
972
1023
  {
973
- // aabb test
974
1024
  for (const o of objects)
975
1025
  isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
976
1026
  }
977
- else
1027
+ else // circle test
978
1028
  {
979
- // circle test
980
1029
  const sizeSquared = size*size;
981
1030
  for (const o of objects)
982
1031
  pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
@@ -1016,7 +1065,7 @@ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObject
1016
1065
  class EngineObject
1017
1066
  {
1018
1067
  /** Create an engine object and adds it to the list of objects
1019
- * @param {Vector2} [position=new Vector2(0,0)] - World space position of the object
1068
+ * @param {Vector2} [position=new Vector2()] - World space position of the object
1020
1069
  * @param {Vector2} [size=objectDefaultSize] - World space size of the object
1021
1070
  * @param {Number} [tileIndex=-1] - Tile to use to render object, untextured if -1
1022
1071
  * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
@@ -1061,12 +1110,15 @@ class EngineObject
1061
1110
  this.gravityScale = 1;
1062
1111
  /** @property {Number} [renderOrder=0] - Objects are sorted by render order */
1063
1112
  this.renderOrder = renderOrder;
1113
+ /** @property {Vector2} [velocity=new Vector2()] - Velocity of the object */
1114
+ this.velocity = new Vector2();
1115
+ /** @property {Number} [angleVelocity=0] - Angular velocity of the object */
1116
+ this.angleVelocity = 0;
1064
1117
 
1065
1118
  // init other internal object stuff
1066
1119
  this.spawnTime = time;
1067
- this.velocity = vec2(this.collideSolidObjects = this.angleVelocity = 0);
1068
- this.collideTiles = 1;
1069
1120
  this.children = [];
1121
+ this.collideTiles = 1;
1070
1122
 
1071
1123
  // add to list of objects
1072
1124
  engineObjects.push(this);
@@ -1085,8 +1137,8 @@ class EngineObject
1085
1137
  }
1086
1138
 
1087
1139
  // limit max speed to prevent missing collisions
1088
- this.velocity.x = clamp(this.velocity.x, objectMaxSpeed, -objectMaxSpeed);
1089
- this.velocity.y = clamp(this.velocity.y, objectMaxSpeed, -objectMaxSpeed);
1140
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1141
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1090
1142
 
1091
1143
  // apply physics
1092
1144
  const oldPos = this.pos.copy();
@@ -1134,7 +1186,7 @@ class EngineObject
1134
1186
  // if already was touching, try to push away
1135
1187
  const deltaPos = oldPos.subtract(o.pos);
1136
1188
  const length = deltaPos.length();
1137
- const pushAwayAccel = .001; // push away if alread overlapping
1189
+ const pushAwayAccel = .001; // push away if already overlapping
1138
1190
  const velocity = length < .01 ? randVector(pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
1139
1191
  this.velocity = this.velocity.add(velocity);
1140
1192
  if (o.mass) // push away if not fixed
@@ -1176,8 +1228,8 @@ class EngineObject
1176
1228
 
1177
1229
  // lerp betwen elastic or inelastic based on elasticity
1178
1230
  const elasticity = max(this.elasticity, o.elasticity);
1179
- this.velocity.y = lerp(elasticity, elastic0, inelastic);
1180
- o.velocity.y = lerp(elasticity, elastic1, inelastic);
1231
+ this.velocity.y = lerp(elasticity, inelastic, elastic0);
1232
+ o.velocity.y = lerp(elasticity, inelastic, elastic1);
1181
1233
  }
1182
1234
  }
1183
1235
  if (!smallStepUp && (isBlockedX || !isBlockedY)) // resolve x collision
@@ -1197,8 +1249,8 @@ class EngineObject
1197
1249
 
1198
1250
  // lerp betwen elastic or inelastic based on elasticity
1199
1251
  const elasticity = max(this.elasticity, o.elasticity);
1200
- this.velocity.x = lerp(elasticity, elastic0, inelastic);
1201
- o.velocity.x = lerp(elasticity, elastic1, inelastic);
1252
+ this.velocity.x = lerp(elasticity, inelastic, elastic0);
1253
+ o.velocity.x = lerp(elasticity, inelastic, elastic1);
1202
1254
  }
1203
1255
  else // bounce if other object is fixed
1204
1256
  this.velocity.x *= -this.elasticity;
@@ -1228,7 +1280,7 @@ class EngineObject
1228
1280
 
1229
1281
  // adjust next velocity to settle on ground
1230
1282
  const o = (oldPos.y - this.size.y/2|0) - (oldPos.y - this.size.y/2);
1231
- if (o < 0 && o > -1 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1283
+ if (o < 0 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1232
1284
  this.velocity.y = this.damping ? (o - gravity * this.gravityScale) / this.damping : 0;
1233
1285
 
1234
1286
  // move to previous position
@@ -1329,18 +1381,6 @@ class EngineObject
1329
1381
  {
1330
1382
  ASSERT(collideSolidObjects || !isSolid); // solid objects must be set to collide
1331
1383
 
1332
- // track collidable objects in separate list
1333
- if (collideSolidObjects && !this.collideSolidObjects)
1334
- {
1335
- ASSERT(!engineObjectsCollide.includes(this));
1336
- engineObjectsCollide.push(this);
1337
- }
1338
- else if (!collideSolidObjects && this.collideSolidObjects)
1339
- {
1340
- ASSERT(engineObjectsCollide.includes(this))
1341
- engineObjectsCollide.splice(engineObjectsCollide.indexOf(this), 1);
1342
- }
1343
-
1344
1384
  this.collideSolidObjects = collideSolidObjects;
1345
1385
  this.isSolid = isSolid;
1346
1386
  this.collideTiles = collideTiles;
@@ -1440,17 +1480,13 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1440
1480
  {
1441
1481
  // calculate uvs and render
1442
1482
  const cols = tileImageSize.x / tileSize.x |0;
1443
- const uvSizeX = tileSize.x * tileImageSizeInverse.x;
1444
- const uvSizeY = tileSize.y * tileImageSizeInverse.y;
1483
+ const uvSizeX = tileSize.x / tileImageSize.x;
1484
+ const uvSizeY = tileSize.y / tileImageSize.y;
1445
1485
  const uvX = (tileIndex%cols)*uvSizeX, uvY = (tileIndex/cols|0)*uvSizeY;
1446
-
1447
- // shrink uvs to prevent bleeding
1448
- const shrinkTilesX = tileBleedShrinkFix * tileImageSizeInverse.x;
1449
- const shrinkTilesY = tileBleedShrinkFix * tileImageSizeInverse.y;
1450
1486
 
1451
1487
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
1452
- uvX + shrinkTilesX, uvY + shrinkTilesY,
1453
- uvX - shrinkTilesX + uvSizeX, uvY - shrinkTilesX + uvSizeY,
1488
+ uvX + tileImageFixBleed.x, uvY + tileImageFixBleed.y,
1489
+ uvX - tileImageFixBleed.x + uvSizeX, uvY - tileImageFixBleed.y + uvSizeY,
1454
1490
  color.rgbaInt(), additiveColor.rgbaInt());
1455
1491
  }
1456
1492
  }
@@ -1469,10 +1505,10 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1469
1505
  {
1470
1506
  // calculate uvs and render
1471
1507
  const cols = tileImageSize.x / tileSize.x |0;
1472
- const sX = (tileIndex%cols)*tileSize.x + tileBleedShrinkFix;
1473
- const sY = (tileIndex/cols|0)*tileSize.y + tileBleedShrinkFix;
1474
- const sWidth = tileSize.x - 2*tileBleedShrinkFix;
1475
- const sHeight = tileSize.y - 2*tileBleedShrinkFix;
1508
+ const sX = (tileIndex%cols)*tileSize.x + tileFixBleedScale;
1509
+ const sY = (tileIndex/cols|0)*tileSize.y + tileFixBleedScale;
1510
+ const sWidth = tileSize.x - 2*tileFixBleedScale;
1511
+ const sHeight = tileSize.y - 2*tileFixBleedScale;
1476
1512
  context.globalAlpha = color.a; // only alpha is supported
1477
1513
  context.drawImage(tileImage, sX, sY, sWidth, sHeight, -.5, -.5, 1, 1);
1478
1514
  }
@@ -1555,6 +1591,18 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
1555
1591
  context.restore();
1556
1592
  }
1557
1593
 
1594
+ /** Enable normal or additive blend mode
1595
+ * @param {Boolean} [additive=0]
1596
+ * @param {Boolean} [useWebGL=glEnable]
1597
+ * @memberof Draw */
1598
+ function setBlendMode(additive, useWebGL=glEnable)
1599
+ {
1600
+ if (glEnable && useWebGL)
1601
+ glSetBlendMode(additive);
1602
+ else
1603
+ mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1604
+ }
1605
+
1558
1606
  /** Draw text on overlay canvas in world space
1559
1607
  * @param {String} text
1560
1608
  * @param {Vector2} pos
@@ -1580,16 +1628,93 @@ function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new
1580
1628
  overlayContext.fillText(text, pos.x, pos.y);
1581
1629
  }
1582
1630
 
1583
- /** Enable normal or additive blend mode
1584
- * @param {Boolean} [additive=0]
1585
- * @param {Boolean} [useWebGL=glEnable]
1586
- * @memberof Draw */
1587
- function setBlendMode(additive, useWebGL=glEnable)
1631
+ ///////////////////////////////////////////////////////////////////////////////
1632
+
1633
+ /**
1634
+ * Font Image Object - Draw text on a 2D canvas by using characters in an image
1635
+ * <br> - 96 characters (from space to tilde) are stored in an image
1636
+ * <br> - Uses a default 8x8 font if none is supplied
1637
+ * <br> - You can also use fonts from the main tile sheet
1638
+ * @example
1639
+ * // use built in font
1640
+ * const font = new ImageFont;
1641
+ *
1642
+ * // draw text
1643
+ * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
1644
+ */
1645
+ class FontImage
1588
1646
  {
1589
- if (glEnable && useWebGL)
1590
- glSetBlendMode(additive);
1591
- else
1592
- mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1647
+ /** Create an image font
1648
+ * @param {Image} [image] - The image the font is stored in, if undefined the default font is used
1649
+ * @param {Vector2} [tileSize=vec2(8)] - The size of the font source tiles
1650
+ * @param {Vector2} [paddingSize=vec2(0,1)] - How much extra space to add between characters
1651
+ * @param {Number} [startTileIndex=0] - Tile index in image where font starts
1652
+ * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
1653
+ */
1654
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), startTileIndex=0, context=overlayContext)
1655
+ {
1656
+ if (!image)
1657
+ {
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';
1661
+ }
1662
+
1663
+ this.image = image;
1664
+ this.tileSize = tileSize;
1665
+ this.paddingSize = paddingSize;
1666
+ this.startTileIndex = startTileIndex;
1667
+ this.context = context;
1668
+ }
1669
+
1670
+ /** Draw text in screen space using the image font
1671
+ * @param {String} text
1672
+ * @param {Vector2} pos
1673
+ * @param {Number} [scale=4]
1674
+ * @param {Boolean} [center]
1675
+ */
1676
+ drawTextScreen(text, pos, scale=4, center)
1677
+ {
1678
+ const context = this.context;
1679
+ context.save();
1680
+ context.imageSmoothingEnabled = !cavasPixelated;
1681
+
1682
+ const size = this.tileSize;
1683
+ const drawSize = size.add(this.paddingSize).scale(scale);
1684
+ const cols = this.image.width / this.tileSize.x |0;
1685
+ text.split('\n').forEach((line, i)=>
1686
+ {
1687
+ const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
1688
+ for(let j=line.length; j--;)
1689
+ {
1690
+ // draw each character
1691
+ let charCode = line[j].charCodeAt();
1692
+ if (charCode < 32 || charCode > 127)
1693
+ charCode = 127; // unknown character
1694
+
1695
+ // get the character source location and draw it
1696
+ const tile = this.startTileIndex + charCode - 32;
1697
+ const x = tile % cols;
1698
+ const y = tile / cols |0;
1699
+ const drawPos = pos.add(vec2(j,i).multiply(drawSize));
1700
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
1701
+ drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
1702
+ }
1703
+ });
1704
+
1705
+ context.restore();
1706
+ }
1707
+
1708
+ /** Draw text in world space using the image font
1709
+ * @param {String} text
1710
+ * @param {Vector2} pos
1711
+ * @param {Number} [scale=.25]
1712
+ * @param {Boolean} [center]
1713
+ */
1714
+ drawText(text, pos, scale=1, center)
1715
+ {
1716
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
1717
+ }
1593
1718
  }
1594
1719
 
1595
1720
  ///////////////////////////////////////////////////////////////////////////////
@@ -1653,7 +1778,7 @@ const keyWasReleased = (key, device=0)=> inputData[device] && inputData[device][
1653
1778
 
1654
1779
  /** Clears all input
1655
1780
  * @memberof Input */
1656
- const clearInput = ()=> inputData[0] = [];
1781
+ const clearInput = ()=> inputData = [[]];
1657
1782
 
1658
1783
  /** Returns true if mouse button is down
1659
1784
  * @param {Number} button
@@ -1722,7 +1847,9 @@ const gamepadStick = (stick, gamepad=0)=> stickData[gamepad] ? stickData[gamepa
1722
1847
  ///////////////////////////////////////////////////////////////////////////////
1723
1848
  // Input update called by engine
1724
1849
 
1725
- const inputData = [[]];
1850
+ // store input as a bit field for each key: 1 = isDown, 2 = wasPressed, 4 = wasReleased
1851
+ // mouse and keyboard are stored together in device 0, gamepads are in devices > 0
1852
+ let inputData = [[]];
1726
1853
 
1727
1854
  function inputUpdate()
1728
1855
  {
@@ -1748,36 +1875,38 @@ function inputUpdatePost()
1748
1875
  ///////////////////////////////////////////////////////////////////////////////
1749
1876
  // Keyboard event handlers
1750
1877
 
1751
- onkeydown = e=>
1878
+ onkeydown = (e)=>
1752
1879
  {
1753
1880
  if (debug && e.target != document.body) return;
1754
1881
  e.repeat || (inputData[isUsingGamepad = 0][remapKeyCode(e.keyCode)] = 3);
1755
1882
  debug || e.preventDefault();
1756
1883
  }
1757
- onkeyup = e=>
1884
+ onkeyup = (e)=>
1758
1885
  {
1759
1886
  if (debug && e.target != document.body) return;
1760
1887
  inputData[0][remapKeyCode(e.keyCode)] = 4;
1761
1888
  }
1762
- const remapKeyCode = c=> inputWASDEmulateDirection ? c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
1889
+ const remapKeyCode = (c)=> inputWASDEmulateDirection ? c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
1763
1890
 
1764
1891
  ///////////////////////////////////////////////////////////////////////////////
1765
1892
  // Mouse event handlers
1766
1893
 
1767
- onmousedown = e=> {inputData[isUsingGamepad = 0][e.button] = 3; onmousemove(e); e.button && e.preventDefault();}
1768
- onmouseup = e=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
1769
- onmousemove = e=>
1894
+ onmousedown = (e)=> {inputData[isUsingGamepad = 0][e.button] = 3; onmousemove(e); e.button && e.preventDefault();}
1895
+ onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
1896
+ onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
1897
+ onwheel = (e)=> e.ctrlKey || (mouseWheel = sign(e.deltaY));
1898
+ oncontextmenu = (e)=> !1; // prevent right click menu
1899
+
1900
+ // convert a mouse or touch event position to screen space
1901
+ const mouseToScreen = (mousePos)=>
1770
1902
  {
1771
1903
  if (!mainCanvas)
1772
- return; // fix bug that can occur if user clicks before page loads
1904
+ return vec2(); // fix bug that can occur if user clicks before page loads
1773
1905
 
1774
- // convert mouse pos to canvas space
1775
1906
  const rect = mainCanvas.getBoundingClientRect();
1776
- mousePosScreen = mainCanvasSize.multiply(
1777
- vec2(percent(e.x, rect.right, rect.left), percent(e.y, rect.bottom, rect.top)));
1907
+ return mainCanvasSize.multiply(
1908
+ vec2(percent(mousePos.x, rect.left, rect.right), percent(mousePos.y, rect.top, rect.bottom)));
1778
1909
  }
1779
- onwheel = e=> e.ctrlKey || (mouseWheel = sign(e.deltaY));
1780
- oncontextmenu = e=> !1; // prevent right click menu
1781
1910
 
1782
1911
  ///////////////////////////////////////////////////////////////////////////////
1783
1912
  // Gamepad input
@@ -1785,6 +1914,21 @@ oncontextmenu = e=> !1; // prevent right click menu
1785
1914
  const stickData = [];
1786
1915
  function gamepadsUpdate()
1787
1916
  {
1917
+ if (touchGamepadEnable && touchGamepadTimer.isSet())
1918
+ {
1919
+ // read virtual analog stick
1920
+ const sticks = stickData[0] || (stickData[0] = []);
1921
+ sticks[0] = vec2(touchGamepadStick.x, -touchGamepadStick.y); // flip vertical
1922
+
1923
+ // read virtual gamepad buttons
1924
+ const data = inputData[1] || (inputData[1] = []);
1925
+ for (let i=10; i--;)
1926
+ {
1927
+ const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
1928
+ data[j] = touchGamepadButtons[i] ? 1 + 2*!gamepadIsDown(j,0) : 4*gamepadIsDown(j,0);
1929
+ }
1930
+ }
1931
+
1788
1932
  if (!gamepadsEnable || !navigator.getGamepads || !document.hasFocus() && !debug)
1789
1933
  return;
1790
1934
 
@@ -1802,8 +1946,8 @@ function gamepadsUpdate()
1802
1946
  // read clamp dead zone of analog sticks
1803
1947
  const deadZone = .3, deadZoneMax = .8;
1804
1948
  const applyDeadZone = (v)=>
1805
- v > deadZone ? percent( v, deadZoneMax, deadZone) :
1806
- v < -deadZone ? -percent(-v, deadZoneMax, deadZone) : 0;
1949
+ v > deadZone ? percent( v, deadZone, deadZoneMax) :
1950
+ v < -deadZone ? -percent(-v, deadZone, deadZoneMax) : 0;
1807
1951
 
1808
1952
  // read analog sticks
1809
1953
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
@@ -1815,6 +1959,7 @@ function gamepadsUpdate()
1815
1959
  const button = gamepad.buttons[j];
1816
1960
  data[j] = button.pressed ? 1 + 2*!gamepadIsDown(j,i) : 4*gamepadIsDown(j,i);
1817
1961
  isUsingGamepad |= !i && button.pressed;
1962
+ touchGamepadEnable && touchGamepadTimer.unset(); // disable touch gamepad if using real gamepad
1818
1963
  }
1819
1964
 
1820
1965
  if (gamepadDirectionEmulateStick)
@@ -1828,6 +1973,17 @@ function gamepadsUpdate()
1828
1973
  }
1829
1974
  }
1830
1975
 
1976
+ ///////////////////////////////////////////////////////////////////////////////
1977
+
1978
+ /** Pulse the vibration hardware if it exists
1979
+ * @param {Number} [pattern=100] - a single value in miliseconds or vibration interval array
1980
+ * @memberof Input */
1981
+ const vibrate = (pattern)=> vibrateEnable && Navigator.vibrate && Navigator.vibrate(pattern);
1982
+
1983
+ /** Cancel any ongoing vibration
1984
+ * @memberof Input */
1985
+ const vibrateStop = ()=> vibrate(0);
1986
+
1831
1987
  ///////////////////////////////////////////////////////////////////////////////
1832
1988
  // Touch input
1833
1989
 
@@ -1835,7 +1991,9 @@ function gamepadsUpdate()
1835
1991
  * @const {boolean}
1836
1992
  * @memberof Input */
1837
1993
  const isTouchDevice = window.ontouchstart !== undefined;
1838
- if (touchMouseEnable && isTouchDevice)
1994
+
1995
+ // try to enable touch mouse
1996
+ if (isTouchDevice)
1839
1997
  {
1840
1998
  // handle all touch events the same way
1841
1999
  let wasTouching, hadTouchInput;
@@ -1866,19 +2024,25 @@ if (touchMouseEnable && isTouchDevice)
1866
2024
  }
1867
2025
 
1868
2026
  ///////////////////////////////////////////////////////////////////////////////
1869
- // Touch gamepad - on screen virtual gamepad
2027
+ // touch gamepad, virtual on screen gamepad emulator for touch devices
1870
2028
 
1871
- // create the touch gamepad, called automatically by the engine if touchGamepadSize is set
1872
- const touchGamepadTimer = new Timer;
2029
+ // touch input internal variables
2030
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2(), touchGamepadAnalog = 1;
2031
+
2032
+ // create the touch gamepad, called automatically by the engine
1873
2033
  function touchGamepadCreate()
1874
2034
  {
1875
- if (!touchGamepadSize || !isTouchDevice)
2035
+ if (!touchGamepadEnable || !isTouchDevice)
1876
2036
  return;
1877
2037
 
1878
2038
  ontouchstart = ontouchmove = ontouchend = (e)=>
1879
2039
  {
1880
- if (!touchGamepadSize)
2040
+ if (!touchGamepadEnable)
1881
2041
  return;
2042
+
2043
+ // clear touch gamepad input
2044
+ touchGamepadStick = vec2();
2045
+ touchGamepadButtons = [];
1882
2046
 
1883
2047
  const touching = e.touches.length;
1884
2048
  if (touching)
@@ -1888,6 +2052,13 @@ function touchGamepadCreate()
1888
2052
  // set that gamepad is active
1889
2053
  isUsingGamepad = 1;
1890
2054
  touchGamepadTimer.set();
2055
+
2056
+ if (paused)
2057
+ {
2058
+ // touch anywhere to press start when paused
2059
+ touchGamepadButtons[9] = 1;
2060
+ return;
2061
+ }
1891
2062
  }
1892
2063
 
1893
2064
  // get center of left and right sides
@@ -1895,68 +2066,58 @@ function touchGamepadCreate()
1895
2066
  const buttonCenter = mainCanvasSize.subtract(vec2(touchGamepadSize, touchGamepadSize));
1896
2067
  const startCenter = mainCanvasSize.scale(.5);
1897
2068
 
1898
- // update virtual gamepad
1899
- const data = inputData[1] || (inputData[1] = []);
1900
- const sticks = stickData[0] || (stickData[0] = []);
1901
- sticks[0] = vec2();
1902
-
1903
2069
  // check each touch point
1904
- const buttons = [];
1905
2070
  for (const touch of e.touches)
1906
2071
  {
1907
- const touchPos = vec2(touch.clientX, touch.clientY);
2072
+ const touchPos = mouseToScreen(vec2(touch.clientX, touch.clientY));
1908
2073
  if (touchPos.distance(stickCenter) < touchGamepadSize)
1909
2074
  {
1910
2075
  // virtual analog stick
1911
- sticks[0] = touchPos.subtract(stickCenter).clampLength();
1912
- sticks[0].y = -sticks[0].y; // flip vertical
2076
+ if (touchGamepadAnalog)
2077
+ touchGamepadStick = touchPos.subtract(stickCenter).clampLength(touchGamepadSize/2).scale(2/touchGamepadSize);
2078
+ else
2079
+ {
2080
+ // 8 way dpad
2081
+ const angle = touchPos.subtract(stickCenter).angle();
2082
+ touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
2083
+ }
1913
2084
  }
1914
2085
  else if (touchPos.distance(buttonCenter) < touchGamepadSize)
1915
2086
  {
1916
2087
  // virtual face buttons
1917
2088
  const button = touchPos.subtract(buttonCenter).direction();
1918
- buttons[button > 2 ? 2 : button > 1 ? 3 : button] = 1; // fix button locations
2089
+ touchGamepadButtons[button] = 1;
1919
2090
  }
1920
2091
  else if (touchPos.distance(startCenter) < touchGamepadSize)
1921
2092
  {
1922
- // virtual start button
1923
- buttons[9] = 1;
2093
+ // virtual start button in center
2094
+ touchGamepadButtons[9] = 1;
1924
2095
  }
1925
2096
  }
1926
-
1927
- // read virtual buttons
1928
- for (let j=10; j--;)
1929
- data[j] = buttons[j] ? 1 + 2*!gamepadIsDown(j,0) : 4*gamepadIsDown(j,0);
1930
2097
  }
1931
2098
  }
1932
2099
 
1933
2100
  // render the touch gamepad, called automatically by the engine
1934
2101
  function touchGamepadRender()
1935
2102
  {
1936
- if (!touchGamepadSize || !touchGamepadTimer.isSet())
2103
+ if (!touchGamepadEnable || !touchGamepadTimer.isSet())
1937
2104
  return;
1938
2105
 
1939
- // fade off when not touching
1940
- const alpha = percent(touchGamepadTimer.get(), 4, 5);
1941
- if (!alpha)
2106
+ // fade off when not touching or paused
2107
+ const alpha = percent(touchGamepadTimer.get(), 4, 3);
2108
+ if (!alpha || paused)
1942
2109
  return;
1943
2110
 
1944
2111
  // setup the canvas
1945
2112
  overlayContext.save();
1946
2113
  overlayContext.globalAlpha = alpha*touchGamepadAlpha;
1947
- overlayContext.fillStyle = '#fff6';
1948
2114
  overlayContext.strokeStyle = '#fff';
1949
2115
  overlayContext.lineWidth = 3;
1950
2116
 
1951
- // draw start button
1952
- //overlayContext.beginPath();
1953
- //overlayContext.arc(mainCanvasSize.x/2, mainCanvasSize.y/2, touchGamepadSize*.9, 0,9);
1954
- //overlayContext.fill();
1955
- //overlayContext.stroke();
1956
-
1957
2117
  // draw left analog stick
2118
+ overlayContext.fillStyle = gamepadStick(0).length() > 0 ? '#fff' : '#000';
1958
2119
  overlayContext.beginPath();
1959
- overlayContext.arc(touchGamepadSize, mainCanvasSize.y-touchGamepadSize, touchGamepadSize*.9, 0,9);
2120
+ overlayContext.arc(touchGamepadSize, mainCanvasSize.y-touchGamepadSize, touchGamepadSize/2, 0,9);
1960
2121
  overlayContext.fill();
1961
2122
  overlayContext.stroke();
1962
2123
 
@@ -1964,9 +2125,10 @@ function touchGamepadRender()
1964
2125
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
1965
2126
  for (let i=4; i--;)
1966
2127
  {
1967
- const pos = rightCenter.add((new Vector2).setAngle(i*PI/2, touchGamepadSize*.6));
2128
+ const pos = rightCenter.add((new Vector2).setAngle(i*PI/2, touchGamepadSize/2));
2129
+ overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
1968
2130
  overlayContext.beginPath();
1969
- overlayContext.arc(pos.x, pos.y, touchGamepadSize*.3, 0,9);
2131
+ overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
1970
2132
  overlayContext.fill();
1971
2133
  overlayContext.stroke();
1972
2134
  }
@@ -2045,7 +2207,7 @@ class Sound
2045
2207
  return; // out of range
2046
2208
 
2047
2209
  // attenuate volume by distance
2048
- volume *= percent(lengthSquared**.5, range*this.taper, range);
2210
+ volume *= percent(lengthSquared**.5, range, range*this.taper);
2049
2211
  }
2050
2212
 
2051
2213
  // get pan from screen space coords
@@ -2171,7 +2333,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
2171
2333
 
2172
2334
  /** Stop all queued speech
2173
2335
  * @memberof Audio */
2174
- const stopSpeech = ()=> speechSynthesis && speechSynthesis.cancel();
2336
+ const speakStop = ()=> speechSynthesis && speechSynthesis.cancel();
2175
2337
 
2176
2338
  /** Get frequency of a note on a musical scale
2177
2339
  * @param {Number} semitoneOffset - How many semitones away from the root note
@@ -2202,6 +2364,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2202
2364
  if (!audioContext)
2203
2365
  audioContext = new (window.AudioContext||webkitAudioContext);
2204
2366
 
2367
+ // fix stalled audio
2368
+ audioContext.resume();
2369
+
2370
+ // prevent sounds from building up if they can't be played
2371
+ if (audioContext.state != 'running')
2372
+ return;
2373
+
2205
2374
  // create buffer and source
2206
2375
  const buffer = audioContext.createBuffer(sampleChannels.length, sampleChannels[0].length, zzfxR),
2207
2376
  source = audioContext.createBufferSource();
@@ -2212,11 +2381,18 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2212
2381
  source.playbackRate.value = rate;
2213
2382
  source.loop = loop;
2214
2383
 
2215
- // create pan and gain nodes
2216
- source
2217
- .connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, 1, -1)}))
2218
- .connect(new GainNode(audioContext, {'gain':soundVolume*volume}))
2219
- .connect(audioContext.destination);
2384
+ // create and connect gain node (createGain is more widley spported then GainNode construtor)
2385
+ const gainNode = audioContext.createGain();
2386
+ gainNode.gain.value = soundVolume*volume;
2387
+ gainNode.connect(audioContext.destination);
2388
+
2389
+ // connect source to gain
2390
+ (
2391
+ window.StereoPannerNode ? // create pan node if possible
2392
+ source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)}))
2393
+ : source
2394
+ )
2395
+ .connect(gainNode);
2220
2396
 
2221
2397
  // play and return sound
2222
2398
  source.start();
@@ -2574,8 +2750,9 @@ class TileLayerData
2574
2750
  /**
2575
2751
  * Tile layer object - cached rendering system for tile layers
2576
2752
  * <br> - Each Tile layer is rendered to an off screen canvas
2577
- * <br> - Tile layers are not rendered using WebGL to allow modifications at run time
2578
- * <br> - Tile layers are sorted
2753
+ * <br> - To allow dynamic modifications, layers are rendered using canvas 2d
2754
+ * <br> - Some devices like mobile phones are limited to 4k texture resolution
2755
+ * <br> - So with 16x16 tiles this limits layers to 256x256 on mobile devices
2579
2756
  * @extends EngineObject
2580
2757
  * @example
2581
2758
  * // create tile collision and visible tile layer
@@ -2585,11 +2762,11 @@ class TileLayerData
2585
2762
  class TileLayer extends EngineObject
2586
2763
  {
2587
2764
  /** Create a tile layer object
2588
- * @param {Vector2} [position=new Vector2(0,0)] - World space position
2589
- * @param {Vector2} [size=objectDefaultSize] - World space size
2590
- * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2591
- * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2592
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
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
2593
2770
  */
2594
2771
  constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2595
2772
  {
@@ -2640,7 +2817,7 @@ class TileLayer extends EngineObject
2640
2817
  // flush and copy gl canvas because tile canvas does not use webgl
2641
2818
  glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
2642
2819
 
2643
- // draw the entire cached level onto the main canvas
2820
+ // draw the entire cached level onto the canvas
2644
2821
  const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
2645
2822
  (this.isOverlay ? overlayContext : mainContext).drawImage
2646
2823
  (
@@ -2649,35 +2826,36 @@ class TileLayer extends EngineObject
2649
2826
  );
2650
2827
  }
2651
2828
 
2652
- /** Draw all the tile data to an offscreen canvas using webgl if possible */
2829
+ /** Draw all the tile data to an offscreen canvas
2830
+ * - This may be slow in some browsers
2831
+ */
2653
2832
  redraw()
2654
2833
  {
2655
- this.redrawStart();
2834
+ this.redrawStart(1);
2656
2835
  this.drawAllTileData();
2657
2836
  this.redrawEnd();
2658
2837
  }
2659
2838
 
2660
2839
  /** Call to start the redraw process
2661
- * @param {Boolean} [clear=1] - Should it clear the canvas before drawing */
2662
- redrawStart(clear = 1)
2840
+ * @param {Boolean} [clear=0] - Should it clear the canvas before drawing */
2841
+ redrawStart(clear = 0)
2663
2842
  {
2664
- // clear and set size
2665
- const width = this.size.x * this.tileSize.x;
2666
- const height = this.size.y * this.tileSize.y;
2667
2843
  if (clear)
2668
2844
  {
2669
- this.canvas.width = width;
2670
- this.canvas.height = height;
2845
+ // clear and set size
2846
+ this.canvas.width = this.size.x * this.tileSize.x;
2847
+ this.canvas.height = this.size.y * this.tileSize.y;
2671
2848
  }
2672
2849
 
2673
2850
  // save current render settings
2674
- this.savedRenderSettings = [mainCanvas, mainContext];
2851
+ this.savedRenderSettings = [mainCanvas, mainContext, cameraPos, cameraScale];
2675
2852
 
2676
- // set camera transform for renering
2853
+ // use normal rendering system to render the tiles
2677
2854
  mainCanvas = this.canvas;
2678
2855
  mainContext = this.context;
2679
- mainContext.imageSmoothingEnabled = !cavasPixelated; // disable smoothing for pixel art
2680
- glPreRender(width, height, this.size.x/2, this.size.y/2, this.tileSize.x);
2856
+ cameraPos = this.size.scale(.5);
2857
+ cameraScale = this.tileSize.x;
2858
+ enginePreRender();
2681
2859
  }
2682
2860
 
2683
2861
  /** Call to end the redraw process */
@@ -2688,7 +2866,7 @@ class TileLayer extends EngineObject
2688
2866
  //debugSaveCanvas(this.canvas);
2689
2867
 
2690
2868
  // set stuff back to normal
2691
- [mainCanvas, mainContext] = this.savedRenderSettings;
2869
+ [mainCanvas, mainContext, cameraPos, cameraScale] = this.savedRenderSettings;
2692
2870
  }
2693
2871
 
2694
2872
  /** Draw the tile at a given position
@@ -2716,7 +2894,7 @@ class TileLayer extends EngineObject
2716
2894
  this.drawTileData(vec2(x,y));
2717
2895
  }
2718
2896
 
2719
- /** Draw directly to the 2d canvas in world space (bipass webgl)
2897
+ /** Draw directly to the 2D canvas in world space (bipass webgl)
2720
2898
  * @param {Vector2} pos
2721
2899
  * @param {Vector2} size
2722
2900
  * @param {Number} [angle=0]
@@ -2756,7 +2934,7 @@ class TileLayer extends EngineObject
2756
2934
  else
2757
2935
  {
2758
2936
  const cols = tileImage.width/tileSize.x;
2759
- context.globalAlpha = color.a; // full color not supported in this mode
2937
+ context.globalAlpha = color.a; // only alpha, no color, is supported in this mode
2760
2938
  context.drawImage(tileImage,
2761
2939
  (tileIndex%cols)*tileSize.x, (tileIndex/cols|0)*tileSize.x,
2762
2940
  tileSize.x, tileSize.y, -.5, -.5, 1, 1);
@@ -2824,7 +3002,7 @@ class ParticleEmitter extends EngineObject
2824
3002
  * @param {Number} [randomness=.2] - Apply extra randomness percent
2825
3003
  * @param {Boolean} [collideTiles=0] - Do particles collide against tiles
2826
3004
  * @param {Boolean} [additive=0] - Should particles use addtive blend
2827
- * @param {Boolean} [randomColorLinear=0] - Should color be randomized linearly or across each component
3005
+ * @param {Boolean} [randomColorLinear=1] - Should color be randomized linearly or across each component
2828
3006
  * @param {Number} [renderOrder=0] - Render order for particles (additive is above other stuff by default)
2829
3007
  */
2830
3008
  constructor
@@ -2925,9 +3103,9 @@ class ParticleEmitter extends EngineObject
2925
3103
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
2926
3104
  {
2927
3105
  // emit particles
2928
- if (this.emitRate)
3106
+ if (this.emitRate * particleEmitRateScale)
2929
3107
  {
2930
- const rate = 1/this.emitRate;
3108
+ const rate = 1/this.emitRate/particleEmitRateScale;
2931
3109
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
2932
3110
  this.emitParticle();
2933
3111
  }
@@ -3090,7 +3268,7 @@ function medalsInit(saveName)
3090
3268
  {
3091
3269
  // check if medals are unlocked
3092
3270
  medalsSaveName = saveName;
3093
- debugMedals || medals.forEach(medal=> medal.unlocked = localStorage[medal.storageKey()]);
3271
+ debugMedals || medals.forEach(medal=> medal.unlocked = 1 || localStorage[medal.storageKey()]);
3094
3272
  }
3095
3273
 
3096
3274
  /**
@@ -3123,13 +3301,9 @@ class Medal
3123
3301
  this.name = name;
3124
3302
  this.description = description;
3125
3303
  this.icon = icon;
3126
-
3304
+ this.image = new Image();
3127
3305
  if (src)
3128
- {
3129
- // load image
3130
- this.image = new Image();
3131
3306
  this.image.src = src;
3132
- }
3133
3307
  }
3134
3308
 
3135
3309
  /** Unlocks a medal if not already unlocked */
@@ -3159,21 +3333,23 @@ class Medal
3159
3333
  const y = -medalDisplayHeight*hidePercent;
3160
3334
 
3161
3335
  // draw containing rect and clip to that region
3162
- context.beginPath(context.save());
3336
+ context.save();
3337
+ context.beginPath();
3163
3338
  context.fillStyle = '#ddd'
3164
3339
  context.fill(context.rect(x, y, width, medalDisplayHeight));
3165
- context.strokeStyle = context.fillStyle = '#000';
3166
- context.lineWidth = 2;
3340
+ context.strokeStyle = '#000';
3341
+ context.lineWidth = 3;
3167
3342
  context.stroke();
3168
3343
  context.clip();
3169
3344
 
3170
3345
  // draw the icon and text
3171
3346
  this.renderIcon(x+15+medalDisplayIconSize/2, y+medalDisplayHeight/2);
3172
3347
  context.textAlign = 'left';
3173
- context.font = '40px '+ fontDefault;
3174
- context.fillText(this.name, x+medalDisplayIconSize+25, y+35);
3175
- context.font = '25px '+ fontDefault;
3176
- context.restore(context.fillText(this.description, x+medalDisplayIconSize+25, y+70));
3348
+ context.font = '38px '+ fontDefault;
3349
+ context.fillText(this.name, x+medalDisplayIconSize+30, y+28);
3350
+ context.font = '24px '+ fontDefault;
3351
+ context.fillText(this.description, x+medalDisplayIconSize+30, y+60);
3352
+ context.restore();
3177
3353
  }
3178
3354
 
3179
3355
  /** Render the icon for a medal
@@ -3185,11 +3361,11 @@ class Medal
3185
3361
  {
3186
3362
  // draw the image or icon
3187
3363
  const context = overlayContext;
3364
+ context.fillStyle = '#000';
3188
3365
  context.textAlign = 'center';
3189
3366
  context.textBaseline = 'middle';
3190
- context.font = size*.6 + 'px '+ fontDefault;
3191
- context.fillStyle = '#000';
3192
- if (this.image)
3367
+ context.font = size*.7 + 'px '+ fontDefault;
3368
+ if (this.image.src)
3193
3369
  context.drawImage(this.image, x-size/2, y-size/2, size, size);
3194
3370
  else
3195
3371
  context.fillText(this.icon, x, y); // show icon if there is no image
@@ -3265,7 +3441,6 @@ class Newgrounds
3265
3441
  if (medal)
3266
3442
  {
3267
3443
  // copy newgrounds medal data
3268
- medal.image = new Image();
3269
3444
  medal.image.src = newgroundsMedal['icon'];
3270
3445
  medal.name = newgroundsMedal['name'];
3271
3446
  medal.description = newgroundsMedal['description'];
@@ -3394,32 +3569,27 @@ function glInit()
3394
3569
 
3395
3570
  // create the canvas and tile texture
3396
3571
  glCanvas = document.createElement('canvas');
3397
- glContext = glCanvas.getContext('webgl');
3398
-
3572
+ glContext = glCanvas.getContext('webgl', {antialias: false});
3573
+ glCanvas.style = styleCanvas;
3399
3574
  glTileTexture = glCreateTexture(tileImage);
3400
3575
 
3401
- if (glOverlay)
3402
- {
3403
- // some browsers are much faster without copying the gl buffer so we just overlay it instead
3404
- document.body.appendChild(glCanvas);
3405
- glCanvas.style = mainCanvas.style.cssText;
3406
- }
3576
+ // some browsers are much faster without copying the gl buffer so we just overlay it instead
3577
+ glOverlay && document.body.appendChild(glCanvas);
3407
3578
 
3408
3579
  // setup vertex and fragment shaders
3409
3580
  glShader = glCreateProgram(
3410
3581
  'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3411
3582
  'uniform mat4 m;'+ // transform matrix
3412
- 'attribute float a;'+ // angle
3413
- 'attribute vec2 p,s,t;'+ // position, size, uv
3414
- 'attribute vec4 c,b;'+ // color, additiveColor
3583
+ 'attribute vec2 p,t;'+ // position, uv
3584
+ 'attribute vec4 c,a;'+ // color, additiveColor
3415
3585
  'varying vec2 v;'+ // return uv
3416
3586
  'varying vec4 d,e;'+ // return color, additiveColor
3417
3587
  'void main(){'+ // shader entry point
3418
- 'gl_Position=m*vec4((s*cos(a)-vec2(-s.y,s)*sin(a))*.5+p,1,1);'+// transform position
3419
- 'v=t;d=c;e=b;'+ // pass stuff to fragment shader
3588
+ 'gl_Position=m*vec4(p,1,1);'+// transform position
3589
+ 'v=t;d=c;e=a;'+ // pass stuff to fragment shader
3420
3590
  '}' // end of shader
3421
3591
  ,
3422
- 'precision lowp float;'+ // use highp for better accuracy, lowp for better perf
3592
+ 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3423
3593
  'varying vec2 v;'+ // uv
3424
3594
  'varying vec4 d,e;'+ // color, additiveColor
3425
3595
  'uniform sampler2D j;'+ // texture
@@ -3435,6 +3605,7 @@ function glInit()
3435
3605
  glColorData = new Uint32Array(glVertexData);
3436
3606
 
3437
3607
  // setup the vertex data array
3608
+ let offset = glBatchCount = 0;
3438
3609
  const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
3439
3610
  {
3440
3611
  const location = glContext.getAttribLocation(glShader, name);
@@ -3442,13 +3613,10 @@ function glInit()
3442
3613
  glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3443
3614
  offset += size*typeSize;
3444
3615
  }
3445
- let offset = glBatchCount = 0;
3446
- initVertexAttribArray('a', gl_FLOAT, 4, 1); // angle
3447
3616
  initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3448
- initVertexAttribArray('s', gl_FLOAT, 4, 2); // size
3449
3617
  initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
3450
3618
  initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
3451
- initVertexAttribArray('b', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3619
+ initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3452
3620
  }
3453
3621
 
3454
3622
  /** Set the WebGl blend mode, normally you should call setBlendMode instead
@@ -3469,7 +3637,8 @@ function glSetBlendMode(additive)
3469
3637
  function glSetTexture(texture=glTileTexture)
3470
3638
  {
3471
3639
  if (!glEnable) return;
3472
-
3640
+
3641
+ // must flush cache with the old texture to set a new one
3473
3642
  if (texture != glActiveTexture)
3474
3643
  glFlush();
3475
3644
 
@@ -3562,6 +3731,7 @@ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3562
3731
 
3563
3732
  // clear and set to same size as main canvas
3564
3733
  glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height);
3734
+ glContext.clear(gl_COLOR_BUFFER_BIT);
3565
3735
 
3566
3736
  // set up the shader
3567
3737
  glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
@@ -3605,15 +3775,13 @@ function glFlush()
3605
3775
  * @memberof WebGL */
3606
3776
  function glCopyToContext(context, forceDraw)
3607
3777
  {
3608
- if (!glEnable || !glBatchCount) return;
3778
+ if (!glEnable || !glBatchCount) return;
3609
3779
 
3610
3780
  glFlush();
3781
+
3782
+ // do not draw in overlay mode because the canvas is visible
3611
3783
  if (!glOverlay || forceDraw)
3612
- {
3613
- // do not draw/clear in overlay mode because the canvas is visible
3614
3784
  context.drawImage(glCanvas, 0, 0);
3615
- glContext.clear(gl_COLOR_BUFFER_BIT);
3616
- }
3617
3785
  }
3618
3786
 
3619
3787
  /** Add a sprite to the gl draw list, used by all gl draw functions
@@ -3636,51 +3804,49 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffff
3636
3804
  // flush if there is no room for more verts or if different blend mode
3637
3805
  if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
3638
3806
  glFlush();
3807
+
3808
+ // prepare to create the verts from size and angle
3809
+ 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;
3639
3811
 
3640
3812
  // setup 2 triangles to form a quad
3641
3813
  let offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT;
3642
3814
 
3643
3815
  // vertex 0
3644
- glPositionData[offset++] = angle;
3645
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3646
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = -sizeY;
3647
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3648
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3816
+ glPositionData[offset++] = x - cX - sY;
3817
+ glPositionData[offset++] = y - cY + sX;
3818
+ glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3819
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3649
3820
 
3650
3821
  // vertex 1
3651
- glPositionData[offset++] = angle;
3652
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3653
- glPositionData[offset++] = sizeX; glPositionData[offset++] = sizeY;
3654
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3655
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3822
+ glPositionData[offset++] = x + cX + sY;
3823
+ glPositionData[offset++] = y + cY - sX;
3824
+ glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3825
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3656
3826
 
3657
3827
  // vertex 2
3658
- glPositionData[offset++] = angle;
3659
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3660
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = sizeY;
3661
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv0Y;
3662
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3828
+ glPositionData[offset++] = x - cX + sY;
3829
+ glPositionData[offset++] = y + cY + sX;
3830
+ glPositionData[offset++] = uv0X; glPositionData[offset++] = uv0Y;
3831
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3663
3832
 
3664
3833
  // vertex 0
3665
- glPositionData[offset++] = angle;
3666
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3667
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = -sizeY;
3668
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3669
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3834
+ glPositionData[offset++] = x - cX - sY;
3835
+ glPositionData[offset++] = y - cY + sX;
3836
+ glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3837
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3670
3838
 
3671
3839
  // vertex 3
3672
- glPositionData[offset++] = angle;
3673
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3674
- glPositionData[offset++] = sizeX; glPositionData[offset++] = -sizeY;
3675
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv1Y;
3676
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3840
+ glPositionData[offset++] = x + cX - sY;
3841
+ glPositionData[offset++] = y - cY - sX;
3842
+ glPositionData[offset++] = uv1X; glPositionData[offset++] = uv1Y;
3843
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3677
3844
 
3678
3845
  // vertex 1
3679
- glPositionData[offset++] = angle;
3680
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3681
- glPositionData[offset++] = sizeX; glPositionData[offset++] = sizeY;
3682
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3683
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3846
+ glPositionData[offset++] = x + cX + sY;
3847
+ glPositionData[offset++] = y + cY - sX;
3848
+ glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3849
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3684
3850
  }
3685
3851
 
3686
3852
  ///////////////////////////////////////////////////////////////////////////////
@@ -3712,6 +3878,6 @@ gl_LINK_STATUS = 35714,
3712
3878
 
3713
3879
  // constants for batch rendering
3714
3880
  gl_VERTICES_PER_QUAD = 6,
3715
- gl_INDICIES_PER_VERT = 9,
3881
+ gl_INDICIES_PER_VERT = 6,
3716
3882
  gl_MAX_BATCH = 1<<16,
3717
- gl_VERTEX_BYTE_STRIDE = 4 + (4 * 2) * 3 + (4) * 2; // float + vec2 * 3 + (char * 4) * 2
3883
+ gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2; // vec2 * 2 + (char * 4) * 2