littlejsengine 1.26.0 → 1.26.1
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/CHANGELOG.md +12 -0
- package/REFERENCE.md +9 -3
- package/dist/littlejs.d.ts +23 -3
- package/dist/littlejs.esm.js +46 -16
- package/dist/littlejs.esm.min.js +2 -2
- package/dist/littlejs.js +45 -16
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +45 -16
- package/package.json +1 -1
- package/plugins/postProcess.js +5 -1
- package/plugins/render3d.js +14 -4
- package/src/engine.js +16 -5
- package/src/engineDraw.js +3 -0
- package/src/engineExport.js +1 -0
- package/src/engineInput.js +7 -6
package/dist/littlejs.release.js
CHANGED
|
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
|
|
|
35
35
|
* @type {string}
|
|
36
36
|
* @default
|
|
37
37
|
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.26.
|
|
38
|
+
const engineVersion = '1.26.1';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -69,6 +69,8 @@ let engineObjectsCollideStaticLast = [];
|
|
|
69
69
|
let frame = 0;
|
|
70
70
|
|
|
71
71
|
/** Current engine time since start in seconds
|
|
72
|
+
* - Below 20 FPS it falls behind the clock, since a frame catches up at most 50 ms of updates, so a timer in
|
|
73
|
+
* real seconds, one that must keep up on a slow device, goes by timeReal
|
|
72
74
|
* @type {number}
|
|
73
75
|
* @memberof Engine */
|
|
74
76
|
let time = 0;
|
|
@@ -94,8 +96,13 @@ function getPaused() { return paused; }
|
|
|
94
96
|
* @memberof Engine */
|
|
95
97
|
function setPaused(isPaused=true) { paused = isPaused; }
|
|
96
98
|
|
|
99
|
+
/** Frames drawn per second, smoothed over the last few seconds, in release builds too
|
|
100
|
+
* @type {number}
|
|
101
|
+
* @memberof Engine */
|
|
102
|
+
let averageFPS = 0;
|
|
103
|
+
|
|
97
104
|
// Engine internal variables
|
|
98
|
-
let frameTimeLastMS = 0, frameTimeBufferMS = 0
|
|
105
|
+
let frameTimeLastMS = 0, frameTimeBufferMS = 0;
|
|
99
106
|
|
|
100
107
|
// delta smoothing, after Time Delta Smoothing by Frank Force (2013): a frame is on screen for whole display frames,
|
|
101
108
|
// so each delta is rounded to them and the rest is carried to the next, keeping the total real time; the frame
|
|
@@ -436,7 +443,9 @@ function engineLoadingScreenDraw(elapsed)
|
|
|
436
443
|
* @param {GameCallback} [gameRender] - Called before objects are rendered, use for drawing backgrounds/world elements
|
|
437
444
|
* @param {GameCallback} [gameRenderPost] - Called after objects are rendered, use for drawing UI/overlays
|
|
438
445
|
* @param {Array<string>|string} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png']), or one path
|
|
439
|
-
* @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
|
|
446
|
+
* @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body; it is
|
|
447
|
+
* styled for a game (no scroll bars or selection, touch-action none),
|
|
448
|
+
* where its own inline style does not say otherwise
|
|
440
449
|
* It keeps its own inline styles and the canvas centers inside it, but the canvas is still sized from the window,
|
|
441
450
|
* so set canvasFixedSize or canvasMaxSize to fit a smaller element
|
|
442
451
|
* @example
|
|
@@ -499,8 +508,10 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
499
508
|
// by ~page-load-time when RAF starts handing real timestamps
|
|
500
509
|
if (!frameTimeLastMS) frameTimeDeltaMS = 0;
|
|
501
510
|
frameTimeLastMS = frameTimeMS;
|
|
502
|
-
|
|
503
|
-
|
|
511
|
+
// the first frame's rate seeds the average, so it does not start out climbing from 0; a gap of over a
|
|
512
|
+
// second, a hidden tab coming back, is not a frame
|
|
513
|
+
if (frameTimeDeltaMS && frameTimeDeltaMS < 1e3)
|
|
514
|
+
averageFPS = averageFPS ? lerp(averageFPS, 1e3/frameTimeDeltaMS, .05) : 1e3/frameTimeDeltaMS;
|
|
504
515
|
// the time the frame will be on screen, in whole display frames; engineStep's steps are exact already
|
|
505
516
|
if (!manualStepAtStart)
|
|
506
517
|
frameTimeDeltaMS = engineSmoothDelta(frameTimeDeltaMS);
|
|
@@ -5014,6 +5025,9 @@ class SpriteAnimator
|
|
|
5014
5025
|
* across the sprite or the mesh's own uv
|
|
5015
5026
|
* - Names in 3D only: worldPos, worldNormal, cameraPos, sunDirection, sunColor, ambientColor, ambientGroundColor,
|
|
5016
5027
|
* lightCount, lights[i], lightColors[i] and shadow()
|
|
5028
|
+
* - In 3D the snippet may also define void mainNormal(inout vec3 n), given the normal facing the camera after the
|
|
5029
|
+
* normal map, in world space, to bend it per pixel for waves or ripples; the lighting, specular, reflection and
|
|
5030
|
+
* fog then all use it, as they use a normal map
|
|
5017
5031
|
* - In 3D the shadow map is drawn without the Shader, cut only by the texture's alpha, so a snippet that removes
|
|
5018
5032
|
* parts of a surface still shadows with the whole of it
|
|
5019
5033
|
* @example
|
|
@@ -7165,8 +7179,9 @@ function inputInit()
|
|
|
7165
7179
|
}
|
|
7166
7180
|
function onContextMenu(e)
|
|
7167
7181
|
{
|
|
7168
|
-
// prevent right click menu, but a text field keeps its copy and paste menu
|
|
7169
|
-
|
|
7182
|
+
// prevent right click menu, but a text field keeps its copy and paste menu and a link its open in new tab
|
|
7183
|
+
const target = /** @type {HTMLElement} */ (e.target);
|
|
7184
|
+
if (inputPreventDefault && !isTextInput(target) && !target?.closest?.('a[href]'))
|
|
7170
7185
|
e.preventDefault();
|
|
7171
7186
|
}
|
|
7172
7187
|
function onBlur()
|
|
@@ -7187,6 +7202,10 @@ function inputInit()
|
|
|
7187
7202
|
// handle all touch events the same way
|
|
7188
7203
|
function handleTouch(e)
|
|
7189
7204
|
{
|
|
7205
|
+
// fix stalled audio requiring user interaction, with touch input off too since the page's sound needs it
|
|
7206
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
7207
|
+
audioResume();
|
|
7208
|
+
|
|
7190
7209
|
if (!touchInputEnable)
|
|
7191
7210
|
{
|
|
7192
7211
|
// turned off mid touch, the finger that drove the mouse lets go of it
|
|
@@ -7197,10 +7216,6 @@ function inputInit()
|
|
|
7197
7216
|
}
|
|
7198
7217
|
inputLastTouchTime = performance.now();
|
|
7199
7218
|
|
|
7200
|
-
// fix stalled audio requiring user interaction
|
|
7201
|
-
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
7202
|
-
audioResume();
|
|
7203
|
-
|
|
7204
7219
|
// when the touch gamepad is enabled it owns touch input: suppress the
|
|
7205
7220
|
// touch->mouse passthrough entirely unless touchGamepadPassthrough is set
|
|
7206
7221
|
// (its own zones drive gameplay via pointer events)
|
|
@@ -13459,6 +13474,10 @@ class PostProcessPlugin
|
|
|
13459
13474
|
/** @property {string} - The shadertoy style mainImage code it shades with, see setShaderCode */
|
|
13460
13475
|
this.shaderCode = shaderCode || postProcessEffects(); // no code passes the frame through
|
|
13461
13476
|
|
|
13477
|
+
/** @property {boolean} - Is the pass on? Off, the frame shows as the engine drew it, the shader is kept,
|
|
13478
|
+
* and the feedback texture holds the last frame drawn with it on */
|
|
13479
|
+
this.enabled = true;
|
|
13480
|
+
|
|
13462
13481
|
/** @property {Object<string, number|Array<number>>} - The game's own values for the shader, a uniform each
|
|
13463
13482
|
* by its name, a number a float and a list of 2 to 4 numbers a vector, set every frame as they are; an
|
|
13464
13483
|
* effect setting can be one of these names, so it changes every frame without making the shader again, all
|
|
@@ -13544,7 +13563,7 @@ class PostProcessPlugin
|
|
|
13544
13563
|
}
|
|
13545
13564
|
function postProcessRender()
|
|
13546
13565
|
{
|
|
13547
|
-
if (headlessMode || !glEnable) return;
|
|
13566
|
+
if (headlessMode || !glEnable || !postProcess.enabled) return;
|
|
13548
13567
|
|
|
13549
13568
|
// clear out the buffer, before anything here binds its own
|
|
13550
13569
|
glFlush();
|
|
@@ -23974,7 +23993,8 @@ function raycastBox(ray, pos, size, rotation)
|
|
|
23974
23993
|
* - EngineObject3D is an EngineObject with a 3D position, rotation and mesh
|
|
23975
23994
|
* - The 3D scene draws under the 2D sprites, so HUD and text land on top
|
|
23976
23995
|
* - Lighting is the sun plus ambient, with optional extra lights, fog and shadows
|
|
23977
|
-
* - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to
|
|
23996
|
+
* - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to, and a
|
|
23997
|
+
* mainNormal in it bends the normal the lighting uses
|
|
23978
23998
|
* - Meshes and the basic builders are in render3dMesh.js, EngineObject3D, instancing and lights in
|
|
23979
23999
|
* render3dObject.js, both right after this one; the other builders, terrain, particles, camera controls and the
|
|
23980
24000
|
* OBJ loader are in the Render3D Extras plugin, which goes after those
|
|
@@ -24552,7 +24572,9 @@ class Render3DPlugin
|
|
|
24552
24572
|
/** @property {Vector3|undefined} - Center of the shadowed area, read each frame, undefined follows the camera
|
|
24553
24573
|
* @type {Vector3|undefined} */
|
|
24554
24574
|
this.shadowCenter = undefined;
|
|
24555
|
-
/** @property {number} - Stops surfaces shadowing themselves, raise for speckles, lower if shadows drift off
|
|
24575
|
+
/** @property {number} - Stops surfaces shadowing themselves, raise for speckles, lower if shadows drift off;
|
|
24576
|
+
* a share of the shadow map's depth, twice shadowRange for the sun and a spotlight's radius, so the gap behind
|
|
24577
|
+
* a caster grows with the range as the map's texels do, and the speckles they make stay away */
|
|
24556
24578
|
this.shadowBias = .003;
|
|
24557
24579
|
/** @property {number} - How much to blur the shadow edges */
|
|
24558
24580
|
this.shadowSoftness = 1;
|
|
@@ -25586,6 +25608,9 @@ class Camera3D
|
|
|
25586
25608
|
/** @property {boolean} - Line the 3D camera up with the 2D camera, so 3D things at z=0 sit on the 2D sprites;
|
|
25587
25609
|
* it lines up with the 2D view of the main canvas, whatever canvas size a screenToRay is given */
|
|
25588
25610
|
this.align2D = false;
|
|
25611
|
+
/** @property {number} - The z of the plane align2D lines up with the 2D view, the camera sitting its
|
|
25612
|
+
* distance in front of it */
|
|
25613
|
+
this.align2DZ = 0;
|
|
25589
25614
|
}
|
|
25590
25615
|
|
|
25591
25616
|
/** Returns the camera's world transform
|
|
@@ -25656,7 +25681,7 @@ class Camera3D
|
|
|
25656
25681
|
false&&ASSERT(!canvasHeight || distance < this.far,
|
|
25657
25682
|
'align2D needs this camera distance to match the 2D view, raise camera.far past it', distance);
|
|
25658
25683
|
this.orthographic &&= halfHeight * 2; // an orthographic camera stays orthographic and shows the same height
|
|
25659
|
-
this.pos = vec3(cameraPos.x, cameraPos.y, distance);
|
|
25684
|
+
this.pos = vec3(cameraPos.x, cameraPos.y, this.align2DZ + distance);
|
|
25660
25685
|
this.rotation = vec3(0, 0, -cameraAngle); // 2D angles turn the other way
|
|
25661
25686
|
}
|
|
25662
25687
|
}
|
|
@@ -25708,7 +25733,8 @@ const RENDER3D_SNIPPET_NAMES =
|
|
|
25708
25733
|
'#define lights extraLights\n' +
|
|
25709
25734
|
'#define lightColors extraLightColors\n';
|
|
25710
25735
|
|
|
25711
|
-
// the fragment shader; given a Shader's snippet, its mainImage replaces the texture sample and all else is the same
|
|
25736
|
+
// the fragment shader; given a Shader's snippet, its mainImage replaces the texture sample and all else is the same,
|
|
25737
|
+
// and a mainNormal in it bends the normal after the normal map, before all the lighting reads it
|
|
25712
25738
|
// uniforms: lightDir (xyz the way the sunlight travels, w = emissive, 1 or more skips the lighting),
|
|
25713
25739
|
// lightColor (the sun's rgb, a = specular), ambientFog (rgb, a = fogEnd), fogColor (rgb, a = fogStart),
|
|
25714
25740
|
// cameraPos, tex, shadowMap, shadowParams (x = shadows on, y = bias, z = blur step in texture space,
|
|
@@ -25717,6 +25743,7 @@ const RENDER3D_SNIPPET_NAMES =
|
|
|
25717
25743
|
// emissive map is on), skyTop, skyHorizon, skyBottom (what a reflection shows), normalTex, emissiveTex
|
|
25718
25744
|
function render3DFragmentSource(fragmentCode)
|
|
25719
25745
|
{
|
|
25746
|
+
const hasNormal = /\bmainNormal\s*\(/.test(fragmentCode || '');
|
|
25720
25747
|
return '#version 300 es\n' +
|
|
25721
25748
|
'precision highp float;' +
|
|
25722
25749
|
'uniform vec4 lightDir,lightColor,ambientFog,ambientGround,fogColor,shadowParams;' +
|
|
@@ -25764,6 +25791,7 @@ function render3DFragmentSource(fragmentCode)
|
|
|
25764
25791
|
'return normalize((u*m.x-v*m.y)*inversesqrt(k)+n*m.z);}' +
|
|
25765
25792
|
// a snippet's mainImage is declared here and written after main, so a define in it can not reach main
|
|
25766
25793
|
(fragmentCode ? 'void mainImage(out vec4,vec2);' : '') +
|
|
25794
|
+
(hasNormal ? 'void mainNormal(inout vec3);' : '') +
|
|
25767
25795
|
'void main(){' +
|
|
25768
25796
|
(fragmentCode ? 'vec4 t;mainImage(t,T);' : 'vec4 t=texture(tex,T);') +
|
|
25769
25797
|
'if(premultipliedTexture&&t.a>0.)t.rgb/=t.a;' + // back to straight color, what the lighting and blend expect
|
|
@@ -25773,6 +25801,7 @@ function render3DFragmentSource(fragmentCode)
|
|
|
25773
25801
|
'vec3 n=dot(N,N)>0.?normalize(N):vec3(0,1,0);' +
|
|
25774
25802
|
'if(!gl_FrontFacing)n=-n;' + // only a double sided mesh shows a back face, light it on the side that is seen
|
|
25775
25803
|
'if(materialParams.x!=0.)n=normalMapNormal(n);' +
|
|
25804
|
+
(hasNormal ? 'mainNormal(n);n=normalize(n);' : '') +
|
|
25776
25805
|
'float nl=dot(n,-lightDir.xyz);' +
|
|
25777
25806
|
// the shadow is the sun's, or one spotlight's: shadowParams.x is 1 for the sun, 2 and up for that Light3D
|
|
25778
25807
|
'float sh=shadow(),s=shadowParams.x<1.5?sh:1.;' +
|
package/package.json
CHANGED
package/plugins/postProcess.js
CHANGED
|
@@ -49,6 +49,10 @@ class PostProcessPlugin
|
|
|
49
49
|
/** @property {string} - The shadertoy style mainImage code it shades with, see setShaderCode */
|
|
50
50
|
this.shaderCode = shaderCode || postProcessEffects(); // no code passes the frame through
|
|
51
51
|
|
|
52
|
+
/** @property {boolean} - Is the pass on? Off, the frame shows as the engine drew it, the shader is kept,
|
|
53
|
+
* and the feedback texture holds the last frame drawn with it on */
|
|
54
|
+
this.enabled = true;
|
|
55
|
+
|
|
52
56
|
/** @property {Object<string, number|Array<number>>} - The game's own values for the shader, a uniform each
|
|
53
57
|
* by its name, a number a float and a list of 2 to 4 numbers a vector, set every frame as they are; an
|
|
54
58
|
* effect setting can be one of these names, so it changes every frame without making the shader again, all
|
|
@@ -134,7 +138,7 @@ class PostProcessPlugin
|
|
|
134
138
|
}
|
|
135
139
|
function postProcessRender()
|
|
136
140
|
{
|
|
137
|
-
if (headlessMode || !glEnable) return;
|
|
141
|
+
if (headlessMode || !glEnable || !postProcess.enabled) return;
|
|
138
142
|
|
|
139
143
|
// clear out the buffer, before anything here binds its own
|
|
140
144
|
glFlush();
|
package/plugins/render3d.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* - EngineObject3D is an EngineObject with a 3D position, rotation and mesh
|
|
6
6
|
* - The 3D scene draws under the 2D sprites, so HUD and text land on top
|
|
7
7
|
* - Lighting is the sun plus ambient, with optional extra lights, fog and shadows
|
|
8
|
-
* - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to
|
|
8
|
+
* - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to, and a
|
|
9
|
+
* mainNormal in it bends the normal the lighting uses
|
|
9
10
|
* - Meshes and the basic builders are in render3dMesh.js, EngineObject3D, instancing and lights in
|
|
10
11
|
* render3dObject.js, both right after this one; the other builders, terrain, particles, camera controls and the
|
|
11
12
|
* OBJ loader are in the Render3D Extras plugin, which goes after those
|
|
@@ -585,7 +586,9 @@ class Render3DPlugin
|
|
|
585
586
|
/** @property {Vector3|undefined} - Center of the shadowed area, read each frame, undefined follows the camera
|
|
586
587
|
* @type {Vector3|undefined} */
|
|
587
588
|
this.shadowCenter = undefined;
|
|
588
|
-
/** @property {number} - Stops surfaces shadowing themselves, raise for speckles, lower if shadows drift off
|
|
589
|
+
/** @property {number} - Stops surfaces shadowing themselves, raise for speckles, lower if shadows drift off;
|
|
590
|
+
* a share of the shadow map's depth, twice shadowRange for the sun and a spotlight's radius, so the gap behind
|
|
591
|
+
* a caster grows with the range as the map's texels do, and the speckles they make stay away */
|
|
589
592
|
this.shadowBias = .003;
|
|
590
593
|
/** @property {number} - How much to blur the shadow edges */
|
|
591
594
|
this.shadowSoftness = 1;
|
|
@@ -1619,6 +1622,9 @@ class Camera3D
|
|
|
1619
1622
|
/** @property {boolean} - Line the 3D camera up with the 2D camera, so 3D things at z=0 sit on the 2D sprites;
|
|
1620
1623
|
* it lines up with the 2D view of the main canvas, whatever canvas size a screenToRay is given */
|
|
1621
1624
|
this.align2D = false;
|
|
1625
|
+
/** @property {number} - The z of the plane align2D lines up with the 2D view, the camera sitting its
|
|
1626
|
+
* distance in front of it */
|
|
1627
|
+
this.align2DZ = 0;
|
|
1622
1628
|
}
|
|
1623
1629
|
|
|
1624
1630
|
/** Returns the camera's world transform
|
|
@@ -1689,7 +1695,7 @@ class Camera3D
|
|
|
1689
1695
|
ASSERT(!canvasHeight || distance < this.far,
|
|
1690
1696
|
'align2D needs this camera distance to match the 2D view, raise camera.far past it', distance);
|
|
1691
1697
|
this.orthographic &&= halfHeight * 2; // an orthographic camera stays orthographic and shows the same height
|
|
1692
|
-
this.pos = vec3(cameraPos.x, cameraPos.y, distance);
|
|
1698
|
+
this.pos = vec3(cameraPos.x, cameraPos.y, this.align2DZ + distance);
|
|
1693
1699
|
this.rotation = vec3(0, 0, -cameraAngle); // 2D angles turn the other way
|
|
1694
1700
|
}
|
|
1695
1701
|
}
|
|
@@ -1741,7 +1747,8 @@ const RENDER3D_SNIPPET_NAMES =
|
|
|
1741
1747
|
'#define lights extraLights\n' +
|
|
1742
1748
|
'#define lightColors extraLightColors\n';
|
|
1743
1749
|
|
|
1744
|
-
// the fragment shader; given a Shader's snippet, its mainImage replaces the texture sample and all else is the same
|
|
1750
|
+
// the fragment shader; given a Shader's snippet, its mainImage replaces the texture sample and all else is the same,
|
|
1751
|
+
// and a mainNormal in it bends the normal after the normal map, before all the lighting reads it
|
|
1745
1752
|
// uniforms: lightDir (xyz the way the sunlight travels, w = emissive, 1 or more skips the lighting),
|
|
1746
1753
|
// lightColor (the sun's rgb, a = specular), ambientFog (rgb, a = fogEnd), fogColor (rgb, a = fogStart),
|
|
1747
1754
|
// cameraPos, tex, shadowMap, shadowParams (x = shadows on, y = bias, z = blur step in texture space,
|
|
@@ -1750,6 +1757,7 @@ const RENDER3D_SNIPPET_NAMES =
|
|
|
1750
1757
|
// emissive map is on), skyTop, skyHorizon, skyBottom (what a reflection shows), normalTex, emissiveTex
|
|
1751
1758
|
function render3DFragmentSource(fragmentCode)
|
|
1752
1759
|
{
|
|
1760
|
+
const hasNormal = /\bmainNormal\s*\(/.test(fragmentCode || '');
|
|
1753
1761
|
return '#version 300 es\n' +
|
|
1754
1762
|
'precision highp float;' +
|
|
1755
1763
|
'uniform vec4 lightDir,lightColor,ambientFog,ambientGround,fogColor,shadowParams;' +
|
|
@@ -1797,6 +1805,7 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1797
1805
|
'return normalize((u*m.x-v*m.y)*inversesqrt(k)+n*m.z);}' +
|
|
1798
1806
|
// a snippet's mainImage is declared here and written after main, so a define in it can not reach main
|
|
1799
1807
|
(fragmentCode ? 'void mainImage(out vec4,vec2);' : '') +
|
|
1808
|
+
(hasNormal ? 'void mainNormal(inout vec3);' : '') +
|
|
1800
1809
|
'void main(){' +
|
|
1801
1810
|
(fragmentCode ? 'vec4 t;mainImage(t,T);' : 'vec4 t=texture(tex,T);') +
|
|
1802
1811
|
'if(premultipliedTexture&&t.a>0.)t.rgb/=t.a;' + // back to straight color, what the lighting and blend expect
|
|
@@ -1806,6 +1815,7 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1806
1815
|
'vec3 n=dot(N,N)>0.?normalize(N):vec3(0,1,0);' +
|
|
1807
1816
|
'if(!gl_FrontFacing)n=-n;' + // only a double sided mesh shows a back face, light it on the side that is seen
|
|
1808
1817
|
'if(materialParams.x!=0.)n=normalMapNormal(n);' +
|
|
1818
|
+
(hasNormal ? 'mainNormal(n);n=normalize(n);' : '') +
|
|
1809
1819
|
'float nl=dot(n,-lightDir.xyz);' +
|
|
1810
1820
|
// the shadow is the sun's, or one spotlight's: shadowParams.x is 1 for the sun, 2 and up for that Light3D
|
|
1811
1821
|
'float sh=shadow(),s=shadowParams.x<1.5?sh:1.;' +
|
package/src/engine.js
CHANGED
|
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
|
|
|
32
32
|
* @type {string}
|
|
33
33
|
* @default
|
|
34
34
|
* @memberof Engine */
|
|
35
|
-
const engineVersion = '1.26.
|
|
35
|
+
const engineVersion = '1.26.1';
|
|
36
36
|
|
|
37
37
|
/** Frames per second to update
|
|
38
38
|
* @type {number}
|
|
@@ -66,6 +66,8 @@ let engineObjectsCollideStaticLast = [];
|
|
|
66
66
|
let frame = 0;
|
|
67
67
|
|
|
68
68
|
/** Current engine time since start in seconds
|
|
69
|
+
* - Below 20 FPS it falls behind the clock, since a frame catches up at most 50 ms of updates, so a timer in
|
|
70
|
+
* real seconds, one that must keep up on a slow device, goes by timeReal
|
|
69
71
|
* @type {number}
|
|
70
72
|
* @memberof Engine */
|
|
71
73
|
let time = 0;
|
|
@@ -91,8 +93,13 @@ function getPaused() { return paused; }
|
|
|
91
93
|
* @memberof Engine */
|
|
92
94
|
function setPaused(isPaused=true) { paused = isPaused; }
|
|
93
95
|
|
|
96
|
+
/** Frames drawn per second, smoothed over the last few seconds, in release builds too
|
|
97
|
+
* @type {number}
|
|
98
|
+
* @memberof Engine */
|
|
99
|
+
let averageFPS = 0;
|
|
100
|
+
|
|
94
101
|
// Engine internal variables
|
|
95
|
-
let frameTimeLastMS = 0, frameTimeBufferMS = 0
|
|
102
|
+
let frameTimeLastMS = 0, frameTimeBufferMS = 0;
|
|
96
103
|
|
|
97
104
|
// delta smoothing, after Time Delta Smoothing by Frank Force (2013): a frame is on screen for whole display frames,
|
|
98
105
|
// so each delta is rounded to them and the rest is carried to the next, keeping the total real time; the frame
|
|
@@ -433,7 +440,9 @@ function engineLoadingScreenDraw(elapsed)
|
|
|
433
440
|
* @param {GameCallback} [gameRender] - Called before objects are rendered, use for drawing backgrounds/world elements
|
|
434
441
|
* @param {GameCallback} [gameRenderPost] - Called after objects are rendered, use for drawing UI/overlays
|
|
435
442
|
* @param {Array<string>|string} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png']), or one path
|
|
436
|
-
* @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
|
|
443
|
+
* @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body; it is
|
|
444
|
+
* styled for a game (no scroll bars or selection, touch-action none),
|
|
445
|
+
* where its own inline style does not say otherwise
|
|
437
446
|
* It keeps its own inline styles and the canvas centers inside it, but the canvas is still sized from the window,
|
|
438
447
|
* so set canvasFixedSize or canvasMaxSize to fit a smaller element
|
|
439
448
|
* @example
|
|
@@ -496,8 +505,10 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
496
505
|
// by ~page-load-time when RAF starts handing real timestamps
|
|
497
506
|
if (!frameTimeLastMS) frameTimeDeltaMS = 0;
|
|
498
507
|
frameTimeLastMS = frameTimeMS;
|
|
499
|
-
|
|
500
|
-
|
|
508
|
+
// the first frame's rate seeds the average, so it does not start out climbing from 0; a gap of over a
|
|
509
|
+
// second, a hidden tab coming back, is not a frame
|
|
510
|
+
if (frameTimeDeltaMS && frameTimeDeltaMS < 1e3)
|
|
511
|
+
averageFPS = averageFPS ? lerp(averageFPS, 1e3/frameTimeDeltaMS, .05) : 1e3/frameTimeDeltaMS;
|
|
501
512
|
// the time the frame will be on screen, in whole display frames; engineStep's steps are exact already
|
|
502
513
|
if (!manualStepAtStart)
|
|
503
514
|
frameTimeDeltaMS = engineSmoothDelta(frameTimeDeltaMS);
|
package/src/engineDraw.js
CHANGED
|
@@ -548,6 +548,9 @@ class SpriteAnimator
|
|
|
548
548
|
* across the sprite or the mesh's own uv
|
|
549
549
|
* - Names in 3D only: worldPos, worldNormal, cameraPos, sunDirection, sunColor, ambientColor, ambientGroundColor,
|
|
550
550
|
* lightCount, lights[i], lightColors[i] and shadow()
|
|
551
|
+
* - In 3D the snippet may also define void mainNormal(inout vec3 n), given the normal facing the camera after the
|
|
552
|
+
* normal map, in world space, to bend it per pixel for waves or ripples; the lighting, specular, reflection and
|
|
553
|
+
* fog then all use it, as they use a normal map
|
|
551
554
|
* - In 3D the shadow map is drawn without the Shader, cut only by the texture's alpha, so a snippet that removes
|
|
552
555
|
* parts of a surface still shadows with the whole of it
|
|
553
556
|
* @example
|
package/src/engineExport.js
CHANGED
package/src/engineInput.js
CHANGED
|
@@ -656,8 +656,9 @@ function inputInit()
|
|
|
656
656
|
}
|
|
657
657
|
function onContextMenu(e)
|
|
658
658
|
{
|
|
659
|
-
// prevent right click menu, but a text field keeps its copy and paste menu
|
|
660
|
-
|
|
659
|
+
// prevent right click menu, but a text field keeps its copy and paste menu and a link its open in new tab
|
|
660
|
+
const target = /** @type {HTMLElement} */ (e.target);
|
|
661
|
+
if (inputPreventDefault && !isTextInput(target) && !target?.closest?.('a[href]'))
|
|
661
662
|
e.preventDefault();
|
|
662
663
|
}
|
|
663
664
|
function onBlur()
|
|
@@ -678,6 +679,10 @@ function inputInit()
|
|
|
678
679
|
// handle all touch events the same way
|
|
679
680
|
function handleTouch(e)
|
|
680
681
|
{
|
|
682
|
+
// fix stalled audio requiring user interaction, with touch input off too since the page's sound needs it
|
|
683
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
684
|
+
audioResume();
|
|
685
|
+
|
|
681
686
|
if (!touchInputEnable)
|
|
682
687
|
{
|
|
683
688
|
// turned off mid touch, the finger that drove the mouse lets go of it
|
|
@@ -688,10 +693,6 @@ function inputInit()
|
|
|
688
693
|
}
|
|
689
694
|
inputLastTouchTime = performance.now();
|
|
690
695
|
|
|
691
|
-
// fix stalled audio requiring user interaction
|
|
692
|
-
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
693
|
-
audioResume();
|
|
694
|
-
|
|
695
696
|
// when the touch gamepad is enabled it owns touch input: suppress the
|
|
696
697
|
// touch->mouse passthrough entirely unless touchGamepadPassthrough is set
|
|
697
698
|
// (its own zones drive gameplay via pointer events)
|