littlejsengine 1.9.3 → 1.9.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.
package/dist/littlejs.js CHANGED
@@ -264,6 +264,10 @@ function debugRender()
264
264
  {
265
265
  const saveContext = mainContext;
266
266
  mainContext = overlayContext;
267
+
268
+ // draw red rectangle around screen
269
+ const cameraSize = getCameraSize();
270
+ debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008');
267
271
 
268
272
  // mouse pick
269
273
  let bestDistance = Infinity, bestObject;
@@ -353,7 +357,7 @@ function debugRender()
353
357
  overlayContext.restore();
354
358
  });
355
359
 
356
- // remove expired pritives
360
+ // remove expired primitives
357
361
  debugPrimitives = debugPrimitives.filter(r=>r.time<0);
358
362
  }
359
363
 
@@ -538,16 +542,57 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
538
542
  function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
539
543
 
540
544
  /** Returns true if two axis aligned bounding boxes are overlapping
541
- * @param {Vector2} pointA - Center of box A
542
- * @param {Vector2} sizeA - Size of box A
543
- * @param {Vector2} pointB - Center of box B
544
- * @param {Vector2} [sizeB=(0,0)] - Size of box B, a point if undefined
545
- * @return {Boolean} - True if overlapping
545
+ * @param {Vector2} posA - Center of box A
546
+ * @param {Vector2} sizeA - Size of box A
547
+ * @param {Vector2} posB - Center of box B
548
+ * @param {Vector2} [sizeB=(0,0)] - Size of box B, a point if undefined
549
+ * @return {Boolean} - True if overlapping
546
550
  * @memberof Utilities */
547
- function isOverlapping(pointA, sizeA, pointB, sizeB=vec2())
551
+ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
548
552
  {
549
- return abs(pointA.x - pointB.x)*2 < sizeA.x + sizeB.x
550
- && abs(pointA.y - pointB.y)*2 < sizeA.y + sizeB.y;
553
+ return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
554
+ && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
555
+ }
556
+
557
+ /** Returns true if a line segment is intersecting an axis aligned box
558
+ * @param {Vector2} start - Start of raycast
559
+ * @param {Vector2} end - End of raycast
560
+ * @param {Vector2} pos - Center of box
561
+ * @param {Vector2} size - Size of box
562
+ * @return {Boolean} - True if intersecting
563
+ * @memberof Utilities */
564
+ function isIntersecting(start, end, pos, size)
565
+ {
566
+ // Liang-Barsky algorithm
567
+ const boxMin = pos.subtract(size.scale(.5));
568
+ const boxMax = boxMin.add(size);
569
+ const delta = end.subtract(start);
570
+ const a = start.subtract(boxMin);
571
+ const b = start.subtract(boxMax);
572
+ const p = [-delta.x, delta.x, -delta.y, delta.y];
573
+ const q = [a.x, -b.x, a.y, -b.y];
574
+ let tMin = 0, tMax = 1;
575
+ for (let i = 4; i--;)
576
+ {
577
+ if (p[i])
578
+ {
579
+ const t = q[i] / p[i];
580
+ if (p[i] < 0)
581
+ {
582
+ if (t > tMax) return false;
583
+ tMin = max(t, tMin);
584
+ }
585
+ else
586
+ {
587
+ if (t < tMin) return false;
588
+ tMax = min(t, tMax);
589
+ }
590
+ }
591
+ else if (q[i] < 0)
592
+ return false;
593
+ }
594
+
595
+ return true;
551
596
  }
552
597
 
553
598
  /** Returns an oscillating wave between 0 and amplitude with frequency of 1 Hz by default
@@ -1047,7 +1092,7 @@ class Color
1047
1092
 
1048
1093
  /** Returns this color expressed in hsla format
1049
1094
  * @return {Array} */
1050
- getHSLA()
1095
+ HSLA()
1051
1096
  {
1052
1097
  const r = clamp(this.r);
1053
1098
  const g = clamp(this.g);
@@ -1731,6 +1776,8 @@ class EngineObject
1731
1776
  this.collideSolidObjects = false;
1732
1777
  /** @property {Boolean} - Object collides with and blocks other objects */
1733
1778
  this.isSolid = false;
1779
+ /** @property {Boolean} - Object collides with raycasts */
1780
+ this.collideRaycast = false;
1734
1781
 
1735
1782
  // add to list of objects
1736
1783
  engineObjects.push(this);
@@ -1935,13 +1982,7 @@ class EngineObject
1935
1982
  * @param {Number} tileData - the value of the tile at the position
1936
1983
  * @param {Vector2} pos - tile where the collision occured
1937
1984
  * @return {Boolean} - true if the collision should be resolved */
1938
- collideWithTile(tileData, pos) { return tileData > 0; }
1939
-
1940
- /** Called to check if a tile raycast hit
1941
- * @param {Number} tileData - the value of the tile at the position
1942
- * @param {Vector2} pos - tile where the raycast is
1943
- * @return {Boolean} - true if the raycast should hit */
1944
- collideWithTileRaycast(tileData, pos) { return tileData > 0; }
1985
+ collideWithTile(tileData, pos) { return tileData > 0; }
1945
1986
 
1946
1987
  /** Called to check if a object collision should be resolved
1947
1988
  * @param {EngineObject} object - the object to test against
@@ -1988,16 +2029,18 @@ class EngineObject
1988
2029
  }
1989
2030
 
1990
2031
  /** Set how this object collides
1991
- * @param {Boolean} [collideSolidObjects] - Does it collide with solid objects
1992
- * @param {Boolean} [isSolid] - Does it collide with and block other objects (expensive in large numbers)
1993
- * @param {Boolean} [collideTiles] - Does it collide with the tile collision */
1994
- setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true)
2032
+ * @param {Boolean} [collideSolidObjects] - Does it collide with solid objects?
2033
+ * @param {Boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
2034
+ * @param {Boolean} [collideTiles] - Does it collide with the tile collision?
2035
+ * @param {Boolean} [collideRaycast] - Does it collide with raycasts? */
2036
+ setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true, collideRaycast=true)
1995
2037
  {
1996
2038
  ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
1997
2039
 
1998
2040
  this.collideSolidObjects = collideSolidObjects;
1999
2041
  this.isSolid = isSolid;
2000
2042
  this.collideTiles = collideTiles;
2043
+ this.collideRaycast = collideRaycast;
2001
2044
  }
2002
2045
 
2003
2046
  /** Returns string containg info about this object for debugging
@@ -2137,13 +2180,23 @@ class TileInfo
2137
2180
  this.textureIndex = textureIndex;
2138
2181
  }
2139
2182
 
2140
- /** Returns an offset copy of this tile, useful for animation
2183
+ /** Returns a copy of this tile offset by a vector
2141
2184
  * @param {Vector2} offset - Offset to apply in pixels
2142
2185
  * @return {TileInfo}
2143
2186
  */
2144
2187
  offset(offset)
2145
2188
  { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex); }
2146
2189
 
2190
+ /** Returns a copy of this tile offset by a number of animation frames
2191
+ * @param {Number} frame - Offset to apply in animation frames
2192
+ * @return {TileInfo}
2193
+ */
2194
+ frame(frame)
2195
+ {
2196
+ ASSERT(typeof frame == 'number');
2197
+ return this.offset(vec2(frame*this.size.x, 0));
2198
+ }
2199
+
2147
2200
  /** Returns the texture info for this tile
2148
2201
  * @return {TextureInfo}
2149
2202
  */
@@ -2294,21 +2347,6 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2294
2347
  drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
2295
2348
  }
2296
2349
 
2297
- /** Draw colored polygon using passed in points
2298
- * @param {Array} points - Array of Vector2 points
2299
- * @param {Color} [color=(1,1,1,1)]
2300
- * @param {Boolean} [screenSpace=false]
2301
- * @param {CanvasRenderingContext2D} [context=mainContext]
2302
- * @memberof Draw */
2303
- function drawPoly(points, color=new Color, screenSpace, context=mainContext)
2304
- {
2305
- context.fillStyle = color.toString();
2306
- context.beginPath();
2307
- for (const point of screenSpace ? points : points.map(worldToScreen))
2308
- context.lineTo(point.x, point.y);
2309
- context.fill();
2310
- }
2311
-
2312
2350
  /** Draw colored line between two points
2313
2351
  * @param {Vector2} posA
2314
2352
  * @param {Vector2} posB
@@ -2735,9 +2773,21 @@ function mouseToScreen(mousePos)
2735
2773
  ///////////////////////////////////////////////////////////////////////////////
2736
2774
  // Gamepad input
2737
2775
 
2776
+ // gamepad internal variables
2738
2777
  const stickData = [];
2778
+
2779
+ // gamepads are updated by engine every frame automatically
2739
2780
  function gamepadsUpdate()
2740
2781
  {
2782
+ const applyDeadZones = (v)=>
2783
+ {
2784
+ const min=.3, max=.8;
2785
+ const deadZone = (v)=>
2786
+ v > min ? percent( v, min, max) :
2787
+ v < -min ? -percent(-v, min, max) : 0;
2788
+ return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
2789
+ }
2790
+
2741
2791
  // update touch gamepad if enabled
2742
2792
  if (touchGamepadEnable && isTouchDevice)
2743
2793
  {
@@ -2749,7 +2799,16 @@ function gamepadsUpdate()
2749
2799
  {
2750
2800
  // read virtual analog stick
2751
2801
  const sticks = stickData[0] || (stickData[0] = []);
2752
- sticks[0] = vec2(touchGamepadStick.x, -touchGamepadStick.y); // flip vertical
2802
+ sticks[0] = vec2();
2803
+ if (touchGamepadAnalog)
2804
+ sticks[0] = applyDeadZones(touchGamepadStick);
2805
+ else if (touchGamepadStick.lengthSquared() > .3)
2806
+ {
2807
+ // convert to 8 way dpad
2808
+ sticks[0].x = Math.round(touchGamepadStick.x);
2809
+ sticks[0].y = -Math.round(touchGamepadStick.y);
2810
+ sticks[0] = sticks[0].clampLength();
2811
+ }
2753
2812
 
2754
2813
  // read virtual gamepad buttons
2755
2814
  const data = inputData[1] || (inputData[1] = []);
@@ -2761,7 +2820,12 @@ function gamepadsUpdate()
2761
2820
  }
2762
2821
  }
2763
2822
 
2764
- if (!gamepadsEnable || !navigator || !navigator.getGamepads || !document.hasFocus() && !debug)
2823
+ // return if gamepads are disabled or not supported
2824
+ if (!gamepadsEnable || !navigator || !navigator.getGamepads)
2825
+ return;
2826
+
2827
+ // only poll gamepads when focused or in debug mode
2828
+ if (!debug && !document.hasFocus())
2765
2829
  return;
2766
2830
 
2767
2831
  // poll gamepads
@@ -2775,14 +2839,9 @@ function gamepadsUpdate()
2775
2839
 
2776
2840
  if (gamepad)
2777
2841
  {
2778
- // read clamp dead zone of analog sticks
2779
- const deadZone = .3, deadZoneMax = .8, applyDeadZone = (v)=>
2780
- v > deadZone ? percent( v, deadZone, deadZoneMax) :
2781
- v < -deadZone ? -percent(-v, deadZone, deadZoneMax) : 0;
2782
-
2783
2842
  // read analog sticks
2784
2843
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
2785
- sticks[j>>1] = vec2(applyDeadZone(gamepad.axes[j]), applyDeadZone(-gamepad.axes[j+1])).clampLength();
2844
+ sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
2786
2845
 
2787
2846
  // read buttons
2788
2847
  for (let j = gamepad.buttons.length; j--;)
@@ -2911,14 +2970,7 @@ function createTouchGamepad()
2911
2970
  if (touchPos.distance(stickCenter) < touchGamepadSize)
2912
2971
  {
2913
2972
  // virtual analog stick
2914
- if (touchGamepadAnalog)
2915
- touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
2916
- else
2917
- {
2918
- // 8 way dpad
2919
- const angle = touchPos.subtract(stickCenter).angle();
2920
- touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
2921
- }
2973
+ touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
2922
2974
  }
2923
2975
  else if (touchPos.distance(buttonCenter) < touchGamepadSize)
2924
2976
  {
@@ -3177,7 +3229,7 @@ class SoundWave extends Sound
3177
3229
  * 1, 0, 9, 1 // channel notes
3178
3230
  * ],
3179
3231
  * [ // channel 1
3180
- * 0, 1, // instrument 1, right speaker
3232
+ * 0, 1, // instrument 0, right speaker
3181
3233
  * 0, 12, 17, -1 // channel notes
3182
3234
  * ]
3183
3235
  * ],
@@ -3641,7 +3693,7 @@ function tileCollisionTest(pos, size=vec2(), object)
3641
3693
  }
3642
3694
  }
3643
3695
 
3644
- /** Return the center of tile if any that is hit (does not return the exact intersection)
3696
+ /** Return the center of first tile hit (does not return the exact intersection)
3645
3697
  * @param {Vector2} posStart
3646
3698
  * @param {Vector2} posEnd
3647
3699
  * @param {EngineObject} [object]
@@ -3837,8 +3889,11 @@ class TileLayer extends EngineObject
3837
3889
  mainCanvas.height = mainCanvasSize.y;
3838
3890
  }
3839
3891
 
3840
- // begin a new render for the tile canvas
3841
- enginePreRender();
3892
+ // disable smoothing for pixel art
3893
+ this.context.imageSmoothingEnabled = !canvasPixelated;
3894
+
3895
+ // setup gl rendering if enabled
3896
+ glEnable && glPreRender();
3842
3897
  }
3843
3898
 
3844
3899
  /** Call to end the redraw process */
@@ -4987,7 +5042,7 @@ const engineName = 'LittleJS';
4987
5042
  * @type {String}
4988
5043
  * @default
4989
5044
  * @memberof Engine */
4990
- const engineVersion = '1.9.3';
5045
+ const engineVersion = '1.9.5';
4991
5046
 
4992
5047
  /** Frames per second to update
4993
5048
  * @type {Number}
@@ -5054,6 +5109,19 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5054
5109
  {
5055
5110
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
5056
5111
 
5112
+ // Called automatically by engine to setup render system
5113
+ function enginePreRender()
5114
+ {
5115
+ // save canvas size
5116
+ mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
5117
+
5118
+ // disable smoothing for pixel art
5119
+ mainContext.imageSmoothingEnabled = !canvasPixelated;
5120
+
5121
+ // setup gl rendering if enabled
5122
+ glEnable && glPreRender();
5123
+ }
5124
+
5057
5125
  // internal update loop for engine
5058
5126
  function engineUpdate(frameTimeMS=0)
5059
5127
  {
@@ -5069,7 +5137,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5069
5137
  timeReal += frameTimeDeltaMS / 1e3;
5070
5138
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
5071
5139
  if (!debugSpeedUp)
5072
- frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp incase of slow framerate
5140
+ frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
5073
5141
  updateCanvas();
5074
5142
 
5075
5143
  if (paused)
@@ -5211,7 +5279,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5211
5279
  }
5212
5280
  image.src = src;
5213
5281
  })
5214
- )
5282
+ );
5215
5283
 
5216
5284
  // draw splash screen
5217
5285
  showSplashScreen && promises.push(new Promise(resolve =>
@@ -5236,19 +5304,6 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5236
5304
  });
5237
5305
  }
5238
5306
 
5239
- // Called automatically by engine to setup render system
5240
- function enginePreRender()
5241
- {
5242
- // save canvas size
5243
- mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
5244
-
5245
- // disable smoothing for pixel art
5246
- mainContext.imageSmoothingEnabled = !canvasPixelated;
5247
-
5248
- // setup gl rendering if enabled
5249
- glEnable && glPreRender();
5250
- }
5251
-
5252
5307
  /** Update each engine object, remove destroyed objects, and update time
5253
5308
  * @memberof Engine */
5254
5309
  function engineObjectsUpdate()
@@ -5282,30 +5337,63 @@ function engineObjectsDestroy()
5282
5337
  engineObjects = engineObjects.filter(o=>!o.destroyed);
5283
5338
  }
5284
5339
 
5285
- /** Triggers a callback for each object within a given area
5286
- * @param {Vector2} [pos] - Center of test area
5340
+ /** Collects all object within a given area
5341
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
5287
5342
  * @param {Number|Vector2} [size] - Radius of circle if float, rectangle size if Vector2
5288
- * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
5289
5343
  * @param {Array} [objects=engineObjects] - List of objects to check
5344
+ * @return {Array} - List of collected objects
5290
5345
  * @memberof Engine */
5291
- function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
5346
+ function engineObjectsCollect(pos, size, objects=engineObjects)
5292
5347
  {
5348
+ const collectedObjects = [];
5293
5349
  if (!pos) // all objects
5294
5350
  {
5295
5351
  for (const o of objects)
5296
- callbackFunction(o);
5352
+ collectedObjects.push(o);
5297
5353
  }
5298
- else if (typeof size === 'object') // bounding box test
5354
+ else if (size instanceof Vector2) // bounding box test
5299
5355
  {
5300
5356
  for (const o of objects)
5301
- isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
5357
+ isOverlapping(pos, size, o.pos, o.size) && collectedObjects.push(o);
5302
5358
  }
5303
5359
  else // circle test
5304
5360
  {
5305
5361
  const sizeSquared = size*size;
5306
5362
  for (const o of objects)
5307
- pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
5363
+ pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
5308
5364
  }
5365
+ return collectedObjects;
5366
+ }
5367
+
5368
+ /** Triggers a callback for each object within a given area
5369
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
5370
+ * @param {Number|Vector2} [size] - Radius of circle if float, rectangle size if Vector2
5371
+ * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
5372
+ * @param {Array} [objects=engineObjects] - List of objects to check
5373
+ * @memberof Engine */
5374
+ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
5375
+ { engineObjectsCollect(pos, size, objects).forEach(o => callbackFunction(o)); }
5376
+
5377
+ /** Return a list of objects intersecting a ray
5378
+ * @param {Vector2} start
5379
+ * @param {Vector2} end
5380
+ * @param {Array} [objects=engineObjects] - List of objects to check
5381
+ * @return {Array} - List of objects hit
5382
+ * @memberof Engine */
5383
+ function engineObjectsRaycast(start, end, objects=engineObjects)
5384
+ {
5385
+ const hitObjects = [];
5386
+ for (const o of objects)
5387
+ {
5388
+ if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size))
5389
+ {
5390
+ debugRaycast && debugRect(o.pos, o.size, '#f00');
5391
+ hitObjects.push(o);
5392
+ }
5393
+ }
5394
+
5395
+ debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
5396
+ return hitObjects;
5309
5397
  }
5310
5398
 
5311
5399
  ///////////////////////////////////////////////////////////////////////////////
@@ -5393,16 +5481,16 @@ function drawEngineSplashScreen(t)
5393
5481
  rect(37,14,9,6);
5394
5482
 
5395
5483
  // big stack
5396
- rect(50,20,10,-10,color(0,1));
5397
- rect(50,20,6.5,-10,color(0,2));
5398
- rect(50,20,3.5,-10,color(0,3));
5399
- rect(50,20,10,-10);
5400
- circle(55,2,11.4,.5,PI-.5,color(3,3));
5401
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
5402
- circle(55,2,11.4,.5,PI-.5);
5403
- rect(45,7,20,-7,color(0,2));
5404
- rect(45,0,20,3,color(0,3));
5405
- rect(45,0,20,7);
5484
+ rect(50,20,10,-8,color(0,1))
5485
+ rect(50,20,6.5,-8,color(0,2))
5486
+ rect(50,20,3.5,-8,color(0,3))
5487
+ rect(50,20,10,-8)
5488
+ circle(55,2,11.4,.5,PI-.5,color(3,3))
5489
+ circle(55,2,11.4,.5,PI/2,color(3,2),1)
5490
+ circle(55,2,11.4,.5,PI-.5)
5491
+ rect(45,7,20,-7,color(0,2))
5492
+ rect(45,-1,20,4,color(0,3))
5493
+ rect(45,-1,20,8)
5406
5494
 
5407
5495
  // engine
5408
5496
  for (let i=5; i--;)