littlejsengine 1.4.7 → 1.4.9

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 = 32;
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
 
@@ -770,20 +783,20 @@ let vibrateEnable = 1;
770
783
  ///////////////////////////////////////////////////////////////////////////////
771
784
  // Audio settings
772
785
 
773
- /** Volume scale to apply to all sound, music and speech
786
+ /** All audio code can be disabled and removed from build
774
787
  * @default
775
788
  * @memberof Settings */
776
- let soundVolume = .5;
789
+ let soundEnable = 1;
777
790
 
778
- /** All audio code can be disabled and removed from build
791
+ /** Volume scale to apply to all sound, music and speech
779
792
  * @default
780
793
  * @memberof Settings */
781
- let soundEnable = 1;
794
+ let soundVolume = .5;
782
795
 
783
796
  /** Default range where sound no longer plays
784
797
  * @default
785
798
  * @memberof Settings */
786
- let soundDefaultRange = 30;
799
+ let soundDefaultRange = 40;
787
800
 
788
801
  /** Default range percent to start tapering off sound (0-1)
789
802
  * @default
@@ -803,20 +816,20 @@ let medalDisplayTime = 5;
803
816
  * @memberof Settings */
804
817
  let medalDisplaySlideTime = .5;
805
818
 
806
- /** Width of medal display
819
+ /** Size of medal display
807
820
  * @default
808
821
  * @memberof Settings */
809
- let medalDisplayWidth = 640;
822
+ let medalDisplaySize = vec2(640, 80);
810
823
 
811
- /** Height of medal display
824
+ /** Size of icon in medal display
812
825
  * @default
813
826
  * @memberof Settings */
814
- let medalDisplayHeight = 80;
827
+ let medalDisplayIconSize = 50;
815
828
 
816
- /** Size of icon in medal display
829
+ /** Set to stop medals from being unlockable (like if cheats are enabled)
817
830
  * @default
818
831
  * @memberof Settings */
819
- let medalDisplayIconSize = 50;
832
+ let medalsPreventUnlock;
820
833
  /*
821
834
  LittleJS - The Tiny JavaScript Game Engine That Can!
822
835
  MIT License - Copyright 2021 Frank Force
@@ -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.9';
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();
@@ -1226,9 +1230,9 @@ class EngineObject
1226
1230
 
1227
1231
  // apply physics
1228
1232
  const oldPos = this.pos.copy();
1229
- this.pos.x += this.velocity.x = this.damping * this.velocity.x;
1230
- this.pos.y += this.velocity.y = this.damping * this.velocity.y + gravity * this.gravityScale;
1231
- this.angle += this.angleVelocity *= this.angleDamping;
1233
+ this.velocity.y += gravity * this.gravityScale;
1234
+ this.pos.x += this.velocity.x *= this.damping;
1235
+ this.pos.y += this.velocity.y *= this.damping;
1232
1236
 
1233
1237
  // physics sanity checks
1234
1238
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
@@ -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
@@ -1286,7 +1290,7 @@ class EngineObject
1286
1290
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
1287
1291
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
1288
1292
 
1289
- if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
1293
+ if (smallStepUp | isBlockedY | !isBlockedX) // resolve y collision
1290
1294
  {
1291
1295
  // push outside object collision
1292
1296
  this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
@@ -1316,7 +1320,7 @@ class EngineObject
1316
1320
  o.velocity.y = lerp(elasticity, inelastic, elastic1);
1317
1321
  }
1318
1322
  }
1319
- if (!smallStepUp && (isBlockedX || !isBlockedY)) // resolve x collision
1323
+ if (!smallStepUp & isBlockedX) // resolve x collision
1320
1324
  {
1321
1325
  // push outside collision
1322
1326
  this.pos.x = o.pos.x + (sizeBoth.x/2 + epsilon) * sign(oldPos.x - o.pos.x);
@@ -1354,7 +1358,7 @@ class EngineObject
1354
1358
  // test which side we bounced off (or both if a corner)
1355
1359
  const isBlockedY = tileCollisionTest(new Vector2(oldPos.x, this.pos.y), this.size, this);
1356
1360
  const isBlockedX = tileCollisionTest(new Vector2(this.pos.x, oldPos.y), this.size, this);
1357
- if (isBlockedY || !isBlockedX)
1361
+ if (isBlockedY | !isBlockedX)
1358
1362
  {
1359
1363
  // set if landed on ground
1360
1364
  this.groundObject = wasMovingDown;
@@ -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
@@ -1773,25 +1780,23 @@ function drawText(text, pos, size=1, color, lineWidth, lineColor, textAlign, fon
1773
1780
  * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
1774
1781
  */
1775
1782
 
1783
+ // default font image created by engine
1776
1784
  let engineFontImage;
1777
1785
 
1778
1786
  class FontImage
1779
1787
  {
1780
1788
  /** Create an image font
1781
- * @param {HTMLImageElement} [image] - The image the font is stored in, if undefined the default font is used
1782
- * @param {Vector2} [tileSize=vec2(8)] - The size of the font source tiles
1789
+ * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
1790
+ * @param {Vector2} [tileSize=vec2(8)] - Size of the font source tiles
1783
1791
  * @param {Vector2} [paddingSize=vec2(0,1)] - How much extra space to add between characters
1784
1792
  * @param {Number} [startTileIndex=0] - Tile index in image where font starts
1785
1793
  * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
1786
1794
  */
1787
1795
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), startTileIndex=0, context=overlayContext)
1788
1796
  {
1789
- if (!image && !engineFontImage)
1790
- {
1791
- // load default font image
1792
- engineFontImage = new Image();
1793
- engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
1794
- }
1797
+ // load default font image
1798
+ if (!engineFontImage)
1799
+ (engineFontImage = new Image).src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
1795
1800
 
1796
1801
  this.image = image || engineFontImage;
1797
1802
  this.tileSize = tileSize;
@@ -1856,7 +1861,7 @@ class FontImage
1856
1861
  /** Returns true if fullscreen mode is active
1857
1862
  * @return {Boolean}
1858
1863
  * @memberof Draw */
1859
- const isFullscreen =()=> document.fullscreenElement;
1864
+ const isFullscreen = ()=> document.fullscreenElement;
1860
1865
 
1861
1866
  /** Toggle fullsceen mode
1862
1867
  * @memberof Draw */
@@ -1866,16 +1871,9 @@ function toggleFullscreen()
1866
1871
  {
1867
1872
  if (document.exitFullscreen)
1868
1873
  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
1874
  }
1875
+ else if (document.body.requestFullscreen)
1876
+ document.body.requestFullscreen();
1879
1877
  }
1880
1878
 
1881
1879
  /**
@@ -1894,7 +1892,7 @@ function toggleFullscreen()
1894
1892
  * @param {Number} [device=0]
1895
1893
  * @return {Boolean}
1896
1894
  * @memberof Input */
1897
- const keyIsDown = (key, device=0)=> inputData[device] && inputData[device][key] & 1 ? 1 : 0;
1895
+ const keyIsDown = (key, device=0)=> inputData[device] && inputData[device][key] & 1;
1898
1896
 
1899
1897
  /** Returns true if device key was pressed this frame
1900
1898
  * @param {Number} key
@@ -2134,35 +2132,40 @@ const isTouchDevice = window.ontouchstart !== undefined;
2134
2132
  if (isTouchDevice)
2135
2133
  {
2136
2134
  // override mouse events
2137
- const mouseDown = onmousedown, mouseUp = onmouseup, mouseMove = onmousemove;
2138
- onmousedown = onmouseup = onmousemove = (e)=> 0;
2135
+ let wasTouching, mouseDown = onmousedown, mouseUp = onmouseup;
2136
+ onmousedown = onmouseup = ()=> 0;
2139
2137
 
2140
- // handle all touch events the same way
2141
- let wasTouching, hadTouch;
2142
- ontouchstart = ontouchmove = ontouchend = (e)=>
2138
+ // setup touch input
2139
+ ontouchstart = (e)=>
2143
2140
  {
2144
- e.button = 0; // all touches are left click
2141
+ // fix mobile audio, force it to play a sound on first touch
2142
+ zzfx(0);
2145
2143
 
2146
- // check if touching and pass to mouse events
2147
- const touching = e.touches.length;
2148
- if (touching)
2144
+ // handle all touch events the same way
2145
+ ontouchstart = ontouchmove = ontouchend = (e)=>
2149
2146
  {
2150
- // fix mobile audio, force it to play a sound on first touch
2151
- hadTouch || zzfx(0, hadTouch=1);
2147
+ e.button = 0; // all touches are left click
2152
2148
 
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);
2149
+ // check if touching and pass to mouse events
2150
+ const touching = e.touches.length;
2151
+ if (touching)
2152
+ {
2153
+ // set event pos and pass it along
2154
+ e.x = e.touches[0].clientX;
2155
+ e.y = e.touches[0].clientY;
2156
+ wasTouching ? onmousemove(e) : mouseDown(e);
2157
+ }
2158
+ else if (wasTouching)
2159
+ mouseUp(e);
2160
+
2161
+ // set was touching
2162
+ wasTouching = touching;
2160
2163
 
2161
- // set was touching
2162
- wasTouching = touching;
2164
+ // must return true so the document will get focus
2165
+ return true;
2166
+ }
2163
2167
 
2164
- // must return true so the document will get focus
2165
- return true;
2168
+ return ontouchstart(e);
2166
2169
  }
2167
2170
  }
2168
2171
 
@@ -2170,7 +2173,7 @@ if (isTouchDevice)
2170
2173
  // touch gamepad, virtual on screen gamepad emulator for touch devices
2171
2174
 
2172
2175
  // touch input internal variables
2173
- let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
2176
+ let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2174
2177
 
2175
2178
  // create the touch gamepad, called automatically by the engine
2176
2179
  function touchGamepadCreate()
@@ -2178,65 +2181,73 @@ function touchGamepadCreate()
2178
2181
  if (!touchGamepadEnable || !isTouchDevice)
2179
2182
  return;
2180
2183
 
2181
- ontouchstart = ontouchmove = ontouchend = (e)=>
2184
+ // touch input internal variables
2185
+ touchGamepadButtons = [];
2186
+ touchGamepadStick = vec2();
2187
+
2188
+ // setup touch input
2189
+ ontouchstart = (e)=>
2182
2190
  {
2183
- if (!touchGamepadEnable)
2184
- return;
2191
+ // fix mobile audio, force it to play a sound on first touch
2192
+ zzfx(0);
2185
2193
 
2186
- // clear touch gamepad input
2187
- touchGamepadStick = vec2();
2188
- touchGamepadButtons = [];
2189
-
2190
- const touching = e.touches.length;
2191
- if (touching)
2194
+ ontouchstart = ontouchmove = ontouchend = (e)=>
2192
2195
  {
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)
2196
+ // clear touch gamepad input
2197
+ touchGamepadStick = vec2();
2198
+ touchGamepadButtons = [];
2199
+
2200
+ const touching = e.touches.length;
2201
+ if (touching)
2200
2202
  {
2201
- // touch anywhere to press start when paused
2202
- touchGamepadButtons[9] = 1;
2203
- return;
2203
+ // set that gamepad is active
2204
+ isUsingGamepad = 1;
2205
+ touchGamepadTimer.set();
2206
+
2207
+ if (paused)
2208
+ {
2209
+ // touch anywhere to press start when paused
2210
+ touchGamepadButtons[9] = 1;
2211
+ return;
2212
+ }
2204
2213
  }
2205
- }
2206
2214
 
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);
2215
+ // get center of left and right sides
2216
+ const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2217
+ const buttonCenter = mainCanvasSize.subtract(vec2(touchGamepadSize, touchGamepadSize));
2218
+ const startCenter = mainCanvasSize.scale(.5);
2211
2219
 
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)
2220
+ // check each touch point
2221
+ for (const touch of e.touches)
2217
2222
  {
2218
- // virtual analog stick
2219
- if (touchGamepadAnalog)
2220
- touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
2221
- else
2223
+ const touchPos = mouseToScreen(vec2(touch.clientX, touch.clientY));
2224
+ if (touchPos.distance(stickCenter) < touchGamepadSize)
2222
2225
  {
2223
- // 8 way dpad
2224
- const angle = touchPos.subtract(stickCenter).angle();
2225
- touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
2226
+ // virtual analog stick
2227
+ if (touchGamepadAnalog)
2228
+ touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
2229
+ else
2230
+ {
2231
+ // 8 way dpad
2232
+ const angle = touchPos.subtract(stickCenter).angle();
2233
+ touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
2234
+ }
2235
+ }
2236
+ else if (touchPos.distance(buttonCenter) < touchGamepadSize)
2237
+ {
2238
+ // virtual face buttons
2239
+ const button = touchPos.subtract(buttonCenter).direction();
2240
+ touchGamepadButtons[button] = 1;
2241
+ }
2242
+ else if (touchPos.distance(startCenter) < touchGamepadSize)
2243
+ {
2244
+ // virtual start button in center
2245
+ touchGamepadButtons[9] = 1;
2226
2246
  }
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
2247
  }
2239
2248
  }
2249
+
2250
+ return ontouchstart(e);
2240
2251
  }
2241
2252
  }
2242
2253
 
@@ -2354,7 +2365,7 @@ class Sound
2354
2365
  {
2355
2366
  if (!soundEnable) return;
2356
2367
 
2357
- let pan = 0;
2368
+ let pan;
2358
2369
  if (pos)
2359
2370
  {
2360
2371
  const range = this.range;
@@ -2384,7 +2395,7 @@ class Sound
2384
2395
  * @param {Number} [volume=1] - How much to scale volume by (in addition to range fade)
2385
2396
  * @return {AudioBufferSourceNode} - The audio, can be used to stop sound later
2386
2397
  */
2387
- playNote(semitoneOffset, pos, volume=1)
2398
+ playNote(semitoneOffset, pos, volume)
2388
2399
  {
2389
2400
  if (!soundEnable) return;
2390
2401
 
@@ -2439,7 +2450,7 @@ class Music
2439
2450
  * @param {Boolean} [loop=1] - True if the music should loop when it reaches the end
2440
2451
  * @return {AudioBufferSourceNode} - The audio node, can be used to stop sound later
2441
2452
  */
2442
- play(volume = 1, loop = 1)
2453
+ play(volume, loop = 1)
2443
2454
  {
2444
2455
  if (!soundEnable) return;
2445
2456
 
@@ -2521,7 +2532,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2521
2532
 
2522
2533
  // create audio context
2523
2534
  if (!audioContext)
2524
- audioContext = new (window.AudioContext||webkitAudioContext);
2535
+ audioContext = new AudioContext;
2525
2536
 
2526
2537
  // fix stalled audio
2527
2538
  audioContext.resume();
@@ -2540,18 +2551,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2540
2551
  source.playbackRate.value = rate;
2541
2552
  source.loop = loop;
2542
2553
 
2543
- // create and connect gain node (createGain is more widley spported then GainNode construtor)
2554
+ // create and connect gain node (createGain is more widely spported then GainNode construtor)
2544
2555
  const gainNode = audioContext.createGain();
2545
2556
  gainNode.gain.value = soundVolume*volume;
2546
2557
  gainNode.connect(audioContext.destination);
2547
2558
 
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);
2559
+ // connect source to stereo panner and gain
2560
+ source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
2555
2561
 
2556
2562
  // play and return sound
2557
2563
  source.start();
@@ -2669,10 +2675,8 @@ function zzfxG
2669
2675
  * @memberof Audio */
2670
2676
  function zzfxM(instruments, patterns, sequence, BPM = 125)
2671
2677
  {
2678
+ let i, j, k;
2672
2679
  let instrumentParameters;
2673
- let i;
2674
- let j;
2675
- let k;
2676
2680
  let note;
2677
2681
  let sample;
2678
2682
  let patternChannel;
@@ -2842,14 +2846,11 @@ function tileCollisionRaycast(posStart, posEnd, object)
2842
2846
  {
2843
2847
  // test if a ray collides with tiles from start to end
2844
2848
  // 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);
2849
+ const posDelta = (posEnd = posEnd.floor()).subtract(posStart = posStart.floor());
2848
2850
  const dx = abs(posDelta.x), dy = -abs(posDelta.y);
2849
2851
  const sx = sign(posDelta.x), sy = sign(posDelta.y);
2850
- let e = dx + dy;
2851
2852
 
2852
- for (let x = posStart.x, y = posStart.y;;)
2853
+ for (let x = posStart.x, y = posStart.y, e = dx + dy;;)
2853
2854
  {
2854
2855
  const tileData = getTileCollisionData(vec2(x,y));
2855
2856
  if (tileData && (object ? object.collideWithTileRaycast(tileData, new Vector2(x, y)) : tileData > 0))
@@ -2941,7 +2942,7 @@ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1)
2941
2942
  // init tile data
2942
2943
  this.data = [];
2943
2944
  for (let j = this.size.area(); j--;)
2944
- this.data.push(new TileLayerData());
2945
+ this.data.push(new TileLayerData);
2945
2946
  }
2946
2947
 
2947
2948
  /** Set data at a given position in the array
@@ -2997,21 +2998,23 @@ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1)
2997
2998
  * @param {Boolean} [clear=0] - Should it clear the canvas before drawing */
2998
2999
  redrawStart(clear = 0)
2999
3000
  {
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
3001
  // save current render settings
3008
- this.savedRenderSettings = [mainCanvas, mainContext, cameraPos, cameraScale];
3002
+ this.savedRenderSettings = [mainCanvas, mainContext, mainCanvasSize, cameraPos, cameraScale];
3009
3003
 
3010
- // use normal rendering system to render the tiles
3004
+ // hack: use normal rendering system to render the tiles
3011
3005
  mainCanvas = this.canvas;
3012
3006
  mainContext = this.context;
3013
3007
  cameraPos = this.size.scale(.5);
3014
3008
  cameraScale = this.tileSize.x;
3009
+
3010
+ if (clear)
3011
+ {
3012
+ // clear and set size
3013
+ mainCanvas.width = this.size.x * this.tileSize.x;
3014
+ mainCanvas.height = this.size.y * this.tileSize.y;
3015
+ }
3016
+
3017
+ // begin a new render for the tile canvas
3015
3018
  enginePreRender();
3016
3019
  }
3017
3020
 
@@ -3023,7 +3026,7 @@ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1)
3023
3026
  //debugSaveCanvas(this.canvas);
3024
3027
 
3025
3028
  // set stuff back to normal
3026
- [mainCanvas, mainContext, cameraPos, cameraScale] = this.savedRenderSettings;
3029
+ [mainCanvas, mainContext, mainCanvasSize, cameraPos, cameraScale] = this.savedRenderSettings;
3027
3030
  }
3028
3031
 
3029
3032
  /** Draw the tile at a given position
@@ -3264,7 +3267,7 @@ class ParticleEmitter extends EngineObject
3264
3267
  this.parent && super.update();
3265
3268
 
3266
3269
  // update emitter
3267
- if (!this.emitTime || this.getAliveTime() <= this.emitTime)
3270
+ if (!this.emitTime | this.getAliveTime() <= this.emitTime)
3268
3271
  {
3269
3272
  // emit particles
3270
3273
  if (this.emitRate * particleEmitRateScale)
@@ -3288,7 +3291,7 @@ class ParticleEmitter extends EngineObject
3288
3291
  let pos = this.emitSize.x != undefined ? // check if vec2 was used for size
3289
3292
  (new Vector2(rand(-.5,.5), rand(-.5,.5)))
3290
3293
  .multiply(this.emitSize).rotate(this.angle) // box emitter
3291
- : randInCircle(this.emitSize * .5); // circle emitter
3294
+ : randInCircle(this.emitSize/2); // circle emitter
3292
3295
  let angle = rand(this.particleConeAngle, -this.particleConeAngle);
3293
3296
  if (!this.localSpace)
3294
3297
  {
@@ -3331,7 +3334,7 @@ class ParticleEmitter extends EngineObject
3331
3334
  particle.additive = this.additive;
3332
3335
  particle.renderOrder = this.renderOrder;
3333
3336
  particle.trailScale = this.trailScale;
3334
- particle.mirror = rand()<.5;
3337
+ particle.mirror = randInt(2);
3335
3338
  particle.localSpaceEmitter = this.localSpace && this;
3336
3339
 
3337
3340
  // setup callbacks for particles
@@ -3360,7 +3363,8 @@ class Particle extends EngineObject
3360
3363
  * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
3361
3364
  * @param {Number} [angle=0] - Angle to rotate the particle
3362
3365
  */
3363
- constructor(pos, tileIndex, tileSize, angle) { super(pos, new Vector2, tileIndex, tileSize, angle); }
3366
+ constructor(pos, tileIndex, tileSize, angle)
3367
+ { super(pos, new Vector2, tileIndex, tileSize, angle); }
3364
3368
 
3365
3369
  /** Render the particle, automatically called each frame, sorted by renderOrder */
3366
3370
  render()
@@ -3429,10 +3433,6 @@ class Particle extends EngineObject
3429
3433
  * @memberof Medals */
3430
3434
  const medals = [];
3431
3435
 
3432
- /** Set to stop medals from being unlockable (like if cheats are enabled)
3433
- * @memberof Medals */
3434
- let medalsPreventUnlock;
3435
-
3436
3436
  // Engine internal variables not exposed to documentation
3437
3437
  let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
3438
3438
 
@@ -3480,9 +3480,8 @@ class Medal
3480
3480
  this.name = name;
3481
3481
  this.description = description;
3482
3482
  this.icon = icon;
3483
- this.image = new Image();
3484
3483
  if (src)
3485
- this.image.src = src;
3484
+ (this.image = new Image).src = src;
3486
3485
  }
3487
3486
 
3488
3487
  /** Unlocks a medal if not already unlocked */
@@ -3495,10 +3494,7 @@ class Medal
3495
3494
  ASSERT(medalsSaveName); // save name must be set
3496
3495
  localStorage[this.storageKey()] = this.unlocked = 1;
3497
3496
  medalsDisplayQueue.push(this);
3498
-
3499
- // save for newgrounds and OS13K
3500
3497
  newgrounds && newgrounds.unlockMedal(this.id);
3501
- localStorage['OS13kTrophy,' + this.icon + ',' + medalsSaveName + ',' + this.name] = this.description;
3502
3498
  }
3503
3499
 
3504
3500
  /** Render a medal
@@ -3507,27 +3503,26 @@ class Medal
3507
3503
  render(hidePercent=0)
3508
3504
  {
3509
3505
  const context = overlayContext;
3510
- const width = min(medalDisplayWidth, mainCanvas.width);
3506
+ const width = min(medalDisplaySize.x, mainCanvas.width);
3511
3507
  const x = overlayCanvas.width - width;
3512
- const y = -medalDisplayHeight*hidePercent;
3508
+ const y = -medalDisplaySize.y*hidePercent;
3513
3509
 
3514
3510
  // draw containing rect and clip to that region
3515
3511
  context.save();
3516
3512
  context.beginPath();
3517
- context.fillStyle = '#ddd'
3518
- context.fill(context.rect(x, y, width, medalDisplayHeight));
3519
- context.strokeStyle = '#000';
3513
+ context.fillStyle = new Color(.9,.9,.9);
3514
+ context.strokeStyle = new Color(0,0,0);
3520
3515
  context.lineWidth = 3;
3516
+ context.fill(context.rect(x, y, width, medalDisplaySize.y));
3521
3517
  context.stroke();
3522
3518
  context.clip();
3523
3519
 
3524
3520
  // 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);
3521
+ this.renderIcon(vec2(x+15+medalDisplayIconSize/2, y+medalDisplaySize.y/2));
3522
+ const pos = vec2(x+medalDisplayIconSize+30, y+28);
3523
+ drawTextScreen(this.name, pos, 38, new Color(0,0,0), 0, 0, 'left');
3524
+ pos.y += 32;
3525
+ drawTextScreen(this.description, pos, 24, new Color(0,0,0), 0, 0, 'left');
3531
3526
  context.restore();
3532
3527
  }
3533
3528
 
@@ -3536,18 +3531,13 @@ class Medal
3536
3531
  * @param {Number} y - Screen space Y position
3537
3532
  * @param {Number} [size=medalDisplayIconSize] - Screen space size
3538
3533
  */
3539
- renderIcon(x, y, size=medalDisplayIconSize)
3534
+ renderIcon(pos, size=medalDisplayIconSize)
3540
3535
  {
3541
3536
  // 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);
3537
+ if (this.image)
3538
+ overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
3549
3539
  else
3550
- context.fillText(this.icon, x, y); // show icon if there is no image
3540
+ drawTextScreen(this.icon, pos, size*.7, new Color(0,0,0));
3551
3541
  }
3552
3542
 
3553
3543
  // Get local storage key used by the medal
@@ -3605,18 +3595,20 @@ class Newgrounds
3605
3595
  constructor(app_id, cipher)
3606
3596
  {
3607
3597
  ASSERT(!newgrounds && app_id);
3598
+
3608
3599
  this.app_id = app_id;
3609
3600
  this.cipher = cipher;
3610
3601
  this.host = location ? location.hostname : '';
3611
-
3602
+
3612
3603
  // create an instance of CryptoJS for encrypted calls
3613
- cipher && (this.cryptoJS = CryptoJS());
3604
+ if (cipher)
3605
+ this.cryptoJS = this.CryptoJS();
3614
3606
 
3615
3607
  // get session id from url search params
3616
3608
  const url = new URL(location.href);
3617
- this.session_id = url.searchParams.get('ngio_session_id') || 0;
3609
+ this.session_id = url.searchParams.get('ngio_session_id');
3618
3610
 
3619
- if (this.session_id == 0)
3611
+ if (!this.session_id)
3620
3612
  return; // only use newgrounds when logged in
3621
3613
 
3622
3614
  // get medals
@@ -3629,6 +3621,7 @@ class Newgrounds
3629
3621
  if (medal)
3630
3622
  {
3631
3623
  // copy newgrounds medal data
3624
+ medal.image = new Image;
3632
3625
  medal.image.src = newgroundsMedal['icon'];
3633
3626
  medal.name = newgroundsMedal['name'];
3634
3627
  medal.description = newgroundsMedal['description'];
@@ -3713,13 +3706,16 @@ class Newgrounds
3713
3706
  debugMedals && console.log(xmlHttp.responseText);
3714
3707
  return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
3715
3708
  }
3716
- }
3717
3709
 
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
3710
+ CryptoJS()
3711
+ {
3712
+ ///////////////////////////////////////////////////////////////////////////////
3713
+ // Crypto-JS - https://github.com/brix/crypto-js [The MIT License (MIT)]
3714
+ // Copyright (c) 2009-2013 Jeff Mott Copyright (c) 2013-2016 Evan Vosberg
3721
3715
 
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));
3716
+ return 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));
3717
+ }
3718
+ }
3723
3719
  /**
3724
3720
  * LittleJS WebGL Interface
3725
3721
  * <br> - All webgl used by the engine is wrapped up here
@@ -3749,7 +3745,7 @@ let glContext;
3749
3745
  let glTileTexture;
3750
3746
 
3751
3747
  // WebGL internal variables not exposed to documentation
3752
- let glActiveTexture, glShader, glArrayBuffer, glVertexData, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3748
+ let glActiveTexture, glShader, glArrayBuffer, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3753
3749
 
3754
3750
  ///////////////////////////////////////////////////////////////////////////////
3755
3751
 
@@ -3770,27 +3766,25 @@ function glInit()
3770
3766
  'uniform mat4 m;'+ // transform matrix
3771
3767
  'attribute vec2 p,t;'+ // position, uv
3772
3768
  'attribute vec4 c,a;'+ // color, additiveColor
3773
- 'varying vec2 v;'+ // return uv
3774
- 'varying vec4 d,e;'+ // return color, additiveColor
3769
+ 'varying vec4 v,d,e;'+ // return uv, color, additiveColor
3775
3770
  'void main(){'+ // shader entry point
3776
3771
  'gl_Position=m*vec4(p,1,1);'+ // transform position
3777
- 'v=t;d=c;e=a;'+ // pass stuff to fragment shader
3772
+ 'v=vec4(t,p);d=c;e=a;'+ // pass stuff to fragment shader
3778
3773
  '}' // end of shader
3779
3774
  ,
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
3775
+ 'precision highp float;'+ // use highp for better accuracy
3776
+ 'varying vec4 v,d,e;'+ // uv, color, additiveColor
3777
+ 'uniform sampler2D s;'+ // texture
3778
+ 'void main(){'+ // shader entry point
3779
+ 'gl_FragColor=texture2D(s,v.xy)*d+e;'+ // modulate texture by color plus additive
3780
+ '}' // end of shader
3787
3781
  );
3788
3782
 
3789
3783
  // init buffers
3790
- glVertexData = new ArrayBuffer(gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE);
3784
+ const vertexData = new ArrayBuffer(gl_VERTEX_BUFFER_SIZE);
3791
3785
  glArrayBuffer = glContext.createBuffer();
3792
- glPositionData = new Float32Array(glVertexData);
3793
- glColorData = new Uint32Array(glVertexData);
3786
+ glPositionData = new Float32Array(vertexData);
3787
+ glColorData = new Uint32Array(vertexData);
3794
3788
  glBatchCount = 0;
3795
3789
  }
3796
3790
 
@@ -3865,18 +3859,19 @@ function glCreateTexture(image)
3865
3859
  image && image.width && glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
3866
3860
 
3867
3861
  // 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);
3862
+ const filter = cavasPixelated ? gl_NEAREST : gl_LINEAR;
3863
+ glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
3864
+ glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
3870
3865
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE);
3871
3866
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE);
3872
3867
  return texture;
3873
3868
  }
3874
3869
 
3875
3870
  // called automatically by engine before render
3876
- function glPreRender(width, height, cameraX, cameraY, cameraScale)
3871
+ function glPreRender()
3877
3872
  {
3878
3873
  // clear and set to same size as main canvas
3879
- glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height);
3874
+ glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
3880
3875
  glContext.clear(gl_COLOR_BUFFER_BIT);
3881
3876
 
3882
3877
  // set up the shader
@@ -3884,7 +3879,7 @@ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3884
3879
  glContext.activeTexture(gl_TEXTURE0);
3885
3880
  glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
3886
3881
  glContext.bindBuffer(gl_ARRAY_BUFFER, glArrayBuffer);
3887
- glContext.bufferData(gl_ARRAY_BUFFER, glVertexData.byteLength, gl_DYNAMIC_DRAW);
3882
+ glContext.bufferData(gl_ARRAY_BUFFER, gl_VERTEX_BUFFER_SIZE, gl_DYNAMIC_DRAW);
3888
3883
  glSetBlendMode();
3889
3884
 
3890
3885
  // set vertex attributes
@@ -3902,14 +3897,14 @@ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3902
3897
  initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3903
3898
 
3904
3899
  // build the transform matrix
3905
- const sx = 2 * cameraScale / width;
3906
- const sy = 2 * cameraScale / height;
3900
+ const sx = 2 * cameraScale / mainCanvas.width;
3901
+ const sy = 2 * cameraScale / mainCanvas.height;
3907
3902
  glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), 0,
3908
3903
  new Float32Array([
3909
3904
  sx, 0, 0, 0,
3910
3905
  0, sy, 0, 0,
3911
3906
  1, 1, -1, 1,
3912
- -1-sx*cameraX, -1-sy*cameraY, 0, 0
3907
+ -1-sx*cameraPos.x, -1-sy*cameraPos.y, 0, 0
3913
3908
  ])
3914
3909
  );
3915
3910
  }
@@ -3957,10 +3952,10 @@ function glCopyToContext(context, forceDraw)
3957
3952
  * @param uv0Y
3958
3953
  * @param uv1X
3959
3954
  * @param uv1Y
3960
- * @param [rgba=0xffffffff]
3955
+ * @param rgba
3961
3956
  * @param [rgbaAdditive=0]
3962
3957
  * @memberof WebGL */
3963
- function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffffff, rgbaAdditive=0)
3958
+ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
3964
3959
  {
3965
3960
  // flush if there is no room for more verts or if different blend mode
3966
3961
  if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
@@ -3971,43 +3966,16 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffff
3971
3966
  const cx = c*sizeX, cy = c*sizeY, sx = s*sizeX, sy = s*sizeY;
3972
3967
 
3973
3968
  // 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;
3969
+ for(let i=6, offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT; i--;)
3970
+ {
3971
+ const a = i-4&&i>1, b = i-5&&i-2&&i-1;
3972
+ glPositionData[offset++] = x + (a?-cx:cx) + (b?sy:-sy);
3973
+ glPositionData[offset++] = y + (b?cy:-cy) + (a?sx:-sx);
3974
+ glPositionData[offset++] = a ? uv0X : uv1X;
3975
+ glPositionData[offset++] = b ? uv0Y : uv1Y;
3976
+ glColorData[offset++] = rgba;
3977
+ glColorData[offset++] = rgbaAdditive;
3978
+ }
4011
3979
  }
4012
3980
 
4013
3981
  ///////////////////////////////////////////////////////////////////////////////
@@ -4059,14 +4027,13 @@ function glRenderPostProcess()
4059
4027
  return;
4060
4028
 
4061
4029
  // prepare to render post process shader
4062
- const width = mainCanvas.width, height = mainCanvas.height;
4063
4030
  if (glEnable)
4064
4031
  {
4065
4032
  glFlush(); // clear out the buffer
4066
4033
  mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
4067
4034
  }
4068
- else
4069
- glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height); // set viewport
4035
+ else // set viewport
4036
+ glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
4070
4037
 
4071
4038
  // setup shader program to draw one triangle
4072
4039
  glContext.useProgram(glPostShader);
@@ -4090,7 +4057,7 @@ function glRenderPostProcess()
4090
4057
  const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
4091
4058
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
4092
4059
  glContext.uniform1f(uniformLocation('iTime'), time);
4093
- glContext.uniform3f(uniformLocation('iResolution'), width, height, 1);
4060
+ glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
4094
4061
  glContext.drawArrays(gl_TRIANGLES, 0, 3);
4095
4062
  }
4096
4063
 
@@ -4130,4 +4097,5 @@ gl_UNPACK_FLIP_Y_WEBGL = 37440,
4130
4097
  gl_VERTICES_PER_QUAD = 6,
4131
4098
  gl_INDICIES_PER_VERT = 6,
4132
4099
  gl_MAX_BATCH = 1<<16,
4133
- gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2; // vec2 * 2 + (char * 4) * 2
4100
+ gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2, // vec2 * 2 + (char * 4) * 2
4101
+ gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE;