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/dist/littlejs.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.0';
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, averageFPS = 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
- if (debug || debugWatermark)
503
- averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
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);
@@ -5748,6 +5759,9 @@ class SpriteAnimator
5748
5759
  * across the sprite or the mesh's own uv
5749
5760
  * - Names in 3D only: worldPos, worldNormal, cameraPos, sunDirection, sunColor, ambientColor, ambientGroundColor,
5750
5761
  * lightCount, lights[i], lightColors[i] and shadow()
5762
+ * - In 3D the snippet may also define void mainNormal(inout vec3 n), given the normal facing the camera after the
5763
+ * normal map, in world space, to bend it per pixel for waves or ripples; the lighting, specular, reflection and
5764
+ * fog then all use it, as they use a normal map
5751
5765
  * - In 3D the shadow map is drawn without the Shader, cut only by the texture's alpha, so a snippet that removes
5752
5766
  * parts of a surface still shadows with the whole of it
5753
5767
  * @example
@@ -7899,8 +7913,9 @@ function inputInit()
7899
7913
  }
7900
7914
  function onContextMenu(e)
7901
7915
  {
7902
- // prevent right click menu, but a text field keeps its copy and paste menu
7903
- if (inputPreventDefault && !isTextInput(e.target))
7916
+ // prevent right click menu, but a text field keeps its copy and paste menu and a link its open in new tab
7917
+ const target = /** @type {HTMLElement} */ (e.target);
7918
+ if (inputPreventDefault && !isTextInput(target) && !target?.closest?.('a[href]'))
7904
7919
  e.preventDefault();
7905
7920
  }
7906
7921
  function onBlur()
@@ -7921,6 +7936,10 @@ function inputInit()
7921
7936
  // handle all touch events the same way
7922
7937
  function handleTouch(e)
7923
7938
  {
7939
+ // fix stalled audio requiring user interaction, with touch input off too since the page's sound needs it
7940
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
7941
+ audioResume();
7942
+
7924
7943
  if (!touchInputEnable)
7925
7944
  {
7926
7945
  // turned off mid touch, the finger that drove the mouse lets go of it
@@ -7931,10 +7950,6 @@ function inputInit()
7931
7950
  }
7932
7951
  inputLastTouchTime = performance.now();
7933
7952
 
7934
- // fix stalled audio requiring user interaction
7935
- if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
7936
- audioResume();
7937
-
7938
7953
  // when the touch gamepad is enabled it owns touch input: suppress the
7939
7954
  // touch->mouse passthrough entirely unless touchGamepadPassthrough is set
7940
7955
  // (its own zones drive gameplay via pointer events)
@@ -14193,6 +14208,10 @@ class PostProcessPlugin
14193
14208
  /** @property {string} - The shadertoy style mainImage code it shades with, see setShaderCode */
14194
14209
  this.shaderCode = shaderCode || postProcessEffects(); // no code passes the frame through
14195
14210
 
14211
+ /** @property {boolean} - Is the pass on? Off, the frame shows as the engine drew it, the shader is kept,
14212
+ * and the feedback texture holds the last frame drawn with it on */
14213
+ this.enabled = true;
14214
+
14196
14215
  /** @property {Object<string, number|Array<number>>} - The game's own values for the shader, a uniform each
14197
14216
  * by its name, a number a float and a list of 2 to 4 numbers a vector, set every frame as they are; an
14198
14217
  * effect setting can be one of these names, so it changes every frame without making the shader again, all
@@ -14278,7 +14297,7 @@ class PostProcessPlugin
14278
14297
  }
14279
14298
  function postProcessRender()
14280
14299
  {
14281
- if (headlessMode || !glEnable) return;
14300
+ if (headlessMode || !glEnable || !postProcess.enabled) return;
14282
14301
 
14283
14302
  // clear out the buffer, before anything here binds its own
14284
14303
  glFlush();
@@ -24708,7 +24727,8 @@ function raycastBox(ray, pos, size, rotation)
24708
24727
  * - EngineObject3D is an EngineObject with a 3D position, rotation and mesh
24709
24728
  * - The 3D scene draws under the 2D sprites, so HUD and text land on top
24710
24729
  * - Lighting is the sun plus ambient, with optional extra lights, fog and shadows
24711
- * - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to
24730
+ * - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to, and a
24731
+ * mainNormal in it bends the normal the lighting uses
24712
24732
  * - Meshes and the basic builders are in render3dMesh.js, EngineObject3D, instancing and lights in
24713
24733
  * render3dObject.js, both right after this one; the other builders, terrain, particles, camera controls and the
24714
24734
  * OBJ loader are in the Render3D Extras plugin, which goes after those
@@ -25286,7 +25306,9 @@ class Render3DPlugin
25286
25306
  /** @property {Vector3|undefined} - Center of the shadowed area, read each frame, undefined follows the camera
25287
25307
  * @type {Vector3|undefined} */
25288
25308
  this.shadowCenter = undefined;
25289
- /** @property {number} - Stops surfaces shadowing themselves, raise for speckles, lower if shadows drift off */
25309
+ /** @property {number} - Stops surfaces shadowing themselves, raise for speckles, lower if shadows drift off;
25310
+ * a share of the shadow map's depth, twice shadowRange for the sun and a spotlight's radius, so the gap behind
25311
+ * a caster grows with the range as the map's texels do, and the speckles they make stay away */
25290
25312
  this.shadowBias = .003;
25291
25313
  /** @property {number} - How much to blur the shadow edges */
25292
25314
  this.shadowSoftness = 1;
@@ -26320,6 +26342,9 @@ class Camera3D
26320
26342
  /** @property {boolean} - Line the 3D camera up with the 2D camera, so 3D things at z=0 sit on the 2D sprites;
26321
26343
  * it lines up with the 2D view of the main canvas, whatever canvas size a screenToRay is given */
26322
26344
  this.align2D = false;
26345
+ /** @property {number} - The z of the plane align2D lines up with the 2D view, the camera sitting its
26346
+ * distance in front of it */
26347
+ this.align2DZ = 0;
26323
26348
  }
26324
26349
 
26325
26350
  /** Returns the camera's world transform
@@ -26390,7 +26415,7 @@ class Camera3D
26390
26415
  ASSERT(!canvasHeight || distance < this.far,
26391
26416
  'align2D needs this camera distance to match the 2D view, raise camera.far past it', distance);
26392
26417
  this.orthographic &&= halfHeight * 2; // an orthographic camera stays orthographic and shows the same height
26393
- this.pos = vec3(cameraPos.x, cameraPos.y, distance);
26418
+ this.pos = vec3(cameraPos.x, cameraPos.y, this.align2DZ + distance);
26394
26419
  this.rotation = vec3(0, 0, -cameraAngle); // 2D angles turn the other way
26395
26420
  }
26396
26421
  }
@@ -26442,7 +26467,8 @@ const RENDER3D_SNIPPET_NAMES =
26442
26467
  '#define lights extraLights\n' +
26443
26468
  '#define lightColors extraLightColors\n';
26444
26469
 
26445
- // the fragment shader; given a Shader's snippet, its mainImage replaces the texture sample and all else is the same
26470
+ // the fragment shader; given a Shader's snippet, its mainImage replaces the texture sample and all else is the same,
26471
+ // and a mainNormal in it bends the normal after the normal map, before all the lighting reads it
26446
26472
  // uniforms: lightDir (xyz the way the sunlight travels, w = emissive, 1 or more skips the lighting),
26447
26473
  // lightColor (the sun's rgb, a = specular), ambientFog (rgb, a = fogEnd), fogColor (rgb, a = fogStart),
26448
26474
  // cameraPos, tex, shadowMap, shadowParams (x = shadows on, y = bias, z = blur step in texture space,
@@ -26451,6 +26477,7 @@ const RENDER3D_SNIPPET_NAMES =
26451
26477
  // emissive map is on), skyTop, skyHorizon, skyBottom (what a reflection shows), normalTex, emissiveTex
26452
26478
  function render3DFragmentSource(fragmentCode)
26453
26479
  {
26480
+ const hasNormal = /\bmainNormal\s*\(/.test(fragmentCode || '');
26454
26481
  return '#version 300 es\n' +
26455
26482
  'precision highp float;' +
26456
26483
  'uniform vec4 lightDir,lightColor,ambientFog,ambientGround,fogColor,shadowParams;' +
@@ -26498,6 +26525,7 @@ function render3DFragmentSource(fragmentCode)
26498
26525
  'return normalize((u*m.x-v*m.y)*inversesqrt(k)+n*m.z);}' +
26499
26526
  // a snippet's mainImage is declared here and written after main, so a define in it can not reach main
26500
26527
  (fragmentCode ? 'void mainImage(out vec4,vec2);' : '') +
26528
+ (hasNormal ? 'void mainNormal(inout vec3);' : '') +
26501
26529
  'void main(){' +
26502
26530
  (fragmentCode ? 'vec4 t;mainImage(t,T);' : 'vec4 t=texture(tex,T);') +
26503
26531
  'if(premultipliedTexture&&t.a>0.)t.rgb/=t.a;' + // back to straight color, what the lighting and blend expect
@@ -26507,6 +26535,7 @@ function render3DFragmentSource(fragmentCode)
26507
26535
  'vec3 n=dot(N,N)>0.?normalize(N):vec3(0,1,0);' +
26508
26536
  'if(!gl_FrontFacing)n=-n;' + // only a double sided mesh shows a back face, light it on the side that is seen
26509
26537
  'if(materialParams.x!=0.)n=normalMapNormal(n);' +
26538
+ (hasNormal ? 'mainNormal(n);n=normalize(n);' : '') +
26510
26539
  'float nl=dot(n,-lightDir.xyz);' +
26511
26540
  // the shadow is the sun's, or one spotlight's: shadowParams.x is 1 for the sun, 2 and up for that Light3D
26512
26541
  'float sh=shadow(),s=shadowParams.x<1.5?sh:1.;' +