littlejsengine 1.15.8 → 1.16.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 (53) hide show
  1. package/dist/littlejs.d.ts +87 -85
  2. package/dist/littlejs.esm.js +578 -532
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +562 -521
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +485 -444
  7. package/examples/box2d/game.js +2 -2
  8. package/examples/breakout/game.js +3 -3
  9. package/examples/electron/game.js +1 -2
  10. package/examples/electron/index.html +2 -2
  11. package/examples/htmlMenu/game.js +0 -1
  12. package/examples/index.html +1 -1
  13. package/examples/module/game.js +1 -2
  14. package/examples/particles/index.html +1 -1
  15. package/examples/platformer/game.js +11 -4
  16. package/examples/puzzle/game.js +2 -2
  17. package/examples/shorts/base.html +3 -4
  18. package/examples/shorts/box2dPool.js +2 -2
  19. package/examples/shorts/empty.js +1 -1
  20. package/examples/shorts/{systemFont.js → fontImage.js} +3 -3
  21. package/examples/shorts/fps.js +1 -1
  22. package/examples/shorts/helloWorld.js +2 -2
  23. package/examples/shorts/nineSlice.js +2 -2
  24. package/examples/shorts/slidingPuzzle.js +1 -1
  25. package/examples/shorts/uiSystem.js +1 -1
  26. package/examples/starter/game.js +1 -2
  27. package/examples/starter/index.html +2 -3
  28. package/examples/stress/index.html +1 -1
  29. package/examples/typescript/game.js +1 -2
  30. package/examples/typescript/game.ts +1 -2
  31. package/examples/uiSystem/game.js +4 -7
  32. package/package.json +1 -1
  33. package/plugins/box2d.js +8 -8
  34. package/plugins/drawUtilities.js +6 -6
  35. package/plugins/postProcess.js +13 -20
  36. package/plugins/uiSystem.js +15 -3
  37. package/reference.md +4 -6
  38. package/src/engine.js +240 -53
  39. package/src/engineAudio.js +3 -3
  40. package/src/engineBuild.js +0 -1
  41. package/src/engineDebug.js +78 -78
  42. package/src/engineDraw.js +107 -109
  43. package/src/engineExport.js +16 -11
  44. package/src/engineInput.js +14 -2
  45. package/src/engineMedals.js +3 -3
  46. package/src/engineObject.js +4 -2
  47. package/src/engineParticles.js +8 -6
  48. package/src/engineRelease.js +1 -1
  49. package/src/engineSettings.js +44 -30
  50. package/src/engineTileLayer.js +6 -6
  51. package/src/engineUtilities.js +2 -10
  52. package/src/engineWebGL.js +6 -5
  53. package/src/engineSplash.js +0 -173
@@ -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.16.1';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -180,8 +180,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
180
180
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
181
181
 
182
182
  // disable smoothing for pixel art
183
- overlayContext.imageSmoothingEnabled =
184
- mainContext.imageSmoothingEnabled = !tilesPixelated;
183
+ mainContext.imageSmoothingEnabled = !tilesPixelated;
185
184
 
186
185
  // setup gl rendering if enabled
187
186
  glPreRender();
@@ -193,7 +192,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
193
192
  // update time keeping
194
193
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
195
194
  frameTimeLastMS = frameTimeMS;
196
- if (debug || showWatermark)
195
+ if (debug || debugWatermark)
197
196
  averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
198
197
  const debugSpeedUp = debug && keyIsDown('Equal'); // +
199
198
  const debugSpeedDown = debug && keyIsDown('Minus'); // -
@@ -221,6 +220,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
221
220
  debugUpdate();
222
221
  gameUpdatePost();
223
222
  inputUpdatePost();
223
+ if (debugVideoCaptureIsActive())
224
+ renderFrame();
224
225
  }
225
226
  else
226
227
  {
@@ -284,19 +285,19 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
284
285
  glFlush();
285
286
  debugVideoCaptureUpdate();
286
287
 
287
- if (showWatermark && !debugVideoCaptureIsActive())
288
+ if (debugWatermark && !debugVideoCaptureIsActive())
288
289
  {
289
290
  // update fps display
290
- overlayContext.textAlign = 'right';
291
- overlayContext.textBaseline = 'top';
292
- overlayContext.font = '1em monospace';
293
- overlayContext.fillStyle = '#000';
291
+ mainContext.textAlign = 'right';
292
+ mainContext.textBaseline = 'top';
293
+ mainContext.font = '1em monospace';
294
+ mainContext.fillStyle = '#000';
294
295
  const text = engineName + ' ' + 'v' + engineVersion + ' / '
295
296
  + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
296
297
  + (glEnable ? ' GL' : ' 2D') ;
297
- overlayContext.fillText(text, mainCanvas.width-3, 3);
298
- overlayContext.fillStyle = '#fff';
299
- overlayContext.fillText(text, mainCanvas.width-2, 2);
298
+ mainContext.fillText(text, mainCanvas.width-3, 3);
299
+ mainContext.fillStyle = '#fff';
300
+ mainContext.fillText(text, mainCanvas.width-2, 2);
300
301
  }
301
302
  drawCount = 0;
302
303
  }
@@ -308,42 +309,60 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
308
309
 
309
310
  if (canvasFixedSize.x)
310
311
  {
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%';
312
+ // set canvas fixed size
313
+ mainCanvasSize = canvasFixedSize.copy();
314
+
315
+ // fit to window using css width and height
316
+ const innerAspect = innerWidth / innerHeight;
317
+ const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
318
+ const w = innerAspect < fixedAspect ? '100%' : '';
319
+ const h = innerAspect < fixedAspect ? '' : '100%';
320
+ mainCanvas.style.width = w;
321
+ mainCanvas.style.height = h;
322
+ if (glCanvas)
323
+ {
324
+ glCanvas.style.width = w;
325
+ glCanvas.style.height = h;
326
+ }
320
327
  }
321
328
  else
322
329
  {
323
- // clear canvas and set size to same as window
324
- mainCanvas.width = min(innerWidth, canvasMaxSize.x);
325
- mainCanvas.height = min(innerHeight, canvasMaxSize.y);
330
+ // get main canvas size based on window size
331
+ mainCanvasSize.x = min(innerWidth, canvasMaxSize.x);
332
+ mainCanvasSize.y = min(innerHeight, canvasMaxSize.y);
333
+
334
+ // responsive aspect ratio with native resolution
335
+ const innerAspect = innerWidth / innerHeight;
336
+ ASSERT(canvasMinAspect <= canvasMaxAspect);
337
+ if (canvasMaxAspect && innerAspect > canvasMaxAspect)
338
+ {
339
+ // full height
340
+ const w = mainCanvasSize.y * canvasMaxAspect | 0;
341
+ mainCanvasSize.x = min(w, canvasMaxSize.x);
342
+ }
343
+ else if (innerAspect < canvasMinAspect)
344
+ {
345
+ // full width
346
+ const h = mainCanvasSize.x / canvasMinAspect | 0;
347
+ mainCanvasSize.y = min(h, canvasMaxSize.y);
348
+ }
326
349
  }
327
350
 
351
+ // clear main canvas and set size
352
+ mainCanvas.width = mainCanvasSize.x;
353
+ mainCanvas.height = mainCanvasSize.y;
354
+
328
355
  // apply the clear color to main canvas
329
- if (canvasClearColor.a > 0)
356
+ if (canvasClearColor.a > 0 && !glEnable)
330
357
  {
331
358
  mainContext.fillStyle = canvasClearColor.toString();
332
359
  mainContext.fillRect(0, 0, mainCanvasSize.x, mainCanvasSize.y);
333
360
  mainContext.fillStyle = BLACK.toString();
334
361
  }
335
362
 
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
363
  // set default line join and cap
344
- const lineJoin = 'round', lineCap = 'round';
345
- mainContext.lineJoin = overlayContext.lineJoin = lineJoin;
346
- mainContext.lineCap = overlayContext.lineCap = lineCap;
364
+ mainContext.lineJoin = 'round';
365
+ mainContext.lineCap = 'round';
347
366
  }
348
367
 
349
368
  // wait for gameInit to load
@@ -355,6 +374,9 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
355
374
  if (headlessMode)
356
375
  return startEngine();
357
376
 
377
+ // setup webgl
378
+ glInit(rootElement);
379
+
358
380
  // setup html
359
381
  const styleRoot =
360
382
  'margin:0;' + // fill the window
@@ -363,37 +385,32 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
363
385
  'user-select:none;' + // prevent hold to select
364
386
  '-webkit-user-select:none;' + // compatibility for ios
365
387
  'touch-action:none;' + // prevent mobile pinch to resize
366
- '-webkit-touch-callout:none';// compatibility for ios
388
+ '-webkit-touch-callout:none'; // compatibility for ios
367
389
  rootElement.style.cssText = styleRoot;
368
- drawCanvas = mainCanvas = document.createElement('canvas');
369
- rootElement.appendChild(mainCanvas);
390
+ drawCanvas = mainCanvas = rootElement.appendChild(document.createElement('canvas'));
370
391
  drawContext = mainContext = mainCanvas.getContext('2d');
371
392
 
372
393
  // init stuff and start engine
373
394
  inputInit();
374
395
  audioInit();
375
396
  debugInit();
376
- glInit();
377
397
 
378
- // create overlay canvas for hud to appear above gl canvas
379
- overlayCanvas = document.createElement('canvas')
380
- rootElement.appendChild(overlayCanvas);
381
- overlayContext = overlayCanvas.getContext('2d');
382
-
383
- // set canvases
398
+ // setup canvases
399
+ // transform way is still more reliable then flexbox or grid
384
400
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
385
401
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
386
- mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
402
+ mainCanvas.style.cssText = styleCanvas;
387
403
  if (glCanvas)
388
404
  glCanvas.style.cssText = styleCanvas;
389
405
  setCanvasPixelated(canvasPixelated);
390
- setOverlayCanvasPixelated(overlayCanvasPixelated);
391
406
  updateCanvas();
392
407
  glPreRender();
393
408
 
394
- // create offscreen canvas for image processing
395
- workCanvas = new OffscreenCanvas(256, 256);
396
- workContext = workCanvas.getContext('2d', { willReadFrequently: true });
409
+ // create offscreen canvases for image processing
410
+ workCanvas = new OffscreenCanvas(64, 64);
411
+ workContext = workCanvas.getContext('2d');
412
+ workReadCanvas = new OffscreenCanvas(64, 64);
413
+ workReadContext = workReadCanvas.getContext('2d', { willReadFrequently: true });
397
414
 
398
415
  // create promises for loading images
399
416
  const promises = imageSources.map((src, textureIndex)=>
@@ -444,9 +461,176 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
444
461
  // wait for all the promises to finish
445
462
  await Promise.all(promises);
446
463
  return startEngine();
464
+
465
+ ///////////////////////////////////////////////////////////////////////////
466
+ // LittleJS Splash Screen
467
+ function drawEngineSplashScreen(t)
468
+ {
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();
628
+ }
629
+ ///////////////////////////////////////////////////////////////////////////
447
630
  }
448
631
 
449
632
  /** Update each engine object, remove destroyed objects, and update time
633
+ * can be called manually if objects need to be updated outside of main loop
450
634
  * @memberof Engine */
451
635
  function engineObjectsUpdate()
452
636
  {
@@ -498,18 +682,21 @@ function engineObjectsDestroy()
498
682
  function engineObjectsCollect(pos, size, objects=engineObjects)
499
683
  {
500
684
  const collectedObjects = [];
501
- if (!pos) // all objects
685
+ if (!pos)
502
686
  {
687
+ // all objects
503
688
  for (const o of objects)
504
689
  collectedObjects.push(o);
505
690
  }
506
- else if (size instanceof Vector2) // bounding box test
691
+ else if (size instanceof Vector2)
507
692
  {
693
+ // bounding box test
508
694
  for (const o of objects)
509
695
  o.isOverlapping(pos, size) && collectedObjects.push(o);
510
696
  }
511
- else // circle test
697
+ else
512
698
  {
699
+ // circle test
513
700
  const sizeSquared = size*size;
514
701
  for (const o of objects)
515
702
  pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
@@ -552,173 +739,6 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
552
739
 
553
740
  debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
554
741
  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
742
  }
723
743
  /**
724
744
  * LittleJS - Release Mode
@@ -726,7 +746,7 @@ function drawEngineSplashScreen(t)
726
746
  * - Debug functionality is disabled to reduce size and increase performance
727
747
  */
728
748
 
729
- let showWatermark = 0;
749
+ let debugWatermark = 0;
730
750
  let debugKey = '';
731
751
  const debug = 0;
732
752
  const debugOverlay = 0;
@@ -886,11 +906,7 @@ function percent(value, valueA, valueB)
886
906
  * @return {number}
887
907
  * @memberof Utilities */
888
908
  function lerp(valueA, valueB, percent)
889
- {
890
- if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
891
- console.warn('lerp() parameter order changed! use lerp(start, end, p)');
892
- return valueA + clamp(percent) * (valueB-valueA);
893
- }
909
+ { return valueA + clamp(percent) * (valueB-valueA); }
894
910
 
895
911
  /** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
896
912
  * A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
@@ -921,11 +937,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
921
937
  * @return {number}
922
938
  * @memberof Utilities */
923
939
  function lerpWrap(valueA, valueB, percent, wrapSize=1)
924
- {
925
- if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
926
- console.warn('lerpWrap() parameter order changed! use lerpWrap(start, end, p)');
927
- return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize);
928
- }
940
+ { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
929
941
 
930
942
  /** Returns signed wrapped distance between the two angles passed in
931
943
  * @param {number} angleA
@@ -2039,6 +2051,20 @@ let canvasClearColor = CLEAR_BLACK;
2039
2051
  * @memberof Settings */
2040
2052
  let canvasMaxSize = vec2(1920, 1080);
2041
2053
 
2054
+ /** Minimum aspect ratio of the canvas (width/height), unused if 0
2055
+ * Can be used with canvasMaxAspect to limit aspect ratio
2056
+ * @type {number}
2057
+ * @default
2058
+ * @memberof Settings */
2059
+ let canvasMinAspect = 0;
2060
+
2061
+ /** Maximum aspect ratio of the canvas (width/height), unused if 0
2062
+ * Can be used with canvasMinAspect to limit aspect ratio
2063
+ * @type {number}
2064
+ * @default
2065
+ * @memberof Settings */
2066
+ let canvasMaxAspect = 0;
2067
+
2042
2068
  /** Fixed size of the canvas, if enabled canvas size never changes
2043
2069
  * - you may also need to set mainCanvasSize if using screen space coords in startup
2044
2070
  * @type {Vector2}
@@ -2051,15 +2077,7 @@ let canvasFixedSize = vec2();
2051
2077
  * @type {boolean}
2052
2078
  * @default
2053
2079
  * @memberof Settings */
2054
- let canvasPixelated = true;
2055
-
2056
- /** Use nearest canvas scaling for more pixelated look
2057
- * - If enabled sets css image-rendering:pixelated
2058
- * - This defaults to false because text looks better with smoothing
2059
- * @type {boolean}
2060
- * @default
2061
- * @memberof Settings */
2062
- let overlayCanvasPixelated = false;
2080
+ let canvasPixelated = false;
2063
2081
 
2064
2082
  /** Disables texture filtering for crisper pixel art
2065
2083
  * @type {boolean}
@@ -2107,13 +2125,19 @@ let glCircleSides = 32;
2107
2125
  * @type {Vector2}
2108
2126
  * @default Vector2(16,16)
2109
2127
  * @memberof Settings */
2110
- let tileSizeDefault = vec2(16);
2128
+ let tileDefaultSize = vec2(16);
2111
2129
 
2112
- /** How many pixels smaller to draw tiles to prevent bleeding from neighbors
2130
+ /** Default padding pixels around tiles
2113
2131
  * @type {number}
2114
2132
  * @default
2115
2133
  * @memberof Settings */
2116
- let tileFixBleedScale = 0;
2134
+ let tileDefaultPadding = 0;
2135
+
2136
+ /** Default amount of pixels smaller to draw tiles to prevent neighbor bleeding
2137
+ * @type {number}
2138
+ * @default
2139
+ * @memberof Settings */
2140
+ let tileDefaultBleed = 0;
2117
2141
 
2118
2142
  ///////////////////////////////////////////////////////////////////////////////
2119
2143
  // Object settings
@@ -2335,13 +2359,22 @@ function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
2335
2359
  * @memberof Settings */
2336
2360
  function setCanvasMaxSize(size) { canvasMaxSize = size.copy(); }
2337
2361
 
2362
+ /** Set minimum aspect ratio of the canvas (width/height), unused if 0
2363
+ * @param {number} aspect
2364
+ * @memberof Settings */
2365
+ function setCanvasMinAspect(aspect) { canvasMinAspect = aspect; }
2366
+
2367
+ /** Set maximum aspect ratio of the canvas (width/height), unused if 0
2368
+ * @param {number} aspect
2369
+ * @memberof Settings */
2370
+ function setCanvasMaxAspect(aspect) { canvasMaxAspect = aspect; }
2371
+
2338
2372
  /** Set fixed size of the canvas
2339
2373
  * @param {Vector2} size
2340
2374
  * @memberof Settings */
2341
2375
  function setCanvasFixedSize(size) { canvasFixedSize = size.copy(); }
2342
2376
 
2343
2377
  /** Use nearest scaling algorithm for canvas for more pixelated look
2344
- * - If enabled sets css image-rendering:pixelated
2345
2378
  * @param {boolean} pixelated
2346
2379
  * @memberof Settings */
2347
2380
  function setCanvasPixelated(pixelated)
@@ -2353,18 +2386,6 @@ function setCanvasPixelated(pixelated)
2353
2386
  glCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2354
2387
  }
2355
2388
 
2356
- /** Use nearest scaling algorithm for canvas for more pixelated look
2357
- * - If enabled sets css image-rendering:pixelated
2358
- * - This defaults to false because text looks better with smoothing
2359
- * @param {boolean} pixelated
2360
- * @memberof Settings */
2361
- function setOverlayCanvasPixelated(pixelated)
2362
- {
2363
- overlayCanvasPixelated = pixelated;
2364
- if (overlayCanvas)
2365
- overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2366
- }
2367
-
2368
2389
  /** Disables texture filtering for crisper pixel art
2369
2390
  * @param {boolean} pixelated
2370
2391
  * @memberof Settings */
@@ -2408,12 +2429,17 @@ function setGLCircleSides(sides) { glCircleSides = sides; }
2408
2429
  /** Set default size of tiles in pixels
2409
2430
  * @param {Vector2} size
2410
2431
  * @memberof Settings */
2411
- function setTileSizeDefault(size) { tileSizeDefault = size.copy(); }
2432
+ function setTileDefaultSize(size) { tileDefaultSize = size.copy(); }
2412
2433
 
2413
- /** Set to prevent tile bleeding from neighbors in pixels
2414
- * @param {number} scale
2434
+ /** Default padding pixels around tiles
2435
+ * @param {number} padding
2436
+ * @memberof Settings */
2437
+ function setTileDefaultPadding(padding) { tileDefaultPadding = padding; }
2438
+
2439
+ /** Default amount of pixels smaller to draw tiles to prevent neighbor bleeding
2440
+ * @param {number} bleed
2415
2441
  * @memberof Settings */
2416
- function setTileFixBleedScale(scale) { tileFixBleedScale = scale; }
2442
+ function setTileDefaultBleed(bleed) { tileDefaultBleed = bleed; }
2417
2443
 
2418
2444
  /** Set if collisions between objects are enabled
2419
2445
  * @param {boolean} enable
@@ -2564,7 +2590,7 @@ function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUn
2564
2590
  /** Set if watermark with FPS should be shown
2565
2591
  * @param {boolean} show
2566
2592
  * @memberof Debug */
2567
- function setShowWatermark(show) { showWatermark = show; }
2593
+ function setDebugWatermark(show) { debugWatermark = show; }
2568
2594
 
2569
2595
  /** Set key code used to toggle debug mode, Esc by default
2570
2596
  * @param {string} key
@@ -3000,10 +3026,11 @@ class EngineObject
3000
3026
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
3001
3027
  getMirrorSign() { return this.mirror ? -1 : 1; }
3002
3028
 
3003
- /** Attaches a child to this with a given local transform
3029
+ /** Attaches a child to this with a local transform, returns child for chaining
3004
3030
  * @param {EngineObject} child
3005
3031
  * @param {Vector2} [localPos=(0,0)]
3006
- * @param {number} [localAngle] */
3032
+ * @param {number} [localAngle]
3033
+ * @return {EngineObject} The child object added */
3007
3034
  addChild(child, localPos=vec2(), localAngle=0)
3008
3035
  {
3009
3036
  ASSERT(!child.parent && !this.children.includes(child));
@@ -3013,6 +3040,7 @@ class EngineObject
3013
3040
  child.parent = this;
3014
3041
  child.localPos = localPos.copy();
3015
3042
  child.localAngle = localAngle;
3043
+ return child;
3016
3044
  }
3017
3045
 
3018
3046
  /** Removes a child from this one
@@ -3103,7 +3131,6 @@ class EngineObject
3103
3131
  * There are 3 canvas/contexts available to draw to...
3104
3132
  * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
3105
3133
  * glCanvas - Used by the accelerated WebGL batch rendering system.
3106
- * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
3107
3134
  *
3108
3135
  * The WebGL rendering system is very fast with some caveats...
3109
3136
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
@@ -3123,16 +3150,6 @@ let mainCanvas;
3123
3150
  * @memberof Draw */
3124
3151
  let mainContext;
3125
3152
 
3126
- /** A canvas that appears on top of everything the same size as mainCanvas
3127
- * @type {HTMLCanvasElement}
3128
- * @memberof Draw */
3129
- let overlayCanvas;
3130
-
3131
- /** 2d context for overlayCanvas
3132
- * @type {CanvasRenderingContext2D}
3133
- * @memberof Draw */
3134
- let overlayContext;
3135
-
3136
3153
  /** The default canvas to use for drawing, usually mainCanvas
3137
3154
  * @type {HTMLCanvasElement|OffscreenCanvas}
3138
3155
  * @memberof Draw */
@@ -3153,6 +3170,16 @@ let workCanvas;
3153
3170
  * @memberof Draw */
3154
3171
  let workContext;
3155
3172
 
3173
+ /** Offscreen canvas with willReadFrequently that can be used for image processing
3174
+ * @type {OffscreenCanvas}
3175
+ * @memberof Draw */
3176
+ let workReadCanvas;
3177
+
3178
+ /** Offscreen canvas with willReadFrequently that can be used for image processing
3179
+ * @type {OffscreenCanvasRenderingContext2D}
3180
+ * @memberof Draw */
3181
+ let workReadContext;
3182
+
3156
3183
  /** The size of the main canvas (and other secondary canvases)
3157
3184
  * @type {Vector2}
3158
3185
  * @memberof Draw */
@@ -3175,7 +3202,7 @@ let drawCount;
3175
3202
  * - This can take vecs or floats for easier use and conversion
3176
3203
  * - If an index is passed in, the tile size and index will determine the position
3177
3204
  * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
3178
- * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
3205
+ * @param {Vector2|number} [size] - Size of tile in pixels
3179
3206
  * @param {number} [textureIndex] - Texture index to use
3180
3207
  * @param {number} [padding] - How many pixels padding around tiles
3181
3208
  * @return {TileInfo}
@@ -3185,7 +3212,7 @@ let drawCount;
3185
3212
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
3186
3213
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
3187
3214
  * @memberof Draw */
3188
- function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3215
+ function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding)
3189
3216
  {
3190
3217
  if (headlessMode)
3191
3218
  return new TileInfo;
@@ -3224,13 +3251,13 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3224
3251
  class TileInfo
3225
3252
  {
3226
3253
  /** Create a tile info object
3227
- * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3228
- * @param {Vector2} [size=tileSizeDefault] - Size of tile in pixels
3229
- * @param {number} [textureIndex] - Texture index to use
3230
- * @param {number} [padding] - How many pixels padding around tiles
3231
- * @param {number} [bleedScale] - How many pixels smaller to draw tiles
3254
+ * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3255
+ * @param {Vector2} [size] - Size of tile in pixels
3256
+ * @param {number} [textureIndex] - Texture index to use
3257
+ * @param {number} [padding] - How many pixels padding around tiles
3258
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3232
3259
  */
3233
- constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0, bleedScale=tileFixBleedScale)
3260
+ constructor(pos=vec2(), size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3234
3261
  {
3235
3262
  /** @property {Vector2} - Top left corner of tile in pixels */
3236
3263
  this.pos = pos.copy();
@@ -3243,7 +3270,7 @@ class TileInfo
3243
3270
  /** @property {TextureInfo} - The texture info for this tile */
3244
3271
  this.textureInfo = textureInfos[this.textureIndex];
3245
3272
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
3246
- this.bleedScale = bleedScale;
3273
+ this.bleed = bleed;
3247
3274
  }
3248
3275
 
3249
3276
  /** Returns a copy of this tile offset by a vector
@@ -3251,7 +3278,7 @@ class TileInfo
3251
3278
  * @return {TileInfo}
3252
3279
  */
3253
3280
  offset(offset)
3254
- { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleedScale); }
3281
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleed); }
3255
3282
 
3256
3283
  /** Returns a copy of this tile offset by a number of animation frames
3257
3284
  * @param {number} frame - Offset to apply in animation frames
@@ -3274,7 +3301,7 @@ class TileInfo
3274
3301
  this.size = textureInfo.size.copy();
3275
3302
  this.textureInfo = textureInfo;
3276
3303
  // do not use padding or bleed
3277
- this.bleedScale = this.padding = 0;
3304
+ this.bleed = this.padding = 0;
3278
3305
  return this;
3279
3306
  }
3280
3307
  }
@@ -3340,7 +3367,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3340
3367
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3341
3368
 
3342
3369
  const textureInfo = tileInfo && tileInfo.textureInfo;
3343
- const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
3370
+ const bleed = tileInfo?.bleed ?? 0;
3344
3371
  if (useWebGL && glEnable)
3345
3372
  {
3346
3373
  ASSERT(!!glContext, 'WebGL is not enabled!');
@@ -3355,13 +3382,13 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3355
3382
  const w = tileInfo.size.x * sizeInverse.x;
3356
3383
  const h = tileInfo.size.y * sizeInverse.y;
3357
3384
  glSetTexture(textureInfo.glTexture);
3358
- if (bleedScale)
3385
+ if (bleed)
3359
3386
  {
3360
- const tileImageFixBleedX = sizeInverse.x*bleedScale;
3361
- const tileImageFixBleedY = sizeInverse.y*bleedScale;
3387
+ const bleedX = sizeInverse.x*bleed;
3388
+ const bleedY = sizeInverse.y*bleed;
3362
3389
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
3363
- x + tileImageFixBleedX, y + tileImageFixBleedY,
3364
- x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
3390
+ x + bleedX, y + bleedY,
3391
+ x - bleedX + w, y - bleedY + h,
3365
3392
  color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
3366
3393
  }
3367
3394
  else
@@ -3389,7 +3416,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3389
3416
  // calculate uvs and render
3390
3417
  const x = tileInfo.pos.x, y = tileInfo.pos.y;
3391
3418
  const w = tileInfo.size.x, h = tileInfo.size.y;
3392
- drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor, bleedScale);
3419
+ drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor, bleed);
3393
3420
  }
3394
3421
  else
3395
3422
  {
@@ -3754,26 +3781,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3754
3781
  drawTextScreen(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
3755
3782
  }
3756
3783
 
3757
- /** Draw text on overlay canvas in world space
3758
- * Automatically splits new lines into rows
3759
- * @param {string|number} text
3760
- * @param {Vector2} pos
3761
- * @param {number} [size]
3762
- * @param {Color} [color=(1,1,1,1)]
3763
- * @param {number} [lineWidth]
3764
- * @param {Color} [lineColor=(0,0,0,1)]
3765
- * @param {CanvasTextAlign} [textAlign='center']
3766
- * @param {string} [font=fontDefault]
3767
- * @param {string} [fontStyle]
3768
- * @param {number} [maxWidth]
3769
- * @param {number} [angle]
3770
- * @memberof Draw */
3771
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
3772
- {
3773
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
3774
- }
3775
-
3776
- /** Draw text on overlay canvas in screen space
3784
+ /** Draw text in screen space
3777
3785
  * Automatically splits new lines into rows
3778
3786
  * @param {string|number} text
3779
3787
  * @param {Vector2} pos
@@ -3786,9 +3794,9 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3786
3794
  * @param {string} [fontStyle]
3787
3795
  * @param {number} [maxWidth]
3788
3796
  * @param {number} [angle]
3789
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3797
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
3790
3798
  * @memberof Draw */
3791
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
3799
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=mainContext)
3792
3800
  {
3793
3801
  ASSERT(isString(text), 'text must be a string');
3794
3802
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3832,16 +3840,16 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3832
3840
  * @memberof Draw */
3833
3841
  function screenToWorld(screenPos)
3834
3842
  {
3843
+ ASSERT(isVector2(screenPos), 'screenPos must be a vec2');
3844
+
3835
3845
  let x = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3836
3846
  let y = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3837
3847
  if (cameraAngle)
3838
3848
  {
3839
3849
  // apply camera rotation
3840
3850
  const c = cos(-cameraAngle), s = sin(-cameraAngle);
3841
- const rotatedX = x * c - y * s;
3842
- const rotatedY = x * s + y * c;
3843
- x = rotatedX;
3844
- y = rotatedY;
3851
+ const xr = x * c - y * s, yr = x * s + y * c;
3852
+ x = xr; y = yr;
3845
3853
  }
3846
3854
  return new Vector2(x + cameraPos.x, y + cameraPos.y);
3847
3855
  }
@@ -3852,16 +3860,16 @@ function screenToWorld(screenPos)
3852
3860
  * @memberof Draw */
3853
3861
  function worldToScreen(worldPos)
3854
3862
  {
3863
+ ASSERT(isVector2(worldPos), 'worldPos must be a vec2');
3864
+
3855
3865
  let x = worldPos.x - cameraPos.x;
3856
3866
  let y = worldPos.y - cameraPos.y;
3857
3867
  if (cameraAngle)
3858
3868
  {
3859
3869
  // apply inverse camera rotation
3860
3870
  const c = cos(cameraAngle), s = sin(cameraAngle);
3861
- const rotatedX = x * c - y * s;
3862
- const rotatedY = x * s + y * c;
3863
- x = rotatedX;
3864
- y = rotatedY;
3871
+ const xr = x * c - y * s, yr = x * s + y * c;
3872
+ x = xr; y = yr;
3865
3873
  }
3866
3874
  return new Vector2
3867
3875
  (
@@ -3876,16 +3884,16 @@ function worldToScreen(worldPos)
3876
3884
  * @memberof Draw */
3877
3885
  function screenToWorldDelta(screenDelta)
3878
3886
  {
3887
+ ASSERT(isVector2(screenDelta), 'screenDelta must be a vec2');
3888
+
3879
3889
  let x = screenDelta.x / cameraScale;
3880
3890
  let y = screenDelta.y / -cameraScale;
3881
3891
  if (cameraAngle)
3882
3892
  {
3883
3893
  // apply camera rotation
3884
3894
  const c = cos(-cameraAngle), s = sin(-cameraAngle);
3885
- const rotatedX = x * c - y * s;
3886
- const rotatedY = x * s + y * c;
3887
- x = rotatedX;
3888
- y = rotatedY;
3895
+ const xr = x * c - y * s, yr = x * s + y * c;
3896
+ x = xr; y = yr;
3889
3897
  }
3890
3898
  return new Vector2(x, y);
3891
3899
  }
@@ -3896,16 +3904,16 @@ function screenToWorldDelta(screenDelta)
3896
3904
  * @memberof Draw */
3897
3905
  function worldToScreenDelta(worldDelta)
3898
3906
  {
3907
+ ASSERT(isVector2(worldDelta), 'worldDelta must be a vec2');
3908
+
3899
3909
  let x = worldDelta.x;
3900
3910
  let y = worldDelta.y;
3901
3911
  if (cameraAngle)
3902
3912
  {
3903
3913
  // apply inverse camera rotation
3904
3914
  const c = cos(cameraAngle), s = sin(cameraAngle);
3905
- const rotatedX = x * c - y * s;
3906
- const rotatedY = x * s + y * c;
3907
- x = rotatedX;
3908
- y = rotatedY;
3915
+ const xr = x * c - y * s, yr = x * s + y * c;
3916
+ x = xr; y = yr;
3909
3917
  }
3910
3918
  return new Vector2(x * cameraScale, y * -cameraScale);
3911
3919
  }
@@ -3918,6 +3926,10 @@ function worldToScreenDelta(worldDelta)
3918
3926
  * @memberof Draw */
3919
3927
  function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
3920
3928
  {
3929
+ ASSERT(isVector2(screenPos), 'screenPos must be a vec2');
3930
+ ASSERT(isVector2(screenSize), 'screenSize must be a vec2');
3931
+ ASSERT(isNumber(screenAngle), 'screenAngle must be a number');
3932
+
3921
3933
  return [
3922
3934
  screenToWorld(screenPos),
3923
3935
  screenSize.scale(1/cameraScale),
@@ -3933,6 +3945,10 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
3933
3945
  * @memberof Draw */
3934
3946
  function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
3935
3947
  {
3948
+ ASSERT(isVector2(worldPos), 'worldPos must be a vec2');
3949
+ ASSERT(isVector2(worldSize), 'worldSize must be a vec2');
3950
+ ASSERT(isNumber(worldAngle), 'worldAngle must be a number');
3951
+
3936
3952
  return [
3937
3953
  worldToScreen(worldPos),
3938
3954
  worldSize.scale(cameraScale),
@@ -3945,8 +3961,8 @@ function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
3945
3961
  * @memberof Draw */
3946
3962
  function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
3947
3963
 
3948
- /** Check if a point or circle is on screen
3949
- * If size is a Vector2, uses the largest dimension as diameter
3964
+ /** Check if a box, point, or circle is on screen with a circle test
3965
+ * If size is a Vector2, uses the length as diameter
3950
3966
  * This can be used to cull offscreen objects from render or update
3951
3967
  * @param {Vector2} pos - world space position
3952
3968
  * @param {Vector2|number} size - world space size or diameter
@@ -3954,12 +3970,30 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
3954
3970
  * @memberof Draw */
3955
3971
  function isOnScreen(pos, size=0)
3956
3972
  {
3957
- pos = worldToScreen(pos);
3973
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3974
+ ASSERT(isVector2(size) || isNumber(size), 'size must be a vec2 or number');
3975
+
3976
+ // optimized circle on screen test
3977
+ // pos = worldToScreen(pos);
3978
+ let x = pos.x - cameraPos.x;
3979
+ let y = pos.y - cameraPos.y;
3980
+ if (cameraAngle)
3981
+ {
3982
+ // apply inverse camera rotation
3983
+ const c = cos(cameraAngle), s = sin(cameraAngle);
3984
+ const xr = x * c - y * s, yr = x * s + y * c;
3985
+ x = xr; y = yr;
3986
+ }
3987
+ x *= cameraScale*2; y *= -cameraScale*2;
3988
+
3958
3989
  if (size instanceof Vector2)
3959
- size = max(size.x, size.y); // use largest dimension
3960
- size *= cameraScale/2;
3961
- return pos.x + size > 0 && pos.x - size < mainCanvasSize.x &&
3962
- pos.y + size > 0 && pos.y - size < mainCanvasSize.y;
3990
+ size = size.length(); // use length of vector as diameter
3991
+ size *= cameraScale;
3992
+
3993
+ // check against screen bounds
3994
+ const w = mainCanvasSize.x, h = mainCanvasSize.y;
3995
+ return x + size > -w && x - size < w &&
3996
+ y + size > -h && y - size < h;
3963
3997
  }
3964
3998
 
3965
3999
  /** Enable normal or additive blend mode
@@ -3973,18 +4007,19 @@ function setBlendMode(additive=false, context)
3973
4007
  context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
3974
4008
  }
3975
4009
 
3976
- /** Combines all LittleJS canvases onto the main canvas and clears them
3977
- * This is necessary for things like saving a screenshot
4010
+ /** Combines LittleJS canvases onto the main canvas
4011
+ * This is necessary for things like screenshots and video
3978
4012
  * @memberof Draw */
3979
4013
  function combineCanvases()
3980
4014
  {
3981
- // combine canvases
3982
- glCopyToContext(mainContext);
3983
- mainContext.drawImage(overlayCanvas, 0, 0);
3984
-
3985
- // clear canvases
3986
- glClearCanvas();
3987
- overlayCanvas.width |= 0;
4015
+ const w = mainCanvasSize.x, h = mainCanvasSize.y;
4016
+ workCanvas.width = w;
4017
+ workCanvas.height = h;
4018
+ workContext.fillRect(0,0,w,h); // remove background alpha
4019
+ glCopyToContext(workContext);
4020
+ workContext.drawImage(mainCanvas, 0, 0);
4021
+ mainCanvas.width |= 0;
4022
+ mainContext.drawImage(workCanvas, 0, 0);
3988
4023
  }
3989
4024
 
3990
4025
  /** Helper function to draw an image with color and additive color applied
@@ -4001,18 +4036,18 @@ function combineCanvases()
4001
4036
  * @param {number} dHeight
4002
4037
  * @param {Color} color
4003
4038
  * @param {Color} [additiveColor]
4004
- * @param {number} [bleedScale] - How much to shrink the source, used to fix bleeding
4039
+ * @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding
4005
4040
  * @memberof Draw */
4006
- function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleedScale=0)
4041
+ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
4007
4042
  {
4008
4043
  function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
4009
4044
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
4010
- const sx2 = bleedScale;
4011
- const sy2 = bleedScale;
4045
+ const sx2 = bleed;
4046
+ const sy2 = bleed;
4012
4047
  sWidth = max(1,sWidth|0);
4013
4048
  sHeight = max(1,sHeight|0);
4014
- const sWidth2 = sWidth - 2*bleedScale;
4015
- const sHeight2 = sHeight - 2*bleedScale;
4049
+ const sWidth2 = sWidth - 2*bleed;
4050
+ const sHeight2 = sHeight - 2*bleed;
4016
4051
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
4017
4052
  {
4018
4053
  // white texture with no additive alpha, no need to tint
@@ -4023,12 +4058,12 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4023
4058
  else
4024
4059
  {
4025
4060
  // copy to offscreen canvas
4026
- workCanvas.width = sWidth;
4027
- workCanvas.height = sHeight;
4028
- workContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
4061
+ workReadCanvas.width = sWidth;
4062
+ workReadCanvas.height = sHeight;
4063
+ workReadContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
4029
4064
 
4030
4065
  // tint image using offscreen work context
4031
- const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
4066
+ const imageData = workReadContext.getImageData(0, 0, sWidth, sHeight);
4032
4067
  const data = imageData.data;
4033
4068
  if (additiveColor && !isBlack(additiveColor))
4034
4069
  {
@@ -4037,8 +4072,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4037
4072
  const colorAdd = [additiveColor.r * 255, additiveColor.g * 255, additiveColor.b * 255, additiveColor.a * 255];
4038
4073
  for (let i = 0; i < data.length; ++i)
4039
4074
  data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
4040
- workContext.putImageData(imageData, 0, 0);
4041
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4075
+ workReadContext.putImageData(imageData, 0, 0);
4076
+ context.drawImage(workReadCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4042
4077
  }
4043
4078
  else
4044
4079
  {
@@ -4049,9 +4084,9 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4049
4084
  data[i+1] *= color.g;
4050
4085
  data[i+2] *= color.b;
4051
4086
  }
4052
- workContext.putImageData(imageData, 0, 0);
4087
+ workReadContext.putImageData(imageData, 0, 0);
4053
4088
  context.globalAlpha = color.a;
4054
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4089
+ context.drawImage(workReadCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4055
4090
  context.globalAlpha = 1;
4056
4091
  }
4057
4092
  }
@@ -4113,7 +4148,7 @@ class FontImage
4113
4148
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
4114
4149
  {
4115
4150
  // load default font image
4116
- if (!engineFontImage)
4151
+ if (!image && !engineFontImage)
4117
4152
  {
4118
4153
  engineFontImage = new Image;
4119
4154
  engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
@@ -4136,23 +4171,14 @@ class FontImage
4136
4171
  this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
4137
4172
  }
4138
4173
 
4139
- /** Draw text on overlay canvas in world space using the image font
4140
- * @param {string|number} text
4141
- * @param {Vector2} pos
4142
- * @param {number} [scale]
4143
- * @param {boolean} [center]
4144
- */
4145
- drawTextOverlay(text, pos, scale=4, center)
4146
- { this.drawText(text, pos, scale, center, overlayContext); }
4147
-
4148
- /** Draw text on overlay canvas in screen space using the image font
4174
+ /** Draw text in screen space using the image font
4149
4175
  * @param {string|number} text
4150
4176
  * @param {Vector2} pos
4151
4177
  * @param {number} [scale]
4152
4178
  * @param {boolean} [center]
4153
4179
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4154
4180
  */
4155
- drawTextScreen(text, pos, scale=4, center=true, context=overlayContext)
4181
+ drawTextScreen(text, pos, scale=4, center=true, context=mainContext)
4156
4182
  {
4157
4183
  context.save();
4158
4184
  const size = this.tileSize;
@@ -4423,6 +4449,16 @@ function gamepadConnected(gamepad=gamepadPrimary)
4423
4449
  return !!inputData[gamepad+1];
4424
4450
  }
4425
4451
 
4452
+ /** Returns how many control sticks the passed in gamepad has
4453
+ * @param {number} [gamepad]
4454
+ * @return {number}
4455
+ * @memberof Input */
4456
+ function gamepadStickCount(gamepad=gamepadPrimary)
4457
+ {
4458
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
4459
+ return gamepadStickData[gamepad]?.length ?? 0;
4460
+ }
4461
+
4426
4462
  /** True if a touch device has been detected
4427
4463
  * @memberof Input */
4428
4464
  const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
@@ -4763,9 +4799,11 @@ function inputUpdate()
4763
4799
  dpad.set(x, -y);
4764
4800
  sticks[0] = dpad.clampLength(); // clamp to circle
4765
4801
  }
4766
- const rightTouchStick = touchGamepadSticks[1] ?? vec2();
4767
4802
  if (touchGamepadButtonCount === 1)
4803
+ {
4804
+ const rightTouchStick = touchGamepadSticks[1] ?? vec2();
4768
4805
  sticks[1] = applyDeadZones(rightTouchStick);
4806
+ }
4769
4807
 
4770
4808
  // read virtual gamepad buttons
4771
4809
  const data = inputData[1] ?? (inputData[1] = []);
@@ -4890,7 +4928,7 @@ function inputRender()
4890
4928
  return;
4891
4929
 
4892
4930
  // setup the canvas
4893
- const context = overlayContext;
4931
+ const context = mainContext;
4894
4932
  context.save();
4895
4933
  context.globalAlpha = alpha*touchGamepadAlpha;
4896
4934
  context.strokeStyle = '#fff';
@@ -5132,12 +5170,12 @@ class Sound
5132
5170
  ///////////////////////////////////////////////////////////////////////////////
5133
5171
 
5134
5172
  /**
5135
- * Sound Wave Object - Stores a wave sound for later use and can be played positionally
5136
- * - this can be used to play wave, mp3, and ogg files
5173
+ * Sound Wave Object - Loads and stores an audio file for later use
5174
+ * - this can be used to load and play wave, mp3, and ogg files
5137
5175
  * @extends Sound
5138
5176
  * @memberof Audio
5139
5177
  * @example
5140
- * // create a sound
5178
+ * // load an audio asset file
5141
5179
  * const sound_example = new SoundWave('sound.mp3');
5142
5180
  *
5143
5181
  * // play the sound
@@ -5857,8 +5895,7 @@ class CanvasLayer extends EngineObject
5857
5895
  /** Destroy this canvas layer */
5858
5896
  destroy()
5859
5897
  {
5860
- if (this.destroyed)
5861
- return;
5898
+ if (this.destroyed) return;
5862
5899
 
5863
5900
  this.textureInfo.destroyWebGLTexture();
5864
5901
  super.destroy();
@@ -6090,8 +6127,8 @@ class TileLayer extends CanvasLayer
6090
6127
  if (!this.context) return;
6091
6128
 
6092
6129
  // save current render settings
6093
- /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number]} */
6094
- this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale];
6130
+ /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6131
+ this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
6095
6132
 
6096
6133
  // set the draw canvas and context to this layer
6097
6134
  // use camera settings to match this layer's canvas
@@ -6100,6 +6137,7 @@ class TileLayer extends CanvasLayer
6100
6137
  cameraPos = this.size.scale(.5);
6101
6138
  const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
6102
6139
  cameraScale = tileSize.x;
6140
+ canvasClearColor = CLEAR_BLACK;
6103
6141
  mainCanvasSize = this.size.multiply(tileSize);
6104
6142
  if (clear)
6105
6143
  {
@@ -6109,7 +6147,7 @@ class TileLayer extends CanvasLayer
6109
6147
  }
6110
6148
 
6111
6149
  // disable smoothing for pixel art
6112
- this.context.imageSmoothingEnabled = !tilesPixelated;
6150
+ drawContext.imageSmoothingEnabled = !tilesPixelated;
6113
6151
 
6114
6152
  // setup gl rendering if enabled
6115
6153
  glPreRender();
@@ -6125,7 +6163,7 @@ class TileLayer extends CanvasLayer
6125
6163
  //debugSaveCanvas(this.canvas);
6126
6164
 
6127
6165
  // set stuff back to normal
6128
- [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale] = this.savedRenderSettings;
6166
+ [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6129
6167
  }
6130
6168
 
6131
6169
  /** Draw the tile at a given position in the tile grid
@@ -6410,9 +6448,9 @@ class ParticleEmitter extends EngineObject
6410
6448
  /** @property {Color} - Color at start of life 2, randomized between start colors */
6411
6449
  this.colorStartB = colorStartB.copy();
6412
6450
  /** @property {Color} - Color at end of life 1, randomized between end colors */
6413
- this.colorEndA = colorEndA.copy();
6451
+ this.colorEndA = colorEndA.copy();
6414
6452
  /** @property {Color} - Color at end of life 2, randomized between end colors */
6415
- this.colorEndB = colorEndB.copy();
6453
+ this.colorEndB = colorEndB.copy();
6416
6454
  /** @property {boolean} - Should color be randomized linearly or across each component */
6417
6455
  this.randomColorLinear = randomColorLinear;
6418
6456
 
@@ -6496,8 +6534,10 @@ class ParticleEmitter extends EngineObject
6496
6534
  if (debugParticles)
6497
6535
  {
6498
6536
  // show emitter bounds
6499
- const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
6500
- debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
6537
+ if (typeof this.emitSize === 'number')
6538
+ debugCircle(this.pos, this.emitSize/2, '#0f0');
6539
+ else
6540
+ debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle);
6501
6541
  }
6502
6542
  }
6503
6543
 
@@ -6507,8 +6547,8 @@ class ParticleEmitter extends EngineObject
6507
6547
  {
6508
6548
  // spawn a particle
6509
6549
  let pos = typeof this.emitSize === 'number' ? // check if number was used
6510
- randInCircle(this.emitSize/2) // circle emitter
6511
- : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
6550
+ randInCircle(this.emitSize/2) // circle emitter
6551
+ : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
6512
6552
  .multiply(this.emitSize).rotate(this.angle)
6513
6553
  let angle = rand(this.particleConeAngle, -this.particleConeAngle);
6514
6554
  if (!this.localSpace)
@@ -6836,10 +6876,10 @@ class Medal
6836
6876
  */
6837
6877
  render(hidePercent=0)
6838
6878
  {
6839
- const context = overlayContext;
6879
+ const context = mainContext;
6840
6880
  const width = min(medalDisplaySize.x, mainCanvas.width);
6841
6881
  const height = medalDisplaySize.y;
6842
- const x = overlayCanvas.width - width;
6882
+ const x = mainCanvas.width - width;
6843
6883
  const y = -height*hidePercent;
6844
6884
  const backgroundColor = hsl(0,0,.9);
6845
6885
 
@@ -6880,7 +6920,7 @@ class Medal
6880
6920
  {
6881
6921
  // draw the image or icon
6882
6922
  if (this.image)
6883
- overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
6923
+ mainContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
6884
6924
  else
6885
6925
  drawTextScreen(this.icon, pos, size*.7, BLACK);
6886
6926
  }
@@ -6901,7 +6941,7 @@ class Medal
6901
6941
  * @namespace WebGL
6902
6942
  */
6903
6943
 
6904
- /** The WebGL canvas which appears above the main canvas and below the overlay canvas
6944
+ /** The WebGL canvas which appears below the main canvas
6905
6945
  * @type {HTMLCanvasElement}
6906
6946
  * @memberof WebGL */
6907
6947
  let glCanvas;
@@ -6931,7 +6971,7 @@ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE |
6931
6971
  ///////////////////////////////////////////////////////////////////////////////
6932
6972
 
6933
6973
  // Initialize WebGL, called automatically by the engine
6934
- function glInit()
6974
+ function glInit(rootElement)
6935
6975
  {
6936
6976
  // keep set of texture infos so they can be restored if context is lost
6937
6977
  glTextureInfos = new Set;
@@ -6955,8 +6995,7 @@ function glInit()
6955
6995
  return;
6956
6996
  }
6957
6997
 
6958
- // attach the WebGL canvas
6959
- const rootElement = mainCanvas.parentElement;
6998
+ // attach the WebGL canvas;
6960
6999
  rootElement.appendChild(glCanvas);
6961
7000
 
6962
7001
  // startup webgl
@@ -7174,6 +7213,9 @@ function glClearCanvas()
7174
7213
  glCanvas.width = drawCanvas.width;
7175
7214
  glCanvas.height = drawCanvas.height;
7176
7215
  glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
7216
+ const color = canvasClearColor;
7217
+ if (color.a > 0)
7218
+ glContext.clearColor(color.r, color.g, color.b, color.a);
7177
7219
  glContext.clear(glContext.COLOR_BUFFER_BIT);
7178
7220
  }
7179
7221
 
@@ -7403,7 +7445,6 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
7403
7445
  glFlush();
7404
7446
  glSetInstancedMode();
7405
7447
 
7406
- glPolyMode = false;
7407
7448
  let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
7408
7449
  glPositionData[offset++] = x;
7409
7450
  glPositionData[offset++] = y;
@@ -7924,13 +7965,12 @@ class PostProcessPlugin
7924
7965
  {
7925
7966
  /** Create global post processing shader
7926
7967
  * @param {string} shaderCode
7927
- * @param {boolean} [includeOverlay]
7928
7968
  * @param {boolean} [includeMainCanvas]
7929
7969
  * @example
7930
7970
  * // create the post process plugin object
7931
7971
  * new PostProcessPlugin(shaderCode);
7932
7972
  */
7933
- constructor(shaderCode, includeOverlay=false, includeMainCanvas=true)
7973
+ constructor(shaderCode, includeMainCanvas=true)
7934
7974
  {
7935
7975
  ASSERT(!postProcess, 'Post process already initialized');
7936
7976
  postProcess = this;
@@ -8002,17 +8042,19 @@ class PostProcessPlugin
8002
8042
  // clear out the buffer
8003
8043
  glFlush();
8004
8044
 
8005
- if (includeMainCanvas || includeOverlay)
8045
+ // setup texture
8046
+ glContext.activeTexture(glContext.TEXTURE0);
8047
+ glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
8048
+ if (includeMainCanvas)
8006
8049
  {
8007
- // copy WebGL to the main canvas
8008
- mainContext.drawImage(glCanvas, 0, 0);
8009
-
8010
- if (includeOverlay)
8011
- {
8012
- // copy overlay canvas so it will be included in post processing
8013
- mainContext.drawImage(overlayCanvas, 0, 0);
8014
- overlayCanvas.width |= 0; // clear overlay canvas
8015
- }
8050
+ // copy main canvas to work canvas
8051
+ workCanvas.width = mainCanvasSize.x;
8052
+ workCanvas.height = mainCanvasSize.y;
8053
+ glCopyToContext(workContext);
8054
+ workContext.drawImage(mainCanvas, 0, 0);
8055
+
8056
+ // copy work canvas to texture
8057
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
8016
8058
  }
8017
8059
 
8018
8060
  // setup shader program to draw a quad
@@ -8021,14 +8063,6 @@ class PostProcessPlugin
8021
8063
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
8022
8064
  glContext.disable(glContext.BLEND);
8023
8065
 
8024
- // set textures, pass in the 2d canvas and gl canvas in separate texture channels
8025
- glContext.activeTexture(glContext.TEXTURE0);
8026
- glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
8027
- if (includeMainCanvas || includeOverlay)
8028
- {
8029
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
8030
- }
8031
-
8032
8066
  // set vertex position attribute
8033
8067
  const vertexByteStride = 8;
8034
8068
  const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
@@ -8257,12 +8291,14 @@ class UISystemPlugin
8257
8291
  * // create the ui plugin object
8258
8292
  * new UISystemPlugin;
8259
8293
  */
8260
- constructor(context=overlayContext)
8294
+ constructor(context=mainContext)
8261
8295
  {
8262
8296
  ASSERT(!uiSystem, 'UI system already initialized');
8263
8297
  uiSystem = this;
8264
8298
 
8265
8299
  // default settings
8300
+ /** @property {boolean} - Activate when mouse is pressed down instead of clicked */
8301
+ this.activateOnPress = false;
8266
8302
  /** @property {Color} - Default fill color for UI elements */
8267
8303
  this.defaultColor = WHITE;
8268
8304
  /** @property {Color} - Default outline color for UI elements */
@@ -8303,8 +8339,6 @@ class UISystemPlugin
8303
8339
  // navigation properties
8304
8340
  /** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
8305
8341
  this.navigationObject = undefined;
8306
- /** @property {number} - Gamepad index to use for UI navigation */
8307
- this.navigationGamepadIndex = 0;
8308
8342
  /** @property {Timer} - Cooldown timer for navigation inputs */
8309
8343
  this.navigationTimer = new Timer(undefined, true);
8310
8344
  /** @property {number} - Time between navigation inputs in seconds */
@@ -8712,9 +8746,8 @@ class UISystemPlugin
8712
8746
  const both = uiSystem.navigationDirection === 2;
8713
8747
  if (isUsingGamepad)
8714
8748
  {
8715
- const gamepad = this.navigationGamepadIndex;
8716
- const stick = gamepadStick(0, gamepad);
8717
- const dpad = gamepadDpad(gamepad);
8749
+ const stick = gamepadStick(0, gamepadPrimary);
8750
+ const dpad = gamepadDpad(gamepadPrimary);
8718
8751
  if (both)
8719
8752
  return -(stick.y || dpad.y) || (stick.x || dpad.x);
8720
8753
  return vertical ? -(stick.y || dpad.y) : (stick.x || dpad.x);
@@ -8740,9 +8773,8 @@ class UISystemPlugin
8740
8773
  const vertical = uiSystem.navigationDirection === 1;
8741
8774
  if (isUsingGamepad)
8742
8775
  {
8743
- const gamepad = this.navigationGamepadIndex;
8744
- const stick = gamepadStick(0, gamepad);
8745
- const dpad = gamepadDpad(gamepad);
8776
+ const stick = gamepadStick(0, gamepadPrimary);
8777
+ const dpad = gamepadDpad(gamepadPrimary);
8746
8778
  return !vertical ? (stick.y || dpad.y) : (stick.x || dpad.x);
8747
8779
  }
8748
8780
  const back = !vertical ? 'ArrowUp' : 'ArrowLeft';
@@ -8754,8 +8786,7 @@ class UISystemPlugin
8754
8786
  * @return {boolean} */
8755
8787
  getNavigationWasPressed()
8756
8788
  {
8757
- const gamepad = this.navigationGamepadIndex;
8758
- return isUsingGamepad ? gamepadWasPressed(0, gamepad) :
8789
+ return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
8759
8790
  keyWasPressed('Space') || keyWasPressed('Enter');
8760
8791
  }
8761
8792
 
@@ -8919,13 +8950,15 @@ class UIObject
8919
8950
  uiSystem.uiObjects.push(this);
8920
8951
  }
8921
8952
 
8922
- /** Add a child UIObject to this object
8923
- * @param {UIObject} child */
8953
+ /** Add a child UIObject to this object, returns child for chaining
8954
+ * @param {UIObject} child
8955
+ * @return {UIObject} The child object added */
8924
8956
  addChild(child)
8925
8957
  {
8926
8958
  ASSERT(!child.parent && !this.children.includes(child));
8927
8959
  this.children.push(child);
8928
8960
  child.parent = this;
8961
+ return child;
8929
8962
  }
8930
8963
 
8931
8964
  /** Remove a child UIObject from this object
@@ -8998,8 +9031,16 @@ class UIObject
8998
9031
  if (uiSystem.activeObject && !isActive)
8999
9032
  uiSystem.activeObject.onRelease();
9000
9033
  uiSystem.activeObject = this;
9034
+
9035
+ if (uiSystem.activateOnPress)
9036
+ {
9037
+ this.onClick();
9038
+ if (!this.soundPress && this.soundClick)
9039
+ this.soundClick.play();
9040
+ }
9001
9041
  }
9002
9042
  }
9043
+ if (!uiSystem.activateOnPress)
9003
9044
  if (!mouseDown && this.isActiveObject() && this.interactive)
9004
9045
  {
9005
9046
  this.onClick();
@@ -11439,33 +11480,33 @@ async function box2dInit()
11439
11480
  color = getDebugColor(color);
11440
11481
  point1 = box2d.vec2FromPointer(point1);
11441
11482
  point2 = box2d.vec2FromPointer(point2);
11442
- drawLine(point1, point2, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
11483
+ drawLine(point1, point2, debugLineWidth, color, vec2(), 0, false);
11443
11484
  };
11444
11485
  debugDraw.DrawPolygon = function(vertices, vertexCount, color)
11445
11486
  {
11446
11487
  color = getDebugColor(color);
11447
11488
  const points = getPointsList(vertices, vertexCount);
11448
- drawPoly(points, CLEAR_WHITE, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
11489
+ drawPoly(points, CLEAR_WHITE, debugLineWidth, color, vec2(), 0, false);
11449
11490
  };
11450
11491
  debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
11451
11492
  {
11452
11493
  color = getDebugColor(color);
11453
11494
  const points = getPointsList(vertices, vertexCount);
11454
- drawPoly(points, color, 0, color, vec2(), 0, false, false, overlayContext);
11495
+ drawPoly(points, color, 0, color, vec2(), 0, false);
11455
11496
  };
11456
11497
  debugDraw.DrawCircle = function(center, radius, color)
11457
11498
  {
11458
11499
  color = getDebugColor(color);
11459
11500
  center = box2d.vec2FromPointer(center);
11460
- drawCircle(center, radius*2, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
11501
+ drawCircle(center, radius*2, CLEAR_WHITE, debugLineWidth, color, false);
11461
11502
  };
11462
11503
  debugDraw.DrawSolidCircle = function(center, radius, axis, color)
11463
11504
  {
11464
11505
  color = getDebugColor(color);
11465
11506
  center = box2d.vec2FromPointer(center);
11466
11507
  axis = box2d.vec2FromPointer(axis).scale(radius);
11467
- drawCircle(center, radius*2, color, debugLineWidth, color, false, false, overlayContext);
11468
- drawLine(vec2(), axis, debugLineWidth, color, center, 0, false, false, overlayContext);
11508
+ drawCircle(center, radius*2, color, debugLineWidth, color, false);
11509
+ drawLine(vec2(), axis, debugLineWidth, color, center, 0, false);
11469
11510
  };
11470
11511
  debugDraw.DrawTransform = function(transform)
11471
11512
  {
@@ -11474,8 +11515,8 @@ async function box2dInit()
11474
11515
  const angle = -transform.get_q().GetAngle();
11475
11516
  const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
11476
11517
  const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
11477
- drawLine(vec2(), p1, debugLineWidth, c1, pos, angle, false, false, overlayContext);
11478
- drawLine(vec2(), p2, debugLineWidth, c2, pos, angle, false, false, overlayContext);
11518
+ drawLine(vec2(), p1, debugLineWidth, c1, pos, angle, false);
11519
+ drawLine(vec2(), p2, debugLineWidth, c2, pos, angle, false);
11479
11520
  }
11480
11521
 
11481
11522
  debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
@@ -11495,8 +11536,8 @@ async function box2dInit()
11495
11536
 
11496
11537
  ///////////////////////////////////////////////////////////////////////////////
11497
11538
 
11498
- /** Draw a scalable nine-slice UI element to the overlay canvas in screen space
11499
- * This function can not apply color because it draws using the overlay 2d context
11539
+ /** Draw a scalable nine-slice UI element to the main canvas in screen space
11540
+ * This function can not apply color because it draws using the 2d context
11500
11541
  * @param {Vector2} pos - Screen space position
11501
11542
  * @param {Vector2} size - Screen space size
11502
11543
  * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
@@ -11506,7 +11547,7 @@ async function box2dInit()
11506
11547
  * @memberof DrawUtilities */
11507
11548
  function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
11508
11549
  {
11509
- drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
11550
+ drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true);
11510
11551
  }
11511
11552
 
11512
11553
  /** Draw a scalable nine-slice UI element in world space
@@ -11555,8 +11596,8 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
11555
11596
  }
11556
11597
  }
11557
11598
 
11558
- /** Draw a scalable three-slice UI element to the overlay canvas in screen space
11559
- * This function can not apply color because it draws using the overlay 2d context
11599
+ /** Draw a scalable three-slice UI element to the main canvas in screen space
11600
+ * This function can not apply color because it draws using the 2d context
11560
11601
  * @param {Vector2} pos - Screen space position
11561
11602
  * @param {Vector2} size - Screen space size
11562
11603
  * @param {TileInfo} startTile - Starting tile for the three-slice pattern
@@ -11566,7 +11607,7 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
11566
11607
  * @memberof DrawUtilities */
11567
11608
  function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
11568
11609
  {
11569
- drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
11610
+ drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true);
11570
11611
  }
11571
11612
 
11572
11613
  /** Draw a scalable three-slice UI element in world space