littlejsengine 1.18.18 → 1.18.21

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
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.18.18';
38
+ const engineVersion = '1.18.21';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -474,6 +474,11 @@ function engineObjectsUpdate()
474
474
  // get list of solid objects for physics optimization
475
475
  engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
476
476
 
477
+ // update physics before object update
478
+ for (const o of engineObjects)
479
+ if (!o.parent && !o.destroyed)
480
+ o.updatePhysics();
481
+
477
482
  // recursive object update
478
483
  function updateChildObject(o)
479
484
  {
@@ -489,7 +494,6 @@ function engineObjectsUpdate()
489
494
 
490
495
  // update top level objects
491
496
  o.update();
492
- o.updatePhysics();
493
497
  for (const child of o.children)
494
498
  updateChildObject(child);
495
499
  o.updateTransforms();
@@ -1237,6 +1241,7 @@ function debugVideoCaptureStart()
1237
1241
  {
1238
1242
  LOG('Video capture not supported in this browser!');
1239
1243
  silentAudioSource?.stop();
1244
+ audioStreamDestination && audioMasterGain.disconnect(audioStreamDestination);
1240
1245
  return;
1241
1246
  }
1242
1247
 
@@ -1265,6 +1270,8 @@ function debugVideoCaptureStop()
1265
1270
  debugVideoCapture.silentAudioSource?.stop();
1266
1271
  debugVideoCapture.mediaRecorder?.stop();
1267
1272
  debugVideoCapture.videoTrack?.stop();
1273
+ if (debugVideoCapture.audioStreamDestination)
1274
+ audioMasterGain.disconnect(debugVideoCapture.audioStreamDestination);
1268
1275
  debugVideoCapture = undefined;
1269
1276
  }
1270
1277
 
@@ -1503,7 +1510,7 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
1503
1510
  * @param {number} value
1504
1511
  * @return {boolean}
1505
1512
  * @memberof Math */
1506
- function isPowerOfTwo(value) { return !(value & (value - 1)); }
1513
+ function isPowerOfTwo(value) { return value > 0 && !(value & (value - 1)); }
1507
1514
 
1508
1515
  /** Returns the nearest power of two not less than the value
1509
1516
  * @param {number} value
@@ -1580,7 +1587,7 @@ function isIntersecting(start, end, pos, size)
1580
1587
  * @memberof Math */
1581
1588
  function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
1582
1589
  {
1583
- const phase = (offset + t*frequency) % 1;
1590
+ const phase = mod(offset + t*frequency, 1);
1584
1591
  let value;
1585
1592
 
1586
1593
  if (type === 1) // triangle
@@ -2516,7 +2523,7 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
2516
2523
  class Timer
2517
2524
  {
2518
2525
  /** Create a timer object set time passed in
2519
- * @param {number} [timeLeft] - How much time left before the timer
2526
+ * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds (undefined = unset)
2520
2527
  * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
2521
2528
  constructor(timeLeft, useRealTime=false)
2522
2529
  {
@@ -3115,6 +3122,7 @@ let vibrateEnable = true;
3115
3122
  let soundEnable = true;
3116
3123
 
3117
3124
  /** Volume scale to apply to all sound, music and speech
3125
+ * Use setSoundVolume to also update the audio master gain immediately
3118
3126
  * @type {number}
3119
3127
  * @default
3120
3128
  * @memberof Settings */
@@ -3669,8 +3677,6 @@ class EngineObject
3669
3677
 
3670
3678
  // notify objects of collision and check if should be resolved
3671
3679
  const collide1 = this.collideWithObject(o);
3672
- // callback may have destroyed us; stop resolving against more objects
3673
- if (this.destroyed) return;
3674
3680
  const collide2 = o.collideWithObject(this);
3675
3681
  if (!collide1 || !collide2) continue;
3676
3682
 
@@ -4196,6 +4202,14 @@ class TileInfo
4196
4202
  return this.offset(new Vector2(x));
4197
4203
  }
4198
4204
 
4205
+ /**
4206
+ * Returns a tile info for an index using this tile as reference
4207
+ * @param {Vector2|number} [index=0]
4208
+ * @return {TileInfo}
4209
+ */
4210
+ index(index)
4211
+ { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
4212
+
4199
4213
  /**
4200
4214
  * Set this tile to use a full image in a texture info
4201
4215
  * @param {TextureInfo} [textureInfo]
@@ -4209,14 +4223,6 @@ class TileInfo
4209
4223
  this.bleed = this.padding = 0;
4210
4224
  return this;
4211
4225
  }
4212
-
4213
- /**
4214
- * Returns a tile info for an index using this tile as reference
4215
- * @param {Vector2|number} [index=0]
4216
- * @return {TileInfo}
4217
- */
4218
- tile(index)
4219
- { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
4220
4226
  }
4221
4227
 
4222
4228
  /**
@@ -5914,7 +5920,10 @@ function inputInit()
5914
5920
  {
5915
5921
  inputData[0][e.code] = (inputData[0][e.code]&2) | 4;
5916
5922
  if (inputWASDEmulateDirection)
5917
- inputData[0][remapKey(e.code)] = 4;
5923
+ {
5924
+ const remap = remapKey(e.code);
5925
+ inputData[0][remap] = (inputData[0][remap]&2) | 4;
5926
+ }
5918
5927
  }
5919
5928
  function remapKey(k)
5920
5929
  {
@@ -5989,7 +5998,7 @@ function inputInit()
5989
5998
  document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
5990
5999
 
5991
6000
  // handle all touch events the same way
5992
- let wasTouching;
6001
+ let wasTouching, touchIdentifier;
5993
6002
  function handleTouch(e)
5994
6003
  {
5995
6004
  if (!touchInputEnable) return;
@@ -6020,10 +6029,11 @@ function inputInit()
6020
6029
  const pos = vec2(gameTouches[0].clientX, gameTouches[0].clientY);
6021
6030
  const mousePosScreenLast = mousePosScreen;
6022
6031
  mousePosScreen = mouseEventToScreen(pos);
6023
- if (wasTouching)
6032
+ if (wasTouching && gameTouches[0].identifier === touchIdentifier)
6024
6033
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
6025
- else
6034
+ else if (!wasTouching)
6026
6035
  inputData[0][button] = 3;
6036
+ touchIdentifier = gameTouches[0].identifier;
6027
6037
  }
6028
6038
  else if (wasTouching)
6029
6039
  inputData[0][button] = inputData[0][button] & 2 | 4;
@@ -7181,7 +7191,7 @@ class SoundInstance
7181
7191
  * @param {number} [rate] - How quickly to speak
7182
7192
  * @param {number} [pitch] - How much to change the pitch by
7183
7193
  * @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
7184
- * @return {SpeechSynthesisUtterance} - The utterance that was spoken
7194
+ * @return {SpeechSynthesisUtterance|undefined} - The utterance that was spoken, or undefined if speech is unavailable
7185
7195
  * @memberof Audio */
7186
7196
  function speak(text, volume=1, rate=1, pitch=1, language='')
7187
7197
  {
@@ -7234,7 +7244,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
7234
7244
  * @param {number} [pan] - How much to apply stereo panning
7235
7245
  * @param {boolean} [loop] - True if the sound should loop when it reaches the end
7236
7246
  * @param {number} [sampleRate=44100] - Sample rate for the sound
7237
- * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
7247
+ * @param {GainNode} [gainNode] - Optional gain node for volume control while playing (disconnected when the sound ends)
7238
7248
  * @param {number} [offset] - Offset in seconds to start playback from
7239
7249
  * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
7240
7250
  * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
@@ -7476,10 +7486,14 @@ function tileCollisionGetData(pos, solidOnly=true)
7476
7486
  // check all tile collision layers
7477
7487
  for (const layer of tileCollisionLayers)
7478
7488
  if (!solidOnly || layer.isSolid)
7479
- if (pos.arrayCheck(layer.size))
7480
7489
  {
7481
- const data = layer.getCollisionData(pos);
7482
- if (data) return data;
7490
+ // convert world pos to layer local space
7491
+ const layerPos = pos.subtract(layer.pos);
7492
+ if (layerPos.arrayCheck(layer.size))
7493
+ {
7494
+ const data = layer.getCollisionData(layerPos);
7495
+ if (data) return data;
7496
+ }
7483
7497
  }
7484
7498
  return 0;
7485
7499
  }
@@ -7754,7 +7768,7 @@ class TileLayer extends CanvasLayer
7754
7768
 
7755
7769
  /** @property {TileInfo} - Default tile info for layer */
7756
7770
  this.tileInfo = undefined;
7757
- /** @property {Array<TileLayerData>} - Default tile info for layer */
7771
+ /** @property {Array<TileLayerData>} - Array of tile data for the layer */
7758
7772
  this.data = [];
7759
7773
  /** @property {boolean} - Is this layer using a webgl texture? */
7760
7774
  this.isUsingWebGL = false;
@@ -7839,7 +7853,7 @@ class TileLayer extends CanvasLayer
7839
7853
 
7840
7854
  const size = this.drawSize || this.size;
7841
7855
  const pos = this.pos.add(size.scale(.5));
7842
- this.draw(pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
7856
+ this.draw(pos, size, this.color, this.angle, this.mirror, this.additiveColor);
7843
7857
  }
7844
7858
 
7845
7859
  /** Called after this layer is redrawn, does nothing by default */
@@ -7928,7 +7942,7 @@ class TileLayer extends CanvasLayer
7928
7942
  const d = this.getData(layerPos);
7929
7943
  if (!d || !d.tile) return;
7930
7944
 
7931
- const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
7945
+ const tileInfo = this.tileInfo && this.tileInfo.index(d.tile);
7932
7946
  this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
7933
7947
  }
7934
7948
 
@@ -8222,8 +8236,8 @@ class TileCollisionLayer extends TileLayer
8222
8236
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
8223
8237
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
8224
8238
  * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
8225
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
8226
- * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
8239
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
8240
+ * .5, 1 // randomness, collide
8227
8241
  * );
8228
8242
  */
8229
8243
  class ParticleEmitter extends EngineObject
@@ -8615,13 +8629,12 @@ class Particle
8615
8629
  const hitLayer = tileCollisionTest(this.pos);
8616
8630
  if (!testCollision(oldPos))
8617
8631
  {
8618
- // testCollision already invoked collideCallback with the
8619
- // correct (this, data, pos) args; no need to re-check here.
8620
8632
  // test which side we bounced off (or both if a corner)
8621
8633
  const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
8622
8634
  const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
8623
- const hitRestitution = max(restitution, hitLayer.restitution);
8624
- const hitFriction = max(friction, hitLayer.friction);
8635
+ // collide callback may hit where the layer test does not, so hitLayer can be undefined
8636
+ const hitRestitution = hitLayer ? max(restitution, hitLayer.restitution) : restitution;
8637
+ const hitFriction = hitLayer ? max(friction, hitLayer.friction) : friction;
8625
8638
  if (isBlockedX)
8626
8639
  {
8627
8640
  // move to previous X position and bounce
@@ -10270,7 +10283,8 @@ class NewgroundsPlugin
10270
10283
  return;
10271
10284
  }
10272
10285
  debugMedals && LOG(xmlHttp.responseText);
10273
- return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
10286
+ try { return xmlHttp.responseText && JSON.parse(xmlHttp.responseText); }
10287
+ catch(e) { debugMedals && LOG('newgrounds response is not valid JSON', e); }
10274
10288
  }
10275
10289
  }
10276
10290
  /**
@@ -10289,8 +10303,8 @@ class NewgroundsPlugin
10289
10303
  let postProcess;
10290
10304
 
10291
10305
  /////////////////////////////////////////////////////////////////////////
10292
- /**
10293
- * UI System Global Object
10306
+ /**
10307
+ * Post Process Plugin - Applies a full screen shader to the rendered output
10294
10308
  * @memberof PostProcess
10295
10309
  */
10296
10310
  class PostProcessPlugin
@@ -10404,8 +10418,9 @@ class PostProcessPlugin
10404
10418
  workCanvas.height = mainCanvasSize.y;
10405
10419
  glCopyToContext(workContext);
10406
10420
  workContext.drawImage(mainCanvas, 0, 0);
10407
- mainCanvas.width |= 0
10408
-
10421
+ mainCanvas.width |= 0; // setting size clears the main canvas
10422
+
10423
+
10409
10424
  // copy work canvas to texture
10410
10425
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
10411
10426
  }
@@ -11510,7 +11525,7 @@ class UISystemPlugin
11510
11525
  }
11511
11526
 
11512
11527
  /** Get other axis navigation direction from gamepad or keyboard
11513
- * @return {Vector2} */
11528
+ * @return {number} */
11514
11529
  getNavigationOtherDirection()
11515
11530
  {
11516
11531
  if (uiSystem.navigationDirection === 2)
@@ -11599,6 +11614,7 @@ class UISystemPlugin
11599
11614
  uiSystem.navigationDirection = savedNavigationDirection;
11600
11615
  inputClear();
11601
11616
  }
11617
+ return confirmMenu;
11602
11618
  }
11603
11619
  }
11604
11620
 
@@ -12032,7 +12048,7 @@ class UITextInput extends UIObject
12032
12048
  this.onClick();
12033
12049
  }
12034
12050
 
12035
- /** Stop editing the text edited */
12051
+ /** Stop editing the text */
12036
12052
  stopEditing()
12037
12053
  {
12038
12054
  if (!this.isKeyInputObject())
@@ -12195,7 +12211,7 @@ class UICheckbox extends UIObject
12195
12211
  ASSERT(isStringLike(text), 'ui checkbox must be a string');
12196
12212
  ASSERT(isColor(color), 'ui checkbox color must be a color');
12197
12213
 
12198
- /** @property {boolean} - Current percentage value of this slider 0-1 */
12214
+ /** @property {boolean} - Is the checkbox currently checked? */
12199
12215
  this.checked = checked;
12200
12216
  // set properties
12201
12217
  this.text = text;
@@ -12960,7 +12976,7 @@ class Box2dObject extends EngineObject
12960
12976
  shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
12961
12977
  const f = this.addShape(shape, density, friction, restitution, isSensor);
12962
12978
  fixtures.push(f);
12963
- i < points.length && edgePoints.push(points[i].copy());
12979
+ edgePoints.push(points[i].copy());
12964
12980
  }
12965
12981
  this.edgeLoops.push(edgePoints);
12966
12982
  return fixtures;
@@ -13026,7 +13042,7 @@ class Box2dObject extends EngineObject
13026
13042
  /** Sets the position
13027
13043
  * @param {Vector2} pos */
13028
13044
  setPosition(pos)
13029
- { this.setTransform(pos, this.body.GetAngle()); }
13045
+ { this.setTransform(pos, -this.body.GetAngle()); }
13030
13046
 
13031
13047
  /** Sets the angle
13032
13048
  * @param {number} angle */
@@ -13123,6 +13139,7 @@ class Box2dObject extends EngineObject
13123
13139
  filter.set_categoryBits(categoryBits);
13124
13140
  filter.set_maskBits(0xffff & ~ignoreCategoryBits);
13125
13141
  filter.set_groupIndex(groupIndex);
13142
+ fixture.SetFilterData(filter); // applies and refilters contacts
13126
13143
  });
13127
13144
  }
13128
13145
 
@@ -13658,7 +13675,7 @@ class Box2dRevoluteJoint extends Box2dJoint
13658
13675
  jointDef.set_bodyB(objectB.body);
13659
13676
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
13660
13677
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
13661
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
13678
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
13662
13679
  jointDef.set_collideConnected(collide);
13663
13680
  super(jointDef);
13664
13681
  }
@@ -13805,14 +13822,14 @@ class Box2dPrismaticJoint extends Box2dJoint
13805
13822
  anchor ||= box2d.vec2From(objectB.body.GetPosition());
13806
13823
  const localAnchorA = objectA.worldToLocal(anchor);
13807
13824
  const localAnchorB = objectB.worldToLocal(anchor);
13808
- const localAxisA = objectB.worldToLocalVector(worldAxis);
13825
+ const localAxisA = objectA.worldToLocalVector(worldAxis);
13809
13826
  const jointDef = new box2d.instance.b2PrismaticJointDef();
13810
13827
  jointDef.set_bodyA(objectA.body);
13811
13828
  jointDef.set_bodyB(objectB.body);
13812
13829
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
13813
13830
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
13814
13831
  jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
13815
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
13832
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
13816
13833
  jointDef.set_collideConnected(collide);
13817
13834
  super(jointDef);
13818
13835
  }
@@ -13915,7 +13932,7 @@ class Box2dWheelJoint extends Box2dJoint
13915
13932
  anchor ||= box2d.vec2From(objectB.body.GetPosition());
13916
13933
  const localAnchorA = objectA.worldToLocal(anchor);
13917
13934
  const localAnchorB = objectB.worldToLocal(anchor);
13918
- const localAxisA = objectB.worldToLocalVector(worldAxis);
13935
+ const localAxisA = objectA.worldToLocalVector(worldAxis);
13919
13936
  const jointDef = new box2d.instance.b2WheelJointDef();
13920
13937
  jointDef.set_bodyA(objectA.body);
13921
13938
  jointDef.set_bodyB(objectB.body);
@@ -14015,7 +14032,7 @@ class Box2dWeldJoint extends Box2dJoint
14015
14032
  jointDef.set_bodyB(objectB.body);
14016
14033
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
14017
14034
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
14018
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
14035
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
14019
14036
  jointDef.set_collideConnected(collide);
14020
14037
  super(jointDef);
14021
14038
  }
@@ -14462,7 +14479,7 @@ class Box2dPlugin
14462
14479
  * @param {number} [lineWidth]
14463
14480
  * @param {boolean} [useWebGL=glEnable]
14464
14481
  * @param {CanvasRenderingContext2D} [context] */
14465
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebgl, context)
14482
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
14466
14483
  {
14467
14484
  const shape = box2d.castShapeObject(fixture.GetShape());
14468
14485
  switch (shape.GetType())
@@ -14472,20 +14489,20 @@ class Box2dPlugin
14472
14489
  let points = [];
14473
14490
  for (let i=shape.GetVertexCount(); i--;)
14474
14491
  points.push(box2d.vec2From(shape.GetVertex(i)));
14475
- drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebgl, false, context);
14492
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, false, context);
14476
14493
  break;
14477
14494
  }
14478
14495
  case box2d.instance.b2Shape.e_circle:
14479
14496
  {
14480
14497
  const radius = shape.get_m_radius();
14481
- drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebgl, false, context);
14498
+ drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebGL, false, context);
14482
14499
  break;
14483
14500
  }
14484
14501
  case box2d.instance.b2Shape.e_edge:
14485
14502
  {
14486
14503
  const v1 = box2d.vec2From(shape.get_m_vertex1());
14487
14504
  const v2 = box2d.vec2From(shape.get_m_vertex2());
14488
- drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebgl, false, context);
14505
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebGL, false, context);
14489
14506
  break;
14490
14507
  }
14491
14508
  }
@@ -15316,7 +15333,11 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
15316
15333
  const callback = (value) =>
15317
15334
  {
15318
15335
  let obj = target;
15319
- for (const k of parts) obj = obj[k];
15336
+ for (const k of parts)
15337
+ {
15338
+ obj = obj[k];
15339
+ ASSERT(obj != null, 'tweenProperty path does not resolve: ' + propertyPath);
15340
+ }
15320
15341
  obj[lastKey] = value;
15321
15342
  };
15322
15343
  return new Tween(callback, start, end, duration, options);
@@ -16199,3 +16220,157 @@ class PathFinder
16199
16220
  }
16200
16221
  }
16201
16222
 
16223
+ /**
16224
+ * LittleJS Three.js Plugin
16225
+ * - Renders a three.js scene on a canvas behind the LittleJS canvases
16226
+ * - The three.js module is passed in by the user, nothing is bundled
16227
+ * - Keep canvasClearColor transparent so the 3D scene shows through
16228
+ * - Aligned camera mode locks the 3D camera to the LittleJS 2D camera
16229
+ * - ThreeJSObject lets LittleJS physics drive a three.js mesh
16230
+ * - Call new ThreeJSPlugin(THREE) in gameInit to set up
16231
+ * @namespace ThreeJS
16232
+ */
16233
+
16234
+ ///////////////////////////////////////////////////////////////////////////////
16235
+
16236
+ /** Global ThreeJS plugin object
16237
+ * @type {ThreeJSPlugin}
16238
+ * @memberof ThreeJS */
16239
+ let threeJS;
16240
+
16241
+ ///////////////////////////////////////////////////////////////////////////////
16242
+ /**
16243
+ * ThreeJS Plugin - Renders a three.js scene behind the LittleJS canvas
16244
+ * @example
16245
+ * // in gameInit, with three.js loaded by the user
16246
+ * new ThreeJSPlugin(THREE);
16247
+ * threeJS.scene.add(new THREE.AmbientLight);
16248
+ * @memberof ThreeJS
16249
+ */
16250
+ class ThreeJSPlugin
16251
+ {
16252
+ /** Set up the three.js rendering layer, call in gameInit
16253
+ * @param {Object} THREE - The three.js module, supplied by the user
16254
+ * @param {number} [cameraFOV] - Vertical field of view in degrees */
16255
+ constructor(THREE, cameraFOV=60)
16256
+ {
16257
+ ASSERT(!threeJS, 'ThreeJS plugin already initialized');
16258
+ threeJS = this;
16259
+ if (headlessMode) return;
16260
+ ASSERT(mainCanvas, 'ThreeJS plugin must be created after engineInit, call in gameInit');
16261
+ ASSERT(THREE && THREE.WebGLRenderer, 'three.js module must be passed in');
16262
+
16263
+ /** @property {Object} - The three.js module passed into the constructor */
16264
+ this.THREE = THREE;
16265
+ /** @property {Object} - The three.js renderer */
16266
+ this.renderer = new THREE.WebGLRenderer({antialias: true});
16267
+ /** @property {Object} - The three.js scene, add lights and meshes here */
16268
+ this.scene = new THREE.Scene();
16269
+ /** @property {Object} - The three.js perspective camera */
16270
+ this.camera = new THREE.PerspectiveCamera(cameraFOV, 1, .1, 1e3);
16271
+ /** @property {boolean} - Lock the camera to the LittleJS 2D camera so the z=0 plane matches world space */
16272
+ this.cameraAlign2D = true;
16273
+
16274
+ // insert the canvas below the engine canvases and match the layout
16275
+ const threeCanvas = this.renderer.domElement;
16276
+ const rootElement = mainCanvas.parentElement;
16277
+ rootElement.insertBefore(threeCanvas, rootElement.firstChild);
16278
+ threeCanvas.style.cssText = mainCanvas.style.cssText;
16279
+
16280
+ // render automatically each frame after the engine renders
16281
+ engineAddPlugin(undefined, ()=> this.render());
16282
+ }
16283
+
16284
+ /** Position the camera so the z=0 plane exactly matches LittleJS world space,
16285
+ * called automatically when cameraAlign2D is set */
16286
+ alignCamera2D()
16287
+ {
16288
+ const halfHeight = mainCanvasSize.y / 2 / cameraScale; // half visible height in world units
16289
+ const distance = halfHeight / tan(this.camera.fov/2 * PI/180);
16290
+ this.camera.position.set(cameraPos.x, cameraPos.y, distance);
16291
+ // reset all axes in case a free camera was used, littlejs angles are clockwise
16292
+ this.camera.rotation.set(0, 0, -cameraAngle);
16293
+ }
16294
+
16295
+ /** Sync the canvas layout and render the scene, called automatically each frame */
16296
+ render()
16297
+ {
16298
+ if (!this.renderer) return; // headless mode
16299
+
16300
+ // keep renderer size and css in sync with the LittleJS canvas
16301
+ const threeCanvas = this.renderer.domElement;
16302
+ if (threeCanvas.width != mainCanvasSize.x || threeCanvas.height != mainCanvasSize.y)
16303
+ {
16304
+ this.renderer.setSize(mainCanvasSize.x, mainCanvasSize.y, false);
16305
+ this.camera.aspect = mainCanvasSize.x / mainCanvasSize.y;
16306
+ this.camera.updateProjectionMatrix();
16307
+ }
16308
+ if (threeCanvas.style.cssText != mainCanvas.style.cssText)
16309
+ threeCanvas.style.cssText = mainCanvas.style.cssText;
16310
+
16311
+ if (this.cameraAlign2D)
16312
+ this.alignCamera2D();
16313
+ this.renderer.render(this.scene, this.camera);
16314
+ }
16315
+ }
16316
+
16317
+ ///////////////////////////////////////////////////////////////////////////////
16318
+ /**
16319
+ * ThreeJS Object - EngineObject that drives a three.js mesh
16320
+ * - LittleJS physics moves the object and the mesh follows automatically
16321
+ * - Destroying the object removes the mesh from the scene
16322
+ * @extends EngineObject
16323
+ * @memberof ThreeJS
16324
+ */
16325
+ class ThreeJSObject extends EngineObject
16326
+ {
16327
+ /** Create an engine object that drives a three.js mesh
16328
+ * @param {Vector2} [pos] - World space position
16329
+ * @param {Vector2} [size] - World space size
16330
+ * @param {Object} [mesh] - The three.js object3d to drive
16331
+ * @param {number} [z] - Mesh height above the 2D plane */
16332
+ constructor(pos, size, mesh, z=0)
16333
+ {
16334
+ super(pos, size);
16335
+ ASSERT(threeJS, 'ThreeJS plugin must be initialized first');
16336
+
16337
+ /** @property {Object} - The three.js object3d this object drives */
16338
+ this.mesh = mesh;
16339
+ /** @property {number} - Mesh height above the 2D plane */
16340
+ this.z = z;
16341
+ if (mesh)
16342
+ {
16343
+ threeJS.scene.add(mesh);
16344
+ this.syncMesh();
16345
+ }
16346
+ }
16347
+
16348
+ /** Update the object and sync the mesh to its transform */
16349
+ update()
16350
+ {
16351
+ super.update();
16352
+ this.syncMesh();
16353
+ }
16354
+
16355
+ /** Copy this object's transform to the mesh */
16356
+ syncMesh()
16357
+ {
16358
+ if (!this.mesh) return;
16359
+ this.mesh.position.set(this.pos.x, this.pos.y, this.z);
16360
+ this.mesh.rotation.z = -this.angle; // littlejs angles are clockwise
16361
+ }
16362
+
16363
+ /** The mesh is this object's visual, the default 2D rendering is skipped */
16364
+ render() {}
16365
+
16366
+ /** Destroy this object and remove its mesh from the scene
16367
+ * @param {boolean} [immediate] */
16368
+ destroy(immediate)
16369
+ {
16370
+ if (this.destroyed) return;
16371
+ // note: frequently destroyed objects should also dispose geometry and material
16372
+ this.mesh && threeJS.scene.remove(this.mesh);
16373
+ super.destroy(immediate);
16374
+ }
16375
+ }
16376
+