littlejsengine 1.4.0 → 1.4.6

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 (40) hide show
  1. package/build.bat +1 -1
  2. package/buildSetup.bat +2 -2
  3. package/engine/engine.all.js +244 -161
  4. package/engine/engine.all.min.js +1 -1
  5. package/engine/engine.all.module.js +4812 -0
  6. package/engine/engine.all.release.js +228 -138
  7. package/engine/engine.all.release.module.js +4457 -0
  8. package/engine/engine.js +12 -8
  9. package/engine/engineBuild.bat +21 -2
  10. package/engine/engineDebug.js +16 -23
  11. package/engine/engineDraw.js +13 -13
  12. package/engine/engineExport.js +333 -0
  13. package/engine/engineInput.js +8 -7
  14. package/engine/engineMedals.js +10 -6
  15. package/engine/engineObject.js +4 -4
  16. package/engine/engineParticles.js +45 -21
  17. package/engine/engineTileLayer.js +1 -1
  18. package/engine/engineUtilities.js +21 -25
  19. package/engine/engineWebGL.js +114 -54
  20. package/engine/index.d.ts +41 -30
  21. package/examples/breakout/game.js +63 -11
  22. package/examples/breakout/gameObjects.js +14 -12
  23. package/examples/breakout/index.html +3 -3
  24. package/examples/empty/game.js +35 -0
  25. package/examples/empty/index.html +9 -0
  26. package/examples/empty/tiles.png +0 -0
  27. package/examples/module/game.js +122 -0
  28. package/examples/module/index.html +9 -0
  29. package/examples/module/tiles.png +0 -0
  30. package/examples/particles/index.html +14 -14
  31. package/examples/particles/tiles.png +0 -0
  32. package/examples/platformer/gameObjects.js +5 -5
  33. package/examples/platformer/gamePlayer.js +7 -5
  34. package/examples/platformer/index.html +6 -6
  35. package/examples/puzzle/index.html +2 -2
  36. package/examples/stress/index.html +2 -2
  37. package/game.js +9 -11
  38. package/index.html +13 -13
  39. package/package.json +35 -25
  40. package/README.md +0 -54
@@ -233,55 +233,49 @@ const debugRender = ()=>
233
233
 
234
234
  if (debugOverlay)
235
235
  {
236
+ const saveContext = mainContext;
237
+ mainContext = overlayContext;
238
+
236
239
  // mouse pick
237
240
  let bestDistance = Infinity, bestObject;
238
241
  for (const o of engineObjects)
239
242
  {
240
243
  if (o.canvas || o.destroyed)
241
- continue; // skip tile layers
242
-
243
- const size = o.size.copy();
244
- if (!size.x || !size.y)
244
+ continue;
245
+ if (!o.size.x || !o.size.y)
245
246
  continue;
246
247
 
247
248
  const distance = mousePos.distanceSquared(o.pos);
248
249
  if (distance < bestDistance)
249
250
  {
250
251
  bestDistance = distance;
251
- bestObject = o
252
+ bestObject = o;
252
253
  }
253
254
 
254
255
  // show object info
255
- const color = new Color(
256
- o.collideTiles?1:0,
257
- o.collideSolidObjects?1:0,
258
- o.isSolid?1:0,
259
- o.parent ? .2 : .5);
260
- size.x = max(size.x, .2);
261
- size.y = max(size.y, .2);
262
- drawRect(o.pos, size, color);
263
- drawRect(o.pos, size.scale(.8), o.parent ? new Color(1,1,1,.5) : new Color(0,0,0,.8));
264
- o.parent && drawLine(o.pos, o.parent.pos, .1, new Color(0,0,1,.5));
256
+ const size = vec2(max(o.size.x, .2), max(o.size.y, .2));
257
+ const color1 = new Color(!!o.collideTiles, !!o.collideSolidObjects, !!o.isSolid, o.parent?.2:.5);
258
+ const color2 = o.parent ? new Color(1,1,1,.5) : new Color(0,0,0,.8);
259
+ drawRect(o.pos, size, color1, o.angle, 0);
260
+ drawRect(o.pos, size.scale(.8), color2, o.angle, 0);
261
+ o.parent && drawLine(o.pos, o.parent.pos, .1, new Color(0,0,1,.5), 0);
265
262
  }
266
263
 
267
264
  if (bestObject)
268
265
  {
269
- const saveContext = mainContext;
270
- mainContext = overlayContext
271
266
  const raycastHitPos = tileCollisionRaycast(bestObject.pos, mousePos);
272
267
  raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), new Color(0,1,1,.3));
273
- drawRect(mousePos.floor().add(vec2(.5)), vec2(1), new Color(0,0,1,.5));
274
- drawLine(mousePos, bestObject.pos, .1, !raycastHitPos ? new Color(0,1,0,.5) : new Color(1,0,0,.5));
268
+ drawRect(mousePos.floor().add(vec2(.5)), vec2(1), new Color(0,0,1,.5), 0, 0);
269
+ drawLine(mousePos, bestObject.pos, .1, raycastHitPos ? new Color(1,0,0,.5) : new Color(0,1,0,.5), 0);
275
270
 
276
271
  const debugText = 'mouse pos = ' + mousePos +
277
272
  '\nmouse collision = ' + getTileCollisionData(mousePos) +
278
273
  '\n\n--- object info ---\n' +
279
274
  bestObject.toString();
280
- drawTextScreen(debugText, mousePosScreen, 24, new Color, .05, undefined, undefined, 'monospace');
281
- mainContext = saveContext;
275
+ drawTextScreen(debugText, mousePosScreen, 24, new Color, .05, 0, 0, 'monospace');
282
276
  }
283
277
 
284
- glCopyToContext(mainContext);
278
+ glCopyToContext(mainContext = saveContext);
285
279
  }
286
280
 
287
281
  {
@@ -294,7 +288,6 @@ const debugRender = ()=>
294
288
 
295
289
  // create canvas transform from world space to screen space
296
290
  const pos = worldToScreen(p.pos);
297
-
298
291
  overlayContext.translate(pos.x|0, pos.y|0);
299
292
  overlayContext.rotate(p.angle);
300
293
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
@@ -830,10 +823,10 @@ class Color
830
823
  * @return {Array} */
831
824
  getHSLA()
832
825
  {
833
- const r = this.r;
834
- const g = this.g;
835
- const b = this.b;
836
- const a = this.a;
826
+ const r = clamp(this.r);
827
+ const g = clamp(this.g);
828
+ const b = clamp(this.b);
829
+ const a = clamp(this.a);
837
830
  const max = Math.max(r, g, b);
838
831
  const min = Math.min(r, g, b);
839
832
  const l = (max + min) / 2;
@@ -869,20 +862,13 @@ class Color
869
862
  ).clamp();
870
863
  }
871
864
 
872
- /** Returns this color expressed as an CSS color value
865
+ /** Returns this color expressed as a hex color code
866
+ * @param {Boolean} [useAlpha=1] - if alpha should be included in result
873
867
  * @return {String} */
874
- toString()
868
+ toString(useAlpha = 1)
875
869
  {
876
- 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);
877
- return `rgb(${this.r*255|0},${this.g*255|0},${this.b*255|0},${this.a})`;
878
- }
879
-
880
- /** Returns this color expressed as 32 bit integer RGBA value
881
- * @return {Number} */
882
- rgbaInt()
883
- {
884
- 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);
885
- return (this.r*255|0) + (this.g*255<<8) + (this.b*255<<16) + (this.a*255<<24);
870
+ const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
871
+ return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
886
872
  }
887
873
 
888
874
  /** Set this color from a hex code
@@ -890,21 +876,24 @@ class Color
890
876
  * @return {Color} */
891
877
  setHex(hex)
892
878
  {
893
- const fromHex = (a)=> parseInt(hex.slice(a,a+2), 16)/255;
879
+ const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
894
880
  this.r = fromHex(1);
895
881
  this.g = fromHex(3),
896
882
  this.b = fromHex(5);
897
- this.a = 1;
898
- ASSERT(this.r>=0 && this.r<=1 && this.g>=0 && this.g<=1 && this.b>=0 && this.b<=1);
883
+ this.a = hex.length > 7 ? fromHex(7) : 1;
899
884
  return this;
900
885
  }
901
-
902
- /** Returns this color expressed as a hex code
903
- * @return {String} */
904
- getHex()
886
+
887
+ /** Returns this color expressed as 32 bit RGBA value
888
+ * @return {Number} */
889
+ rgbaInt()
905
890
  {
906
- const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
907
- return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b);
891
+ const toByte = (c)=> clamp(c)*255|0;
892
+ const r = toByte(this.r);
893
+ const g = toByte(this.g)<<8;
894
+ const b = toByte(this.b)<<16;
895
+ const a = toByte(this.a)<<24;
896
+ return r + g + b + a;
908
897
  }
909
898
  }
910
899
 
@@ -1481,7 +1470,7 @@ class EngineObject
1481
1470
  * @return {Boolean} - true if the raycast should hit */
1482
1471
  collideWithTileRaycast(tileData, pos) { return tileData > 0; }
1483
1472
 
1484
- /** Called to check if a tile raycast hit
1473
+ /** Called to check if a object collision should be resolved
1485
1474
  * @param {EngineObject} object - the object to test against
1486
1475
  * @return {Boolean} - true if the collision should be resolved
1487
1476
  */
@@ -1526,10 +1515,10 @@ class EngineObject
1526
1515
  }
1527
1516
 
1528
1517
  /** Set how this object collides
1529
- * @param {boolean} [collideSolidObjects=0] - Does it collide with solid objects
1530
- * @param {boolean} [isSolid=0] - Does it collide with and block other objects (expensive in large numbers)
1518
+ * @param {boolean} [collideSolidObjects=1] - Does it collide with solid objects
1519
+ * @param {boolean} [isSolid=1] - Does it collide with and block other objects (expensive in large numbers)
1531
1520
  * @param {boolean} [collideTiles=1] - Does it collide with the tile collision */
1532
- setCollision(collideSolidObjects=0, isSolid=0, collideTiles=1)
1521
+ setCollision(collideSolidObjects=1, isSolid=1, collideTiles=1)
1533
1522
  {
1534
1523
  ASSERT(collideSolidObjects || !isSolid); // solid objects must be set to collide
1535
1524
 
@@ -1792,21 +1781,21 @@ function setBlendMode(additive, useWebGL=glEnable)
1792
1781
  * @param {Color} [lineColor=new Color(0,0,0)]
1793
1782
  * @param {String} [textAlign='center']
1794
1783
  * @memberof Draw */
1795
- function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault)
1784
+ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
1796
1785
  {
1797
- overlayContext.fillStyle = color;
1798
- overlayContext.lineWidth = lineWidth;
1799
- overlayContext.strokeStyle = lineColor;
1800
- overlayContext.textAlign = textAlign;
1801
- overlayContext.font = size + 'px '+ font;
1802
- overlayContext.textBaseline = 'middle';
1803
- overlayContext.lineJoin = 'round';
1786
+ context.fillStyle = color;
1787
+ context.lineWidth = lineWidth;
1788
+ context.strokeStyle = lineColor;
1789
+ context.textAlign = textAlign;
1790
+ context.font = size + 'px '+ font;
1791
+ context.textBaseline = 'middle';
1792
+ context.lineJoin = 'round';
1804
1793
 
1805
1794
  pos = pos.copy();
1806
1795
  (text+'').split('\n').forEach(line=>
1807
1796
  {
1808
- lineWidth && overlayContext.strokeText(line, pos.x, pos.y);
1809
- overlayContext.fillText(line, pos.x, pos.y);
1797
+ lineWidth && context.strokeText(line, pos.x, pos.y);
1798
+ context.fillText(line, pos.x, pos.y);
1810
1799
  pos.y += size;
1811
1800
  });
1812
1801
  }
@@ -1823,7 +1812,7 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
1823
1812
  * @memberof Draw */
1824
1813
  function drawText(text, pos, size=1, color, lineWidth, lineColor, textAlign, font)
1825
1814
  {
1826
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font);
1815
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, mainContext);
1827
1816
  }
1828
1817
 
1829
1818
  ///////////////////////////////////////////////////////////////////////////////
@@ -1846,7 +1835,7 @@ let engineFontImage;
1846
1835
  class FontImage
1847
1836
  {
1848
1837
  /** Create an image font
1849
- * @param {Image} [image] - The image the font is stored in, if undefined the default font is used
1838
+ * @param {HTMLImageElement} [image] - The image the font is stored in, if undefined the default font is used
1850
1839
  * @param {Vector2} [tileSize=vec2(8)] - The size of the font source tiles
1851
1840
  * @param {Vector2} [paddingSize=vec2(0,1)] - How much extra space to add between characters
1852
1841
  * @param {Number} [startTileIndex=0] - Tile index in image where font starts
@@ -1945,6 +1934,7 @@ function toggleFullscreen()
1945
1934
  document.body.mozRequestFullScreen();
1946
1935
  }
1947
1936
  }
1937
+
1948
1938
  /**
1949
1939
  * LittleJS Input System
1950
1940
  * <br> - Tracks key down, pressed, and released
@@ -2059,7 +2049,7 @@ let inputData = [[]];
2059
2049
  function inputUpdate()
2060
2050
  {
2061
2051
  // clear input when lost focus (prevent stuck keys)
2062
- document.hasFocus() || clearInput();
2052
+ isTouchDevice || document.hasFocus() || clearInput();
2063
2053
 
2064
2054
  // update mouse world space position
2065
2055
  mousePos = screenToWorld(mousePosScreen);
@@ -2134,7 +2124,7 @@ function gamepadsUpdate()
2134
2124
  }
2135
2125
  }
2136
2126
 
2137
- if (!gamepadsEnable || !navigator.getGamepads || !document.hasFocus() && !debug)
2127
+ if (!gamepadsEnable || !navigator || !navigator.getGamepads || !document.hasFocus() && !debug)
2138
2128
  return;
2139
2129
 
2140
2130
  // poll gamepads
@@ -2183,7 +2173,7 @@ function gamepadsUpdate()
2183
2173
  /** Pulse the vibration hardware if it exists
2184
2174
  * @param {Number} [pattern=100] - a single value in miliseconds or vibration interval array
2185
2175
  * @memberof Input */
2186
- const vibrate = (pattern)=> vibrateEnable && Navigator.vibrate && Navigator.vibrate(pattern);
2176
+ const vibrate = (pattern)=> vibrateEnable && navigator && navigator.vibrate && navigator.vibrate(pattern);
2187
2177
 
2188
2178
  /** Cancel any ongoing vibration
2189
2179
  * @memberof Input */
@@ -2201,7 +2191,7 @@ const isTouchDevice = window.ontouchstart !== undefined;
2201
2191
  if (isTouchDevice)
2202
2192
  {
2203
2193
  // handle all touch events the same way
2204
- let wasTouching, hadTouchInput;
2194
+ let wasTouching, hadTouch;
2205
2195
  ontouchstart = ontouchmove = ontouchend = (e)=>
2206
2196
  {
2207
2197
  e.button = 0; // all touches are left click
@@ -2210,7 +2200,8 @@ if (isTouchDevice)
2210
2200
  const touching = e.touches.length;
2211
2201
  if (touching)
2212
2202
  {
2213
- hadTouchInput || zzfx(0, hadTouchInput=1) ; // fix mobile audio, force it to play a sound the first time
2203
+ // fix mobile audio, force it to play a sound on first touch
2204
+ hadTouch || zzfx(0, hadTouch=1);
2214
2205
 
2215
2206
  // set event pos and pass it along
2216
2207
  e.x = e.touches[0].clientX;
@@ -2223,8 +2214,8 @@ if (isTouchDevice)
2223
2214
  // set was touching
2224
2215
  wasTouching = touching;
2225
2216
 
2226
- // prevent normal mouse events from being called
2227
- return !e.cancelable;
2217
+ // must return true so the document will get focus
2218
+ return true;
2228
2219
  }
2229
2220
  }
2230
2221
 
@@ -3081,7 +3072,7 @@ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1)
3081
3072
  redrawEnd()
3082
3073
  {
3083
3074
  ASSERT(mainContext == this.context); // must call redrawStart() before drawing tiles
3084
- glCopyToContext(mainContext, 1);
3075
+ glEnable && glCopyToContext(mainContext, 1);
3085
3076
  //debugSaveCanvas(this.canvas);
3086
3077
 
3087
3078
  // set stuff back to normal
@@ -3225,6 +3216,7 @@ class ParticleEmitter extends EngineObject
3225
3216
  * @param {Boolean} [additive=0] - Should particles use addtive blend
3226
3217
  * @param {Boolean} [randomColorLinear=1] - Should color be randomized linearly or across each component
3227
3218
  * @param {Number} [renderOrder=0] - Render order for particles (additive is above other stuff by default)
3219
+ * @param {Boolean} [localSpace=0] - Should it be in local space of emitter (world space is default)
3228
3220
  */
3229
3221
  constructor
3230
3222
  (
@@ -3254,7 +3246,8 @@ class ParticleEmitter extends EngineObject
3254
3246
  collideTiles,
3255
3247
  additive,
3256
3248
  randomColorLinear = 1,
3257
- renderOrder = additive ? 1e9 : 0
3249
+ renderOrder = additive ? 1e9 : 0,
3250
+ localSpace
3258
3251
  )
3259
3252
  {
3260
3253
  super(pos, new Vector2, tileIndex, tileSize, angle, undefined, renderOrder);
@@ -3308,6 +3301,8 @@ class ParticleEmitter extends EngineObject
3308
3301
  this.collideTiles = collideTiles;
3309
3302
  /** @property {Number} - Should particles use addtive blend */
3310
3303
  this.additive = additive;
3304
+ /** @property {Boolean} - Should it be in local space of emitter */
3305
+ this.localSpace = localSpace;
3311
3306
  /** @property {Number} - If set the partile is drawn as a trail, stretched in the drection of velocity */
3312
3307
  this.trailScale = 0;
3313
3308
 
@@ -3343,30 +3338,38 @@ class ParticleEmitter extends EngineObject
3343
3338
  emitParticle()
3344
3339
  {
3345
3340
  // spawn a particle
3346
- const pos = this.emitSize.x != undefined ? // check if vec2 was used for size
3347
- (new Vector2(rand(-.5,.5), rand(-.5,.5))).multiply(this.emitSize).rotate(this.angle) // box emitter
3348
- : randInCircle(this.emitSize * .5); // circle emitter
3349
- const particle = new Particle(this.pos.add(pos), this.tileIndex, this.tileSize,
3350
- this.angle + rand(this.particleConeAngle, -this.particleConeAngle));
3341
+ let pos = this.emitSize.x != undefined ? // check if vec2 was used for size
3342
+ (new Vector2(rand(-.5,.5), rand(-.5,.5)))
3343
+ .multiply(this.emitSize).rotate(this.angle) // box emitter
3344
+ : randInCircle(this.emitSize * .5); // circle emitter
3345
+ let angle = rand(this.particleConeAngle, -this.particleConeAngle);
3346
+ if (!this.localSpace)
3347
+ {
3348
+ pos = this.pos.add(pos);
3349
+ angle += this.angle;
3350
+ }
3351
+
3352
+ const particle = new Particle(pos, this.tileIndex, this.tileSize, angle);
3351
3353
 
3352
3354
  // randomness scales each paremeter by a percentage
3353
3355
  const randomness = this.randomness;
3354
3356
  const randomizeScale = (v)=> v + v*rand(randomness, -randomness);
3355
3357
 
3356
3358
  // randomize particle settings
3357
- const particleTime = randomizeScale(this.particleTime);
3358
- const sizeStart = randomizeScale(this.sizeStart);
3359
- const sizeEnd = randomizeScale(this.sizeEnd);
3360
- const speed = randomizeScale(this.speed);
3361
- const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
3362
- const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
3363
- const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
3364
- const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
3359
+ const particleTime = randomizeScale(this.particleTime);
3360
+ const sizeStart = randomizeScale(this.sizeStart);
3361
+ const sizeEnd = randomizeScale(this.sizeEnd);
3362
+ const speed = randomizeScale(this.speed);
3363
+ const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
3364
+ const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
3365
+ const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
3366
+ const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
3367
+ const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
3365
3368
 
3366
3369
  // build particle settings
3367
3370
  particle.colorStart = colorStart;
3368
3371
  particle.colorEndDelta = colorEnd.subtract(colorStart);
3369
- particle.velocity = (new Vector2).setAngle(this.angle + coneAngle, speed);
3372
+ particle.velocity = (new Vector2).setAngle(velocityAngle, speed);
3370
3373
  particle.angleVelocity = angleSpeed;
3371
3374
  particle.lifeTime = particleTime;
3372
3375
  particle.sizeStart = sizeStart;
@@ -3382,6 +3385,7 @@ class ParticleEmitter extends EngineObject
3382
3385
  particle.renderOrder = this.renderOrder;
3383
3386
  particle.trailScale = this.trailScale;
3384
3387
  particle.mirror = rand()<.5;
3388
+ particle.localSpaceEmitter = this.localSpace && this;
3385
3389
 
3386
3390
  // setup callbacks for particles
3387
3391
  particle.destroyCallback = this.particleDestroyCallback;
@@ -3428,20 +3432,31 @@ class Particle extends EngineObject
3428
3432
 
3429
3433
  // draw the particle
3430
3434
  this.additive && setBlendMode(1);
3435
+
3436
+ let pos = this.pos, angle = this.angle;
3437
+ if (this.localSpaceEmitter)
3438
+ {
3439
+ // in local space of emitter
3440
+ pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
3441
+ angle += this.localSpaceEmitter.angle;
3442
+ }
3431
3443
  if (this.trailScale)
3432
3444
  {
3433
3445
  // trail style particles
3434
- const speed = this.velocity.length();
3435
- const direction = this.velocity.scale(1/speed);
3446
+ let velocity = this.velocity;
3447
+ if (this.localSpaceEmitter)
3448
+ velocity = velocity.rotate(-this.localSpaceEmitter.angle);
3449
+ const speed = velocity.length();
3450
+ const direction = velocity.scale(1/speed);
3436
3451
  const trailLength = speed * this.trailScale;
3437
3452
  size.y = max(size.x, trailLength);
3438
- this.angle = direction.angle();
3439
- drawTile(this.pos.add(direction.multiply(vec2(0,-trailLength/2))), size, this.tileIndex, this.tileSize, color, this.angle, this.mirror);
3453
+ angle = direction.angle();
3454
+ drawTile(pos.add(direction.multiply(vec2(0,-trailLength/2))), size, this.tileIndex, this.tileSize, color, angle, this.mirror);
3440
3455
  }
3441
3456
  else
3442
- drawTile(this.pos, size, this.tileIndex, this.tileSize, color, this.angle, this.mirror);
3457
+ drawTile(pos, size, this.tileIndex, this.tileSize, color, angle, this.mirror);
3443
3458
  this.additive && setBlendMode();
3444
- debugParticles && debugRect(this.pos, size, '#f005', 0, this.angle);
3459
+ debugParticles && debugRect(pos, size, '#f005', 0, angle);
3445
3460
 
3446
3461
  if (p == 1)
3447
3462
  {
@@ -3471,11 +3486,6 @@ const medals = [];
3471
3486
  * @memberof Medals */
3472
3487
  let medalsPreventUnlock;
3473
3488
 
3474
- /** This can used to enable Newgrounds functionality
3475
- * @type {Newgrounds}
3476
- * @memberof Medals */
3477
- let newgrounds;
3478
-
3479
3489
  // Engine internal variables not exposed to documentation
3480
3490
  let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
3481
3491
 
@@ -3490,7 +3500,7 @@ function medalsInit(saveName)
3490
3500
  {
3491
3501
  // check if medals are unlocked
3492
3502
  medalsSaveName = saveName;
3493
- debugMedals || medals.forEach(medal=> localStorage[medal.storageKey()]);
3503
+ debugMedals || medals.forEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
3494
3504
  }
3495
3505
 
3496
3506
  /**
@@ -3623,6 +3633,15 @@ function medalsRender()
3623
3633
 
3624
3634
  ///////////////////////////////////////////////////////////////////////////////
3625
3635
 
3636
+ // global Newgrounds object
3637
+ let newgrounds;
3638
+
3639
+ /** This can used to enable Newgrounds functionality
3640
+ * @param {Number} app_id - The newgrounds App ID
3641
+ * @param {String} [cipher] - The encryption Key (AES-128/Base64)
3642
+ * @memberof Medals */
3643
+ function newgroundsInit(app_id, cipher) { newgrounds = new Newgrounds(app_id, cipher); }
3644
+
3626
3645
  /**
3627
3646
  * Newgrounds API wrapper object
3628
3647
  * @example
@@ -3783,15 +3802,13 @@ let glContext;
3783
3802
  let glTileTexture;
3784
3803
 
3785
3804
  // WebGL internal variables not exposed to documentation
3786
- let glActiveTexture, glShader, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3805
+ let glActiveTexture, glShader, glArrayBuffer, glVertexData, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3787
3806
 
3788
3807
  ///////////////////////////////////////////////////////////////////////////////
3789
3808
 
3790
3809
  // Init WebGL, called automatically by the engine
3791
3810
  function glInit()
3792
3811
  {
3793
- if (!glEnable) return;
3794
-
3795
3812
  // create the canvas and tile texture
3796
3813
  glCanvas = document.createElement('canvas');
3797
3814
  glContext = glCanvas.getContext('webgl', {antialias: false});
@@ -3824,24 +3841,11 @@ function glInit()
3824
3841
  );
3825
3842
 
3826
3843
  // init buffers
3827
- const glVertexData = new ArrayBuffer(gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE);
3828
- glCreateBuffer(gl_ARRAY_BUFFER, glVertexData.byteLength, gl_DYNAMIC_DRAW);
3844
+ glVertexData = new ArrayBuffer(gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE);
3845
+ glArrayBuffer = glContext.createBuffer();
3829
3846
  glPositionData = new Float32Array(glVertexData);
3830
3847
  glColorData = new Uint32Array(glVertexData);
3831
-
3832
- // setup the vertex data array
3833
- let offset = glBatchCount = 0;
3834
- const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
3835
- {
3836
- const location = glContext.getAttribLocation(glShader, name);
3837
- glContext.enableVertexAttribArray(location);
3838
- glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3839
- offset += size*typeSize;
3840
- }
3841
- initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3842
- initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
3843
- initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
3844
- initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3848
+ glBatchCount = 0;
3845
3849
  }
3846
3850
 
3847
3851
  /** Set the WebGl blend mode, normally you should call setBlendMode instead
@@ -3849,8 +3853,6 @@ function glInit()
3849
3853
  * @memberof WebGL */
3850
3854
  function glSetBlendMode(additive)
3851
3855
  {
3852
- if (!glEnable) return;
3853
-
3854
3856
  // setup blending
3855
3857
  glAdditive = additive;
3856
3858
  }
@@ -3861,8 +3863,6 @@ function glSetBlendMode(additive)
3861
3863
  * @memberof WebGL */
3862
3864
  function glSetTexture(texture=glTileTexture)
3863
3865
  {
3864
- if (!glEnable) return;
3865
-
3866
3866
  // must flush cache with the old texture to set a new one
3867
3867
  if (texture != glActiveTexture)
3868
3868
  glFlush();
@@ -3877,8 +3877,6 @@ function glSetTexture(texture=glTileTexture)
3877
3877
  * @memberof WebGL */
3878
3878
  function glCompileShader(source, type)
3879
3879
  {
3880
- if (!glEnable) return;
3881
-
3882
3880
  // build the shader
3883
3881
  const shader = glContext.createShader(type);
3884
3882
  glContext.shaderSource(shader, source);
@@ -3897,8 +3895,6 @@ function glCompileShader(source, type)
3897
3895
  * @memberof WebGL */
3898
3896
  function glCreateProgram(vsSource, fsSource)
3899
3897
  {
3900
- if (!glEnable) return;
3901
-
3902
3898
  // build the program
3903
3899
  const program = glContext.createProgram();
3904
3900
  glContext.attachShader(program, glCompileShader(vsSource, gl_VERTEX_SHADER));
@@ -3911,35 +3907,16 @@ function glCreateProgram(vsSource, fsSource)
3911
3907
  return program;
3912
3908
  }
3913
3909
 
3914
- /** Create WebGL buffer
3915
- * @param bufferType
3916
- * @param size
3917
- * @param usage
3918
- * @return {WebGLBuffer}
3919
- * @memberof WebGL */
3920
- function glCreateBuffer(bufferType, size, usage)
3921
- {
3922
- if (!glEnable) return;
3923
-
3924
- // build the buffer
3925
- const buffer = glContext.createBuffer();
3926
- glContext.bindBuffer(bufferType, buffer);
3927
- glContext.bufferData(bufferType, size, usage);
3928
- return buffer;
3929
- }
3930
-
3931
3910
  /** Create WebGL texture from an image and set the texture settings
3932
3911
  * @param {Image} image
3933
3912
  * @return {WebGLTexture}
3934
3913
  * @memberof WebGL */
3935
3914
  function glCreateTexture(image)
3936
3915
  {
3937
- if (!glEnable || !image || !image.width) return;
3938
-
3939
3916
  // build the texture
3940
3917
  const texture = glContext.createTexture();
3941
3918
  glContext.bindTexture(gl_TEXTURE_2D, texture);
3942
- glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
3919
+ image && image.width && glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
3943
3920
 
3944
3921
  // use point filtering for pixelated rendering
3945
3922
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, cavasPixelated ? gl_NEAREST : gl_LINEAR);
@@ -3952,16 +3929,31 @@ function glCreateTexture(image)
3952
3929
  // called automatically by engine before render
3953
3930
  function glPreRender(width, height, cameraX, cameraY, cameraScale)
3954
3931
  {
3955
- if (!glEnable) return;
3956
-
3957
3932
  // clear and set to same size as main canvas
3958
3933
  glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height);
3959
3934
  glContext.clear(gl_COLOR_BUFFER_BIT);
3960
3935
 
3961
3936
  // set up the shader
3962
- glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
3963
3937
  glContext.useProgram(glShader);
3938
+ glContext.activeTexture(gl_TEXTURE0);
3939
+ glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
3940
+ glContext.bindBuffer(gl_ARRAY_BUFFER, glArrayBuffer);
3941
+ glContext.bufferData(gl_ARRAY_BUFFER, glVertexData.byteLength, gl_DYNAMIC_DRAW);
3964
3942
  glSetBlendMode();
3943
+
3944
+ // set vertex attributes
3945
+ let offset = 0;
3946
+ const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
3947
+ {
3948
+ const location = glContext.getAttribLocation(glShader, name);
3949
+ glContext.enableVertexAttribArray(location);
3950
+ glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3951
+ offset += size*typeSize;
3952
+ }
3953
+ initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3954
+ initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
3955
+ initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
3956
+ initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3965
3957
 
3966
3958
  // build the transform matrix
3967
3959
  const sx = 2 * cameraScale / width;
@@ -3980,7 +3972,7 @@ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3980
3972
  * @memberof WebGL */
3981
3973
  function glFlush()
3982
3974
  {
3983
- if (!glEnable || !glBatchCount) return;
3975
+ if (!glBatchCount) return;
3984
3976
 
3985
3977
  const destBlend = glBatchAdditive ? gl_ONE : gl_ONE_MINUS_SRC_ALPHA;
3986
3978
  glContext.blendFuncSeparate(gl_SRC_ALPHA, destBlend, gl_ONE, destBlend);
@@ -4000,7 +3992,7 @@ function glFlush()
4000
3992
  * @memberof WebGL */
4001
3993
  function glCopyToContext(context, forceDraw)
4002
3994
  {
4003
- if (!glEnable || !glBatchCount && !forceDraw) return;
3995
+ if (!glBatchCount && !forceDraw) return;
4004
3996
 
4005
3997
  glFlush();
4006
3998
 
@@ -4024,8 +4016,6 @@ function glCopyToContext(context, forceDraw)
4024
4016
  * @memberof WebGL */
4025
4017
  function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffffff, rgbaAdditive=0)
4026
4018
  {
4027
- if (!glEnable) return;
4028
-
4029
4019
  // flush if there is no room for more verts or if different blend mode
4030
4020
  if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
4031
4021
  glFlush();
@@ -4074,6 +4064,90 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffff
4074
4064
  glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
4075
4065
  }
4076
4066
 
4067
+ ///////////////////////////////////////////////////////////////////////////////
4068
+ // post processing - can be enabled to pass other canvases through a final shader
4069
+
4070
+ let glPostShader, glPostArrayBuffer, glPostTexture;
4071
+
4072
+ /** Set up a post processing shader
4073
+ * @param {String} shaderCode
4074
+ * @memberof WebGL */
4075
+ function glInitPostProcess(shaderCode)
4076
+ {
4077
+ ASSERT(!glPostShader); // can only have 1 post effects shader
4078
+
4079
+ if (!shaderCode) // default shader
4080
+ shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture2D(iChannel0,p/iResolution.xy);}';
4081
+
4082
+ // create the shader
4083
+ glPostShader = glCreateProgram(
4084
+ 'precision highp float;'+ // use highp for better accuracy
4085
+ 'attribute vec2 p;'+ // position
4086
+ 'void main(){'+ // shader entry point
4087
+ 'gl_Position=vec4(p,1,1);'+ // set position
4088
+ '}' // end of shader
4089
+ ,
4090
+ 'precision highp float;'+ // use highp for better accuracy
4091
+ 'uniform sampler2D iChannel0;'+ // input texture
4092
+ 'uniform vec3 iResolution;'+ // size of output texture
4093
+ 'uniform float iTime;'+ // time passed
4094
+ '\n' + shaderCode + '\n'+ // insert custom shader code
4095
+ 'void main(){'+ // shader entry point
4096
+ 'mainImage(gl_FragColor,gl_FragCoord.xy);'+ // call post process function
4097
+ 'gl_FragColor.a=1.;'+ // always use full alpha
4098
+ '}' // end of shader
4099
+ );
4100
+
4101
+ // create buffer and texture
4102
+ glPostArrayBuffer = glContext.createBuffer();
4103
+ glPostTexture = glCreateTexture();
4104
+
4105
+ // hide the original 2d canvas
4106
+ mainCanvas.style.visibility = 'hidden';
4107
+ }
4108
+
4109
+ // Render the post processing shader, called automatically by the engine
4110
+ function glRenderPostProcess()
4111
+ {
4112
+ if (!glPostShader)
4113
+ return;
4114
+
4115
+ // prepare to render post process shader
4116
+ const width = mainCanvas.width, height = mainCanvas.height;
4117
+ if (glEnable)
4118
+ {
4119
+ glFlush(); // clear out the buffer
4120
+ mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
4121
+ }
4122
+ else
4123
+ glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height); // set viewport
4124
+
4125
+ // setup shader program to draw one triangle
4126
+ glContext.useProgram(glPostShader);
4127
+ glContext.disable(gl_BLEND);
4128
+ glContext.bindBuffer(gl_ARRAY_BUFFER, glPostArrayBuffer);
4129
+ glContext.bufferData(gl_ARRAY_BUFFER, new Float32Array([-3,1,1,-3,1,1]), gl_STATIC_DRAW);
4130
+ glContext.pixelStorei(gl_UNPACK_FLIP_Y_WEBGL, true);
4131
+
4132
+ // set textures, pass in the 2d canvas and gl canvas in separate texture channels
4133
+ glContext.activeTexture(gl_TEXTURE0);
4134
+ glContext.bindTexture(gl_TEXTURE_2D, glPostTexture);
4135
+ glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, mainCanvas);
4136
+
4137
+ // set vertex position attribute
4138
+ const vertexByteStride = 8;
4139
+ const pLocation = glContext.getAttribLocation(glPostShader, 'p');
4140
+ glContext.enableVertexAttribArray(pLocation);
4141
+ glContext.vertexAttribPointer(pLocation, 2, gl_FLOAT, 0, vertexByteStride, 0);
4142
+
4143
+ // set uniforms and draw
4144
+ const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
4145
+ glContext.uniform1i(uniformLocation('iChannel0'), 0);
4146
+ glContext.uniform1f(uniformLocation('iTime'), time);
4147
+ glContext.uniform3f(uniformLocation('iResolution'), width, height, 1);
4148
+ glContext.drawArrays(gl_TRIANGLES, 0, 3);
4149
+ }
4150
+
4077
4151
  ///////////////////////////////////////////////////////////////////////////////
4078
4152
  // store gl constants as integers so their name doesn't use space in minifed
4079
4153
  const
@@ -4084,6 +4158,7 @@ gl_ONE_MINUS_SRC_ALPHA = 771,
4084
4158
  gl_BLEND = 3042,
4085
4159
  gl_TEXTURE_2D = 3553,
4086
4160
  gl_UNSIGNED_BYTE = 5121,
4161
+ gl_BYTE = 5120,
4087
4162
  gl_FLOAT = 5126,
4088
4163
  gl_RGBA = 6408,
4089
4164
  gl_NEAREST = 9728,
@@ -4094,12 +4169,16 @@ gl_TEXTURE_WRAP_S = 10242,
4094
4169
  gl_TEXTURE_WRAP_T = 10243,
4095
4170
  gl_COLOR_BUFFER_BIT = 16384,
4096
4171
  gl_CLAMP_TO_EDGE = 33071,
4172
+ gl_TEXTURE0 = 33984,
4173
+ gl_TEXTURE1 = 33985,
4097
4174
  gl_ARRAY_BUFFER = 34962,
4175
+ gl_STATIC_DRAW = 35044,
4098
4176
  gl_DYNAMIC_DRAW = 35048,
4099
4177
  gl_FRAGMENT_SHADER = 35632,
4100
4178
  gl_VERTEX_SHADER = 35633,
4101
4179
  gl_COMPILE_STATUS = 35713,
4102
4180
  gl_LINK_STATUS = 35714,
4181
+ gl_UNPACK_FLIP_Y_WEBGL = 37440,
4103
4182
 
4104
4183
  // constants for batch rendering
4105
4184
  gl_VERTICES_PER_QUAD = 6,
@@ -4130,7 +4209,7 @@ gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2; // vec2 * 2 + (char * 4) * 2
4130
4209
  const engineName = 'LittleJS';
4131
4210
 
4132
4211
  /** Version of engine */
4133
- const engineVersion = '1.4.0';
4212
+ const engineVersion = '1.4.6';
4134
4213
 
4135
4214
  /** Frames per second to update objects
4136
4215
  * @default */
@@ -4165,8 +4244,7 @@ let frameTimeLastMS = 0, frameTimeBufferMS = 0, tileImageSize, tileImageFixBleed
4165
4244
  let averageFPS, drawCount;
4166
4245
 
4167
4246
  // css text used for elements created by engine
4168
- const styleBody = 'margin:0;overflow:hidden;background:#000' +
4169
- ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
4247
+ const styleBody = 'margin:0;overflow:hidden;background:#000';
4170
4248
  const styleCanvas = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
4171
4249
 
4172
4250
  ///////////////////////////////////////////////////////////////////////////////
@@ -4196,7 +4274,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4196
4274
 
4197
4275
  // init stuff and start engine
4198
4276
  debugInit();
4199
- glInit();
4277
+ glEnable && glInit();
4200
4278
 
4201
4279
  // create overlay canvas for hud to appear above gl canvas
4202
4280
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
@@ -4216,10 +4294,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4216
4294
  frameTimeLastMS = frameTimeMS;
4217
4295
  if (debug || showWatermark)
4218
4296
  averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
4297
+ const debugSpeedUp = debug && keyIsDown(107); // +
4298
+ const debugSpeedDown = debug && keyIsDown(109); // -
4219
4299
  if (debug)
4220
- frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
4300
+ frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1; // +/- to speed/slow time
4221
4301
  timeReal += frameTimeDeltaMS / 1e3;
4222
- frameTimeBufferMS = min(frameTimeBufferMS + !paused * frameTimeDeltaMS, 50); // clamp incase of slow framerate
4302
+ frameTimeBufferMS += !paused * frameTimeDeltaMS;
4303
+ if (!debugSpeedUp)
4304
+ frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp incase of slow framerate
4223
4305
 
4224
4306
  if (canvasFixedSize.x)
4225
4307
  {
@@ -4292,10 +4374,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4292
4374
  for (const o of engineObjects)
4293
4375
  o.destroyed || o.render();
4294
4376
  gameRenderPost();
4377
+ glRenderPostProcess();
4295
4378
  medalsRender();
4296
4379
  touchGamepadRender();
4297
4380
  debugRender();
4298
- glCopyToContext(mainContext);
4381
+ glEnable && glCopyToContext(mainContext);
4299
4382
 
4300
4383
  if (showWatermark)
4301
4384
  {
@@ -4330,7 +4413,7 @@ function enginePreRender()
4330
4413
  mainContext.imageSmoothingEnabled = !cavasPixelated;
4331
4414
 
4332
4415
  // setup gl rendering if enabled
4333
- glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
4416
+ glEnable && glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
4334
4417
  }
4335
4418
 
4336
4419
  ///////////////////////////////////////////////////////////////////////////////