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.
- package/dist/littlejs.d.ts +85 -10
- package/dist/littlejs.esm.js +212 -18
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +211 -18
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +211 -18
- package/examples/electron/index.html +2 -2
- package/examples/index.html +1 -0
- package/examples/shorts/base.html +2 -2
- package/examples/shorts/song.mp3 +0 -0
- package/examples/shorts/video.webm +0 -0
- package/examples/shorts/videoPlayer.js +33 -0
- package/examples/starter/index.html +3 -3
- package/package.json +1 -1
- package/plugins/pluginExport.js +1 -0
- package/plugins/uiSystem.js +174 -0
- package/src/engine.js +5 -5
- package/src/engineDraw.js +1 -1
- package/src/engineParticles.js +3 -0
- package/src/engineUtilities.js +28 -12
package/dist/littlejs.release.js
CHANGED
|
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
|
|
|
33
33
|
* @type {string}
|
|
34
34
|
* @default
|
|
35
35
|
* @memberof Engine */
|
|
36
|
-
const engineVersion = '1.15.
|
|
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
|
-
* @
|
|
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]
|
|
494
|
-
* @param {Vector2|number} [size]
|
|
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>}
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
|
1911
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
1948
|
+
active() { return this.getGlobalTime() < this.time; }
|
|
1937
1949
|
|
|
1938
1950
|
/** Returns true if set and elapsed
|
|
1939
1951
|
* @return {boolean} */
|
|
1940
|
-
elapsed() { return
|
|
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()?
|
|
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 -
|
|
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); //
|
|
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.
|
|
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.
|
|
13
|
+
<script src=game.js?1.15.2></script>
|
package/examples/index.html
CHANGED
|
@@ -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.
|
|
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>
|
package/examples/shorts/song.mp3
CHANGED
|
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.
|
|
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.
|
|
34
|
+
<script src=game.js?1.15.2></script>
|
package/package.json
CHANGED
package/plugins/pluginExport.js
CHANGED
package/plugins/uiSystem.js
CHANGED
|
@@ -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
|
}
|