littlejsengine 1.14.28 → 1.15.3

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.
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.15.0';
36
+ const engineVersion = '1.15.3';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -134,7 +134,7 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
134
134
 
135
135
  /**
136
136
  * @callback GameInitCallback - Called after the engine starts, can be async
137
- * @returns {void|Promise<void>}
137
+ * @return {void|Promise<void>}
138
138
  * @memberof Engine
139
139
  */
140
140
  /**
@@ -490,10 +490,10 @@ function engineObjectsDestroy()
490
490
  }
491
491
 
492
492
  /** Collects all object within a given area
493
- * @param {Vector2} [pos] - Center of test area, or undefined for all objects
494
- * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
493
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
494
+ * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
495
495
  * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
496
- * @return {Array<EngineObject>} - List of collected objects
496
+ * @return {Array<EngineObject>} - List of collected objects
497
497
  * @memberof Engine */
498
498
  function engineObjectsCollect(pos, size, objects=engineObjects)
499
499
  {
@@ -908,7 +908,7 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
908
908
  * @param {number} valueA
909
909
  * @param {number} valueB
910
910
  * @param {number} [wrapSize]
911
- * @returns {number}
911
+ * @return {number}
912
912
  * @memberof Utilities */
913
913
  function distanceWrap(valueA, valueB, wrapSize=1)
914
914
  { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
@@ -918,7 +918,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
918
918
  * @param {number} valueB
919
919
  * @param {number} percent
920
920
  * @param {number} [wrapSize]
921
- * @returns {number}
921
+ * @return {number}
922
922
  * @memberof Utilities */
923
923
  function lerpWrap(valueA, valueB, percent, wrapSize=1)
924
924
  {
@@ -930,7 +930,7 @@ function lerpWrap(valueA, valueB, percent, wrapSize=1)
930
930
  /** Returns signed wrapped distance between the two angles passed in
931
931
  * @param {number} angleA
932
932
  * @param {number} angleB
933
- * @returns {number}
933
+ * @return {number}
934
934
  * @memberof Utilities */
935
935
  function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
936
936
 
@@ -938,7 +938,7 @@ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*P
938
938
  * @param {number} angleA
939
939
  * @param {number} angleB
940
940
  * @param {number} percent
941
- * @returns {number}
941
+ * @return {number}
942
942
  * @memberof Utilities */
943
943
  function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
944
944
 
@@ -1907,11 +1907,14 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1907
1907
  class Timer
1908
1908
  {
1909
1909
  /** Create a timer object set time passed in
1910
- * @param {number} [timeLeft] - How much time left before the timer elapses in seconds */
1911
- constructor(timeLeft)
1910
+ * @param {number} [timeLeft] - How much time left before the timer
1911
+ * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
1912
+ constructor(timeLeft, useRealTime=false)
1912
1913
  {
1913
1914
  ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
1914
- this.time = timeLeft === undefined ? undefined : time + timeLeft;
1915
+ this.useRealTime = useRealTime;
1916
+ const globalTime = this.getGlobalTime();
1917
+ this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
1915
1918
  this.setTime = timeLeft;
1916
1919
  }
1917
1920
 
@@ -1920,10 +1923,19 @@ class Timer
1920
1923
  set(timeLeft=0)
1921
1924
  {
1922
1925
  ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
1923
- this.time = time + timeLeft;
1926
+ const globalTime = this.getGlobalTime();
1927
+ this.time = globalTime + timeLeft;
1924
1928
  this.setTime = timeLeft;
1925
1929
  }
1926
1930
 
1931
+ /** Set if the timer should keep running even when the game is paused
1932
+ * @param {boolean} [useRealTime] */
1933
+ setUseRealTime(useRealTime=true)
1934
+ {
1935
+ ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
1936
+ this.useRealTime = useRealTime;
1937
+ }
1938
+
1927
1939
  /** Unset the timer */
1928
1940
  unset() { this.time = undefined; }
1929
1941
 
@@ -1933,24 +1945,28 @@ class Timer
1933
1945
 
1934
1946
  /** Returns true if set and has not elapsed
1935
1947
  * @return {boolean} */
1936
- active() { return time < this.time; }
1948
+ active() { return this.getGlobalTime() < this.time; }
1937
1949
 
1938
1950
  /** Returns true if set and elapsed
1939
1951
  * @return {boolean} */
1940
- elapsed() { return time >= this.time; }
1952
+ elapsed() { return this.getGlobalTime() >= this.time; }
1941
1953
 
1942
1954
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1943
1955
  * @return {number} */
1944
- get() { return this.isSet()? time - this.time : 0; }
1956
+ get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
1945
1957
 
1946
1958
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
1947
1959
  * @return {number} */
1948
- getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
1960
+ getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
1949
1961
 
1950
1962
  /** Get the time this timer was set to, returns 0 if not set
1951
1963
  * @return {number} */
1952
1964
  getSetTime() { return this.isSet() ? this.setTime : 0; }
1953
1965
 
1966
+ /** Get the current global time this timer is based on
1967
+ * @return {number} */
1968
+ getGlobalTime() { return this.useRealTime ? timeReal : time; }
1969
+
1954
1970
  /** Returns this timer expressed as a string
1955
1971
  * @return {string} */
1956
1972
  toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
@@ -2981,7 +2997,9 @@ class EngineObject
2981
2997
  {
2982
2998
  ASSERT(child.parent === this && this.children.includes(child));
2983
2999
  ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
2984
- this.children.splice(this.children.indexOf(child), 1);
3000
+ const index = this.children.indexOf(child);
3001
+ ASSERT(index >= 0, 'child not found in children array');
3002
+ index >= 0 && this.children.splice(index, 1);
2985
3003
  child.parent = 0;
2986
3004
  }
2987
3005
 
@@ -3300,15 +3318,11 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3300
3318
 
3301
3319
  const textureInfo = tileInfo && tileInfo.textureInfo;
3302
3320
  const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
3303
- if (useWebGL)
3321
+ if (useWebGL && glEnable)
3304
3322
  {
3305
3323
  ASSERT(!!glContext, 'WebGL is not enabled!');
3306
3324
  if (screenSpace)
3307
- {
3308
- // convert to world space
3309
- pos = screenToWorld(pos);
3310
- size = size.scale(1/cameraScale);
3311
- }
3325
+ [pos, size, angle] = screenToWorldTransform(pos, size, angle);
3312
3326
  if (textureInfo)
3313
3327
  {
3314
3328
  // calculate uvs and render
@@ -3344,7 +3358,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3344
3358
  {
3345
3359
  // normal canvas 2D rendering method (slower)
3346
3360
  ++drawCount;
3347
- size = new Vector2(size.x, -size.y); // fix upside down sprites
3361
+ size = new Vector2(size.x, -size.y); // flip upside down sprites
3348
3362
  drawCanvas2D(pos, size, angle, mirror, (context)=>
3349
3363
  {
3350
3364
  if (textureInfo)
@@ -3396,7 +3410,8 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3396
3410
  ASSERT(isColor(colorTop) && isColor(colorBottom), 'color is invalid');
3397
3411
  ASSERT(isNumber(angle), 'angle must be a number');
3398
3412
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3399
- if (useWebGL)
3413
+
3414
+ if (useWebGL && glEnable)
3400
3415
  {
3401
3416
  ASSERT(!!glContext, 'WebGL is not enabled!');
3402
3417
  if (screenSpace)
@@ -3404,6 +3419,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3404
3419
  // convert to world space
3405
3420
  pos = screenToWorld(pos);
3406
3421
  size = size.scale(1/cameraScale);
3422
+ angle += cameraAngle;
3407
3423
  }
3408
3424
  // build 4 corner points for the rectangle
3409
3425
  const points = [], colors = [];
@@ -3459,17 +3475,14 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3459
3475
  ASSERT(isVector2(pos), 'pos must be a vec2');
3460
3476
  ASSERT(isNumber(angle), 'angle must be a number');
3461
3477
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3462
- if (useWebGL)
3478
+
3479
+ if (useWebGL && glEnable)
3463
3480
  {
3464
3481
  ASSERT(!!glContext, 'WebGL is not enabled!');
3465
- let scale = 1;
3482
+ let size = vec2(1);
3466
3483
  if (screenSpace)
3467
- {
3468
- // convert to world space
3469
- pos = screenToWorld(pos);
3470
- scale = 1/cameraScale;
3471
- }
3472
- glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, scale, scale, angle, wrap);
3484
+ [pos, size, angle] = screenToWorldTransform(pos, size, angle);
3485
+ glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, size.x, size.y, angle, wrap);
3473
3486
  }
3474
3487
  else
3475
3488
  {
@@ -3565,19 +3578,15 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3565
3578
  ASSERT(isNumber(angle), 'angle must be a number');
3566
3579
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3567
3580
 
3568
- if (useWebGL)
3581
+ if (useWebGL && glEnable)
3569
3582
  {
3570
3583
  ASSERT(!!glContext, 'WebGL is not enabled!');
3571
- let scale = 1;
3584
+ let size = vec2(1);
3572
3585
  if (screenSpace)
3573
- {
3574
- // convert to world space
3575
- pos = screenToWorld(pos);
3576
- scale = 1/cameraScale;
3577
- }
3578
- glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
3586
+ [pos, size, angle] = screenToWorldTransform(pos, size, angle);
3587
+ glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, size.x, size.y, angle);
3579
3588
  if (lineWidth > 0)
3580
- glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, scale, scale, angle);
3589
+ glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, size.x, size.y, angle);
3581
3590
  }
3582
3591
  else
3583
3592
  {
@@ -3620,7 +3629,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
3620
3629
  ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
3621
3630
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3622
3631
 
3623
- if (useWebGL)
3632
+ if (useWebGL && glEnable)
3624
3633
  {
3625
3634
  // draw as a regular polygon
3626
3635
  const sides = glCircleSides;
@@ -3683,14 +3692,10 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3683
3692
  ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
3684
3693
 
3685
3694
  if (!screenSpace)
3686
- {
3687
- // transform from world space to screen space
3688
- pos = worldToScreen(pos);
3689
- size = size.scale(cameraScale);
3690
- }
3695
+ [pos, size, angle] = worldToScreenTransform(pos, size, angle);
3691
3696
  context.save();
3692
3697
  context.translate(pos.x+.5, pos.y+.5);
3693
- context.rotate(angle-cameraAngle);
3698
+ context.rotate(angle);
3694
3699
  context.scale(mirror ? -size.x : size.x, -size.y);
3695
3700
  drawFunction(context);
3696
3701
  context.restore();
@@ -3716,7 +3721,14 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3716
3721
  * @memberof Draw */
3717
3722
  function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
3718
3723
  {
3719
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
3724
+ // convert to screen space
3725
+ pos = worldToScreen(pos);
3726
+ size *= cameraScale;
3727
+ lineWidth *= cameraScale;
3728
+ angle -= cameraAngle;
3729
+ angle *= -1;
3730
+
3731
+ drawTextScreen(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
3720
3732
  }
3721
3733
 
3722
3734
  /** Draw text on overlay canvas in world space
@@ -3875,6 +3887,36 @@ function worldToScreenDelta(worldDelta)
3875
3887
  return new Vector2(x * cameraScale, y * -cameraScale);
3876
3888
  }
3877
3889
 
3890
+ /** Convert screen space transform to world space
3891
+ * @param {Vector2} screenPos
3892
+ * @param {Vector2} screenSize
3893
+ * @param {number} [screenAngle]
3894
+ * @return {[Vector2, Vector2, number]} - [pos, size, angle]
3895
+ * @memberof Draw */
3896
+ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
3897
+ {
3898
+ return [
3899
+ screenToWorld(screenPos),
3900
+ screenSize.scale(1/cameraScale),
3901
+ screenAngle + cameraAngle
3902
+ ];
3903
+ }
3904
+
3905
+ /** Convert world space transform to screen space
3906
+ * @param {Vector2} worldPos
3907
+ * @param {Vector2} worldSize
3908
+ * @param {number} [worldAngle]
3909
+ * @return {[Vector2, Vector2, number]} - [pos, size, angle]
3910
+ * @memberof Draw */
3911
+ function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
3912
+ {
3913
+ return [
3914
+ worldToScreen(worldPos),
3915
+ worldSize.scale(cameraScale),
3916
+ worldAngle - cameraAngle
3917
+ ];
3918
+ }
3919
+
3878
3920
  /** Get the camera's visible area in world space
3879
3921
  * @return {Vector2}
3880
3922
  * @memberof Draw */
@@ -4282,7 +4324,7 @@ function inputUpdate()
4282
4324
  if (headlessMode) return;
4283
4325
 
4284
4326
  // clear input when lost focus (prevent stuck keys)
4285
- if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
4327
+ if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
4286
4328
  inputClear();
4287
4329
 
4288
4330
  // update mouse world space position and delta
@@ -4840,7 +4882,6 @@ class Sound
4840
4882
  ASSERT(isNumber(pitch), 'pitch must be a number');
4841
4883
  ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
4842
4884
 
4843
-
4844
4885
  if (!soundEnable || headlessMode) return;
4845
4886
  if (!this.sampleChannels) return;
4846
4887
 
@@ -5973,7 +6014,7 @@ class TileCollisionLayer extends TileLayer
5973
6014
  // remove from collision layers array and destroy
5974
6015
  const index = tileCollisionLayers.indexOf(this);
5975
6016
  ASSERT(index >= 0, 'tile collision layer not found in array');
5976
- tileCollisionLayers.splice(index, 1);
6017
+ index >= 0 && tileCollisionLayers.splice(index, 1);
5977
6018
  super.destroy();
5978
6019
  }
5979
6020
 
@@ -6235,6 +6276,9 @@ class ParticleEmitter extends EngineObject
6235
6276
  this.previousPos = this.pos.copy();
6236
6277
  }
6237
6278
 
6279
+ /** Emitters do not have physics */
6280
+ updatePhysics() {}
6281
+
6238
6282
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
6239
6283
  update()
6240
6284
  {
@@ -8016,6 +8060,7 @@ class UISystemPlugin
8016
8060
  ASSERT(!uiSystem, 'UI system already initialized');
8017
8061
  uiSystem = this;
8018
8062
 
8063
+ // default settings
8019
8064
  /** @property {Color} - Default fill color for UI elements */
8020
8065
  this.defaultColor = WHITE;
8021
8066
  /** @property {Color} - Default outline color for UI elements */
@@ -8044,6 +8089,13 @@ class UISystemPlugin
8044
8089
  this.defaultSoundRelease = undefined;
8045
8090
  /** @property {Sound} - Default sound when interactive UI element is clicked */
8046
8091
  this.defaultSoundClick = undefined;
8092
+ /** @property {Color} - Color for shadow */
8093
+ this.defaultShadowColor = CLEAR_BLACK;
8094
+ /** @property {number} - Size of shadow blur */
8095
+ this.defaultShadowBlur = 5;
8096
+ /** @property {Vector2} - Offset of shadow blur */
8097
+ this.defaultShadowOffset = vec2(5);
8098
+ // system state
8047
8099
  /** @property {Array<UIObject>} - List of all UI elements */
8048
8100
  this.uiObjects = [];
8049
8101
  /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
@@ -8088,11 +8140,16 @@ class UISystemPlugin
8088
8140
  // reset hover object at start of update
8089
8141
  uiSystem.lastHoverObject = uiSystem.hoverObject;
8090
8142
  uiSystem.hoverObject = undefined;
8143
+
8144
+ // update in reverse order so topmost objects get priority
8091
8145
  for (let i = uiSystem.uiObjects.length; i--;)
8092
8146
  {
8093
8147
  const o = uiSystem.uiObjects[i];
8094
8148
  o.parent || updateObject(o)
8095
8149
  }
8150
+
8151
+ // remove destroyed objects
8152
+ uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
8096
8153
  }
8097
8154
  function uiRender()
8098
8155
  {
@@ -8129,8 +8186,11 @@ class UISystemPlugin
8129
8186
  * @param {number} [lineWidth]
8130
8187
  * @param {Color} [lineColor]
8131
8188
  * @param {number} [cornerRadius]
8132
- * @param {Color} [gradientColor] */
8133
- drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
8189
+ * @param {Color} [gradientColor]
8190
+ * @param {Color} [shadowColor]
8191
+ * @param {number} [shadowBlur]
8192
+ * @param {Color} [shadowOffset] */
8193
+ drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
8134
8194
  {
8135
8195
  ASSERT(isVector2(pos), 'pos must be a vec2');
8136
8196
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -8152,12 +8212,22 @@ class UISystemPlugin
8152
8212
  }
8153
8213
  else
8154
8214
  context.fillStyle = color.toString();
8215
+ if (shadowBlur || shadowOffset.x || shadowOffset.y)
8216
+ if (shadowColor.a > 0)
8217
+ {
8218
+ // setup shadow
8219
+ context.shadowColor = shadowColor.toString();
8220
+ context.shadowBlur = shadowBlur;
8221
+ context.shadowOffsetX = shadowOffset.x;
8222
+ context.shadowOffsetY = shadowOffset.y;
8223
+ }
8155
8224
  context.beginPath();
8156
8225
  if (cornerRadius && context['roundRect'])
8157
8226
  context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
8158
8227
  else
8159
8228
  context.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
8160
8229
  context.fill();
8230
+ context.shadowColor = '#0000'
8161
8231
  if (lineWidth)
8162
8232
  {
8163
8233
  context.strokeStyle = lineColor.toString();
@@ -8262,6 +8332,15 @@ class UISystemPlugin
8262
8332
  p.x -= sInv*mainCanvasSize.x/2;
8263
8333
  return p;
8264
8334
  }
8335
+
8336
+ /** Destroy and remove all objects
8337
+ * @memberof Engine */
8338
+ destroyObjects()
8339
+ {
8340
+ for (const o of this.uiObjects)
8341
+ o.parent || o.destroy();
8342
+ this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
8343
+ }
8265
8344
  }
8266
8345
 
8267
8346
  ///////////////////////////////////////////////////////////////////////////////
@@ -8337,6 +8416,12 @@ class UIObject
8337
8416
  this.dragActivate = false;
8338
8417
  /** @property {boolean} - True if this can be a hover object */
8339
8418
  this.canBeHover = true;
8419
+ /** @property {Color} - Color for shadow, undefined if no shadow */
8420
+ this.shadowColor = uiSystem.defaultShadowColor?.copy();
8421
+ /** @property {number} - Size of shadow blur */
8422
+ this.shadowBlur = uiSystem.defaultShadowBlur;
8423
+ /** @property {Vector2} - Offset of shadow blur */
8424
+ this.shadowOffset = uiSystem.defaultShadowOffset.copy();
8340
8425
  uiSystem.uiObjects.push(this);
8341
8426
 
8342
8427
  /** @property {Vector2} - How much to offset the text shadow or undefined */
@@ -8363,6 +8448,22 @@ class UIObject
8363
8448
  child.parent = undefined;
8364
8449
  }
8365
8450
 
8451
+
8452
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
8453
+ destroy()
8454
+ {
8455
+ if (this.destroyed)
8456
+ return;
8457
+
8458
+ // disconnect from parent and destroy children
8459
+ this.destroyed = 1;
8460
+ this.parent && this.parent.removeChild(this);
8461
+ for (const child of this.children)
8462
+ {
8463
+ child.parent = 0;
8464
+ child.destroy();
8465
+ }
8466
+ }
8366
8467
  /** Check if the mouse is overlapping a box in screen space
8367
8468
  * @return {boolean} - True if overlapping
8368
8469
  */
@@ -8438,7 +8539,7 @@ class UIObject
8438
8539
 
8439
8540
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
8440
8541
  const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8441
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
8542
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
8442
8543
  }
8443
8544
 
8444
8545
  /** Special update when object is not visible */
@@ -8742,6 +8843,150 @@ class UIScrollbar extends UIObject
8742
8843
  uiSystem.drawText(this.text, this.pos, textSize,
8743
8844
  this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
8744
8845
  }
8846
+ }
8847
+
8848
+ ///////////////////////////////////////////////////////////////////////////////
8849
+ /**
8850
+ * VideoPlayerUIObject - A UI object that plays video
8851
+ * @extends UIObject
8852
+ * @example
8853
+ * // Create a video player UI object
8854
+ * const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'cutscene.mp4', true);
8855
+ * video.play();
8856
+ * @memberof UISystem
8857
+ */
8858
+ class UIVideo extends UIObject
8859
+ {
8860
+ /** Create a video player UI object
8861
+ * @param {Vector2} [pos]
8862
+ * @param {Vector2} [size]
8863
+ * @param {string} src - Video file path or URL
8864
+ * @param {boolean} [autoplay=false] - Start playing immediately?
8865
+ * @param {boolean} [loop=false] - Loop the video?
8866
+ * @param {number} [volume=1] - Volume percent scaled by global volume (0-1)
8867
+ */
8868
+ constructor(pos, size, src, autoplay=false, loop=false, volume=1)
8869
+ {
8870
+ super(pos, size || vec2());
8871
+
8872
+ ASSERT(isString(src), 'video src must be a string');
8873
+ ASSERT(isNumber(volume), 'video volume must be a number');
8874
+
8875
+ this.color = BLACK; // default to black background
8876
+ this.cornerRadius = 0; // default to no corner radius
8877
+
8878
+ /** @property {float} - The video volume */
8879
+ this.volume = volume;
8880
+
8881
+ // create video element
8882
+ /** @property {HTMLVideoElement} - The video player */
8883
+ this.video = document.createElement('video');
8884
+ this.video.loop = loop;
8885
+ this.video.volume = clamp(volume * soundVolume);
8886
+ this.video.muted = !soundEnable;
8887
+ this.video.style.display = 'none';
8888
+ this.video.src = src;
8889
+ document.body.appendChild(this.video);
8890
+ autoplay && this.play();
8891
+ }
8892
+
8893
+ /** Play or resume the video
8894
+ * @return {Promise} Promise that resolves when playback starts */
8895
+ play()
8896
+ {
8897
+ // try to play the video, catch any errors (autoplay may be blocked)
8898
+ const promise = this.video.play();
8899
+ promise?.catch(()=>{});
8900
+ return promise;
8901
+ }
8902
+
8903
+ /** Pause the video */
8904
+ pause() { this.video.pause(); }
8905
+
8906
+ /** Stop and reset the video */
8907
+ stop() { this.video.pause(); this.video.currentTime = 0; }
8908
+
8909
+ /** Check if video is currently loading
8910
+ * @return {boolean} */
8911
+ isLoadng()
8912
+ { return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
8913
+
8914
+ /** Check if video is currently paused
8915
+ * @return {boolean} */
8916
+ isPaused() { return this.video.paused; }
8917
+
8918
+ /** Check if video is currently playing
8919
+ * @return {boolean} */
8920
+ isPlaying()
8921
+ { return !this.isPaused() && !this.hasEnded() && !this.isLoadng(); }
8922
+
8923
+ /** Check if video has ended playing
8924
+ * @return {boolean} */
8925
+ hasEnded() { return this.video.ended; }
8926
+
8927
+ /** Set volume (0-1)
8928
+ * @param {number} volume - Volume level (0-1) */
8929
+ setVolume(volume)
8930
+ {
8931
+ this.volume = volume;
8932
+ this.video.volume = clamp(volume * soundVolume);
8933
+ }
8934
+
8935
+ /** Set playback speed
8936
+ * @param {number} rate - Playback rate multiplier */
8937
+ setPlaybackRate(rate) { this.video.playbackRate = rate; }
8938
+
8939
+ /** Get current time in seconds
8940
+ * @return {number} Current playback time */
8941
+ getCurrentTime() { return this.video.currentTime || 0; }
8942
+
8943
+ /** Get duration in seconds
8944
+ * @return {number} Total video duration */
8945
+ getDuration() { return this.video.duration || 0; }
8946
+
8947
+ /** Get the native video dimensions
8948
+ * @return {Vector2} Video dimensions (may be 0,0 if metadata not loaded) */
8949
+ getVideoSize()
8950
+ { return vec2(this.video.videoWidth, this.video.videoHeight); }
8951
+
8952
+ /** Seek to time in seconds
8953
+ * @param {number} time - Time in seconds to seek to */
8954
+ setTime(time)
8955
+ { this.video.currentTime = clamp(time, 0, this.getDuration()); }
8956
+
8957
+ update()
8958
+ {
8959
+ super.update();
8960
+
8961
+ // update volume based on global sound volume
8962
+ this.video.volume = clamp(this.volume * soundVolume);
8963
+ }
8964
+
8965
+ /** Render video to UI canvas */
8966
+ render()
8967
+ {
8968
+ super.render();
8969
+
8970
+ if (this.isLoadng())
8971
+ return;
8972
+ const context = uiSystem.uiContext;
8973
+ const s = this.size;
8974
+ context.save();
8975
+ context.translate(this.pos.x, this.pos.y);
8976
+ context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
8977
+ context.restore();
8978
+ }
8979
+
8980
+ /** Clean up video on destroy */
8981
+ destroy()
8982
+ {
8983
+ if (this.destroyed)
8984
+ return;
8985
+
8986
+ this.video.pause();
8987
+ this.video.remove();
8988
+ super.destroy();
8989
+ }
8745
8990
  }
8746
8991
  /**
8747
8992
  * LittleJS Box2D Physics Plugin
@@ -7,7 +7,7 @@
7
7
  </head><body>
8
8
 
9
9
  <!-- LittleJS Engine -->
10
- <script src=../../dist/littlejs.js?1.15.0></script>
10
+ <script src=../../dist/littlejs.js?1.15.3></script>
11
11
 
12
12
  <!-- Add your game scripts here -->
13
- <script src=game.js?1.15.0></script>
13
+ <script src=game.js?1.15.3></script>
@@ -99,6 +99,7 @@ const exampleList =
99
99
  new ExampleInfo('Timers', 'timers.js', 'Timer objects and UI', false, 'delay, interval, slider'),
100
100
  new ExampleInfo('Sound Effects', 'sound.js', 'ZzFX sound effect generator', false, 'audio, volume, ui'),
101
101
  new ExampleInfo('Music', 'music.js', 'Basic load, play, pause, and stop', false, 'music, sound, audio, streaming, volume, ui'),
102
+ new ExampleInfo('Video', 'videoPlayer.js', 'Basic video play, pause, and stop', false, 'movie, sound, audio, streaming, volume, ui'),
102
103
  new ExampleInfo('Font Image', 'systemFont.js', 'Bitmap font system with built-in system font', false, 'text, characters'),
103
104
  new ExampleInfo('Medals', 'medals.js', 'Achievement system', false, 'unlock, progress, newgrounds'),
104
105
  new ExampleInfo('Tile Raycast', 'tileRaycast.js', 'Example of the tile layer raycast collision', false, 'level, map, grid'),
@@ -1,8 +1,9 @@
1
1
  <!DOCTYPE html><meta charset=utf-8><body>
2
- <script src=../../dist/littlejs.js?1.15.0></script>
2
+ <script src=../../dist/littlejs.js?1.15.3></script>
3
3
  <script src=../../dist/box2d.wasm.js></script>
4
4
 
5
5
  <!-- LittleJS Engine Source
6
+ <script src=../../src/engine.js></script>
6
7
  <script src=../../src/engineDebug.js></script>
7
8
  <script src=../../src/engineUtilities.js></script>
8
9
  <script src=../../src/engineSettings.js></script>
@@ -14,7 +15,6 @@
14
15
  <script src=../../src/engineParticles.js></script>
15
16
  <script src=../../src/engineMedals.js></script>
16
17
  <script src=../../src/engineWebGL.js></script>
17
- <script src=../../src/engine.js></script>
18
18
  <script src=../../plugins/box2d.js></script>
19
19
  <script src=../../plugins/box2d.wasm.js></script>
20
20
  <script src=../../plugins/drawUtilities.js></script>
@@ -39,7 +39,7 @@ class CarObject extends Box2dObject
39
39
  joint.setSpringFrequencyHz(frequency);
40
40
  joint.setMaxMotorTorque(maxTorque);
41
41
  joint.enableMotor(!i);
42
- const friction = 1;
42
+ const friction = 20;
43
43
  wheel.addCircle(2, vec2(), 1, friction);
44
44
  wheel.motorJoint = joint;
45
45
  this.wheels[i] = wheel;