littlejsengine 1.16.1 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/littlejs.d.ts +287 -240
  2. package/dist/littlejs.esm.js +1419 -1259
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +1076 -910
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +977 -815
  7. package/examples/box2d/gameObjects.js +6 -6
  8. package/examples/breakout/game.js +5 -6
  9. package/examples/electron/index.html +2 -2
  10. package/examples/electron/tiles.png +0 -0
  11. package/examples/htmlMenu/tiles.png +0 -0
  12. package/examples/index.html +1 -1
  13. package/examples/logo.png +0 -0
  14. package/examples/logo2.png +0 -0
  15. package/examples/module/tiles.png +0 -0
  16. package/examples/platformer/gameEffects.js +24 -23
  17. package/examples/platformer/gameLevel.js +24 -25
  18. package/examples/shorts/base.html +2 -1
  19. package/examples/shorts/clock.js +3 -3
  20. package/examples/shorts/fontImage.js +4 -3
  21. package/examples/shorts/parallax.js +1 -1
  22. package/examples/shorts/sequencer.js +1 -1
  23. package/examples/shorts/shapes.js +1 -1
  24. package/examples/shorts/texture.js +10 -6
  25. package/examples/shorts/tiles.png +0 -0
  26. package/examples/shorts/tiltedView.js +2 -0
  27. package/examples/starter/index.html +3 -2
  28. package/examples/starter/tiles.png +0 -0
  29. package/examples/typescript/tiles.png +0 -0
  30. package/examples/uiSystem/game.js +1 -1
  31. package/examples/uiSystem/tiles.png +0 -0
  32. package/package.json +1 -1
  33. package/plugins/postProcess.js +47 -28
  34. package/plugins/uiSystem.js +16 -16
  35. package/reference.md +6 -6
  36. package/src/engine.js +186 -198
  37. package/src/engineAudio.js +6 -10
  38. package/src/engineBuild.js +1 -0
  39. package/src/engineDebug.js +100 -98
  40. package/src/engineDraw.js +185 -148
  41. package/src/engineExport.js +327 -333
  42. package/src/engineFont.png +0 -0
  43. package/src/engineInput.js +17 -26
  44. package/src/engineMath.js +1112 -0
  45. package/src/engineMedals.js +4 -4
  46. package/src/engineObject.js +27 -27
  47. package/src/engineParticles.js +2 -2
  48. package/src/engineRelease.js +1 -3
  49. package/src/engineSettings.js +2 -2
  50. package/src/engineTileLayer.js +200 -176
  51. package/src/engineUtilities.js +48 -1097
  52. package/src/engineWebGL.js +127 -79
@@ -512,7 +512,7 @@ class UISystemPlugin
512
512
  const up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight';
513
513
  if (both)
514
514
  {
515
- return keyIsDown(up) || keyIsDown(left) ? -1 :
515
+ return keyIsDown(up) || keyIsDown(left) ? -1 :
516
516
  keyIsDown(down) || keyIsDown(right) ? 1 : 0;
517
517
  }
518
518
  const back = vertical ? up : left;
@@ -543,7 +543,7 @@ class UISystemPlugin
543
543
  * @return {boolean} */
544
544
  getNavigationWasPressed()
545
545
  {
546
- return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
546
+ return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
547
547
  keyWasPressed('Space') || keyWasPressed('Enter');
548
548
  }
549
549
 
@@ -591,7 +591,7 @@ class UISystemPlugin
591
591
  buttonYes.textHeight = 40;
592
592
  buttonYes.navigationIndex = 1;
593
593
  buttonYes.hoverColor = hsl(0,1,.5);
594
- buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
594
+ buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
595
595
  confirmMenu.addChild(buttonYes);
596
596
 
597
597
  // no button
@@ -621,8 +621,8 @@ class UISystemPlugin
621
621
  class UIObject
622
622
  {
623
623
  /** Create a UIObject
624
- * @param {Vector2} [pos=(0,0)]
625
- * @param {Vector2} [size=(1,1)]
624
+ * @param {Vector2} [pos=vec2()]
625
+ * @param {Vector2} [size=vec2(1)]
626
626
  */
627
627
  constructor(pos=vec2(), size=vec2())
628
628
  {
@@ -637,7 +637,7 @@ class UIObject
637
637
  this.size = size.copy();
638
638
  /** @property {Color} - Color of the object */
639
639
  this.color = uiSystem.defaultColor.copy();
640
- /** @property {Color} - Color of the object when active, uses color if undefined */
640
+ /** @property {Color} - Color of the object when active, uses hoverColor if undefined */
641
641
  this.activeColor = undefined;
642
642
  /** @property {string} - Text for this ui object */
643
643
  this.text = undefined;
@@ -735,10 +735,10 @@ class UIObject
735
735
 
736
736
  // disconnect from parent and destroy children
737
737
  this.destroyed = 1;
738
- this.parent && this.parent.removeChild(this);
738
+ this.parent?.removeChild(this);
739
739
  for (const child of this.children)
740
740
  {
741
- child.parent = 0;
741
+ child.parent = undefined;
742
742
  child.destroy();
743
743
  }
744
744
  }
@@ -762,7 +762,7 @@ class UIObject
762
762
  this.onUpdate();
763
763
 
764
764
  // unset active if disabled
765
- if (this.disabled && this == uiSystem.activeObject)
765
+ if (this.disabled && this === uiSystem.activeObject)
766
766
  uiSystem.activeObject = undefined;
767
767
 
768
768
  const wasHover = uiSystem.lastHoverObject === this;
@@ -834,10 +834,10 @@ class UIObject
834
834
  this.interactive && this.isActiveObject() && !this.disabled ?
835
835
  this.color : this.lineColor;
836
836
  const color = isNavigationObject ? this.hoverColor :
837
- this.disabled ? this.disabledColor :
838
- this.interactive ?
839
- this.isHoverObject() ? this.hoverColor :
840
- this.isActiveObject() ? this.activeColor || this.color :
837
+ this.disabled ? this.disabledColor :
838
+ this.interactive ?
839
+ this.isActiveObject() ? this.activeColor || this.hoverColor :
840
+ this.isHoverObject() ? this.hoverColor :
841
841
  this.color : this.color;
842
842
  const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
843
843
 
@@ -849,7 +849,7 @@ class UIObject
849
849
  getTextSize()
850
850
  {
851
851
  return vec2(
852
- this.textWidth || this.textFitScale * this.size.x,
852
+ this.textWidth || this.textFitScale * this.size.x,
853
853
  this.textHeight || this.textFitScale * this.size.y);
854
854
  }
855
855
 
@@ -897,9 +897,9 @@ class UIObject
897
897
  renderDebug(visible=true)
898
898
  {
899
899
  // apply color based on state
900
- const color =
900
+ const color =
901
901
  !visible ? GREEN :
902
- this.isHoverObject() ? YELLOW :
902
+ this.isHoverObject() ? YELLOW :
903
903
  this.disabled ? PURPLE :
904
904
  this.interactive ? RED : BLUE;
905
905
  uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);
package/reference.md CHANGED
@@ -375,7 +375,7 @@ CanvasLayer(position, size) // Create a canvas layer object
375
375
  CanvasLayer.canvas // The canvas used by this layer
376
376
  CanvasLayer.context // The 2D context of the canvas
377
377
  CanvasLayer.getImageData() // Get image data from canvas
378
- CanvasLayer.useWebGL() // Creates or updates WebGL texture
378
+ CanvasLayer.updateWebGL() // Creates or updates WebGL texture
379
379
 
380
380
  // LittleJS Layer System
381
381
  TileLayer(position, size, tileInfo, scale) // Create a tile layer object
@@ -428,11 +428,11 @@ debugCircle(pos, size, color='#fff', time=0, fill) // Draw debug circle
428
428
  debugPoint(pos, color, time, angle) // Draw debug point
429
429
  debugLine(posA, posB, color, width=.1, time) // Draw debug line
430
430
  debugText(text, pos, size=1, color='#fff', time=0, angle=0) // Draw debug text
431
- debugAABB(pA, sA, pB, sB, color) // Draw a debug axis aligned box
432
- debugClear() // Clear all debug primitives
433
- debugSaveCanvas(canvas, filename) // Save canvas to a file
434
- debugSaveText(text, filename) // Save text to a file
435
- debugSaveDataURL(dataURL, filename) // Save url to a file
431
+ debugAABB(pA, sA, pB, sB, color) // Draw a debug axis aligned box
432
+ debugClear() // Clear all debug primitives
433
+ saveCanvas(canvas, filename) // Save canvas to a file
434
+ saveText(text, filename) // Save text to a file
435
+ saveDataURL(dataURL, filename) // Save url to a file
436
436
 
437
437
  // Debug settings
438
438
  debug // Is debug enabled?
package/src/engine.js CHANGED
@@ -30,7 +30,7 @@ const engineName = 'LittleJS';
30
30
  * @type {string}
31
31
  * @default
32
32
  * @memberof Engine */
33
- const engineVersion = '1.16.1';
33
+ const engineVersion = '1.17.1';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {number}
@@ -269,33 +269,20 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
269
269
  if (!wasUpdated)
270
270
  updateCanvas();
271
271
 
272
- // render sort then render while removing destroyed objects
272
+ // render the game and objects
273
273
  enginePreRender();
274
274
  gameRender();
275
275
  engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
276
276
  for (const o of engineObjects)
277
277
  o.destroyed || o.render();
278
+
279
+ // post rendering
278
280
  gameRenderPost();
279
281
  pluginList.forEach(plugin=>plugin.render?.());
280
282
  inputRender();
281
283
  debugRender();
282
284
  glFlush();
283
- debugVideoCaptureUpdate();
284
-
285
- if (debugWatermark && !debugVideoCaptureIsActive())
286
- {
287
- // update fps display
288
- mainContext.textAlign = 'right';
289
- mainContext.textBaseline = 'top';
290
- mainContext.font = '1em monospace';
291
- mainContext.fillStyle = '#000';
292
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
293
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
294
- + (glEnable ? ' GL' : ' 2D') ;
295
- mainContext.fillText(text, mainCanvas.width-3, 3);
296
- mainContext.fillStyle = '#fff';
297
- mainContext.fillText(text, mainCanvas.width-2, 2);
298
- }
285
+ debugRenderPost();
299
286
  drawCount = 0;
300
287
  }
301
288
  }
@@ -330,7 +317,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
330
317
 
331
318
  // responsive aspect ratio with native resolution
332
319
  const innerAspect = innerWidth / innerHeight;
333
- ASSERT(canvasMinAspect <= canvasMaxAspect);
320
+ ASSERT(canvasMinAspect <= canvasMaxAspect);
334
321
  if (canvasMaxAspect && innerAspect > canvasMaxAspect)
335
322
  {
336
323
  // full height
@@ -361,15 +348,9 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
361
348
  mainContext.lineJoin = 'round';
362
349
  mainContext.lineCap = 'round';
363
350
  }
364
-
365
- // wait for gameInit to load
366
- async function startEngine()
367
- {
368
- await gameInit();
369
- engineUpdate();
370
- }
371
- if (headlessMode)
372
- return startEngine();
351
+
352
+ // skip setup if headless
353
+ if (headlessMode) return startEngine();
373
354
 
374
355
  // setup webgl
375
356
  glInit(rootElement);
@@ -384,7 +365,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
384
365
  'touch-action:none;' + // prevent mobile pinch to resize
385
366
  '-webkit-touch-callout:none'; // compatibility for ios
386
367
  rootElement.style.cssText = styleRoot;
387
- drawCanvas = mainCanvas = rootElement.appendChild(document.createElement('canvas'));
368
+ mainCanvas = rootElement.appendChild(document.createElement('canvas'));
388
369
  drawContext = mainContext = mainCanvas.getContext('2d');
389
370
 
390
371
  // init stuff and start engine
@@ -393,7 +374,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
393
374
  debugInit();
394
375
 
395
376
  // setup canvases
396
- // transform way is still more reliable then flexbox or grid
377
+ // transform way is still more reliable than flexbox or grid
397
378
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
398
379
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
399
380
  mainCanvas.style.cssText = styleCanvas;
@@ -438,6 +419,9 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
438
419
  }));
439
420
  }
440
421
 
422
+ // load engine font image
423
+ promises.push(fontImageInit());
424
+
441
425
  if (showSplashScreen)
442
426
  {
443
427
  // draw splash screen
@@ -449,7 +433,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
449
433
  function updateSplash()
450
434
  {
451
435
  inputClear();
452
- drawEngineSplashScreen(t+=.01);
436
+ drawEngineLogo(t+=.01);
453
437
  t>1 ? resolve() : setTimeout(updateSplash, 16);
454
438
  }
455
439
  }));
@@ -459,171 +443,12 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
459
443
  await Promise.all(promises);
460
444
  return startEngine();
461
445
 
462
- ///////////////////////////////////////////////////////////////////////////
463
- // LittleJS Splash Screen
464
- function drawEngineSplashScreen(t)
446
+ async function startEngine()
465
447
  {
466
- const x = mainContext;
467
- const w = mainCanvas.width = innerWidth;
468
- const h = mainCanvas.height = innerHeight;
469
-
470
- {
471
- // background
472
- const p3 = percent(t, 1, .8);
473
- const p4 = percent(t, 0, .5);
474
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
475
- g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
476
- g.addColorStop(1,hsl(0,0,0,p3).toString());
477
- x.save();
478
- x.fillStyle = g;
479
- x.fillRect(0,0,w,h);
480
- }
481
-
482
- // draw LittleJS logo...
483
- const rect = (X, Y, W, H, C)=>
484
- {
485
- x.beginPath();
486
- x.rect(X,Y,W,C?H*p:H);
487
- x.fillStyle = C;
488
- C ? x.fill() : x.stroke();
489
- };
490
- const line = (X, Y, Z, W)=>
491
- {
492
- x.beginPath();
493
- x.lineTo(X,Y);
494
- x.lineTo(Z,W);
495
- x.stroke();
496
- };
497
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
498
- {
499
- const D = (A+B)/2, E = p*(B-A)/2;
500
- x.beginPath();
501
- F && x.lineTo(X,Y);
502
- x.arc(X,Y,R,D-E,D+E);
503
- x.fillStyle = C;
504
- C ? x.fill() : x.stroke();
505
- };
506
- const color = (c=0, l=0) =>
507
- hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();
508
- const alpha = wave(1,1,t);
509
- const p = percent(alpha, .1, .5);
510
-
511
- // setup
512
- x.translate(w/2,h/2);
513
- const size = min(6, min(w,h)/99); // fit to screen
514
- x.scale(size,size);
515
- x.translate(-40,-35);
516
- x.lineJoin = x.lineCap = 'round';
517
- x.lineWidth = .1 + p*1.9;
518
-
519
- // drawing effect
520
- const p2 = percent(alpha,.1,1);
521
- x.setLineDash([99*p2,99]);
522
-
523
- // cab top
524
- rect(7,16,18,-8,color(2,2));
525
- rect(7,8,18,4,color(2,3));
526
- rect(25,8,8,8,color(2,1));
527
- rect(25,8,-18,8);
528
- rect(25,8,8,8);
529
-
530
- // cab
531
- rect(25,16,7,23,color());
532
- rect(11,39,14,-23,color(1,1));
533
- rect(11,16,14,18,color(1,2));
534
- rect(11,16,14,8,color(1,3));
535
- rect(25,16,-14,24);
536
-
537
- // cab window
538
- rect(15,29,6,-9,color(2,2));
539
- circle(15,21,5,0,PI/2,color(2,4),1);
540
- rect(21,21,-6,9);
541
-
542
- // little stack
543
- rect(37,14,9,6,color(3,2));
544
- rect(37,14,4.5,6,color(3,3));
545
- rect(37,14,9,6);
546
-
547
- // big stack
548
- rect(50,20,10,-8,color(0,1));
549
- rect(50,20,6.5,-8,color(0,2));
550
- rect(50,20,3.5,-8,color(0,3));
551
- rect(50,20,10,-8);
552
- circle(55,2,11.4,.5,PI-.5,color(3,3));
553
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
554
- circle(55,2,11.4,.5,PI-.5);
555
- rect(45,7,20,-7,color(0,2));
556
- rect(45,-1,20,4,color(0,3));
557
- rect(45,-1,20,8);
558
-
559
- // engine
560
- for (let i=5; i--;)
561
- {
562
- // stagger radius to fix slight seam
563
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
564
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
565
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
566
- }
567
-
568
- // engine outline
569
- circle(36,30,10,PI/2,PI*3/2);
570
- circle(48,30,10,PI/2,PI*3/2);
571
- circle(60,30,10);
572
- line(36,20,60,20);
573
-
574
- // engine front light
575
- circle(60,30,4,PI,3*PI,color(3,2));
576
- circle(60,30,4,PI,2*PI,color(3,3));
577
- circle(60,30,4,PI,3*PI);
578
-
579
- // front brush
580
- for (let i=6; i--;)
581
- {
582
- x.beginPath();
583
- x.lineTo(53,54);
584
- x.lineTo(53,40);
585
- x.lineTo(53+(1+i*2.9)*p,40);
586
- x.lineTo(53+(4+i*3.5)*p,54);
587
- x.fillStyle = color(0,i%2+2);
588
- x.fill();
589
- i%2 && x.stroke();
590
- }
591
-
592
- // wheels
593
- rect(6,40,5,5);
594
- rect(6,40,5,5,color());
595
- rect(15,54,38,-14,color());
596
- for (let i=3; i--;)
597
- for (let j=2; j--;)
598
- {
599
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
600
- x.stroke();
601
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
602
- x.stroke();
603
- }
604
- line(6,40,68,40); // center
605
- line(77,54,4,54); // bottom
606
-
607
- // draw engine name
608
- const s = engineName;
609
- x.font = '900 16px arial';
610
- x.textAlign = 'center';
611
- x.textBaseline = 'top';
612
- x.lineWidth = .1+p*3.9;
613
- let w2 = 0;
614
- for (let i=0; i<s.length; ++i)
615
- w2 += x.measureText(s[i]).width;
616
- for (let j=2; j--;)
617
- for (let i=0, X=41-w2/2; i<s.length; ++i)
618
- {
619
- x.fillStyle = color(i,2);
620
- const w = x.measureText(s[i]).width;
621
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
622
- X += w;
623
- }
624
- x.restore();
448
+ // wait for gameInit to load
449
+ await gameInit();
450
+ engineUpdate();
625
451
  }
626
- ///////////////////////////////////////////////////////////////////////////
627
452
  }
628
453
 
629
454
  /** Update each engine object, remove destroyed objects, and update time
@@ -637,8 +462,7 @@ function engineObjectsUpdate()
637
462
  // recursive object update
638
463
  function updateObject(o)
639
464
  {
640
- if (o.destroyed)
641
- return;
465
+ if (o.destroyed) return;
642
466
 
643
467
  o.update();
644
468
  for (const child of o.children)
@@ -646,8 +470,7 @@ function engineObjectsUpdate()
646
470
  }
647
471
  for (const o of engineObjects)
648
472
  {
649
- if (o.parent)
650
- continue;
473
+ if (o.parent || o.destroyed) continue;
651
474
 
652
475
  // update top level objects
653
476
  o.update();
@@ -736,4 +559,169 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
736
559
 
737
560
  debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
738
561
  return hitObjects;
562
+ }
563
+
564
+ ///////////////////////////////////////////////////////////////////////////////
565
+ function drawEngineLogo(t)
566
+ {
567
+ // LittleJS Logo and Splash Screen
568
+ const x = mainContext;
569
+ const w = mainCanvas.width = innerWidth;
570
+ const h = mainCanvas.height = innerHeight;
571
+
572
+ {
573
+ // background
574
+ const p3 = percent(t, 1, .8);
575
+ const p4 = percent(t, 0, .5);
576
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
577
+ g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
578
+ g.addColorStop(1,hsl(0,0,0,p3).toString());
579
+ x.save();
580
+ x.fillStyle = g;
581
+ x.fillRect(0,0,w,h);
582
+ }
583
+
584
+ // draw LittleJS logo...
585
+ const rect = (X, Y, W, H, C)=>
586
+ {
587
+ x.beginPath();
588
+ x.rect(X,Y,W,C?H*p:H);
589
+ x.fillStyle = C;
590
+ C ? x.fill() : x.stroke();
591
+ };
592
+ const line = (X, Y, Z, W)=>
593
+ {
594
+ x.beginPath();
595
+ x.lineTo(X,Y);
596
+ x.lineTo(Z,W);
597
+ x.stroke();
598
+ };
599
+ const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
600
+ {
601
+ const D = (A+B)/2, E = p*(B-A)/2;
602
+ x.beginPath();
603
+ F && x.lineTo(X,Y);
604
+ x.arc(X,Y,R,D-E,D+E);
605
+ x.fillStyle = C;
606
+ C ? x.fill() : x.stroke();
607
+ };
608
+ const color = (c=0, l=0) =>
609
+ hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
610
+ const alpha = wave(1,1,t);
611
+ const p = percent(alpha, .1, .5);
612
+
613
+ // setup
614
+ x.translate(w/2,h/2);
615
+ const size = min(6, min(w,h)/99); // fit to screen
616
+ x.scale(size,size);
617
+ x.translate(-40,-35);
618
+ x.lineJoin = x.lineCap = 'round';
619
+ x.lineWidth = .1 + p*1.9;
620
+
621
+ // drawing effect
622
+ const p2 = percent(alpha,.1,1);
623
+ x.setLineDash([99*p2,99]);
624
+
625
+ // cab top
626
+ rect(7,16,18,-8,color(2,2));
627
+ rect(7,8,18,4,color(2,3));
628
+ rect(25,8,8,8,color(2,1));
629
+ rect(25,8,-18,8);
630
+ rect(25,8,8,8);
631
+
632
+ // cab
633
+ rect(25,16,7,24,color());
634
+ rect(11,39,14,-23,color(1,1));
635
+ rect(11,16,14,18,color(1,2));
636
+ rect(11,16,14,8,color(1,3));
637
+ rect(25,16,-14,24);
638
+
639
+ // cab window
640
+ rect(15,29,6,-9,color(2,2));
641
+ circle(15,21,5,0,PI/2,color(2,4),1);
642
+ rect(21,21,-6,9);
643
+
644
+ // little stack
645
+ rect(37,14,9,6,color(3,2));
646
+ rect(37,14,4.5,6,color(3,3));
647
+ rect(37,14,9,6);
648
+
649
+ // big stack
650
+ rect(50,20,10,-8,color(0,1));
651
+ rect(50,20,6.5,-8,color(0,2));
652
+ rect(50,20,3.5,-8,color(0,3));
653
+ rect(50,20,10,-8);
654
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
655
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
656
+ circle(55,2,11.4,.5,PI-.5);
657
+ rect(45,7,20,-7,color(0,2));
658
+ rect(45,-1,20,4,color(0,3));
659
+ rect(45,-1,20,8);
660
+
661
+ // engine
662
+ for (let i=5; i--;)
663
+ {
664
+ // stagger radius to fix slight seam
665
+ circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
666
+ circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
667
+ circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
668
+ }
669
+
670
+ // engine outline
671
+ circle(36,30,10,PI/2,PI*3/2);
672
+ circle(48,30,10,PI/2,PI*3/2);
673
+ circle(60,30,10);
674
+ line(36,20,60,20);
675
+
676
+ // engine front light
677
+ circle(60,30,4,PI,3*PI,color(3,2));
678
+ circle(60,30,4,PI,2*PI,color(3,3));
679
+ circle(60,30,4,PI,3*PI);
680
+
681
+ // front brush
682
+ for (let i=6; i--;)
683
+ {
684
+ x.beginPath();
685
+ x.lineTo(53,54);
686
+ x.lineTo(53,40);
687
+ x.lineTo(53+(1+i*2.9)*p,40);
688
+ x.lineTo(53+(4+i*3.5)*p,54);
689
+ x.fillStyle = color(0,i%2+2);
690
+ x.fill();
691
+ i%2 && x.stroke();
692
+ }
693
+
694
+ // wheels
695
+ rect(6,40,5,5);
696
+ rect(6,40,5,5,color());
697
+ rect(15,54,38,-14,color());
698
+ for (let i=3; i--;)
699
+ for (let j=2; j--;)
700
+ {
701
+ circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
702
+ x.stroke();
703
+ circle(15*i+15,47,j?7:1,0,PI,color(i,2));
704
+ x.stroke();
705
+ }
706
+ line(6,40,68,40); // center
707
+ line(77,54,4,54); // bottom
708
+
709
+ // draw engine name
710
+ const s = engineName;
711
+ x.font = '900 16px arial';
712
+ x.textAlign = 'center';
713
+ x.textBaseline = 'top';
714
+ x.lineWidth = .1+p*3.9;
715
+ let w2 = 0;
716
+ for (let i=0; i<s.length; ++i)
717
+ w2 += x.measureText(s[i]).width;
718
+ for (let j=2; j--;)
719
+ for (let i=0, X=41-w2/2; i<s.length; ++i)
720
+ {
721
+ x.fillStyle = color(i,2);
722
+ const w = x.measureText(s[i]).width;
723
+ x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
724
+ X += w;
725
+ }
726
+ x.restore();
739
727
  }
@@ -86,8 +86,7 @@ class Sound
86
86
  {
87
87
  // remove randomness so it can be applied on playback
88
88
  const randomnessIndex = 1, defaultRandomness = .05;
89
- this.randomness = zzfxSound[randomnessIndex] !== undefined ?
90
- zzfxSound[randomnessIndex] : defaultRandomness;
89
+ this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
91
90
  zzfxSound[randomnessIndex] = 0;
92
91
 
93
92
  // generate the zzfx samples
@@ -167,7 +166,7 @@ class Sound
167
166
  * @return {number} - How long the sound is in seconds (undefined if loading)
168
167
  */
169
168
  getDuration()
170
- { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
169
+ { return this.sampleChannels?.[0].length / this.sampleRate || 0; }
171
170
 
172
171
  /** Check if sound is loaded, for sounds fetched from a url
173
172
  * @return {boolean} - True if sound is loaded and ready to play
@@ -259,8 +258,7 @@ class SoundWave extends Sound
259
258
  this.sampleRate = audioBuffer.sampleRate;
260
259
  this.sampleChannels = sampleChannels;
261
260
  this.loadedPercent = 1;
262
- if (this.onloadCallback)
263
- this.onloadCallback(this);
261
+ this.onloadCallback?.(this);
264
262
  }
265
263
  }
266
264
 
@@ -383,8 +381,7 @@ class SoundInstance
383
381
  /** Pause this sound instance */
384
382
  pause()
385
383
  {
386
- if (this.isPaused())
387
- return;
384
+ if (this.isPaused()) return;
388
385
 
389
386
  // save current time and stop sound
390
387
  this.pausedTime = this.getCurrentTime();
@@ -396,8 +393,7 @@ class SoundInstance
396
393
  /** Unpauses this sound instance */
397
394
  resume()
398
395
  {
399
- if (!this.isPaused())
400
- return;
396
+ if (!this.isPaused()) return;
401
397
 
402
398
  // restart sound from paused time
403
399
  this.start(this.pausedTime);
@@ -465,7 +461,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
465
461
 
466
462
  /** Stop all queued speech
467
463
  * @memberof Audio */
468
- function speakStop() {speechSynthesis && speechSynthesis.cancel();}
464
+ function speakStop() {speechSynthesis?.cancel();}
469
465
 
470
466
  /** Get frequency of a note on a musical scale
471
467
  * @param {number} semitoneOffset - How many semitones away from the root note
@@ -16,6 +16,7 @@ const SOURCE_FOLDER = 'src';
16
16
  const PLUGIN_FOLDER = 'plugins';
17
17
  const engineSourceFiles =
18
18
  [
19
+ `${SOURCE_FOLDER}/engineMath.js`,
19
20
  `${SOURCE_FOLDER}/engineUtilities.js`,
20
21
  `${SOURCE_FOLDER}/engineSettings.js`,
21
22
  `${SOURCE_FOLDER}/engineObject.js`,