littlejsengine 1.14.28 → 1.15.2

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.2';
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'; }
@@ -3344,7 +3360,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3344
3360
  {
3345
3361
  // normal canvas 2D rendering method (slower)
3346
3362
  ++drawCount;
3347
- size = new Vector2(size.x, -size.y); // fix upside down sprites
3363
+ size = new Vector2(size.x, -size.y); // flip upside down sprites
3348
3364
  drawCanvas2D(pos, size, angle, mirror, (context)=>
3349
3365
  {
3350
3366
  if (textureInfo)
@@ -6235,6 +6251,9 @@ class ParticleEmitter extends EngineObject
6235
6251
  this.previousPos = this.pos.copy();
6236
6252
  }
6237
6253
 
6254
+ /** Emitters do not have physics */
6255
+ updatePhysics() {}
6256
+
6238
6257
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
6239
6258
  update()
6240
6259
  {
@@ -8088,11 +8107,16 @@ class UISystemPlugin
8088
8107
  // reset hover object at start of update
8089
8108
  uiSystem.lastHoverObject = uiSystem.hoverObject;
8090
8109
  uiSystem.hoverObject = undefined;
8110
+
8111
+ // update in reverse order so topmost objects get priority
8091
8112
  for (let i = uiSystem.uiObjects.length; i--;)
8092
8113
  {
8093
8114
  const o = uiSystem.uiObjects[i];
8094
8115
  o.parent || updateObject(o)
8095
8116
  }
8117
+
8118
+ // remove destroyed objects
8119
+ uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
8096
8120
  }
8097
8121
  function uiRender()
8098
8122
  {
@@ -8262,6 +8286,15 @@ class UISystemPlugin
8262
8286
  p.x -= sInv*mainCanvasSize.x/2;
8263
8287
  return p;
8264
8288
  }
8289
+
8290
+ /** Destroy and remove all objects
8291
+ * @memberof Engine */
8292
+ destroyObjects()
8293
+ {
8294
+ for (const o of this.uiObjects)
8295
+ o.parent || o.destroy();
8296
+ this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
8297
+ }
8265
8298
  }
8266
8299
 
8267
8300
  ///////////////////////////////////////////////////////////////////////////////
@@ -8363,6 +8396,22 @@ class UIObject
8363
8396
  child.parent = undefined;
8364
8397
  }
8365
8398
 
8399
+
8400
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
8401
+ destroy()
8402
+ {
8403
+ if (this.destroyed)
8404
+ return;
8405
+
8406
+ // disconnect from parent and destroy children
8407
+ this.destroyed = 1;
8408
+ this.parent && this.parent.removeChild(this);
8409
+ for (const child of this.children)
8410
+ {
8411
+ child.parent = 0;
8412
+ child.destroy();
8413
+ }
8414
+ }
8366
8415
  /** Check if the mouse is overlapping a box in screen space
8367
8416
  * @return {boolean} - True if overlapping
8368
8417
  */
@@ -8742,6 +8791,150 @@ class UIScrollbar extends UIObject
8742
8791
  uiSystem.drawText(this.text, this.pos, textSize,
8743
8792
  this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
8744
8793
  }
8794
+ }
8795
+
8796
+ ///////////////////////////////////////////////////////////////////////////////
8797
+ /**
8798
+ * VideoPlayerUIObject - A UI object that plays video
8799
+ * @extends UIObject
8800
+ * @example
8801
+ * // Create a video player UI object
8802
+ * const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'cutscene.mp4', true);
8803
+ * video.play();
8804
+ * @memberof UISystem
8805
+ */
8806
+ class UIVideo extends UIObject
8807
+ {
8808
+ /** Create a video player UI object
8809
+ * @param {Vector2} [pos]
8810
+ * @param {Vector2} [size]
8811
+ * @param {string} src - Video file path or URL
8812
+ * @param {boolean} [autoplay=false] - Start playing immediately?
8813
+ * @param {boolean} [loop=false] - Loop the video?
8814
+ * @param {number} [volume=1] - Volume percent scaled by global volume (0-1)
8815
+ */
8816
+ constructor(pos, size, src, autoplay=false, loop=false, volume=1)
8817
+ {
8818
+ super(pos, size || vec2());
8819
+
8820
+ ASSERT(isString(src), 'video src must be a string');
8821
+ ASSERT(isNumber(volume), 'video volume must be a number');
8822
+
8823
+ this.color = BLACK; // default to black background
8824
+ this.cornerRadius = 0; // default to no corner radius
8825
+
8826
+ /** @property {float} - The video volume */
8827
+ this.volume = volume;
8828
+
8829
+ // create video element
8830
+ /** @property {HTMLVideoElement} - The video player */
8831
+ this.video = document.createElement('video');
8832
+ this.video.loop = loop;
8833
+ this.video.volume = clamp(volume * soundVolume);
8834
+ this.video.muted = !soundEnable;
8835
+ this.video.style.display = 'none';
8836
+ this.video.src = src;
8837
+ document.body.appendChild(this.video);
8838
+ autoplay && this.play();
8839
+ }
8840
+
8841
+ /** Play or resume the video
8842
+ * @return {Promise} Promise that resolves when playback starts */
8843
+ play()
8844
+ {
8845
+ // try to play the video, catch any errors (autoplay may be blocked)
8846
+ const promise = this.video.play();
8847
+ promise?.catch(()=>{});
8848
+ return promise;
8849
+ }
8850
+
8851
+ /** Pause the video */
8852
+ pause() { this.video.pause(); }
8853
+
8854
+ /** Stop and reset the video */
8855
+ stop() { this.video.pause(); this.video.currentTime = 0; }
8856
+
8857
+ /** Check if video is currently loading
8858
+ * @return {boolean} */
8859
+ isLoadng()
8860
+ { return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
8861
+
8862
+ /** Check if video is currently paused
8863
+ * @return {boolean} */
8864
+ isPaused() { return this.video.paused; }
8865
+
8866
+ /** Check if video is currently playing
8867
+ * @return {boolean} */
8868
+ isPlaying()
8869
+ { return !this.isPaused() && !this.hasEnded() && !this.isLoadng(); }
8870
+
8871
+ /** Check if video has ended playing
8872
+ * @return {boolean} */
8873
+ hasEnded() { return this.video.ended; }
8874
+
8875
+ /** Set volume (0-1)
8876
+ * @param {number} volume - Volume level (0-1) */
8877
+ setVolume(volume)
8878
+ {
8879
+ this.volume = volume;
8880
+ this.video.volume = clamp(volume * soundVolume);
8881
+ }
8882
+
8883
+ /** Set playback speed
8884
+ * @param {number} rate - Playback rate multiplier */
8885
+ setPlaybackRate(rate) { this.video.playbackRate = rate; }
8886
+
8887
+ /** Get current time in seconds
8888
+ * @return {number} Current playback time */
8889
+ getCurrentTime() { return this.video.currentTime || 0; }
8890
+
8891
+ /** Get duration in seconds
8892
+ * @return {number} Total video duration */
8893
+ getDuration() { return this.video.duration || 0; }
8894
+
8895
+ /** Get the native video dimensions
8896
+ * @return {Vector2} Video dimensions (may be 0,0 if metadata not loaded) */
8897
+ getVideoSize()
8898
+ { return vec2(this.video.videoWidth, this.video.videoHeight); }
8899
+
8900
+ /** Seek to time in seconds
8901
+ * @param {number} time - Time in seconds to seek to */
8902
+ setTime(time)
8903
+ { this.video.currentTime = clamp(time, 0, this.getDuration()); }
8904
+
8905
+ update()
8906
+ {
8907
+ super.update();
8908
+
8909
+ // update volume based on global sound volume
8910
+ this.video.volume = clamp(this.volume * soundVolume);
8911
+ }
8912
+
8913
+ /** Render video to UI canvas */
8914
+ render()
8915
+ {
8916
+ super.render();
8917
+
8918
+ if (this.isLoadng())
8919
+ return;
8920
+ const context = uiSystem.uiContext;
8921
+ const s = this.size;
8922
+ context.save();
8923
+ context.translate(this.pos.x, this.pos.y);
8924
+ context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
8925
+ context.restore();
8926
+ }
8927
+
8928
+ /** Clean up video on destroy */
8929
+ destroy()
8930
+ {
8931
+ if (this.destroyed)
8932
+ return;
8933
+
8934
+ this.video.pause();
8935
+ this.video.remove();
8936
+ super.destroy();
8937
+ }
8745
8938
  }
8746
8939
  /**
8747
8940
  * 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.2></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.2></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.2></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>
Binary file
Binary file
@@ -0,0 +1,33 @@
1
+ function gameInit()
2
+ {
3
+ new UISystemPlugin;
4
+ canvasClearColor = hsl(0,0,.1);
5
+ uiSystem.defaultCornerRadius = 20;
6
+
7
+ // video player
8
+ const center = mainCanvasSize.scale(.5);
9
+ const videoPos = center.add(vec2(0, -50));
10
+ const videoSize = vec2(480, 270);
11
+ const filename = 'video.webm';
12
+ const autoplay = true;
13
+ videoPlayer = new UIVideo(videoPos, videoSize, filename, autoplay);
14
+ videoPlayer.lineWidth = 5;
15
+ videoPlayer.lineColor = WHITE;
16
+
17
+ // play/pause button
18
+ const buttonSize = vec2(200, 100);
19
+ const playButtonPos = center.add(vec2(-130, 170));
20
+ const playButton = new UIButton(playButtonPos, buttonSize);
21
+ playButton.onClick = ()=> videoPlayer.isPaused() ?
22
+ videoPlayer.play() : videoPlayer.pause();
23
+ playButton.onUpdate = ()=> playButton.text =
24
+ videoPlayer.isPaused() ? 'Play' : 'Pause';
25
+
26
+ // status text
27
+ const statusTextPos = center.add(vec2(130, 170));
28
+ const statusText = new UIText(statusTextPos, vec2(250, 90));
29
+ statusText.textColor = WHITE;
30
+ statusText.onUpdate = ()=> statusText.text =
31
+ videoPlayer.hasEnded() ? 'Ended' :
32
+ videoPlayer.isPlaying() ? 'Playing' : 'Paused';
33
+ }
@@ -7,9 +7,10 @@
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.2></script>
11
11
 
12
12
  <!-- LittleJS Engine Source
13
+ <script src=../../src/engine.js></script>
13
14
  <script src=../../src/engineDebug.js></script>
14
15
  <script src=../../src/engineUtilities.js></script>
15
16
  <script src=../../src/engineSettings.js></script>
@@ -21,7 +22,6 @@
21
22
  <script src=../../src/engineParticles.js></script>
22
23
  <script src=../../src/engineMedals.js></script>
23
24
  <script src=../../src/engineWebGL.js></script>
24
- <script src=../../src/engine.js></script>
25
25
  <script src=../../plugins/box2d.js></script>
26
26
  <script src=../../plugins/box2d.wasm.js></script>
27
27
  <script src=../../plugins/drawUtilities.js></script>
@@ -31,4 +31,4 @@
31
31
  -->
32
32
 
33
33
  <!-- Add your game scripts here -->
34
- <script src=game.js?1.15.0></script>
34
+ <script src=game.js?1.15.2></script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.14.28",
3
+ "version": "1.15.2",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
@@ -25,6 +25,7 @@ export
25
25
  UIButton,
26
26
  UICheckbox,
27
27
  UIScrollbar,
28
+ UIVideo,
28
29
 
29
30
  // Box2D Physics
30
31
  box2d,
@@ -108,11 +108,16 @@ class UISystemPlugin
108
108
  // reset hover object at start of update
109
109
  uiSystem.lastHoverObject = uiSystem.hoverObject;
110
110
  uiSystem.hoverObject = undefined;
111
+
112
+ // update in reverse order so topmost objects get priority
111
113
  for (let i = uiSystem.uiObjects.length; i--;)
112
114
  {
113
115
  const o = uiSystem.uiObjects[i];
114
116
  o.parent || updateObject(o)
115
117
  }
118
+
119
+ // remove destroyed objects
120
+ uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
116
121
  }
117
122
  function uiRender()
118
123
  {
@@ -282,6 +287,15 @@ class UISystemPlugin
282
287
  p.x -= sInv*mainCanvasSize.x/2;
283
288
  return p;
284
289
  }
290
+
291
+ /** Destroy and remove all objects
292
+ * @memberof Engine */
293
+ destroyObjects()
294
+ {
295
+ for (const o of this.uiObjects)
296
+ o.parent || o.destroy();
297
+ this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
298
+ }
285
299
  }
286
300
 
287
301
  ///////////////////////////////////////////////////////////////////////////////
@@ -383,6 +397,22 @@ class UIObject
383
397
  child.parent = undefined;
384
398
  }
385
399
 
400
+
401
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
402
+ destroy()
403
+ {
404
+ if (this.destroyed)
405
+ return;
406
+
407
+ // disconnect from parent and destroy children
408
+ this.destroyed = 1;
409
+ this.parent && this.parent.removeChild(this);
410
+ for (const child of this.children)
411
+ {
412
+ child.parent = 0;
413
+ child.destroy();
414
+ }
415
+ }
386
416
  /** Check if the mouse is overlapping a box in screen space
387
417
  * @return {boolean} - True if overlapping
388
418
  */
@@ -762,4 +792,148 @@ class UIScrollbar extends UIObject
762
792
  uiSystem.drawText(this.text, this.pos, textSize,
763
793
  this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
764
794
  }
795
+ }
796
+
797
+ ///////////////////////////////////////////////////////////////////////////////
798
+ /**
799
+ * VideoPlayerUIObject - A UI object that plays video
800
+ * @extends UIObject
801
+ * @example
802
+ * // Create a video player UI object
803
+ * const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'cutscene.mp4', true);
804
+ * video.play();
805
+ * @memberof UISystem
806
+ */
807
+ class UIVideo extends UIObject
808
+ {
809
+ /** Create a video player UI object
810
+ * @param {Vector2} [pos]
811
+ * @param {Vector2} [size]
812
+ * @param {string} src - Video file path or URL
813
+ * @param {boolean} [autoplay=false] - Start playing immediately?
814
+ * @param {boolean} [loop=false] - Loop the video?
815
+ * @param {number} [volume=1] - Volume percent scaled by global volume (0-1)
816
+ */
817
+ constructor(pos, size, src, autoplay=false, loop=false, volume=1)
818
+ {
819
+ super(pos, size || vec2());
820
+
821
+ ASSERT(isString(src), 'video src must be a string');
822
+ ASSERT(isNumber(volume), 'video volume must be a number');
823
+
824
+ this.color = BLACK; // default to black background
825
+ this.cornerRadius = 0; // default to no corner radius
826
+
827
+ /** @property {float} - The video volume */
828
+ this.volume = volume;
829
+
830
+ // create video element
831
+ /** @property {HTMLVideoElement} - The video player */
832
+ this.video = document.createElement('video');
833
+ this.video.loop = loop;
834
+ this.video.volume = clamp(volume * soundVolume);
835
+ this.video.muted = !soundEnable;
836
+ this.video.style.display = 'none';
837
+ this.video.src = src;
838
+ document.body.appendChild(this.video);
839
+ autoplay && this.play();
840
+ }
841
+
842
+ /** Play or resume the video
843
+ * @return {Promise} Promise that resolves when playback starts */
844
+ play()
845
+ {
846
+ // try to play the video, catch any errors (autoplay may be blocked)
847
+ const promise = this.video.play();
848
+ promise?.catch(()=>{});
849
+ return promise;
850
+ }
851
+
852
+ /** Pause the video */
853
+ pause() { this.video.pause(); }
854
+
855
+ /** Stop and reset the video */
856
+ stop() { this.video.pause(); this.video.currentTime = 0; }
857
+
858
+ /** Check if video is currently loading
859
+ * @return {boolean} */
860
+ isLoadng()
861
+ { return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
862
+
863
+ /** Check if video is currently paused
864
+ * @return {boolean} */
865
+ isPaused() { return this.video.paused; }
866
+
867
+ /** Check if video is currently playing
868
+ * @return {boolean} */
869
+ isPlaying()
870
+ { return !this.isPaused() && !this.hasEnded() && !this.isLoadng(); }
871
+
872
+ /** Check if video has ended playing
873
+ * @return {boolean} */
874
+ hasEnded() { return this.video.ended; }
875
+
876
+ /** Set volume (0-1)
877
+ * @param {number} volume - Volume level (0-1) */
878
+ setVolume(volume)
879
+ {
880
+ this.volume = volume;
881
+ this.video.volume = clamp(volume * soundVolume);
882
+ }
883
+
884
+ /** Set playback speed
885
+ * @param {number} rate - Playback rate multiplier */
886
+ setPlaybackRate(rate) { this.video.playbackRate = rate; }
887
+
888
+ /** Get current time in seconds
889
+ * @return {number} Current playback time */
890
+ getCurrentTime() { return this.video.currentTime || 0; }
891
+
892
+ /** Get duration in seconds
893
+ * @return {number} Total video duration */
894
+ getDuration() { return this.video.duration || 0; }
895
+
896
+ /** Get the native video dimensions
897
+ * @return {Vector2} Video dimensions (may be 0,0 if metadata not loaded) */
898
+ getVideoSize()
899
+ { return vec2(this.video.videoWidth, this.video.videoHeight); }
900
+
901
+ /** Seek to time in seconds
902
+ * @param {number} time - Time in seconds to seek to */
903
+ setTime(time)
904
+ { this.video.currentTime = clamp(time, 0, this.getDuration()); }
905
+
906
+ update()
907
+ {
908
+ super.update();
909
+
910
+ // update volume based on global sound volume
911
+ this.video.volume = clamp(this.volume * soundVolume);
912
+ }
913
+
914
+ /** Render video to UI canvas */
915
+ render()
916
+ {
917
+ super.render();
918
+
919
+ if (this.isLoadng())
920
+ return;
921
+ const context = uiSystem.uiContext;
922
+ const s = this.size;
923
+ context.save();
924
+ context.translate(this.pos.x, this.pos.y);
925
+ context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
926
+ context.restore();
927
+ }
928
+
929
+ /** Clean up video on destroy */
930
+ destroy()
931
+ {
932
+ if (this.destroyed)
933
+ return;
934
+
935
+ this.video.pause();
936
+ this.video.remove();
937
+ super.destroy();
938
+ }
765
939
  }