littlejsengine 1.4.7 → 1.4.8

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.
@@ -137,7 +137,7 @@ const wave = (frequency=1, amplitude=1, t=time)=> amplitude/2 * (1 - Math.cos(t*
137
137
  * @param {Number} t - time in seconds
138
138
  * @return {String}
139
139
  * @memberof Utilities */
140
- const formatTime = (t)=> (t/60|0)+':'+(t%60<10?'0':'')+(t%60|0);
140
+ const formatTime = (t)=> (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0);
141
141
 
142
142
  ///////////////////////////////////////////////////////////////////////////////
143
143
 
@@ -161,7 +161,7 @@ const randInt = (a=1, b=0)=> rand(a,b)|0;
161
161
  /** Randomly returns either -1 or 1
162
162
  * @return {Number}
163
163
  * @memberof Random */
164
- const randSign = ()=> (rand(2)|0) * 2 - 1;
164
+ const randSign = ()=> randInt(2) * 2 - 1;
165
165
 
166
166
  /** Returns a random Vector2 within a circular shape
167
167
  * @param {Number} [radius=1]
@@ -185,10 +185,15 @@ const randVector = (length=1)=> new Vector2().setAngle(rand(2*PI), length);
185
185
  const randColor = (cA = new Color, cB = new Color(0,0,0,1), linear)=>
186
186
  linear ? cA.lerp(cB, rand()) : new Color(rand(cA.r,cB.r),rand(cA.g,cB.g),rand(cA.b,cB.b),rand(cA.a,cB.a));
187
187
 
188
- /** The seed used by the randSeeded function, should not be 0
188
+ /** Seed used by the randSeeded function
189
189
  * @memberof Random */
190
190
  let randSeed = 1;
191
191
 
192
+ /** Set seed used by the randSeeded function, should not be 0
193
+ * @param {Number} seed
194
+ * @memberof Random */
195
+ const setRandSeed = (seed)=> randSeed = seed;
196
+
192
197
  /** Returns a seeded random value between the two values passed in using randSeed
193
198
  * @param {Number} [valueA=1]
194
199
  * @param {Number} [valueB=0]
@@ -216,6 +221,14 @@ const randSeeded = (a=1, b=0)=>
216
221
  */
217
222
  const vec2 = (x=0, y)=> x.x == undefined ? new Vector2(x, y == undefined? x : y) : new Vector2(x.x, x.y);
218
223
 
224
+ /**
225
+ * Check if object is a valid Vector2
226
+ * @param {Vector2} vector
227
+ * @return {Boolean}
228
+ * @memberof Utilities
229
+ */
230
+ const isVector2 = (v)=> !isNaN(v.x) && !isNaN(v.y);
231
+
219
232
  /**
220
233
  * 2D Vector object with vector math library
221
234
  * <br> - Functions do not change this so they can be chained together
@@ -245,27 +258,27 @@ class Vector2
245
258
  /** Returns a copy of this vector plus the vector passed in
246
259
  * @param {Vector2} vector
247
260
  * @return {Vector2} */
248
- add(v) { ASSERT(v.x!=undefined); return new Vector2(this.x + v.x, this.y + v.y); }
261
+ add(v) { ASSERT(isVector2(v)); return new Vector2(this.x + v.x, this.y + v.y); }
249
262
 
250
263
  /** Returns a copy of this vector minus the vector passed in
251
264
  * @param {Vector2} vector
252
265
  * @return {Vector2} */
253
- subtract(v) { ASSERT(v.x!=undefined); return new Vector2(this.x - v.x, this.y - v.y); }
266
+ subtract(v) { ASSERT(isVector2(v)); return new Vector2(this.x - v.x, this.y - v.y); }
254
267
 
255
268
  /** Returns a copy of this vector times the vector passed in
256
269
  * @param {Vector2} vector
257
270
  * @return {Vector2} */
258
- multiply(v) { ASSERT(v.x!=undefined); return new Vector2(this.x * v.x, this.y * v.y); }
271
+ multiply(v) { ASSERT(isVector2(v)); return new Vector2(this.x * v.x, this.y * v.y); }
259
272
 
260
273
  /** Returns a copy of this vector divided by the vector passed in
261
274
  * @param {Vector2} vector
262
275
  * @return {Vector2} */
263
- divide(v) { ASSERT(v.x!=undefined); return new Vector2(this.x / v.x, this.y / v.y); }
276
+ divide(v) { ASSERT(isVector2(v)); return new Vector2(this.x / v.x, this.y / v.y); }
264
277
 
265
278
  /** Returns a copy of this vector scaled by the vector passed in
266
279
  * @param {Number} scale
267
280
  * @return {Vector2} */
268
- scale(s) { ASSERT(s.x==undefined); return new Vector2(this.x * s, this.y * s); }
281
+ scale(s) { ASSERT(!isVector2(s)); return new Vector2(this.x * s, this.y * s); }
269
282
 
270
283
  /** Returns the length of this vector
271
284
  * @return {Number} */
@@ -298,12 +311,12 @@ class Vector2
298
311
  /** Returns the dot product of this and the vector passed in
299
312
  * @param {Vector2} vector
300
313
  * @return {Number} */
301
- dot(v) { ASSERT(v.x!=undefined); return this.x*v.x + this.y*v.y; }
314
+ dot(v) { ASSERT(isVector2(v)); return this.x*v.x + this.y*v.y; }
302
315
 
303
316
  /** Returns the cross product of this and the vector passed in
304
317
  * @param {Vector2} vector
305
318
  * @return {Number} */
306
- cross(v) { ASSERT(v.x!=undefined); return this.x*v.y - this.y*v.x; }
319
+ cross(v) { ASSERT(isVector2(v)); return this.x*v.y - this.y*v.x; }
307
320
 
308
321
  /** Returns the angle of this vector, up is angle 0
309
322
  * @return {Number} */
@@ -339,7 +352,7 @@ class Vector2
339
352
  * @param {Vector2} vector
340
353
  * @param {Number} percent
341
354
  * @return {Vector2} */
342
- lerp(v, p) { ASSERT(v.x!=undefined); return this.add(v.subtract(this).scale(clamp(p))); }
355
+ lerp(v, p) { ASSERT(isVector2(v)); return this.add(v.subtract(this).scale(clamp(p))); }
343
356
 
344
357
  /** Returns true if this vector is within the bounds of an array size passed in
345
358
  * @param {Vector2} arraySize
@@ -350,7 +363,7 @@ class Vector2
350
363
  * @param {float} digits - precision to display
351
364
  * @return {String} */
352
365
  toString(digits=3)
353
- { return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`; }
366
+ { if (debug) { return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`; }}
354
367
  }
355
368
 
356
369
  ///////////////////////////////////////////////////////////////////////////////
@@ -588,7 +601,7 @@ class Timer
588
601
 
589
602
  /** Returns this timer expressed as a string
590
603
  * @return {String} */
591
- toString() { if (debug) { return this.unset() ? 'unset' : Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ); } }
604
+ toString() { if (debug) { return this.unset() ? 'unset' : Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ); }}
592
605
 
593
606
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
594
607
  * @return {Number} */
@@ -601,6 +614,20 @@ class Timer
601
614
 
602
615
  'use strict';
603
616
 
617
+ ///////////////////////////////////////////////////////////////////////////////
618
+ // Camera settings
619
+
620
+ /** Position of camera in world space
621
+ * @type {Vector2}
622
+ * @default
623
+ * @memberof Settings */
624
+ let cameraPos = vec2();
625
+
626
+ /** Scale of camera in world space
627
+ * @default
628
+ * @memberof Settings */
629
+ let cameraScale = 16;
630
+
604
631
  ///////////////////////////////////////////////////////////////////////////////
605
632
  // Display settings
606
633
 
@@ -627,6 +654,19 @@ let cavasPixelated = 1;
627
654
  * @memberof Settings */
628
655
  let fontDefault = 'arial';
629
656
 
657
+ ///////////////////////////////////////////////////////////////////////////////
658
+ // WebGL settings
659
+
660
+ /** Enable webgl rendering, webgl can be disabled and removed from build (with some features disabled)
661
+ * @default
662
+ * @memberof Settings */
663
+ let glEnable = 1;
664
+
665
+ /** Fixes slow rendering in some browsers by not compositing the WebGL canvas
666
+ * @default
667
+ * @memberof Settings */
668
+ let glOverlay = 1;
669
+
630
670
  ///////////////////////////////////////////////////////////////////////////////
631
671
  // Tile sheet settings
632
672
 
@@ -695,33 +735,6 @@ let gravity = 0;
695
735
  * @memberof Settings */
696
736
  let particleEmitRateScale = 1;
697
737
 
698
- ///////////////////////////////////////////////////////////////////////////////
699
- // Camera settings
700
-
701
- /** Position of camera in world space
702
- * @type {Vector2}
703
- * @default
704
- * @memberof Settings */
705
- let cameraPos = vec2();
706
-
707
- /** Scale of camera in world space
708
- * @default
709
- * @memberof Settings */
710
- let cameraScale = max(tileSizeDefault.x, tileSizeDefault.y);
711
-
712
- ///////////////////////////////////////////////////////////////////////////////
713
- // WebGL settings
714
-
715
- /** Enable webgl rendering, webgl can be disabled and removed from build (with some features disabled)
716
- * @default
717
- * @memberof Settings */
718
- let glEnable = 1;
719
-
720
- /** Fixes slow rendering in some browsers by not compositing the WebGL canvas
721
- * @default
722
- * @memberof Settings */
723
- let glOverlay = 1;
724
-
725
738
  ///////////////////////////////////////////////////////////////////////////////
726
739
  // Input settings
727
740
 
@@ -832,6 +845,7 @@ let medalDisplayIconSize = 50;
832
845
  - Particle effect system
833
846
  - Medal system tracks and displays achievements
834
847
  - Debug tools and debug rendering system
848
+ - Post processing effects
835
849
  - Call engineInit() to start it up!
836
850
  */
837
851
 
@@ -841,7 +855,7 @@ let medalDisplayIconSize = 50;
841
855
  const engineName = 'LittleJS';
842
856
 
843
857
  /** Version of engine */
844
- const engineVersion = '1.4.7';
858
+ const engineVersion = '1.4.8';
845
859
 
846
860
  /** Frames per second to update objects
847
861
  * @default */
@@ -866,14 +880,13 @@ let time = 0;
866
880
  /** Actual clock time since start in seconds (not affected by pause or frame rate clamping) */
867
881
  let timeReal = 0;
868
882
 
869
- /** Is the game paused? Causes time and objects to not be updated. */
883
+ /** Is the game paused? Causes time and objects to not be updated */
870
884
  let paused = 0;
871
885
 
872
- // Engine internal variables not exposed to documentation
873
- let tileImageSize, tileImageFixBleed;
874
-
875
- // Engine stat tracking, if showWatermark is true
876
- let averageFPS, drawCount;
886
+ /** Set if game is paused
887
+ * @param {Boolean} paused
888
+ */
889
+ function setPaused(_paused) { paused = _paused; }
877
890
 
878
891
  ///////////////////////////////////////////////////////////////////////////////
879
892
 
@@ -894,13 +907,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
894
907
  tileImageFixBleed = vec2(tileFixBleedScale).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
895
908
  debug && (tileImage.onload=()=>ASSERT(1)); // tile sheet can not reloaded
896
909
 
897
- // setup css
910
+ // setup html
898
911
  const styleBody = 'margin:0;overflow:hidden;background:#000' + // fill the window
899
912
  ';touch-action:none' + // prevent mobile pinch to resize
900
913
  ';user-select:none' + // prevent mobile hold to select
901
- ';-webkit-user-select:none;-moz-user-select:none'; // compatibility for mobile
902
-
903
- // setup html
914
+ ';-webkit-user-select:none'; // compatibility for ios
904
915
  document.body.style = styleBody;
905
916
  document.body.appendChild(mainCanvas = document.createElement('canvas'));
906
917
  mainContext = mainCanvas.getContext('2d');
@@ -915,7 +926,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
915
926
 
916
927
  // set canvas style to fill the window
917
928
  const styleCanvas = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
918
- (glCanvas||mainCanvas).style = overlayCanvas.style = mainCanvas.style = styleCanvas;
929
+ (glCanvas||mainCanvas).style = mainCanvas.style = overlayCanvas.style = styleCanvas;
919
930
 
920
931
  gameInit();
921
932
  touchGamepadCreate();
@@ -923,16 +934,16 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
923
934
  };
924
935
 
925
936
  // frame time tracking
926
- let frameTimeLastMS = 0, frameTimeBufferMS = 0;
937
+ let frameTimeLastMS = 0, frameTimeBufferMS, averageFPS;
927
938
 
928
939
  // main update loop
929
- const engineUpdate = (frameTimeMS=0)=>
940
+ function engineUpdate(frameTimeMS=0)
930
941
  {
931
942
  // update time keeping
932
943
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
933
944
  frameTimeLastMS = frameTimeMS;
934
945
  if (debug || showWatermark)
935
- averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
946
+ averageFPS = lerp(.05, averageFPS, 1e3/(frameTimeDeltaMS||1));
936
947
  const debugSpeedUp = debug && keyIsDown(107); // +
937
948
  const debugSpeedDown = debug && keyIsDown(109); // -
938
949
  if (debug)
@@ -944,24 +955,19 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
944
955
 
945
956
  if (canvasFixedSize.x)
946
957
  {
947
- // clear set fixed size
958
+ // clear canvas and set fixed size
948
959
  overlayCanvas.width = mainCanvas.width = canvasFixedSize.x;
949
960
  overlayCanvas.height = mainCanvas.height = canvasFixedSize.y;
950
961
 
951
962
  // fit to window by adding space on top or bottom if necessary
952
963
  const aspect = innerWidth / innerHeight;
953
964
  const fixedAspect = mainCanvas.width / mainCanvas.height;
954
- mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
955
- mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
956
- if (glCanvas)
957
- {
958
- glCanvas.style.width = mainCanvas.style.width;
959
- glCanvas.style.height = mainCanvas.style.height;
960
- }
965
+ (glCanvas||mainCanvas).style.width = mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
966
+ (glCanvas||mainCanvas).style.height = mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
961
967
  }
962
968
  else
963
969
  {
964
- // clear and set size to same as window
970
+ // clear canvas and set size to same as window
965
971
  overlayCanvas.width = mainCanvas.width = min(innerWidth, canvasMaxSize.x);
966
972
  overlayCanvas.height = mainCanvas.height = min(innerHeight, canvasMaxSize.y);
967
973
  }
@@ -1028,7 +1034,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
1028
1034
  overlayContext.fillStyle = '#000';
1029
1035
  const text = engineName + ' ' + 'v' + engineVersion + ' / '
1030
1036
  + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
1031
- + ' ' + (glEnable ? 'GL' : '2D') ;
1037
+ + (glEnable ? ' GL' : ' 2D') ;
1032
1038
  overlayContext.fillText(text, mainCanvas.width-3, 3);
1033
1039
  overlayContext.fillStyle = '#fff';
1034
1040
  overlayContext.fillText(text, mainCanvas.width-2, 2);
@@ -1042,7 +1048,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
1042
1048
  tileImageSource ? tileImage.src = tileImageSource : tileImage.onload();
1043
1049
  }
1044
1050
 
1045
- // called by engine to setup render system
1051
+ // Called automatically by engine to setup render system
1046
1052
  function enginePreRender()
1047
1053
  {
1048
1054
  // save canvas size
@@ -1052,12 +1058,10 @@ function enginePreRender()
1052
1058
  mainContext.imageSmoothingEnabled = !cavasPixelated;
1053
1059
 
1054
1060
  // setup gl rendering if enabled
1055
- glEnable && glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
1061
+ glEnable && glPreRender();
1056
1062
  }
1057
1063
 
1058
- ///////////////////////////////////////////////////////////////////////////////
1059
-
1060
- /** Calls update on each engine object (recursively if child), removes destroyed objects, and updated time */
1064
+ /** Update each engine object, remove destroyed objects, and update time */
1061
1065
  function engineObjectsUpdate()
1062
1066
  {
1063
1067
  // get list of solid objects for physics optimzation
@@ -1160,7 +1164,7 @@ class EngineObject
1160
1164
  constructor(pos=vec2(), size=objectDefaultSize, tileIndex=-1, tileSize=tileSizeDefault, angle=0, color, renderOrder=0)
1161
1165
  {
1162
1166
  // set passed in params
1163
- ASSERT(pos && pos.x != undefined && size.x != undefined); // ensure pos and size are vec2s
1167
+ ASSERT(isVector2(pos) && isVector2(size)); // ensure pos and size are vec2s
1164
1168
 
1165
1169
  /** @property {Vector2} - World space position of the object */
1166
1170
  this.pos = pos.copy();
@@ -1250,7 +1254,7 @@ class EngineObject
1250
1254
  if (this.collideSolidObjects)
1251
1255
  {
1252
1256
  // check collisions against solid objects
1253
- const epsilon = 1e-3; // necessary to push slightly outside of the collision
1257
+ const epsilon = .001; // necessary to push slightly outside of the collision
1254
1258
  for (const o of engineObjectsCollide)
1255
1259
  {
1256
1260
  // non solid objects don't collide with eachother
@@ -1513,11 +1517,6 @@ class EngineObject
1513
1517
 
1514
1518
  'use strict';
1515
1519
 
1516
- /** Tile sheet for batch rendering system
1517
- * @type {Image}
1518
- * @memberof Draw */
1519
- const tileImage = new Image();
1520
-
1521
1520
  /** The primary 2D canvas visible to the user
1522
1521
  * @type {HTMLCanvasElement}
1523
1522
  * @memberof Draw */
@@ -1543,6 +1542,14 @@ let overlayContext;
1543
1542
  * @memberof Draw */
1544
1543
  let mainCanvasSize = vec2();
1545
1544
 
1545
+ /** Tile sheet for batch rendering system
1546
+ * @type {Image}
1547
+ * @memberof Draw */
1548
+ const tileImage = new Image;
1549
+
1550
+ // Engine internal variables not exposed to documentation
1551
+ let tileImageSize, tileImageFixBleed, drawCount;
1552
+
1546
1553
  /** Convert from screen to world space coordinates
1547
1554
  * - if calling outside of render, you may need to manually set mainCanvasSize
1548
1555
  * @param {Vector2} screenPos
@@ -1567,7 +1574,7 @@ const worldToScreen = (worldPos)=>
1567
1574
 
1568
1575
  /** Draw textured tile centered in world space, with color applied if using WebGL
1569
1576
  * @param {Vector2} pos - Center of the tile in world space
1570
- * @param {Vector2} [size=new Vector2(1,1)] - Size of the tile in world space, width and height
1577
+ * @param {Vector2} [size=new Vector2(1,1)] - Size of the tile in world space
1571
1578
  * @param {Number} [tileIndex=-1] - Tile index to use, negative is untextured
1572
1579
  * @param {Vector2} [tileSize=tileSizeDefault] - Tile size in source pixels
1573
1580
  * @param {Color} [color=new Color(1,1,1)] - Color to modulate with
@@ -1786,10 +1793,10 @@ class FontImage
1786
1793
  */
1787
1794
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), startTileIndex=0, context=overlayContext)
1788
1795
  {
1789
- if (!image && !engineFontImage)
1796
+ if (!engineFontImage)
1790
1797
  {
1791
1798
  // load default font image
1792
- engineFontImage = new Image();
1799
+ engineFontImage = new Image;
1793
1800
  engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
1794
1801
  }
1795
1802
 
@@ -1856,7 +1863,7 @@ class FontImage
1856
1863
  /** Returns true if fullscreen mode is active
1857
1864
  * @return {Boolean}
1858
1865
  * @memberof Draw */
1859
- const isFullscreen =()=> document.fullscreenElement;
1866
+ const isFullscreen = ()=> document.fullscreenElement;
1860
1867
 
1861
1868
  /** Toggle fullsceen mode
1862
1869
  * @memberof Draw */
@@ -1866,16 +1873,9 @@ function toggleFullscreen()
1866
1873
  {
1867
1874
  if (document.exitFullscreen)
1868
1875
  document.exitFullscreen();
1869
- else if (document.mozCancelFullScreen)
1870
- document.mozCancelFullScreen();
1871
- }
1872
- else
1873
- {
1874
- if (document.body.webkitRequestFullScreen)
1875
- document.body.webkitRequestFullScreen();
1876
- else if (document.body.mozRequestFullScreen)
1877
- document.body.mozRequestFullScreen();
1878
1876
  }
1877
+ else if (document.body.requestFullscreen)
1878
+ document.body.requestFullscreen();
1879
1879
  }
1880
1880
 
1881
1881
  /**
@@ -1894,7 +1894,7 @@ function toggleFullscreen()
1894
1894
  * @param {Number} [device=0]
1895
1895
  * @return {Boolean}
1896
1896
  * @memberof Input */
1897
- const keyIsDown = (key, device=0)=> inputData[device] && inputData[device][key] & 1 ? 1 : 0;
1897
+ const keyIsDown = (key, device=0)=> inputData[device] && inputData[device][key] & 1;
1898
1898
 
1899
1899
  /** Returns true if device key was pressed this frame
1900
1900
  * @param {Number} key
@@ -2134,35 +2134,40 @@ const isTouchDevice = window.ontouchstart !== undefined;
2134
2134
  if (isTouchDevice)
2135
2135
  {
2136
2136
  // override mouse events
2137
- const mouseDown = onmousedown, mouseUp = onmouseup, mouseMove = onmousemove;
2138
- onmousedown = onmouseup = onmousemove = (e)=> 0;
2137
+ let wasTouching, mouseDown = onmousedown, mouseUp = onmouseup;
2138
+ onmousedown = onmouseup = ()=> 0;
2139
2139
 
2140
- // handle all touch events the same way
2141
- let wasTouching, hadTouch;
2142
- ontouchstart = ontouchmove = ontouchend = (e)=>
2140
+ // setup touch input
2141
+ ontouchstart = (e)=>
2143
2142
  {
2144
- e.button = 0; // all touches are left click
2143
+ // fix mobile audio, force it to play a sound on first touch
2144
+ zzfx(0);
2145
2145
 
2146
- // check if touching and pass to mouse events
2147
- const touching = e.touches.length;
2148
- if (touching)
2146
+ // handle all touch events the same way
2147
+ ontouchstart = ontouchmove = ontouchend = (e)=>
2149
2148
  {
2150
- // fix mobile audio, force it to play a sound on first touch
2151
- hadTouch || zzfx(0, hadTouch=1);
2149
+ e.button = 0; // all touches are left click
2152
2150
 
2153
- // set event pos and pass it along
2154
- e.x = e.touches[0].clientX;
2155
- e.y = e.touches[0].clientY;
2156
- wasTouching ? mouseMove(e) : mouseDown(e);
2157
- }
2158
- else if (wasTouching)
2159
- mouseUp(e);
2151
+ // check if touching and pass to mouse events
2152
+ const touching = e.touches.length;
2153
+ if (touching)
2154
+ {
2155
+ // set event pos and pass it along
2156
+ e.x = e.touches[0].clientX;
2157
+ e.y = e.touches[0].clientY;
2158
+ wasTouching ? onmousemove(e) : mouseDown(e);
2159
+ }
2160
+ else if (wasTouching)
2161
+ mouseUp(e);
2160
2162
 
2161
- // set was touching
2162
- wasTouching = touching;
2163
+ // set was touching
2164
+ wasTouching = touching;
2163
2165
 
2164
- // must return true so the document will get focus
2165
- return true;
2166
+ // must return true so the document will get focus
2167
+ return true;
2168
+ }
2169
+
2170
+ return ontouchstart(e);
2166
2171
  }
2167
2172
  }
2168
2173
 
@@ -2170,7 +2175,7 @@ if (isTouchDevice)
2170
2175
  // touch gamepad, virtual on screen gamepad emulator for touch devices
2171
2176
 
2172
2177
  // touch input internal variables
2173
- let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
2178
+ let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2174
2179
 
2175
2180
  // create the touch gamepad, called automatically by the engine
2176
2181
  function touchGamepadCreate()
@@ -2178,65 +2183,73 @@ function touchGamepadCreate()
2178
2183
  if (!touchGamepadEnable || !isTouchDevice)
2179
2184
  return;
2180
2185
 
2181
- ontouchstart = ontouchmove = ontouchend = (e)=>
2186
+ // touch input internal variables
2187
+ touchGamepadButtons = [];
2188
+ touchGamepadStick = vec2();
2189
+
2190
+ // setup touch input
2191
+ ontouchstart = (e)=>
2182
2192
  {
2183
- if (!touchGamepadEnable)
2184
- return;
2193
+ // fix mobile audio, force it to play a sound on first touch
2194
+ zzfx(0);
2185
2195
 
2186
- // clear touch gamepad input
2187
- touchGamepadStick = vec2();
2188
- touchGamepadButtons = [];
2189
-
2190
- const touching = e.touches.length;
2191
- if (touching)
2196
+ ontouchstart = ontouchmove = ontouchend = (e)=>
2192
2197
  {
2193
- touchGamepadTimer.isSet() || zzfx(0) ; // fix mobile audio, force it to play a sound the first time
2194
-
2195
- // set that gamepad is active
2196
- isUsingGamepad = 1;
2197
- touchGamepadTimer.set();
2198
-
2199
- if (paused)
2198
+ // clear touch gamepad input
2199
+ touchGamepadStick = vec2();
2200
+ touchGamepadButtons = [];
2201
+
2202
+ const touching = e.touches.length;
2203
+ if (touching)
2200
2204
  {
2201
- // touch anywhere to press start when paused
2202
- touchGamepadButtons[9] = 1;
2203
- return;
2205
+ // set that gamepad is active
2206
+ isUsingGamepad = 1;
2207
+ touchGamepadTimer.set();
2208
+
2209
+ if (paused)
2210
+ {
2211
+ // touch anywhere to press start when paused
2212
+ touchGamepadButtons[9] = 1;
2213
+ return;
2214
+ }
2204
2215
  }
2205
- }
2206
2216
 
2207
- // get center of left and right sides
2208
- const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2209
- const buttonCenter = mainCanvasSize.subtract(vec2(touchGamepadSize, touchGamepadSize));
2210
- const startCenter = mainCanvasSize.scale(.5);
2217
+ // get center of left and right sides
2218
+ const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2219
+ const buttonCenter = mainCanvasSize.subtract(vec2(touchGamepadSize, touchGamepadSize));
2220
+ const startCenter = mainCanvasSize.scale(.5);
2211
2221
 
2212
- // check each touch point
2213
- for (const touch of e.touches)
2214
- {
2215
- const touchPos = mouseToScreen(vec2(touch.clientX, touch.clientY));
2216
- if (touchPos.distance(stickCenter) < touchGamepadSize)
2222
+ // check each touch point
2223
+ for (const touch of e.touches)
2217
2224
  {
2218
- // virtual analog stick
2219
- if (touchGamepadAnalog)
2220
- touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
2221
- else
2225
+ const touchPos = mouseToScreen(vec2(touch.clientX, touch.clientY));
2226
+ if (touchPos.distance(stickCenter) < touchGamepadSize)
2227
+ {
2228
+ // virtual analog stick
2229
+ if (touchGamepadAnalog)
2230
+ touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
2231
+ else
2232
+ {
2233
+ // 8 way dpad
2234
+ const angle = touchPos.subtract(stickCenter).angle();
2235
+ touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
2236
+ }
2237
+ }
2238
+ else if (touchPos.distance(buttonCenter) < touchGamepadSize)
2222
2239
  {
2223
- // 8 way dpad
2224
- const angle = touchPos.subtract(stickCenter).angle();
2225
- touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
2240
+ // virtual face buttons
2241
+ const button = touchPos.subtract(buttonCenter).direction();
2242
+ touchGamepadButtons[button] = 1;
2243
+ }
2244
+ else if (touchPos.distance(startCenter) < touchGamepadSize)
2245
+ {
2246
+ // virtual start button in center
2247
+ touchGamepadButtons[9] = 1;
2226
2248
  }
2227
- }
2228
- else if (touchPos.distance(buttonCenter) < touchGamepadSize)
2229
- {
2230
- // virtual face buttons
2231
- const button = touchPos.subtract(buttonCenter).direction();
2232
- touchGamepadButtons[button] = 1;
2233
- }
2234
- else if (touchPos.distance(startCenter) < touchGamepadSize)
2235
- {
2236
- // virtual start button in center
2237
- touchGamepadButtons[9] = 1;
2238
2249
  }
2239
2250
  }
2251
+
2252
+ return ontouchstart(e);
2240
2253
  }
2241
2254
  }
2242
2255
 
@@ -2354,7 +2367,7 @@ class Sound
2354
2367
  {
2355
2368
  if (!soundEnable) return;
2356
2369
 
2357
- let pan = 0;
2370
+ let pan;
2358
2371
  if (pos)
2359
2372
  {
2360
2373
  const range = this.range;
@@ -2384,7 +2397,7 @@ class Sound
2384
2397
  * @param {Number} [volume=1] - How much to scale volume by (in addition to range fade)
2385
2398
  * @return {AudioBufferSourceNode} - The audio, can be used to stop sound later
2386
2399
  */
2387
- playNote(semitoneOffset, pos, volume=1)
2400
+ playNote(semitoneOffset, pos, volume)
2388
2401
  {
2389
2402
  if (!soundEnable) return;
2390
2403
 
@@ -2439,7 +2452,7 @@ class Music
2439
2452
  * @param {Boolean} [loop=1] - True if the music should loop when it reaches the end
2440
2453
  * @return {AudioBufferSourceNode} - The audio node, can be used to stop sound later
2441
2454
  */
2442
- play(volume = 1, loop = 1)
2455
+ play(volume, loop = 1)
2443
2456
  {
2444
2457
  if (!soundEnable) return;
2445
2458
 
@@ -2521,7 +2534,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2521
2534
 
2522
2535
  // create audio context
2523
2536
  if (!audioContext)
2524
- audioContext = new (window.AudioContext||webkitAudioContext);
2537
+ audioContext = new AudioContext;
2525
2538
 
2526
2539
  // fix stalled audio
2527
2540
  audioContext.resume();
@@ -2540,18 +2553,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2540
2553
  source.playbackRate.value = rate;
2541
2554
  source.loop = loop;
2542
2555
 
2543
- // create and connect gain node (createGain is more widley spported then GainNode construtor)
2556
+ // create and connect gain node (createGain is more widely spported then GainNode construtor)
2544
2557
  const gainNode = audioContext.createGain();
2545
2558
  gainNode.gain.value = soundVolume*volume;
2546
2559
  gainNode.connect(audioContext.destination);
2547
2560
 
2548
- // connect source to gain
2549
- (
2550
- window.StereoPannerNode ? // create pan node if possible
2551
- source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)}))
2552
- : source
2553
- )
2554
- .connect(gainNode);
2561
+ // connect source to stereo panner and gain
2562
+ source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
2555
2563
 
2556
2564
  // play and return sound
2557
2565
  source.start();
@@ -2669,10 +2677,8 @@ function zzfxG
2669
2677
  * @memberof Audio */
2670
2678
  function zzfxM(instruments, patterns, sequence, BPM = 125)
2671
2679
  {
2680
+ let i, j, k;
2672
2681
  let instrumentParameters;
2673
- let i;
2674
- let j;
2675
- let k;
2676
2682
  let note;
2677
2683
  let sample;
2678
2684
  let patternChannel;
@@ -2842,14 +2848,11 @@ function tileCollisionRaycast(posStart, posEnd, object)
2842
2848
  {
2843
2849
  // test if a ray collides with tiles from start to end
2844
2850
  // todo: a way to get the exact hit point, it must still register as inside the hit tile
2845
- posStart = posStart.floor();
2846
- posEnd = posEnd.floor();
2847
- const posDelta = posEnd.subtract(posStart);
2851
+ const posDelta = (posEnd = posEnd.floor()).subtract(posStart = posStart.floor());
2848
2852
  const dx = abs(posDelta.x), dy = -abs(posDelta.y);
2849
2853
  const sx = sign(posDelta.x), sy = sign(posDelta.y);
2850
- let e = dx + dy;
2851
2854
 
2852
- for (let x = posStart.x, y = posStart.y;;)
2855
+ for (let x = posStart.x, y = posStart.y, e = dx + dy;;)
2853
2856
  {
2854
2857
  const tileData = getTileCollisionData(vec2(x,y));
2855
2858
  if (tileData && (object ? object.collideWithTileRaycast(tileData, new Vector2(x, y)) : tileData > 0))
@@ -2941,7 +2944,7 @@ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1)
2941
2944
  // init tile data
2942
2945
  this.data = [];
2943
2946
  for (let j = this.size.area(); j--;)
2944
- this.data.push(new TileLayerData());
2947
+ this.data.push(new TileLayerData);
2945
2948
  }
2946
2949
 
2947
2950
  /** Set data at a given position in the array
@@ -2997,21 +3000,23 @@ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1)
2997
3000
  * @param {Boolean} [clear=0] - Should it clear the canvas before drawing */
2998
3001
  redrawStart(clear = 0)
2999
3002
  {
3000
- if (clear)
3001
- {
3002
- // clear and set size
3003
- this.canvas.width = this.size.x * this.tileSize.x;
3004
- this.canvas.height = this.size.y * this.tileSize.y;
3005
- }
3006
-
3007
3003
  // save current render settings
3008
- this.savedRenderSettings = [mainCanvas, mainContext, cameraPos, cameraScale];
3004
+ this.savedRenderSettings = [mainCanvas, mainContext, mainCanvasSize, cameraPos, cameraScale];
3009
3005
 
3010
- // use normal rendering system to render the tiles
3006
+ // hack: use normal rendering system to render the tiles
3011
3007
  mainCanvas = this.canvas;
3012
3008
  mainContext = this.context;
3013
3009
  cameraPos = this.size.scale(.5);
3014
3010
  cameraScale = this.tileSize.x;
3011
+
3012
+ if (clear)
3013
+ {
3014
+ // clear and set size
3015
+ mainCanvas.width = this.size.x * this.tileSize.x;
3016
+ mainCanvas.height = this.size.y * this.tileSize.y;
3017
+ }
3018
+
3019
+ // begin a new render for the tile canvas
3015
3020
  enginePreRender();
3016
3021
  }
3017
3022
 
@@ -3023,7 +3028,7 @@ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1)
3023
3028
  //debugSaveCanvas(this.canvas);
3024
3029
 
3025
3030
  // set stuff back to normal
3026
- [mainCanvas, mainContext, cameraPos, cameraScale] = this.savedRenderSettings;
3031
+ [mainCanvas, mainContext, mainCanvasSize, cameraPos, cameraScale] = this.savedRenderSettings;
3027
3032
  }
3028
3033
 
3029
3034
  /** Draw the tile at a given position
@@ -3264,7 +3269,7 @@ class ParticleEmitter extends EngineObject
3264
3269
  this.parent && super.update();
3265
3270
 
3266
3271
  // update emitter
3267
- if (!this.emitTime || this.getAliveTime() <= this.emitTime)
3272
+ if (!this.emitTime | this.getAliveTime() <= this.emitTime)
3268
3273
  {
3269
3274
  // emit particles
3270
3275
  if (this.emitRate * particleEmitRateScale)
@@ -3286,9 +3291,9 @@ class ParticleEmitter extends EngineObject
3286
3291
  {
3287
3292
  // spawn a particle
3288
3293
  let pos = this.emitSize.x != undefined ? // check if vec2 was used for size
3289
- (new Vector2(rand(-.5,.5), rand(-.5,.5)))
3290
- .multiply(this.emitSize).rotate(this.angle) // box emitter
3291
- : randInCircle(this.emitSize * .5); // circle emitter
3294
+ (new Vector2(rand(-1,1), rand(-1,1)))
3295
+ .multiply(this.emitSize/2).rotate(this.angle) // box emitter
3296
+ : randInCircle(this.emitSize/2); // circle emitter
3292
3297
  let angle = rand(this.particleConeAngle, -this.particleConeAngle);
3293
3298
  if (!this.localSpace)
3294
3299
  {
@@ -3331,7 +3336,7 @@ class ParticleEmitter extends EngineObject
3331
3336
  particle.additive = this.additive;
3332
3337
  particle.renderOrder = this.renderOrder;
3333
3338
  particle.trailScale = this.trailScale;
3334
- particle.mirror = rand()<.5;
3339
+ particle.mirror = randInt(2);
3335
3340
  particle.localSpaceEmitter = this.localSpace && this;
3336
3341
 
3337
3342
  // setup callbacks for particles
@@ -3360,7 +3365,8 @@ class Particle extends EngineObject
3360
3365
  * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
3361
3366
  * @param {Number} [angle=0] - Angle to rotate the particle
3362
3367
  */
3363
- constructor(pos, tileIndex, tileSize, angle) { super(pos, new Vector2, tileIndex, tileSize, angle); }
3368
+ constructor(pos, tileIndex, tileSize, angle)
3369
+ { super(pos, new Vector2, tileIndex, tileSize, angle); }
3364
3370
 
3365
3371
  /** Render the particle, automatically called each frame, sorted by renderOrder */
3366
3372
  render()
@@ -3480,7 +3486,7 @@ class Medal
3480
3486
  this.name = name;
3481
3487
  this.description = description;
3482
3488
  this.icon = icon;
3483
- this.image = new Image();
3489
+ this.image = new Image;
3484
3490
  if (src)
3485
3491
  this.image.src = src;
3486
3492
  }
@@ -3495,10 +3501,7 @@ class Medal
3495
3501
  ASSERT(medalsSaveName); // save name must be set
3496
3502
  localStorage[this.storageKey()] = this.unlocked = 1;
3497
3503
  medalsDisplayQueue.push(this);
3498
-
3499
- // save for newgrounds and OS13K
3500
3504
  newgrounds && newgrounds.unlockMedal(this.id);
3501
- localStorage['OS13kTrophy,' + this.icon + ',' + medalsSaveName + ',' + this.name] = this.description;
3502
3505
  }
3503
3506
 
3504
3507
  /** Render a medal
@@ -3514,20 +3517,19 @@ class Medal
3514
3517
  // draw containing rect and clip to that region
3515
3518
  context.save();
3516
3519
  context.beginPath();
3517
- context.fillStyle = '#ddd'
3518
- context.fill(context.rect(x, y, width, medalDisplayHeight));
3519
- context.strokeStyle = '#000';
3520
+ context.fillStyle = new Color(.9,.9,.9);
3521
+ context.strokeStyle = new Color(0,0,0);
3520
3522
  context.lineWidth = 3;
3523
+ context.fill(context.rect(x, y, width, medalDisplayHeight));
3521
3524
  context.stroke();
3522
3525
  context.clip();
3523
3526
 
3524
3527
  // draw the icon and text
3525
- this.renderIcon(x+15+medalDisplayIconSize/2, y+medalDisplayHeight/2);
3526
- context.textAlign = 'left';
3527
- context.font = '38px '+ fontDefault;
3528
- context.fillText(this.name, x+medalDisplayIconSize+30, y+28);
3529
- context.font = '24px '+ fontDefault;
3530
- context.fillText(this.description, x+medalDisplayIconSize+30, y+60);
3528
+ this.renderIcon(vec2(x+15+medalDisplayIconSize/2, y+medalDisplayHeight/2));
3529
+ const pos = vec2(x+medalDisplayIconSize+30, y+28);
3530
+ drawTextScreen(this.name, pos, 38, new Color(0,0,0), 0, 0, 'left');
3531
+ pos.y += 32;
3532
+ drawTextScreen(this.description, pos, 24, new Color(0,0,0), 0, 0, 'left');
3531
3533
  context.restore();
3532
3534
  }
3533
3535
 
@@ -3536,18 +3538,13 @@ class Medal
3536
3538
  * @param {Number} y - Screen space Y position
3537
3539
  * @param {Number} [size=medalDisplayIconSize] - Screen space size
3538
3540
  */
3539
- renderIcon(x, y, size=medalDisplayIconSize)
3541
+ renderIcon(pos, size=medalDisplayIconSize)
3540
3542
  {
3541
3543
  // draw the image or icon
3542
- const context = overlayContext;
3543
- context.fillStyle = '#000';
3544
- context.textAlign = 'center';
3545
- context.textBaseline = 'middle';
3546
- context.font = size*.7 + 'px '+ fontDefault;
3547
- if (this.image.src)
3548
- context.drawImage(this.image, x-size/2, y-size/2, size, size);
3544
+ if (this.image)
3545
+ overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
3549
3546
  else
3550
- context.fillText(this.icon, x, y); // show icon if there is no image
3547
+ drawTextScreen(this.icon, pos, size*.7, new Color(0,0,0));
3551
3548
  }
3552
3549
 
3553
3550
  // Get local storage key used by the medal
@@ -3605,13 +3602,21 @@ class Newgrounds
3605
3602
  constructor(app_id, cipher)
3606
3603
  {
3607
3604
  ASSERT(!newgrounds && app_id);
3605
+
3606
+ // create an instance of CryptoJS for encrypted calls
3607
+ if (cipher)
3608
+ {
3609
+ ///////////////////////////////////////////////////////////////////////////////
3610
+ // Crypto-JS - https://github.com/brix/crypto-js [The MIT License (MIT)]
3611
+ // Copyright (c) 2009-2013 Jeff Mott Copyright (c) 2013-2016 Evan Vosberg
3612
+
3613
+ this.cryptoJS = eval(Function("[M='GBMGXz^oVYPPKKbB`agTXU|LxPc_ZBcMrZvCr~wyGfWrwk@ATqlqeTp^N?p{we}jIpEnB_sEr`l?YDkDhWhprc|Er|XETG?pTl`e}dIc[_N~}fzRycIfpW{HTolvoPB_FMe_eH~BTMx]yyOhv?biWPCGc]kABencBhgERHGf{OL`Dj`c^sh@canhy[secghiyotcdOWgO{tJIE^JtdGQRNSCrwKYciZOa]Y@tcRATYKzv|sXpboHcbCBf`}SKeXPFM|RiJsSNaIb]QPc[D]Jy_O^XkOVTZep`ONmntLL`Qz~UupHBX_Ia~WX]yTRJIxG`ioZ{fefLJFhdyYoyLPvqgH?b`[TMnTwwfzDXhfM?rKs^aFr|nyBdPmVHTtAjXoYUloEziWDCw_suyYT~lSMksI~ZNCS[Bex~j]Vz?kx`gdYSEMCsHpjbyxQvw|XxX_^nQYue{sBzVWQKYndtYQMWRef{bOHSfQhiNdtR{o?cUAHQAABThwHPT}F{VvFmgN`E@FiFYS`UJmpQNM`X|tPKHlccT}z}k{sACHL?Rt@MkWplxO`ASgh?hBsuuP|xD~LSH~KBlRs]t|l|_tQAroDRqWS^SEr[sYdPB}TAROtW{mIkE|dWOuLgLmJrucGLpebrAFKWjikTUzS|j}M}szasKOmrjy[?hpwnEfX[jGpLt@^v_eNwSQHNwtOtDgWD{rk|UgASs@mziIXrsHN_|hZuxXlPJOsA^^?QY^yGoCBx{ekLuZzRqQZdsNSx@ezDAn{XNj@fRXIwrDX?{ZQHwTEfu@GhxDOykqts|n{jOeZ@c`dvTY?e^]ATvWpb?SVyg]GC?SlzteilZJAL]mlhLjYZazY__qcVFYvt@|bIQnSno@OXyt]OulzkWqH`rYFWrwGs`v|~XeTsIssLrbmHZCYHiJrX}eEzSssH}]l]IhPQhPoQ}rCXLyhFIT[clhzYOvyHqigxmjz`phKUU^TPf[GRAIhNqSOdayFP@FmKmuIzMOeoqdpxyCOwCthcLq?n`L`tLIBboNn~uXeFcPE{C~mC`h]jUUUQe^`UqvzCutYCgct|SBrAeiYQW?X~KzCz}guXbsUw?pLsg@hDArw?KeJD[BN?GD@wgFWCiHq@Ypp_QKFixEKWqRp]oJFuVIEvjDcTFu~Zz]a{IcXhWuIdMQjJ]lwmGQ|]g~c]Hl]pl`Pd^?loIcsoNir_kikBYyg?NarXZEGYspt_vLBIoj}LI[uBFvm}tbqvC|xyR~a{kob|HlctZslTGtPDhBKsNsoZPuH`U`Fqg{gKnGSHVLJ^O`zmNgMn~{rsQuoymw^JY?iUBvw_~mMr|GrPHTERS[MiNpY[Mm{ggHpzRaJaoFomtdaQ_?xuTRm}@KjU~RtPsAdxa|uHmy}n^i||FVL[eQAPrWfLm^ndczgF~Nk~aplQvTUpHvnTya]kOenZlLAQIm{lPl@CCTchvCF[fI{^zPkeYZTiamoEcKmBMfZhk_j_~Fjp|wPVZlkh_nHu]@tP|hS@^G^PdsQ~f[RqgTDqezxNFcaO}HZhb|MMiNSYSAnQWCDJukT~e|OTgc}sf[cnr?fyzTa|EwEtRG|I~|IO}O]S|rp]CQ}}DWhSjC_|z|oY|FYl@WkCOoPuWuqr{fJu?Brs^_EBI[@_OCKs}?]O`jnDiXBvaIWhhMAQDNb{U`bqVR}oqVAvR@AZHEBY@depD]OLh`kf^UsHhzKT}CS}HQKy}Q~AeMydXPQztWSSzDnghULQgMAmbWIZ|lWWeEXrE^EeNoZApooEmrXe{NAnoDf`m}UNlRdqQ@jOc~HLOMWs]IDqJHYoMziEedGBPOxOb?[X`KxkFRg@`mgFYnP{hSaxwZfBQqTm}_?RSEaQga]w[vxc]hMne}VfSlqUeMo_iqmd`ilnJXnhdj^EEFifvZyxYFRf^VaqBhLyrGlk~qowqzHOBlOwtx?i{m~`n^G?Yxzxux}b{LSlx]dS~thO^lYE}bzKmUEzwW^{rPGhbEov[Plv??xtyKJshbG`KuO?hjBdS@Ru}iGpvFXJRrvOlrKN?`I_n_tplk}kgwSXuKylXbRQ]]?a|{xiT[li?k]CJpwy^o@ebyGQrPfF`aszGKp]baIx~H?ElETtFh]dz[OjGl@C?]VDhr}OE@V]wLTc[WErXacM{We`F|utKKjgllAxvsVYBZ@HcuMgLboFHVZmi}eIXAIFhS@A@FGRbjeoJWZ_NKd^oEH`qgy`q[Tq{x?LRP|GfBFFJV|fgZs`MLbpPYUdIV^]mD@FG]pYAT^A^RNCcXVrPsgk{jTrAIQPs_`mD}rOqAZA[}RETFz]WkXFTz_m{N@{W@_fPKZLT`@aIqf|L^Mb|crNqZ{BVsijzpGPEKQQZGlApDn`ruH}cvF|iXcNqK}cxe_U~HRnKV}sCYb`D~oGvwG[Ca|UaybXea~DdD~LiIbGRxJ_VGheI{ika}KC[OZJLn^IBkPrQj_EuoFwZ}DpoBRcK]Q}?EmTv~i_Tul{bky?Iit~tgS|o}JL_VYcCQdjeJ_MfaA`FgCgc[Ii|CBHwq~nbJeYTK{e`CNstKfTKPzw{jdhp|qsZyP_FcugxCFNpKitlR~vUrx^NrSVsSTaEgnxZTmKc`R|lGJeX}ccKLsQZQhsFkeFd|ckHIVTlGMg`~uPwuHRJS_CPuN_ogXe{Ba}dO_UBhuNXby|h?JlgBIqMKx^_u{molgL[W_iavNQuOq?ap]PGB`clAicnl@k~pA?MWHEZ{HuTLsCpOxxrKlBh]FyMjLdFl|nMIvTHyGAlPogqfZ?PlvlFJvYnDQd}R@uAhtJmDfe|iJqdkYr}r@mEjjIetDl_I`TELfoR|qTBu@Tic[BaXjP?dCS~MUK[HPRI}OUOwAaf|_}HZzrwXvbnNgltjTwkBE~MztTQhtRSWoQHajMoVyBBA`kdgK~h`o[J`dm~pm]tk@i`[F~F]DBlJKklrkR]SNw@{aG~Vhl`KINsQkOy?WhcqUMTGDOM_]bUjVd|Yh_KUCCgIJ|LDIGZCPls{RzbVWVLEhHvWBzKq|^N?DyJB|__aCUjoEgsARki}j@DQXS`RNU|DJ^a~d{sh_Iu{ONcUtSrGWW@cvUjefHHi}eSSGrNtO?cTPBShLqzwMVjWQQCCFB^culBjZHEK_{dO~Q`YhJYFn]jq~XSnG@[lQr]eKrjXpG~L^h~tDgEma^AUFThlaR{xyuP@[^VFwXSeUbVetufa@dX]CLyAnDV@Bs[DnpeghJw^?UIana}r_CKGDySoRudklbgio}kIDpA@McDoPK?iYcG?_zOmnWfJp}a[JLR[stXMo?_^Ng[whQlrDbrawZeSZ~SJstIObdDSfAA{MV}?gNunLOnbMv_~KFQUAjIMj^GkoGxuYtYbGDImEYiwEMyTpMxN_LSnSMdl{bg@dtAnAMvhDTBR_FxoQgANniRqxd`pWv@rFJ|mWNWmh[GMJz_Nq`BIN@KsjMPASXORcdHjf~rJfgZYe_uulzqM_KdPlMsuvU^YJuLtofPhGonVOQxCMuXliNvJIaoC?hSxcxKVVxWlNs^ENDvCtSmO~WxI[itnjs^RDvI@KqG}YekaSbTaB]ki]XM@[ZnDAP~@|BzLRgOzmjmPkRE@_sobkT|SszXK[rZN?F]Z_u}Yue^[BZgLtR}FHzWyxWEX^wXC]MJmiVbQuBzkgRcKGUhOvUc_bga|Tx`KEM`JWEgTpFYVeXLCm|mctZR@uKTDeUONPozBeIkrY`cz]]~WPGMUf`MNUGHDbxZuO{gmsKYkAGRPqjc|_FtblEOwy}dnwCHo]PJhN~JoteaJ?dmYZeB^Xd?X^pOKDbOMF@Ugg^hETLdhwlA}PL@_ur|o{VZosP?ntJ_kG][g{Zq`Tu]dzQlSWiKfnxDnk}KOzp~tdFstMobmy[oPYjyOtUzMWdjcNSUAjRuqhLS@AwB^{BFnqjCmmlk?jpn}TksS{KcKkDboXiwK]qMVjm~V`LgWhjS^nLGwfhAYrjDSBL_{cRus~{?xar_xqPlArrYFd?pHKdMEZzzjJpfC?Hv}mAuIDkyBxFpxhstTx`IO{rp}XGuQ]VtbHerlRc_LFGWK[XluFcNGUtDYMZny[M^nVKVeMllQI[xtvwQnXFlWYqxZZFp_|]^oWX[{pOMpxXxvkbyJA[DrPzwD|LW|QcV{Nw~U^dgguSpG]ClmO@j_TENIGjPWwgdVbHganhM?ema|dBaqla|WBd`poj~klxaasKxGG^xbWquAl~_lKWxUkDFagMnE{zHug{b`A~IYcQYBF_E}wiA}K@yxWHrZ{[d~|ARsYsjeNWzkMs~IOqqp[yzDE|WFrivsidTcnbHFRoW@XpAV`lv_zj?B~tPCppRjgbbDTALeFaOf?VcjnKTQMLyp{NwdylHCqmo?oelhjWuXj~}{fpuX`fra?GNkDiChYgVSh{R[BgF~eQa^WVz}ATI_CpY?g_diae]|ijH`TyNIF}|D_xpmBq_JpKih{Ba|sWzhnAoyraiDvk`h{qbBfsylBGmRH}DRPdryEsSaKS~tIaeF[s]I~xxHVrcNe@Jjxa@jlhZueLQqHh_]twVMqG_EGuwyab{nxOF?`HCle}nBZzlTQjkLmoXbXhOtBglFoMz?eqre`HiE@vNwBulglmQjj]DB@pPkPUgA^sjOAUNdSu_`oAzar?n?eMnw{{hYmslYi[TnlJD'",...']charCodeAtUinyxpf',"for(;e<10359;c[e++]=p-=128,A=A?p-A&&A:p==34&&p)for(p=1;p<128;y=f.map((n,x)=>(U=r[n]*2+1,U=Math.log(U/(h-U)),t-=a[x]*U,U/500)),t=~-h/(1+Math.exp(t))|1,i=o%h<t,o=o%h+(i?t:h-t)*(o>>17)-!i*t,f.map((n,x)=>(U=r[n]+=(i*h/2-r[n]<<13)/((C[n]+=C[n]<5)+1/20)>>13,a[x]+=y[x]*(i-t/h))),p=p*2+i)for(f='010202103203210431053105410642065206541'.split(t=0).map((n,x)=>(U=0,[...n].map((n,x)=>(U=U*997+(c[e-n]|0)|0)),h*32-1&U*997+p+!!A*129)*12+x);o<h*32;o=o*64|M.charCodeAt(d++)&63);for(C=String.fromCharCode(...c);r=/[\0-#?@\\\\~]/.exec(C);)with(C.split(r))C=join(shift());return C")([],[],1<<17,[0,0,0,0,0,0,0,0,0,0,0,0],new Uint16Array(51e6).fill(1<<15),new Uint8Array(51e6),0,0,0,0));
3614
+ }
3615
+
3608
3616
  this.app_id = app_id;
3609
3617
  this.cipher = cipher;
3610
3618
  this.host = location ? location.hostname : '';
3611
3619
 
3612
- // create an instance of CryptoJS for encrypted calls
3613
- cipher && (this.cryptoJS = CryptoJS());
3614
-
3615
3620
  // get session id from url search params
3616
3621
  const url = new URL(location.href);
3617
3622
  this.session_id = url.searchParams.get('ngio_session_id') || 0;
@@ -3714,12 +3719,6 @@ class Newgrounds
3714
3719
  return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
3715
3720
  }
3716
3721
  }
3717
-
3718
- ///////////////////////////////////////////////////////////////////////////////
3719
- // Crypto-JS - https://github.com/brix/crypto-js [The MIT License (MIT)]
3720
- // Copyright (c) 2009-2013 Jeff Mott Copyright (c) 2013-2016 Evan Vosberg
3721
-
3722
- const CryptoJS=()=>eval(Function("[M='GBMGXz^oVYPPKKbB`agTXU|LxPc_ZBcMrZvCr~wyGfWrwk@ATqlqeTp^N?p{we}jIpEnB_sEr`l?YDkDhWhprc|Er|XETG?pTl`e}dIc[_N~}fzRycIfpW{HTolvoPB_FMe_eH~BTMx]yyOhv?biWPCGc]kABencBhgERHGf{OL`Dj`c^sh@canhy[secghiyotcdOWgO{tJIE^JtdGQRNSCrwKYciZOa]Y@tcRATYKzv|sXpboHcbCBf`}SKeXPFM|RiJsSNaIb]QPc[D]Jy_O^XkOVTZep`ONmntLL`Qz~UupHBX_Ia~WX]yTRJIxG`ioZ{fefLJFhdyYoyLPvqgH?b`[TMnTwwfzDXhfM?rKs^aFr|nyBdPmVHTtAjXoYUloEziWDCw_suyYT~lSMksI~ZNCS[Bex~j]Vz?kx`gdYSEMCsHpjbyxQvw|XxX_^nQYue{sBzVWQKYndtYQMWRef{bOHSfQhiNdtR{o?cUAHQAABThwHPT}F{VvFmgN`E@FiFYS`UJmpQNM`X|tPKHlccT}z}k{sACHL?Rt@MkWplxO`ASgh?hBsuuP|xD~LSH~KBlRs]t|l|_tQAroDRqWS^SEr[sYdPB}TAROtW{mIkE|dWOuLgLmJrucGLpebrAFKWjikTUzS|j}M}szasKOmrjy[?hpwnEfX[jGpLt@^v_eNwSQHNwtOtDgWD{rk|UgASs@mziIXrsHN_|hZuxXlPJOsA^^?QY^yGoCBx{ekLuZzRqQZdsNSx@ezDAn{XNj@fRXIwrDX?{ZQHwTEfu@GhxDOykqts|n{jOeZ@c`dvTY?e^]ATvWpb?SVyg]GC?SlzteilZJAL]mlhLjYZazY__qcVFYvt@|bIQnSno@OXyt]OulzkWqH`rYFWrwGs`v|~XeTsIssLrbmHZCYHiJrX}eEzSssH}]l]IhPQhPoQ}rCXLyhFIT[clhzYOvyHqigxmjz`phKUU^TPf[GRAIhNqSOdayFP@FmKmuIzMOeoqdpxyCOwCthcLq?n`L`tLIBboNn~uXeFcPE{C~mC`h]jUUUQe^`UqvzCutYCgct|SBrAeiYQW?X~KzCz}guXbsUw?pLsg@hDArw?KeJD[BN?GD@wgFWCiHq@Ypp_QKFixEKWqRp]oJFuVIEvjDcTFu~Zz]a{IcXhWuIdMQjJ]lwmGQ|]g~c]Hl]pl`Pd^?loIcsoNir_kikBYyg?NarXZEGYspt_vLBIoj}LI[uBFvm}tbqvC|xyR~a{kob|HlctZslTGtPDhBKsNsoZPuH`U`Fqg{gKnGSHVLJ^O`zmNgMn~{rsQuoymw^JY?iUBvw_~mMr|GrPHTERS[MiNpY[Mm{ggHpzRaJaoFomtdaQ_?xuTRm}@KjU~RtPsAdxa|uHmy}n^i||FVL[eQAPrWfLm^ndczgF~Nk~aplQvTUpHvnTya]kOenZlLAQIm{lPl@CCTchvCF[fI{^zPkeYZTiamoEcKmBMfZhk_j_~Fjp|wPVZlkh_nHu]@tP|hS@^G^PdsQ~f[RqgTDqezxNFcaO}HZhb|MMiNSYSAnQWCDJukT~e|OTgc}sf[cnr?fyzTa|EwEtRG|I~|IO}O]S|rp]CQ}}DWhSjC_|z|oY|FYl@WkCOoPuWuqr{fJu?Brs^_EBI[@_OCKs}?]O`jnDiXBvaIWhhMAQDNb{U`bqVR}oqVAvR@AZHEBY@depD]OLh`kf^UsHhzKT}CS}HQKy}Q~AeMydXPQztWSSzDnghULQgMAmbWIZ|lWWeEXrE^EeNoZApooEmrXe{NAnoDf`m}UNlRdqQ@jOc~HLOMWs]IDqJHYoMziEedGBPOxOb?[X`KxkFRg@`mgFYnP{hSaxwZfBQqTm}_?RSEaQga]w[vxc]hMne}VfSlqUeMo_iqmd`ilnJXnhdj^EEFifvZyxYFRf^VaqBhLyrGlk~qowqzHOBlOwtx?i{m~`n^G?Yxzxux}b{LSlx]dS~thO^lYE}bzKmUEzwW^{rPGhbEov[Plv??xtyKJshbG`KuO?hjBdS@Ru}iGpvFXJRrvOlrKN?`I_n_tplk}kgwSXuKylXbRQ]]?a|{xiT[li?k]CJpwy^o@ebyGQrPfF`aszGKp]baIx~H?ElETtFh]dz[OjGl@C?]VDhr}OE@V]wLTc[WErXacM{We`F|utKKjgllAxvsVYBZ@HcuMgLboFHVZmi}eIXAIFhS@A@FGRbjeoJWZ_NKd^oEH`qgy`q[Tq{x?LRP|GfBFFJV|fgZs`MLbpPYUdIV^]mD@FG]pYAT^A^RNCcXVrPsgk{jTrAIQPs_`mD}rOqAZA[}RETFz]WkXFTz_m{N@{W@_fPKZLT`@aIqf|L^Mb|crNqZ{BVsijzpGPEKQQZGlApDn`ruH}cvF|iXcNqK}cxe_U~HRnKV}sCYb`D~oGvwG[Ca|UaybXea~DdD~LiIbGRxJ_VGheI{ika}KC[OZJLn^IBkPrQj_EuoFwZ}DpoBRcK]Q}?EmTv~i_Tul{bky?Iit~tgS|o}JL_VYcCQdjeJ_MfaA`FgCgc[Ii|CBHwq~nbJeYTK{e`CNstKfTKPzw{jdhp|qsZyP_FcugxCFNpKitlR~vUrx^NrSVsSTaEgnxZTmKc`R|lGJeX}ccKLsQZQhsFkeFd|ckHIVTlGMg`~uPwuHRJS_CPuN_ogXe{Ba}dO_UBhuNXby|h?JlgBIqMKx^_u{molgL[W_iavNQuOq?ap]PGB`clAicnl@k~pA?MWHEZ{HuTLsCpOxxrKlBh]FyMjLdFl|nMIvTHyGAlPogqfZ?PlvlFJvYnDQd}R@uAhtJmDfe|iJqdkYr}r@mEjjIetDl_I`TELfoR|qTBu@Tic[BaXjP?dCS~MUK[HPRI}OUOwAaf|_}HZzrwXvbnNgltjTwkBE~MztTQhtRSWoQHajMoVyBBA`kdgK~h`o[J`dm~pm]tk@i`[F~F]DBlJKklrkR]SNw@{aG~Vhl`KINsQkOy?WhcqUMTGDOM_]bUjVd|Yh_KUCCgIJ|LDIGZCPls{RzbVWVLEhHvWBzKq|^N?DyJB|__aCUjoEgsARki}j@DQXS`RNU|DJ^a~d{sh_Iu{ONcUtSrGWW@cvUjefHHi}eSSGrNtO?cTPBShLqzwMVjWQQCCFB^culBjZHEK_{dO~Q`YhJYFn]jq~XSnG@[lQr]eKrjXpG~L^h~tDgEma^AUFThlaR{xyuP@[^VFwXSeUbVetufa@dX]CLyAnDV@Bs[DnpeghJw^?UIana}r_CKGDySoRudklbgio}kIDpA@McDoPK?iYcG?_zOmnWfJp}a[JLR[stXMo?_^Ng[whQlrDbrawZeSZ~SJstIObdDSfAA{MV}?gNunLOnbMv_~KFQUAjIMj^GkoGxuYtYbGDImEYiwEMyTpMxN_LSnSMdl{bg@dtAnAMvhDTBR_FxoQgANniRqxd`pWv@rFJ|mWNWmh[GMJz_Nq`BIN@KsjMPASXORcdHjf~rJfgZYe_uulzqM_KdPlMsuvU^YJuLtofPhGonVOQxCMuXliNvJIaoC?hSxcxKVVxWlNs^ENDvCtSmO~WxI[itnjs^RDvI@KqG}YekaSbTaB]ki]XM@[ZnDAP~@|BzLRgOzmjmPkRE@_sobkT|SszXK[rZN?F]Z_u}Yue^[BZgLtR}FHzWyxWEX^wXC]MJmiVbQuBzkgRcKGUhOvUc_bga|Tx`KEM`JWEgTpFYVeXLCm|mctZR@uKTDeUONPozBeIkrY`cz]]~WPGMUf`MNUGHDbxZuO{gmsKYkAGRPqjc|_FtblEOwy}dnwCHo]PJhN~JoteaJ?dmYZeB^Xd?X^pOKDbOMF@Ugg^hETLdhwlA}PL@_ur|o{VZosP?ntJ_kG][g{Zq`Tu]dzQlSWiKfnxDnk}KOzp~tdFstMobmy[oPYjyOtUzMWdjcNSUAjRuqhLS@AwB^{BFnqjCmmlk?jpn}TksS{KcKkDboXiwK]qMVjm~V`LgWhjS^nLGwfhAYrjDSBL_{cRus~{?xar_xqPlArrYFd?pHKdMEZzzjJpfC?Hv}mAuIDkyBxFpxhstTx`IO{rp}XGuQ]VtbHerlRc_LFGWK[XluFcNGUtDYMZny[M^nVKVeMllQI[xtvwQnXFlWYqxZZFp_|]^oWX[{pOMpxXxvkbyJA[DrPzwD|LW|QcV{Nw~U^dgguSpG]ClmO@j_TENIGjPWwgdVbHganhM?ema|dBaqla|WBd`poj~klxaasKxGG^xbWquAl~_lKWxUkDFagMnE{zHug{b`A~IYcQYBF_E}wiA}K@yxWHrZ{[d~|ARsYsjeNWzkMs~IOqqp[yzDE|WFrivsidTcnbHFRoW@XpAV`lv_zj?B~tPCppRjgbbDTALeFaOf?VcjnKTQMLyp{NwdylHCqmo?oelhjWuXj~}{fpuX`fra?GNkDiChYgVSh{R[BgF~eQa^WVz}ATI_CpY?g_diae]|ijH`TyNIF}|D_xpmBq_JpKih{Ba|sWzhnAoyraiDvk`h{qbBfsylBGmRH}DRPdryEsSaKS~tIaeF[s]I~xxHVrcNe@Jjxa@jlhZueLQqHh_]twVMqG_EGuwyab{nxOF?`HCle}nBZzlTQjkLmoXbXhOtBglFoMz?eqre`HiE@vNwBulglmQjj]DB@pPkPUgA^sjOAUNdSu_`oAzar?n?eMnw{{hYmslYi[TnlJD'",...']charCodeAtUinyxpf',"for(;e<10359;c[e++]=p-=128,A=A?p-A&&A:p==34&&p)for(p=1;p<128;y=f.map((n,x)=>(U=r[n]*2+1,U=Math.log(U/(h-U)),t-=a[x]*U,U/500)),t=~-h/(1+Math.exp(t))|1,i=o%h<t,o=o%h+(i?t:h-t)*(o>>17)-!i*t,f.map((n,x)=>(U=r[n]+=(i*h/2-r[n]<<13)/((C[n]+=C[n]<5)+1/20)>>13,a[x]+=y[x]*(i-t/h))),p=p*2+i)for(f='010202103203210431053105410642065206541'.split(t=0).map((n,x)=>(U=0,[...n].map((n,x)=>(U=U*997+(c[e-n]|0)|0)),h*32-1&U*997+p+!!A*129)*12+x);o<h*32;o=o*64|M.charCodeAt(d++)&63);for(C=String.fromCharCode(...c);r=/[\0-#?@\\\\~]/.exec(C);)with(C.split(r))C=join(shift());return C")([],[],1<<17,[0,0,0,0,0,0,0,0,0,0,0,0],new Uint16Array(51e6).fill(1<<15),new Uint8Array(51e6),0,0,0,0));
3723
3722
  /**
3724
3723
  * LittleJS WebGL Interface
3725
3724
  * <br> - All webgl used by the engine is wrapped up here
@@ -3749,7 +3748,7 @@ let glContext;
3749
3748
  let glTileTexture;
3750
3749
 
3751
3750
  // WebGL internal variables not exposed to documentation
3752
- let glActiveTexture, glShader, glArrayBuffer, glVertexData, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3751
+ let glActiveTexture, glShader, glArrayBuffer, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3753
3752
 
3754
3753
  ///////////////////////////////////////////////////////////////////////////////
3755
3754
 
@@ -3770,27 +3769,25 @@ function glInit()
3770
3769
  'uniform mat4 m;'+ // transform matrix
3771
3770
  'attribute vec2 p,t;'+ // position, uv
3772
3771
  'attribute vec4 c,a;'+ // color, additiveColor
3773
- 'varying vec2 v;'+ // return uv
3774
- 'varying vec4 d,e;'+ // return color, additiveColor
3772
+ 'varying vec4 v,d,e;'+ // return uv, color, additiveColor
3775
3773
  'void main(){'+ // shader entry point
3776
3774
  'gl_Position=m*vec4(p,1,1);'+ // transform position
3777
- 'v=t;d=c;e=a;'+ // pass stuff to fragment shader
3775
+ 'v=vec4(t,p);d=c;e=a;'+ // pass stuff to fragment shader
3778
3776
  '}' // end of shader
3779
3777
  ,
3780
- 'precision highp float;'+ // use highp for better accuracy
3781
- 'varying vec2 v;'+ // uv
3782
- 'varying vec4 d,e;'+ // color, additiveColor
3783
- 'uniform sampler2D s;'+ // texture
3784
- 'void main(){'+ // shader entry point
3785
- 'gl_FragColor=texture2D(s,v)*d+e;'+ // modulate texture by color plus additive
3786
- '}' // end of shader
3778
+ 'precision highp float;'+ // use highp for better accuracy
3779
+ 'varying vec4 v,d,e;'+ // uv, color, additiveColor
3780
+ 'uniform sampler2D s;'+ // texture
3781
+ 'void main(){'+ // shader entry point
3782
+ 'gl_FragColor=texture2D(s,v.xy)*d+e;'+ // modulate texture by color plus additive
3783
+ '}' // end of shader
3787
3784
  );
3788
3785
 
3789
3786
  // init buffers
3790
- glVertexData = new ArrayBuffer(gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE);
3787
+ const vertexData = new ArrayBuffer(gl_VERTEX_BUFFER_SIZE);
3791
3788
  glArrayBuffer = glContext.createBuffer();
3792
- glPositionData = new Float32Array(glVertexData);
3793
- glColorData = new Uint32Array(glVertexData);
3789
+ glPositionData = new Float32Array(vertexData);
3790
+ glColorData = new Uint32Array(vertexData);
3794
3791
  glBatchCount = 0;
3795
3792
  }
3796
3793
 
@@ -3865,18 +3862,19 @@ function glCreateTexture(image)
3865
3862
  image && image.width && glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
3866
3863
 
3867
3864
  // use point filtering for pixelated rendering
3868
- glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, cavasPixelated ? gl_NEAREST : gl_LINEAR);
3869
- glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, cavasPixelated ? gl_NEAREST : gl_LINEAR);
3865
+ const filter = cavasPixelated ? gl_NEAREST : gl_LINEAR;
3866
+ glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
3867
+ glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
3870
3868
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE);
3871
3869
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE);
3872
3870
  return texture;
3873
3871
  }
3874
3872
 
3875
3873
  // called automatically by engine before render
3876
- function glPreRender(width, height, cameraX, cameraY, cameraScale)
3874
+ function glPreRender()
3877
3875
  {
3878
3876
  // clear and set to same size as main canvas
3879
- glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height);
3877
+ glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
3880
3878
  glContext.clear(gl_COLOR_BUFFER_BIT);
3881
3879
 
3882
3880
  // set up the shader
@@ -3884,7 +3882,7 @@ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3884
3882
  glContext.activeTexture(gl_TEXTURE0);
3885
3883
  glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
3886
3884
  glContext.bindBuffer(gl_ARRAY_BUFFER, glArrayBuffer);
3887
- glContext.bufferData(gl_ARRAY_BUFFER, glVertexData.byteLength, gl_DYNAMIC_DRAW);
3885
+ glContext.bufferData(gl_ARRAY_BUFFER, gl_VERTEX_BUFFER_SIZE, gl_DYNAMIC_DRAW);
3888
3886
  glSetBlendMode();
3889
3887
 
3890
3888
  // set vertex attributes
@@ -3902,14 +3900,14 @@ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3902
3900
  initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3903
3901
 
3904
3902
  // build the transform matrix
3905
- const sx = 2 * cameraScale / width;
3906
- const sy = 2 * cameraScale / height;
3903
+ const sx = 2 * cameraScale / mainCanvas.width;
3904
+ const sy = 2 * cameraScale / mainCanvas.height;
3907
3905
  glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), 0,
3908
3906
  new Float32Array([
3909
3907
  sx, 0, 0, 0,
3910
3908
  0, sy, 0, 0,
3911
3909
  1, 1, -1, 1,
3912
- -1-sx*cameraX, -1-sy*cameraY, 0, 0
3910
+ -1-sx*cameraPos.x, -1-sy*cameraPos.y, 0, 0
3913
3911
  ])
3914
3912
  );
3915
3913
  }
@@ -3957,10 +3955,10 @@ function glCopyToContext(context, forceDraw)
3957
3955
  * @param uv0Y
3958
3956
  * @param uv1X
3959
3957
  * @param uv1Y
3960
- * @param [rgba=0xffffffff]
3958
+ * @param rgba
3961
3959
  * @param [rgbaAdditive=0]
3962
3960
  * @memberof WebGL */
3963
- function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffffff, rgbaAdditive=0)
3961
+ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
3964
3962
  {
3965
3963
  // flush if there is no room for more verts or if different blend mode
3966
3964
  if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
@@ -3971,43 +3969,16 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffff
3971
3969
  const cx = c*sizeX, cy = c*sizeY, sx = s*sizeX, sy = s*sizeY;
3972
3970
 
3973
3971
  // setup 2 triangles to form a quad
3974
- let offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT;
3975
-
3976
- // vertex 0
3977
- glPositionData[offset++] = x - cx - sy;
3978
- glPositionData[offset++] = y - cy + sx;
3979
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3980
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3981
-
3982
- // vertex 1
3983
- glPositionData[offset++] = x + cx + sy;
3984
- glPositionData[offset++] = y + cy - sx;
3985
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3986
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3987
-
3988
- // vertex 2
3989
- glPositionData[offset++] = x - cx + sy;
3990
- glPositionData[offset++] = y + cy + sx;
3991
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv0Y;
3992
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3993
-
3994
- // vertex 0
3995
- glPositionData[offset++] = x - cx - sy;
3996
- glPositionData[offset++] = y - cy + sx;
3997
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3998
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3999
-
4000
- // vertex 3
4001
- glPositionData[offset++] = x + cx - sy;
4002
- glPositionData[offset++] = y - cy - sx;
4003
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv1Y;
4004
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
4005
-
4006
- // vertex 1
4007
- glPositionData[offset++] = x + cx + sy;
4008
- glPositionData[offset++] = y + cy - sx;
4009
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
4010
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3972
+ for(let i=6, offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT; i--;)
3973
+ {
3974
+ const a = i-4&&i>1, b = i-5&&i-2&&i-1;
3975
+ glPositionData[offset++] = x + (a?-cx:cx) + (b?sy:-sy);
3976
+ glPositionData[offset++] = y + (b?cy:-cy) + (a?sx:-sx);
3977
+ glPositionData[offset++] = a ? uv0X : uv1X;
3978
+ glPositionData[offset++] = b ? uv0Y : uv1Y;
3979
+ glColorData[offset++] = rgba;
3980
+ glColorData[offset++] = rgbaAdditive;
3981
+ }
4011
3982
  }
4012
3983
 
4013
3984
  ///////////////////////////////////////////////////////////////////////////////
@@ -4059,14 +4030,13 @@ function glRenderPostProcess()
4059
4030
  return;
4060
4031
 
4061
4032
  // prepare to render post process shader
4062
- const width = mainCanvas.width, height = mainCanvas.height;
4063
4033
  if (glEnable)
4064
4034
  {
4065
4035
  glFlush(); // clear out the buffer
4066
4036
  mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
4067
4037
  }
4068
- else
4069
- glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height); // set viewport
4038
+ else // set viewport
4039
+ glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
4070
4040
 
4071
4041
  // setup shader program to draw one triangle
4072
4042
  glContext.useProgram(glPostShader);
@@ -4090,7 +4060,7 @@ function glRenderPostProcess()
4090
4060
  const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
4091
4061
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
4092
4062
  glContext.uniform1f(uniformLocation('iTime'), time);
4093
- glContext.uniform3f(uniformLocation('iResolution'), width, height, 1);
4063
+ glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
4094
4064
  glContext.drawArrays(gl_TRIANGLES, 0, 3);
4095
4065
  }
4096
4066
 
@@ -4130,4 +4100,5 @@ gl_UNPACK_FLIP_Y_WEBGL = 37440,
4130
4100
  gl_VERTICES_PER_QUAD = 6,
4131
4101
  gl_INDICIES_PER_VERT = 6,
4132
4102
  gl_MAX_BATCH = 1<<16,
4133
- gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2; // vec2 * 2 + (char * 4) * 2
4103
+ gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2, // vec2 * 2 + (char * 4) * 2
4104
+ gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE;