littlejsengine 1.15.8 → 1.15.9

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.
@@ -133,6 +133,7 @@ declare module "littlejsengine" {
133
133
  * @memberof Engine */
134
134
  export function engineInit(gameInit: GameInitCallback, gameUpdate: GameCallback, gameUpdatePost: GameCallback, gameRender: GameCallback, gameRenderPost: GameCallback, imageSources?: Array<string>, rootElement?: HTMLElement): Promise<void>;
135
135
  /** Update each engine object, remove destroyed objects, and update time
136
+ * can be called manually if objects need to be updated outside of main loop
136
137
  * @memberof Engine */
137
138
  export function engineObjectsUpdate(): void;
138
139
  /** Destroy and remove all objects
@@ -348,6 +349,18 @@ declare module "littlejsengine" {
348
349
  * @default Vector2(1920,1080)
349
350
  * @memberof Settings */
350
351
  export let canvasMaxSize: Vector2;
352
+ /** Minimum aspect ratio of the canvas (width/height), unused if 0
353
+ * Can be used with canvasMaxAspect to limit aspect ratio
354
+ * @type {number}
355
+ * @default
356
+ * @memberof Settings */
357
+ export let canvasMinAspect: number;
358
+ /** Maximum aspect ratio of the canvas (width/height), unused if 0
359
+ * Can be used with canvasMinAspect to limit aspect ratio
360
+ * @type {number}
361
+ * @default
362
+ * @memberof Settings */
363
+ export let canvasMaxAspect: number;
351
364
  /** Fixed size of the canvas, if enabled canvas size never changes
352
365
  * - you may also need to set mainCanvasSize if using screen space coords in startup
353
366
  * @type {Vector2}
@@ -557,6 +570,14 @@ declare module "littlejsengine" {
557
570
  * @param {Vector2} size
558
571
  * @memberof Settings */
559
572
  export function setCanvasMaxSize(size: Vector2): void;
573
+ /** Set minimum aspect ratio of the canvas (width/height), unused if 0
574
+ * @param {number} aspect
575
+ * @memberof Settings */
576
+ export function setCanvasMinAspect(aspect: number): void;
577
+ /** Set maximum aspect ratio of the canvas (width/height), unused if 0
578
+ * @param {number} aspect
579
+ * @memberof Settings */
580
+ export function setCanvasMaxAspect(aspect: number): void;
560
581
  /** Set fixed size of the canvas
561
582
  * @param {Vector2} size
562
583
  * @memberof Settings */
@@ -3210,8 +3231,6 @@ declare module "littlejsengine" {
3210
3231
  nativeHeight: number;
3211
3232
  /** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
3212
3233
  navigationObject: any;
3213
- /** @property {number} - Gamepad index to use for UI navigation */
3214
- navigationGamepadIndex: number;
3215
3234
  /** @property {Timer} - Cooldown timer for navigation inputs */
3216
3235
  navigationTimer: Timer;
3217
3236
  /** @property {number} - Time between navigation inputs in seconds */
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.15.8';
36
+ const engineVersion = '1.15.9';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -308,23 +308,48 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
308
308
 
309
309
  if (canvasFixedSize.x)
310
310
  {
311
- // clear canvas and set fixed size
312
- mainCanvas.width = canvasFixedSize.x;
313
- mainCanvas.height = canvasFixedSize.y;
314
-
315
- // fit to window by adding space on top or bottom if necessary
316
- const aspect = innerWidth / innerHeight;
317
- const fixedAspect = mainCanvas.width / mainCanvas.height;
318
- (glCanvas||mainCanvas).style.width = mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
319
- (glCanvas||mainCanvas).style.height = mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
311
+ // set canvas fixed size
312
+ mainCanvasSize = canvasFixedSize.copy();
313
+
314
+ // fit to window using css width and height
315
+ const innerAspect = innerWidth / innerHeight;
316
+ const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
317
+ const w = innerAspect < fixedAspect ? '100%' : '';
318
+ const h = innerAspect < fixedAspect ? '' : '100%';
319
+ overlayCanvas.style.width = mainCanvas.style.width = w;
320
+ overlayCanvas.style.height = mainCanvas.style.height = h;
321
+ if (glCanvas)
322
+ {
323
+ glCanvas.style.width = w;
324
+ glCanvas.style.height = h;
325
+ }
320
326
  }
321
327
  else
322
328
  {
323
- // clear canvas and set size to same as window
324
- mainCanvas.width = min(innerWidth, canvasMaxSize.x);
325
- mainCanvas.height = min(innerHeight, canvasMaxSize.y);
329
+ // get main canvas size based on window size
330
+ mainCanvasSize.x = min(innerWidth, canvasMaxSize.x);
331
+ mainCanvasSize.y = min(innerHeight, canvasMaxSize.y);
332
+
333
+ // responsive aspect ratio with native resolution
334
+ const innerAspect = innerWidth / innerHeight;
335
+ if (canvasMaxAspect && innerAspect > canvasMaxAspect)
336
+ {
337
+ // full height
338
+ const w = mainCanvasSize.y * canvasMaxAspect | 0;
339
+ mainCanvasSize.x = min(w, canvasMaxSize.x);
340
+ }
341
+ else if (innerAspect < canvasMinAspect)
342
+ {
343
+ // full width
344
+ const h = mainCanvasSize.x / canvasMinAspect | 0;
345
+ mainCanvasSize.y = min(h, canvasMaxSize.y);
346
+ }
326
347
  }
327
348
 
349
+ // clear main and overlay canvas and set size
350
+ overlayCanvas.width = mainCanvas.width = mainCanvasSize.x;
351
+ overlayCanvas.height = mainCanvas.height = mainCanvasSize.y;
352
+
328
353
  // apply the clear color to main canvas
329
354
  if (canvasClearColor.a > 0)
330
355
  {
@@ -333,13 +358,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
333
358
  mainContext.fillStyle = BLACK.toString();
334
359
  }
335
360
 
336
- // clear overlay canvas and set size
337
- overlayCanvas.width = mainCanvas.width;
338
- overlayCanvas.height = mainCanvas.height;
339
-
340
- // save canvas size
341
- mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
342
-
343
361
  // set default line join and cap
344
362
  const lineJoin = 'round', lineCap = 'round';
345
363
  mainContext.lineJoin = overlayContext.lineJoin = lineJoin;
@@ -363,7 +381,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
363
381
  'user-select:none;' + // prevent hold to select
364
382
  '-webkit-user-select:none;' + // compatibility for ios
365
383
  'touch-action:none;' + // prevent mobile pinch to resize
366
- '-webkit-touch-callout:none';// compatibility for ios
384
+ '-webkit-touch-callout:none'; // compatibility for ios
367
385
  rootElement.style.cssText = styleRoot;
368
386
  drawCanvas = mainCanvas = document.createElement('canvas');
369
387
  rootElement.appendChild(mainCanvas);
@@ -380,7 +398,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
380
398
  rootElement.appendChild(overlayCanvas);
381
399
  overlayContext = overlayCanvas.getContext('2d');
382
400
 
383
- // set canvases
401
+ // setup canvases
402
+ // transform way is still more reliable then flexbox or grid
384
403
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
385
404
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
386
405
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
@@ -444,9 +463,176 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
444
463
  // wait for all the promises to finish
445
464
  await Promise.all(promises);
446
465
  return startEngine();
466
+
467
+ ///////////////////////////////////////////////////////////////////////////
468
+ // LittleJS Splash Screen
469
+ function drawEngineSplashScreen(t)
470
+ {
471
+ const x = overlayContext;
472
+ const w = overlayCanvas.width = innerWidth;
473
+ const h = overlayCanvas.height = innerHeight;
474
+
475
+ {
476
+ // background
477
+ const p3 = percent(t, 1, .8);
478
+ const p4 = percent(t, 0, .5);
479
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
480
+ g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
481
+ g.addColorStop(1,hsl(0,0,0,p3).toString());
482
+ x.save();
483
+ x.fillStyle = g;
484
+ x.fillRect(0,0,w,h);
485
+ }
486
+
487
+ // draw LittleJS logo...
488
+ const rect = (X, Y, W, H, C)=>
489
+ {
490
+ x.beginPath();
491
+ x.rect(X,Y,W,C?H*p:H);
492
+ x.fillStyle = C;
493
+ C ? x.fill() : x.stroke();
494
+ };
495
+ const line = (X, Y, Z, W)=>
496
+ {
497
+ x.beginPath();
498
+ x.lineTo(X,Y);
499
+ x.lineTo(Z,W);
500
+ x.stroke();
501
+ };
502
+ const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
503
+ {
504
+ const D = (A+B)/2, E = p*(B-A)/2;
505
+ x.beginPath();
506
+ F && x.lineTo(X,Y);
507
+ x.arc(X,Y,R,D-E,D+E);
508
+ x.fillStyle = C;
509
+ C ? x.fill() : x.stroke();
510
+ };
511
+ const color = (c=0, l=0) =>
512
+ hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();
513
+ const alpha = wave(1,1,t);
514
+ const p = percent(alpha, .1, .5);
515
+
516
+ // setup
517
+ x.translate(w/2,h/2);
518
+ const size = min(6, min(w,h)/99); // fit to screen
519
+ x.scale(size,size);
520
+ x.translate(-40,-35);
521
+ x.lineJoin = x.lineCap = 'round';
522
+ x.lineWidth = .1 + p*1.9;
523
+
524
+ // drawing effect
525
+ const p2 = percent(alpha,.1,1);
526
+ x.setLineDash([99*p2,99]);
527
+
528
+ // cab top
529
+ rect(7,16,18,-8,color(2,2));
530
+ rect(7,8,18,4,color(2,3));
531
+ rect(25,8,8,8,color(2,1));
532
+ rect(25,8,-18,8);
533
+ rect(25,8,8,8);
534
+
535
+ // cab
536
+ rect(25,16,7,23,color());
537
+ rect(11,39,14,-23,color(1,1));
538
+ rect(11,16,14,18,color(1,2));
539
+ rect(11,16,14,8,color(1,3));
540
+ rect(25,16,-14,24);
541
+
542
+ // cab window
543
+ rect(15,29,6,-9,color(2,2));
544
+ circle(15,21,5,0,PI/2,color(2,4),1);
545
+ rect(21,21,-6,9);
546
+
547
+ // little stack
548
+ rect(37,14,9,6,color(3,2));
549
+ rect(37,14,4.5,6,color(3,3));
550
+ rect(37,14,9,6);
551
+
552
+ // big stack
553
+ rect(50,20,10,-8,color(0,1));
554
+ rect(50,20,6.5,-8,color(0,2));
555
+ rect(50,20,3.5,-8,color(0,3));
556
+ rect(50,20,10,-8);
557
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
558
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
559
+ circle(55,2,11.4,.5,PI-.5);
560
+ rect(45,7,20,-7,color(0,2));
561
+ rect(45,-1,20,4,color(0,3));
562
+ rect(45,-1,20,8);
563
+
564
+ // engine
565
+ for (let i=5; i--;)
566
+ {
567
+ // stagger radius to fix slight seam
568
+ circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
569
+ circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
570
+ circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
571
+ }
572
+
573
+ // engine outline
574
+ circle(36,30,10,PI/2,PI*3/2);
575
+ circle(48,30,10,PI/2,PI*3/2);
576
+ circle(60,30,10);
577
+ line(36,20,60,20);
578
+
579
+ // engine front light
580
+ circle(60,30,4,PI,3*PI,color(3,2));
581
+ circle(60,30,4,PI,2*PI,color(3,3));
582
+ circle(60,30,4,PI,3*PI);
583
+
584
+ // front brush
585
+ for (let i=6; i--;)
586
+ {
587
+ x.beginPath();
588
+ x.lineTo(53,54);
589
+ x.lineTo(53,40);
590
+ x.lineTo(53+(1+i*2.9)*p,40);
591
+ x.lineTo(53+(4+i*3.5)*p,54);
592
+ x.fillStyle = color(0,i%2+2);
593
+ x.fill();
594
+ i%2 && x.stroke();
595
+ }
596
+
597
+ // wheels
598
+ rect(6,40,5,5);
599
+ rect(6,40,5,5,color());
600
+ rect(15,54,38,-14,color());
601
+ for (let i=3; i--;)
602
+ for (let j=2; j--;)
603
+ {
604
+ circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
605
+ x.stroke();
606
+ circle(15*i+15,47,j?7:1,0,PI,color(i,2));
607
+ x.stroke();
608
+ }
609
+ line(6,40,68,40); // center
610
+ line(77,54,4,54); // bottom
611
+
612
+ // draw engine name
613
+ const s = engineName;
614
+ x.font = '900 16px arial';
615
+ x.textAlign = 'center';
616
+ x.textBaseline = 'top';
617
+ x.lineWidth = .1+p*3.9;
618
+ let w2 = 0;
619
+ for (let i=0; i<s.length; ++i)
620
+ w2 += x.measureText(s[i]).width;
621
+ for (let j=2; j--;)
622
+ for (let i=0, X=41-w2/2; i<s.length; ++i)
623
+ {
624
+ x.fillStyle = color(i,2);
625
+ const w = x.measureText(s[i]).width;
626
+ x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
627
+ X += w;
628
+ }
629
+ x.restore();
630
+ }
631
+ ///////////////////////////////////////////////////////////////////////////
447
632
  }
448
633
 
449
634
  /** Update each engine object, remove destroyed objects, and update time
635
+ * can be called manually if objects need to be updated outside of main loop
450
636
  * @memberof Engine */
451
637
  function engineObjectsUpdate()
452
638
  {
@@ -498,18 +684,21 @@ function engineObjectsDestroy()
498
684
  function engineObjectsCollect(pos, size, objects=engineObjects)
499
685
  {
500
686
  const collectedObjects = [];
501
- if (!pos) // all objects
687
+ if (!pos)
502
688
  {
689
+ // all objects
503
690
  for (const o of objects)
504
691
  collectedObjects.push(o);
505
692
  }
506
- else if (size instanceof Vector2) // bounding box test
693
+ else if (size instanceof Vector2)
507
694
  {
695
+ // bounding box test
508
696
  for (const o of objects)
509
697
  o.isOverlapping(pos, size) && collectedObjects.push(o);
510
698
  }
511
- else // circle test
699
+ else
512
700
  {
701
+ // circle test
513
702
  const sizeSquared = size*size;
514
703
  for (const o of objects)
515
704
  pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
@@ -552,173 +741,6 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
552
741
 
553
742
  debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
554
743
  return hitObjects;
555
- }
556
-
557
- ///////////////////////////////////////////////////////////////////////////////
558
- // LittleJS splash screen and logo
559
-
560
- function drawEngineSplashScreen(t)
561
- {
562
- const x = overlayContext;
563
- const w = overlayCanvas.width = innerWidth;
564
- const h = overlayCanvas.height = innerHeight;
565
-
566
- {
567
- // background
568
- const p3 = percent(t, 1, .8);
569
- const p4 = percent(t, 0, .5);
570
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
571
- g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
572
- g.addColorStop(1,hsl(0,0,0,p3).toString());
573
- x.save();
574
- x.fillStyle = g;
575
- x.fillRect(0,0,w,h);
576
- }
577
-
578
- // draw LittleJS logo...
579
- const rect = (X, Y, W, H, C)=>
580
- {
581
- x.beginPath();
582
- x.rect(X,Y,W,C?H*p:H);
583
- x.fillStyle = C;
584
- C ? x.fill() : x.stroke();
585
- };
586
- const line = (X, Y, Z, W)=>
587
- {
588
- x.beginPath();
589
- x.lineTo(X,Y);
590
- x.lineTo(Z,W);
591
- x.stroke();
592
- };
593
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
594
- {
595
- const D = (A+B)/2, E = p*(B-A)/2;
596
- x.beginPath();
597
- F && x.lineTo(X,Y);
598
- x.arc(X,Y,R,D-E,D+E);
599
- x.fillStyle = C;
600
- C ? x.fill() : x.stroke();
601
- };
602
- const color = (c=0, l=0) =>
603
- hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();
604
- const alpha = wave(1,1,t);
605
- const p = percent(alpha, .1, .5);
606
-
607
- // setup
608
- x.translate(w/2,h/2);
609
- const size = min(6, min(w,h)/99); // fit to screen
610
- x.scale(size,size);
611
- x.translate(-40,-35);
612
- x.lineJoin = x.lineCap = 'round';
613
- x.lineWidth = .1 + p*1.9;
614
-
615
- // drawing effect
616
- const p2 = percent(alpha,.1,1);
617
- x.setLineDash([99*p2,99]);
618
-
619
- // cab top
620
- rect(7,16,18,-8,color(2,2));
621
- rect(7,8,18,4,color(2,3));
622
- rect(25,8,8,8,color(2,1));
623
- rect(25,8,-18,8);
624
- rect(25,8,8,8);
625
-
626
- // cab
627
- rect(25,16,7,23,color());
628
- rect(11,39,14,-23,color(1,1));
629
- rect(11,16,14,18,color(1,2));
630
- rect(11,16,14,8,color(1,3));
631
- rect(25,16,-14,24);
632
-
633
- // cab window
634
- rect(15,29,6,-9,color(2,2));
635
- circle(15,21,5,0,PI/2,color(2,4),1);
636
- rect(21,21,-6,9);
637
-
638
- // little stack
639
- rect(37,14,9,6,color(3,2));
640
- rect(37,14,4.5,6,color(3,3));
641
- rect(37,14,9,6);
642
-
643
- // big stack
644
- rect(50,20,10,-8,color(0,1));
645
- rect(50,20,6.5,-8,color(0,2));
646
- rect(50,20,3.5,-8,color(0,3));
647
- rect(50,20,10,-8);
648
- circle(55,2,11.4,.5,PI-.5,color(3,3));
649
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
650
- circle(55,2,11.4,.5,PI-.5);
651
- rect(45,7,20,-7,color(0,2));
652
- rect(45,-1,20,4,color(0,3));
653
- rect(45,-1,20,8);
654
-
655
- // engine
656
- for (let i=5; i--;)
657
- {
658
- // stagger radius to fix slight seam
659
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
660
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
661
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
662
- }
663
-
664
- // engine outline
665
- circle(36,30,10,PI/2,PI*3/2);
666
- circle(48,30,10,PI/2,PI*3/2);
667
- circle(60,30,10);
668
- line(36,20,60,20);
669
-
670
- // engine front light
671
- circle(60,30,4,PI,3*PI,color(3,2));
672
- circle(60,30,4,PI,2*PI,color(3,3));
673
- circle(60,30,4,PI,3*PI);
674
-
675
- // front brush
676
- for (let i=6; i--;)
677
- {
678
- x.beginPath();
679
- x.lineTo(53,54);
680
- x.lineTo(53,40);
681
- x.lineTo(53+(1+i*2.9)*p,40);
682
- x.lineTo(53+(4+i*3.5)*p,54);
683
- x.fillStyle = color(0,i%2+2);
684
- x.fill();
685
- i%2 && x.stroke();
686
- }
687
-
688
- // wheels
689
- rect(6,40,5,5);
690
- rect(6,40,5,5,color());
691
- rect(15,54,38,-14,color());
692
- for (let i=3; i--;)
693
- for (let j=2; j--;)
694
- {
695
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
696
- x.stroke();
697
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
698
- x.stroke();
699
- }
700
- line(6,40,68,40); // center
701
- line(77,54,4,54); // bottom
702
-
703
- // draw engine name
704
- const s = engineName;
705
- x.font = '900 16px arial';
706
- x.textAlign = 'center';
707
- x.textBaseline = 'top';
708
- x.lineWidth = .1+p*3.9;
709
- let w2 = 0;
710
- for (let i=0; i<s.length; ++i)
711
- w2 += x.measureText(s[i]).width;
712
- for (let j=2; j--;)
713
- for (let i=0, X=41-w2/2; i<s.length; ++i)
714
- {
715
- x.fillStyle = color(i,2);
716
- const w = x.measureText(s[i]).width;
717
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
718
- X += w;
719
- }
720
-
721
- x.restore();
722
744
  }
723
745
  /**
724
746
  * LittleJS Debug System
@@ -2716,6 +2738,20 @@ let canvasClearColor = CLEAR_BLACK;
2716
2738
  * @memberof Settings */
2717
2739
  let canvasMaxSize = vec2(1920, 1080);
2718
2740
 
2741
+ /** Minimum aspect ratio of the canvas (width/height), unused if 0
2742
+ * Can be used with canvasMaxAspect to limit aspect ratio
2743
+ * @type {number}
2744
+ * @default
2745
+ * @memberof Settings */
2746
+ let canvasMinAspect = 0;
2747
+
2748
+ /** Maximum aspect ratio of the canvas (width/height), unused if 0
2749
+ * Can be used with canvasMinAspect to limit aspect ratio
2750
+ * @type {number}
2751
+ * @default
2752
+ * @memberof Settings */
2753
+ let canvasMaxAspect = 0;
2754
+
2719
2755
  /** Fixed size of the canvas, if enabled canvas size never changes
2720
2756
  * - you may also need to set mainCanvasSize if using screen space coords in startup
2721
2757
  * @type {Vector2}
@@ -3012,6 +3048,16 @@ function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
3012
3048
  * @memberof Settings */
3013
3049
  function setCanvasMaxSize(size) { canvasMaxSize = size.copy(); }
3014
3050
 
3051
+ /** Set minimum aspect ratio of the canvas (width/height), unused if 0
3052
+ * @param {number} aspect
3053
+ * @memberof Settings */
3054
+ function setCanvasMinAspect(aspect) { canvasMinAspect = aspect; }
3055
+
3056
+ /** Set maximum aspect ratio of the canvas (width/height), unused if 0
3057
+ * @param {number} aspect
3058
+ * @memberof Settings */
3059
+ function setCanvasMaxAspect(aspect) { canvasMaxAspect = aspect; }
3060
+
3015
3061
  /** Set fixed size of the canvas
3016
3062
  * @param {Vector2} size
3017
3063
  * @memberof Settings */
@@ -5100,6 +5146,16 @@ function gamepadConnected(gamepad=gamepadPrimary)
5100
5146
  return !!inputData[gamepad+1];
5101
5147
  }
5102
5148
 
5149
+ /** Returns how many control sticks the passed in gamepad has
5150
+ * @param {number} [gamepad]
5151
+ * @return {number}
5152
+ * @memberof Input */
5153
+ function gamepadStickCount(gamepad=gamepadPrimary)
5154
+ {
5155
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
5156
+ return gamepadStickData[gamepad]?.length ?? 0;
5157
+ }
5158
+
5103
5159
  /** True if a touch device has been detected
5104
5160
  * @memberof Input */
5105
5161
  const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
@@ -5440,9 +5496,11 @@ function inputUpdate()
5440
5496
  dpad.set(x, -y);
5441
5497
  sticks[0] = dpad.clampLength(); // clamp to circle
5442
5498
  }
5443
- const rightTouchStick = touchGamepadSticks[1] ?? vec2();
5444
5499
  if (touchGamepadButtonCount === 1)
5500
+ {
5501
+ const rightTouchStick = touchGamepadSticks[1] ?? vec2();
5445
5502
  sticks[1] = applyDeadZones(rightTouchStick);
5503
+ }
5446
5504
 
5447
5505
  // read virtual gamepad buttons
5448
5506
  const data = inputData[1] ?? (inputData[1] = []);
@@ -6534,8 +6592,7 @@ class CanvasLayer extends EngineObject
6534
6592
  /** Destroy this canvas layer */
6535
6593
  destroy()
6536
6594
  {
6537
- if (this.destroyed)
6538
- return;
6595
+ if (this.destroyed) return;
6539
6596
 
6540
6597
  this.textureInfo.destroyWebGLTexture();
6541
6598
  super.destroy();
@@ -7087,9 +7144,9 @@ class ParticleEmitter extends EngineObject
7087
7144
  /** @property {Color} - Color at start of life 2, randomized between start colors */
7088
7145
  this.colorStartB = colorStartB.copy();
7089
7146
  /** @property {Color} - Color at end of life 1, randomized between end colors */
7090
- this.colorEndA = colorEndA.copy();
7147
+ this.colorEndA = colorEndA.copy();
7091
7148
  /** @property {Color} - Color at end of life 2, randomized between end colors */
7092
- this.colorEndB = colorEndB.copy();
7149
+ this.colorEndB = colorEndB.copy();
7093
7150
  /** @property {boolean} - Should color be randomized linearly or across each component */
7094
7151
  this.randomColorLinear = randomColorLinear;
7095
7152
 
@@ -7173,8 +7230,10 @@ class ParticleEmitter extends EngineObject
7173
7230
  if (debugParticles)
7174
7231
  {
7175
7232
  // show emitter bounds
7176
- const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
7177
- debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
7233
+ if (typeof this.emitSize === 'number')
7234
+ debugCircle(this.pos, this.emitSize/2, '#0f0');
7235
+ else
7236
+ debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle);
7178
7237
  }
7179
7238
  }
7180
7239
 
@@ -7184,8 +7243,8 @@ class ParticleEmitter extends EngineObject
7184
7243
  {
7185
7244
  // spawn a particle
7186
7245
  let pos = typeof this.emitSize === 'number' ? // check if number was used
7187
- randInCircle(this.emitSize/2) // circle emitter
7188
- : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
7246
+ randInCircle(this.emitSize/2) // circle emitter
7247
+ : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
7189
7248
  .multiply(this.emitSize).rotate(this.angle)
7190
7249
  let angle = rand(this.particleConeAngle, -this.particleConeAngle);
7191
7250
  if (!this.localSpace)
@@ -8980,8 +9039,6 @@ class UISystemPlugin
8980
9039
  // navigation properties
8981
9040
  /** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
8982
9041
  this.navigationObject = undefined;
8983
- /** @property {number} - Gamepad index to use for UI navigation */
8984
- this.navigationGamepadIndex = 0;
8985
9042
  /** @property {Timer} - Cooldown timer for navigation inputs */
8986
9043
  this.navigationTimer = new Timer(undefined, true);
8987
9044
  /** @property {number} - Time between navigation inputs in seconds */
@@ -9389,9 +9446,8 @@ class UISystemPlugin
9389
9446
  const both = uiSystem.navigationDirection === 2;
9390
9447
  if (isUsingGamepad)
9391
9448
  {
9392
- const gamepad = this.navigationGamepadIndex;
9393
- const stick = gamepadStick(0, gamepad);
9394
- const dpad = gamepadDpad(gamepad);
9449
+ const stick = gamepadStick(0, gamepadPrimary);
9450
+ const dpad = gamepadDpad(gamepadPrimary);
9395
9451
  if (both)
9396
9452
  return -(stick.y || dpad.y) || (stick.x || dpad.x);
9397
9453
  return vertical ? -(stick.y || dpad.y) : (stick.x || dpad.x);
@@ -9417,9 +9473,8 @@ class UISystemPlugin
9417
9473
  const vertical = uiSystem.navigationDirection === 1;
9418
9474
  if (isUsingGamepad)
9419
9475
  {
9420
- const gamepad = this.navigationGamepadIndex;
9421
- const stick = gamepadStick(0, gamepad);
9422
- const dpad = gamepadDpad(gamepad);
9476
+ const stick = gamepadStick(0, gamepadPrimary);
9477
+ const dpad = gamepadDpad(gamepadPrimary);
9423
9478
  return !vertical ? (stick.y || dpad.y) : (stick.x || dpad.x);
9424
9479
  }
9425
9480
  const back = !vertical ? 'ArrowUp' : 'ArrowLeft';
@@ -9431,8 +9486,7 @@ class UISystemPlugin
9431
9486
  * @return {boolean} */
9432
9487
  getNavigationWasPressed()
9433
9488
  {
9434
- const gamepad = this.navigationGamepadIndex;
9435
- return isUsingGamepad ? gamepadWasPressed(0, gamepad) :
9489
+ return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
9436
9490
  keyWasPressed('Space') || keyWasPressed('Enter');
9437
9491
  }
9438
9492
 
@@ -12352,6 +12406,8 @@ export
12352
12406
  canvasColorTiles,
12353
12407
  canvasClearColor,
12354
12408
  canvasMaxSize,
12409
+ canvasMinAspect,
12410
+ canvasMaxAspect,
12355
12411
  canvasFixedSize,
12356
12412
  canvasPixelated,
12357
12413
  overlayCanvasPixelated,
@@ -12395,6 +12451,8 @@ export
12395
12451
  setCanvasColorTiles,
12396
12452
  setCanvasClearColor,
12397
12453
  setCanvasMaxSize,
12454
+ setCanvasMinAspect,
12455
+ setCanvasMaxAspect,
12398
12456
  setCanvasFixedSize,
12399
12457
  setCanvasPixelated,
12400
12458
  setOverlayCanvasPixelated,