littlejsengine 1.3.8 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +30 -9
  2. package/build.bat +1 -1
  3. package/buildSetup.bat +2 -2
  4. package/engine/engine.all.js +276 -167
  5. package/engine/engine.all.min.js +1 -1
  6. package/engine/engine.all.module.js +4812 -0
  7. package/engine/engine.all.release.js +259 -143
  8. package/engine/engine.all.release.module.js +4457 -0
  9. package/engine/engine.js +12 -8
  10. package/engine/engineBuild.bat +1 -1
  11. package/engine/engineBuildModule.mjs +14 -0
  12. package/engine/engineDebug.js +17 -24
  13. package/engine/engineDraw.js +16 -16
  14. package/engine/engineExportModule.js +333 -0
  15. package/engine/engineInput.js +9 -8
  16. package/engine/engineMedals.js +10 -6
  17. package/engine/engineObject.js +4 -4
  18. package/engine/engineParticles.js +45 -21
  19. package/engine/engineTileLayer.js +2 -2
  20. package/engine/engineUtilities.js +47 -25
  21. package/engine/engineWebGL.js +114 -54
  22. package/engine/index.d.ts +64 -30
  23. package/examples/breakout/game.js +63 -11
  24. package/examples/breakout/gameObjects.js +14 -12
  25. package/examples/breakout/index.html +3 -3
  26. package/examples/empty/game.js +35 -0
  27. package/examples/empty/index.html +9 -0
  28. package/examples/empty/tiles.png +0 -0
  29. package/examples/module/game.js +122 -0
  30. package/examples/module/index.html +9 -0
  31. package/examples/module/tiles.png +0 -0
  32. package/examples/particles/index.html +14 -14
  33. package/examples/particles/tiles.png +0 -0
  34. package/examples/platformer/game.js +1 -1
  35. package/examples/platformer/gameObjects.js +7 -7
  36. package/examples/platformer/gamePlayer.js +7 -5
  37. package/examples/platformer/index.html +6 -6
  38. package/examples/puzzle/index.html +2 -2
  39. package/examples/stress/index.html +2 -2
  40. package/game.js +10 -12
  41. package/index.html +13 -13
  42. package/package.json +36 -32
@@ -355,6 +355,28 @@ class Vector2
355
355
 
356
356
  ///////////////////////////////////////////////////////////////////////////////
357
357
 
358
+ /**
359
+ * Create a color object with RGBA values
360
+ * @param {Number} [r=1]
361
+ * @param {Number} [g=1]
362
+ * @param {Number} [b=1]
363
+ * @param {Number} [a=1]
364
+ * @return {Color}
365
+ * @memberof Utilities
366
+ */
367
+ const colorRGBA = (r, g, b, a)=> new Color(r, g, b, a);
368
+
369
+ /**
370
+ * Create a color object with HSLA values
371
+ * @param {Number} [h=0]
372
+ * @param {Number} [s=0]
373
+ * @param {Number} [l=1]
374
+ * @param {Number} [a=1]
375
+ * @return {Color}
376
+ * @memberof Utilities
377
+ */
378
+ const colorHSLA = (h, s, l, a)=> new Color().setHSLA(h, s, l, a);
379
+
358
380
  /**
359
381
  * Color object (red, green, blue, alpha) with some helpful functions
360
382
  * @example
@@ -446,10 +468,10 @@ class Color
446
468
  * @return {Array} */
447
469
  getHSLA()
448
470
  {
449
- const r = this.r;
450
- const g = this.g;
451
- const b = this.b;
452
- const a = this.a;
471
+ const r = clamp(this.r);
472
+ const g = clamp(this.g);
473
+ const b = clamp(this.b);
474
+ const a = clamp(this.a);
453
475
  const max = Math.max(r, g, b);
454
476
  const min = Math.min(r, g, b);
455
477
  const l = (max + min) / 2;
@@ -485,20 +507,13 @@ class Color
485
507
  ).clamp();
486
508
  }
487
509
 
488
- /** Returns this color expressed as an CSS color value
510
+ /** Returns this color expressed as a hex color code
511
+ * @param {Boolean} [useAlpha=1] - if alpha should be included in result
489
512
  * @return {String} */
490
- toString()
513
+ toString(useAlpha = 1)
491
514
  {
492
- ASSERT(this.r>=0 && this.r<=1 && this.g>=0 && this.g<=1 && this.b>=0 && this.b<=1 && this.a>=0 && this.a<=1);
493
- return `rgb(${this.r*255|0},${this.g*255|0},${this.b*255|0},${this.a})`;
494
- }
495
-
496
- /** Returns this color expressed as 32 bit integer RGBA value
497
- * @return {Number} */
498
- rgbaInt()
499
- {
500
- ASSERT(this.r>=0 && this.r<=1 && this.g>=0 && this.g<=1 && this.b>=0 && this.b<=1 && this.a>=0 && this.a<=1);
501
- return (this.r*255|0) + (this.g*255<<8) + (this.b*255<<16) + (this.a*255<<24);
515
+ const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
516
+ return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
502
517
  }
503
518
 
504
519
  /** Set this color from a hex code
@@ -506,21 +521,24 @@ class Color
506
521
  * @return {Color} */
507
522
  setHex(hex)
508
523
  {
509
- const fromHex = (a)=> parseInt(hex.slice(a,a+2), 16)/255;
524
+ const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
510
525
  this.r = fromHex(1);
511
526
  this.g = fromHex(3),
512
527
  this.b = fromHex(5);
513
- this.a = 1;
514
- ASSERT(this.r>=0 && this.r<=1 && this.g>=0 && this.g<=1 && this.b>=0 && this.b<=1);
528
+ this.a = hex.length > 7 ? fromHex(7) : 1;
515
529
  return this;
516
530
  }
517
-
518
- /** Returns this color expressed as a hex code
519
- * @return {String} */
520
- getHex()
531
+
532
+ /** Returns this color expressed as 32 bit RGBA value
533
+ * @return {Number} */
534
+ rgbaInt()
521
535
  {
522
- const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
523
- return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b);
536
+ const toByte = (c)=> clamp(c)*255|0;
537
+ const r = toByte(this.r);
538
+ const g = toByte(this.g)<<8;
539
+ const b = toByte(this.b)<<16;
540
+ const a = toByte(this.a)<<24;
541
+ return r + g + b + a;
524
542
  }
525
543
  }
526
544
 
@@ -571,6 +589,10 @@ class Timer
571
589
  /** Returns this timer expressed as a string
572
590
  * @return {String} */
573
591
  toString() { if (debug) { return this.unset() ? 'unset' : Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ); } }
592
+
593
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
594
+ * @return {Number} */
595
+ valueOf() { return this.get(); }
574
596
  }
575
597
  /**
576
598
  * LittleJS Engine Settings
@@ -819,7 +841,7 @@ let medalDisplayIconSize = 50;
819
841
  const engineName = 'LittleJS';
820
842
 
821
843
  /** Version of engine */
822
- const engineVersion = '1.3.8';
844
+ const engineVersion = '1.4.5';
823
845
 
824
846
  /** Frames per second to update objects
825
847
  * @default */
@@ -854,8 +876,7 @@ let frameTimeLastMS = 0, frameTimeBufferMS = 0, tileImageSize, tileImageFixBleed
854
876
  let averageFPS, drawCount;
855
877
 
856
878
  // css text used for elements created by engine
857
- const styleBody = 'margin:0;overflow:hidden;background:#000' +
858
- ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
879
+ const styleBody = 'margin:0;overflow:hidden;background:#000';
859
880
  const styleCanvas = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
860
881
 
861
882
  ///////////////////////////////////////////////////////////////////////////////
@@ -885,7 +906,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
885
906
 
886
907
  // init stuff and start engine
887
908
  debugInit();
888
- glInit();
909
+ glEnable && glInit();
889
910
 
890
911
  // create overlay canvas for hud to appear above gl canvas
891
912
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
@@ -905,10 +926,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
905
926
  frameTimeLastMS = frameTimeMS;
906
927
  if (debug || showWatermark)
907
928
  averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
929
+ const debugSpeedUp = debug && keyIsDown(107); // +
930
+ const debugSpeedDown = debug && keyIsDown(109); // -
908
931
  if (debug)
909
- frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
932
+ frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1; // +/- to speed/slow time
910
933
  timeReal += frameTimeDeltaMS / 1e3;
911
- frameTimeBufferMS = min(frameTimeBufferMS + !paused * frameTimeDeltaMS, 50); // clamp incase of slow framerate
934
+ frameTimeBufferMS += !paused * frameTimeDeltaMS;
935
+ if (!debugSpeedUp)
936
+ frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp incase of slow framerate
912
937
 
913
938
  if (canvasFixedSize.x)
914
939
  {
@@ -981,10 +1006,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
981
1006
  for (const o of engineObjects)
982
1007
  o.destroyed || o.render();
983
1008
  gameRenderPost();
1009
+ glRenderPostProcess();
984
1010
  medalsRender();
985
1011
  touchGamepadRender();
986
1012
  debugRender();
987
- glCopyToContext(mainContext);
1013
+ glEnable && glCopyToContext(mainContext);
988
1014
 
989
1015
  if (showWatermark)
990
1016
  {
@@ -1019,7 +1045,7 @@ function enginePreRender()
1019
1045
  mainContext.imageSmoothingEnabled = !cavasPixelated;
1020
1046
 
1021
1047
  // setup gl rendering if enabled
1022
- glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
1048
+ glEnable && glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
1023
1049
  }
1024
1050
 
1025
1051
  ///////////////////////////////////////////////////////////////////////////////
@@ -1380,7 +1406,7 @@ class EngineObject
1380
1406
  * @return {Boolean} - true if the raycast should hit */
1381
1407
  collideWithTileRaycast(tileData, pos) { return tileData > 0; }
1382
1408
 
1383
- /** Called to check if a tile raycast hit
1409
+ /** Called to check if a object collision should be resolved
1384
1410
  * @param {EngineObject} object - the object to test against
1385
1411
  * @return {Boolean} - true if the collision should be resolved
1386
1412
  */
@@ -1425,10 +1451,10 @@ class EngineObject
1425
1451
  }
1426
1452
 
1427
1453
  /** Set how this object collides
1428
- * @param {boolean} [collideSolidObjects=0] - Does it collide with solid objects
1429
- * @param {boolean} [isSolid=0] - Does it collide with and block other objects (expensive in large numbers)
1454
+ * @param {boolean} [collideSolidObjects=1] - Does it collide with solid objects
1455
+ * @param {boolean} [isSolid=1] - Does it collide with and block other objects (expensive in large numbers)
1430
1456
  * @param {boolean} [collideTiles=1] - Does it collide with the tile collision */
1431
- setCollision(collideSolidObjects=0, isSolid=0, collideTiles=1)
1457
+ setCollision(collideSolidObjects=1, isSolid=1, collideTiles=1)
1432
1458
  {
1433
1459
  ASSERT(collideSolidObjects || !isSolid); // solid objects must be set to collide
1434
1460
 
@@ -1645,7 +1671,7 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL)
1645
1671
  {
1646
1672
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
1647
1673
  const size = vec2(thickness, halfDelta.length()*2);
1648
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), 0, 0, useWebGL);
1674
+ drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL);
1649
1675
  }
1650
1676
 
1651
1677
  /** Draw directly to a 2d canvas context in world space
@@ -1662,7 +1688,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
1662
1688
  pos = worldToScreen(pos);
1663
1689
  size = size.scale(cameraScale);
1664
1690
  context.save();
1665
- context.translate(pos.x+.5|0, pos.y-.5|0);
1691
+ context.translate(pos.x+.5|0, pos.y+.5|0);
1666
1692
  context.rotate(angle);
1667
1693
  context.scale(mirror ? -size.x : size.x, size.y);
1668
1694
  drawFunction(context);
@@ -1691,21 +1717,21 @@ function setBlendMode(additive, useWebGL=glEnable)
1691
1717
  * @param {Color} [lineColor=new Color(0,0,0)]
1692
1718
  * @param {String} [textAlign='center']
1693
1719
  * @memberof Draw */
1694
- function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault)
1720
+ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
1695
1721
  {
1696
- overlayContext.fillStyle = color;
1697
- overlayContext.lineWidth = lineWidth;
1698
- overlayContext.strokeStyle = lineColor;
1699
- overlayContext.textAlign = textAlign;
1700
- overlayContext.font = size + 'px '+ font;
1701
- overlayContext.textBaseline = 'middle';
1702
- overlayContext.lineJoin = 'round';
1722
+ context.fillStyle = color;
1723
+ context.lineWidth = lineWidth;
1724
+ context.strokeStyle = lineColor;
1725
+ context.textAlign = textAlign;
1726
+ context.font = size + 'px '+ font;
1727
+ context.textBaseline = 'middle';
1728
+ context.lineJoin = 'round';
1703
1729
 
1704
1730
  pos = pos.copy();
1705
1731
  (text+'').split('\n').forEach(line=>
1706
1732
  {
1707
- lineWidth && overlayContext.strokeText(line, pos.x, pos.y);
1708
- overlayContext.fillText(line, pos.x, pos.y);
1733
+ lineWidth && context.strokeText(line, pos.x, pos.y);
1734
+ context.fillText(line, pos.x, pos.y);
1709
1735
  pos.y += size;
1710
1736
  });
1711
1737
  }
@@ -1722,7 +1748,7 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
1722
1748
  * @memberof Draw */
1723
1749
  function drawText(text, pos, size=1, color, lineWidth, lineColor, textAlign, font)
1724
1750
  {
1725
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font);
1751
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, mainContext);
1726
1752
  }
1727
1753
 
1728
1754
  ///////////////////////////////////////////////////////////////////////////////
@@ -1745,7 +1771,7 @@ let engineFontImage;
1745
1771
  class FontImage
1746
1772
  {
1747
1773
  /** Create an image font
1748
- * @param {Image} [image] - The image the font is stored in, if undefined the default font is used
1774
+ * @param {HTMLImageElement} [image] - The image the font is stored in, if undefined the default font is used
1749
1775
  * @param {Vector2} [tileSize=vec2(8)] - The size of the font source tiles
1750
1776
  * @param {Vector2} [paddingSize=vec2(0,1)] - How much extra space to add between characters
1751
1777
  * @param {Number} [startTileIndex=0] - Tile index in image where font starts
@@ -1782,7 +1808,7 @@ class FontImage
1782
1808
  const size = this.tileSize;
1783
1809
  const drawSize = size.add(this.paddingSize).scale(scale);
1784
1810
  const cols = this.image.width / this.tileSize.x |0;
1785
- text.split('\n').forEach((line, i)=>
1811
+ (text+'').split('\n').forEach((line, i)=>
1786
1812
  {
1787
1813
  const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
1788
1814
  for(let j=line.length; j--;)
@@ -1844,6 +1870,7 @@ function toggleFullscreen()
1844
1870
  document.body.mozRequestFullScreen();
1845
1871
  }
1846
1872
  }
1873
+
1847
1874
  /**
1848
1875
  * LittleJS Input System
1849
1876
  * <br> - Tracks key down, pressed, and released
@@ -1958,7 +1985,7 @@ let inputData = [[]];
1958
1985
  function inputUpdate()
1959
1986
  {
1960
1987
  // clear input when lost focus (prevent stuck keys)
1961
- document.hasFocus() || clearInput();
1988
+ isTouchDevice || document.hasFocus() || clearInput();
1962
1989
 
1963
1990
  // update mouse world space position
1964
1991
  mousePos = screenToWorld(mousePosScreen);
@@ -2033,7 +2060,7 @@ function gamepadsUpdate()
2033
2060
  }
2034
2061
  }
2035
2062
 
2036
- if (!gamepadsEnable || !navigator.getGamepads || !document.hasFocus() && !debug)
2063
+ if (!gamepadsEnable || !navigator || !navigator.getGamepads || !document.hasFocus() && !debug)
2037
2064
  return;
2038
2065
 
2039
2066
  // poll gamepads
@@ -2082,7 +2109,7 @@ function gamepadsUpdate()
2082
2109
  /** Pulse the vibration hardware if it exists
2083
2110
  * @param {Number} [pattern=100] - a single value in miliseconds or vibration interval array
2084
2111
  * @memberof Input */
2085
- const vibrate = (pattern)=> vibrateEnable && Navigator.vibrate && Navigator.vibrate(pattern);
2112
+ const vibrate = (pattern)=> vibrateEnable && navigator && navigator.vibrate && navigator.vibrate(pattern);
2086
2113
 
2087
2114
  /** Cancel any ongoing vibration
2088
2115
  * @memberof Input */
@@ -2100,7 +2127,7 @@ const isTouchDevice = window.ontouchstart !== undefined;
2100
2127
  if (isTouchDevice)
2101
2128
  {
2102
2129
  // handle all touch events the same way
2103
- let wasTouching, hadTouchInput;
2130
+ let wasTouching, hadTouch;
2104
2131
  ontouchstart = ontouchmove = ontouchend = (e)=>
2105
2132
  {
2106
2133
  e.button = 0; // all touches are left click
@@ -2109,7 +2136,8 @@ if (isTouchDevice)
2109
2136
  const touching = e.touches.length;
2110
2137
  if (touching)
2111
2138
  {
2112
- hadTouchInput || zzfx(0, hadTouchInput=1) ; // fix mobile audio, force it to play a sound the first time
2139
+ // fix mobile audio, force it to play a sound on first touch
2140
+ hadTouch || zzfx(0, hadTouch=1);
2113
2141
 
2114
2142
  // set event pos and pass it along
2115
2143
  e.x = e.touches[0].clientX;
@@ -2122,8 +2150,8 @@ if (isTouchDevice)
2122
2150
  // set was touching
2123
2151
  wasTouching = touching;
2124
2152
 
2125
- // prevent normal mouse events from being called
2126
- return !e.cancelable;
2153
+ // must return true so the document will get focus
2154
+ return true;
2127
2155
  }
2128
2156
  }
2129
2157
 
@@ -2208,7 +2236,7 @@ function touchGamepadRender()
2208
2236
  return;
2209
2237
 
2210
2238
  // fade off when not touching or paused
2211
- const alpha = percent(touchGamepadTimer.get(), 4, 3);
2239
+ const alpha = percent(touchGamepadTimer, 4, 3);
2212
2240
  if (!alpha || paused)
2213
2241
  return;
2214
2242
 
@@ -2980,7 +3008,7 @@ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1)
2980
3008
  redrawEnd()
2981
3009
  {
2982
3010
  ASSERT(mainContext == this.context); // must call redrawStart() before drawing tiles
2983
- glCopyToContext(mainContext, 1);
3011
+ glEnable && glCopyToContext(mainContext, 1);
2984
3012
  //debugSaveCanvas(this.canvas);
2985
3013
 
2986
3014
  // set stuff back to normal
@@ -3054,7 +3082,7 @@ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1)
3054
3082
  const cols = tileImage.width/tileSize.x;
3055
3083
  context.globalAlpha = color.a; // only alpha, no color, is supported in this mode
3056
3084
  context.drawImage(tileImage,
3057
- (tileIndex%cols)*tileSize.x, (tileIndex/cols|0)*tileSize.x,
3085
+ (tileIndex%cols)*tileSize.x, (tileIndex/cols|0)*tileSize.y,
3058
3086
  tileSize.x, tileSize.y, -.5, -.5, 1, 1);
3059
3087
  }
3060
3088
  });
@@ -3124,6 +3152,7 @@ class ParticleEmitter extends EngineObject
3124
3152
  * @param {Boolean} [additive=0] - Should particles use addtive blend
3125
3153
  * @param {Boolean} [randomColorLinear=1] - Should color be randomized linearly or across each component
3126
3154
  * @param {Number} [renderOrder=0] - Render order for particles (additive is above other stuff by default)
3155
+ * @param {Boolean} [localSpace=0] - Should it be in local space of emitter (world space is default)
3127
3156
  */
3128
3157
  constructor
3129
3158
  (
@@ -3153,7 +3182,8 @@ class ParticleEmitter extends EngineObject
3153
3182
  collideTiles,
3154
3183
  additive,
3155
3184
  randomColorLinear = 1,
3156
- renderOrder = additive ? 1e9 : 0
3185
+ renderOrder = additive ? 1e9 : 0,
3186
+ localSpace
3157
3187
  )
3158
3188
  {
3159
3189
  super(pos, new Vector2, tileIndex, tileSize, angle, undefined, renderOrder);
@@ -3207,6 +3237,8 @@ class ParticleEmitter extends EngineObject
3207
3237
  this.collideTiles = collideTiles;
3208
3238
  /** @property {Number} - Should particles use addtive blend */
3209
3239
  this.additive = additive;
3240
+ /** @property {Boolean} - Should it be in local space of emitter */
3241
+ this.localSpace = localSpace;
3210
3242
  /** @property {Number} - If set the partile is drawn as a trail, stretched in the drection of velocity */
3211
3243
  this.trailScale = 0;
3212
3244
 
@@ -3242,30 +3274,38 @@ class ParticleEmitter extends EngineObject
3242
3274
  emitParticle()
3243
3275
  {
3244
3276
  // spawn a particle
3245
- const pos = this.emitSize.x != undefined ? // check if vec2 was used for size
3246
- (new Vector2(rand(-.5,.5), rand(-.5,.5))).multiply(this.emitSize).rotate(this.angle) // box emitter
3247
- : randInCircle(this.emitSize * .5); // circle emitter
3248
- const particle = new Particle(this.pos.add(pos), this.tileIndex, this.tileSize,
3249
- this.angle + rand(this.particleConeAngle, -this.particleConeAngle));
3277
+ let pos = this.emitSize.x != undefined ? // check if vec2 was used for size
3278
+ (new Vector2(rand(-.5,.5), rand(-.5,.5)))
3279
+ .multiply(this.emitSize).rotate(this.angle) // box emitter
3280
+ : randInCircle(this.emitSize * .5); // circle emitter
3281
+ let angle = rand(this.particleConeAngle, -this.particleConeAngle);
3282
+ if (!this.localSpace)
3283
+ {
3284
+ pos = this.pos.add(pos);
3285
+ angle += this.angle;
3286
+ }
3287
+
3288
+ const particle = new Particle(pos, this.tileIndex, this.tileSize, angle);
3250
3289
 
3251
3290
  // randomness scales each paremeter by a percentage
3252
3291
  const randomness = this.randomness;
3253
3292
  const randomizeScale = (v)=> v + v*rand(randomness, -randomness);
3254
3293
 
3255
3294
  // randomize particle settings
3256
- const particleTime = randomizeScale(this.particleTime);
3257
- const sizeStart = randomizeScale(this.sizeStart);
3258
- const sizeEnd = randomizeScale(this.sizeEnd);
3259
- const speed = randomizeScale(this.speed);
3260
- const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
3261
- const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
3262
- const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
3263
- const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
3295
+ const particleTime = randomizeScale(this.particleTime);
3296
+ const sizeStart = randomizeScale(this.sizeStart);
3297
+ const sizeEnd = randomizeScale(this.sizeEnd);
3298
+ const speed = randomizeScale(this.speed);
3299
+ const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
3300
+ const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
3301
+ const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
3302
+ const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
3303
+ const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
3264
3304
 
3265
3305
  // build particle settings
3266
3306
  particle.colorStart = colorStart;
3267
3307
  particle.colorEndDelta = colorEnd.subtract(colorStart);
3268
- particle.velocity = (new Vector2).setAngle(this.angle + coneAngle, speed);
3308
+ particle.velocity = (new Vector2).setAngle(velocityAngle, speed);
3269
3309
  particle.angleVelocity = angleSpeed;
3270
3310
  particle.lifeTime = particleTime;
3271
3311
  particle.sizeStart = sizeStart;
@@ -3281,6 +3321,7 @@ class ParticleEmitter extends EngineObject
3281
3321
  particle.renderOrder = this.renderOrder;
3282
3322
  particle.trailScale = this.trailScale;
3283
3323
  particle.mirror = rand()<.5;
3324
+ particle.localSpaceEmitter = this.localSpace && this;
3284
3325
 
3285
3326
  // setup callbacks for particles
3286
3327
  particle.destroyCallback = this.particleDestroyCallback;
@@ -3327,20 +3368,31 @@ class Particle extends EngineObject
3327
3368
 
3328
3369
  // draw the particle
3329
3370
  this.additive && setBlendMode(1);
3371
+
3372
+ let pos = this.pos, angle = this.angle;
3373
+ if (this.localSpaceEmitter)
3374
+ {
3375
+ // in local space of emitter
3376
+ pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
3377
+ angle += this.localSpaceEmitter.angle;
3378
+ }
3330
3379
  if (this.trailScale)
3331
3380
  {
3332
3381
  // trail style particles
3333
- const speed = this.velocity.length();
3334
- const direction = this.velocity.scale(1/speed);
3382
+ let velocity = this.velocity;
3383
+ if (this.localSpaceEmitter)
3384
+ velocity = velocity.rotate(-this.localSpaceEmitter.angle);
3385
+ const speed = velocity.length();
3386
+ const direction = velocity.scale(1/speed);
3335
3387
  const trailLength = speed * this.trailScale;
3336
3388
  size.y = max(size.x, trailLength);
3337
- this.angle = direction.angle();
3338
- drawTile(this.pos.add(direction.multiply(vec2(0,-trailLength/2))), size, this.tileIndex, this.tileSize, color, this.angle, this.mirror);
3389
+ angle = direction.angle();
3390
+ drawTile(pos.add(direction.multiply(vec2(0,-trailLength/2))), size, this.tileIndex, this.tileSize, color, angle, this.mirror);
3339
3391
  }
3340
3392
  else
3341
- drawTile(this.pos, size, this.tileIndex, this.tileSize, color, this.angle, this.mirror);
3393
+ drawTile(pos, size, this.tileIndex, this.tileSize, color, angle, this.mirror);
3342
3394
  this.additive && setBlendMode();
3343
- debugParticles && debugRect(this.pos, size, '#f005', 0, this.angle);
3395
+ debugParticles && debugRect(pos, size, '#f005', 0, angle);
3344
3396
 
3345
3397
  if (p == 1)
3346
3398
  {
@@ -3370,11 +3422,6 @@ const medals = [];
3370
3422
  * @memberof Medals */
3371
3423
  let medalsPreventUnlock;
3372
3424
 
3373
- /** This can used to enable Newgrounds functionality
3374
- * @type {Newgrounds}
3375
- * @memberof Medals */
3376
- let newgrounds;
3377
-
3378
3425
  // Engine internal variables not exposed to documentation
3379
3426
  let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
3380
3427
 
@@ -3389,7 +3436,7 @@ function medalsInit(saveName)
3389
3436
  {
3390
3437
  // check if medals are unlocked
3391
3438
  medalsSaveName = saveName;
3392
- debugMedals || medals.forEach(medal=> localStorage[medal.storageKey()]);
3439
+ debugMedals || medals.forEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
3393
3440
  }
3394
3441
 
3395
3442
  /**
@@ -3522,6 +3569,15 @@ function medalsRender()
3522
3569
 
3523
3570
  ///////////////////////////////////////////////////////////////////////////////
3524
3571
 
3572
+ // global Newgrounds object
3573
+ let newgrounds;
3574
+
3575
+ /** This can used to enable Newgrounds functionality
3576
+ * @param {Number} app_id - The newgrounds App ID
3577
+ * @param {String} [cipher] - The encryption Key (AES-128/Base64)
3578
+ * @memberof Medals */
3579
+ function newgroundsInit(app_id, cipher) { newgrounds = new Newgrounds(app_id, cipher); }
3580
+
3525
3581
  /**
3526
3582
  * Newgrounds API wrapper object
3527
3583
  * @example
@@ -3682,15 +3738,13 @@ let glContext;
3682
3738
  let glTileTexture;
3683
3739
 
3684
3740
  // WebGL internal variables not exposed to documentation
3685
- let glActiveTexture, glShader, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3741
+ let glActiveTexture, glShader, glArrayBuffer, glVertexData, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3686
3742
 
3687
3743
  ///////////////////////////////////////////////////////////////////////////////
3688
3744
 
3689
3745
  // Init WebGL, called automatically by the engine
3690
3746
  function glInit()
3691
3747
  {
3692
- if (!glEnable) return;
3693
-
3694
3748
  // create the canvas and tile texture
3695
3749
  glCanvas = document.createElement('canvas');
3696
3750
  glContext = glCanvas.getContext('webgl', {antialias: false});
@@ -3723,24 +3777,11 @@ function glInit()
3723
3777
  );
3724
3778
 
3725
3779
  // init buffers
3726
- const glVertexData = new ArrayBuffer(gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE);
3727
- glCreateBuffer(gl_ARRAY_BUFFER, glVertexData.byteLength, gl_DYNAMIC_DRAW);
3780
+ glVertexData = new ArrayBuffer(gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE);
3781
+ glArrayBuffer = glContext.createBuffer();
3728
3782
  glPositionData = new Float32Array(glVertexData);
3729
3783
  glColorData = new Uint32Array(glVertexData);
3730
-
3731
- // setup the vertex data array
3732
- let offset = glBatchCount = 0;
3733
- const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
3734
- {
3735
- const location = glContext.getAttribLocation(glShader, name);
3736
- glContext.enableVertexAttribArray(location);
3737
- glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3738
- offset += size*typeSize;
3739
- }
3740
- initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3741
- initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
3742
- initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
3743
- initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3784
+ glBatchCount = 0;
3744
3785
  }
3745
3786
 
3746
3787
  /** Set the WebGl blend mode, normally you should call setBlendMode instead
@@ -3748,8 +3789,6 @@ function glInit()
3748
3789
  * @memberof WebGL */
3749
3790
  function glSetBlendMode(additive)
3750
3791
  {
3751
- if (!glEnable) return;
3752
-
3753
3792
  // setup blending
3754
3793
  glAdditive = additive;
3755
3794
  }
@@ -3760,8 +3799,6 @@ function glSetBlendMode(additive)
3760
3799
  * @memberof WebGL */
3761
3800
  function glSetTexture(texture=glTileTexture)
3762
3801
  {
3763
- if (!glEnable) return;
3764
-
3765
3802
  // must flush cache with the old texture to set a new one
3766
3803
  if (texture != glActiveTexture)
3767
3804
  glFlush();
@@ -3776,8 +3813,6 @@ function glSetTexture(texture=glTileTexture)
3776
3813
  * @memberof WebGL */
3777
3814
  function glCompileShader(source, type)
3778
3815
  {
3779
- if (!glEnable) return;
3780
-
3781
3816
  // build the shader
3782
3817
  const shader = glContext.createShader(type);
3783
3818
  glContext.shaderSource(shader, source);
@@ -3796,8 +3831,6 @@ function glCompileShader(source, type)
3796
3831
  * @memberof WebGL */
3797
3832
  function glCreateProgram(vsSource, fsSource)
3798
3833
  {
3799
- if (!glEnable) return;
3800
-
3801
3834
  // build the program
3802
3835
  const program = glContext.createProgram();
3803
3836
  glContext.attachShader(program, glCompileShader(vsSource, gl_VERTEX_SHADER));
@@ -3810,35 +3843,16 @@ function glCreateProgram(vsSource, fsSource)
3810
3843
  return program;
3811
3844
  }
3812
3845
 
3813
- /** Create WebGL buffer
3814
- * @param bufferType
3815
- * @param size
3816
- * @param usage
3817
- * @return {WebGLBuffer}
3818
- * @memberof WebGL */
3819
- function glCreateBuffer(bufferType, size, usage)
3820
- {
3821
- if (!glEnable) return;
3822
-
3823
- // build the buffer
3824
- const buffer = glContext.createBuffer();
3825
- glContext.bindBuffer(bufferType, buffer);
3826
- glContext.bufferData(bufferType, size, usage);
3827
- return buffer;
3828
- }
3829
-
3830
3846
  /** Create WebGL texture from an image and set the texture settings
3831
3847
  * @param {Image} image
3832
3848
  * @return {WebGLTexture}
3833
3849
  * @memberof WebGL */
3834
3850
  function glCreateTexture(image)
3835
3851
  {
3836
- if (!glEnable || !image || !image.width) return;
3837
-
3838
3852
  // build the texture
3839
3853
  const texture = glContext.createTexture();
3840
3854
  glContext.bindTexture(gl_TEXTURE_2D, texture);
3841
- glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
3855
+ image && image.width && glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
3842
3856
 
3843
3857
  // use point filtering for pixelated rendering
3844
3858
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, cavasPixelated ? gl_NEAREST : gl_LINEAR);
@@ -3851,16 +3865,31 @@ function glCreateTexture(image)
3851
3865
  // called automatically by engine before render
3852
3866
  function glPreRender(width, height, cameraX, cameraY, cameraScale)
3853
3867
  {
3854
- if (!glEnable) return;
3855
-
3856
3868
  // clear and set to same size as main canvas
3857
3869
  glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height);
3858
3870
  glContext.clear(gl_COLOR_BUFFER_BIT);
3859
3871
 
3860
3872
  // set up the shader
3861
- glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
3862
3873
  glContext.useProgram(glShader);
3874
+ glContext.activeTexture(gl_TEXTURE0);
3875
+ glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
3876
+ glContext.bindBuffer(gl_ARRAY_BUFFER, glArrayBuffer);
3877
+ glContext.bufferData(gl_ARRAY_BUFFER, glVertexData.byteLength, gl_DYNAMIC_DRAW);
3863
3878
  glSetBlendMode();
3879
+
3880
+ // set vertex attributes
3881
+ let offset = 0;
3882
+ const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
3883
+ {
3884
+ const location = glContext.getAttribLocation(glShader, name);
3885
+ glContext.enableVertexAttribArray(location);
3886
+ glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3887
+ offset += size*typeSize;
3888
+ }
3889
+ initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3890
+ initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
3891
+ initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
3892
+ initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3864
3893
 
3865
3894
  // build the transform matrix
3866
3895
  const sx = 2 * cameraScale / width;
@@ -3879,7 +3908,7 @@ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3879
3908
  * @memberof WebGL */
3880
3909
  function glFlush()
3881
3910
  {
3882
- if (!glEnable || !glBatchCount) return;
3911
+ if (!glBatchCount) return;
3883
3912
 
3884
3913
  const destBlend = glBatchAdditive ? gl_ONE : gl_ONE_MINUS_SRC_ALPHA;
3885
3914
  glContext.blendFuncSeparate(gl_SRC_ALPHA, destBlend, gl_ONE, destBlend);
@@ -3899,7 +3928,7 @@ function glFlush()
3899
3928
  * @memberof WebGL */
3900
3929
  function glCopyToContext(context, forceDraw)
3901
3930
  {
3902
- if ((!glEnable || !glBatchCount) && !forceDraw) return;
3931
+ if (!glBatchCount && !forceDraw) return;
3903
3932
 
3904
3933
  glFlush();
3905
3934
 
@@ -3923,8 +3952,6 @@ function glCopyToContext(context, forceDraw)
3923
3952
  * @memberof WebGL */
3924
3953
  function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffffff, rgbaAdditive=0)
3925
3954
  {
3926
- if (!glEnable) return;
3927
-
3928
3955
  // flush if there is no room for more verts or if different blend mode
3929
3956
  if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
3930
3957
  glFlush();
@@ -3973,6 +4000,90 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffff
3973
4000
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3974
4001
  }
3975
4002
 
4003
+ ///////////////////////////////////////////////////////////////////////////////
4004
+ // post processing - can be enabled to pass other canvases through a final shader
4005
+
4006
+ let glPostShader, glPostArrayBuffer, glPostTexture;
4007
+
4008
+ /** Set up a post processing shader
4009
+ * @param {String} shaderCode
4010
+ * @memberof WebGL */
4011
+ function glInitPostProcess(shaderCode)
4012
+ {
4013
+ ASSERT(!glPostShader); // can only have 1 post effects shader
4014
+
4015
+ if (!shaderCode) // default shader
4016
+ shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture2D(iChannel0,p/iResolution.xy);}';
4017
+
4018
+ // create the shader
4019
+ glPostShader = glCreateProgram(
4020
+ 'precision highp float;'+ // use highp for better accuracy
4021
+ 'attribute vec2 p;'+ // position
4022
+ 'void main(){'+ // shader entry point
4023
+ 'gl_Position=vec4(p,1,1);'+ // set position
4024
+ '}' // end of shader
4025
+ ,
4026
+ 'precision highp float;'+ // use highp for better accuracy
4027
+ 'uniform sampler2D iChannel0;'+ // input texture
4028
+ 'uniform vec3 iResolution;'+ // size of output texture
4029
+ 'uniform float iTime;'+ // time passed
4030
+ '\n' + shaderCode + '\n'+ // insert custom shader code
4031
+ 'void main(){'+ // shader entry point
4032
+ 'mainImage(gl_FragColor,gl_FragCoord.xy);'+ // call post process function
4033
+ 'gl_FragColor.a=1.;'+ // always use full alpha
4034
+ '}' // end of shader
4035
+ );
4036
+
4037
+ // create buffer and texture
4038
+ glPostArrayBuffer = glContext.createBuffer();
4039
+ glPostTexture = glCreateTexture();
4040
+
4041
+ // hide the original 2d canvas
4042
+ mainCanvas.style.visibility = 'hidden';
4043
+ }
4044
+
4045
+ // Render the post processing shader, called automatically by the engine
4046
+ function glRenderPostProcess()
4047
+ {
4048
+ if (!glPostShader)
4049
+ return;
4050
+
4051
+ // prepare to render post process shader
4052
+ const width = mainCanvas.width, height = mainCanvas.height;
4053
+ if (glEnable)
4054
+ {
4055
+ glFlush(); // clear out the buffer
4056
+ mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
4057
+ }
4058
+ else
4059
+ glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height); // set viewport
4060
+
4061
+ // setup shader program to draw one triangle
4062
+ glContext.useProgram(glPostShader);
4063
+ glContext.disable(gl_BLEND);
4064
+ glContext.bindBuffer(gl_ARRAY_BUFFER, glPostArrayBuffer);
4065
+ glContext.bufferData(gl_ARRAY_BUFFER, new Float32Array([-3,1,1,-3,1,1]), gl_STATIC_DRAW);
4066
+ glContext.pixelStorei(gl_UNPACK_FLIP_Y_WEBGL, true);
4067
+
4068
+ // set textures, pass in the 2d canvas and gl canvas in separate texture channels
4069
+ glContext.activeTexture(gl_TEXTURE0);
4070
+ glContext.bindTexture(gl_TEXTURE_2D, glPostTexture);
4071
+ glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, mainCanvas);
4072
+
4073
+ // set vertex position attribute
4074
+ const vertexByteStride = 8;
4075
+ const pLocation = glContext.getAttribLocation(glPostShader, 'p');
4076
+ glContext.enableVertexAttribArray(pLocation);
4077
+ glContext.vertexAttribPointer(pLocation, 2, gl_FLOAT, 0, vertexByteStride, 0);
4078
+
4079
+ // set uniforms and draw
4080
+ const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
4081
+ glContext.uniform1i(uniformLocation('iChannel0'), 0);
4082
+ glContext.uniform1f(uniformLocation('iTime'), time);
4083
+ glContext.uniform3f(uniformLocation('iResolution'), width, height, 1);
4084
+ glContext.drawArrays(gl_TRIANGLES, 0, 3);
4085
+ }
4086
+
3976
4087
  ///////////////////////////////////////////////////////////////////////////////
3977
4088
  // store gl constants as integers so their name doesn't use space in minifed
3978
4089
  const
@@ -3983,6 +4094,7 @@ gl_ONE_MINUS_SRC_ALPHA = 771,
3983
4094
  gl_BLEND = 3042,
3984
4095
  gl_TEXTURE_2D = 3553,
3985
4096
  gl_UNSIGNED_BYTE = 5121,
4097
+ gl_BYTE = 5120,
3986
4098
  gl_FLOAT = 5126,
3987
4099
  gl_RGBA = 6408,
3988
4100
  gl_NEAREST = 9728,
@@ -3993,12 +4105,16 @@ gl_TEXTURE_WRAP_S = 10242,
3993
4105
  gl_TEXTURE_WRAP_T = 10243,
3994
4106
  gl_COLOR_BUFFER_BIT = 16384,
3995
4107
  gl_CLAMP_TO_EDGE = 33071,
4108
+ gl_TEXTURE0 = 33984,
4109
+ gl_TEXTURE1 = 33985,
3996
4110
  gl_ARRAY_BUFFER = 34962,
4111
+ gl_STATIC_DRAW = 35044,
3997
4112
  gl_DYNAMIC_DRAW = 35048,
3998
4113
  gl_FRAGMENT_SHADER = 35632,
3999
4114
  gl_VERTEX_SHADER = 35633,
4000
4115
  gl_COMPILE_STATUS = 35713,
4001
4116
  gl_LINK_STATUS = 35714,
4117
+ gl_UNPACK_FLIP_Y_WEBGL = 37440,
4002
4118
 
4003
4119
  // constants for batch rendering
4004
4120
  gl_VERTICES_PER_QUAD = 6,