littlejsengine 1.16.2 → 1.17.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/littlejs.d.ts +291 -246
  2. package/dist/littlejs.esm.js +1520 -1271
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +1178 -920
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +1170 -912
  7. package/examples/box2d/gameObjects.js +4 -4
  8. package/examples/breakout/game.js +5 -6
  9. package/examples/electron/index.html +2 -2
  10. package/examples/electron/tiles.png +0 -0
  11. package/examples/htmlMenu/tiles.png +0 -0
  12. package/examples/index.html +9 -8
  13. package/examples/logo.png +0 -0
  14. package/examples/module/build.bat +7 -0
  15. package/examples/module/build.js +121 -0
  16. package/examples/module/tiles.png +0 -0
  17. package/examples/platformer/game.js +1 -1
  18. package/examples/platformer/gameEffects.js +25 -29
  19. package/examples/platformer/gameLevel.js +27 -28
  20. package/examples/puzzle/game.js +2 -2
  21. package/examples/shorts/base.html +4 -1
  22. package/examples/shorts/box2dPool.js +2 -2
  23. package/examples/shorts/box2dTileLayer.js +48 -0
  24. package/examples/shorts/clock.js +3 -3
  25. package/examples/shorts/fontImage.js +4 -3
  26. package/examples/shorts/music.js +2 -2
  27. package/examples/shorts/musicPlayer.js +2 -2
  28. package/examples/shorts/parallax.js +1 -1
  29. package/examples/shorts/sequencer.js +1 -1
  30. package/examples/shorts/shapes.js +1 -1
  31. package/examples/shorts/texture.js +10 -6
  32. package/examples/shorts/tiles.png +0 -0
  33. package/examples/shorts/tiltedView.js +2 -0
  34. package/examples/starter/index.html +2 -2
  35. package/examples/starter/tiles.png +0 -0
  36. package/examples/typescript/tiles.png +0 -0
  37. package/examples/uiSystem/game.js +1 -1
  38. package/examples/uiSystem/tiles.png +0 -0
  39. package/package.json +5 -5
  40. package/plugins/box2d.js +167 -63
  41. package/plugins/postProcess.js +47 -28
  42. package/plugins/uiSystem.js +18 -18
  43. package/plugins/zzfxm.js +2 -2
  44. package/reference.md +4 -3
  45. package/src/engine.js +182 -181
  46. package/src/engineAudio.js +47 -63
  47. package/src/engineDebug.js +8 -8
  48. package/src/engineDraw.js +184 -124
  49. package/src/engineExport.js +325 -334
  50. package/src/engineFont.png +0 -0
  51. package/src/engineInput.js +18 -24
  52. package/src/engineMath.js +24 -113
  53. package/src/engineObject.js +27 -26
  54. package/src/engineParticles.js +2 -2
  55. package/src/engineTileLayer.js +229 -192
  56. package/src/engineUtilities.js +100 -4
  57. package/src/engineWebGL.js +124 -73
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.16.2';
36
+ const engineVersion = '1.17.5';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -88,8 +88,9 @@ function getPaused() { return paused; }
88
88
  * @memberof Engine */
89
89
  function setPaused(isPaused=true) { paused = isPaused; }
90
90
 
91
- // Frame time tracking
91
+ // Engine internal variables
92
92
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
93
+ let showEngineVersion = true;
93
94
 
94
95
  ///////////////////////////////////////////////////////////////////////////////
95
96
  // plugin hooks
@@ -153,16 +154,17 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
153
154
  * @example
154
155
  * // Basic engine startup
155
156
  * engineInit(
156
- * () => { LOG('Game initialized!'); }, // gameInit
157
- * () => { updateGameLogic(); }, // gameUpdate
158
- * () => { updateUI(); }, // gameUpdatePost
159
- * () => { drawBackground(); }, // gameRender
160
- * () => { drawHUD(); }, // gameRenderPost
157
+ * ()=> { LOG('Game initialized!'); }, // gameInit
158
+ * ()=> { updateGameLogic(); }, // gameUpdate
159
+ * ()=> { updateUI(); }, // gameUpdatePost
160
+ * ()=> { drawBackground(); }, // gameRender
161
+ * ()=> { drawHUD(); }, // gameRenderPost
161
162
  * ['tiles.png', 'tilesLevel.png'] // images to load
162
163
  * );
163
164
  * @memberof Engine */
164
165
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
165
166
  {
167
+ showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
166
168
  ASSERT(!mainContext, 'engine already initialized');
167
169
  ASSERT(isArray(imageSources), 'pass in images as array');
168
170
 
@@ -320,7 +322,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
320
322
 
321
323
  // responsive aspect ratio with native resolution
322
324
  const innerAspect = innerWidth / innerHeight;
323
- ASSERT(canvasMinAspect <= canvasMaxAspect);
325
+ ASSERT(canvasMinAspect <= canvasMaxAspect);
324
326
  if (canvasMaxAspect && innerAspect > canvasMaxAspect)
325
327
  {
326
328
  // full height
@@ -377,7 +379,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
377
379
  debugInit();
378
380
 
379
381
  // setup canvases
380
- // transform way is still more reliable then flexbox or grid
382
+ // transform way is still more reliable than flexbox or grid
381
383
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
382
384
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
383
385
  mainCanvas.style.cssText = styleCanvas;
@@ -422,18 +424,20 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
422
424
  }));
423
425
  }
424
426
 
427
+ // load engine font image
428
+ promises.push(fontImageInit());
429
+
425
430
  if (showSplashScreen)
426
431
  {
427
432
  // draw splash screen
428
433
  promises.push(new Promise(resolve =>
429
434
  {
430
435
  let t = 0;
431
- console.log(`${engineName} Engine v${engineVersion}`);
432
436
  updateSplash();
433
437
  function updateSplash()
434
438
  {
435
439
  inputClear();
436
- drawEngineSplashScreen(t+=.01);
440
+ drawEngineLogo(t+=.01);
437
441
  t>1 ? resolve() : setTimeout(updateSplash, 16);
438
442
  }
439
443
  }));
@@ -449,172 +453,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
449
453
  await gameInit();
450
454
  engineUpdate();
451
455
  }
452
-
453
- ///////////////////////////////////////////////////////////////////////////
454
- // LittleJS Splash Screen
455
- function drawEngineSplashScreen(t)
456
- {
457
- const x = mainContext;
458
- const w = mainCanvas.width = innerWidth;
459
- const h = mainCanvas.height = innerHeight;
460
-
461
- {
462
- // background
463
- const p3 = percent(t, 1, .8);
464
- const p4 = percent(t, 0, .5);
465
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
466
- g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
467
- g.addColorStop(1,hsl(0,0,0,p3).toString());
468
- x.save();
469
- x.fillStyle = g;
470
- x.fillRect(0,0,w,h);
471
- }
472
-
473
- // draw LittleJS logo...
474
- const rect = (X, Y, W, H, C)=>
475
- {
476
- x.beginPath();
477
- x.rect(X,Y,W,C?H*p:H);
478
- x.fillStyle = C;
479
- C ? x.fill() : x.stroke();
480
- };
481
- const line = (X, Y, Z, W)=>
482
- {
483
- x.beginPath();
484
- x.lineTo(X,Y);
485
- x.lineTo(Z,W);
486
- x.stroke();
487
- };
488
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
489
- {
490
- const D = (A+B)/2, E = p*(B-A)/2;
491
- x.beginPath();
492
- F && x.lineTo(X,Y);
493
- x.arc(X,Y,R,D-E,D+E);
494
- x.fillStyle = C;
495
- C ? x.fill() : x.stroke();
496
- };
497
- const color = (c=0, l=0) =>
498
- hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();
499
- const alpha = wave(1,1,t);
500
- const p = percent(alpha, .1, .5);
501
-
502
- // setup
503
- x.translate(w/2,h/2);
504
- const size = min(6, min(w,h)/99); // fit to screen
505
- x.scale(size,size);
506
- x.translate(-40,-35);
507
- x.lineJoin = x.lineCap = 'round';
508
- x.lineWidth = .1 + p*1.9;
509
-
510
- // drawing effect
511
- const p2 = percent(alpha,.1,1);
512
- x.setLineDash([99*p2,99]);
513
-
514
- // cab top
515
- rect(7,16,18,-8,color(2,2));
516
- rect(7,8,18,4,color(2,3));
517
- rect(25,8,8,8,color(2,1));
518
- rect(25,8,-18,8);
519
- rect(25,8,8,8);
520
-
521
- // cab
522
- rect(25,16,7,23,color());
523
- rect(11,39,14,-23,color(1,1));
524
- rect(11,16,14,18,color(1,2));
525
- rect(11,16,14,8,color(1,3));
526
- rect(25,16,-14,24);
527
-
528
- // cab window
529
- rect(15,29,6,-9,color(2,2));
530
- circle(15,21,5,0,PI/2,color(2,4),1);
531
- rect(21,21,-6,9);
532
-
533
- // little stack
534
- rect(37,14,9,6,color(3,2));
535
- rect(37,14,4.5,6,color(3,3));
536
- rect(37,14,9,6);
537
-
538
- // big stack
539
- rect(50,20,10,-8,color(0,1));
540
- rect(50,20,6.5,-8,color(0,2));
541
- rect(50,20,3.5,-8,color(0,3));
542
- rect(50,20,10,-8);
543
- circle(55,2,11.4,.5,PI-.5,color(3,3));
544
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
545
- circle(55,2,11.4,.5,PI-.5);
546
- rect(45,7,20,-7,color(0,2));
547
- rect(45,-1,20,4,color(0,3));
548
- rect(45,-1,20,8);
549
-
550
- // engine
551
- for (let i=5; i--;)
552
- {
553
- // stagger radius to fix slight seam
554
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
555
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
556
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
557
- }
558
-
559
- // engine outline
560
- circle(36,30,10,PI/2,PI*3/2);
561
- circle(48,30,10,PI/2,PI*3/2);
562
- circle(60,30,10);
563
- line(36,20,60,20);
564
-
565
- // engine front light
566
- circle(60,30,4,PI,3*PI,color(3,2));
567
- circle(60,30,4,PI,2*PI,color(3,3));
568
- circle(60,30,4,PI,3*PI);
569
-
570
- // front brush
571
- for (let i=6; i--;)
572
- {
573
- x.beginPath();
574
- x.lineTo(53,54);
575
- x.lineTo(53,40);
576
- x.lineTo(53+(1+i*2.9)*p,40);
577
- x.lineTo(53+(4+i*3.5)*p,54);
578
- x.fillStyle = color(0,i%2+2);
579
- x.fill();
580
- i%2 && x.stroke();
581
- }
582
-
583
- // wheels
584
- rect(6,40,5,5);
585
- rect(6,40,5,5,color());
586
- rect(15,54,38,-14,color());
587
- for (let i=3; i--;)
588
- for (let j=2; j--;)
589
- {
590
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
591
- x.stroke();
592
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
593
- x.stroke();
594
- }
595
- line(6,40,68,40); // center
596
- line(77,54,4,54); // bottom
597
-
598
- // draw engine name
599
- const s = engineName;
600
- x.font = '900 16px arial';
601
- x.textAlign = 'center';
602
- x.textBaseline = 'top';
603
- x.lineWidth = .1+p*3.9;
604
- let w2 = 0;
605
- for (let i=0; i<s.length; ++i)
606
- w2 += x.measureText(s[i]).width;
607
- for (let j=2; j--;)
608
- for (let i=0, X=41-w2/2; i<s.length; ++i)
609
- {
610
- x.fillStyle = color(i,2);
611
- const w = x.measureText(s[i]).width;
612
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
613
- X += w;
614
- }
615
- x.restore();
616
- }
617
- ///////////////////////////////////////////////////////////////////////////
618
456
  }
619
457
 
620
458
  /** Update each engine object, remove destroyed objects, and update time
@@ -628,8 +466,7 @@ function engineObjectsUpdate()
628
466
  // recursive object update
629
467
  function updateObject(o)
630
468
  {
631
- if (o.destroyed)
632
- return;
469
+ if (o.destroyed) return;
633
470
 
634
471
  o.update();
635
472
  for (const child of o.children)
@@ -637,8 +474,7 @@ function engineObjectsUpdate()
637
474
  }
638
475
  for (const o of engineObjects)
639
476
  {
640
- if (o.parent)
641
- continue;
477
+ if (o.parent || o.destroyed) continue;
642
478
 
643
479
  // update top level objects
644
480
  o.update();
@@ -727,6 +563,171 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
727
563
 
728
564
  debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
729
565
  return hitObjects;
566
+ }
567
+
568
+ ///////////////////////////////////////////////////////////////////////////////
569
+ function drawEngineLogo(t)
570
+ {
571
+ // LittleJS Logo and Splash Screen
572
+ const x = mainContext;
573
+ const w = mainCanvas.width = innerWidth;
574
+ const h = mainCanvas.height = innerHeight;
575
+
576
+ {
577
+ // background
578
+ const p3 = percent(t, 1, .8);
579
+ const p4 = percent(t, 0, .5);
580
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
581
+ g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
582
+ g.addColorStop(1,hsl(0,0,0,p3).toString());
583
+ x.save();
584
+ x.fillStyle = g;
585
+ x.fillRect(0,0,w,h);
586
+ }
587
+
588
+ // draw LittleJS logo...
589
+ const rect = (X, Y, W, H, C)=>
590
+ {
591
+ x.beginPath();
592
+ x.rect(X,Y,W,C?H*p:H);
593
+ x.fillStyle = C;
594
+ C ? x.fill() : x.stroke();
595
+ };
596
+ const line = (X, Y, Z, W)=>
597
+ {
598
+ x.beginPath();
599
+ x.lineTo(X,Y);
600
+ x.lineTo(Z,W);
601
+ x.stroke();
602
+ };
603
+ const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
604
+ {
605
+ const D = (A+B)/2, E = p*(B-A)/2;
606
+ x.beginPath();
607
+ F && x.lineTo(X,Y);
608
+ x.arc(X,Y,R,D-E,D+E);
609
+ x.fillStyle = C;
610
+ C ? x.fill() : x.stroke();
611
+ };
612
+ const color = (c=0, l=0)=>
613
+ hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
614
+ const alpha = wave(1,1,t);
615
+ const p = percent(alpha, .1, .5);
616
+
617
+ // setup
618
+ x.translate(w/2,h/2);
619
+ const size = min(6, min(w,h)/99); // fit to screen
620
+ x.scale(size,size);
621
+ x.translate(-40,-35);
622
+ x.lineJoin = x.lineCap = 'round';
623
+ x.lineWidth = .1 + p*1.9;
624
+
625
+ // drawing effect
626
+ const p2 = percent(alpha,.1,1);
627
+ x.setLineDash([99*p2,99]);
628
+
629
+ // cab top
630
+ rect(7,16,18,-8,color(2,2));
631
+ rect(7,8,18,4,color(2,3));
632
+ rect(25,8,8,8,color(2,1));
633
+ rect(25,8,-18,8);
634
+ rect(25,8,8,8);
635
+
636
+ // cab
637
+ rect(25,16,7,24,color());
638
+ rect(11,39,14,-23,color(1,1));
639
+ rect(11,16,14,18,color(1,2));
640
+ rect(11,16,14,8,color(1,3));
641
+ rect(25,16,-14,24);
642
+
643
+ // cab window
644
+ rect(15,29,6,-9,color(2,2));
645
+ circle(15,21,5,0,PI/2,color(2,4),1);
646
+ rect(21,21,-6,9);
647
+
648
+ // little stack
649
+ rect(37,14,9,6,color(3,2));
650
+ rect(37,14,4.5,6,color(3,3));
651
+ rect(37,14,9,6);
652
+
653
+ // big stack
654
+ rect(50,20,10,-8,color(0,1));
655
+ rect(50,20,6.5,-8,color(0,2));
656
+ rect(50,20,3.5,-8,color(0,3));
657
+ rect(50,20,10,-8);
658
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
659
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
660
+ circle(55,2,11.4,.5,PI-.5);
661
+ rect(45,7,20,-7,color(0,2));
662
+ rect(45,-1,20,4,color(0,3));
663
+ rect(45,-1,20,8);
664
+
665
+ // engine
666
+ for (let i=5; i--;)
667
+ {
668
+ // stagger radius to fix slight seam
669
+ circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
670
+ circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
671
+ circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
672
+ }
673
+
674
+ // engine outline
675
+ circle(36,30,10,PI/2,PI*3/2);
676
+ circle(48,30,10,PI/2,PI*3/2);
677
+ circle(60,30,10);
678
+ line(36,20,60,20);
679
+
680
+ // engine front light
681
+ circle(60,30,4,PI,3*PI,color(3,2));
682
+ circle(60,30,4,PI,2*PI,color(3,3));
683
+ circle(60,30,4,PI,3*PI);
684
+
685
+ // front brush
686
+ for (let i=6; i--;)
687
+ {
688
+ x.beginPath();
689
+ x.lineTo(53,54);
690
+ x.lineTo(53,40);
691
+ x.lineTo(53+(1+i*2.9)*p,40);
692
+ x.lineTo(53+(4+i*3.5)*p,54);
693
+ x.fillStyle = color(0,i%2+2);
694
+ x.fill();
695
+ i%2 && x.stroke();
696
+ }
697
+
698
+ // wheels
699
+ rect(6,40,5,5);
700
+ rect(6,40,5,5,color());
701
+ rect(15,54,38,-14,color());
702
+ for (let i=3; i--;)
703
+ for (let j=2; j--;)
704
+ {
705
+ circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
706
+ x.stroke();
707
+ circle(15*i+15,47,j?7:1,0,PI,color(i,2));
708
+ x.stroke();
709
+ }
710
+ line(6,40,68,40); // center
711
+ line(77,54,4,54); // bottom
712
+
713
+ // draw engine name
714
+ const s = engineName;
715
+ x.font = '900 16px arial';
716
+ x.textAlign = 'center';
717
+ x.textBaseline = 'top';
718
+ x.lineWidth = .1+p*3.9;
719
+ let w2 = 0;
720
+ for (let i=0; i<s.length; ++i)
721
+ w2 += x.measureText(s[i]).width;
722
+ for (let j=2; j--;)
723
+ for (let i=0, X=41-w2/2; i<s.length; ++i)
724
+ {
725
+ x.fillStyle = color(i,2);
726
+ const w = x.measureText(s[i]).width;
727
+ x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
728
+ X += w;
729
+ }
730
+ x.restore();
730
731
  }
731
732
  /**
732
733
  * LittleJS Debug System
@@ -777,7 +778,7 @@ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParti
777
778
  /** Asserts if the expression is false, does nothing in release builds
778
779
  * Halts execution if the assert fails and throws an error
779
780
  * @param {boolean} assert
780
- * @param {...Object} [output] - error message output
781
+ * @param {...Object} output - error message output
781
782
  * @memberof Debug */
782
783
  function ASSERT(assert, ...output)
783
784
  {
@@ -787,13 +788,13 @@ function ASSERT(assert, ...output)
787
788
  }
788
789
 
789
790
  /** Log to console if debug is enabled, does nothing in release builds
790
- * @param {...Object} [output] - message output
791
+ * @param {...Object} output - message output
791
792
  * @memberof Debug */
792
793
  function LOG(...output) { console.log(...output); }
793
794
 
794
795
  /** Draw a debug rectangle in world space
795
796
  * @param {Vector2} pos
796
- * @param {Vector2} [size=Vector2()]
797
+ * @param {Vector2} [size=vec2(0)]
797
798
  * @param {Color|string} [color]
798
799
  * @param {number} [time]
799
800
  * @param {number} [angle]
@@ -1117,7 +1118,7 @@ function debugRender()
1117
1118
  if (tileCollisionTest(mousePos))
1118
1119
  {
1119
1120
  // show floored tile pick for tile collision
1120
- drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(1,1,0,.5));
1121
+ drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(1,1,0,.5), 0, false);
1121
1122
  }
1122
1123
  }
1123
1124
 
@@ -1196,8 +1197,8 @@ function debugRender()
1196
1197
  if (debugObject)
1197
1198
  {
1198
1199
  const raycastHitPos = tileCollisionRaycast(debugObject.pos, mousePos);
1199
- raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3));
1200
- drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5));
1200
+ raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3), 0, false);
1201
+ drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5), undefined, undefined, false);
1201
1202
 
1202
1203
  let debugText = 'mouse pos = ' + mousePos;
1203
1204
  if (tileCollisionLayers.length)
@@ -1434,8 +1435,8 @@ function debugProtectConstant(obj)
1434
1435
  props.forEach(prop =>
1435
1436
  {
1436
1437
  Object.defineProperty(obj, prop, {
1437
- get: () => values[prop],
1438
- set: (value) =>
1438
+ get: ()=> values[prop],
1439
+ set: (value)=>
1439
1440
  {
1440
1441
  ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
1441
1442
  },
@@ -1448,12 +1449,11 @@ function debugProtectConstant(obj)
1448
1449
  return Object.freeze(obj);
1449
1450
  }
1450
1451
  /**
1451
- * LittleJS Utility Classes and Functions
1452
+ * LittleJS Math Classes and Functions
1452
1453
  * - General purpose math library
1453
- * - Vector2 - fast, simple, easy 2D vector class
1454
- * - Color - holds a rgba color with some math functions
1455
- * - Timer - tracks time automatically
1456
1454
  * - RandomGenerator - seeded random number generator
1455
+ * - Vector2 - fast, simple, easy 2D vector class
1456
+ * - Color - holds a rgba color with math functions
1457
1457
  * @namespace Math
1458
1458
  */
1459
1459
 
@@ -1464,25 +1464,25 @@ function debugProtectConstant(obj)
1464
1464
  const PI = Math.PI;
1465
1465
 
1466
1466
  /** Returns absolute value of value passed in
1467
- * @param {number} value
1467
+ * @param {number} x
1468
1468
  * @return {number}
1469
1469
  * @memberof Math */
1470
1470
  const abs = Math.abs;
1471
1471
 
1472
1472
  /** Returns floored value of value passed in
1473
- * @param {number} value
1473
+ * @param {number} x
1474
1474
  * @return {number}
1475
1475
  * @memberof Math */
1476
1476
  const floor = Math.floor;
1477
1477
 
1478
1478
  /** Returns ceiled value of value passed in
1479
- * @param {number} value
1479
+ * @param {number} x
1480
1480
  * @return {number}
1481
1481
  * @memberof Math */
1482
1482
  const ceil = Math.ceil;
1483
1483
 
1484
1484
  /** Returns rounded value passed in
1485
- * @param {number} value
1485
+ * @param {number} x
1486
1486
  * @return {number}
1487
1487
  * @memberof Math */
1488
1488
  const round = Math.round;
@@ -1500,7 +1500,7 @@ const min = Math.min;
1500
1500
  const max = Math.max;
1501
1501
 
1502
1502
  /** Returns the sign of value passed in
1503
- * @param {number} value
1503
+ * @param {number} x
1504
1504
  * @return {number}
1505
1505
  * @memberof Math */
1506
1506
  const sign = Math.sign;
@@ -1512,25 +1512,25 @@ const sign = Math.sign;
1512
1512
  const hypot = Math.hypot;
1513
1513
 
1514
1514
  /** Returns log2 of value passed in
1515
- * @param {number} value
1515
+ * @param {number} x
1516
1516
  * @return {number}
1517
1517
  * @memberof Math */
1518
1518
  const log2 = Math.log2;
1519
1519
 
1520
1520
  /** Returns sin of value passed in
1521
- * @param {number} value
1521
+ * @param {number} x
1522
1522
  * @return {number}
1523
1523
  * @memberof Math */
1524
1524
  const sin = Math.sin;
1525
1525
 
1526
1526
  /** Returns cos of value passed in
1527
- * @param {number} value
1527
+ * @param {number} x
1528
1528
  * @return {number}
1529
1529
  * @memberof Math */
1530
1530
  const cos = Math.cos;
1531
1531
 
1532
1532
  /** Returns tan of value passed in
1533
- * @param {number} value
1533
+ * @param {number} x
1534
1534
  * @return {number}
1535
1535
  * @memberof Math */
1536
1536
  const tan = Math.tan;
@@ -1641,11 +1641,11 @@ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
1641
1641
 
1642
1642
  /** Returns true if two axis aligned bounding boxes are overlapping
1643
1643
  * this can be used for simple collision detection between objects
1644
- * @param {Vector2} posA - Center of box A
1645
- * @param {Vector2} sizeA - Size of box A
1646
- * @param {Vector2} posB - Center of box B
1647
- * @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
1648
- * @return {boolean} - True if overlapping
1644
+ * @param {Vector2} posA - Center of box A
1645
+ * @param {Vector2} sizeA - Size of box A
1646
+ * @param {Vector2} posB - Center of box B
1647
+ * @param {Vector2} [sizeB=vec2()] - Size of box B, uses a point if undefined
1648
+ * @return {boolean} - True if overlapping
1649
1649
  * @memberof Math */
1650
1650
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
1651
1651
  {
@@ -1719,7 +1719,7 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1719
1719
  * @param {any} s
1720
1720
  * @return {boolean}
1721
1721
  * @memberof Math */
1722
- function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1722
+ function isString(s) { return s != null && typeof s?.toString() === 'string'; }
1723
1723
 
1724
1724
  /**
1725
1725
  * Check if object is an array
@@ -1863,8 +1863,8 @@ function randInCircle(radius=1, minRadius=0)
1863
1863
  { return radius > 0 ? randVec2(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
1864
1864
 
1865
1865
  /** Returns a random color between the two passed in colors, combine components if linear
1866
- * @param {Color} [colorA=(1,1,1,1)]
1867
- * @param {Color} [colorB=(0,0,0,1)]
1866
+ * @param {Color} [colorA=WHITE]
1867
+ * @param {Color} [colorB=BLACK]
1868
1868
  * @param {boolean} [linear]
1869
1869
  * @return {Color}
1870
1870
  * @memberof Random */
@@ -1943,8 +1943,8 @@ class RandomGenerator
1943
1943
  { return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
1944
1944
 
1945
1945
  /** Returns a random color between the two passed in colors, combine components if linear
1946
- * @param {Color} [colorA=(1,1,1,1)]
1947
- * @param {Color} [colorB=(0,0,0,1)]
1946
+ * @param {Color} [colorA=WHITE]
1947
+ * @param {Color} [colorB=BLACK]
1948
1948
  * @param {boolean} [linear]
1949
1949
  * @return {Color} */
1950
1950
  randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
@@ -1988,7 +1988,7 @@ class RandomGenerator
1988
1988
  * a = vec2(5); // set a to (5, 5)
1989
1989
  * b = vec2(); // set b to (0, 0)
1990
1990
  * @memberof Math */
1991
- function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
1991
+ function vec2(x=0, y) { return new Vector2(x, y ?? x); }
1992
1992
 
1993
1993
  /**
1994
1994
  * Check if object is a valid Vector2
@@ -2196,10 +2196,6 @@ class Vector2
2196
2196
  * @return {number} */
2197
2197
  area() { return abs(this.x * this.y); }
2198
2198
 
2199
- /** Returns true if this vector is (0,0)
2200
- * @return {boolean} */
2201
- isZero() { return !this.x && !this.y; }
2202
-
2203
2199
  /** Returns a new vector that is p percent between this and the vector passed in
2204
2200
  * @param {Vector2} v - other vector
2205
2201
  * @param {number} percent
@@ -2561,7 +2557,97 @@ const PURPLE = debugProtectConstant(rgb(.5,0,1));
2561
2557
  /** Color - Magenta #ff00ff
2562
2558
  * @type {Color}
2563
2559
  * @memberof Math */
2564
- const MAGENTA = debugProtectConstant(rgb(1,0,1));
2560
+ const MAGENTA = debugProtectConstant(rgb(1,0,1));
2561
+ /**
2562
+ * LittleJS Utility Classes and Functions
2563
+ * - General purpose utilities
2564
+ * - Timer - tracks time automatically
2565
+ * @namespace Utilities
2566
+ */
2567
+
2568
+ /** Formats seconds to mm:ss style for display purposes
2569
+ * @param {number} t - time in seconds
2570
+ * @return {string}
2571
+ * @memberof Utilities */
2572
+ function formatTime(t)
2573
+ {
2574
+ const sign = t < 0 ? '-' : '';
2575
+ t = abs(t)|0;
2576
+ return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
2577
+ }
2578
+
2579
+ /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
2580
+ * @param {string} url - URL of JSON file
2581
+ * @return {Promise<object>}
2582
+ * @memberof Utilities */
2583
+ async function fetchJSON(url)
2584
+ {
2585
+ const response = await fetch(url);
2586
+ if (!response.ok)
2587
+ throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
2588
+ return response.json();
2589
+ }
2590
+
2591
+ ///////////////////////////////////////////////////////////////////////////////
2592
+
2593
+ /** Save a text file to disk
2594
+ * @param {string} text
2595
+ * @param {string} [filename]
2596
+ * @param {string} [type]
2597
+ * @memberof Utilities */
2598
+ function saveText(text, filename='text', type='text/plain')
2599
+ { saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
2600
+
2601
+ /** Save a canvas to disk
2602
+ * @param {HTMLCanvasElement|OffscreenCanvas} canvas
2603
+ * @param {string} [filename]
2604
+ * @param {string} [type]
2605
+ * @memberof Utilities */
2606
+ function saveCanvas(canvas, filename='screenshot', type='image/png')
2607
+ {
2608
+ if (canvas instanceof OffscreenCanvas)
2609
+ {
2610
+ // copy to temporary canvas and save
2611
+ const saveCanvas = document.createElement('canvas');
2612
+ saveCanvas.width = canvas.width;
2613
+ saveCanvas.height = canvas.height;
2614
+ saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
2615
+ saveDataURL(saveCanvas.toDataURL(type), filename);
2616
+ }
2617
+ else
2618
+ saveDataURL(canvas.toDataURL(type), filename);
2619
+ }
2620
+
2621
+ /** Save a data url to disk
2622
+ * @param {string} url
2623
+ * @param {string} [filename]
2624
+ * @param {number} [revokeTime] - how long before revoking the url
2625
+ * @memberof Utilities */
2626
+ function saveDataURL(url, filename='download', revokeTime)
2627
+ {
2628
+ ASSERT(isString(url), 'saveDataURL requires url string');
2629
+ ASSERT(isString(filename), 'saveDataURL requires filename string');
2630
+
2631
+ // create link for saving screenshots
2632
+ const link = document.createElement('a');
2633
+ link.download = filename;
2634
+ link.href = url;
2635
+ link.click();
2636
+ if (revokeTime !== undefined)
2637
+ setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
2638
+ }
2639
+
2640
+ /** Share content using the native share dialog if available
2641
+ * @param {string} title - title of the share
2642
+ * @param {string} url - url to share
2643
+ * @param {Function} [callback] - Called when share is complete
2644
+ * @memberof Utilities */
2645
+ function shareURL(title, url, callback)
2646
+ {
2647
+ ASSERT(isString(title), 'shareURL requires title string');
2648
+ ASSERT(isString(url), 'shareURL requires url string');
2649
+ navigator.share?.({title, url}).then(()=>callback?.());
2650
+ }
2565
2651
 
2566
2652
  ///////////////////////////////////////////////////////////////////////////////
2567
2653
 
@@ -2646,84 +2732,6 @@ class Timer
2646
2732
  * @return {number} */
2647
2733
  valueOf() { return this.get(); }
2648
2734
  }
2649
- /**
2650
- * LittleJS Utility Classes and Functions
2651
- * - General purpose math library
2652
- * - Vector2 - fast, simple, easy 2D vector class
2653
- * - Color - holds a rgba color with some math functions
2654
- * - Timer - tracks time automatically
2655
- * - RandomGenerator - seeded random number generator
2656
- * @namespace Utilities
2657
- */
2658
-
2659
- /** Formats seconds to mm:ss style for display purposes
2660
- * @param {number} t - time in seconds
2661
- * @return {string}
2662
- * @memberof Utilities */
2663
- function formatTime(t)
2664
- {
2665
- const sign = t < 0 ? '-' : '';
2666
- t = abs(t)|0;
2667
- return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
2668
- }
2669
-
2670
- /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
2671
- * @param {string} url - URL of JSON file
2672
- * @return {Promise<object>}
2673
- * @memberof Utilities */
2674
- async function fetchJSON(url)
2675
- {
2676
- const response = await fetch(url);
2677
- if (!response.ok)
2678
- throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
2679
- return response.json();
2680
- }
2681
-
2682
- ///////////////////////////////////////////////////////////////////////////////
2683
-
2684
- /** Save a text file to disk
2685
- * @param {string} text
2686
- * @param {string} [filename]
2687
- * @param {string} [type]
2688
- * @memberof Utilities */
2689
- function saveText(text, filename='text', type='text/plain')
2690
- { saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
2691
-
2692
- /** Save a canvas to disk
2693
- * @param {HTMLCanvasElement|OffscreenCanvas} canvas
2694
- * @param {string} [filename]
2695
- * @param {string} [type]
2696
- * @memberof Utilities */
2697
- function saveCanvas(canvas, filename='screenshot', type='image/png')
2698
- {
2699
- if (canvas instanceof OffscreenCanvas)
2700
- {
2701
- // copy to temporary canvas and save
2702
- const saveCanvas = document.createElement('canvas');
2703
- saveCanvas.width = canvas.width;
2704
- saveCanvas.height = canvas.height;
2705
- saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
2706
- saveDataURL(saveCanvas.toDataURL(type), filename);
2707
- }
2708
- else
2709
- saveDataURL(canvas.toDataURL(type), filename);
2710
- }
2711
-
2712
- /** Save a data url to disk
2713
- * @param {string} url
2714
- * @param {string} [filename]
2715
- * @param {number} [revokeTime] - how long before revoking the url
2716
- * @memberof Utilities */
2717
- function saveDataURL(url, filename='download', revokeTime)
2718
- {
2719
- // create link for saving screenshots
2720
- const link = document.createElement('a');
2721
- link.download = filename;
2722
- link.href = url;
2723
- link.click();
2724
- if (revokeTime !== undefined)
2725
- setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
2726
- }
2727
2735
  /**
2728
2736
  * LittleJS Engine Settings
2729
2737
  * - All settings for the engine are here
@@ -3350,10 +3358,10 @@ function setDebugKey(key) { debugKey = key; }
3350
3358
  class EngineObject
3351
3359
  {
3352
3360
  /** Create an engine object and adds it to the list of objects
3353
- * @param {Vector2} [pos=(0,0)] - World space position of the object
3354
- * @param {Vector2} [size=(1,1)] - World space size of the object
3355
- * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
3356
- * @param {number} [angle] - Angle the object is rotated by
3361
+ * @param {Vector2} [pos=vec2()] - World space position of the object
3362
+ * @param {Vector2} [size=vec2(1)] - World space size of the object
3363
+ * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
3364
+ * @param {number} [angle] - Angle the object is rotated by
3357
3365
  * @param {Color} [color=WHITE] - Color to apply to tile when rendered
3358
3366
  * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
3359
3367
  */
@@ -3481,15 +3489,16 @@ class EngineObject
3481
3489
  // physics sanity checks
3482
3490
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
3483
3491
  ASSERT(this.damping >= 0 && this.damping <= 1);
3484
- if (!enablePhysicsSolver || !this.mass) // don't do collision for static objects
3485
- return;
3492
+
3493
+ // don't do collision for static objects or if solver disabled
3494
+ if (!enablePhysicsSolver || !this.mass) return;
3486
3495
 
3487
3496
  const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
3488
3497
  if (this.groundObject)
3489
3498
  {
3490
3499
  // apply friction in local space of ground object
3491
3500
  const friction = max(this.friction, this.groundObject.friction);
3492
- const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
3501
+ const groundSpeed = this.groundObject.velocity.x;
3493
3502
  this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
3494
3503
  this.groundObject = undefined;
3495
3504
  }
@@ -3500,19 +3509,19 @@ class EngineObject
3500
3509
  const epsilon = .001; // necessary to push slightly outside of the collision
3501
3510
  for (const o of engineObjectsCollide)
3502
3511
  {
3512
+ // skip destroyed, child objects, or self collision
3513
+ if (o.destroyed || o.parent || o === this) continue;
3514
+
3503
3515
  // non solid objects don't collide with each other
3504
- if ((!this.isSolid && !o.isSolid) || o.destroyed || o.parent || o === this)
3505
- continue;
3516
+ if (!this.isSolid && !o.isSolid) continue;
3506
3517
 
3507
3518
  // check collision
3508
- if (!this.isOverlappingObject(o))
3509
- continue;
3519
+ if (!this.isOverlappingObject(o)) continue;
3510
3520
 
3511
3521
  // notify objects of collision and check if should be resolved
3512
3522
  const collide1 = this.collideWithObject(o);
3513
3523
  const collide2 = o.collideWithObject(this);
3514
- if (!collide1 || !collide2)
3515
- continue;
3524
+ if (!collide1 || !collide2) continue;
3516
3525
 
3517
3526
  if (isOverlapping(oldPos, this.size, o.pos, o.size))
3518
3527
  {
@@ -3593,7 +3602,7 @@ class EngineObject
3593
3602
  if (this.collideTiles)
3594
3603
  {
3595
3604
  // check collision against tiles
3596
- const hitLayer = tileCollisionTest(this.pos, this.size, this)
3605
+ const hitLayer = tileCollisionTest(this.pos, this.size, this);
3597
3606
  if (hitLayer)
3598
3607
  {
3599
3608
  // if already was stuck in collision, don't do anything
@@ -3614,7 +3623,7 @@ class EngineObject
3614
3623
  const delta = y - this.pos.y;
3615
3624
  if (delta < maxMoveUp)
3616
3625
  if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
3617
- {
3626
+ {
3618
3627
  this.pos.y = y;
3619
3628
  debugPhysics && debugRect(this.pos, this.size, '#ff0');
3620
3629
  return;
@@ -3674,10 +3683,10 @@ class EngineObject
3674
3683
 
3675
3684
  // disconnect from parent and destroy children
3676
3685
  this.destroyed = 1;
3677
- this.parent && this.parent.removeChild(this);
3686
+ this.parent?.removeChild(this);
3678
3687
  for (const child of this.children)
3679
3688
  {
3680
- child.parent = 0;
3689
+ child.parent = undefined;
3681
3690
  child.destroy();
3682
3691
  }
3683
3692
  }
@@ -3698,15 +3707,15 @@ class EngineObject
3698
3707
  * @param {Vector2} vec - world space vector */
3699
3708
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
3700
3709
 
3701
- /** Called to check if a tile collision should be resolved
3710
+ /** 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.
3702
3711
  * @param {number} tileData - the value of the tile at the position
3703
- * @param {Vector2} pos - tile where the collision occurred
3704
- * @return {boolean} - true if the collision should be resolved */
3712
+ * @param {Vector2} pos - tile where the collision occurred
3713
+ * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
3705
3714
  collideWithTile(tileData, pos) { return tileData > 0; }
3706
3715
 
3707
- /** Called to check if a object collision should be resolved
3716
+ /** 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.
3708
3717
  * @param {EngineObject} object - the object to test against
3709
- * @return {boolean} - true if the collision should be resolved
3718
+ * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
3710
3719
  */
3711
3720
  collideWithObject(object) { return true; }
3712
3721
 
@@ -3747,9 +3756,9 @@ class EngineObject
3747
3756
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
3748
3757
  getMirrorSign() { return this.mirror ? -1 : 1; }
3749
3758
 
3750
- /** Attaches a child to this with a local transform, returns child for chaining
3759
+ /** Attaches a child to this with a local transform, returns child for chaining
3751
3760
  * @param {EngineObject} child
3752
- * @param {Vector2} [localPos=(0,0)]
3761
+ * @param {Vector2} [localPos=vec2()]
3753
3762
  * @param {number} [localAngle]
3754
3763
  * @return {EngineObject} The child object added */
3755
3764
  addChild(child, localPos=vec2(), localAngle=0)
@@ -3773,7 +3782,7 @@ class EngineObject
3773
3782
  const index = this.children.indexOf(child);
3774
3783
  ASSERT(index >= 0, 'child not found in children array');
3775
3784
  index >= 0 && this.children.splice(index, 1);
3776
- child.parent = 0;
3785
+ child.parent = undefined;
3777
3786
  }
3778
3787
 
3779
3788
  /** Check if overlapping another engine object
@@ -3785,7 +3794,7 @@ class EngineObject
3785
3794
 
3786
3795
  /** Check if overlapping a point or aligned bounding box
3787
3796
  * @param {Vector2} pos - Center of box
3788
- * @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
3797
+ * @param {Vector2} [size=vec2()] - Size of box, uses a point if undefined
3789
3798
  * @return {boolean} */
3790
3799
  isOverlapping(pos, size=vec2())
3791
3800
  { return isOverlapping(this.pos, this.size, pos, size); }
@@ -3916,10 +3925,11 @@ let drawCount;
3916
3925
  * Create a tile info object using a grid based system
3917
3926
  * - This can take vecs or floats for easier use and conversion
3918
3927
  * - If an index is passed in, the tile size and index will determine the position
3919
- * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
3928
+ * @param {Vector2|number} [index=0] - Index of the tile in 1d or 2d form
3920
3929
  * @param {Vector2|number} [size] - Size of tile in pixels
3921
- * @param {number} [textureIndex] - Texture index to use
3930
+ * @param {TextureInfo|number} [texture] - Texture index or info to use
3922
3931
  * @param {number} [padding] - How many pixels padding around tiles
3932
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3923
3933
  * @return {TileInfo}
3924
3934
  * @example
3925
3935
  * tile(2) // a tile at index 2 using the default tile size of 16
@@ -3927,36 +3937,43 @@ let drawCount;
3927
3937
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
3928
3938
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
3929
3939
  * @memberof Draw */
3930
- function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding)
3940
+ function tile(index=new Vector2, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3931
3941
  {
3932
- if (headlessMode)
3933
- return new TileInfo;
3942
+ ASSERT(isVector2(index) || typeof index === 'number', 'index must be a vec2 or number');
3943
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
3944
+ ASSERT(isNumber(texture) || texture instanceof TextureInfo, 'texture must be a number or TextureInfo');
3945
+ ASSERT(isNumber(padding), 'padding must be a number');
3946
+
3947
+ if (headlessMode) return new TileInfo;
3934
3948
 
3935
- // if size is a number, make it a vector
3936
3949
  if (typeof size === 'number')
3937
3950
  {
3951
+ // if size is a number, make it a vector
3938
3952
  ASSERT(size > 0);
3939
3953
  size = new Vector2(size, size);
3940
3954
  }
3941
3955
 
3942
3956
  // create tile info object
3943
- const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
3957
+ const textureInfo = typeof texture === 'number' ?
3958
+ textureInfos[texture] : texture;
3944
3959
 
3945
3960
  // get the position of the tile
3946
- const textureInfo = textureInfos[textureIndex];
3947
- ASSERT(!!textureInfo, 'Texture not loaded');
3948
3961
  const sizePaddedX = size.x + padding*2;
3949
3962
  const sizePaddedY = size.y + padding*2;
3950
- if (typeof pos === 'number')
3963
+ let x, y;
3964
+ if (typeof index === 'number')
3951
3965
  {
3952
3966
  const cols = textureInfo.size.x / sizePaddedX |0;
3953
- ASSERT(cols > 0, 'Tile size is too big for texture');
3954
- const posX = pos % cols, posY = (pos / cols) |0;
3955
- tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
3967
+ x = index % cols;
3968
+ y = index / cols |0;
3956
3969
  }
3957
3970
  else
3958
- tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
3959
- return tileInfo;
3971
+ {
3972
+ x = index.x;
3973
+ y = index.y;
3974
+ }
3975
+ const pos = new Vector2(x*sizePaddedX + padding, y*sizePaddedY + padding);
3976
+ return new TileInfo(pos, size, textureInfo, padding, bleed);
3960
3977
  }
3961
3978
 
3962
3979
  /**
@@ -3966,24 +3983,22 @@ function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=til
3966
3983
  class TileInfo
3967
3984
  {
3968
3985
  /** Create a tile info object
3969
- * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3986
+ * @param {Vector2} [pos=vec2()] - Top left corner of tile in pixels
3970
3987
  * @param {Vector2} [size] - Size of tile in pixels
3971
- * @param {number} [textureIndex] - Texture index to use
3972
- * @param {number} [padding] - How many pixels padding around tiles
3973
- * @param {number} [bleed] - How many pixels smaller to draw tiles
3988
+ * @param {TextureInfo} [textureInfo] - Texture info to use
3989
+ * @param {number} [padding] - How many pixels padding around tiles
3990
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3974
3991
  */
3975
- constructor(pos=vec2(), size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3992
+ constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
3976
3993
  {
3977
3994
  /** @property {Vector2} - Top left corner of tile in pixels */
3978
3995
  this.pos = pos.copy();
3979
3996
  /** @property {Vector2} - Size of tile in pixels */
3980
3997
  this.size = size.copy();
3981
- /** @property {number} - Texture index to use */
3982
- this.textureIndex = textureIndex;
3983
3998
  /** @property {number} - How many pixels padding around tiles */
3984
3999
  this.padding = padding;
3985
4000
  /** @property {TextureInfo} - The texture info for this tile */
3986
- this.textureInfo = textureInfos[this.textureIndex];
4001
+ this.textureInfo = textureInfo;
3987
4002
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
3988
4003
  this.bleed = bleed;
3989
4004
  }
@@ -3993,7 +4008,7 @@ class TileInfo
3993
4008
  * @return {TileInfo}
3994
4009
  */
3995
4010
  offset(offset)
3996
- { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleed); }
4011
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed); }
3997
4012
 
3998
4013
  /** Returns a copy of this tile offset by a number of animation frames
3999
4014
  * @param {number} frame - Offset to apply in animation frames
@@ -4002,23 +4017,33 @@ class TileInfo
4002
4017
  frame(frame)
4003
4018
  {
4004
4019
  ASSERT(typeof frame === 'number');
4005
- return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
4020
+ const w = this.size.x + this.padding*2;
4021
+ const x = frame*w;
4022
+ ASSERT(x < this.textureInfo.size.x, 'frame extends beyond texture width!');
4023
+ return this.offset(new Vector2(x));
4006
4024
  }
4007
4025
 
4008
4026
  /**
4009
4027
  * Set this tile to use a full image in a texture info
4010
- * @param {TextureInfo} textureInfo
4028
+ * @param {TextureInfo} [textureInfo]
4011
4029
  * @return {TileInfo}
4012
4030
  */
4013
- setFullImage(textureInfo)
4031
+ setFullImage(textureInfo=this.textureInfo)
4014
4032
  {
4033
+ this.textureInfo = textureInfo;
4015
4034
  this.pos = new Vector2;
4016
4035
  this.size = textureInfo.size.copy();
4017
- this.textureInfo = textureInfo;
4018
- // do not use padding or bleed
4019
4036
  this.bleed = this.padding = 0;
4020
4037
  return this;
4021
4038
  }
4039
+
4040
+ /**
4041
+ * Returns a tile info for an index using this tile as refrence
4042
+ * @param {Vector2|number} [index=0]
4043
+ * @return {TileInfo}
4044
+ */
4045
+ tile(index)
4046
+ { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
4022
4047
  }
4023
4048
 
4024
4049
  /**
@@ -4059,19 +4084,19 @@ class TextureInfo
4059
4084
  ///////////////////////////////////////////////////////////////////////////////
4060
4085
  // Drawing functions
4061
4086
 
4062
- /** Draw textured tile centered in world space, with color applied if using WebGL
4063
- * @param {Vector2} pos - Center of the tile in world space
4064
- * @param {Vector2} [size=(1,1)] - Size of the tile in world space
4065
- * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
4066
- * @param {Color} [color=(1,1,1,1)] - Color to modulate with
4067
- * @param {number} [angle] - Angle to rotate by
4068
- * @param {boolean} [mirror] - Is image flipped along the Y axis?
4069
- * @param {Color} [additiveColor] - Additive color to be applied if any
4087
+ /** Draw textured tile centered in world space
4088
+ * @param {Vector2} pos - Center of the tile in world space
4089
+ * @param {Vector2} [size=vec2(1)] - Size of the tile in world space
4090
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
4091
+ * @param {Color} [color=WHITE] - Color to modulate with
4092
+ * @param {number} [angle] - Angle to rotate by
4093
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
4094
+ * @param {Color} [additiveColor] - Additive color to be applied if any
4070
4095
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
4071
4096
  * @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
4072
4097
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
4073
4098
  * @memberof Draw */
4074
- function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
4099
+ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
4075
4100
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
4076
4101
  {
4077
4102
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4081,7 +4106,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
4081
4106
  ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
4082
4107
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4083
4108
 
4084
- const textureInfo = tileInfo && tileInfo.textureInfo;
4109
+ const textureInfo = tileInfo?.textureInfo;
4085
4110
  const bleed = tileInfo?.bleed ?? 0;
4086
4111
  if (useWebGL && glEnable)
4087
4112
  {
@@ -4146,8 +4171,8 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
4146
4171
 
4147
4172
  /** Draw colored rect centered on pos
4148
4173
  * @param {Vector2} pos
4149
- * @param {Vector2} [size=(1,1)]
4150
- * @param {Color} [color=(1,1,1,1)]
4174
+ * @param {Vector2} [size=vec2(1)]
4175
+ * @param {Color} [color=WHITE]
4151
4176
  * @param {number} [angle]
4152
4177
  * @param {boolean} [useWebGL=glEnable]
4153
4178
  * @param {boolean} [screenSpace]
@@ -4160,9 +4185,9 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
4160
4185
 
4161
4186
  /** Draw a rect centered on pos with a gradient from top to bottom
4162
4187
  * @param {Vector2} pos
4163
- * @param {Vector2} [size=(1,1)]
4164
- * @param {Color} [colorTop=(1,1,1,1)]
4165
- * @param {Color} [colorBottom=(0,0,0,1)]
4188
+ * @param {Vector2} [size=vec2(1)]
4189
+ * @param {Color} [colorTop=WHITE]
4190
+ * @param {Color} [colorBottom=BLACK]
4166
4191
  * @param {number} [angle]
4167
4192
  * @param {boolean} [useWebGL=glEnable]
4168
4193
  * @param {boolean} [screenSpace]
@@ -4224,9 +4249,9 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
4224
4249
  /** Draw connected lines between a series of points
4225
4250
  * @param {Array<Vector2>} points
4226
4251
  * @param {number} [width]
4227
- * @param {Color} [color=(1,1,1,1)]
4252
+ * @param {Color} [color=WHITE]
4228
4253
  * @param {boolean} [wrap] - Should the last point connect to the first?
4229
- * @param {Vector2} [pos=(0,0)] - Offset to apply
4254
+ * @param {Vector2} [pos=vec2()] - Offset to apply
4230
4255
  * @param {number} [angle] - Angle to rotate by
4231
4256
  * @param {boolean} [useWebGL=glEnable]
4232
4257
  * @param {boolean} [screenSpace]
@@ -4261,13 +4286,9 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
4261
4286
  for (let i=0; i<points.length; ++i)
4262
4287
  {
4263
4288
  const point = points[i];
4264
- if (i)
4265
- context.lineTo(point.x, point.y);
4266
- else
4267
- context.moveTo(point.x, point.y);
4289
+ context.lineTo(point.x, point.y);
4268
4290
  }
4269
- if (wrap)
4270
- context.closePath();
4291
+ wrap && context.closePath();
4271
4292
  context.stroke();
4272
4293
  }, screenSpace, context);
4273
4294
  }
@@ -4277,8 +4298,8 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
4277
4298
  * @param {Vector2} posA
4278
4299
  * @param {Vector2} posB
4279
4300
  * @param {number} [width]
4280
- * @param {Color} [color=(1,1,1,1)]
4281
- * @param {Vector2} [pos=(0,0)] - Offset to apply
4301
+ * @param {Color} [color=WHITE]
4302
+ * @param {Vector2} [pos=vec2()] - Offset to apply
4282
4303
  * @param {number} [angle] - Angle to rotate by
4283
4304
  * @param {boolean} [useWebGL=glEnable]
4284
4305
  * @param {boolean} [screenSpace]
@@ -4297,12 +4318,12 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
4297
4318
 
4298
4319
  /** Draw colored regular polygon using passed in number of sides
4299
4320
  * @param {Vector2} pos
4300
- * @param {Vector2} [size=(1,1)]
4321
+ * @param {Vector2} [size=vec2(1)]
4301
4322
  * @param {number} [sides]
4302
- * @param {Color} [color=(1,1,1,1)]
4323
+ * @param {Color} [color=WHITE]
4303
4324
  * @param {number} [angle]
4304
4325
  * @param {number} [lineWidth]
4305
- * @param {Color} [lineColor=(0,0,0,1)]
4326
+ * @param {Color} [lineColor=BLACK]
4306
4327
  * @param {boolean} [useWebGL=glEnable]
4307
4328
  * @param {boolean} [screenSpace]
4308
4329
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -4325,10 +4346,10 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
4325
4346
 
4326
4347
  /** Draw colored polygon using passed in points
4327
4348
  * @param {Array<Vector2>} points - Array of Vector2 points
4328
- * @param {Color} [color=(1,1,1,1)]
4349
+ * @param {Color} [color=WHITE]
4329
4350
  * @param {number} [lineWidth]
4330
- * @param {Color} [lineColor=(0,0,0,1)]
4331
- * @param {Vector2} [pos=(0,0)] - Offset to apply
4351
+ * @param {Color} [lineColor=BLACK]
4352
+ * @param {Vector2} [pos=vec2()] - Offset to apply
4332
4353
  * @param {number} [angle] - Angle to rotate by
4333
4354
  * @param {boolean} [useWebGL=glEnable]
4334
4355
  * @param {boolean} [screenSpace]
@@ -4375,11 +4396,11 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
4375
4396
 
4376
4397
  /** Draw colored ellipse using passed in point
4377
4398
  * @param {Vector2} pos
4378
- * @param {Vector2} [size=(1,1)] - Width and height diameter
4379
- * @param {Color} [color=(1,1,1,1)]
4399
+ * @param {Vector2} [size=vec2(1)] - Width and height diameter
4400
+ * @param {Color} [color=WHITE]
4380
4401
  * @param {number} [angle]
4381
4402
  * @param {number} [lineWidth]
4382
- * @param {Color} [lineColor=(0,0,0,1)]
4403
+ * @param {Color} [lineColor=BLACK]
4383
4404
  * @param {boolean} [useWebGL=glEnable]
4384
4405
  * @param {boolean} [screenSpace]
4385
4406
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -4391,9 +4412,12 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
4391
4412
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
4392
4413
  ASSERT(isNumber(angle), 'angle must be a number');
4393
4414
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
4394
- ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
4415
+ ASSERT(lineWidth >= 0, 'lineWidth must be a positive value or 0');
4395
4416
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4396
4417
 
4418
+ // clamp line width to prevent artifacts
4419
+ lineWidth = clamp(lineWidth, 0, Math.min(size.x, size.y));
4420
+
4397
4421
  if (useWebGL && glEnable)
4398
4422
  {
4399
4423
  // draw as a regular polygon
@@ -4421,9 +4445,9 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
4421
4445
  /** Draw colored circle using passed in point
4422
4446
  * @param {Vector2} pos
4423
4447
  * @param {number} [size=1] - Diameter
4424
- * @param {Color} [color=(1,1,1,1)]
4448
+ * @param {Color} [color=WHITE]
4425
4449
  * @param {number} [lineWidth=0]
4426
- * @param {Color} [lineColor=(0,0,0,1)]
4450
+ * @param {Color} [lineColor=BLACK]
4427
4451
  * @param {boolean} [useWebGL=glEnable]
4428
4452
  * @param {boolean} [screenSpace]
4429
4453
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -4478,9 +4502,9 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
4478
4502
  * @param {string|number} text
4479
4503
  * @param {Vector2} pos
4480
4504
  * @param {number} [size]
4481
- * @param {Color} [color=(1,1,1,1)]
4505
+ * @param {Color} [color=WHITE]
4482
4506
  * @param {number} [lineWidth]
4483
- * @param {Color} [lineColor=(0,0,0,1)]
4507
+ * @param {Color} [lineColor=BLACK]
4484
4508
  * @param {CanvasTextAlign} [textAlign='center']
4485
4509
  * @param {string} [font=fontDefault]
4486
4510
  * @param {string} [fontStyle]
@@ -4504,10 +4528,10 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
4504
4528
  * Automatically splits new lines into rows
4505
4529
  * @param {string|number} text
4506
4530
  * @param {Vector2} pos
4507
- * @param {number} [size]
4508
- * @param {Color} [color=(1,1,1,1)]
4531
+ * @param {number} size
4532
+ * @param {Color} [color=WHITE]
4509
4533
  * @param {number} [lineWidth]
4510
- * @param {Color} [lineColor=(0,0,0,1)]
4534
+ * @param {Color} [lineColor=BLACK]
4511
4535
  * @param {CanvasTextAlign} [textAlign]
4512
4536
  * @param {string} [font=fontDefault]
4513
4537
  * @param {string} [fontStyle]
@@ -4515,7 +4539,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
4515
4539
  * @param {number} [angle]
4516
4540
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4517
4541
  * @memberof Draw */
4518
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
4542
+ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
4519
4543
  {
4520
4544
  ASSERT(isString(text), 'text must be a string');
4521
4545
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4639,7 +4663,7 @@ function worldToScreenDelta(worldDelta)
4639
4663
 
4640
4664
  /** Convert screen space transform to world space
4641
4665
  * @param {Vector2} screenPos
4642
- * @param {Vector2} screenSize
4666
+ * @param {Vector2} screenSize
4643
4667
  * @param {number} [screenAngle]
4644
4668
  * @return {[Vector2, Vector2, number]} - [pos, size, angle]
4645
4669
  * @memberof Draw */
@@ -4700,10 +4724,9 @@ function isOnScreen(pos, size=0)
4700
4724
  * @param {boolean} [additive]
4701
4725
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4702
4726
  * @memberof Draw */
4703
- function setBlendMode(additive=false, context)
4727
+ function setBlendMode(additive=false, context=drawContext)
4704
4728
  {
4705
4729
  glAdditive = additive;
4706
- context ||= drawContext;
4707
4730
  context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
4708
4731
  }
4709
4732
 
@@ -4718,7 +4741,6 @@ function combineCanvases()
4718
4741
  workContext.fillRect(0,0,w,h); // remove background alpha
4719
4742
  glCopyToContext(workContext);
4720
4743
  workContext.drawImage(mainCanvas, 0, 0);
4721
- mainCanvas.width |= 0;
4722
4744
  mainContext.drawImage(workCanvas, 0, 0);
4723
4745
  }
4724
4746
 
@@ -4823,17 +4845,22 @@ function setCursor(cursorStyle = 'auto')
4823
4845
 
4824
4846
  ///////////////////////////////////////////////////////////////////////////////
4825
4847
 
4848
+ /** Engine font image, 8x8 font provided by the engine
4849
+ * @type {FontImage}
4850
+ * @memberof Draw */
4826
4851
  let engineFontImage;
4827
4852
 
4828
4853
  /**
4829
- * Font Image Object - Draw text on a 2D canvas by using characters in an image
4854
+ * Font Image Object - Draw text by using tiles in an image
4830
4855
  * - 96 characters (from space to tilde) are stored in an image
4831
- * - Uses a default 8x8 font if none is supplied
4832
- * - You can also use fonts from the main tile sheet
4856
+ * - A 8x8 default engine font is supplied for general use
4857
+ * - This system is WebGL enabled for fast text rendering
4858
+ * - Fonts can also be colored and scaled along each axis
4859
+ *
4833
4860
  * @memberof Draw
4834
4861
  * @example
4835
4862
  * // use built in font
4836
- * const font = new FontImage;
4863
+ * const font = engineFontImage;
4837
4864
  *
4838
4865
  * // draw text
4839
4866
  * font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
@@ -4841,70 +4868,112 @@ let engineFontImage;
4841
4868
  class FontImage
4842
4869
  {
4843
4870
  /** Create an image font
4844
- * @param {HTMLImageElement} [image] - Image for the font, default if undefined
4845
- * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
4846
- * @param {Vector2} [paddingSize=(0,1)] - How much space between characters
4871
+ * @param {TileInfo} tileInfo - Tile info of first characeter in font
4847
4872
  */
4848
- constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
4873
+ constructor(tileInfo)
4849
4874
  {
4850
- // load default font image
4851
- if (!image && !engineFontImage)
4852
- {
4853
- engineFontImage = new Image;
4854
- engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
4855
- }
4856
-
4857
- this.image = image || engineFontImage;
4858
- this.tileSize = tileSize;
4859
- this.paddingSize = paddingSize;
4875
+ ASSERT(!!tileInfo, 'tileInfo is required for FontImage');
4876
+
4877
+ /** @property {TileInfo} - Tile info for the font */
4878
+ this.tileInfo = tileInfo.frame(0);
4860
4879
  }
4861
4880
 
4862
4881
  /** Draw text in world space using the image font
4863
- * @param {string|number} text
4882
+ * @param {string|number} text
4864
4883
  * @param {Vector2} pos
4865
- * @param {number} [scale=.25]
4866
- * @param {boolean} [center]
4867
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4884
+ * @param {Vector2|number} [size]
4885
+ * @param {boolean} [center=true]
4886
+ * @param {Color} [color=WHITE]
4887
+ * @param {boolean} [useWebGL=glEnable]
4888
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4868
4889
  */
4869
- drawText(text, pos, scale=1, center, context=drawContext)
4890
+ drawText(text, pos, size=1, center, color, useWebGL, context)
4870
4891
  {
4871
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
4892
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
4893
+
4894
+ if (typeof size === 'number')
4895
+ {
4896
+ // if size is a number, make it a vector
4897
+ ASSERT(size > 0);
4898
+ size *= cameraScale;
4899
+ size = new Vector2(size, size);
4900
+ }
4901
+ else
4902
+ size = size.scale(cameraScale);
4903
+ this.drawTextScreen(text, worldToScreen(pos), size, center, color, useWebGL, context);
4872
4904
  }
4873
4905
 
4874
4906
  /** Draw text in screen space using the image font
4875
- * @param {string|number} text
4907
+ * @param {string|number} text
4876
4908
  * @param {Vector2} pos
4877
- * @param {number} [scale]
4909
+ * @param {Vector2|number} size
4878
4910
  * @param {boolean} [center]
4879
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4911
+ * @param {Color} [color=WHITE]
4912
+ * @param {boolean} [useWebGL=glEnable]
4913
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4880
4914
  */
4881
- drawTextScreen(text, pos, scale=4, center=true, context=drawContext)
4915
+ drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
4882
4916
  {
4883
- context.save();
4884
- const size = this.tileSize;
4885
- const drawSize = size.add(this.paddingSize).scale(scale);
4886
- const cols = this.image.width / this.tileSize.x |0;
4887
- (text+'').split('\n').forEach((line, i)=>
4917
+ ASSERT(isString(text), 'text must be a string');
4918
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4919
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
4920
+ ASSERT(isColor(color), 'color must be a color');
4921
+
4922
+ // if size is a number, make it a vector
4923
+ size = typeof size === 'number' ? new Vector2(size, size) : size;
4924
+
4925
+ // precache objects for drawing
4926
+ const drawPos = new Vector2;
4927
+ const tileInfo = this.tileInfo;
4928
+ const padding = tileInfo.padding;
4929
+ const sizePaddedX = tileInfo.size.x + padding*2;
4930
+ const sizePaddedY = tileInfo.size.y + padding*2;
4931
+ const cols = tileInfo.textureInfo.size.x / sizePaddedX |0;
4932
+
4933
+ // draw each line of text
4934
+ (text+'').split('\n').forEach((line, j)=>
4888
4935
  {
4889
- const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
4890
- for (let j=line.length; j--;)
4936
+ const centerOffset = center ? (line.length-1) * size.x / 2 : 0;
4937
+ for (let i=line.length; i--;)
4891
4938
  {
4892
- // draw each character
4893
- let charCode = line[j].charCodeAt(0);
4894
- if (charCode < 32 || charCode > 127)
4895
- charCode = 127; // unknown character
4896
-
4897
- // get the character source location and draw it
4898
- const tile = charCode - 32;
4899
- const x = tile % cols;
4900
- const y = tile / cols |0;
4901
- const drawPos = pos.add(vec2(j,i).multiply(drawSize));
4902
- context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
4903
- drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
4939
+ // get the character index
4940
+ const charCode = line.charCodeAt(i);
4941
+ const index = charCode < 32 || charCode > 127 ?
4942
+ 95 : charCode - 32; // handle out of range characters
4943
+
4944
+ // get the position of the tile
4945
+ const x = index % cols;
4946
+ const y = index / cols |0;
4947
+ tileInfo.pos.x = x*sizePaddedX + padding;
4948
+ tileInfo.pos.y = y*sizePaddedY + padding;
4949
+
4950
+ // draw the tile
4951
+ drawPos.x = pos.x + i * size.x - centerOffset |0;
4952
+ drawPos.y = pos.y + j * size.y |0;
4953
+ drawTile(drawPos, size, tileInfo, color, 0, false, undefined, useWebGL, true, context);
4904
4954
  }
4905
4955
  });
4906
- context.restore();
4907
4956
  }
4957
+ }
4958
+
4959
+ // load engine font, called automatically on startup
4960
+ function fontImageInit()
4961
+ {
4962
+ return new Promise(resolve =>
4963
+ {
4964
+ // create the engine font
4965
+ const image = new Image;
4966
+ image.onerror = image.onload = ()=>
4967
+ {
4968
+ const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
4969
+ const textureInfo = new TextureInfo(image);
4970
+ const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
4971
+ engineFontImage = new FontImage(tileInfo);
4972
+ resolve();
4973
+ }
4974
+ image.crossOrigin = 'anonymous';
4975
+ 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==';
4976
+ });
4908
4977
  }
4909
4978
  /**
4910
4979
  * LittleJS Input System
@@ -4961,6 +5030,10 @@ let inputPreventDefault = true;
4961
5030
  * @memberof Input */
4962
5031
  let gamepadPrimary = 0;
4963
5032
 
5033
+ /** True if a touch device has been detected
5034
+ * @memberof Input */
5035
+ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
5036
+
4964
5037
  /** Prevents input continuing to the default browser handling
4965
5038
  * This is useful to disable for html menus so the browser can handle input normally
4966
5039
  * @param {boolean} preventDefault
@@ -5159,10 +5232,6 @@ function gamepadStickCount(gamepad=gamepadPrimary)
5159
5232
  return gamepadStickData[gamepad]?.length ?? 0;
5160
5233
  }
5161
5234
 
5162
- /** True if a touch device has been detected
5163
- * @memberof Input */
5164
- const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
5165
-
5166
5235
  ///////////////////////////////////////////////////////////////////////////////
5167
5236
 
5168
5237
  /** Pulse the vibration hardware if it exists
@@ -5171,7 +5240,7 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
5171
5240
  function vibrate(pattern=100)
5172
5241
  {
5173
5242
  ASSERT(isNumber(pattern) || isArray(pattern), 'pattern must be a number or array');
5174
- vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern);
5243
+ vibrateEnable && !headlessMode && navigator?.vibrate?.(pattern);
5175
5244
  }
5176
5245
 
5177
5246
  /** Cancel any ongoing vibration
@@ -5289,8 +5358,8 @@ function inputInit()
5289
5358
  }
5290
5359
  function onMouseUp(e)
5291
5360
  {
5292
- if (isTouchDevice && touchInputEnable)
5293
- return;
5361
+ if (isTouchDevice && touchInputEnable) return;
5362
+
5294
5363
  inputData[0][e.button] = (inputData[0][e.button]&2) | 4;
5295
5364
  }
5296
5365
  function onMouseMove(e)
@@ -5314,9 +5383,9 @@ function inputInit()
5314
5383
  function touchInputInit()
5315
5384
  {
5316
5385
  // add non passive touch event listeners
5317
- document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
5318
- document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
5319
- document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
5386
+ document.addEventListener('touchstart', (e)=> handleTouch(e), { passive: false });
5387
+ document.addEventListener('touchmove', (e)=> handleTouch(e), { passive: false });
5388
+ document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
5320
5389
 
5321
5390
  // handle all touch events the same way
5322
5391
  let wasTouching;
@@ -5476,8 +5545,7 @@ function inputUpdate()
5476
5545
  // update touch gamepad if enabled
5477
5546
  if (touchGamepadEnable && isTouchDevice)
5478
5547
  {
5479
- if (!touchGamepadTimer.isSet())
5480
- return;
5548
+ if (!touchGamepadTimer.isSet()) return;
5481
5549
 
5482
5550
  // read virtual analog stick
5483
5551
  gamepadPrimary = 0; // touch gamepad uses index 0
@@ -5515,17 +5583,15 @@ function inputUpdate()
5515
5583
  }
5516
5584
 
5517
5585
  // return if gamepads are disabled or not supported
5518
- if (!gamepadsEnable || !navigator || !navigator.getGamepads)
5519
- return;
5586
+ if (!gamepadsEnable || !navigator || !navigator.getGamepads) return;
5520
5587
 
5521
5588
  // only poll gamepads when focused or in debug mode
5522
- if (!debug && !document.hasFocus())
5523
- return;
5589
+ if (!debug && !document.hasFocus()) return;
5524
5590
 
5525
5591
  // poll gamepads
5526
5592
  const maxGamepads = 8;
5527
5593
  const gamepads = navigator.getGamepads();
5528
- const gamepadCount = min(maxGamepads, gamepads.length)
5594
+ const gamepadCount = min(maxGamepads, gamepads.length);
5529
5595
  for (let i=0; i<gamepadCount; ++i)
5530
5596
  {
5531
5597
  // get or create gamepad data
@@ -5557,8 +5623,7 @@ function inputUpdate()
5557
5623
  data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
5558
5624
 
5559
5625
  // check for any input on this gamepad, analog must be full press
5560
- if (button.pressed)
5561
- if (!button.value || button.value > .9)
5626
+ if (button.pressed && (!button.value || button.value > .9))
5562
5627
  hadInput = true;
5563
5628
  }
5564
5629
 
@@ -5587,7 +5652,7 @@ function inputUpdate()
5587
5652
  }
5588
5653
 
5589
5654
  // copy dpad to left analog stick when pressed
5590
- if (gamepadDirectionEmulateStick && !dpad.isZero())
5655
+ if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
5591
5656
  sticks[0] = dpad.clampLength();
5592
5657
  }
5593
5658
 
@@ -5616,13 +5681,11 @@ function inputRender()
5616
5681
  function touchGamepadRender()
5617
5682
  {
5618
5683
  if (!touchInputEnable || !isTouchDevice || headlessMode) return;
5619
- if (!touchGamepadEnable || !touchGamepadTimer.isSet())
5620
- return;
5684
+ if (!touchGamepadEnable || !touchGamepadTimer.isSet()) return;
5621
5685
 
5622
5686
  // fade off when not touching or paused
5623
5687
  const alpha = percent(touchGamepadTimer.get(), 4, 3);
5624
- if (!alpha || paused)
5625
- return;
5688
+ if (!alpha || paused) return;
5626
5689
 
5627
5690
  // setup the canvas
5628
5691
  const context = mainContext;
@@ -5733,29 +5796,46 @@ function audioInit()
5733
5796
  ///////////////////////////////////////////////////////////////////////////////
5734
5797
 
5735
5798
  /**
5736
- * Sound Object - Stores a sound for later use and can be played positionally
5799
+ * Sound Object - Stores a sound for later
5800
+ * - this can be used to load and play wave, mp3, and ogg files
5801
+ * - it can also create sounds using the ZzFX sound generator
5802
+ * - can attenuate and apply stereo panning to sounds
5803
+ * - sound instance control with pause/resume capability
5737
5804
  *
5738
5805
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
5739
5806
  * @memberof Audio
5740
5807
  * @example
5741
- * // create a sound
5808
+ * // load an audio asset file
5809
+ * const sound_example = new Sound('sound.mp3');
5810
+ *
5811
+ * // create a zzfx sound
5742
5812
  * const sound_example = new Sound([.5,.5]);
5743
5813
  *
5744
- * // play the sound
5814
+ * // play a sound
5745
5815
  * sound_example.play();
5746
5816
  */
5747
5817
  class Sound
5748
5818
  {
5749
- /** Create a sound object and cache the zzfx samples for later use
5750
- * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
5819
+ /**
5820
+ * @callback SoundLoadCallback - Function called when sound is loaded
5821
+ * @param {Sound} sound
5822
+ * @memberof Audio
5823
+ */
5824
+
5825
+ /** Create a sound object and cache the audio for later use
5826
+ * @param {string|Array} [asset] - Filename of audio file or zzfx array
5827
+ * @param {number} [randomness] - How much to randomize frequency each time sound plays, for zzfx sounds the zzfx default is used if undefined
5751
5828
  * @param {number} [range=soundDefaultRange] - World space max range of sound
5752
5829
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5830
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5753
5831
  */
5754
- constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
5832
+ constructor(asset, randomness, range=soundDefaultRange, taper=soundDefaultTaper, onloadCallback)
5755
5833
  {
5756
5834
  if (!soundEnable || headlessMode) return;
5757
5835
 
5758
- ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
5836
+ ASSERT(!asset || isArray(asset) || isString(asset), 'asset must be a file name or zzfx array');
5837
+ ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
5838
+ ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
5759
5839
  ASSERT(isNumber(range), 'range must be a number');
5760
5840
  ASSERT(isNumber(taper), 'taper must be a number');
5761
5841
 
@@ -5764,24 +5844,35 @@ class Sound
5764
5844
  /** @property {number} - At what percentage of range should it start tapering */
5765
5845
  this.taper = taper;
5766
5846
  /** @property {number} - How much to randomize frequency each time sound plays */
5767
- this.randomness = 0;
5847
+ this.randomness = randomness ?? 0;
5768
5848
  /** @property {number} - Sample rate for this sound */
5769
5849
  this.sampleRate = audioDefaultSampleRate;
5770
5850
  /** @property {number} - Percentage of this sound currently loaded */
5771
5851
  this.loadedPercent = 0;
5852
+ /** @property {SoundLoadCallback} - function to call when sound is loaded */
5853
+ this.onloadCallback = onloadCallback;
5772
5854
 
5773
- // generate zzfx sound now for fast playback
5774
- if (zzfxSound)
5855
+ if (Array.isArray(asset))
5775
5856
  {
5857
+ // generate zzfx sound
5858
+ const zzfxSound = asset;
5859
+
5776
5860
  // remove randomness so it can be applied on playback
5777
- const randomnessIndex = 1, defaultRandomness = .05;
5778
- this.randomness = zzfxSound[randomnessIndex] !== undefined ?
5779
- zzfxSound[randomnessIndex] : defaultRandomness;
5861
+ const defaultRandomness = randomness ?? .05;
5862
+ const randomnessIndex = 1;
5863
+ this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
5780
5864
  zzfxSound[randomnessIndex] = 0;
5781
5865
 
5782
5866
  // generate the zzfx samples
5783
5867
  this.sampleChannels = [zzfxG(...zzfxSound)];
5784
5868
  this.loadedPercent = 1;
5869
+ onloadCallback?.(this);
5870
+ }
5871
+ else if (typeof asset === 'string')
5872
+ {
5873
+ // load the audio file
5874
+ const filename = asset;
5875
+ this.loadSound(filename);
5785
5876
  }
5786
5877
  }
5787
5878
 
@@ -5833,7 +5924,7 @@ class Sound
5833
5924
  * @param {number} [volume] - Volume to play the music at
5834
5925
  * @param {boolean} [loop] - Should the music loop?
5835
5926
  * @param {boolean} [paused] - Should the music start paused
5836
- * @return {SoundInstance} - The audio source node
5927
+ * @return {SoundInstance} - The sound instance
5837
5928
  */
5838
5929
  playMusic(volume=1, loop=true, paused=false)
5839
5930
  { return this.play(undefined, volume, 1, 0, loop, paused); }
@@ -5843,7 +5934,7 @@ class Sound
5843
5934
  * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
5844
5935
  * @param {Vector2} [pos] - World space position to play the sound if any
5845
5936
  * @param {number} [volume=1] - How much to scale volume by
5846
- * @return {SoundInstance} - The audio source node
5937
+ * @return {SoundInstance} - The sound instance
5847
5938
  */
5848
5939
  playNote(semitoneOffset=0, pos, volume)
5849
5940
  {
@@ -5856,57 +5947,14 @@ class Sound
5856
5947
  * @return {number} - How long the sound is in seconds (undefined if loading)
5857
5948
  */
5858
5949
  getDuration()
5859
- { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
5950
+ { return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
5860
5951
 
5861
5952
  /** Check if sound is loaded, for sounds fetched from a url
5862
5953
  * @return {boolean} - True if sound is loaded and ready to play
5863
5954
  */
5864
5955
  isLoaded() { return this.loadedPercent === 1; }
5865
- }
5866
-
5867
- ///////////////////////////////////////////////////////////////////////////////
5868
-
5869
- /**
5870
- * Sound Wave Object - Loads and stores an audio file for later use
5871
- * - this can be used to load and play wave, mp3, and ogg files
5872
- * @extends Sound
5873
- * @memberof Audio
5874
- * @example
5875
- * // load an audio asset file
5876
- * const sound_example = new SoundWave('sound.mp3');
5877
- *
5878
- * // play the sound
5879
- * sound_example.play();
5880
- */
5881
- class SoundWave extends Sound
5882
- {
5883
- /**
5884
- * @callback SoundLoadCallback - Function called when sound is loaded
5885
- * @param {SoundWave} sound
5886
- * @memberof Audio
5887
- */
5888
5956
 
5889
- /** Create a sound object and cache the wave file for later use
5890
- * @param {string} filename - Filename of audio file to load
5891
- * @param {number} [randomness] - How much to randomize frequency each time sound plays
5892
- * @param {number} [range=soundDefaultRange] - World space max range of sound
5893
- * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5894
- * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5895
- */
5896
- constructor(filename, randomness=0, range, taper, onloadCallback)
5897
- {
5898
- super(undefined, range, taper);
5899
- if (!soundEnable || headlessMode) return;
5900
- ASSERT(!filename || isString(filename), 'filename must be a string');
5901
- ASSERT(isNumber(randomness), 'randomness must be a number');
5902
-
5903
- /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
5904
- this.onloadCallback = onloadCallback;
5905
- this.randomness = randomness;
5906
- filename && this.loadSound(filename);
5907
- }
5908
-
5909
- /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
5957
+ /** Loads a sound from a URL and decodes it into sample data.
5910
5958
  * @param {string} filename
5911
5959
  * @return {Promise<void>} */
5912
5960
  async loadSound(filename)
@@ -5948,8 +5996,7 @@ class SoundWave extends Sound
5948
5996
  this.sampleRate = audioBuffer.sampleRate;
5949
5997
  this.sampleChannels = sampleChannels;
5950
5998
  this.loadedPercent = 1;
5951
- if (this.onloadCallback)
5952
- this.onloadCallback(this);
5999
+ this.onloadCallback?.(this);
5953
6000
  }
5954
6001
  }
5955
6002
 
@@ -6152,7 +6199,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
6152
6199
 
6153
6200
  /** Stop all queued speech
6154
6201
  * @memberof Audio */
6155
- function speakStop() {speechSynthesis && speechSynthesis.cancel();}
6202
+ function speakStop() {speechSynthesis?.cancel();}
6156
6203
 
6157
6204
  /** Get frequency of a note on a musical scale
6158
6205
  * @param {number} semitoneOffset - How many semitones away from the root note
@@ -6411,7 +6458,7 @@ function tileCollisionGetData(pos)
6411
6458
 
6412
6459
  /** Check if a tile layer collides with another object
6413
6460
  * @param {Vector2} pos
6414
- * @param {Vector2} [size=(0,0)]
6461
+ * @param {Vector2} [size=vec2()]
6415
6462
  * @param {EngineObject} [object] - An object or undefined for generic test
6416
6463
  * @param {boolean} [solidOnly] - Only check solid layers if true
6417
6464
  * @return {TileCollisionLayer}
@@ -6529,20 +6576,20 @@ function tileLayersLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLa
6529
6576
  class TileLayerData
6530
6577
  {
6531
6578
  /** Create a tile layer data object, one for each tile in a TileLayer
6532
- * @param {number} [tile] - The tile to use, untextured if undefined
6579
+ * @param {number} [tile] - The tile to use, untextured if undefined
6533
6580
  * @param {number} [direction] - Integer direction of tile, in 90 degree increments
6534
- * @param {boolean} [mirror] - If the tile should be mirrored along the x axis
6535
- * @param {Color} [color] - Color of the tile */
6581
+ * @param {boolean} [mirror] - If the tile should be mirrored along the x axis
6582
+ * @param {Color} [color] - Color of the tile */
6536
6583
  constructor(tile, direction=0, mirror=false, color=new Color)
6537
6584
  {
6538
- /** @property {number} - The tile to use, untextured if undefined */
6539
- this.tile = tile;
6540
- /** @property {number} - Integer direction of tile, in 90 degree increments */
6585
+ /** @property {number} - The tile to use, untextured if undefined */
6586
+ this.tile = tile;
6587
+ /** @property {number} - Integer direction of tile, in 90 degree increments */
6541
6588
  this.direction = direction;
6542
6589
  /** @property {boolean} - If the tile should be mirrored along the x axis */
6543
- this.mirror = mirror;
6544
- /** @property {Color} - Color of the tile */
6545
- this.color = color.copy();
6590
+ this.mirror = mirror;
6591
+ /** @property {Color} - Color of the tile */
6592
+ this.color = color.copy();
6546
6593
  }
6547
6594
 
6548
6595
  /** Set this tile to clear, it will not be rendered */
@@ -6553,7 +6600,7 @@ class TileLayerData
6553
6600
  /**
6554
6601
  * Canvas Layer - cached off screen rendering system
6555
6602
  * - Contains an offscreen canvas that can be rendered to
6556
- * - WebGL rendering is optional, call useWebGL to enable
6603
+ * - WebGL rendering is optional, call updateWebGL to enable/update
6557
6604
  * @extends EngineObject
6558
6605
  * @memberof TileLayers
6559
6606
  * @example
@@ -6562,29 +6609,27 @@ class TileLayerData
6562
6609
  class CanvasLayer extends EngineObject
6563
6610
  {
6564
6611
  /** Create a canvas layer object
6565
- * @param {Vector2} [position] - World space position of the layer
6612
+ * @param {Vector2} [pos] - World space position of the layer
6566
6613
  * @param {Vector2} [size] - World space size of the layer
6567
6614
  * @param {number} [angle] - Angle the layer is rotated by
6568
6615
  * @param {number} [renderOrder] - Objects sorted by renderOrder
6569
6616
  * @param {Vector2} [canvasSize] - Default size of canvas, can be changed later
6617
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6570
6618
  */
6571
- constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512))
6619
+ constructor(pos, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=true)
6572
6620
  {
6573
6621
  ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
6574
- super(position, size, undefined, angle, WHITE, renderOrder);
6622
+ super(pos, size, undefined, angle, WHITE, renderOrder);
6575
6623
 
6576
6624
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
6577
6625
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
6578
6626
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
6579
6627
  this.context = this.canvas?.getContext('2d');
6580
6628
  /** @property {TextureInfo} - Texture info to use for this object rendering */
6581
- const useWebGL = false; // do not use webgl by default
6582
6629
  this.textureInfo = new TextureInfo(this.canvas, useWebGL);
6583
- /** @property {boolean} - True if WebGL texture needs to be refreshed */
6584
- this.refreshWebGL = false;
6585
6630
 
6586
6631
  // disable physics by default
6587
- this.mass = this.gravityScale = this.friction = this.restitution = 0;
6632
+ this.mass = 0;
6588
6633
  }
6589
6634
 
6590
6635
  /** Destroy this canvas layer */
@@ -6599,7 +6644,7 @@ class CanvasLayer extends EngineObject
6599
6644
  // Render the layer, called automatically by the engine
6600
6645
  render()
6601
6646
  {
6602
- this.draw(this.pos, this.size, this.angle, this.color, this.mirror, this.additiveColor);
6647
+ this.draw(this.pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
6603
6648
  }
6604
6649
 
6605
6650
  /** Draw this canvas layer centered in world space, with color applied if using WebGL
@@ -6612,103 +6657,54 @@ class CanvasLayer extends EngineObject
6612
6657
  * @param {boolean} [screenSpace] - If true the pos and size are in screen space
6613
6658
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
6614
6659
  * @memberof Draw */
6615
- draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
6660
+ draw(pos, size, color=WHITE, angle=0, mirror=false, additiveColor, screenSpace=false, context)
6616
6661
  {
6617
- const useWebGL = glEnable && this.textureInfo.hasWebGL();
6618
- if (useWebGL && this.refreshWebGL)
6619
- {
6620
- // update the WebGL texture
6621
- this.textureInfo.createWebGLTexture();
6622
- this.refreshWebGL = false;
6623
- }
6624
-
6625
6662
  // draw the canvas layer as a single tile that uses the whole texture
6626
6663
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
6664
+ const useWebGL = this.hasWebGL();
6627
6665
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
6628
6666
  }
6629
6667
 
6630
- /**
6631
- * @callback Canvas2DDrawCallback - Function that draws to a canvas 2D context
6632
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
6633
- * @memberof TileLayers
6634
- */
6635
-
6636
- /** Draw onto the layer canvas in world space (bypass WebGL)
6668
+ /** Draw a tile onto the layer canvas in world space
6637
6669
  * @param {Vector2} pos
6638
- * @param {Vector2} size
6639
- * @param {number} angle
6640
- * @param {boolean} mirror
6641
- * @param {Canvas2DDrawCallback} drawFunction */
6642
- drawCanvas2D(pos, size, angle, mirror, drawFunction)
6670
+ * @param {Vector2} [size=vec2(1)]
6671
+ * @param {TileInfo} [tileInfo]
6672
+ * @param {Color} [color=WHITE]
6673
+ * @param {number} [angle]
6674
+ * @param {boolean} [mirror] */
6675
+ drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
6643
6676
  {
6644
- if (!this.context) return;
6645
-
6646
- const context = this.context;
6647
- context.save();
6648
6677
  pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
6649
6678
  size = size.multiply(this.tileInfo.size);
6650
- context.translate(pos.x, this.canvas.height - pos.y);
6651
- context.rotate(angle);
6652
- context.scale(mirror ? -size.x : size.x, size.y);
6653
- drawFunction(context);
6654
- context.restore();
6655
- }
6679
+ pos.y = this.canvas.height - pos.y;
6656
6680
 
6657
- /** Draw a tile onto the layer canvas in world space
6658
- * @param {Vector2} pos
6659
- * @param {Vector2} [size=(1,1)]
6660
- * @param {TileInfo} [tileInfo]
6661
- * @param {Color} [color=(1,1,1,1)]
6662
- * @param {number} [angle=0]
6663
- * @param {boolean} [mirror=false] */
6664
- drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle, mirror)
6665
- {
6666
- this.drawCanvas2D(pos, size, angle, mirror, (context)=>
6667
- {
6668
- const textureInfo = tileInfo && tileInfo.textureInfo;
6669
- if (textureInfo)
6670
- {
6671
- context.globalAlpha = color.a; // only alpha is supported
6672
- context.drawImage(textureInfo.image,
6673
- tileInfo.pos.x, tileInfo.pos.y,
6674
- tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
6675
- context.globalAlpha = 1;
6676
- }
6677
- else
6678
- {
6679
- // untextured
6680
- context.fillStyle = color.toString();
6681
- context.fillRect(-.5, -.5, 1, 1);
6682
- }
6683
- });
6681
+ // draw the tile onto the layer canvas
6682
+ const oldMainCanvasSize = mainCanvasSize;
6683
+ mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
6684
+ const useWebGL = this.hasWebGL();
6685
+ useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
6686
+ const drawContext = useWebGL ? undefined : this.context;
6687
+ drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
6688
+ useWebGL && glSetRenderTarget();
6689
+ mainCanvasSize = oldMainCanvasSize;
6684
6690
  }
6685
6691
 
6686
6692
  /** Draw a rectangle onto the layer canvas in world space
6687
6693
  * @param {Vector2} pos
6688
- * @param {Vector2} [size=(1,1)]
6689
- * @param {Color} [color=(1,1,1,1)]
6690
- * @param {number} [angle=0] */
6694
+ * @param {Vector2} [size=vec2(1)]
6695
+ * @param {Color} [color=WHITE]
6696
+ * @param {number} [angle] */
6691
6697
  drawRect(pos, size, color, angle)
6692
6698
  { this.drawTile(pos, size, undefined, color, angle); }
6693
6699
 
6694
- /** Create or update the WebGL texture for this layer
6695
- * @param {boolean} [enable] - enable WebGL rendering and update the texture
6696
- * @param {boolean} [immediate] - shoulkd the texture be updated immediately
6697
- */
6698
- useWebGL(enable=true, immediate=false)
6699
- {
6700
- if (!immediate && enable && this.textureInfo.hasWebGL())
6701
- {
6702
- // refresh the texture when needed
6703
- this.refreshWebGL = true;
6704
- return;
6705
- }
6700
+ /** Create WebGL texture if necessary and copy layer canvas to it */
6701
+ updateWebGL()
6702
+ { this.textureInfo.createWebGLTexture(); }
6706
6703
 
6707
- if (enable)
6708
- this.textureInfo.createWebGLTexture();
6709
- else
6710
- this.textureInfo.destroyWebGLTexture();
6711
- }
6704
+ /** Check if this layer is using WebGL
6705
+ * @return {boolean} */
6706
+ hasWebGL()
6707
+ { return glEnable && this.textureInfo.hasWebGL(); }
6712
6708
  }
6713
6709
 
6714
6710
  ///////////////////////////////////////////////////////////////////////////////
@@ -6727,59 +6723,93 @@ class CanvasLayer extends EngineObject
6727
6723
  class TileLayer extends CanvasLayer
6728
6724
  {
6729
6725
  /** Create a tile layer object
6730
- * @param {Vector2} position - World space position
6731
- * @param {Vector2} size - World space size
6732
- * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6726
+ * @param {Vector2} pos - World space position
6727
+ * @param {Vector2} size - World space size
6728
+ * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6733
6729
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6730
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6734
6731
  */
6735
- constructor(position, size, tileInfo=tile(), renderOrder=0)
6732
+ constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
6736
6733
  {
6737
6734
  const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
6738
- super(position, size, 0, renderOrder, canvasSize);
6739
-
6740
- // set tile info
6741
- this.tileInfo = tileInfo;
6742
-
6743
- // init tile data
6735
+ super(pos, size, 0, renderOrder, canvasSize, useWebGL);
6736
+
6737
+ /** @property {TileInfo} - Default tile info for layer */
6738
+ this.tileInfo = undefined;
6739
+ /** @property {Array<TileLayerData>} - Default tile info for layer */
6744
6740
  this.data = [];
6745
- for (let j = this.size.area(); j--;)
6746
- this.data.push(new TileLayerData);
6741
+ /** @property {boolean} - Is this layer using a webgl texture? */
6742
+ this.isUsingWebGL = false;
6747
6743
 
6748
6744
  if (headlessMode)
6749
6745
  {
6750
- // disable rendering
6751
- this.redraw = () => {};
6752
- this.render = () => {};
6753
- this.redrawStart = () => {};
6754
- this.redrawEnd = () => {};
6755
- this.drawTileData = () => {};
6756
- this.drawCanvas2D = () => {};
6757
- this.useWebGL = () => {};
6746
+ // disable rendering in headless mode
6747
+ this.render = ()=> {};
6748
+ this.redraw = ()=> {};
6749
+ this.redrawStart = ()=> {};
6750
+ this.redrawEnd = ()=> {};
6751
+ this.drawTileData = ()=> {};
6752
+ this.redrawTileData = ()=> {};
6753
+ this.drawLayerTile = ()=> {};
6754
+ this.drawLayerRect = ()=> {};
6755
+ this.clearLayerRect = ()=> {};
6756
+ return;
6758
6757
  }
6758
+
6759
+ if (tileInfo)
6760
+ {
6761
+ // set tile info
6762
+ this.tileInfo = tileInfo.frame(0);
6763
+ this.tileInfo.bleed = 0; // disable bleed for tile layers
6764
+ }
6765
+
6766
+ // init tile data
6767
+ for (let j = this.size.area(); j--;)
6768
+ this.data.push(new TileLayerData);
6759
6769
  }
6760
6770
 
6761
6771
  /** Set data at a given position in the array
6762
6772
  * @param {Vector2} layerPos - Local position in array
6763
- * @param {TileLayerData} data - Data to set
6773
+ * @param {TileLayerData} data - Data to set
6764
6774
  * @param {boolean} [redraw] - Force the tile to redraw if true */
6765
6775
  setData(layerPos, data, redraw=false)
6766
6776
  {
6777
+ layerPos = layerPos.floor();
6767
6778
  ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6768
6779
  ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
6769
- if (layerPos.arrayCheck(this.size))
6770
- {
6771
- this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
6772
- redraw && this.drawTileData(layerPos);
6773
- }
6780
+
6781
+ if (!layerPos.arrayCheck(this.size)) return;
6782
+ this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
6783
+
6784
+ if (!redraw) return;
6785
+ const isRedraw = drawContext === this.context;
6786
+ isRedraw ? this.drawTileData(layerPos) : this.redrawTileData(layerPos);
6774
6787
  }
6775
6788
 
6789
+ /** Clear data at a given position in the array
6790
+ * @param {Vector2} layerPos - Local position in array
6791
+ * @param {boolean} [redraw] - Force the tile to redraw if true */
6792
+ clearData(layerPos, redraw=false)
6793
+ { this.setData(layerPos, new TileLayerData, redraw) }
6794
+
6776
6795
  /** Get data at a given position in the array
6777
6796
  * @param {Vector2} layerPos - Local position in array
6778
6797
  * @return {TileLayerData} */
6779
6798
  getData(layerPos)
6780
6799
  {
6781
6800
  ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6782
- return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
6801
+ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
6802
+ }
6803
+
6804
+ // Update the tile layer, refresh texture if needed
6805
+ update()
6806
+ {
6807
+ if (!glEnable && this.isUsingWebGL)
6808
+ {
6809
+ // redraw the layer if webgl was disabled or context lost
6810
+ this.isUsingWebGL = false;
6811
+ this.redraw();
6812
+ }
6783
6813
  }
6784
6814
 
6785
6815
  // Render the tile layer, called automatically by the engine
@@ -6787,40 +6817,35 @@ class TileLayer extends CanvasLayer
6787
6817
  {
6788
6818
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
6789
6819
 
6790
- if (this.refreshWebGL)
6791
- {
6792
- // update the WebGL texture
6793
- this.textureInfo.createWebGLTexture();
6794
- this.refreshWebGL = false;
6795
- }
6796
-
6797
- // draw the tile layer as a single tile
6798
- const tileInfo = new TileInfo().setFullImage(this.textureInfo);
6799
6820
  const size = this.drawSize || this.size;
6800
6821
  const pos = this.pos.add(size.scale(.5));
6801
- const useWebGL = glEnable && this.textureInfo.hasWebGL();
6802
- drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
6822
+ this.draw(pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
6803
6823
  }
6804
6824
 
6825
+ /** Called after this layer is redrawn, does nothing by default */
6826
+ onRedraw() {}
6827
+
6805
6828
  /** Draw all the tile data to an offscreen canvas
6806
- * - This may be slow in some browsers but only needs to be done once */
6829
+ * - This may be slow if not using webgl but only needs to be done once */
6807
6830
  redraw()
6808
6831
  {
6809
6832
  this.redrawStart(true);
6810
6833
  for (let x = this.size.x; x--;)
6811
6834
  for (let y = this.size.y; y--;)
6812
6835
  this.drawTileData(vec2(x,y), false);
6836
+ this.isUsingWebGL && glFlush();
6837
+ this.onRedraw();
6813
6838
  this.redrawEnd();
6814
- this.useWebGL();
6815
6839
  }
6816
6840
 
6817
6841
  /** Call to start the redraw process
6818
- * - This can be used to manually update small parts of the level
6842
+ * - This can be used to manually update parts of the level
6819
6843
  * @param {boolean} [clear] - Should it clear the canvas before drawing */
6820
6844
  redrawStart(clear=false)
6821
6845
  {
6822
6846
  if (!this.context) return;
6823
-
6847
+ ASSERT(drawContext !== this.context);
6848
+
6824
6849
  // save current render settings
6825
6850
  /** @type {[CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6826
6851
  this.savedRenderSettings = [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
@@ -6828,65 +6853,119 @@ class TileLayer extends CanvasLayer
6828
6853
  // set the draw canvas and context to this layer
6829
6854
  // use camera settings to match this layer's canvas
6830
6855
  drawContext = this.context;
6831
- cameraPos = this.size.scale(.5);
6832
- const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
6833
- cameraScale = tileSize.x;
6834
- canvasClearColor = CLEAR_BLACK;
6856
+ const tileSize = this.tileInfo?.size ?? vec2(1);
6835
6857
  mainCanvasSize = this.size.multiply(tileSize);
6836
- if (clear)
6858
+ canvasClearColor = CLEAR_BLACK;
6859
+ cameraPos = this.size.multiply(tileSize).scale(.5);
6860
+ cameraScale = 1;
6861
+
6862
+ // set render target to this layer
6863
+ this.isUsingWebGL = this.hasWebGL();
6864
+ if (this.isUsingWebGL)
6865
+ glSetRenderTarget(this.textureInfo.glTexture, clear);
6866
+ else
6837
6867
  {
6838
- // clear and set size
6839
- this.canvas.width = mainCanvasSize.x;
6840
- this.canvas.height = mainCanvasSize.y;
6868
+ // disable smoothing for pixel art
6869
+ this.context.imageSmoothingEnabled = !tilesPixelated;
6870
+ if (clear)
6871
+ {
6872
+ // clear and set size
6873
+ this.canvas.width = mainCanvasSize.x;
6874
+ this.canvas.height = mainCanvasSize.y;
6875
+ }
6841
6876
  }
6842
-
6843
- // disable smoothing for pixel art
6844
- drawContext.imageSmoothingEnabled = !tilesPixelated;
6845
-
6846
- // setup gl rendering if enabled
6847
- glPreRender();
6848
6877
  }
6849
6878
 
6850
6879
  /** Call to end the redraw process */
6851
6880
  redrawEnd()
6852
6881
  {
6853
6882
  if (!this.context) return;
6883
+ ASSERT(drawContext === this.context);
6884
+
6885
+ // set stuff back to normal
6886
+ if (this.isUsingWebGL)
6887
+ glSetRenderTarget();
6888
+ [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6889
+ }
6890
+
6891
+ /** Draw the tile at a given position in the tile layer
6892
+ * This can be used to clear out tiles when they are destroyed
6893
+ * Tiles can also be redrawn if inside a redrawStart/End block
6894
+ * @param {Vector2} layerPos
6895
+ * @param {boolean} [clear] - should the old tile be cleared out
6896
+ */
6897
+ drawTileData(layerPos, clear=true)
6898
+ {
6899
+ if (!this.context) return;
6900
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6901
+
6902
+ // clear out where the tile was, can be skipped for fully opaque tiles
6903
+ const drawSize = this.tileInfo?.size ?? vec2(1);
6904
+ const drawPos = layerPos.multiply(drawSize);
6905
+ clear && this.clearLayerRect(drawPos, drawSize);
6906
+
6907
+ // draw the tile if it has layer data
6908
+ const d = this.getData(layerPos);
6909
+ if (!d.tile) return;
6910
+
6911
+ const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
6912
+ this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
6913
+ }
6914
+
6915
+ /** Draw the tile at a given position in the tile layer
6916
+ * This can be used to clear tiles when they are destroyed
6917
+ * For better performance use drawTileData inside a redrawStart/End block
6918
+ * @param {Vector2} layerPos
6919
+ * @param {boolean} [clear] - should the old tile be cleared
6920
+ */
6921
+ redrawTileData(layerPos, clear=true)
6922
+ {
6923
+ if (!this.context) return;
6924
+ ASSERT(drawContext !== this.context, 'redrawStart() should not be active when calling redrawTileData(), instead use drawTileData()');
6854
6925
 
6855
- ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6856
- glCopyToContext(drawContext);
6857
- //saveCanvas(this.canvas);
6926
+ this.redrawStart();
6927
+ this.drawTileData(layerPos, clear);
6928
+ this.redrawEnd();
6929
+ }
6858
6930
 
6859
- // set stuff back to normal
6860
- [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6931
+ /** Draw textured tile in layer space
6932
+ * @param {Vector2} pos - Position in pixel coordinates
6933
+ * @param {Vector2} [size=vec2(1)] - Size of the tile
6934
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
6935
+ * @param {Color} [color=WHITE] - Color to modulate with
6936
+ * @param {number} [angle] - Angle to rotate by
6937
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
6938
+ * @param {Color} [additiveColor] - Additive color to be applied if any */
6939
+ drawLayerTile(pos, size=vec2(1), tileInfo, color=WHITE,
6940
+ angle=0, mirror, additiveColor)
6941
+ {
6942
+ const drawPos = pos.add(size.scale(.5));
6943
+ drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor, this.isUsingWebGL);
6861
6944
  }
6862
6945
 
6863
- /** Draw the tile at a given position in the tile grid
6864
- * This can be used to clear out tiles when they are destroyed
6865
- * Tiles can also be redrawn if inside a redrawStart/End block
6866
- * @param {Vector2} layerPos
6867
- * @param {boolean} [clear] - should the old tile be cleared out
6946
+ /** Clear a rectangle in layer space
6947
+ * @param {Vector2} pos
6948
+ * @param {Vector2} size
6949
+ * @param {Color} [color=WHITE] - Color to modulate with
6950
+ * @param {number} [angle] - Angle to rotate by
6868
6951
  */
6869
- drawTileData(layerPos, clear=true)
6952
+ drawLayerRect(pos, size, color, angle=0)
6953
+ { this.drawLayerTile(pos, size, undefined, color, angle); }
6954
+
6955
+ /** Clear a rectangle in layer space
6956
+ * @param {Vector2} pos - position in pixel coordinates
6957
+ * @param {Vector2} size
6958
+ */
6959
+ clearLayerRect(pos, size)
6870
6960
  {
6871
- if (!this.context) return;
6872
-
6873
- // clear out where the tile was, for full opaque tiles this can be skipped
6874
- const s = this.tileInfo.size;
6875
- if (clear)
6876
- {
6877
- const pos = layerPos.multiply(s);
6878
- this.context.clearRect(pos.x, this.canvas.height-pos.y, s.x, -s.y);
6879
- }
6961
+ ASSERT(drawContext === this.context, 'must call redrawStart() before clearing tiles');
6880
6962
 
6881
- // draw the tile if it has layer data
6882
- const d = this.getData(layerPos);
6883
- if (d.tile !== undefined)
6884
- {
6885
- ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6886
- const pos = layerPos.add(vec2(.5));
6887
- const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
6888
- drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
6889
- }
6963
+ const x = pos.x, y = this.canvas.height - pos.y - size.y;
6964
+ const useWebGL = this.hasWebGL();
6965
+ if (useWebGL)
6966
+ glClearRect(x, y, size.x, size.y);
6967
+ else
6968
+ this.context.clearRect(x, y, size.x, size.y);
6890
6969
  }
6891
6970
  }
6892
6971
 
@@ -6895,21 +6974,21 @@ class TileLayer extends CanvasLayer
6895
6974
  * Tile Collision Layer - a tile layer with collision
6896
6975
  * - adds collision data and functions to TileLayer
6897
6976
  * - there can be multiple tile collision layers
6898
- * - tile collision layers should not overlap each other
6899
6977
  * @extends TileLayer
6900
6978
  * @memberof TileLayers
6901
6979
  */
6902
6980
  class TileCollisionLayer extends TileLayer
6903
6981
  {
6904
6982
  /** Create a tile layer object
6905
- * @param {Vector2} position - World space position
6906
- * @param {Vector2} size - World space size
6907
- * @param {TileInfo} [tileInfo] - Tile info for layer
6983
+ * @param {Vector2} pos - World space position
6984
+ * @param {Vector2} size - World space size
6985
+ * @param {TileInfo} [tileInfo] - Tile info for layer
6908
6986
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6987
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6909
6988
  */
6910
- constructor(position, size, tileInfo=tile(), renderOrder=0)
6989
+ constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
6911
6990
  {
6912
- super(position, size.floor(), tileInfo, renderOrder);
6991
+ super(pos, size.floor(), tileInfo, renderOrder, useWebGL);
6913
6992
 
6914
6993
  /** @property {Array<number>} - The tile collision grid */
6915
6994
  this.collisionData = [];
@@ -6945,29 +7024,34 @@ class TileCollisionLayer extends TileLayer
6945
7024
  this.collisionData.fill(0);
6946
7025
  }
6947
7026
 
6948
- /** Set tile collision data for a given cell in the grid
6949
- * @param {Vector2} gridPos
7027
+ /** Set tile collision data for a given cell in the layer
7028
+ * @param {Vector2} layerPos
6950
7029
  * @param {number} [data] */
6951
- setCollisionData(gridPos, data=1)
7030
+ setCollisionData(layerPos, data=1)
6952
7031
  {
6953
- ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6954
- const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6955
- gridPos.arrayCheck(this.size) && (this.collisionData[i] = data);
7032
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
7033
+ const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
7034
+ layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
6956
7035
  }
6957
7036
 
6958
- /** Get tile collision data for a given cell in the grid
6959
- * @param {Vector2} gridPos
7037
+ /** Clear tile collision data for a given cell in the layer
7038
+ * @param {Vector2} layerPos */
7039
+ clearCollisionData(layerPos)
7040
+ { this.setCollisionData(layerPos, 0); }
7041
+
7042
+ /** Get tile collision data for a given cell in the layer
7043
+ * @param {Vector2} layerPos
6960
7044
  * @return {number} */
6961
- getCollisionData(gridPos)
7045
+ getCollisionData(layerPos)
6962
7046
  {
6963
- ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6964
- const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6965
- return gridPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
7047
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
7048
+ const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
7049
+ return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
6966
7050
  }
6967
7051
 
6968
7052
  /** Check if collision with another object should occur
6969
7053
  * @param {Vector2} pos
6970
- * @param {Vector2} [size=(0,0)]
7054
+ * @param {Vector2} [size=vec2()]
6971
7055
  * @param {EngineObject} [object]
6972
7056
  * @return {boolean} */
6973
7057
  collisionTest(pos, size=new Vector2, object)
@@ -7287,7 +7371,7 @@ class ParticleEmitter extends EngineObject
7287
7371
  particle.mirror = randBool();
7288
7372
 
7289
7373
  // call particle create callback
7290
- this.particleCreateCallback && this.particleCreateCallback(particle);
7374
+ this.particleCreateCallback?.(particle);
7291
7375
 
7292
7376
  // return the newly created particle
7293
7377
  return particle;
@@ -7372,7 +7456,7 @@ class Particle extends EngineObject
7372
7456
  const c = this.colorEnd;
7373
7457
  this.color.set(c.r, c.g, c.b, c.a);
7374
7458
  this.size.set(this.sizeEnd, this.sizeEnd);
7375
- this.destroyCallback && this.destroyCallback(this);
7459
+ this.destroyCallback?.(this);
7376
7460
  this.destroyed = 1;
7377
7461
  }
7378
7462
  }
@@ -7650,7 +7734,7 @@ let glContext;
7650
7734
  let glAntialias = true;
7651
7735
 
7652
7736
  // WebGL internal variables not exposed to documentation
7653
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glCanBeEnabled = true;
7737
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glCanBeEnabled = true;
7654
7738
 
7655
7739
  // WebGL internal constants
7656
7740
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -7726,7 +7810,7 @@ function glInit(rootElement)
7726
7810
  // setup instanced rendering shader program
7727
7811
  glShader = glCreateProgram(
7728
7812
  '#version 300 es\n' + // specify GLSL ES version
7729
- 'precision highp float;'+ // use highp for better accuracy
7813
+ 'precision highp float;'+ // use highp for accuracy
7730
7814
  'uniform mat4 m;'+ // transform matrix
7731
7815
  'in vec2 g;'+ // in: geometry
7732
7816
  'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
@@ -7741,7 +7825,7 @@ function glInit(rootElement)
7741
7825
  '}' // end of shader
7742
7826
  ,
7743
7827
  '#version 300 es\n' + // specify GLSL ES version
7744
- 'precision highp float;'+ // use highp for better accuracy
7828
+ 'precision highp float;'+ // use highp for accuracy
7745
7829
  'uniform sampler2D s;'+ // texture
7746
7830
  'in vec2 v;'+ // in: uv
7747
7831
  'in vec4 d,e;'+ // in: color, additiveColor
@@ -7779,45 +7863,62 @@ function glInit(rootElement)
7779
7863
  glColorData = new Uint32Array(glInstanceData);
7780
7864
  glArrayBuffer = glContext.createBuffer();
7781
7865
  glGeometryBuffer = glContext.createBuffer();
7866
+ glFramebuffer = glContext.createFramebuffer();
7867
+ glBatchCount = 0;
7782
7868
 
7783
7869
  // create the geometry buffer, triangle strip square
7784
- const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
7870
+ const geometry = new Float32Array([0,0,1,0,0,1,1,1]);
7785
7871
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7786
7872
  glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
7873
+
7874
+ let offset, shader, stride;
7875
+ const initVertexAttrib = (name, type, typeSize, size, divisor=0)=>
7876
+ {
7877
+ const location = glContext.getAttribLocation(shader, name);
7878
+ const normalize = typeSize === 1;
7879
+ const fixedStride = typeSize && stride;
7880
+ glContext.enableVertexAttribArray(location);
7881
+ glContext.vertexAttribPointer(location, size, type, normalize, fixedStride, offset);
7882
+ glContext.vertexAttribDivisor(location, divisor);
7883
+ offset += size*typeSize;
7884
+ }
7885
+
7886
+ // setup VAO for instanced rendering
7887
+ glInstancedVAO = glContext.createVertexArray();
7888
+ glContext.bindVertexArray(glInstancedVAO);
7889
+
7890
+ // configure instanced vertex attributes
7891
+ offset = 0, shader = glShader, stride = gl_INSTANCE_BYTE_STRIDE;
7892
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7893
+ initVertexAttrib('g', glContext.FLOAT, 0, 2); // geometry
7894
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7895
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7896
+ initVertexAttrib('p', glContext.FLOAT, 4, 4, 1); // position & size
7897
+ initVertexAttrib('u', glContext.FLOAT, 4, 4, 1); // texture coords
7898
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4, 1); // color
7899
+ initVertexAttrib('a', glContext.UNSIGNED_BYTE, 1, 4, 1); // additiveColor
7900
+ initVertexAttrib('r', glContext.FLOAT, 4, 1, 1); // rotation
7901
+
7902
+ // setup VAO for poly rendering
7903
+ glPolyVAO = glContext.createVertexArray();
7904
+ glContext.bindVertexArray(glPolyVAO);
7905
+
7906
+ // configure poly vertex attributes
7907
+ offset = 0, shader = glPolyShader, stride = gl_POLY_VERTEX_BYTE_STRIDE;
7908
+ initVertexAttrib('p', glContext.FLOAT, 4, 2); // position
7909
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7787
7910
  }
7788
7911
  }
7789
7912
 
7790
- function glSetInstancedMode()
7913
+ function glSetInstancedMode(force=false)
7791
7914
  {
7792
- if (!glPolyMode) return;
7915
+ if (!force && !glPolyMode) return;
7793
7916
 
7794
7917
  // setup instanced mode
7795
7918
  glFlush();
7796
7919
  glPolyMode = false;
7797
7920
  glContext.useProgram(glShader);
7798
-
7799
- // set vertex attributes
7800
- let offset = 0;
7801
- const initVertexAttribArray = (name, type, typeSize, size)=>
7802
- {
7803
- const location = glContext.getAttribLocation(glShader, name);
7804
- const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
7805
- const divisor = typeSize && 1; // only if not geometry
7806
- const normalize = typeSize === 1; // only if color
7807
- glContext.enableVertexAttribArray(location);
7808
- glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
7809
- glContext.vertexAttribDivisor(location, divisor);
7810
- offset += size*typeSize;
7811
- }
7812
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7813
- initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
7814
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7815
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7816
- initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
7817
- initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
7818
- initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7819
- initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
7820
- initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
7921
+ glContext.bindVertexArray(glInstancedVAO);
7821
7922
  }
7822
7923
 
7823
7924
  function glSetPolyMode()
@@ -7828,36 +7929,30 @@ function glSetPolyMode()
7828
7929
  glFlush();
7829
7930
  glPolyMode = true;
7830
7931
  glContext.useProgram(glPolyShader);
7831
-
7832
- // set vertex attributes
7833
- let offset = 0;
7834
- const initVertexAttribArray = (name, type, typeSize, size)=>
7835
- {
7836
- const location = glContext.getAttribLocation(glPolyShader, name);
7837
- const normalize = typeSize === 1; // only normalize if color
7838
- const stride = gl_POLY_VERTEX_BYTE_STRIDE;
7839
- glContext.enableVertexAttribArray(location);
7840
- glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
7841
- glContext.vertexAttribDivisor(location, 0);
7842
- offset += size*typeSize;
7843
- }
7844
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7845
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7846
- initVertexAttribArray('p', glContext.FLOAT, 4, 2); // position
7847
- initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7932
+ glContext.bindVertexArray(glPolyVAO);
7848
7933
  }
7849
7934
 
7850
7935
  // Setup WebGL render each frame, called automatically by engine
7851
7936
  // Also used by tile layer rendering when redrawing tiles
7852
- function glPreRender()
7937
+ function glPreRender(clear=true)
7853
7938
  {
7854
7939
  if (!glEnable || !glContext) return;
7855
7940
 
7856
- // clear the canvas
7857
- glClearCanvas();
7941
+ ASSERT(!glBatchCount, 'glPreRender called with unflushed batch.');
7942
+
7943
+ if (!glRenderTarget)
7944
+ {
7945
+ // set to same size as main canvas
7946
+ glCanvas.width = mainCanvasSize.x;
7947
+ glCanvas.height = mainCanvasSize.y;
7948
+ }
7949
+ glContext.viewport(0, 0, mainCanvasSize.x, mainCanvasSize.y);
7950
+ clear && glClearCanvas();
7858
7951
 
7859
7952
  // build the transform matrix
7860
7953
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
7954
+ if (glRenderTarget)
7955
+ s.y = -s.y; // invert y when using render target
7861
7956
  const rotatedCam = cameraPos.rotate(-cameraAngle);
7862
7957
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
7863
7958
  const ca = cos(cameraAngle);
@@ -7869,7 +7964,7 @@ function glPreRender()
7869
7964
  p.x, p.y, 0, 1];
7870
7965
 
7871
7966
  // set the same transform matrix for both shaders
7872
- const initUniform = (program, uniform, value) =>
7967
+ const initUniform = (program, uniform, value)=>
7873
7968
  {
7874
7969
  glContext.useProgram(program);
7875
7970
  const location = glContext.getUniformLocation(program, uniform);
@@ -7886,12 +7981,14 @@ function glPreRender()
7886
7981
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
7887
7982
  }
7888
7983
 
7984
+ // rebind the array buffer
7985
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7986
+
7889
7987
  // start with additive blending off
7890
7988
  glAdditive = glBatchAdditive = false;
7891
7989
 
7892
- // force it to set instanced mode by first setting poly mode true
7893
- glPolyMode = true;
7894
- glSetInstancedMode();
7990
+ // force it to set instanced mode
7991
+ glSetInstancedMode(true);
7895
7992
  }
7896
7993
 
7897
7994
  /** Clear the canvas and setup the viewport
@@ -7900,13 +7997,9 @@ function glClearCanvas()
7900
7997
  {
7901
7998
  if (!glContext) return;
7902
7999
 
7903
- // clear and set to same size as main canvas
7904
- glCanvas.width = mainCanvasSize.x;
7905
- glCanvas.height = mainCanvasSize.y;
7906
- glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
8000
+ // clear using the canvasClearColor
7907
8001
  const color = canvasClearColor;
7908
- if (color.a > 0)
7909
- glContext.clearColor(color.r, color.g, color.b, color.a);
8002
+ glContext.clearColor(color.r, color.g, color.b, color.a);
7910
8003
  glContext.clear(glContext.COLOR_BUFFER_BIT);
7911
8004
  }
7912
8005
 
@@ -7983,7 +8076,7 @@ function glCreateTexture(image)
7983
8076
  // build the texture
7984
8077
  const texture = glContext.createTexture();
7985
8078
  let mipMap = false;
7986
- if (image && image.width)
8079
+ if (image?.width)
7987
8080
  {
7988
8081
  glSetTextureData(texture, image);
7989
8082
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
@@ -8004,7 +8097,9 @@ function glCreateTexture(image)
8004
8097
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
8005
8098
  if (mipMap)
8006
8099
  glContext.generateMipmap(glContext.TEXTURE_2D);
8007
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
8100
+
8101
+ // rebind active texture
8102
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
8008
8103
  return texture;
8009
8104
  }
8010
8105
 
@@ -8027,10 +8122,12 @@ function glSetTextureData(texture, image)
8027
8122
  if (!glContext) return;
8028
8123
 
8029
8124
  // build the texture
8030
- ASSERT(!!image && image.width > 0, 'Invalid image data.');
8125
+ ASSERT(image?.width > 0, 'Invalid image data.');
8031
8126
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
8032
8127
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
8033
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
8128
+
8129
+ // rebind active texture
8130
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
8034
8131
  }
8035
8132
 
8036
8133
  /** Tells WebGL to create or update the glTexture and start tracking it
@@ -8249,6 +8346,48 @@ function glDrawColoredPoints(points, pointColors)
8249
8346
  glBatchCount += vertCount;
8250
8347
  }
8251
8348
 
8349
+ /** Set the WebGL render target to the given texture or back to the canvas
8350
+ * @param {WebGLTexture} [texture] - a texture or undefined to use normal glCanvas
8351
+ * @param {boolean} [clear] - should the render target be cleared
8352
+ * @memberof WebGL */
8353
+ function glSetRenderTarget(texture, clear=false)
8354
+ {
8355
+ if (texture)
8356
+ {
8357
+ glRenderTarget = texture;
8358
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
8359
+ glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
8360
+ glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
8361
+ glPreRender(clear);
8362
+ }
8363
+ else
8364
+ {
8365
+ glFlush();
8366
+ glRenderTarget = undefined;
8367
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
8368
+ }
8369
+ }
8370
+
8371
+ /** Clear out a rectangle area of the WebGL canvas or render target
8372
+ * @param {number} x
8373
+ * @param {number} y
8374
+ * @param {number} width
8375
+ * @param {number} height
8376
+ * @memberof WebGL */
8377
+ function glClearRect(x, y, width, height)
8378
+ {
8379
+ if (!glEnable) return;
8380
+
8381
+ // Enable scissor test to clear only the specified area
8382
+ glContext.enable(glContext.SCISSOR_TEST);
8383
+ glContext.scissor(x, y, width, height);
8384
+ glContext.clearColor(0, 0, 0, 0);
8385
+ glContext.clear(glContext.COLOR_BUFFER_BIT);
8386
+ glContext.disable(glContext.SCISSOR_TEST);
8387
+ }
8388
+
8389
+ ///////////////////////////////////////////////////////////////////////////////
8390
+
8252
8391
  // WebGL internal function to convert polygon to outline triangle strip
8253
8392
  function glMakeOutline(points, width, wrap=true)
8254
8393
  {
@@ -8382,23 +8521,20 @@ function glPolyStrip(points)
8382
8521
  const a = points[i0], b = points[i1], c = points[i2];
8383
8522
 
8384
8523
  // check if convex
8385
- if (cross(a, b, c) < e)
8386
- continue;
8524
+ if (cross(a, b, c) < e) continue;
8387
8525
 
8388
8526
  // check if any other point is inside
8389
8527
  let hasInside = false;
8390
8528
  for (let j = 0; j < indices.length; j++)
8391
8529
  {
8392
8530
  const k = indices[j];
8393
- if (k === i0 || k === i1 || k === i2)
8394
- continue;
8531
+ if (k === i0 || k === i1 || k === i2) continue;
8532
+
8395
8533
  const p = points[k];
8396
8534
  hasInside = pointInTriangle(p, a, b, c);
8397
- if (hasInside)
8398
- break;
8535
+ if (hasInside) break;
8399
8536
  }
8400
- if (hasInside)
8401
- continue;
8537
+ if (hasInside) continue;
8402
8538
 
8403
8539
  // found valid ear
8404
8540
  triangles.push([i0, i1, i2]);
@@ -8423,8 +8559,7 @@ function glPolyStrip(points)
8423
8559
  worstIndex = i;
8424
8560
  }
8425
8561
  }
8426
- if (worstIndex < 0)
8427
- break;
8562
+ if (worstIndex < 0) break;
8428
8563
 
8429
8564
  const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
8430
8565
  const i1 = indices[worstIndex];
@@ -8655,14 +8790,16 @@ class PostProcessPlugin
8655
8790
  {
8656
8791
  /** Create global post processing shader
8657
8792
  * @param {string} shaderCode
8658
- * @param {boolean} [includeMainCanvas]
8659
- * @example
8660
- * // create the post process plugin object
8661
- * new PostProcessPlugin(shaderCode);
8662
- */
8663
- constructor(shaderCode, includeMainCanvas=true)
8793
+ * @param {boolean} [includeMainCanvas] - combine mainCanvs onto glCanvas
8794
+ * @param {boolean} [feedbackTexture] - use glCanvas from previous frame as the texture
8795
+ * @example
8796
+ * // create the post process plugin object
8797
+ * new PostProcessPlugin(shaderCode);
8798
+ */
8799
+ constructor(shaderCode, includeMainCanvas=false, feedbackTexture=false)
8664
8800
  {
8665
8801
  ASSERT(!postProcess, 'Post process already initialized');
8802
+ ASSERT(!(includeMainCanvas && feedbackTexture), 'Post process cannot both include main canvas and use feedback texture');
8666
8803
  postProcess = this;
8667
8804
 
8668
8805
  if (!shaderCode) // default shader pass through
@@ -8670,9 +8807,10 @@ class PostProcessPlugin
8670
8807
 
8671
8808
  /** @property {WebGLProgram} - Shader for post processing */
8672
8809
  this.shader = undefined;
8673
-
8674
8810
  /** @property {WebGLTexture} - Texture for post processing */
8675
8811
  this.texture = undefined;
8812
+ /** @property {WebGLVertexArrayObject} - Vertex array object */
8813
+ this.vao = undefined;
8676
8814
 
8677
8815
  // setup the post processing plugin
8678
8816
  initPostProcess();
@@ -8681,7 +8819,6 @@ class PostProcessPlugin
8681
8819
  function initPostProcess()
8682
8820
  {
8683
8821
  if (headlessMode) return;
8684
-
8685
8822
  if (!glEnable)
8686
8823
  {
8687
8824
  console.warn('PostProcessPlugin: WebGL not enabled!');
@@ -8692,14 +8829,14 @@ class PostProcessPlugin
8692
8829
  postProcess.texture = glCreateTexture();
8693
8830
  postProcess.shader = glCreateProgram(
8694
8831
  '#version 300 es\n' + // specify GLSL ES version
8695
- 'precision highp float;'+ // use highp for better accuracy
8832
+ 'precision highp float;'+ // use highp for accuracy
8696
8833
  'in vec2 p;'+ // position
8697
8834
  'void main(){'+ // shader entry point
8698
8835
  'gl_Position=vec4(p+p-1.,1,1);'+ // set position
8699
8836
  '}' // end of shader
8700
8837
  ,
8701
8838
  '#version 300 es\n' + // specify GLSL ES version
8702
- 'precision highp float;'+ // use highp for better accuracy
8839
+ 'precision highp float;'+ // use highp for accuracy
8703
8840
  'uniform sampler2D iChannel0;'+ // input texture
8704
8841
  'uniform vec3 iResolution;'+ // size of output texture
8705
8842
  'uniform float iTime;'+ // time
@@ -8710,6 +8847,17 @@ class PostProcessPlugin
8710
8847
  'c.a=1.;'+ // always use full alpha
8711
8848
  '}' // end of shader
8712
8849
  );
8850
+
8851
+ // setup VAO for post processing
8852
+ postProcess.vao = glContext.createVertexArray();
8853
+ glContext.bindVertexArray(postProcess.vao);
8854
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
8855
+
8856
+ // configure vertex attributes
8857
+ const vertexByteStride = 8;
8858
+ const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
8859
+ glContext.enableVertexAttribArray(pLocation);
8860
+ glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
8713
8861
  }
8714
8862
  function postProcessContextLost()
8715
8863
  {
@@ -8724,14 +8872,17 @@ class PostProcessPlugin
8724
8872
  }
8725
8873
  function postProcessRender()
8726
8874
  {
8727
- if (headlessMode) return;
8728
-
8729
- if (!glEnable)
8730
- return;
8875
+ if (headlessMode || !glEnable) return;
8731
8876
 
8732
8877
  // clear out the buffer
8733
8878
  glFlush();
8734
-
8879
+
8880
+ // setup shader program to draw a quad
8881
+ glContext.useProgram(postProcess.shader);
8882
+ glContext.bindVertexArray(postProcess.vao);
8883
+ glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, true);
8884
+ glContext.disable(glContext.BLEND);
8885
+
8735
8886
  // setup texture
8736
8887
  glContext.activeTexture(glContext.TEXTURE0);
8737
8888
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
@@ -8742,29 +8893,32 @@ class PostProcessPlugin
8742
8893
  workCanvas.height = mainCanvasSize.y;
8743
8894
  glCopyToContext(workContext);
8744
8895
  workContext.drawImage(mainCanvas, 0, 0);
8896
+ mainCanvas.width |= 0
8745
8897
 
8746
8898
  // copy work canvas to texture
8747
8899
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
8748
8900
  }
8749
-
8750
- // setup shader program to draw a quad
8751
- glContext.useProgram(postProcess.shader);
8752
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
8753
- glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
8754
- glContext.disable(glContext.BLEND);
8755
-
8756
- // set vertex position attribute
8757
- const vertexByteStride = 8;
8758
- const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
8759
- glContext.enableVertexAttribArray(pLocation);
8760
- glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
8761
-
8901
+ else if (!feedbackTexture)
8902
+ {
8903
+ // copy glCanvas to texture
8904
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
8905
+ }
8906
+
8762
8907
  // set uniforms and draw
8763
8908
  const uniformLocation = (name)=>glContext.getUniformLocation(postProcess.shader, name);
8764
8909
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
8765
8910
  glContext.uniform1f(uniformLocation('iTime'), time);
8766
8911
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
8767
8912
  glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
8913
+
8914
+ if (feedbackTexture)
8915
+ {
8916
+ // pass glCanvas back to overlay texture
8917
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
8918
+ }
8919
+
8920
+ // force it to set instanced mode
8921
+ glSetInstancedMode(true);
8768
8922
  }
8769
8923
  }
8770
8924
  }
@@ -8823,7 +8977,7 @@ class ZzFXMusic extends Sound
8823
8977
  /** Play the music that loops by default
8824
8978
  * @param {number} [volume] - Volume to play the music at
8825
8979
  * @param {boolean} [loop] - Should the music loop?
8826
- * @return {AudioBufferSourceNode} - The audio source node
8980
+ * @return {SoundInstance} - The sound instance
8827
8981
  */
8828
8982
  playMusic(volume=1, loop=true)
8829
8983
  { return super.play(undefined, volume, 1, 0, loop); }
@@ -8870,7 +9024,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
8870
9024
  sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
8871
9025
 
8872
9026
  // for each pattern in sequence
8873
- sequence.forEach((patternIndex, sequenceIndex) => {
9027
+ sequence.forEach((patternIndex, sequenceIndex)=> {
8874
9028
  // get pattern for current channel, use empty 1 note pattern if none found
8875
9029
  patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
8876
9030
 
@@ -8963,7 +9117,7 @@ let uiDebug = 0;
8963
9117
 
8964
9118
  /** Enable UI system debug drawing
8965
9119
  * 0=off, 1=normal, 2=show invisible
8966
- * @param {number|boolean} enable
9120
+ * @param {number|boolean} debugMode
8967
9121
  * @memberof UISystem */
8968
9122
  function uiSetDebug(debugMode)
8969
9123
  { uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
@@ -9445,7 +9599,7 @@ class UISystemPlugin
9445
9599
  const up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight';
9446
9600
  if (both)
9447
9601
  {
9448
- return keyIsDown(up) || keyIsDown(left) ? -1 :
9602
+ return keyIsDown(up) || keyIsDown(left) ? -1 :
9449
9603
  keyIsDown(down) || keyIsDown(right) ? 1 : 0;
9450
9604
  }
9451
9605
  const back = vertical ? up : left;
@@ -9476,7 +9630,7 @@ class UISystemPlugin
9476
9630
  * @return {boolean} */
9477
9631
  getNavigationWasPressed()
9478
9632
  {
9479
- return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
9633
+ return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
9480
9634
  keyWasPressed('Space') || keyWasPressed('Enter');
9481
9635
  }
9482
9636
 
@@ -9524,7 +9678,7 @@ class UISystemPlugin
9524
9678
  buttonYes.textHeight = 40;
9525
9679
  buttonYes.navigationIndex = 1;
9526
9680
  buttonYes.hoverColor = hsl(0,1,.5);
9527
- buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
9681
+ buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
9528
9682
  confirmMenu.addChild(buttonYes);
9529
9683
 
9530
9684
  // no button
@@ -9554,8 +9708,8 @@ class UISystemPlugin
9554
9708
  class UIObject
9555
9709
  {
9556
9710
  /** Create a UIObject
9557
- * @param {Vector2} [pos=(0,0)]
9558
- * @param {Vector2} [size=(1,1)]
9711
+ * @param {Vector2} [pos=vec2()]
9712
+ * @param {Vector2} [size=vec2(1)]
9559
9713
  */
9560
9714
  constructor(pos=vec2(), size=vec2())
9561
9715
  {
@@ -9570,7 +9724,7 @@ class UIObject
9570
9724
  this.size = size.copy();
9571
9725
  /** @property {Color} - Color of the object */
9572
9726
  this.color = uiSystem.defaultColor.copy();
9573
- /** @property {Color} - Color of the object when active, uses color if undefined */
9727
+ /** @property {Color} - Color of the object when active, uses hoverColor if undefined */
9574
9728
  this.activeColor = undefined;
9575
9729
  /** @property {string} - Text for this ui object */
9576
9730
  this.text = undefined;
@@ -9668,10 +9822,10 @@ class UIObject
9668
9822
 
9669
9823
  // disconnect from parent and destroy children
9670
9824
  this.destroyed = 1;
9671
- this.parent && this.parent.removeChild(this);
9825
+ this.parent?.removeChild(this);
9672
9826
  for (const child of this.children)
9673
9827
  {
9674
- child.parent = 0;
9828
+ child.parent = undefined;
9675
9829
  child.destroy();
9676
9830
  }
9677
9831
  }
@@ -9767,10 +9921,10 @@ class UIObject
9767
9921
  this.interactive && this.isActiveObject() && !this.disabled ?
9768
9922
  this.color : this.lineColor;
9769
9923
  const color = isNavigationObject ? this.hoverColor :
9770
- this.disabled ? this.disabledColor :
9771
- this.interactive ?
9772
- this.isHoverObject() ? this.hoverColor :
9773
- this.isActiveObject() ? this.activeColor || this.color :
9924
+ this.disabled ? this.disabledColor :
9925
+ this.interactive ?
9926
+ this.isActiveObject() ? this.activeColor || this.hoverColor :
9927
+ this.isHoverObject() ? this.hoverColor :
9774
9928
  this.color : this.color;
9775
9929
  const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
9776
9930
 
@@ -9782,7 +9936,7 @@ class UIObject
9782
9936
  getTextSize()
9783
9937
  {
9784
9938
  return vec2(
9785
- this.textWidth || this.textFitScale * this.size.x,
9939
+ this.textWidth || this.textFitScale * this.size.x,
9786
9940
  this.textHeight || this.textFitScale * this.size.y);
9787
9941
  }
9788
9942
 
@@ -9830,9 +9984,9 @@ class UIObject
9830
9984
  renderDebug(visible=true)
9831
9985
  {
9832
9986
  // apply color based on state
9833
- const color =
9987
+ const color =
9834
9988
  !visible ? GREEN :
9835
- this.isHoverObject() ? YELLOW :
9989
+ this.isHoverObject() ? YELLOW :
9836
9990
  this.disabled ? PURPLE :
9837
9991
  this.interactive ? RED : BLUE;
9838
9992
  uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);
@@ -10164,8 +10318,8 @@ class UIScrollbar extends UIObject
10164
10318
  class UIVideo extends UIObject
10165
10319
  {
10166
10320
  /** Create a video player UI object
10167
- * @param {Vector2} [pos]
10168
- * @param {Vector2} [size]
10321
+ * @param {Vector2} pos
10322
+ * @param {Vector2} size
10169
10323
  * @param {string} src - Video file path or URL
10170
10324
  * @param {boolean} [autoplay=false] - Start playing immediately?
10171
10325
  * @param {boolean} [loop=false] - Loop the video?
@@ -10305,6 +10459,7 @@ class UIVideo extends UIObject
10305
10459
  * - Contact begin and end callbacks
10306
10460
  * - Wraps b2Vec2 type to/from Vector2
10307
10461
  * - Raycasting and querying
10462
+ * - Box2dTileLayer for grid based collision
10308
10463
  * - Every type of joint
10309
10464
  * - Debug physics drawing
10310
10465
  * @namespace Box2D
@@ -10354,21 +10509,24 @@ class Box2dObject extends EngineObject
10354
10509
  bodyDef.set_type(bodyType);
10355
10510
  bodyDef.set_position(box2d.vec2dTo(pos));
10356
10511
  bodyDef.set_angle(-angle);
10512
+
10513
+ /** @property {Object} - The Box2d body */
10357
10514
  this.body = box2d.world.CreateBody(bodyDef);
10358
- this.body.object = this;
10515
+ /** @property {Color} - Line color used for default box2d drawing */
10359
10516
  this.lineColor = BLACK;
10360
- box2d.objects.push(this);
10361
-
10362
- // edge lists and loops for drawing
10517
+ /** @property {Array<Object>} - List of all edges for default box2d drawing */
10363
10518
  this.edgeLists = [];
10519
+ /** @property {Array<Object>} - List of all edge loops for default box2d drawing */
10364
10520
  this.edgeLoops = [];
10521
+
10522
+ this.body.object = this; // link body to this object
10523
+ box2d.objects.push(this); // keep track of all box2d objects
10365
10524
  }
10366
10525
 
10367
10526
  /** Destroy this object and its physics body */
10368
10527
  destroy()
10369
10528
  {
10370
- if (this.destroyed)
10371
- return;
10529
+ if (this.destroyed) return;
10372
10530
 
10373
10531
  // destroy physics body, fixtures, and joints
10374
10532
  ASSERT(this.body, 'Box2dObject has no body to destroy');
@@ -10399,11 +10557,12 @@ class Box2dObject extends EngineObject
10399
10557
  }
10400
10558
 
10401
10559
  /** Draws all this object's fixtures
10402
- * @param {Color} [color]
10403
- * @param {Color} [lineColor]
10404
- * @param {number} [lineWidth]
10560
+ * @param {Color} [color]
10561
+ * @param {Color} [lineColor]
10562
+ * @param {number} [lineWidth]
10563
+ * @param {boolean} [useWebGL=glEnable]
10405
10564
  * @param {CanvasRenderingContext2D} [context] */
10406
- drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
10565
+ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
10407
10566
  {
10408
10567
  // draw non-edge fixtures
10409
10568
  this.getFixtureList().forEach((fixture)=>
@@ -10411,7 +10570,7 @@ class Box2dObject extends EngineObject
10411
10570
  const shape = box2d.castObjectType(fixture.GetShape());
10412
10571
  if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
10413
10572
  {
10414
- box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
10573
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, useWebGL, context);
10415
10574
  }
10416
10575
  });
10417
10576
 
@@ -10637,6 +10796,14 @@ class Box2dObject extends EngineObject
10637
10796
  return fixtures;
10638
10797
  }
10639
10798
 
10799
+ /** Destroy a fixture from the body
10800
+ * @param {Object} [fixture] */
10801
+ destroyFixture(fixture) { this.body.DestroyFixture(fixture); }
10802
+
10803
+ /** Destroy all fixture from the body */
10804
+ destroyAllFixtures()
10805
+ { this.getFixtureList().forEach(fixture=>this.destroyFixture(fixture)); }
10806
+
10640
10807
  ///////////////////////////////////////////////////////////////////////////////
10641
10808
  // physics get functions
10642
10809
 
@@ -10869,6 +11036,136 @@ class Box2dObject extends EngineObject
10869
11036
  }
10870
11037
  }
10871
11038
 
11039
+ ///////////////////////////////////////////////////////////////////////////////
11040
+ /**
11041
+ * Box2D Static Object - Box2d with a static physics body
11042
+ * @extends Box2dObject
11043
+ * @memberof Box2D
11044
+ */
11045
+ class Box2dStaticObject extends Box2dObject
11046
+ {
11047
+ /** Create a LittleJS object with Box2d physics
11048
+ * @param {Vector2} [pos]
11049
+ * @param {Vector2} [size]
11050
+ * @param {TileInfo} [tileInfo]
11051
+ * @param {number} [angle]
11052
+ * @param {Color} [color]
11053
+ * @param {number} [renderOrder] */
11054
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
11055
+ {
11056
+ const bodyType = box2d.bodyTypeStatic;
11057
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
11058
+ }
11059
+ }
11060
+
11061
+ ///////////////////////////////////////////////////////////////////////////////
11062
+ /**
11063
+ * Box2D Kiematic Object - Box2d with a kinematic physics body
11064
+ * @extends Box2dObject
11065
+ * @memberof Box2D
11066
+ */
11067
+ class Box2dKiematicObject extends Box2dObject
11068
+ {
11069
+ /** Create a LittleJS object with Box2d physics
11070
+ * @param {Vector2} [pos]
11071
+ * @param {Vector2} [size]
11072
+ * @param {TileInfo} [tileInfo]
11073
+ * @param {number} [angle]
11074
+ * @param {Color} [color]
11075
+ * @param {number} [renderOrder] */
11076
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
11077
+ {
11078
+ const bodyType = box2d.bodyTypeKinematic;
11079
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
11080
+ }
11081
+ }
11082
+
11083
+ ///////////////////////////////////////////////////////////////////////////////
11084
+ /**
11085
+ * Box2d Tile Layer
11086
+ * - adds Box2d support to tile layers
11087
+ * - creates static box2d fixtures for solid tiles
11088
+ * @extends Box2dObject
11089
+ * @memberof Box2D
11090
+ */
11091
+ class Box2dTileLayer extends Box2dStaticObject
11092
+ {
11093
+ /** Create a Box2d tile layer object
11094
+ * @param {TileCollisionLayer} tileLayer - Tile layer for this object */
11095
+ constructor(tileLayer)
11096
+ {
11097
+ ASSERT(tileLayer instanceof TileCollisionLayer, 'tileLayer must be a TileCollisionLayer');
11098
+ super(tileLayer.pos, tileLayer.size);
11099
+
11100
+ /** @property {TileLayer} - The tile layer */
11101
+ this.tileLayer = tileLayer;
11102
+ this.addChild(tileLayer);
11103
+ }
11104
+
11105
+ render()
11106
+ {
11107
+ // do not render fixtures, tile layer handles rendering
11108
+ }
11109
+
11110
+ /** Create box2d collision fixtures for solid tiles
11111
+ * @param {number} [friction]
11112
+ * @param {number} [restitution] */
11113
+ buildCollision(friction=.2, restitution=0)
11114
+ {
11115
+ // destroy all fixtures and create new ones
11116
+ this.destroyAllFixtures();
11117
+
11118
+ // create box2d object for this layer
11119
+ const size = this.tileLayer.size;
11120
+ this.pos = this.tileLayer.pos.copy();
11121
+ this.size = size.copy();
11122
+
11123
+ // track which tiles have been processed
11124
+ const processed = [];
11125
+ const getIndex = (x, y)=> x + y * size.x;
11126
+ const isSolidUnprocessed = (x, y)=>
11127
+ !processed[getIndex(x, y)] &&
11128
+ this.tileLayer.getCollisionData(vec2(x, y)) > 0;
11129
+
11130
+ // combine tiles into larger boxes
11131
+ for (let x = 0; x < size.x; ++x)
11132
+ for (let y = 0; y < size.y; ++y)
11133
+ {
11134
+ if (!isSolidUnprocessed(x, y)) continue;
11135
+
11136
+ // find max width by scanning right
11137
+ let width = 1, height = 1, canExpand = true;
11138
+ while (isSolidUnprocessed(x + width, y))
11139
+ ++width;
11140
+
11141
+ // find max height by scanning up, ensuring all rows have the same width
11142
+ while (canExpand)
11143
+ {
11144
+ for (let checkX = 0; checkX < width; ++checkX)
11145
+ {
11146
+ if (!isSolidUnprocessed(x + checkX, y + height))
11147
+ {
11148
+ canExpand = false;
11149
+ break;
11150
+ }
11151
+ }
11152
+ if (canExpand)
11153
+ ++height;
11154
+ }
11155
+
11156
+ // mark all tiles in this rectangle as processed
11157
+ for (let rectX = width; rectX--;)
11158
+ for (let rectY = height; rectY--;)
11159
+ processed[getIndex(x + rectX, y + rectY)] = true;
11160
+
11161
+ // create a single fixture for the entire rectangle
11162
+ const shapeSize = vec2(width, height);
11163
+ const offset = vec2(x + width/2, y + height/2);
11164
+ this.addBox(shapeSize, offset, 0, 0, friction, restitution);
11165
+ }
11166
+ }
11167
+ }
11168
+
10872
11169
  ///////////////////////////////////////////////////////////////////////////////
10873
11170
  /**
10874
11171
  * Box2D Raycast Result
@@ -10910,6 +11207,7 @@ class Box2dJoint
10910
11207
  * @param {Object} jointDef */
10911
11208
  constructor(jointDef)
10912
11209
  {
11210
+ /** @property {Object} - The Box2d joint */
10913
11211
  this.box2dJoint = box2d.castObjectType(box2d.world.CreateJoint(jointDef));
10914
11212
  }
10915
11213
 
@@ -11256,7 +11554,7 @@ class Box2dGearJoint extends Box2dJoint
11256
11554
  * @param {Box2dObject} objectB
11257
11555
  * @param {Box2dJoint} joint1
11258
11556
  * @param {Box2dJoint} joint2
11259
- * @param {ratio} [ratio] */
11557
+ * @param {number} [ratio] */
11260
11558
  constructor(objectA, objectB, joint1, joint2, ratio=1)
11261
11559
  {
11262
11560
  const jointDef = new box2d.instance.b2GearJointDef();
@@ -11398,50 +11696,6 @@ class Box2dPrismaticJoint extends Box2dJoint
11398
11696
  getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
11399
11697
  }
11400
11698
 
11401
- ///////////////////////////////////////////////////////////////////////////////
11402
- /**
11403
- * Box2D Static Object - Box2d with a static physics body
11404
- * @extends Box2dObject
11405
- * @memberof Box2D
11406
- */
11407
- class Box2dStaticObject extends Box2dObject
11408
- {
11409
- /** Create a LittleJS object with Box2d physics
11410
- * @param {Vector2} [pos]
11411
- * @param {Vector2} [size]
11412
- * @param {TileInfo} [tileInfo]
11413
- * @param {number} [angle]
11414
- * @param {Color} [color]
11415
- * @param {number} [renderOrder] */
11416
- constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
11417
- {
11418
- const bodyType = box2d.bodyTypeStatic;
11419
- super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
11420
- }
11421
- }
11422
-
11423
- ///////////////////////////////////////////////////////////////////////////////
11424
- /**
11425
- * Box2D Kiematic Object - Box2d with a kinematic physics body
11426
- * @extends Box2dObject
11427
- * @memberof Box2D
11428
- */
11429
- class Box2dKiematicObject extends Box2dObject
11430
- {
11431
- /** Create a LittleJS object with Box2d physics
11432
- * @param {Vector2} [pos]
11433
- * @param {Vector2} [size]
11434
- * @param {TileInfo} [tileInfo]
11435
- * @param {number} [angle]
11436
- * @param {Color} [color]
11437
- * @param {number} [renderOrder] */
11438
- constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
11439
- {
11440
- const bodyType = box2d.bodyTypeKinematic;
11441
- super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
11442
- }
11443
- }
11444
-
11445
11699
  ///////////////////////////////////////////////////////////////////////////////
11446
11700
  /**
11447
11701
  * Box2D Wheel Joint
@@ -11802,10 +12056,13 @@ class Box2dPlugin
11802
12056
  {
11803
12057
  ASSERT(!box2d, 'Box2D already initialized');
11804
12058
  box2d = this;
12059
+
12060
+ /** @property {Object} - The Box2d instance */
11805
12061
  this.instance = instance;
12062
+ /** @property {Object} - The Box2d world */
11806
12063
  this.world = new box2d.instance.b2World();
12064
+ /** @property {Array<Box2dObject>} - List of all Box2d objects */
11807
12065
  this.objects = [];
11808
-
11809
12066
  /** @property {number} - Velocity iterations per update*/
11810
12067
  this.velocityIterations = 8;
11811
12068
  /** @property {number} - Position iterations per update*/
@@ -12004,8 +12261,9 @@ class Box2dPlugin
12004
12261
  * @param {Color} [color]
12005
12262
  * @param {Color} [lineColor]
12006
12263
  * @param {number} [lineWidth]
12264
+ * @param {boolean} [useWebGL=glEnable]
12007
12265
  * @param {CanvasRenderingContext2D} [context] */
12008
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
12266
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebgl, context)
12009
12267
  {
12010
12268
  const shape = box2d.castObjectType(fixture.GetShape());
12011
12269
  switch (shape.GetType())
@@ -12015,20 +12273,20 @@ class Box2dPlugin
12015
12273
  let points = [];
12016
12274
  for (let i=shape.GetVertexCount(); i--;)
12017
12275
  points.push(box2d.vec2From(shape.GetVertex(i)));
12018
- drawPoly(points, color, lineWidth, lineColor, pos, angle);
12276
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebgl, false, context);
12019
12277
  break;
12020
12278
  }
12021
12279
  case box2d.instance.b2Shape.e_circle:
12022
12280
  {
12023
12281
  const radius = shape.get_m_radius();
12024
- drawCircle(pos, radius*2, color, lineWidth, lineColor);
12282
+ drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebgl, false, context);
12025
12283
  break;
12026
12284
  }
12027
12285
  case box2d.instance.b2Shape.e_edge:
12028
12286
  {
12029
12287
  const v1 = box2d.vec2From(shape.get_m_vertex1());
12030
12288
  const v2 = box2d.vec2From(shape.get_m_vertex2());
12031
- drawLine(v1, v2, lineWidth, lineColor, pos, angle);
12289
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebgl, false, context);
12032
12290
  break;
12033
12291
  }
12034
12292
  }
@@ -12046,7 +12304,7 @@ class Box2dPlugin
12046
12304
  }
12047
12305
 
12048
12306
  /** converts a box2d vec2 pointer to a Vector2
12049
- * @param {Object} v */
12307
+ * @param {Object} vp */
12050
12308
  vec2FromPointer(vp)
12051
12309
  {
12052
12310
  const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
@@ -12158,7 +12416,7 @@ async function box2dInit()
12158
12416
  const box2dColorPointer = (c)=>
12159
12417
  box2dColor(box2d.instance.wrapPointer(c, box2d.instance.b2Color));
12160
12418
  const getDebugColor = (color)=>box2dColorPointer(color).scale(1,.8);
12161
- const getPointsList = (vertices, vertexCount) =>
12419
+ const getPointsList = (vertices, vertexCount)=>
12162
12420
  {
12163
12421
  const points = [];
12164
12422
  for (let i=vertexCount; i--;)
@@ -12354,357 +12612,348 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
12354
12612
 
12355
12613
  export
12356
12614
  {
12357
- // Engine
12358
- engineName,
12359
- engineVersion,
12360
- frameRate,
12361
- timeDelta,
12362
- engineObjects,
12363
- frame,
12364
- time,
12365
- timeReal,
12366
- paused,
12367
- getPaused,
12368
- setPaused,
12369
- engineInit,
12370
- engineObjectsUpdate,
12371
- engineObjectsDestroy,
12372
- engineObjectsCollect,
12373
- engineObjectsCallback,
12374
- engineObjectsRaycast,
12375
- engineAddPlugin,
12376
-
12377
- // Globals
12378
- debug,
12379
- debugOverlay,
12380
- debugWatermark,
12381
-
12382
- // Debug
12383
- ASSERT,
12384
- LOG,
12385
- debugRect,
12386
- debugPoly,
12387
- debugCircle,
12388
- debugPoint,
12389
- debugLine,
12390
- debugOverlap,
12391
- debugText,
12392
- debugClear,
12393
- debugScreenshot,
12394
- debugShowErrors,
12395
- debugVideoCaptureStart,
12396
- debugVideoCaptureStop,
12397
- debugVideoCaptureIsActive,
12398
-
12399
- // Settings
12400
- cameraPos,
12401
- cameraAngle,
12402
- cameraScale,
12403
- canvasColorTiles,
12404
- canvasClearColor,
12405
- canvasMaxSize,
12406
- canvasMinAspect,
12407
- canvasMaxAspect,
12408
- canvasFixedSize,
12409
- canvasPixelated,
12410
- tilesPixelated,
12411
- fontDefault,
12412
- showSplashScreen,
12413
- headlessMode,
12414
- tileDefaultSize,
12415
- tileDefaultPadding,
12416
- tileDefaultBleed,
12417
- enablePhysicsSolver,
12418
- objectDefaultMass,
12419
- objectDefaultDamping,
12420
- objectDefaultAngleDamping,
12421
- objectDefaultRestitution,
12422
- objectDefaultFriction,
12423
- objectMaxSpeed,
12424
- gravity,
12425
- particleEmitRateScale,
12426
- glEnable,
12427
- gamepadsEnable,
12428
- gamepadDirectionEmulateStick,
12429
- inputWASDEmulateDirection,
12430
- touchGamepadEnable,
12431
- touchGamepadCenterButton,
12432
- touchGamepadAnalog,
12433
- touchGamepadSize,
12434
- touchGamepadAlpha,
12435
- vibrateEnable,
12436
- soundEnable,
12437
- soundVolume,
12438
- soundDefaultRange,
12439
- soundDefaultTaper,
12440
- medalDisplayTime,
12441
- medalDisplaySlideTime,
12442
- medalDisplaySize,
12443
-
12444
- // Setters for globals
12445
- setCameraPos,
12446
- setCameraAngle,
12447
- setCameraScale,
12448
- setCanvasColorTiles,
12449
- setCanvasClearColor,
12450
- setCanvasMaxSize,
12451
- setCanvasMinAspect,
12452
- setCanvasMaxAspect,
12453
- setCanvasFixedSize,
12454
- setCanvasPixelated,
12455
- setTilesPixelated,
12456
- setFontDefault,
12457
- setShowSplashScreen,
12458
- setHeadlessMode,
12459
- setGLEnable,
12460
- setTileDefaultSize,
12461
- setTileDefaultPadding,
12462
- setTileDefaultBleed,
12463
- setEnablePhysicsSolver,
12464
- setObjectDefaultMass,
12465
- setObjectDefaultDamping,
12466
- setObjectDefaultAngleDamping,
12467
- setObjectDefaultRestitution,
12468
- setObjectDefaultFriction,
12469
- setObjectMaxSpeed,
12470
- setGravity,
12471
- setParticleEmitRateScale,
12472
- setTouchInputEnable,
12473
- setGamepadsEnable,
12474
- setGamepadDirectionEmulateStick,
12475
- setInputWASDEmulateDirection,
12476
- setTouchGamepadEnable,
12477
- setTouchGamepadCenterButton,
12478
- setTouchGamepadButtonCount,
12479
- setTouchGamepadAnalog,
12480
- setTouchGamepadSize,
12481
- setTouchGamepadAlpha,
12482
- setVibrateEnable,
12483
- setSoundEnable,
12484
- setSoundVolume,
12485
- setSoundDefaultRange,
12486
- setSoundDefaultTaper,
12487
- setMedalDisplayTime,
12488
- setMedalDisplaySlideTime,
12489
- setMedalDisplaySize,
12490
- setMedalsPreventUnlock,
12491
- setDebugWatermark,
12492
- setDebugKey,
12493
-
12494
- // Math
12495
- PI,
12496
- abs,
12497
- floor,
12498
- ceil,
12499
- round,
12500
- min,
12501
- max,
12502
- sign,
12503
- hypot,
12504
- log2,
12505
- sin,
12506
- cos,
12507
- tan,
12508
- atan2,
12509
- mod,
12510
- clamp,
12511
- percent,
12512
- distanceWrap,
12513
- lerpWrap,
12514
- distanceAngle,
12515
- lerpAngle,
12516
- lerp,
12517
- smoothStep,
12518
- nearestPowerOfTwo,
12519
- isOverlapping,
12520
- isIntersecting,
12521
- wave,
12522
-
12523
- // Utilities
12524
- formatTime,
12525
- fetchJSON,
12526
- saveText,
12527
- saveCanvas,
12528
- saveDataURL,
12529
-
12530
- // Random
12531
- rand,
12532
- randInt,
12533
- randBool,
12534
- randSign,
12535
- randInCircle,
12536
- randVec2,
12537
- randColor,
12538
-
12539
- // Utility Classes
12540
- RandomGenerator,
12541
- Vector2,
12542
- Color,
12543
- Timer,
12544
- vec2,
12545
- rgb,
12546
- hsl,
12547
- isColor,
12548
- isVector2,
12549
- isNumber,
12550
- isString,
12551
- isArray,
12552
-
12553
- // Default Colors
12554
- WHITE,
12555
- CLEAR_WHITE,
12556
- BLACK,
12557
- CLEAR_BLACK,
12558
- GRAY,
12559
- RED,
12560
- ORANGE,
12561
- YELLOW,
12562
- GREEN,
12563
- CYAN,
12564
- BLUE,
12565
- PURPLE,
12566
- MAGENTA,
12567
-
12568
- // Draw
12569
- tile,
12570
- TileInfo,
12571
- TextureInfo,
12572
- mainCanvas,
12573
- mainContext,
12574
- drawContext,
12575
- workCanvas,
12576
- workContext,
12577
- workReadCanvas,
12578
- workReadContext,
12579
- mainCanvasSize,
12580
- textureInfos,
12581
- drawCount,
12582
- screenToWorld,
12583
- worldToScreen,
12584
- screenToWorldDelta,
12585
- worldToScreenDelta,
12586
- screenToWorldTransform,
12587
- drawTile,
12588
- drawRect,
12589
- drawRectGradient,
12590
- drawLineList,
12591
- drawLine,
12592
- drawPoly,
12593
- drawEllipse,
12594
- drawCircle,
12595
- drawCanvas2D,
12596
- drawText,
12597
- drawTextScreen,
12598
- setBlendMode,
12599
- combineCanvases,
12600
- engineFontImage,
12601
- FontImage,
12602
- isFullscreen,
12603
- toggleFullscreen,
12604
- setCursor,
12605
- getCameraSize,
12606
-
12607
- // WebGL
12608
- glCanvas,
12609
- glContext,
12610
- glClearCanvas,
12611
- glSetTexture,
12612
- glCompileShader,
12613
- glCreateProgram,
12614
- glCreateTexture,
12615
- glDeleteTexture,
12616
- glSetTextureData,
12617
- glFlush,
12618
- glCopyToContext,
12619
- glSetAntialias,
12620
- glDraw,
12621
- glDrawPointsTransform,
12622
- glDrawOutlineTransform,
12623
- glDrawPoints,
12624
- glDrawColoredPoints,
12625
- glAntialias,
12626
- glShader,
12627
- glPolyShader,
12628
- glPolyMode,
12629
- glAdditive,
12630
- glBatchAdditive,
12631
- glActiveTexture,
12632
- glArrayBuffer,
12633
- glGeometryBuffer,
12634
- glPositionData,
12635
- glColorData,
12636
- glBatchCount,
12637
-
12638
- // Input
12639
- keyIsDown,
12640
- keyWasPressed,
12641
- keyWasReleased,
12642
- keyDirection,
12643
- inputClear,
12644
- inputClearKey,
12645
- mouseIsDown,
12646
- mouseWasPressed,
12647
- mouseWasReleased,
12648
- mousePos,
12649
- mousePosScreen,
12650
- mouseDelta,
12651
- mouseDeltaScreen,
12652
- mouseWheel,
12653
- mouseInWindow,
12654
- isUsingGamepad,
12655
- inputPreventDefault,
12656
- gamepadPrimary,
12657
- setInputPreventDefault,
12658
- gamepadIsDown,
12659
- gamepadWasPressed,
12660
- gamepadWasReleased,
12661
- gamepadStick,
12662
- gamepadDpad,
12663
- gamepadConnected,
12664
- vibrate,
12665
- vibrateStop,
12666
- isTouchDevice,
12667
- pointerLockRequest,
12668
- pointerLockExit,
12669
- pointerLockIsActive,
12670
-
12671
- // Audio
12672
- audioContext,
12673
- audioMasterGain,
12674
- audioDefaultSampleRate,
12675
- Sound,
12676
- SoundWave,
12677
- SoundInstance,
12678
- speak,
12679
- speakStop,
12680
- getNoteFrequency,
12681
- playSamples,
12682
- zzfx,
12683
- zzfxG,
12684
-
12685
- // Base Object
12686
- EngineObject,
12687
-
12688
- // Tiles
12689
- tileCollisionLayers,
12690
- tileCollisionGetData,
12691
- tileCollisionTest,
12692
- tileCollisionRaycast,
12693
- tileLayersLoad,
12694
- TileLayerData,
12695
- CanvasLayer,
12696
- TileLayer,
12697
- TileCollisionLayer,
12698
-
12699
- // Particles
12700
- ParticleEmitter,
12701
- Particle,
12702
-
12703
- // Medals
12704
- medals,
12705
- medalsPreventUnlock,
12706
- medalsInit,
12707
- Medal,
12615
+ // Engine
12616
+ engineName,
12617
+ engineVersion,
12618
+ frameRate,
12619
+ timeDelta,
12620
+ engineObjects,
12621
+ frame,
12622
+ time,
12623
+ timeReal,
12624
+ paused,
12625
+ getPaused,
12626
+ setPaused,
12627
+ engineInit,
12628
+ engineObjectsUpdate,
12629
+ engineObjectsDestroy,
12630
+ engineObjectsCollect,
12631
+ engineObjectsCallback,
12632
+ engineObjectsRaycast,
12633
+ engineAddPlugin,
12634
+
12635
+ // Globals
12636
+ debug,
12637
+ debugOverlay,
12638
+ debugWatermark,
12639
+
12640
+ // Debug
12641
+ ASSERT,
12642
+ LOG,
12643
+ debugRect,
12644
+ debugPoly,
12645
+ debugCircle,
12646
+ debugPoint,
12647
+ debugLine,
12648
+ debugOverlap,
12649
+ debugText,
12650
+ debugClear,
12651
+ debugScreenshot,
12652
+ debugShowErrors,
12653
+ debugVideoCaptureStart,
12654
+ debugVideoCaptureStop,
12655
+ debugVideoCaptureIsActive,
12656
+
12657
+ // Settings
12658
+ cameraPos,
12659
+ cameraAngle,
12660
+ cameraScale,
12661
+ canvasColorTiles,
12662
+ canvasClearColor,
12663
+ canvasMaxSize,
12664
+ canvasMinAspect,
12665
+ canvasMaxAspect,
12666
+ canvasFixedSize,
12667
+ canvasPixelated,
12668
+ tilesPixelated,
12669
+ fontDefault,
12670
+ showSplashScreen,
12671
+ headlessMode,
12672
+ tileDefaultSize,
12673
+ tileDefaultPadding,
12674
+ tileDefaultBleed,
12675
+ enablePhysicsSolver,
12676
+ objectDefaultMass,
12677
+ objectDefaultDamping,
12678
+ objectDefaultAngleDamping,
12679
+ objectDefaultRestitution,
12680
+ objectDefaultFriction,
12681
+ objectMaxSpeed,
12682
+ gravity,
12683
+ particleEmitRateScale,
12684
+ glEnable,
12685
+ gamepadsEnable,
12686
+ gamepadDirectionEmulateStick,
12687
+ inputWASDEmulateDirection,
12688
+ touchGamepadEnable,
12689
+ touchGamepadCenterButton,
12690
+ touchGamepadAnalog,
12691
+ touchGamepadSize,
12692
+ touchGamepadAlpha,
12693
+ vibrateEnable,
12694
+ soundEnable,
12695
+ soundVolume,
12696
+ soundDefaultRange,
12697
+ soundDefaultTaper,
12698
+ medalDisplayTime,
12699
+ medalDisplaySlideTime,
12700
+ medalDisplaySize,
12701
+
12702
+ // Setters for globals
12703
+ setCameraPos,
12704
+ setCameraAngle,
12705
+ setCameraScale,
12706
+ setCanvasColorTiles,
12707
+ setCanvasClearColor,
12708
+ setCanvasMaxSize,
12709
+ setCanvasMinAspect,
12710
+ setCanvasMaxAspect,
12711
+ setCanvasFixedSize,
12712
+ setCanvasPixelated,
12713
+ setTilesPixelated,
12714
+ setFontDefault,
12715
+ setShowSplashScreen,
12716
+ setHeadlessMode,
12717
+ setGLEnable,
12718
+ setTileDefaultSize,
12719
+ setTileDefaultPadding,
12720
+ setTileDefaultBleed,
12721
+ setEnablePhysicsSolver,
12722
+ setObjectDefaultMass,
12723
+ setObjectDefaultDamping,
12724
+ setObjectDefaultAngleDamping,
12725
+ setObjectDefaultRestitution,
12726
+ setObjectDefaultFriction,
12727
+ setObjectMaxSpeed,
12728
+ setGravity,
12729
+ setParticleEmitRateScale,
12730
+ setTouchInputEnable,
12731
+ setGamepadsEnable,
12732
+ setGamepadDirectionEmulateStick,
12733
+ setInputWASDEmulateDirection,
12734
+ setTouchGamepadEnable,
12735
+ setTouchGamepadCenterButton,
12736
+ setTouchGamepadButtonCount,
12737
+ setTouchGamepadAnalog,
12738
+ setTouchGamepadSize,
12739
+ setTouchGamepadAlpha,
12740
+ setVibrateEnable,
12741
+ setSoundEnable,
12742
+ setSoundVolume,
12743
+ setSoundDefaultRange,
12744
+ setSoundDefaultTaper,
12745
+ setMedalDisplayTime,
12746
+ setMedalDisplaySlideTime,
12747
+ setMedalDisplaySize,
12748
+ setMedalsPreventUnlock,
12749
+ setDebugWatermark,
12750
+ setDebugKey,
12751
+
12752
+ // Math
12753
+ PI,
12754
+ abs,
12755
+ floor,
12756
+ ceil,
12757
+ round,
12758
+ min,
12759
+ max,
12760
+ sign,
12761
+ hypot,
12762
+ log2,
12763
+ sin,
12764
+ cos,
12765
+ tan,
12766
+ atan2,
12767
+ mod,
12768
+ clamp,
12769
+ percent,
12770
+ distanceWrap,
12771
+ lerpWrap,
12772
+ distanceAngle,
12773
+ lerpAngle,
12774
+ lerp,
12775
+ smoothStep,
12776
+ nearestPowerOfTwo,
12777
+ isOverlapping,
12778
+ isIntersecting,
12779
+ wave,
12780
+
12781
+ // Utilities
12782
+ formatTime,
12783
+ fetchJSON,
12784
+ saveText,
12785
+ saveCanvas,
12786
+ saveDataURL,
12787
+ shareURL,
12788
+
12789
+ // Random
12790
+ rand,
12791
+ randInt,
12792
+ randBool,
12793
+ randSign,
12794
+ randInCircle,
12795
+ randVec2,
12796
+ randColor,
12797
+
12798
+ // Utility Classes
12799
+ RandomGenerator,
12800
+ Vector2,
12801
+ Color,
12802
+ Timer,
12803
+ vec2,
12804
+ rgb,
12805
+ hsl,
12806
+ isColor,
12807
+ isVector2,
12808
+ isNumber,
12809
+ isString,
12810
+ isArray,
12811
+
12812
+ // Default Colors
12813
+ WHITE,
12814
+ CLEAR_WHITE,
12815
+ BLACK,
12816
+ CLEAR_BLACK,
12817
+ GRAY,
12818
+ RED,
12819
+ ORANGE,
12820
+ YELLOW,
12821
+ GREEN,
12822
+ CYAN,
12823
+ BLUE,
12824
+ PURPLE,
12825
+ MAGENTA,
12826
+
12827
+ // Draw
12828
+ tile,
12829
+ TileInfo,
12830
+ TextureInfo,
12831
+ mainCanvas,
12832
+ mainContext,
12833
+ drawContext,
12834
+ workCanvas,
12835
+ workContext,
12836
+ workReadCanvas,
12837
+ workReadContext,
12838
+ mainCanvasSize,
12839
+ textureInfos,
12840
+ drawCount,
12841
+ screenToWorld,
12842
+ worldToScreen,
12843
+ screenToWorldDelta,
12844
+ worldToScreenDelta,
12845
+ screenToWorldTransform,
12846
+ drawTile,
12847
+ drawRect,
12848
+ drawRectGradient,
12849
+ drawLineList,
12850
+ drawLine,
12851
+ drawPoly,
12852
+ drawEllipse,
12853
+ drawCircle,
12854
+ drawCanvas2D,
12855
+ drawText,
12856
+ drawTextScreen,
12857
+ setBlendMode,
12858
+ combineCanvases,
12859
+ engineFontImage,
12860
+ FontImage,
12861
+ isFullscreen,
12862
+ toggleFullscreen,
12863
+ setCursor,
12864
+ getCameraSize,
12865
+
12866
+ // WebGL
12867
+ glCanvas,
12868
+ glContext,
12869
+ glAntialias,
12870
+ glClearCanvas,
12871
+ glSetTexture,
12872
+ glCompileShader,
12873
+ glCreateProgram,
12874
+ glCreateTexture,
12875
+ glDeleteTexture,
12876
+ glSetTextureData,
12877
+ glFlush,
12878
+ glCopyToContext,
12879
+ glSetAntialias,
12880
+ glDraw,
12881
+ glDrawPointsTransform,
12882
+ glDrawOutlineTransform,
12883
+ glDrawPoints,
12884
+ glDrawColoredPoints,
12885
+ glSetRenderTarget,
12886
+ glClearRect,
12887
+
12888
+ // Input
12889
+ keyIsDown,
12890
+ keyWasPressed,
12891
+ keyWasReleased,
12892
+ keyDirection,
12893
+ inputClear,
12894
+ inputClearKey,
12895
+ mouseIsDown,
12896
+ mouseWasPressed,
12897
+ mouseWasReleased,
12898
+ mousePos,
12899
+ mousePosScreen,
12900
+ mouseDelta,
12901
+ mouseDeltaScreen,
12902
+ mouseWheel,
12903
+ mouseInWindow,
12904
+ isUsingGamepad,
12905
+ inputPreventDefault,
12906
+ gamepadPrimary,
12907
+ isTouchDevice,
12908
+ setInputPreventDefault,
12909
+ gamepadIsDown,
12910
+ gamepadWasPressed,
12911
+ gamepadWasReleased,
12912
+ gamepadStick,
12913
+ gamepadDpad,
12914
+ gamepadConnected,
12915
+ vibrate,
12916
+ vibrateStop,
12917
+ pointerLockRequest,
12918
+ pointerLockExit,
12919
+ pointerLockIsActive,
12920
+
12921
+ // Audio
12922
+ audioContext,
12923
+ audioMasterGain,
12924
+ audioDefaultSampleRate,
12925
+ Sound,
12926
+ SoundInstance,
12927
+ speak,
12928
+ speakStop,
12929
+ getNoteFrequency,
12930
+ playSamples,
12931
+ zzfx,
12932
+ zzfxG,
12933
+
12934
+ // Base Object
12935
+ EngineObject,
12936
+
12937
+ // Tiles
12938
+ tileCollisionLayers,
12939
+ tileCollisionGetData,
12940
+ tileCollisionTest,
12941
+ tileCollisionRaycast,
12942
+ tileLayersLoad,
12943
+ TileLayerData,
12944
+ CanvasLayer,
12945
+ TileLayer,
12946
+ TileCollisionLayer,
12947
+
12948
+ // Particles
12949
+ ParticleEmitter,
12950
+ Particle,
12951
+
12952
+ // Medals
12953
+ medals,
12954
+ medalsPreventUnlock,
12955
+ medalsInit,
12956
+ Medal,
12708
12957
  }
12709
12958
  /**
12710
12959
  * LittleJS Module Plugins Export