littlejsengine 1.19.3 → 1.21.0

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.
Files changed (54) hide show
  1. package/FAQ.md +841 -820
  2. package/README.md +29 -25
  3. package/dist/littlejs.d.ts +2857 -955
  4. package/dist/littlejs.esm.js +33428 -20995
  5. package/dist/littlejs.esm.min.js +4 -3
  6. package/dist/littlejs.js +33386 -20996
  7. package/dist/littlejs.min.js +2 -2
  8. package/dist/littlejs.release.js +27841 -20422
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +28 -24
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +786 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -332
  16. package/plugins/math3d.js +148 -27
  17. package/plugins/medalSystem.js +326 -280
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +25 -0
  21. package/plugins/postProcess.js +96 -24
  22. package/plugins/render3d.js +2067 -1632
  23. package/plugins/render3dDebug.js +110 -0
  24. package/plugins/render3dEditor.js +1812 -0
  25. package/plugins/render3dExtras.js +1948 -0
  26. package/plugins/render3dLevel.js +189 -0
  27. package/plugins/render3dVoxels.js +514 -0
  28. package/plugins/sceneSystem.js +58 -0
  29. package/plugins/textureSheet.js +496 -458
  30. package/plugins/threejs.js +183 -163
  31. package/plugins/tweakables.js +411 -0
  32. package/plugins/tweenSystem.js +265 -145
  33. package/plugins/uiSystem.js +2024 -1687
  34. package/plugins/zzfxm.js +159 -159
  35. package/src/engine.js +378 -97
  36. package/src/engineAudio.js +231 -127
  37. package/src/engineBuild.mjs +292 -256
  38. package/src/engineDebug.js +967 -743
  39. package/src/engineDraw.js +391 -198
  40. package/src/engineEditor.js +2452 -0
  41. package/src/engineExport.js +18 -0
  42. package/src/engineInput.js +1466 -1293
  43. package/src/engineLogo.js +19 -0
  44. package/src/engineMath.js +1274 -1252
  45. package/src/engineObject.js +197 -82
  46. package/src/engineParticles.js +583 -532
  47. package/src/engineRelease.js +79 -45
  48. package/src/engineSettings.js +115 -29
  49. package/src/engineTileLayer.js +995 -741
  50. package/src/engineUtilities.js +19 -13
  51. package/src/engineWebGL.js +239 -82
  52. package/src/jsconfig.json +17 -13
  53. package/plugins/box2d.wasm.js +0 -630
  54. package/plugins/box2d.wasm.wasm +0 -0
package/plugins/math3d.js CHANGED
@@ -288,6 +288,7 @@ class Vector3
288
288
 
289
289
  // scratch for multiply, nothing keeps a reference to it
290
290
  const matrix4Scratch = new Float32Array(16);
291
+ const matrix4Identity = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]);
291
292
 
292
293
  /**
293
294
  * 4x4 transform matrix for moving, rotating and scaling points in 3D
@@ -330,22 +331,23 @@ class Matrix4
330
331
  return r;
331
332
  }
332
333
 
333
- /** Returns a new rotation matrix, rolled first, then pitched, then yawed
334
+ /** Returns a rotation matrix, rolled first, then pitched, then yawed
334
335
  * @param {Vector3} euler - vec3(pitch, yaw, roll) in radians
336
+ * @param {Matrix4} [matrix] - Written into instead of a new one, for a loop that builds many
335
337
  * @return {Matrix4} */
336
- static rotation(euler)
338
+ static rotation(euler, matrix=new Matrix4)
337
339
  {
338
340
  ASSERT_VECTOR3_VALID(euler);
339
341
  const cx = cos(euler.x), sx = sin(euler.x);
340
342
  const cy = cos(euler.y), sy = sin(euler.y);
341
343
  const cz = cos(euler.z), sz = sin(euler.z);
342
- const r = new Matrix4;
343
- const m = r.m;
344
- // R = Ry * Rx * Rz written out, column major
345
- m[0] = cy*cz + sy*sx*sz; m[1] = cx*sz; m[2] = -sy*cz + cy*sx*sz;
346
- m[4] = -cy*sz + sy*sx*cz; m[5] = cx*cz; m[6] = sy*sz + cy*sx*cz;
347
- m[8] = sy*cx; m[9] = -sx; m[10] = cy*cx;
348
- return r;
344
+ const m = matrix.m;
345
+ // R = Ry * Rx * Rz written out, column major, every element set so a reused matrix comes out clean
346
+ m[0] = cy*cz + sy*sx*sz; m[1] = cx*sz; m[2] = -sy*cz + cy*sx*sz; m[3] = 0;
347
+ m[4] = -cy*sz + sy*sx*cz; m[5] = cx*cz; m[6] = sy*sz + cy*sx*cz; m[7] = 0;
348
+ m[8] = sy*cx; m[9] = -sx; m[10] = cy*cx; m[11] = 0;
349
+ m[12] = m[13] = m[14] = 0; m[15] = 1;
350
+ return matrix;
349
351
  }
350
352
 
351
353
  /** Returns a new scale matrix
@@ -416,8 +418,10 @@ class Matrix4
416
418
  if (!z.lengthSquared())
417
419
  z = vec3(0, 0, 1); // eye is on the target, face -Z
418
420
  let x = up.cross(z).normalize();
419
- if (!x.lengthSquared()) // up is along the view direction, pick another
420
- x = (abs(z.y) > .99 ? vec3(0, 0, 1) : vec3(0, 1, 0)).cross(z).normalize();
421
+ // up is along the view direction, pick another; looking straight down or up keeps +X as the right axis,
422
+ // as three.js does and as a view tilting there from the +Z side ends up
423
+ if (!x.lengthSquared())
424
+ x = (abs(z.y) > .99 ? vec3(0, 0, z.y > 0 ? -1 : 1) : vec3(0, 1, 0)).cross(z).normalize();
421
425
  const y = z.cross(x);
422
426
  return new Matrix4([x.x, x.y, x.z, 0, y.x, y.y, y.z, 0, z.x, z.y, z.z, 0, eye.x, eye.y, eye.z, 1]);
423
427
  }
@@ -530,6 +534,41 @@ class Matrix4
530
534
  * @return {Vector3} */
531
535
  getTranslation() { return new Vector3(this.m[12], this.m[13], this.m[14]); }
532
536
 
537
+ /** Returns the determinant of the rotation and scale part: negative when the matrix mirrors, 0 when it flattens a
538
+ * shape and has no inverse
539
+ * @return {number} */
540
+ determinant()
541
+ {
542
+ const m = this.m;
543
+ return m[0]*(m[5]*m[10] - m[6]*m[9]) - m[4]*(m[1]*m[10] - m[2]*m[9]) + m[8]*(m[1]*m[6] - m[2]*m[5]);
544
+ }
545
+
546
+ /** Returns the scale part of this matrix, the length of each axis; a mirroring matrix shows as a negative x
547
+ * @return {Vector3} */
548
+ getScale()
549
+ {
550
+ const m = this.m;
551
+ const x = hypot(m[0], m[1], m[2]), y = hypot(m[4], m[5], m[6]), z = hypot(m[8], m[9], m[10]);
552
+ return new Vector3(this.determinant() < 0 ? -x : x, y, z);
553
+ }
554
+
555
+ /** Returns the rotation part of this matrix as vec3(pitch, yaw, roll), the angles Matrix4.rotation builds it from
556
+ * - The scale is divided out first, so it works on a full transform
557
+ * - A matrix with shear, from a scaled parent with a turned child, has no exact answer and gets the nearest
558
+ * @return {Vector3} */
559
+ getRotation()
560
+ {
561
+ // the axes at unit length, see Matrix4.rotation for which element is which
562
+ const m = this.m, s = this.getScale();
563
+ const sx = s.x || 1, sy = s.y || 1, sz = s.z || 1;
564
+ const m1 = m[1] / sx, m5 = m[5] / sy, m8 = m[8] / sz, m9 = m[9] / sz, m10 = m[10] / sz;
565
+ const pitch = Math.asin(clamp(-m9, -1, 1));
566
+ if (abs(m9) < 1 - 1e-6)
567
+ return new Vector3(pitch, atan2(m8, m10), atan2(m1, m5));
568
+ // straight up or down: yaw and roll turn about the same axis, so the roll is zero and yaw takes it all
569
+ return new Vector3(pitch, atan2(m[4] / sy * -m9, m[0] / sx), 0);
570
+ }
571
+
533
572
  /** Returns a string representation of this matrix for debugging
534
573
  * @return {string} */
535
574
  toString()
@@ -550,18 +589,20 @@ class Matrix4
550
589
  * @param {Vector3} [pos]
551
590
  * @param {Vector3} [rotation] - vec3(pitch, yaw, roll) in radians
552
591
  * @param {Vector3} [scale]
592
+ * @param {Matrix4} [matrix] - Written into instead of a new one, for a loop that builds many
553
593
  * @return {Matrix4}
554
594
  * @memberof Math3D
555
595
  */
556
- function buildMatrix(pos, rotation, scale)
596
+ function buildMatrix(pos, rotation, scale, matrix=new Matrix4)
557
597
  {
558
598
  ASSERT(!pos || isVector3(pos), 'pos must be a Vector3', pos);
559
599
  ASSERT(!scale || isVector3(scale), 'scale must be a Vector3', scale);
600
+ ASSERT(matrix instanceof Matrix4, 'the matrix to write into must be a Matrix4');
560
601
  // scale the rotation columns and drop the position in, instead of multiplying three matrices
561
602
  // an object that is not turned at all is most of a big scene, and identity is what the six
562
603
  // trig calls would have worked out to anyway
563
- const turned = rotation && (rotation.x || rotation.y || rotation.z);
564
- const matrix = turned ? Matrix4.rotation(rotation) : new Matrix4, m = matrix.m;
604
+ const turned = rotation && (rotation.x || rotation.y || rotation.z), m = matrix.m;
605
+ turned ? Matrix4.rotation(rotation, matrix) : m.set(matrix4Identity);
565
606
  if (scale)
566
607
  {
567
608
  m[0] *= scale.x; m[1] *= scale.x; m[2] *= scale.x;
@@ -611,36 +652,62 @@ class Ray3D
611
652
 
612
653
  ///////////////////////////////////////////////////////////////////////////////
613
654
  // 3D collision helpers, none of them change anything that is passed in
614
- // Boxes sit centered on pos and take a full size, like drawRect
655
+ // Boxes sit centered on pos and take a full size, like drawRect, upright unless given a rotation, an Euler vec3 like
656
+ // rotation3D, which turns them about their center
615
657
  // Cylinders stand up the Y axis, centered on pos, with a full height
616
658
  // Names that could be mistaken for 2D functions get a 3D suffix
617
659
 
660
+ // whether a rotation turns anything, a missing one does not
661
+ const isTurned3D = (rotation)=> !!rotation && !!(rotation.x || rotation.y || rotation.z);
662
+
663
+ // a turned box's three unit axes, the columns of its rotation
664
+ function boxAxes3D(rotation)
665
+ {
666
+ const m = buildMatrix(undefined, rotation).m;
667
+ return [vec3(m[0], m[1], m[2]), vec3(m[4], m[5], m[6]), vec3(m[8], m[9], m[10])];
668
+ }
669
+
670
+ // a point in a turned box's own space, from its center, and a vector from that space back to the world's
671
+ function boxLocal3D(point, pos, axes)
672
+ {
673
+ const d = point.subtract(pos);
674
+ return vec3(d.dot(axes[0]), d.dot(axes[1]), d.dot(axes[2]));
675
+ }
676
+ const boxWorld3D = (v, axes)=> axes[0].scale(v.x).add(axes[1].scale(v.y)).add(axes[2].scale(v.z));
677
+
618
678
  /**
619
- * Check if a point is inside an axis aligned box, boundary is inclusive
679
+ * Check if a point is inside a box, boundary is inclusive
620
680
  * @param {Vector3} point
621
681
  * @param {Vector3} pos - Center of the box
622
682
  * @param {Vector3} size - Full size of the box
683
+ * @param {Vector3} [rotation] - How the box is turned, upright when left out
623
684
  * @return {boolean}
624
685
  * @memberof Math3D
625
686
  */
626
- function isPointInBox3D(point, pos, size)
687
+ function isPointInBox3D(point, pos, size, rotation)
627
688
  {
689
+ if (isTurned3D(rotation))
690
+ return isPointInBox3D(boxLocal3D(point, pos, boxAxes3D(rotation)), vec3(), size);
628
691
  return abs(point.x - pos.x) <= size.x/2 &&
629
692
  abs(point.y - pos.y) <= size.y/2 &&
630
693
  abs(point.z - pos.z) <= size.z/2;
631
694
  }
632
695
 
633
696
  /**
634
- * Check if two axis aligned boxes are overlapping, touching edges do not overlap
697
+ * Check if two boxes are overlapping, touching edges do not overlap
635
698
  * @param {Vector3} posA
636
699
  * @param {Vector3} sizeA - Full size of box A
637
700
  * @param {Vector3} posB
638
701
  * @param {Vector3} [sizeB] - Full size of box B, zero for a point
702
+ * @param {Vector3} [rotationA] - How box A is turned, upright when left out
703
+ * @param {Vector3} [rotationB] - How box B is turned
639
704
  * @return {boolean}
640
705
  * @memberof Math3D
641
706
  */
642
- function isOverlapping3D(posA, sizeA, posB, sizeB=vec3())
707
+ function isOverlapping3D(posA, sizeA, posB, sizeB=vec3(), rotationA, rotationB)
643
708
  {
709
+ if (isTurned3D(rotationA) || isTurned3D(rotationB))
710
+ return !!collideBoxBox3D(posA, sizeA, posB, sizeB, rotationA, rotationB);
644
711
  const d = posA.subtract(posB);
645
712
  return abs(d.x) < (sizeA.x + sizeB.x)/2 &&
646
713
  abs(d.y) < (sizeA.y + sizeB.y)/2 &&
@@ -669,16 +736,19 @@ function collideSphereSphere(posA, radiusA, posB, radiusB)
669
736
  }
670
737
 
671
738
  /**
672
- * Returns the vector to move a sphere out of an axis aligned box, or undefined
739
+ * Returns the vector to move a sphere out of a box, or undefined
673
740
  * @param {Vector3} pos - Sphere center
674
741
  * @param {number} radius
675
742
  * @param {Vector3} boxPos
676
743
  * @param {Vector3} boxSize - Full size of the box
744
+ * @param {Vector3} [boxRotation] - How the box is turned, upright when left out
677
745
  * @return {Vector3|undefined}
678
746
  * @memberof Math3D
679
747
  */
680
- function collideSphereBox(pos, radius, boxPos, boxSize)
748
+ function collideSphereBox(pos, radius, boxPos, boxSize, boxRotation)
681
749
  {
750
+ if (isTurned3D(boxRotation))
751
+ return collideSphereOrientedBox3D(pos, radius, boxPos, boxSize, boxAxes3D(boxRotation));
682
752
  const h = boxSize.scale(.5);
683
753
  const closest = vec3(
684
754
  clamp(pos.x, boxPos.x - h.x, boxPos.x + h.x),
@@ -766,15 +836,20 @@ function collideSphereCylinder(pos, radius, cylinderPos, cylinderRadius, cylinde
766
836
  /**
767
837
  * Returns the vector to move box A by so it no longer overlaps box B, the shortest way out, or undefined
768
838
  * - The 3D twin of collideBoxBox
839
+ * - Turned boxes are tested by the separating axis test, the push is along the direction they overlap least on
769
840
  * @param {Vector3} posA
770
841
  * @param {Vector3} sizeA - Full size of box A
771
842
  * @param {Vector3} posB
772
843
  * @param {Vector3} sizeB - Full size of box B
844
+ * @param {Vector3} [rotationA] - How box A is turned, upright when left out
845
+ * @param {Vector3} [rotationB] - How box B is turned
773
846
  * @return {Vector3|undefined}
774
847
  * @memberof Math3D
775
848
  */
776
- function collideBoxBox3D(posA, sizeA, posB, sizeB)
849
+ function collideBoxBox3D(posA, sizeA, posB, sizeB, rotationA, rotationB)
777
850
  {
851
+ if (isTurned3D(rotationA) || isTurned3D(rotationB))
852
+ return collideOrientedBoxes3D(posA, sizeA, boxAxes3D(rotationA), posB, sizeB, boxAxes3D(rotationB));
778
853
  const d = posA.subtract(posB);
779
854
  const overlapX = (sizeA.x + sizeB.x)/2 - abs(d.x);
780
855
  const overlapY = (sizeA.y + sizeB.y)/2 - abs(d.y);
@@ -784,6 +859,44 @@ function collideBoxBox3D(posA, sizeA, posB, sizeB)
784
859
  return pushOutAxis3D(d, overlapX, overlapY, overlapZ);
785
860
  }
786
861
 
862
+ // the push to move a sphere out of a box turned to these axes: in the box's own space it is upright, and the push
863
+ // goes back out turned
864
+ function collideSphereOrientedBox3D(pos, radius, boxPos, boxSize, axes)
865
+ {
866
+ const push = collideSphereBox(boxLocal3D(pos, boxPos, axes), radius, vec3(), boxSize);
867
+ return push && boxWorld3D(push, axes);
868
+ }
869
+
870
+ // the world's axes, for an upright box among turned ones
871
+ const BOX_WORLD_AXES = Object.freeze([vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1)]);
872
+
873
+ // the shortest push to move box A out of box B by the separating axis test: each box's three face directions and
874
+ // the nine across an edge of each; a gap on any of them is no touch, and the push is along the least overlap, from
875
+ // B to A; an edge direction has to overlap clearly less to win, so a box lying on another leaves by the face
876
+ function collideOrientedBoxes3D(posA, sizeA, axesA, posB, sizeB, axesB)
877
+ {
878
+ const d = posA.subtract(posB), ha = sizeA.scale(.5), hb = sizeB.scale(.5);
879
+ const reach = (h, axes, n)=> h.x * abs(axes[0].dot(n)) + h.y * abs(axes[1].dot(n)) + h.z * abs(axes[2].dot(n));
880
+ let push, least = Infinity;
881
+ const separated = (axis, weight)=>
882
+ {
883
+ const lengthSquared = axis.lengthSquared();
884
+ if (lengthSquared < 1e-12) return false; // parallel edges give no direction to test
885
+ const n = axis.scale(lengthSquared ** -.5), along = d.dot(n);
886
+ const overlap = reach(ha, axesA, n) + reach(hb, axesB, n) - abs(along);
887
+ if (overlap <= 0) return true;
888
+ if (overlap * weight < least)
889
+ least = overlap * weight, push = n.scale(along < 0 ? -overlap : overlap);
890
+ return false;
891
+ };
892
+ for (const axis of [...axesA, ...axesB])
893
+ if (separated(axis, 1)) return;
894
+ for (const a of axesA)
895
+ for (const b of axesB)
896
+ if (separated(a.cross(b), 1.05)) return;
897
+ return push;
898
+ }
899
+
787
900
  /**
788
901
  * Returns the distance along the ray to the first intersection with a sphere, or undefined
789
902
  * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
@@ -798,12 +911,12 @@ function raycastSphere(ray, pos, radius)
798
911
  {
799
912
  const {origin, direction} = ray;
800
913
  const oc = origin.subtract(pos);
914
+ const c = oc.dot(oc) - radius*radius;
915
+ if (c < 0)
916
+ return 0; // origin is inside the sphere, even for a ray of no length, as raycastBox gives
801
917
  const a = direction.dot(direction);
802
918
  if (!a)
803
919
  return undefined;
804
- const c = oc.dot(oc) - radius*radius;
805
- if (c < 0)
806
- return 0; // origin is inside the sphere
807
920
  const b = 2*oc.dot(direction);
808
921
  const discriminant = b*b - 4*a*c;
809
922
  if (discriminant < 0)
@@ -832,17 +945,25 @@ function raycastPlane(ray, planePos, planeNormal)
832
945
  }
833
946
 
834
947
  /**
835
- * Returns the distance along the ray to the first intersection with an axis aligned box, or undefined
948
+ * Returns the distance along the ray to the first intersection with a box, or undefined
836
949
  * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
837
950
  * - A ray starting inside the box is already there, so it gets back 0
838
951
  * @param {Ray3D} ray
839
952
  * @param {Vector3} pos - Center of the box
840
953
  * @param {Vector3} size - Full size of the box
954
+ * @param {Vector3} [rotation] - How the box is turned, upright when left out
841
955
  * @return {number|undefined}
842
956
  * @memberof Math3D
843
957
  */
844
- function raycastBox(ray, pos, size)
958
+ function raycastBox(ray, pos, size, rotation)
845
959
  {
960
+ if (isTurned3D(rotation))
961
+ {
962
+ // the ray in the box's own space, where the box is upright; turning keeps the distances
963
+ const axes = boxAxes3D(rotation), d = ray.direction;
964
+ const direction = vec3(d.dot(axes[0]), d.dot(axes[1]), d.dot(axes[2]));
965
+ return raycastBox(new Ray3D(boxLocal3D(ray.origin, pos, axes), direction), vec3(), size);
966
+ }
846
967
  const {origin, direction} = ray;
847
968
  const h = size.scale(.5);
848
969
  const boxMin = pos.subtract(h), boxMax = pos.add(h);