littlejsengine 1.16.2 → 1.17.5

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 (57) hide show
  1. package/dist/littlejs.d.ts +291 -246
  2. package/dist/littlejs.esm.js +1520 -1271
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +1178 -920
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +1170 -912
  7. package/examples/box2d/gameObjects.js +4 -4
  8. package/examples/breakout/game.js +5 -6
  9. package/examples/electron/index.html +2 -2
  10. package/examples/electron/tiles.png +0 -0
  11. package/examples/htmlMenu/tiles.png +0 -0
  12. package/examples/index.html +9 -8
  13. package/examples/logo.png +0 -0
  14. package/examples/module/build.bat +7 -0
  15. package/examples/module/build.js +121 -0
  16. package/examples/module/tiles.png +0 -0
  17. package/examples/platformer/game.js +1 -1
  18. package/examples/platformer/gameEffects.js +25 -29
  19. package/examples/platformer/gameLevel.js +27 -28
  20. package/examples/puzzle/game.js +2 -2
  21. package/examples/shorts/base.html +4 -1
  22. package/examples/shorts/box2dPool.js +2 -2
  23. package/examples/shorts/box2dTileLayer.js +48 -0
  24. package/examples/shorts/clock.js +3 -3
  25. package/examples/shorts/fontImage.js +4 -3
  26. package/examples/shorts/music.js +2 -2
  27. package/examples/shorts/musicPlayer.js +2 -2
  28. package/examples/shorts/parallax.js +1 -1
  29. package/examples/shorts/sequencer.js +1 -1
  30. package/examples/shorts/shapes.js +1 -1
  31. package/examples/shorts/texture.js +10 -6
  32. package/examples/shorts/tiles.png +0 -0
  33. package/examples/shorts/tiltedView.js +2 -0
  34. package/examples/starter/index.html +2 -2
  35. package/examples/starter/tiles.png +0 -0
  36. package/examples/typescript/tiles.png +0 -0
  37. package/examples/uiSystem/game.js +1 -1
  38. package/examples/uiSystem/tiles.png +0 -0
  39. package/package.json +5 -5
  40. package/plugins/box2d.js +167 -63
  41. package/plugins/postProcess.js +47 -28
  42. package/plugins/uiSystem.js +18 -18
  43. package/plugins/zzfxm.js +2 -2
  44. package/reference.md +4 -3
  45. package/src/engine.js +182 -181
  46. package/src/engineAudio.js +47 -63
  47. package/src/engineDebug.js +8 -8
  48. package/src/engineDraw.js +184 -124
  49. package/src/engineExport.js +325 -334
  50. package/src/engineFont.png +0 -0
  51. package/src/engineInput.js +18 -24
  52. package/src/engineMath.js +24 -113
  53. package/src/engineObject.js +27 -26
  54. package/src/engineParticles.js +2 -2
  55. package/src/engineTileLayer.js +229 -192
  56. package/src/engineUtilities.js +100 -4
  57. package/src/engineWebGL.js +124 -73
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.16.2';
36
+ const engineVersion = '1.17.5';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -88,8 +88,9 @@ function getPaused() { return paused; }
88
88
  * @memberof Engine */
89
89
  function setPaused(isPaused=true) { paused = isPaused; }
90
90
 
91
- // Frame time tracking
91
+ // Engine internal variables
92
92
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
93
+ let showEngineVersion = true;
93
94
 
94
95
  ///////////////////////////////////////////////////////////////////////////////
95
96
  // plugin hooks
@@ -153,16 +154,17 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
153
154
  * @example
154
155
  * // Basic engine startup
155
156
  * engineInit(
156
- * () => { LOG('Game initialized!'); }, // gameInit
157
- * () => { updateGameLogic(); }, // gameUpdate
158
- * () => { updateUI(); }, // gameUpdatePost
159
- * () => { drawBackground(); }, // gameRender
160
- * () => { drawHUD(); }, // gameRenderPost
157
+ * ()=> { LOG('Game initialized!'); }, // gameInit
158
+ * ()=> { updateGameLogic(); }, // gameUpdate
159
+ * ()=> { updateUI(); }, // gameUpdatePost
160
+ * ()=> { drawBackground(); }, // gameRender
161
+ * ()=> { drawHUD(); }, // gameRenderPost
161
162
  * ['tiles.png', 'tilesLevel.png'] // images to load
162
163
  * );
163
164
  * @memberof Engine */
164
165
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
165
166
  {
167
+ showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
166
168
  ASSERT(!mainContext, 'engine already initialized');
167
169
  ASSERT(isArray(imageSources), 'pass in images as array');
168
170
 
@@ -320,7 +322,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
320
322
 
321
323
  // responsive aspect ratio with native resolution
322
324
  const innerAspect = innerWidth / innerHeight;
323
- ASSERT(canvasMinAspect <= canvasMaxAspect);
325
+ ASSERT(canvasMinAspect <= canvasMaxAspect);
324
326
  if (canvasMaxAspect && innerAspect > canvasMaxAspect)
325
327
  {
326
328
  // full height
@@ -377,7 +379,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
377
379
  debugInit();
378
380
 
379
381
  // setup canvases
380
- // transform way is still more reliable then flexbox or grid
382
+ // transform way is still more reliable than flexbox or grid
381
383
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
382
384
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
383
385
  mainCanvas.style.cssText = styleCanvas;
@@ -422,18 +424,20 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
422
424
  }));
423
425
  }
424
426
 
427
+ // load engine font image
428
+ promises.push(fontImageInit());
429
+
425
430
  if (showSplashScreen)
426
431
  {
427
432
  // draw splash screen
428
433
  promises.push(new Promise(resolve =>
429
434
  {
430
435
  let t = 0;
431
- console.log(`${engineName} Engine v${engineVersion}`);
432
436
  updateSplash();
433
437
  function updateSplash()
434
438
  {
435
439
  inputClear();
436
- drawEngineSplashScreen(t+=.01);
440
+ drawEngineLogo(t+=.01);
437
441
  t>1 ? resolve() : setTimeout(updateSplash, 16);
438
442
  }
439
443
  }));
@@ -449,172 +453,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
449
453
  await gameInit();
450
454
  engineUpdate();
451
455
  }
452
-
453
- ///////////////////////////////////////////////////////////////////////////
454
- // LittleJS Splash Screen
455
- function drawEngineSplashScreen(t)
456
- {
457
- const x = mainContext;
458
- const w = mainCanvas.width = innerWidth;
459
- const h = mainCanvas.height = innerHeight;
460
-
461
- {
462
- // background
463
- const p3 = percent(t, 1, .8);
464
- const p4 = percent(t, 0, .5);
465
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
466
- g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
467
- g.addColorStop(1,hsl(0,0,0,p3).toString());
468
- x.save();
469
- x.fillStyle = g;
470
- x.fillRect(0,0,w,h);
471
- }
472
-
473
- // draw LittleJS logo...
474
- const rect = (X, Y, W, H, C)=>
475
- {
476
- x.beginPath();
477
- x.rect(X,Y,W,C?H*p:H);
478
- x.fillStyle = C;
479
- C ? x.fill() : x.stroke();
480
- };
481
- const line = (X, Y, Z, W)=>
482
- {
483
- x.beginPath();
484
- x.lineTo(X,Y);
485
- x.lineTo(Z,W);
486
- x.stroke();
487
- };
488
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
489
- {
490
- const D = (A+B)/2, E = p*(B-A)/2;
491
- x.beginPath();
492
- F && x.lineTo(X,Y);
493
- x.arc(X,Y,R,D-E,D+E);
494
- x.fillStyle = C;
495
- C ? x.fill() : x.stroke();
496
- };
497
- const color = (c=0, l=0) =>
498
- hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();
499
- const alpha = wave(1,1,t);
500
- const p = percent(alpha, .1, .5);
501
-
502
- // setup
503
- x.translate(w/2,h/2);
504
- const size = min(6, min(w,h)/99); // fit to screen
505
- x.scale(size,size);
506
- x.translate(-40,-35);
507
- x.lineJoin = x.lineCap = 'round';
508
- x.lineWidth = .1 + p*1.9;
509
-
510
- // drawing effect
511
- const p2 = percent(alpha,.1,1);
512
- x.setLineDash([99*p2,99]);
513
-
514
- // cab top
515
- rect(7,16,18,-8,color(2,2));
516
- rect(7,8,18,4,color(2,3));
517
- rect(25,8,8,8,color(2,1));
518
- rect(25,8,-18,8);
519
- rect(25,8,8,8);
520
-
521
- // cab
522
- rect(25,16,7,23,color());
523
- rect(11,39,14,-23,color(1,1));
524
- rect(11,16,14,18,color(1,2));
525
- rect(11,16,14,8,color(1,3));
526
- rect(25,16,-14,24);
527
-
528
- // cab window
529
- rect(15,29,6,-9,color(2,2));
530
- circle(15,21,5,0,PI/2,color(2,4),1);
531
- rect(21,21,-6,9);
532
-
533
- // little stack
534
- rect(37,14,9,6,color(3,2));
535
- rect(37,14,4.5,6,color(3,3));
536
- rect(37,14,9,6);
537
-
538
- // big stack
539
- rect(50,20,10,-8,color(0,1));
540
- rect(50,20,6.5,-8,color(0,2));
541
- rect(50,20,3.5,-8,color(0,3));
542
- rect(50,20,10,-8);
543
- circle(55,2,11.4,.5,PI-.5,color(3,3));
544
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
545
- circle(55,2,11.4,.5,PI-.5);
546
- rect(45,7,20,-7,color(0,2));
547
- rect(45,-1,20,4,color(0,3));
548
- rect(45,-1,20,8);
549
-
550
- // engine
551
- for (let i=5; i--;)
552
- {
553
- // stagger radius to fix slight seam
554
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
555
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
556
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
557
- }
558
-
559
- // engine outline
560
- circle(36,30,10,PI/2,PI*3/2);
561
- circle(48,30,10,PI/2,PI*3/2);
562
- circle(60,30,10);
563
- line(36,20,60,20);
564
-
565
- // engine front light
566
- circle(60,30,4,PI,3*PI,color(3,2));
567
- circle(60,30,4,PI,2*PI,color(3,3));
568
- circle(60,30,4,PI,3*PI);
569
-
570
- // front brush
571
- for (let i=6; i--;)
572
- {
573
- x.beginPath();
574
- x.lineTo(53,54);
575
- x.lineTo(53,40);
576
- x.lineTo(53+(1+i*2.9)*p,40);
577
- x.lineTo(53+(4+i*3.5)*p,54);
578
- x.fillStyle = color(0,i%2+2);
579
- x.fill();
580
- i%2 && x.stroke();
581
- }
582
-
583
- // wheels
584
- rect(6,40,5,5);
585
- rect(6,40,5,5,color());
586
- rect(15,54,38,-14,color());
587
- for (let i=3; i--;)
588
- for (let j=2; j--;)
589
- {
590
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
591
- x.stroke();
592
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
593
- x.stroke();
594
- }
595
- line(6,40,68,40); // center
596
- line(77,54,4,54); // bottom
597
-
598
- // draw engine name
599
- const s = engineName;
600
- x.font = '900 16px arial';
601
- x.textAlign = 'center';
602
- x.textBaseline = 'top';
603
- x.lineWidth = .1+p*3.9;
604
- let w2 = 0;
605
- for (let i=0; i<s.length; ++i)
606
- w2 += x.measureText(s[i]).width;
607
- for (let j=2; j--;)
608
- for (let i=0, X=41-w2/2; i<s.length; ++i)
609
- {
610
- x.fillStyle = color(i,2);
611
- const w = x.measureText(s[i]).width;
612
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
613
- X += w;
614
- }
615
- x.restore();
616
- }
617
- ///////////////////////////////////////////////////////////////////////////
618
456
  }
619
457
 
620
458
  /** Update each engine object, remove destroyed objects, and update time
@@ -628,8 +466,7 @@ function engineObjectsUpdate()
628
466
  // recursive object update
629
467
  function updateObject(o)
630
468
  {
631
- if (o.destroyed)
632
- return;
469
+ if (o.destroyed) return;
633
470
 
634
471
  o.update();
635
472
  for (const child of o.children)
@@ -637,8 +474,7 @@ function engineObjectsUpdate()
637
474
  }
638
475
  for (const o of engineObjects)
639
476
  {
640
- if (o.parent)
641
- continue;
477
+ if (o.parent || o.destroyed) continue;
642
478
 
643
479
  // update top level objects
644
480
  o.update();
@@ -727,6 +563,171 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
727
563
 
728
564
  debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
729
565
  return hitObjects;
566
+ }
567
+
568
+ ///////////////////////////////////////////////////////////////////////////////
569
+ function drawEngineLogo(t)
570
+ {
571
+ // LittleJS Logo and Splash Screen
572
+ const x = mainContext;
573
+ const w = mainCanvas.width = innerWidth;
574
+ const h = mainCanvas.height = innerHeight;
575
+
576
+ {
577
+ // background
578
+ const p3 = percent(t, 1, .8);
579
+ const p4 = percent(t, 0, .5);
580
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
581
+ g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
582
+ g.addColorStop(1,hsl(0,0,0,p3).toString());
583
+ x.save();
584
+ x.fillStyle = g;
585
+ x.fillRect(0,0,w,h);
586
+ }
587
+
588
+ // draw LittleJS logo...
589
+ const rect = (X, Y, W, H, C)=>
590
+ {
591
+ x.beginPath();
592
+ x.rect(X,Y,W,C?H*p:H);
593
+ x.fillStyle = C;
594
+ C ? x.fill() : x.stroke();
595
+ };
596
+ const line = (X, Y, Z, W)=>
597
+ {
598
+ x.beginPath();
599
+ x.lineTo(X,Y);
600
+ x.lineTo(Z,W);
601
+ x.stroke();
602
+ };
603
+ const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
604
+ {
605
+ const D = (A+B)/2, E = p*(B-A)/2;
606
+ x.beginPath();
607
+ F && x.lineTo(X,Y);
608
+ x.arc(X,Y,R,D-E,D+E);
609
+ x.fillStyle = C;
610
+ C ? x.fill() : x.stroke();
611
+ };
612
+ const color = (c=0, l=0)=>
613
+ hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
614
+ const alpha = wave(1,1,t);
615
+ const p = percent(alpha, .1, .5);
616
+
617
+ // setup
618
+ x.translate(w/2,h/2);
619
+ const size = min(6, min(w,h)/99); // fit to screen
620
+ x.scale(size,size);
621
+ x.translate(-40,-35);
622
+ x.lineJoin = x.lineCap = 'round';
623
+ x.lineWidth = .1 + p*1.9;
624
+
625
+ // drawing effect
626
+ const p2 = percent(alpha,.1,1);
627
+ x.setLineDash([99*p2,99]);
628
+
629
+ // cab top
630
+ rect(7,16,18,-8,color(2,2));
631
+ rect(7,8,18,4,color(2,3));
632
+ rect(25,8,8,8,color(2,1));
633
+ rect(25,8,-18,8);
634
+ rect(25,8,8,8);
635
+
636
+ // cab
637
+ rect(25,16,7,24,color());
638
+ rect(11,39,14,-23,color(1,1));
639
+ rect(11,16,14,18,color(1,2));
640
+ rect(11,16,14,8,color(1,3));
641
+ rect(25,16,-14,24);
642
+
643
+ // cab window
644
+ rect(15,29,6,-9,color(2,2));
645
+ circle(15,21,5,0,PI/2,color(2,4),1);
646
+ rect(21,21,-6,9);
647
+
648
+ // little stack
649
+ rect(37,14,9,6,color(3,2));
650
+ rect(37,14,4.5,6,color(3,3));
651
+ rect(37,14,9,6);
652
+
653
+ // big stack
654
+ rect(50,20,10,-8,color(0,1));
655
+ rect(50,20,6.5,-8,color(0,2));
656
+ rect(50,20,3.5,-8,color(0,3));
657
+ rect(50,20,10,-8);
658
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
659
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
660
+ circle(55,2,11.4,.5,PI-.5);
661
+ rect(45,7,20,-7,color(0,2));
662
+ rect(45,-1,20,4,color(0,3));
663
+ rect(45,-1,20,8);
664
+
665
+ // engine
666
+ for (let i=5; i--;)
667
+ {
668
+ // stagger radius to fix slight seam
669
+ circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
670
+ circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
671
+ circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
672
+ }
673
+
674
+ // engine outline
675
+ circle(36,30,10,PI/2,PI*3/2);
676
+ circle(48,30,10,PI/2,PI*3/2);
677
+ circle(60,30,10);
678
+ line(36,20,60,20);
679
+
680
+ // engine front light
681
+ circle(60,30,4,PI,3*PI,color(3,2));
682
+ circle(60,30,4,PI,2*PI,color(3,3));
683
+ circle(60,30,4,PI,3*PI);
684
+
685
+ // front brush
686
+ for (let i=6; i--;)
687
+ {
688
+ x.beginPath();
689
+ x.lineTo(53,54);
690
+ x.lineTo(53,40);
691
+ x.lineTo(53+(1+i*2.9)*p,40);
692
+ x.lineTo(53+(4+i*3.5)*p,54);
693
+ x.fillStyle = color(0,i%2+2);
694
+ x.fill();
695
+ i%2 && x.stroke();
696
+ }
697
+
698
+ // wheels
699
+ rect(6,40,5,5);
700
+ rect(6,40,5,5,color());
701
+ rect(15,54,38,-14,color());
702
+ for (let i=3; i--;)
703
+ for (let j=2; j--;)
704
+ {
705
+ circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
706
+ x.stroke();
707
+ circle(15*i+15,47,j?7:1,0,PI,color(i,2));
708
+ x.stroke();
709
+ }
710
+ line(6,40,68,40); // center
711
+ line(77,54,4,54); // bottom
712
+
713
+ // draw engine name
714
+ const s = engineName;
715
+ x.font = '900 16px arial';
716
+ x.textAlign = 'center';
717
+ x.textBaseline = 'top';
718
+ x.lineWidth = .1+p*3.9;
719
+ let w2 = 0;
720
+ for (let i=0; i<s.length; ++i)
721
+ w2 += x.measureText(s[i]).width;
722
+ for (let j=2; j--;)
723
+ for (let i=0, X=41-w2/2; i<s.length; ++i)
724
+ {
725
+ x.fillStyle = color(i,2);
726
+ const w = x.measureText(s[i]).width;
727
+ x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
728
+ X += w;
729
+ }
730
+ x.restore();
730
731
  }
731
732
  /**
732
733
  * LittleJS - Release Mode
@@ -767,12 +768,11 @@ function debugVideoCaptureStop (){}
767
768
  function debugVideoCaptureUpdate(){}
768
769
  function debugProtectConstant(o){ return o; }
769
770
  /**
770
- * LittleJS Utility Classes and Functions
771
+ * LittleJS Math Classes and Functions
771
772
  * - General purpose math library
772
- * - Vector2 - fast, simple, easy 2D vector class
773
- * - Color - holds a rgba color with some math functions
774
- * - Timer - tracks time automatically
775
773
  * - RandomGenerator - seeded random number generator
774
+ * - Vector2 - fast, simple, easy 2D vector class
775
+ * - Color - holds a rgba color with math functions
776
776
  * @namespace Math
777
777
  */
778
778
 
@@ -783,25 +783,25 @@ function debugProtectConstant(o){ return o; }
783
783
  const PI = Math.PI;
784
784
 
785
785
  /** Returns absolute value of value passed in
786
- * @param {number} value
786
+ * @param {number} x
787
787
  * @return {number}
788
788
  * @memberof Math */
789
789
  const abs = Math.abs;
790
790
 
791
791
  /** Returns floored value of value passed in
792
- * @param {number} value
792
+ * @param {number} x
793
793
  * @return {number}
794
794
  * @memberof Math */
795
795
  const floor = Math.floor;
796
796
 
797
797
  /** Returns ceiled value of value passed in
798
- * @param {number} value
798
+ * @param {number} x
799
799
  * @return {number}
800
800
  * @memberof Math */
801
801
  const ceil = Math.ceil;
802
802
 
803
803
  /** Returns rounded value passed in
804
- * @param {number} value
804
+ * @param {number} x
805
805
  * @return {number}
806
806
  * @memberof Math */
807
807
  const round = Math.round;
@@ -819,7 +819,7 @@ const min = Math.min;
819
819
  const max = Math.max;
820
820
 
821
821
  /** Returns the sign of value passed in
822
- * @param {number} value
822
+ * @param {number} x
823
823
  * @return {number}
824
824
  * @memberof Math */
825
825
  const sign = Math.sign;
@@ -831,25 +831,25 @@ const sign = Math.sign;
831
831
  const hypot = Math.hypot;
832
832
 
833
833
  /** Returns log2 of value passed in
834
- * @param {number} value
834
+ * @param {number} x
835
835
  * @return {number}
836
836
  * @memberof Math */
837
837
  const log2 = Math.log2;
838
838
 
839
839
  /** Returns sin of value passed in
840
- * @param {number} value
840
+ * @param {number} x
841
841
  * @return {number}
842
842
  * @memberof Math */
843
843
  const sin = Math.sin;
844
844
 
845
845
  /** Returns cos of value passed in
846
- * @param {number} value
846
+ * @param {number} x
847
847
  * @return {number}
848
848
  * @memberof Math */
849
849
  const cos = Math.cos;
850
850
 
851
851
  /** Returns tan of value passed in
852
- * @param {number} value
852
+ * @param {number} x
853
853
  * @return {number}
854
854
  * @memberof Math */
855
855
  const tan = Math.tan;
@@ -960,11 +960,11 @@ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
960
960
 
961
961
  /** Returns true if two axis aligned bounding boxes are overlapping
962
962
  * this can be used for simple collision detection between objects
963
- * @param {Vector2} posA - Center of box A
964
- * @param {Vector2} sizeA - Size of box A
965
- * @param {Vector2} posB - Center of box B
966
- * @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
967
- * @return {boolean} - True if overlapping
963
+ * @param {Vector2} posA - Center of box A
964
+ * @param {Vector2} sizeA - Size of box A
965
+ * @param {Vector2} posB - Center of box B
966
+ * @param {Vector2} [sizeB=vec2()] - Size of box B, uses a point if undefined
967
+ * @return {boolean} - True if overlapping
968
968
  * @memberof Math */
969
969
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
970
970
  {
@@ -1038,7 +1038,7 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1038
1038
  * @param {any} s
1039
1039
  * @return {boolean}
1040
1040
  * @memberof Math */
1041
- function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1041
+ function isString(s) { return s != null && typeof s?.toString() === 'string'; }
1042
1042
 
1043
1043
  /**
1044
1044
  * Check if object is an array
@@ -1182,8 +1182,8 @@ function randInCircle(radius=1, minRadius=0)
1182
1182
  { return radius > 0 ? randVec2(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
1183
1183
 
1184
1184
  /** Returns a random color between the two passed in colors, combine components if linear
1185
- * @param {Color} [colorA=(1,1,1,1)]
1186
- * @param {Color} [colorB=(0,0,0,1)]
1185
+ * @param {Color} [colorA=WHITE]
1186
+ * @param {Color} [colorB=BLACK]
1187
1187
  * @param {boolean} [linear]
1188
1188
  * @return {Color}
1189
1189
  * @memberof Random */
@@ -1262,8 +1262,8 @@ class RandomGenerator
1262
1262
  { return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
1263
1263
 
1264
1264
  /** Returns a random color between the two passed in colors, combine components if linear
1265
- * @param {Color} [colorA=(1,1,1,1)]
1266
- * @param {Color} [colorB=(0,0,0,1)]
1265
+ * @param {Color} [colorA=WHITE]
1266
+ * @param {Color} [colorB=BLACK]
1267
1267
  * @param {boolean} [linear]
1268
1268
  * @return {Color} */
1269
1269
  randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
@@ -1307,7 +1307,7 @@ class RandomGenerator
1307
1307
  * a = vec2(5); // set a to (5, 5)
1308
1308
  * b = vec2(); // set b to (0, 0)
1309
1309
  * @memberof Math */
1310
- function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
1310
+ function vec2(x=0, y) { return new Vector2(x, y ?? x); }
1311
1311
 
1312
1312
  /**
1313
1313
  * Check if object is a valid Vector2
@@ -1515,10 +1515,6 @@ class Vector2
1515
1515
  * @return {number} */
1516
1516
  area() { return abs(this.x * this.y); }
1517
1517
 
1518
- /** Returns true if this vector is (0,0)
1519
- * @return {boolean} */
1520
- isZero() { return !this.x && !this.y; }
1521
-
1522
1518
  /** Returns a new vector that is p percent between this and the vector passed in
1523
1519
  * @param {Vector2} v - other vector
1524
1520
  * @param {number} percent
@@ -1880,7 +1876,97 @@ const PURPLE = debugProtectConstant(rgb(.5,0,1));
1880
1876
  /** Color - Magenta #ff00ff
1881
1877
  * @type {Color}
1882
1878
  * @memberof Math */
1883
- const MAGENTA = debugProtectConstant(rgb(1,0,1));
1879
+ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1880
+ /**
1881
+ * LittleJS Utility Classes and Functions
1882
+ * - General purpose utilities
1883
+ * - Timer - tracks time automatically
1884
+ * @namespace Utilities
1885
+ */
1886
+
1887
+ /** Formats seconds to mm:ss style for display purposes
1888
+ * @param {number} t - time in seconds
1889
+ * @return {string}
1890
+ * @memberof Utilities */
1891
+ function formatTime(t)
1892
+ {
1893
+ const sign = t < 0 ? '-' : '';
1894
+ t = abs(t)|0;
1895
+ return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
1896
+ }
1897
+
1898
+ /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
1899
+ * @param {string} url - URL of JSON file
1900
+ * @return {Promise<object>}
1901
+ * @memberof Utilities */
1902
+ async function fetchJSON(url)
1903
+ {
1904
+ const response = await fetch(url);
1905
+ if (!response.ok)
1906
+ throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1907
+ return response.json();
1908
+ }
1909
+
1910
+ ///////////////////////////////////////////////////////////////////////////////
1911
+
1912
+ /** Save a text file to disk
1913
+ * @param {string} text
1914
+ * @param {string} [filename]
1915
+ * @param {string} [type]
1916
+ * @memberof Utilities */
1917
+ function saveText(text, filename='text', type='text/plain')
1918
+ { saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
1919
+
1920
+ /** Save a canvas to disk
1921
+ * @param {HTMLCanvasElement|OffscreenCanvas} canvas
1922
+ * @param {string} [filename]
1923
+ * @param {string} [type]
1924
+ * @memberof Utilities */
1925
+ function saveCanvas(canvas, filename='screenshot', type='image/png')
1926
+ {
1927
+ if (canvas instanceof OffscreenCanvas)
1928
+ {
1929
+ // copy to temporary canvas and save
1930
+ const saveCanvas = document.createElement('canvas');
1931
+ saveCanvas.width = canvas.width;
1932
+ saveCanvas.height = canvas.height;
1933
+ saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
1934
+ saveDataURL(saveCanvas.toDataURL(type), filename);
1935
+ }
1936
+ else
1937
+ saveDataURL(canvas.toDataURL(type), filename);
1938
+ }
1939
+
1940
+ /** Save a data url to disk
1941
+ * @param {string} url
1942
+ * @param {string} [filename]
1943
+ * @param {number} [revokeTime] - how long before revoking the url
1944
+ * @memberof Utilities */
1945
+ function saveDataURL(url, filename='download', revokeTime)
1946
+ {
1947
+ ASSERT(isString(url), 'saveDataURL requires url string');
1948
+ ASSERT(isString(filename), 'saveDataURL requires filename string');
1949
+
1950
+ // create link for saving screenshots
1951
+ const link = document.createElement('a');
1952
+ link.download = filename;
1953
+ link.href = url;
1954
+ link.click();
1955
+ if (revokeTime !== undefined)
1956
+ setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
1957
+ }
1958
+
1959
+ /** Share content using the native share dialog if available
1960
+ * @param {string} title - title of the share
1961
+ * @param {string} url - url to share
1962
+ * @param {Function} [callback] - Called when share is complete
1963
+ * @memberof Utilities */
1964
+ function shareURL(title, url, callback)
1965
+ {
1966
+ ASSERT(isString(title), 'shareURL requires title string');
1967
+ ASSERT(isString(url), 'shareURL requires url string');
1968
+ navigator.share?.({title, url}).then(()=>callback?.());
1969
+ }
1884
1970
 
1885
1971
  ///////////////////////////////////////////////////////////////////////////////
1886
1972
 
@@ -1965,84 +2051,6 @@ class Timer
1965
2051
  * @return {number} */
1966
2052
  valueOf() { return this.get(); }
1967
2053
  }
1968
- /**
1969
- * LittleJS Utility Classes and Functions
1970
- * - General purpose math library
1971
- * - Vector2 - fast, simple, easy 2D vector class
1972
- * - Color - holds a rgba color with some math functions
1973
- * - Timer - tracks time automatically
1974
- * - RandomGenerator - seeded random number generator
1975
- * @namespace Utilities
1976
- */
1977
-
1978
- /** Formats seconds to mm:ss style for display purposes
1979
- * @param {number} t - time in seconds
1980
- * @return {string}
1981
- * @memberof Utilities */
1982
- function formatTime(t)
1983
- {
1984
- const sign = t < 0 ? '-' : '';
1985
- t = abs(t)|0;
1986
- return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
1987
- }
1988
-
1989
- /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
1990
- * @param {string} url - URL of JSON file
1991
- * @return {Promise<object>}
1992
- * @memberof Utilities */
1993
- async function fetchJSON(url)
1994
- {
1995
- const response = await fetch(url);
1996
- if (!response.ok)
1997
- throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1998
- return response.json();
1999
- }
2000
-
2001
- ///////////////////////////////////////////////////////////////////////////////
2002
-
2003
- /** Save a text file to disk
2004
- * @param {string} text
2005
- * @param {string} [filename]
2006
- * @param {string} [type]
2007
- * @memberof Utilities */
2008
- function saveText(text, filename='text', type='text/plain')
2009
- { saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
2010
-
2011
- /** Save a canvas to disk
2012
- * @param {HTMLCanvasElement|OffscreenCanvas} canvas
2013
- * @param {string} [filename]
2014
- * @param {string} [type]
2015
- * @memberof Utilities */
2016
- function saveCanvas(canvas, filename='screenshot', type='image/png')
2017
- {
2018
- if (canvas instanceof OffscreenCanvas)
2019
- {
2020
- // copy to temporary canvas and save
2021
- const saveCanvas = document.createElement('canvas');
2022
- saveCanvas.width = canvas.width;
2023
- saveCanvas.height = canvas.height;
2024
- saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
2025
- saveDataURL(saveCanvas.toDataURL(type), filename);
2026
- }
2027
- else
2028
- saveDataURL(canvas.toDataURL(type), filename);
2029
- }
2030
-
2031
- /** Save a data url to disk
2032
- * @param {string} url
2033
- * @param {string} [filename]
2034
- * @param {number} [revokeTime] - how long before revoking the url
2035
- * @memberof Utilities */
2036
- function saveDataURL(url, filename='download', revokeTime)
2037
- {
2038
- // create link for saving screenshots
2039
- const link = document.createElement('a');
2040
- link.download = filename;
2041
- link.href = url;
2042
- link.click();
2043
- if (revokeTime !== undefined)
2044
- setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
2045
- }
2046
2054
  /**
2047
2055
  * LittleJS Engine Settings
2048
2056
  * - All settings for the engine are here
@@ -2669,10 +2677,10 @@ function setDebugKey(key) { debugKey = key; }
2669
2677
  class EngineObject
2670
2678
  {
2671
2679
  /** Create an engine object and adds it to the list of objects
2672
- * @param {Vector2} [pos=(0,0)] - World space position of the object
2673
- * @param {Vector2} [size=(1,1)] - World space size of the object
2674
- * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2675
- * @param {number} [angle] - Angle the object is rotated by
2680
+ * @param {Vector2} [pos=vec2()] - World space position of the object
2681
+ * @param {Vector2} [size=vec2(1)] - World space size of the object
2682
+ * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2683
+ * @param {number} [angle] - Angle the object is rotated by
2676
2684
  * @param {Color} [color=WHITE] - Color to apply to tile when rendered
2677
2685
  * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2678
2686
  */
@@ -2800,15 +2808,16 @@ class EngineObject
2800
2808
  // physics sanity checks
2801
2809
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
2802
2810
  ASSERT(this.damping >= 0 && this.damping <= 1);
2803
- if (!enablePhysicsSolver || !this.mass) // don't do collision for static objects
2804
- return;
2811
+
2812
+ // don't do collision for static objects or if solver disabled
2813
+ if (!enablePhysicsSolver || !this.mass) return;
2805
2814
 
2806
2815
  const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
2807
2816
  if (this.groundObject)
2808
2817
  {
2809
2818
  // apply friction in local space of ground object
2810
2819
  const friction = max(this.friction, this.groundObject.friction);
2811
- const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
2820
+ const groundSpeed = this.groundObject.velocity.x;
2812
2821
  this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
2813
2822
  this.groundObject = undefined;
2814
2823
  }
@@ -2819,19 +2828,19 @@ class EngineObject
2819
2828
  const epsilon = .001; // necessary to push slightly outside of the collision
2820
2829
  for (const o of engineObjectsCollide)
2821
2830
  {
2831
+ // skip destroyed, child objects, or self collision
2832
+ if (o.destroyed || o.parent || o === this) continue;
2833
+
2822
2834
  // non solid objects don't collide with each other
2823
- if ((!this.isSolid && !o.isSolid) || o.destroyed || o.parent || o === this)
2824
- continue;
2835
+ if (!this.isSolid && !o.isSolid) continue;
2825
2836
 
2826
2837
  // check collision
2827
- if (!this.isOverlappingObject(o))
2828
- continue;
2838
+ if (!this.isOverlappingObject(o)) continue;
2829
2839
 
2830
2840
  // notify objects of collision and check if should be resolved
2831
2841
  const collide1 = this.collideWithObject(o);
2832
2842
  const collide2 = o.collideWithObject(this);
2833
- if (!collide1 || !collide2)
2834
- continue;
2843
+ if (!collide1 || !collide2) continue;
2835
2844
 
2836
2845
  if (isOverlapping(oldPos, this.size, o.pos, o.size))
2837
2846
  {
@@ -2912,7 +2921,7 @@ class EngineObject
2912
2921
  if (this.collideTiles)
2913
2922
  {
2914
2923
  // check collision against tiles
2915
- const hitLayer = tileCollisionTest(this.pos, this.size, this)
2924
+ const hitLayer = tileCollisionTest(this.pos, this.size, this);
2916
2925
  if (hitLayer)
2917
2926
  {
2918
2927
  // if already was stuck in collision, don't do anything
@@ -2933,7 +2942,7 @@ class EngineObject
2933
2942
  const delta = y - this.pos.y;
2934
2943
  if (delta < maxMoveUp)
2935
2944
  if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
2936
- {
2945
+ {
2937
2946
  this.pos.y = y;
2938
2947
  debugPhysics && debugRect(this.pos, this.size, '#ff0');
2939
2948
  return;
@@ -2993,10 +3002,10 @@ class EngineObject
2993
3002
 
2994
3003
  // disconnect from parent and destroy children
2995
3004
  this.destroyed = 1;
2996
- this.parent && this.parent.removeChild(this);
3005
+ this.parent?.removeChild(this);
2997
3006
  for (const child of this.children)
2998
3007
  {
2999
- child.parent = 0;
3008
+ child.parent = undefined;
3000
3009
  child.destroy();
3001
3010
  }
3002
3011
  }
@@ -3017,15 +3026,15 @@ class EngineObject
3017
3026
  * @param {Vector2} vec - world space vector */
3018
3027
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
3019
3028
 
3020
- /** Called to check if a tile collision should be resolved
3029
+ /** Called to check if a tile collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
3021
3030
  * @param {number} tileData - the value of the tile at the position
3022
- * @param {Vector2} pos - tile where the collision occurred
3023
- * @return {boolean} - true if the collision should be resolved */
3031
+ * @param {Vector2} pos - tile where the collision occurred
3032
+ * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
3024
3033
  collideWithTile(tileData, pos) { return tileData > 0; }
3025
3034
 
3026
- /** Called to check if a object collision should be resolved
3035
+ /** Called by the engine to check if an object collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
3027
3036
  * @param {EngineObject} object - the object to test against
3028
- * @return {boolean} - true if the collision should be resolved
3037
+ * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
3029
3038
  */
3030
3039
  collideWithObject(object) { return true; }
3031
3040
 
@@ -3066,9 +3075,9 @@ class EngineObject
3066
3075
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
3067
3076
  getMirrorSign() { return this.mirror ? -1 : 1; }
3068
3077
 
3069
- /** Attaches a child to this with a local transform, returns child for chaining
3078
+ /** Attaches a child to this with a local transform, returns child for chaining
3070
3079
  * @param {EngineObject} child
3071
- * @param {Vector2} [localPos=(0,0)]
3080
+ * @param {Vector2} [localPos=vec2()]
3072
3081
  * @param {number} [localAngle]
3073
3082
  * @return {EngineObject} The child object added */
3074
3083
  addChild(child, localPos=vec2(), localAngle=0)
@@ -3092,7 +3101,7 @@ class EngineObject
3092
3101
  const index = this.children.indexOf(child);
3093
3102
  ASSERT(index >= 0, 'child not found in children array');
3094
3103
  index >= 0 && this.children.splice(index, 1);
3095
- child.parent = 0;
3104
+ child.parent = undefined;
3096
3105
  }
3097
3106
 
3098
3107
  /** Check if overlapping another engine object
@@ -3104,7 +3113,7 @@ class EngineObject
3104
3113
 
3105
3114
  /** Check if overlapping a point or aligned bounding box
3106
3115
  * @param {Vector2} pos - Center of box
3107
- * @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
3116
+ * @param {Vector2} [size=vec2()] - Size of box, uses a point if undefined
3108
3117
  * @return {boolean} */
3109
3118
  isOverlapping(pos, size=vec2())
3110
3119
  { return isOverlapping(this.pos, this.size, pos, size); }
@@ -3235,10 +3244,11 @@ let drawCount;
3235
3244
  * Create a tile info object using a grid based system
3236
3245
  * - This can take vecs or floats for easier use and conversion
3237
3246
  * - If an index is passed in, the tile size and index will determine the position
3238
- * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
3247
+ * @param {Vector2|number} [index=0] - Index of the tile in 1d or 2d form
3239
3248
  * @param {Vector2|number} [size] - Size of tile in pixels
3240
- * @param {number} [textureIndex] - Texture index to use
3249
+ * @param {TextureInfo|number} [texture] - Texture index or info to use
3241
3250
  * @param {number} [padding] - How many pixels padding around tiles
3251
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3242
3252
  * @return {TileInfo}
3243
3253
  * @example
3244
3254
  * tile(2) // a tile at index 2 using the default tile size of 16
@@ -3246,36 +3256,43 @@ let drawCount;
3246
3256
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
3247
3257
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
3248
3258
  * @memberof Draw */
3249
- function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding)
3259
+ function tile(index=new Vector2, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3250
3260
  {
3251
- if (headlessMode)
3252
- return new TileInfo;
3261
+ ASSERT(isVector2(index) || typeof index === 'number', 'index must be a vec2 or number');
3262
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
3263
+ ASSERT(isNumber(texture) || texture instanceof TextureInfo, 'texture must be a number or TextureInfo');
3264
+ ASSERT(isNumber(padding), 'padding must be a number');
3265
+
3266
+ if (headlessMode) return new TileInfo;
3253
3267
 
3254
- // if size is a number, make it a vector
3255
3268
  if (typeof size === 'number')
3256
3269
  {
3270
+ // if size is a number, make it a vector
3257
3271
  ASSERT(size > 0);
3258
3272
  size = new Vector2(size, size);
3259
3273
  }
3260
3274
 
3261
3275
  // create tile info object
3262
- const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
3276
+ const textureInfo = typeof texture === 'number' ?
3277
+ textureInfos[texture] : texture;
3263
3278
 
3264
3279
  // get the position of the tile
3265
- const textureInfo = textureInfos[textureIndex];
3266
- ASSERT(!!textureInfo, 'Texture not loaded');
3267
3280
  const sizePaddedX = size.x + padding*2;
3268
3281
  const sizePaddedY = size.y + padding*2;
3269
- if (typeof pos === 'number')
3282
+ let x, y;
3283
+ if (typeof index === 'number')
3270
3284
  {
3271
3285
  const cols = textureInfo.size.x / sizePaddedX |0;
3272
- ASSERT(cols > 0, 'Tile size is too big for texture');
3273
- const posX = pos % cols, posY = (pos / cols) |0;
3274
- tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
3286
+ x = index % cols;
3287
+ y = index / cols |0;
3275
3288
  }
3276
3289
  else
3277
- tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
3278
- return tileInfo;
3290
+ {
3291
+ x = index.x;
3292
+ y = index.y;
3293
+ }
3294
+ const pos = new Vector2(x*sizePaddedX + padding, y*sizePaddedY + padding);
3295
+ return new TileInfo(pos, size, textureInfo, padding, bleed);
3279
3296
  }
3280
3297
 
3281
3298
  /**
@@ -3285,24 +3302,22 @@ function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=til
3285
3302
  class TileInfo
3286
3303
  {
3287
3304
  /** Create a tile info object
3288
- * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3305
+ * @param {Vector2} [pos=vec2()] - Top left corner of tile in pixels
3289
3306
  * @param {Vector2} [size] - Size of tile in pixels
3290
- * @param {number} [textureIndex] - Texture index to use
3291
- * @param {number} [padding] - How many pixels padding around tiles
3292
- * @param {number} [bleed] - How many pixels smaller to draw tiles
3307
+ * @param {TextureInfo} [textureInfo] - Texture info to use
3308
+ * @param {number} [padding] - How many pixels padding around tiles
3309
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3293
3310
  */
3294
- constructor(pos=vec2(), size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3311
+ constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
3295
3312
  {
3296
3313
  /** @property {Vector2} - Top left corner of tile in pixels */
3297
3314
  this.pos = pos.copy();
3298
3315
  /** @property {Vector2} - Size of tile in pixels */
3299
3316
  this.size = size.copy();
3300
- /** @property {number} - Texture index to use */
3301
- this.textureIndex = textureIndex;
3302
3317
  /** @property {number} - How many pixels padding around tiles */
3303
3318
  this.padding = padding;
3304
3319
  /** @property {TextureInfo} - The texture info for this tile */
3305
- this.textureInfo = textureInfos[this.textureIndex];
3320
+ this.textureInfo = textureInfo;
3306
3321
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
3307
3322
  this.bleed = bleed;
3308
3323
  }
@@ -3312,7 +3327,7 @@ class TileInfo
3312
3327
  * @return {TileInfo}
3313
3328
  */
3314
3329
  offset(offset)
3315
- { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleed); }
3330
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed); }
3316
3331
 
3317
3332
  /** Returns a copy of this tile offset by a number of animation frames
3318
3333
  * @param {number} frame - Offset to apply in animation frames
@@ -3321,23 +3336,33 @@ class TileInfo
3321
3336
  frame(frame)
3322
3337
  {
3323
3338
  ASSERT(typeof frame === 'number');
3324
- return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
3339
+ const w = this.size.x + this.padding*2;
3340
+ const x = frame*w;
3341
+ ASSERT(x < this.textureInfo.size.x, 'frame extends beyond texture width!');
3342
+ return this.offset(new Vector2(x));
3325
3343
  }
3326
3344
 
3327
3345
  /**
3328
3346
  * Set this tile to use a full image in a texture info
3329
- * @param {TextureInfo} textureInfo
3347
+ * @param {TextureInfo} [textureInfo]
3330
3348
  * @return {TileInfo}
3331
3349
  */
3332
- setFullImage(textureInfo)
3350
+ setFullImage(textureInfo=this.textureInfo)
3333
3351
  {
3352
+ this.textureInfo = textureInfo;
3334
3353
  this.pos = new Vector2;
3335
3354
  this.size = textureInfo.size.copy();
3336
- this.textureInfo = textureInfo;
3337
- // do not use padding or bleed
3338
3355
  this.bleed = this.padding = 0;
3339
3356
  return this;
3340
3357
  }
3358
+
3359
+ /**
3360
+ * Returns a tile info for an index using this tile as refrence
3361
+ * @param {Vector2|number} [index=0]
3362
+ * @return {TileInfo}
3363
+ */
3364
+ tile(index)
3365
+ { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
3341
3366
  }
3342
3367
 
3343
3368
  /**
@@ -3378,19 +3403,19 @@ class TextureInfo
3378
3403
  ///////////////////////////////////////////////////////////////////////////////
3379
3404
  // Drawing functions
3380
3405
 
3381
- /** Draw textured tile centered in world space, with color applied if using WebGL
3382
- * @param {Vector2} pos - Center of the tile in world space
3383
- * @param {Vector2} [size=(1,1)] - Size of the tile in world space
3384
- * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
3385
- * @param {Color} [color=(1,1,1,1)] - Color to modulate with
3386
- * @param {number} [angle] - Angle to rotate by
3387
- * @param {boolean} [mirror] - Is image flipped along the Y axis?
3388
- * @param {Color} [additiveColor] - Additive color to be applied if any
3406
+ /** Draw textured tile centered in world space
3407
+ * @param {Vector2} pos - Center of the tile in world space
3408
+ * @param {Vector2} [size=vec2(1)] - Size of the tile in world space
3409
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
3410
+ * @param {Color} [color=WHITE] - Color to modulate with
3411
+ * @param {number} [angle] - Angle to rotate by
3412
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
3413
+ * @param {Color} [additiveColor] - Additive color to be applied if any
3389
3414
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
3390
3415
  * @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
3391
3416
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
3392
3417
  * @memberof Draw */
3393
- function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3418
+ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
3394
3419
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
3395
3420
  {
3396
3421
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3400,7 +3425,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3400
3425
  ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
3401
3426
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3402
3427
 
3403
- const textureInfo = tileInfo && tileInfo.textureInfo;
3428
+ const textureInfo = tileInfo?.textureInfo;
3404
3429
  const bleed = tileInfo?.bleed ?? 0;
3405
3430
  if (useWebGL && glEnable)
3406
3431
  {
@@ -3465,8 +3490,8 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3465
3490
 
3466
3491
  /** Draw colored rect centered on pos
3467
3492
  * @param {Vector2} pos
3468
- * @param {Vector2} [size=(1,1)]
3469
- * @param {Color} [color=(1,1,1,1)]
3493
+ * @param {Vector2} [size=vec2(1)]
3494
+ * @param {Color} [color=WHITE]
3470
3495
  * @param {number} [angle]
3471
3496
  * @param {boolean} [useWebGL=glEnable]
3472
3497
  * @param {boolean} [screenSpace]
@@ -3479,9 +3504,9 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3479
3504
 
3480
3505
  /** Draw a rect centered on pos with a gradient from top to bottom
3481
3506
  * @param {Vector2} pos
3482
- * @param {Vector2} [size=(1,1)]
3483
- * @param {Color} [colorTop=(1,1,1,1)]
3484
- * @param {Color} [colorBottom=(0,0,0,1)]
3507
+ * @param {Vector2} [size=vec2(1)]
3508
+ * @param {Color} [colorTop=WHITE]
3509
+ * @param {Color} [colorBottom=BLACK]
3485
3510
  * @param {number} [angle]
3486
3511
  * @param {boolean} [useWebGL=glEnable]
3487
3512
  * @param {boolean} [screenSpace]
@@ -3543,9 +3568,9 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3543
3568
  /** Draw connected lines between a series of points
3544
3569
  * @param {Array<Vector2>} points
3545
3570
  * @param {number} [width]
3546
- * @param {Color} [color=(1,1,1,1)]
3571
+ * @param {Color} [color=WHITE]
3547
3572
  * @param {boolean} [wrap] - Should the last point connect to the first?
3548
- * @param {Vector2} [pos=(0,0)] - Offset to apply
3573
+ * @param {Vector2} [pos=vec2()] - Offset to apply
3549
3574
  * @param {number} [angle] - Angle to rotate by
3550
3575
  * @param {boolean} [useWebGL=glEnable]
3551
3576
  * @param {boolean} [screenSpace]
@@ -3580,13 +3605,9 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3580
3605
  for (let i=0; i<points.length; ++i)
3581
3606
  {
3582
3607
  const point = points[i];
3583
- if (i)
3584
- context.lineTo(point.x, point.y);
3585
- else
3586
- context.moveTo(point.x, point.y);
3608
+ context.lineTo(point.x, point.y);
3587
3609
  }
3588
- if (wrap)
3589
- context.closePath();
3610
+ wrap && context.closePath();
3590
3611
  context.stroke();
3591
3612
  }, screenSpace, context);
3592
3613
  }
@@ -3596,8 +3617,8 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3596
3617
  * @param {Vector2} posA
3597
3618
  * @param {Vector2} posB
3598
3619
  * @param {number} [width]
3599
- * @param {Color} [color=(1,1,1,1)]
3600
- * @param {Vector2} [pos=(0,0)] - Offset to apply
3620
+ * @param {Color} [color=WHITE]
3621
+ * @param {Vector2} [pos=vec2()] - Offset to apply
3601
3622
  * @param {number} [angle] - Angle to rotate by
3602
3623
  * @param {boolean} [useWebGL=glEnable]
3603
3624
  * @param {boolean} [screenSpace]
@@ -3616,12 +3637,12 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3616
3637
 
3617
3638
  /** Draw colored regular polygon using passed in number of sides
3618
3639
  * @param {Vector2} pos
3619
- * @param {Vector2} [size=(1,1)]
3640
+ * @param {Vector2} [size=vec2(1)]
3620
3641
  * @param {number} [sides]
3621
- * @param {Color} [color=(1,1,1,1)]
3642
+ * @param {Color} [color=WHITE]
3622
3643
  * @param {number} [angle]
3623
3644
  * @param {number} [lineWidth]
3624
- * @param {Color} [lineColor=(0,0,0,1)]
3645
+ * @param {Color} [lineColor=BLACK]
3625
3646
  * @param {boolean} [useWebGL=glEnable]
3626
3647
  * @param {boolean} [screenSpace]
3627
3648
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -3644,10 +3665,10 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3644
3665
 
3645
3666
  /** Draw colored polygon using passed in points
3646
3667
  * @param {Array<Vector2>} points - Array of Vector2 points
3647
- * @param {Color} [color=(1,1,1,1)]
3668
+ * @param {Color} [color=WHITE]
3648
3669
  * @param {number} [lineWidth]
3649
- * @param {Color} [lineColor=(0,0,0,1)]
3650
- * @param {Vector2} [pos=(0,0)] - Offset to apply
3670
+ * @param {Color} [lineColor=BLACK]
3671
+ * @param {Vector2} [pos=vec2()] - Offset to apply
3651
3672
  * @param {number} [angle] - Angle to rotate by
3652
3673
  * @param {boolean} [useWebGL=glEnable]
3653
3674
  * @param {boolean} [screenSpace]
@@ -3694,11 +3715,11 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3694
3715
 
3695
3716
  /** Draw colored ellipse using passed in point
3696
3717
  * @param {Vector2} pos
3697
- * @param {Vector2} [size=(1,1)] - Width and height diameter
3698
- * @param {Color} [color=(1,1,1,1)]
3718
+ * @param {Vector2} [size=vec2(1)] - Width and height diameter
3719
+ * @param {Color} [color=WHITE]
3699
3720
  * @param {number} [angle]
3700
3721
  * @param {number} [lineWidth]
3701
- * @param {Color} [lineColor=(0,0,0,1)]
3722
+ * @param {Color} [lineColor=BLACK]
3702
3723
  * @param {boolean} [useWebGL=glEnable]
3703
3724
  * @param {boolean} [screenSpace]
3704
3725
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -3710,9 +3731,12 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
3710
3731
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3711
3732
  ASSERT(isNumber(angle), 'angle must be a number');
3712
3733
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3713
- ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
3734
+ ASSERT(lineWidth >= 0, 'lineWidth must be a positive value or 0');
3714
3735
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3715
3736
 
3737
+ // clamp line width to prevent artifacts
3738
+ lineWidth = clamp(lineWidth, 0, Math.min(size.x, size.y));
3739
+
3716
3740
  if (useWebGL && glEnable)
3717
3741
  {
3718
3742
  // draw as a regular polygon
@@ -3740,9 +3764,9 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
3740
3764
  /** Draw colored circle using passed in point
3741
3765
  * @param {Vector2} pos
3742
3766
  * @param {number} [size=1] - Diameter
3743
- * @param {Color} [color=(1,1,1,1)]
3767
+ * @param {Color} [color=WHITE]
3744
3768
  * @param {number} [lineWidth=0]
3745
- * @param {Color} [lineColor=(0,0,0,1)]
3769
+ * @param {Color} [lineColor=BLACK]
3746
3770
  * @param {boolean} [useWebGL=glEnable]
3747
3771
  * @param {boolean} [screenSpace]
3748
3772
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -3797,9 +3821,9 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3797
3821
  * @param {string|number} text
3798
3822
  * @param {Vector2} pos
3799
3823
  * @param {number} [size]
3800
- * @param {Color} [color=(1,1,1,1)]
3824
+ * @param {Color} [color=WHITE]
3801
3825
  * @param {number} [lineWidth]
3802
- * @param {Color} [lineColor=(0,0,0,1)]
3826
+ * @param {Color} [lineColor=BLACK]
3803
3827
  * @param {CanvasTextAlign} [textAlign='center']
3804
3828
  * @param {string} [font=fontDefault]
3805
3829
  * @param {string} [fontStyle]
@@ -3823,10 +3847,10 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3823
3847
  * Automatically splits new lines into rows
3824
3848
  * @param {string|number} text
3825
3849
  * @param {Vector2} pos
3826
- * @param {number} [size]
3827
- * @param {Color} [color=(1,1,1,1)]
3850
+ * @param {number} size
3851
+ * @param {Color} [color=WHITE]
3828
3852
  * @param {number} [lineWidth]
3829
- * @param {Color} [lineColor=(0,0,0,1)]
3853
+ * @param {Color} [lineColor=BLACK]
3830
3854
  * @param {CanvasTextAlign} [textAlign]
3831
3855
  * @param {string} [font=fontDefault]
3832
3856
  * @param {string} [fontStyle]
@@ -3834,7 +3858,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3834
3858
  * @param {number} [angle]
3835
3859
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3836
3860
  * @memberof Draw */
3837
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
3861
+ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
3838
3862
  {
3839
3863
  ASSERT(isString(text), 'text must be a string');
3840
3864
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3958,7 +3982,7 @@ function worldToScreenDelta(worldDelta)
3958
3982
 
3959
3983
  /** Convert screen space transform to world space
3960
3984
  * @param {Vector2} screenPos
3961
- * @param {Vector2} screenSize
3985
+ * @param {Vector2} screenSize
3962
3986
  * @param {number} [screenAngle]
3963
3987
  * @return {[Vector2, Vector2, number]} - [pos, size, angle]
3964
3988
  * @memberof Draw */
@@ -4019,10 +4043,9 @@ function isOnScreen(pos, size=0)
4019
4043
  * @param {boolean} [additive]
4020
4044
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4021
4045
  * @memberof Draw */
4022
- function setBlendMode(additive=false, context)
4046
+ function setBlendMode(additive=false, context=drawContext)
4023
4047
  {
4024
4048
  glAdditive = additive;
4025
- context ||= drawContext;
4026
4049
  context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
4027
4050
  }
4028
4051
 
@@ -4037,7 +4060,6 @@ function combineCanvases()
4037
4060
  workContext.fillRect(0,0,w,h); // remove background alpha
4038
4061
  glCopyToContext(workContext);
4039
4062
  workContext.drawImage(mainCanvas, 0, 0);
4040
- mainCanvas.width |= 0;
4041
4063
  mainContext.drawImage(workCanvas, 0, 0);
4042
4064
  }
4043
4065
 
@@ -4142,17 +4164,22 @@ function setCursor(cursorStyle = 'auto')
4142
4164
 
4143
4165
  ///////////////////////////////////////////////////////////////////////////////
4144
4166
 
4167
+ /** Engine font image, 8x8 font provided by the engine
4168
+ * @type {FontImage}
4169
+ * @memberof Draw */
4145
4170
  let engineFontImage;
4146
4171
 
4147
4172
  /**
4148
- * Font Image Object - Draw text on a 2D canvas by using characters in an image
4173
+ * Font Image Object - Draw text by using tiles in an image
4149
4174
  * - 96 characters (from space to tilde) are stored in an image
4150
- * - Uses a default 8x8 font if none is supplied
4151
- * - You can also use fonts from the main tile sheet
4175
+ * - A 8x8 default engine font is supplied for general use
4176
+ * - This system is WebGL enabled for fast text rendering
4177
+ * - Fonts can also be colored and scaled along each axis
4178
+ *
4152
4179
  * @memberof Draw
4153
4180
  * @example
4154
4181
  * // use built in font
4155
- * const font = new FontImage;
4182
+ * const font = engineFontImage;
4156
4183
  *
4157
4184
  * // draw text
4158
4185
  * font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
@@ -4160,70 +4187,112 @@ let engineFontImage;
4160
4187
  class FontImage
4161
4188
  {
4162
4189
  /** Create an image font
4163
- * @param {HTMLImageElement} [image] - Image for the font, default if undefined
4164
- * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
4165
- * @param {Vector2} [paddingSize=(0,1)] - How much space between characters
4190
+ * @param {TileInfo} tileInfo - Tile info of first characeter in font
4166
4191
  */
4167
- constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
4192
+ constructor(tileInfo)
4168
4193
  {
4169
- // load default font image
4170
- if (!image && !engineFontImage)
4171
- {
4172
- engineFontImage = new Image;
4173
- engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
4174
- }
4175
-
4176
- this.image = image || engineFontImage;
4177
- this.tileSize = tileSize;
4178
- this.paddingSize = paddingSize;
4194
+ ASSERT(!!tileInfo, 'tileInfo is required for FontImage');
4195
+
4196
+ /** @property {TileInfo} - Tile info for the font */
4197
+ this.tileInfo = tileInfo.frame(0);
4179
4198
  }
4180
4199
 
4181
4200
  /** Draw text in world space using the image font
4182
- * @param {string|number} text
4201
+ * @param {string|number} text
4183
4202
  * @param {Vector2} pos
4184
- * @param {number} [scale=.25]
4185
- * @param {boolean} [center]
4186
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4203
+ * @param {Vector2|number} [size]
4204
+ * @param {boolean} [center=true]
4205
+ * @param {Color} [color=WHITE]
4206
+ * @param {boolean} [useWebGL=glEnable]
4207
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4187
4208
  */
4188
- drawText(text, pos, scale=1, center, context=drawContext)
4209
+ drawText(text, pos, size=1, center, color, useWebGL, context)
4189
4210
  {
4190
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
4211
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
4212
+
4213
+ if (typeof size === 'number')
4214
+ {
4215
+ // if size is a number, make it a vector
4216
+ ASSERT(size > 0);
4217
+ size *= cameraScale;
4218
+ size = new Vector2(size, size);
4219
+ }
4220
+ else
4221
+ size = size.scale(cameraScale);
4222
+ this.drawTextScreen(text, worldToScreen(pos), size, center, color, useWebGL, context);
4191
4223
  }
4192
4224
 
4193
4225
  /** Draw text in screen space using the image font
4194
- * @param {string|number} text
4226
+ * @param {string|number} text
4195
4227
  * @param {Vector2} pos
4196
- * @param {number} [scale]
4228
+ * @param {Vector2|number} size
4197
4229
  * @param {boolean} [center]
4198
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4230
+ * @param {Color} [color=WHITE]
4231
+ * @param {boolean} [useWebGL=glEnable]
4232
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4199
4233
  */
4200
- drawTextScreen(text, pos, scale=4, center=true, context=drawContext)
4234
+ drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
4201
4235
  {
4202
- context.save();
4203
- const size = this.tileSize;
4204
- const drawSize = size.add(this.paddingSize).scale(scale);
4205
- const cols = this.image.width / this.tileSize.x |0;
4206
- (text+'').split('\n').forEach((line, i)=>
4236
+ ASSERT(isString(text), 'text must be a string');
4237
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4238
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
4239
+ ASSERT(isColor(color), 'color must be a color');
4240
+
4241
+ // if size is a number, make it a vector
4242
+ size = typeof size === 'number' ? new Vector2(size, size) : size;
4243
+
4244
+ // precache objects for drawing
4245
+ const drawPos = new Vector2;
4246
+ const tileInfo = this.tileInfo;
4247
+ const padding = tileInfo.padding;
4248
+ const sizePaddedX = tileInfo.size.x + padding*2;
4249
+ const sizePaddedY = tileInfo.size.y + padding*2;
4250
+ const cols = tileInfo.textureInfo.size.x / sizePaddedX |0;
4251
+
4252
+ // draw each line of text
4253
+ (text+'').split('\n').forEach((line, j)=>
4207
4254
  {
4208
- const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
4209
- for (let j=line.length; j--;)
4255
+ const centerOffset = center ? (line.length-1) * size.x / 2 : 0;
4256
+ for (let i=line.length; i--;)
4210
4257
  {
4211
- // draw each character
4212
- let charCode = line[j].charCodeAt(0);
4213
- if (charCode < 32 || charCode > 127)
4214
- charCode = 127; // unknown character
4215
-
4216
- // get the character source location and draw it
4217
- const tile = charCode - 32;
4218
- const x = tile % cols;
4219
- const y = tile / cols |0;
4220
- const drawPos = pos.add(vec2(j,i).multiply(drawSize));
4221
- context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
4222
- drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
4258
+ // get the character index
4259
+ const charCode = line.charCodeAt(i);
4260
+ const index = charCode < 32 || charCode > 127 ?
4261
+ 95 : charCode - 32; // handle out of range characters
4262
+
4263
+ // get the position of the tile
4264
+ const x = index % cols;
4265
+ const y = index / cols |0;
4266
+ tileInfo.pos.x = x*sizePaddedX + padding;
4267
+ tileInfo.pos.y = y*sizePaddedY + padding;
4268
+
4269
+ // draw the tile
4270
+ drawPos.x = pos.x + i * size.x - centerOffset |0;
4271
+ drawPos.y = pos.y + j * size.y |0;
4272
+ drawTile(drawPos, size, tileInfo, color, 0, false, undefined, useWebGL, true, context);
4223
4273
  }
4224
4274
  });
4225
- context.restore();
4226
4275
  }
4276
+ }
4277
+
4278
+ // load engine font, called automatically on startup
4279
+ function fontImageInit()
4280
+ {
4281
+ return new Promise(resolve =>
4282
+ {
4283
+ // create the engine font
4284
+ const image = new Image;
4285
+ image.onerror = image.onload = ()=>
4286
+ {
4287
+ const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
4288
+ const textureInfo = new TextureInfo(image);
4289
+ const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
4290
+ engineFontImage = new FontImage(tileInfo);
4291
+ resolve();
4292
+ }
4293
+ image.crossOrigin = 'anonymous';
4294
+ image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAeAQMAAABnrVXaAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAAjpJREFUOMu9kzFu2zAUhn+CAROgqrk+B2l0BWYxMjlXeYaAtFtbdA1sGgHqRQfI0CNkSG5AwYB0BQ8d5Bsomwah6CPVeGg6tEPzAxLwyI+P78cP4u9lNO9OoMKnLMOobG5020/yaj/MrRcCGh1gBbyiLTPJEYaIiom5KM9Jq7KgynMGtb6L4GL4MF2H4LQKCXTvDVw2I4MsgZT7QLExdiutH+D08VOP3INXRrWX1/mmpbkNgAPYRVANb4xpcegYvhiNbIXauQICEjBuYLfMakaakWQeXxiZ0VDtuJCKs3ztMV59QtsHJNcRxDzfdL21ty3PrfIcXTN+E+GFAv6T5nbT9jd50/WFxb5ksdAv49qS6ouymG66ji08UMT6moykYLAo+V0j23GN4m829ZySAD5K7QsBfQTvOG8eE+gTeGYRAmnNAubN3hf5Zv9tJWDHp/VTuaSm7SN4fyINQqaNO3RMVxvpSPXnOChnRNvFcGY0gnwiPswYwTKVPE0zVtX3mTEIOoFzaqLrGuJaV+Uqumb71fVk/VoOH3cdLNQP/FHi8hV0CQNoqBZsUPlLPMsdCJro9QAaQQ0woDy9BJm0eTxCFnO9srcYlhNVlfR2EyTrph1uUtbUtAJifwRgrKuYdXVHeb0YI3QpawohQHkloI3J5FuVwI5ORxC9k2Tuz9Ir1IjgeIPGMHYkAZe2RuYkmWFmt3gGbTPOmBUWVTmRmHtGrfpzG/yuQNOKa6gBB/WA9khitPgl6/GP+gl2Af6tCbvaygAAAABJRU5ErkJggg==';
4295
+ });
4227
4296
  }
4228
4297
  /**
4229
4298
  * LittleJS Input System
@@ -4280,6 +4349,10 @@ let inputPreventDefault = true;
4280
4349
  * @memberof Input */
4281
4350
  let gamepadPrimary = 0;
4282
4351
 
4352
+ /** True if a touch device has been detected
4353
+ * @memberof Input */
4354
+ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4355
+
4283
4356
  /** Prevents input continuing to the default browser handling
4284
4357
  * This is useful to disable for html menus so the browser can handle input normally
4285
4358
  * @param {boolean} preventDefault
@@ -4478,10 +4551,6 @@ function gamepadStickCount(gamepad=gamepadPrimary)
4478
4551
  return gamepadStickData[gamepad]?.length ?? 0;
4479
4552
  }
4480
4553
 
4481
- /** True if a touch device has been detected
4482
- * @memberof Input */
4483
- const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4484
-
4485
4554
  ///////////////////////////////////////////////////////////////////////////////
4486
4555
 
4487
4556
  /** Pulse the vibration hardware if it exists
@@ -4490,7 +4559,7 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4490
4559
  function vibrate(pattern=100)
4491
4560
  {
4492
4561
  ASSERT(isNumber(pattern) || isArray(pattern), 'pattern must be a number or array');
4493
- vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern);
4562
+ vibrateEnable && !headlessMode && navigator?.vibrate?.(pattern);
4494
4563
  }
4495
4564
 
4496
4565
  /** Cancel any ongoing vibration
@@ -4608,8 +4677,8 @@ function inputInit()
4608
4677
  }
4609
4678
  function onMouseUp(e)
4610
4679
  {
4611
- if (isTouchDevice && touchInputEnable)
4612
- return;
4680
+ if (isTouchDevice && touchInputEnable) return;
4681
+
4613
4682
  inputData[0][e.button] = (inputData[0][e.button]&2) | 4;
4614
4683
  }
4615
4684
  function onMouseMove(e)
@@ -4633,9 +4702,9 @@ function inputInit()
4633
4702
  function touchInputInit()
4634
4703
  {
4635
4704
  // add non passive touch event listeners
4636
- document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
4637
- document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
4638
- document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
4705
+ document.addEventListener('touchstart', (e)=> handleTouch(e), { passive: false });
4706
+ document.addEventListener('touchmove', (e)=> handleTouch(e), { passive: false });
4707
+ document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
4639
4708
 
4640
4709
  // handle all touch events the same way
4641
4710
  let wasTouching;
@@ -4795,8 +4864,7 @@ function inputUpdate()
4795
4864
  // update touch gamepad if enabled
4796
4865
  if (touchGamepadEnable && isTouchDevice)
4797
4866
  {
4798
- if (!touchGamepadTimer.isSet())
4799
- return;
4867
+ if (!touchGamepadTimer.isSet()) return;
4800
4868
 
4801
4869
  // read virtual analog stick
4802
4870
  gamepadPrimary = 0; // touch gamepad uses index 0
@@ -4834,17 +4902,15 @@ function inputUpdate()
4834
4902
  }
4835
4903
 
4836
4904
  // return if gamepads are disabled or not supported
4837
- if (!gamepadsEnable || !navigator || !navigator.getGamepads)
4838
- return;
4905
+ if (!gamepadsEnable || !navigator || !navigator.getGamepads) return;
4839
4906
 
4840
4907
  // only poll gamepads when focused or in debug mode
4841
- if (!debug && !document.hasFocus())
4842
- return;
4908
+ if (!debug && !document.hasFocus()) return;
4843
4909
 
4844
4910
  // poll gamepads
4845
4911
  const maxGamepads = 8;
4846
4912
  const gamepads = navigator.getGamepads();
4847
- const gamepadCount = min(maxGamepads, gamepads.length)
4913
+ const gamepadCount = min(maxGamepads, gamepads.length);
4848
4914
  for (let i=0; i<gamepadCount; ++i)
4849
4915
  {
4850
4916
  // get or create gamepad data
@@ -4876,8 +4942,7 @@ function inputUpdate()
4876
4942
  data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
4877
4943
 
4878
4944
  // check for any input on this gamepad, analog must be full press
4879
- if (button.pressed)
4880
- if (!button.value || button.value > .9)
4945
+ if (button.pressed && (!button.value || button.value > .9))
4881
4946
  hadInput = true;
4882
4947
  }
4883
4948
 
@@ -4906,7 +4971,7 @@ function inputUpdate()
4906
4971
  }
4907
4972
 
4908
4973
  // copy dpad to left analog stick when pressed
4909
- if (gamepadDirectionEmulateStick && !dpad.isZero())
4974
+ if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
4910
4975
  sticks[0] = dpad.clampLength();
4911
4976
  }
4912
4977
 
@@ -4935,13 +5000,11 @@ function inputRender()
4935
5000
  function touchGamepadRender()
4936
5001
  {
4937
5002
  if (!touchInputEnable || !isTouchDevice || headlessMode) return;
4938
- if (!touchGamepadEnable || !touchGamepadTimer.isSet())
4939
- return;
5003
+ if (!touchGamepadEnable || !touchGamepadTimer.isSet()) return;
4940
5004
 
4941
5005
  // fade off when not touching or paused
4942
5006
  const alpha = percent(touchGamepadTimer.get(), 4, 3);
4943
- if (!alpha || paused)
4944
- return;
5007
+ if (!alpha || paused) return;
4945
5008
 
4946
5009
  // setup the canvas
4947
5010
  const context = mainContext;
@@ -5052,29 +5115,46 @@ function audioInit()
5052
5115
  ///////////////////////////////////////////////////////////////////////////////
5053
5116
 
5054
5117
  /**
5055
- * Sound Object - Stores a sound for later use and can be played positionally
5118
+ * Sound Object - Stores a sound for later
5119
+ * - this can be used to load and play wave, mp3, and ogg files
5120
+ * - it can also create sounds using the ZzFX sound generator
5121
+ * - can attenuate and apply stereo panning to sounds
5122
+ * - sound instance control with pause/resume capability
5056
5123
  *
5057
5124
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
5058
5125
  * @memberof Audio
5059
5126
  * @example
5060
- * // create a sound
5127
+ * // load an audio asset file
5128
+ * const sound_example = new Sound('sound.mp3');
5129
+ *
5130
+ * // create a zzfx sound
5061
5131
  * const sound_example = new Sound([.5,.5]);
5062
5132
  *
5063
- * // play the sound
5133
+ * // play a sound
5064
5134
  * sound_example.play();
5065
5135
  */
5066
5136
  class Sound
5067
5137
  {
5068
- /** Create a sound object and cache the zzfx samples for later use
5069
- * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
5138
+ /**
5139
+ * @callback SoundLoadCallback - Function called when sound is loaded
5140
+ * @param {Sound} sound
5141
+ * @memberof Audio
5142
+ */
5143
+
5144
+ /** Create a sound object and cache the audio for later use
5145
+ * @param {string|Array} [asset] - Filename of audio file or zzfx array
5146
+ * @param {number} [randomness] - How much to randomize frequency each time sound plays, for zzfx sounds the zzfx default is used if undefined
5070
5147
  * @param {number} [range=soundDefaultRange] - World space max range of sound
5071
5148
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5149
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5072
5150
  */
5073
- constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
5151
+ constructor(asset, randomness, range=soundDefaultRange, taper=soundDefaultTaper, onloadCallback)
5074
5152
  {
5075
5153
  if (!soundEnable || headlessMode) return;
5076
5154
 
5077
- ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
5155
+ ASSERT(!asset || isArray(asset) || isString(asset), 'asset must be a file name or zzfx array');
5156
+ ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
5157
+ ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
5078
5158
  ASSERT(isNumber(range), 'range must be a number');
5079
5159
  ASSERT(isNumber(taper), 'taper must be a number');
5080
5160
 
@@ -5083,24 +5163,35 @@ class Sound
5083
5163
  /** @property {number} - At what percentage of range should it start tapering */
5084
5164
  this.taper = taper;
5085
5165
  /** @property {number} - How much to randomize frequency each time sound plays */
5086
- this.randomness = 0;
5166
+ this.randomness = randomness ?? 0;
5087
5167
  /** @property {number} - Sample rate for this sound */
5088
5168
  this.sampleRate = audioDefaultSampleRate;
5089
5169
  /** @property {number} - Percentage of this sound currently loaded */
5090
5170
  this.loadedPercent = 0;
5171
+ /** @property {SoundLoadCallback} - function to call when sound is loaded */
5172
+ this.onloadCallback = onloadCallback;
5091
5173
 
5092
- // generate zzfx sound now for fast playback
5093
- if (zzfxSound)
5174
+ if (Array.isArray(asset))
5094
5175
  {
5176
+ // generate zzfx sound
5177
+ const zzfxSound = asset;
5178
+
5095
5179
  // remove randomness so it can be applied on playback
5096
- const randomnessIndex = 1, defaultRandomness = .05;
5097
- this.randomness = zzfxSound[randomnessIndex] !== undefined ?
5098
- zzfxSound[randomnessIndex] : defaultRandomness;
5180
+ const defaultRandomness = randomness ?? .05;
5181
+ const randomnessIndex = 1;
5182
+ this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
5099
5183
  zzfxSound[randomnessIndex] = 0;
5100
5184
 
5101
5185
  // generate the zzfx samples
5102
5186
  this.sampleChannels = [zzfxG(...zzfxSound)];
5103
5187
  this.loadedPercent = 1;
5188
+ onloadCallback?.(this);
5189
+ }
5190
+ else if (typeof asset === 'string')
5191
+ {
5192
+ // load the audio file
5193
+ const filename = asset;
5194
+ this.loadSound(filename);
5104
5195
  }
5105
5196
  }
5106
5197
 
@@ -5152,7 +5243,7 @@ class Sound
5152
5243
  * @param {number} [volume] - Volume to play the music at
5153
5244
  * @param {boolean} [loop] - Should the music loop?
5154
5245
  * @param {boolean} [paused] - Should the music start paused
5155
- * @return {SoundInstance} - The audio source node
5246
+ * @return {SoundInstance} - The sound instance
5156
5247
  */
5157
5248
  playMusic(volume=1, loop=true, paused=false)
5158
5249
  { return this.play(undefined, volume, 1, 0, loop, paused); }
@@ -5162,7 +5253,7 @@ class Sound
5162
5253
  * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
5163
5254
  * @param {Vector2} [pos] - World space position to play the sound if any
5164
5255
  * @param {number} [volume=1] - How much to scale volume by
5165
- * @return {SoundInstance} - The audio source node
5256
+ * @return {SoundInstance} - The sound instance
5166
5257
  */
5167
5258
  playNote(semitoneOffset=0, pos, volume)
5168
5259
  {
@@ -5175,57 +5266,14 @@ class Sound
5175
5266
  * @return {number} - How long the sound is in seconds (undefined if loading)
5176
5267
  */
5177
5268
  getDuration()
5178
- { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
5269
+ { return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
5179
5270
 
5180
5271
  /** Check if sound is loaded, for sounds fetched from a url
5181
5272
  * @return {boolean} - True if sound is loaded and ready to play
5182
5273
  */
5183
5274
  isLoaded() { return this.loadedPercent === 1; }
5184
- }
5185
-
5186
- ///////////////////////////////////////////////////////////////////////////////
5187
-
5188
- /**
5189
- * Sound Wave Object - Loads and stores an audio file for later use
5190
- * - this can be used to load and play wave, mp3, and ogg files
5191
- * @extends Sound
5192
- * @memberof Audio
5193
- * @example
5194
- * // load an audio asset file
5195
- * const sound_example = new SoundWave('sound.mp3');
5196
- *
5197
- * // play the sound
5198
- * sound_example.play();
5199
- */
5200
- class SoundWave extends Sound
5201
- {
5202
- /**
5203
- * @callback SoundLoadCallback - Function called when sound is loaded
5204
- * @param {SoundWave} sound
5205
- * @memberof Audio
5206
- */
5207
5275
 
5208
- /** Create a sound object and cache the wave file for later use
5209
- * @param {string} filename - Filename of audio file to load
5210
- * @param {number} [randomness] - How much to randomize frequency each time sound plays
5211
- * @param {number} [range=soundDefaultRange] - World space max range of sound
5212
- * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5213
- * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5214
- */
5215
- constructor(filename, randomness=0, range, taper, onloadCallback)
5216
- {
5217
- super(undefined, range, taper);
5218
- if (!soundEnable || headlessMode) return;
5219
- ASSERT(!filename || isString(filename), 'filename must be a string');
5220
- ASSERT(isNumber(randomness), 'randomness must be a number');
5221
-
5222
- /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
5223
- this.onloadCallback = onloadCallback;
5224
- this.randomness = randomness;
5225
- filename && this.loadSound(filename);
5226
- }
5227
-
5228
- /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
5276
+ /** Loads a sound from a URL and decodes it into sample data.
5229
5277
  * @param {string} filename
5230
5278
  * @return {Promise<void>} */
5231
5279
  async loadSound(filename)
@@ -5267,8 +5315,7 @@ class SoundWave extends Sound
5267
5315
  this.sampleRate = audioBuffer.sampleRate;
5268
5316
  this.sampleChannels = sampleChannels;
5269
5317
  this.loadedPercent = 1;
5270
- if (this.onloadCallback)
5271
- this.onloadCallback(this);
5318
+ this.onloadCallback?.(this);
5272
5319
  }
5273
5320
  }
5274
5321
 
@@ -5471,7 +5518,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
5471
5518
 
5472
5519
  /** Stop all queued speech
5473
5520
  * @memberof Audio */
5474
- function speakStop() {speechSynthesis && speechSynthesis.cancel();}
5521
+ function speakStop() {speechSynthesis?.cancel();}
5475
5522
 
5476
5523
  /** Get frequency of a note on a musical scale
5477
5524
  * @param {number} semitoneOffset - How many semitones away from the root note
@@ -5730,7 +5777,7 @@ function tileCollisionGetData(pos)
5730
5777
 
5731
5778
  /** Check if a tile layer collides with another object
5732
5779
  * @param {Vector2} pos
5733
- * @param {Vector2} [size=(0,0)]
5780
+ * @param {Vector2} [size=vec2()]
5734
5781
  * @param {EngineObject} [object] - An object or undefined for generic test
5735
5782
  * @param {boolean} [solidOnly] - Only check solid layers if true
5736
5783
  * @return {TileCollisionLayer}
@@ -5848,20 +5895,20 @@ function tileLayersLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLa
5848
5895
  class TileLayerData
5849
5896
  {
5850
5897
  /** Create a tile layer data object, one for each tile in a TileLayer
5851
- * @param {number} [tile] - The tile to use, untextured if undefined
5898
+ * @param {number} [tile] - The tile to use, untextured if undefined
5852
5899
  * @param {number} [direction] - Integer direction of tile, in 90 degree increments
5853
- * @param {boolean} [mirror] - If the tile should be mirrored along the x axis
5854
- * @param {Color} [color] - Color of the tile */
5900
+ * @param {boolean} [mirror] - If the tile should be mirrored along the x axis
5901
+ * @param {Color} [color] - Color of the tile */
5855
5902
  constructor(tile, direction=0, mirror=false, color=new Color)
5856
5903
  {
5857
- /** @property {number} - The tile to use, untextured if undefined */
5858
- this.tile = tile;
5859
- /** @property {number} - Integer direction of tile, in 90 degree increments */
5904
+ /** @property {number} - The tile to use, untextured if undefined */
5905
+ this.tile = tile;
5906
+ /** @property {number} - Integer direction of tile, in 90 degree increments */
5860
5907
  this.direction = direction;
5861
5908
  /** @property {boolean} - If the tile should be mirrored along the x axis */
5862
- this.mirror = mirror;
5863
- /** @property {Color} - Color of the tile */
5864
- this.color = color.copy();
5909
+ this.mirror = mirror;
5910
+ /** @property {Color} - Color of the tile */
5911
+ this.color = color.copy();
5865
5912
  }
5866
5913
 
5867
5914
  /** Set this tile to clear, it will not be rendered */
@@ -5872,7 +5919,7 @@ class TileLayerData
5872
5919
  /**
5873
5920
  * Canvas Layer - cached off screen rendering system
5874
5921
  * - Contains an offscreen canvas that can be rendered to
5875
- * - WebGL rendering is optional, call useWebGL to enable
5922
+ * - WebGL rendering is optional, call updateWebGL to enable/update
5876
5923
  * @extends EngineObject
5877
5924
  * @memberof TileLayers
5878
5925
  * @example
@@ -5881,29 +5928,27 @@ class TileLayerData
5881
5928
  class CanvasLayer extends EngineObject
5882
5929
  {
5883
5930
  /** Create a canvas layer object
5884
- * @param {Vector2} [position] - World space position of the layer
5931
+ * @param {Vector2} [pos] - World space position of the layer
5885
5932
  * @param {Vector2} [size] - World space size of the layer
5886
5933
  * @param {number} [angle] - Angle the layer is rotated by
5887
5934
  * @param {number} [renderOrder] - Objects sorted by renderOrder
5888
5935
  * @param {Vector2} [canvasSize] - Default size of canvas, can be changed later
5936
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
5889
5937
  */
5890
- constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512))
5938
+ constructor(pos, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=true)
5891
5939
  {
5892
5940
  ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
5893
- super(position, size, undefined, angle, WHITE, renderOrder);
5941
+ super(pos, size, undefined, angle, WHITE, renderOrder);
5894
5942
 
5895
5943
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
5896
5944
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
5897
5945
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
5898
5946
  this.context = this.canvas?.getContext('2d');
5899
5947
  /** @property {TextureInfo} - Texture info to use for this object rendering */
5900
- const useWebGL = false; // do not use webgl by default
5901
5948
  this.textureInfo = new TextureInfo(this.canvas, useWebGL);
5902
- /** @property {boolean} - True if WebGL texture needs to be refreshed */
5903
- this.refreshWebGL = false;
5904
5949
 
5905
5950
  // disable physics by default
5906
- this.mass = this.gravityScale = this.friction = this.restitution = 0;
5951
+ this.mass = 0;
5907
5952
  }
5908
5953
 
5909
5954
  /** Destroy this canvas layer */
@@ -5918,7 +5963,7 @@ class CanvasLayer extends EngineObject
5918
5963
  // Render the layer, called automatically by the engine
5919
5964
  render()
5920
5965
  {
5921
- this.draw(this.pos, this.size, this.angle, this.color, this.mirror, this.additiveColor);
5966
+ this.draw(this.pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
5922
5967
  }
5923
5968
 
5924
5969
  /** Draw this canvas layer centered in world space, with color applied if using WebGL
@@ -5931,103 +5976,54 @@ class CanvasLayer extends EngineObject
5931
5976
  * @param {boolean} [screenSpace] - If true the pos and size are in screen space
5932
5977
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
5933
5978
  * @memberof Draw */
5934
- draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5979
+ draw(pos, size, color=WHITE, angle=0, mirror=false, additiveColor, screenSpace=false, context)
5935
5980
  {
5936
- const useWebGL = glEnable && this.textureInfo.hasWebGL();
5937
- if (useWebGL && this.refreshWebGL)
5938
- {
5939
- // update the WebGL texture
5940
- this.textureInfo.createWebGLTexture();
5941
- this.refreshWebGL = false;
5942
- }
5943
-
5944
5981
  // draw the canvas layer as a single tile that uses the whole texture
5945
5982
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5983
+ const useWebGL = this.hasWebGL();
5946
5984
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5947
5985
  }
5948
5986
 
5949
- /**
5950
- * @callback Canvas2DDrawCallback - Function that draws to a canvas 2D context
5951
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
5952
- * @memberof TileLayers
5953
- */
5954
-
5955
- /** Draw onto the layer canvas in world space (bypass WebGL)
5987
+ /** Draw a tile onto the layer canvas in world space
5956
5988
  * @param {Vector2} pos
5957
- * @param {Vector2} size
5958
- * @param {number} angle
5959
- * @param {boolean} mirror
5960
- * @param {Canvas2DDrawCallback} drawFunction */
5961
- drawCanvas2D(pos, size, angle, mirror, drawFunction)
5989
+ * @param {Vector2} [size=vec2(1)]
5990
+ * @param {TileInfo} [tileInfo]
5991
+ * @param {Color} [color=WHITE]
5992
+ * @param {number} [angle]
5993
+ * @param {boolean} [mirror] */
5994
+ drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
5962
5995
  {
5963
- if (!this.context) return;
5964
-
5965
- const context = this.context;
5966
- context.save();
5967
5996
  pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
5968
5997
  size = size.multiply(this.tileInfo.size);
5969
- context.translate(pos.x, this.canvas.height - pos.y);
5970
- context.rotate(angle);
5971
- context.scale(mirror ? -size.x : size.x, size.y);
5972
- drawFunction(context);
5973
- context.restore();
5974
- }
5998
+ pos.y = this.canvas.height - pos.y;
5975
5999
 
5976
- /** Draw a tile onto the layer canvas in world space
5977
- * @param {Vector2} pos
5978
- * @param {Vector2} [size=(1,1)]
5979
- * @param {TileInfo} [tileInfo]
5980
- * @param {Color} [color=(1,1,1,1)]
5981
- * @param {number} [angle=0]
5982
- * @param {boolean} [mirror=false] */
5983
- drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle, mirror)
5984
- {
5985
- this.drawCanvas2D(pos, size, angle, mirror, (context)=>
5986
- {
5987
- const textureInfo = tileInfo && tileInfo.textureInfo;
5988
- if (textureInfo)
5989
- {
5990
- context.globalAlpha = color.a; // only alpha is supported
5991
- context.drawImage(textureInfo.image,
5992
- tileInfo.pos.x, tileInfo.pos.y,
5993
- tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
5994
- context.globalAlpha = 1;
5995
- }
5996
- else
5997
- {
5998
- // untextured
5999
- context.fillStyle = color.toString();
6000
- context.fillRect(-.5, -.5, 1, 1);
6001
- }
6002
- });
6000
+ // draw the tile onto the layer canvas
6001
+ const oldMainCanvasSize = mainCanvasSize;
6002
+ mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
6003
+ const useWebGL = this.hasWebGL();
6004
+ useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
6005
+ const drawContext = useWebGL ? undefined : this.context;
6006
+ drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
6007
+ useWebGL && glSetRenderTarget();
6008
+ mainCanvasSize = oldMainCanvasSize;
6003
6009
  }
6004
6010
 
6005
6011
  /** Draw a rectangle onto the layer canvas in world space
6006
6012
  * @param {Vector2} pos
6007
- * @param {Vector2} [size=(1,1)]
6008
- * @param {Color} [color=(1,1,1,1)]
6009
- * @param {number} [angle=0] */
6013
+ * @param {Vector2} [size=vec2(1)]
6014
+ * @param {Color} [color=WHITE]
6015
+ * @param {number} [angle] */
6010
6016
  drawRect(pos, size, color, angle)
6011
6017
  { this.drawTile(pos, size, undefined, color, angle); }
6012
6018
 
6013
- /** Create or update the WebGL texture for this layer
6014
- * @param {boolean} [enable] - enable WebGL rendering and update the texture
6015
- * @param {boolean} [immediate] - shoulkd the texture be updated immediately
6016
- */
6017
- useWebGL(enable=true, immediate=false)
6018
- {
6019
- if (!immediate && enable && this.textureInfo.hasWebGL())
6020
- {
6021
- // refresh the texture when needed
6022
- this.refreshWebGL = true;
6023
- return;
6024
- }
6019
+ /** Create WebGL texture if necessary and copy layer canvas to it */
6020
+ updateWebGL()
6021
+ { this.textureInfo.createWebGLTexture(); }
6025
6022
 
6026
- if (enable)
6027
- this.textureInfo.createWebGLTexture();
6028
- else
6029
- this.textureInfo.destroyWebGLTexture();
6030
- }
6023
+ /** Check if this layer is using WebGL
6024
+ * @return {boolean} */
6025
+ hasWebGL()
6026
+ { return glEnable && this.textureInfo.hasWebGL(); }
6031
6027
  }
6032
6028
 
6033
6029
  ///////////////////////////////////////////////////////////////////////////////
@@ -6046,59 +6042,93 @@ class CanvasLayer extends EngineObject
6046
6042
  class TileLayer extends CanvasLayer
6047
6043
  {
6048
6044
  /** Create a tile layer object
6049
- * @param {Vector2} position - World space position
6050
- * @param {Vector2} size - World space size
6051
- * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6045
+ * @param {Vector2} pos - World space position
6046
+ * @param {Vector2} size - World space size
6047
+ * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6052
6048
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6049
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6053
6050
  */
6054
- constructor(position, size, tileInfo=tile(), renderOrder=0)
6051
+ constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
6055
6052
  {
6056
6053
  const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
6057
- super(position, size, 0, renderOrder, canvasSize);
6058
-
6059
- // set tile info
6060
- this.tileInfo = tileInfo;
6061
-
6062
- // init tile data
6054
+ super(pos, size, 0, renderOrder, canvasSize, useWebGL);
6055
+
6056
+ /** @property {TileInfo} - Default tile info for layer */
6057
+ this.tileInfo = undefined;
6058
+ /** @property {Array<TileLayerData>} - Default tile info for layer */
6063
6059
  this.data = [];
6064
- for (let j = this.size.area(); j--;)
6065
- this.data.push(new TileLayerData);
6060
+ /** @property {boolean} - Is this layer using a webgl texture? */
6061
+ this.isUsingWebGL = false;
6066
6062
 
6067
6063
  if (headlessMode)
6068
6064
  {
6069
- // disable rendering
6070
- this.redraw = () => {};
6071
- this.render = () => {};
6072
- this.redrawStart = () => {};
6073
- this.redrawEnd = () => {};
6074
- this.drawTileData = () => {};
6075
- this.drawCanvas2D = () => {};
6076
- this.useWebGL = () => {};
6065
+ // disable rendering in headless mode
6066
+ this.render = ()=> {};
6067
+ this.redraw = ()=> {};
6068
+ this.redrawStart = ()=> {};
6069
+ this.redrawEnd = ()=> {};
6070
+ this.drawTileData = ()=> {};
6071
+ this.redrawTileData = ()=> {};
6072
+ this.drawLayerTile = ()=> {};
6073
+ this.drawLayerRect = ()=> {};
6074
+ this.clearLayerRect = ()=> {};
6075
+ return;
6077
6076
  }
6077
+
6078
+ if (tileInfo)
6079
+ {
6080
+ // set tile info
6081
+ this.tileInfo = tileInfo.frame(0);
6082
+ this.tileInfo.bleed = 0; // disable bleed for tile layers
6083
+ }
6084
+
6085
+ // init tile data
6086
+ for (let j = this.size.area(); j--;)
6087
+ this.data.push(new TileLayerData);
6078
6088
  }
6079
6089
 
6080
6090
  /** Set data at a given position in the array
6081
6091
  * @param {Vector2} layerPos - Local position in array
6082
- * @param {TileLayerData} data - Data to set
6092
+ * @param {TileLayerData} data - Data to set
6083
6093
  * @param {boolean} [redraw] - Force the tile to redraw if true */
6084
6094
  setData(layerPos, data, redraw=false)
6085
6095
  {
6096
+ layerPos = layerPos.floor();
6086
6097
  ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6087
6098
  ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
6088
- if (layerPos.arrayCheck(this.size))
6089
- {
6090
- this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
6091
- redraw && this.drawTileData(layerPos);
6092
- }
6099
+
6100
+ if (!layerPos.arrayCheck(this.size)) return;
6101
+ this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
6102
+
6103
+ if (!redraw) return;
6104
+ const isRedraw = drawContext === this.context;
6105
+ isRedraw ? this.drawTileData(layerPos) : this.redrawTileData(layerPos);
6093
6106
  }
6094
6107
 
6108
+ /** Clear data at a given position in the array
6109
+ * @param {Vector2} layerPos - Local position in array
6110
+ * @param {boolean} [redraw] - Force the tile to redraw if true */
6111
+ clearData(layerPos, redraw=false)
6112
+ { this.setData(layerPos, new TileLayerData, redraw) }
6113
+
6095
6114
  /** Get data at a given position in the array
6096
6115
  * @param {Vector2} layerPos - Local position in array
6097
6116
  * @return {TileLayerData} */
6098
6117
  getData(layerPos)
6099
6118
  {
6100
6119
  ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6101
- return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
6120
+ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
6121
+ }
6122
+
6123
+ // Update the tile layer, refresh texture if needed
6124
+ update()
6125
+ {
6126
+ if (!glEnable && this.isUsingWebGL)
6127
+ {
6128
+ // redraw the layer if webgl was disabled or context lost
6129
+ this.isUsingWebGL = false;
6130
+ this.redraw();
6131
+ }
6102
6132
  }
6103
6133
 
6104
6134
  // Render the tile layer, called automatically by the engine
@@ -6106,40 +6136,35 @@ class TileLayer extends CanvasLayer
6106
6136
  {
6107
6137
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
6108
6138
 
6109
- if (this.refreshWebGL)
6110
- {
6111
- // update the WebGL texture
6112
- this.textureInfo.createWebGLTexture();
6113
- this.refreshWebGL = false;
6114
- }
6115
-
6116
- // draw the tile layer as a single tile
6117
- const tileInfo = new TileInfo().setFullImage(this.textureInfo);
6118
6139
  const size = this.drawSize || this.size;
6119
6140
  const pos = this.pos.add(size.scale(.5));
6120
- const useWebGL = glEnable && this.textureInfo.hasWebGL();
6121
- drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
6141
+ this.draw(pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
6122
6142
  }
6123
6143
 
6144
+ /** Called after this layer is redrawn, does nothing by default */
6145
+ onRedraw() {}
6146
+
6124
6147
  /** Draw all the tile data to an offscreen canvas
6125
- * - This may be slow in some browsers but only needs to be done once */
6148
+ * - This may be slow if not using webgl but only needs to be done once */
6126
6149
  redraw()
6127
6150
  {
6128
6151
  this.redrawStart(true);
6129
6152
  for (let x = this.size.x; x--;)
6130
6153
  for (let y = this.size.y; y--;)
6131
6154
  this.drawTileData(vec2(x,y), false);
6155
+ this.isUsingWebGL && glFlush();
6156
+ this.onRedraw();
6132
6157
  this.redrawEnd();
6133
- this.useWebGL();
6134
6158
  }
6135
6159
 
6136
6160
  /** Call to start the redraw process
6137
- * - This can be used to manually update small parts of the level
6161
+ * - This can be used to manually update parts of the level
6138
6162
  * @param {boolean} [clear] - Should it clear the canvas before drawing */
6139
6163
  redrawStart(clear=false)
6140
6164
  {
6141
6165
  if (!this.context) return;
6142
-
6166
+ ASSERT(drawContext !== this.context);
6167
+
6143
6168
  // save current render settings
6144
6169
  /** @type {[CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6145
6170
  this.savedRenderSettings = [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
@@ -6147,65 +6172,119 @@ class TileLayer extends CanvasLayer
6147
6172
  // set the draw canvas and context to this layer
6148
6173
  // use camera settings to match this layer's canvas
6149
6174
  drawContext = this.context;
6150
- cameraPos = this.size.scale(.5);
6151
- const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
6152
- cameraScale = tileSize.x;
6153
- canvasClearColor = CLEAR_BLACK;
6175
+ const tileSize = this.tileInfo?.size ?? vec2(1);
6154
6176
  mainCanvasSize = this.size.multiply(tileSize);
6155
- if (clear)
6177
+ canvasClearColor = CLEAR_BLACK;
6178
+ cameraPos = this.size.multiply(tileSize).scale(.5);
6179
+ cameraScale = 1;
6180
+
6181
+ // set render target to this layer
6182
+ this.isUsingWebGL = this.hasWebGL();
6183
+ if (this.isUsingWebGL)
6184
+ glSetRenderTarget(this.textureInfo.glTexture, clear);
6185
+ else
6156
6186
  {
6157
- // clear and set size
6158
- this.canvas.width = mainCanvasSize.x;
6159
- this.canvas.height = mainCanvasSize.y;
6187
+ // disable smoothing for pixel art
6188
+ this.context.imageSmoothingEnabled = !tilesPixelated;
6189
+ if (clear)
6190
+ {
6191
+ // clear and set size
6192
+ this.canvas.width = mainCanvasSize.x;
6193
+ this.canvas.height = mainCanvasSize.y;
6194
+ }
6160
6195
  }
6161
-
6162
- // disable smoothing for pixel art
6163
- drawContext.imageSmoothingEnabled = !tilesPixelated;
6164
-
6165
- // setup gl rendering if enabled
6166
- glPreRender();
6167
6196
  }
6168
6197
 
6169
6198
  /** Call to end the redraw process */
6170
6199
  redrawEnd()
6171
6200
  {
6172
6201
  if (!this.context) return;
6202
+ ASSERT(drawContext === this.context);
6203
+
6204
+ // set stuff back to normal
6205
+ if (this.isUsingWebGL)
6206
+ glSetRenderTarget();
6207
+ [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6208
+ }
6209
+
6210
+ /** Draw the tile at a given position in the tile layer
6211
+ * This can be used to clear out tiles when they are destroyed
6212
+ * Tiles can also be redrawn if inside a redrawStart/End block
6213
+ * @param {Vector2} layerPos
6214
+ * @param {boolean} [clear] - should the old tile be cleared out
6215
+ */
6216
+ drawTileData(layerPos, clear=true)
6217
+ {
6218
+ if (!this.context) return;
6219
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6220
+
6221
+ // clear out where the tile was, can be skipped for fully opaque tiles
6222
+ const drawSize = this.tileInfo?.size ?? vec2(1);
6223
+ const drawPos = layerPos.multiply(drawSize);
6224
+ clear && this.clearLayerRect(drawPos, drawSize);
6225
+
6226
+ // draw the tile if it has layer data
6227
+ const d = this.getData(layerPos);
6228
+ if (!d.tile) return;
6229
+
6230
+ const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
6231
+ this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
6232
+ }
6233
+
6234
+ /** Draw the tile at a given position in the tile layer
6235
+ * This can be used to clear tiles when they are destroyed
6236
+ * For better performance use drawTileData inside a redrawStart/End block
6237
+ * @param {Vector2} layerPos
6238
+ * @param {boolean} [clear] - should the old tile be cleared
6239
+ */
6240
+ redrawTileData(layerPos, clear=true)
6241
+ {
6242
+ if (!this.context) return;
6243
+ ASSERT(drawContext !== this.context, 'redrawStart() should not be active when calling redrawTileData(), instead use drawTileData()');
6173
6244
 
6174
- ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6175
- glCopyToContext(drawContext);
6176
- //saveCanvas(this.canvas);
6245
+ this.redrawStart();
6246
+ this.drawTileData(layerPos, clear);
6247
+ this.redrawEnd();
6248
+ }
6177
6249
 
6178
- // set stuff back to normal
6179
- [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6250
+ /** Draw textured tile in layer space
6251
+ * @param {Vector2} pos - Position in pixel coordinates
6252
+ * @param {Vector2} [size=vec2(1)] - Size of the tile
6253
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
6254
+ * @param {Color} [color=WHITE] - Color to modulate with
6255
+ * @param {number} [angle] - Angle to rotate by
6256
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
6257
+ * @param {Color} [additiveColor] - Additive color to be applied if any */
6258
+ drawLayerTile(pos, size=vec2(1), tileInfo, color=WHITE,
6259
+ angle=0, mirror, additiveColor)
6260
+ {
6261
+ const drawPos = pos.add(size.scale(.5));
6262
+ drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor, this.isUsingWebGL);
6180
6263
  }
6181
6264
 
6182
- /** Draw the tile at a given position in the tile grid
6183
- * This can be used to clear out tiles when they are destroyed
6184
- * Tiles can also be redrawn if inside a redrawStart/End block
6185
- * @param {Vector2} layerPos
6186
- * @param {boolean} [clear] - should the old tile be cleared out
6265
+ /** Clear a rectangle in layer space
6266
+ * @param {Vector2} pos
6267
+ * @param {Vector2} size
6268
+ * @param {Color} [color=WHITE] - Color to modulate with
6269
+ * @param {number} [angle] - Angle to rotate by
6187
6270
  */
6188
- drawTileData(layerPos, clear=true)
6271
+ drawLayerRect(pos, size, color, angle=0)
6272
+ { this.drawLayerTile(pos, size, undefined, color, angle); }
6273
+
6274
+ /** Clear a rectangle in layer space
6275
+ * @param {Vector2} pos - position in pixel coordinates
6276
+ * @param {Vector2} size
6277
+ */
6278
+ clearLayerRect(pos, size)
6189
6279
  {
6190
- if (!this.context) return;
6191
-
6192
- // clear out where the tile was, for full opaque tiles this can be skipped
6193
- const s = this.tileInfo.size;
6194
- if (clear)
6195
- {
6196
- const pos = layerPos.multiply(s);
6197
- this.context.clearRect(pos.x, this.canvas.height-pos.y, s.x, -s.y);
6198
- }
6280
+ ASSERT(drawContext === this.context, 'must call redrawStart() before clearing tiles');
6199
6281
 
6200
- // draw the tile if it has layer data
6201
- const d = this.getData(layerPos);
6202
- if (d.tile !== undefined)
6203
- {
6204
- ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6205
- const pos = layerPos.add(vec2(.5));
6206
- const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
6207
- drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
6208
- }
6282
+ const x = pos.x, y = this.canvas.height - pos.y - size.y;
6283
+ const useWebGL = this.hasWebGL();
6284
+ if (useWebGL)
6285
+ glClearRect(x, y, size.x, size.y);
6286
+ else
6287
+ this.context.clearRect(x, y, size.x, size.y);
6209
6288
  }
6210
6289
  }
6211
6290
 
@@ -6214,21 +6293,21 @@ class TileLayer extends CanvasLayer
6214
6293
  * Tile Collision Layer - a tile layer with collision
6215
6294
  * - adds collision data and functions to TileLayer
6216
6295
  * - there can be multiple tile collision layers
6217
- * - tile collision layers should not overlap each other
6218
6296
  * @extends TileLayer
6219
6297
  * @memberof TileLayers
6220
6298
  */
6221
6299
  class TileCollisionLayer extends TileLayer
6222
6300
  {
6223
6301
  /** Create a tile layer object
6224
- * @param {Vector2} position - World space position
6225
- * @param {Vector2} size - World space size
6226
- * @param {TileInfo} [tileInfo] - Tile info for layer
6302
+ * @param {Vector2} pos - World space position
6303
+ * @param {Vector2} size - World space size
6304
+ * @param {TileInfo} [tileInfo] - Tile info for layer
6227
6305
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6306
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6228
6307
  */
6229
- constructor(position, size, tileInfo=tile(), renderOrder=0)
6308
+ constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
6230
6309
  {
6231
- super(position, size.floor(), tileInfo, renderOrder);
6310
+ super(pos, size.floor(), tileInfo, renderOrder, useWebGL);
6232
6311
 
6233
6312
  /** @property {Array<number>} - The tile collision grid */
6234
6313
  this.collisionData = [];
@@ -6264,29 +6343,34 @@ class TileCollisionLayer extends TileLayer
6264
6343
  this.collisionData.fill(0);
6265
6344
  }
6266
6345
 
6267
- /** Set tile collision data for a given cell in the grid
6268
- * @param {Vector2} gridPos
6346
+ /** Set tile collision data for a given cell in the layer
6347
+ * @param {Vector2} layerPos
6269
6348
  * @param {number} [data] */
6270
- setCollisionData(gridPos, data=1)
6349
+ setCollisionData(layerPos, data=1)
6271
6350
  {
6272
- ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6273
- const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6274
- gridPos.arrayCheck(this.size) && (this.collisionData[i] = data);
6351
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6352
+ const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
6353
+ layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
6275
6354
  }
6276
6355
 
6277
- /** Get tile collision data for a given cell in the grid
6278
- * @param {Vector2} gridPos
6356
+ /** Clear tile collision data for a given cell in the layer
6357
+ * @param {Vector2} layerPos */
6358
+ clearCollisionData(layerPos)
6359
+ { this.setCollisionData(layerPos, 0); }
6360
+
6361
+ /** Get tile collision data for a given cell in the layer
6362
+ * @param {Vector2} layerPos
6279
6363
  * @return {number} */
6280
- getCollisionData(gridPos)
6364
+ getCollisionData(layerPos)
6281
6365
  {
6282
- ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6283
- const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6284
- return gridPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
6366
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6367
+ const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
6368
+ return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
6285
6369
  }
6286
6370
 
6287
6371
  /** Check if collision with another object should occur
6288
6372
  * @param {Vector2} pos
6289
- * @param {Vector2} [size=(0,0)]
6373
+ * @param {Vector2} [size=vec2()]
6290
6374
  * @param {EngineObject} [object]
6291
6375
  * @return {boolean} */
6292
6376
  collisionTest(pos, size=new Vector2, object)
@@ -6606,7 +6690,7 @@ class ParticleEmitter extends EngineObject
6606
6690
  particle.mirror = randBool();
6607
6691
 
6608
6692
  // call particle create callback
6609
- this.particleCreateCallback && this.particleCreateCallback(particle);
6693
+ this.particleCreateCallback?.(particle);
6610
6694
 
6611
6695
  // return the newly created particle
6612
6696
  return particle;
@@ -6691,7 +6775,7 @@ class Particle extends EngineObject
6691
6775
  const c = this.colorEnd;
6692
6776
  this.color.set(c.r, c.g, c.b, c.a);
6693
6777
  this.size.set(this.sizeEnd, this.sizeEnd);
6694
- this.destroyCallback && this.destroyCallback(this);
6778
+ this.destroyCallback?.(this);
6695
6779
  this.destroyed = 1;
6696
6780
  }
6697
6781
  }
@@ -6969,7 +7053,7 @@ let glContext;
6969
7053
  let glAntialias = true;
6970
7054
 
6971
7055
  // WebGL internal variables not exposed to documentation
6972
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glCanBeEnabled = true;
7056
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glCanBeEnabled = true;
6973
7057
 
6974
7058
  // WebGL internal constants
6975
7059
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -7045,7 +7129,7 @@ function glInit(rootElement)
7045
7129
  // setup instanced rendering shader program
7046
7130
  glShader = glCreateProgram(
7047
7131
  '#version 300 es\n' + // specify GLSL ES version
7048
- 'precision highp float;'+ // use highp for better accuracy
7132
+ 'precision highp float;'+ // use highp for accuracy
7049
7133
  'uniform mat4 m;'+ // transform matrix
7050
7134
  'in vec2 g;'+ // in: geometry
7051
7135
  'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
@@ -7060,7 +7144,7 @@ function glInit(rootElement)
7060
7144
  '}' // end of shader
7061
7145
  ,
7062
7146
  '#version 300 es\n' + // specify GLSL ES version
7063
- 'precision highp float;'+ // use highp for better accuracy
7147
+ 'precision highp float;'+ // use highp for accuracy
7064
7148
  'uniform sampler2D s;'+ // texture
7065
7149
  'in vec2 v;'+ // in: uv
7066
7150
  'in vec4 d,e;'+ // in: color, additiveColor
@@ -7098,45 +7182,62 @@ function glInit(rootElement)
7098
7182
  glColorData = new Uint32Array(glInstanceData);
7099
7183
  glArrayBuffer = glContext.createBuffer();
7100
7184
  glGeometryBuffer = glContext.createBuffer();
7185
+ glFramebuffer = glContext.createFramebuffer();
7186
+ glBatchCount = 0;
7101
7187
 
7102
7188
  // create the geometry buffer, triangle strip square
7103
- const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
7189
+ const geometry = new Float32Array([0,0,1,0,0,1,1,1]);
7104
7190
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7105
7191
  glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
7192
+
7193
+ let offset, shader, stride;
7194
+ const initVertexAttrib = (name, type, typeSize, size, divisor=0)=>
7195
+ {
7196
+ const location = glContext.getAttribLocation(shader, name);
7197
+ const normalize = typeSize === 1;
7198
+ const fixedStride = typeSize && stride;
7199
+ glContext.enableVertexAttribArray(location);
7200
+ glContext.vertexAttribPointer(location, size, type, normalize, fixedStride, offset);
7201
+ glContext.vertexAttribDivisor(location, divisor);
7202
+ offset += size*typeSize;
7203
+ }
7204
+
7205
+ // setup VAO for instanced rendering
7206
+ glInstancedVAO = glContext.createVertexArray();
7207
+ glContext.bindVertexArray(glInstancedVAO);
7208
+
7209
+ // configure instanced vertex attributes
7210
+ offset = 0, shader = glShader, stride = gl_INSTANCE_BYTE_STRIDE;
7211
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7212
+ initVertexAttrib('g', glContext.FLOAT, 0, 2); // geometry
7213
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7214
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7215
+ initVertexAttrib('p', glContext.FLOAT, 4, 4, 1); // position & size
7216
+ initVertexAttrib('u', glContext.FLOAT, 4, 4, 1); // texture coords
7217
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4, 1); // color
7218
+ initVertexAttrib('a', glContext.UNSIGNED_BYTE, 1, 4, 1); // additiveColor
7219
+ initVertexAttrib('r', glContext.FLOAT, 4, 1, 1); // rotation
7220
+
7221
+ // setup VAO for poly rendering
7222
+ glPolyVAO = glContext.createVertexArray();
7223
+ glContext.bindVertexArray(glPolyVAO);
7224
+
7225
+ // configure poly vertex attributes
7226
+ offset = 0, shader = glPolyShader, stride = gl_POLY_VERTEX_BYTE_STRIDE;
7227
+ initVertexAttrib('p', glContext.FLOAT, 4, 2); // position
7228
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7106
7229
  }
7107
7230
  }
7108
7231
 
7109
- function glSetInstancedMode()
7232
+ function glSetInstancedMode(force=false)
7110
7233
  {
7111
- if (!glPolyMode) return;
7234
+ if (!force && !glPolyMode) return;
7112
7235
 
7113
7236
  // setup instanced mode
7114
7237
  glFlush();
7115
7238
  glPolyMode = false;
7116
7239
  glContext.useProgram(glShader);
7117
-
7118
- // set vertex attributes
7119
- let offset = 0;
7120
- const initVertexAttribArray = (name, type, typeSize, size)=>
7121
- {
7122
- const location = glContext.getAttribLocation(glShader, name);
7123
- const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
7124
- const divisor = typeSize && 1; // only if not geometry
7125
- const normalize = typeSize === 1; // only if color
7126
- glContext.enableVertexAttribArray(location);
7127
- glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
7128
- glContext.vertexAttribDivisor(location, divisor);
7129
- offset += size*typeSize;
7130
- }
7131
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7132
- initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
7133
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7134
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7135
- initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
7136
- initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
7137
- initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7138
- initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
7139
- initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
7240
+ glContext.bindVertexArray(glInstancedVAO);
7140
7241
  }
7141
7242
 
7142
7243
  function glSetPolyMode()
@@ -7147,36 +7248,30 @@ function glSetPolyMode()
7147
7248
  glFlush();
7148
7249
  glPolyMode = true;
7149
7250
  glContext.useProgram(glPolyShader);
7150
-
7151
- // set vertex attributes
7152
- let offset = 0;
7153
- const initVertexAttribArray = (name, type, typeSize, size)=>
7154
- {
7155
- const location = glContext.getAttribLocation(glPolyShader, name);
7156
- const normalize = typeSize === 1; // only normalize if color
7157
- const stride = gl_POLY_VERTEX_BYTE_STRIDE;
7158
- glContext.enableVertexAttribArray(location);
7159
- glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
7160
- glContext.vertexAttribDivisor(location, 0);
7161
- offset += size*typeSize;
7162
- }
7163
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7164
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7165
- initVertexAttribArray('p', glContext.FLOAT, 4, 2); // position
7166
- initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7251
+ glContext.bindVertexArray(glPolyVAO);
7167
7252
  }
7168
7253
 
7169
7254
  // Setup WebGL render each frame, called automatically by engine
7170
7255
  // Also used by tile layer rendering when redrawing tiles
7171
- function glPreRender()
7256
+ function glPreRender(clear=true)
7172
7257
  {
7173
7258
  if (!glEnable || !glContext) return;
7174
7259
 
7175
- // clear the canvas
7176
- glClearCanvas();
7260
+ ASSERT(!glBatchCount, 'glPreRender called with unflushed batch.');
7261
+
7262
+ if (!glRenderTarget)
7263
+ {
7264
+ // set to same size as main canvas
7265
+ glCanvas.width = mainCanvasSize.x;
7266
+ glCanvas.height = mainCanvasSize.y;
7267
+ }
7268
+ glContext.viewport(0, 0, mainCanvasSize.x, mainCanvasSize.y);
7269
+ clear && glClearCanvas();
7177
7270
 
7178
7271
  // build the transform matrix
7179
7272
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
7273
+ if (glRenderTarget)
7274
+ s.y = -s.y; // invert y when using render target
7180
7275
  const rotatedCam = cameraPos.rotate(-cameraAngle);
7181
7276
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
7182
7277
  const ca = cos(cameraAngle);
@@ -7188,7 +7283,7 @@ function glPreRender()
7188
7283
  p.x, p.y, 0, 1];
7189
7284
 
7190
7285
  // set the same transform matrix for both shaders
7191
- const initUniform = (program, uniform, value) =>
7286
+ const initUniform = (program, uniform, value)=>
7192
7287
  {
7193
7288
  glContext.useProgram(program);
7194
7289
  const location = glContext.getUniformLocation(program, uniform);
@@ -7205,12 +7300,14 @@ function glPreRender()
7205
7300
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
7206
7301
  }
7207
7302
 
7303
+ // rebind the array buffer
7304
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7305
+
7208
7306
  // start with additive blending off
7209
7307
  glAdditive = glBatchAdditive = false;
7210
7308
 
7211
- // force it to set instanced mode by first setting poly mode true
7212
- glPolyMode = true;
7213
- glSetInstancedMode();
7309
+ // force it to set instanced mode
7310
+ glSetInstancedMode(true);
7214
7311
  }
7215
7312
 
7216
7313
  /** Clear the canvas and setup the viewport
@@ -7219,13 +7316,9 @@ function glClearCanvas()
7219
7316
  {
7220
7317
  if (!glContext) return;
7221
7318
 
7222
- // clear and set to same size as main canvas
7223
- glCanvas.width = mainCanvasSize.x;
7224
- glCanvas.height = mainCanvasSize.y;
7225
- glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
7319
+ // clear using the canvasClearColor
7226
7320
  const color = canvasClearColor;
7227
- if (color.a > 0)
7228
- glContext.clearColor(color.r, color.g, color.b, color.a);
7321
+ glContext.clearColor(color.r, color.g, color.b, color.a);
7229
7322
  glContext.clear(glContext.COLOR_BUFFER_BIT);
7230
7323
  }
7231
7324
 
@@ -7302,7 +7395,7 @@ function glCreateTexture(image)
7302
7395
  // build the texture
7303
7396
  const texture = glContext.createTexture();
7304
7397
  let mipMap = false;
7305
- if (image && image.width)
7398
+ if (image?.width)
7306
7399
  {
7307
7400
  glSetTextureData(texture, image);
7308
7401
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
@@ -7323,7 +7416,9 @@ function glCreateTexture(image)
7323
7416
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
7324
7417
  if (mipMap)
7325
7418
  glContext.generateMipmap(glContext.TEXTURE_2D);
7326
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
7419
+
7420
+ // rebind active texture
7421
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
7327
7422
  return texture;
7328
7423
  }
7329
7424
 
@@ -7346,10 +7441,12 @@ function glSetTextureData(texture, image)
7346
7441
  if (!glContext) return;
7347
7442
 
7348
7443
  // build the texture
7349
- ASSERT(!!image && image.width > 0, 'Invalid image data.');
7444
+ ASSERT(image?.width > 0, 'Invalid image data.');
7350
7445
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
7351
7446
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
7352
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
7447
+
7448
+ // rebind active texture
7449
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
7353
7450
  }
7354
7451
 
7355
7452
  /** Tells WebGL to create or update the glTexture and start tracking it
@@ -7568,6 +7665,48 @@ function glDrawColoredPoints(points, pointColors)
7568
7665
  glBatchCount += vertCount;
7569
7666
  }
7570
7667
 
7668
+ /** Set the WebGL render target to the given texture or back to the canvas
7669
+ * @param {WebGLTexture} [texture] - a texture or undefined to use normal glCanvas
7670
+ * @param {boolean} [clear] - should the render target be cleared
7671
+ * @memberof WebGL */
7672
+ function glSetRenderTarget(texture, clear=false)
7673
+ {
7674
+ if (texture)
7675
+ {
7676
+ glRenderTarget = texture;
7677
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
7678
+ glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
7679
+ glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
7680
+ glPreRender(clear);
7681
+ }
7682
+ else
7683
+ {
7684
+ glFlush();
7685
+ glRenderTarget = undefined;
7686
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
7687
+ }
7688
+ }
7689
+
7690
+ /** Clear out a rectangle area of the WebGL canvas or render target
7691
+ * @param {number} x
7692
+ * @param {number} y
7693
+ * @param {number} width
7694
+ * @param {number} height
7695
+ * @memberof WebGL */
7696
+ function glClearRect(x, y, width, height)
7697
+ {
7698
+ if (!glEnable) return;
7699
+
7700
+ // Enable scissor test to clear only the specified area
7701
+ glContext.enable(glContext.SCISSOR_TEST);
7702
+ glContext.scissor(x, y, width, height);
7703
+ glContext.clearColor(0, 0, 0, 0);
7704
+ glContext.clear(glContext.COLOR_BUFFER_BIT);
7705
+ glContext.disable(glContext.SCISSOR_TEST);
7706
+ }
7707
+
7708
+ ///////////////////////////////////////////////////////////////////////////////
7709
+
7571
7710
  // WebGL internal function to convert polygon to outline triangle strip
7572
7711
  function glMakeOutline(points, width, wrap=true)
7573
7712
  {
@@ -7701,23 +7840,20 @@ function glPolyStrip(points)
7701
7840
  const a = points[i0], b = points[i1], c = points[i2];
7702
7841
 
7703
7842
  // check if convex
7704
- if (cross(a, b, c) < e)
7705
- continue;
7843
+ if (cross(a, b, c) < e) continue;
7706
7844
 
7707
7845
  // check if any other point is inside
7708
7846
  let hasInside = false;
7709
7847
  for (let j = 0; j < indices.length; j++)
7710
7848
  {
7711
7849
  const k = indices[j];
7712
- if (k === i0 || k === i1 || k === i2)
7713
- continue;
7850
+ if (k === i0 || k === i1 || k === i2) continue;
7851
+
7714
7852
  const p = points[k];
7715
7853
  hasInside = pointInTriangle(p, a, b, c);
7716
- if (hasInside)
7717
- break;
7854
+ if (hasInside) break;
7718
7855
  }
7719
- if (hasInside)
7720
- continue;
7856
+ if (hasInside) continue;
7721
7857
 
7722
7858
  // found valid ear
7723
7859
  triangles.push([i0, i1, i2]);
@@ -7742,8 +7878,7 @@ function glPolyStrip(points)
7742
7878
  worstIndex = i;
7743
7879
  }
7744
7880
  }
7745
- if (worstIndex < 0)
7746
- break;
7881
+ if (worstIndex < 0) break;
7747
7882
 
7748
7883
  const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
7749
7884
  const i1 = indices[worstIndex];
@@ -7974,14 +8109,16 @@ class PostProcessPlugin
7974
8109
  {
7975
8110
  /** Create global post processing shader
7976
8111
  * @param {string} shaderCode
7977
- * @param {boolean} [includeMainCanvas]
7978
- * @example
7979
- * // create the post process plugin object
7980
- * new PostProcessPlugin(shaderCode);
7981
- */
7982
- constructor(shaderCode, includeMainCanvas=true)
8112
+ * @param {boolean} [includeMainCanvas] - combine mainCanvs onto glCanvas
8113
+ * @param {boolean} [feedbackTexture] - use glCanvas from previous frame as the texture
8114
+ * @example
8115
+ * // create the post process plugin object
8116
+ * new PostProcessPlugin(shaderCode);
8117
+ */
8118
+ constructor(shaderCode, includeMainCanvas=false, feedbackTexture=false)
7983
8119
  {
7984
8120
  ASSERT(!postProcess, 'Post process already initialized');
8121
+ ASSERT(!(includeMainCanvas && feedbackTexture), 'Post process cannot both include main canvas and use feedback texture');
7985
8122
  postProcess = this;
7986
8123
 
7987
8124
  if (!shaderCode) // default shader pass through
@@ -7989,9 +8126,10 @@ class PostProcessPlugin
7989
8126
 
7990
8127
  /** @property {WebGLProgram} - Shader for post processing */
7991
8128
  this.shader = undefined;
7992
-
7993
8129
  /** @property {WebGLTexture} - Texture for post processing */
7994
8130
  this.texture = undefined;
8131
+ /** @property {WebGLVertexArrayObject} - Vertex array object */
8132
+ this.vao = undefined;
7995
8133
 
7996
8134
  // setup the post processing plugin
7997
8135
  initPostProcess();
@@ -8000,7 +8138,6 @@ class PostProcessPlugin
8000
8138
  function initPostProcess()
8001
8139
  {
8002
8140
  if (headlessMode) return;
8003
-
8004
8141
  if (!glEnable)
8005
8142
  {
8006
8143
  console.warn('PostProcessPlugin: WebGL not enabled!');
@@ -8011,14 +8148,14 @@ class PostProcessPlugin
8011
8148
  postProcess.texture = glCreateTexture();
8012
8149
  postProcess.shader = glCreateProgram(
8013
8150
  '#version 300 es\n' + // specify GLSL ES version
8014
- 'precision highp float;'+ // use highp for better accuracy
8151
+ 'precision highp float;'+ // use highp for accuracy
8015
8152
  'in vec2 p;'+ // position
8016
8153
  'void main(){'+ // shader entry point
8017
8154
  'gl_Position=vec4(p+p-1.,1,1);'+ // set position
8018
8155
  '}' // end of shader
8019
8156
  ,
8020
8157
  '#version 300 es\n' + // specify GLSL ES version
8021
- 'precision highp float;'+ // use highp for better accuracy
8158
+ 'precision highp float;'+ // use highp for accuracy
8022
8159
  'uniform sampler2D iChannel0;'+ // input texture
8023
8160
  'uniform vec3 iResolution;'+ // size of output texture
8024
8161
  'uniform float iTime;'+ // time
@@ -8029,6 +8166,17 @@ class PostProcessPlugin
8029
8166
  'c.a=1.;'+ // always use full alpha
8030
8167
  '}' // end of shader
8031
8168
  );
8169
+
8170
+ // setup VAO for post processing
8171
+ postProcess.vao = glContext.createVertexArray();
8172
+ glContext.bindVertexArray(postProcess.vao);
8173
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
8174
+
8175
+ // configure vertex attributes
8176
+ const vertexByteStride = 8;
8177
+ const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
8178
+ glContext.enableVertexAttribArray(pLocation);
8179
+ glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
8032
8180
  }
8033
8181
  function postProcessContextLost()
8034
8182
  {
@@ -8043,14 +8191,17 @@ class PostProcessPlugin
8043
8191
  }
8044
8192
  function postProcessRender()
8045
8193
  {
8046
- if (headlessMode) return;
8047
-
8048
- if (!glEnable)
8049
- return;
8194
+ if (headlessMode || !glEnable) return;
8050
8195
 
8051
8196
  // clear out the buffer
8052
8197
  glFlush();
8053
-
8198
+
8199
+ // setup shader program to draw a quad
8200
+ glContext.useProgram(postProcess.shader);
8201
+ glContext.bindVertexArray(postProcess.vao);
8202
+ glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, true);
8203
+ glContext.disable(glContext.BLEND);
8204
+
8054
8205
  // setup texture
8055
8206
  glContext.activeTexture(glContext.TEXTURE0);
8056
8207
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
@@ -8061,29 +8212,32 @@ class PostProcessPlugin
8061
8212
  workCanvas.height = mainCanvasSize.y;
8062
8213
  glCopyToContext(workContext);
8063
8214
  workContext.drawImage(mainCanvas, 0, 0);
8215
+ mainCanvas.width |= 0
8064
8216
 
8065
8217
  // copy work canvas to texture
8066
8218
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
8067
8219
  }
8068
-
8069
- // setup shader program to draw a quad
8070
- glContext.useProgram(postProcess.shader);
8071
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
8072
- glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
8073
- glContext.disable(glContext.BLEND);
8074
-
8075
- // set vertex position attribute
8076
- const vertexByteStride = 8;
8077
- const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
8078
- glContext.enableVertexAttribArray(pLocation);
8079
- glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
8080
-
8220
+ else if (!feedbackTexture)
8221
+ {
8222
+ // copy glCanvas to texture
8223
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
8224
+ }
8225
+
8081
8226
  // set uniforms and draw
8082
8227
  const uniformLocation = (name)=>glContext.getUniformLocation(postProcess.shader, name);
8083
8228
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
8084
8229
  glContext.uniform1f(uniformLocation('iTime'), time);
8085
8230
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
8086
8231
  glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
8232
+
8233
+ if (feedbackTexture)
8234
+ {
8235
+ // pass glCanvas back to overlay texture
8236
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
8237
+ }
8238
+
8239
+ // force it to set instanced mode
8240
+ glSetInstancedMode(true);
8087
8241
  }
8088
8242
  }
8089
8243
  }
@@ -8142,7 +8296,7 @@ class ZzFXMusic extends Sound
8142
8296
  /** Play the music that loops by default
8143
8297
  * @param {number} [volume] - Volume to play the music at
8144
8298
  * @param {boolean} [loop] - Should the music loop?
8145
- * @return {AudioBufferSourceNode} - The audio source node
8299
+ * @return {SoundInstance} - The sound instance
8146
8300
  */
8147
8301
  playMusic(volume=1, loop=true)
8148
8302
  { return super.play(undefined, volume, 1, 0, loop); }
@@ -8189,7 +8343,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
8189
8343
  sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
8190
8344
 
8191
8345
  // for each pattern in sequence
8192
- sequence.forEach((patternIndex, sequenceIndex) => {
8346
+ sequence.forEach((patternIndex, sequenceIndex)=> {
8193
8347
  // get pattern for current channel, use empty 1 note pattern if none found
8194
8348
  patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
8195
8349
 
@@ -8282,7 +8436,7 @@ let uiDebug = 0;
8282
8436
 
8283
8437
  /** Enable UI system debug drawing
8284
8438
  * 0=off, 1=normal, 2=show invisible
8285
- * @param {number|boolean} enable
8439
+ * @param {number|boolean} debugMode
8286
8440
  * @memberof UISystem */
8287
8441
  function uiSetDebug(debugMode)
8288
8442
  { uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
@@ -8764,7 +8918,7 @@ class UISystemPlugin
8764
8918
  const up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight';
8765
8919
  if (both)
8766
8920
  {
8767
- return keyIsDown(up) || keyIsDown(left) ? -1 :
8921
+ return keyIsDown(up) || keyIsDown(left) ? -1 :
8768
8922
  keyIsDown(down) || keyIsDown(right) ? 1 : 0;
8769
8923
  }
8770
8924
  const back = vertical ? up : left;
@@ -8795,7 +8949,7 @@ class UISystemPlugin
8795
8949
  * @return {boolean} */
8796
8950
  getNavigationWasPressed()
8797
8951
  {
8798
- return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
8952
+ return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
8799
8953
  keyWasPressed('Space') || keyWasPressed('Enter');
8800
8954
  }
8801
8955
 
@@ -8843,7 +8997,7 @@ class UISystemPlugin
8843
8997
  buttonYes.textHeight = 40;
8844
8998
  buttonYes.navigationIndex = 1;
8845
8999
  buttonYes.hoverColor = hsl(0,1,.5);
8846
- buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
9000
+ buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
8847
9001
  confirmMenu.addChild(buttonYes);
8848
9002
 
8849
9003
  // no button
@@ -8873,8 +9027,8 @@ class UISystemPlugin
8873
9027
  class UIObject
8874
9028
  {
8875
9029
  /** Create a UIObject
8876
- * @param {Vector2} [pos=(0,0)]
8877
- * @param {Vector2} [size=(1,1)]
9030
+ * @param {Vector2} [pos=vec2()]
9031
+ * @param {Vector2} [size=vec2(1)]
8878
9032
  */
8879
9033
  constructor(pos=vec2(), size=vec2())
8880
9034
  {
@@ -8889,7 +9043,7 @@ class UIObject
8889
9043
  this.size = size.copy();
8890
9044
  /** @property {Color} - Color of the object */
8891
9045
  this.color = uiSystem.defaultColor.copy();
8892
- /** @property {Color} - Color of the object when active, uses color if undefined */
9046
+ /** @property {Color} - Color of the object when active, uses hoverColor if undefined */
8893
9047
  this.activeColor = undefined;
8894
9048
  /** @property {string} - Text for this ui object */
8895
9049
  this.text = undefined;
@@ -8987,10 +9141,10 @@ class UIObject
8987
9141
 
8988
9142
  // disconnect from parent and destroy children
8989
9143
  this.destroyed = 1;
8990
- this.parent && this.parent.removeChild(this);
9144
+ this.parent?.removeChild(this);
8991
9145
  for (const child of this.children)
8992
9146
  {
8993
- child.parent = 0;
9147
+ child.parent = undefined;
8994
9148
  child.destroy();
8995
9149
  }
8996
9150
  }
@@ -9086,10 +9240,10 @@ class UIObject
9086
9240
  this.interactive && this.isActiveObject() && !this.disabled ?
9087
9241
  this.color : this.lineColor;
9088
9242
  const color = isNavigationObject ? this.hoverColor :
9089
- this.disabled ? this.disabledColor :
9090
- this.interactive ?
9091
- this.isHoverObject() ? this.hoverColor :
9092
- this.isActiveObject() ? this.activeColor || this.color :
9243
+ this.disabled ? this.disabledColor :
9244
+ this.interactive ?
9245
+ this.isActiveObject() ? this.activeColor || this.hoverColor :
9246
+ this.isHoverObject() ? this.hoverColor :
9093
9247
  this.color : this.color;
9094
9248
  const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
9095
9249
 
@@ -9101,7 +9255,7 @@ class UIObject
9101
9255
  getTextSize()
9102
9256
  {
9103
9257
  return vec2(
9104
- this.textWidth || this.textFitScale * this.size.x,
9258
+ this.textWidth || this.textFitScale * this.size.x,
9105
9259
  this.textHeight || this.textFitScale * this.size.y);
9106
9260
  }
9107
9261
 
@@ -9149,9 +9303,9 @@ class UIObject
9149
9303
  renderDebug(visible=true)
9150
9304
  {
9151
9305
  // apply color based on state
9152
- const color =
9306
+ const color =
9153
9307
  !visible ? GREEN :
9154
- this.isHoverObject() ? YELLOW :
9308
+ this.isHoverObject() ? YELLOW :
9155
9309
  this.disabled ? PURPLE :
9156
9310
  this.interactive ? RED : BLUE;
9157
9311
  uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);
@@ -9483,8 +9637,8 @@ class UIScrollbar extends UIObject
9483
9637
  class UIVideo extends UIObject
9484
9638
  {
9485
9639
  /** Create a video player UI object
9486
- * @param {Vector2} [pos]
9487
- * @param {Vector2} [size]
9640
+ * @param {Vector2} pos
9641
+ * @param {Vector2} size
9488
9642
  * @param {string} src - Video file path or URL
9489
9643
  * @param {boolean} [autoplay=false] - Start playing immediately?
9490
9644
  * @param {boolean} [loop=false] - Loop the video?
@@ -9624,6 +9778,7 @@ class UIVideo extends UIObject
9624
9778
  * - Contact begin and end callbacks
9625
9779
  * - Wraps b2Vec2 type to/from Vector2
9626
9780
  * - Raycasting and querying
9781
+ * - Box2dTileLayer for grid based collision
9627
9782
  * - Every type of joint
9628
9783
  * - Debug physics drawing
9629
9784
  * @namespace Box2D
@@ -9673,21 +9828,24 @@ class Box2dObject extends EngineObject
9673
9828
  bodyDef.set_type(bodyType);
9674
9829
  bodyDef.set_position(box2d.vec2dTo(pos));
9675
9830
  bodyDef.set_angle(-angle);
9831
+
9832
+ /** @property {Object} - The Box2d body */
9676
9833
  this.body = box2d.world.CreateBody(bodyDef);
9677
- this.body.object = this;
9834
+ /** @property {Color} - Line color used for default box2d drawing */
9678
9835
  this.lineColor = BLACK;
9679
- box2d.objects.push(this);
9680
-
9681
- // edge lists and loops for drawing
9836
+ /** @property {Array<Object>} - List of all edges for default box2d drawing */
9682
9837
  this.edgeLists = [];
9838
+ /** @property {Array<Object>} - List of all edge loops for default box2d drawing */
9683
9839
  this.edgeLoops = [];
9840
+
9841
+ this.body.object = this; // link body to this object
9842
+ box2d.objects.push(this); // keep track of all box2d objects
9684
9843
  }
9685
9844
 
9686
9845
  /** Destroy this object and its physics body */
9687
9846
  destroy()
9688
9847
  {
9689
- if (this.destroyed)
9690
- return;
9848
+ if (this.destroyed) return;
9691
9849
 
9692
9850
  // destroy physics body, fixtures, and joints
9693
9851
  ASSERT(this.body, 'Box2dObject has no body to destroy');
@@ -9718,11 +9876,12 @@ class Box2dObject extends EngineObject
9718
9876
  }
9719
9877
 
9720
9878
  /** Draws all this object's fixtures
9721
- * @param {Color} [color]
9722
- * @param {Color} [lineColor]
9723
- * @param {number} [lineWidth]
9879
+ * @param {Color} [color]
9880
+ * @param {Color} [lineColor]
9881
+ * @param {number} [lineWidth]
9882
+ * @param {boolean} [useWebGL=glEnable]
9724
9883
  * @param {CanvasRenderingContext2D} [context] */
9725
- drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
9884
+ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
9726
9885
  {
9727
9886
  // draw non-edge fixtures
9728
9887
  this.getFixtureList().forEach((fixture)=>
@@ -9730,7 +9889,7 @@ class Box2dObject extends EngineObject
9730
9889
  const shape = box2d.castObjectType(fixture.GetShape());
9731
9890
  if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
9732
9891
  {
9733
- box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
9892
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, useWebGL, context);
9734
9893
  }
9735
9894
  });
9736
9895
 
@@ -9956,6 +10115,14 @@ class Box2dObject extends EngineObject
9956
10115
  return fixtures;
9957
10116
  }
9958
10117
 
10118
+ /** Destroy a fixture from the body
10119
+ * @param {Object} [fixture] */
10120
+ destroyFixture(fixture) { this.body.DestroyFixture(fixture); }
10121
+
10122
+ /** Destroy all fixture from the body */
10123
+ destroyAllFixtures()
10124
+ { this.getFixtureList().forEach(fixture=>this.destroyFixture(fixture)); }
10125
+
9959
10126
  ///////////////////////////////////////////////////////////////////////////////
9960
10127
  // physics get functions
9961
10128
 
@@ -10188,6 +10355,136 @@ class Box2dObject extends EngineObject
10188
10355
  }
10189
10356
  }
10190
10357
 
10358
+ ///////////////////////////////////////////////////////////////////////////////
10359
+ /**
10360
+ * Box2D Static Object - Box2d with a static physics body
10361
+ * @extends Box2dObject
10362
+ * @memberof Box2D
10363
+ */
10364
+ class Box2dStaticObject extends Box2dObject
10365
+ {
10366
+ /** Create a LittleJS object with Box2d physics
10367
+ * @param {Vector2} [pos]
10368
+ * @param {Vector2} [size]
10369
+ * @param {TileInfo} [tileInfo]
10370
+ * @param {number} [angle]
10371
+ * @param {Color} [color]
10372
+ * @param {number} [renderOrder] */
10373
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10374
+ {
10375
+ const bodyType = box2d.bodyTypeStatic;
10376
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10377
+ }
10378
+ }
10379
+
10380
+ ///////////////////////////////////////////////////////////////////////////////
10381
+ /**
10382
+ * Box2D Kiematic Object - Box2d with a kinematic physics body
10383
+ * @extends Box2dObject
10384
+ * @memberof Box2D
10385
+ */
10386
+ class Box2dKiematicObject extends Box2dObject
10387
+ {
10388
+ /** Create a LittleJS object with Box2d physics
10389
+ * @param {Vector2} [pos]
10390
+ * @param {Vector2} [size]
10391
+ * @param {TileInfo} [tileInfo]
10392
+ * @param {number} [angle]
10393
+ * @param {Color} [color]
10394
+ * @param {number} [renderOrder] */
10395
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10396
+ {
10397
+ const bodyType = box2d.bodyTypeKinematic;
10398
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10399
+ }
10400
+ }
10401
+
10402
+ ///////////////////////////////////////////////////////////////////////////////
10403
+ /**
10404
+ * Box2d Tile Layer
10405
+ * - adds Box2d support to tile layers
10406
+ * - creates static box2d fixtures for solid tiles
10407
+ * @extends Box2dObject
10408
+ * @memberof Box2D
10409
+ */
10410
+ class Box2dTileLayer extends Box2dStaticObject
10411
+ {
10412
+ /** Create a Box2d tile layer object
10413
+ * @param {TileCollisionLayer} tileLayer - Tile layer for this object */
10414
+ constructor(tileLayer)
10415
+ {
10416
+ ASSERT(tileLayer instanceof TileCollisionLayer, 'tileLayer must be a TileCollisionLayer');
10417
+ super(tileLayer.pos, tileLayer.size);
10418
+
10419
+ /** @property {TileLayer} - The tile layer */
10420
+ this.tileLayer = tileLayer;
10421
+ this.addChild(tileLayer);
10422
+ }
10423
+
10424
+ render()
10425
+ {
10426
+ // do not render fixtures, tile layer handles rendering
10427
+ }
10428
+
10429
+ /** Create box2d collision fixtures for solid tiles
10430
+ * @param {number} [friction]
10431
+ * @param {number} [restitution] */
10432
+ buildCollision(friction=.2, restitution=0)
10433
+ {
10434
+ // destroy all fixtures and create new ones
10435
+ this.destroyAllFixtures();
10436
+
10437
+ // create box2d object for this layer
10438
+ const size = this.tileLayer.size;
10439
+ this.pos = this.tileLayer.pos.copy();
10440
+ this.size = size.copy();
10441
+
10442
+ // track which tiles have been processed
10443
+ const processed = [];
10444
+ const getIndex = (x, y)=> x + y * size.x;
10445
+ const isSolidUnprocessed = (x, y)=>
10446
+ !processed[getIndex(x, y)] &&
10447
+ this.tileLayer.getCollisionData(vec2(x, y)) > 0;
10448
+
10449
+ // combine tiles into larger boxes
10450
+ for (let x = 0; x < size.x; ++x)
10451
+ for (let y = 0; y < size.y; ++y)
10452
+ {
10453
+ if (!isSolidUnprocessed(x, y)) continue;
10454
+
10455
+ // find max width by scanning right
10456
+ let width = 1, height = 1, canExpand = true;
10457
+ while (isSolidUnprocessed(x + width, y))
10458
+ ++width;
10459
+
10460
+ // find max height by scanning up, ensuring all rows have the same width
10461
+ while (canExpand)
10462
+ {
10463
+ for (let checkX = 0; checkX < width; ++checkX)
10464
+ {
10465
+ if (!isSolidUnprocessed(x + checkX, y + height))
10466
+ {
10467
+ canExpand = false;
10468
+ break;
10469
+ }
10470
+ }
10471
+ if (canExpand)
10472
+ ++height;
10473
+ }
10474
+
10475
+ // mark all tiles in this rectangle as processed
10476
+ for (let rectX = width; rectX--;)
10477
+ for (let rectY = height; rectY--;)
10478
+ processed[getIndex(x + rectX, y + rectY)] = true;
10479
+
10480
+ // create a single fixture for the entire rectangle
10481
+ const shapeSize = vec2(width, height);
10482
+ const offset = vec2(x + width/2, y + height/2);
10483
+ this.addBox(shapeSize, offset, 0, 0, friction, restitution);
10484
+ }
10485
+ }
10486
+ }
10487
+
10191
10488
  ///////////////////////////////////////////////////////////////////////////////
10192
10489
  /**
10193
10490
  * Box2D Raycast Result
@@ -10229,6 +10526,7 @@ class Box2dJoint
10229
10526
  * @param {Object} jointDef */
10230
10527
  constructor(jointDef)
10231
10528
  {
10529
+ /** @property {Object} - The Box2d joint */
10232
10530
  this.box2dJoint = box2d.castObjectType(box2d.world.CreateJoint(jointDef));
10233
10531
  }
10234
10532
 
@@ -10575,7 +10873,7 @@ class Box2dGearJoint extends Box2dJoint
10575
10873
  * @param {Box2dObject} objectB
10576
10874
  * @param {Box2dJoint} joint1
10577
10875
  * @param {Box2dJoint} joint2
10578
- * @param {ratio} [ratio] */
10876
+ * @param {number} [ratio] */
10579
10877
  constructor(objectA, objectB, joint1, joint2, ratio=1)
10580
10878
  {
10581
10879
  const jointDef = new box2d.instance.b2GearJointDef();
@@ -10717,50 +11015,6 @@ class Box2dPrismaticJoint extends Box2dJoint
10717
11015
  getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
10718
11016
  }
10719
11017
 
10720
- ///////////////////////////////////////////////////////////////////////////////
10721
- /**
10722
- * Box2D Static Object - Box2d with a static physics body
10723
- * @extends Box2dObject
10724
- * @memberof Box2D
10725
- */
10726
- class Box2dStaticObject extends Box2dObject
10727
- {
10728
- /** Create a LittleJS object with Box2d physics
10729
- * @param {Vector2} [pos]
10730
- * @param {Vector2} [size]
10731
- * @param {TileInfo} [tileInfo]
10732
- * @param {number} [angle]
10733
- * @param {Color} [color]
10734
- * @param {number} [renderOrder] */
10735
- constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10736
- {
10737
- const bodyType = box2d.bodyTypeStatic;
10738
- super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10739
- }
10740
- }
10741
-
10742
- ///////////////////////////////////////////////////////////////////////////////
10743
- /**
10744
- * Box2D Kiematic Object - Box2d with a kinematic physics body
10745
- * @extends Box2dObject
10746
- * @memberof Box2D
10747
- */
10748
- class Box2dKiematicObject extends Box2dObject
10749
- {
10750
- /** Create a LittleJS object with Box2d physics
10751
- * @param {Vector2} [pos]
10752
- * @param {Vector2} [size]
10753
- * @param {TileInfo} [tileInfo]
10754
- * @param {number} [angle]
10755
- * @param {Color} [color]
10756
- * @param {number} [renderOrder] */
10757
- constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10758
- {
10759
- const bodyType = box2d.bodyTypeKinematic;
10760
- super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10761
- }
10762
- }
10763
-
10764
11018
  ///////////////////////////////////////////////////////////////////////////////
10765
11019
  /**
10766
11020
  * Box2D Wheel Joint
@@ -11121,10 +11375,13 @@ class Box2dPlugin
11121
11375
  {
11122
11376
  ASSERT(!box2d, 'Box2D already initialized');
11123
11377
  box2d = this;
11378
+
11379
+ /** @property {Object} - The Box2d instance */
11124
11380
  this.instance = instance;
11381
+ /** @property {Object} - The Box2d world */
11125
11382
  this.world = new box2d.instance.b2World();
11383
+ /** @property {Array<Box2dObject>} - List of all Box2d objects */
11126
11384
  this.objects = [];
11127
-
11128
11385
  /** @property {number} - Velocity iterations per update*/
11129
11386
  this.velocityIterations = 8;
11130
11387
  /** @property {number} - Position iterations per update*/
@@ -11323,8 +11580,9 @@ class Box2dPlugin
11323
11580
  * @param {Color} [color]
11324
11581
  * @param {Color} [lineColor]
11325
11582
  * @param {number} [lineWidth]
11583
+ * @param {boolean} [useWebGL=glEnable]
11326
11584
  * @param {CanvasRenderingContext2D} [context] */
11327
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
11585
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebgl, context)
11328
11586
  {
11329
11587
  const shape = box2d.castObjectType(fixture.GetShape());
11330
11588
  switch (shape.GetType())
@@ -11334,20 +11592,20 @@ class Box2dPlugin
11334
11592
  let points = [];
11335
11593
  for (let i=shape.GetVertexCount(); i--;)
11336
11594
  points.push(box2d.vec2From(shape.GetVertex(i)));
11337
- drawPoly(points, color, lineWidth, lineColor, pos, angle);
11595
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebgl, false, context);
11338
11596
  break;
11339
11597
  }
11340
11598
  case box2d.instance.b2Shape.e_circle:
11341
11599
  {
11342
11600
  const radius = shape.get_m_radius();
11343
- drawCircle(pos, radius*2, color, lineWidth, lineColor);
11601
+ drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebgl, false, context);
11344
11602
  break;
11345
11603
  }
11346
11604
  case box2d.instance.b2Shape.e_edge:
11347
11605
  {
11348
11606
  const v1 = box2d.vec2From(shape.get_m_vertex1());
11349
11607
  const v2 = box2d.vec2From(shape.get_m_vertex2());
11350
- drawLine(v1, v2, lineWidth, lineColor, pos, angle);
11608
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebgl, false, context);
11351
11609
  break;
11352
11610
  }
11353
11611
  }
@@ -11365,7 +11623,7 @@ class Box2dPlugin
11365
11623
  }
11366
11624
 
11367
11625
  /** converts a box2d vec2 pointer to a Vector2
11368
- * @param {Object} v */
11626
+ * @param {Object} vp */
11369
11627
  vec2FromPointer(vp)
11370
11628
  {
11371
11629
  const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
@@ -11477,7 +11735,7 @@ async function box2dInit()
11477
11735
  const box2dColorPointer = (c)=>
11478
11736
  box2dColor(box2d.instance.wrapPointer(c, box2d.instance.b2Color));
11479
11737
  const getDebugColor = (color)=>box2dColorPointer(color).scale(1,.8);
11480
- const getPointsList = (vertices, vertexCount) =>
11738
+ const getPointsList = (vertices, vertexCount)=>
11481
11739
  {
11482
11740
  const points = [];
11483
11741
  for (let i=vertexCount; i--;)