littlejsengine 1.4.0 → 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 (41) 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 +244 -161
  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 +228 -138
  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 +16 -23
  13. package/engine/engineDraw.js +13 -13
  14. package/engine/engineExportModule.js +333 -0
  15. package/engine/engineInput.js +8 -7
  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 +1 -1
  20. package/engine/engineUtilities.js +21 -25
  21. package/engine/engineWebGL.js +114 -54
  22. package/engine/index.d.ts +41 -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/gameObjects.js +5 -5
  35. package/examples/platformer/gamePlayer.js +7 -5
  36. package/examples/platformer/index.html +6 -6
  37. package/examples/puzzle/index.html +2 -2
  38. package/examples/stress/index.html +2 -2
  39. package/game.js +9 -11
  40. package/index.html +13 -13
  41. package/package.json +36 -25
@@ -468,10 +468,10 @@ class Color
468
468
  * @return {Array} */
469
469
  getHSLA()
470
470
  {
471
- const r = this.r;
472
- const g = this.g;
473
- const b = this.b;
474
- 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);
475
475
  const max = Math.max(r, g, b);
476
476
  const min = Math.min(r, g, b);
477
477
  const l = (max + min) / 2;
@@ -507,20 +507,13 @@ class Color
507
507
  ).clamp();
508
508
  }
509
509
 
510
- /** 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
511
512
  * @return {String} */
512
- toString()
513
+ toString(useAlpha = 1)
513
514
  {
514
- 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);
515
- return `rgb(${this.r*255|0},${this.g*255|0},${this.b*255|0},${this.a})`;
516
- }
517
-
518
- /** Returns this color expressed as 32 bit integer RGBA value
519
- * @return {Number} */
520
- rgbaInt()
521
- {
522
- 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);
523
- 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) : '');
524
517
  }
525
518
 
526
519
  /** Set this color from a hex code
@@ -528,21 +521,24 @@ class Color
528
521
  * @return {Color} */
529
522
  setHex(hex)
530
523
  {
531
- const fromHex = (a)=> parseInt(hex.slice(a,a+2), 16)/255;
524
+ const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
532
525
  this.r = fromHex(1);
533
526
  this.g = fromHex(3),
534
527
  this.b = fromHex(5);
535
- this.a = 1;
536
- 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;
537
529
  return this;
538
530
  }
539
-
540
- /** Returns this color expressed as a hex code
541
- * @return {String} */
542
- getHex()
531
+
532
+ /** Returns this color expressed as 32 bit RGBA value
533
+ * @return {Number} */
534
+ rgbaInt()
543
535
  {
544
- const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
545
- 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;
546
542
  }
547
543
  }
548
544
 
@@ -845,7 +841,7 @@ let medalDisplayIconSize = 50;
845
841
  const engineName = 'LittleJS';
846
842
 
847
843
  /** Version of engine */
848
- const engineVersion = '1.4.0';
844
+ const engineVersion = '1.4.5';
849
845
 
850
846
  /** Frames per second to update objects
851
847
  * @default */
@@ -880,8 +876,7 @@ let frameTimeLastMS = 0, frameTimeBufferMS = 0, tileImageSize, tileImageFixBleed
880
876
  let averageFPS, drawCount;
881
877
 
882
878
  // css text used for elements created by engine
883
- const styleBody = 'margin:0;overflow:hidden;background:#000' +
884
- ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
879
+ const styleBody = 'margin:0;overflow:hidden;background:#000';
885
880
  const styleCanvas = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
886
881
 
887
882
  ///////////////////////////////////////////////////////////////////////////////
@@ -911,7 +906,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
911
906
 
912
907
  // init stuff and start engine
913
908
  debugInit();
914
- glInit();
909
+ glEnable && glInit();
915
910
 
916
911
  // create overlay canvas for hud to appear above gl canvas
917
912
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
@@ -931,10 +926,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
931
926
  frameTimeLastMS = frameTimeMS;
932
927
  if (debug || showWatermark)
933
928
  averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
929
+ const debugSpeedUp = debug && keyIsDown(107); // +
930
+ const debugSpeedDown = debug && keyIsDown(109); // -
934
931
  if (debug)
935
- frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
932
+ frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1; // +/- to speed/slow time
936
933
  timeReal += frameTimeDeltaMS / 1e3;
937
- 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
938
937
 
939
938
  if (canvasFixedSize.x)
940
939
  {
@@ -1007,10 +1006,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
1007
1006
  for (const o of engineObjects)
1008
1007
  o.destroyed || o.render();
1009
1008
  gameRenderPost();
1009
+ glRenderPostProcess();
1010
1010
  medalsRender();
1011
1011
  touchGamepadRender();
1012
1012
  debugRender();
1013
- glCopyToContext(mainContext);
1013
+ glEnable && glCopyToContext(mainContext);
1014
1014
 
1015
1015
  if (showWatermark)
1016
1016
  {
@@ -1045,7 +1045,7 @@ function enginePreRender()
1045
1045
  mainContext.imageSmoothingEnabled = !cavasPixelated;
1046
1046
 
1047
1047
  // setup gl rendering if enabled
1048
- glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
1048
+ glEnable && glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
1049
1049
  }
1050
1050
 
1051
1051
  ///////////////////////////////////////////////////////////////////////////////
@@ -1406,7 +1406,7 @@ class EngineObject
1406
1406
  * @return {Boolean} - true if the raycast should hit */
1407
1407
  collideWithTileRaycast(tileData, pos) { return tileData > 0; }
1408
1408
 
1409
- /** Called to check if a tile raycast hit
1409
+ /** Called to check if a object collision should be resolved
1410
1410
  * @param {EngineObject} object - the object to test against
1411
1411
  * @return {Boolean} - true if the collision should be resolved
1412
1412
  */
@@ -1451,10 +1451,10 @@ class EngineObject
1451
1451
  }
1452
1452
 
1453
1453
  /** Set how this object collides
1454
- * @param {boolean} [collideSolidObjects=0] - Does it collide with solid objects
1455
- * @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)
1456
1456
  * @param {boolean} [collideTiles=1] - Does it collide with the tile collision */
1457
- setCollision(collideSolidObjects=0, isSolid=0, collideTiles=1)
1457
+ setCollision(collideSolidObjects=1, isSolid=1, collideTiles=1)
1458
1458
  {
1459
1459
  ASSERT(collideSolidObjects || !isSolid); // solid objects must be set to collide
1460
1460
 
@@ -1717,21 +1717,21 @@ function setBlendMode(additive, useWebGL=glEnable)
1717
1717
  * @param {Color} [lineColor=new Color(0,0,0)]
1718
1718
  * @param {String} [textAlign='center']
1719
1719
  * @memberof Draw */
1720
- 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)
1721
1721
  {
1722
- overlayContext.fillStyle = color;
1723
- overlayContext.lineWidth = lineWidth;
1724
- overlayContext.strokeStyle = lineColor;
1725
- overlayContext.textAlign = textAlign;
1726
- overlayContext.font = size + 'px '+ font;
1727
- overlayContext.textBaseline = 'middle';
1728
- 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';
1729
1729
 
1730
1730
  pos = pos.copy();
1731
1731
  (text+'').split('\n').forEach(line=>
1732
1732
  {
1733
- lineWidth && overlayContext.strokeText(line, pos.x, pos.y);
1734
- overlayContext.fillText(line, pos.x, pos.y);
1733
+ lineWidth && context.strokeText(line, pos.x, pos.y);
1734
+ context.fillText(line, pos.x, pos.y);
1735
1735
  pos.y += size;
1736
1736
  });
1737
1737
  }
@@ -1748,7 +1748,7 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
1748
1748
  * @memberof Draw */
1749
1749
  function drawText(text, pos, size=1, color, lineWidth, lineColor, textAlign, font)
1750
1750
  {
1751
- 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);
1752
1752
  }
1753
1753
 
1754
1754
  ///////////////////////////////////////////////////////////////////////////////
@@ -1771,7 +1771,7 @@ let engineFontImage;
1771
1771
  class FontImage
1772
1772
  {
1773
1773
  /** Create an image font
1774
- * @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
1775
1775
  * @param {Vector2} [tileSize=vec2(8)] - The size of the font source tiles
1776
1776
  * @param {Vector2} [paddingSize=vec2(0,1)] - How much extra space to add between characters
1777
1777
  * @param {Number} [startTileIndex=0] - Tile index in image where font starts
@@ -1870,6 +1870,7 @@ function toggleFullscreen()
1870
1870
  document.body.mozRequestFullScreen();
1871
1871
  }
1872
1872
  }
1873
+
1873
1874
  /**
1874
1875
  * LittleJS Input System
1875
1876
  * <br> - Tracks key down, pressed, and released
@@ -1984,7 +1985,7 @@ let inputData = [[]];
1984
1985
  function inputUpdate()
1985
1986
  {
1986
1987
  // clear input when lost focus (prevent stuck keys)
1987
- document.hasFocus() || clearInput();
1988
+ isTouchDevice || document.hasFocus() || clearInput();
1988
1989
 
1989
1990
  // update mouse world space position
1990
1991
  mousePos = screenToWorld(mousePosScreen);
@@ -2059,7 +2060,7 @@ function gamepadsUpdate()
2059
2060
  }
2060
2061
  }
2061
2062
 
2062
- if (!gamepadsEnable || !navigator.getGamepads || !document.hasFocus() && !debug)
2063
+ if (!gamepadsEnable || !navigator || !navigator.getGamepads || !document.hasFocus() && !debug)
2063
2064
  return;
2064
2065
 
2065
2066
  // poll gamepads
@@ -2108,7 +2109,7 @@ function gamepadsUpdate()
2108
2109
  /** Pulse the vibration hardware if it exists
2109
2110
  * @param {Number} [pattern=100] - a single value in miliseconds or vibration interval array
2110
2111
  * @memberof Input */
2111
- const vibrate = (pattern)=> vibrateEnable && Navigator.vibrate && Navigator.vibrate(pattern);
2112
+ const vibrate = (pattern)=> vibrateEnable && navigator && navigator.vibrate && navigator.vibrate(pattern);
2112
2113
 
2113
2114
  /** Cancel any ongoing vibration
2114
2115
  * @memberof Input */
@@ -2126,7 +2127,7 @@ const isTouchDevice = window.ontouchstart !== undefined;
2126
2127
  if (isTouchDevice)
2127
2128
  {
2128
2129
  // handle all touch events the same way
2129
- let wasTouching, hadTouchInput;
2130
+ let wasTouching, hadTouch;
2130
2131
  ontouchstart = ontouchmove = ontouchend = (e)=>
2131
2132
  {
2132
2133
  e.button = 0; // all touches are left click
@@ -2135,7 +2136,8 @@ if (isTouchDevice)
2135
2136
  const touching = e.touches.length;
2136
2137
  if (touching)
2137
2138
  {
2138
- 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);
2139
2141
 
2140
2142
  // set event pos and pass it along
2141
2143
  e.x = e.touches[0].clientX;
@@ -2148,8 +2150,8 @@ if (isTouchDevice)
2148
2150
  // set was touching
2149
2151
  wasTouching = touching;
2150
2152
 
2151
- // prevent normal mouse events from being called
2152
- return !e.cancelable;
2153
+ // must return true so the document will get focus
2154
+ return true;
2153
2155
  }
2154
2156
  }
2155
2157
 
@@ -3006,7 +3008,7 @@ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1)
3006
3008
  redrawEnd()
3007
3009
  {
3008
3010
  ASSERT(mainContext == this.context); // must call redrawStart() before drawing tiles
3009
- glCopyToContext(mainContext, 1);
3011
+ glEnable && glCopyToContext(mainContext, 1);
3010
3012
  //debugSaveCanvas(this.canvas);
3011
3013
 
3012
3014
  // set stuff back to normal
@@ -3150,6 +3152,7 @@ class ParticleEmitter extends EngineObject
3150
3152
  * @param {Boolean} [additive=0] - Should particles use addtive blend
3151
3153
  * @param {Boolean} [randomColorLinear=1] - Should color be randomized linearly or across each component
3152
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)
3153
3156
  */
3154
3157
  constructor
3155
3158
  (
@@ -3179,7 +3182,8 @@ class ParticleEmitter extends EngineObject
3179
3182
  collideTiles,
3180
3183
  additive,
3181
3184
  randomColorLinear = 1,
3182
- renderOrder = additive ? 1e9 : 0
3185
+ renderOrder = additive ? 1e9 : 0,
3186
+ localSpace
3183
3187
  )
3184
3188
  {
3185
3189
  super(pos, new Vector2, tileIndex, tileSize, angle, undefined, renderOrder);
@@ -3233,6 +3237,8 @@ class ParticleEmitter extends EngineObject
3233
3237
  this.collideTiles = collideTiles;
3234
3238
  /** @property {Number} - Should particles use addtive blend */
3235
3239
  this.additive = additive;
3240
+ /** @property {Boolean} - Should it be in local space of emitter */
3241
+ this.localSpace = localSpace;
3236
3242
  /** @property {Number} - If set the partile is drawn as a trail, stretched in the drection of velocity */
3237
3243
  this.trailScale = 0;
3238
3244
 
@@ -3268,30 +3274,38 @@ class ParticleEmitter extends EngineObject
3268
3274
  emitParticle()
3269
3275
  {
3270
3276
  // spawn a particle
3271
- const pos = this.emitSize.x != undefined ? // check if vec2 was used for size
3272
- (new Vector2(rand(-.5,.5), rand(-.5,.5))).multiply(this.emitSize).rotate(this.angle) // box emitter
3273
- : randInCircle(this.emitSize * .5); // circle emitter
3274
- const particle = new Particle(this.pos.add(pos), this.tileIndex, this.tileSize,
3275
- 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);
3276
3289
 
3277
3290
  // randomness scales each paremeter by a percentage
3278
3291
  const randomness = this.randomness;
3279
3292
  const randomizeScale = (v)=> v + v*rand(randomness, -randomness);
3280
3293
 
3281
3294
  // randomize particle settings
3282
- const particleTime = randomizeScale(this.particleTime);
3283
- const sizeStart = randomizeScale(this.sizeStart);
3284
- const sizeEnd = randomizeScale(this.sizeEnd);
3285
- const speed = randomizeScale(this.speed);
3286
- const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
3287
- const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
3288
- const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
3289
- 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;
3290
3304
 
3291
3305
  // build particle settings
3292
3306
  particle.colorStart = colorStart;
3293
3307
  particle.colorEndDelta = colorEnd.subtract(colorStart);
3294
- particle.velocity = (new Vector2).setAngle(this.angle + coneAngle, speed);
3308
+ particle.velocity = (new Vector2).setAngle(velocityAngle, speed);
3295
3309
  particle.angleVelocity = angleSpeed;
3296
3310
  particle.lifeTime = particleTime;
3297
3311
  particle.sizeStart = sizeStart;
@@ -3307,6 +3321,7 @@ class ParticleEmitter extends EngineObject
3307
3321
  particle.renderOrder = this.renderOrder;
3308
3322
  particle.trailScale = this.trailScale;
3309
3323
  particle.mirror = rand()<.5;
3324
+ particle.localSpaceEmitter = this.localSpace && this;
3310
3325
 
3311
3326
  // setup callbacks for particles
3312
3327
  particle.destroyCallback = this.particleDestroyCallback;
@@ -3353,20 +3368,31 @@ class Particle extends EngineObject
3353
3368
 
3354
3369
  // draw the particle
3355
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
+ }
3356
3379
  if (this.trailScale)
3357
3380
  {
3358
3381
  // trail style particles
3359
- const speed = this.velocity.length();
3360
- 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);
3361
3387
  const trailLength = speed * this.trailScale;
3362
3388
  size.y = max(size.x, trailLength);
3363
- this.angle = direction.angle();
3364
- 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);
3365
3391
  }
3366
3392
  else
3367
- 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);
3368
3394
  this.additive && setBlendMode();
3369
- debugParticles && debugRect(this.pos, size, '#f005', 0, this.angle);
3395
+ debugParticles && debugRect(pos, size, '#f005', 0, angle);
3370
3396
 
3371
3397
  if (p == 1)
3372
3398
  {
@@ -3396,11 +3422,6 @@ const medals = [];
3396
3422
  * @memberof Medals */
3397
3423
  let medalsPreventUnlock;
3398
3424
 
3399
- /** This can used to enable Newgrounds functionality
3400
- * @type {Newgrounds}
3401
- * @memberof Medals */
3402
- let newgrounds;
3403
-
3404
3425
  // Engine internal variables not exposed to documentation
3405
3426
  let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
3406
3427
 
@@ -3415,7 +3436,7 @@ function medalsInit(saveName)
3415
3436
  {
3416
3437
  // check if medals are unlocked
3417
3438
  medalsSaveName = saveName;
3418
- debugMedals || medals.forEach(medal=> localStorage[medal.storageKey()]);
3439
+ debugMedals || medals.forEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
3419
3440
  }
3420
3441
 
3421
3442
  /**
@@ -3548,6 +3569,15 @@ function medalsRender()
3548
3569
 
3549
3570
  ///////////////////////////////////////////////////////////////////////////////
3550
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
+
3551
3581
  /**
3552
3582
  * Newgrounds API wrapper object
3553
3583
  * @example
@@ -3708,15 +3738,13 @@ let glContext;
3708
3738
  let glTileTexture;
3709
3739
 
3710
3740
  // WebGL internal variables not exposed to documentation
3711
- let glActiveTexture, glShader, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3741
+ let glActiveTexture, glShader, glArrayBuffer, glVertexData, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3712
3742
 
3713
3743
  ///////////////////////////////////////////////////////////////////////////////
3714
3744
 
3715
3745
  // Init WebGL, called automatically by the engine
3716
3746
  function glInit()
3717
3747
  {
3718
- if (!glEnable) return;
3719
-
3720
3748
  // create the canvas and tile texture
3721
3749
  glCanvas = document.createElement('canvas');
3722
3750
  glContext = glCanvas.getContext('webgl', {antialias: false});
@@ -3749,24 +3777,11 @@ function glInit()
3749
3777
  );
3750
3778
 
3751
3779
  // init buffers
3752
- const glVertexData = new ArrayBuffer(gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE);
3753
- 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();
3754
3782
  glPositionData = new Float32Array(glVertexData);
3755
3783
  glColorData = new Uint32Array(glVertexData);
3756
-
3757
- // setup the vertex data array
3758
- let offset = glBatchCount = 0;
3759
- const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
3760
- {
3761
- const location = glContext.getAttribLocation(glShader, name);
3762
- glContext.enableVertexAttribArray(location);
3763
- glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3764
- offset += size*typeSize;
3765
- }
3766
- initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3767
- initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
3768
- initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
3769
- initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3784
+ glBatchCount = 0;
3770
3785
  }
3771
3786
 
3772
3787
  /** Set the WebGl blend mode, normally you should call setBlendMode instead
@@ -3774,8 +3789,6 @@ function glInit()
3774
3789
  * @memberof WebGL */
3775
3790
  function glSetBlendMode(additive)
3776
3791
  {
3777
- if (!glEnable) return;
3778
-
3779
3792
  // setup blending
3780
3793
  glAdditive = additive;
3781
3794
  }
@@ -3786,8 +3799,6 @@ function glSetBlendMode(additive)
3786
3799
  * @memberof WebGL */
3787
3800
  function glSetTexture(texture=glTileTexture)
3788
3801
  {
3789
- if (!glEnable) return;
3790
-
3791
3802
  // must flush cache with the old texture to set a new one
3792
3803
  if (texture != glActiveTexture)
3793
3804
  glFlush();
@@ -3802,8 +3813,6 @@ function glSetTexture(texture=glTileTexture)
3802
3813
  * @memberof WebGL */
3803
3814
  function glCompileShader(source, type)
3804
3815
  {
3805
- if (!glEnable) return;
3806
-
3807
3816
  // build the shader
3808
3817
  const shader = glContext.createShader(type);
3809
3818
  glContext.shaderSource(shader, source);
@@ -3822,8 +3831,6 @@ function glCompileShader(source, type)
3822
3831
  * @memberof WebGL */
3823
3832
  function glCreateProgram(vsSource, fsSource)
3824
3833
  {
3825
- if (!glEnable) return;
3826
-
3827
3834
  // build the program
3828
3835
  const program = glContext.createProgram();
3829
3836
  glContext.attachShader(program, glCompileShader(vsSource, gl_VERTEX_SHADER));
@@ -3836,35 +3843,16 @@ function glCreateProgram(vsSource, fsSource)
3836
3843
  return program;
3837
3844
  }
3838
3845
 
3839
- /** Create WebGL buffer
3840
- * @param bufferType
3841
- * @param size
3842
- * @param usage
3843
- * @return {WebGLBuffer}
3844
- * @memberof WebGL */
3845
- function glCreateBuffer(bufferType, size, usage)
3846
- {
3847
- if (!glEnable) return;
3848
-
3849
- // build the buffer
3850
- const buffer = glContext.createBuffer();
3851
- glContext.bindBuffer(bufferType, buffer);
3852
- glContext.bufferData(bufferType, size, usage);
3853
- return buffer;
3854
- }
3855
-
3856
3846
  /** Create WebGL texture from an image and set the texture settings
3857
3847
  * @param {Image} image
3858
3848
  * @return {WebGLTexture}
3859
3849
  * @memberof WebGL */
3860
3850
  function glCreateTexture(image)
3861
3851
  {
3862
- if (!glEnable || !image || !image.width) return;
3863
-
3864
3852
  // build the texture
3865
3853
  const texture = glContext.createTexture();
3866
3854
  glContext.bindTexture(gl_TEXTURE_2D, texture);
3867
- 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);
3868
3856
 
3869
3857
  // use point filtering for pixelated rendering
3870
3858
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, cavasPixelated ? gl_NEAREST : gl_LINEAR);
@@ -3877,16 +3865,31 @@ function glCreateTexture(image)
3877
3865
  // called automatically by engine before render
3878
3866
  function glPreRender(width, height, cameraX, cameraY, cameraScale)
3879
3867
  {
3880
- if (!glEnable) return;
3881
-
3882
3868
  // clear and set to same size as main canvas
3883
3869
  glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height);
3884
3870
  glContext.clear(gl_COLOR_BUFFER_BIT);
3885
3871
 
3886
3872
  // set up the shader
3887
- glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
3888
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);
3889
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
3890
3893
 
3891
3894
  // build the transform matrix
3892
3895
  const sx = 2 * cameraScale / width;
@@ -3905,7 +3908,7 @@ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3905
3908
  * @memberof WebGL */
3906
3909
  function glFlush()
3907
3910
  {
3908
- if (!glEnable || !glBatchCount) return;
3911
+ if (!glBatchCount) return;
3909
3912
 
3910
3913
  const destBlend = glBatchAdditive ? gl_ONE : gl_ONE_MINUS_SRC_ALPHA;
3911
3914
  glContext.blendFuncSeparate(gl_SRC_ALPHA, destBlend, gl_ONE, destBlend);
@@ -3925,7 +3928,7 @@ function glFlush()
3925
3928
  * @memberof WebGL */
3926
3929
  function glCopyToContext(context, forceDraw)
3927
3930
  {
3928
- if (!glEnable || !glBatchCount && !forceDraw) return;
3931
+ if (!glBatchCount && !forceDraw) return;
3929
3932
 
3930
3933
  glFlush();
3931
3934
 
@@ -3949,8 +3952,6 @@ function glCopyToContext(context, forceDraw)
3949
3952
  * @memberof WebGL */
3950
3953
  function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffffff, rgbaAdditive=0)
3951
3954
  {
3952
- if (!glEnable) return;
3953
-
3954
3955
  // flush if there is no room for more verts or if different blend mode
3955
3956
  if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
3956
3957
  glFlush();
@@ -3999,6 +4000,90 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffff
3999
4000
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
4000
4001
  }
4001
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
+
4002
4087
  ///////////////////////////////////////////////////////////////////////////////
4003
4088
  // store gl constants as integers so their name doesn't use space in minifed
4004
4089
  const
@@ -4009,6 +4094,7 @@ gl_ONE_MINUS_SRC_ALPHA = 771,
4009
4094
  gl_BLEND = 3042,
4010
4095
  gl_TEXTURE_2D = 3553,
4011
4096
  gl_UNSIGNED_BYTE = 5121,
4097
+ gl_BYTE = 5120,
4012
4098
  gl_FLOAT = 5126,
4013
4099
  gl_RGBA = 6408,
4014
4100
  gl_NEAREST = 9728,
@@ -4019,12 +4105,16 @@ gl_TEXTURE_WRAP_S = 10242,
4019
4105
  gl_TEXTURE_WRAP_T = 10243,
4020
4106
  gl_COLOR_BUFFER_BIT = 16384,
4021
4107
  gl_CLAMP_TO_EDGE = 33071,
4108
+ gl_TEXTURE0 = 33984,
4109
+ gl_TEXTURE1 = 33985,
4022
4110
  gl_ARRAY_BUFFER = 34962,
4111
+ gl_STATIC_DRAW = 35044,
4023
4112
  gl_DYNAMIC_DRAW = 35048,
4024
4113
  gl_FRAGMENT_SHADER = 35632,
4025
4114
  gl_VERTEX_SHADER = 35633,
4026
4115
  gl_COMPILE_STATUS = 35713,
4027
4116
  gl_LINK_STATUS = 35714,
4117
+ gl_UNPACK_FLIP_Y_WEBGL = 37440,
4028
4118
 
4029
4119
  // constants for batch rendering
4030
4120
  gl_VERTICES_PER_QUAD = 6,