littlejsengine 1.16.2 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/littlejs.d.ts +200 -166
  2. package/dist/littlejs.esm.js +1231 -1086
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +888 -735
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +884 -731
  7. package/examples/box2d/gameObjects.js +1 -1
  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 +1 -1
  13. package/examples/logo.png +0 -0
  14. package/examples/module/tiles.png +0 -0
  15. package/examples/platformer/gameEffects.js +23 -22
  16. package/examples/platformer/gameLevel.js +24 -25
  17. package/examples/shorts/base.html +1 -1
  18. package/examples/shorts/clock.js +3 -3
  19. package/examples/shorts/fontImage.js +4 -3
  20. package/examples/shorts/parallax.js +1 -1
  21. package/examples/shorts/sequencer.js +1 -1
  22. package/examples/shorts/shapes.js +1 -1
  23. package/examples/shorts/texture.js +10 -6
  24. package/examples/shorts/tiles.png +0 -0
  25. package/examples/shorts/tiltedView.js +2 -0
  26. package/examples/starter/index.html +2 -2
  27. package/examples/starter/tiles.png +0 -0
  28. package/examples/typescript/tiles.png +0 -0
  29. package/examples/uiSystem/game.js +1 -1
  30. package/examples/uiSystem/tiles.png +0 -0
  31. package/package.json +1 -1
  32. package/plugins/postProcess.js +47 -28
  33. package/plugins/uiSystem.js +15 -15
  34. package/reference.md +1 -1
  35. package/src/engine.js +174 -174
  36. package/src/engineAudio.js +4 -6
  37. package/src/engineDebug.js +4 -4
  38. package/src/engineDraw.js +179 -122
  39. package/src/engineExport.js +326 -334
  40. package/src/engineFont.png +0 -0
  41. package/src/engineInput.js +14 -20
  42. package/src/engineMath.js +15 -104
  43. package/src/engineObject.js +26 -25
  44. package/src/engineParticles.js +2 -2
  45. package/src/engineTileLayer.js +196 -170
  46. package/src/engineUtilities.js +100 -4
  47. package/src/engineWebGL.js +123 -72
@@ -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.1';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -320,7 +320,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
320
320
 
321
321
  // responsive aspect ratio with native resolution
322
322
  const innerAspect = innerWidth / innerHeight;
323
- ASSERT(canvasMinAspect <= canvasMaxAspect);
323
+ ASSERT(canvasMinAspect <= canvasMaxAspect);
324
324
  if (canvasMaxAspect && innerAspect > canvasMaxAspect)
325
325
  {
326
326
  // full height
@@ -377,7 +377,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
377
377
  debugInit();
378
378
 
379
379
  // setup canvases
380
- // transform way is still more reliable then flexbox or grid
380
+ // transform way is still more reliable than flexbox or grid
381
381
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
382
382
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
383
383
  mainCanvas.style.cssText = styleCanvas;
@@ -422,6 +422,9 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
422
422
  }));
423
423
  }
424
424
 
425
+ // load engine font image
426
+ promises.push(fontImageInit());
427
+
425
428
  if (showSplashScreen)
426
429
  {
427
430
  // draw splash screen
@@ -433,7 +436,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
433
436
  function updateSplash()
434
437
  {
435
438
  inputClear();
436
- drawEngineSplashScreen(t+=.01);
439
+ drawEngineLogo(t+=.01);
437
440
  t>1 ? resolve() : setTimeout(updateSplash, 16);
438
441
  }
439
442
  }));
@@ -449,172 +452,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
449
452
  await gameInit();
450
453
  engineUpdate();
451
454
  }
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
455
  }
619
456
 
620
457
  /** Update each engine object, remove destroyed objects, and update time
@@ -628,8 +465,7 @@ function engineObjectsUpdate()
628
465
  // recursive object update
629
466
  function updateObject(o)
630
467
  {
631
- if (o.destroyed)
632
- return;
468
+ if (o.destroyed) return;
633
469
 
634
470
  o.update();
635
471
  for (const child of o.children)
@@ -637,8 +473,7 @@ function engineObjectsUpdate()
637
473
  }
638
474
  for (const o of engineObjects)
639
475
  {
640
- if (o.parent)
641
- continue;
476
+ if (o.parent || o.destroyed) continue;
642
477
 
643
478
  // update top level objects
644
479
  o.update();
@@ -727,6 +562,171 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
727
562
 
728
563
  debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
729
564
  return hitObjects;
565
+ }
566
+
567
+ ///////////////////////////////////////////////////////////////////////////////
568
+ function drawEngineLogo(t)
569
+ {
570
+ // LittleJS Logo and Splash Screen
571
+ const x = mainContext;
572
+ const w = mainCanvas.width = innerWidth;
573
+ const h = mainCanvas.height = innerHeight;
574
+
575
+ {
576
+ // background
577
+ const p3 = percent(t, 1, .8);
578
+ const p4 = percent(t, 0, .5);
579
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
580
+ g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
581
+ g.addColorStop(1,hsl(0,0,0,p3).toString());
582
+ x.save();
583
+ x.fillStyle = g;
584
+ x.fillRect(0,0,w,h);
585
+ }
586
+
587
+ // draw LittleJS logo...
588
+ const rect = (X, Y, W, H, C)=>
589
+ {
590
+ x.beginPath();
591
+ x.rect(X,Y,W,C?H*p:H);
592
+ x.fillStyle = C;
593
+ C ? x.fill() : x.stroke();
594
+ };
595
+ const line = (X, Y, Z, W)=>
596
+ {
597
+ x.beginPath();
598
+ x.lineTo(X,Y);
599
+ x.lineTo(Z,W);
600
+ x.stroke();
601
+ };
602
+ const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
603
+ {
604
+ const D = (A+B)/2, E = p*(B-A)/2;
605
+ x.beginPath();
606
+ F && x.lineTo(X,Y);
607
+ x.arc(X,Y,R,D-E,D+E);
608
+ x.fillStyle = C;
609
+ C ? x.fill() : x.stroke();
610
+ };
611
+ const color = (c=0, l=0) =>
612
+ hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
613
+ const alpha = wave(1,1,t);
614
+ const p = percent(alpha, .1, .5);
615
+
616
+ // setup
617
+ x.translate(w/2,h/2);
618
+ const size = min(6, min(w,h)/99); // fit to screen
619
+ x.scale(size,size);
620
+ x.translate(-40,-35);
621
+ x.lineJoin = x.lineCap = 'round';
622
+ x.lineWidth = .1 + p*1.9;
623
+
624
+ // drawing effect
625
+ const p2 = percent(alpha,.1,1);
626
+ x.setLineDash([99*p2,99]);
627
+
628
+ // cab top
629
+ rect(7,16,18,-8,color(2,2));
630
+ rect(7,8,18,4,color(2,3));
631
+ rect(25,8,8,8,color(2,1));
632
+ rect(25,8,-18,8);
633
+ rect(25,8,8,8);
634
+
635
+ // cab
636
+ rect(25,16,7,24,color());
637
+ rect(11,39,14,-23,color(1,1));
638
+ rect(11,16,14,18,color(1,2));
639
+ rect(11,16,14,8,color(1,3));
640
+ rect(25,16,-14,24);
641
+
642
+ // cab window
643
+ rect(15,29,6,-9,color(2,2));
644
+ circle(15,21,5,0,PI/2,color(2,4),1);
645
+ rect(21,21,-6,9);
646
+
647
+ // little stack
648
+ rect(37,14,9,6,color(3,2));
649
+ rect(37,14,4.5,6,color(3,3));
650
+ rect(37,14,9,6);
651
+
652
+ // big stack
653
+ rect(50,20,10,-8,color(0,1));
654
+ rect(50,20,6.5,-8,color(0,2));
655
+ rect(50,20,3.5,-8,color(0,3));
656
+ rect(50,20,10,-8);
657
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
658
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
659
+ circle(55,2,11.4,.5,PI-.5);
660
+ rect(45,7,20,-7,color(0,2));
661
+ rect(45,-1,20,4,color(0,3));
662
+ rect(45,-1,20,8);
663
+
664
+ // engine
665
+ for (let i=5; i--;)
666
+ {
667
+ // stagger radius to fix slight seam
668
+ circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
669
+ circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
670
+ circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
671
+ }
672
+
673
+ // engine outline
674
+ circle(36,30,10,PI/2,PI*3/2);
675
+ circle(48,30,10,PI/2,PI*3/2);
676
+ circle(60,30,10);
677
+ line(36,20,60,20);
678
+
679
+ // engine front light
680
+ circle(60,30,4,PI,3*PI,color(3,2));
681
+ circle(60,30,4,PI,2*PI,color(3,3));
682
+ circle(60,30,4,PI,3*PI);
683
+
684
+ // front brush
685
+ for (let i=6; i--;)
686
+ {
687
+ x.beginPath();
688
+ x.lineTo(53,54);
689
+ x.lineTo(53,40);
690
+ x.lineTo(53+(1+i*2.9)*p,40);
691
+ x.lineTo(53+(4+i*3.5)*p,54);
692
+ x.fillStyle = color(0,i%2+2);
693
+ x.fill();
694
+ i%2 && x.stroke();
695
+ }
696
+
697
+ // wheels
698
+ rect(6,40,5,5);
699
+ rect(6,40,5,5,color());
700
+ rect(15,54,38,-14,color());
701
+ for (let i=3; i--;)
702
+ for (let j=2; j--;)
703
+ {
704
+ circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
705
+ x.stroke();
706
+ circle(15*i+15,47,j?7:1,0,PI,color(i,2));
707
+ x.stroke();
708
+ }
709
+ line(6,40,68,40); // center
710
+ line(77,54,4,54); // bottom
711
+
712
+ // draw engine name
713
+ const s = engineName;
714
+ x.font = '900 16px arial';
715
+ x.textAlign = 'center';
716
+ x.textBaseline = 'top';
717
+ x.lineWidth = .1+p*3.9;
718
+ let w2 = 0;
719
+ for (let i=0; i<s.length; ++i)
720
+ w2 += x.measureText(s[i]).width;
721
+ for (let j=2; j--;)
722
+ for (let i=0, X=41-w2/2; i<s.length; ++i)
723
+ {
724
+ x.fillStyle = color(i,2);
725
+ const w = x.measureText(s[i]).width;
726
+ x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
727
+ X += w;
728
+ }
729
+ x.restore();
730
730
  }
731
731
  /**
732
732
  * LittleJS - Release Mode
@@ -767,12 +767,11 @@ function debugVideoCaptureStop (){}
767
767
  function debugVideoCaptureUpdate(){}
768
768
  function debugProtectConstant(o){ return o; }
769
769
  /**
770
- * LittleJS Utility Classes and Functions
770
+ * LittleJS Math Classes and Functions
771
771
  * - 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
772
  * - RandomGenerator - seeded random number generator
773
+ * - Vector2 - fast, simple, easy 2D vector class
774
+ * - Color - holds a rgba color with math functions
776
775
  * @namespace Math
777
776
  */
778
777
 
@@ -960,11 +959,11 @@ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
960
959
 
961
960
  /** Returns true if two axis aligned bounding boxes are overlapping
962
961
  * 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
962
+ * @param {Vector2} posA - Center of box A
963
+ * @param {Vector2} sizeA - Size of box A
964
+ * @param {Vector2} posB - Center of box B
965
+ * @param {Vector2} [sizeB=vec2()] - Size of box B, uses a point if undefined
966
+ * @return {boolean} - True if overlapping
968
967
  * @memberof Math */
969
968
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
970
969
  {
@@ -1038,7 +1037,7 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1038
1037
  * @param {any} s
1039
1038
  * @return {boolean}
1040
1039
  * @memberof Math */
1041
- function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1040
+ function isString(s) { return s != null && typeof s?.toString() === 'string'; }
1042
1041
 
1043
1042
  /**
1044
1043
  * Check if object is an array
@@ -1182,8 +1181,8 @@ function randInCircle(radius=1, minRadius=0)
1182
1181
  { return radius > 0 ? randVec2(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
1183
1182
 
1184
1183
  /** 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)]
1184
+ * @param {Color} [colorA=WHITE]
1185
+ * @param {Color} [colorB=BLACK]
1187
1186
  * @param {boolean} [linear]
1188
1187
  * @return {Color}
1189
1188
  * @memberof Random */
@@ -1262,8 +1261,8 @@ class RandomGenerator
1262
1261
  { return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
1263
1262
 
1264
1263
  /** 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)]
1264
+ * @param {Color} [colorA=WHITE]
1265
+ * @param {Color} [colorB=BLACK]
1267
1266
  * @param {boolean} [linear]
1268
1267
  * @return {Color} */
1269
1268
  randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
@@ -1307,7 +1306,7 @@ class RandomGenerator
1307
1306
  * a = vec2(5); // set a to (5, 5)
1308
1307
  * b = vec2(); // set b to (0, 0)
1309
1308
  * @memberof Math */
1310
- function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
1309
+ function vec2(x=0, y) { return new Vector2(x, y ?? x); }
1311
1310
 
1312
1311
  /**
1313
1312
  * Check if object is a valid Vector2
@@ -1515,10 +1514,6 @@ class Vector2
1515
1514
  * @return {number} */
1516
1515
  area() { return abs(this.x * this.y); }
1517
1516
 
1518
- /** Returns true if this vector is (0,0)
1519
- * @return {boolean} */
1520
- isZero() { return !this.x && !this.y; }
1521
-
1522
1517
  /** Returns a new vector that is p percent between this and the vector passed in
1523
1518
  * @param {Vector2} v - other vector
1524
1519
  * @param {number} percent
@@ -1880,7 +1875,97 @@ const PURPLE = debugProtectConstant(rgb(.5,0,1));
1880
1875
  /** Color - Magenta #ff00ff
1881
1876
  * @type {Color}
1882
1877
  * @memberof Math */
1883
- const MAGENTA = debugProtectConstant(rgb(1,0,1));
1878
+ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1879
+ /**
1880
+ * LittleJS Utility Classes and Functions
1881
+ * - General purpose utilities
1882
+ * - Timer - tracks time automatically
1883
+ * @namespace Utilities
1884
+ */
1885
+
1886
+ /** Formats seconds to mm:ss style for display purposes
1887
+ * @param {number} t - time in seconds
1888
+ * @return {string}
1889
+ * @memberof Utilities */
1890
+ function formatTime(t)
1891
+ {
1892
+ const sign = t < 0 ? '-' : '';
1893
+ t = abs(t)|0;
1894
+ return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
1895
+ }
1896
+
1897
+ /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
1898
+ * @param {string} url - URL of JSON file
1899
+ * @return {Promise<object>}
1900
+ * @memberof Utilities */
1901
+ async function fetchJSON(url)
1902
+ {
1903
+ const response = await fetch(url);
1904
+ if (!response.ok)
1905
+ throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1906
+ return response.json();
1907
+ }
1908
+
1909
+ ///////////////////////////////////////////////////////////////////////////////
1910
+
1911
+ /** Save a text file to disk
1912
+ * @param {string} text
1913
+ * @param {string} [filename]
1914
+ * @param {string} [type]
1915
+ * @memberof Utilities */
1916
+ function saveText(text, filename='text', type='text/plain')
1917
+ { saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
1918
+
1919
+ /** Save a canvas to disk
1920
+ * @param {HTMLCanvasElement|OffscreenCanvas} canvas
1921
+ * @param {string} [filename]
1922
+ * @param {string} [type]
1923
+ * @memberof Utilities */
1924
+ function saveCanvas(canvas, filename='screenshot', type='image/png')
1925
+ {
1926
+ if (canvas instanceof OffscreenCanvas)
1927
+ {
1928
+ // copy to temporary canvas and save
1929
+ const saveCanvas = document.createElement('canvas');
1930
+ saveCanvas.width = canvas.width;
1931
+ saveCanvas.height = canvas.height;
1932
+ saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
1933
+ saveDataURL(saveCanvas.toDataURL(type), filename);
1934
+ }
1935
+ else
1936
+ saveDataURL(canvas.toDataURL(type), filename);
1937
+ }
1938
+
1939
+ /** Save a data url to disk
1940
+ * @param {string} url
1941
+ * @param {string} [filename]
1942
+ * @param {number} [revokeTime] - how long before revoking the url
1943
+ * @memberof Utilities */
1944
+ function saveDataURL(url, filename='download', revokeTime)
1945
+ {
1946
+ ASSERT(isString(url), 'saveDataURL requires url string');
1947
+ ASSERT(isString(filename), 'saveDataURL requires filename string');
1948
+
1949
+ // create link for saving screenshots
1950
+ const link = document.createElement('a');
1951
+ link.download = filename;
1952
+ link.href = url;
1953
+ link.click();
1954
+ if (revokeTime !== undefined)
1955
+ setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
1956
+ }
1957
+
1958
+ /** Share content using the native share dialog if available
1959
+ * @param {string} title - title of the share
1960
+ * @param {string} url - url to share
1961
+ * @param {Function} [callback] - Called when share is complete
1962
+ * @memberof Utilities */
1963
+ function shareURL(title, url, callback)
1964
+ {
1965
+ ASSERT(isString(title), 'shareURL requires title string');
1966
+ ASSERT(isString(url), 'shareURL requires url string');
1967
+ navigator.share?.({title, url}).then(()=>callback?.());
1968
+ }
1884
1969
 
1885
1970
  ///////////////////////////////////////////////////////////////////////////////
1886
1971
 
@@ -1965,84 +2050,6 @@ class Timer
1965
2050
  * @return {number} */
1966
2051
  valueOf() { return this.get(); }
1967
2052
  }
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
2053
  /**
2047
2054
  * LittleJS Engine Settings
2048
2055
  * - All settings for the engine are here
@@ -2669,10 +2676,10 @@ function setDebugKey(key) { debugKey = key; }
2669
2676
  class EngineObject
2670
2677
  {
2671
2678
  /** 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
2679
+ * @param {Vector2} [pos=vec2()] - World space position of the object
2680
+ * @param {Vector2} [size=vec2(1)] - World space size of the object
2681
+ * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2682
+ * @param {number} [angle] - Angle the object is rotated by
2676
2683
  * @param {Color} [color=WHITE] - Color to apply to tile when rendered
2677
2684
  * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2678
2685
  */
@@ -2800,15 +2807,16 @@ class EngineObject
2800
2807
  // physics sanity checks
2801
2808
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
2802
2809
  ASSERT(this.damping >= 0 && this.damping <= 1);
2803
- if (!enablePhysicsSolver || !this.mass) // don't do collision for static objects
2804
- return;
2810
+
2811
+ // don't do collision for static objects or if solver disabled
2812
+ if (!enablePhysicsSolver || !this.mass) return;
2805
2813
 
2806
2814
  const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
2807
2815
  if (this.groundObject)
2808
2816
  {
2809
2817
  // apply friction in local space of ground object
2810
2818
  const friction = max(this.friction, this.groundObject.friction);
2811
- const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
2819
+ const groundSpeed = this.groundObject.velocity.x;
2812
2820
  this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
2813
2821
  this.groundObject = undefined;
2814
2822
  }
@@ -2819,19 +2827,19 @@ class EngineObject
2819
2827
  const epsilon = .001; // necessary to push slightly outside of the collision
2820
2828
  for (const o of engineObjectsCollide)
2821
2829
  {
2830
+ // skip destroyed, child objects, or self collision
2831
+ if (o.destroyed || o.parent || o === this) continue;
2832
+
2822
2833
  // non solid objects don't collide with each other
2823
- if ((!this.isSolid && !o.isSolid) || o.destroyed || o.parent || o === this)
2824
- continue;
2834
+ if (!this.isSolid && !o.isSolid) continue;
2825
2835
 
2826
2836
  // check collision
2827
- if (!this.isOverlappingObject(o))
2828
- continue;
2837
+ if (!this.isOverlappingObject(o)) continue;
2829
2838
 
2830
2839
  // notify objects of collision and check if should be resolved
2831
2840
  const collide1 = this.collideWithObject(o);
2832
2841
  const collide2 = o.collideWithObject(this);
2833
- if (!collide1 || !collide2)
2834
- continue;
2842
+ if (!collide1 || !collide2) continue;
2835
2843
 
2836
2844
  if (isOverlapping(oldPos, this.size, o.pos, o.size))
2837
2845
  {
@@ -2933,7 +2941,7 @@ class EngineObject
2933
2941
  const delta = y - this.pos.y;
2934
2942
  if (delta < maxMoveUp)
2935
2943
  if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
2936
- {
2944
+ {
2937
2945
  this.pos.y = y;
2938
2946
  debugPhysics && debugRect(this.pos, this.size, '#ff0');
2939
2947
  return;
@@ -2993,10 +3001,10 @@ class EngineObject
2993
3001
 
2994
3002
  // disconnect from parent and destroy children
2995
3003
  this.destroyed = 1;
2996
- this.parent && this.parent.removeChild(this);
3004
+ this.parent?.removeChild(this);
2997
3005
  for (const child of this.children)
2998
3006
  {
2999
- child.parent = 0;
3007
+ child.parent = undefined;
3000
3008
  child.destroy();
3001
3009
  }
3002
3010
  }
@@ -3017,15 +3025,15 @@ class EngineObject
3017
3025
  * @param {Vector2} vec - world space vector */
3018
3026
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
3019
3027
 
3020
- /** Called to check if a tile collision should be resolved
3028
+ /** 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
3029
  * @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 */
3030
+ * @param {Vector2} pos - tile where the collision occurred
3031
+ * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
3024
3032
  collideWithTile(tileData, pos) { return tileData > 0; }
3025
3033
 
3026
- /** Called to check if a object collision should be resolved
3034
+ /** 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
3035
  * @param {EngineObject} object - the object to test against
3028
- * @return {boolean} - true if the collision should be resolved
3036
+ * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
3029
3037
  */
3030
3038
  collideWithObject(object) { return true; }
3031
3039
 
@@ -3066,9 +3074,9 @@ class EngineObject
3066
3074
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
3067
3075
  getMirrorSign() { return this.mirror ? -1 : 1; }
3068
3076
 
3069
- /** Attaches a child to this with a local transform, returns child for chaining
3077
+ /** Attaches a child to this with a local transform, returns child for chaining
3070
3078
  * @param {EngineObject} child
3071
- * @param {Vector2} [localPos=(0,0)]
3079
+ * @param {Vector2} [localPos=vec2()]
3072
3080
  * @param {number} [localAngle]
3073
3081
  * @return {EngineObject} The child object added */
3074
3082
  addChild(child, localPos=vec2(), localAngle=0)
@@ -3092,7 +3100,7 @@ class EngineObject
3092
3100
  const index = this.children.indexOf(child);
3093
3101
  ASSERT(index >= 0, 'child not found in children array');
3094
3102
  index >= 0 && this.children.splice(index, 1);
3095
- child.parent = 0;
3103
+ child.parent = undefined;
3096
3104
  }
3097
3105
 
3098
3106
  /** Check if overlapping another engine object
@@ -3104,7 +3112,7 @@ class EngineObject
3104
3112
 
3105
3113
  /** Check if overlapping a point or aligned bounding box
3106
3114
  * @param {Vector2} pos - Center of box
3107
- * @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
3115
+ * @param {Vector2} [size=vec2()] - Size of box, uses a point if undefined
3108
3116
  * @return {boolean} */
3109
3117
  isOverlapping(pos, size=vec2())
3110
3118
  { return isOverlapping(this.pos, this.size, pos, size); }
@@ -3235,10 +3243,11 @@ let drawCount;
3235
3243
  * Create a tile info object using a grid based system
3236
3244
  * - This can take vecs or floats for easier use and conversion
3237
3245
  * - 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
3246
+ * @param {Vector2|number} [index=0] - Index of the tile in 1d or 2d form
3239
3247
  * @param {Vector2|number} [size] - Size of tile in pixels
3240
- * @param {number} [textureIndex] - Texture index to use
3248
+ * @param {TextureInfo|number} [texture] - Texture index or info to use
3241
3249
  * @param {number} [padding] - How many pixels padding around tiles
3250
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3242
3251
  * @return {TileInfo}
3243
3252
  * @example
3244
3253
  * tile(2) // a tile at index 2 using the default tile size of 16
@@ -3246,36 +3255,43 @@ let drawCount;
3246
3255
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
3247
3256
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
3248
3257
  * @memberof Draw */
3249
- function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding)
3258
+ function tile(index=new Vector2, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3250
3259
  {
3251
- if (headlessMode)
3252
- return new TileInfo;
3260
+ ASSERT(isVector2(index) || typeof index === 'number', 'index must be a vec2 or number');
3261
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
3262
+ ASSERT(isNumber(texture) || texture instanceof TextureInfo, 'texture must be a number or TextureInfo');
3263
+ ASSERT(isNumber(padding), 'padding must be a number');
3264
+
3265
+ if (headlessMode) return new TileInfo;
3253
3266
 
3254
- // if size is a number, make it a vector
3255
3267
  if (typeof size === 'number')
3256
3268
  {
3269
+ // if size is a number, make it a vector
3257
3270
  ASSERT(size > 0);
3258
3271
  size = new Vector2(size, size);
3259
3272
  }
3260
3273
 
3261
3274
  // create tile info object
3262
- const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
3275
+ const textureInfo = typeof texture === 'number' ?
3276
+ textureInfos[texture] : texture;
3263
3277
 
3264
3278
  // get the position of the tile
3265
- const textureInfo = textureInfos[textureIndex];
3266
- ASSERT(!!textureInfo, 'Texture not loaded');
3267
3279
  const sizePaddedX = size.x + padding*2;
3268
3280
  const sizePaddedY = size.y + padding*2;
3269
- if (typeof pos === 'number')
3281
+ let x, y;
3282
+ if (typeof index === 'number')
3270
3283
  {
3271
3284
  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);
3285
+ x = index % cols;
3286
+ y = index / cols |0;
3275
3287
  }
3276
3288
  else
3277
- tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
3278
- return tileInfo;
3289
+ {
3290
+ x = index.x;
3291
+ y = index.y;
3292
+ }
3293
+ const pos = new Vector2(x*sizePaddedX + padding, y*sizePaddedY + padding);
3294
+ return new TileInfo(pos, size, textureInfo, padding, bleed);
3279
3295
  }
3280
3296
 
3281
3297
  /**
@@ -3285,24 +3301,22 @@ function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=til
3285
3301
  class TileInfo
3286
3302
  {
3287
3303
  /** Create a tile info object
3288
- * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3304
+ * @param {Vector2} [pos=vec2()] - Top left corner of tile in pixels
3289
3305
  * @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
3306
+ * @param {TextureInfo} [textureInfo] - Texture info to use
3307
+ * @param {number} [padding] - How many pixels padding around tiles
3308
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3293
3309
  */
3294
- constructor(pos=vec2(), size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3310
+ constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
3295
3311
  {
3296
3312
  /** @property {Vector2} - Top left corner of tile in pixels */
3297
3313
  this.pos = pos.copy();
3298
3314
  /** @property {Vector2} - Size of tile in pixels */
3299
3315
  this.size = size.copy();
3300
- /** @property {number} - Texture index to use */
3301
- this.textureIndex = textureIndex;
3302
3316
  /** @property {number} - How many pixels padding around tiles */
3303
3317
  this.padding = padding;
3304
3318
  /** @property {TextureInfo} - The texture info for this tile */
3305
- this.textureInfo = textureInfos[this.textureIndex];
3319
+ this.textureInfo = textureInfo;
3306
3320
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
3307
3321
  this.bleed = bleed;
3308
3322
  }
@@ -3312,7 +3326,7 @@ class TileInfo
3312
3326
  * @return {TileInfo}
3313
3327
  */
3314
3328
  offset(offset)
3315
- { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleed); }
3329
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed); }
3316
3330
 
3317
3331
  /** Returns a copy of this tile offset by a number of animation frames
3318
3332
  * @param {number} frame - Offset to apply in animation frames
@@ -3321,23 +3335,33 @@ class TileInfo
3321
3335
  frame(frame)
3322
3336
  {
3323
3337
  ASSERT(typeof frame === 'number');
3324
- return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
3338
+ const w = this.size.x + this.padding*2;
3339
+ const x = frame*w;
3340
+ ASSERT(x < this.textureInfo.size.x, 'frame extends beyond texture width!');
3341
+ return this.offset(new Vector2(x));
3325
3342
  }
3326
3343
 
3327
3344
  /**
3328
3345
  * Set this tile to use a full image in a texture info
3329
- * @param {TextureInfo} textureInfo
3346
+ * @param {TextureInfo} [textureInfo]
3330
3347
  * @return {TileInfo}
3331
3348
  */
3332
- setFullImage(textureInfo)
3349
+ setFullImage(textureInfo=this.textureInfo)
3333
3350
  {
3351
+ this.textureInfo = textureInfo;
3334
3352
  this.pos = new Vector2;
3335
3353
  this.size = textureInfo.size.copy();
3336
- this.textureInfo = textureInfo;
3337
- // do not use padding or bleed
3338
3354
  this.bleed = this.padding = 0;
3339
3355
  return this;
3340
3356
  }
3357
+
3358
+ /**
3359
+ * Returns a tile info for an index using this tile as refrence
3360
+ * @param {Vector2|number} [index=0]
3361
+ * @return {TileInfo}
3362
+ */
3363
+ tile(index)
3364
+ { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
3341
3365
  }
3342
3366
 
3343
3367
  /**
@@ -3378,19 +3402,19 @@ class TextureInfo
3378
3402
  ///////////////////////////////////////////////////////////////////////////////
3379
3403
  // Drawing functions
3380
3404
 
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
3405
+ /** Draw textured tile centered in world space
3406
+ * @param {Vector2} pos - Center of the tile in world space
3407
+ * @param {Vector2} [size=vec2(1)] - Size of the tile in world space
3408
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
3409
+ * @param {Color} [color=WHITE] - Color to modulate with
3410
+ * @param {number} [angle] - Angle to rotate by
3411
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
3412
+ * @param {Color} [additiveColor] - Additive color to be applied if any
3389
3413
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
3390
3414
  * @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
3391
3415
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
3392
3416
  * @memberof Draw */
3393
- function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3417
+ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
3394
3418
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
3395
3419
  {
3396
3420
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3465,8 +3489,8 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3465
3489
 
3466
3490
  /** Draw colored rect centered on pos
3467
3491
  * @param {Vector2} pos
3468
- * @param {Vector2} [size=(1,1)]
3469
- * @param {Color} [color=(1,1,1,1)]
3492
+ * @param {Vector2} [size=vec2(1)]
3493
+ * @param {Color} [color=WHITE]
3470
3494
  * @param {number} [angle]
3471
3495
  * @param {boolean} [useWebGL=glEnable]
3472
3496
  * @param {boolean} [screenSpace]
@@ -3479,9 +3503,9 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3479
3503
 
3480
3504
  /** Draw a rect centered on pos with a gradient from top to bottom
3481
3505
  * @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)]
3506
+ * @param {Vector2} [size=vec2(1)]
3507
+ * @param {Color} [colorTop=WHITE]
3508
+ * @param {Color} [colorBottom=BLACK]
3485
3509
  * @param {number} [angle]
3486
3510
  * @param {boolean} [useWebGL=glEnable]
3487
3511
  * @param {boolean} [screenSpace]
@@ -3543,9 +3567,9 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3543
3567
  /** Draw connected lines between a series of points
3544
3568
  * @param {Array<Vector2>} points
3545
3569
  * @param {number} [width]
3546
- * @param {Color} [color=(1,1,1,1)]
3570
+ * @param {Color} [color=WHITE]
3547
3571
  * @param {boolean} [wrap] - Should the last point connect to the first?
3548
- * @param {Vector2} [pos=(0,0)] - Offset to apply
3572
+ * @param {Vector2} [pos=vec2()] - Offset to apply
3549
3573
  * @param {number} [angle] - Angle to rotate by
3550
3574
  * @param {boolean} [useWebGL=glEnable]
3551
3575
  * @param {boolean} [screenSpace]
@@ -3580,13 +3604,9 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3580
3604
  for (let i=0; i<points.length; ++i)
3581
3605
  {
3582
3606
  const point = points[i];
3583
- if (i)
3584
- context.lineTo(point.x, point.y);
3585
- else
3586
- context.moveTo(point.x, point.y);
3607
+ context.lineTo(point.x, point.y);
3587
3608
  }
3588
- if (wrap)
3589
- context.closePath();
3609
+ wrap && context.closePath();
3590
3610
  context.stroke();
3591
3611
  }, screenSpace, context);
3592
3612
  }
@@ -3596,8 +3616,8 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3596
3616
  * @param {Vector2} posA
3597
3617
  * @param {Vector2} posB
3598
3618
  * @param {number} [width]
3599
- * @param {Color} [color=(1,1,1,1)]
3600
- * @param {Vector2} [pos=(0,0)] - Offset to apply
3619
+ * @param {Color} [color=WHITE]
3620
+ * @param {Vector2} [pos=vec2()] - Offset to apply
3601
3621
  * @param {number} [angle] - Angle to rotate by
3602
3622
  * @param {boolean} [useWebGL=glEnable]
3603
3623
  * @param {boolean} [screenSpace]
@@ -3616,12 +3636,12 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3616
3636
 
3617
3637
  /** Draw colored regular polygon using passed in number of sides
3618
3638
  * @param {Vector2} pos
3619
- * @param {Vector2} [size=(1,1)]
3639
+ * @param {Vector2} [size=vec2(1)]
3620
3640
  * @param {number} [sides]
3621
- * @param {Color} [color=(1,1,1,1)]
3641
+ * @param {Color} [color=WHITE]
3622
3642
  * @param {number} [angle]
3623
3643
  * @param {number} [lineWidth]
3624
- * @param {Color} [lineColor=(0,0,0,1)]
3644
+ * @param {Color} [lineColor=BLACK]
3625
3645
  * @param {boolean} [useWebGL=glEnable]
3626
3646
  * @param {boolean} [screenSpace]
3627
3647
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -3644,10 +3664,10 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3644
3664
 
3645
3665
  /** Draw colored polygon using passed in points
3646
3666
  * @param {Array<Vector2>} points - Array of Vector2 points
3647
- * @param {Color} [color=(1,1,1,1)]
3667
+ * @param {Color} [color=WHITE]
3648
3668
  * @param {number} [lineWidth]
3649
- * @param {Color} [lineColor=(0,0,0,1)]
3650
- * @param {Vector2} [pos=(0,0)] - Offset to apply
3669
+ * @param {Color} [lineColor=BLACK]
3670
+ * @param {Vector2} [pos=vec2()] - Offset to apply
3651
3671
  * @param {number} [angle] - Angle to rotate by
3652
3672
  * @param {boolean} [useWebGL=glEnable]
3653
3673
  * @param {boolean} [screenSpace]
@@ -3694,11 +3714,11 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3694
3714
 
3695
3715
  /** Draw colored ellipse using passed in point
3696
3716
  * @param {Vector2} pos
3697
- * @param {Vector2} [size=(1,1)] - Width and height diameter
3698
- * @param {Color} [color=(1,1,1,1)]
3717
+ * @param {Vector2} [size=vec2(1)] - Width and height diameter
3718
+ * @param {Color} [color=WHITE]
3699
3719
  * @param {number} [angle]
3700
3720
  * @param {number} [lineWidth]
3701
- * @param {Color} [lineColor=(0,0,0,1)]
3721
+ * @param {Color} [lineColor=BLACK]
3702
3722
  * @param {boolean} [useWebGL=glEnable]
3703
3723
  * @param {boolean} [screenSpace]
3704
3724
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -3740,9 +3760,9 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
3740
3760
  /** Draw colored circle using passed in point
3741
3761
  * @param {Vector2} pos
3742
3762
  * @param {number} [size=1] - Diameter
3743
- * @param {Color} [color=(1,1,1,1)]
3763
+ * @param {Color} [color=WHITE]
3744
3764
  * @param {number} [lineWidth=0]
3745
- * @param {Color} [lineColor=(0,0,0,1)]
3765
+ * @param {Color} [lineColor=BLACK]
3746
3766
  * @param {boolean} [useWebGL=glEnable]
3747
3767
  * @param {boolean} [screenSpace]
3748
3768
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -3797,9 +3817,9 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3797
3817
  * @param {string|number} text
3798
3818
  * @param {Vector2} pos
3799
3819
  * @param {number} [size]
3800
- * @param {Color} [color=(1,1,1,1)]
3820
+ * @param {Color} [color=WHITE]
3801
3821
  * @param {number} [lineWidth]
3802
- * @param {Color} [lineColor=(0,0,0,1)]
3822
+ * @param {Color} [lineColor=BLACK]
3803
3823
  * @param {CanvasTextAlign} [textAlign='center']
3804
3824
  * @param {string} [font=fontDefault]
3805
3825
  * @param {string} [fontStyle]
@@ -3823,10 +3843,10 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3823
3843
  * Automatically splits new lines into rows
3824
3844
  * @param {string|number} text
3825
3845
  * @param {Vector2} pos
3826
- * @param {number} [size]
3827
- * @param {Color} [color=(1,1,1,1)]
3846
+ * @param {number} size
3847
+ * @param {Color} [color=WHITE]
3828
3848
  * @param {number} [lineWidth]
3829
- * @param {Color} [lineColor=(0,0,0,1)]
3849
+ * @param {Color} [lineColor=BLACK]
3830
3850
  * @param {CanvasTextAlign} [textAlign]
3831
3851
  * @param {string} [font=fontDefault]
3832
3852
  * @param {string} [fontStyle]
@@ -3834,7 +3854,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3834
3854
  * @param {number} [angle]
3835
3855
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3836
3856
  * @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)
3857
+ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
3838
3858
  {
3839
3859
  ASSERT(isString(text), 'text must be a string');
3840
3860
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3958,7 +3978,7 @@ function worldToScreenDelta(worldDelta)
3958
3978
 
3959
3979
  /** Convert screen space transform to world space
3960
3980
  * @param {Vector2} screenPos
3961
- * @param {Vector2} screenSize
3981
+ * @param {Vector2} screenSize
3962
3982
  * @param {number} [screenAngle]
3963
3983
  * @return {[Vector2, Vector2, number]} - [pos, size, angle]
3964
3984
  * @memberof Draw */
@@ -4019,10 +4039,9 @@ function isOnScreen(pos, size=0)
4019
4039
  * @param {boolean} [additive]
4020
4040
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4021
4041
  * @memberof Draw */
4022
- function setBlendMode(additive=false, context)
4042
+ function setBlendMode(additive=false, context=drawContext)
4023
4043
  {
4024
4044
  glAdditive = additive;
4025
- context ||= drawContext;
4026
4045
  context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
4027
4046
  }
4028
4047
 
@@ -4037,7 +4056,6 @@ function combineCanvases()
4037
4056
  workContext.fillRect(0,0,w,h); // remove background alpha
4038
4057
  glCopyToContext(workContext);
4039
4058
  workContext.drawImage(mainCanvas, 0, 0);
4040
- mainCanvas.width |= 0;
4041
4059
  mainContext.drawImage(workCanvas, 0, 0);
4042
4060
  }
4043
4061
 
@@ -4142,17 +4160,22 @@ function setCursor(cursorStyle = 'auto')
4142
4160
 
4143
4161
  ///////////////////////////////////////////////////////////////////////////////
4144
4162
 
4163
+ /** Engine font image, 8x8 font provided by the engine
4164
+ * @type {FontImage}
4165
+ * @memberof Draw */
4145
4166
  let engineFontImage;
4146
4167
 
4147
4168
  /**
4148
- * Font Image Object - Draw text on a 2D canvas by using characters in an image
4169
+ * Font Image Object - Draw text by using tiles in an image
4149
4170
  * - 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
4171
+ * - A 8x8 default engine font is supplied for general use
4172
+ * - This system is WebGL enabled for fast text rendering
4173
+ * - Fonts can also be colored and scaled along each axis
4174
+ *
4152
4175
  * @memberof Draw
4153
4176
  * @example
4154
4177
  * // use built in font
4155
- * const font = new FontImage;
4178
+ * const font = engineFontImage;
4156
4179
  *
4157
4180
  * // draw text
4158
4181
  * font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
@@ -4160,70 +4183,112 @@ let engineFontImage;
4160
4183
  class FontImage
4161
4184
  {
4162
4185
  /** 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
4186
+ * @param {TileInfo} tileInfo - Tile info of first characeter in font
4166
4187
  */
4167
- constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
4188
+ constructor(tileInfo)
4168
4189
  {
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;
4190
+ ASSERT(!!tileInfo, 'tileInfo is required for FontImage');
4191
+
4192
+ /** @property {TileInfo} - Tile info for the font */
4193
+ this.tileInfo = tileInfo.frame(0);
4179
4194
  }
4180
4195
 
4181
4196
  /** Draw text in world space using the image font
4182
- * @param {string|number} text
4197
+ * @param {string|number} text
4183
4198
  * @param {Vector2} pos
4184
- * @param {number} [scale=.25]
4185
- * @param {boolean} [center]
4186
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4199
+ * @param {Vector2|number} [size]
4200
+ * @param {boolean} [center=true]
4201
+ * @param {Color} [color=WHITE]
4202
+ * @param {boolean} [useWebGL=glEnable]
4203
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4187
4204
  */
4188
- drawText(text, pos, scale=1, center, context=drawContext)
4205
+ drawText(text, pos, size=1, center, color, useWebGL, context)
4189
4206
  {
4190
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
4207
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
4208
+
4209
+ if (typeof size === 'number')
4210
+ {
4211
+ // if size is a number, make it a vector
4212
+ ASSERT(size > 0);
4213
+ size *= cameraScale;
4214
+ size = new Vector2(size, size);
4215
+ }
4216
+ else
4217
+ size = size.scale(cameraScale);
4218
+ this.drawTextScreen(text, worldToScreen(pos), size, center, color, useWebGL, context);
4191
4219
  }
4192
4220
 
4193
4221
  /** Draw text in screen space using the image font
4194
- * @param {string|number} text
4222
+ * @param {string|number} text
4195
4223
  * @param {Vector2} pos
4196
- * @param {number} [scale]
4224
+ * @param {Vector2|number} size
4197
4225
  * @param {boolean} [center]
4198
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4226
+ * @param {Color} [color=WHITE]
4227
+ * @param {boolean} [useWebGL=glEnable]
4228
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4199
4229
  */
4200
- drawTextScreen(text, pos, scale=4, center=true, context=drawContext)
4230
+ drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
4201
4231
  {
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)=>
4232
+ ASSERT(isString(text), 'text must be a string');
4233
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4234
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
4235
+ ASSERT(isColor(color), 'color must be a color');
4236
+
4237
+ // if size is a number, make it a vector
4238
+ size = typeof size === 'number' ? new Vector2(size, size) : size;
4239
+
4240
+ // precache objects for drawing
4241
+ const drawPos = new Vector2;
4242
+ const tileInfo = this.tileInfo;
4243
+ const padding = tileInfo.padding;
4244
+ const sizePaddedX = tileInfo.size.x + padding*2;
4245
+ const sizePaddedY = tileInfo.size.y + padding*2;
4246
+ const cols = tileInfo.textureInfo.size.x / sizePaddedX |0;
4247
+
4248
+ // draw each line of text
4249
+ (text+'').split('\n').forEach((line, j)=>
4207
4250
  {
4208
- const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
4209
- for (let j=line.length; j--;)
4251
+ const centerOffset = center ? (line.length-1) * size.x / 2 : 0;
4252
+ for (let i=line.length; i--;)
4210
4253
  {
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);
4254
+ // get the character index
4255
+ const charCode = line.charCodeAt(i);
4256
+ const index = charCode < 32 || charCode > 127 ?
4257
+ 95 : charCode - 32; // handle out of range characters
4258
+
4259
+ // get the position of the tile
4260
+ const x = index % cols;
4261
+ const y = index / cols |0;
4262
+ tileInfo.pos.x = x*sizePaddedX + padding;
4263
+ tileInfo.pos.y = y*sizePaddedY + padding;
4264
+
4265
+ // draw the tile
4266
+ drawPos.x = pos.x + i * size.x - centerOffset |0;
4267
+ drawPos.y = pos.y + j * size.y |0;
4268
+ drawTile(drawPos, size, tileInfo, color, 0, false, undefined, useWebGL, true, context);
4223
4269
  }
4224
4270
  });
4225
- context.restore();
4226
4271
  }
4272
+ }
4273
+
4274
+ // load engine font, called automatically on startup
4275
+ function fontImageInit()
4276
+ {
4277
+ return new Promise(resolve =>
4278
+ {
4279
+ // create the engine font
4280
+ const image = new Image;
4281
+ image.onerror = image.onload = ()=>
4282
+ {
4283
+ const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
4284
+ const textureInfo = new TextureInfo(image);
4285
+ const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
4286
+ engineFontImage = new FontImage(tileInfo);
4287
+ resolve();
4288
+ }
4289
+ image.crossOrigin = 'anonymous';
4290
+ 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==';
4291
+ });
4227
4292
  }
4228
4293
  /**
4229
4294
  * LittleJS Input System
@@ -4280,6 +4345,10 @@ let inputPreventDefault = true;
4280
4345
  * @memberof Input */
4281
4346
  let gamepadPrimary = 0;
4282
4347
 
4348
+ /** True if a touch device has been detected
4349
+ * @memberof Input */
4350
+ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4351
+
4283
4352
  /** Prevents input continuing to the default browser handling
4284
4353
  * This is useful to disable for html menus so the browser can handle input normally
4285
4354
  * @param {boolean} preventDefault
@@ -4478,10 +4547,6 @@ function gamepadStickCount(gamepad=gamepadPrimary)
4478
4547
  return gamepadStickData[gamepad]?.length ?? 0;
4479
4548
  }
4480
4549
 
4481
- /** True if a touch device has been detected
4482
- * @memberof Input */
4483
- const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4484
-
4485
4550
  ///////////////////////////////////////////////////////////////////////////////
4486
4551
 
4487
4552
  /** Pulse the vibration hardware if it exists
@@ -4490,7 +4555,7 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4490
4555
  function vibrate(pattern=100)
4491
4556
  {
4492
4557
  ASSERT(isNumber(pattern) || isArray(pattern), 'pattern must be a number or array');
4493
- vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern);
4558
+ vibrateEnable && !headlessMode && navigator?.vibrate?.(pattern);
4494
4559
  }
4495
4560
 
4496
4561
  /** Cancel any ongoing vibration
@@ -4608,8 +4673,8 @@ function inputInit()
4608
4673
  }
4609
4674
  function onMouseUp(e)
4610
4675
  {
4611
- if (isTouchDevice && touchInputEnable)
4612
- return;
4676
+ if (isTouchDevice && touchInputEnable) return;
4677
+
4613
4678
  inputData[0][e.button] = (inputData[0][e.button]&2) | 4;
4614
4679
  }
4615
4680
  function onMouseMove(e)
@@ -4795,8 +4860,7 @@ function inputUpdate()
4795
4860
  // update touch gamepad if enabled
4796
4861
  if (touchGamepadEnable && isTouchDevice)
4797
4862
  {
4798
- if (!touchGamepadTimer.isSet())
4799
- return;
4863
+ if (!touchGamepadTimer.isSet()) return;
4800
4864
 
4801
4865
  // read virtual analog stick
4802
4866
  gamepadPrimary = 0; // touch gamepad uses index 0
@@ -4834,12 +4898,10 @@ function inputUpdate()
4834
4898
  }
4835
4899
 
4836
4900
  // return if gamepads are disabled or not supported
4837
- if (!gamepadsEnable || !navigator || !navigator.getGamepads)
4838
- return;
4901
+ if (!gamepadsEnable || !navigator || !navigator.getGamepads) return;
4839
4902
 
4840
4903
  // only poll gamepads when focused or in debug mode
4841
- if (!debug && !document.hasFocus())
4842
- return;
4904
+ if (!debug && !document.hasFocus()) return;
4843
4905
 
4844
4906
  // poll gamepads
4845
4907
  const maxGamepads = 8;
@@ -4876,8 +4938,7 @@ function inputUpdate()
4876
4938
  data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
4877
4939
 
4878
4940
  // check for any input on this gamepad, analog must be full press
4879
- if (button.pressed)
4880
- if (!button.value || button.value > .9)
4941
+ if (button.pressed && (!button.value || button.value > .9))
4881
4942
  hadInput = true;
4882
4943
  }
4883
4944
 
@@ -4906,7 +4967,7 @@ function inputUpdate()
4906
4967
  }
4907
4968
 
4908
4969
  // copy dpad to left analog stick when pressed
4909
- if (gamepadDirectionEmulateStick && !dpad.isZero())
4970
+ if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
4910
4971
  sticks[0] = dpad.clampLength();
4911
4972
  }
4912
4973
 
@@ -4935,13 +4996,11 @@ function inputRender()
4935
4996
  function touchGamepadRender()
4936
4997
  {
4937
4998
  if (!touchInputEnable || !isTouchDevice || headlessMode) return;
4938
- if (!touchGamepadEnable || !touchGamepadTimer.isSet())
4939
- return;
4999
+ if (!touchGamepadEnable || !touchGamepadTimer.isSet()) return;
4940
5000
 
4941
5001
  // fade off when not touching or paused
4942
5002
  const alpha = percent(touchGamepadTimer.get(), 4, 3);
4943
- if (!alpha || paused)
4944
- return;
5003
+ if (!alpha || paused) return;
4945
5004
 
4946
5005
  // setup the canvas
4947
5006
  const context = mainContext;
@@ -5094,8 +5153,7 @@ class Sound
5094
5153
  {
5095
5154
  // remove randomness so it can be applied on playback
5096
5155
  const randomnessIndex = 1, defaultRandomness = .05;
5097
- this.randomness = zzfxSound[randomnessIndex] !== undefined ?
5098
- zzfxSound[randomnessIndex] : defaultRandomness;
5156
+ this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
5099
5157
  zzfxSound[randomnessIndex] = 0;
5100
5158
 
5101
5159
  // generate the zzfx samples
@@ -5175,7 +5233,7 @@ class Sound
5175
5233
  * @return {number} - How long the sound is in seconds (undefined if loading)
5176
5234
  */
5177
5235
  getDuration()
5178
- { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
5236
+ { return this.sampleChannels?.[0].length / this.sampleRate || 0; }
5179
5237
 
5180
5238
  /** Check if sound is loaded, for sounds fetched from a url
5181
5239
  * @return {boolean} - True if sound is loaded and ready to play
@@ -5267,8 +5325,7 @@ class SoundWave extends Sound
5267
5325
  this.sampleRate = audioBuffer.sampleRate;
5268
5326
  this.sampleChannels = sampleChannels;
5269
5327
  this.loadedPercent = 1;
5270
- if (this.onloadCallback)
5271
- this.onloadCallback(this);
5328
+ this.onloadCallback?.(this);
5272
5329
  }
5273
5330
  }
5274
5331
 
@@ -5471,7 +5528,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
5471
5528
 
5472
5529
  /** Stop all queued speech
5473
5530
  * @memberof Audio */
5474
- function speakStop() {speechSynthesis && speechSynthesis.cancel();}
5531
+ function speakStop() {speechSynthesis?.cancel();}
5475
5532
 
5476
5533
  /** Get frequency of a note on a musical scale
5477
5534
  * @param {number} semitoneOffset - How many semitones away from the root note
@@ -5730,7 +5787,7 @@ function tileCollisionGetData(pos)
5730
5787
 
5731
5788
  /** Check if a tile layer collides with another object
5732
5789
  * @param {Vector2} pos
5733
- * @param {Vector2} [size=(0,0)]
5790
+ * @param {Vector2} [size=vec2()]
5734
5791
  * @param {EngineObject} [object] - An object or undefined for generic test
5735
5792
  * @param {boolean} [solidOnly] - Only check solid layers if true
5736
5793
  * @return {TileCollisionLayer}
@@ -5848,20 +5905,20 @@ function tileLayersLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLa
5848
5905
  class TileLayerData
5849
5906
  {
5850
5907
  /** Create a tile layer data object, one for each tile in a TileLayer
5851
- * @param {number} [tile] - The tile to use, untextured if undefined
5908
+ * @param {number} [tile] - The tile to use, untextured if undefined
5852
5909
  * @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 */
5910
+ * @param {boolean} [mirror] - If the tile should be mirrored along the x axis
5911
+ * @param {Color} [color] - Color of the tile */
5855
5912
  constructor(tile, direction=0, mirror=false, color=new Color)
5856
5913
  {
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 */
5914
+ /** @property {number} - The tile to use, untextured if undefined */
5915
+ this.tile = tile;
5916
+ /** @property {number} - Integer direction of tile, in 90 degree increments */
5860
5917
  this.direction = direction;
5861
5918
  /** @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();
5919
+ this.mirror = mirror;
5920
+ /** @property {Color} - Color of the tile */
5921
+ this.color = color.copy();
5865
5922
  }
5866
5923
 
5867
5924
  /** Set this tile to clear, it will not be rendered */
@@ -5872,7 +5929,7 @@ class TileLayerData
5872
5929
  /**
5873
5930
  * Canvas Layer - cached off screen rendering system
5874
5931
  * - Contains an offscreen canvas that can be rendered to
5875
- * - WebGL rendering is optional, call useWebGL to enable
5932
+ * - WebGL rendering is optional, call updateWebGL to enable/update
5876
5933
  * @extends EngineObject
5877
5934
  * @memberof TileLayers
5878
5935
  * @example
@@ -5886,8 +5943,9 @@ class CanvasLayer extends EngineObject
5886
5943
  * @param {number} [angle] - Angle the layer is rotated by
5887
5944
  * @param {number} [renderOrder] - Objects sorted by renderOrder
5888
5945
  * @param {Vector2} [canvasSize] - Default size of canvas, can be changed later
5946
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
5889
5947
  */
5890
- constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512))
5948
+ constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=glEnable)
5891
5949
  {
5892
5950
  ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
5893
5951
  super(position, size, undefined, angle, WHITE, renderOrder);
@@ -5897,13 +5955,10 @@ class CanvasLayer extends EngineObject
5897
5955
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
5898
5956
  this.context = this.canvas?.getContext('2d');
5899
5957
  /** @property {TextureInfo} - Texture info to use for this object rendering */
5900
- const useWebGL = false; // do not use webgl by default
5901
5958
  this.textureInfo = new TextureInfo(this.canvas, useWebGL);
5902
- /** @property {boolean} - True if WebGL texture needs to be refreshed */
5903
- this.refreshWebGL = false;
5904
5959
 
5905
5960
  // disable physics by default
5906
- this.mass = this.gravityScale = this.friction = this.restitution = 0;
5961
+ this.mass = 0;
5907
5962
  }
5908
5963
 
5909
5964
  /** Destroy this canvas layer */
@@ -5918,7 +5973,7 @@ class CanvasLayer extends EngineObject
5918
5973
  // Render the layer, called automatically by the engine
5919
5974
  render()
5920
5975
  {
5921
- this.draw(this.pos, this.size, this.angle, this.color, this.mirror, this.additiveColor);
5976
+ this.draw(this.pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
5922
5977
  }
5923
5978
 
5924
5979
  /** Draw this canvas layer centered in world space, with color applied if using WebGL
@@ -5931,103 +5986,54 @@ class CanvasLayer extends EngineObject
5931
5986
  * @param {boolean} [screenSpace] - If true the pos and size are in screen space
5932
5987
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
5933
5988
  * @memberof Draw */
5934
- draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5989
+ draw(pos, size, color=WHITE, angle=0, mirror=false, additiveColor, screenSpace=false, context)
5935
5990
  {
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
5991
  // draw the canvas layer as a single tile that uses the whole texture
5945
5992
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5993
+ const useWebGL = this.hasWebGL();
5946
5994
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5947
5995
  }
5948
5996
 
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)
5997
+ /** Draw a tile onto the layer canvas in world space
5956
5998
  * @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)
5999
+ * @param {Vector2} [size=vec2(1)]
6000
+ * @param {TileInfo} [tileInfo]
6001
+ * @param {Color} [color=WHITE]
6002
+ * @param {number} [angle]
6003
+ * @param {boolean} [mirror] */
6004
+ drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
5962
6005
  {
5963
- if (!this.context) return;
5964
-
5965
- const context = this.context;
5966
- context.save();
5967
6006
  pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
5968
6007
  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
- }
6008
+ pos.y = this.canvas.height - pos.y;
5975
6009
 
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
- });
6010
+ // draw the tile onto the layer canvas
6011
+ const oldMainCanvasSize = mainCanvasSize;
6012
+ mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
6013
+ const useWebGL = this.hasWebGL();
6014
+ useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
6015
+ const drawContext = useWebGL ? undefined : this.context;
6016
+ drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
6017
+ useWebGL && glSetRenderTarget();
6018
+ mainCanvasSize = oldMainCanvasSize;
6003
6019
  }
6004
6020
 
6005
6021
  /** Draw a rectangle onto the layer canvas in world space
6006
6022
  * @param {Vector2} pos
6007
- * @param {Vector2} [size=(1,1)]
6008
- * @param {Color} [color=(1,1,1,1)]
6009
- * @param {number} [angle=0] */
6023
+ * @param {Vector2} [size=vec2(1)]
6024
+ * @param {Color} [color=WHITE]
6025
+ * @param {number} [angle] */
6010
6026
  drawRect(pos, size, color, angle)
6011
6027
  { this.drawTile(pos, size, undefined, color, angle); }
6012
6028
 
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
- }
6029
+ /** Create WebGL texture if necessary and copy layer canvas to it */
6030
+ updateWebGL()
6031
+ { this.textureInfo.createWebGLTexture(); }
6025
6032
 
6026
- if (enable)
6027
- this.textureInfo.createWebGLTexture();
6028
- else
6029
- this.textureInfo.destroyWebGLTexture();
6030
- }
6033
+ /** Check if this layer is using WebGL
6034
+ * @return {boolean} */
6035
+ hasWebGL()
6036
+ { return glEnable && this.textureInfo.hasWebGL(); }
6031
6037
  }
6032
6038
 
6033
6039
  ///////////////////////////////////////////////////////////////////////////////
@@ -6046,50 +6052,67 @@ class CanvasLayer extends EngineObject
6046
6052
  class TileLayer extends CanvasLayer
6047
6053
  {
6048
6054
  /** 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)
6055
+ * @param {Vector2} position - World space position
6056
+ * @param {Vector2} size - World space size
6057
+ * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6052
6058
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6053
6059
  */
6054
6060
  constructor(position, size, tileInfo=tile(), renderOrder=0)
6055
6061
  {
6056
6062
  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
6063
+ const useWebGL = true;
6064
+ super(position, size, 0, renderOrder, canvasSize, useWebGL);
6065
+
6066
+ /** @property {TileInfo} - Default tile info for layer */
6067
+ this.tileInfo = undefined;
6068
+ /** @property {Array<TileLayerData>} - Default tile info for layer */
6063
6069
  this.data = [];
6064
- for (let j = this.size.area(); j--;)
6065
- this.data.push(new TileLayerData);
6070
+ /** @property {boolean} - Is this layer using a webgl texture? */
6071
+ this.isUsingWebGL = false;
6066
6072
 
6067
6073
  if (headlessMode)
6068
6074
  {
6069
- // disable rendering
6070
- this.redraw = () => {};
6071
- this.render = () => {};
6072
- this.redrawStart = () => {};
6073
- this.redrawEnd = () => {};
6074
- this.drawTileData = () => {};
6075
- this.drawCanvas2D = () => {};
6076
- this.useWebGL = () => {};
6075
+ // disable rendering in headless mode
6076
+ this.render = () => {};
6077
+ this.redraw = () => {};
6078
+ this.redrawStart = () => {};
6079
+ this.redrawEnd = () => {};
6080
+ this.drawTileData = () => {};
6081
+ this.redrawTileData = () => {};
6082
+ this.drawLayerTile = () => {};
6083
+ this.drawLayerRect = () => {};
6084
+ this.clearLayerRect = () => {};
6085
+ return;
6077
6086
  }
6087
+
6088
+ if (tileInfo)
6089
+ {
6090
+ // set tile info
6091
+ this.tileInfo = tileInfo.frame(0);
6092
+ this.tileInfo.bleed = 0; // disable bleed for tile layers
6093
+ }
6094
+
6095
+ // init tile data
6096
+ for (let j = this.size.area(); j--;)
6097
+ this.data.push(new TileLayerData);
6078
6098
  }
6079
6099
 
6080
6100
  /** Set data at a given position in the array
6081
6101
  * @param {Vector2} layerPos - Local position in array
6082
- * @param {TileLayerData} data - Data to set
6102
+ * @param {TileLayerData} data - Data to set
6083
6103
  * @param {boolean} [redraw] - Force the tile to redraw if true */
6084
6104
  setData(layerPos, data, redraw=false)
6085
6105
  {
6106
+ layerPos = layerPos.floor();
6086
6107
  ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6087
6108
  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
- }
6109
+
6110
+ if (!layerPos.arrayCheck(this.size)) return;
6111
+ this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
6112
+
6113
+ if (!redraw) return;
6114
+ const isRedraw = drawContext === this.context;
6115
+ isRedraw ? this.drawTileData(layerPos) : this.redrawTileData(layerPos);
6093
6116
  }
6094
6117
 
6095
6118
  /** Get data at a given position in the array
@@ -6098,7 +6121,18 @@ class TileLayer extends CanvasLayer
6098
6121
  getData(layerPos)
6099
6122
  {
6100
6123
  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];
6124
+ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
6125
+ }
6126
+
6127
+ // Update the tile layer, refresh texture if needed
6128
+ update()
6129
+ {
6130
+ if (!glEnable && this.isUsingWebGL)
6131
+ {
6132
+ // redraw the layer if webgl was disabled or context lost
6133
+ this.isUsingWebGL = false;
6134
+ this.redraw();
6135
+ }
6102
6136
  }
6103
6137
 
6104
6138
  // Render the tile layer, called automatically by the engine
@@ -6106,40 +6140,35 @@ class TileLayer extends CanvasLayer
6106
6140
  {
6107
6141
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
6108
6142
 
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
6143
  const size = this.drawSize || this.size;
6119
6144
  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);
6145
+ this.draw(pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
6122
6146
  }
6123
6147
 
6148
+ /** Called after this layer is redrawn, does nothing by default */
6149
+ onRedraw() {}
6150
+
6124
6151
  /** Draw all the tile data to an offscreen canvas
6125
- * - This may be slow in some browsers but only needs to be done once */
6152
+ * - This may be slow if not using webgl but only needs to be done once */
6126
6153
  redraw()
6127
6154
  {
6128
6155
  this.redrawStart(true);
6129
6156
  for (let x = this.size.x; x--;)
6130
6157
  for (let y = this.size.y; y--;)
6131
6158
  this.drawTileData(vec2(x,y), false);
6159
+ this.hasWebGL() && glFlush();
6160
+ this.onRedraw();
6132
6161
  this.redrawEnd();
6133
- this.useWebGL();
6134
6162
  }
6135
6163
 
6136
6164
  /** Call to start the redraw process
6137
- * - This can be used to manually update small parts of the level
6165
+ * - This can be used to manually update parts of the level
6138
6166
  * @param {boolean} [clear] - Should it clear the canvas before drawing */
6139
6167
  redrawStart(clear=false)
6140
6168
  {
6141
6169
  if (!this.context) return;
6142
-
6170
+ ASSERT(drawContext !== this.context);
6171
+
6143
6172
  // save current render settings
6144
6173
  /** @type {[CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6145
6174
  this.savedRenderSettings = [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
@@ -6147,33 +6176,37 @@ class TileLayer extends CanvasLayer
6147
6176
  // set the draw canvas and context to this layer
6148
6177
  // use camera settings to match this layer's canvas
6149
6178
  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;
6179
+ const tileSize = this.tileInfo?.size ?? vec2(1);
6154
6180
  mainCanvasSize = this.size.multiply(tileSize);
6155
- if (clear)
6181
+ canvasClearColor = CLEAR_BLACK;
6182
+ cameraPos = this.size.multiply(tileSize).scale(.5);
6183
+ cameraScale = 1;
6184
+
6185
+ // set render target to this layer
6186
+ this.isUsingWebGL = this.hasWebGL();
6187
+ if (this.isUsingWebGL)
6188
+ glSetRenderTarget(this.textureInfo.glTexture, clear);
6189
+ else
6156
6190
  {
6157
- // clear and set size
6158
- this.canvas.width = mainCanvasSize.x;
6159
- this.canvas.height = mainCanvasSize.y;
6191
+ // disable smoothing for pixel art
6192
+ this.context.imageSmoothingEnabled = !tilesPixelated;
6193
+ if (clear)
6194
+ {
6195
+ // clear and set size
6196
+ this.canvas.width = mainCanvasSize.x;
6197
+ this.canvas.height = mainCanvasSize.y;
6198
+ }
6160
6199
  }
6161
-
6162
- // disable smoothing for pixel art
6163
- drawContext.imageSmoothingEnabled = !tilesPixelated;
6164
-
6165
- // setup gl rendering if enabled
6166
- glPreRender();
6167
6200
  }
6168
6201
 
6169
6202
  /** Call to end the redraw process */
6170
6203
  redrawEnd()
6171
6204
  {
6172
6205
  if (!this.context) return;
6206
+ ASSERT(drawContext === this.context);
6173
6207
 
6174
- ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6175
- glCopyToContext(drawContext);
6176
- //saveCanvas(this.canvas);
6208
+ if (glEnable && this.textureInfo.glTexture)
6209
+ glSetRenderTarget();
6177
6210
 
6178
6211
  // set stuff back to normal
6179
6212
  [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
@@ -6188,24 +6221,75 @@ class TileLayer extends CanvasLayer
6188
6221
  drawTileData(layerPos, clear=true)
6189
6222
  {
6190
6223
  if (!this.context) return;
6224
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6191
6225
 
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
- }
6226
+ // clear out where the tile was, can be skipped for fully opaque tiles
6227
+ const drawSize = this.tileInfo?.size ?? vec2(1);
6228
+ const drawPos = layerPos.multiply(drawSize);
6229
+ clear && this.clearLayerRect(drawPos, drawSize);
6199
6230
 
6200
6231
  // draw the tile if it has layer data
6201
6232
  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
- }
6233
+ if (!d.tile) return;
6234
+
6235
+ const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
6236
+ this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
6237
+ }
6238
+
6239
+ /** Draw the tile at a given position in the tile grid
6240
+ * This can be used to clear tiles when they are destroyed
6241
+ * For better performance use drawTileData inside a redrawStart/End block
6242
+ * @param {Vector2} layerPos
6243
+ * @param {boolean} [clear] - should the old tile be cleared
6244
+ */
6245
+ redrawTileData(layerPos, clear=true)
6246
+ {
6247
+ if (!this.context) return;
6248
+ ASSERT(drawContext !== this.context, 'redrawStart() should not be active when calling redrawTileData(), instead use drawTileData()');
6249
+
6250
+ this.redrawStart();
6251
+ this.drawTileData(layerPos, clear);
6252
+ this.redrawEnd();
6253
+ }
6254
+
6255
+ /** Draw textured tile in layer space
6256
+ * @param {Vector2} pos - Position in pixel coordinates
6257
+ * @param {Vector2} [size=vec2(1)] - Size of the tile
6258
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
6259
+ * @param {Color} [color=WHITE] - Color to modulate with
6260
+ * @param {number} [angle] - Angle to rotate by
6261
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
6262
+ * @param {Color} [additiveColor] - Additive color to be applied if any */
6263
+ drawLayerTile(pos, size=vec2(1), tileInfo, color=WHITE,
6264
+ angle=0, mirror, additiveColor)
6265
+ {
6266
+ const drawPos = pos.add(size.scale(.5));
6267
+ drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor);
6268
+ }
6269
+
6270
+ /** Clear a rectangle in layer space
6271
+ * @param {Vector2} pos
6272
+ * @param {Vector2} size
6273
+ * @param {Color} [color=WHITE] - Color to modulate with
6274
+ * @param {number} [angle] - Angle to rotate by
6275
+ */
6276
+ drawLayerRect(pos, size, color, angle=0)
6277
+ { this.drawLayerTile(pos, size, undefined, color, angle); }
6278
+
6279
+ /** Clear a rectangle in layer space
6280
+ * @param {Vector2} pos - position in pixel coordinates
6281
+ * @param {Vector2} size
6282
+ */
6283
+ clearLayerRect(pos, size)
6284
+ {
6285
+ ASSERT(drawContext === this.context, 'must call redrawStart() before clearing tiles');
6286
+
6287
+ const x = pos.x, y = this.canvas.height - pos.y - size.y;
6288
+ const useWebGL = this.hasWebGL();
6289
+ if (useWebGL)
6290
+ glClearRect(x, y, size.x, size.y);
6291
+ else
6292
+ this.context.clearRect(x, y, size.x, size.y);
6209
6293
  }
6210
6294
  }
6211
6295
 
@@ -6214,7 +6298,6 @@ class TileLayer extends CanvasLayer
6214
6298
  * Tile Collision Layer - a tile layer with collision
6215
6299
  * - adds collision data and functions to TileLayer
6216
6300
  * - there can be multiple tile collision layers
6217
- * - tile collision layers should not overlap each other
6218
6301
  * @extends TileLayer
6219
6302
  * @memberof TileLayers
6220
6303
  */
@@ -6286,7 +6369,7 @@ class TileCollisionLayer extends TileLayer
6286
6369
 
6287
6370
  /** Check if collision with another object should occur
6288
6371
  * @param {Vector2} pos
6289
- * @param {Vector2} [size=(0,0)]
6372
+ * @param {Vector2} [size=vec2()]
6290
6373
  * @param {EngineObject} [object]
6291
6374
  * @return {boolean} */
6292
6375
  collisionTest(pos, size=new Vector2, object)
@@ -6606,7 +6689,7 @@ class ParticleEmitter extends EngineObject
6606
6689
  particle.mirror = randBool();
6607
6690
 
6608
6691
  // call particle create callback
6609
- this.particleCreateCallback && this.particleCreateCallback(particle);
6692
+ this.particleCreateCallback?.(particle);
6610
6693
 
6611
6694
  // return the newly created particle
6612
6695
  return particle;
@@ -6691,7 +6774,7 @@ class Particle extends EngineObject
6691
6774
  const c = this.colorEnd;
6692
6775
  this.color.set(c.r, c.g, c.b, c.a);
6693
6776
  this.size.set(this.sizeEnd, this.sizeEnd);
6694
- this.destroyCallback && this.destroyCallback(this);
6777
+ this.destroyCallback?.(this);
6695
6778
  this.destroyed = 1;
6696
6779
  }
6697
6780
  }
@@ -6969,7 +7052,7 @@ let glContext;
6969
7052
  let glAntialias = true;
6970
7053
 
6971
7054
  // WebGL internal variables not exposed to documentation
6972
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glCanBeEnabled = true;
7055
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glCanBeEnabled = true;
6973
7056
 
6974
7057
  // WebGL internal constants
6975
7058
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -7045,7 +7128,7 @@ function glInit(rootElement)
7045
7128
  // setup instanced rendering shader program
7046
7129
  glShader = glCreateProgram(
7047
7130
  '#version 300 es\n' + // specify GLSL ES version
7048
- 'precision highp float;'+ // use highp for better accuracy
7131
+ 'precision highp float;'+ // use highp for accuracy
7049
7132
  'uniform mat4 m;'+ // transform matrix
7050
7133
  'in vec2 g;'+ // in: geometry
7051
7134
  'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
@@ -7060,7 +7143,7 @@ function glInit(rootElement)
7060
7143
  '}' // end of shader
7061
7144
  ,
7062
7145
  '#version 300 es\n' + // specify GLSL ES version
7063
- 'precision highp float;'+ // use highp for better accuracy
7146
+ 'precision highp float;'+ // use highp for accuracy
7064
7147
  'uniform sampler2D s;'+ // texture
7065
7148
  'in vec2 v;'+ // in: uv
7066
7149
  'in vec4 d,e;'+ // in: color, additiveColor
@@ -7098,45 +7181,62 @@ function glInit(rootElement)
7098
7181
  glColorData = new Uint32Array(glInstanceData);
7099
7182
  glArrayBuffer = glContext.createBuffer();
7100
7183
  glGeometryBuffer = glContext.createBuffer();
7184
+ glFramebuffer = glContext.createFramebuffer();
7185
+ glBatchCount = 0;
7101
7186
 
7102
7187
  // create the geometry buffer, triangle strip square
7103
- const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
7188
+ const geometry = new Float32Array([0,0,1,0,0,1,1,1]);
7104
7189
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7105
7190
  glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
7191
+
7192
+ let offset, shader, stride;
7193
+ const initVertexAttrib = (name, type, typeSize, size, divisor=0)=>
7194
+ {
7195
+ const location = glContext.getAttribLocation(shader, name);
7196
+ const normalize = typeSize === 1;
7197
+ const fixedStride = typeSize && stride;
7198
+ glContext.enableVertexAttribArray(location);
7199
+ glContext.vertexAttribPointer(location, size, type, normalize, fixedStride, offset);
7200
+ glContext.vertexAttribDivisor(location, divisor);
7201
+ offset += size*typeSize;
7202
+ }
7203
+
7204
+ // setup VAO for instanced rendering
7205
+ glInstancedVAO = glContext.createVertexArray();
7206
+ glContext.bindVertexArray(glInstancedVAO);
7207
+
7208
+ // configure instanced vertex attributes
7209
+ offset = 0, shader = glShader, stride = gl_INSTANCE_BYTE_STRIDE;
7210
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7211
+ initVertexAttrib('g', glContext.FLOAT, 0, 2); // geometry
7212
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7213
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7214
+ initVertexAttrib('p', glContext.FLOAT, 4, 4, 1); // position & size
7215
+ initVertexAttrib('u', glContext.FLOAT, 4, 4, 1); // texture coords
7216
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4, 1); // color
7217
+ initVertexAttrib('a', glContext.UNSIGNED_BYTE, 1, 4, 1); // additiveColor
7218
+ initVertexAttrib('r', glContext.FLOAT, 4, 1, 1); // rotation
7219
+
7220
+ // setup VAO for poly rendering
7221
+ glPolyVAO = glContext.createVertexArray();
7222
+ glContext.bindVertexArray(glPolyVAO);
7223
+
7224
+ // configure poly vertex attributes
7225
+ offset = 0, shader = glPolyShader, stride = gl_POLY_VERTEX_BYTE_STRIDE;
7226
+ initVertexAttrib('p', glContext.FLOAT, 4, 2); // position
7227
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7106
7228
  }
7107
7229
  }
7108
7230
 
7109
- function glSetInstancedMode()
7231
+ function glSetInstancedMode(force=false)
7110
7232
  {
7111
- if (!glPolyMode) return;
7233
+ if (!force && !glPolyMode) return;
7112
7234
 
7113
7235
  // setup instanced mode
7114
7236
  glFlush();
7115
7237
  glPolyMode = false;
7116
7238
  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
7239
+ glContext.bindVertexArray(glInstancedVAO);
7140
7240
  }
7141
7241
 
7142
7242
  function glSetPolyMode()
@@ -7147,36 +7247,30 @@ function glSetPolyMode()
7147
7247
  glFlush();
7148
7248
  glPolyMode = true;
7149
7249
  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
7250
+ glContext.bindVertexArray(glPolyVAO);
7167
7251
  }
7168
7252
 
7169
7253
  // Setup WebGL render each frame, called automatically by engine
7170
7254
  // Also used by tile layer rendering when redrawing tiles
7171
- function glPreRender()
7255
+ function glPreRender(clear=true)
7172
7256
  {
7173
7257
  if (!glEnable || !glContext) return;
7174
7258
 
7175
- // clear the canvas
7176
- glClearCanvas();
7259
+ ASSERT(!glBatchCount, 'glPreRender called with unflushed batch.');
7260
+
7261
+ if (!glRenderTarget)
7262
+ {
7263
+ // set to same size as main canvas
7264
+ glCanvas.width = mainCanvasSize.x;
7265
+ glCanvas.height = mainCanvasSize.y;
7266
+ }
7267
+ glContext.viewport(0, 0, mainCanvasSize.x, mainCanvasSize.y);
7268
+ clear && glClearCanvas();
7177
7269
 
7178
7270
  // build the transform matrix
7179
7271
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
7272
+ if (glRenderTarget)
7273
+ s.y = -s.y; // invert y when using render target
7180
7274
  const rotatedCam = cameraPos.rotate(-cameraAngle);
7181
7275
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
7182
7276
  const ca = cos(cameraAngle);
@@ -7205,12 +7299,14 @@ function glPreRender()
7205
7299
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
7206
7300
  }
7207
7301
 
7302
+ // rebind the array buffer
7303
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7304
+
7208
7305
  // start with additive blending off
7209
7306
  glAdditive = glBatchAdditive = false;
7210
7307
 
7211
- // force it to set instanced mode by first setting poly mode true
7212
- glPolyMode = true;
7213
- glSetInstancedMode();
7308
+ // force it to set instanced mode
7309
+ glSetInstancedMode(true);
7214
7310
  }
7215
7311
 
7216
7312
  /** Clear the canvas and setup the viewport
@@ -7219,13 +7315,9 @@ function glClearCanvas()
7219
7315
  {
7220
7316
  if (!glContext) return;
7221
7317
 
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);
7318
+ // clear using the canvasClearColor
7226
7319
  const color = canvasClearColor;
7227
- if (color.a > 0)
7228
- glContext.clearColor(color.r, color.g, color.b, color.a);
7320
+ glContext.clearColor(color.r, color.g, color.b, color.a);
7229
7321
  glContext.clear(glContext.COLOR_BUFFER_BIT);
7230
7322
  }
7231
7323
 
@@ -7302,7 +7394,7 @@ function glCreateTexture(image)
7302
7394
  // build the texture
7303
7395
  const texture = glContext.createTexture();
7304
7396
  let mipMap = false;
7305
- if (image && image.width)
7397
+ if (image?.width)
7306
7398
  {
7307
7399
  glSetTextureData(texture, image);
7308
7400
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
@@ -7323,7 +7415,9 @@ function glCreateTexture(image)
7323
7415
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
7324
7416
  if (mipMap)
7325
7417
  glContext.generateMipmap(glContext.TEXTURE_2D);
7326
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
7418
+
7419
+ // rebind active texture
7420
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
7327
7421
  return texture;
7328
7422
  }
7329
7423
 
@@ -7346,10 +7440,12 @@ function glSetTextureData(texture, image)
7346
7440
  if (!glContext) return;
7347
7441
 
7348
7442
  // build the texture
7349
- ASSERT(!!image && image.width > 0, 'Invalid image data.');
7443
+ ASSERT(image?.width > 0, 'Invalid image data.');
7350
7444
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
7351
7445
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
7352
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
7446
+
7447
+ // rebind active texture
7448
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
7353
7449
  }
7354
7450
 
7355
7451
  /** Tells WebGL to create or update the glTexture and start tracking it
@@ -7568,6 +7664,48 @@ function glDrawColoredPoints(points, pointColors)
7568
7664
  glBatchCount += vertCount;
7569
7665
  }
7570
7666
 
7667
+ /** Set the WebGL render target to the given texture or back to the canvas
7668
+ * @param {WebGLTexture} [texture] - a texture or undefined to use normal glCanvas
7669
+ * @param {boolean} [clear] - should the render target be cleared
7670
+ * @memberof WebGL */
7671
+ function glSetRenderTarget(texture, clear=false)
7672
+ {
7673
+ if (texture)
7674
+ {
7675
+ glRenderTarget = texture;
7676
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
7677
+ glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
7678
+ glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
7679
+ glPreRender(clear);
7680
+ }
7681
+ else
7682
+ {
7683
+ glFlush();
7684
+ glRenderTarget = undefined;
7685
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
7686
+ }
7687
+ }
7688
+
7689
+ /** Clear out a rectangle area of the WebGL canvas or render target
7690
+ * @param {number} x
7691
+ * @param {number} y
7692
+ * @param {number} width
7693
+ * @param {number} height
7694
+ * @memberof WebGL */
7695
+ function glClearRect(x, y, width, height)
7696
+ {
7697
+ if (!glEnable) return;
7698
+
7699
+ // Enable scissor test to clear only the specified area
7700
+ glContext.enable(glContext.SCISSOR_TEST);
7701
+ glContext.scissor(x, y, width, height);
7702
+ glContext.clearColor(0, 0, 0, 0);
7703
+ glContext.clear(glContext.COLOR_BUFFER_BIT);
7704
+ glContext.disable(glContext.SCISSOR_TEST);
7705
+ }
7706
+
7707
+ ///////////////////////////////////////////////////////////////////////////////
7708
+
7571
7709
  // WebGL internal function to convert polygon to outline triangle strip
7572
7710
  function glMakeOutline(points, width, wrap=true)
7573
7711
  {
@@ -7701,23 +7839,20 @@ function glPolyStrip(points)
7701
7839
  const a = points[i0], b = points[i1], c = points[i2];
7702
7840
 
7703
7841
  // check if convex
7704
- if (cross(a, b, c) < e)
7705
- continue;
7842
+ if (cross(a, b, c) < e) continue;
7706
7843
 
7707
7844
  // check if any other point is inside
7708
7845
  let hasInside = false;
7709
7846
  for (let j = 0; j < indices.length; j++)
7710
7847
  {
7711
7848
  const k = indices[j];
7712
- if (k === i0 || k === i1 || k === i2)
7713
- continue;
7849
+ if (k === i0 || k === i1 || k === i2) continue;
7850
+
7714
7851
  const p = points[k];
7715
7852
  hasInside = pointInTriangle(p, a, b, c);
7716
- if (hasInside)
7717
- break;
7853
+ if (hasInside) break;
7718
7854
  }
7719
- if (hasInside)
7720
- continue;
7855
+ if (hasInside) continue;
7721
7856
 
7722
7857
  // found valid ear
7723
7858
  triangles.push([i0, i1, i2]);
@@ -7742,8 +7877,7 @@ function glPolyStrip(points)
7742
7877
  worstIndex = i;
7743
7878
  }
7744
7879
  }
7745
- if (worstIndex < 0)
7746
- break;
7880
+ if (worstIndex < 0) break;
7747
7881
 
7748
7882
  const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
7749
7883
  const i1 = indices[worstIndex];
@@ -7974,14 +8108,16 @@ class PostProcessPlugin
7974
8108
  {
7975
8109
  /** Create global post processing shader
7976
8110
  * @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)
8111
+ * @param {boolean} [includeMainCanvas] - combine mainCanvs onto glCanvas
8112
+ * @param {boolean} [feedbackTexture] - use glCanvas from previous frame as the texture
8113
+ * @example
8114
+ * // create the post process plugin object
8115
+ * new PostProcessPlugin(shaderCode);
8116
+ */
8117
+ constructor(shaderCode, includeMainCanvas=false, feedbackTexture=false)
7983
8118
  {
7984
8119
  ASSERT(!postProcess, 'Post process already initialized');
8120
+ ASSERT(!(includeMainCanvas && feedbackTexture), 'Post process cannot both include main canvas and use feedback texture');
7985
8121
  postProcess = this;
7986
8122
 
7987
8123
  if (!shaderCode) // default shader pass through
@@ -7989,9 +8125,10 @@ class PostProcessPlugin
7989
8125
 
7990
8126
  /** @property {WebGLProgram} - Shader for post processing */
7991
8127
  this.shader = undefined;
7992
-
7993
8128
  /** @property {WebGLTexture} - Texture for post processing */
7994
8129
  this.texture = undefined;
8130
+ /** @property {WebGLVertexArrayObject} - Vertex array object */
8131
+ this.vao = undefined;
7995
8132
 
7996
8133
  // setup the post processing plugin
7997
8134
  initPostProcess();
@@ -8000,7 +8137,6 @@ class PostProcessPlugin
8000
8137
  function initPostProcess()
8001
8138
  {
8002
8139
  if (headlessMode) return;
8003
-
8004
8140
  if (!glEnable)
8005
8141
  {
8006
8142
  console.warn('PostProcessPlugin: WebGL not enabled!');
@@ -8011,14 +8147,14 @@ class PostProcessPlugin
8011
8147
  postProcess.texture = glCreateTexture();
8012
8148
  postProcess.shader = glCreateProgram(
8013
8149
  '#version 300 es\n' + // specify GLSL ES version
8014
- 'precision highp float;'+ // use highp for better accuracy
8150
+ 'precision highp float;'+ // use highp for accuracy
8015
8151
  'in vec2 p;'+ // position
8016
8152
  'void main(){'+ // shader entry point
8017
8153
  'gl_Position=vec4(p+p-1.,1,1);'+ // set position
8018
8154
  '}' // end of shader
8019
8155
  ,
8020
8156
  '#version 300 es\n' + // specify GLSL ES version
8021
- 'precision highp float;'+ // use highp for better accuracy
8157
+ 'precision highp float;'+ // use highp for accuracy
8022
8158
  'uniform sampler2D iChannel0;'+ // input texture
8023
8159
  'uniform vec3 iResolution;'+ // size of output texture
8024
8160
  'uniform float iTime;'+ // time
@@ -8029,6 +8165,17 @@ class PostProcessPlugin
8029
8165
  'c.a=1.;'+ // always use full alpha
8030
8166
  '}' // end of shader
8031
8167
  );
8168
+
8169
+ // setup VAO for post processing
8170
+ postProcess.vao = glContext.createVertexArray();
8171
+ glContext.bindVertexArray(postProcess.vao);
8172
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
8173
+
8174
+ // configure vertex attributes
8175
+ const vertexByteStride = 8;
8176
+ const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
8177
+ glContext.enableVertexAttribArray(pLocation);
8178
+ glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
8032
8179
  }
8033
8180
  function postProcessContextLost()
8034
8181
  {
@@ -8043,14 +8190,17 @@ class PostProcessPlugin
8043
8190
  }
8044
8191
  function postProcessRender()
8045
8192
  {
8046
- if (headlessMode) return;
8047
-
8048
- if (!glEnable)
8049
- return;
8193
+ if (headlessMode || !glEnable) return;
8050
8194
 
8051
8195
  // clear out the buffer
8052
8196
  glFlush();
8053
-
8197
+
8198
+ // setup shader program to draw a quad
8199
+ glContext.useProgram(postProcess.shader);
8200
+ glContext.bindVertexArray(postProcess.vao);
8201
+ glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, true);
8202
+ glContext.disable(glContext.BLEND);
8203
+
8054
8204
  // setup texture
8055
8205
  glContext.activeTexture(glContext.TEXTURE0);
8056
8206
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
@@ -8061,29 +8211,32 @@ class PostProcessPlugin
8061
8211
  workCanvas.height = mainCanvasSize.y;
8062
8212
  glCopyToContext(workContext);
8063
8213
  workContext.drawImage(mainCanvas, 0, 0);
8214
+ mainCanvas.width |= 0
8064
8215
 
8065
8216
  // copy work canvas to texture
8066
8217
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
8067
8218
  }
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
-
8219
+ else if (!feedbackTexture)
8220
+ {
8221
+ // copy glCanvas to texture
8222
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
8223
+ }
8224
+
8081
8225
  // set uniforms and draw
8082
8226
  const uniformLocation = (name)=>glContext.getUniformLocation(postProcess.shader, name);
8083
8227
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
8084
8228
  glContext.uniform1f(uniformLocation('iTime'), time);
8085
8229
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
8086
8230
  glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
8231
+
8232
+ if (feedbackTexture)
8233
+ {
8234
+ // pass glCanvas back to overlay texture
8235
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
8236
+ }
8237
+
8238
+ // force it to set instanced mode
8239
+ glSetInstancedMode(true);
8087
8240
  }
8088
8241
  }
8089
8242
  }
@@ -8764,7 +8917,7 @@ class UISystemPlugin
8764
8917
  const up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight';
8765
8918
  if (both)
8766
8919
  {
8767
- return keyIsDown(up) || keyIsDown(left) ? -1 :
8920
+ return keyIsDown(up) || keyIsDown(left) ? -1 :
8768
8921
  keyIsDown(down) || keyIsDown(right) ? 1 : 0;
8769
8922
  }
8770
8923
  const back = vertical ? up : left;
@@ -8795,7 +8948,7 @@ class UISystemPlugin
8795
8948
  * @return {boolean} */
8796
8949
  getNavigationWasPressed()
8797
8950
  {
8798
- return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
8951
+ return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
8799
8952
  keyWasPressed('Space') || keyWasPressed('Enter');
8800
8953
  }
8801
8954
 
@@ -8843,7 +8996,7 @@ class UISystemPlugin
8843
8996
  buttonYes.textHeight = 40;
8844
8997
  buttonYes.navigationIndex = 1;
8845
8998
  buttonYes.hoverColor = hsl(0,1,.5);
8846
- buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
8999
+ buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
8847
9000
  confirmMenu.addChild(buttonYes);
8848
9001
 
8849
9002
  // no button
@@ -8873,8 +9026,8 @@ class UISystemPlugin
8873
9026
  class UIObject
8874
9027
  {
8875
9028
  /** Create a UIObject
8876
- * @param {Vector2} [pos=(0,0)]
8877
- * @param {Vector2} [size=(1,1)]
9029
+ * @param {Vector2} [pos=vec2()]
9030
+ * @param {Vector2} [size=vec2(1)]
8878
9031
  */
8879
9032
  constructor(pos=vec2(), size=vec2())
8880
9033
  {
@@ -8889,7 +9042,7 @@ class UIObject
8889
9042
  this.size = size.copy();
8890
9043
  /** @property {Color} - Color of the object */
8891
9044
  this.color = uiSystem.defaultColor.copy();
8892
- /** @property {Color} - Color of the object when active, uses color if undefined */
9045
+ /** @property {Color} - Color of the object when active, uses hoverColor if undefined */
8893
9046
  this.activeColor = undefined;
8894
9047
  /** @property {string} - Text for this ui object */
8895
9048
  this.text = undefined;
@@ -8987,10 +9140,10 @@ class UIObject
8987
9140
 
8988
9141
  // disconnect from parent and destroy children
8989
9142
  this.destroyed = 1;
8990
- this.parent && this.parent.removeChild(this);
9143
+ this.parent?.removeChild(this);
8991
9144
  for (const child of this.children)
8992
9145
  {
8993
- child.parent = 0;
9146
+ child.parent = undefined;
8994
9147
  child.destroy();
8995
9148
  }
8996
9149
  }
@@ -9086,10 +9239,10 @@ class UIObject
9086
9239
  this.interactive && this.isActiveObject() && !this.disabled ?
9087
9240
  this.color : this.lineColor;
9088
9241
  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 :
9242
+ this.disabled ? this.disabledColor :
9243
+ this.interactive ?
9244
+ this.isActiveObject() ? this.activeColor || this.hoverColor :
9245
+ this.isHoverObject() ? this.hoverColor :
9093
9246
  this.color : this.color;
9094
9247
  const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
9095
9248
 
@@ -9101,7 +9254,7 @@ class UIObject
9101
9254
  getTextSize()
9102
9255
  {
9103
9256
  return vec2(
9104
- this.textWidth || this.textFitScale * this.size.x,
9257
+ this.textWidth || this.textFitScale * this.size.x,
9105
9258
  this.textHeight || this.textFitScale * this.size.y);
9106
9259
  }
9107
9260
 
@@ -9149,9 +9302,9 @@ class UIObject
9149
9302
  renderDebug(visible=true)
9150
9303
  {
9151
9304
  // apply color based on state
9152
- const color =
9305
+ const color =
9153
9306
  !visible ? GREEN :
9154
- this.isHoverObject() ? YELLOW :
9307
+ this.isHoverObject() ? YELLOW :
9155
9308
  this.disabled ? PURPLE :
9156
9309
  this.interactive ? RED : BLUE;
9157
9310
  uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);