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.
- package/dist/littlejs.d.ts +117 -19
- package/dist/littlejs.esm.js +339 -79
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +338 -79
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +302 -57
- package/examples/electron/index.html +2 -2
- package/examples/index.html +1 -0
- package/examples/shorts/base.html +2 -2
- package/examples/shorts/box2dCar.js +1 -1
- package/examples/shorts/music.js +1 -0
- package/examples/shorts/musicPlayer.js +1 -0
- package/examples/shorts/sequencer.js +2 -0
- package/examples/shorts/song.mp3 +0 -0
- package/examples/shorts/sound.js +1 -0
- package/examples/shorts/timers.js +1 -0
- package/examples/shorts/uiSystem.js +1 -0
- package/examples/shorts/video.webm +0 -0
- package/examples/shorts/videoPlayer.js +34 -0
- package/examples/starter/index.html +3 -3
- package/examples/uiSystem/game.js +1 -0
- package/package.json +1 -1
- package/plugins/pluginExport.js +1 -0
- package/plugins/uiSystem.js +204 -3
- package/src/engine.js +5 -5
- package/src/engineAudio.js +0 -1
- package/src/engineDebug.js +36 -22
- package/src/engineDraw.js +57 -33
- package/src/engineInput.js +1 -1
- package/src/engineObject.js +3 -1
- package/src/engineParticles.js +3 -0
- package/src/engineTileLayer.js +1 -1
- package/src/engineUtilities.js +28 -12
package/dist/littlejs.esm.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.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
|
-
* @
|
|
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
|
{
|
|
@@ -790,8 +790,9 @@ function LOG(...output) { console.log(...output); }
|
|
|
790
790
|
* @param {number} [time]
|
|
791
791
|
* @param {number} [angle]
|
|
792
792
|
* @param {boolean} [fill]
|
|
793
|
+
* @param {boolean} [screenSpace]
|
|
793
794
|
* @memberof Debug */
|
|
794
|
-
function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
|
|
795
|
+
function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, screenSpace=false)
|
|
795
796
|
{
|
|
796
797
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
797
798
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -806,7 +807,7 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
|
|
|
806
807
|
pos = pos.copy();
|
|
807
808
|
size = size.copy();
|
|
808
809
|
const timer = new Timer(time);
|
|
809
|
-
debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill});
|
|
810
|
+
debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill, screenSpace});
|
|
810
811
|
}
|
|
811
812
|
|
|
812
813
|
/** Draw a debug poly in world space
|
|
@@ -816,8 +817,9 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
|
|
|
816
817
|
* @param {number} [time]
|
|
817
818
|
* @param {number} [angle]
|
|
818
819
|
* @param {boolean} [fill]
|
|
820
|
+
* @param {boolean} [screenSpace]
|
|
819
821
|
* @memberof Debug */
|
|
820
|
-
function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
|
|
822
|
+
function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false, screenSpace=false)
|
|
821
823
|
{
|
|
822
824
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
823
825
|
ASSERT(isArray(points), 'points must be an array');
|
|
@@ -830,7 +832,7 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
|
|
|
830
832
|
pos = pos.copy();
|
|
831
833
|
points = points.map(p=>p.copy());
|
|
832
834
|
const timer = new Timer(time);
|
|
833
|
-
debugPrimitives.push({pos, points, color, timer, angle, fill});
|
|
835
|
+
debugPrimitives.push({pos, points, color, timer, angle, fill, screenSpace});
|
|
834
836
|
}
|
|
835
837
|
|
|
836
838
|
/** Draw a debug circle in world space
|
|
@@ -839,8 +841,9 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
|
|
|
839
841
|
* @param {Color|string} [color]
|
|
840
842
|
* @param {number} [time]
|
|
841
843
|
* @param {boolean} [fill]
|
|
844
|
+
* @param {boolean} [screenSpace]
|
|
842
845
|
* @memberof Debug */
|
|
843
|
-
function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
|
|
846
|
+
function debugCircle(pos, size=0, color=WHITE, time=0, fill=false, screenSpace=false)
|
|
844
847
|
{
|
|
845
848
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
846
849
|
ASSERT(isNumber(size), 'size must be a number');
|
|
@@ -851,7 +854,7 @@ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
|
|
|
851
854
|
color = color.toString();
|
|
852
855
|
pos = pos.copy();
|
|
853
856
|
const timer = new Timer(time);
|
|
854
|
-
debugPrimitives.push({pos, size, color, timer, angle:0, fill});
|
|
857
|
+
debugPrimitives.push({pos, size, color, timer, angle:0, fill, screenSpace});
|
|
855
858
|
}
|
|
856
859
|
|
|
857
860
|
/** Draw a debug point in world space
|
|
@@ -859,9 +862,10 @@ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
|
|
|
859
862
|
* @param {Color|string} [color]
|
|
860
863
|
* @param {number} [time]
|
|
861
864
|
* @param {number} [angle]
|
|
865
|
+
* @param {boolean} [screenSpace]
|
|
862
866
|
* @memberof Debug */
|
|
863
|
-
function debugPoint(pos, color, time, angle)
|
|
864
|
-
{ debugRect(pos, undefined, color, time, angle); }
|
|
867
|
+
function debugPoint(pos, color, time, angle, screenSpace=false)
|
|
868
|
+
{ debugRect(pos, undefined, color, time, angle, false, screenSpace); }
|
|
865
869
|
|
|
866
870
|
/** Draw a debug line in world space
|
|
867
871
|
* @param {Vector2} posA
|
|
@@ -869,8 +873,9 @@ function debugPoint(pos, color, time, angle)
|
|
|
869
873
|
* @param {Color|string} [color]
|
|
870
874
|
* @param {number} [width]
|
|
871
875
|
* @param {number} [time]
|
|
876
|
+
* @param {boolean} [screenSpace]
|
|
872
877
|
* @memberof Debug */
|
|
873
|
-
function debugLine(posA, posB, color, width=.1, time)
|
|
878
|
+
function debugLine(posA, posB, color, width=.1, time, screenSpace=false)
|
|
874
879
|
{
|
|
875
880
|
ASSERT(isVector2(posA), 'posA must be a vec2');
|
|
876
881
|
ASSERT(isVector2(posB), 'posB must be sa vec2');
|
|
@@ -878,7 +883,7 @@ function debugLine(posA, posB, color, width=.1, time)
|
|
|
878
883
|
|
|
879
884
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
880
885
|
const size = vec2(width, halfDelta.length()*2);
|
|
881
|
-
debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
|
|
886
|
+
debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true, screenSpace);
|
|
882
887
|
}
|
|
883
888
|
|
|
884
889
|
/** Draw a debug combined axis aligned bounding box in world space
|
|
@@ -887,8 +892,10 @@ function debugLine(posA, posB, color, width=.1, time)
|
|
|
887
892
|
* @param {Vector2} posB
|
|
888
893
|
* @param {Vector2} sizeB
|
|
889
894
|
* @param {Color|string} [color]
|
|
895
|
+
* @param {number} [time]
|
|
896
|
+
* @param {boolean} [screenSpace]
|
|
890
897
|
* @memberof Debug */
|
|
891
|
-
function debugOverlap(posA, sizeA, posB, sizeB, color)
|
|
898
|
+
function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
|
|
892
899
|
{
|
|
893
900
|
ASSERT(isVector2(posA), 'posA must be a vec2');
|
|
894
901
|
ASSERT(isVector2(posB), 'posB must be a vec2');
|
|
@@ -903,7 +910,7 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
|
|
|
903
910
|
max(posA.x + sizeA.x/2, posB.x + sizeB.x/2),
|
|
904
911
|
max(posA.y + sizeA.y/2, posB.y + sizeB.y/2)
|
|
905
912
|
);
|
|
906
|
-
debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color);
|
|
913
|
+
debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color, time, 0, false, screenSpace);
|
|
907
914
|
}
|
|
908
915
|
|
|
909
916
|
/** Draw a debug axis aligned bounding box in world space
|
|
@@ -914,8 +921,9 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
|
|
|
914
921
|
* @param {number} [time]
|
|
915
922
|
* @param {number} [angle]
|
|
916
923
|
* @param {string} [font]
|
|
924
|
+
* @param {boolean} [screenSpace]
|
|
917
925
|
* @memberof Debug */
|
|
918
|
-
function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace')
|
|
926
|
+
function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace', screenSpace=false)
|
|
919
927
|
{
|
|
920
928
|
ASSERT(isString(text), 'text must be a string');
|
|
921
929
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
@@ -929,7 +937,7 @@ function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monosp
|
|
|
929
937
|
color = color.toString();
|
|
930
938
|
pos = pos.copy();
|
|
931
939
|
const timer = new Timer(time);
|
|
932
|
-
debugPrimitives.push({text, pos, size, color, timer, angle, font});
|
|
940
|
+
debugPrimitives.push({text, pos, size, color, timer, angle, font, screenSpace});
|
|
933
941
|
}
|
|
934
942
|
|
|
935
943
|
/** Clear all debug primitives in the list
|
|
@@ -1139,21 +1147,26 @@ function debugRender()
|
|
|
1139
1147
|
{
|
|
1140
1148
|
// draw debug primitives
|
|
1141
1149
|
overlayContext.lineWidth = 2;
|
|
1142
|
-
const pointSize = debugPointSize * cameraScale;
|
|
1143
1150
|
debugPrimitives.forEach(p=>
|
|
1144
1151
|
{
|
|
1145
1152
|
overlayContext.save();
|
|
1146
1153
|
|
|
1147
1154
|
// create canvas transform from world space to screen space
|
|
1148
|
-
|
|
1155
|
+
// without scaling because we want consistent pixel sizes
|
|
1156
|
+
let pos = p.pos, scale = 1, angle = p.angle;
|
|
1157
|
+
if (!p.screenSpace)
|
|
1158
|
+
{
|
|
1159
|
+
pos = worldToScreen(p.pos);
|
|
1160
|
+
scale = cameraScale;
|
|
1161
|
+
angle -= cameraAngle;
|
|
1162
|
+
}
|
|
1149
1163
|
overlayContext.translate(pos.x|0, pos.y|0);
|
|
1150
|
-
overlayContext.rotate(
|
|
1164
|
+
overlayContext.rotate(angle);
|
|
1151
1165
|
overlayContext.scale(1, p.text ? 1 : -1);
|
|
1152
1166
|
overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
|
|
1153
|
-
|
|
1154
1167
|
if (p.text !== undefined)
|
|
1155
1168
|
{
|
|
1156
|
-
overlayContext.font = p.size*
|
|
1169
|
+
overlayContext.font = p.size*scale + 'px '+ p.font;
|
|
1157
1170
|
overlayContext.textAlign = 'center';
|
|
1158
1171
|
overlayContext.textBaseline = 'middle';
|
|
1159
1172
|
overlayContext.fillText(p.text, 0, 0);
|
|
@@ -1164,7 +1177,7 @@ function debugRender()
|
|
|
1164
1177
|
overlayContext.beginPath();
|
|
1165
1178
|
for (const point of p.points)
|
|
1166
1179
|
{
|
|
1167
|
-
const p2 = point.scale(
|
|
1180
|
+
const p2 = point.scale(scale).floor();
|
|
1168
1181
|
overlayContext.lineTo(p2.x, p2.y);
|
|
1169
1182
|
}
|
|
1170
1183
|
overlayContext.closePath();
|
|
@@ -1174,13 +1187,14 @@ function debugRender()
|
|
|
1174
1187
|
else if (p.size === 0 || (p.size.x === 0 && p.size.y === 0))
|
|
1175
1188
|
{
|
|
1176
1189
|
// point
|
|
1190
|
+
const pointSize = debugPointSize * scale;
|
|
1177
1191
|
overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
|
|
1178
1192
|
overlayContext.fillRect(-1, -pointSize/2, 3, pointSize);
|
|
1179
1193
|
}
|
|
1180
1194
|
else if (p.size.x !== undefined)
|
|
1181
1195
|
{
|
|
1182
1196
|
// rect
|
|
1183
|
-
const s = p.size.scale(
|
|
1197
|
+
const s = p.size.scale(scale).floor();
|
|
1184
1198
|
const w = s.x, h = s.y;
|
|
1185
1199
|
p.fill && overlayContext.fillRect(-w/2|0, -h/2|0, w, h);
|
|
1186
1200
|
overlayContext.strokeRect(-w/2|0, -h/2|0, w, h);
|
|
@@ -1189,7 +1203,7 @@ function debugRender()
|
|
|
1189
1203
|
{
|
|
1190
1204
|
// circle
|
|
1191
1205
|
overlayContext.beginPath();
|
|
1192
|
-
overlayContext.arc(0, 0, p.size*
|
|
1206
|
+
overlayContext.arc(0, 0, p.size*scale/2, 0, 9);
|
|
1193
1207
|
p.fill && overlayContext.fill();
|
|
1194
1208
|
overlayContext.stroke();
|
|
1195
1209
|
}
|
|
@@ -1555,7 +1569,7 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
|
|
|
1555
1569
|
* @param {number} valueA
|
|
1556
1570
|
* @param {number} valueB
|
|
1557
1571
|
* @param {number} [wrapSize]
|
|
1558
|
-
* @
|
|
1572
|
+
* @return {number}
|
|
1559
1573
|
* @memberof Utilities */
|
|
1560
1574
|
function distanceWrap(valueA, valueB, wrapSize=1)
|
|
1561
1575
|
{ const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
|
|
@@ -1565,7 +1579,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
|
|
|
1565
1579
|
* @param {number} valueB
|
|
1566
1580
|
* @param {number} percent
|
|
1567
1581
|
* @param {number} [wrapSize]
|
|
1568
|
-
* @
|
|
1582
|
+
* @return {number}
|
|
1569
1583
|
* @memberof Utilities */
|
|
1570
1584
|
function lerpWrap(valueA, valueB, percent, wrapSize=1)
|
|
1571
1585
|
{
|
|
@@ -1577,7 +1591,7 @@ function lerpWrap(valueA, valueB, percent, wrapSize=1)
|
|
|
1577
1591
|
/** Returns signed wrapped distance between the two angles passed in
|
|
1578
1592
|
* @param {number} angleA
|
|
1579
1593
|
* @param {number} angleB
|
|
1580
|
-
* @
|
|
1594
|
+
* @return {number}
|
|
1581
1595
|
* @memberof Utilities */
|
|
1582
1596
|
function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
|
|
1583
1597
|
|
|
@@ -1585,7 +1599,7 @@ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*P
|
|
|
1585
1599
|
* @param {number} angleA
|
|
1586
1600
|
* @param {number} angleB
|
|
1587
1601
|
* @param {number} percent
|
|
1588
|
-
* @
|
|
1602
|
+
* @return {number}
|
|
1589
1603
|
* @memberof Utilities */
|
|
1590
1604
|
function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
|
|
1591
1605
|
|
|
@@ -2554,11 +2568,14 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
|
2554
2568
|
class Timer
|
|
2555
2569
|
{
|
|
2556
2570
|
/** Create a timer object set time passed in
|
|
2557
|
-
* @param {number} [timeLeft] - How much time left before the timer
|
|
2558
|
-
|
|
2571
|
+
* @param {number} [timeLeft] - How much time left before the timer
|
|
2572
|
+
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
2573
|
+
constructor(timeLeft, useRealTime=false)
|
|
2559
2574
|
{
|
|
2560
2575
|
ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
|
|
2561
|
-
this.
|
|
2576
|
+
this.useRealTime = useRealTime;
|
|
2577
|
+
const globalTime = this.getGlobalTime();
|
|
2578
|
+
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
2562
2579
|
this.setTime = timeLeft;
|
|
2563
2580
|
}
|
|
2564
2581
|
|
|
@@ -2567,10 +2584,19 @@ class Timer
|
|
|
2567
2584
|
set(timeLeft=0)
|
|
2568
2585
|
{
|
|
2569
2586
|
ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
2570
|
-
|
|
2587
|
+
const globalTime = this.getGlobalTime();
|
|
2588
|
+
this.time = globalTime + timeLeft;
|
|
2571
2589
|
this.setTime = timeLeft;
|
|
2572
2590
|
}
|
|
2573
2591
|
|
|
2592
|
+
/** Set if the timer should keep running even when the game is paused
|
|
2593
|
+
* @param {boolean} [useRealTime] */
|
|
2594
|
+
setUseRealTime(useRealTime=true)
|
|
2595
|
+
{
|
|
2596
|
+
ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
2597
|
+
this.useRealTime = useRealTime;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2574
2600
|
/** Unset the timer */
|
|
2575
2601
|
unset() { this.time = undefined; }
|
|
2576
2602
|
|
|
@@ -2580,24 +2606,28 @@ class Timer
|
|
|
2580
2606
|
|
|
2581
2607
|
/** Returns true if set and has not elapsed
|
|
2582
2608
|
* @return {boolean} */
|
|
2583
|
-
active() { return
|
|
2609
|
+
active() { return this.getGlobalTime() < this.time; }
|
|
2584
2610
|
|
|
2585
2611
|
/** Returns true if set and elapsed
|
|
2586
2612
|
* @return {boolean} */
|
|
2587
|
-
elapsed() { return
|
|
2613
|
+
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
2588
2614
|
|
|
2589
2615
|
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
2590
2616
|
* @return {number} */
|
|
2591
|
-
get() { return this.isSet()?
|
|
2617
|
+
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
2592
2618
|
|
|
2593
2619
|
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
2594
2620
|
* @return {number} */
|
|
2595
|
-
getPercent() { return this.isSet()? 1-percent(this.time -
|
|
2621
|
+
getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
|
|
2596
2622
|
|
|
2597
2623
|
/** Get the time this timer was set to, returns 0 if not set
|
|
2598
2624
|
* @return {number} */
|
|
2599
2625
|
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
2600
2626
|
|
|
2627
|
+
/** Get the current global time this timer is based on
|
|
2628
|
+
* @return {number} */
|
|
2629
|
+
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
2630
|
+
|
|
2601
2631
|
/** Returns this timer expressed as a string
|
|
2602
2632
|
* @return {string} */
|
|
2603
2633
|
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
@@ -3628,7 +3658,9 @@ class EngineObject
|
|
|
3628
3658
|
{
|
|
3629
3659
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
3630
3660
|
ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
3631
|
-
this.children.
|
|
3661
|
+
const index = this.children.indexOf(child);
|
|
3662
|
+
ASSERT(index >= 0, 'child not found in children array');
|
|
3663
|
+
index >= 0 && this.children.splice(index, 1);
|
|
3632
3664
|
child.parent = 0;
|
|
3633
3665
|
}
|
|
3634
3666
|
|
|
@@ -3947,15 +3979,11 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
|
|
|
3947
3979
|
|
|
3948
3980
|
const textureInfo = tileInfo && tileInfo.textureInfo;
|
|
3949
3981
|
const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
|
|
3950
|
-
if (useWebGL)
|
|
3982
|
+
if (useWebGL && glEnable)
|
|
3951
3983
|
{
|
|
3952
3984
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
3953
3985
|
if (screenSpace)
|
|
3954
|
-
|
|
3955
|
-
// convert to world space
|
|
3956
|
-
pos = screenToWorld(pos);
|
|
3957
|
-
size = size.scale(1/cameraScale);
|
|
3958
|
-
}
|
|
3986
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
3959
3987
|
if (textureInfo)
|
|
3960
3988
|
{
|
|
3961
3989
|
// calculate uvs and render
|
|
@@ -3991,7 +4019,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
|
|
|
3991
4019
|
{
|
|
3992
4020
|
// normal canvas 2D rendering method (slower)
|
|
3993
4021
|
++drawCount;
|
|
3994
|
-
size = new Vector2(size.x, -size.y); //
|
|
4022
|
+
size = new Vector2(size.x, -size.y); // flip upside down sprites
|
|
3995
4023
|
drawCanvas2D(pos, size, angle, mirror, (context)=>
|
|
3996
4024
|
{
|
|
3997
4025
|
if (textureInfo)
|
|
@@ -4043,7 +4071,8 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
4043
4071
|
ASSERT(isColor(colorTop) && isColor(colorBottom), 'color is invalid');
|
|
4044
4072
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4045
4073
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4046
|
-
|
|
4074
|
+
|
|
4075
|
+
if (useWebGL && glEnable)
|
|
4047
4076
|
{
|
|
4048
4077
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
4049
4078
|
if (screenSpace)
|
|
@@ -4051,6 +4080,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
4051
4080
|
// convert to world space
|
|
4052
4081
|
pos = screenToWorld(pos);
|
|
4053
4082
|
size = size.scale(1/cameraScale);
|
|
4083
|
+
angle += cameraAngle;
|
|
4054
4084
|
}
|
|
4055
4085
|
// build 4 corner points for the rectangle
|
|
4056
4086
|
const points = [], colors = [];
|
|
@@ -4106,17 +4136,14 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
4106
4136
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4107
4137
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4108
4138
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4109
|
-
|
|
4139
|
+
|
|
4140
|
+
if (useWebGL && glEnable)
|
|
4110
4141
|
{
|
|
4111
4142
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
4112
|
-
let
|
|
4143
|
+
let size = vec2(1);
|
|
4113
4144
|
if (screenSpace)
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
pos = screenToWorld(pos);
|
|
4117
|
-
scale = 1/cameraScale;
|
|
4118
|
-
}
|
|
4119
|
-
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, scale, scale, angle, wrap);
|
|
4145
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
4146
|
+
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, size.x, size.y, angle, wrap);
|
|
4120
4147
|
}
|
|
4121
4148
|
else
|
|
4122
4149
|
{
|
|
@@ -4212,19 +4239,15 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
4212
4239
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4213
4240
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4214
4241
|
|
|
4215
|
-
if (useWebGL)
|
|
4242
|
+
if (useWebGL && glEnable)
|
|
4216
4243
|
{
|
|
4217
4244
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
4218
|
-
let
|
|
4245
|
+
let size = vec2(1);
|
|
4219
4246
|
if (screenSpace)
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
pos = screenToWorld(pos);
|
|
4223
|
-
scale = 1/cameraScale;
|
|
4224
|
-
}
|
|
4225
|
-
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
|
|
4247
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
4248
|
+
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, size.x, size.y, angle);
|
|
4226
4249
|
if (lineWidth > 0)
|
|
4227
|
-
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y,
|
|
4250
|
+
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, size.x, size.y, angle);
|
|
4228
4251
|
}
|
|
4229
4252
|
else
|
|
4230
4253
|
{
|
|
@@ -4267,7 +4290,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
4267
4290
|
ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
|
|
4268
4291
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4269
4292
|
|
|
4270
|
-
if (useWebGL)
|
|
4293
|
+
if (useWebGL && glEnable)
|
|
4271
4294
|
{
|
|
4272
4295
|
// draw as a regular polygon
|
|
4273
4296
|
const sides = glCircleSides;
|
|
@@ -4330,14 +4353,10 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
4330
4353
|
ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
|
|
4331
4354
|
|
|
4332
4355
|
if (!screenSpace)
|
|
4333
|
-
|
|
4334
|
-
// transform from world space to screen space
|
|
4335
|
-
pos = worldToScreen(pos);
|
|
4336
|
-
size = size.scale(cameraScale);
|
|
4337
|
-
}
|
|
4356
|
+
[pos, size, angle] = worldToScreenTransform(pos, size, angle);
|
|
4338
4357
|
context.save();
|
|
4339
4358
|
context.translate(pos.x+.5, pos.y+.5);
|
|
4340
|
-
context.rotate(angle
|
|
4359
|
+
context.rotate(angle);
|
|
4341
4360
|
context.scale(mirror ? -size.x : size.x, -size.y);
|
|
4342
4361
|
drawFunction(context);
|
|
4343
4362
|
context.restore();
|
|
@@ -4363,7 +4382,14 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
4363
4382
|
* @memberof Draw */
|
|
4364
4383
|
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
|
|
4365
4384
|
{
|
|
4366
|
-
|
|
4385
|
+
// convert to screen space
|
|
4386
|
+
pos = worldToScreen(pos);
|
|
4387
|
+
size *= cameraScale;
|
|
4388
|
+
lineWidth *= cameraScale;
|
|
4389
|
+
angle -= cameraAngle;
|
|
4390
|
+
angle *= -1;
|
|
4391
|
+
|
|
4392
|
+
drawTextScreen(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
|
|
4367
4393
|
}
|
|
4368
4394
|
|
|
4369
4395
|
/** Draw text on overlay canvas in world space
|
|
@@ -4522,6 +4548,36 @@ function worldToScreenDelta(worldDelta)
|
|
|
4522
4548
|
return new Vector2(x * cameraScale, y * -cameraScale);
|
|
4523
4549
|
}
|
|
4524
4550
|
|
|
4551
|
+
/** Convert screen space transform to world space
|
|
4552
|
+
* @param {Vector2} screenPos
|
|
4553
|
+
* @param {Vector2} screenSize
|
|
4554
|
+
* @param {number} [screenAngle]
|
|
4555
|
+
* @return {[Vector2, Vector2, number]} - [pos, size, angle]
|
|
4556
|
+
* @memberof Draw */
|
|
4557
|
+
function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
|
|
4558
|
+
{
|
|
4559
|
+
return [
|
|
4560
|
+
screenToWorld(screenPos),
|
|
4561
|
+
screenSize.scale(1/cameraScale),
|
|
4562
|
+
screenAngle + cameraAngle
|
|
4563
|
+
];
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
/** Convert world space transform to screen space
|
|
4567
|
+
* @param {Vector2} worldPos
|
|
4568
|
+
* @param {Vector2} worldSize
|
|
4569
|
+
* @param {number} [worldAngle]
|
|
4570
|
+
* @return {[Vector2, Vector2, number]} - [pos, size, angle]
|
|
4571
|
+
* @memberof Draw */
|
|
4572
|
+
function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
|
|
4573
|
+
{
|
|
4574
|
+
return [
|
|
4575
|
+
worldToScreen(worldPos),
|
|
4576
|
+
worldSize.scale(cameraScale),
|
|
4577
|
+
worldAngle - cameraAngle
|
|
4578
|
+
];
|
|
4579
|
+
}
|
|
4580
|
+
|
|
4525
4581
|
/** Get the camera's visible area in world space
|
|
4526
4582
|
* @return {Vector2}
|
|
4527
4583
|
* @memberof Draw */
|
|
@@ -4929,7 +4985,7 @@ function inputUpdate()
|
|
|
4929
4985
|
if (headlessMode) return;
|
|
4930
4986
|
|
|
4931
4987
|
// clear input when lost focus (prevent stuck keys)
|
|
4932
|
-
if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
|
|
4988
|
+
if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
|
|
4933
4989
|
inputClear();
|
|
4934
4990
|
|
|
4935
4991
|
// update mouse world space position and delta
|
|
@@ -5487,7 +5543,6 @@ class Sound
|
|
|
5487
5543
|
ASSERT(isNumber(pitch), 'pitch must be a number');
|
|
5488
5544
|
ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
|
|
5489
5545
|
|
|
5490
|
-
|
|
5491
5546
|
if (!soundEnable || headlessMode) return;
|
|
5492
5547
|
if (!this.sampleChannels) return;
|
|
5493
5548
|
|
|
@@ -6620,7 +6675,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6620
6675
|
// remove from collision layers array and destroy
|
|
6621
6676
|
const index = tileCollisionLayers.indexOf(this);
|
|
6622
6677
|
ASSERT(index >= 0, 'tile collision layer not found in array');
|
|
6623
|
-
tileCollisionLayers.splice(index, 1);
|
|
6678
|
+
index >= 0 && tileCollisionLayers.splice(index, 1);
|
|
6624
6679
|
super.destroy();
|
|
6625
6680
|
}
|
|
6626
6681
|
|
|
@@ -6882,6 +6937,9 @@ class ParticleEmitter extends EngineObject
|
|
|
6882
6937
|
this.previousPos = this.pos.copy();
|
|
6883
6938
|
}
|
|
6884
6939
|
|
|
6940
|
+
/** Emitters do not have physics */
|
|
6941
|
+
updatePhysics() {}
|
|
6942
|
+
|
|
6885
6943
|
/** Update the emitter to spawn particles, called automatically by engine once each frame */
|
|
6886
6944
|
update()
|
|
6887
6945
|
{
|
|
@@ -8663,6 +8721,7 @@ class UISystemPlugin
|
|
|
8663
8721
|
ASSERT(!uiSystem, 'UI system already initialized');
|
|
8664
8722
|
uiSystem = this;
|
|
8665
8723
|
|
|
8724
|
+
// default settings
|
|
8666
8725
|
/** @property {Color} - Default fill color for UI elements */
|
|
8667
8726
|
this.defaultColor = WHITE;
|
|
8668
8727
|
/** @property {Color} - Default outline color for UI elements */
|
|
@@ -8691,6 +8750,13 @@ class UISystemPlugin
|
|
|
8691
8750
|
this.defaultSoundRelease = undefined;
|
|
8692
8751
|
/** @property {Sound} - Default sound when interactive UI element is clicked */
|
|
8693
8752
|
this.defaultSoundClick = undefined;
|
|
8753
|
+
/** @property {Color} - Color for shadow */
|
|
8754
|
+
this.defaultShadowColor = CLEAR_BLACK;
|
|
8755
|
+
/** @property {number} - Size of shadow blur */
|
|
8756
|
+
this.defaultShadowBlur = 5;
|
|
8757
|
+
/** @property {Vector2} - Offset of shadow blur */
|
|
8758
|
+
this.defaultShadowOffset = vec2(5);
|
|
8759
|
+
// system state
|
|
8694
8760
|
/** @property {Array<UIObject>} - List of all UI elements */
|
|
8695
8761
|
this.uiObjects = [];
|
|
8696
8762
|
/** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
|
|
@@ -8735,11 +8801,16 @@ class UISystemPlugin
|
|
|
8735
8801
|
// reset hover object at start of update
|
|
8736
8802
|
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
8737
8803
|
uiSystem.hoverObject = undefined;
|
|
8804
|
+
|
|
8805
|
+
// update in reverse order so topmost objects get priority
|
|
8738
8806
|
for (let i = uiSystem.uiObjects.length; i--;)
|
|
8739
8807
|
{
|
|
8740
8808
|
const o = uiSystem.uiObjects[i];
|
|
8741
8809
|
o.parent || updateObject(o)
|
|
8742
8810
|
}
|
|
8811
|
+
|
|
8812
|
+
// remove destroyed objects
|
|
8813
|
+
uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
|
|
8743
8814
|
}
|
|
8744
8815
|
function uiRender()
|
|
8745
8816
|
{
|
|
@@ -8776,8 +8847,11 @@ class UISystemPlugin
|
|
|
8776
8847
|
* @param {number} [lineWidth]
|
|
8777
8848
|
* @param {Color} [lineColor]
|
|
8778
8849
|
* @param {number} [cornerRadius]
|
|
8779
|
-
* @param {Color} [gradientColor]
|
|
8780
|
-
|
|
8850
|
+
* @param {Color} [gradientColor]
|
|
8851
|
+
* @param {Color} [shadowColor]
|
|
8852
|
+
* @param {number} [shadowBlur]
|
|
8853
|
+
* @param {Color} [shadowOffset] */
|
|
8854
|
+
drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
|
|
8781
8855
|
{
|
|
8782
8856
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
8783
8857
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -8799,12 +8873,22 @@ class UISystemPlugin
|
|
|
8799
8873
|
}
|
|
8800
8874
|
else
|
|
8801
8875
|
context.fillStyle = color.toString();
|
|
8876
|
+
if (shadowBlur || shadowOffset.x || shadowOffset.y)
|
|
8877
|
+
if (shadowColor.a > 0)
|
|
8878
|
+
{
|
|
8879
|
+
// setup shadow
|
|
8880
|
+
context.shadowColor = shadowColor.toString();
|
|
8881
|
+
context.shadowBlur = shadowBlur;
|
|
8882
|
+
context.shadowOffsetX = shadowOffset.x;
|
|
8883
|
+
context.shadowOffsetY = shadowOffset.y;
|
|
8884
|
+
}
|
|
8802
8885
|
context.beginPath();
|
|
8803
8886
|
if (cornerRadius && context['roundRect'])
|
|
8804
8887
|
context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
|
|
8805
8888
|
else
|
|
8806
8889
|
context.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
|
|
8807
8890
|
context.fill();
|
|
8891
|
+
context.shadowColor = '#0000'
|
|
8808
8892
|
if (lineWidth)
|
|
8809
8893
|
{
|
|
8810
8894
|
context.strokeStyle = lineColor.toString();
|
|
@@ -8909,6 +8993,15 @@ class UISystemPlugin
|
|
|
8909
8993
|
p.x -= sInv*mainCanvasSize.x/2;
|
|
8910
8994
|
return p;
|
|
8911
8995
|
}
|
|
8996
|
+
|
|
8997
|
+
/** Destroy and remove all objects
|
|
8998
|
+
* @memberof Engine */
|
|
8999
|
+
destroyObjects()
|
|
9000
|
+
{
|
|
9001
|
+
for (const o of this.uiObjects)
|
|
9002
|
+
o.parent || o.destroy();
|
|
9003
|
+
this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
|
|
9004
|
+
}
|
|
8912
9005
|
}
|
|
8913
9006
|
|
|
8914
9007
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -8984,6 +9077,12 @@ class UIObject
|
|
|
8984
9077
|
this.dragActivate = false;
|
|
8985
9078
|
/** @property {boolean} - True if this can be a hover object */
|
|
8986
9079
|
this.canBeHover = true;
|
|
9080
|
+
/** @property {Color} - Color for shadow, undefined if no shadow */
|
|
9081
|
+
this.shadowColor = uiSystem.defaultShadowColor?.copy();
|
|
9082
|
+
/** @property {number} - Size of shadow blur */
|
|
9083
|
+
this.shadowBlur = uiSystem.defaultShadowBlur;
|
|
9084
|
+
/** @property {Vector2} - Offset of shadow blur */
|
|
9085
|
+
this.shadowOffset = uiSystem.defaultShadowOffset.copy();
|
|
8987
9086
|
uiSystem.uiObjects.push(this);
|
|
8988
9087
|
|
|
8989
9088
|
/** @property {Vector2} - How much to offset the text shadow or undefined */
|
|
@@ -9010,6 +9109,22 @@ class UIObject
|
|
|
9010
9109
|
child.parent = undefined;
|
|
9011
9110
|
}
|
|
9012
9111
|
|
|
9112
|
+
|
|
9113
|
+
/** Destroy this object, destroy its children, detach its parent, and mark it for removal */
|
|
9114
|
+
destroy()
|
|
9115
|
+
{
|
|
9116
|
+
if (this.destroyed)
|
|
9117
|
+
return;
|
|
9118
|
+
|
|
9119
|
+
// disconnect from parent and destroy children
|
|
9120
|
+
this.destroyed = 1;
|
|
9121
|
+
this.parent && this.parent.removeChild(this);
|
|
9122
|
+
for (const child of this.children)
|
|
9123
|
+
{
|
|
9124
|
+
child.parent = 0;
|
|
9125
|
+
child.destroy();
|
|
9126
|
+
}
|
|
9127
|
+
}
|
|
9013
9128
|
/** Check if the mouse is overlapping a box in screen space
|
|
9014
9129
|
* @return {boolean} - True if overlapping
|
|
9015
9130
|
*/
|
|
@@ -9085,7 +9200,7 @@ class UIObject
|
|
|
9085
9200
|
|
|
9086
9201
|
const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
|
|
9087
9202
|
const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
|
|
9088
|
-
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
|
|
9203
|
+
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
9089
9204
|
}
|
|
9090
9205
|
|
|
9091
9206
|
/** Special update when object is not visible */
|
|
@@ -9389,6 +9504,150 @@ class UIScrollbar extends UIObject
|
|
|
9389
9504
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
9390
9505
|
this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
9391
9506
|
}
|
|
9507
|
+
}
|
|
9508
|
+
|
|
9509
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
9510
|
+
/**
|
|
9511
|
+
* VideoPlayerUIObject - A UI object that plays video
|
|
9512
|
+
* @extends UIObject
|
|
9513
|
+
* @example
|
|
9514
|
+
* // Create a video player UI object
|
|
9515
|
+
* const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'cutscene.mp4', true);
|
|
9516
|
+
* video.play();
|
|
9517
|
+
* @memberof UISystem
|
|
9518
|
+
*/
|
|
9519
|
+
class UIVideo extends UIObject
|
|
9520
|
+
{
|
|
9521
|
+
/** Create a video player UI object
|
|
9522
|
+
* @param {Vector2} [pos]
|
|
9523
|
+
* @param {Vector2} [size]
|
|
9524
|
+
* @param {string} src - Video file path or URL
|
|
9525
|
+
* @param {boolean} [autoplay=false] - Start playing immediately?
|
|
9526
|
+
* @param {boolean} [loop=false] - Loop the video?
|
|
9527
|
+
* @param {number} [volume=1] - Volume percent scaled by global volume (0-1)
|
|
9528
|
+
*/
|
|
9529
|
+
constructor(pos, size, src, autoplay=false, loop=false, volume=1)
|
|
9530
|
+
{
|
|
9531
|
+
super(pos, size || vec2());
|
|
9532
|
+
|
|
9533
|
+
ASSERT(isString(src), 'video src must be a string');
|
|
9534
|
+
ASSERT(isNumber(volume), 'video volume must be a number');
|
|
9535
|
+
|
|
9536
|
+
this.color = BLACK; // default to black background
|
|
9537
|
+
this.cornerRadius = 0; // default to no corner radius
|
|
9538
|
+
|
|
9539
|
+
/** @property {float} - The video volume */
|
|
9540
|
+
this.volume = volume;
|
|
9541
|
+
|
|
9542
|
+
// create video element
|
|
9543
|
+
/** @property {HTMLVideoElement} - The video player */
|
|
9544
|
+
this.video = document.createElement('video');
|
|
9545
|
+
this.video.loop = loop;
|
|
9546
|
+
this.video.volume = clamp(volume * soundVolume);
|
|
9547
|
+
this.video.muted = !soundEnable;
|
|
9548
|
+
this.video.style.display = 'none';
|
|
9549
|
+
this.video.src = src;
|
|
9550
|
+
document.body.appendChild(this.video);
|
|
9551
|
+
autoplay && this.play();
|
|
9552
|
+
}
|
|
9553
|
+
|
|
9554
|
+
/** Play or resume the video
|
|
9555
|
+
* @return {Promise} Promise that resolves when playback starts */
|
|
9556
|
+
play()
|
|
9557
|
+
{
|
|
9558
|
+
// try to play the video, catch any errors (autoplay may be blocked)
|
|
9559
|
+
const promise = this.video.play();
|
|
9560
|
+
promise?.catch(()=>{});
|
|
9561
|
+
return promise;
|
|
9562
|
+
}
|
|
9563
|
+
|
|
9564
|
+
/** Pause the video */
|
|
9565
|
+
pause() { this.video.pause(); }
|
|
9566
|
+
|
|
9567
|
+
/** Stop and reset the video */
|
|
9568
|
+
stop() { this.video.pause(); this.video.currentTime = 0; }
|
|
9569
|
+
|
|
9570
|
+
/** Check if video is currently loading
|
|
9571
|
+
* @return {boolean} */
|
|
9572
|
+
isLoadng()
|
|
9573
|
+
{ return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
|
|
9574
|
+
|
|
9575
|
+
/** Check if video is currently paused
|
|
9576
|
+
* @return {boolean} */
|
|
9577
|
+
isPaused() { return this.video.paused; }
|
|
9578
|
+
|
|
9579
|
+
/** Check if video is currently playing
|
|
9580
|
+
* @return {boolean} */
|
|
9581
|
+
isPlaying()
|
|
9582
|
+
{ return !this.isPaused() && !this.hasEnded() && !this.isLoadng(); }
|
|
9583
|
+
|
|
9584
|
+
/** Check if video has ended playing
|
|
9585
|
+
* @return {boolean} */
|
|
9586
|
+
hasEnded() { return this.video.ended; }
|
|
9587
|
+
|
|
9588
|
+
/** Set volume (0-1)
|
|
9589
|
+
* @param {number} volume - Volume level (0-1) */
|
|
9590
|
+
setVolume(volume)
|
|
9591
|
+
{
|
|
9592
|
+
this.volume = volume;
|
|
9593
|
+
this.video.volume = clamp(volume * soundVolume);
|
|
9594
|
+
}
|
|
9595
|
+
|
|
9596
|
+
/** Set playback speed
|
|
9597
|
+
* @param {number} rate - Playback rate multiplier */
|
|
9598
|
+
setPlaybackRate(rate) { this.video.playbackRate = rate; }
|
|
9599
|
+
|
|
9600
|
+
/** Get current time in seconds
|
|
9601
|
+
* @return {number} Current playback time */
|
|
9602
|
+
getCurrentTime() { return this.video.currentTime || 0; }
|
|
9603
|
+
|
|
9604
|
+
/** Get duration in seconds
|
|
9605
|
+
* @return {number} Total video duration */
|
|
9606
|
+
getDuration() { return this.video.duration || 0; }
|
|
9607
|
+
|
|
9608
|
+
/** Get the native video dimensions
|
|
9609
|
+
* @return {Vector2} Video dimensions (may be 0,0 if metadata not loaded) */
|
|
9610
|
+
getVideoSize()
|
|
9611
|
+
{ return vec2(this.video.videoWidth, this.video.videoHeight); }
|
|
9612
|
+
|
|
9613
|
+
/** Seek to time in seconds
|
|
9614
|
+
* @param {number} time - Time in seconds to seek to */
|
|
9615
|
+
setTime(time)
|
|
9616
|
+
{ this.video.currentTime = clamp(time, 0, this.getDuration()); }
|
|
9617
|
+
|
|
9618
|
+
update()
|
|
9619
|
+
{
|
|
9620
|
+
super.update();
|
|
9621
|
+
|
|
9622
|
+
// update volume based on global sound volume
|
|
9623
|
+
this.video.volume = clamp(this.volume * soundVolume);
|
|
9624
|
+
}
|
|
9625
|
+
|
|
9626
|
+
/** Render video to UI canvas */
|
|
9627
|
+
render()
|
|
9628
|
+
{
|
|
9629
|
+
super.render();
|
|
9630
|
+
|
|
9631
|
+
if (this.isLoadng())
|
|
9632
|
+
return;
|
|
9633
|
+
const context = uiSystem.uiContext;
|
|
9634
|
+
const s = this.size;
|
|
9635
|
+
context.save();
|
|
9636
|
+
context.translate(this.pos.x, this.pos.y);
|
|
9637
|
+
context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
|
|
9638
|
+
context.restore();
|
|
9639
|
+
}
|
|
9640
|
+
|
|
9641
|
+
/** Clean up video on destroy */
|
|
9642
|
+
destroy()
|
|
9643
|
+
{
|
|
9644
|
+
if (this.destroyed)
|
|
9645
|
+
return;
|
|
9646
|
+
|
|
9647
|
+
this.video.pause();
|
|
9648
|
+
this.video.remove();
|
|
9649
|
+
super.destroy();
|
|
9650
|
+
}
|
|
9392
9651
|
}
|
|
9393
9652
|
/**
|
|
9394
9653
|
* LittleJS Box2D Physics Plugin
|
|
@@ -11819,6 +12078,7 @@ export
|
|
|
11819
12078
|
UIButton,
|
|
11820
12079
|
UICheckbox,
|
|
11821
12080
|
UIScrollbar,
|
|
12081
|
+
UIVideo,
|
|
11822
12082
|
|
|
11823
12083
|
// Box2D Physics
|
|
11824
12084
|
box2d,
|