littlejsengine 1.16.1 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/littlejs.d.ts +287 -240
  2. package/dist/littlejs.esm.js +1419 -1259
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +1076 -910
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +977 -815
  7. package/examples/box2d/gameObjects.js +6 -6
  8. package/examples/breakout/game.js +5 -6
  9. package/examples/electron/index.html +2 -2
  10. package/examples/electron/tiles.png +0 -0
  11. package/examples/htmlMenu/tiles.png +0 -0
  12. package/examples/index.html +1 -1
  13. package/examples/logo.png +0 -0
  14. package/examples/logo2.png +0 -0
  15. package/examples/module/tiles.png +0 -0
  16. package/examples/platformer/gameEffects.js +24 -23
  17. package/examples/platformer/gameLevel.js +24 -25
  18. package/examples/shorts/base.html +2 -1
  19. package/examples/shorts/clock.js +3 -3
  20. package/examples/shorts/fontImage.js +4 -3
  21. package/examples/shorts/parallax.js +1 -1
  22. package/examples/shorts/sequencer.js +1 -1
  23. package/examples/shorts/shapes.js +1 -1
  24. package/examples/shorts/texture.js +10 -6
  25. package/examples/shorts/tiles.png +0 -0
  26. package/examples/shorts/tiltedView.js +2 -0
  27. package/examples/starter/index.html +3 -2
  28. package/examples/starter/tiles.png +0 -0
  29. package/examples/typescript/tiles.png +0 -0
  30. package/examples/uiSystem/game.js +1 -1
  31. package/examples/uiSystem/tiles.png +0 -0
  32. package/package.json +1 -1
  33. package/plugins/postProcess.js +47 -28
  34. package/plugins/uiSystem.js +16 -16
  35. package/reference.md +6 -6
  36. package/src/engine.js +186 -198
  37. package/src/engineAudio.js +6 -10
  38. package/src/engineBuild.js +1 -0
  39. package/src/engineDebug.js +100 -98
  40. package/src/engineDraw.js +185 -148
  41. package/src/engineExport.js +327 -333
  42. package/src/engineFont.png +0 -0
  43. package/src/engineInput.js +17 -26
  44. package/src/engineMath.js +1112 -0
  45. package/src/engineMedals.js +4 -4
  46. package/src/engineObject.js +27 -27
  47. package/src/engineParticles.js +2 -2
  48. package/src/engineRelease.js +1 -3
  49. package/src/engineSettings.js +2 -2
  50. package/src/engineTileLayer.js +200 -176
  51. package/src/engineUtilities.js +48 -1097
  52. package/src/engineWebGL.js +127 -79
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.16.1';
36
+ const engineVersion = '1.17.1';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -272,33 +272,20 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
272
272
  if (!wasUpdated)
273
273
  updateCanvas();
274
274
 
275
- // render sort then render while removing destroyed objects
275
+ // render the game and objects
276
276
  enginePreRender();
277
277
  gameRender();
278
278
  engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
279
279
  for (const o of engineObjects)
280
280
  o.destroyed || o.render();
281
+
282
+ // post rendering
281
283
  gameRenderPost();
282
284
  pluginList.forEach(plugin=>plugin.render?.());
283
285
  inputRender();
284
286
  debugRender();
285
287
  glFlush();
286
- debugVideoCaptureUpdate();
287
-
288
- if (debugWatermark && !debugVideoCaptureIsActive())
289
- {
290
- // update fps display
291
- mainContext.textAlign = 'right';
292
- mainContext.textBaseline = 'top';
293
- mainContext.font = '1em monospace';
294
- mainContext.fillStyle = '#000';
295
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
296
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
297
- + (glEnable ? ' GL' : ' 2D') ;
298
- mainContext.fillText(text, mainCanvas.width-3, 3);
299
- mainContext.fillStyle = '#fff';
300
- mainContext.fillText(text, mainCanvas.width-2, 2);
301
- }
288
+ debugRenderPost();
302
289
  drawCount = 0;
303
290
  }
304
291
  }
@@ -333,7 +320,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
333
320
 
334
321
  // responsive aspect ratio with native resolution
335
322
  const innerAspect = innerWidth / innerHeight;
336
- ASSERT(canvasMinAspect <= canvasMaxAspect);
323
+ ASSERT(canvasMinAspect <= canvasMaxAspect);
337
324
  if (canvasMaxAspect && innerAspect > canvasMaxAspect)
338
325
  {
339
326
  // full height
@@ -364,15 +351,9 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
364
351
  mainContext.lineJoin = 'round';
365
352
  mainContext.lineCap = 'round';
366
353
  }
367
-
368
- // wait for gameInit to load
369
- async function startEngine()
370
- {
371
- await gameInit();
372
- engineUpdate();
373
- }
374
- if (headlessMode)
375
- return startEngine();
354
+
355
+ // skip setup if headless
356
+ if (headlessMode) return startEngine();
376
357
 
377
358
  // setup webgl
378
359
  glInit(rootElement);
@@ -387,7 +368,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
387
368
  'touch-action:none;' + // prevent mobile pinch to resize
388
369
  '-webkit-touch-callout:none'; // compatibility for ios
389
370
  rootElement.style.cssText = styleRoot;
390
- drawCanvas = mainCanvas = rootElement.appendChild(document.createElement('canvas'));
371
+ mainCanvas = rootElement.appendChild(document.createElement('canvas'));
391
372
  drawContext = mainContext = mainCanvas.getContext('2d');
392
373
 
393
374
  // init stuff and start engine
@@ -396,7 +377,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
396
377
  debugInit();
397
378
 
398
379
  // setup canvases
399
- // transform way is still more reliable then flexbox or grid
380
+ // transform way is still more reliable than flexbox or grid
400
381
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
401
382
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
402
383
  mainCanvas.style.cssText = styleCanvas;
@@ -441,6 +422,9 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
441
422
  }));
442
423
  }
443
424
 
425
+ // load engine font image
426
+ promises.push(fontImageInit());
427
+
444
428
  if (showSplashScreen)
445
429
  {
446
430
  // draw splash screen
@@ -452,7 +436,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
452
436
  function updateSplash()
453
437
  {
454
438
  inputClear();
455
- drawEngineSplashScreen(t+=.01);
439
+ drawEngineLogo(t+=.01);
456
440
  t>1 ? resolve() : setTimeout(updateSplash, 16);
457
441
  }
458
442
  }));
@@ -462,171 +446,12 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
462
446
  await Promise.all(promises);
463
447
  return startEngine();
464
448
 
465
- ///////////////////////////////////////////////////////////////////////////
466
- // LittleJS Splash Screen
467
- function drawEngineSplashScreen(t)
449
+ async function startEngine()
468
450
  {
469
- const x = mainContext;
470
- const w = mainCanvas.width = innerWidth;
471
- const h = mainCanvas.height = innerHeight;
472
-
473
- {
474
- // background
475
- const p3 = percent(t, 1, .8);
476
- const p4 = percent(t, 0, .5);
477
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
478
- g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
479
- g.addColorStop(1,hsl(0,0,0,p3).toString());
480
- x.save();
481
- x.fillStyle = g;
482
- x.fillRect(0,0,w,h);
483
- }
484
-
485
- // draw LittleJS logo...
486
- const rect = (X, Y, W, H, C)=>
487
- {
488
- x.beginPath();
489
- x.rect(X,Y,W,C?H*p:H);
490
- x.fillStyle = C;
491
- C ? x.fill() : x.stroke();
492
- };
493
- const line = (X, Y, Z, W)=>
494
- {
495
- x.beginPath();
496
- x.lineTo(X,Y);
497
- x.lineTo(Z,W);
498
- x.stroke();
499
- };
500
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
501
- {
502
- const D = (A+B)/2, E = p*(B-A)/2;
503
- x.beginPath();
504
- F && x.lineTo(X,Y);
505
- x.arc(X,Y,R,D-E,D+E);
506
- x.fillStyle = C;
507
- C ? x.fill() : x.stroke();
508
- };
509
- const color = (c=0, l=0) =>
510
- hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();
511
- const alpha = wave(1,1,t);
512
- const p = percent(alpha, .1, .5);
513
-
514
- // setup
515
- x.translate(w/2,h/2);
516
- const size = min(6, min(w,h)/99); // fit to screen
517
- x.scale(size,size);
518
- x.translate(-40,-35);
519
- x.lineJoin = x.lineCap = 'round';
520
- x.lineWidth = .1 + p*1.9;
521
-
522
- // drawing effect
523
- const p2 = percent(alpha,.1,1);
524
- x.setLineDash([99*p2,99]);
525
-
526
- // cab top
527
- rect(7,16,18,-8,color(2,2));
528
- rect(7,8,18,4,color(2,3));
529
- rect(25,8,8,8,color(2,1));
530
- rect(25,8,-18,8);
531
- rect(25,8,8,8);
532
-
533
- // cab
534
- rect(25,16,7,23,color());
535
- rect(11,39,14,-23,color(1,1));
536
- rect(11,16,14,18,color(1,2));
537
- rect(11,16,14,8,color(1,3));
538
- rect(25,16,-14,24);
539
-
540
- // cab window
541
- rect(15,29,6,-9,color(2,2));
542
- circle(15,21,5,0,PI/2,color(2,4),1);
543
- rect(21,21,-6,9);
544
-
545
- // little stack
546
- rect(37,14,9,6,color(3,2));
547
- rect(37,14,4.5,6,color(3,3));
548
- rect(37,14,9,6);
549
-
550
- // big stack
551
- rect(50,20,10,-8,color(0,1));
552
- rect(50,20,6.5,-8,color(0,2));
553
- rect(50,20,3.5,-8,color(0,3));
554
- rect(50,20,10,-8);
555
- circle(55,2,11.4,.5,PI-.5,color(3,3));
556
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
557
- circle(55,2,11.4,.5,PI-.5);
558
- rect(45,7,20,-7,color(0,2));
559
- rect(45,-1,20,4,color(0,3));
560
- rect(45,-1,20,8);
561
-
562
- // engine
563
- for (let i=5; i--;)
564
- {
565
- // stagger radius to fix slight seam
566
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
567
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
568
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
569
- }
570
-
571
- // engine outline
572
- circle(36,30,10,PI/2,PI*3/2);
573
- circle(48,30,10,PI/2,PI*3/2);
574
- circle(60,30,10);
575
- line(36,20,60,20);
576
-
577
- // engine front light
578
- circle(60,30,4,PI,3*PI,color(3,2));
579
- circle(60,30,4,PI,2*PI,color(3,3));
580
- circle(60,30,4,PI,3*PI);
581
-
582
- // front brush
583
- for (let i=6; i--;)
584
- {
585
- x.beginPath();
586
- x.lineTo(53,54);
587
- x.lineTo(53,40);
588
- x.lineTo(53+(1+i*2.9)*p,40);
589
- x.lineTo(53+(4+i*3.5)*p,54);
590
- x.fillStyle = color(0,i%2+2);
591
- x.fill();
592
- i%2 && x.stroke();
593
- }
594
-
595
- // wheels
596
- rect(6,40,5,5);
597
- rect(6,40,5,5,color());
598
- rect(15,54,38,-14,color());
599
- for (let i=3; i--;)
600
- for (let j=2; j--;)
601
- {
602
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
603
- x.stroke();
604
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
605
- x.stroke();
606
- }
607
- line(6,40,68,40); // center
608
- line(77,54,4,54); // bottom
609
-
610
- // draw engine name
611
- const s = engineName;
612
- x.font = '900 16px arial';
613
- x.textAlign = 'center';
614
- x.textBaseline = 'top';
615
- x.lineWidth = .1+p*3.9;
616
- let w2 = 0;
617
- for (let i=0; i<s.length; ++i)
618
- w2 += x.measureText(s[i]).width;
619
- for (let j=2; j--;)
620
- for (let i=0, X=41-w2/2; i<s.length; ++i)
621
- {
622
- x.fillStyle = color(i,2);
623
- const w = x.measureText(s[i]).width;
624
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
625
- X += w;
626
- }
627
- x.restore();
451
+ // wait for gameInit to load
452
+ await gameInit();
453
+ engineUpdate();
628
454
  }
629
- ///////////////////////////////////////////////////////////////////////////
630
455
  }
631
456
 
632
457
  /** Update each engine object, remove destroyed objects, and update time
@@ -640,8 +465,7 @@ function engineObjectsUpdate()
640
465
  // recursive object update
641
466
  function updateObject(o)
642
467
  {
643
- if (o.destroyed)
644
- return;
468
+ if (o.destroyed) return;
645
469
 
646
470
  o.update();
647
471
  for (const child of o.children)
@@ -649,8 +473,7 @@ function engineObjectsUpdate()
649
473
  }
650
474
  for (const o of engineObjects)
651
475
  {
652
- if (o.parent)
653
- continue;
476
+ if (o.parent || o.destroyed) continue;
654
477
 
655
478
  // update top level objects
656
479
  o.update();
@@ -739,6 +562,171 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
739
562
 
740
563
  debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
741
564
  return hitObjects;
565
+ }
566
+
567
+ ///////////////////////////////////////////////////////////////////////////////
568
+ function drawEngineLogo(t)
569
+ {
570
+ // LittleJS Logo and Splash Screen
571
+ const x = mainContext;
572
+ const w = mainCanvas.width = innerWidth;
573
+ const h = mainCanvas.height = innerHeight;
574
+
575
+ {
576
+ // background
577
+ const p3 = percent(t, 1, .8);
578
+ const p4 = percent(t, 0, .5);
579
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
580
+ g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
581
+ g.addColorStop(1,hsl(0,0,0,p3).toString());
582
+ x.save();
583
+ x.fillStyle = g;
584
+ x.fillRect(0,0,w,h);
585
+ }
586
+
587
+ // draw LittleJS logo...
588
+ const rect = (X, Y, W, H, C)=>
589
+ {
590
+ x.beginPath();
591
+ x.rect(X,Y,W,C?H*p:H);
592
+ x.fillStyle = C;
593
+ C ? x.fill() : x.stroke();
594
+ };
595
+ const line = (X, Y, Z, W)=>
596
+ {
597
+ x.beginPath();
598
+ x.lineTo(X,Y);
599
+ x.lineTo(Z,W);
600
+ x.stroke();
601
+ };
602
+ const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
603
+ {
604
+ const D = (A+B)/2, E = p*(B-A)/2;
605
+ x.beginPath();
606
+ F && x.lineTo(X,Y);
607
+ x.arc(X,Y,R,D-E,D+E);
608
+ x.fillStyle = C;
609
+ C ? x.fill() : x.stroke();
610
+ };
611
+ const color = (c=0, l=0) =>
612
+ hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
613
+ const alpha = wave(1,1,t);
614
+ const p = percent(alpha, .1, .5);
615
+
616
+ // setup
617
+ x.translate(w/2,h/2);
618
+ const size = min(6, min(w,h)/99); // fit to screen
619
+ x.scale(size,size);
620
+ x.translate(-40,-35);
621
+ x.lineJoin = x.lineCap = 'round';
622
+ x.lineWidth = .1 + p*1.9;
623
+
624
+ // drawing effect
625
+ const p2 = percent(alpha,.1,1);
626
+ x.setLineDash([99*p2,99]);
627
+
628
+ // cab top
629
+ rect(7,16,18,-8,color(2,2));
630
+ rect(7,8,18,4,color(2,3));
631
+ rect(25,8,8,8,color(2,1));
632
+ rect(25,8,-18,8);
633
+ rect(25,8,8,8);
634
+
635
+ // cab
636
+ rect(25,16,7,24,color());
637
+ rect(11,39,14,-23,color(1,1));
638
+ rect(11,16,14,18,color(1,2));
639
+ rect(11,16,14,8,color(1,3));
640
+ rect(25,16,-14,24);
641
+
642
+ // cab window
643
+ rect(15,29,6,-9,color(2,2));
644
+ circle(15,21,5,0,PI/2,color(2,4),1);
645
+ rect(21,21,-6,9);
646
+
647
+ // little stack
648
+ rect(37,14,9,6,color(3,2));
649
+ rect(37,14,4.5,6,color(3,3));
650
+ rect(37,14,9,6);
651
+
652
+ // big stack
653
+ rect(50,20,10,-8,color(0,1));
654
+ rect(50,20,6.5,-8,color(0,2));
655
+ rect(50,20,3.5,-8,color(0,3));
656
+ rect(50,20,10,-8);
657
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
658
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
659
+ circle(55,2,11.4,.5,PI-.5);
660
+ rect(45,7,20,-7,color(0,2));
661
+ rect(45,-1,20,4,color(0,3));
662
+ rect(45,-1,20,8);
663
+
664
+ // engine
665
+ for (let i=5; i--;)
666
+ {
667
+ // stagger radius to fix slight seam
668
+ circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
669
+ circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
670
+ circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
671
+ }
672
+
673
+ // engine outline
674
+ circle(36,30,10,PI/2,PI*3/2);
675
+ circle(48,30,10,PI/2,PI*3/2);
676
+ circle(60,30,10);
677
+ line(36,20,60,20);
678
+
679
+ // engine front light
680
+ circle(60,30,4,PI,3*PI,color(3,2));
681
+ circle(60,30,4,PI,2*PI,color(3,3));
682
+ circle(60,30,4,PI,3*PI);
683
+
684
+ // front brush
685
+ for (let i=6; i--;)
686
+ {
687
+ x.beginPath();
688
+ x.lineTo(53,54);
689
+ x.lineTo(53,40);
690
+ x.lineTo(53+(1+i*2.9)*p,40);
691
+ x.lineTo(53+(4+i*3.5)*p,54);
692
+ x.fillStyle = color(0,i%2+2);
693
+ x.fill();
694
+ i%2 && x.stroke();
695
+ }
696
+
697
+ // wheels
698
+ rect(6,40,5,5);
699
+ rect(6,40,5,5,color());
700
+ rect(15,54,38,-14,color());
701
+ for (let i=3; i--;)
702
+ for (let j=2; j--;)
703
+ {
704
+ circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
705
+ x.stroke();
706
+ circle(15*i+15,47,j?7:1,0,PI,color(i,2));
707
+ x.stroke();
708
+ }
709
+ line(6,40,68,40); // center
710
+ line(77,54,4,54); // bottom
711
+
712
+ // draw engine name
713
+ const s = engineName;
714
+ x.font = '900 16px arial';
715
+ x.textAlign = 'center';
716
+ x.textBaseline = 'top';
717
+ x.lineWidth = .1+p*3.9;
718
+ let w2 = 0;
719
+ for (let i=0; i<s.length; ++i)
720
+ w2 += x.measureText(s[i]).width;
721
+ for (let j=2; j--;)
722
+ for (let i=0, X=41-w2/2; i<s.length; ++i)
723
+ {
724
+ x.fillStyle = color(i,2);
725
+ const w = x.measureText(s[i]).width;
726
+ x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
727
+ X += w;
728
+ }
729
+ x.restore();
742
730
  }
743
731
  /**
744
732
  * LittleJS - Release Mode
@@ -762,6 +750,7 @@ function LOG (){}
762
750
  function debugInit (){}
763
751
  function debugUpdate (){}
764
752
  function debugRender (){}
753
+ function debugRenderPost (){}
765
754
  function debugRect (){}
766
755
  function debugPoly (){}
767
756
  function debugCircle (){}
@@ -771,9 +760,6 @@ function debugOverlap (){}
771
760
  function debugText (){}
772
761
  function debugClear (){}
773
762
  function debugScreenshot (){}
774
- function debugSaveCanvas (){}
775
- function debugSaveText (){}
776
- function debugSaveDataURL(){}
777
763
  function debugShowErrors(){}
778
764
  function debugVideoCaptureIsActive(){ return false; }
779
765
  function debugVideoCaptureStart (){}
@@ -781,105 +767,104 @@ function debugVideoCaptureStop (){}
781
767
  function debugVideoCaptureUpdate(){}
782
768
  function debugProtectConstant(o){ return o; }
783
769
  /**
784
- * LittleJS Utility Classes and Functions
770
+ * LittleJS Math Classes and Functions
785
771
  * - General purpose math library
786
- * - Vector2 - fast, simple, easy 2D vector class
787
- * - Color - holds a rgba color with some math functions
788
- * - Timer - tracks time automatically
789
772
  * - RandomGenerator - seeded random number generator
790
- * @namespace Utilities
773
+ * - Vector2 - fast, simple, easy 2D vector class
774
+ * - Color - holds a rgba color with math functions
775
+ * @namespace Math
791
776
  */
792
777
 
793
778
  /** The value of PI
794
779
  * @type {number}
795
780
  * @default Math.PI
796
- * @memberof Utilities */
781
+ * @memberof Math */
797
782
  const PI = Math.PI;
798
783
 
799
784
  /** Returns absolute value of value passed in
800
785
  * @param {number} value
801
786
  * @return {number}
802
- * @memberof Utilities */
787
+ * @memberof Math */
803
788
  const abs = Math.abs;
804
789
 
805
790
  /** Returns floored value of value passed in
806
791
  * @param {number} value
807
792
  * @return {number}
808
- * @memberof Utilities */
793
+ * @memberof Math */
809
794
  const floor = Math.floor;
810
795
 
811
796
  /** Returns ceiled value of value passed in
812
797
  * @param {number} value
813
798
  * @return {number}
814
- * @memberof Utilities */
799
+ * @memberof Math */
815
800
  const ceil = Math.ceil;
816
801
 
817
802
  /** Returns rounded value passed in
818
803
  * @param {number} value
819
804
  * @return {number}
820
- * @memberof Utilities */
805
+ * @memberof Math */
821
806
  const round = Math.round;
822
807
 
823
808
  /** Returns lowest value passed in
824
809
  * @param {...number} values
825
810
  * @return {number}
826
- * @memberof Utilities */
811
+ * @memberof Math */
827
812
  const min = Math.min;
828
813
 
829
814
  /** Returns highest value passed in
830
815
  * @param {...number} values
831
816
  * @return {number}
832
- * @memberof Utilities */
817
+ * @memberof Math */
833
818
  const max = Math.max;
834
819
 
835
820
  /** Returns the sign of value passed in
836
821
  * @param {number} value
837
822
  * @return {number}
838
- * @memberof Utilities */
823
+ * @memberof Math */
839
824
  const sign = Math.sign;
840
825
 
841
826
  /** Returns hypotenuse of values passed in
842
827
  * @param {...number} values
843
828
  * @return {number}
844
- * @memberof Utilities */
829
+ * @memberof Math */
845
830
  const hypot = Math.hypot;
846
831
 
847
832
  /** Returns log2 of value passed in
848
833
  * @param {number} value
849
834
  * @return {number}
850
- * @memberof Utilities */
835
+ * @memberof Math */
851
836
  const log2 = Math.log2;
852
837
 
853
838
  /** Returns sin of value passed in
854
839
  * @param {number} value
855
840
  * @return {number}
856
- * @memberof Utilities */
841
+ * @memberof Math */
857
842
  const sin = Math.sin;
858
843
 
859
844
  /** Returns cos of value passed in
860
845
  * @param {number} value
861
846
  * @return {number}
862
- * @memberof Utilities */
847
+ * @memberof Math */
863
848
  const cos = Math.cos;
864
849
 
865
850
  /** Returns tan of value passed in
866
851
  * @param {number} value
867
852
  * @return {number}
868
- * @memberof Utilities */
853
+ * @memberof Math */
869
854
  const tan = Math.tan;
870
855
 
871
856
  /** Returns atan2 of values passed in
872
857
  * @param {number} y
873
858
  * @param {number} x
874
859
  * @return {number}
875
- * @memberof Utilities */
860
+ * @memberof Math */
876
861
  const atan2 = Math.atan2;
877
862
 
878
863
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
879
864
  * @param {number} dividend
880
865
  * @param {number} [divisor]
881
866
  * @return {number}
882
- * @memberof Utilities */
867
+ * @memberof Math */
883
868
  function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % divisor; }
884
869
 
885
870
  /** Clamps the value between max and min
@@ -887,7 +872,7 @@ function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % di
887
872
  * @param {number} [min]
888
873
  * @param {number} [max]
889
874
  * @return {number}
890
- * @memberof Utilities */
875
+ * @memberof Math */
891
876
  function clamp(value, min=0, max=1) { return value < min ? min : value > max ? max : value; }
892
877
 
893
878
  /** Returns what percentage the value is between valueA and valueB
@@ -895,7 +880,7 @@ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? m
895
880
  * @param {number} valueA
896
881
  * @param {number} valueB
897
882
  * @return {number}
898
- * @memberof Utilities */
883
+ * @memberof Math */
899
884
  function percent(value, valueA, valueB)
900
885
  { return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
901
886
 
@@ -904,7 +889,7 @@ function percent(value, valueA, valueB)
904
889
  * @param {number} valueB
905
890
  * @param {number} percent
906
891
  * @return {number}
907
- * @memberof Utilities */
892
+ * @memberof Math */
908
893
  function lerp(valueA, valueB, percent)
909
894
  { return valueA + clamp(percent) * (valueB-valueA); }
910
895
 
@@ -916,7 +901,7 @@ function lerp(valueA, valueB, percent)
916
901
  * @param {number} lerpA
917
902
  * @param {number} lerpB
918
903
  * @return {number}
919
- * @memberof Utilities */
904
+ * @memberof Math */
920
905
  function percentLerp(value, percentA, percentB, lerpA, lerpB)
921
906
  { return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
922
907
 
@@ -925,7 +910,7 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
925
910
  * @param {number} valueB
926
911
  * @param {number} [wrapSize]
927
912
  * @return {number}
928
- * @memberof Utilities */
913
+ * @memberof Math */
929
914
  function distanceWrap(valueA, valueB, wrapSize=1)
930
915
  { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
931
916
 
@@ -935,7 +920,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
935
920
  * @param {number} percent
936
921
  * @param {number} [wrapSize]
937
922
  * @return {number}
938
- * @memberof Utilities */
923
+ * @memberof Math */
939
924
  function lerpWrap(valueA, valueB, percent, wrapSize=1)
940
925
  { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
941
926
 
@@ -943,7 +928,7 @@ function lerpWrap(valueA, valueB, percent, wrapSize=1)
943
928
  * @param {number} angleA
944
929
  * @param {number} angleB
945
930
  * @return {number}
946
- * @memberof Utilities */
931
+ * @memberof Math */
947
932
  function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
948
933
 
949
934
  /** Linearly interpolates between the angles passed in with wrapping
@@ -951,35 +936,35 @@ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*P
951
936
  * @param {number} angleB
952
937
  * @param {number} percent
953
938
  * @return {number}
954
- * @memberof Utilities */
939
+ * @memberof Math */
955
940
  function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
956
941
 
957
942
  /** Applies smoothstep function to the percentage value
958
943
  * @param {number} percent
959
944
  * @return {number}
960
- * @memberof Utilities */
945
+ * @memberof Math */
961
946
  function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
962
947
 
963
948
  /** Checks if the value passed in is a power of two
964
949
  * @param {number} value
965
950
  * @return {boolean}
966
- * @memberof Utilities */
951
+ * @memberof Math */
967
952
  function isPowerOfTwo(value) { return !(value & (value - 1)); }
968
953
 
969
954
  /** Returns the nearest power of two not less than the value
970
955
  * @param {number} value
971
956
  * @return {number}
972
- * @memberof Utilities */
957
+ * @memberof Math */
973
958
  function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
974
959
 
975
960
  /** Returns true if two axis aligned bounding boxes are overlapping
976
961
  * this can be used for simple collision detection between objects
977
- * @param {Vector2} posA - Center of box A
978
- * @param {Vector2} sizeA - Size of box A
979
- * @param {Vector2} posB - Center of box B
980
- * @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
981
- * @return {boolean} - True if overlapping
982
- * @memberof Utilities */
962
+ * @param {Vector2} posA - Center of box A
963
+ * @param {Vector2} sizeA - Size of box A
964
+ * @param {Vector2} posB - Center of box B
965
+ * @param {Vector2} [sizeB=vec2()] - Size of box B, uses a point if undefined
966
+ * @return {boolean} - True if overlapping
967
+ * @memberof Math */
983
968
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
984
969
  {
985
970
  const dx = (posA.x - posB.x)*2;
@@ -995,7 +980,7 @@ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
995
980
  * @param {Vector2} pos - Center of box
996
981
  * @param {Vector2} size - Size of box
997
982
  * @return {boolean} - True if intersecting
998
- * @memberof Utilities */
983
+ * @memberof Math */
999
984
  function isIntersecting(start, end, pos, size)
1000
985
  {
1001
986
  // Liang-Barsky algorithm
@@ -1036,52 +1021,29 @@ function isIntersecting(start, end, pos, size)
1036
1021
  * @param {number} [t=time] - Value to use for time of the wave
1037
1022
  * @param {number} [offset] - Value to use for time offset of the wave
1038
1023
  * @return {number} - Value waving between 0 and amplitude
1039
- * @memberof Utilities */
1024
+ * @memberof Math */
1040
1025
  function wave(frequency=1, amplitude=1, t=time, offset=0)
1041
1026
  { return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
1042
1027
 
1043
- /** Formats seconds to mm:ss style for display purposes
1044
- * @param {number} t - time in seconds
1045
- * @return {string}
1046
- * @memberof Utilities */
1047
- function formatTime(t)
1048
- {
1049
- const sign = t < 0 ? '-' : '';
1050
- t = abs(t)|0;
1051
- return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
1052
- }
1053
-
1054
- /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
1055
- * @param {string} url - URL of JSON file
1056
- * @return {Promise<object>}
1057
- * @memberof Utilities */
1058
- async function fetchJSON(url)
1059
- {
1060
- const response = await fetch(url);
1061
- if (!response.ok)
1062
- throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1063
- return response.json();
1064
- }
1065
-
1066
1028
  /**
1067
1029
  * Check if object is a valid number, not NaN or undefined, but it may be infinite
1068
1030
  * @param {any} n
1069
1031
  * @return {boolean}
1070
- * @memberof Utilities */
1032
+ * @memberof Math */
1071
1033
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1072
1034
 
1073
1035
  /**
1074
1036
  * Check if object is a valid string or can be converted to one
1075
1037
  * @param {any} s
1076
1038
  * @return {boolean}
1077
- * @memberof Utilities */
1078
- function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1039
+ * @memberof Math */
1040
+ function isString(s) { return s != null && typeof s?.toString() === 'string'; }
1079
1041
 
1080
1042
  /**
1081
1043
  * Check if object is an array
1082
1044
  * @param {any} a
1083
1045
  * @return {boolean}
1084
- * @memberof Utilities */
1046
+ * @memberof Math */
1085
1047
  function isArray(a) { return Array.isArray(a); }
1086
1048
 
1087
1049
  /**
@@ -1097,7 +1059,7 @@ function isArray(a) { return Array.isArray(a); }
1097
1059
  * @param {LineTestFunction} testFunction - Check if colliding
1098
1060
  * @param {Vector2} [normal] - Optional vector to store the normal
1099
1061
  * @return {Vector2|undefined} - Position of the collision or undefined if none found
1100
- * @memberof Utilities */
1062
+ * @memberof Math */
1101
1063
  function lineTest(posStart, posEnd, testFunction, normal)
1102
1064
  {
1103
1065
  ASSERT(isVector2(posStart), 'posStart must be a vec2');
@@ -1109,8 +1071,7 @@ function lineTest(posStart, posEnd, testFunction, normal)
1109
1071
  const dx = posEnd.x - posStart.x;
1110
1072
  const dy = posEnd.y - posStart.y;
1111
1073
  const totalLength = hypot(dx, dy);
1112
- if (!totalLength)
1113
- return;
1074
+ if (!totalLength) return;
1114
1075
 
1115
1076
  // current integer cell we are in
1116
1077
  const pos = posStart.floor();
@@ -1220,8 +1181,8 @@ function randInCircle(radius=1, minRadius=0)
1220
1181
  { return radius > 0 ? randVec2(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
1221
1182
 
1222
1183
  /** Returns a random color between the two passed in colors, combine components if linear
1223
- * @param {Color} [colorA=(1,1,1,1)]
1224
- * @param {Color} [colorB=(0,0,0,1)]
1184
+ * @param {Color} [colorA=WHITE]
1185
+ * @param {Color} [colorB=BLACK]
1225
1186
  * @param {boolean} [linear]
1226
1187
  * @return {Color}
1227
1188
  * @memberof Random */
@@ -1300,8 +1261,8 @@ class RandomGenerator
1300
1261
  { return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
1301
1262
 
1302
1263
  /** Returns a random color between the two passed in colors, combine components if linear
1303
- * @param {Color} [colorA=(1,1,1,1)]
1304
- * @param {Color} [colorB=(0,0,0,1)]
1264
+ * @param {Color} [colorA=WHITE]
1265
+ * @param {Color} [colorB=BLACK]
1305
1266
  * @param {boolean} [linear]
1306
1267
  * @return {Color} */
1307
1268
  randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
@@ -1344,14 +1305,14 @@ class RandomGenerator
1344
1305
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
1345
1306
  * a = vec2(5); // set a to (5, 5)
1346
1307
  * b = vec2(); // set b to (0, 0)
1347
- * @memberof Utilities */
1348
- function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
1308
+ * @memberof Math */
1309
+ function vec2(x=0, y) { return new Vector2(x, y ?? x); }
1349
1310
 
1350
1311
  /**
1351
1312
  * Check if object is a valid Vector2
1352
1313
  * @param {any} v
1353
1314
  * @return {boolean}
1354
- * @memberof Utilities */
1315
+ * @memberof Math */
1355
1316
  function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
1356
1317
 
1357
1318
  // vector2 asserts
@@ -1553,10 +1514,6 @@ class Vector2
1553
1514
  * @return {number} */
1554
1515
  area() { return abs(this.x * this.y); }
1555
1516
 
1556
- /** Returns true if this vector is (0,0)
1557
- * @return {boolean} */
1558
- isZero() { return !this.x && !this.y; }
1559
-
1560
1517
  /** Returns a new vector that is p percent between this and the vector passed in
1561
1518
  * @param {Vector2} v - other vector
1562
1519
  * @param {number} percent
@@ -1601,7 +1558,7 @@ class Vector2
1601
1558
  * @param {number} [b=1] - blue
1602
1559
  * @param {number} [a=1] - alpha
1603
1560
  * @return {Color}
1604
- * @memberof Utilities
1561
+ * @memberof Math
1605
1562
  */
1606
1563
  function rgb(r, g, b, a) { return new Color(r, g, b, a); }
1607
1564
 
@@ -1612,14 +1569,14 @@ function rgb(r, g, b, a) { return new Color(r, g, b, a); }
1612
1569
  * @param {number} [l=1] - lightness
1613
1570
  * @param {number} [a=1] - alpha
1614
1571
  * @return {Color}
1615
- * @memberof Utilities */
1572
+ * @memberof Math */
1616
1573
  function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
1617
1574
 
1618
1575
  /**
1619
1576
  * Check if object is a valid Color
1620
1577
  * @param {any} c
1621
1578
  * @return {boolean}
1622
- * @memberof Utilities */
1579
+ * @memberof Math */
1623
1580
  function isColor(c) { return c instanceof Color && c.isValid(); }
1624
1581
 
1625
1582
  // color asserts
@@ -1800,7 +1757,7 @@ class Color
1800
1757
  toString(useAlpha = true)
1801
1758
  {
1802
1759
  if (debug && !this.isValid())
1803
- return `#000`;
1760
+ return '#000';
1804
1761
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
1805
1762
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
1806
1763
  }
@@ -1857,68 +1814,158 @@ class Color
1857
1814
 
1858
1815
  /** Color - White #ffffff
1859
1816
  * @type {Color}
1860
- * @memberof Utilities */
1817
+ * @memberof Math */
1861
1818
  const WHITE = debugProtectConstant(rgb());
1862
1819
 
1863
1820
  /** Color - Clear White #757474ff with 0 alpha
1864
1821
  * @type {Color}
1865
- * @memberof Utilities */
1822
+ * @memberof Math */
1866
1823
  const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
1867
1824
 
1868
1825
  /** Color - Black #000000
1869
1826
  * @type {Color}
1870
- * @memberof Utilities */
1827
+ * @memberof Math */
1871
1828
  const BLACK = debugProtectConstant(rgb(0,0,0));
1872
1829
 
1873
1830
  /** Color - Clear Black #000000 with 0 alpha
1874
1831
  * @type {Color}
1875
- * @memberof Utilities */
1832
+ * @memberof Math */
1876
1833
  const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
1877
1834
 
1878
1835
  /** Color - Gray #808080
1879
1836
  * @type {Color}
1880
- * @memberof Utilities */
1837
+ * @memberof Math */
1881
1838
  const GRAY = debugProtectConstant(rgb(.5,.5,.5));
1882
1839
 
1883
1840
  /** Color - Red #ff0000
1884
1841
  * @type {Color}
1885
- * @memberof Utilities */
1842
+ * @memberof Math */
1886
1843
  const RED = debugProtectConstant(rgb(1,0,0));
1887
1844
 
1888
1845
  /** Color - Orange #ff8000
1889
1846
  * @type {Color}
1890
- * @memberof Utilities */
1847
+ * @memberof Math */
1891
1848
  const ORANGE = debugProtectConstant(rgb(1,.5,0));
1892
1849
 
1893
1850
  /** Color - Yellow #ffff00
1894
1851
  * @type {Color}
1895
- * @memberof Utilities */
1852
+ * @memberof Math */
1896
1853
  const YELLOW = debugProtectConstant(rgb(1,1,0));
1897
1854
 
1898
1855
  /** Color - Green #00ff00
1899
1856
  * @type {Color}
1900
- * @memberof Utilities */
1857
+ * @memberof Math */
1901
1858
  const GREEN = debugProtectConstant(rgb(0,1,0));
1902
1859
 
1903
1860
  /** Color - Cyan #00ffff
1904
1861
  * @type {Color}
1905
- * @memberof Utilities */
1862
+ * @memberof Math */
1906
1863
  const CYAN = debugProtectConstant(rgb(0,1,1));
1907
1864
 
1908
1865
  /** Color - Blue #0000ff
1909
1866
  * @type {Color}
1910
- * @memberof Utilities */
1867
+ * @memberof Math */
1911
1868
  const BLUE = debugProtectConstant(rgb(0,0,1));
1912
1869
 
1913
1870
  /** Color - Purple #8000ff
1914
1871
  * @type {Color}
1915
- * @memberof Utilities */
1872
+ * @memberof Math */
1916
1873
  const PURPLE = debugProtectConstant(rgb(.5,0,1));
1917
1874
 
1918
1875
  /** Color - Magenta #ff00ff
1919
1876
  * @type {Color}
1877
+ * @memberof Math */
1878
+ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1879
+ /**
1880
+ * LittleJS Utility Classes and Functions
1881
+ * - General purpose utilities
1882
+ * - Timer - tracks time automatically
1883
+ * @namespace Utilities
1884
+ */
1885
+
1886
+ /** Formats seconds to mm:ss style for display purposes
1887
+ * @param {number} t - time in seconds
1888
+ * @return {string}
1889
+ * @memberof Utilities */
1890
+ function formatTime(t)
1891
+ {
1892
+ const sign = t < 0 ? '-' : '';
1893
+ t = abs(t)|0;
1894
+ return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
1895
+ }
1896
+
1897
+ /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
1898
+ * @param {string} url - URL of JSON file
1899
+ * @return {Promise<object>}
1900
+ * @memberof Utilities */
1901
+ async function fetchJSON(url)
1902
+ {
1903
+ const response = await fetch(url);
1904
+ if (!response.ok)
1905
+ throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1906
+ return response.json();
1907
+ }
1908
+
1909
+ ///////////////////////////////////////////////////////////////////////////////
1910
+
1911
+ /** Save a text file to disk
1912
+ * @param {string} text
1913
+ * @param {string} [filename]
1914
+ * @param {string} [type]
1920
1915
  * @memberof Utilities */
1921
- const MAGENTA = debugProtectConstant(rgb(1,0,1));
1916
+ function saveText(text, filename='text', type='text/plain')
1917
+ { saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
1918
+
1919
+ /** Save a canvas to disk
1920
+ * @param {HTMLCanvasElement|OffscreenCanvas} canvas
1921
+ * @param {string} [filename]
1922
+ * @param {string} [type]
1923
+ * @memberof Utilities */
1924
+ function saveCanvas(canvas, filename='screenshot', type='image/png')
1925
+ {
1926
+ if (canvas instanceof OffscreenCanvas)
1927
+ {
1928
+ // copy to temporary canvas and save
1929
+ const saveCanvas = document.createElement('canvas');
1930
+ saveCanvas.width = canvas.width;
1931
+ saveCanvas.height = canvas.height;
1932
+ saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
1933
+ saveDataURL(saveCanvas.toDataURL(type), filename);
1934
+ }
1935
+ else
1936
+ saveDataURL(canvas.toDataURL(type), filename);
1937
+ }
1938
+
1939
+ /** Save a data url to disk
1940
+ * @param {string} url
1941
+ * @param {string} [filename]
1942
+ * @param {number} [revokeTime] - how long before revoking the url
1943
+ * @memberof Utilities */
1944
+ function saveDataURL(url, filename='download', revokeTime)
1945
+ {
1946
+ ASSERT(isString(url), 'saveDataURL requires url string');
1947
+ ASSERT(isString(filename), 'saveDataURL requires filename string');
1948
+
1949
+ // create link for saving screenshots
1950
+ const link = document.createElement('a');
1951
+ link.download = filename;
1952
+ link.href = url;
1953
+ link.click();
1954
+ if (revokeTime !== undefined)
1955
+ setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
1956
+ }
1957
+
1958
+ /** Share content using the native share dialog if available
1959
+ * @param {string} title - title of the share
1960
+ * @param {string} url - url to share
1961
+ * @param {Function} [callback] - Called when share is complete
1962
+ * @memberof Utilities */
1963
+ function shareURL(title, url, callback)
1964
+ {
1965
+ ASSERT(isString(title), 'shareURL requires title string');
1966
+ ASSERT(isString(url), 'shareURL requires url string');
1967
+ navigator.share?.({title, url}).then(()=>callback?.());
1968
+ }
1922
1969
 
1923
1970
  ///////////////////////////////////////////////////////////////////////////////
1924
1971
 
@@ -2040,7 +2087,7 @@ let cameraScale = 32;
2040
2087
  * @memberof Settings */
2041
2088
  let canvasColorTiles = true;
2042
2089
 
2043
- /** Color to clear the canvas to before render
2090
+ /** Color to clear the canvas to before render, does not clear if alpha is 0
2044
2091
  * @type {Color}
2045
2092
  * @memberof Draw */
2046
2093
  let canvasClearColor = CLEAR_BLACK;
@@ -2349,7 +2396,7 @@ function setCameraScale(scale) { cameraScale = scale; }
2349
2396
  * @memberof Settings */
2350
2397
  function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
2351
2398
 
2352
- /** Set color to clear the canvas to before render
2399
+ /** Set color to clear the canvas to before render, does not clear if alpha is 0
2353
2400
  * @param {Color} color
2354
2401
  * @memberof Settings */
2355
2402
  function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
@@ -2629,10 +2676,10 @@ function setDebugKey(key) { debugKey = key; }
2629
2676
  class EngineObject
2630
2677
  {
2631
2678
  /** Create an engine object and adds it to the list of objects
2632
- * @param {Vector2} [pos=(0,0)] - World space position of the object
2633
- * @param {Vector2} [size=(1,1)] - World space size of the object
2634
- * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2635
- * @param {number} [angle] - Angle the object is rotated by
2679
+ * @param {Vector2} [pos=vec2()] - World space position of the object
2680
+ * @param {Vector2} [size=vec2(1)] - World space size of the object
2681
+ * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2682
+ * @param {number} [angle] - Angle the object is rotated by
2636
2683
  * @param {Color} [color=WHITE] - Color to apply to tile when rendered
2637
2684
  * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2638
2685
  */
@@ -2760,15 +2807,16 @@ class EngineObject
2760
2807
  // physics sanity checks
2761
2808
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
2762
2809
  ASSERT(this.damping >= 0 && this.damping <= 1);
2763
- if (!enablePhysicsSolver || !this.mass) // don't do collision for static objects
2764
- return;
2810
+
2811
+ // don't do collision for static objects or if solver disabled
2812
+ if (!enablePhysicsSolver || !this.mass) return;
2765
2813
 
2766
2814
  const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
2767
2815
  if (this.groundObject)
2768
2816
  {
2769
2817
  // apply friction in local space of ground object
2770
2818
  const friction = max(this.friction, this.groundObject.friction);
2771
- const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
2819
+ const groundSpeed = this.groundObject.velocity.x;
2772
2820
  this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
2773
2821
  this.groundObject = undefined;
2774
2822
  }
@@ -2779,19 +2827,19 @@ class EngineObject
2779
2827
  const epsilon = .001; // necessary to push slightly outside of the collision
2780
2828
  for (const o of engineObjectsCollide)
2781
2829
  {
2830
+ // skip destroyed, child objects, or self collision
2831
+ if (o.destroyed || o.parent || o === this) continue;
2832
+
2782
2833
  // non solid objects don't collide with each other
2783
- if ((!this.isSolid && !o.isSolid) || o.destroyed || o.parent || o === this)
2784
- continue;
2834
+ if (!this.isSolid && !o.isSolid) continue;
2785
2835
 
2786
2836
  // check collision
2787
- if (!this.isOverlappingObject(o))
2788
- continue;
2837
+ if (!this.isOverlappingObject(o)) continue;
2789
2838
 
2790
2839
  // notify objects of collision and check if should be resolved
2791
2840
  const collide1 = this.collideWithObject(o);
2792
2841
  const collide2 = o.collideWithObject(this);
2793
- if (!collide1 || !collide2)
2794
- continue;
2842
+ if (!collide1 || !collide2) continue;
2795
2843
 
2796
2844
  if (isOverlapping(oldPos, this.size, o.pos, o.size))
2797
2845
  {
@@ -2893,7 +2941,7 @@ class EngineObject
2893
2941
  const delta = y - this.pos.y;
2894
2942
  if (delta < maxMoveUp)
2895
2943
  if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
2896
- {
2944
+ {
2897
2945
  this.pos.y = y;
2898
2946
  debugPhysics && debugRect(this.pos, this.size, '#ff0');
2899
2947
  return;
@@ -2953,10 +3001,10 @@ class EngineObject
2953
3001
 
2954
3002
  // disconnect from parent and destroy children
2955
3003
  this.destroyed = 1;
2956
- this.parent && this.parent.removeChild(this);
3004
+ this.parent?.removeChild(this);
2957
3005
  for (const child of this.children)
2958
3006
  {
2959
- child.parent = 0;
3007
+ child.parent = undefined;
2960
3008
  child.destroy();
2961
3009
  }
2962
3010
  }
@@ -2977,15 +3025,15 @@ class EngineObject
2977
3025
  * @param {Vector2} vec - world space vector */
2978
3026
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
2979
3027
 
2980
- /** Called to check if a tile collision should be resolved
3028
+ /** Called to check if a tile collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
2981
3029
  * @param {number} tileData - the value of the tile at the position
2982
- * @param {Vector2} pos - tile where the collision occurred
2983
- * @return {boolean} - true if the collision should be resolved */
3030
+ * @param {Vector2} pos - tile where the collision occurred
3031
+ * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
2984
3032
  collideWithTile(tileData, pos) { return tileData > 0; }
2985
3033
 
2986
- /** Called to check if a object collision should be resolved
3034
+ /** Called by the engine to check if an object collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
2987
3035
  * @param {EngineObject} object - the object to test against
2988
- * @return {boolean} - true if the collision should be resolved
3036
+ * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
2989
3037
  */
2990
3038
  collideWithObject(object) { return true; }
2991
3039
 
@@ -3026,9 +3074,9 @@ class EngineObject
3026
3074
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
3027
3075
  getMirrorSign() { return this.mirror ? -1 : 1; }
3028
3076
 
3029
- /** Attaches a child to this with a local transform, returns child for chaining
3077
+ /** Attaches a child to this with a local transform, returns child for chaining
3030
3078
  * @param {EngineObject} child
3031
- * @param {Vector2} [localPos=(0,0)]
3079
+ * @param {Vector2} [localPos=vec2()]
3032
3080
  * @param {number} [localAngle]
3033
3081
  * @return {EngineObject} The child object added */
3034
3082
  addChild(child, localPos=vec2(), localAngle=0)
@@ -3052,7 +3100,7 @@ class EngineObject
3052
3100
  const index = this.children.indexOf(child);
3053
3101
  ASSERT(index >= 0, 'child not found in children array');
3054
3102
  index >= 0 && this.children.splice(index, 1);
3055
- child.parent = 0;
3103
+ child.parent = undefined;
3056
3104
  }
3057
3105
 
3058
3106
  /** Check if overlapping another engine object
@@ -3064,7 +3112,7 @@ class EngineObject
3064
3112
 
3065
3113
  /** Check if overlapping a point or aligned bounding box
3066
3114
  * @param {Vector2} pos - Center of box
3067
- * @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
3115
+ * @param {Vector2} [size=vec2()] - Size of box, uses a point if undefined
3068
3116
  * @return {boolean} */
3069
3117
  isOverlapping(pos, size=vec2())
3070
3118
  { return isOverlapping(this.pos, this.size, pos, size); }
@@ -3107,8 +3155,7 @@ class EngineObject
3107
3155
  /** Render debug info for this object */
3108
3156
  renderDebugInfo()
3109
3157
  {
3110
- if (!debug)
3111
- return;
3158
+ if (!debug) return;
3112
3159
 
3113
3160
  // show object info for debugging
3114
3161
  const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
@@ -3150,11 +3197,6 @@ let mainCanvas;
3150
3197
  * @memberof Draw */
3151
3198
  let mainContext;
3152
3199
 
3153
- /** The default canvas to use for drawing, usually mainCanvas
3154
- * @type {HTMLCanvasElement|OffscreenCanvas}
3155
- * @memberof Draw */
3156
- let drawCanvas;
3157
-
3158
3200
  /** The default 2d context to use for drawing, usually mainContext
3159
3201
  * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
3160
3202
  * @memberof Draw */
@@ -3201,10 +3243,11 @@ let drawCount;
3201
3243
  * Create a tile info object using a grid based system
3202
3244
  * - This can take vecs or floats for easier use and conversion
3203
3245
  * - If an index is passed in, the tile size and index will determine the position
3204
- * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
3246
+ * @param {Vector2|number} [index=0] - Index of the tile in 1d or 2d form
3205
3247
  * @param {Vector2|number} [size] - Size of tile in pixels
3206
- * @param {number} [textureIndex] - Texture index to use
3248
+ * @param {TextureInfo|number} [texture] - Texture index or info to use
3207
3249
  * @param {number} [padding] - How many pixels padding around tiles
3250
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3208
3251
  * @return {TileInfo}
3209
3252
  * @example
3210
3253
  * tile(2) // a tile at index 2 using the default tile size of 16
@@ -3212,36 +3255,43 @@ let drawCount;
3212
3255
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
3213
3256
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
3214
3257
  * @memberof Draw */
3215
- function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding)
3258
+ function tile(index=new Vector2, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3216
3259
  {
3217
- if (headlessMode)
3218
- return new TileInfo;
3260
+ ASSERT(isVector2(index) || typeof index === 'number', 'index must be a vec2 or number');
3261
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
3262
+ ASSERT(isNumber(texture) || texture instanceof TextureInfo, 'texture must be a number or TextureInfo');
3263
+ ASSERT(isNumber(padding), 'padding must be a number');
3264
+
3265
+ if (headlessMode) return new TileInfo;
3219
3266
 
3220
- // if size is a number, make it a vector
3221
3267
  if (typeof size === 'number')
3222
3268
  {
3269
+ // if size is a number, make it a vector
3223
3270
  ASSERT(size > 0);
3224
3271
  size = new Vector2(size, size);
3225
3272
  }
3226
3273
 
3227
3274
  // create tile info object
3228
- const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
3275
+ const textureInfo = typeof texture === 'number' ?
3276
+ textureInfos[texture] : texture;
3229
3277
 
3230
3278
  // get the position of the tile
3231
- const textureInfo = textureInfos[textureIndex];
3232
- ASSERT(!!textureInfo, 'Texture not loaded');
3233
3279
  const sizePaddedX = size.x + padding*2;
3234
3280
  const sizePaddedY = size.y + padding*2;
3235
- if (typeof pos === 'number')
3281
+ let x, y;
3282
+ if (typeof index === 'number')
3236
3283
  {
3237
3284
  const cols = textureInfo.size.x / sizePaddedX |0;
3238
- ASSERT(cols > 0, 'Tile size is too big for texture');
3239
- const posX = pos % cols, posY = (pos / cols) |0;
3240
- tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
3285
+ x = index % cols;
3286
+ y = index / cols |0;
3241
3287
  }
3242
3288
  else
3243
- tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
3244
- return tileInfo;
3289
+ {
3290
+ x = index.x;
3291
+ y = index.y;
3292
+ }
3293
+ const pos = new Vector2(x*sizePaddedX + padding, y*sizePaddedY + padding);
3294
+ return new TileInfo(pos, size, textureInfo, padding, bleed);
3245
3295
  }
3246
3296
 
3247
3297
  /**
@@ -3251,24 +3301,22 @@ function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=til
3251
3301
  class TileInfo
3252
3302
  {
3253
3303
  /** Create a tile info object
3254
- * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3304
+ * @param {Vector2} [pos=vec2()] - Top left corner of tile in pixels
3255
3305
  * @param {Vector2} [size] - Size of tile in pixels
3256
- * @param {number} [textureIndex] - Texture index to use
3257
- * @param {number} [padding] - How many pixels padding around tiles
3258
- * @param {number} [bleed] - How many pixels smaller to draw tiles
3306
+ * @param {TextureInfo} [textureInfo] - Texture info to use
3307
+ * @param {number} [padding] - How many pixels padding around tiles
3308
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3259
3309
  */
3260
- constructor(pos=vec2(), size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3310
+ constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
3261
3311
  {
3262
3312
  /** @property {Vector2} - Top left corner of tile in pixels */
3263
3313
  this.pos = pos.copy();
3264
3314
  /** @property {Vector2} - Size of tile in pixels */
3265
3315
  this.size = size.copy();
3266
- /** @property {number} - Texture index to use */
3267
- this.textureIndex = textureIndex;
3268
3316
  /** @property {number} - How many pixels padding around tiles */
3269
3317
  this.padding = padding;
3270
3318
  /** @property {TextureInfo} - The texture info for this tile */
3271
- this.textureInfo = textureInfos[this.textureIndex];
3319
+ this.textureInfo = textureInfo;
3272
3320
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
3273
3321
  this.bleed = bleed;
3274
3322
  }
@@ -3278,7 +3326,7 @@ class TileInfo
3278
3326
  * @return {TileInfo}
3279
3327
  */
3280
3328
  offset(offset)
3281
- { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleed); }
3329
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed); }
3282
3330
 
3283
3331
  /** Returns a copy of this tile offset by a number of animation frames
3284
3332
  * @param {number} frame - Offset to apply in animation frames
@@ -3287,23 +3335,33 @@ class TileInfo
3287
3335
  frame(frame)
3288
3336
  {
3289
3337
  ASSERT(typeof frame === 'number');
3290
- return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
3338
+ const w = this.size.x + this.padding*2;
3339
+ const x = frame*w;
3340
+ ASSERT(x < this.textureInfo.size.x, 'frame extends beyond texture width!');
3341
+ return this.offset(new Vector2(x));
3291
3342
  }
3292
3343
 
3293
3344
  /**
3294
3345
  * Set this tile to use a full image in a texture info
3295
- * @param {TextureInfo} textureInfo
3346
+ * @param {TextureInfo} [textureInfo]
3296
3347
  * @return {TileInfo}
3297
3348
  */
3298
- setFullImage(textureInfo)
3349
+ setFullImage(textureInfo=this.textureInfo)
3299
3350
  {
3351
+ this.textureInfo = textureInfo;
3300
3352
  this.pos = new Vector2;
3301
3353
  this.size = textureInfo.size.copy();
3302
- this.textureInfo = textureInfo;
3303
- // do not use padding or bleed
3304
3354
  this.bleed = this.padding = 0;
3305
3355
  return this;
3306
3356
  }
3357
+
3358
+ /**
3359
+ * Returns a tile info for an index using this tile as refrence
3360
+ * @param {Vector2|number} [index=0]
3361
+ * @return {TileInfo}
3362
+ */
3363
+ tile(index)
3364
+ { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
3307
3365
  }
3308
3366
 
3309
3367
  /**
@@ -3344,19 +3402,19 @@ class TextureInfo
3344
3402
  ///////////////////////////////////////////////////////////////////////////////
3345
3403
  // Drawing functions
3346
3404
 
3347
- /** Draw textured tile centered in world space, with color applied if using WebGL
3348
- * @param {Vector2} pos - Center of the tile in world space
3349
- * @param {Vector2} [size=(1,1)] - Size of the tile in world space
3350
- * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
3351
- * @param {Color} [color=(1,1,1,1)] - Color to modulate with
3352
- * @param {number} [angle] - Angle to rotate by
3353
- * @param {boolean} [mirror] - Is image flipped along the Y axis?
3354
- * @param {Color} [additiveColor] - Additive color to be applied if any
3405
+ /** Draw textured tile centered in world space
3406
+ * @param {Vector2} pos - Center of the tile in world space
3407
+ * @param {Vector2} [size=vec2(1)] - Size of the tile in world space
3408
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
3409
+ * @param {Color} [color=WHITE] - Color to modulate with
3410
+ * @param {number} [angle] - Angle to rotate by
3411
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
3412
+ * @param {Color} [additiveColor] - Additive color to be applied if any
3355
3413
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
3356
3414
  * @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
3357
3415
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
3358
3416
  * @memberof Draw */
3359
- function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3417
+ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
3360
3418
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
3361
3419
  {
3362
3420
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3431,8 +3489,8 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3431
3489
 
3432
3490
  /** Draw colored rect centered on pos
3433
3491
  * @param {Vector2} pos
3434
- * @param {Vector2} [size=(1,1)]
3435
- * @param {Color} [color=(1,1,1,1)]
3492
+ * @param {Vector2} [size=vec2(1)]
3493
+ * @param {Color} [color=WHITE]
3436
3494
  * @param {number} [angle]
3437
3495
  * @param {boolean} [useWebGL=glEnable]
3438
3496
  * @param {boolean} [screenSpace]
@@ -3445,9 +3503,9 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3445
3503
 
3446
3504
  /** Draw a rect centered on pos with a gradient from top to bottom
3447
3505
  * @param {Vector2} pos
3448
- * @param {Vector2} [size=(1,1)]
3449
- * @param {Color} [colorTop=(1,1,1,1)]
3450
- * @param {Color} [colorBottom=(0,0,0,1)]
3506
+ * @param {Vector2} [size=vec2(1)]
3507
+ * @param {Color} [colorTop=WHITE]
3508
+ * @param {Color} [colorBottom=BLACK]
3451
3509
  * @param {number} [angle]
3452
3510
  * @param {boolean} [useWebGL=glEnable]
3453
3511
  * @param {boolean} [screenSpace]
@@ -3509,9 +3567,9 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3509
3567
  /** Draw connected lines between a series of points
3510
3568
  * @param {Array<Vector2>} points
3511
3569
  * @param {number} [width]
3512
- * @param {Color} [color=(1,1,1,1)]
3570
+ * @param {Color} [color=WHITE]
3513
3571
  * @param {boolean} [wrap] - Should the last point connect to the first?
3514
- * @param {Vector2} [pos=(0,0)] - Offset to apply
3572
+ * @param {Vector2} [pos=vec2()] - Offset to apply
3515
3573
  * @param {number} [angle] - Angle to rotate by
3516
3574
  * @param {boolean} [useWebGL=glEnable]
3517
3575
  * @param {boolean} [screenSpace]
@@ -3546,13 +3604,9 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3546
3604
  for (let i=0; i<points.length; ++i)
3547
3605
  {
3548
3606
  const point = points[i];
3549
- if (i)
3550
- context.lineTo(point.x, point.y);
3551
- else
3552
- context.moveTo(point.x, point.y);
3607
+ context.lineTo(point.x, point.y);
3553
3608
  }
3554
- if (wrap)
3555
- context.closePath();
3609
+ wrap && context.closePath();
3556
3610
  context.stroke();
3557
3611
  }, screenSpace, context);
3558
3612
  }
@@ -3562,8 +3616,8 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3562
3616
  * @param {Vector2} posA
3563
3617
  * @param {Vector2} posB
3564
3618
  * @param {number} [width]
3565
- * @param {Color} [color=(1,1,1,1)]
3566
- * @param {Vector2} [pos=(0,0)] - Offset to apply
3619
+ * @param {Color} [color=WHITE]
3620
+ * @param {Vector2} [pos=vec2()] - Offset to apply
3567
3621
  * @param {number} [angle] - Angle to rotate by
3568
3622
  * @param {boolean} [useWebGL=glEnable]
3569
3623
  * @param {boolean} [screenSpace]
@@ -3582,12 +3636,12 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3582
3636
 
3583
3637
  /** Draw colored regular polygon using passed in number of sides
3584
3638
  * @param {Vector2} pos
3585
- * @param {Vector2} [size=(1,1)]
3639
+ * @param {Vector2} [size=vec2(1)]
3586
3640
  * @param {number} [sides]
3587
- * @param {Color} [color=(1,1,1,1)]
3641
+ * @param {Color} [color=WHITE]
3588
3642
  * @param {number} [angle]
3589
3643
  * @param {number} [lineWidth]
3590
- * @param {Color} [lineColor=(0,0,0,1)]
3644
+ * @param {Color} [lineColor=BLACK]
3591
3645
  * @param {boolean} [useWebGL=glEnable]
3592
3646
  * @param {boolean} [screenSpace]
3593
3647
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -3610,10 +3664,10 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3610
3664
 
3611
3665
  /** Draw colored polygon using passed in points
3612
3666
  * @param {Array<Vector2>} points - Array of Vector2 points
3613
- * @param {Color} [color=(1,1,1,1)]
3667
+ * @param {Color} [color=WHITE]
3614
3668
  * @param {number} [lineWidth]
3615
- * @param {Color} [lineColor=(0,0,0,1)]
3616
- * @param {Vector2} [pos=(0,0)] - Offset to apply
3669
+ * @param {Color} [lineColor=BLACK]
3670
+ * @param {Vector2} [pos=vec2()] - Offset to apply
3617
3671
  * @param {number} [angle] - Angle to rotate by
3618
3672
  * @param {boolean} [useWebGL=glEnable]
3619
3673
  * @param {boolean} [screenSpace]
@@ -3660,11 +3714,11 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3660
3714
 
3661
3715
  /** Draw colored ellipse using passed in point
3662
3716
  * @param {Vector2} pos
3663
- * @param {Vector2} [size=(1,1)] - Width and height diameter
3664
- * @param {Color} [color=(1,1,1,1)]
3717
+ * @param {Vector2} [size=vec2(1)] - Width and height diameter
3718
+ * @param {Color} [color=WHITE]
3665
3719
  * @param {number} [angle]
3666
3720
  * @param {number} [lineWidth]
3667
- * @param {Color} [lineColor=(0,0,0,1)]
3721
+ * @param {Color} [lineColor=BLACK]
3668
3722
  * @param {boolean} [useWebGL=glEnable]
3669
3723
  * @param {boolean} [screenSpace]
3670
3724
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -3706,9 +3760,9 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
3706
3760
  /** Draw colored circle using passed in point
3707
3761
  * @param {Vector2} pos
3708
3762
  * @param {number} [size=1] - Diameter
3709
- * @param {Color} [color=(1,1,1,1)]
3763
+ * @param {Color} [color=WHITE]
3710
3764
  * @param {number} [lineWidth=0]
3711
- * @param {Color} [lineColor=(0,0,0,1)]
3765
+ * @param {Color} [lineColor=BLACK]
3712
3766
  * @param {boolean} [useWebGL=glEnable]
3713
3767
  * @param {boolean} [screenSpace]
3714
3768
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -3742,7 +3796,11 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3742
3796
  ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
3743
3797
 
3744
3798
  if (!screenSpace)
3745
- [pos, size, angle] = worldToScreenTransform(pos, size, angle);
3799
+ {
3800
+ pos = worldToScreen(pos);
3801
+ size = size.scale(cameraScale);
3802
+ angle -= cameraAngle;
3803
+ }
3746
3804
  context.save();
3747
3805
  context.translate(pos.x+.5, pos.y+.5);
3748
3806
  context.rotate(angle);
@@ -3759,9 +3817,9 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3759
3817
  * @param {string|number} text
3760
3818
  * @param {Vector2} pos
3761
3819
  * @param {number} [size]
3762
- * @param {Color} [color=(1,1,1,1)]
3820
+ * @param {Color} [color=WHITE]
3763
3821
  * @param {number} [lineWidth]
3764
- * @param {Color} [lineColor=(0,0,0,1)]
3822
+ * @param {Color} [lineColor=BLACK]
3765
3823
  * @param {CanvasTextAlign} [textAlign='center']
3766
3824
  * @param {string} [font=fontDefault]
3767
3825
  * @param {string} [fontStyle]
@@ -3785,18 +3843,18 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3785
3843
  * Automatically splits new lines into rows
3786
3844
  * @param {string|number} text
3787
3845
  * @param {Vector2} pos
3788
- * @param {number} [size]
3789
- * @param {Color} [color=(1,1,1,1)]
3846
+ * @param {number} size
3847
+ * @param {Color} [color=WHITE]
3790
3848
  * @param {number} [lineWidth]
3791
- * @param {Color} [lineColor=(0,0,0,1)]
3849
+ * @param {Color} [lineColor=BLACK]
3792
3850
  * @param {CanvasTextAlign} [textAlign]
3793
3851
  * @param {string} [font=fontDefault]
3794
3852
  * @param {string} [fontStyle]
3795
3853
  * @param {number} [maxWidth]
3796
3854
  * @param {number} [angle]
3797
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
3855
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3798
3856
  * @memberof Draw */
3799
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=mainContext)
3857
+ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
3800
3858
  {
3801
3859
  ASSERT(isString(text), 'text must be a string');
3802
3860
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3920,7 +3978,7 @@ function worldToScreenDelta(worldDelta)
3920
3978
 
3921
3979
  /** Convert screen space transform to world space
3922
3980
  * @param {Vector2} screenPos
3923
- * @param {Vector2} screenSize
3981
+ * @param {Vector2} screenSize
3924
3982
  * @param {number} [screenAngle]
3925
3983
  * @return {[Vector2, Vector2, number]} - [pos, size, angle]
3926
3984
  * @memberof Draw */
@@ -3937,25 +3995,6 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
3937
3995
  ];
3938
3996
  }
3939
3997
 
3940
- /** Convert world space transform to screen space
3941
- * @param {Vector2} worldPos
3942
- * @param {Vector2} worldSize
3943
- * @param {number} [worldAngle]
3944
- * @return {[Vector2, Vector2, number]} - [pos, size, angle]
3945
- * @memberof Draw */
3946
- function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
3947
- {
3948
- ASSERT(isVector2(worldPos), 'worldPos must be a vec2');
3949
- ASSERT(isVector2(worldSize), 'worldSize must be a vec2');
3950
- ASSERT(isNumber(worldAngle), 'worldAngle must be a number');
3951
-
3952
- return [
3953
- worldToScreen(worldPos),
3954
- worldSize.scale(cameraScale),
3955
- worldAngle - cameraAngle
3956
- ];
3957
- }
3958
-
3959
3998
  /** Get the size of the camera window in world space
3960
3999
  * @return {Vector2}
3961
4000
  * @memberof Draw */
@@ -4000,10 +4039,9 @@ function isOnScreen(pos, size=0)
4000
4039
  * @param {boolean} [additive]
4001
4040
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4002
4041
  * @memberof Draw */
4003
- function setBlendMode(additive=false, context)
4042
+ function setBlendMode(additive=false, context=drawContext)
4004
4043
  {
4005
4044
  glAdditive = additive;
4006
- context ||= drawContext;
4007
4045
  context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
4008
4046
  }
4009
4047
 
@@ -4018,7 +4056,6 @@ function combineCanvases()
4018
4056
  workContext.fillRect(0,0,w,h); // remove background alpha
4019
4057
  glCopyToContext(workContext);
4020
4058
  workContext.drawImage(mainCanvas, 0, 0);
4021
- mainCanvas.width |= 0;
4022
4059
  mainContext.drawImage(workCanvas, 0, 0);
4023
4060
  }
4024
4061
 
@@ -4123,17 +4160,22 @@ function setCursor(cursorStyle = 'auto')
4123
4160
 
4124
4161
  ///////////////////////////////////////////////////////////////////////////////
4125
4162
 
4163
+ /** Engine font image, 8x8 font provided by the engine
4164
+ * @type {FontImage}
4165
+ * @memberof Draw */
4126
4166
  let engineFontImage;
4127
4167
 
4128
4168
  /**
4129
- * Font Image Object - Draw text on a 2D canvas by using characters in an image
4169
+ * Font Image Object - Draw text by using tiles in an image
4130
4170
  * - 96 characters (from space to tilde) are stored in an image
4131
- * - Uses a default 8x8 font if none is supplied
4132
- * - You can also use fonts from the main tile sheet
4171
+ * - A 8x8 default engine font is supplied for general use
4172
+ * - This system is WebGL enabled for fast text rendering
4173
+ * - Fonts can also be colored and scaled along each axis
4174
+ *
4133
4175
  * @memberof Draw
4134
4176
  * @example
4135
4177
  * // use built in font
4136
- * const font = new FontImage;
4178
+ * const font = engineFontImage;
4137
4179
  *
4138
4180
  * // draw text
4139
4181
  * font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
@@ -4141,70 +4183,112 @@ let engineFontImage;
4141
4183
  class FontImage
4142
4184
  {
4143
4185
  /** Create an image font
4144
- * @param {HTMLImageElement} [image] - Image for the font, default if undefined
4145
- * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
4146
- * @param {Vector2} [paddingSize=(0,1)] - How much space between characters
4186
+ * @param {TileInfo} tileInfo - Tile info of first characeter in font
4147
4187
  */
4148
- constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
4188
+ constructor(tileInfo)
4149
4189
  {
4150
- // load default font image
4151
- if (!image && !engineFontImage)
4152
- {
4153
- engineFontImage = new Image;
4154
- engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
4155
- }
4156
-
4157
- this.image = image || engineFontImage;
4158
- this.tileSize = tileSize;
4159
- this.paddingSize = paddingSize;
4190
+ ASSERT(!!tileInfo, 'tileInfo is required for FontImage');
4191
+
4192
+ /** @property {TileInfo} - Tile info for the font */
4193
+ this.tileInfo = tileInfo.frame(0);
4160
4194
  }
4161
4195
 
4162
4196
  /** Draw text in world space using the image font
4163
- * @param {string|number} text
4197
+ * @param {string|number} text
4164
4198
  * @param {Vector2} pos
4165
- * @param {number} [scale=.25]
4166
- * @param {boolean} [center]
4167
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4199
+ * @param {Vector2|number} [size]
4200
+ * @param {boolean} [center=true]
4201
+ * @param {Color} [color=WHITE]
4202
+ * @param {boolean} [useWebGL=glEnable]
4203
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4168
4204
  */
4169
- drawText(text, pos, scale=1, center, context=drawContext)
4205
+ drawText(text, pos, size=1, center, color, useWebGL, context)
4170
4206
  {
4171
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
4207
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
4208
+
4209
+ if (typeof size === 'number')
4210
+ {
4211
+ // if size is a number, make it a vector
4212
+ ASSERT(size > 0);
4213
+ size *= cameraScale;
4214
+ size = new Vector2(size, size);
4215
+ }
4216
+ else
4217
+ size = size.scale(cameraScale);
4218
+ this.drawTextScreen(text, worldToScreen(pos), size, center, color, useWebGL, context);
4172
4219
  }
4173
4220
 
4174
4221
  /** Draw text in screen space using the image font
4175
- * @param {string|number} text
4222
+ * @param {string|number} text
4176
4223
  * @param {Vector2} pos
4177
- * @param {number} [scale]
4224
+ * @param {Vector2|number} size
4178
4225
  * @param {boolean} [center]
4179
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4226
+ * @param {Color} [color=WHITE]
4227
+ * @param {boolean} [useWebGL=glEnable]
4228
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4180
4229
  */
4181
- drawTextScreen(text, pos, scale=4, center=true, context=mainContext)
4230
+ drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
4182
4231
  {
4183
- context.save();
4184
- const size = this.tileSize;
4185
- const drawSize = size.add(this.paddingSize).scale(scale);
4186
- const cols = this.image.width / this.tileSize.x |0;
4187
- (text+'').split('\n').forEach((line, i)=>
4232
+ ASSERT(isString(text), 'text must be a string');
4233
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4234
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
4235
+ ASSERT(isColor(color), 'color must be a color');
4236
+
4237
+ // if size is a number, make it a vector
4238
+ size = typeof size === 'number' ? new Vector2(size, size) : size;
4239
+
4240
+ // precache objects for drawing
4241
+ const drawPos = new Vector2;
4242
+ const tileInfo = this.tileInfo;
4243
+ const padding = tileInfo.padding;
4244
+ const sizePaddedX = tileInfo.size.x + padding*2;
4245
+ const sizePaddedY = tileInfo.size.y + padding*2;
4246
+ const cols = tileInfo.textureInfo.size.x / sizePaddedX |0;
4247
+
4248
+ // draw each line of text
4249
+ (text+'').split('\n').forEach((line, j)=>
4188
4250
  {
4189
- const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
4190
- for (let j=line.length; j--;)
4251
+ const centerOffset = center ? (line.length-1) * size.x / 2 : 0;
4252
+ for (let i=line.length; i--;)
4191
4253
  {
4192
- // draw each character
4193
- let charCode = line[j].charCodeAt(0);
4194
- if (charCode < 32 || charCode > 127)
4195
- charCode = 127; // unknown character
4196
-
4197
- // get the character source location and draw it
4198
- const tile = charCode - 32;
4199
- const x = tile % cols;
4200
- const y = tile / cols |0;
4201
- const drawPos = pos.add(vec2(j,i).multiply(drawSize));
4202
- context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
4203
- drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
4254
+ // get the character index
4255
+ const charCode = line.charCodeAt(i);
4256
+ const index = charCode < 32 || charCode > 127 ?
4257
+ 95 : charCode - 32; // handle out of range characters
4258
+
4259
+ // get the position of the tile
4260
+ const x = index % cols;
4261
+ const y = index / cols |0;
4262
+ tileInfo.pos.x = x*sizePaddedX + padding;
4263
+ tileInfo.pos.y = y*sizePaddedY + padding;
4264
+
4265
+ // draw the tile
4266
+ drawPos.x = pos.x + i * size.x - centerOffset |0;
4267
+ drawPos.y = pos.y + j * size.y |0;
4268
+ drawTile(drawPos, size, tileInfo, color, 0, false, undefined, useWebGL, true, context);
4204
4269
  }
4205
4270
  });
4206
- context.restore();
4207
4271
  }
4272
+ }
4273
+
4274
+ // load engine font, called automatically on startup
4275
+ function fontImageInit()
4276
+ {
4277
+ return new Promise(resolve =>
4278
+ {
4279
+ // create the engine font
4280
+ const image = new Image;
4281
+ image.onerror = image.onload = ()=>
4282
+ {
4283
+ const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
4284
+ const textureInfo = new TextureInfo(image);
4285
+ const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
4286
+ engineFontImage = new FontImage(tileInfo);
4287
+ resolve();
4288
+ }
4289
+ image.crossOrigin = 'anonymous';
4290
+ image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAeAQMAAABnrVXaAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAAjpJREFUOMu9kzFu2zAUhn+CAROgqrk+B2l0BWYxMjlXeYaAtFtbdA1sGgHqRQfI0CNkSG5AwYB0BQ8d5Bsomwah6CPVeGg6tEPzAxLwyI+P78cP4u9lNO9OoMKnLMOobG5020/yaj/MrRcCGh1gBbyiLTPJEYaIiom5KM9Jq7KgynMGtb6L4GL4MF2H4LQKCXTvDVw2I4MsgZT7QLExdiutH+D08VOP3INXRrWX1/mmpbkNgAPYRVANb4xpcegYvhiNbIXauQICEjBuYLfMakaakWQeXxiZ0VDtuJCKs3ztMV59QtsHJNcRxDzfdL21ty3PrfIcXTN+E+GFAv6T5nbT9jd50/WFxb5ksdAv49qS6ouymG66ji08UMT6moykYLAo+V0j23GN4m829ZySAD5K7QsBfQTvOG8eE+gTeGYRAmnNAubN3hf5Zv9tJWDHp/VTuaSm7SN4fyINQqaNO3RMVxvpSPXnOChnRNvFcGY0gnwiPswYwTKVPE0zVtX3mTEIOoFzaqLrGuJaV+Uqumb71fVk/VoOH3cdLNQP/FHi8hV0CQNoqBZsUPlLPMsdCJro9QAaQQ0woDy9BJm0eTxCFnO9srcYlhNVlfR2EyTrph1uUtbUtAJifwRgrKuYdXVHeb0YI3QpawohQHkloI3J5FuVwI5ORxC9k2Tuz9Ir1IjgeIPGMHYkAZe2RuYkmWFmt3gGbTPOmBUWVTmRmHtGrfpzG/yuQNOKa6gBB/WA9khitPgl6/GP+gl2Af6tCbvaygAAAABJRU5ErkJggg==';
4291
+ });
4208
4292
  }
4209
4293
  /**
4210
4294
  * LittleJS Input System
@@ -4261,6 +4345,10 @@ let inputPreventDefault = true;
4261
4345
  * @memberof Input */
4262
4346
  let gamepadPrimary = 0;
4263
4347
 
4348
+ /** True if a touch device has been detected
4349
+ * @memberof Input */
4350
+ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4351
+
4264
4352
  /** Prevents input continuing to the default browser handling
4265
4353
  * This is useful to disable for html menus so the browser can handle input normally
4266
4354
  * @param {boolean} preventDefault
@@ -4459,10 +4547,6 @@ function gamepadStickCount(gamepad=gamepadPrimary)
4459
4547
  return gamepadStickData[gamepad]?.length ?? 0;
4460
4548
  }
4461
4549
 
4462
- /** True if a touch device has been detected
4463
- * @memberof Input */
4464
- const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4465
-
4466
4550
  ///////////////////////////////////////////////////////////////////////////////
4467
4551
 
4468
4552
  /** Pulse the vibration hardware if it exists
@@ -4471,7 +4555,7 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4471
4555
  function vibrate(pattern=100)
4472
4556
  {
4473
4557
  ASSERT(isNumber(pattern) || isArray(pattern), 'pattern must be a number or array');
4474
- vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern);
4558
+ vibrateEnable && !headlessMode && navigator?.vibrate?.(pattern);
4475
4559
  }
4476
4560
 
4477
4561
  /** Cancel any ongoing vibration
@@ -4571,8 +4655,7 @@ function inputInit()
4571
4655
  }
4572
4656
  function onMouseDown(e)
4573
4657
  {
4574
- if (isTouchDevice && touchInputEnable)
4575
- return;
4658
+ if (isTouchDevice && touchInputEnable) return;
4576
4659
 
4577
4660
  // fix stalled audio requiring user interaction
4578
4661
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
@@ -4590,8 +4673,8 @@ function inputInit()
4590
4673
  }
4591
4674
  function onMouseUp(e)
4592
4675
  {
4593
- if (isTouchDevice && touchInputEnable)
4594
- return;
4676
+ if (isTouchDevice && touchInputEnable) return;
4677
+
4595
4678
  inputData[0][e.button] = (inputData[0][e.button]&2) | 4;
4596
4679
  }
4597
4680
  function onMouseMove(e)
@@ -4623,8 +4706,7 @@ function inputInit()
4623
4706
  let wasTouching;
4624
4707
  function handleTouch(e)
4625
4708
  {
4626
- if (!touchInputEnable)
4627
- return;
4709
+ if (!touchInputEnable) return;
4628
4710
 
4629
4711
  // route touch to gamepad
4630
4712
  if (touchGamepadEnable)
@@ -4688,8 +4770,7 @@ function inputInit()
4688
4770
  }
4689
4771
 
4690
4772
  // don't process touch gamepad if paused
4691
- if (paused)
4692
- return;
4773
+ if (paused) return;
4693
4774
 
4694
4775
  // get center of left and right sides
4695
4776
  const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
@@ -4779,8 +4860,7 @@ function inputUpdate()
4779
4860
  // update touch gamepad if enabled
4780
4861
  if (touchGamepadEnable && isTouchDevice)
4781
4862
  {
4782
- if (!touchGamepadTimer.isSet())
4783
- return;
4863
+ if (!touchGamepadTimer.isSet()) return;
4784
4864
 
4785
4865
  // read virtual analog stick
4786
4866
  gamepadPrimary = 0; // touch gamepad uses index 0
@@ -4818,12 +4898,10 @@ function inputUpdate()
4818
4898
  }
4819
4899
 
4820
4900
  // return if gamepads are disabled or not supported
4821
- if (!gamepadsEnable || !navigator || !navigator.getGamepads)
4822
- return;
4901
+ if (!gamepadsEnable || !navigator || !navigator.getGamepads) return;
4823
4902
 
4824
4903
  // only poll gamepads when focused or in debug mode
4825
- if (!debug && !document.hasFocus())
4826
- return;
4904
+ if (!debug && !document.hasFocus()) return;
4827
4905
 
4828
4906
  // poll gamepads
4829
4907
  const maxGamepads = 8;
@@ -4860,8 +4938,7 @@ function inputUpdate()
4860
4938
  data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
4861
4939
 
4862
4940
  // check for any input on this gamepad, analog must be full press
4863
- if (button.pressed)
4864
- if (!button.value || button.value > .9)
4941
+ if (button.pressed && (!button.value || button.value > .9))
4865
4942
  hadInput = true;
4866
4943
  }
4867
4944
 
@@ -4890,7 +4967,7 @@ function inputUpdate()
4890
4967
  }
4891
4968
 
4892
4969
  // copy dpad to left analog stick when pressed
4893
- if (gamepadDirectionEmulateStick && !dpad.isZero())
4970
+ if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
4894
4971
  sticks[0] = dpad.clampLength();
4895
4972
  }
4896
4973
 
@@ -4919,13 +4996,11 @@ function inputRender()
4919
4996
  function touchGamepadRender()
4920
4997
  {
4921
4998
  if (!touchInputEnable || !isTouchDevice || headlessMode) return;
4922
- if (!touchGamepadEnable || !touchGamepadTimer.isSet())
4923
- return;
4999
+ if (!touchGamepadEnable || !touchGamepadTimer.isSet()) return;
4924
5000
 
4925
5001
  // fade off when not touching or paused
4926
5002
  const alpha = percent(touchGamepadTimer.get(), 4, 3);
4927
- if (!alpha || paused)
4928
- return;
5003
+ if (!alpha || paused) return;
4929
5004
 
4930
5005
  // setup the canvas
4931
5006
  const context = mainContext;
@@ -5078,8 +5153,7 @@ class Sound
5078
5153
  {
5079
5154
  // remove randomness so it can be applied on playback
5080
5155
  const randomnessIndex = 1, defaultRandomness = .05;
5081
- this.randomness = zzfxSound[randomnessIndex] !== undefined ?
5082
- zzfxSound[randomnessIndex] : defaultRandomness;
5156
+ this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
5083
5157
  zzfxSound[randomnessIndex] = 0;
5084
5158
 
5085
5159
  // generate the zzfx samples
@@ -5159,7 +5233,7 @@ class Sound
5159
5233
  * @return {number} - How long the sound is in seconds (undefined if loading)
5160
5234
  */
5161
5235
  getDuration()
5162
- { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
5236
+ { return this.sampleChannels?.[0].length / this.sampleRate || 0; }
5163
5237
 
5164
5238
  /** Check if sound is loaded, for sounds fetched from a url
5165
5239
  * @return {boolean} - True if sound is loaded and ready to play
@@ -5251,8 +5325,7 @@ class SoundWave extends Sound
5251
5325
  this.sampleRate = audioBuffer.sampleRate;
5252
5326
  this.sampleChannels = sampleChannels;
5253
5327
  this.loadedPercent = 1;
5254
- if (this.onloadCallback)
5255
- this.onloadCallback(this);
5328
+ this.onloadCallback?.(this);
5256
5329
  }
5257
5330
  }
5258
5331
 
@@ -5375,8 +5448,7 @@ class SoundInstance
5375
5448
  /** Pause this sound instance */
5376
5449
  pause()
5377
5450
  {
5378
- if (this.isPaused())
5379
- return;
5451
+ if (this.isPaused()) return;
5380
5452
 
5381
5453
  // save current time and stop sound
5382
5454
  this.pausedTime = this.getCurrentTime();
@@ -5388,8 +5460,7 @@ class SoundInstance
5388
5460
  /** Unpauses this sound instance */
5389
5461
  resume()
5390
5462
  {
5391
- if (!this.isPaused())
5392
- return;
5463
+ if (!this.isPaused()) return;
5393
5464
 
5394
5465
  // restart sound from paused time
5395
5466
  this.start(this.pausedTime);
@@ -5457,7 +5528,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
5457
5528
 
5458
5529
  /** Stop all queued speech
5459
5530
  * @memberof Audio */
5460
- function speakStop() {speechSynthesis && speechSynthesis.cancel();}
5531
+ function speakStop() {speechSynthesis?.cancel();}
5461
5532
 
5462
5533
  /** Get frequency of a note on a musical scale
5463
5534
  * @param {number} semitoneOffset - How many semitones away from the root note
@@ -5716,7 +5787,7 @@ function tileCollisionGetData(pos)
5716
5787
 
5717
5788
  /** Check if a tile layer collides with another object
5718
5789
  * @param {Vector2} pos
5719
- * @param {Vector2} [size=(0,0)]
5790
+ * @param {Vector2} [size=vec2()]
5720
5791
  * @param {EngineObject} [object] - An object or undefined for generic test
5721
5792
  * @param {boolean} [solidOnly] - Only check solid layers if true
5722
5793
  * @return {TileCollisionLayer}
@@ -5834,20 +5905,20 @@ function tileLayersLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLa
5834
5905
  class TileLayerData
5835
5906
  {
5836
5907
  /** Create a tile layer data object, one for each tile in a TileLayer
5837
- * @param {number} [tile] - The tile to use, untextured if undefined
5908
+ * @param {number} [tile] - The tile to use, untextured if undefined
5838
5909
  * @param {number} [direction] - Integer direction of tile, in 90 degree increments
5839
- * @param {boolean} [mirror] - If the tile should be mirrored along the x axis
5840
- * @param {Color} [color] - Color of the tile */
5910
+ * @param {boolean} [mirror] - If the tile should be mirrored along the x axis
5911
+ * @param {Color} [color] - Color of the tile */
5841
5912
  constructor(tile, direction=0, mirror=false, color=new Color)
5842
5913
  {
5843
- /** @property {number} - The tile to use, untextured if undefined */
5844
- this.tile = tile;
5845
- /** @property {number} - Integer direction of tile, in 90 degree increments */
5914
+ /** @property {number} - The tile to use, untextured if undefined */
5915
+ this.tile = tile;
5916
+ /** @property {number} - Integer direction of tile, in 90 degree increments */
5846
5917
  this.direction = direction;
5847
5918
  /** @property {boolean} - If the tile should be mirrored along the x axis */
5848
- this.mirror = mirror;
5849
- /** @property {Color} - Color of the tile */
5850
- this.color = color.copy();
5919
+ this.mirror = mirror;
5920
+ /** @property {Color} - Color of the tile */
5921
+ this.color = color.copy();
5851
5922
  }
5852
5923
 
5853
5924
  /** Set this tile to clear, it will not be rendered */
@@ -5858,7 +5929,7 @@ class TileLayerData
5858
5929
  /**
5859
5930
  * Canvas Layer - cached off screen rendering system
5860
5931
  * - Contains an offscreen canvas that can be rendered to
5861
- * - WebGL rendering is optional, call useWebGL to enable
5932
+ * - WebGL rendering is optional, call updateWebGL to enable/update
5862
5933
  * @extends EngineObject
5863
5934
  * @memberof TileLayers
5864
5935
  * @example
@@ -5872,8 +5943,9 @@ class CanvasLayer extends EngineObject
5872
5943
  * @param {number} [angle] - Angle the layer is rotated by
5873
5944
  * @param {number} [renderOrder] - Objects sorted by renderOrder
5874
5945
  * @param {Vector2} [canvasSize] - Default size of canvas, can be changed later
5946
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
5875
5947
  */
5876
- constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512))
5948
+ constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=glEnable)
5877
5949
  {
5878
5950
  ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
5879
5951
  super(position, size, undefined, angle, WHITE, renderOrder);
@@ -5883,13 +5955,10 @@ class CanvasLayer extends EngineObject
5883
5955
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
5884
5956
  this.context = this.canvas?.getContext('2d');
5885
5957
  /** @property {TextureInfo} - Texture info to use for this object rendering */
5886
- const useWebGL = false; // do not use webgl by default
5887
5958
  this.textureInfo = new TextureInfo(this.canvas, useWebGL);
5888
- /** @property {boolean} - True if WebGL texture needs to be refreshed */
5889
- this.refreshWebGL = false;
5890
5959
 
5891
5960
  // disable physics by default
5892
- this.mass = this.gravityScale = this.friction = this.restitution = 0;
5961
+ this.mass = 0;
5893
5962
  }
5894
5963
 
5895
5964
  /** Destroy this canvas layer */
@@ -5904,7 +5973,7 @@ class CanvasLayer extends EngineObject
5904
5973
  // Render the layer, called automatically by the engine
5905
5974
  render()
5906
5975
  {
5907
- this.draw(this.pos, this.size, this.angle, this.color, this.mirror, this.additiveColor);
5976
+ this.draw(this.pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
5908
5977
  }
5909
5978
 
5910
5979
  /** Draw this canvas layer centered in world space, with color applied if using WebGL
@@ -5917,103 +5986,54 @@ class CanvasLayer extends EngineObject
5917
5986
  * @param {boolean} [screenSpace] - If true the pos and size are in screen space
5918
5987
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
5919
5988
  * @memberof Draw */
5920
- draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5989
+ draw(pos, size, color=WHITE, angle=0, mirror=false, additiveColor, screenSpace=false, context)
5921
5990
  {
5922
- const useWebGL = glEnable && this.textureInfo.hasWebGL();
5923
- if (useWebGL && this.refreshWebGL)
5924
- {
5925
- // update the WebGL texture
5926
- this.textureInfo.createWebGLTexture();
5927
- this.refreshWebGL = false;
5928
- }
5929
-
5930
5991
  // draw the canvas layer as a single tile that uses the whole texture
5931
5992
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5993
+ const useWebGL = this.hasWebGL();
5932
5994
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5933
5995
  }
5934
5996
 
5935
- /**
5936
- * @callback Canvas2DDrawCallback - Function that draws to a canvas 2D context
5937
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
5938
- * @memberof TileLayers
5939
- */
5940
-
5941
- /** Draw onto the layer canvas in world space (bypass WebGL)
5997
+ /** Draw a tile onto the layer canvas in world space
5942
5998
  * @param {Vector2} pos
5943
- * @param {Vector2} size
5944
- * @param {number} angle
5945
- * @param {boolean} mirror
5946
- * @param {Canvas2DDrawCallback} drawFunction */
5947
- drawCanvas2D(pos, size, angle, mirror, drawFunction)
5999
+ * @param {Vector2} [size=vec2(1)]
6000
+ * @param {TileInfo} [tileInfo]
6001
+ * @param {Color} [color=WHITE]
6002
+ * @param {number} [angle]
6003
+ * @param {boolean} [mirror] */
6004
+ drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
5948
6005
  {
5949
- if (!this.context) return;
5950
-
5951
- const context = this.context;
5952
- context.save();
5953
6006
  pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
5954
6007
  size = size.multiply(this.tileInfo.size);
5955
- context.translate(pos.x, this.canvas.height - pos.y);
5956
- context.rotate(angle);
5957
- context.scale(mirror ? -size.x : size.x, size.y);
5958
- drawFunction(context);
5959
- context.restore();
5960
- }
6008
+ pos.y = this.canvas.height - pos.y;
5961
6009
 
5962
- /** Draw a tile onto the layer canvas in world space
5963
- * @param {Vector2} pos
5964
- * @param {Vector2} [size=(1,1)]
5965
- * @param {TileInfo} [tileInfo]
5966
- * @param {Color} [color=(1,1,1,1)]
5967
- * @param {number} [angle=0]
5968
- * @param {boolean} [mirror=false] */
5969
- drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle, mirror)
5970
- {
5971
- this.drawCanvas2D(pos, size, angle, mirror, (context)=>
5972
- {
5973
- const textureInfo = tileInfo && tileInfo.textureInfo;
5974
- if (textureInfo)
5975
- {
5976
- context.globalAlpha = color.a; // only alpha is supported
5977
- context.drawImage(textureInfo.image,
5978
- tileInfo.pos.x, tileInfo.pos.y,
5979
- tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
5980
- context.globalAlpha = 1;
5981
- }
5982
- else
5983
- {
5984
- // untextured
5985
- context.fillStyle = color.toString();
5986
- context.fillRect(-.5, -.5, 1, 1);
5987
- }
5988
- });
6010
+ // draw the tile onto the layer canvas
6011
+ const oldMainCanvasSize = mainCanvasSize;
6012
+ mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
6013
+ const useWebGL = this.hasWebGL();
6014
+ useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
6015
+ const drawContext = useWebGL ? undefined : this.context;
6016
+ drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
6017
+ useWebGL && glSetRenderTarget();
6018
+ mainCanvasSize = oldMainCanvasSize;
5989
6019
  }
5990
6020
 
5991
6021
  /** Draw a rectangle onto the layer canvas in world space
5992
6022
  * @param {Vector2} pos
5993
- * @param {Vector2} [size=(1,1)]
5994
- * @param {Color} [color=(1,1,1,1)]
5995
- * @param {number} [angle=0] */
6023
+ * @param {Vector2} [size=vec2(1)]
6024
+ * @param {Color} [color=WHITE]
6025
+ * @param {number} [angle] */
5996
6026
  drawRect(pos, size, color, angle)
5997
6027
  { this.drawTile(pos, size, undefined, color, angle); }
5998
6028
 
5999
- /** Create or update the WebGL texture for this layer
6000
- * @param {boolean} [enable] - enable WebGL rendering and update the texture
6001
- * @param {boolean} [immediate] - shoulkd the texture be updated immediately
6002
- */
6003
- useWebGL(enable=true, immediate=false)
6004
- {
6005
- if (!immediate && enable && this.textureInfo.hasWebGL())
6006
- {
6007
- // refresh the texture when needed
6008
- this.refreshWebGL = true;
6009
- return;
6010
- }
6029
+ /** Create WebGL texture if necessary and copy layer canvas to it */
6030
+ updateWebGL()
6031
+ { this.textureInfo.createWebGLTexture(); }
6011
6032
 
6012
- if (enable)
6013
- this.textureInfo.createWebGLTexture();
6014
- else
6015
- this.textureInfo.destroyWebGLTexture();
6016
- }
6033
+ /** Check if this layer is using WebGL
6034
+ * @return {boolean} */
6035
+ hasWebGL()
6036
+ { return glEnable && this.textureInfo.hasWebGL(); }
6017
6037
  }
6018
6038
 
6019
6039
  ///////////////////////////////////////////////////////////////////////////////
@@ -6032,50 +6052,67 @@ class CanvasLayer extends EngineObject
6032
6052
  class TileLayer extends CanvasLayer
6033
6053
  {
6034
6054
  /** Create a tile layer object
6035
- * @param {Vector2} position - World space position
6036
- * @param {Vector2} size - World space size
6037
- * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6055
+ * @param {Vector2} position - World space position
6056
+ * @param {Vector2} size - World space size
6057
+ * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6038
6058
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6039
6059
  */
6040
6060
  constructor(position, size, tileInfo=tile(), renderOrder=0)
6041
6061
  {
6042
6062
  const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
6043
- super(position, size, 0, renderOrder, canvasSize);
6044
-
6045
- // set tile info
6046
- this.tileInfo = tileInfo;
6047
-
6048
- // init tile data
6063
+ const useWebGL = true;
6064
+ super(position, size, 0, renderOrder, canvasSize, useWebGL);
6065
+
6066
+ /** @property {TileInfo} - Default tile info for layer */
6067
+ this.tileInfo = undefined;
6068
+ /** @property {Array<TileLayerData>} - Default tile info for layer */
6049
6069
  this.data = [];
6050
- for (let j = this.size.area(); j--;)
6051
- this.data.push(new TileLayerData);
6070
+ /** @property {boolean} - Is this layer using a webgl texture? */
6071
+ this.isUsingWebGL = false;
6052
6072
 
6053
6073
  if (headlessMode)
6054
6074
  {
6055
- // disable rendering
6056
- this.redraw = () => {};
6057
- this.render = () => {};
6058
- this.redrawStart = () => {};
6059
- this.redrawEnd = () => {};
6060
- this.drawTileData = () => {};
6061
- this.drawCanvas2D = () => {};
6062
- this.useWebGL = () => {};
6075
+ // disable rendering in headless mode
6076
+ this.render = () => {};
6077
+ this.redraw = () => {};
6078
+ this.redrawStart = () => {};
6079
+ this.redrawEnd = () => {};
6080
+ this.drawTileData = () => {};
6081
+ this.redrawTileData = () => {};
6082
+ this.drawLayerTile = () => {};
6083
+ this.drawLayerRect = () => {};
6084
+ this.clearLayerRect = () => {};
6085
+ return;
6063
6086
  }
6087
+
6088
+ if (tileInfo)
6089
+ {
6090
+ // set tile info
6091
+ this.tileInfo = tileInfo.frame(0);
6092
+ this.tileInfo.bleed = 0; // disable bleed for tile layers
6093
+ }
6094
+
6095
+ // init tile data
6096
+ for (let j = this.size.area(); j--;)
6097
+ this.data.push(new TileLayerData);
6064
6098
  }
6065
6099
 
6066
6100
  /** Set data at a given position in the array
6067
6101
  * @param {Vector2} layerPos - Local position in array
6068
- * @param {TileLayerData} data - Data to set
6102
+ * @param {TileLayerData} data - Data to set
6069
6103
  * @param {boolean} [redraw] - Force the tile to redraw if true */
6070
6104
  setData(layerPos, data, redraw=false)
6071
6105
  {
6106
+ layerPos = layerPos.floor();
6072
6107
  ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6073
6108
  ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
6074
- if (layerPos.arrayCheck(this.size))
6075
- {
6076
- this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
6077
- redraw && this.drawTileData(layerPos);
6078
- }
6109
+
6110
+ if (!layerPos.arrayCheck(this.size)) return;
6111
+ this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
6112
+
6113
+ if (!redraw) return;
6114
+ const isRedraw = drawContext === this.context;
6115
+ isRedraw ? this.drawTileData(layerPos) : this.redrawTileData(layerPos);
6079
6116
  }
6080
6117
 
6081
6118
  /** Get data at a given position in the array
@@ -6084,7 +6121,18 @@ class TileLayer extends CanvasLayer
6084
6121
  getData(layerPos)
6085
6122
  {
6086
6123
  ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6087
- return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
6124
+ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
6125
+ }
6126
+
6127
+ // Update the tile layer, refresh texture if needed
6128
+ update()
6129
+ {
6130
+ if (!glEnable && this.isUsingWebGL)
6131
+ {
6132
+ // redraw the layer if webgl was disabled or context lost
6133
+ this.isUsingWebGL = false;
6134
+ this.redraw();
6135
+ }
6088
6136
  }
6089
6137
 
6090
6138
  // Render the tile layer, called automatically by the engine
@@ -6092,78 +6140,76 @@ class TileLayer extends CanvasLayer
6092
6140
  {
6093
6141
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
6094
6142
 
6095
- if (this.refreshWebGL)
6096
- {
6097
- // update the WebGL texture
6098
- this.textureInfo.createWebGLTexture();
6099
- this.refreshWebGL = false;
6100
- }
6101
-
6102
- // draw the tile layer as a single tile
6103
- const tileInfo = new TileInfo().setFullImage(this.textureInfo);
6104
6143
  const size = this.drawSize || this.size;
6105
6144
  const pos = this.pos.add(size.scale(.5));
6106
- const useWebGL = glEnable && this.textureInfo.hasWebGL();
6107
- drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
6145
+ this.draw(pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
6108
6146
  }
6109
6147
 
6148
+ /** Called after this layer is redrawn, does nothing by default */
6149
+ onRedraw() {}
6150
+
6110
6151
  /** Draw all the tile data to an offscreen canvas
6111
- * - This may be slow in some browsers but only needs to be done once */
6152
+ * - This may be slow if not using webgl but only needs to be done once */
6112
6153
  redraw()
6113
6154
  {
6114
6155
  this.redrawStart(true);
6115
6156
  for (let x = this.size.x; x--;)
6116
6157
  for (let y = this.size.y; y--;)
6117
6158
  this.drawTileData(vec2(x,y), false);
6159
+ this.hasWebGL() && glFlush();
6160
+ this.onRedraw();
6118
6161
  this.redrawEnd();
6119
- this.useWebGL();
6120
6162
  }
6121
6163
 
6122
6164
  /** Call to start the redraw process
6123
- * - This can be used to manually update small parts of the level
6165
+ * - This can be used to manually update parts of the level
6124
6166
  * @param {boolean} [clear] - Should it clear the canvas before drawing */
6125
6167
  redrawStart(clear=false)
6126
6168
  {
6127
6169
  if (!this.context) return;
6128
-
6170
+ ASSERT(drawContext !== this.context);
6171
+
6129
6172
  // save current render settings
6130
- /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6131
- this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
6173
+ /** @type {[CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6174
+ this.savedRenderSettings = [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
6132
6175
 
6133
6176
  // set the draw canvas and context to this layer
6134
6177
  // use camera settings to match this layer's canvas
6135
- drawCanvas = this.canvas;
6136
6178
  drawContext = this.context;
6137
- cameraPos = this.size.scale(.5);
6138
- const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
6139
- cameraScale = tileSize.x;
6140
- canvasClearColor = CLEAR_BLACK;
6179
+ const tileSize = this.tileInfo?.size ?? vec2(1);
6141
6180
  mainCanvasSize = this.size.multiply(tileSize);
6142
- if (clear)
6181
+ canvasClearColor = CLEAR_BLACK;
6182
+ cameraPos = this.size.multiply(tileSize).scale(.5);
6183
+ cameraScale = 1;
6184
+
6185
+ // set render target to this layer
6186
+ this.isUsingWebGL = this.hasWebGL();
6187
+ if (this.isUsingWebGL)
6188
+ glSetRenderTarget(this.textureInfo.glTexture, clear);
6189
+ else
6143
6190
  {
6144
- // clear and set size
6145
- drawCanvas.width = mainCanvasSize.x;
6146
- drawCanvas.height = mainCanvasSize.y;
6191
+ // disable smoothing for pixel art
6192
+ this.context.imageSmoothingEnabled = !tilesPixelated;
6193
+ if (clear)
6194
+ {
6195
+ // clear and set size
6196
+ this.canvas.width = mainCanvasSize.x;
6197
+ this.canvas.height = mainCanvasSize.y;
6198
+ }
6147
6199
  }
6148
-
6149
- // disable smoothing for pixel art
6150
- drawContext.imageSmoothingEnabled = !tilesPixelated;
6151
-
6152
- // setup gl rendering if enabled
6153
- glPreRender();
6154
6200
  }
6155
6201
 
6156
6202
  /** Call to end the redraw process */
6157
6203
  redrawEnd()
6158
6204
  {
6159
6205
  if (!this.context) return;
6206
+ ASSERT(drawContext === this.context);
6160
6207
 
6161
- ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6162
- glCopyToContext(drawContext);
6163
- //debugSaveCanvas(this.canvas);
6208
+ if (glEnable && this.textureInfo.glTexture)
6209
+ glSetRenderTarget();
6164
6210
 
6165
6211
  // set stuff back to normal
6166
- [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6212
+ [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6167
6213
  }
6168
6214
 
6169
6215
  /** Draw the tile at a given position in the tile grid
@@ -6175,24 +6221,75 @@ class TileLayer extends CanvasLayer
6175
6221
  drawTileData(layerPos, clear=true)
6176
6222
  {
6177
6223
  if (!this.context) return;
6224
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6178
6225
 
6179
- // clear out where the tile was, for full opaque tiles this can be skipped
6180
- const s = this.tileInfo.size;
6181
- if (clear)
6182
- {
6183
- const pos = layerPos.multiply(s);
6184
- this.context.clearRect(pos.x, this.canvas.height-pos.y, s.x, -s.y);
6185
- }
6226
+ // clear out where the tile was, can be skipped for fully opaque tiles
6227
+ const drawSize = this.tileInfo?.size ?? vec2(1);
6228
+ const drawPos = layerPos.multiply(drawSize);
6229
+ clear && this.clearLayerRect(drawPos, drawSize);
6186
6230
 
6187
6231
  // draw the tile if it has layer data
6188
6232
  const d = this.getData(layerPos);
6189
- if (d.tile !== undefined)
6190
- {
6191
- ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6192
- const pos = layerPos.add(vec2(.5));
6193
- const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
6194
- drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
6195
- }
6233
+ if (!d.tile) return;
6234
+
6235
+ const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
6236
+ this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
6237
+ }
6238
+
6239
+ /** Draw the tile at a given position in the tile grid
6240
+ * This can be used to clear tiles when they are destroyed
6241
+ * For better performance use drawTileData inside a redrawStart/End block
6242
+ * @param {Vector2} layerPos
6243
+ * @param {boolean} [clear] - should the old tile be cleared
6244
+ */
6245
+ redrawTileData(layerPos, clear=true)
6246
+ {
6247
+ if (!this.context) return;
6248
+ ASSERT(drawContext !== this.context, 'redrawStart() should not be active when calling redrawTileData(), instead use drawTileData()');
6249
+
6250
+ this.redrawStart();
6251
+ this.drawTileData(layerPos, clear);
6252
+ this.redrawEnd();
6253
+ }
6254
+
6255
+ /** Draw textured tile in layer space
6256
+ * @param {Vector2} pos - Position in pixel coordinates
6257
+ * @param {Vector2} [size=vec2(1)] - Size of the tile
6258
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
6259
+ * @param {Color} [color=WHITE] - Color to modulate with
6260
+ * @param {number} [angle] - Angle to rotate by
6261
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
6262
+ * @param {Color} [additiveColor] - Additive color to be applied if any */
6263
+ drawLayerTile(pos, size=vec2(1), tileInfo, color=WHITE,
6264
+ angle=0, mirror, additiveColor)
6265
+ {
6266
+ const drawPos = pos.add(size.scale(.5));
6267
+ drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor);
6268
+ }
6269
+
6270
+ /** Clear a rectangle in layer space
6271
+ * @param {Vector2} pos
6272
+ * @param {Vector2} size
6273
+ * @param {Color} [color=WHITE] - Color to modulate with
6274
+ * @param {number} [angle] - Angle to rotate by
6275
+ */
6276
+ drawLayerRect(pos, size, color, angle=0)
6277
+ { this.drawLayerTile(pos, size, undefined, color, angle); }
6278
+
6279
+ /** Clear a rectangle in layer space
6280
+ * @param {Vector2} pos - position in pixel coordinates
6281
+ * @param {Vector2} size
6282
+ */
6283
+ clearLayerRect(pos, size)
6284
+ {
6285
+ ASSERT(drawContext === this.context, 'must call redrawStart() before clearing tiles');
6286
+
6287
+ const x = pos.x, y = this.canvas.height - pos.y - size.y;
6288
+ const useWebGL = this.hasWebGL();
6289
+ if (useWebGL)
6290
+ glClearRect(x, y, size.x, size.y);
6291
+ else
6292
+ this.context.clearRect(x, y, size.x, size.y);
6196
6293
  }
6197
6294
  }
6198
6295
 
@@ -6201,7 +6298,6 @@ class TileLayer extends CanvasLayer
6201
6298
  * Tile Collision Layer - a tile layer with collision
6202
6299
  * - adds collision data and functions to TileLayer
6203
6300
  * - there can be multiple tile collision layers
6204
- * - tile collision layers should not overlap each other
6205
6301
  * @extends TileLayer
6206
6302
  * @memberof TileLayers
6207
6303
  */
@@ -6231,8 +6327,7 @@ class TileCollisionLayer extends TileLayer
6231
6327
  /** Destroy this tile layer */
6232
6328
  destroy()
6233
6329
  {
6234
- if (this.destroyed)
6235
- return;
6330
+ if (this.destroyed) return;
6236
6331
 
6237
6332
  // remove from collision layers array and destroy
6238
6333
  const index = tileCollisionLayers.indexOf(this);
@@ -6274,7 +6369,7 @@ class TileCollisionLayer extends TileLayer
6274
6369
 
6275
6370
  /** Check if collision with another object should occur
6276
6371
  * @param {Vector2} pos
6277
- * @param {Vector2} [size=(0,0)]
6372
+ * @param {Vector2} [size=vec2()]
6278
6373
  * @param {EngineObject} [object]
6279
6374
  * @return {boolean} */
6280
6375
  collisionTest(pos, size=new Vector2, object)
@@ -6594,7 +6689,7 @@ class ParticleEmitter extends EngineObject
6594
6689
  particle.mirror = randBool();
6595
6690
 
6596
6691
  // call particle create callback
6597
- this.particleCreateCallback && this.particleCreateCallback(particle);
6692
+ this.particleCreateCallback?.(particle);
6598
6693
 
6599
6694
  // return the newly created particle
6600
6695
  return particle;
@@ -6679,7 +6774,7 @@ class Particle extends EngineObject
6679
6774
  const c = this.colorEnd;
6680
6775
  this.color.set(c.r, c.g, c.b, c.a);
6681
6776
  this.size.set(this.sizeEnd, this.sizeEnd);
6682
- this.destroyCallback && this.destroyCallback(this);
6777
+ this.destroyCallback?.(this);
6683
6778
  this.destroyed = 1;
6684
6779
  }
6685
6780
  }
@@ -6769,10 +6864,11 @@ function medalsInit(saveName)
6769
6864
 
6770
6865
  // engine automatically renders medals
6771
6866
  engineAddPlugin(undefined, medalsRender);
6867
+
6868
+ // plugin functions
6772
6869
  function medalsRender()
6773
6870
  {
6774
- if (!medalsDisplayQueue.length)
6775
- return;
6871
+ if (!medalsDisplayQueue.length) return;
6776
6872
 
6777
6873
  // update first medal in queue
6778
6874
  const medal = medalsDisplayQueue[0];
@@ -6862,8 +6958,7 @@ class Medal
6862
6958
  /** Unlocks a medal if not already unlocked */
6863
6959
  unlock()
6864
6960
  {
6865
- if (medalsPreventUnlock || this.unlocked)
6866
- return;
6961
+ if (medalsPreventUnlock || this.unlocked) return;
6867
6962
 
6868
6963
  // save the medal
6869
6964
  ASSERT(medalsSaveName, 'save name must be set');
@@ -6957,7 +7052,7 @@ let glContext;
6957
7052
  let glAntialias = true;
6958
7053
 
6959
7054
  // WebGL internal variables not exposed to documentation
6960
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glCanBeEnabled = true;
7055
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glCanBeEnabled = true;
6961
7056
 
6962
7057
  // WebGL internal constants
6963
7058
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -7033,7 +7128,7 @@ function glInit(rootElement)
7033
7128
  // setup instanced rendering shader program
7034
7129
  glShader = glCreateProgram(
7035
7130
  '#version 300 es\n' + // specify GLSL ES version
7036
- 'precision highp float;'+ // use highp for better accuracy
7131
+ 'precision highp float;'+ // use highp for accuracy
7037
7132
  'uniform mat4 m;'+ // transform matrix
7038
7133
  'in vec2 g;'+ // in: geometry
7039
7134
  'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
@@ -7048,7 +7143,7 @@ function glInit(rootElement)
7048
7143
  '}' // end of shader
7049
7144
  ,
7050
7145
  '#version 300 es\n' + // specify GLSL ES version
7051
- 'precision highp float;'+ // use highp for better accuracy
7146
+ 'precision highp float;'+ // use highp for accuracy
7052
7147
  'uniform sampler2D s;'+ // texture
7053
7148
  'in vec2 v;'+ // in: uv
7054
7149
  'in vec4 d,e;'+ // in: color, additiveColor
@@ -7086,87 +7181,96 @@ function glInit(rootElement)
7086
7181
  glColorData = new Uint32Array(glInstanceData);
7087
7182
  glArrayBuffer = glContext.createBuffer();
7088
7183
  glGeometryBuffer = glContext.createBuffer();
7184
+ glFramebuffer = glContext.createFramebuffer();
7185
+ glBatchCount = 0;
7089
7186
 
7090
7187
  // create the geometry buffer, triangle strip square
7091
- const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
7188
+ const geometry = new Float32Array([0,0,1,0,0,1,1,1]);
7092
7189
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7093
7190
  glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
7191
+
7192
+ let offset, shader, stride;
7193
+ const initVertexAttrib = (name, type, typeSize, size, divisor=0)=>
7194
+ {
7195
+ const location = glContext.getAttribLocation(shader, name);
7196
+ const normalize = typeSize === 1;
7197
+ const fixedStride = typeSize && stride;
7198
+ glContext.enableVertexAttribArray(location);
7199
+ glContext.vertexAttribPointer(location, size, type, normalize, fixedStride, offset);
7200
+ glContext.vertexAttribDivisor(location, divisor);
7201
+ offset += size*typeSize;
7202
+ }
7203
+
7204
+ // setup VAO for instanced rendering
7205
+ glInstancedVAO = glContext.createVertexArray();
7206
+ glContext.bindVertexArray(glInstancedVAO);
7207
+
7208
+ // configure instanced vertex attributes
7209
+ offset = 0, shader = glShader, stride = gl_INSTANCE_BYTE_STRIDE;
7210
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7211
+ initVertexAttrib('g', glContext.FLOAT, 0, 2); // geometry
7212
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7213
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7214
+ initVertexAttrib('p', glContext.FLOAT, 4, 4, 1); // position & size
7215
+ initVertexAttrib('u', glContext.FLOAT, 4, 4, 1); // texture coords
7216
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4, 1); // color
7217
+ initVertexAttrib('a', glContext.UNSIGNED_BYTE, 1, 4, 1); // additiveColor
7218
+ initVertexAttrib('r', glContext.FLOAT, 4, 1, 1); // rotation
7219
+
7220
+ // setup VAO for poly rendering
7221
+ glPolyVAO = glContext.createVertexArray();
7222
+ glContext.bindVertexArray(glPolyVAO);
7223
+
7224
+ // configure poly vertex attributes
7225
+ offset = 0, shader = glPolyShader, stride = gl_POLY_VERTEX_BYTE_STRIDE;
7226
+ initVertexAttrib('p', glContext.FLOAT, 4, 2); // position
7227
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7094
7228
  }
7095
7229
  }
7096
7230
 
7097
- function glSetInstancedMode()
7231
+ function glSetInstancedMode(force=false)
7098
7232
  {
7099
- if (!glPolyMode)
7100
- return;
7233
+ if (!force && !glPolyMode) return;
7101
7234
 
7102
7235
  // setup instanced mode
7103
7236
  glFlush();
7104
7237
  glPolyMode = false;
7105
7238
  glContext.useProgram(glShader);
7106
-
7107
- // set vertex attributes
7108
- let offset = 0;
7109
- const initVertexAttribArray = (name, type, typeSize, size)=>
7110
- {
7111
- const location = glContext.getAttribLocation(glShader, name);
7112
- const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
7113
- const divisor = typeSize && 1; // only if not geometry
7114
- const normalize = typeSize === 1; // only if color
7115
- glContext.enableVertexAttribArray(location);
7116
- glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
7117
- glContext.vertexAttribDivisor(location, divisor);
7118
- offset += size*typeSize;
7119
- }
7120
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7121
- initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
7122
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7123
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7124
- initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
7125
- initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
7126
- initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7127
- initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
7128
- initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
7239
+ glContext.bindVertexArray(glInstancedVAO);
7129
7240
  }
7130
7241
 
7131
7242
  function glSetPolyMode()
7132
7243
  {
7133
- if (glPolyMode)
7134
- return;
7244
+ if (glPolyMode) return;
7135
7245
 
7136
7246
  // setup poly mode
7137
7247
  glFlush();
7138
7248
  glPolyMode = true;
7139
7249
  glContext.useProgram(glPolyShader);
7140
-
7141
- // set vertex attributes
7142
- let offset = 0;
7143
- const initVertexAttribArray = (name, type, typeSize, size)=>
7144
- {
7145
- const location = glContext.getAttribLocation(glPolyShader, name);
7146
- const normalize = typeSize === 1; // only normalize if color
7147
- const stride = gl_POLY_VERTEX_BYTE_STRIDE;
7148
- glContext.enableVertexAttribArray(location);
7149
- glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
7150
- glContext.vertexAttribDivisor(location, 0);
7151
- offset += size*typeSize;
7152
- }
7153
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7154
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7155
- initVertexAttribArray('p', glContext.FLOAT, 4, 2); // position
7156
- initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7250
+ glContext.bindVertexArray(glPolyVAO);
7157
7251
  }
7158
7252
 
7159
7253
  // Setup WebGL render each frame, called automatically by engine
7160
7254
  // Also used by tile layer rendering when redrawing tiles
7161
- function glPreRender()
7255
+ function glPreRender(clear=true)
7162
7256
  {
7163
7257
  if (!glEnable || !glContext) return;
7164
7258
 
7165
- // clear the canvas
7166
- glClearCanvas();
7259
+ ASSERT(!glBatchCount, 'glPreRender called with unflushed batch.');
7260
+
7261
+ if (!glRenderTarget)
7262
+ {
7263
+ // set to same size as main canvas
7264
+ glCanvas.width = mainCanvasSize.x;
7265
+ glCanvas.height = mainCanvasSize.y;
7266
+ }
7267
+ glContext.viewport(0, 0, mainCanvasSize.x, mainCanvasSize.y);
7268
+ clear && glClearCanvas();
7167
7269
 
7168
7270
  // build the transform matrix
7169
7271
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
7272
+ if (glRenderTarget)
7273
+ s.y = -s.y; // invert y when using render target
7170
7274
  const rotatedCam = cameraPos.rotate(-cameraAngle);
7171
7275
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
7172
7276
  const ca = cos(cameraAngle);
@@ -7195,12 +7299,14 @@ function glPreRender()
7195
7299
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
7196
7300
  }
7197
7301
 
7302
+ // rebind the array buffer
7303
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7304
+
7198
7305
  // start with additive blending off
7199
7306
  glAdditive = glBatchAdditive = false;
7200
7307
 
7201
- // force it to set instanced mode by first setting poly mode true
7202
- glPolyMode = true;
7203
- glSetInstancedMode();
7308
+ // force it to set instanced mode
7309
+ glSetInstancedMode(true);
7204
7310
  }
7205
7311
 
7206
7312
  /** Clear the canvas and setup the viewport
@@ -7209,13 +7315,9 @@ function glClearCanvas()
7209
7315
  {
7210
7316
  if (!glContext) return;
7211
7317
 
7212
- // clear and set to same size as main canvas
7213
- glCanvas.width = drawCanvas.width;
7214
- glCanvas.height = drawCanvas.height;
7215
- glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
7318
+ // clear using the canvasClearColor
7216
7319
  const color = canvasClearColor;
7217
- if (color.a > 0)
7218
- glContext.clearColor(color.r, color.g, color.b, color.a);
7320
+ glContext.clearColor(color.r, color.g, color.b, color.a);
7219
7321
  glContext.clear(glContext.COLOR_BUFFER_BIT);
7220
7322
  }
7221
7323
 
@@ -7227,8 +7329,7 @@ function glClearCanvas()
7227
7329
  function glSetTexture(texture, wrap=false)
7228
7330
  {
7229
7331
  // must flush cache with the old texture to set a new one
7230
- if (!glContext || texture === glActiveTexture)
7231
- return;
7332
+ if (!glContext || texture === glActiveTexture) return;
7232
7333
 
7233
7334
  glFlush();
7234
7335
  glActiveTexture = texture;
@@ -7293,7 +7394,7 @@ function glCreateTexture(image)
7293
7394
  // build the texture
7294
7395
  const texture = glContext.createTexture();
7295
7396
  let mipMap = false;
7296
- if (image && image.width)
7397
+ if (image?.width)
7297
7398
  {
7298
7399
  glSetTextureData(texture, image);
7299
7400
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
@@ -7314,7 +7415,9 @@ function glCreateTexture(image)
7314
7415
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
7315
7416
  if (mipMap)
7316
7417
  glContext.generateMipmap(glContext.TEXTURE_2D);
7317
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
7418
+
7419
+ // rebind active texture
7420
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
7318
7421
  return texture;
7319
7422
  }
7320
7423
 
@@ -7324,6 +7427,7 @@ function glCreateTexture(image)
7324
7427
  function glDeleteTexture(texture)
7325
7428
  {
7326
7429
  if (!glContext) return;
7430
+
7327
7431
  glContext.deleteTexture(texture);
7328
7432
  }
7329
7433
 
@@ -7336,10 +7440,12 @@ function glSetTextureData(texture, image)
7336
7440
  if (!glContext) return;
7337
7441
 
7338
7442
  // build the texture
7339
- ASSERT(!!image && image.width > 0, 'Invalid image data.');
7443
+ ASSERT(image?.width > 0, 'Invalid image data.');
7340
7444
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
7341
7445
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
7342
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
7446
+
7447
+ // rebind active texture
7448
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
7343
7449
  }
7344
7450
 
7345
7451
  /** Tells WebGL to create or update the glTexture and start tracking it
@@ -7408,8 +7514,7 @@ function glFlush()
7408
7514
  * @memberof WebGL */
7409
7515
  function glCopyToContext(context)
7410
7516
  {
7411
- if (!glEnable || !glContext)
7412
- return;
7517
+ if (!glEnable || !glContext) return;
7413
7518
 
7414
7519
  glFlush();
7415
7520
  context.drawImage(glCanvas, 0, 0);
@@ -7559,6 +7664,48 @@ function glDrawColoredPoints(points, pointColors)
7559
7664
  glBatchCount += vertCount;
7560
7665
  }
7561
7666
 
7667
+ /** Set the WebGL render target to the given texture or back to the canvas
7668
+ * @param {WebGLTexture} [texture] - a texture or undefined to use normal glCanvas
7669
+ * @param {boolean} [clear] - should the render target be cleared
7670
+ * @memberof WebGL */
7671
+ function glSetRenderTarget(texture, clear=false)
7672
+ {
7673
+ if (texture)
7674
+ {
7675
+ glRenderTarget = texture;
7676
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
7677
+ glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
7678
+ glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
7679
+ glPreRender(clear);
7680
+ }
7681
+ else
7682
+ {
7683
+ glFlush();
7684
+ glRenderTarget = undefined;
7685
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
7686
+ }
7687
+ }
7688
+
7689
+ /** Clear out a rectangle area of the WebGL canvas or render target
7690
+ * @param {number} x
7691
+ * @param {number} y
7692
+ * @param {number} width
7693
+ * @param {number} height
7694
+ * @memberof WebGL */
7695
+ function glClearRect(x, y, width, height)
7696
+ {
7697
+ if (!glEnable) return;
7698
+
7699
+ // Enable scissor test to clear only the specified area
7700
+ glContext.enable(glContext.SCISSOR_TEST);
7701
+ glContext.scissor(x, y, width, height);
7702
+ glContext.clearColor(0, 0, 0, 0);
7703
+ glContext.clear(glContext.COLOR_BUFFER_BIT);
7704
+ glContext.disable(glContext.SCISSOR_TEST);
7705
+ }
7706
+
7707
+ ///////////////////////////////////////////////////////////////////////////////
7708
+
7562
7709
  // WebGL internal function to convert polygon to outline triangle strip
7563
7710
  function glMakeOutline(points, width, wrap=true)
7564
7711
  {
@@ -7692,23 +7839,20 @@ function glPolyStrip(points)
7692
7839
  const a = points[i0], b = points[i1], c = points[i2];
7693
7840
 
7694
7841
  // check if convex
7695
- if (cross(a, b, c) < e)
7696
- continue;
7842
+ if (cross(a, b, c) < e) continue;
7697
7843
 
7698
7844
  // check if any other point is inside
7699
7845
  let hasInside = false;
7700
7846
  for (let j = 0; j < indices.length; j++)
7701
7847
  {
7702
7848
  const k = indices[j];
7703
- if (k === i0 || k === i1 || k === i2)
7704
- continue;
7849
+ if (k === i0 || k === i1 || k === i2) continue;
7850
+
7705
7851
  const p = points[k];
7706
7852
  hasInside = pointInTriangle(p, a, b, c);
7707
- if (hasInside)
7708
- break;
7853
+ if (hasInside) break;
7709
7854
  }
7710
- if (hasInside)
7711
- continue;
7855
+ if (hasInside) continue;
7712
7856
 
7713
7857
  // found valid ear
7714
7858
  triangles.push([i0, i1, i2]);
@@ -7733,8 +7877,7 @@ function glPolyStrip(points)
7733
7877
  worstIndex = i;
7734
7878
  }
7735
7879
  }
7736
- if (worstIndex < 0)
7737
- break;
7880
+ if (worstIndex < 0) break;
7738
7881
 
7739
7882
  const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
7740
7883
  const i1 = indices[worstIndex];
@@ -7965,14 +8108,16 @@ class PostProcessPlugin
7965
8108
  {
7966
8109
  /** Create global post processing shader
7967
8110
  * @param {string} shaderCode
7968
- * @param {boolean} [includeMainCanvas]
7969
- * @example
7970
- * // create the post process plugin object
7971
- * new PostProcessPlugin(shaderCode);
7972
- */
7973
- constructor(shaderCode, includeMainCanvas=true)
8111
+ * @param {boolean} [includeMainCanvas] - combine mainCanvs onto glCanvas
8112
+ * @param {boolean} [feedbackTexture] - use glCanvas from previous frame as the texture
8113
+ * @example
8114
+ * // create the post process plugin object
8115
+ * new PostProcessPlugin(shaderCode);
8116
+ */
8117
+ constructor(shaderCode, includeMainCanvas=false, feedbackTexture=false)
7974
8118
  {
7975
8119
  ASSERT(!postProcess, 'Post process already initialized');
8120
+ ASSERT(!(includeMainCanvas && feedbackTexture), 'Post process cannot both include main canvas and use feedback texture');
7976
8121
  postProcess = this;
7977
8122
 
7978
8123
  if (!shaderCode) // default shader pass through
@@ -7980,9 +8125,10 @@ class PostProcessPlugin
7980
8125
 
7981
8126
  /** @property {WebGLProgram} - Shader for post processing */
7982
8127
  this.shader = undefined;
7983
-
7984
8128
  /** @property {WebGLTexture} - Texture for post processing */
7985
8129
  this.texture = undefined;
8130
+ /** @property {WebGLVertexArrayObject} - Vertex array object */
8131
+ this.vao = undefined;
7986
8132
 
7987
8133
  // setup the post processing plugin
7988
8134
  initPostProcess();
@@ -7991,7 +8137,6 @@ class PostProcessPlugin
7991
8137
  function initPostProcess()
7992
8138
  {
7993
8139
  if (headlessMode) return;
7994
-
7995
8140
  if (!glEnable)
7996
8141
  {
7997
8142
  console.warn('PostProcessPlugin: WebGL not enabled!');
@@ -8002,14 +8147,14 @@ class PostProcessPlugin
8002
8147
  postProcess.texture = glCreateTexture();
8003
8148
  postProcess.shader = glCreateProgram(
8004
8149
  '#version 300 es\n' + // specify GLSL ES version
8005
- 'precision highp float;'+ // use highp for better accuracy
8150
+ 'precision highp float;'+ // use highp for accuracy
8006
8151
  'in vec2 p;'+ // position
8007
8152
  'void main(){'+ // shader entry point
8008
8153
  'gl_Position=vec4(p+p-1.,1,1);'+ // set position
8009
8154
  '}' // end of shader
8010
8155
  ,
8011
8156
  '#version 300 es\n' + // specify GLSL ES version
8012
- 'precision highp float;'+ // use highp for better accuracy
8157
+ 'precision highp float;'+ // use highp for accuracy
8013
8158
  'uniform sampler2D iChannel0;'+ // input texture
8014
8159
  'uniform vec3 iResolution;'+ // size of output texture
8015
8160
  'uniform float iTime;'+ // time
@@ -8020,6 +8165,17 @@ class PostProcessPlugin
8020
8165
  'c.a=1.;'+ // always use full alpha
8021
8166
  '}' // end of shader
8022
8167
  );
8168
+
8169
+ // setup VAO for post processing
8170
+ postProcess.vao = glContext.createVertexArray();
8171
+ glContext.bindVertexArray(postProcess.vao);
8172
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
8173
+
8174
+ // configure vertex attributes
8175
+ const vertexByteStride = 8;
8176
+ const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
8177
+ glContext.enableVertexAttribArray(pLocation);
8178
+ glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
8023
8179
  }
8024
8180
  function postProcessContextLost()
8025
8181
  {
@@ -8034,14 +8190,17 @@ class PostProcessPlugin
8034
8190
  }
8035
8191
  function postProcessRender()
8036
8192
  {
8037
- if (headlessMode) return;
8038
-
8039
- if (!glEnable)
8040
- return;
8193
+ if (headlessMode || !glEnable) return;
8041
8194
 
8042
8195
  // clear out the buffer
8043
8196
  glFlush();
8044
-
8197
+
8198
+ // setup shader program to draw a quad
8199
+ glContext.useProgram(postProcess.shader);
8200
+ glContext.bindVertexArray(postProcess.vao);
8201
+ glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, true);
8202
+ glContext.disable(glContext.BLEND);
8203
+
8045
8204
  // setup texture
8046
8205
  glContext.activeTexture(glContext.TEXTURE0);
8047
8206
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
@@ -8052,29 +8211,32 @@ class PostProcessPlugin
8052
8211
  workCanvas.height = mainCanvasSize.y;
8053
8212
  glCopyToContext(workContext);
8054
8213
  workContext.drawImage(mainCanvas, 0, 0);
8214
+ mainCanvas.width |= 0
8055
8215
 
8056
8216
  // copy work canvas to texture
8057
8217
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
8058
8218
  }
8059
-
8060
- // setup shader program to draw a quad
8061
- glContext.useProgram(postProcess.shader);
8062
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
8063
- glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
8064
- glContext.disable(glContext.BLEND);
8065
-
8066
- // set vertex position attribute
8067
- const vertexByteStride = 8;
8068
- const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
8069
- glContext.enableVertexAttribArray(pLocation);
8070
- glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
8071
-
8219
+ else if (!feedbackTexture)
8220
+ {
8221
+ // copy glCanvas to texture
8222
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
8223
+ }
8224
+
8072
8225
  // set uniforms and draw
8073
8226
  const uniformLocation = (name)=>glContext.getUniformLocation(postProcess.shader, name);
8074
8227
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
8075
8228
  glContext.uniform1f(uniformLocation('iTime'), time);
8076
8229
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
8077
8230
  glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
8231
+
8232
+ if (feedbackTexture)
8233
+ {
8234
+ // pass glCanvas back to overlay texture
8235
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
8236
+ }
8237
+
8238
+ // force it to set instanced mode
8239
+ glSetInstancedMode(true);
8078
8240
  }
8079
8241
  }
8080
8242
  }
@@ -8755,7 +8917,7 @@ class UISystemPlugin
8755
8917
  const up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight';
8756
8918
  if (both)
8757
8919
  {
8758
- return keyIsDown(up) || keyIsDown(left) ? -1 :
8920
+ return keyIsDown(up) || keyIsDown(left) ? -1 :
8759
8921
  keyIsDown(down) || keyIsDown(right) ? 1 : 0;
8760
8922
  }
8761
8923
  const back = vertical ? up : left;
@@ -8786,7 +8948,7 @@ class UISystemPlugin
8786
8948
  * @return {boolean} */
8787
8949
  getNavigationWasPressed()
8788
8950
  {
8789
- return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
8951
+ return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
8790
8952
  keyWasPressed('Space') || keyWasPressed('Enter');
8791
8953
  }
8792
8954
 
@@ -8834,7 +8996,7 @@ class UISystemPlugin
8834
8996
  buttonYes.textHeight = 40;
8835
8997
  buttonYes.navigationIndex = 1;
8836
8998
  buttonYes.hoverColor = hsl(0,1,.5);
8837
- buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
8999
+ buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
8838
9000
  confirmMenu.addChild(buttonYes);
8839
9001
 
8840
9002
  // no button
@@ -8864,8 +9026,8 @@ class UISystemPlugin
8864
9026
  class UIObject
8865
9027
  {
8866
9028
  /** Create a UIObject
8867
- * @param {Vector2} [pos=(0,0)]
8868
- * @param {Vector2} [size=(1,1)]
9029
+ * @param {Vector2} [pos=vec2()]
9030
+ * @param {Vector2} [size=vec2(1)]
8869
9031
  */
8870
9032
  constructor(pos=vec2(), size=vec2())
8871
9033
  {
@@ -8880,7 +9042,7 @@ class UIObject
8880
9042
  this.size = size.copy();
8881
9043
  /** @property {Color} - Color of the object */
8882
9044
  this.color = uiSystem.defaultColor.copy();
8883
- /** @property {Color} - Color of the object when active, uses color if undefined */
9045
+ /** @property {Color} - Color of the object when active, uses hoverColor if undefined */
8884
9046
  this.activeColor = undefined;
8885
9047
  /** @property {string} - Text for this ui object */
8886
9048
  this.text = undefined;
@@ -8978,10 +9140,10 @@ class UIObject
8978
9140
 
8979
9141
  // disconnect from parent and destroy children
8980
9142
  this.destroyed = 1;
8981
- this.parent && this.parent.removeChild(this);
9143
+ this.parent?.removeChild(this);
8982
9144
  for (const child of this.children)
8983
9145
  {
8984
- child.parent = 0;
9146
+ child.parent = undefined;
8985
9147
  child.destroy();
8986
9148
  }
8987
9149
  }
@@ -9005,7 +9167,7 @@ class UIObject
9005
9167
  this.onUpdate();
9006
9168
 
9007
9169
  // unset active if disabled
9008
- if (this.disabled && this == uiSystem.activeObject)
9170
+ if (this.disabled && this === uiSystem.activeObject)
9009
9171
  uiSystem.activeObject = undefined;
9010
9172
 
9011
9173
  const wasHover = uiSystem.lastHoverObject === this;
@@ -9077,10 +9239,10 @@ class UIObject
9077
9239
  this.interactive && this.isActiveObject() && !this.disabled ?
9078
9240
  this.color : this.lineColor;
9079
9241
  const color = isNavigationObject ? this.hoverColor :
9080
- this.disabled ? this.disabledColor :
9081
- this.interactive ?
9082
- this.isHoverObject() ? this.hoverColor :
9083
- this.isActiveObject() ? this.activeColor || this.color :
9242
+ this.disabled ? this.disabledColor :
9243
+ this.interactive ?
9244
+ this.isActiveObject() ? this.activeColor || this.hoverColor :
9245
+ this.isHoverObject() ? this.hoverColor :
9084
9246
  this.color : this.color;
9085
9247
  const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
9086
9248
 
@@ -9092,7 +9254,7 @@ class UIObject
9092
9254
  getTextSize()
9093
9255
  {
9094
9256
  return vec2(
9095
- this.textWidth || this.textFitScale * this.size.x,
9257
+ this.textWidth || this.textFitScale * this.size.x,
9096
9258
  this.textHeight || this.textFitScale * this.size.y);
9097
9259
  }
9098
9260
 
@@ -9140,9 +9302,9 @@ class UIObject
9140
9302
  renderDebug(visible=true)
9141
9303
  {
9142
9304
  // apply color based on state
9143
- const color =
9305
+ const color =
9144
9306
  !visible ? GREEN :
9145
- this.isHoverObject() ? YELLOW :
9307
+ this.isHoverObject() ? YELLOW :
9146
9308
  this.disabled ? PURPLE :
9147
9309
  this.interactive ? RED : BLUE;
9148
9310
  uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);