littlejsengine 1.16.2 → 1.17.5

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 (57) hide show
  1. package/dist/littlejs.d.ts +291 -246
  2. package/dist/littlejs.esm.js +1520 -1271
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +1178 -920
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +1170 -912
  7. package/examples/box2d/gameObjects.js +4 -4
  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 +9 -8
  13. package/examples/logo.png +0 -0
  14. package/examples/module/build.bat +7 -0
  15. package/examples/module/build.js +121 -0
  16. package/examples/module/tiles.png +0 -0
  17. package/examples/platformer/game.js +1 -1
  18. package/examples/platformer/gameEffects.js +25 -29
  19. package/examples/platformer/gameLevel.js +27 -28
  20. package/examples/puzzle/game.js +2 -2
  21. package/examples/shorts/base.html +4 -1
  22. package/examples/shorts/box2dPool.js +2 -2
  23. package/examples/shorts/box2dTileLayer.js +48 -0
  24. package/examples/shorts/clock.js +3 -3
  25. package/examples/shorts/fontImage.js +4 -3
  26. package/examples/shorts/music.js +2 -2
  27. package/examples/shorts/musicPlayer.js +2 -2
  28. package/examples/shorts/parallax.js +1 -1
  29. package/examples/shorts/sequencer.js +1 -1
  30. package/examples/shorts/shapes.js +1 -1
  31. package/examples/shorts/texture.js +10 -6
  32. package/examples/shorts/tiles.png +0 -0
  33. package/examples/shorts/tiltedView.js +2 -0
  34. package/examples/starter/index.html +2 -2
  35. package/examples/starter/tiles.png +0 -0
  36. package/examples/typescript/tiles.png +0 -0
  37. package/examples/uiSystem/game.js +1 -1
  38. package/examples/uiSystem/tiles.png +0 -0
  39. package/package.json +5 -5
  40. package/plugins/box2d.js +167 -63
  41. package/plugins/postProcess.js +47 -28
  42. package/plugins/uiSystem.js +18 -18
  43. package/plugins/zzfxm.js +2 -2
  44. package/reference.md +4 -3
  45. package/src/engine.js +182 -181
  46. package/src/engineAudio.js +47 -63
  47. package/src/engineDebug.js +8 -8
  48. package/src/engineDraw.js +184 -124
  49. package/src/engineExport.js +325 -334
  50. package/src/engineFont.png +0 -0
  51. package/src/engineInput.js +18 -24
  52. package/src/engineMath.js +24 -113
  53. package/src/engineObject.js +27 -26
  54. package/src/engineParticles.js +2 -2
  55. package/src/engineTileLayer.js +229 -192
  56. package/src/engineUtilities.js +100 -4
  57. package/src/engineWebGL.js +124 -73
@@ -30,7 +30,7 @@ let uiDebug = 0;
30
30
 
31
31
  /** Enable UI system debug drawing
32
32
  * 0=off, 1=normal, 2=show invisible
33
- * @param {number|boolean} enable
33
+ * @param {number|boolean} debugMode
34
34
  * @memberof UISystem */
35
35
  function uiSetDebug(debugMode)
36
36
  { uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
@@ -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
  }
@@ -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);
@@ -1231,8 +1231,8 @@ class UIScrollbar extends UIObject
1231
1231
  class UIVideo extends UIObject
1232
1232
  {
1233
1233
  /** Create a video player UI object
1234
- * @param {Vector2} [pos]
1235
- * @param {Vector2} [size]
1234
+ * @param {Vector2} pos
1235
+ * @param {Vector2} size
1236
1236
  * @param {string} src - Video file path or URL
1237
1237
  * @param {boolean} [autoplay=false] - Start playing immediately?
1238
1238
  * @param {boolean} [loop=false] - Loop the video?
package/plugins/zzfxm.js CHANGED
@@ -55,7 +55,7 @@ class ZzFXMusic extends Sound
55
55
  /** Play the music that loops by default
56
56
  * @param {number} [volume] - Volume to play the music at
57
57
  * @param {boolean} [loop] - Should the music loop?
58
- * @return {AudioBufferSourceNode} - The audio source node
58
+ * @return {SoundInstance} - The sound instance
59
59
  */
60
60
  playMusic(volume=1, loop=true)
61
61
  { return super.play(undefined, volume, 1, 0, loop); }
@@ -102,7 +102,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
102
102
  sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
103
103
 
104
104
  // for each pattern in sequence
105
- sequence.forEach((patternIndex, sequenceIndex) => {
105
+ sequence.forEach((patternIndex, sequenceIndex)=> {
106
106
  // get pattern for current channel, use empty 1 note pattern if none found
107
107
  patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
108
108
 
package/reference.md CHANGED
@@ -205,8 +205,8 @@ tileDefaultBleed = .3 // How much smaller to draw tiles to prevent bleeding
205
205
 
206
206
  ```javascript
207
207
  // Sound Object
208
- Sound(zzfxSound, range, taper) // Create a zzfx sound
209
- SoundWave(filename, randomness=0, range, taper) // Load a wave, mp3, or ogg
208
+ Sound(zzfxSound, randomness, range, taper) // Create a zzfx sound
209
+ Sound(filename, randomness, range, taper) // Load a wave, mp3, or ogg
210
210
  Sound.play(pos, volume=1, pitch=1, randomness=1, loop) // Play a sound, returns SoundInstance
211
211
  Sound.playMusic(volume=1, loop=true) // Play as music with looping
212
212
  Sound.playNote(semitoneOffset, pos, volume=1) // Play as note with a semitone offset
@@ -375,11 +375,12 @@ 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
382
382
  TileLayer.setData(layerPos, data, redraw) // Set data at position
383
+ TileLayer.clearData(layerPos, redraw) // Clear data at position
383
384
  TileLayer.getData(layerPos) // Get data at position
384
385
  TileLayer.redraw() // Draw to an offscreen canvas
385
386
  TileLayer.drawTileData(layerPos, clear=true) // Draw the tile
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.2';
33
+ const engineVersion = '1.17.5';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {number}
@@ -85,8 +85,9 @@ function getPaused() { return paused; }
85
85
  * @memberof Engine */
86
86
  function setPaused(isPaused=true) { paused = isPaused; }
87
87
 
88
- // Frame time tracking
88
+ // Engine internal variables
89
89
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
90
+ let showEngineVersion = true;
90
91
 
91
92
  ///////////////////////////////////////////////////////////////////////////////
92
93
  // plugin hooks
@@ -150,16 +151,17 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
150
151
  * @example
151
152
  * // Basic engine startup
152
153
  * engineInit(
153
- * () => { LOG('Game initialized!'); }, // gameInit
154
- * () => { updateGameLogic(); }, // gameUpdate
155
- * () => { updateUI(); }, // gameUpdatePost
156
- * () => { drawBackground(); }, // gameRender
157
- * () => { drawHUD(); }, // gameRenderPost
154
+ * ()=> { LOG('Game initialized!'); }, // gameInit
155
+ * ()=> { updateGameLogic(); }, // gameUpdate
156
+ * ()=> { updateUI(); }, // gameUpdatePost
157
+ * ()=> { drawBackground(); }, // gameRender
158
+ * ()=> { drawHUD(); }, // gameRenderPost
158
159
  * ['tiles.png', 'tilesLevel.png'] // images to load
159
160
  * );
160
161
  * @memberof Engine */
161
162
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
162
163
  {
164
+ showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
163
165
  ASSERT(!mainContext, 'engine already initialized');
164
166
  ASSERT(isArray(imageSources), 'pass in images as array');
165
167
 
@@ -317,7 +319,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
317
319
 
318
320
  // responsive aspect ratio with native resolution
319
321
  const innerAspect = innerWidth / innerHeight;
320
- ASSERT(canvasMinAspect <= canvasMaxAspect);
322
+ ASSERT(canvasMinAspect <= canvasMaxAspect);
321
323
  if (canvasMaxAspect && innerAspect > canvasMaxAspect)
322
324
  {
323
325
  // full height
@@ -374,7 +376,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
374
376
  debugInit();
375
377
 
376
378
  // setup canvases
377
- // transform way is still more reliable then flexbox or grid
379
+ // transform way is still more reliable than flexbox or grid
378
380
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
379
381
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
380
382
  mainCanvas.style.cssText = styleCanvas;
@@ -419,18 +421,20 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
419
421
  }));
420
422
  }
421
423
 
424
+ // load engine font image
425
+ promises.push(fontImageInit());
426
+
422
427
  if (showSplashScreen)
423
428
  {
424
429
  // draw splash screen
425
430
  promises.push(new Promise(resolve =>
426
431
  {
427
432
  let t = 0;
428
- console.log(`${engineName} Engine v${engineVersion}`);
429
433
  updateSplash();
430
434
  function updateSplash()
431
435
  {
432
436
  inputClear();
433
- drawEngineSplashScreen(t+=.01);
437
+ drawEngineLogo(t+=.01);
434
438
  t>1 ? resolve() : setTimeout(updateSplash, 16);
435
439
  }
436
440
  }));
@@ -446,172 +450,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
446
450
  await gameInit();
447
451
  engineUpdate();
448
452
  }
449
-
450
- ///////////////////////////////////////////////////////////////////////////
451
- // LittleJS Splash Screen
452
- function drawEngineSplashScreen(t)
453
- {
454
- const x = mainContext;
455
- const w = mainCanvas.width = innerWidth;
456
- const h = mainCanvas.height = innerHeight;
457
-
458
- {
459
- // background
460
- const p3 = percent(t, 1, .8);
461
- const p4 = percent(t, 0, .5);
462
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
463
- g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
464
- g.addColorStop(1,hsl(0,0,0,p3).toString());
465
- x.save();
466
- x.fillStyle = g;
467
- x.fillRect(0,0,w,h);
468
- }
469
-
470
- // draw LittleJS logo...
471
- const rect = (X, Y, W, H, C)=>
472
- {
473
- x.beginPath();
474
- x.rect(X,Y,W,C?H*p:H);
475
- x.fillStyle = C;
476
- C ? x.fill() : x.stroke();
477
- };
478
- const line = (X, Y, Z, W)=>
479
- {
480
- x.beginPath();
481
- x.lineTo(X,Y);
482
- x.lineTo(Z,W);
483
- x.stroke();
484
- };
485
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
486
- {
487
- const D = (A+B)/2, E = p*(B-A)/2;
488
- x.beginPath();
489
- F && x.lineTo(X,Y);
490
- x.arc(X,Y,R,D-E,D+E);
491
- x.fillStyle = C;
492
- C ? x.fill() : x.stroke();
493
- };
494
- const color = (c=0, l=0) =>
495
- hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();
496
- const alpha = wave(1,1,t);
497
- const p = percent(alpha, .1, .5);
498
-
499
- // setup
500
- x.translate(w/2,h/2);
501
- const size = min(6, min(w,h)/99); // fit to screen
502
- x.scale(size,size);
503
- x.translate(-40,-35);
504
- x.lineJoin = x.lineCap = 'round';
505
- x.lineWidth = .1 + p*1.9;
506
-
507
- // drawing effect
508
- const p2 = percent(alpha,.1,1);
509
- x.setLineDash([99*p2,99]);
510
-
511
- // cab top
512
- rect(7,16,18,-8,color(2,2));
513
- rect(7,8,18,4,color(2,3));
514
- rect(25,8,8,8,color(2,1));
515
- rect(25,8,-18,8);
516
- rect(25,8,8,8);
517
-
518
- // cab
519
- rect(25,16,7,23,color());
520
- rect(11,39,14,-23,color(1,1));
521
- rect(11,16,14,18,color(1,2));
522
- rect(11,16,14,8,color(1,3));
523
- rect(25,16,-14,24);
524
-
525
- // cab window
526
- rect(15,29,6,-9,color(2,2));
527
- circle(15,21,5,0,PI/2,color(2,4),1);
528
- rect(21,21,-6,9);
529
-
530
- // little stack
531
- rect(37,14,9,6,color(3,2));
532
- rect(37,14,4.5,6,color(3,3));
533
- rect(37,14,9,6);
534
-
535
- // big stack
536
- rect(50,20,10,-8,color(0,1));
537
- rect(50,20,6.5,-8,color(0,2));
538
- rect(50,20,3.5,-8,color(0,3));
539
- rect(50,20,10,-8);
540
- circle(55,2,11.4,.5,PI-.5,color(3,3));
541
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
542
- circle(55,2,11.4,.5,PI-.5);
543
- rect(45,7,20,-7,color(0,2));
544
- rect(45,-1,20,4,color(0,3));
545
- rect(45,-1,20,8);
546
-
547
- // engine
548
- for (let i=5; i--;)
549
- {
550
- // stagger radius to fix slight seam
551
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
552
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
553
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
554
- }
555
-
556
- // engine outline
557
- circle(36,30,10,PI/2,PI*3/2);
558
- circle(48,30,10,PI/2,PI*3/2);
559
- circle(60,30,10);
560
- line(36,20,60,20);
561
-
562
- // engine front light
563
- circle(60,30,4,PI,3*PI,color(3,2));
564
- circle(60,30,4,PI,2*PI,color(3,3));
565
- circle(60,30,4,PI,3*PI);
566
-
567
- // front brush
568
- for (let i=6; i--;)
569
- {
570
- x.beginPath();
571
- x.lineTo(53,54);
572
- x.lineTo(53,40);
573
- x.lineTo(53+(1+i*2.9)*p,40);
574
- x.lineTo(53+(4+i*3.5)*p,54);
575
- x.fillStyle = color(0,i%2+2);
576
- x.fill();
577
- i%2 && x.stroke();
578
- }
579
-
580
- // wheels
581
- rect(6,40,5,5);
582
- rect(6,40,5,5,color());
583
- rect(15,54,38,-14,color());
584
- for (let i=3; i--;)
585
- for (let j=2; j--;)
586
- {
587
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
588
- x.stroke();
589
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
590
- x.stroke();
591
- }
592
- line(6,40,68,40); // center
593
- line(77,54,4,54); // bottom
594
-
595
- // draw engine name
596
- const s = engineName;
597
- x.font = '900 16px arial';
598
- x.textAlign = 'center';
599
- x.textBaseline = 'top';
600
- x.lineWidth = .1+p*3.9;
601
- let w2 = 0;
602
- for (let i=0; i<s.length; ++i)
603
- w2 += x.measureText(s[i]).width;
604
- for (let j=2; j--;)
605
- for (let i=0, X=41-w2/2; i<s.length; ++i)
606
- {
607
- x.fillStyle = color(i,2);
608
- const w = x.measureText(s[i]).width;
609
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
610
- X += w;
611
- }
612
- x.restore();
613
- }
614
- ///////////////////////////////////////////////////////////////////////////
615
453
  }
616
454
 
617
455
  /** Update each engine object, remove destroyed objects, and update time
@@ -625,8 +463,7 @@ function engineObjectsUpdate()
625
463
  // recursive object update
626
464
  function updateObject(o)
627
465
  {
628
- if (o.destroyed)
629
- return;
466
+ if (o.destroyed) return;
630
467
 
631
468
  o.update();
632
469
  for (const child of o.children)
@@ -634,8 +471,7 @@ function engineObjectsUpdate()
634
471
  }
635
472
  for (const o of engineObjects)
636
473
  {
637
- if (o.parent)
638
- continue;
474
+ if (o.parent || o.destroyed) continue;
639
475
 
640
476
  // update top level objects
641
477
  o.update();
@@ -724,4 +560,169 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
724
560
 
725
561
  debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
726
562
  return hitObjects;
563
+ }
564
+
565
+ ///////////////////////////////////////////////////////////////////////////////
566
+ function drawEngineLogo(t)
567
+ {
568
+ // LittleJS Logo and Splash Screen
569
+ const x = mainContext;
570
+ const w = mainCanvas.width = innerWidth;
571
+ const h = mainCanvas.height = innerHeight;
572
+
573
+ {
574
+ // background
575
+ const p3 = percent(t, 1, .8);
576
+ const p4 = percent(t, 0, .5);
577
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
578
+ g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
579
+ g.addColorStop(1,hsl(0,0,0,p3).toString());
580
+ x.save();
581
+ x.fillStyle = g;
582
+ x.fillRect(0,0,w,h);
583
+ }
584
+
585
+ // draw LittleJS logo...
586
+ const rect = (X, Y, W, H, C)=>
587
+ {
588
+ x.beginPath();
589
+ x.rect(X,Y,W,C?H*p:H);
590
+ x.fillStyle = C;
591
+ C ? x.fill() : x.stroke();
592
+ };
593
+ const line = (X, Y, Z, W)=>
594
+ {
595
+ x.beginPath();
596
+ x.lineTo(X,Y);
597
+ x.lineTo(Z,W);
598
+ x.stroke();
599
+ };
600
+ const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
601
+ {
602
+ const D = (A+B)/2, E = p*(B-A)/2;
603
+ x.beginPath();
604
+ F && x.lineTo(X,Y);
605
+ x.arc(X,Y,R,D-E,D+E);
606
+ x.fillStyle = C;
607
+ C ? x.fill() : x.stroke();
608
+ };
609
+ const color = (c=0, l=0)=>
610
+ hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
611
+ const alpha = wave(1,1,t);
612
+ const p = percent(alpha, .1, .5);
613
+
614
+ // setup
615
+ x.translate(w/2,h/2);
616
+ const size = min(6, min(w,h)/99); // fit to screen
617
+ x.scale(size,size);
618
+ x.translate(-40,-35);
619
+ x.lineJoin = x.lineCap = 'round';
620
+ x.lineWidth = .1 + p*1.9;
621
+
622
+ // drawing effect
623
+ const p2 = percent(alpha,.1,1);
624
+ x.setLineDash([99*p2,99]);
625
+
626
+ // cab top
627
+ rect(7,16,18,-8,color(2,2));
628
+ rect(7,8,18,4,color(2,3));
629
+ rect(25,8,8,8,color(2,1));
630
+ rect(25,8,-18,8);
631
+ rect(25,8,8,8);
632
+
633
+ // cab
634
+ rect(25,16,7,24,color());
635
+ rect(11,39,14,-23,color(1,1));
636
+ rect(11,16,14,18,color(1,2));
637
+ rect(11,16,14,8,color(1,3));
638
+ rect(25,16,-14,24);
639
+
640
+ // cab window
641
+ rect(15,29,6,-9,color(2,2));
642
+ circle(15,21,5,0,PI/2,color(2,4),1);
643
+ rect(21,21,-6,9);
644
+
645
+ // little stack
646
+ rect(37,14,9,6,color(3,2));
647
+ rect(37,14,4.5,6,color(3,3));
648
+ rect(37,14,9,6);
649
+
650
+ // big stack
651
+ rect(50,20,10,-8,color(0,1));
652
+ rect(50,20,6.5,-8,color(0,2));
653
+ rect(50,20,3.5,-8,color(0,3));
654
+ rect(50,20,10,-8);
655
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
656
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
657
+ circle(55,2,11.4,.5,PI-.5);
658
+ rect(45,7,20,-7,color(0,2));
659
+ rect(45,-1,20,4,color(0,3));
660
+ rect(45,-1,20,8);
661
+
662
+ // engine
663
+ for (let i=5; i--;)
664
+ {
665
+ // stagger radius to fix slight seam
666
+ circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
667
+ circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
668
+ circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
669
+ }
670
+
671
+ // engine outline
672
+ circle(36,30,10,PI/2,PI*3/2);
673
+ circle(48,30,10,PI/2,PI*3/2);
674
+ circle(60,30,10);
675
+ line(36,20,60,20);
676
+
677
+ // engine front light
678
+ circle(60,30,4,PI,3*PI,color(3,2));
679
+ circle(60,30,4,PI,2*PI,color(3,3));
680
+ circle(60,30,4,PI,3*PI);
681
+
682
+ // front brush
683
+ for (let i=6; i--;)
684
+ {
685
+ x.beginPath();
686
+ x.lineTo(53,54);
687
+ x.lineTo(53,40);
688
+ x.lineTo(53+(1+i*2.9)*p,40);
689
+ x.lineTo(53+(4+i*3.5)*p,54);
690
+ x.fillStyle = color(0,i%2+2);
691
+ x.fill();
692
+ i%2 && x.stroke();
693
+ }
694
+
695
+ // wheels
696
+ rect(6,40,5,5);
697
+ rect(6,40,5,5,color());
698
+ rect(15,54,38,-14,color());
699
+ for (let i=3; i--;)
700
+ for (let j=2; j--;)
701
+ {
702
+ circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
703
+ x.stroke();
704
+ circle(15*i+15,47,j?7:1,0,PI,color(i,2));
705
+ x.stroke();
706
+ }
707
+ line(6,40,68,40); // center
708
+ line(77,54,4,54); // bottom
709
+
710
+ // draw engine name
711
+ const s = engineName;
712
+ x.font = '900 16px arial';
713
+ x.textAlign = 'center';
714
+ x.textBaseline = 'top';
715
+ x.lineWidth = .1+p*3.9;
716
+ let w2 = 0;
717
+ for (let i=0; i<s.length; ++i)
718
+ w2 += x.measureText(s[i]).width;
719
+ for (let j=2; j--;)
720
+ for (let i=0, X=41-w2/2; i<s.length; ++i)
721
+ {
722
+ x.fillStyle = color(i,2);
723
+ const w = x.measureText(s[i]).width;
724
+ x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
725
+ X += w;
726
+ }
727
+ x.restore();
727
728
  }