littlejsengine 1.18.19 → 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.19';
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 */
@@ -4194,6 +4202,14 @@ class TileInfo
4194
4202
  return this.offset(new Vector2(x));
4195
4203
  }
4196
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
+
4197
4213
  /**
4198
4214
  * Set this tile to use a full image in a texture info
4199
4215
  * @param {TextureInfo} [textureInfo]
@@ -4207,14 +4223,6 @@ class TileInfo
4207
4223
  this.bleed = this.padding = 0;
4208
4224
  return this;
4209
4225
  }
4210
-
4211
- /**
4212
- * Returns a tile info for an index using this tile as reference
4213
- * @param {Vector2|number} [index=0]
4214
- * @return {TileInfo}
4215
- */
4216
- tile(index)
4217
- { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
4218
4226
  }
4219
4227
 
4220
4228
  /**
@@ -5912,7 +5920,10 @@ function inputInit()
5912
5920
  {
5913
5921
  inputData[0][e.code] = (inputData[0][e.code]&2) | 4;
5914
5922
  if (inputWASDEmulateDirection)
5915
- inputData[0][remapKey(e.code)] = 4;
5923
+ {
5924
+ const remap = remapKey(e.code);
5925
+ inputData[0][remap] = (inputData[0][remap]&2) | 4;
5926
+ }
5916
5927
  }
5917
5928
  function remapKey(k)
5918
5929
  {
@@ -5987,7 +5998,7 @@ function inputInit()
5987
5998
  document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
5988
5999
 
5989
6000
  // handle all touch events the same way
5990
- let wasTouching;
6001
+ let wasTouching, touchIdentifier;
5991
6002
  function handleTouch(e)
5992
6003
  {
5993
6004
  if (!touchInputEnable) return;
@@ -6018,10 +6029,11 @@ function inputInit()
6018
6029
  const pos = vec2(gameTouches[0].clientX, gameTouches[0].clientY);
6019
6030
  const mousePosScreenLast = mousePosScreen;
6020
6031
  mousePosScreen = mouseEventToScreen(pos);
6021
- if (wasTouching)
6032
+ if (wasTouching && gameTouches[0].identifier === touchIdentifier)
6022
6033
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
6023
- else
6034
+ else if (!wasTouching)
6024
6035
  inputData[0][button] = 3;
6036
+ touchIdentifier = gameTouches[0].identifier;
6025
6037
  }
6026
6038
  else if (wasTouching)
6027
6039
  inputData[0][button] = inputData[0][button] & 2 | 4;
@@ -7179,7 +7191,7 @@ class SoundInstance
7179
7191
  * @param {number} [rate] - How quickly to speak
7180
7192
  * @param {number} [pitch] - How much to change the pitch by
7181
7193
  * @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
7182
- * @return {SpeechSynthesisUtterance} - The utterance that was spoken
7194
+ * @return {SpeechSynthesisUtterance|undefined} - The utterance that was spoken, or undefined if speech is unavailable
7183
7195
  * @memberof Audio */
7184
7196
  function speak(text, volume=1, rate=1, pitch=1, language='')
7185
7197
  {
@@ -7232,7 +7244,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
7232
7244
  * @param {number} [pan] - How much to apply stereo panning
7233
7245
  * @param {boolean} [loop] - True if the sound should loop when it reaches the end
7234
7246
  * @param {number} [sampleRate=44100] - Sample rate for the sound
7235
- * @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)
7236
7248
  * @param {number} [offset] - Offset in seconds to start playback from
7237
7249
  * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
7238
7250
  * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
@@ -7474,10 +7486,14 @@ function tileCollisionGetData(pos, solidOnly=true)
7474
7486
  // check all tile collision layers
7475
7487
  for (const layer of tileCollisionLayers)
7476
7488
  if (!solidOnly || layer.isSolid)
7477
- if (pos.arrayCheck(layer.size))
7478
7489
  {
7479
- const data = layer.getCollisionData(pos);
7480
- 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
+ }
7481
7497
  }
7482
7498
  return 0;
7483
7499
  }
@@ -7752,7 +7768,7 @@ class TileLayer extends CanvasLayer
7752
7768
 
7753
7769
  /** @property {TileInfo} - Default tile info for layer */
7754
7770
  this.tileInfo = undefined;
7755
- /** @property {Array<TileLayerData>} - Default tile info for layer */
7771
+ /** @property {Array<TileLayerData>} - Array of tile data for the layer */
7756
7772
  this.data = [];
7757
7773
  /** @property {boolean} - Is this layer using a webgl texture? */
7758
7774
  this.isUsingWebGL = false;
@@ -7837,7 +7853,7 @@ class TileLayer extends CanvasLayer
7837
7853
 
7838
7854
  const size = this.drawSize || this.size;
7839
7855
  const pos = this.pos.add(size.scale(.5));
7840
- 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);
7841
7857
  }
7842
7858
 
7843
7859
  /** Called after this layer is redrawn, does nothing by default */
@@ -7926,7 +7942,7 @@ class TileLayer extends CanvasLayer
7926
7942
  const d = this.getData(layerPos);
7927
7943
  if (!d || !d.tile) return;
7928
7944
 
7929
- const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
7945
+ const tileInfo = this.tileInfo && this.tileInfo.index(d.tile);
7930
7946
  this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
7931
7947
  }
7932
7948
 
@@ -8220,8 +8236,8 @@ class TileCollisionLayer extends TileLayer
8220
8236
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
8221
8237
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
8222
8238
  * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
8223
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
8224
- * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
8239
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
8240
+ * .5, 1 // randomness, collide
8225
8241
  * );
8226
8242
  */
8227
8243
  class ParticleEmitter extends EngineObject
@@ -8613,13 +8629,12 @@ class Particle
8613
8629
  const hitLayer = tileCollisionTest(this.pos);
8614
8630
  if (!testCollision(oldPos))
8615
8631
  {
8616
- // testCollision already invoked collideCallback with the
8617
- // correct (this, data, pos) args; no need to re-check here.
8618
8632
  // test which side we bounced off (or both if a corner)
8619
8633
  const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
8620
8634
  const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
8621
- const hitRestitution = max(restitution, hitLayer.restitution);
8622
- 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;
8623
8638
  if (isBlockedX)
8624
8639
  {
8625
8640
  // move to previous X position and bounce
@@ -10268,7 +10283,8 @@ class NewgroundsPlugin
10268
10283
  return;
10269
10284
  }
10270
10285
  debugMedals && LOG(xmlHttp.responseText);
10271
- 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); }
10272
10288
  }
10273
10289
  }
10274
10290
  /**
@@ -10287,8 +10303,8 @@ class NewgroundsPlugin
10287
10303
  let postProcess;
10288
10304
 
10289
10305
  /////////////////////////////////////////////////////////////////////////
10290
- /**
10291
- * UI System Global Object
10306
+ /**
10307
+ * Post Process Plugin - Applies a full screen shader to the rendered output
10292
10308
  * @memberof PostProcess
10293
10309
  */
10294
10310
  class PostProcessPlugin
@@ -10402,8 +10418,9 @@ class PostProcessPlugin
10402
10418
  workCanvas.height = mainCanvasSize.y;
10403
10419
  glCopyToContext(workContext);
10404
10420
  workContext.drawImage(mainCanvas, 0, 0);
10405
- mainCanvas.width |= 0
10406
-
10421
+ mainCanvas.width |= 0; // setting size clears the main canvas
10422
+
10423
+
10407
10424
  // copy work canvas to texture
10408
10425
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
10409
10426
  }
@@ -11508,7 +11525,7 @@ class UISystemPlugin
11508
11525
  }
11509
11526
 
11510
11527
  /** Get other axis navigation direction from gamepad or keyboard
11511
- * @return {Vector2} */
11528
+ * @return {number} */
11512
11529
  getNavigationOtherDirection()
11513
11530
  {
11514
11531
  if (uiSystem.navigationDirection === 2)
@@ -11597,6 +11614,7 @@ class UISystemPlugin
11597
11614
  uiSystem.navigationDirection = savedNavigationDirection;
11598
11615
  inputClear();
11599
11616
  }
11617
+ return confirmMenu;
11600
11618
  }
11601
11619
  }
11602
11620
 
@@ -12030,7 +12048,7 @@ class UITextInput extends UIObject
12030
12048
  this.onClick();
12031
12049
  }
12032
12050
 
12033
- /** Stop editing the text edited */
12051
+ /** Stop editing the text */
12034
12052
  stopEditing()
12035
12053
  {
12036
12054
  if (!this.isKeyInputObject())
@@ -12193,7 +12211,7 @@ class UICheckbox extends UIObject
12193
12211
  ASSERT(isStringLike(text), 'ui checkbox must be a string');
12194
12212
  ASSERT(isColor(color), 'ui checkbox color must be a color');
12195
12213
 
12196
- /** @property {boolean} - Current percentage value of this slider 0-1 */
12214
+ /** @property {boolean} - Is the checkbox currently checked? */
12197
12215
  this.checked = checked;
12198
12216
  // set properties
12199
12217
  this.text = text;
@@ -12958,7 +12976,7 @@ class Box2dObject extends EngineObject
12958
12976
  shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
12959
12977
  const f = this.addShape(shape, density, friction, restitution, isSensor);
12960
12978
  fixtures.push(f);
12961
- i < points.length && edgePoints.push(points[i].copy());
12979
+ edgePoints.push(points[i].copy());
12962
12980
  }
12963
12981
  this.edgeLoops.push(edgePoints);
12964
12982
  return fixtures;
@@ -13024,7 +13042,7 @@ class Box2dObject extends EngineObject
13024
13042
  /** Sets the position
13025
13043
  * @param {Vector2} pos */
13026
13044
  setPosition(pos)
13027
- { this.setTransform(pos, this.body.GetAngle()); }
13045
+ { this.setTransform(pos, -this.body.GetAngle()); }
13028
13046
 
13029
13047
  /** Sets the angle
13030
13048
  * @param {number} angle */
@@ -13121,6 +13139,7 @@ class Box2dObject extends EngineObject
13121
13139
  filter.set_categoryBits(categoryBits);
13122
13140
  filter.set_maskBits(0xffff & ~ignoreCategoryBits);
13123
13141
  filter.set_groupIndex(groupIndex);
13142
+ fixture.SetFilterData(filter); // applies and refilters contacts
13124
13143
  });
13125
13144
  }
13126
13145
 
@@ -13656,7 +13675,7 @@ class Box2dRevoluteJoint extends Box2dJoint
13656
13675
  jointDef.set_bodyB(objectB.body);
13657
13676
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
13658
13677
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
13659
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
13678
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
13660
13679
  jointDef.set_collideConnected(collide);
13661
13680
  super(jointDef);
13662
13681
  }
@@ -13803,14 +13822,14 @@ class Box2dPrismaticJoint extends Box2dJoint
13803
13822
  anchor ||= box2d.vec2From(objectB.body.GetPosition());
13804
13823
  const localAnchorA = objectA.worldToLocal(anchor);
13805
13824
  const localAnchorB = objectB.worldToLocal(anchor);
13806
- const localAxisA = objectB.worldToLocalVector(worldAxis);
13825
+ const localAxisA = objectA.worldToLocalVector(worldAxis);
13807
13826
  const jointDef = new box2d.instance.b2PrismaticJointDef();
13808
13827
  jointDef.set_bodyA(objectA.body);
13809
13828
  jointDef.set_bodyB(objectB.body);
13810
13829
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
13811
13830
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
13812
13831
  jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
13813
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
13832
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
13814
13833
  jointDef.set_collideConnected(collide);
13815
13834
  super(jointDef);
13816
13835
  }
@@ -13913,7 +13932,7 @@ class Box2dWheelJoint extends Box2dJoint
13913
13932
  anchor ||= box2d.vec2From(objectB.body.GetPosition());
13914
13933
  const localAnchorA = objectA.worldToLocal(anchor);
13915
13934
  const localAnchorB = objectB.worldToLocal(anchor);
13916
- const localAxisA = objectB.worldToLocalVector(worldAxis);
13935
+ const localAxisA = objectA.worldToLocalVector(worldAxis);
13917
13936
  const jointDef = new box2d.instance.b2WheelJointDef();
13918
13937
  jointDef.set_bodyA(objectA.body);
13919
13938
  jointDef.set_bodyB(objectB.body);
@@ -14013,7 +14032,7 @@ class Box2dWeldJoint extends Box2dJoint
14013
14032
  jointDef.set_bodyB(objectB.body);
14014
14033
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
14015
14034
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
14016
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
14035
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
14017
14036
  jointDef.set_collideConnected(collide);
14018
14037
  super(jointDef);
14019
14038
  }
@@ -14460,7 +14479,7 @@ class Box2dPlugin
14460
14479
  * @param {number} [lineWidth]
14461
14480
  * @param {boolean} [useWebGL=glEnable]
14462
14481
  * @param {CanvasRenderingContext2D} [context] */
14463
- 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)
14464
14483
  {
14465
14484
  const shape = box2d.castShapeObject(fixture.GetShape());
14466
14485
  switch (shape.GetType())
@@ -14470,20 +14489,20 @@ class Box2dPlugin
14470
14489
  let points = [];
14471
14490
  for (let i=shape.GetVertexCount(); i--;)
14472
14491
  points.push(box2d.vec2From(shape.GetVertex(i)));
14473
- drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebgl, false, context);
14492
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, false, context);
14474
14493
  break;
14475
14494
  }
14476
14495
  case box2d.instance.b2Shape.e_circle:
14477
14496
  {
14478
14497
  const radius = shape.get_m_radius();
14479
- drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebgl, false, context);
14498
+ drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebGL, false, context);
14480
14499
  break;
14481
14500
  }
14482
14501
  case box2d.instance.b2Shape.e_edge:
14483
14502
  {
14484
14503
  const v1 = box2d.vec2From(shape.get_m_vertex1());
14485
14504
  const v2 = box2d.vec2From(shape.get_m_vertex2());
14486
- drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebgl, false, context);
14505
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebGL, false, context);
14487
14506
  break;
14488
14507
  }
14489
14508
  }
@@ -15314,7 +15333,11 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
15314
15333
  const callback = (value) =>
15315
15334
  {
15316
15335
  let obj = target;
15317
- 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
+ }
15318
15341
  obj[lastKey] = value;
15319
15342
  };
15320
15343
  return new Tween(callback, start, end, duration, options);
@@ -16197,3 +16220,157 @@ class PathFinder
16197
16220
  }
16198
16221
  }
16199
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
+