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 Debug System
@@ -781,7 +769,7 @@ let debugKey = 'Escape';
781
769
  let debugOverlay = false;
782
770
 
783
771
  // Engine internal variables not exposed to documentation
784
- let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugMedals = false, debugTakeScreenshot, downloadLink, debugCanvas;
772
+ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugMedals = false, debugTakeScreenshot;
785
773
 
786
774
  ///////////////////////////////////////////////////////////////////////////////
787
775
  // Debug helper functions
@@ -805,7 +793,7 @@ function LOG(...output) { console.log(...output); }
805
793
 
806
794
  /** Draw a debug rectangle in world space
807
795
  * @param {Vector2} pos
808
- * @param {Vector2} [size=Vector2()]
796
+ * @param {Vector2} [size=vec2(0)]
809
797
  * @param {Color|string} [color]
810
798
  * @param {number} [time]
811
799
  * @param {number} [angle]
@@ -968,46 +956,6 @@ function debugClear() { debugPrimitives = []; }
968
956
  * @memberof Debug */
969
957
  function debugScreenshot() { debugTakeScreenshot = 1; }
970
958
 
971
- /** Save a canvas to disk
972
- * @param {HTMLCanvasElement|OffscreenCanvas} canvas
973
- * @param {string} [filename]
974
- * @param {string} [type]
975
- * @memberof Debug */
976
- function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
977
- {
978
- if (canvas instanceof OffscreenCanvas)
979
- {
980
- // copy to temporary canvas and save
981
- if (!debugCanvas)
982
- debugCanvas = document.createElement('canvas');
983
- debugCanvas.width = canvas.width;
984
- debugCanvas.height = canvas.height;
985
- debugCanvas.getContext('2d').drawImage(canvas, 0, 0);
986
- debugSaveDataURL(debugCanvas.toDataURL(type), filename);
987
- }
988
- else
989
- debugSaveDataURL(canvas.toDataURL(type), filename);
990
- }
991
-
992
- /** Save a text file to disk
993
- * @param {string} text
994
- * @param {string} [filename]
995
- * @param {string} [type]
996
- * @memberof Debug */
997
- function debugSaveText(text, filename='text', type='text/plain')
998
- { debugSaveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
999
-
1000
- /** Save a data url to disk
1001
- * @param {string} dataURL
1002
- * @param {string} filename
1003
- * @memberof Debug */
1004
- function debugSaveDataURL(dataURL, filename)
1005
- {
1006
- downloadLink.download = filename;
1007
- downloadLink.href = dataURL;
1008
- downloadLink.click();
1009
- }
1010
-
1011
959
  /** Breaks on all asserts/errors, hides the canvas, and shows message in plain text
1012
960
  * This is a good function to call at the start of your game to catch all errors
1013
961
  * In release builds this function has no effect
@@ -1043,21 +991,11 @@ function debugShowErrors()
1043
991
 
1044
992
  function debugInit()
1045
993
  {
1046
- // create link for saving screenshots
1047
- downloadLink = document.createElement('a');
1048
994
  }
1049
995
 
1050
996
  function debugUpdate()
1051
997
  {
1052
- if (!debug)
1053
- return;
1054
-
1055
- if (debugVideoCaptureIsActive())
1056
- {
1057
- // control to stop video capture
1058
- if (keyWasPressed('Digit6') || keyWasPressed(debugKey))
1059
- debugVideoCaptureStop();
1060
- }
998
+ if (!debug) return;
1061
999
 
1062
1000
  if (keyWasPressed(debugKey)) // Esc
1063
1001
  debugOverlay = !debugOverlay;
@@ -1075,9 +1013,15 @@ function debugUpdate()
1075
1013
  debugRaycast = !debugRaycast;
1076
1014
  if (keyWasPressed('Digit5'))
1077
1015
  debugScreenshot();
1078
- if (keyWasPressed('Digit6') && !debugVideoCaptureIsActive())
1079
- debugVideoCaptureStart();
1080
1016
  }
1017
+ if (debugVideoCaptureIsActive())
1018
+ {
1019
+ // control to stop video capture
1020
+ if (!debugOverlay || keyWasPressed('Digit6'))
1021
+ debugVideoCaptureStop();
1022
+ }
1023
+ else if (debugOverlay && keyWasPressed('Digit6'))
1024
+ debugVideoCaptureStart();
1081
1025
  }
1082
1026
 
1083
1027
  function debugRender()
@@ -1092,7 +1036,7 @@ function debugRender()
1092
1036
  {
1093
1037
  // combine canvases, remove alpha and save
1094
1038
  combineCanvases();
1095
- debugSaveCanvas(mainCanvas);
1039
+ saveCanvas(mainCanvas);
1096
1040
  debugTakeScreenshot = 0;
1097
1041
  }
1098
1042
 
@@ -1173,7 +1117,7 @@ function debugRender()
1173
1117
  if (tileCollisionTest(mousePos))
1174
1118
  {
1175
1119
  // show floored tile pick for tile collision
1176
- drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(1,1,0,.5));
1120
+ drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(1,1,0,.5), 0, false);
1177
1121
  }
1178
1122
  }
1179
1123
 
@@ -1252,8 +1196,8 @@ function debugRender()
1252
1196
  if (debugObject)
1253
1197
  {
1254
1198
  const raycastHitPos = tileCollisionRaycast(debugObject.pos, mousePos);
1255
- raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3));
1256
- drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5));
1199
+ raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3), 0, false);
1200
+ drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5), undefined, undefined, false);
1257
1201
 
1258
1202
  let debugText = 'mouse pos = ' + mousePos;
1259
1203
  if (tileCollisionLayers.length)
@@ -1338,11 +1282,34 @@ function debugRender()
1338
1282
  }
1339
1283
  }
1340
1284
 
1285
+ function debugRenderPost()
1286
+ {
1287
+ if (debugVideoCaptureIsActive())
1288
+ {
1289
+ debugVideoCaptureUpdate();
1290
+ return;
1291
+ }
1292
+
1293
+ if (!debugWatermark) return;
1294
+
1295
+ // update fps display
1296
+ mainContext.textAlign = 'right';
1297
+ mainContext.textBaseline = 'top';
1298
+ mainContext.font = '1em monospace';
1299
+ mainContext.fillStyle = '#000';
1300
+ const text = engineName + ' v' + engineVersion + ' / '
1301
+ + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
1302
+ + (glEnable ? ' GL' : ' 2D') ;
1303
+ mainContext.fillText(text, mainCanvas.width-3, 3);
1304
+ mainContext.fillStyle = '#fff';
1305
+ mainContext.fillText(text, mainCanvas.width-2, 2);
1306
+ }
1307
+
1341
1308
  ///////////////////////////////////////////////////////////////////////////////
1342
1309
  // video capture - records video and audio at 60 fps using MediaRecorder API
1343
1310
 
1344
1311
  // internal variables used to capture video
1345
- let debugVideoCapture, debugVideoCaptureTrack, debugVideoCaptureIcon, debugVideoCaptureTimer;
1312
+ let debugVideoCapture, debugVideoCaptureIcon;
1346
1313
 
1347
1314
  /** Check if video capture is active
1348
1315
  * @memberof Debug */
@@ -1352,80 +1319,103 @@ function debugVideoCaptureIsActive() { return !!debugVideoCapture; }
1352
1319
  * @memberof Debug */
1353
1320
  function debugVideoCaptureStart()
1354
1321
  {
1355
- if (debugVideoCaptureIsActive())
1356
- return; // already recording
1322
+ ASSERT(!debugVideoCaptureIsActive(), 'Already capturing video!');
1357
1323
 
1358
- // captureStream passing in 0 to only capture when requestFrame() is called
1324
+ if (!debugVideoCaptureIcon)
1325
+ {
1326
+ // create recording icon to show it is capturing video
1327
+ debugVideoCaptureIcon = document.createElement('div');
1328
+ debugVideoCaptureIcon.style.position = 'absolute';
1329
+ debugVideoCaptureIcon.style.padding = '9px';
1330
+ debugVideoCaptureIcon.style.color = '#f00';
1331
+ debugVideoCaptureIcon.style.font = '50px monospace';
1332
+ document.body.appendChild(debugVideoCaptureIcon);
1333
+ }
1334
+ // show recording icon
1335
+ debugVideoCaptureIcon.textContent = '';
1336
+ debugVideoCaptureIcon.style.display = '';
1337
+
1338
+ // setup captureStream to capture manually by passing 0
1359
1339
  const stream = mainCanvas.captureStream(0);
1340
+ const videoTrack = stream.getVideoTracks()[0];
1341
+ const captureTimer = new Timer(0, true);
1360
1342
  const chunks = [];
1361
- debugVideoCaptureTrack = stream.getVideoTracks()[0];
1362
- if (debugVideoCaptureTrack.applyConstraints)
1363
- debugVideoCaptureTrack.applyConstraints({frameRate:60}); // force 60 fps
1364
- debugVideoCapture = new MediaRecorder(stream, {mimeType:'video/webm;codecs=vp8'});
1365
- debugVideoCapture.ondataavailable = (e)=> chunks.push(e.data);
1366
- debugVideoCapture.onstop = ()=>
1343
+ videoTrack.applyConstraints({frameRate:frameRate});
1344
+
1345
+ // set up the media recorder
1346
+ const mediaRecorder = new MediaRecorder(stream,
1347
+ {mimeType:'video/webm;codecs=vp8'});
1348
+ mediaRecorder.ondataavailable = (e)=> chunks.push(e.data);
1349
+ mediaRecorder.onstop = ()=>
1367
1350
  {
1368
1351
  const blob = new Blob(chunks, {type: 'video/webm'});
1369
1352
  const url = URL.createObjectURL(blob);
1370
- downloadLink.download = 'capture.webm';
1371
- downloadLink.href = url;
1372
- downloadLink.click();
1373
- URL.revokeObjectURL(url);
1353
+ saveDataURL(url, 'capture.webm', 1e3);
1374
1354
  };
1375
1355
 
1376
- if (audioMasterGain)
1356
+ let audioStreamDestination, silentAudioSource;
1357
+ if (soundEnable)
1377
1358
  {
1359
+ // create silent audio source
1360
+ // fixes issue where video can not start recording without audio
1361
+ silentAudioSource = new ConstantSourceNode(audioContext, { offset: 0 });
1362
+ silentAudioSource.connect(audioMasterGain);
1363
+ silentAudioSource.start();
1364
+
1378
1365
  // connect to audio master gain node
1379
- const audioStreamDestination = audioContext.createMediaStreamDestination();
1366
+ audioStreamDestination = audioContext.createMediaStreamDestination();
1380
1367
  audioMasterGain.connect(audioStreamDestination);
1381
1368
  for (const track of audioStreamDestination.stream.getAudioTracks())
1382
1369
  stream.addTrack(track); // add audio tracks to capture stream
1383
1370
  }
1384
1371
 
1385
1372
  // start recording
1373
+ try { mediaRecorder.start(); }
1374
+ catch(e)
1375
+ {
1376
+ LOG('Video capture not supported in this browser!');
1377
+ silentAudioSource?.stop();
1378
+ return;
1379
+ }
1380
+
1386
1381
  LOG('Video capture started.');
1387
- debugVideoCapture.start();
1388
- debugVideoCaptureTimer = new Timer(0, true);
1389
1382
 
1390
- if (!debugVideoCaptureIcon)
1383
+ // save debug video info
1384
+ debugVideoCapture =
1391
1385
  {
1392
- // create recording icon to show it is capturing video
1393
- debugVideoCaptureIcon = document.createElement('div');
1394
- debugVideoCaptureIcon.style.position = 'absolute';
1395
- debugVideoCaptureIcon.style.padding = '9px';
1396
- debugVideoCaptureIcon.style.color = '#f00';
1397
- debugVideoCaptureIcon.style.font = '50px monospace';
1398
- document.body.appendChild(debugVideoCaptureIcon);
1399
- }
1400
- // show recording icon
1401
- debugVideoCaptureIcon.textContent = '';
1402
- debugVideoCaptureIcon.style.display = '';
1386
+ mediaRecorder,
1387
+ captureTimer,
1388
+ videoTrack,
1389
+ silentAudioSource,
1390
+ audioStreamDestination
1391
+ };
1403
1392
  }
1404
1393
 
1405
1394
  /** Stop capturing video and save to disk
1406
1395
  * @memberof Debug */
1407
1396
  function debugVideoCaptureStop()
1408
1397
  {
1409
- if (!debugVideoCaptureIsActive())
1410
- return; // not recording
1398
+ ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
1411
1399
 
1412
1400
  // stop recording
1413
- LOG(`Video capture ended. ${debugVideoCaptureTimer.get().toFixed(2)} seconds recorded.`);
1414
- debugVideoCapture.stop();
1415
- debugVideoCapture = 0;
1401
+ LOG(`Video capture ended. ${debugVideoCapture.captureTimer.get().toFixed(2)} seconds recorded.`);
1416
1402
  debugVideoCaptureIcon.style.display = 'none';
1403
+ debugVideoCapture.silentAudioSource?.stop();
1404
+ debugVideoCapture.mediaRecorder?.stop();
1405
+ debugVideoCapture.videoTrack?.stop();
1406
+ debugVideoCapture = undefined;
1417
1407
  }
1418
1408
 
1419
1409
  // update video capture, called automatically by engine
1420
1410
  function debugVideoCaptureUpdate()
1421
1411
  {
1422
- if (!debugVideoCaptureIsActive())
1423
- return; // not recording
1412
+ ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
1424
1413
 
1425
1414
  // save the video frame
1426
1415
  combineCanvases();
1427
- debugVideoCaptureTrack.requestFrame();
1428
- debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
1416
+ debugVideoCapture.videoTrack.requestFrame();
1417
+ debugVideoCaptureIcon.textContent = '● REC '
1418
+ + formatTime(debugVideoCapture.captureTimer);
1429
1419
  }
1430
1420
 
1431
1421
  ///////////////////////////////////////////////////////////////////////////////
@@ -1458,105 +1448,104 @@ function debugProtectConstant(obj)
1458
1448
  return Object.freeze(obj);
1459
1449
  }
1460
1450
  /**
1461
- * LittleJS Utility Classes and Functions
1451
+ * LittleJS Math Classes and Functions
1462
1452
  * - General purpose math library
1463
- * - Vector2 - fast, simple, easy 2D vector class
1464
- * - Color - holds a rgba color with some math functions
1465
- * - Timer - tracks time automatically
1466
1453
  * - RandomGenerator - seeded random number generator
1467
- * @namespace Utilities
1454
+ * - Vector2 - fast, simple, easy 2D vector class
1455
+ * - Color - holds a rgba color with math functions
1456
+ * @namespace Math
1468
1457
  */
1469
1458
 
1470
1459
  /** The value of PI
1471
1460
  * @type {number}
1472
1461
  * @default Math.PI
1473
- * @memberof Utilities */
1462
+ * @memberof Math */
1474
1463
  const PI = Math.PI;
1475
1464
 
1476
1465
  /** Returns absolute value of value passed in
1477
1466
  * @param {number} value
1478
1467
  * @return {number}
1479
- * @memberof Utilities */
1468
+ * @memberof Math */
1480
1469
  const abs = Math.abs;
1481
1470
 
1482
1471
  /** Returns floored value of value passed in
1483
1472
  * @param {number} value
1484
1473
  * @return {number}
1485
- * @memberof Utilities */
1474
+ * @memberof Math */
1486
1475
  const floor = Math.floor;
1487
1476
 
1488
1477
  /** Returns ceiled value of value passed in
1489
1478
  * @param {number} value
1490
1479
  * @return {number}
1491
- * @memberof Utilities */
1480
+ * @memberof Math */
1492
1481
  const ceil = Math.ceil;
1493
1482
 
1494
1483
  /** Returns rounded value passed in
1495
1484
  * @param {number} value
1496
1485
  * @return {number}
1497
- * @memberof Utilities */
1486
+ * @memberof Math */
1498
1487
  const round = Math.round;
1499
1488
 
1500
1489
  /** Returns lowest value passed in
1501
1490
  * @param {...number} values
1502
1491
  * @return {number}
1503
- * @memberof Utilities */
1492
+ * @memberof Math */
1504
1493
  const min = Math.min;
1505
1494
 
1506
1495
  /** Returns highest value passed in
1507
1496
  * @param {...number} values
1508
1497
  * @return {number}
1509
- * @memberof Utilities */
1498
+ * @memberof Math */
1510
1499
  const max = Math.max;
1511
1500
 
1512
1501
  /** Returns the sign of value passed in
1513
1502
  * @param {number} value
1514
1503
  * @return {number}
1515
- * @memberof Utilities */
1504
+ * @memberof Math */
1516
1505
  const sign = Math.sign;
1517
1506
 
1518
1507
  /** Returns hypotenuse of values passed in
1519
1508
  * @param {...number} values
1520
1509
  * @return {number}
1521
- * @memberof Utilities */
1510
+ * @memberof Math */
1522
1511
  const hypot = Math.hypot;
1523
1512
 
1524
1513
  /** Returns log2 of value passed in
1525
1514
  * @param {number} value
1526
1515
  * @return {number}
1527
- * @memberof Utilities */
1516
+ * @memberof Math */
1528
1517
  const log2 = Math.log2;
1529
1518
 
1530
1519
  /** Returns sin of value passed in
1531
1520
  * @param {number} value
1532
1521
  * @return {number}
1533
- * @memberof Utilities */
1522
+ * @memberof Math */
1534
1523
  const sin = Math.sin;
1535
1524
 
1536
1525
  /** Returns cos of value passed in
1537
1526
  * @param {number} value
1538
1527
  * @return {number}
1539
- * @memberof Utilities */
1528
+ * @memberof Math */
1540
1529
  const cos = Math.cos;
1541
1530
 
1542
1531
  /** Returns tan of value passed in
1543
1532
  * @param {number} value
1544
1533
  * @return {number}
1545
- * @memberof Utilities */
1534
+ * @memberof Math */
1546
1535
  const tan = Math.tan;
1547
1536
 
1548
1537
  /** Returns atan2 of values passed in
1549
1538
  * @param {number} y
1550
1539
  * @param {number} x
1551
1540
  * @return {number}
1552
- * @memberof Utilities */
1541
+ * @memberof Math */
1553
1542
  const atan2 = Math.atan2;
1554
1543
 
1555
1544
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
1556
1545
  * @param {number} dividend
1557
1546
  * @param {number} [divisor]
1558
1547
  * @return {number}
1559
- * @memberof Utilities */
1548
+ * @memberof Math */
1560
1549
  function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % divisor; }
1561
1550
 
1562
1551
  /** Clamps the value between max and min
@@ -1564,7 +1553,7 @@ function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % di
1564
1553
  * @param {number} [min]
1565
1554
  * @param {number} [max]
1566
1555
  * @return {number}
1567
- * @memberof Utilities */
1556
+ * @memberof Math */
1568
1557
  function clamp(value, min=0, max=1) { return value < min ? min : value > max ? max : value; }
1569
1558
 
1570
1559
  /** Returns what percentage the value is between valueA and valueB
@@ -1572,7 +1561,7 @@ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? m
1572
1561
  * @param {number} valueA
1573
1562
  * @param {number} valueB
1574
1563
  * @return {number}
1575
- * @memberof Utilities */
1564
+ * @memberof Math */
1576
1565
  function percent(value, valueA, valueB)
1577
1566
  { return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
1578
1567
 
@@ -1581,7 +1570,7 @@ function percent(value, valueA, valueB)
1581
1570
  * @param {number} valueB
1582
1571
  * @param {number} percent
1583
1572
  * @return {number}
1584
- * @memberof Utilities */
1573
+ * @memberof Math */
1585
1574
  function lerp(valueA, valueB, percent)
1586
1575
  { return valueA + clamp(percent) * (valueB-valueA); }
1587
1576
 
@@ -1593,7 +1582,7 @@ function lerp(valueA, valueB, percent)
1593
1582
  * @param {number} lerpA
1594
1583
  * @param {number} lerpB
1595
1584
  * @return {number}
1596
- * @memberof Utilities */
1585
+ * @memberof Math */
1597
1586
  function percentLerp(value, percentA, percentB, lerpA, lerpB)
1598
1587
  { return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
1599
1588
 
@@ -1602,7 +1591,7 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
1602
1591
  * @param {number} valueB
1603
1592
  * @param {number} [wrapSize]
1604
1593
  * @return {number}
1605
- * @memberof Utilities */
1594
+ * @memberof Math */
1606
1595
  function distanceWrap(valueA, valueB, wrapSize=1)
1607
1596
  { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
1608
1597
 
@@ -1612,7 +1601,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
1612
1601
  * @param {number} percent
1613
1602
  * @param {number} [wrapSize]
1614
1603
  * @return {number}
1615
- * @memberof Utilities */
1604
+ * @memberof Math */
1616
1605
  function lerpWrap(valueA, valueB, percent, wrapSize=1)
1617
1606
  { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
1618
1607
 
@@ -1620,7 +1609,7 @@ function lerpWrap(valueA, valueB, percent, wrapSize=1)
1620
1609
  * @param {number} angleA
1621
1610
  * @param {number} angleB
1622
1611
  * @return {number}
1623
- * @memberof Utilities */
1612
+ * @memberof Math */
1624
1613
  function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
1625
1614
 
1626
1615
  /** Linearly interpolates between the angles passed in with wrapping
@@ -1628,35 +1617,35 @@ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*P
1628
1617
  * @param {number} angleB
1629
1618
  * @param {number} percent
1630
1619
  * @return {number}
1631
- * @memberof Utilities */
1620
+ * @memberof Math */
1632
1621
  function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
1633
1622
 
1634
1623
  /** Applies smoothstep function to the percentage value
1635
1624
  * @param {number} percent
1636
1625
  * @return {number}
1637
- * @memberof Utilities */
1626
+ * @memberof Math */
1638
1627
  function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
1639
1628
 
1640
1629
  /** Checks if the value passed in is a power of two
1641
1630
  * @param {number} value
1642
1631
  * @return {boolean}
1643
- * @memberof Utilities */
1632
+ * @memberof Math */
1644
1633
  function isPowerOfTwo(value) { return !(value & (value - 1)); }
1645
1634
 
1646
1635
  /** Returns the nearest power of two not less than the value
1647
1636
  * @param {number} value
1648
1637
  * @return {number}
1649
- * @memberof Utilities */
1638
+ * @memberof Math */
1650
1639
  function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
1651
1640
 
1652
1641
  /** Returns true if two axis aligned bounding boxes are overlapping
1653
1642
  * this can be used for simple collision detection between objects
1654
- * @param {Vector2} posA - Center of box A
1655
- * @param {Vector2} sizeA - Size of box A
1656
- * @param {Vector2} posB - Center of box B
1657
- * @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
1658
- * @return {boolean} - True if overlapping
1659
- * @memberof Utilities */
1643
+ * @param {Vector2} posA - Center of box A
1644
+ * @param {Vector2} sizeA - Size of box A
1645
+ * @param {Vector2} posB - Center of box B
1646
+ * @param {Vector2} [sizeB=vec2()] - Size of box B, uses a point if undefined
1647
+ * @return {boolean} - True if overlapping
1648
+ * @memberof Math */
1660
1649
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
1661
1650
  {
1662
1651
  const dx = (posA.x - posB.x)*2;
@@ -1672,7 +1661,7 @@ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
1672
1661
  * @param {Vector2} pos - Center of box
1673
1662
  * @param {Vector2} size - Size of box
1674
1663
  * @return {boolean} - True if intersecting
1675
- * @memberof Utilities */
1664
+ * @memberof Math */
1676
1665
  function isIntersecting(start, end, pos, size)
1677
1666
  {
1678
1667
  // Liang-Barsky algorithm
@@ -1713,52 +1702,29 @@ function isIntersecting(start, end, pos, size)
1713
1702
  * @param {number} [t=time] - Value to use for time of the wave
1714
1703
  * @param {number} [offset] - Value to use for time offset of the wave
1715
1704
  * @return {number} - Value waving between 0 and amplitude
1716
- * @memberof Utilities */
1705
+ * @memberof Math */
1717
1706
  function wave(frequency=1, amplitude=1, t=time, offset=0)
1718
1707
  { return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
1719
1708
 
1720
- /** Formats seconds to mm:ss style for display purposes
1721
- * @param {number} t - time in seconds
1722
- * @return {string}
1723
- * @memberof Utilities */
1724
- function formatTime(t)
1725
- {
1726
- const sign = t < 0 ? '-' : '';
1727
- t = abs(t)|0;
1728
- return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
1729
- }
1730
-
1731
- /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
1732
- * @param {string} url - URL of JSON file
1733
- * @return {Promise<object>}
1734
- * @memberof Utilities */
1735
- async function fetchJSON(url)
1736
- {
1737
- const response = await fetch(url);
1738
- if (!response.ok)
1739
- throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1740
- return response.json();
1741
- }
1742
-
1743
1709
  /**
1744
1710
  * Check if object is a valid number, not NaN or undefined, but it may be infinite
1745
1711
  * @param {any} n
1746
1712
  * @return {boolean}
1747
- * @memberof Utilities */
1713
+ * @memberof Math */
1748
1714
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1749
1715
 
1750
1716
  /**
1751
1717
  * Check if object is a valid string or can be converted to one
1752
1718
  * @param {any} s
1753
1719
  * @return {boolean}
1754
- * @memberof Utilities */
1755
- function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1720
+ * @memberof Math */
1721
+ function isString(s) { return s != null && typeof s?.toString() === 'string'; }
1756
1722
 
1757
1723
  /**
1758
1724
  * Check if object is an array
1759
1725
  * @param {any} a
1760
1726
  * @return {boolean}
1761
- * @memberof Utilities */
1727
+ * @memberof Math */
1762
1728
  function isArray(a) { return Array.isArray(a); }
1763
1729
 
1764
1730
  /**
@@ -1774,7 +1740,7 @@ function isArray(a) { return Array.isArray(a); }
1774
1740
  * @param {LineTestFunction} testFunction - Check if colliding
1775
1741
  * @param {Vector2} [normal] - Optional vector to store the normal
1776
1742
  * @return {Vector2|undefined} - Position of the collision or undefined if none found
1777
- * @memberof Utilities */
1743
+ * @memberof Math */
1778
1744
  function lineTest(posStart, posEnd, testFunction, normal)
1779
1745
  {
1780
1746
  ASSERT(isVector2(posStart), 'posStart must be a vec2');
@@ -1786,8 +1752,7 @@ function lineTest(posStart, posEnd, testFunction, normal)
1786
1752
  const dx = posEnd.x - posStart.x;
1787
1753
  const dy = posEnd.y - posStart.y;
1788
1754
  const totalLength = hypot(dx, dy);
1789
- if (!totalLength)
1790
- return;
1755
+ if (!totalLength) return;
1791
1756
 
1792
1757
  // current integer cell we are in
1793
1758
  const pos = posStart.floor();
@@ -1897,8 +1862,8 @@ function randInCircle(radius=1, minRadius=0)
1897
1862
  { return radius > 0 ? randVec2(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
1898
1863
 
1899
1864
  /** Returns a random color between the two passed in colors, combine components if linear
1900
- * @param {Color} [colorA=(1,1,1,1)]
1901
- * @param {Color} [colorB=(0,0,0,1)]
1865
+ * @param {Color} [colorA=WHITE]
1866
+ * @param {Color} [colorB=BLACK]
1902
1867
  * @param {boolean} [linear]
1903
1868
  * @return {Color}
1904
1869
  * @memberof Random */
@@ -1977,8 +1942,8 @@ class RandomGenerator
1977
1942
  { return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
1978
1943
 
1979
1944
  /** Returns a random color between the two passed in colors, combine components if linear
1980
- * @param {Color} [colorA=(1,1,1,1)]
1981
- * @param {Color} [colorB=(0,0,0,1)]
1945
+ * @param {Color} [colorA=WHITE]
1946
+ * @param {Color} [colorB=BLACK]
1982
1947
  * @param {boolean} [linear]
1983
1948
  * @return {Color} */
1984
1949
  randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
@@ -2021,14 +1986,14 @@ class RandomGenerator
2021
1986
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
2022
1987
  * a = vec2(5); // set a to (5, 5)
2023
1988
  * b = vec2(); // set b to (0, 0)
2024
- * @memberof Utilities */
2025
- function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
1989
+ * @memberof Math */
1990
+ function vec2(x=0, y) { return new Vector2(x, y ?? x); }
2026
1991
 
2027
1992
  /**
2028
1993
  * Check if object is a valid Vector2
2029
1994
  * @param {any} v
2030
1995
  * @return {boolean}
2031
- * @memberof Utilities */
1996
+ * @memberof Math */
2032
1997
  function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
2033
1998
 
2034
1999
  // vector2 asserts
@@ -2230,10 +2195,6 @@ class Vector2
2230
2195
  * @return {number} */
2231
2196
  area() { return abs(this.x * this.y); }
2232
2197
 
2233
- /** Returns true if this vector is (0,0)
2234
- * @return {boolean} */
2235
- isZero() { return !this.x && !this.y; }
2236
-
2237
2198
  /** Returns a new vector that is p percent between this and the vector passed in
2238
2199
  * @param {Vector2} v - other vector
2239
2200
  * @param {number} percent
@@ -2278,7 +2239,7 @@ class Vector2
2278
2239
  * @param {number} [b=1] - blue
2279
2240
  * @param {number} [a=1] - alpha
2280
2241
  * @return {Color}
2281
- * @memberof Utilities
2242
+ * @memberof Math
2282
2243
  */
2283
2244
  function rgb(r, g, b, a) { return new Color(r, g, b, a); }
2284
2245
 
@@ -2289,14 +2250,14 @@ function rgb(r, g, b, a) { return new Color(r, g, b, a); }
2289
2250
  * @param {number} [l=1] - lightness
2290
2251
  * @param {number} [a=1] - alpha
2291
2252
  * @return {Color}
2292
- * @memberof Utilities */
2253
+ * @memberof Math */
2293
2254
  function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
2294
2255
 
2295
2256
  /**
2296
2257
  * Check if object is a valid Color
2297
2258
  * @param {any} c
2298
2259
  * @return {boolean}
2299
- * @memberof Utilities */
2260
+ * @memberof Math */
2300
2261
  function isColor(c) { return c instanceof Color && c.isValid(); }
2301
2262
 
2302
2263
  // color asserts
@@ -2477,7 +2438,7 @@ class Color
2477
2438
  toString(useAlpha = true)
2478
2439
  {
2479
2440
  if (debug && !this.isValid())
2480
- return `#000`;
2441
+ return '#000';
2481
2442
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
2482
2443
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
2483
2444
  }
@@ -2534,68 +2495,158 @@ class Color
2534
2495
 
2535
2496
  /** Color - White #ffffff
2536
2497
  * @type {Color}
2537
- * @memberof Utilities */
2498
+ * @memberof Math */
2538
2499
  const WHITE = debugProtectConstant(rgb());
2539
2500
 
2540
2501
  /** Color - Clear White #757474ff with 0 alpha
2541
2502
  * @type {Color}
2542
- * @memberof Utilities */
2503
+ * @memberof Math */
2543
2504
  const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
2544
2505
 
2545
2506
  /** Color - Black #000000
2546
2507
  * @type {Color}
2547
- * @memberof Utilities */
2508
+ * @memberof Math */
2548
2509
  const BLACK = debugProtectConstant(rgb(0,0,0));
2549
2510
 
2550
2511
  /** Color - Clear Black #000000 with 0 alpha
2551
2512
  * @type {Color}
2552
- * @memberof Utilities */
2513
+ * @memberof Math */
2553
2514
  const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
2554
2515
 
2555
2516
  /** Color - Gray #808080
2556
2517
  * @type {Color}
2557
- * @memberof Utilities */
2518
+ * @memberof Math */
2558
2519
  const GRAY = debugProtectConstant(rgb(.5,.5,.5));
2559
2520
 
2560
2521
  /** Color - Red #ff0000
2561
2522
  * @type {Color}
2562
- * @memberof Utilities */
2523
+ * @memberof Math */
2563
2524
  const RED = debugProtectConstant(rgb(1,0,0));
2564
2525
 
2565
2526
  /** Color - Orange #ff8000
2566
2527
  * @type {Color}
2567
- * @memberof Utilities */
2528
+ * @memberof Math */
2568
2529
  const ORANGE = debugProtectConstant(rgb(1,.5,0));
2569
2530
 
2570
2531
  /** Color - Yellow #ffff00
2571
2532
  * @type {Color}
2572
- * @memberof Utilities */
2533
+ * @memberof Math */
2573
2534
  const YELLOW = debugProtectConstant(rgb(1,1,0));
2574
2535
 
2575
2536
  /** Color - Green #00ff00
2576
2537
  * @type {Color}
2577
- * @memberof Utilities */
2538
+ * @memberof Math */
2578
2539
  const GREEN = debugProtectConstant(rgb(0,1,0));
2579
2540
 
2580
2541
  /** Color - Cyan #00ffff
2581
2542
  * @type {Color}
2582
- * @memberof Utilities */
2543
+ * @memberof Math */
2583
2544
  const CYAN = debugProtectConstant(rgb(0,1,1));
2584
2545
 
2585
2546
  /** Color - Blue #0000ff
2586
2547
  * @type {Color}
2587
- * @memberof Utilities */
2548
+ * @memberof Math */
2588
2549
  const BLUE = debugProtectConstant(rgb(0,0,1));
2589
2550
 
2590
2551
  /** Color - Purple #8000ff
2591
2552
  * @type {Color}
2592
- * @memberof Utilities */
2553
+ * @memberof Math */
2593
2554
  const PURPLE = debugProtectConstant(rgb(.5,0,1));
2594
2555
 
2595
2556
  /** Color - Magenta #ff00ff
2596
2557
  * @type {Color}
2558
+ * @memberof Math */
2559
+ const MAGENTA = debugProtectConstant(rgb(1,0,1));
2560
+ /**
2561
+ * LittleJS Utility Classes and Functions
2562
+ * - General purpose utilities
2563
+ * - Timer - tracks time automatically
2564
+ * @namespace Utilities
2565
+ */
2566
+
2567
+ /** Formats seconds to mm:ss style for display purposes
2568
+ * @param {number} t - time in seconds
2569
+ * @return {string}
2570
+ * @memberof Utilities */
2571
+ function formatTime(t)
2572
+ {
2573
+ const sign = t < 0 ? '-' : '';
2574
+ t = abs(t)|0;
2575
+ return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
2576
+ }
2577
+
2578
+ /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
2579
+ * @param {string} url - URL of JSON file
2580
+ * @return {Promise<object>}
2581
+ * @memberof Utilities */
2582
+ async function fetchJSON(url)
2583
+ {
2584
+ const response = await fetch(url);
2585
+ if (!response.ok)
2586
+ throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
2587
+ return response.json();
2588
+ }
2589
+
2590
+ ///////////////////////////////////////////////////////////////////////////////
2591
+
2592
+ /** Save a text file to disk
2593
+ * @param {string} text
2594
+ * @param {string} [filename]
2595
+ * @param {string} [type]
2597
2596
  * @memberof Utilities */
2598
- const MAGENTA = debugProtectConstant(rgb(1,0,1));
2597
+ function saveText(text, filename='text', type='text/plain')
2598
+ { saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
2599
+
2600
+ /** Save a canvas to disk
2601
+ * @param {HTMLCanvasElement|OffscreenCanvas} canvas
2602
+ * @param {string} [filename]
2603
+ * @param {string} [type]
2604
+ * @memberof Utilities */
2605
+ function saveCanvas(canvas, filename='screenshot', type='image/png')
2606
+ {
2607
+ if (canvas instanceof OffscreenCanvas)
2608
+ {
2609
+ // copy to temporary canvas and save
2610
+ const saveCanvas = document.createElement('canvas');
2611
+ saveCanvas.width = canvas.width;
2612
+ saveCanvas.height = canvas.height;
2613
+ saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
2614
+ saveDataURL(saveCanvas.toDataURL(type), filename);
2615
+ }
2616
+ else
2617
+ saveDataURL(canvas.toDataURL(type), filename);
2618
+ }
2619
+
2620
+ /** Save a data url to disk
2621
+ * @param {string} url
2622
+ * @param {string} [filename]
2623
+ * @param {number} [revokeTime] - how long before revoking the url
2624
+ * @memberof Utilities */
2625
+ function saveDataURL(url, filename='download', revokeTime)
2626
+ {
2627
+ ASSERT(isString(url), 'saveDataURL requires url string');
2628
+ ASSERT(isString(filename), 'saveDataURL requires filename string');
2629
+
2630
+ // create link for saving screenshots
2631
+ const link = document.createElement('a');
2632
+ link.download = filename;
2633
+ link.href = url;
2634
+ link.click();
2635
+ if (revokeTime !== undefined)
2636
+ setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
2637
+ }
2638
+
2639
+ /** Share content using the native share dialog if available
2640
+ * @param {string} title - title of the share
2641
+ * @param {string} url - url to share
2642
+ * @param {Function} [callback] - Called when share is complete
2643
+ * @memberof Utilities */
2644
+ function shareURL(title, url, callback)
2645
+ {
2646
+ ASSERT(isString(title), 'shareURL requires title string');
2647
+ ASSERT(isString(url), 'shareURL requires url string');
2648
+ navigator.share?.({title, url}).then(()=>callback?.());
2649
+ }
2599
2650
 
2600
2651
  ///////////////////////////////////////////////////////////////////////////////
2601
2652
 
@@ -2717,7 +2768,7 @@ let cameraScale = 32;
2717
2768
  * @memberof Settings */
2718
2769
  let canvasColorTiles = true;
2719
2770
 
2720
- /** Color to clear the canvas to before render
2771
+ /** Color to clear the canvas to before render, does not clear if alpha is 0
2721
2772
  * @type {Color}
2722
2773
  * @memberof Draw */
2723
2774
  let canvasClearColor = CLEAR_BLACK;
@@ -3026,7 +3077,7 @@ function setCameraScale(scale) { cameraScale = scale; }
3026
3077
  * @memberof Settings */
3027
3078
  function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
3028
3079
 
3029
- /** Set color to clear the canvas to before render
3080
+ /** Set color to clear the canvas to before render, does not clear if alpha is 0
3030
3081
  * @param {Color} color
3031
3082
  * @memberof Settings */
3032
3083
  function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
@@ -3306,10 +3357,10 @@ function setDebugKey(key) { debugKey = key; }
3306
3357
  class EngineObject
3307
3358
  {
3308
3359
  /** Create an engine object and adds it to the list of objects
3309
- * @param {Vector2} [pos=(0,0)] - World space position of the object
3310
- * @param {Vector2} [size=(1,1)] - World space size of the object
3311
- * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
3312
- * @param {number} [angle] - Angle the object is rotated by
3360
+ * @param {Vector2} [pos=vec2()] - World space position of the object
3361
+ * @param {Vector2} [size=vec2(1)] - World space size of the object
3362
+ * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
3363
+ * @param {number} [angle] - Angle the object is rotated by
3313
3364
  * @param {Color} [color=WHITE] - Color to apply to tile when rendered
3314
3365
  * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
3315
3366
  */
@@ -3437,15 +3488,16 @@ class EngineObject
3437
3488
  // physics sanity checks
3438
3489
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
3439
3490
  ASSERT(this.damping >= 0 && this.damping <= 1);
3440
- if (!enablePhysicsSolver || !this.mass) // don't do collision for static objects
3441
- return;
3491
+
3492
+ // don't do collision for static objects or if solver disabled
3493
+ if (!enablePhysicsSolver || !this.mass) return;
3442
3494
 
3443
3495
  const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
3444
3496
  if (this.groundObject)
3445
3497
  {
3446
3498
  // apply friction in local space of ground object
3447
3499
  const friction = max(this.friction, this.groundObject.friction);
3448
- const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
3500
+ const groundSpeed = this.groundObject.velocity.x;
3449
3501
  this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
3450
3502
  this.groundObject = undefined;
3451
3503
  }
@@ -3456,19 +3508,19 @@ class EngineObject
3456
3508
  const epsilon = .001; // necessary to push slightly outside of the collision
3457
3509
  for (const o of engineObjectsCollide)
3458
3510
  {
3511
+ // skip destroyed, child objects, or self collision
3512
+ if (o.destroyed || o.parent || o === this) continue;
3513
+
3459
3514
  // non solid objects don't collide with each other
3460
- if ((!this.isSolid && !o.isSolid) || o.destroyed || o.parent || o === this)
3461
- continue;
3515
+ if (!this.isSolid && !o.isSolid) continue;
3462
3516
 
3463
3517
  // check collision
3464
- if (!this.isOverlappingObject(o))
3465
- continue;
3518
+ if (!this.isOverlappingObject(o)) continue;
3466
3519
 
3467
3520
  // notify objects of collision and check if should be resolved
3468
3521
  const collide1 = this.collideWithObject(o);
3469
3522
  const collide2 = o.collideWithObject(this);
3470
- if (!collide1 || !collide2)
3471
- continue;
3523
+ if (!collide1 || !collide2) continue;
3472
3524
 
3473
3525
  if (isOverlapping(oldPos, this.size, o.pos, o.size))
3474
3526
  {
@@ -3570,7 +3622,7 @@ class EngineObject
3570
3622
  const delta = y - this.pos.y;
3571
3623
  if (delta < maxMoveUp)
3572
3624
  if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
3573
- {
3625
+ {
3574
3626
  this.pos.y = y;
3575
3627
  debugPhysics && debugRect(this.pos, this.size, '#ff0');
3576
3628
  return;
@@ -3630,10 +3682,10 @@ class EngineObject
3630
3682
 
3631
3683
  // disconnect from parent and destroy children
3632
3684
  this.destroyed = 1;
3633
- this.parent && this.parent.removeChild(this);
3685
+ this.parent?.removeChild(this);
3634
3686
  for (const child of this.children)
3635
3687
  {
3636
- child.parent = 0;
3688
+ child.parent = undefined;
3637
3689
  child.destroy();
3638
3690
  }
3639
3691
  }
@@ -3654,15 +3706,15 @@ class EngineObject
3654
3706
  * @param {Vector2} vec - world space vector */
3655
3707
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
3656
3708
 
3657
- /** Called to check if a tile collision should be resolved
3709
+ /** 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.
3658
3710
  * @param {number} tileData - the value of the tile at the position
3659
- * @param {Vector2} pos - tile where the collision occurred
3660
- * @return {boolean} - true if the collision should be resolved */
3711
+ * @param {Vector2} pos - tile where the collision occurred
3712
+ * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
3661
3713
  collideWithTile(tileData, pos) { return tileData > 0; }
3662
3714
 
3663
- /** Called to check if a object collision should be resolved
3715
+ /** 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.
3664
3716
  * @param {EngineObject} object - the object to test against
3665
- * @return {boolean} - true if the collision should be resolved
3717
+ * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
3666
3718
  */
3667
3719
  collideWithObject(object) { return true; }
3668
3720
 
@@ -3703,9 +3755,9 @@ class EngineObject
3703
3755
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
3704
3756
  getMirrorSign() { return this.mirror ? -1 : 1; }
3705
3757
 
3706
- /** Attaches a child to this with a local transform, returns child for chaining
3758
+ /** Attaches a child to this with a local transform, returns child for chaining
3707
3759
  * @param {EngineObject} child
3708
- * @param {Vector2} [localPos=(0,0)]
3760
+ * @param {Vector2} [localPos=vec2()]
3709
3761
  * @param {number} [localAngle]
3710
3762
  * @return {EngineObject} The child object added */
3711
3763
  addChild(child, localPos=vec2(), localAngle=0)
@@ -3729,7 +3781,7 @@ class EngineObject
3729
3781
  const index = this.children.indexOf(child);
3730
3782
  ASSERT(index >= 0, 'child not found in children array');
3731
3783
  index >= 0 && this.children.splice(index, 1);
3732
- child.parent = 0;
3784
+ child.parent = undefined;
3733
3785
  }
3734
3786
 
3735
3787
  /** Check if overlapping another engine object
@@ -3741,7 +3793,7 @@ class EngineObject
3741
3793
 
3742
3794
  /** Check if overlapping a point or aligned bounding box
3743
3795
  * @param {Vector2} pos - Center of box
3744
- * @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
3796
+ * @param {Vector2} [size=vec2()] - Size of box, uses a point if undefined
3745
3797
  * @return {boolean} */
3746
3798
  isOverlapping(pos, size=vec2())
3747
3799
  { return isOverlapping(this.pos, this.size, pos, size); }
@@ -3784,8 +3836,7 @@ class EngineObject
3784
3836
  /** Render debug info for this object */
3785
3837
  renderDebugInfo()
3786
3838
  {
3787
- if (!debug)
3788
- return;
3839
+ if (!debug) return;
3789
3840
 
3790
3841
  // show object info for debugging
3791
3842
  const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
@@ -3827,11 +3878,6 @@ let mainCanvas;
3827
3878
  * @memberof Draw */
3828
3879
  let mainContext;
3829
3880
 
3830
- /** The default canvas to use for drawing, usually mainCanvas
3831
- * @type {HTMLCanvasElement|OffscreenCanvas}
3832
- * @memberof Draw */
3833
- let drawCanvas;
3834
-
3835
3881
  /** The default 2d context to use for drawing, usually mainContext
3836
3882
  * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
3837
3883
  * @memberof Draw */
@@ -3878,10 +3924,11 @@ let drawCount;
3878
3924
  * Create a tile info object using a grid based system
3879
3925
  * - This can take vecs or floats for easier use and conversion
3880
3926
  * - If an index is passed in, the tile size and index will determine the position
3881
- * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
3927
+ * @param {Vector2|number} [index=0] - Index of the tile in 1d or 2d form
3882
3928
  * @param {Vector2|number} [size] - Size of tile in pixels
3883
- * @param {number} [textureIndex] - Texture index to use
3929
+ * @param {TextureInfo|number} [texture] - Texture index or info to use
3884
3930
  * @param {number} [padding] - How many pixels padding around tiles
3931
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3885
3932
  * @return {TileInfo}
3886
3933
  * @example
3887
3934
  * tile(2) // a tile at index 2 using the default tile size of 16
@@ -3889,36 +3936,43 @@ let drawCount;
3889
3936
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
3890
3937
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
3891
3938
  * @memberof Draw */
3892
- function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding)
3939
+ function tile(index=new Vector2, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3893
3940
  {
3894
- if (headlessMode)
3895
- return new TileInfo;
3941
+ ASSERT(isVector2(index) || typeof index === 'number', 'index must be a vec2 or number');
3942
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
3943
+ ASSERT(isNumber(texture) || texture instanceof TextureInfo, 'texture must be a number or TextureInfo');
3944
+ ASSERT(isNumber(padding), 'padding must be a number');
3945
+
3946
+ if (headlessMode) return new TileInfo;
3896
3947
 
3897
- // if size is a number, make it a vector
3898
3948
  if (typeof size === 'number')
3899
3949
  {
3950
+ // if size is a number, make it a vector
3900
3951
  ASSERT(size > 0);
3901
3952
  size = new Vector2(size, size);
3902
3953
  }
3903
3954
 
3904
3955
  // create tile info object
3905
- const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
3956
+ const textureInfo = typeof texture === 'number' ?
3957
+ textureInfos[texture] : texture;
3906
3958
 
3907
3959
  // get the position of the tile
3908
- const textureInfo = textureInfos[textureIndex];
3909
- ASSERT(!!textureInfo, 'Texture not loaded');
3910
3960
  const sizePaddedX = size.x + padding*2;
3911
3961
  const sizePaddedY = size.y + padding*2;
3912
- if (typeof pos === 'number')
3962
+ let x, y;
3963
+ if (typeof index === 'number')
3913
3964
  {
3914
3965
  const cols = textureInfo.size.x / sizePaddedX |0;
3915
- ASSERT(cols > 0, 'Tile size is too big for texture');
3916
- const posX = pos % cols, posY = (pos / cols) |0;
3917
- tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
3966
+ x = index % cols;
3967
+ y = index / cols |0;
3918
3968
  }
3919
3969
  else
3920
- tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
3921
- return tileInfo;
3970
+ {
3971
+ x = index.x;
3972
+ y = index.y;
3973
+ }
3974
+ const pos = new Vector2(x*sizePaddedX + padding, y*sizePaddedY + padding);
3975
+ return new TileInfo(pos, size, textureInfo, padding, bleed);
3922
3976
  }
3923
3977
 
3924
3978
  /**
@@ -3928,24 +3982,22 @@ function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=til
3928
3982
  class TileInfo
3929
3983
  {
3930
3984
  /** Create a tile info object
3931
- * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3985
+ * @param {Vector2} [pos=vec2()] - Top left corner of tile in pixels
3932
3986
  * @param {Vector2} [size] - Size of tile in pixels
3933
- * @param {number} [textureIndex] - Texture index to use
3934
- * @param {number} [padding] - How many pixels padding around tiles
3935
- * @param {number} [bleed] - How many pixels smaller to draw tiles
3987
+ * @param {TextureInfo} [textureInfo] - Texture info to use
3988
+ * @param {number} [padding] - How many pixels padding around tiles
3989
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3936
3990
  */
3937
- constructor(pos=vec2(), size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3991
+ constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
3938
3992
  {
3939
3993
  /** @property {Vector2} - Top left corner of tile in pixels */
3940
3994
  this.pos = pos.copy();
3941
3995
  /** @property {Vector2} - Size of tile in pixels */
3942
3996
  this.size = size.copy();
3943
- /** @property {number} - Texture index to use */
3944
- this.textureIndex = textureIndex;
3945
3997
  /** @property {number} - How many pixels padding around tiles */
3946
3998
  this.padding = padding;
3947
3999
  /** @property {TextureInfo} - The texture info for this tile */
3948
- this.textureInfo = textureInfos[this.textureIndex];
4000
+ this.textureInfo = textureInfo;
3949
4001
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
3950
4002
  this.bleed = bleed;
3951
4003
  }
@@ -3955,7 +4007,7 @@ class TileInfo
3955
4007
  * @return {TileInfo}
3956
4008
  */
3957
4009
  offset(offset)
3958
- { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleed); }
4010
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed); }
3959
4011
 
3960
4012
  /** Returns a copy of this tile offset by a number of animation frames
3961
4013
  * @param {number} frame - Offset to apply in animation frames
@@ -3964,23 +4016,33 @@ class TileInfo
3964
4016
  frame(frame)
3965
4017
  {
3966
4018
  ASSERT(typeof frame === 'number');
3967
- return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
4019
+ const w = this.size.x + this.padding*2;
4020
+ const x = frame*w;
4021
+ ASSERT(x < this.textureInfo.size.x, 'frame extends beyond texture width!');
4022
+ return this.offset(new Vector2(x));
3968
4023
  }
3969
4024
 
3970
4025
  /**
3971
4026
  * Set this tile to use a full image in a texture info
3972
- * @param {TextureInfo} textureInfo
4027
+ * @param {TextureInfo} [textureInfo]
3973
4028
  * @return {TileInfo}
3974
4029
  */
3975
- setFullImage(textureInfo)
4030
+ setFullImage(textureInfo=this.textureInfo)
3976
4031
  {
4032
+ this.textureInfo = textureInfo;
3977
4033
  this.pos = new Vector2;
3978
4034
  this.size = textureInfo.size.copy();
3979
- this.textureInfo = textureInfo;
3980
- // do not use padding or bleed
3981
4035
  this.bleed = this.padding = 0;
3982
4036
  return this;
3983
4037
  }
4038
+
4039
+ /**
4040
+ * Returns a tile info for an index using this tile as refrence
4041
+ * @param {Vector2|number} [index=0]
4042
+ * @return {TileInfo}
4043
+ */
4044
+ tile(index)
4045
+ { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
3984
4046
  }
3985
4047
 
3986
4048
  /**
@@ -4021,19 +4083,19 @@ class TextureInfo
4021
4083
  ///////////////////////////////////////////////////////////////////////////////
4022
4084
  // Drawing functions
4023
4085
 
4024
- /** Draw textured tile centered in world space, with color applied if using WebGL
4025
- * @param {Vector2} pos - Center of the tile in world space
4026
- * @param {Vector2} [size=(1,1)] - Size of the tile in world space
4027
- * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
4028
- * @param {Color} [color=(1,1,1,1)] - Color to modulate with
4029
- * @param {number} [angle] - Angle to rotate by
4030
- * @param {boolean} [mirror] - Is image flipped along the Y axis?
4031
- * @param {Color} [additiveColor] - Additive color to be applied if any
4086
+ /** Draw textured tile centered in world space
4087
+ * @param {Vector2} pos - Center of the tile in world space
4088
+ * @param {Vector2} [size=vec2(1)] - Size of the tile in world space
4089
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
4090
+ * @param {Color} [color=WHITE] - Color to modulate with
4091
+ * @param {number} [angle] - Angle to rotate by
4092
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
4093
+ * @param {Color} [additiveColor] - Additive color to be applied if any
4032
4094
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
4033
4095
  * @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
4034
4096
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
4035
4097
  * @memberof Draw */
4036
- function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
4098
+ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
4037
4099
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
4038
4100
  {
4039
4101
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4108,8 +4170,8 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
4108
4170
 
4109
4171
  /** Draw colored rect centered on pos
4110
4172
  * @param {Vector2} pos
4111
- * @param {Vector2} [size=(1,1)]
4112
- * @param {Color} [color=(1,1,1,1)]
4173
+ * @param {Vector2} [size=vec2(1)]
4174
+ * @param {Color} [color=WHITE]
4113
4175
  * @param {number} [angle]
4114
4176
  * @param {boolean} [useWebGL=glEnable]
4115
4177
  * @param {boolean} [screenSpace]
@@ -4122,9 +4184,9 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
4122
4184
 
4123
4185
  /** Draw a rect centered on pos with a gradient from top to bottom
4124
4186
  * @param {Vector2} pos
4125
- * @param {Vector2} [size=(1,1)]
4126
- * @param {Color} [colorTop=(1,1,1,1)]
4127
- * @param {Color} [colorBottom=(0,0,0,1)]
4187
+ * @param {Vector2} [size=vec2(1)]
4188
+ * @param {Color} [colorTop=WHITE]
4189
+ * @param {Color} [colorBottom=BLACK]
4128
4190
  * @param {number} [angle]
4129
4191
  * @param {boolean} [useWebGL=glEnable]
4130
4192
  * @param {boolean} [screenSpace]
@@ -4186,9 +4248,9 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
4186
4248
  /** Draw connected lines between a series of points
4187
4249
  * @param {Array<Vector2>} points
4188
4250
  * @param {number} [width]
4189
- * @param {Color} [color=(1,1,1,1)]
4251
+ * @param {Color} [color=WHITE]
4190
4252
  * @param {boolean} [wrap] - Should the last point connect to the first?
4191
- * @param {Vector2} [pos=(0,0)] - Offset to apply
4253
+ * @param {Vector2} [pos=vec2()] - Offset to apply
4192
4254
  * @param {number} [angle] - Angle to rotate by
4193
4255
  * @param {boolean} [useWebGL=glEnable]
4194
4256
  * @param {boolean} [screenSpace]
@@ -4223,13 +4285,9 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
4223
4285
  for (let i=0; i<points.length; ++i)
4224
4286
  {
4225
4287
  const point = points[i];
4226
- if (i)
4227
- context.lineTo(point.x, point.y);
4228
- else
4229
- context.moveTo(point.x, point.y);
4288
+ context.lineTo(point.x, point.y);
4230
4289
  }
4231
- if (wrap)
4232
- context.closePath();
4290
+ wrap && context.closePath();
4233
4291
  context.stroke();
4234
4292
  }, screenSpace, context);
4235
4293
  }
@@ -4239,8 +4297,8 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
4239
4297
  * @param {Vector2} posA
4240
4298
  * @param {Vector2} posB
4241
4299
  * @param {number} [width]
4242
- * @param {Color} [color=(1,1,1,1)]
4243
- * @param {Vector2} [pos=(0,0)] - Offset to apply
4300
+ * @param {Color} [color=WHITE]
4301
+ * @param {Vector2} [pos=vec2()] - Offset to apply
4244
4302
  * @param {number} [angle] - Angle to rotate by
4245
4303
  * @param {boolean} [useWebGL=glEnable]
4246
4304
  * @param {boolean} [screenSpace]
@@ -4259,12 +4317,12 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
4259
4317
 
4260
4318
  /** Draw colored regular polygon using passed in number of sides
4261
4319
  * @param {Vector2} pos
4262
- * @param {Vector2} [size=(1,1)]
4320
+ * @param {Vector2} [size=vec2(1)]
4263
4321
  * @param {number} [sides]
4264
- * @param {Color} [color=(1,1,1,1)]
4322
+ * @param {Color} [color=WHITE]
4265
4323
  * @param {number} [angle]
4266
4324
  * @param {number} [lineWidth]
4267
- * @param {Color} [lineColor=(0,0,0,1)]
4325
+ * @param {Color} [lineColor=BLACK]
4268
4326
  * @param {boolean} [useWebGL=glEnable]
4269
4327
  * @param {boolean} [screenSpace]
4270
4328
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -4287,10 +4345,10 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
4287
4345
 
4288
4346
  /** Draw colored polygon using passed in points
4289
4347
  * @param {Array<Vector2>} points - Array of Vector2 points
4290
- * @param {Color} [color=(1,1,1,1)]
4348
+ * @param {Color} [color=WHITE]
4291
4349
  * @param {number} [lineWidth]
4292
- * @param {Color} [lineColor=(0,0,0,1)]
4293
- * @param {Vector2} [pos=(0,0)] - Offset to apply
4350
+ * @param {Color} [lineColor=BLACK]
4351
+ * @param {Vector2} [pos=vec2()] - Offset to apply
4294
4352
  * @param {number} [angle] - Angle to rotate by
4295
4353
  * @param {boolean} [useWebGL=glEnable]
4296
4354
  * @param {boolean} [screenSpace]
@@ -4337,11 +4395,11 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
4337
4395
 
4338
4396
  /** Draw colored ellipse using passed in point
4339
4397
  * @param {Vector2} pos
4340
- * @param {Vector2} [size=(1,1)] - Width and height diameter
4341
- * @param {Color} [color=(1,1,1,1)]
4398
+ * @param {Vector2} [size=vec2(1)] - Width and height diameter
4399
+ * @param {Color} [color=WHITE]
4342
4400
  * @param {number} [angle]
4343
4401
  * @param {number} [lineWidth]
4344
- * @param {Color} [lineColor=(0,0,0,1)]
4402
+ * @param {Color} [lineColor=BLACK]
4345
4403
  * @param {boolean} [useWebGL=glEnable]
4346
4404
  * @param {boolean} [screenSpace]
4347
4405
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -4383,9 +4441,9 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
4383
4441
  /** Draw colored circle using passed in point
4384
4442
  * @param {Vector2} pos
4385
4443
  * @param {number} [size=1] - Diameter
4386
- * @param {Color} [color=(1,1,1,1)]
4444
+ * @param {Color} [color=WHITE]
4387
4445
  * @param {number} [lineWidth=0]
4388
- * @param {Color} [lineColor=(0,0,0,1)]
4446
+ * @param {Color} [lineColor=BLACK]
4389
4447
  * @param {boolean} [useWebGL=glEnable]
4390
4448
  * @param {boolean} [screenSpace]
4391
4449
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
@@ -4419,7 +4477,11 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
4419
4477
  ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
4420
4478
 
4421
4479
  if (!screenSpace)
4422
- [pos, size, angle] = worldToScreenTransform(pos, size, angle);
4480
+ {
4481
+ pos = worldToScreen(pos);
4482
+ size = size.scale(cameraScale);
4483
+ angle -= cameraAngle;
4484
+ }
4423
4485
  context.save();
4424
4486
  context.translate(pos.x+.5, pos.y+.5);
4425
4487
  context.rotate(angle);
@@ -4436,9 +4498,9 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
4436
4498
  * @param {string|number} text
4437
4499
  * @param {Vector2} pos
4438
4500
  * @param {number} [size]
4439
- * @param {Color} [color=(1,1,1,1)]
4501
+ * @param {Color} [color=WHITE]
4440
4502
  * @param {number} [lineWidth]
4441
- * @param {Color} [lineColor=(0,0,0,1)]
4503
+ * @param {Color} [lineColor=BLACK]
4442
4504
  * @param {CanvasTextAlign} [textAlign='center']
4443
4505
  * @param {string} [font=fontDefault]
4444
4506
  * @param {string} [fontStyle]
@@ -4462,18 +4524,18 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
4462
4524
  * Automatically splits new lines into rows
4463
4525
  * @param {string|number} text
4464
4526
  * @param {Vector2} pos
4465
- * @param {number} [size]
4466
- * @param {Color} [color=(1,1,1,1)]
4527
+ * @param {number} size
4528
+ * @param {Color} [color=WHITE]
4467
4529
  * @param {number} [lineWidth]
4468
- * @param {Color} [lineColor=(0,0,0,1)]
4530
+ * @param {Color} [lineColor=BLACK]
4469
4531
  * @param {CanvasTextAlign} [textAlign]
4470
4532
  * @param {string} [font=fontDefault]
4471
4533
  * @param {string} [fontStyle]
4472
4534
  * @param {number} [maxWidth]
4473
4535
  * @param {number} [angle]
4474
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
4536
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4475
4537
  * @memberof Draw */
4476
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=mainContext)
4538
+ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
4477
4539
  {
4478
4540
  ASSERT(isString(text), 'text must be a string');
4479
4541
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4597,7 +4659,7 @@ function worldToScreenDelta(worldDelta)
4597
4659
 
4598
4660
  /** Convert screen space transform to world space
4599
4661
  * @param {Vector2} screenPos
4600
- * @param {Vector2} screenSize
4662
+ * @param {Vector2} screenSize
4601
4663
  * @param {number} [screenAngle]
4602
4664
  * @return {[Vector2, Vector2, number]} - [pos, size, angle]
4603
4665
  * @memberof Draw */
@@ -4614,25 +4676,6 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
4614
4676
  ];
4615
4677
  }
4616
4678
 
4617
- /** Convert world space transform to screen space
4618
- * @param {Vector2} worldPos
4619
- * @param {Vector2} worldSize
4620
- * @param {number} [worldAngle]
4621
- * @return {[Vector2, Vector2, number]} - [pos, size, angle]
4622
- * @memberof Draw */
4623
- function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
4624
- {
4625
- ASSERT(isVector2(worldPos), 'worldPos must be a vec2');
4626
- ASSERT(isVector2(worldSize), 'worldSize must be a vec2');
4627
- ASSERT(isNumber(worldAngle), 'worldAngle must be a number');
4628
-
4629
- return [
4630
- worldToScreen(worldPos),
4631
- worldSize.scale(cameraScale),
4632
- worldAngle - cameraAngle
4633
- ];
4634
- }
4635
-
4636
4679
  /** Get the size of the camera window in world space
4637
4680
  * @return {Vector2}
4638
4681
  * @memberof Draw */
@@ -4677,10 +4720,9 @@ function isOnScreen(pos, size=0)
4677
4720
  * @param {boolean} [additive]
4678
4721
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4679
4722
  * @memberof Draw */
4680
- function setBlendMode(additive=false, context)
4723
+ function setBlendMode(additive=false, context=drawContext)
4681
4724
  {
4682
4725
  glAdditive = additive;
4683
- context ||= drawContext;
4684
4726
  context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
4685
4727
  }
4686
4728
 
@@ -4695,7 +4737,6 @@ function combineCanvases()
4695
4737
  workContext.fillRect(0,0,w,h); // remove background alpha
4696
4738
  glCopyToContext(workContext);
4697
4739
  workContext.drawImage(mainCanvas, 0, 0);
4698
- mainCanvas.width |= 0;
4699
4740
  mainContext.drawImage(workCanvas, 0, 0);
4700
4741
  }
4701
4742
 
@@ -4800,17 +4841,22 @@ function setCursor(cursorStyle = 'auto')
4800
4841
 
4801
4842
  ///////////////////////////////////////////////////////////////////////////////
4802
4843
 
4844
+ /** Engine font image, 8x8 font provided by the engine
4845
+ * @type {FontImage}
4846
+ * @memberof Draw */
4803
4847
  let engineFontImage;
4804
4848
 
4805
4849
  /**
4806
- * Font Image Object - Draw text on a 2D canvas by using characters in an image
4850
+ * Font Image Object - Draw text by using tiles in an image
4807
4851
  * - 96 characters (from space to tilde) are stored in an image
4808
- * - Uses a default 8x8 font if none is supplied
4809
- * - You can also use fonts from the main tile sheet
4852
+ * - A 8x8 default engine font is supplied for general use
4853
+ * - This system is WebGL enabled for fast text rendering
4854
+ * - Fonts can also be colored and scaled along each axis
4855
+ *
4810
4856
  * @memberof Draw
4811
4857
  * @example
4812
4858
  * // use built in font
4813
- * const font = new FontImage;
4859
+ * const font = engineFontImage;
4814
4860
  *
4815
4861
  * // draw text
4816
4862
  * font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
@@ -4818,70 +4864,112 @@ let engineFontImage;
4818
4864
  class FontImage
4819
4865
  {
4820
4866
  /** Create an image font
4821
- * @param {HTMLImageElement} [image] - Image for the font, default if undefined
4822
- * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
4823
- * @param {Vector2} [paddingSize=(0,1)] - How much space between characters
4867
+ * @param {TileInfo} tileInfo - Tile info of first characeter in font
4824
4868
  */
4825
- constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
4869
+ constructor(tileInfo)
4826
4870
  {
4827
- // load default font image
4828
- if (!image && !engineFontImage)
4829
- {
4830
- engineFontImage = new Image;
4831
- engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
4832
- }
4833
-
4834
- this.image = image || engineFontImage;
4835
- this.tileSize = tileSize;
4836
- this.paddingSize = paddingSize;
4871
+ ASSERT(!!tileInfo, 'tileInfo is required for FontImage');
4872
+
4873
+ /** @property {TileInfo} - Tile info for the font */
4874
+ this.tileInfo = tileInfo.frame(0);
4837
4875
  }
4838
4876
 
4839
4877
  /** Draw text in world space using the image font
4840
- * @param {string|number} text
4878
+ * @param {string|number} text
4841
4879
  * @param {Vector2} pos
4842
- * @param {number} [scale=.25]
4843
- * @param {boolean} [center]
4844
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4880
+ * @param {Vector2|number} [size]
4881
+ * @param {boolean} [center=true]
4882
+ * @param {Color} [color=WHITE]
4883
+ * @param {boolean} [useWebGL=glEnable]
4884
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4845
4885
  */
4846
- drawText(text, pos, scale=1, center, context=drawContext)
4886
+ drawText(text, pos, size=1, center, color, useWebGL, context)
4847
4887
  {
4848
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
4888
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
4889
+
4890
+ if (typeof size === 'number')
4891
+ {
4892
+ // if size is a number, make it a vector
4893
+ ASSERT(size > 0);
4894
+ size *= cameraScale;
4895
+ size = new Vector2(size, size);
4896
+ }
4897
+ else
4898
+ size = size.scale(cameraScale);
4899
+ this.drawTextScreen(text, worldToScreen(pos), size, center, color, useWebGL, context);
4849
4900
  }
4850
4901
 
4851
4902
  /** Draw text in screen space using the image font
4852
- * @param {string|number} text
4903
+ * @param {string|number} text
4853
4904
  * @param {Vector2} pos
4854
- * @param {number} [scale]
4905
+ * @param {Vector2|number} size
4855
4906
  * @param {boolean} [center]
4856
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4907
+ * @param {Color} [color=WHITE]
4908
+ * @param {boolean} [useWebGL=glEnable]
4909
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4857
4910
  */
4858
- drawTextScreen(text, pos, scale=4, center=true, context=mainContext)
4911
+ drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
4859
4912
  {
4860
- context.save();
4861
- const size = this.tileSize;
4862
- const drawSize = size.add(this.paddingSize).scale(scale);
4863
- const cols = this.image.width / this.tileSize.x |0;
4864
- (text+'').split('\n').forEach((line, i)=>
4913
+ ASSERT(isString(text), 'text must be a string');
4914
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4915
+ ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
4916
+ ASSERT(isColor(color), 'color must be a color');
4917
+
4918
+ // if size is a number, make it a vector
4919
+ size = typeof size === 'number' ? new Vector2(size, size) : size;
4920
+
4921
+ // precache objects for drawing
4922
+ const drawPos = new Vector2;
4923
+ const tileInfo = this.tileInfo;
4924
+ const padding = tileInfo.padding;
4925
+ const sizePaddedX = tileInfo.size.x + padding*2;
4926
+ const sizePaddedY = tileInfo.size.y + padding*2;
4927
+ const cols = tileInfo.textureInfo.size.x / sizePaddedX |0;
4928
+
4929
+ // draw each line of text
4930
+ (text+'').split('\n').forEach((line, j)=>
4865
4931
  {
4866
- const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
4867
- for (let j=line.length; j--;)
4932
+ const centerOffset = center ? (line.length-1) * size.x / 2 : 0;
4933
+ for (let i=line.length; i--;)
4868
4934
  {
4869
- // draw each character
4870
- let charCode = line[j].charCodeAt(0);
4871
- if (charCode < 32 || charCode > 127)
4872
- charCode = 127; // unknown character
4873
-
4874
- // get the character source location and draw it
4875
- const tile = charCode - 32;
4876
- const x = tile % cols;
4877
- const y = tile / cols |0;
4878
- const drawPos = pos.add(vec2(j,i).multiply(drawSize));
4879
- context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
4880
- drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
4935
+ // get the character index
4936
+ const charCode = line.charCodeAt(i);
4937
+ const index = charCode < 32 || charCode > 127 ?
4938
+ 95 : charCode - 32; // handle out of range characters
4939
+
4940
+ // get the position of the tile
4941
+ const x = index % cols;
4942
+ const y = index / cols |0;
4943
+ tileInfo.pos.x = x*sizePaddedX + padding;
4944
+ tileInfo.pos.y = y*sizePaddedY + padding;
4945
+
4946
+ // draw the tile
4947
+ drawPos.x = pos.x + i * size.x - centerOffset |0;
4948
+ drawPos.y = pos.y + j * size.y |0;
4949
+ drawTile(drawPos, size, tileInfo, color, 0, false, undefined, useWebGL, true, context);
4881
4950
  }
4882
4951
  });
4883
- context.restore();
4884
4952
  }
4953
+ }
4954
+
4955
+ // load engine font, called automatically on startup
4956
+ function fontImageInit()
4957
+ {
4958
+ return new Promise(resolve =>
4959
+ {
4960
+ // create the engine font
4961
+ const image = new Image;
4962
+ image.onerror = image.onload = ()=>
4963
+ {
4964
+ const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
4965
+ const textureInfo = new TextureInfo(image);
4966
+ const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
4967
+ engineFontImage = new FontImage(tileInfo);
4968
+ resolve();
4969
+ }
4970
+ image.crossOrigin = 'anonymous';
4971
+ 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==';
4972
+ });
4885
4973
  }
4886
4974
  /**
4887
4975
  * LittleJS Input System
@@ -4938,6 +5026,10 @@ let inputPreventDefault = true;
4938
5026
  * @memberof Input */
4939
5027
  let gamepadPrimary = 0;
4940
5028
 
5029
+ /** True if a touch device has been detected
5030
+ * @memberof Input */
5031
+ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
5032
+
4941
5033
  /** Prevents input continuing to the default browser handling
4942
5034
  * This is useful to disable for html menus so the browser can handle input normally
4943
5035
  * @param {boolean} preventDefault
@@ -5136,10 +5228,6 @@ function gamepadStickCount(gamepad=gamepadPrimary)
5136
5228
  return gamepadStickData[gamepad]?.length ?? 0;
5137
5229
  }
5138
5230
 
5139
- /** True if a touch device has been detected
5140
- * @memberof Input */
5141
- const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
5142
-
5143
5231
  ///////////////////////////////////////////////////////////////////////////////
5144
5232
 
5145
5233
  /** Pulse the vibration hardware if it exists
@@ -5148,7 +5236,7 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
5148
5236
  function vibrate(pattern=100)
5149
5237
  {
5150
5238
  ASSERT(isNumber(pattern) || isArray(pattern), 'pattern must be a number or array');
5151
- vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern);
5239
+ vibrateEnable && !headlessMode && navigator?.vibrate?.(pattern);
5152
5240
  }
5153
5241
 
5154
5242
  /** Cancel any ongoing vibration
@@ -5248,8 +5336,7 @@ function inputInit()
5248
5336
  }
5249
5337
  function onMouseDown(e)
5250
5338
  {
5251
- if (isTouchDevice && touchInputEnable)
5252
- return;
5339
+ if (isTouchDevice && touchInputEnable) return;
5253
5340
 
5254
5341
  // fix stalled audio requiring user interaction
5255
5342
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
@@ -5267,8 +5354,8 @@ function inputInit()
5267
5354
  }
5268
5355
  function onMouseUp(e)
5269
5356
  {
5270
- if (isTouchDevice && touchInputEnable)
5271
- return;
5357
+ if (isTouchDevice && touchInputEnable) return;
5358
+
5272
5359
  inputData[0][e.button] = (inputData[0][e.button]&2) | 4;
5273
5360
  }
5274
5361
  function onMouseMove(e)
@@ -5300,8 +5387,7 @@ function inputInit()
5300
5387
  let wasTouching;
5301
5388
  function handleTouch(e)
5302
5389
  {
5303
- if (!touchInputEnable)
5304
- return;
5390
+ if (!touchInputEnable) return;
5305
5391
 
5306
5392
  // route touch to gamepad
5307
5393
  if (touchGamepadEnable)
@@ -5365,8 +5451,7 @@ function inputInit()
5365
5451
  }
5366
5452
 
5367
5453
  // don't process touch gamepad if paused
5368
- if (paused)
5369
- return;
5454
+ if (paused) return;
5370
5455
 
5371
5456
  // get center of left and right sides
5372
5457
  const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
@@ -5456,8 +5541,7 @@ function inputUpdate()
5456
5541
  // update touch gamepad if enabled
5457
5542
  if (touchGamepadEnable && isTouchDevice)
5458
5543
  {
5459
- if (!touchGamepadTimer.isSet())
5460
- return;
5544
+ if (!touchGamepadTimer.isSet()) return;
5461
5545
 
5462
5546
  // read virtual analog stick
5463
5547
  gamepadPrimary = 0; // touch gamepad uses index 0
@@ -5495,12 +5579,10 @@ function inputUpdate()
5495
5579
  }
5496
5580
 
5497
5581
  // return if gamepads are disabled or not supported
5498
- if (!gamepadsEnable || !navigator || !navigator.getGamepads)
5499
- return;
5582
+ if (!gamepadsEnable || !navigator || !navigator.getGamepads) return;
5500
5583
 
5501
5584
  // only poll gamepads when focused or in debug mode
5502
- if (!debug && !document.hasFocus())
5503
- return;
5585
+ if (!debug && !document.hasFocus()) return;
5504
5586
 
5505
5587
  // poll gamepads
5506
5588
  const maxGamepads = 8;
@@ -5537,8 +5619,7 @@ function inputUpdate()
5537
5619
  data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
5538
5620
 
5539
5621
  // check for any input on this gamepad, analog must be full press
5540
- if (button.pressed)
5541
- if (!button.value || button.value > .9)
5622
+ if (button.pressed && (!button.value || button.value > .9))
5542
5623
  hadInput = true;
5543
5624
  }
5544
5625
 
@@ -5567,7 +5648,7 @@ function inputUpdate()
5567
5648
  }
5568
5649
 
5569
5650
  // copy dpad to left analog stick when pressed
5570
- if (gamepadDirectionEmulateStick && !dpad.isZero())
5651
+ if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
5571
5652
  sticks[0] = dpad.clampLength();
5572
5653
  }
5573
5654
 
@@ -5596,13 +5677,11 @@ function inputRender()
5596
5677
  function touchGamepadRender()
5597
5678
  {
5598
5679
  if (!touchInputEnable || !isTouchDevice || headlessMode) return;
5599
- if (!touchGamepadEnable || !touchGamepadTimer.isSet())
5600
- return;
5680
+ if (!touchGamepadEnable || !touchGamepadTimer.isSet()) return;
5601
5681
 
5602
5682
  // fade off when not touching or paused
5603
5683
  const alpha = percent(touchGamepadTimer.get(), 4, 3);
5604
- if (!alpha || paused)
5605
- return;
5684
+ if (!alpha || paused) return;
5606
5685
 
5607
5686
  // setup the canvas
5608
5687
  const context = mainContext;
@@ -5755,8 +5834,7 @@ class Sound
5755
5834
  {
5756
5835
  // remove randomness so it can be applied on playback
5757
5836
  const randomnessIndex = 1, defaultRandomness = .05;
5758
- this.randomness = zzfxSound[randomnessIndex] !== undefined ?
5759
- zzfxSound[randomnessIndex] : defaultRandomness;
5837
+ this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
5760
5838
  zzfxSound[randomnessIndex] = 0;
5761
5839
 
5762
5840
  // generate the zzfx samples
@@ -5836,7 +5914,7 @@ class Sound
5836
5914
  * @return {number} - How long the sound is in seconds (undefined if loading)
5837
5915
  */
5838
5916
  getDuration()
5839
- { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
5917
+ { return this.sampleChannels?.[0].length / this.sampleRate || 0; }
5840
5918
 
5841
5919
  /** Check if sound is loaded, for sounds fetched from a url
5842
5920
  * @return {boolean} - True if sound is loaded and ready to play
@@ -5928,8 +6006,7 @@ class SoundWave extends Sound
5928
6006
  this.sampleRate = audioBuffer.sampleRate;
5929
6007
  this.sampleChannels = sampleChannels;
5930
6008
  this.loadedPercent = 1;
5931
- if (this.onloadCallback)
5932
- this.onloadCallback(this);
6009
+ this.onloadCallback?.(this);
5933
6010
  }
5934
6011
  }
5935
6012
 
@@ -6052,8 +6129,7 @@ class SoundInstance
6052
6129
  /** Pause this sound instance */
6053
6130
  pause()
6054
6131
  {
6055
- if (this.isPaused())
6056
- return;
6132
+ if (this.isPaused()) return;
6057
6133
 
6058
6134
  // save current time and stop sound
6059
6135
  this.pausedTime = this.getCurrentTime();
@@ -6065,8 +6141,7 @@ class SoundInstance
6065
6141
  /** Unpauses this sound instance */
6066
6142
  resume()
6067
6143
  {
6068
- if (!this.isPaused())
6069
- return;
6144
+ if (!this.isPaused()) return;
6070
6145
 
6071
6146
  // restart sound from paused time
6072
6147
  this.start(this.pausedTime);
@@ -6134,7 +6209,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
6134
6209
 
6135
6210
  /** Stop all queued speech
6136
6211
  * @memberof Audio */
6137
- function speakStop() {speechSynthesis && speechSynthesis.cancel();}
6212
+ function speakStop() {speechSynthesis?.cancel();}
6138
6213
 
6139
6214
  /** Get frequency of a note on a musical scale
6140
6215
  * @param {number} semitoneOffset - How many semitones away from the root note
@@ -6393,7 +6468,7 @@ function tileCollisionGetData(pos)
6393
6468
 
6394
6469
  /** Check if a tile layer collides with another object
6395
6470
  * @param {Vector2} pos
6396
- * @param {Vector2} [size=(0,0)]
6471
+ * @param {Vector2} [size=vec2()]
6397
6472
  * @param {EngineObject} [object] - An object or undefined for generic test
6398
6473
  * @param {boolean} [solidOnly] - Only check solid layers if true
6399
6474
  * @return {TileCollisionLayer}
@@ -6511,20 +6586,20 @@ function tileLayersLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLa
6511
6586
  class TileLayerData
6512
6587
  {
6513
6588
  /** Create a tile layer data object, one for each tile in a TileLayer
6514
- * @param {number} [tile] - The tile to use, untextured if undefined
6589
+ * @param {number} [tile] - The tile to use, untextured if undefined
6515
6590
  * @param {number} [direction] - Integer direction of tile, in 90 degree increments
6516
- * @param {boolean} [mirror] - If the tile should be mirrored along the x axis
6517
- * @param {Color} [color] - Color of the tile */
6591
+ * @param {boolean} [mirror] - If the tile should be mirrored along the x axis
6592
+ * @param {Color} [color] - Color of the tile */
6518
6593
  constructor(tile, direction=0, mirror=false, color=new Color)
6519
6594
  {
6520
- /** @property {number} - The tile to use, untextured if undefined */
6521
- this.tile = tile;
6522
- /** @property {number} - Integer direction of tile, in 90 degree increments */
6595
+ /** @property {number} - The tile to use, untextured if undefined */
6596
+ this.tile = tile;
6597
+ /** @property {number} - Integer direction of tile, in 90 degree increments */
6523
6598
  this.direction = direction;
6524
6599
  /** @property {boolean} - If the tile should be mirrored along the x axis */
6525
- this.mirror = mirror;
6526
- /** @property {Color} - Color of the tile */
6527
- this.color = color.copy();
6600
+ this.mirror = mirror;
6601
+ /** @property {Color} - Color of the tile */
6602
+ this.color = color.copy();
6528
6603
  }
6529
6604
 
6530
6605
  /** Set this tile to clear, it will not be rendered */
@@ -6535,7 +6610,7 @@ class TileLayerData
6535
6610
  /**
6536
6611
  * Canvas Layer - cached off screen rendering system
6537
6612
  * - Contains an offscreen canvas that can be rendered to
6538
- * - WebGL rendering is optional, call useWebGL to enable
6613
+ * - WebGL rendering is optional, call updateWebGL to enable/update
6539
6614
  * @extends EngineObject
6540
6615
  * @memberof TileLayers
6541
6616
  * @example
@@ -6549,8 +6624,9 @@ class CanvasLayer extends EngineObject
6549
6624
  * @param {number} [angle] - Angle the layer is rotated by
6550
6625
  * @param {number} [renderOrder] - Objects sorted by renderOrder
6551
6626
  * @param {Vector2} [canvasSize] - Default size of canvas, can be changed later
6627
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6552
6628
  */
6553
- constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512))
6629
+ constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=glEnable)
6554
6630
  {
6555
6631
  ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
6556
6632
  super(position, size, undefined, angle, WHITE, renderOrder);
@@ -6560,13 +6636,10 @@ class CanvasLayer extends EngineObject
6560
6636
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
6561
6637
  this.context = this.canvas?.getContext('2d');
6562
6638
  /** @property {TextureInfo} - Texture info to use for this object rendering */
6563
- const useWebGL = false; // do not use webgl by default
6564
6639
  this.textureInfo = new TextureInfo(this.canvas, useWebGL);
6565
- /** @property {boolean} - True if WebGL texture needs to be refreshed */
6566
- this.refreshWebGL = false;
6567
6640
 
6568
6641
  // disable physics by default
6569
- this.mass = this.gravityScale = this.friction = this.restitution = 0;
6642
+ this.mass = 0;
6570
6643
  }
6571
6644
 
6572
6645
  /** Destroy this canvas layer */
@@ -6581,7 +6654,7 @@ class CanvasLayer extends EngineObject
6581
6654
  // Render the layer, called automatically by the engine
6582
6655
  render()
6583
6656
  {
6584
- this.draw(this.pos, this.size, this.angle, this.color, this.mirror, this.additiveColor);
6657
+ this.draw(this.pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
6585
6658
  }
6586
6659
 
6587
6660
  /** Draw this canvas layer centered in world space, with color applied if using WebGL
@@ -6594,103 +6667,54 @@ class CanvasLayer extends EngineObject
6594
6667
  * @param {boolean} [screenSpace] - If true the pos and size are in screen space
6595
6668
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
6596
6669
  * @memberof Draw */
6597
- draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
6670
+ draw(pos, size, color=WHITE, angle=0, mirror=false, additiveColor, screenSpace=false, context)
6598
6671
  {
6599
- const useWebGL = glEnable && this.textureInfo.hasWebGL();
6600
- if (useWebGL && this.refreshWebGL)
6601
- {
6602
- // update the WebGL texture
6603
- this.textureInfo.createWebGLTexture();
6604
- this.refreshWebGL = false;
6605
- }
6606
-
6607
6672
  // draw the canvas layer as a single tile that uses the whole texture
6608
6673
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
6674
+ const useWebGL = this.hasWebGL();
6609
6675
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
6610
6676
  }
6611
6677
 
6612
- /**
6613
- * @callback Canvas2DDrawCallback - Function that draws to a canvas 2D context
6614
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
6615
- * @memberof TileLayers
6616
- */
6617
-
6618
- /** Draw onto the layer canvas in world space (bypass WebGL)
6678
+ /** Draw a tile onto the layer canvas in world space
6619
6679
  * @param {Vector2} pos
6620
- * @param {Vector2} size
6621
- * @param {number} angle
6622
- * @param {boolean} mirror
6623
- * @param {Canvas2DDrawCallback} drawFunction */
6624
- drawCanvas2D(pos, size, angle, mirror, drawFunction)
6680
+ * @param {Vector2} [size=vec2(1)]
6681
+ * @param {TileInfo} [tileInfo]
6682
+ * @param {Color} [color=WHITE]
6683
+ * @param {number} [angle]
6684
+ * @param {boolean} [mirror] */
6685
+ drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
6625
6686
  {
6626
- if (!this.context) return;
6627
-
6628
- const context = this.context;
6629
- context.save();
6630
6687
  pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
6631
6688
  size = size.multiply(this.tileInfo.size);
6632
- context.translate(pos.x, this.canvas.height - pos.y);
6633
- context.rotate(angle);
6634
- context.scale(mirror ? -size.x : size.x, size.y);
6635
- drawFunction(context);
6636
- context.restore();
6637
- }
6689
+ pos.y = this.canvas.height - pos.y;
6638
6690
 
6639
- /** Draw a tile onto the layer canvas in world space
6640
- * @param {Vector2} pos
6641
- * @param {Vector2} [size=(1,1)]
6642
- * @param {TileInfo} [tileInfo]
6643
- * @param {Color} [color=(1,1,1,1)]
6644
- * @param {number} [angle=0]
6645
- * @param {boolean} [mirror=false] */
6646
- drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle, mirror)
6647
- {
6648
- this.drawCanvas2D(pos, size, angle, mirror, (context)=>
6649
- {
6650
- const textureInfo = tileInfo && tileInfo.textureInfo;
6651
- if (textureInfo)
6652
- {
6653
- context.globalAlpha = color.a; // only alpha is supported
6654
- context.drawImage(textureInfo.image,
6655
- tileInfo.pos.x, tileInfo.pos.y,
6656
- tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
6657
- context.globalAlpha = 1;
6658
- }
6659
- else
6660
- {
6661
- // untextured
6662
- context.fillStyle = color.toString();
6663
- context.fillRect(-.5, -.5, 1, 1);
6664
- }
6665
- });
6691
+ // draw the tile onto the layer canvas
6692
+ const oldMainCanvasSize = mainCanvasSize;
6693
+ mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
6694
+ const useWebGL = this.hasWebGL();
6695
+ useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
6696
+ const drawContext = useWebGL ? undefined : this.context;
6697
+ drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
6698
+ useWebGL && glSetRenderTarget();
6699
+ mainCanvasSize = oldMainCanvasSize;
6666
6700
  }
6667
6701
 
6668
6702
  /** Draw a rectangle onto the layer canvas in world space
6669
6703
  * @param {Vector2} pos
6670
- * @param {Vector2} [size=(1,1)]
6671
- * @param {Color} [color=(1,1,1,1)]
6672
- * @param {number} [angle=0] */
6704
+ * @param {Vector2} [size=vec2(1)]
6705
+ * @param {Color} [color=WHITE]
6706
+ * @param {number} [angle] */
6673
6707
  drawRect(pos, size, color, angle)
6674
6708
  { this.drawTile(pos, size, undefined, color, angle); }
6675
6709
 
6676
- /** Create or update the WebGL texture for this layer
6677
- * @param {boolean} [enable] - enable WebGL rendering and update the texture
6678
- * @param {boolean} [immediate] - shoulkd the texture be updated immediately
6679
- */
6680
- useWebGL(enable=true, immediate=false)
6681
- {
6682
- if (!immediate && enable && this.textureInfo.hasWebGL())
6683
- {
6684
- // refresh the texture when needed
6685
- this.refreshWebGL = true;
6686
- return;
6687
- }
6710
+ /** Create WebGL texture if necessary and copy layer canvas to it */
6711
+ updateWebGL()
6712
+ { this.textureInfo.createWebGLTexture(); }
6688
6713
 
6689
- if (enable)
6690
- this.textureInfo.createWebGLTexture();
6691
- else
6692
- this.textureInfo.destroyWebGLTexture();
6693
- }
6714
+ /** Check if this layer is using WebGL
6715
+ * @return {boolean} */
6716
+ hasWebGL()
6717
+ { return glEnable && this.textureInfo.hasWebGL(); }
6694
6718
  }
6695
6719
 
6696
6720
  ///////////////////////////////////////////////////////////////////////////////
@@ -6709,50 +6733,67 @@ class CanvasLayer extends EngineObject
6709
6733
  class TileLayer extends CanvasLayer
6710
6734
  {
6711
6735
  /** Create a tile layer object
6712
- * @param {Vector2} position - World space position
6713
- * @param {Vector2} size - World space size
6714
- * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6736
+ * @param {Vector2} position - World space position
6737
+ * @param {Vector2} size - World space size
6738
+ * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6715
6739
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6716
6740
  */
6717
6741
  constructor(position, size, tileInfo=tile(), renderOrder=0)
6718
6742
  {
6719
6743
  const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
6720
- super(position, size, 0, renderOrder, canvasSize);
6721
-
6722
- // set tile info
6723
- this.tileInfo = tileInfo;
6724
-
6725
- // init tile data
6744
+ const useWebGL = true;
6745
+ super(position, size, 0, renderOrder, canvasSize, useWebGL);
6746
+
6747
+ /** @property {TileInfo} - Default tile info for layer */
6748
+ this.tileInfo = undefined;
6749
+ /** @property {Array<TileLayerData>} - Default tile info for layer */
6726
6750
  this.data = [];
6727
- for (let j = this.size.area(); j--;)
6728
- this.data.push(new TileLayerData);
6751
+ /** @property {boolean} - Is this layer using a webgl texture? */
6752
+ this.isUsingWebGL = false;
6729
6753
 
6730
6754
  if (headlessMode)
6731
6755
  {
6732
- // disable rendering
6733
- this.redraw = () => {};
6734
- this.render = () => {};
6735
- this.redrawStart = () => {};
6736
- this.redrawEnd = () => {};
6737
- this.drawTileData = () => {};
6738
- this.drawCanvas2D = () => {};
6739
- this.useWebGL = () => {};
6756
+ // disable rendering in headless mode
6757
+ this.render = () => {};
6758
+ this.redraw = () => {};
6759
+ this.redrawStart = () => {};
6760
+ this.redrawEnd = () => {};
6761
+ this.drawTileData = () => {};
6762
+ this.redrawTileData = () => {};
6763
+ this.drawLayerTile = () => {};
6764
+ this.drawLayerRect = () => {};
6765
+ this.clearLayerRect = () => {};
6766
+ return;
6767
+ }
6768
+
6769
+ if (tileInfo)
6770
+ {
6771
+ // set tile info
6772
+ this.tileInfo = tileInfo.frame(0);
6773
+ this.tileInfo.bleed = 0; // disable bleed for tile layers
6740
6774
  }
6775
+
6776
+ // init tile data
6777
+ for (let j = this.size.area(); j--;)
6778
+ this.data.push(new TileLayerData);
6741
6779
  }
6742
6780
 
6743
6781
  /** Set data at a given position in the array
6744
6782
  * @param {Vector2} layerPos - Local position in array
6745
- * @param {TileLayerData} data - Data to set
6783
+ * @param {TileLayerData} data - Data to set
6746
6784
  * @param {boolean} [redraw] - Force the tile to redraw if true */
6747
6785
  setData(layerPos, data, redraw=false)
6748
6786
  {
6787
+ layerPos = layerPos.floor();
6749
6788
  ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6750
6789
  ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
6751
- if (layerPos.arrayCheck(this.size))
6752
- {
6753
- this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
6754
- redraw && this.drawTileData(layerPos);
6755
- }
6790
+
6791
+ if (!layerPos.arrayCheck(this.size)) return;
6792
+ this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
6793
+
6794
+ if (!redraw) return;
6795
+ const isRedraw = drawContext === this.context;
6796
+ isRedraw ? this.drawTileData(layerPos) : this.redrawTileData(layerPos);
6756
6797
  }
6757
6798
 
6758
6799
  /** Get data at a given position in the array
@@ -6761,7 +6802,18 @@ class TileLayer extends CanvasLayer
6761
6802
  getData(layerPos)
6762
6803
  {
6763
6804
  ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6764
- return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
6805
+ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
6806
+ }
6807
+
6808
+ // Update the tile layer, refresh texture if needed
6809
+ update()
6810
+ {
6811
+ if (!glEnable && this.isUsingWebGL)
6812
+ {
6813
+ // redraw the layer if webgl was disabled or context lost
6814
+ this.isUsingWebGL = false;
6815
+ this.redraw();
6816
+ }
6765
6817
  }
6766
6818
 
6767
6819
  // Render the tile layer, called automatically by the engine
@@ -6769,78 +6821,76 @@ class TileLayer extends CanvasLayer
6769
6821
  {
6770
6822
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
6771
6823
 
6772
- if (this.refreshWebGL)
6773
- {
6774
- // update the WebGL texture
6775
- this.textureInfo.createWebGLTexture();
6776
- this.refreshWebGL = false;
6777
- }
6778
-
6779
- // draw the tile layer as a single tile
6780
- const tileInfo = new TileInfo().setFullImage(this.textureInfo);
6781
6824
  const size = this.drawSize || this.size;
6782
6825
  const pos = this.pos.add(size.scale(.5));
6783
- const useWebGL = glEnable && this.textureInfo.hasWebGL();
6784
- drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
6826
+ this.draw(pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
6785
6827
  }
6786
6828
 
6829
+ /** Called after this layer is redrawn, does nothing by default */
6830
+ onRedraw() {}
6831
+
6787
6832
  /** Draw all the tile data to an offscreen canvas
6788
- * - This may be slow in some browsers but only needs to be done once */
6833
+ * - This may be slow if not using webgl but only needs to be done once */
6789
6834
  redraw()
6790
6835
  {
6791
6836
  this.redrawStart(true);
6792
6837
  for (let x = this.size.x; x--;)
6793
6838
  for (let y = this.size.y; y--;)
6794
6839
  this.drawTileData(vec2(x,y), false);
6840
+ this.hasWebGL() && glFlush();
6841
+ this.onRedraw();
6795
6842
  this.redrawEnd();
6796
- this.useWebGL();
6797
6843
  }
6798
6844
 
6799
6845
  /** Call to start the redraw process
6800
- * - This can be used to manually update small parts of the level
6846
+ * - This can be used to manually update parts of the level
6801
6847
  * @param {boolean} [clear] - Should it clear the canvas before drawing */
6802
6848
  redrawStart(clear=false)
6803
6849
  {
6804
6850
  if (!this.context) return;
6805
-
6851
+ ASSERT(drawContext !== this.context);
6852
+
6806
6853
  // save current render settings
6807
- /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6808
- this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
6854
+ /** @type {[CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6855
+ this.savedRenderSettings = [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
6809
6856
 
6810
6857
  // set the draw canvas and context to this layer
6811
6858
  // use camera settings to match this layer's canvas
6812
- drawCanvas = this.canvas;
6813
6859
  drawContext = this.context;
6814
- cameraPos = this.size.scale(.5);
6815
- const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
6816
- cameraScale = tileSize.x;
6817
- canvasClearColor = CLEAR_BLACK;
6860
+ const tileSize = this.tileInfo?.size ?? vec2(1);
6818
6861
  mainCanvasSize = this.size.multiply(tileSize);
6819
- if (clear)
6862
+ canvasClearColor = CLEAR_BLACK;
6863
+ cameraPos = this.size.multiply(tileSize).scale(.5);
6864
+ cameraScale = 1;
6865
+
6866
+ // set render target to this layer
6867
+ this.isUsingWebGL = this.hasWebGL();
6868
+ if (this.isUsingWebGL)
6869
+ glSetRenderTarget(this.textureInfo.glTexture, clear);
6870
+ else
6820
6871
  {
6821
- // clear and set size
6822
- drawCanvas.width = mainCanvasSize.x;
6823
- drawCanvas.height = mainCanvasSize.y;
6872
+ // disable smoothing for pixel art
6873
+ this.context.imageSmoothingEnabled = !tilesPixelated;
6874
+ if (clear)
6875
+ {
6876
+ // clear and set size
6877
+ this.canvas.width = mainCanvasSize.x;
6878
+ this.canvas.height = mainCanvasSize.y;
6879
+ }
6824
6880
  }
6825
-
6826
- // disable smoothing for pixel art
6827
- drawContext.imageSmoothingEnabled = !tilesPixelated;
6828
-
6829
- // setup gl rendering if enabled
6830
- glPreRender();
6831
6881
  }
6832
6882
 
6833
6883
  /** Call to end the redraw process */
6834
6884
  redrawEnd()
6835
6885
  {
6836
6886
  if (!this.context) return;
6887
+ ASSERT(drawContext === this.context);
6837
6888
 
6838
- ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6839
- glCopyToContext(drawContext);
6840
- //debugSaveCanvas(this.canvas);
6889
+ if (glEnable && this.textureInfo.glTexture)
6890
+ glSetRenderTarget();
6841
6891
 
6842
6892
  // set stuff back to normal
6843
- [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6893
+ [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6844
6894
  }
6845
6895
 
6846
6896
  /** Draw the tile at a given position in the tile grid
@@ -6852,24 +6902,75 @@ class TileLayer extends CanvasLayer
6852
6902
  drawTileData(layerPos, clear=true)
6853
6903
  {
6854
6904
  if (!this.context) return;
6905
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6855
6906
 
6856
- // clear out where the tile was, for full opaque tiles this can be skipped
6857
- const s = this.tileInfo.size;
6858
- if (clear)
6859
- {
6860
- const pos = layerPos.multiply(s);
6861
- this.context.clearRect(pos.x, this.canvas.height-pos.y, s.x, -s.y);
6862
- }
6907
+ // clear out where the tile was, can be skipped for fully opaque tiles
6908
+ const drawSize = this.tileInfo?.size ?? vec2(1);
6909
+ const drawPos = layerPos.multiply(drawSize);
6910
+ clear && this.clearLayerRect(drawPos, drawSize);
6863
6911
 
6864
6912
  // draw the tile if it has layer data
6865
6913
  const d = this.getData(layerPos);
6866
- if (d.tile !== undefined)
6867
- {
6868
- ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6869
- const pos = layerPos.add(vec2(.5));
6870
- const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
6871
- drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
6872
- }
6914
+ if (!d.tile) return;
6915
+
6916
+ const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
6917
+ this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
6918
+ }
6919
+
6920
+ /** Draw the tile at a given position in the tile grid
6921
+ * This can be used to clear tiles when they are destroyed
6922
+ * For better performance use drawTileData inside a redrawStart/End block
6923
+ * @param {Vector2} layerPos
6924
+ * @param {boolean} [clear] - should the old tile be cleared
6925
+ */
6926
+ redrawTileData(layerPos, clear=true)
6927
+ {
6928
+ if (!this.context) return;
6929
+ ASSERT(drawContext !== this.context, 'redrawStart() should not be active when calling redrawTileData(), instead use drawTileData()');
6930
+
6931
+ this.redrawStart();
6932
+ this.drawTileData(layerPos, clear);
6933
+ this.redrawEnd();
6934
+ }
6935
+
6936
+ /** Draw textured tile in layer space
6937
+ * @param {Vector2} pos - Position in pixel coordinates
6938
+ * @param {Vector2} [size=vec2(1)] - Size of the tile
6939
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
6940
+ * @param {Color} [color=WHITE] - Color to modulate with
6941
+ * @param {number} [angle] - Angle to rotate by
6942
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
6943
+ * @param {Color} [additiveColor] - Additive color to be applied if any */
6944
+ drawLayerTile(pos, size=vec2(1), tileInfo, color=WHITE,
6945
+ angle=0, mirror, additiveColor)
6946
+ {
6947
+ const drawPos = pos.add(size.scale(.5));
6948
+ drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor);
6949
+ }
6950
+
6951
+ /** Clear a rectangle in layer space
6952
+ * @param {Vector2} pos
6953
+ * @param {Vector2} size
6954
+ * @param {Color} [color=WHITE] - Color to modulate with
6955
+ * @param {number} [angle] - Angle to rotate by
6956
+ */
6957
+ drawLayerRect(pos, size, color, angle=0)
6958
+ { this.drawLayerTile(pos, size, undefined, color, angle); }
6959
+
6960
+ /** Clear a rectangle in layer space
6961
+ * @param {Vector2} pos - position in pixel coordinates
6962
+ * @param {Vector2} size
6963
+ */
6964
+ clearLayerRect(pos, size)
6965
+ {
6966
+ ASSERT(drawContext === this.context, 'must call redrawStart() before clearing tiles');
6967
+
6968
+ const x = pos.x, y = this.canvas.height - pos.y - size.y;
6969
+ const useWebGL = this.hasWebGL();
6970
+ if (useWebGL)
6971
+ glClearRect(x, y, size.x, size.y);
6972
+ else
6973
+ this.context.clearRect(x, y, size.x, size.y);
6873
6974
  }
6874
6975
  }
6875
6976
 
@@ -6878,7 +6979,6 @@ class TileLayer extends CanvasLayer
6878
6979
  * Tile Collision Layer - a tile layer with collision
6879
6980
  * - adds collision data and functions to TileLayer
6880
6981
  * - there can be multiple tile collision layers
6881
- * - tile collision layers should not overlap each other
6882
6982
  * @extends TileLayer
6883
6983
  * @memberof TileLayers
6884
6984
  */
@@ -6908,8 +7008,7 @@ class TileCollisionLayer extends TileLayer
6908
7008
  /** Destroy this tile layer */
6909
7009
  destroy()
6910
7010
  {
6911
- if (this.destroyed)
6912
- return;
7011
+ if (this.destroyed) return;
6913
7012
 
6914
7013
  // remove from collision layers array and destroy
6915
7014
  const index = tileCollisionLayers.indexOf(this);
@@ -6951,7 +7050,7 @@ class TileCollisionLayer extends TileLayer
6951
7050
 
6952
7051
  /** Check if collision with another object should occur
6953
7052
  * @param {Vector2} pos
6954
- * @param {Vector2} [size=(0,0)]
7053
+ * @param {Vector2} [size=vec2()]
6955
7054
  * @param {EngineObject} [object]
6956
7055
  * @return {boolean} */
6957
7056
  collisionTest(pos, size=new Vector2, object)
@@ -7271,7 +7370,7 @@ class ParticleEmitter extends EngineObject
7271
7370
  particle.mirror = randBool();
7272
7371
 
7273
7372
  // call particle create callback
7274
- this.particleCreateCallback && this.particleCreateCallback(particle);
7373
+ this.particleCreateCallback?.(particle);
7275
7374
 
7276
7375
  // return the newly created particle
7277
7376
  return particle;
@@ -7356,7 +7455,7 @@ class Particle extends EngineObject
7356
7455
  const c = this.colorEnd;
7357
7456
  this.color.set(c.r, c.g, c.b, c.a);
7358
7457
  this.size.set(this.sizeEnd, this.sizeEnd);
7359
- this.destroyCallback && this.destroyCallback(this);
7458
+ this.destroyCallback?.(this);
7360
7459
  this.destroyed = 1;
7361
7460
  }
7362
7461
  }
@@ -7446,10 +7545,11 @@ function medalsInit(saveName)
7446
7545
 
7447
7546
  // engine automatically renders medals
7448
7547
  engineAddPlugin(undefined, medalsRender);
7548
+
7549
+ // plugin functions
7449
7550
  function medalsRender()
7450
7551
  {
7451
- if (!medalsDisplayQueue.length)
7452
- return;
7552
+ if (!medalsDisplayQueue.length) return;
7453
7553
 
7454
7554
  // update first medal in queue
7455
7555
  const medal = medalsDisplayQueue[0];
@@ -7539,8 +7639,7 @@ class Medal
7539
7639
  /** Unlocks a medal if not already unlocked */
7540
7640
  unlock()
7541
7641
  {
7542
- if (medalsPreventUnlock || this.unlocked)
7543
- return;
7642
+ if (medalsPreventUnlock || this.unlocked) return;
7544
7643
 
7545
7644
  // save the medal
7546
7645
  ASSERT(medalsSaveName, 'save name must be set');
@@ -7634,7 +7733,7 @@ let glContext;
7634
7733
  let glAntialias = true;
7635
7734
 
7636
7735
  // WebGL internal variables not exposed to documentation
7637
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glCanBeEnabled = true;
7736
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glCanBeEnabled = true;
7638
7737
 
7639
7738
  // WebGL internal constants
7640
7739
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -7710,7 +7809,7 @@ function glInit(rootElement)
7710
7809
  // setup instanced rendering shader program
7711
7810
  glShader = glCreateProgram(
7712
7811
  '#version 300 es\n' + // specify GLSL ES version
7713
- 'precision highp float;'+ // use highp for better accuracy
7812
+ 'precision highp float;'+ // use highp for accuracy
7714
7813
  'uniform mat4 m;'+ // transform matrix
7715
7814
  'in vec2 g;'+ // in: geometry
7716
7815
  'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
@@ -7725,7 +7824,7 @@ function glInit(rootElement)
7725
7824
  '}' // end of shader
7726
7825
  ,
7727
7826
  '#version 300 es\n' + // specify GLSL ES version
7728
- 'precision highp float;'+ // use highp for better accuracy
7827
+ 'precision highp float;'+ // use highp for accuracy
7729
7828
  'uniform sampler2D s;'+ // texture
7730
7829
  'in vec2 v;'+ // in: uv
7731
7830
  'in vec4 d,e;'+ // in: color, additiveColor
@@ -7763,87 +7862,96 @@ function glInit(rootElement)
7763
7862
  glColorData = new Uint32Array(glInstanceData);
7764
7863
  glArrayBuffer = glContext.createBuffer();
7765
7864
  glGeometryBuffer = glContext.createBuffer();
7865
+ glFramebuffer = glContext.createFramebuffer();
7866
+ glBatchCount = 0;
7766
7867
 
7767
7868
  // create the geometry buffer, triangle strip square
7768
- const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
7869
+ const geometry = new Float32Array([0,0,1,0,0,1,1,1]);
7769
7870
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7770
7871
  glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
7872
+
7873
+ let offset, shader, stride;
7874
+ const initVertexAttrib = (name, type, typeSize, size, divisor=0)=>
7875
+ {
7876
+ const location = glContext.getAttribLocation(shader, name);
7877
+ const normalize = typeSize === 1;
7878
+ const fixedStride = typeSize && stride;
7879
+ glContext.enableVertexAttribArray(location);
7880
+ glContext.vertexAttribPointer(location, size, type, normalize, fixedStride, offset);
7881
+ glContext.vertexAttribDivisor(location, divisor);
7882
+ offset += size*typeSize;
7883
+ }
7884
+
7885
+ // setup VAO for instanced rendering
7886
+ glInstancedVAO = glContext.createVertexArray();
7887
+ glContext.bindVertexArray(glInstancedVAO);
7888
+
7889
+ // configure instanced vertex attributes
7890
+ offset = 0, shader = glShader, stride = gl_INSTANCE_BYTE_STRIDE;
7891
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7892
+ initVertexAttrib('g', glContext.FLOAT, 0, 2); // geometry
7893
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7894
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7895
+ initVertexAttrib('p', glContext.FLOAT, 4, 4, 1); // position & size
7896
+ initVertexAttrib('u', glContext.FLOAT, 4, 4, 1); // texture coords
7897
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4, 1); // color
7898
+ initVertexAttrib('a', glContext.UNSIGNED_BYTE, 1, 4, 1); // additiveColor
7899
+ initVertexAttrib('r', glContext.FLOAT, 4, 1, 1); // rotation
7900
+
7901
+ // setup VAO for poly rendering
7902
+ glPolyVAO = glContext.createVertexArray();
7903
+ glContext.bindVertexArray(glPolyVAO);
7904
+
7905
+ // configure poly vertex attributes
7906
+ offset = 0, shader = glPolyShader, stride = gl_POLY_VERTEX_BYTE_STRIDE;
7907
+ initVertexAttrib('p', glContext.FLOAT, 4, 2); // position
7908
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7771
7909
  }
7772
7910
  }
7773
7911
 
7774
- function glSetInstancedMode()
7912
+ function glSetInstancedMode(force=false)
7775
7913
  {
7776
- if (!glPolyMode)
7777
- return;
7914
+ if (!force && !glPolyMode) return;
7778
7915
 
7779
7916
  // setup instanced mode
7780
7917
  glFlush();
7781
7918
  glPolyMode = false;
7782
7919
  glContext.useProgram(glShader);
7783
-
7784
- // set vertex attributes
7785
- let offset = 0;
7786
- const initVertexAttribArray = (name, type, typeSize, size)=>
7787
- {
7788
- const location = glContext.getAttribLocation(glShader, name);
7789
- const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
7790
- const divisor = typeSize && 1; // only if not geometry
7791
- const normalize = typeSize === 1; // only if color
7792
- glContext.enableVertexAttribArray(location);
7793
- glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
7794
- glContext.vertexAttribDivisor(location, divisor);
7795
- offset += size*typeSize;
7796
- }
7797
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7798
- initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
7799
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7800
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7801
- initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
7802
- initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
7803
- initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7804
- initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
7805
- initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
7920
+ glContext.bindVertexArray(glInstancedVAO);
7806
7921
  }
7807
7922
 
7808
7923
  function glSetPolyMode()
7809
7924
  {
7810
- if (glPolyMode)
7811
- return;
7925
+ if (glPolyMode) return;
7812
7926
 
7813
7927
  // setup poly mode
7814
7928
  glFlush();
7815
7929
  glPolyMode = true;
7816
7930
  glContext.useProgram(glPolyShader);
7817
-
7818
- // set vertex attributes
7819
- let offset = 0;
7820
- const initVertexAttribArray = (name, type, typeSize, size)=>
7821
- {
7822
- const location = glContext.getAttribLocation(glPolyShader, name);
7823
- const normalize = typeSize === 1; // only normalize if color
7824
- const stride = gl_POLY_VERTEX_BYTE_STRIDE;
7825
- glContext.enableVertexAttribArray(location);
7826
- glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
7827
- glContext.vertexAttribDivisor(location, 0);
7828
- offset += size*typeSize;
7829
- }
7830
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7831
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
7832
- initVertexAttribArray('p', glContext.FLOAT, 4, 2); // position
7833
- initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
7931
+ glContext.bindVertexArray(glPolyVAO);
7834
7932
  }
7835
7933
 
7836
7934
  // Setup WebGL render each frame, called automatically by engine
7837
7935
  // Also used by tile layer rendering when redrawing tiles
7838
- function glPreRender()
7936
+ function glPreRender(clear=true)
7839
7937
  {
7840
7938
  if (!glEnable || !glContext) return;
7841
7939
 
7842
- // clear the canvas
7843
- glClearCanvas();
7940
+ ASSERT(!glBatchCount, 'glPreRender called with unflushed batch.');
7941
+
7942
+ if (!glRenderTarget)
7943
+ {
7944
+ // set to same size as main canvas
7945
+ glCanvas.width = mainCanvasSize.x;
7946
+ glCanvas.height = mainCanvasSize.y;
7947
+ }
7948
+ glContext.viewport(0, 0, mainCanvasSize.x, mainCanvasSize.y);
7949
+ clear && glClearCanvas();
7844
7950
 
7845
7951
  // build the transform matrix
7846
7952
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
7953
+ if (glRenderTarget)
7954
+ s.y = -s.y; // invert y when using render target
7847
7955
  const rotatedCam = cameraPos.rotate(-cameraAngle);
7848
7956
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
7849
7957
  const ca = cos(cameraAngle);
@@ -7872,12 +7980,14 @@ function glPreRender()
7872
7980
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
7873
7981
  }
7874
7982
 
7983
+ // rebind the array buffer
7984
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
7985
+
7875
7986
  // start with additive blending off
7876
7987
  glAdditive = glBatchAdditive = false;
7877
7988
 
7878
- // force it to set instanced mode by first setting poly mode true
7879
- glPolyMode = true;
7880
- glSetInstancedMode();
7989
+ // force it to set instanced mode
7990
+ glSetInstancedMode(true);
7881
7991
  }
7882
7992
 
7883
7993
  /** Clear the canvas and setup the viewport
@@ -7886,13 +7996,9 @@ function glClearCanvas()
7886
7996
  {
7887
7997
  if (!glContext) return;
7888
7998
 
7889
- // clear and set to same size as main canvas
7890
- glCanvas.width = drawCanvas.width;
7891
- glCanvas.height = drawCanvas.height;
7892
- glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
7999
+ // clear using the canvasClearColor
7893
8000
  const color = canvasClearColor;
7894
- if (color.a > 0)
7895
- glContext.clearColor(color.r, color.g, color.b, color.a);
8001
+ glContext.clearColor(color.r, color.g, color.b, color.a);
7896
8002
  glContext.clear(glContext.COLOR_BUFFER_BIT);
7897
8003
  }
7898
8004
 
@@ -7904,8 +8010,7 @@ function glClearCanvas()
7904
8010
  function glSetTexture(texture, wrap=false)
7905
8011
  {
7906
8012
  // must flush cache with the old texture to set a new one
7907
- if (!glContext || texture === glActiveTexture)
7908
- return;
8013
+ if (!glContext || texture === glActiveTexture) return;
7909
8014
 
7910
8015
  glFlush();
7911
8016
  glActiveTexture = texture;
@@ -7970,7 +8075,7 @@ function glCreateTexture(image)
7970
8075
  // build the texture
7971
8076
  const texture = glContext.createTexture();
7972
8077
  let mipMap = false;
7973
- if (image && image.width)
8078
+ if (image?.width)
7974
8079
  {
7975
8080
  glSetTextureData(texture, image);
7976
8081
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
@@ -7991,7 +8096,9 @@ function glCreateTexture(image)
7991
8096
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
7992
8097
  if (mipMap)
7993
8098
  glContext.generateMipmap(glContext.TEXTURE_2D);
7994
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
8099
+
8100
+ // rebind active texture
8101
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
7995
8102
  return texture;
7996
8103
  }
7997
8104
 
@@ -8001,6 +8108,7 @@ function glCreateTexture(image)
8001
8108
  function glDeleteTexture(texture)
8002
8109
  {
8003
8110
  if (!glContext) return;
8111
+
8004
8112
  glContext.deleteTexture(texture);
8005
8113
  }
8006
8114
 
@@ -8013,10 +8121,12 @@ function glSetTextureData(texture, image)
8013
8121
  if (!glContext) return;
8014
8122
 
8015
8123
  // build the texture
8016
- ASSERT(!!image && image.width > 0, 'Invalid image data.');
8124
+ ASSERT(image?.width > 0, 'Invalid image data.');
8017
8125
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
8018
8126
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
8019
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
8127
+
8128
+ // rebind active texture
8129
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
8020
8130
  }
8021
8131
 
8022
8132
  /** Tells WebGL to create or update the glTexture and start tracking it
@@ -8085,8 +8195,7 @@ function glFlush()
8085
8195
  * @memberof WebGL */
8086
8196
  function glCopyToContext(context)
8087
8197
  {
8088
- if (!glEnable || !glContext)
8089
- return;
8198
+ if (!glEnable || !glContext) return;
8090
8199
 
8091
8200
  glFlush();
8092
8201
  context.drawImage(glCanvas, 0, 0);
@@ -8236,6 +8345,48 @@ function glDrawColoredPoints(points, pointColors)
8236
8345
  glBatchCount += vertCount;
8237
8346
  }
8238
8347
 
8348
+ /** Set the WebGL render target to the given texture or back to the canvas
8349
+ * @param {WebGLTexture} [texture] - a texture or undefined to use normal glCanvas
8350
+ * @param {boolean} [clear] - should the render target be cleared
8351
+ * @memberof WebGL */
8352
+ function glSetRenderTarget(texture, clear=false)
8353
+ {
8354
+ if (texture)
8355
+ {
8356
+ glRenderTarget = texture;
8357
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
8358
+ glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
8359
+ glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
8360
+ glPreRender(clear);
8361
+ }
8362
+ else
8363
+ {
8364
+ glFlush();
8365
+ glRenderTarget = undefined;
8366
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
8367
+ }
8368
+ }
8369
+
8370
+ /** Clear out a rectangle area of the WebGL canvas or render target
8371
+ * @param {number} x
8372
+ * @param {number} y
8373
+ * @param {number} width
8374
+ * @param {number} height
8375
+ * @memberof WebGL */
8376
+ function glClearRect(x, y, width, height)
8377
+ {
8378
+ if (!glEnable) return;
8379
+
8380
+ // Enable scissor test to clear only the specified area
8381
+ glContext.enable(glContext.SCISSOR_TEST);
8382
+ glContext.scissor(x, y, width, height);
8383
+ glContext.clearColor(0, 0, 0, 0);
8384
+ glContext.clear(glContext.COLOR_BUFFER_BIT);
8385
+ glContext.disable(glContext.SCISSOR_TEST);
8386
+ }
8387
+
8388
+ ///////////////////////////////////////////////////////////////////////////////
8389
+
8239
8390
  // WebGL internal function to convert polygon to outline triangle strip
8240
8391
  function glMakeOutline(points, width, wrap=true)
8241
8392
  {
@@ -8369,23 +8520,20 @@ function glPolyStrip(points)
8369
8520
  const a = points[i0], b = points[i1], c = points[i2];
8370
8521
 
8371
8522
  // check if convex
8372
- if (cross(a, b, c) < e)
8373
- continue;
8523
+ if (cross(a, b, c) < e) continue;
8374
8524
 
8375
8525
  // check if any other point is inside
8376
8526
  let hasInside = false;
8377
8527
  for (let j = 0; j < indices.length; j++)
8378
8528
  {
8379
8529
  const k = indices[j];
8380
- if (k === i0 || k === i1 || k === i2)
8381
- continue;
8530
+ if (k === i0 || k === i1 || k === i2) continue;
8531
+
8382
8532
  const p = points[k];
8383
8533
  hasInside = pointInTriangle(p, a, b, c);
8384
- if (hasInside)
8385
- break;
8534
+ if (hasInside) break;
8386
8535
  }
8387
- if (hasInside)
8388
- continue;
8536
+ if (hasInside) continue;
8389
8537
 
8390
8538
  // found valid ear
8391
8539
  triangles.push([i0, i1, i2]);
@@ -8410,8 +8558,7 @@ function glPolyStrip(points)
8410
8558
  worstIndex = i;
8411
8559
  }
8412
8560
  }
8413
- if (worstIndex < 0)
8414
- break;
8561
+ if (worstIndex < 0) break;
8415
8562
 
8416
8563
  const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
8417
8564
  const i1 = indices[worstIndex];
@@ -8642,14 +8789,16 @@ class PostProcessPlugin
8642
8789
  {
8643
8790
  /** Create global post processing shader
8644
8791
  * @param {string} shaderCode
8645
- * @param {boolean} [includeMainCanvas]
8646
- * @example
8647
- * // create the post process plugin object
8648
- * new PostProcessPlugin(shaderCode);
8649
- */
8650
- constructor(shaderCode, includeMainCanvas=true)
8792
+ * @param {boolean} [includeMainCanvas] - combine mainCanvs onto glCanvas
8793
+ * @param {boolean} [feedbackTexture] - use glCanvas from previous frame as the texture
8794
+ * @example
8795
+ * // create the post process plugin object
8796
+ * new PostProcessPlugin(shaderCode);
8797
+ */
8798
+ constructor(shaderCode, includeMainCanvas=false, feedbackTexture=false)
8651
8799
  {
8652
8800
  ASSERT(!postProcess, 'Post process already initialized');
8801
+ ASSERT(!(includeMainCanvas && feedbackTexture), 'Post process cannot both include main canvas and use feedback texture');
8653
8802
  postProcess = this;
8654
8803
 
8655
8804
  if (!shaderCode) // default shader pass through
@@ -8657,9 +8806,10 @@ class PostProcessPlugin
8657
8806
 
8658
8807
  /** @property {WebGLProgram} - Shader for post processing */
8659
8808
  this.shader = undefined;
8660
-
8661
8809
  /** @property {WebGLTexture} - Texture for post processing */
8662
8810
  this.texture = undefined;
8811
+ /** @property {WebGLVertexArrayObject} - Vertex array object */
8812
+ this.vao = undefined;
8663
8813
 
8664
8814
  // setup the post processing plugin
8665
8815
  initPostProcess();
@@ -8668,7 +8818,6 @@ class PostProcessPlugin
8668
8818
  function initPostProcess()
8669
8819
  {
8670
8820
  if (headlessMode) return;
8671
-
8672
8821
  if (!glEnable)
8673
8822
  {
8674
8823
  console.warn('PostProcessPlugin: WebGL not enabled!');
@@ -8679,14 +8828,14 @@ class PostProcessPlugin
8679
8828
  postProcess.texture = glCreateTexture();
8680
8829
  postProcess.shader = glCreateProgram(
8681
8830
  '#version 300 es\n' + // specify GLSL ES version
8682
- 'precision highp float;'+ // use highp for better accuracy
8831
+ 'precision highp float;'+ // use highp for accuracy
8683
8832
  'in vec2 p;'+ // position
8684
8833
  'void main(){'+ // shader entry point
8685
8834
  'gl_Position=vec4(p+p-1.,1,1);'+ // set position
8686
8835
  '}' // end of shader
8687
8836
  ,
8688
8837
  '#version 300 es\n' + // specify GLSL ES version
8689
- 'precision highp float;'+ // use highp for better accuracy
8838
+ 'precision highp float;'+ // use highp for accuracy
8690
8839
  'uniform sampler2D iChannel0;'+ // input texture
8691
8840
  'uniform vec3 iResolution;'+ // size of output texture
8692
8841
  'uniform float iTime;'+ // time
@@ -8697,6 +8846,17 @@ class PostProcessPlugin
8697
8846
  'c.a=1.;'+ // always use full alpha
8698
8847
  '}' // end of shader
8699
8848
  );
8849
+
8850
+ // setup VAO for post processing
8851
+ postProcess.vao = glContext.createVertexArray();
8852
+ glContext.bindVertexArray(postProcess.vao);
8853
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
8854
+
8855
+ // configure vertex attributes
8856
+ const vertexByteStride = 8;
8857
+ const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
8858
+ glContext.enableVertexAttribArray(pLocation);
8859
+ glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
8700
8860
  }
8701
8861
  function postProcessContextLost()
8702
8862
  {
@@ -8711,14 +8871,17 @@ class PostProcessPlugin
8711
8871
  }
8712
8872
  function postProcessRender()
8713
8873
  {
8714
- if (headlessMode) return;
8715
-
8716
- if (!glEnable)
8717
- return;
8874
+ if (headlessMode || !glEnable) return;
8718
8875
 
8719
8876
  // clear out the buffer
8720
8877
  glFlush();
8721
-
8878
+
8879
+ // setup shader program to draw a quad
8880
+ glContext.useProgram(postProcess.shader);
8881
+ glContext.bindVertexArray(postProcess.vao);
8882
+ glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, true);
8883
+ glContext.disable(glContext.BLEND);
8884
+
8722
8885
  // setup texture
8723
8886
  glContext.activeTexture(glContext.TEXTURE0);
8724
8887
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
@@ -8729,29 +8892,32 @@ class PostProcessPlugin
8729
8892
  workCanvas.height = mainCanvasSize.y;
8730
8893
  glCopyToContext(workContext);
8731
8894
  workContext.drawImage(mainCanvas, 0, 0);
8895
+ mainCanvas.width |= 0
8732
8896
 
8733
8897
  // copy work canvas to texture
8734
8898
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
8735
8899
  }
8736
-
8737
- // setup shader program to draw a quad
8738
- glContext.useProgram(postProcess.shader);
8739
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
8740
- glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
8741
- glContext.disable(glContext.BLEND);
8742
-
8743
- // set vertex position attribute
8744
- const vertexByteStride = 8;
8745
- const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
8746
- glContext.enableVertexAttribArray(pLocation);
8747
- glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
8748
-
8900
+ else if (!feedbackTexture)
8901
+ {
8902
+ // copy glCanvas to texture
8903
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
8904
+ }
8905
+
8749
8906
  // set uniforms and draw
8750
8907
  const uniformLocation = (name)=>glContext.getUniformLocation(postProcess.shader, name);
8751
8908
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
8752
8909
  glContext.uniform1f(uniformLocation('iTime'), time);
8753
8910
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
8754
8911
  glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
8912
+
8913
+ if (feedbackTexture)
8914
+ {
8915
+ // pass glCanvas back to overlay texture
8916
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
8917
+ }
8918
+
8919
+ // force it to set instanced mode
8920
+ glSetInstancedMode(true);
8755
8921
  }
8756
8922
  }
8757
8923
  }
@@ -9432,7 +9598,7 @@ class UISystemPlugin
9432
9598
  const up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight';
9433
9599
  if (both)
9434
9600
  {
9435
- return keyIsDown(up) || keyIsDown(left) ? -1 :
9601
+ return keyIsDown(up) || keyIsDown(left) ? -1 :
9436
9602
  keyIsDown(down) || keyIsDown(right) ? 1 : 0;
9437
9603
  }
9438
9604
  const back = vertical ? up : left;
@@ -9463,7 +9629,7 @@ class UISystemPlugin
9463
9629
  * @return {boolean} */
9464
9630
  getNavigationWasPressed()
9465
9631
  {
9466
- return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
9632
+ return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
9467
9633
  keyWasPressed('Space') || keyWasPressed('Enter');
9468
9634
  }
9469
9635
 
@@ -9511,7 +9677,7 @@ class UISystemPlugin
9511
9677
  buttonYes.textHeight = 40;
9512
9678
  buttonYes.navigationIndex = 1;
9513
9679
  buttonYes.hoverColor = hsl(0,1,.5);
9514
- buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
9680
+ buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
9515
9681
  confirmMenu.addChild(buttonYes);
9516
9682
 
9517
9683
  // no button
@@ -9541,8 +9707,8 @@ class UISystemPlugin
9541
9707
  class UIObject
9542
9708
  {
9543
9709
  /** Create a UIObject
9544
- * @param {Vector2} [pos=(0,0)]
9545
- * @param {Vector2} [size=(1,1)]
9710
+ * @param {Vector2} [pos=vec2()]
9711
+ * @param {Vector2} [size=vec2(1)]
9546
9712
  */
9547
9713
  constructor(pos=vec2(), size=vec2())
9548
9714
  {
@@ -9557,7 +9723,7 @@ class UIObject
9557
9723
  this.size = size.copy();
9558
9724
  /** @property {Color} - Color of the object */
9559
9725
  this.color = uiSystem.defaultColor.copy();
9560
- /** @property {Color} - Color of the object when active, uses color if undefined */
9726
+ /** @property {Color} - Color of the object when active, uses hoverColor if undefined */
9561
9727
  this.activeColor = undefined;
9562
9728
  /** @property {string} - Text for this ui object */
9563
9729
  this.text = undefined;
@@ -9655,10 +9821,10 @@ class UIObject
9655
9821
 
9656
9822
  // disconnect from parent and destroy children
9657
9823
  this.destroyed = 1;
9658
- this.parent && this.parent.removeChild(this);
9824
+ this.parent?.removeChild(this);
9659
9825
  for (const child of this.children)
9660
9826
  {
9661
- child.parent = 0;
9827
+ child.parent = undefined;
9662
9828
  child.destroy();
9663
9829
  }
9664
9830
  }
@@ -9682,7 +9848,7 @@ class UIObject
9682
9848
  this.onUpdate();
9683
9849
 
9684
9850
  // unset active if disabled
9685
- if (this.disabled && this == uiSystem.activeObject)
9851
+ if (this.disabled && this === uiSystem.activeObject)
9686
9852
  uiSystem.activeObject = undefined;
9687
9853
 
9688
9854
  const wasHover = uiSystem.lastHoverObject === this;
@@ -9754,10 +9920,10 @@ class UIObject
9754
9920
  this.interactive && this.isActiveObject() && !this.disabled ?
9755
9921
  this.color : this.lineColor;
9756
9922
  const color = isNavigationObject ? this.hoverColor :
9757
- this.disabled ? this.disabledColor :
9758
- this.interactive ?
9759
- this.isHoverObject() ? this.hoverColor :
9760
- this.isActiveObject() ? this.activeColor || this.color :
9923
+ this.disabled ? this.disabledColor :
9924
+ this.interactive ?
9925
+ this.isActiveObject() ? this.activeColor || this.hoverColor :
9926
+ this.isHoverObject() ? this.hoverColor :
9761
9927
  this.color : this.color;
9762
9928
  const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
9763
9929
 
@@ -9769,7 +9935,7 @@ class UIObject
9769
9935
  getTextSize()
9770
9936
  {
9771
9937
  return vec2(
9772
- this.textWidth || this.textFitScale * this.size.x,
9938
+ this.textWidth || this.textFitScale * this.size.x,
9773
9939
  this.textHeight || this.textFitScale * this.size.y);
9774
9940
  }
9775
9941
 
@@ -9817,9 +9983,9 @@ class UIObject
9817
9983
  renderDebug(visible=true)
9818
9984
  {
9819
9985
  // apply color based on state
9820
- const color =
9986
+ const color =
9821
9987
  !visible ? GREEN :
9822
- this.isHoverObject() ? YELLOW :
9988
+ this.isHoverObject() ? YELLOW :
9823
9989
  this.disabled ? PURPLE :
9824
9990
  this.interactive ? RED : BLUE;
9825
9991
  uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);
@@ -12341,355 +12507,349 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
12341
12507
 
12342
12508
  export
12343
12509
  {
12344
- // Engine
12345
- engineName,
12346
- engineVersion,
12347
- frameRate,
12348
- timeDelta,
12349
- engineObjects,
12350
- frame,
12351
- time,
12352
- timeReal,
12353
- paused,
12354
- getPaused,
12355
- setPaused,
12356
- engineInit,
12357
- engineObjectsUpdate,
12358
- engineObjectsDestroy,
12359
- engineObjectsCollect,
12360
- engineObjectsCallback,
12361
- engineObjectsRaycast,
12362
- engineAddPlugin,
12363
-
12364
- // Globals
12365
- debug,
12366
- debugOverlay,
12367
- debugWatermark,
12368
-
12369
- // Debug
12370
- ASSERT,
12371
- LOG,
12372
- debugRect,
12373
- debugPoly,
12374
- debugCircle,
12375
- debugPoint,
12376
- debugLine,
12377
- debugOverlap,
12378
- debugText,
12379
- debugClear,
12380
- debugScreenshot,
12381
- debugSaveCanvas,
12382
- debugSaveText,
12383
- debugSaveDataURL,
12384
- debugShowErrors,
12385
- debugVideoCaptureIsActive,
12386
- debugVideoCaptureStart,
12387
- debugVideoCaptureStop,
12388
-
12389
- // Settings
12390
- cameraPos,
12391
- cameraAngle,
12392
- cameraScale,
12393
- canvasColorTiles,
12394
- canvasClearColor,
12395
- canvasMaxSize,
12396
- canvasMinAspect,
12397
- canvasMaxAspect,
12398
- canvasFixedSize,
12399
- canvasPixelated,
12400
- tilesPixelated,
12401
- fontDefault,
12402
- showSplashScreen,
12403
- headlessMode,
12404
- tileDefaultSize,
12405
- tileDefaultPadding,
12406
- tileDefaultBleed,
12407
- enablePhysicsSolver,
12408
- objectDefaultMass,
12409
- objectDefaultDamping,
12410
- objectDefaultAngleDamping,
12411
- objectDefaultRestitution,
12412
- objectDefaultFriction,
12413
- objectMaxSpeed,
12414
- gravity,
12415
- particleEmitRateScale,
12416
- glEnable,
12417
- gamepadsEnable,
12418
- gamepadDirectionEmulateStick,
12419
- inputWASDEmulateDirection,
12420
- touchGamepadEnable,
12421
- touchGamepadCenterButton,
12422
- touchGamepadAnalog,
12423
- touchGamepadSize,
12424
- touchGamepadAlpha,
12425
- vibrateEnable,
12426
- soundEnable,
12427
- soundVolume,
12428
- soundDefaultRange,
12429
- soundDefaultTaper,
12430
- medalDisplayTime,
12431
- medalDisplaySlideTime,
12432
- medalDisplaySize,
12433
-
12434
- // Setters for globals
12435
- setCameraPos,
12436
- setCameraAngle,
12437
- setCameraScale,
12438
- setCanvasColorTiles,
12439
- setCanvasClearColor,
12440
- setCanvasMaxSize,
12441
- setCanvasMinAspect,
12442
- setCanvasMaxAspect,
12443
- setCanvasFixedSize,
12444
- setCanvasPixelated,
12445
- setTilesPixelated,
12446
- setFontDefault,
12447
- setShowSplashScreen,
12448
- setHeadlessMode,
12449
- setGLEnable,
12450
- setTileDefaultSize,
12451
- setTileDefaultPadding,
12452
- setTileDefaultBleed,
12453
- setEnablePhysicsSolver,
12454
- setObjectDefaultMass,
12455
- setObjectDefaultDamping,
12456
- setObjectDefaultAngleDamping,
12457
- setObjectDefaultRestitution,
12458
- setObjectDefaultFriction,
12459
- setObjectMaxSpeed,
12460
- setGravity,
12461
- setParticleEmitRateScale,
12462
- setTouchInputEnable,
12463
- setGamepadsEnable,
12464
- setGamepadDirectionEmulateStick,
12465
- setInputWASDEmulateDirection,
12466
- setTouchGamepadEnable,
12467
- setTouchGamepadCenterButton,
12468
- setTouchGamepadButtonCount,
12469
- setTouchGamepadAnalog,
12470
- setTouchGamepadSize,
12471
- setTouchGamepadAlpha,
12472
- setVibrateEnable,
12473
- setSoundEnable,
12474
- setSoundVolume,
12475
- setSoundDefaultRange,
12476
- setSoundDefaultTaper,
12477
- setMedalDisplayTime,
12478
- setMedalDisplaySlideTime,
12479
- setMedalDisplaySize,
12480
- setMedalsPreventUnlock,
12481
- setDebugWatermark,
12482
- setDebugKey,
12483
-
12484
- // Utilities
12485
- PI,
12486
- abs,
12487
- floor,
12488
- ceil,
12489
- round,
12490
- min,
12491
- max,
12492
- sign,
12493
- hypot,
12494
- log2,
12495
- sin,
12496
- cos,
12497
- tan,
12498
- atan2,
12499
- mod,
12500
- clamp,
12501
- percent,
12502
- distanceWrap,
12503
- lerpWrap,
12504
- distanceAngle,
12505
- lerpAngle,
12506
- lerp,
12507
- smoothStep,
12508
- nearestPowerOfTwo,
12509
- isOverlapping,
12510
- isIntersecting,
12511
- wave,
12512
- formatTime,
12513
- fetchJSON,
12514
-
12515
- // Random
12516
- rand,
12517
- randInt,
12518
- randBool,
12519
- randSign,
12520
- randInCircle,
12521
- randVec2,
12522
- randColor,
12523
-
12524
- // Utility Classes
12525
- RandomGenerator,
12526
- Vector2,
12527
- Color,
12528
- Timer,
12529
- vec2,
12530
- rgb,
12531
- hsl,
12532
- isColor,
12533
- isVector2,
12534
- isNumber,
12535
- isString,
12536
- isArray,
12537
-
12538
- // Default Colors
12539
- WHITE,
12540
- CLEAR_WHITE,
12541
- BLACK,
12542
- CLEAR_BLACK,
12543
- GRAY,
12544
- RED,
12545
- ORANGE,
12546
- YELLOW,
12547
- GREEN,
12548
- CYAN,
12549
- BLUE,
12550
- PURPLE,
12551
- MAGENTA,
12552
-
12553
- // Draw
12554
- tile,
12555
- TileInfo,
12556
- TextureInfo,
12557
- mainCanvas,
12558
- mainContext,
12559
- drawCanvas,
12560
- drawContext,
12561
- workCanvas,
12562
- workContext,
12563
- workReadCanvas,
12564
- workReadContext,
12565
- mainCanvasSize,
12566
- textureInfos,
12567
- drawCount,
12568
- screenToWorld,
12569
- worldToScreen,
12570
- screenToWorldDelta,
12571
- worldToScreenDelta,
12572
- drawTile,
12573
- drawRect,
12574
- drawRectGradient,
12575
- drawLineList,
12576
- drawLine,
12577
- drawPoly,
12578
- drawEllipse,
12579
- drawCircle,
12580
- drawCanvas2D,
12581
- drawText,
12582
- drawTextScreen,
12583
- setBlendMode,
12584
- combineCanvases,
12585
- engineFontImage,
12586
- FontImage,
12587
- isFullscreen,
12588
- toggleFullscreen,
12589
- setCursor,
12590
- getCameraSize,
12591
-
12592
- // WebGL
12593
- glCanvas,
12594
- glContext,
12595
- glClearCanvas,
12596
- glSetTexture,
12597
- glCompileShader,
12598
- glCreateProgram,
12599
- glCreateTexture,
12600
- glDeleteTexture,
12601
- glSetTextureData,
12602
- glFlush,
12603
- glCopyToContext,
12604
- glSetAntialias,
12605
- glDraw,
12606
- glDrawPointsTransform,
12607
- glDrawOutlineTransform,
12608
- glDrawPoints,
12609
- glDrawColoredPoints,
12610
- glAntialias,
12611
- glShader,
12612
- glPolyShader,
12613
- glPolyMode,
12614
- glAdditive,
12615
- glBatchAdditive,
12616
- glActiveTexture,
12617
- glArrayBuffer,
12618
- glGeometryBuffer,
12619
- glPositionData,
12620
- glColorData,
12621
- glBatchCount,
12622
-
12623
- // Input
12624
- keyIsDown,
12625
- keyWasPressed,
12626
- keyWasReleased,
12627
- keyDirection,
12628
- inputClear,
12629
- inputClearKey,
12630
- mouseIsDown,
12631
- mouseWasPressed,
12632
- mouseWasReleased,
12633
- mousePos,
12634
- mousePosScreen,
12635
- mouseDelta,
12636
- mouseDeltaScreen,
12637
- mouseWheel,
12638
- mouseInWindow,
12639
- isUsingGamepad,
12640
- inputPreventDefault,
12641
- gamepadPrimary,
12642
- setInputPreventDefault,
12643
- gamepadIsDown,
12644
- gamepadWasPressed,
12645
- gamepadWasReleased,
12646
- gamepadStick,
12647
- gamepadDpad,
12648
- gamepadConnected,
12649
- vibrate,
12650
- vibrateStop,
12651
- isTouchDevice,
12652
- pointerLockRequest,
12653
- pointerLockExit,
12654
- pointerLockIsActive,
12655
-
12656
- // Audio
12657
- audioContext,
12658
- audioMasterGain,
12659
- audioDefaultSampleRate,
12660
- Sound,
12661
- SoundWave,
12662
- SoundInstance,
12663
- speak,
12664
- speakStop,
12665
- getNoteFrequency,
12666
- playSamples,
12667
- zzfx,
12668
- zzfxG,
12669
-
12670
- // Base Object
12671
- EngineObject,
12672
-
12673
- // Tiles
12674
- tileCollisionLayers,
12675
- tileCollisionGetData,
12676
- tileCollisionTest,
12677
- tileCollisionRaycast,
12678
- tileLayersLoad,
12679
- TileLayerData,
12680
- CanvasLayer,
12681
- TileLayer,
12682
- TileCollisionLayer,
12683
-
12684
- // Particles
12685
- ParticleEmitter,
12686
- Particle,
12687
-
12688
- // Medals
12689
- medals,
12690
- medalsPreventUnlock,
12691
- medalsInit,
12692
- Medal,
12510
+ // Engine
12511
+ engineName,
12512
+ engineVersion,
12513
+ frameRate,
12514
+ timeDelta,
12515
+ engineObjects,
12516
+ frame,
12517
+ time,
12518
+ timeReal,
12519
+ paused,
12520
+ getPaused,
12521
+ setPaused,
12522
+ engineInit,
12523
+ engineObjectsUpdate,
12524
+ engineObjectsDestroy,
12525
+ engineObjectsCollect,
12526
+ engineObjectsCallback,
12527
+ engineObjectsRaycast,
12528
+ engineAddPlugin,
12529
+
12530
+ // Globals
12531
+ debug,
12532
+ debugOverlay,
12533
+ debugWatermark,
12534
+
12535
+ // Debug
12536
+ ASSERT,
12537
+ LOG,
12538
+ debugRect,
12539
+ debugPoly,
12540
+ debugCircle,
12541
+ debugPoint,
12542
+ debugLine,
12543
+ debugOverlap,
12544
+ debugText,
12545
+ debugClear,
12546
+ debugScreenshot,
12547
+ debugShowErrors,
12548
+ debugVideoCaptureStart,
12549
+ debugVideoCaptureStop,
12550
+ debugVideoCaptureIsActive,
12551
+
12552
+ // Settings
12553
+ cameraPos,
12554
+ cameraAngle,
12555
+ cameraScale,
12556
+ canvasColorTiles,
12557
+ canvasClearColor,
12558
+ canvasMaxSize,
12559
+ canvasMinAspect,
12560
+ canvasMaxAspect,
12561
+ canvasFixedSize,
12562
+ canvasPixelated,
12563
+ tilesPixelated,
12564
+ fontDefault,
12565
+ showSplashScreen,
12566
+ headlessMode,
12567
+ tileDefaultSize,
12568
+ tileDefaultPadding,
12569
+ tileDefaultBleed,
12570
+ enablePhysicsSolver,
12571
+ objectDefaultMass,
12572
+ objectDefaultDamping,
12573
+ objectDefaultAngleDamping,
12574
+ objectDefaultRestitution,
12575
+ objectDefaultFriction,
12576
+ objectMaxSpeed,
12577
+ gravity,
12578
+ particleEmitRateScale,
12579
+ glEnable,
12580
+ gamepadsEnable,
12581
+ gamepadDirectionEmulateStick,
12582
+ inputWASDEmulateDirection,
12583
+ touchGamepadEnable,
12584
+ touchGamepadCenterButton,
12585
+ touchGamepadAnalog,
12586
+ touchGamepadSize,
12587
+ touchGamepadAlpha,
12588
+ vibrateEnable,
12589
+ soundEnable,
12590
+ soundVolume,
12591
+ soundDefaultRange,
12592
+ soundDefaultTaper,
12593
+ medalDisplayTime,
12594
+ medalDisplaySlideTime,
12595
+ medalDisplaySize,
12596
+
12597
+ // Setters for globals
12598
+ setCameraPos,
12599
+ setCameraAngle,
12600
+ setCameraScale,
12601
+ setCanvasColorTiles,
12602
+ setCanvasClearColor,
12603
+ setCanvasMaxSize,
12604
+ setCanvasMinAspect,
12605
+ setCanvasMaxAspect,
12606
+ setCanvasFixedSize,
12607
+ setCanvasPixelated,
12608
+ setTilesPixelated,
12609
+ setFontDefault,
12610
+ setShowSplashScreen,
12611
+ setHeadlessMode,
12612
+ setGLEnable,
12613
+ setTileDefaultSize,
12614
+ setTileDefaultPadding,
12615
+ setTileDefaultBleed,
12616
+ setEnablePhysicsSolver,
12617
+ setObjectDefaultMass,
12618
+ setObjectDefaultDamping,
12619
+ setObjectDefaultAngleDamping,
12620
+ setObjectDefaultRestitution,
12621
+ setObjectDefaultFriction,
12622
+ setObjectMaxSpeed,
12623
+ setGravity,
12624
+ setParticleEmitRateScale,
12625
+ setTouchInputEnable,
12626
+ setGamepadsEnable,
12627
+ setGamepadDirectionEmulateStick,
12628
+ setInputWASDEmulateDirection,
12629
+ setTouchGamepadEnable,
12630
+ setTouchGamepadCenterButton,
12631
+ setTouchGamepadButtonCount,
12632
+ setTouchGamepadAnalog,
12633
+ setTouchGamepadSize,
12634
+ setTouchGamepadAlpha,
12635
+ setVibrateEnable,
12636
+ setSoundEnable,
12637
+ setSoundVolume,
12638
+ setSoundDefaultRange,
12639
+ setSoundDefaultTaper,
12640
+ setMedalDisplayTime,
12641
+ setMedalDisplaySlideTime,
12642
+ setMedalDisplaySize,
12643
+ setMedalsPreventUnlock,
12644
+ setDebugWatermark,
12645
+ setDebugKey,
12646
+
12647
+ // Math
12648
+ PI,
12649
+ abs,
12650
+ floor,
12651
+ ceil,
12652
+ round,
12653
+ min,
12654
+ max,
12655
+ sign,
12656
+ hypot,
12657
+ log2,
12658
+ sin,
12659
+ cos,
12660
+ tan,
12661
+ atan2,
12662
+ mod,
12663
+ clamp,
12664
+ percent,
12665
+ distanceWrap,
12666
+ lerpWrap,
12667
+ distanceAngle,
12668
+ lerpAngle,
12669
+ lerp,
12670
+ smoothStep,
12671
+ nearestPowerOfTwo,
12672
+ isOverlapping,
12673
+ isIntersecting,
12674
+ wave,
12675
+
12676
+ // Utilities
12677
+ formatTime,
12678
+ fetchJSON,
12679
+ saveText,
12680
+ saveCanvas,
12681
+ saveDataURL,
12682
+ shareURL,
12683
+
12684
+ // Random
12685
+ rand,
12686
+ randInt,
12687
+ randBool,
12688
+ randSign,
12689
+ randInCircle,
12690
+ randVec2,
12691
+ randColor,
12692
+
12693
+ // Utility Classes
12694
+ RandomGenerator,
12695
+ Vector2,
12696
+ Color,
12697
+ Timer,
12698
+ vec2,
12699
+ rgb,
12700
+ hsl,
12701
+ isColor,
12702
+ isVector2,
12703
+ isNumber,
12704
+ isString,
12705
+ isArray,
12706
+
12707
+ // Default Colors
12708
+ WHITE,
12709
+ CLEAR_WHITE,
12710
+ BLACK,
12711
+ CLEAR_BLACK,
12712
+ GRAY,
12713
+ RED,
12714
+ ORANGE,
12715
+ YELLOW,
12716
+ GREEN,
12717
+ CYAN,
12718
+ BLUE,
12719
+ PURPLE,
12720
+ MAGENTA,
12721
+
12722
+ // Draw
12723
+ tile,
12724
+ TileInfo,
12725
+ TextureInfo,
12726
+ mainCanvas,
12727
+ mainContext,
12728
+ drawContext,
12729
+ workCanvas,
12730
+ workContext,
12731
+ workReadCanvas,
12732
+ workReadContext,
12733
+ mainCanvasSize,
12734
+ textureInfos,
12735
+ drawCount,
12736
+ screenToWorld,
12737
+ worldToScreen,
12738
+ screenToWorldDelta,
12739
+ worldToScreenDelta,
12740
+ screenToWorldTransform,
12741
+ drawTile,
12742
+ drawRect,
12743
+ drawRectGradient,
12744
+ drawLineList,
12745
+ drawLine,
12746
+ drawPoly,
12747
+ drawEllipse,
12748
+ drawCircle,
12749
+ drawCanvas2D,
12750
+ drawText,
12751
+ drawTextScreen,
12752
+ setBlendMode,
12753
+ combineCanvases,
12754
+ engineFontImage,
12755
+ FontImage,
12756
+ isFullscreen,
12757
+ toggleFullscreen,
12758
+ setCursor,
12759
+ getCameraSize,
12760
+
12761
+ // WebGL
12762
+ glCanvas,
12763
+ glContext,
12764
+ glAntialias,
12765
+ glClearCanvas,
12766
+ glSetTexture,
12767
+ glCompileShader,
12768
+ glCreateProgram,
12769
+ glCreateTexture,
12770
+ glDeleteTexture,
12771
+ glSetTextureData,
12772
+ glFlush,
12773
+ glCopyToContext,
12774
+ glSetAntialias,
12775
+ glDraw,
12776
+ glDrawPointsTransform,
12777
+ glDrawOutlineTransform,
12778
+ glDrawPoints,
12779
+ glDrawColoredPoints,
12780
+ glSetRenderTarget,
12781
+ glClearRect,
12782
+
12783
+ // Input
12784
+ keyIsDown,
12785
+ keyWasPressed,
12786
+ keyWasReleased,
12787
+ keyDirection,
12788
+ inputClear,
12789
+ inputClearKey,
12790
+ mouseIsDown,
12791
+ mouseWasPressed,
12792
+ mouseWasReleased,
12793
+ mousePos,
12794
+ mousePosScreen,
12795
+ mouseDelta,
12796
+ mouseDeltaScreen,
12797
+ mouseWheel,
12798
+ mouseInWindow,
12799
+ isUsingGamepad,
12800
+ inputPreventDefault,
12801
+ gamepadPrimary,
12802
+ isTouchDevice,
12803
+ setInputPreventDefault,
12804
+ gamepadIsDown,
12805
+ gamepadWasPressed,
12806
+ gamepadWasReleased,
12807
+ gamepadStick,
12808
+ gamepadDpad,
12809
+ gamepadConnected,
12810
+ vibrate,
12811
+ vibrateStop,
12812
+ pointerLockRequest,
12813
+ pointerLockExit,
12814
+ pointerLockIsActive,
12815
+
12816
+ // Audio
12817
+ audioContext,
12818
+ audioMasterGain,
12819
+ audioDefaultSampleRate,
12820
+ Sound,
12821
+ SoundWave,
12822
+ SoundInstance,
12823
+ speak,
12824
+ speakStop,
12825
+ getNoteFrequency,
12826
+ playSamples,
12827
+ zzfx,
12828
+ zzfxG,
12829
+
12830
+ // Base Object
12831
+ EngineObject,
12832
+
12833
+ // Tiles
12834
+ tileCollisionLayers,
12835
+ tileCollisionGetData,
12836
+ tileCollisionTest,
12837
+ tileCollisionRaycast,
12838
+ tileLayersLoad,
12839
+ TileLayerData,
12840
+ CanvasLayer,
12841
+ TileLayer,
12842
+ TileCollisionLayer,
12843
+
12844
+ // Particles
12845
+ ParticleEmitter,
12846
+ Particle,
12847
+
12848
+ // Medals
12849
+ medals,
12850
+ medalsPreventUnlock,
12851
+ medalsInit,
12852
+ Medal,
12693
12853
  }
12694
12854
  /**
12695
12855
  * LittleJS Module Plugins Export