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