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 Debug System
@@ -746,7 +766,7 @@ const debugPointSize = .5;
746
766
  * @type {boolean}
747
767
  * @default
748
768
  * @memberof Debug */
749
- let showWatermark = true;
769
+ let debugWatermark = true;
750
770
 
751
771
  /** Key code used to toggle debug mode, Esc by default
752
772
  * @type {string}
@@ -1032,12 +1052,19 @@ function debugUpdate()
1032
1052
  if (!debug)
1033
1053
  return;
1034
1054
 
1055
+ if (debugVideoCaptureIsActive())
1056
+ {
1057
+ // control to stop video capture
1058
+ if (keyWasPressed('Digit6') || keyWasPressed(debugKey))
1059
+ debugVideoCaptureStop();
1060
+ }
1061
+
1035
1062
  if (keyWasPressed(debugKey)) // Esc
1036
1063
  debugOverlay = !debugOverlay;
1037
1064
  if (debugOverlay)
1038
1065
  {
1039
1066
  if (keyWasPressed('Digit0'))
1040
- showWatermark = !showWatermark;
1067
+ debugWatermark = !debugWatermark;
1041
1068
  if (keyWasPressed('Digit1'))
1042
1069
  debugPhysics = !debugPhysics, debugParticles = false;
1043
1070
  if (keyWasPressed('Digit2'))
@@ -1048,8 +1075,8 @@ function debugUpdate()
1048
1075
  debugRaycast = !debugRaycast;
1049
1076
  if (keyWasPressed('Digit5'))
1050
1077
  debugScreenshot();
1051
- if (keyWasPressed('Digit6'))
1052
- debugVideoCaptureIsActive() ? debugVideoCaptureStop() : debugVideoCaptureStart();
1078
+ if (keyWasPressed('Digit6') && !debugVideoCaptureIsActive())
1079
+ debugVideoCaptureStart();
1053
1080
  }
1054
1081
  }
1055
1082
 
@@ -1065,13 +1092,11 @@ function debugRender()
1065
1092
  {
1066
1093
  // combine canvases, remove alpha and save
1067
1094
  combineCanvases();
1068
- const w = mainCanvas.width, h = mainCanvas.height;
1069
- overlayContext.fillRect(0,0,w,h);
1070
- overlayContext.drawImage(mainCanvas, 0, 0);
1071
- debugSaveCanvas(overlayCanvas);
1095
+ debugSaveCanvas(mainCanvas);
1072
1096
  debugTakeScreenshot = 0;
1073
1097
  }
1074
1098
 
1099
+ const debugContext = mainContext;
1075
1100
  if (debugGamepads && gamepadsEnable)
1076
1101
  {
1077
1102
  // draw gamepads
@@ -1119,9 +1144,6 @@ function debugRender()
1119
1144
  let debugObject;
1120
1145
  if (debugOverlay)
1121
1146
  {
1122
- const saveContext = mainContext;
1123
- mainContext = overlayContext;
1124
-
1125
1147
  // draw red rectangle around screen
1126
1148
  const cameraSize = getCameraSize();
1127
1149
  debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008');
@@ -1153,15 +1175,14 @@ function debugRender()
1153
1175
  // show floored tile pick for tile collision
1154
1176
  drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(1,1,0,.5));
1155
1177
  }
1156
- mainContext = saveContext;
1157
1178
  }
1158
1179
 
1159
1180
  {
1160
1181
  // draw debug primitives
1161
- overlayContext.lineWidth = 2;
1182
+ debugContext.lineWidth = 2;
1162
1183
  debugPrimitives.forEach(p=>
1163
1184
  {
1164
- overlayContext.save();
1185
+ debugContext.save();
1165
1186
 
1166
1187
  // create canvas transform from world space to screen space
1167
1188
  // without scaling because we want consistent pixel sizes
@@ -1172,55 +1193,56 @@ function debugRender()
1172
1193
  scale = cameraScale;
1173
1194
  angle -= cameraAngle;
1174
1195
  }
1175
- overlayContext.translate(pos.x|0, pos.y|0);
1176
- overlayContext.rotate(angle);
1177
- overlayContext.scale(1, p.text ? 1 : -1);
1178
- overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
1196
+ debugContext.translate(pos.x|0, pos.y|0);
1197
+ debugContext.rotate(angle);
1198
+ debugContext.scale(1, p.text ? 1 : -1);
1199
+ debugContext.fillStyle = p.color;
1200
+ debugContext.strokeStyle = p.color;
1179
1201
  if (p.text !== undefined)
1180
1202
  {
1181
- overlayContext.font = p.size*scale + 'px '+ p.font;
1182
- overlayContext.textAlign = 'center';
1183
- overlayContext.textBaseline = 'middle';
1184
- overlayContext.fillText(p.text, 0, 0);
1203
+ debugContext.font = p.size*scale + 'px '+ p.font;
1204
+ debugContext.textAlign = 'center';
1205
+ debugContext.textBaseline = 'middle';
1206
+ debugContext.fillText(p.text, 0, 0);
1185
1207
  }
1186
1208
  else if (p.points !== undefined)
1187
1209
  {
1188
1210
  // poly
1189
- overlayContext.beginPath();
1211
+ debugContext.beginPath();
1190
1212
  for (const point of p.points)
1191
1213
  {
1192
1214
  const p2 = point.scale(scale).floor();
1193
- overlayContext.lineTo(p2.x, p2.y);
1215
+ debugContext.lineTo(p2.x, p2.y);
1194
1216
  }
1195
- overlayContext.closePath();
1196
- p.fill && overlayContext.fill();
1197
- overlayContext.stroke();
1217
+ debugContext.closePath();
1218
+ p.fill && debugContext.fill();
1219
+ debugContext.stroke();
1198
1220
  }
1199
1221
  else if (p.size === 0 || (p.size.x === 0 && p.size.y === 0))
1200
1222
  {
1201
1223
  // point
1202
1224
  const pointSize = debugPointSize * scale;
1203
- overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
1204
- overlayContext.fillRect(-1, -pointSize/2, 3, pointSize);
1225
+ debugContext.fillRect(-pointSize/2, -1, pointSize, 3);
1226
+ debugContext.fillRect(-1, -pointSize/2, 3, pointSize);
1205
1227
  }
1206
1228
  else if (p.size.x !== undefined)
1207
1229
  {
1208
1230
  // rect
1209
1231
  const s = p.size.scale(scale).floor();
1210
1232
  const w = s.x, h = s.y;
1211
- p.fill && overlayContext.fillRect(-w/2|0, -h/2|0, w, h);
1212
- overlayContext.strokeRect(-w/2|0, -h/2|0, w, h);
1233
+ p.fill && debugContext.fillRect(-w/2|0, -h/2|0, w, h);
1234
+ debugContext.strokeRect(-w/2|0, -h/2|0, w, h);
1213
1235
  }
1214
1236
  else
1215
1237
  {
1216
1238
  // circle
1217
- overlayContext.beginPath();
1218
- overlayContext.arc(0, 0, p.size*scale/2, 0, 9);
1219
- p.fill && overlayContext.fill();
1220
- overlayContext.stroke();
1239
+ debugContext.beginPath();
1240
+ debugContext.arc(0, 0, p.size*scale/2, 0, 9);
1241
+ p.fill && debugContext.fill();
1242
+ debugContext.stroke();
1221
1243
  }
1222
1244
 
1223
- overlayContext.restore();
1245
+ debugContext.restore();
1224
1246
  });
1225
1247
 
1226
1248
  // remove expired primitives
@@ -1229,55 +1251,53 @@ function debugRender()
1229
1251
 
1230
1252
  if (debugObject)
1231
1253
  {
1232
- const saveContext = mainContext;
1233
- mainContext = overlayContext;
1234
1254
  const raycastHitPos = tileCollisionRaycast(debugObject.pos, mousePos);
1235
1255
  raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3));
1236
1256
  drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5));
1237
1257
 
1238
- const debugText = 'mouse pos = ' + mousePos +
1239
- '\nmouse collision = ' + tileCollisionGetData(mousePos) +
1240
- '\n\n--- object info ---\n' +
1241
- debugObject.toString();
1258
+ let debugText = 'mouse pos = ' + mousePos;
1259
+ if (tileCollisionLayers.length)
1260
+ debugText += '\nmouse collision = ' + tileCollisionGetData(mousePos);
1261
+ debugText += '\n\n--- object info ---\n';
1262
+ debugText += debugObject.toString();
1242
1263
  drawTextScreen(debugText, mousePosScreen, 24, rgb(), .05, undefined, 'center', 'monospace');
1243
- mainContext = saveContext;
1244
1264
  }
1245
1265
 
1246
1266
  {
1247
1267
  // draw debug overlay
1248
1268
  const fontSize = 20;
1249
1269
  const lineHeight = fontSize * 1.2 | 0;
1250
- overlayContext.save();
1251
- overlayContext.fillStyle = '#fff';
1252
- overlayContext.textAlign = 'left';
1253
- overlayContext.textBaseline = 'top';
1254
- overlayContext.font = fontSize + 'px monospace';
1255
- overlayContext.shadowColor = '#000';
1256
- overlayContext.shadowBlur = 9;
1270
+ debugContext.save();
1271
+ debugContext.fillStyle = '#fff';
1272
+ debugContext.textAlign = 'left';
1273
+ debugContext.textBaseline = 'top';
1274
+ debugContext.font = fontSize + 'px monospace';
1275
+ debugContext.shadowColor = '#000';
1276
+ debugContext.shadowBlur = 9;
1257
1277
 
1258
1278
  let x = 9, y = 0, h = lineHeight;
1259
1279
  if (debugOverlay)
1260
1280
  {
1261
- overlayContext.fillText(`${engineName} v${engineVersion}`, x, y += h/2 );
1262
- overlayContext.fillText('Time: ' + formatTime(time), x, y += h);
1263
- overlayContext.fillText('FPS: ' + averageFPS.toFixed(1) + (glEnable?' WebGL':' Canvas2D'),
1281
+ debugContext.fillText(`${engineName} v${engineVersion}`, x, y += h/2 );
1282
+ debugContext.fillText('Time: ' + formatTime(time), x, y += h);
1283
+ debugContext.fillText('FPS: ' + averageFPS.toFixed(1) + (glEnable?' WebGL':' Canvas2D'),
1264
1284
  x, y += h);
1265
- overlayContext.fillText('Objects: ' + engineObjects.length, x, y += h);
1266
- overlayContext.fillText('Draw Count: ' + drawCount, x, y += h);
1267
- overlayContext.fillText('---------', x, y += h);
1268
- overlayContext.fillStyle = '#f00';
1269
- overlayContext.fillText('ESC: Debug Overlay', x, y += h);
1270
- overlayContext.fillStyle = debugPhysics ? '#f00' : '#fff';
1271
- overlayContext.fillText('1: Debug Physics', x, y += h);
1272
- overlayContext.fillStyle = debugParticles ? '#f00' : '#fff';
1273
- overlayContext.fillText('2: Debug Particles', x, y += h);
1274
- overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
1275
- overlayContext.fillText('3: Debug Gamepads', x, y += h);
1276
- overlayContext.fillStyle = debugRaycast ? '#f00' : '#fff';
1277
- overlayContext.fillText('4: Debug Raycasts', x, y += h);
1278
- overlayContext.fillStyle = '#fff';
1279
- overlayContext.fillText('5: Save Screenshot', x, y += h);
1280
- overlayContext.fillText('6: Toggle Video Capture', x, y += h);
1285
+ debugContext.fillText('Objects: ' + engineObjects.length, x, y += h);
1286
+ debugContext.fillText('Draw Count: ' + drawCount, x, y += h);
1287
+ debugContext.fillText('---------', x, y += h);
1288
+ debugContext.fillStyle = '#f00';
1289
+ debugContext.fillText('ESC: Debug Overlay', x, y += h);
1290
+ debugContext.fillStyle = debugPhysics ? '#f00' : '#fff';
1291
+ debugContext.fillText('1: Debug Physics', x, y += h);
1292
+ debugContext.fillStyle = debugParticles ? '#f00' : '#fff';
1293
+ debugContext.fillText('2: Debug Particles', x, y += h);
1294
+ debugContext.fillStyle = debugGamepads ? '#f00' : '#fff';
1295
+ debugContext.fillText('3: Debug Gamepads', x, y += h);
1296
+ debugContext.fillStyle = debugRaycast ? '#f00' : '#fff';
1297
+ debugContext.fillText('4: Debug Raycasts', x, y += h);
1298
+ debugContext.fillStyle = '#fff';
1299
+ debugContext.fillText('5: Save Screenshot', x, y += h);
1300
+ debugContext.fillText('6: Toggle Video Capture', x, y += h);
1281
1301
 
1282
1302
  let keysPressed = '';
1283
1303
  let mousePressed = '';
@@ -1290,8 +1310,8 @@ function debugRender()
1290
1310
  else if (keyIsDown(i, 0))
1291
1311
  keysPressed += i + ' ' ;
1292
1312
  }
1293
- mousePressed && overlayContext.fillText('Mouse: ' + mousePressed, x, y += h);
1294
- keysPressed && overlayContext.fillText('Keys: ' + keysPressed, x, y += h);
1313
+ mousePressed && debugContext.fillText('Mouse: ' + mousePressed, x, y += h);
1314
+ keysPressed && debugContext.fillText('Keys: ' + keysPressed, x, y += h);
1295
1315
 
1296
1316
  // show gamepad buttons
1297
1317
  for (let i = 1; i < inputData.length; i++)
@@ -1303,18 +1323,18 @@ function debugRender()
1303
1323
  if (keyIsDown(j, i))
1304
1324
  buttonsPressed += j + ' ' ;
1305
1325
  }
1306
- buttonsPressed && overlayContext.fillText(`Gamepad ${i-1}: ` + buttonsPressed, x, y += h);
1326
+ buttonsPressed && debugContext.fillText(`Gamepad ${i-1}: ` + buttonsPressed, x, y += h);
1307
1327
  }
1308
1328
  }
1309
1329
  else
1310
1330
  {
1311
- overlayContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h);
1312
- overlayContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
1313
- overlayContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
1314
- overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
1331
+ debugContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h);
1332
+ debugContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
1333
+ debugContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
1334
+ debugContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
1315
1335
  }
1316
1336
 
1317
- overlayContext.restore();
1337
+ debugContext.restore();
1318
1338
  }
1319
1339
  }
1320
1340
 
@@ -1365,7 +1385,7 @@ function debugVideoCaptureStart()
1365
1385
  // start recording
1366
1386
  LOG('Video capture started.');
1367
1387
  debugVideoCapture.start();
1368
- debugVideoCaptureTimer = new Timer(0);
1388
+ debugVideoCaptureTimer = new Timer(0, true);
1369
1389
 
1370
1390
  if (!debugVideoCaptureIcon)
1371
1391
  {
@@ -1563,11 +1583,7 @@ function percent(value, valueA, valueB)
1563
1583
  * @return {number}
1564
1584
  * @memberof Utilities */
1565
1585
  function lerp(valueA, valueB, percent)
1566
- {
1567
- if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
1568
- console.warn('lerp() parameter order changed! use lerp(start, end, p)');
1569
- return valueA + clamp(percent) * (valueB-valueA);
1570
- }
1586
+ { return valueA + clamp(percent) * (valueB-valueA); }
1571
1587
 
1572
1588
  /** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
1573
1589
  * A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
@@ -1598,11 +1614,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
1598
1614
  * @return {number}
1599
1615
  * @memberof Utilities */
1600
1616
  function lerpWrap(valueA, valueB, percent, wrapSize=1)
1601
- {
1602
- if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
1603
- console.warn('lerpWrap() parameter order changed! use lerpWrap(start, end, p)');
1604
- return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize);
1605
- }
1617
+ { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
1606
1618
 
1607
1619
  /** Returns signed wrapped distance between the two angles passed in
1608
1620
  * @param {number} angleA
@@ -2716,6 +2728,20 @@ let canvasClearColor = CLEAR_BLACK;
2716
2728
  * @memberof Settings */
2717
2729
  let canvasMaxSize = vec2(1920, 1080);
2718
2730
 
2731
+ /** Minimum aspect ratio of the canvas (width/height), unused if 0
2732
+ * Can be used with canvasMaxAspect to limit aspect ratio
2733
+ * @type {number}
2734
+ * @default
2735
+ * @memberof Settings */
2736
+ let canvasMinAspect = 0;
2737
+
2738
+ /** Maximum aspect ratio of the canvas (width/height), unused if 0
2739
+ * Can be used with canvasMinAspect to limit aspect ratio
2740
+ * @type {number}
2741
+ * @default
2742
+ * @memberof Settings */
2743
+ let canvasMaxAspect = 0;
2744
+
2719
2745
  /** Fixed size of the canvas, if enabled canvas size never changes
2720
2746
  * - you may also need to set mainCanvasSize if using screen space coords in startup
2721
2747
  * @type {Vector2}
@@ -2728,15 +2754,7 @@ let canvasFixedSize = vec2();
2728
2754
  * @type {boolean}
2729
2755
  * @default
2730
2756
  * @memberof Settings */
2731
- let canvasPixelated = true;
2732
-
2733
- /** Use nearest canvas scaling for more pixelated look
2734
- * - If enabled sets css image-rendering:pixelated
2735
- * - This defaults to false because text looks better with smoothing
2736
- * @type {boolean}
2737
- * @default
2738
- * @memberof Settings */
2739
- let overlayCanvasPixelated = false;
2757
+ let canvasPixelated = false;
2740
2758
 
2741
2759
  /** Disables texture filtering for crisper pixel art
2742
2760
  * @type {boolean}
@@ -2784,13 +2802,19 @@ let glCircleSides = 32;
2784
2802
  * @type {Vector2}
2785
2803
  * @default Vector2(16,16)
2786
2804
  * @memberof Settings */
2787
- let tileSizeDefault = vec2(16);
2805
+ let tileDefaultSize = vec2(16);
2788
2806
 
2789
- /** How many pixels smaller to draw tiles to prevent bleeding from neighbors
2807
+ /** Default padding pixels around tiles
2790
2808
  * @type {number}
2791
2809
  * @default
2792
2810
  * @memberof Settings */
2793
- let tileFixBleedScale = 0;
2811
+ let tileDefaultPadding = 0;
2812
+
2813
+ /** Default amount of pixels smaller to draw tiles to prevent neighbor bleeding
2814
+ * @type {number}
2815
+ * @default
2816
+ * @memberof Settings */
2817
+ let tileDefaultBleed = 0;
2794
2818
 
2795
2819
  ///////////////////////////////////////////////////////////////////////////////
2796
2820
  // Object settings
@@ -3012,13 +3036,22 @@ function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
3012
3036
  * @memberof Settings */
3013
3037
  function setCanvasMaxSize(size) { canvasMaxSize = size.copy(); }
3014
3038
 
3039
+ /** Set minimum aspect ratio of the canvas (width/height), unused if 0
3040
+ * @param {number} aspect
3041
+ * @memberof Settings */
3042
+ function setCanvasMinAspect(aspect) { canvasMinAspect = aspect; }
3043
+
3044
+ /** Set maximum aspect ratio of the canvas (width/height), unused if 0
3045
+ * @param {number} aspect
3046
+ * @memberof Settings */
3047
+ function setCanvasMaxAspect(aspect) { canvasMaxAspect = aspect; }
3048
+
3015
3049
  /** Set fixed size of the canvas
3016
3050
  * @param {Vector2} size
3017
3051
  * @memberof Settings */
3018
3052
  function setCanvasFixedSize(size) { canvasFixedSize = size.copy(); }
3019
3053
 
3020
3054
  /** Use nearest scaling algorithm for canvas for more pixelated look
3021
- * - If enabled sets css image-rendering:pixelated
3022
3055
  * @param {boolean} pixelated
3023
3056
  * @memberof Settings */
3024
3057
  function setCanvasPixelated(pixelated)
@@ -3030,18 +3063,6 @@ function setCanvasPixelated(pixelated)
3030
3063
  glCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
3031
3064
  }
3032
3065
 
3033
- /** Use nearest scaling algorithm for canvas for more pixelated look
3034
- * - If enabled sets css image-rendering:pixelated
3035
- * - This defaults to false because text looks better with smoothing
3036
- * @param {boolean} pixelated
3037
- * @memberof Settings */
3038
- function setOverlayCanvasPixelated(pixelated)
3039
- {
3040
- overlayCanvasPixelated = pixelated;
3041
- if (overlayCanvas)
3042
- overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
3043
- }
3044
-
3045
3066
  /** Disables texture filtering for crisper pixel art
3046
3067
  * @param {boolean} pixelated
3047
3068
  * @memberof Settings */
@@ -3085,12 +3106,17 @@ function setGLCircleSides(sides) { glCircleSides = sides; }
3085
3106
  /** Set default size of tiles in pixels
3086
3107
  * @param {Vector2} size
3087
3108
  * @memberof Settings */
3088
- function setTileSizeDefault(size) { tileSizeDefault = size.copy(); }
3109
+ function setTileDefaultSize(size) { tileDefaultSize = size.copy(); }
3089
3110
 
3090
- /** Set to prevent tile bleeding from neighbors in pixels
3091
- * @param {number} scale
3111
+ /** Default padding pixels around tiles
3112
+ * @param {number} padding
3113
+ * @memberof Settings */
3114
+ function setTileDefaultPadding(padding) { tileDefaultPadding = padding; }
3115
+
3116
+ /** Default amount of pixels smaller to draw tiles to prevent neighbor bleeding
3117
+ * @param {number} bleed
3092
3118
  * @memberof Settings */
3093
- function setTileFixBleedScale(scale) { tileFixBleedScale = scale; }
3119
+ function setTileDefaultBleed(bleed) { tileDefaultBleed = bleed; }
3094
3120
 
3095
3121
  /** Set if collisions between objects are enabled
3096
3122
  * @param {boolean} enable
@@ -3241,7 +3267,7 @@ function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUn
3241
3267
  /** Set if watermark with FPS should be shown
3242
3268
  * @param {boolean} show
3243
3269
  * @memberof Debug */
3244
- function setShowWatermark(show) { showWatermark = show; }
3270
+ function setDebugWatermark(show) { debugWatermark = show; }
3245
3271
 
3246
3272
  /** Set key code used to toggle debug mode, Esc by default
3247
3273
  * @param {string} key
@@ -3677,10 +3703,11 @@ class EngineObject
3677
3703
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
3678
3704
  getMirrorSign() { return this.mirror ? -1 : 1; }
3679
3705
 
3680
- /** Attaches a child to this with a given local transform
3706
+ /** Attaches a child to this with a local transform, returns child for chaining
3681
3707
  * @param {EngineObject} child
3682
3708
  * @param {Vector2} [localPos=(0,0)]
3683
- * @param {number} [localAngle] */
3709
+ * @param {number} [localAngle]
3710
+ * @return {EngineObject} The child object added */
3684
3711
  addChild(child, localPos=vec2(), localAngle=0)
3685
3712
  {
3686
3713
  ASSERT(!child.parent && !this.children.includes(child));
@@ -3690,6 +3717,7 @@ class EngineObject
3690
3717
  child.parent = this;
3691
3718
  child.localPos = localPos.copy();
3692
3719
  child.localAngle = localAngle;
3720
+ return child;
3693
3721
  }
3694
3722
 
3695
3723
  /** Removes a child from this one
@@ -3780,7 +3808,6 @@ class EngineObject
3780
3808
  * There are 3 canvas/contexts available to draw to...
3781
3809
  * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
3782
3810
  * glCanvas - Used by the accelerated WebGL batch rendering system.
3783
- * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
3784
3811
  *
3785
3812
  * The WebGL rendering system is very fast with some caveats...
3786
3813
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
@@ -3800,16 +3827,6 @@ let mainCanvas;
3800
3827
  * @memberof Draw */
3801
3828
  let mainContext;
3802
3829
 
3803
- /** A canvas that appears on top of everything the same size as mainCanvas
3804
- * @type {HTMLCanvasElement}
3805
- * @memberof Draw */
3806
- let overlayCanvas;
3807
-
3808
- /** 2d context for overlayCanvas
3809
- * @type {CanvasRenderingContext2D}
3810
- * @memberof Draw */
3811
- let overlayContext;
3812
-
3813
3830
  /** The default canvas to use for drawing, usually mainCanvas
3814
3831
  * @type {HTMLCanvasElement|OffscreenCanvas}
3815
3832
  * @memberof Draw */
@@ -3830,6 +3847,16 @@ let workCanvas;
3830
3847
  * @memberof Draw */
3831
3848
  let workContext;
3832
3849
 
3850
+ /** Offscreen canvas with willReadFrequently that can be used for image processing
3851
+ * @type {OffscreenCanvas}
3852
+ * @memberof Draw */
3853
+ let workReadCanvas;
3854
+
3855
+ /** Offscreen canvas with willReadFrequently that can be used for image processing
3856
+ * @type {OffscreenCanvasRenderingContext2D}
3857
+ * @memberof Draw */
3858
+ let workReadContext;
3859
+
3833
3860
  /** The size of the main canvas (and other secondary canvases)
3834
3861
  * @type {Vector2}
3835
3862
  * @memberof Draw */
@@ -3852,7 +3879,7 @@ let drawCount;
3852
3879
  * - This can take vecs or floats for easier use and conversion
3853
3880
  * - If an index is passed in, the tile size and index will determine the position
3854
3881
  * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
3855
- * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
3882
+ * @param {Vector2|number} [size] - Size of tile in pixels
3856
3883
  * @param {number} [textureIndex] - Texture index to use
3857
3884
  * @param {number} [padding] - How many pixels padding around tiles
3858
3885
  * @return {TileInfo}
@@ -3862,7 +3889,7 @@ let drawCount;
3862
3889
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
3863
3890
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
3864
3891
  * @memberof Draw */
3865
- function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3892
+ function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding)
3866
3893
  {
3867
3894
  if (headlessMode)
3868
3895
  return new TileInfo;
@@ -3901,13 +3928,13 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3901
3928
  class TileInfo
3902
3929
  {
3903
3930
  /** Create a tile info object
3904
- * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3905
- * @param {Vector2} [size=tileSizeDefault] - Size of tile in pixels
3906
- * @param {number} [textureIndex] - Texture index to use
3907
- * @param {number} [padding] - How many pixels padding around tiles
3908
- * @param {number} [bleedScale] - How many pixels smaller to draw tiles
3931
+ * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3932
+ * @param {Vector2} [size] - Size of tile in pixels
3933
+ * @param {number} [textureIndex] - Texture index to use
3934
+ * @param {number} [padding] - How many pixels padding around tiles
3935
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3909
3936
  */
3910
- constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0, bleedScale=tileFixBleedScale)
3937
+ constructor(pos=vec2(), size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3911
3938
  {
3912
3939
  /** @property {Vector2} - Top left corner of tile in pixels */
3913
3940
  this.pos = pos.copy();
@@ -3920,7 +3947,7 @@ class TileInfo
3920
3947
  /** @property {TextureInfo} - The texture info for this tile */
3921
3948
  this.textureInfo = textureInfos[this.textureIndex];
3922
3949
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
3923
- this.bleedScale = bleedScale;
3950
+ this.bleed = bleed;
3924
3951
  }
3925
3952
 
3926
3953
  /** Returns a copy of this tile offset by a vector
@@ -3928,7 +3955,7 @@ class TileInfo
3928
3955
  * @return {TileInfo}
3929
3956
  */
3930
3957
  offset(offset)
3931
- { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleedScale); }
3958
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleed); }
3932
3959
 
3933
3960
  /** Returns a copy of this tile offset by a number of animation frames
3934
3961
  * @param {number} frame - Offset to apply in animation frames
@@ -3951,7 +3978,7 @@ class TileInfo
3951
3978
  this.size = textureInfo.size.copy();
3952
3979
  this.textureInfo = textureInfo;
3953
3980
  // do not use padding or bleed
3954
- this.bleedScale = this.padding = 0;
3981
+ this.bleed = this.padding = 0;
3955
3982
  return this;
3956
3983
  }
3957
3984
  }
@@ -4017,7 +4044,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
4017
4044
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4018
4045
 
4019
4046
  const textureInfo = tileInfo && tileInfo.textureInfo;
4020
- const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
4047
+ const bleed = tileInfo?.bleed ?? 0;
4021
4048
  if (useWebGL && glEnable)
4022
4049
  {
4023
4050
  ASSERT(!!glContext, 'WebGL is not enabled!');
@@ -4032,13 +4059,13 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
4032
4059
  const w = tileInfo.size.x * sizeInverse.x;
4033
4060
  const h = tileInfo.size.y * sizeInverse.y;
4034
4061
  glSetTexture(textureInfo.glTexture);
4035
- if (bleedScale)
4062
+ if (bleed)
4036
4063
  {
4037
- const tileImageFixBleedX = sizeInverse.x*bleedScale;
4038
- const tileImageFixBleedY = sizeInverse.y*bleedScale;
4064
+ const bleedX = sizeInverse.x*bleed;
4065
+ const bleedY = sizeInverse.y*bleed;
4039
4066
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
4040
- x + tileImageFixBleedX, y + tileImageFixBleedY,
4041
- x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
4067
+ x + bleedX, y + bleedY,
4068
+ x - bleedX + w, y - bleedY + h,
4042
4069
  color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
4043
4070
  }
4044
4071
  else
@@ -4066,7 +4093,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
4066
4093
  // calculate uvs and render
4067
4094
  const x = tileInfo.pos.x, y = tileInfo.pos.y;
4068
4095
  const w = tileInfo.size.x, h = tileInfo.size.y;
4069
- drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor, bleedScale);
4096
+ drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor, bleed);
4070
4097
  }
4071
4098
  else
4072
4099
  {
@@ -4431,26 +4458,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
4431
4458
  drawTextScreen(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
4432
4459
  }
4433
4460
 
4434
- /** Draw text on overlay canvas in world space
4435
- * Automatically splits new lines into rows
4436
- * @param {string|number} text
4437
- * @param {Vector2} pos
4438
- * @param {number} [size]
4439
- * @param {Color} [color=(1,1,1,1)]
4440
- * @param {number} [lineWidth]
4441
- * @param {Color} [lineColor=(0,0,0,1)]
4442
- * @param {CanvasTextAlign} [textAlign='center']
4443
- * @param {string} [font=fontDefault]
4444
- * @param {string} [fontStyle]
4445
- * @param {number} [maxWidth]
4446
- * @param {number} [angle]
4447
- * @memberof Draw */
4448
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
4449
- {
4450
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
4451
- }
4452
-
4453
- /** Draw text on overlay canvas in screen space
4461
+ /** Draw text in screen space
4454
4462
  * Automatically splits new lines into rows
4455
4463
  * @param {string|number} text
4456
4464
  * @param {Vector2} pos
@@ -4463,9 +4471,9 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
4463
4471
  * @param {string} [fontStyle]
4464
4472
  * @param {number} [maxWidth]
4465
4473
  * @param {number} [angle]
4466
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
4474
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
4467
4475
  * @memberof Draw */
4468
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
4476
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=mainContext)
4469
4477
  {
4470
4478
  ASSERT(isString(text), 'text must be a string');
4471
4479
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4509,16 +4517,16 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4509
4517
  * @memberof Draw */
4510
4518
  function screenToWorld(screenPos)
4511
4519
  {
4520
+ ASSERT(isVector2(screenPos), 'screenPos must be a vec2');
4521
+
4512
4522
  let x = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
4513
4523
  let y = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
4514
4524
  if (cameraAngle)
4515
4525
  {
4516
4526
  // apply camera rotation
4517
4527
  const c = cos(-cameraAngle), s = sin(-cameraAngle);
4518
- const rotatedX = x * c - y * s;
4519
- const rotatedY = x * s + y * c;
4520
- x = rotatedX;
4521
- y = rotatedY;
4528
+ const xr = x * c - y * s, yr = x * s + y * c;
4529
+ x = xr; y = yr;
4522
4530
  }
4523
4531
  return new Vector2(x + cameraPos.x, y + cameraPos.y);
4524
4532
  }
@@ -4529,16 +4537,16 @@ function screenToWorld(screenPos)
4529
4537
  * @memberof Draw */
4530
4538
  function worldToScreen(worldPos)
4531
4539
  {
4540
+ ASSERT(isVector2(worldPos), 'worldPos must be a vec2');
4541
+
4532
4542
  let x = worldPos.x - cameraPos.x;
4533
4543
  let y = worldPos.y - cameraPos.y;
4534
4544
  if (cameraAngle)
4535
4545
  {
4536
4546
  // apply inverse camera rotation
4537
4547
  const c = cos(cameraAngle), s = sin(cameraAngle);
4538
- const rotatedX = x * c - y * s;
4539
- const rotatedY = x * s + y * c;
4540
- x = rotatedX;
4541
- y = rotatedY;
4548
+ const xr = x * c - y * s, yr = x * s + y * c;
4549
+ x = xr; y = yr;
4542
4550
  }
4543
4551
  return new Vector2
4544
4552
  (
@@ -4553,16 +4561,16 @@ function worldToScreen(worldPos)
4553
4561
  * @memberof Draw */
4554
4562
  function screenToWorldDelta(screenDelta)
4555
4563
  {
4564
+ ASSERT(isVector2(screenDelta), 'screenDelta must be a vec2');
4565
+
4556
4566
  let x = screenDelta.x / cameraScale;
4557
4567
  let y = screenDelta.y / -cameraScale;
4558
4568
  if (cameraAngle)
4559
4569
  {
4560
4570
  // apply camera rotation
4561
4571
  const c = cos(-cameraAngle), s = sin(-cameraAngle);
4562
- const rotatedX = x * c - y * s;
4563
- const rotatedY = x * s + y * c;
4564
- x = rotatedX;
4565
- y = rotatedY;
4572
+ const xr = x * c - y * s, yr = x * s + y * c;
4573
+ x = xr; y = yr;
4566
4574
  }
4567
4575
  return new Vector2(x, y);
4568
4576
  }
@@ -4573,16 +4581,16 @@ function screenToWorldDelta(screenDelta)
4573
4581
  * @memberof Draw */
4574
4582
  function worldToScreenDelta(worldDelta)
4575
4583
  {
4584
+ ASSERT(isVector2(worldDelta), 'worldDelta must be a vec2');
4585
+
4576
4586
  let x = worldDelta.x;
4577
4587
  let y = worldDelta.y;
4578
4588
  if (cameraAngle)
4579
4589
  {
4580
4590
  // apply inverse camera rotation
4581
4591
  const c = cos(cameraAngle), s = sin(cameraAngle);
4582
- const rotatedX = x * c - y * s;
4583
- const rotatedY = x * s + y * c;
4584
- x = rotatedX;
4585
- y = rotatedY;
4592
+ const xr = x * c - y * s, yr = x * s + y * c;
4593
+ x = xr; y = yr;
4586
4594
  }
4587
4595
  return new Vector2(x * cameraScale, y * -cameraScale);
4588
4596
  }
@@ -4595,6 +4603,10 @@ function worldToScreenDelta(worldDelta)
4595
4603
  * @memberof Draw */
4596
4604
  function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
4597
4605
  {
4606
+ ASSERT(isVector2(screenPos), 'screenPos must be a vec2');
4607
+ ASSERT(isVector2(screenSize), 'screenSize must be a vec2');
4608
+ ASSERT(isNumber(screenAngle), 'screenAngle must be a number');
4609
+
4598
4610
  return [
4599
4611
  screenToWorld(screenPos),
4600
4612
  screenSize.scale(1/cameraScale),
@@ -4610,6 +4622,10 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
4610
4622
  * @memberof Draw */
4611
4623
  function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
4612
4624
  {
4625
+ ASSERT(isVector2(worldPos), 'worldPos must be a vec2');
4626
+ ASSERT(isVector2(worldSize), 'worldSize must be a vec2');
4627
+ ASSERT(isNumber(worldAngle), 'worldAngle must be a number');
4628
+
4613
4629
  return [
4614
4630
  worldToScreen(worldPos),
4615
4631
  worldSize.scale(cameraScale),
@@ -4622,8 +4638,8 @@ function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
4622
4638
  * @memberof Draw */
4623
4639
  function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
4624
4640
 
4625
- /** Check if a point or circle is on screen
4626
- * If size is a Vector2, uses the largest dimension as diameter
4641
+ /** Check if a box, point, or circle is on screen with a circle test
4642
+ * If size is a Vector2, uses the length as diameter
4627
4643
  * This can be used to cull offscreen objects from render or update
4628
4644
  * @param {Vector2} pos - world space position
4629
4645
  * @param {Vector2|number} size - world space size or diameter
@@ -4631,12 +4647,30 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
4631
4647
  * @memberof Draw */
4632
4648
  function isOnScreen(pos, size=0)
4633
4649
  {
4634
- pos = worldToScreen(pos);
4650
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4651
+ ASSERT(isVector2(size) || isNumber(size), 'size must be a vec2 or number');
4652
+
4653
+ // optimized circle on screen test
4654
+ // pos = worldToScreen(pos);
4655
+ let x = pos.x - cameraPos.x;
4656
+ let y = pos.y - cameraPos.y;
4657
+ if (cameraAngle)
4658
+ {
4659
+ // apply inverse camera rotation
4660
+ const c = cos(cameraAngle), s = sin(cameraAngle);
4661
+ const xr = x * c - y * s, yr = x * s + y * c;
4662
+ x = xr; y = yr;
4663
+ }
4664
+ x *= cameraScale*2; y *= -cameraScale*2;
4665
+
4635
4666
  if (size instanceof Vector2)
4636
- size = max(size.x, size.y); // use largest dimension
4637
- size *= cameraScale/2;
4638
- return pos.x + size > 0 && pos.x - size < mainCanvasSize.x &&
4639
- pos.y + size > 0 && pos.y - size < mainCanvasSize.y;
4667
+ size = size.length(); // use length of vector as diameter
4668
+ size *= cameraScale;
4669
+
4670
+ // check against screen bounds
4671
+ const w = mainCanvasSize.x, h = mainCanvasSize.y;
4672
+ return x + size > -w && x - size < w &&
4673
+ y + size > -h && y - size < h;
4640
4674
  }
4641
4675
 
4642
4676
  /** Enable normal or additive blend mode
@@ -4650,18 +4684,19 @@ function setBlendMode(additive=false, context)
4650
4684
  context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
4651
4685
  }
4652
4686
 
4653
- /** Combines all LittleJS canvases onto the main canvas and clears them
4654
- * This is necessary for things like saving a screenshot
4687
+ /** Combines LittleJS canvases onto the main canvas
4688
+ * This is necessary for things like screenshots and video
4655
4689
  * @memberof Draw */
4656
4690
  function combineCanvases()
4657
4691
  {
4658
- // combine canvases
4659
- glCopyToContext(mainContext);
4660
- mainContext.drawImage(overlayCanvas, 0, 0);
4661
-
4662
- // clear canvases
4663
- glClearCanvas();
4664
- overlayCanvas.width |= 0;
4692
+ const w = mainCanvasSize.x, h = mainCanvasSize.y;
4693
+ workCanvas.width = w;
4694
+ workCanvas.height = h;
4695
+ workContext.fillRect(0,0,w,h); // remove background alpha
4696
+ glCopyToContext(workContext);
4697
+ workContext.drawImage(mainCanvas, 0, 0);
4698
+ mainCanvas.width |= 0;
4699
+ mainContext.drawImage(workCanvas, 0, 0);
4665
4700
  }
4666
4701
 
4667
4702
  /** Helper function to draw an image with color and additive color applied
@@ -4678,18 +4713,18 @@ function combineCanvases()
4678
4713
  * @param {number} dHeight
4679
4714
  * @param {Color} color
4680
4715
  * @param {Color} [additiveColor]
4681
- * @param {number} [bleedScale] - How much to shrink the source, used to fix bleeding
4716
+ * @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding
4682
4717
  * @memberof Draw */
4683
- function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleedScale=0)
4718
+ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
4684
4719
  {
4685
4720
  function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
4686
4721
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
4687
- const sx2 = bleedScale;
4688
- const sy2 = bleedScale;
4722
+ const sx2 = bleed;
4723
+ const sy2 = bleed;
4689
4724
  sWidth = max(1,sWidth|0);
4690
4725
  sHeight = max(1,sHeight|0);
4691
- const sWidth2 = sWidth - 2*bleedScale;
4692
- const sHeight2 = sHeight - 2*bleedScale;
4726
+ const sWidth2 = sWidth - 2*bleed;
4727
+ const sHeight2 = sHeight - 2*bleed;
4693
4728
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
4694
4729
  {
4695
4730
  // white texture with no additive alpha, no need to tint
@@ -4700,12 +4735,12 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4700
4735
  else
4701
4736
  {
4702
4737
  // copy to offscreen canvas
4703
- workCanvas.width = sWidth;
4704
- workCanvas.height = sHeight;
4705
- workContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
4738
+ workReadCanvas.width = sWidth;
4739
+ workReadCanvas.height = sHeight;
4740
+ workReadContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
4706
4741
 
4707
4742
  // tint image using offscreen work context
4708
- const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
4743
+ const imageData = workReadContext.getImageData(0, 0, sWidth, sHeight);
4709
4744
  const data = imageData.data;
4710
4745
  if (additiveColor && !isBlack(additiveColor))
4711
4746
  {
@@ -4714,8 +4749,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4714
4749
  const colorAdd = [additiveColor.r * 255, additiveColor.g * 255, additiveColor.b * 255, additiveColor.a * 255];
4715
4750
  for (let i = 0; i < data.length; ++i)
4716
4751
  data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
4717
- workContext.putImageData(imageData, 0, 0);
4718
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4752
+ workReadContext.putImageData(imageData, 0, 0);
4753
+ context.drawImage(workReadCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4719
4754
  }
4720
4755
  else
4721
4756
  {
@@ -4726,9 +4761,9 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4726
4761
  data[i+1] *= color.g;
4727
4762
  data[i+2] *= color.b;
4728
4763
  }
4729
- workContext.putImageData(imageData, 0, 0);
4764
+ workReadContext.putImageData(imageData, 0, 0);
4730
4765
  context.globalAlpha = color.a;
4731
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4766
+ context.drawImage(workReadCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4732
4767
  context.globalAlpha = 1;
4733
4768
  }
4734
4769
  }
@@ -4790,7 +4825,7 @@ class FontImage
4790
4825
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
4791
4826
  {
4792
4827
  // load default font image
4793
- if (!engineFontImage)
4828
+ if (!image && !engineFontImage)
4794
4829
  {
4795
4830
  engineFontImage = new Image;
4796
4831
  engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
@@ -4813,23 +4848,14 @@ class FontImage
4813
4848
  this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
4814
4849
  }
4815
4850
 
4816
- /** Draw text on overlay canvas in world space using the image font
4817
- * @param {string|number} text
4818
- * @param {Vector2} pos
4819
- * @param {number} [scale]
4820
- * @param {boolean} [center]
4821
- */
4822
- drawTextOverlay(text, pos, scale=4, center)
4823
- { this.drawText(text, pos, scale, center, overlayContext); }
4824
-
4825
- /** Draw text on overlay canvas in screen space using the image font
4851
+ /** Draw text in screen space using the image font
4826
4852
  * @param {string|number} text
4827
4853
  * @param {Vector2} pos
4828
4854
  * @param {number} [scale]
4829
4855
  * @param {boolean} [center]
4830
4856
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4831
4857
  */
4832
- drawTextScreen(text, pos, scale=4, center=true, context=overlayContext)
4858
+ drawTextScreen(text, pos, scale=4, center=true, context=mainContext)
4833
4859
  {
4834
4860
  context.save();
4835
4861
  const size = this.tileSize;
@@ -5100,6 +5126,16 @@ function gamepadConnected(gamepad=gamepadPrimary)
5100
5126
  return !!inputData[gamepad+1];
5101
5127
  }
5102
5128
 
5129
+ /** Returns how many control sticks the passed in gamepad has
5130
+ * @param {number} [gamepad]
5131
+ * @return {number}
5132
+ * @memberof Input */
5133
+ function gamepadStickCount(gamepad=gamepadPrimary)
5134
+ {
5135
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
5136
+ return gamepadStickData[gamepad]?.length ?? 0;
5137
+ }
5138
+
5103
5139
  /** True if a touch device has been detected
5104
5140
  * @memberof Input */
5105
5141
  const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
@@ -5440,9 +5476,11 @@ function inputUpdate()
5440
5476
  dpad.set(x, -y);
5441
5477
  sticks[0] = dpad.clampLength(); // clamp to circle
5442
5478
  }
5443
- const rightTouchStick = touchGamepadSticks[1] ?? vec2();
5444
5479
  if (touchGamepadButtonCount === 1)
5480
+ {
5481
+ const rightTouchStick = touchGamepadSticks[1] ?? vec2();
5445
5482
  sticks[1] = applyDeadZones(rightTouchStick);
5483
+ }
5446
5484
 
5447
5485
  // read virtual gamepad buttons
5448
5486
  const data = inputData[1] ?? (inputData[1] = []);
@@ -5567,7 +5605,7 @@ function inputRender()
5567
5605
  return;
5568
5606
 
5569
5607
  // setup the canvas
5570
- const context = overlayContext;
5608
+ const context = mainContext;
5571
5609
  context.save();
5572
5610
  context.globalAlpha = alpha*touchGamepadAlpha;
5573
5611
  context.strokeStyle = '#fff';
@@ -5809,12 +5847,12 @@ class Sound
5809
5847
  ///////////////////////////////////////////////////////////////////////////////
5810
5848
 
5811
5849
  /**
5812
- * Sound Wave Object - Stores a wave sound for later use and can be played positionally
5813
- * - this can be used to play wave, mp3, and ogg files
5850
+ * Sound Wave Object - Loads and stores an audio file for later use
5851
+ * - this can be used to load and play wave, mp3, and ogg files
5814
5852
  * @extends Sound
5815
5853
  * @memberof Audio
5816
5854
  * @example
5817
- * // create a sound
5855
+ * // load an audio asset file
5818
5856
  * const sound_example = new SoundWave('sound.mp3');
5819
5857
  *
5820
5858
  * // play the sound
@@ -6534,8 +6572,7 @@ class CanvasLayer extends EngineObject
6534
6572
  /** Destroy this canvas layer */
6535
6573
  destroy()
6536
6574
  {
6537
- if (this.destroyed)
6538
- return;
6575
+ if (this.destroyed) return;
6539
6576
 
6540
6577
  this.textureInfo.destroyWebGLTexture();
6541
6578
  super.destroy();
@@ -6767,8 +6804,8 @@ class TileLayer extends CanvasLayer
6767
6804
  if (!this.context) return;
6768
6805
 
6769
6806
  // save current render settings
6770
- /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number]} */
6771
- this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale];
6807
+ /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6808
+ this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
6772
6809
 
6773
6810
  // set the draw canvas and context to this layer
6774
6811
  // use camera settings to match this layer's canvas
@@ -6777,6 +6814,7 @@ class TileLayer extends CanvasLayer
6777
6814
  cameraPos = this.size.scale(.5);
6778
6815
  const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
6779
6816
  cameraScale = tileSize.x;
6817
+ canvasClearColor = CLEAR_BLACK;
6780
6818
  mainCanvasSize = this.size.multiply(tileSize);
6781
6819
  if (clear)
6782
6820
  {
@@ -6786,7 +6824,7 @@ class TileLayer extends CanvasLayer
6786
6824
  }
6787
6825
 
6788
6826
  // disable smoothing for pixel art
6789
- this.context.imageSmoothingEnabled = !tilesPixelated;
6827
+ drawContext.imageSmoothingEnabled = !tilesPixelated;
6790
6828
 
6791
6829
  // setup gl rendering if enabled
6792
6830
  glPreRender();
@@ -6802,7 +6840,7 @@ class TileLayer extends CanvasLayer
6802
6840
  //debugSaveCanvas(this.canvas);
6803
6841
 
6804
6842
  // set stuff back to normal
6805
- [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale] = this.savedRenderSettings;
6843
+ [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6806
6844
  }
6807
6845
 
6808
6846
  /** Draw the tile at a given position in the tile grid
@@ -7087,9 +7125,9 @@ class ParticleEmitter extends EngineObject
7087
7125
  /** @property {Color} - Color at start of life 2, randomized between start colors */
7088
7126
  this.colorStartB = colorStartB.copy();
7089
7127
  /** @property {Color} - Color at end of life 1, randomized between end colors */
7090
- this.colorEndA = colorEndA.copy();
7128
+ this.colorEndA = colorEndA.copy();
7091
7129
  /** @property {Color} - Color at end of life 2, randomized between end colors */
7092
- this.colorEndB = colorEndB.copy();
7130
+ this.colorEndB = colorEndB.copy();
7093
7131
  /** @property {boolean} - Should color be randomized linearly or across each component */
7094
7132
  this.randomColorLinear = randomColorLinear;
7095
7133
 
@@ -7173,8 +7211,10 @@ class ParticleEmitter extends EngineObject
7173
7211
  if (debugParticles)
7174
7212
  {
7175
7213
  // show emitter bounds
7176
- const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
7177
- debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
7214
+ if (typeof this.emitSize === 'number')
7215
+ debugCircle(this.pos, this.emitSize/2, '#0f0');
7216
+ else
7217
+ debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle);
7178
7218
  }
7179
7219
  }
7180
7220
 
@@ -7184,8 +7224,8 @@ class ParticleEmitter extends EngineObject
7184
7224
  {
7185
7225
  // spawn a particle
7186
7226
  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
7227
+ randInCircle(this.emitSize/2) // circle emitter
7228
+ : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
7189
7229
  .multiply(this.emitSize).rotate(this.angle)
7190
7230
  let angle = rand(this.particleConeAngle, -this.particleConeAngle);
7191
7231
  if (!this.localSpace)
@@ -7513,10 +7553,10 @@ class Medal
7513
7553
  */
7514
7554
  render(hidePercent=0)
7515
7555
  {
7516
- const context = overlayContext;
7556
+ const context = mainContext;
7517
7557
  const width = min(medalDisplaySize.x, mainCanvas.width);
7518
7558
  const height = medalDisplaySize.y;
7519
- const x = overlayCanvas.width - width;
7559
+ const x = mainCanvas.width - width;
7520
7560
  const y = -height*hidePercent;
7521
7561
  const backgroundColor = hsl(0,0,.9);
7522
7562
 
@@ -7557,7 +7597,7 @@ class Medal
7557
7597
  {
7558
7598
  // draw the image or icon
7559
7599
  if (this.image)
7560
- overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
7600
+ mainContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
7561
7601
  else
7562
7602
  drawTextScreen(this.icon, pos, size*.7, BLACK);
7563
7603
  }
@@ -7578,7 +7618,7 @@ class Medal
7578
7618
  * @namespace WebGL
7579
7619
  */
7580
7620
 
7581
- /** The WebGL canvas which appears above the main canvas and below the overlay canvas
7621
+ /** The WebGL canvas which appears below the main canvas
7582
7622
  * @type {HTMLCanvasElement}
7583
7623
  * @memberof WebGL */
7584
7624
  let glCanvas;
@@ -7608,7 +7648,7 @@ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE |
7608
7648
  ///////////////////////////////////////////////////////////////////////////////
7609
7649
 
7610
7650
  // Initialize WebGL, called automatically by the engine
7611
- function glInit()
7651
+ function glInit(rootElement)
7612
7652
  {
7613
7653
  // keep set of texture infos so they can be restored if context is lost
7614
7654
  glTextureInfos = new Set;
@@ -7632,8 +7672,7 @@ function glInit()
7632
7672
  return;
7633
7673
  }
7634
7674
 
7635
- // attach the WebGL canvas
7636
- const rootElement = mainCanvas.parentElement;
7675
+ // attach the WebGL canvas;
7637
7676
  rootElement.appendChild(glCanvas);
7638
7677
 
7639
7678
  // startup webgl
@@ -7851,6 +7890,9 @@ function glClearCanvas()
7851
7890
  glCanvas.width = drawCanvas.width;
7852
7891
  glCanvas.height = drawCanvas.height;
7853
7892
  glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
7893
+ const color = canvasClearColor;
7894
+ if (color.a > 0)
7895
+ glContext.clearColor(color.r, color.g, color.b, color.a);
7854
7896
  glContext.clear(glContext.COLOR_BUFFER_BIT);
7855
7897
  }
7856
7898
 
@@ -8080,7 +8122,6 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
8080
8122
  glFlush();
8081
8123
  glSetInstancedMode();
8082
8124
 
8083
- glPolyMode = false;
8084
8125
  let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
8085
8126
  glPositionData[offset++] = x;
8086
8127
  glPositionData[offset++] = y;
@@ -8601,13 +8642,12 @@ class PostProcessPlugin
8601
8642
  {
8602
8643
  /** Create global post processing shader
8603
8644
  * @param {string} shaderCode
8604
- * @param {boolean} [includeOverlay]
8605
8645
  * @param {boolean} [includeMainCanvas]
8606
8646
  * @example
8607
8647
  * // create the post process plugin object
8608
8648
  * new PostProcessPlugin(shaderCode);
8609
8649
  */
8610
- constructor(shaderCode, includeOverlay=false, includeMainCanvas=true)
8650
+ constructor(shaderCode, includeMainCanvas=true)
8611
8651
  {
8612
8652
  ASSERT(!postProcess, 'Post process already initialized');
8613
8653
  postProcess = this;
@@ -8679,17 +8719,19 @@ class PostProcessPlugin
8679
8719
  // clear out the buffer
8680
8720
  glFlush();
8681
8721
 
8682
- if (includeMainCanvas || includeOverlay)
8722
+ // setup texture
8723
+ glContext.activeTexture(glContext.TEXTURE0);
8724
+ glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
8725
+ if (includeMainCanvas)
8683
8726
  {
8684
- // copy WebGL to the main canvas
8685
- mainContext.drawImage(glCanvas, 0, 0);
8686
-
8687
- if (includeOverlay)
8688
- {
8689
- // copy overlay canvas so it will be included in post processing
8690
- mainContext.drawImage(overlayCanvas, 0, 0);
8691
- overlayCanvas.width |= 0; // clear overlay canvas
8692
- }
8727
+ // copy main canvas to work canvas
8728
+ workCanvas.width = mainCanvasSize.x;
8729
+ workCanvas.height = mainCanvasSize.y;
8730
+ glCopyToContext(workContext);
8731
+ workContext.drawImage(mainCanvas, 0, 0);
8732
+
8733
+ // copy work canvas to texture
8734
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
8693
8735
  }
8694
8736
 
8695
8737
  // setup shader program to draw a quad
@@ -8698,14 +8740,6 @@ class PostProcessPlugin
8698
8740
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
8699
8741
  glContext.disable(glContext.BLEND);
8700
8742
 
8701
- // set textures, pass in the 2d canvas and gl canvas in separate texture channels
8702
- glContext.activeTexture(glContext.TEXTURE0);
8703
- glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
8704
- if (includeMainCanvas || includeOverlay)
8705
- {
8706
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
8707
- }
8708
-
8709
8743
  // set vertex position attribute
8710
8744
  const vertexByteStride = 8;
8711
8745
  const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
@@ -8934,12 +8968,14 @@ class UISystemPlugin
8934
8968
  * // create the ui plugin object
8935
8969
  * new UISystemPlugin;
8936
8970
  */
8937
- constructor(context=overlayContext)
8971
+ constructor(context=mainContext)
8938
8972
  {
8939
8973
  ASSERT(!uiSystem, 'UI system already initialized');
8940
8974
  uiSystem = this;
8941
8975
 
8942
8976
  // default settings
8977
+ /** @property {boolean} - Activate when mouse is pressed down instead of clicked */
8978
+ this.activateOnPress = false;
8943
8979
  /** @property {Color} - Default fill color for UI elements */
8944
8980
  this.defaultColor = WHITE;
8945
8981
  /** @property {Color} - Default outline color for UI elements */
@@ -8980,8 +9016,6 @@ class UISystemPlugin
8980
9016
  // navigation properties
8981
9017
  /** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
8982
9018
  this.navigationObject = undefined;
8983
- /** @property {number} - Gamepad index to use for UI navigation */
8984
- this.navigationGamepadIndex = 0;
8985
9019
  /** @property {Timer} - Cooldown timer for navigation inputs */
8986
9020
  this.navigationTimer = new Timer(undefined, true);
8987
9021
  /** @property {number} - Time between navigation inputs in seconds */
@@ -9389,9 +9423,8 @@ class UISystemPlugin
9389
9423
  const both = uiSystem.navigationDirection === 2;
9390
9424
  if (isUsingGamepad)
9391
9425
  {
9392
- const gamepad = this.navigationGamepadIndex;
9393
- const stick = gamepadStick(0, gamepad);
9394
- const dpad = gamepadDpad(gamepad);
9426
+ const stick = gamepadStick(0, gamepadPrimary);
9427
+ const dpad = gamepadDpad(gamepadPrimary);
9395
9428
  if (both)
9396
9429
  return -(stick.y || dpad.y) || (stick.x || dpad.x);
9397
9430
  return vertical ? -(stick.y || dpad.y) : (stick.x || dpad.x);
@@ -9417,9 +9450,8 @@ class UISystemPlugin
9417
9450
  const vertical = uiSystem.navigationDirection === 1;
9418
9451
  if (isUsingGamepad)
9419
9452
  {
9420
- const gamepad = this.navigationGamepadIndex;
9421
- const stick = gamepadStick(0, gamepad);
9422
- const dpad = gamepadDpad(gamepad);
9453
+ const stick = gamepadStick(0, gamepadPrimary);
9454
+ const dpad = gamepadDpad(gamepadPrimary);
9423
9455
  return !vertical ? (stick.y || dpad.y) : (stick.x || dpad.x);
9424
9456
  }
9425
9457
  const back = !vertical ? 'ArrowUp' : 'ArrowLeft';
@@ -9431,8 +9463,7 @@ class UISystemPlugin
9431
9463
  * @return {boolean} */
9432
9464
  getNavigationWasPressed()
9433
9465
  {
9434
- const gamepad = this.navigationGamepadIndex;
9435
- return isUsingGamepad ? gamepadWasPressed(0, gamepad) :
9466
+ return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
9436
9467
  keyWasPressed('Space') || keyWasPressed('Enter');
9437
9468
  }
9438
9469
 
@@ -9596,13 +9627,15 @@ class UIObject
9596
9627
  uiSystem.uiObjects.push(this);
9597
9628
  }
9598
9629
 
9599
- /** Add a child UIObject to this object
9600
- * @param {UIObject} child */
9630
+ /** Add a child UIObject to this object, returns child for chaining
9631
+ * @param {UIObject} child
9632
+ * @return {UIObject} The child object added */
9601
9633
  addChild(child)
9602
9634
  {
9603
9635
  ASSERT(!child.parent && !this.children.includes(child));
9604
9636
  this.children.push(child);
9605
9637
  child.parent = this;
9638
+ return child;
9606
9639
  }
9607
9640
 
9608
9641
  /** Remove a child UIObject from this object
@@ -9675,8 +9708,16 @@ class UIObject
9675
9708
  if (uiSystem.activeObject && !isActive)
9676
9709
  uiSystem.activeObject.onRelease();
9677
9710
  uiSystem.activeObject = this;
9711
+
9712
+ if (uiSystem.activateOnPress)
9713
+ {
9714
+ this.onClick();
9715
+ if (!this.soundPress && this.soundClick)
9716
+ this.soundClick.play();
9717
+ }
9678
9718
  }
9679
9719
  }
9720
+ if (!uiSystem.activateOnPress)
9680
9721
  if (!mouseDown && this.isActiveObject() && this.interactive)
9681
9722
  {
9682
9723
  this.onClick();
@@ -12116,33 +12157,33 @@ async function box2dInit()
12116
12157
  color = getDebugColor(color);
12117
12158
  point1 = box2d.vec2FromPointer(point1);
12118
12159
  point2 = box2d.vec2FromPointer(point2);
12119
- drawLine(point1, point2, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
12160
+ drawLine(point1, point2, debugLineWidth, color, vec2(), 0, false);
12120
12161
  };
12121
12162
  debugDraw.DrawPolygon = function(vertices, vertexCount, color)
12122
12163
  {
12123
12164
  color = getDebugColor(color);
12124
12165
  const points = getPointsList(vertices, vertexCount);
12125
- drawPoly(points, CLEAR_WHITE, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
12166
+ drawPoly(points, CLEAR_WHITE, debugLineWidth, color, vec2(), 0, false);
12126
12167
  };
12127
12168
  debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
12128
12169
  {
12129
12170
  color = getDebugColor(color);
12130
12171
  const points = getPointsList(vertices, vertexCount);
12131
- drawPoly(points, color, 0, color, vec2(), 0, false, false, overlayContext);
12172
+ drawPoly(points, color, 0, color, vec2(), 0, false);
12132
12173
  };
12133
12174
  debugDraw.DrawCircle = function(center, radius, color)
12134
12175
  {
12135
12176
  color = getDebugColor(color);
12136
12177
  center = box2d.vec2FromPointer(center);
12137
- drawCircle(center, radius*2, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
12178
+ drawCircle(center, radius*2, CLEAR_WHITE, debugLineWidth, color, false);
12138
12179
  };
12139
12180
  debugDraw.DrawSolidCircle = function(center, radius, axis, color)
12140
12181
  {
12141
12182
  color = getDebugColor(color);
12142
12183
  center = box2d.vec2FromPointer(center);
12143
12184
  axis = box2d.vec2FromPointer(axis).scale(radius);
12144
- drawCircle(center, radius*2, color, debugLineWidth, color, false, false, overlayContext);
12145
- drawLine(vec2(), axis, debugLineWidth, color, center, 0, false, false, overlayContext);
12185
+ drawCircle(center, radius*2, color, debugLineWidth, color, false);
12186
+ drawLine(vec2(), axis, debugLineWidth, color, center, 0, false);
12146
12187
  };
12147
12188
  debugDraw.DrawTransform = function(transform)
12148
12189
  {
@@ -12151,8 +12192,8 @@ async function box2dInit()
12151
12192
  const angle = -transform.get_q().GetAngle();
12152
12193
  const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
12153
12194
  const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
12154
- drawLine(vec2(), p1, debugLineWidth, c1, pos, angle, false, false, overlayContext);
12155
- drawLine(vec2(), p2, debugLineWidth, c2, pos, angle, false, false, overlayContext);
12195
+ drawLine(vec2(), p1, debugLineWidth, c1, pos, angle, false);
12196
+ drawLine(vec2(), p2, debugLineWidth, c2, pos, angle, false);
12156
12197
  }
12157
12198
 
12158
12199
  debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
@@ -12172,8 +12213,8 @@ async function box2dInit()
12172
12213
 
12173
12214
  ///////////////////////////////////////////////////////////////////////////////
12174
12215
 
12175
- /** Draw a scalable nine-slice UI element to the overlay canvas in screen space
12176
- * This function can not apply color because it draws using the overlay 2d context
12216
+ /** Draw a scalable nine-slice UI element to the main canvas in screen space
12217
+ * This function can not apply color because it draws using the 2d context
12177
12218
  * @param {Vector2} pos - Screen space position
12178
12219
  * @param {Vector2} size - Screen space size
12179
12220
  * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
@@ -12183,7 +12224,7 @@ async function box2dInit()
12183
12224
  * @memberof DrawUtilities */
12184
12225
  function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
12185
12226
  {
12186
- drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
12227
+ drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true);
12187
12228
  }
12188
12229
 
12189
12230
  /** Draw a scalable nine-slice UI element in world space
@@ -12232,8 +12273,8 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
12232
12273
  }
12233
12274
  }
12234
12275
 
12235
- /** Draw a scalable three-slice UI element to the overlay canvas in screen space
12236
- * This function can not apply color because it draws using the overlay 2d context
12276
+ /** Draw a scalable three-slice UI element to the main canvas in screen space
12277
+ * This function can not apply color because it draws using the 2d context
12237
12278
  * @param {Vector2} pos - Screen space position
12238
12279
  * @param {Vector2} size - Screen space size
12239
12280
  * @param {TileInfo} startTile - Starting tile for the three-slice pattern
@@ -12243,7 +12284,7 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
12243
12284
  * @memberof DrawUtilities */
12244
12285
  function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
12245
12286
  {
12246
- drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
12287
+ drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true);
12247
12288
  }
12248
12289
 
12249
12290
  /** Draw a scalable three-slice UI element in world space
@@ -12323,7 +12364,7 @@ export
12323
12364
  // Globals
12324
12365
  debug,
12325
12366
  debugOverlay,
12326
- showWatermark,
12367
+ debugWatermark,
12327
12368
 
12328
12369
  // Debug
12329
12370
  ASSERT,
@@ -12352,15 +12393,17 @@ export
12352
12393
  canvasColorTiles,
12353
12394
  canvasClearColor,
12354
12395
  canvasMaxSize,
12396
+ canvasMinAspect,
12397
+ canvasMaxAspect,
12355
12398
  canvasFixedSize,
12356
12399
  canvasPixelated,
12357
- overlayCanvasPixelated,
12358
12400
  tilesPixelated,
12359
12401
  fontDefault,
12360
12402
  showSplashScreen,
12361
12403
  headlessMode,
12362
- tileSizeDefault,
12363
- tileFixBleedScale,
12404
+ tileDefaultSize,
12405
+ tileDefaultPadding,
12406
+ tileDefaultBleed,
12364
12407
  enablePhysicsSolver,
12365
12408
  objectDefaultMass,
12366
12409
  objectDefaultDamping,
@@ -12395,16 +12438,18 @@ export
12395
12438
  setCanvasColorTiles,
12396
12439
  setCanvasClearColor,
12397
12440
  setCanvasMaxSize,
12441
+ setCanvasMinAspect,
12442
+ setCanvasMaxAspect,
12398
12443
  setCanvasFixedSize,
12399
12444
  setCanvasPixelated,
12400
- setOverlayCanvasPixelated,
12401
12445
  setTilesPixelated,
12402
12446
  setFontDefault,
12403
12447
  setShowSplashScreen,
12404
12448
  setHeadlessMode,
12405
12449
  setGLEnable,
12406
- setTileSizeDefault,
12407
- setTileFixBleedScale,
12450
+ setTileDefaultSize,
12451
+ setTileDefaultPadding,
12452
+ setTileDefaultBleed,
12408
12453
  setEnablePhysicsSolver,
12409
12454
  setObjectDefaultMass,
12410
12455
  setObjectDefaultDamping,
@@ -12433,7 +12478,7 @@ export
12433
12478
  setMedalDisplaySlideTime,
12434
12479
  setMedalDisplaySize,
12435
12480
  setMedalsPreventUnlock,
12436
- setShowWatermark,
12481
+ setDebugWatermark,
12437
12482
  setDebugKey,
12438
12483
 
12439
12484
  // Utilities
@@ -12513,8 +12558,10 @@ export
12513
12558
  mainContext,
12514
12559
  drawCanvas,
12515
12560
  drawContext,
12516
- overlayCanvas,
12517
- overlayContext,
12561
+ workCanvas,
12562
+ workContext,
12563
+ workReadCanvas,
12564
+ workReadContext,
12518
12565
  mainCanvasSize,
12519
12566
  textureInfos,
12520
12567
  drawCount,
@@ -12532,7 +12579,6 @@ export
12532
12579
  drawCircle,
12533
12580
  drawCanvas2D,
12534
12581
  drawText,
12535
- drawTextOverlay,
12536
12582
  drawTextScreen,
12537
12583
  setBlendMode,
12538
12584
  combineCanvases,