littlejsengine 1.15.8 → 1.15.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,11 +7,10 @@
7
7
  </head><body>
8
8
 
9
9
  <!-- LittleJS Engine -->
10
- <script src=../../dist/littlejs.js?1.15.8></script>
10
+ <script src=../../dist/littlejs.js?1.15.9></script>
11
11
 
12
12
  <!-- LittleJS Engine Source
13
13
  <script src=../../src/engine.js></script>
14
- <script src=../../src/engineSplash.js></script>
15
14
  <script src=../../src/engineDebug.js></script>
16
15
  <script src=../../src/engineUtilities.js></script>
17
16
  <script src=../../src/engineSettings.js></script>
@@ -32,4 +31,4 @@
32
31
  -->
33
32
 
34
33
  <!-- Add your game scripts here -->
35
- <script src=game.js?1.15.8></script>
34
+ <script src=game.js?1.15.9></script>
@@ -43,14 +43,14 @@ function createUI()
43
43
  uiMenu.addChild(uiBackground);
44
44
 
45
45
  // example large text
46
- const textTitle = new LJS.UIText(vec2(0,-220), vec2(500, 120), 'Test Title');
46
+ const textTitle = new LJS.UIText(vec2(0,-220), vec2(400, 120), 'Test Title');
47
47
  uiMenu.addChild(textTitle);
48
48
  textTitle.textColor = LJS.RED;
49
49
  textTitle.textLineColor = LJS.BLUE;
50
50
  textTitle.textLineWidth = 4;
51
51
 
52
52
  // example multiline text
53
- const textTest = new LJS.UIText(vec2(-60,-120), vec2(350, 60), 'Test Text\nSecond text line.')
53
+ const textTest = new LJS.UIText(vec2(-60,-120), vec2(300, 60), 'Test Text\nSecond text line.')
54
54
  uiMenu.addChild(textTest);
55
55
 
56
56
  // example tile image
@@ -61,14 +61,11 @@ function createUI()
61
61
  let navigationIndex = 0;
62
62
 
63
63
  // example checkbox
64
- const checkbox = new LJS.UICheckbox(vec2(-140,-20), vec2(40));
64
+ const checkbox = new LJS.UICheckbox(vec2(-140,-20), vec2(50));
65
65
  uiMenu.addChild(checkbox);
66
66
  checkbox.onChange = ()=> button1.disabled = checkbox.checked;
67
67
  checkbox.navigationIndex = ++navigationIndex;
68
-
69
- // text attached to checkbox
70
- const checkboxText = new LJS.UIText(vec2(170,0), vec2(300, 60), 'Test Checkbox');
71
- checkbox.addChild(checkboxText);
68
+ checkbox.text = 'Test Checkbox';
72
69
 
73
70
  // example scrollbar
74
71
  const scrollbar = new LJS.UIScrollbar(vec2(0,60), vec2(350, 50));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.15.8",
3
+ "version": "1.15.9",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
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.15.8';
33
+ const engineVersion = '1.15.9';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {number}
@@ -305,23 +305,48 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
305
305
 
306
306
  if (canvasFixedSize.x)
307
307
  {
308
- // clear canvas and set fixed size
309
- mainCanvas.width = canvasFixedSize.x;
310
- mainCanvas.height = canvasFixedSize.y;
311
-
312
- // fit to window by adding space on top or bottom if necessary
313
- const aspect = innerWidth / innerHeight;
314
- const fixedAspect = mainCanvas.width / mainCanvas.height;
315
- (glCanvas||mainCanvas).style.width = mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
316
- (glCanvas||mainCanvas).style.height = mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
308
+ // set canvas fixed size
309
+ mainCanvasSize = canvasFixedSize.copy();
310
+
311
+ // fit to window using css width and height
312
+ const innerAspect = innerWidth / innerHeight;
313
+ const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
314
+ const w = innerAspect < fixedAspect ? '100%' : '';
315
+ const h = innerAspect < fixedAspect ? '' : '100%';
316
+ overlayCanvas.style.width = mainCanvas.style.width = w;
317
+ overlayCanvas.style.height = mainCanvas.style.height = h;
318
+ if (glCanvas)
319
+ {
320
+ glCanvas.style.width = w;
321
+ glCanvas.style.height = h;
322
+ }
317
323
  }
318
324
  else
319
325
  {
320
- // clear canvas and set size to same as window
321
- mainCanvas.width = min(innerWidth, canvasMaxSize.x);
322
- mainCanvas.height = min(innerHeight, canvasMaxSize.y);
326
+ // get main canvas size based on window size
327
+ mainCanvasSize.x = min(innerWidth, canvasMaxSize.x);
328
+ mainCanvasSize.y = min(innerHeight, canvasMaxSize.y);
329
+
330
+ // responsive aspect ratio with native resolution
331
+ const innerAspect = innerWidth / innerHeight;
332
+ if (canvasMaxAspect && innerAspect > canvasMaxAspect)
333
+ {
334
+ // full height
335
+ const w = mainCanvasSize.y * canvasMaxAspect | 0;
336
+ mainCanvasSize.x = min(w, canvasMaxSize.x);
337
+ }
338
+ else if (innerAspect < canvasMinAspect)
339
+ {
340
+ // full width
341
+ const h = mainCanvasSize.x / canvasMinAspect | 0;
342
+ mainCanvasSize.y = min(h, canvasMaxSize.y);
343
+ }
323
344
  }
324
345
 
346
+ // clear main and overlay canvas and set size
347
+ overlayCanvas.width = mainCanvas.width = mainCanvasSize.x;
348
+ overlayCanvas.height = mainCanvas.height = mainCanvasSize.y;
349
+
325
350
  // apply the clear color to main canvas
326
351
  if (canvasClearColor.a > 0)
327
352
  {
@@ -330,13 +355,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
330
355
  mainContext.fillStyle = BLACK.toString();
331
356
  }
332
357
 
333
- // clear overlay canvas and set size
334
- overlayCanvas.width = mainCanvas.width;
335
- overlayCanvas.height = mainCanvas.height;
336
-
337
- // save canvas size
338
- mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
339
-
340
358
  // set default line join and cap
341
359
  const lineJoin = 'round', lineCap = 'round';
342
360
  mainContext.lineJoin = overlayContext.lineJoin = lineJoin;
@@ -360,7 +378,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
360
378
  'user-select:none;' + // prevent hold to select
361
379
  '-webkit-user-select:none;' + // compatibility for ios
362
380
  'touch-action:none;' + // prevent mobile pinch to resize
363
- '-webkit-touch-callout:none';// compatibility for ios
381
+ '-webkit-touch-callout:none'; // compatibility for ios
364
382
  rootElement.style.cssText = styleRoot;
365
383
  drawCanvas = mainCanvas = document.createElement('canvas');
366
384
  rootElement.appendChild(mainCanvas);
@@ -377,7 +395,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
377
395
  rootElement.appendChild(overlayCanvas);
378
396
  overlayContext = overlayCanvas.getContext('2d');
379
397
 
380
- // set canvases
398
+ // setup canvases
399
+ // transform way is still more reliable then flexbox or grid
381
400
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
382
401
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
383
402
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
@@ -441,9 +460,176 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
441
460
  // wait for all the promises to finish
442
461
  await Promise.all(promises);
443
462
  return startEngine();
463
+
464
+ ///////////////////////////////////////////////////////////////////////////
465
+ // LittleJS Splash Screen
466
+ function drawEngineSplashScreen(t)
467
+ {
468
+ const x = overlayContext;
469
+ const w = overlayCanvas.width = innerWidth;
470
+ const h = overlayCanvas.height = innerHeight;
471
+
472
+ {
473
+ // background
474
+ const p3 = percent(t, 1, .8);
475
+ const p4 = percent(t, 0, .5);
476
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
477
+ g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
478
+ g.addColorStop(1,hsl(0,0,0,p3).toString());
479
+ x.save();
480
+ x.fillStyle = g;
481
+ x.fillRect(0,0,w,h);
482
+ }
483
+
484
+ // draw LittleJS logo...
485
+ const rect = (X, Y, W, H, C)=>
486
+ {
487
+ x.beginPath();
488
+ x.rect(X,Y,W,C?H*p:H);
489
+ x.fillStyle = C;
490
+ C ? x.fill() : x.stroke();
491
+ };
492
+ const line = (X, Y, Z, W)=>
493
+ {
494
+ x.beginPath();
495
+ x.lineTo(X,Y);
496
+ x.lineTo(Z,W);
497
+ x.stroke();
498
+ };
499
+ const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
500
+ {
501
+ const D = (A+B)/2, E = p*(B-A)/2;
502
+ x.beginPath();
503
+ F && x.lineTo(X,Y);
504
+ x.arc(X,Y,R,D-E,D+E);
505
+ x.fillStyle = C;
506
+ C ? x.fill() : x.stroke();
507
+ };
508
+ const color = (c=0, l=0) =>
509
+ hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();
510
+ const alpha = wave(1,1,t);
511
+ const p = percent(alpha, .1, .5);
512
+
513
+ // setup
514
+ x.translate(w/2,h/2);
515
+ const size = min(6, min(w,h)/99); // fit to screen
516
+ x.scale(size,size);
517
+ x.translate(-40,-35);
518
+ x.lineJoin = x.lineCap = 'round';
519
+ x.lineWidth = .1 + p*1.9;
520
+
521
+ // drawing effect
522
+ const p2 = percent(alpha,.1,1);
523
+ x.setLineDash([99*p2,99]);
524
+
525
+ // cab top
526
+ rect(7,16,18,-8,color(2,2));
527
+ rect(7,8,18,4,color(2,3));
528
+ rect(25,8,8,8,color(2,1));
529
+ rect(25,8,-18,8);
530
+ rect(25,8,8,8);
531
+
532
+ // cab
533
+ rect(25,16,7,23,color());
534
+ rect(11,39,14,-23,color(1,1));
535
+ rect(11,16,14,18,color(1,2));
536
+ rect(11,16,14,8,color(1,3));
537
+ rect(25,16,-14,24);
538
+
539
+ // cab window
540
+ rect(15,29,6,-9,color(2,2));
541
+ circle(15,21,5,0,PI/2,color(2,4),1);
542
+ rect(21,21,-6,9);
543
+
544
+ // little stack
545
+ rect(37,14,9,6,color(3,2));
546
+ rect(37,14,4.5,6,color(3,3));
547
+ rect(37,14,9,6);
548
+
549
+ // big stack
550
+ rect(50,20,10,-8,color(0,1));
551
+ rect(50,20,6.5,-8,color(0,2));
552
+ rect(50,20,3.5,-8,color(0,3));
553
+ rect(50,20,10,-8);
554
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
555
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
556
+ circle(55,2,11.4,.5,PI-.5);
557
+ rect(45,7,20,-7,color(0,2));
558
+ rect(45,-1,20,4,color(0,3));
559
+ rect(45,-1,20,8);
560
+
561
+ // engine
562
+ for (let i=5; i--;)
563
+ {
564
+ // stagger radius to fix slight seam
565
+ circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
566
+ circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
567
+ circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
568
+ }
569
+
570
+ // engine outline
571
+ circle(36,30,10,PI/2,PI*3/2);
572
+ circle(48,30,10,PI/2,PI*3/2);
573
+ circle(60,30,10);
574
+ line(36,20,60,20);
575
+
576
+ // engine front light
577
+ circle(60,30,4,PI,3*PI,color(3,2));
578
+ circle(60,30,4,PI,2*PI,color(3,3));
579
+ circle(60,30,4,PI,3*PI);
580
+
581
+ // front brush
582
+ for (let i=6; i--;)
583
+ {
584
+ x.beginPath();
585
+ x.lineTo(53,54);
586
+ x.lineTo(53,40);
587
+ x.lineTo(53+(1+i*2.9)*p,40);
588
+ x.lineTo(53+(4+i*3.5)*p,54);
589
+ x.fillStyle = color(0,i%2+2);
590
+ x.fill();
591
+ i%2 && x.stroke();
592
+ }
593
+
594
+ // wheels
595
+ rect(6,40,5,5);
596
+ rect(6,40,5,5,color());
597
+ rect(15,54,38,-14,color());
598
+ for (let i=3; i--;)
599
+ for (let j=2; j--;)
600
+ {
601
+ circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
602
+ x.stroke();
603
+ circle(15*i+15,47,j?7:1,0,PI,color(i,2));
604
+ x.stroke();
605
+ }
606
+ line(6,40,68,40); // center
607
+ line(77,54,4,54); // bottom
608
+
609
+ // draw engine name
610
+ const s = engineName;
611
+ x.font = '900 16px arial';
612
+ x.textAlign = 'center';
613
+ x.textBaseline = 'top';
614
+ x.lineWidth = .1+p*3.9;
615
+ let w2 = 0;
616
+ for (let i=0; i<s.length; ++i)
617
+ w2 += x.measureText(s[i]).width;
618
+ for (let j=2; j--;)
619
+ for (let i=0, X=41-w2/2; i<s.length; ++i)
620
+ {
621
+ x.fillStyle = color(i,2);
622
+ const w = x.measureText(s[i]).width;
623
+ x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
624
+ X += w;
625
+ }
626
+ x.restore();
627
+ }
628
+ ///////////////////////////////////////////////////////////////////////////
444
629
  }
445
630
 
446
631
  /** Update each engine object, remove destroyed objects, and update time
632
+ * can be called manually if objects need to be updated outside of main loop
447
633
  * @memberof Engine */
448
634
  function engineObjectsUpdate()
449
635
  {
@@ -495,18 +681,21 @@ function engineObjectsDestroy()
495
681
  function engineObjectsCollect(pos, size, objects=engineObjects)
496
682
  {
497
683
  const collectedObjects = [];
498
- if (!pos) // all objects
684
+ if (!pos)
499
685
  {
686
+ // all objects
500
687
  for (const o of objects)
501
688
  collectedObjects.push(o);
502
689
  }
503
- else if (size instanceof Vector2) // bounding box test
690
+ else if (size instanceof Vector2)
504
691
  {
692
+ // bounding box test
505
693
  for (const o of objects)
506
694
  o.isOverlapping(pos, size) && collectedObjects.push(o);
507
695
  }
508
- else // circle test
696
+ else
509
697
  {
698
+ // circle test
510
699
  const sizeSquared = size*size;
511
700
  for (const o of objects)
512
701
  pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
@@ -26,7 +26,6 @@ const engineSourceFiles =
26
26
  `${SOURCE_FOLDER}/engineParticles.js`,
27
27
  `${SOURCE_FOLDER}/engineMedals.js`,
28
28
  `${SOURCE_FOLDER}/engineWebGL.js`,
29
- `${SOURCE_FOLDER}/engineSplash.js`,
30
29
  ];
31
30
  const enginePluginFiles =
32
31
  [
@@ -56,6 +56,8 @@ export
56
56
  canvasColorTiles,
57
57
  canvasClearColor,
58
58
  canvasMaxSize,
59
+ canvasMinAspect,
60
+ canvasMaxAspect,
59
61
  canvasFixedSize,
60
62
  canvasPixelated,
61
63
  overlayCanvasPixelated,
@@ -99,6 +101,8 @@ export
99
101
  setCanvasColorTiles,
100
102
  setCanvasClearColor,
101
103
  setCanvasMaxSize,
104
+ setCanvasMinAspect,
105
+ setCanvasMaxAspect,
102
106
  setCanvasFixedSize,
103
107
  setCanvasPixelated,
104
108
  setOverlayCanvasPixelated,
@@ -243,6 +243,16 @@ function gamepadConnected(gamepad=gamepadPrimary)
243
243
  return !!inputData[gamepad+1];
244
244
  }
245
245
 
246
+ /** Returns how many control sticks the passed in gamepad has
247
+ * @param {number} [gamepad]
248
+ * @return {number}
249
+ * @memberof Input */
250
+ function gamepadStickCount(gamepad=gamepadPrimary)
251
+ {
252
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
253
+ return gamepadStickData[gamepad]?.length ?? 0;
254
+ }
255
+
246
256
  /** True if a touch device has been detected
247
257
  * @memberof Input */
248
258
  const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
@@ -583,9 +593,11 @@ function inputUpdate()
583
593
  dpad.set(x, -y);
584
594
  sticks[0] = dpad.clampLength(); // clamp to circle
585
595
  }
586
- const rightTouchStick = touchGamepadSticks[1] ?? vec2();
587
596
  if (touchGamepadButtonCount === 1)
597
+ {
598
+ const rightTouchStick = touchGamepadSticks[1] ?? vec2();
588
599
  sticks[1] = applyDeadZones(rightTouchStick);
600
+ }
589
601
 
590
602
  // read virtual gamepad buttons
591
603
  const data = inputData[1] ?? (inputData[1] = []);
@@ -109,9 +109,9 @@ class ParticleEmitter extends EngineObject
109
109
  /** @property {Color} - Color at start of life 2, randomized between start colors */
110
110
  this.colorStartB = colorStartB.copy();
111
111
  /** @property {Color} - Color at end of life 1, randomized between end colors */
112
- this.colorEndA = colorEndA.copy();
112
+ this.colorEndA = colorEndA.copy();
113
113
  /** @property {Color} - Color at end of life 2, randomized between end colors */
114
- this.colorEndB = colorEndB.copy();
114
+ this.colorEndB = colorEndB.copy();
115
115
  /** @property {boolean} - Should color be randomized linearly or across each component */
116
116
  this.randomColorLinear = randomColorLinear;
117
117
 
@@ -195,8 +195,10 @@ class ParticleEmitter extends EngineObject
195
195
  if (debugParticles)
196
196
  {
197
197
  // show emitter bounds
198
- const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
199
- debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
198
+ if (typeof this.emitSize === 'number')
199
+ debugCircle(this.pos, this.emitSize/2, '#0f0');
200
+ else
201
+ debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle);
200
202
  }
201
203
  }
202
204
 
@@ -206,8 +208,8 @@ class ParticleEmitter extends EngineObject
206
208
  {
207
209
  // spawn a particle
208
210
  let pos = typeof this.emitSize === 'number' ? // check if number was used
209
- randInCircle(this.emitSize/2) // circle emitter
210
- : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
211
+ randInCircle(this.emitSize/2) // circle emitter
212
+ : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
211
213
  .multiply(this.emitSize).rotate(this.angle)
212
214
  let angle = rand(this.particleConeAngle, -this.particleConeAngle);
213
215
  if (!this.localSpace)
@@ -48,6 +48,20 @@ let canvasClearColor = CLEAR_BLACK;
48
48
  * @memberof Settings */
49
49
  let canvasMaxSize = vec2(1920, 1080);
50
50
 
51
+ /** Minimum aspect ratio of the canvas (width/height), unused if 0
52
+ * Can be used with canvasMaxAspect to limit aspect ratio
53
+ * @type {number}
54
+ * @default
55
+ * @memberof Settings */
56
+ let canvasMinAspect = 0;
57
+
58
+ /** Maximum aspect ratio of the canvas (width/height), unused if 0
59
+ * Can be used with canvasMinAspect to limit aspect ratio
60
+ * @type {number}
61
+ * @default
62
+ * @memberof Settings */
63
+ let canvasMaxAspect = 0;
64
+
51
65
  /** Fixed size of the canvas, if enabled canvas size never changes
52
66
  * - you may also need to set mainCanvasSize if using screen space coords in startup
53
67
  * @type {Vector2}
@@ -344,6 +358,16 @@ function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
344
358
  * @memberof Settings */
345
359
  function setCanvasMaxSize(size) { canvasMaxSize = size.copy(); }
346
360
 
361
+ /** Set minimum aspect ratio of the canvas (width/height), unused if 0
362
+ * @param {number} aspect
363
+ * @memberof Settings */
364
+ function setCanvasMinAspect(aspect) { canvasMinAspect = aspect; }
365
+
366
+ /** Set maximum aspect ratio of the canvas (width/height), unused if 0
367
+ * @param {number} aspect
368
+ * @memberof Settings */
369
+ function setCanvasMaxAspect(aspect) { canvasMaxAspect = aspect; }
370
+
347
371
  /** Set fixed size of the canvas
348
372
  * @param {Vector2} size
349
373
  * @memberof Settings */
@@ -211,8 +211,7 @@ class CanvasLayer extends EngineObject
211
211
  /** Destroy this canvas layer */
212
212
  destroy()
213
213
  {
214
- if (this.destroyed)
215
- return;
214
+ if (this.destroyed) return;
216
215
 
217
216
  this.textureInfo.destroyWebGLTexture();
218
217
  super.destroy();
@@ -1,173 +0,0 @@
1
- /**
2
- * LittleJS Engine Splash Screen
3
- * @namespace Settings
4
- */
5
-
6
- 'use strict';
7
-
8
- ///////////////////////////////////////////////////////////////////////////////
9
- // LittleJS splash screen and logo
10
-
11
- function drawEngineSplashScreen(t)
12
- {
13
- const x = overlayContext;
14
- const w = overlayCanvas.width = innerWidth;
15
- const h = overlayCanvas.height = innerHeight;
16
-
17
- {
18
- // background
19
- const p3 = percent(t, 1, .8);
20
- const p4 = percent(t, 0, .5);
21
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
22
- g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
23
- g.addColorStop(1,hsl(0,0,0,p3).toString());
24
- x.save();
25
- x.fillStyle = g;
26
- x.fillRect(0,0,w,h);
27
- }
28
-
29
- // draw LittleJS logo...
30
- const rect = (X, Y, W, H, C)=>
31
- {
32
- x.beginPath();
33
- x.rect(X,Y,W,C?H*p:H);
34
- x.fillStyle = C;
35
- C ? x.fill() : x.stroke();
36
- };
37
- const line = (X, Y, Z, W)=>
38
- {
39
- x.beginPath();
40
- x.lineTo(X,Y);
41
- x.lineTo(Z,W);
42
- x.stroke();
43
- };
44
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
45
- {
46
- const D = (A+B)/2, E = p*(B-A)/2;
47
- x.beginPath();
48
- F && x.lineTo(X,Y);
49
- x.arc(X,Y,R,D-E,D+E);
50
- x.fillStyle = C;
51
- C ? x.fill() : x.stroke();
52
- };
53
- const color = (c=0, l=0) =>
54
- hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();
55
- const alpha = wave(1,1,t);
56
- const p = percent(alpha, .1, .5);
57
-
58
- // setup
59
- x.translate(w/2,h/2);
60
- const size = min(6, min(w,h)/99); // fit to screen
61
- x.scale(size,size);
62
- x.translate(-40,-35);
63
- x.lineJoin = x.lineCap = 'round';
64
- x.lineWidth = .1 + p*1.9;
65
-
66
- // drawing effect
67
- const p2 = percent(alpha,.1,1);
68
- x.setLineDash([99*p2,99]);
69
-
70
- // cab top
71
- rect(7,16,18,-8,color(2,2));
72
- rect(7,8,18,4,color(2,3));
73
- rect(25,8,8,8,color(2,1));
74
- rect(25,8,-18,8);
75
- rect(25,8,8,8);
76
-
77
- // cab
78
- rect(25,16,7,23,color());
79
- rect(11,39,14,-23,color(1,1));
80
- rect(11,16,14,18,color(1,2));
81
- rect(11,16,14,8,color(1,3));
82
- rect(25,16,-14,24);
83
-
84
- // cab window
85
- rect(15,29,6,-9,color(2,2));
86
- circle(15,21,5,0,PI/2,color(2,4),1);
87
- rect(21,21,-6,9);
88
-
89
- // little stack
90
- rect(37,14,9,6,color(3,2));
91
- rect(37,14,4.5,6,color(3,3));
92
- rect(37,14,9,6);
93
-
94
- // big stack
95
- rect(50,20,10,-8,color(0,1));
96
- rect(50,20,6.5,-8,color(0,2));
97
- rect(50,20,3.5,-8,color(0,3));
98
- rect(50,20,10,-8);
99
- circle(55,2,11.4,.5,PI-.5,color(3,3));
100
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
101
- circle(55,2,11.4,.5,PI-.5);
102
- rect(45,7,20,-7,color(0,2));
103
- rect(45,-1,20,4,color(0,3));
104
- rect(45,-1,20,8);
105
-
106
- // engine
107
- for (let i=5; i--;)
108
- {
109
- // stagger radius to fix slight seam
110
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
111
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
112
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
113
- }
114
-
115
- // engine outline
116
- circle(36,30,10,PI/2,PI*3/2);
117
- circle(48,30,10,PI/2,PI*3/2);
118
- circle(60,30,10);
119
- line(36,20,60,20);
120
-
121
- // engine front light
122
- circle(60,30,4,PI,3*PI,color(3,2));
123
- circle(60,30,4,PI,2*PI,color(3,3));
124
- circle(60,30,4,PI,3*PI);
125
-
126
- // front brush
127
- for (let i=6; i--;)
128
- {
129
- x.beginPath();
130
- x.lineTo(53,54);
131
- x.lineTo(53,40);
132
- x.lineTo(53+(1+i*2.9)*p,40);
133
- x.lineTo(53+(4+i*3.5)*p,54);
134
- x.fillStyle = color(0,i%2+2);
135
- x.fill();
136
- i%2 && x.stroke();
137
- }
138
-
139
- // wheels
140
- rect(6,40,5,5);
141
- rect(6,40,5,5,color());
142
- rect(15,54,38,-14,color());
143
- for (let i=3; i--;)
144
- for (let j=2; j--;)
145
- {
146
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
147
- x.stroke();
148
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
149
- x.stroke();
150
- }
151
- line(6,40,68,40); // center
152
- line(77,54,4,54); // bottom
153
-
154
- // draw engine name
155
- const s = engineName;
156
- x.font = '900 16px arial';
157
- x.textAlign = 'center';
158
- x.textBaseline = 'top';
159
- x.lineWidth = .1+p*3.9;
160
- let w2 = 0;
161
- for (let i=0; i<s.length; ++i)
162
- w2 += x.measureText(s[i]).width;
163
- for (let j=2; j--;)
164
- for (let i=0, X=41-w2/2; i<s.length; ++i)
165
- {
166
- x.fillStyle = color(i,2);
167
- const w = x.measureText(s[i]).width;
168
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
169
- X += w;
170
- }
171
-
172
- x.restore();
173
- }