littlejsengine 1.8.6 → 1.8.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.
Files changed (38) hide show
  1. package/README.md +9 -3
  2. package/build/littlejs.d.ts +18 -9
  3. package/build/littlejs.esm.js +298 -92
  4. package/build/littlejs.esm.min.js +1 -1
  5. package/build/littlejs.js +296 -92
  6. package/build/littlejs.min.js +1 -1
  7. package/build/littlejs.release.js +296 -92
  8. package/examples/breakout/game.js +3 -0
  9. package/examples/breakout/index.html +3 -3
  10. package/examples/breakoutTutorial/index.html +2 -2
  11. package/examples/electron/game.js +3 -0
  12. package/examples/electron/index.html +2 -2
  13. package/examples/js13k/index.html +13 -13
  14. package/examples/logo.png +0 -0
  15. package/examples/module/game.js +3 -0
  16. package/examples/module/index.html +1 -1
  17. package/examples/particles/index.html +1 -1
  18. package/examples/platformer/game.js +3 -0
  19. package/examples/platformer/gameEffects.js +1 -1
  20. package/examples/platformer/index.html +6 -6
  21. package/examples/puzzle/game.js +3 -0
  22. package/examples/puzzle/index.html +2 -2
  23. package/examples/starter/game.js +4 -1
  24. package/examples/starter/index.html +13 -13
  25. package/examples/starter/tiles.png +0 -0
  26. package/examples/stress/index.html +3 -1
  27. package/examples/typescript/game.js +2 -0
  28. package/examples/typescript/game.ts +3 -0
  29. package/examples/typescript/index.html +1 -1
  30. package/package.json +1 -1
  31. package/src/engine.js +189 -9
  32. package/src/engineAudio.js +72 -58
  33. package/src/engineDraw.js +1 -1
  34. package/src/engineExport.js +2 -0
  35. package/src/engineParticles.js +7 -7
  36. package/src/engineSettings.js +11 -0
  37. package/src/engineUtilities.js +4 -5
  38. package/src/engineWebGL.js +12 -12
package/build/littlejs.js CHANGED
@@ -1008,11 +1008,10 @@ class Color
1008
1008
  * @return {Number} */
1009
1009
  rgbaInt()
1010
1010
  {
1011
- const toByte = (c)=> clamp(c)*255|0;
1012
- const r = toByte(this.r);
1013
- const g = toByte(this.g)<<8;
1014
- const b = toByte(this.b)<<16;
1015
- const a = toByte(this.a)<<24;
1011
+ const r = clamp(this.r)*255|0;
1012
+ const g = clamp(this.g)*255<<8;
1013
+ const b = clamp(this.b)*255<<16;
1014
+ const a = clamp(this.a)*255<<24;
1016
1015
  return r + g + b + a;
1017
1016
  }
1018
1017
  }
@@ -1120,6 +1119,12 @@ let canvasPixelated = 1;
1120
1119
  * @memberof Settings */
1121
1120
  let fontDefault = 'arial';
1122
1121
 
1122
+ /** Enable to show the LittleJS splash screen be shown on startup
1123
+ * @type {Boolean}
1124
+ * @default
1125
+ * @memberof Settings */
1126
+ let showSplashScreen = 0;
1127
+
1123
1128
  ///////////////////////////////////////////////////////////////////////////////
1124
1129
  // WebGL settings
1125
1130
 
@@ -1353,6 +1358,11 @@ function setCanvasPixelated(pixelated) { canvasPixelated = pixelated; }
1353
1358
  * @memberof Settings */
1354
1359
  function setFontDefault(font) { fontDefault = font; }
1355
1360
 
1361
+ /** Set if the LittleJS splash screen be shown on startup
1362
+ * @param {Boolean} show
1363
+ * @memberof Settings */
1364
+ function setShowSplashScreen(show) { showSplashScreen = show; }
1365
+
1356
1366
  /** Set if webgl rendering is enabled
1357
1367
  * @param {Boolean} enable
1358
1368
  * @memberof Settings */
@@ -1944,7 +1954,7 @@ let mainCanvasSize = vec2();
1944
1954
  * @memberof Draw */
1945
1955
  let textureInfos = [];
1946
1956
 
1947
- // Engine internal variables not exposed to documentation
1957
+ // Keep track of how many draw calls there were each frame for debugging
1948
1958
  let drawCount;
1949
1959
 
1950
1960
  ///////////////////////////////////////////////////////////////////////////////
@@ -3186,7 +3196,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0, sampleRate
3186
3196
  }
3187
3197
 
3188
3198
  ///////////////////////////////////////////////////////////////////////////////
3189
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.2.0 by Frank Force
3199
+ // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.0 by Frank Force
3190
3200
 
3191
3201
  /** Generate and play a ZzFX sound
3192
3202
  *
@@ -3222,6 +3232,7 @@ const zzfxR = 44100;
3222
3232
  * @param {Number} [sustainVolume=1] - Volume level for sustain (percent)
3223
3233
  * @param {Number} [decay=0] - Decay time, how long to reach sustain after attack (seconds)
3224
3234
  * @param {Number} [tremolo=0] - Trembling effect, rate controlled by repeat time (precent)
3235
+ * @param {Number} [filter=0] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
3225
3236
  * @return {Array} - Array of audio samples
3226
3237
  * @memberof Audio
3227
3238
  */
@@ -3231,78 +3242,91 @@ function zzfxG
3231
3242
  volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3232
3243
  release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3233
3244
  pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3234
- bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0
3245
+ bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
3235
3246
  )
3236
3247
  {
3237
- // locals
3238
- let PI2 = PI*2, startSlide = slide *= 500 * PI2 / zzfxR / zzfxR, b=[],
3239
- startFrequency = frequency *= (1 + randomness*rand(-1,1)) * PI2 / zzfxR,
3240
- t=0, tm=0, i=0, j=1, r=0, c=0, s=0, f, length
3248
+ // init parameters
3249
+ let PI2 = PI*2, sampleRate = zzfxR,
3250
+ startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3251
+ startFrequency = frequency *=
3252
+ rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3253
+ b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3254
+
3255
+ // biquad LP/HP filter
3256
+ quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
3257
+ cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
3258
+ a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
3259
+ b0 = (1 + sign(filter) * cos) / 2 / a0,
3260
+ b1 = -(sign(filter) + cos) / a0, b2 = b0,
3261
+ x2 = 0, x1 = 0, y2 = 0, y1 = 0;
3241
3262
 
3242
3263
  // scale by sample rate
3243
- attack = attack * zzfxR + 9; // minimum attack to prevent pop
3244
- decay *= zzfxR;
3245
- sustain *= zzfxR;
3246
- release *= zzfxR;
3247
- delay *= zzfxR;
3248
- deltaSlide *= 500 * PI2 / zzfxR**3;
3249
- modulation *= PI2 / zzfxR;
3250
- pitchJump *= PI2 / zzfxR;
3251
- pitchJumpTime *= zzfxR;
3252
- repeatTime = repeatTime * zzfxR | 0;
3264
+ attack = attack * sampleRate + 9; // minimum attack to prevent pop
3265
+ decay *= sampleRate;
3266
+ sustain *= sampleRate;
3267
+ release *= sampleRate;
3268
+ delay *= sampleRate;
3269
+ deltaSlide *= 500 * PI2 / sampleRate**3;
3270
+ modulation *= PI2 / sampleRate;
3271
+ pitchJump *= PI2 / sampleRate;
3272
+ pitchJumpTime *= sampleRate;
3273
+ repeatTime = repeatTime * sampleRate | 0;
3274
+ volume *= soundVolume;
3253
3275
 
3254
3276
  // generate waveform
3255
- for (length = attack + decay + sustain + release + delay | 0;
3256
- i < length; b[i++] = s)
3277
+ for(length = attack + decay + sustain + release + delay | 0;
3278
+ i < length; b[i++] = s * volume) // sample
3257
3279
  {
3258
- if (!(++c%(bitCrush*100|0))) // bit crush
3280
+ if (!(++c%(bitCrush*100|0))) // bit crush
3259
3281
  {
3260
- s = shape? shape>1? shape>2? shape>3? // wave shape
3261
- Math.sin((t%PI2)**3) : // 4 noise
3262
- max(min(Math.tan(t),1),-1): // 3 tan
3263
- 1-(2*t/PI2%2+2)%2: // 2 saw
3264
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
3265
- Math.sin(t); // 0 sin
3266
-
3282
+ s = shape? shape>1? shape>2? shape>3? // wave shape
3283
+ Math.sin(t*t) : // 4 noise
3284
+ clamp(Math.tan(t),1,-1): // 3 tan
3285
+ 1-(2*t/PI2%2+2)%2: // 2 saw
3286
+ 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
3287
+ Math.sin(t); // 0 sin
3288
+
3267
3289
  s = (repeatTime ?
3268
3290
  1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
3269
3291
  : 1) *
3270
- sign(s)*(abs(s)**shapeCurve) * // curve 0=square, 2=pointy
3271
- volume * soundVolume * ( // envelope
3272
- i < attack ? i/attack : // attack
3273
- i < attack + decay ? // decay
3274
- 1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
3275
- i < attack + decay + sustain ? // sustain
3276
- sustainVolume : // sustain volume
3277
- i < length - delay ? // release
3278
- (length - i - delay)/release * // release falloff
3279
- sustainVolume : // release volume
3280
- 0); // post release
3281
-
3282
- s = delay ? s/2 + (delay > i ? 0 : // delay
3283
- (i<length-delay? 1 : (length-i)/delay) * // release delay
3284
- b[i-delay|0]/2) : s; // sample delay
3285
- }
3286
-
3287
- f = (frequency += slide += deltaSlide) * // frequency
3288
- Math.cos(modulation*tm++); // modulation
3289
- t += f - f*noise*(1 - (Math.sin(i)+1)*1e9%2); // noise
3290
-
3291
- if (j && ++j > pitchJumpTime) // pitch jump
3292
- {
3293
- frequency += pitchJump; // apply pitch jump
3294
- startFrequency += pitchJump; // also apply to start
3295
- j = 0; // reset pitch jump time
3292
+ sign(s)*(abs(s)**shapeCurve) * // curve
3293
+ (i < attack ? i/attack : // attack
3294
+ i < attack + decay ? // decay
3295
+ 1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
3296
+ i < attack + decay + sustain ? // sustain
3297
+ sustainVolume : // sustain volume
3298
+ i < length - delay ? // release
3299
+ (length - i - delay)/release * // release falloff
3300
+ sustainVolume : // release volume
3301
+ 0); // post release
3302
+
3303
+ s = delay ? s/2 + (delay > i ? 0 : // delay
3304
+ (i<length-delay? 1 : (length-i)/delay) * // release delay
3305
+ b[i-delay|0]/2/volume) : s; // sample delay
3306
+
3307
+ if (filter) // apply filter
3308
+ s = y1 = b2*x2 + b1*(x2=x1) + b0*(x1=s) - a2*y2 - a1*(y2=y1);
3296
3309
  }
3297
3310
 
3298
- if (repeatTime && !(++r % repeatTime)) // repeat
3299
- {
3300
- frequency = startFrequency; // reset frequency
3301
- slide = startSlide; // reset slide
3302
- j ||= 1; // reset pitch jump time
3311
+ f = (frequency += slide += deltaSlide) *// frequency
3312
+ Math.cos(modulation*tm++); // modulation
3313
+ t += f + f*noise*Math.sin(i**5); // noise
3314
+
3315
+ if (j && ++j > pitchJumpTime) // pitch jump
3316
+ {
3317
+ frequency += pitchJump; // apply pitch jump
3318
+ startFrequency += pitchJump; // also apply to start
3319
+ j = 0; // stop pitch jump time
3320
+ }
3321
+
3322
+ if (repeatTime && !(++r % repeatTime)) // repeat
3323
+ {
3324
+ frequency = startFrequency; // reset frequency
3325
+ slide = startSlide; // reset slide
3326
+ j = j || 1; // reset pitch jump time
3303
3327
  }
3304
3328
  }
3305
-
3329
+
3306
3330
  return b;
3307
3331
  }
3308
3332
 
@@ -3798,13 +3822,13 @@ constructor(pos, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderO
3798
3822
  class ParticleEmitter extends EngineObject
3799
3823
  {
3800
3824
  /** Create a particle system with the given settings
3801
- * @param {Vector2} position - World space position of the emitter
3802
- * @param {Number} [angle=0] - Angle to emit the particles
3803
- * @param {Number} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
3804
- * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
3805
- * @param {Number} [emitRate=100] - How many particles per second to spawn, does not emit if 0
3825
+ * @param {Vector2} position - World space position of the emitter
3826
+ * @param {Number} [angle=0] - Angle to emit the particles
3827
+ * @param {Number|Vector2} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
3828
+ * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
3829
+ * @param {Number} [emitRate=100] - How many particles per second to spawn, does not emit if 0
3806
3830
  * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
3807
- * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
3831
+ * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
3808
3832
  * @param {Color} [colorStartA=Color()] - Color at start of life 1, randomized between start colors
3809
3833
  * @param {Color} [colorStartB=Color()] - Color at start of life 2, randomized between start colors
3810
3834
  * @param {Color} [colorEndA=Color(1,1,1,0)] - Color at end of life 1, randomized between end colors
@@ -3860,7 +3884,7 @@ class ParticleEmitter extends EngineObject
3860
3884
  super(pos, vec2(), tileInfo, angle, undefined, renderOrder);
3861
3885
 
3862
3886
  // emitter settings
3863
- /** @property {Number} - World space size of the emitter (float for circle diameter, vec2 for rect) */
3887
+ /** @property {Number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
3864
3888
  this.emitSize = emitSize
3865
3889
  /** @property {Number} - How long to stay alive (0 is forever) */
3866
3890
  this.emitTime = emitTime;
@@ -4392,7 +4416,7 @@ let glCanvas;
4392
4416
  let glContext;
4393
4417
 
4394
4418
  // WebGL internal variables not exposed to documentation
4395
- let glActiveTexture, glShader, glArrayBuffer, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
4419
+ let glActiveTexture, glShader, glArrayBuffer, glVertexData, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
4396
4420
 
4397
4421
  ///////////////////////////////////////////////////////////////////////////////
4398
4422
 
@@ -4429,10 +4453,10 @@ function glInit()
4429
4453
  );
4430
4454
 
4431
4455
  // init buffers
4432
- const vertexData = new ArrayBuffer(gl_VERTEX_BUFFER_SIZE);
4456
+ glVertexData = new ArrayBuffer(gl_VERTEX_BUFFER_SIZE);
4457
+ glPositionData = new Float32Array(glVertexData);
4458
+ glColorData = new Uint32Array(glVertexData);
4433
4459
  glArrayBuffer = glContext.createBuffer();
4434
- glPositionData = new Float32Array(vertexData);
4435
- glColorData = new Uint32Array(vertexData);
4436
4460
  glBatchCount = 0;
4437
4461
  }
4438
4462
 
@@ -4460,7 +4484,7 @@ function glPreRender()
4460
4484
  glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
4461
4485
  offset += size*typeSize;
4462
4486
  }
4463
- initVertexAttribArray('p', gl_FLOAT, 4, 4); // position & texture coords
4487
+ initVertexAttribArray('p', gl_FLOAT, 4, 4); // position & texture
4464
4488
  initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
4465
4489
  initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
4466
4490
 
@@ -4548,6 +4572,7 @@ function glCreateTexture(image)
4548
4572
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
4549
4573
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE);
4550
4574
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE);
4575
+
4551
4576
  return texture;
4552
4577
  }
4553
4578
 
@@ -4562,8 +4587,7 @@ function glFlush()
4562
4587
  glContext.enable(gl_BLEND);
4563
4588
 
4564
4589
  // draw all the sprites in the batch and reset the buffer
4565
- glContext.bufferSubData(gl_ARRAY_BUFFER, 0,
4566
- glPositionData.subarray(0, glBatchCount * gl_INDICIES_PER_VERT));
4590
+ glContext.bufferSubData(gl_ARRAY_BUFFER, 0, glVertexData);
4567
4591
  glContext.drawArrays(gl_TRIANGLE_STRIP, 0, glBatchCount);
4568
4592
  glBatchCount = 0;
4569
4593
  glBatchAdditive = glAdditive;
@@ -4618,11 +4642,11 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdd
4618
4642
  // setup 2 triangle strip quad
4619
4643
  for(let i = vertCount, offset = glBatchCount * gl_INDICIES_PER_VERT; i--;)
4620
4644
  {
4621
- let j = clamp(i-1, 0, 3)*4; // degenerate tri at ends
4622
- glPositionData[offset++] = positionData[j++];
4623
- glPositionData[offset++] = positionData[j++];
4624
- glPositionData[offset++] = positionData[j++];
4625
- glPositionData[offset++] = positionData[j++];
4645
+ const j = clamp(i-1, 0, 3)*4; // degenerate tri at ends
4646
+ glPositionData[offset++] = positionData[j+0];
4647
+ glPositionData[offset++] = positionData[j+1];
4648
+ glPositionData[offset++] = positionData[j+2];
4649
+ glPositionData[offset++] = positionData[j+3];
4626
4650
  glColorData[offset++] = rgba;
4627
4651
  glColorData[offset++] = rgbaAdditive;
4628
4652
  }
@@ -4823,7 +4847,7 @@ const engineName = 'LittleJS';
4823
4847
  * @type {String}
4824
4848
  * @default
4825
4849
  * @memberof Engine */
4826
- const engineVersion = '1.8.6';
4850
+ const engineVersion = '1.8.9';
4827
4851
 
4828
4852
  /** Frames per second to update objects
4829
4853
  * @type {Number}
@@ -5006,7 +5030,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5006
5030
  }
5007
5031
 
5008
5032
  // setup html
5009
- const styleBody =
5033
+ const styleBody =
5010
5034
  'margin:0;overflow:hidden;' + // fill the window
5011
5035
  'background:#000;' + // set background color
5012
5036
  'touch-action:none;' + // prevent mobile pinch to resize
@@ -5027,14 +5051,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5027
5051
 
5028
5052
  // set canvas style
5029
5053
  const styleCanvas =
5030
- 'position:absolute;' + // position
5031
- 'top:50%;left:50%;transform:translate(-50%,-50%);' + // center
5032
- (canvasPixelated?'image-rendering:pixelated':''); // pixelated rendering
5054
+ 'position:absolute;' + // position
5055
+ 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center
5033
5056
  (glCanvas||mainCanvas).style = mainCanvas.style = overlayCanvas.style = styleCanvas;
5034
5057
 
5035
- // load all of the images
5036
- Promise.all(imageSources.map((src, textureIndex)=>
5037
- new Promise((resolve, reject)=>
5058
+ // create promises for loading images
5059
+ const promises = imageSources.map((src, textureIndex)=>
5060
+ new Promise(resolve =>
5038
5061
  {
5039
5062
  const image = new Image;
5040
5063
  image.onerror = image.onload = ()=>
@@ -5044,7 +5067,24 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5044
5067
  }
5045
5068
  image.src = src;
5046
5069
  })
5047
- )).then(()=>
5070
+ )
5071
+
5072
+ // draw splash screen
5073
+ showSplashScreen && promises.push(new Promise(resolve =>
5074
+ {
5075
+ let t = 0;
5076
+ console.log(`LittleJS Engine v${engineVersion}`);
5077
+ updateSplash();
5078
+ function updateSplash()
5079
+ {
5080
+ clearInput();
5081
+ drawEngineSplashScreen(t+=.01);
5082
+ t>1 ? resolve() : setTimeout(updateSplash,16);
5083
+ }
5084
+ }));
5085
+
5086
+ // load all of the images
5087
+ Promise.all(promises).then(()=>
5048
5088
  {
5049
5089
  // start the engine
5050
5090
  gameInit();
@@ -5122,4 +5162,168 @@ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObject
5122
5162
  for (const o of objects)
5123
5163
  pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
5124
5164
  }
5165
+ }
5166
+
5167
+ ///////////////////////////////////////////////////////////////////////////////
5168
+ // LittleJS splash screen and logo
5169
+
5170
+ function drawEngineSplashScreen(t)
5171
+ {
5172
+ const x = mainContext;
5173
+ const w = mainCanvas.width = innerWidth;
5174
+ const h = mainCanvas.height = innerHeight;
5175
+ {
5176
+ // background
5177
+ const p3 = percent(t, 1, .8);
5178
+ const p4 = percent(t, 0, .5);
5179
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,Math.hypot(w,h)*.7);
5180
+ g.addColorStop(0,hsl(0,0,lerp(p4,0,p3/2),p3));
5181
+ g.addColorStop(1,hsl(0,0,0,p3));
5182
+ x.save();
5183
+ x.fillStyle = g;
5184
+ x.fillRect(0,0,w,h);
5185
+ }
5186
+
5187
+ // draw LittleJS logo...
5188
+
5189
+ const rect = (X, Y, W, H, C)=>
5190
+ {
5191
+ x.beginPath();
5192
+ x.rect(X,Y,W,C?H*p:H);
5193
+ x.fillStyle = C;
5194
+ C ? x.fill() : x.stroke();
5195
+ };
5196
+ const line = (X, Y, Z, W)=>
5197
+ {
5198
+ x.beginPath();
5199
+ x.lineTo(X,Y);
5200
+ x.lineTo(Z,W);
5201
+ x.stroke();
5202
+ };
5203
+ const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
5204
+ {
5205
+ const D = (A+B)/2, E = p*(B-A)/2;
5206
+ x.beginPath();
5207
+ F && x.lineTo(X,Y);
5208
+ x.arc(X,Y,R,D-E,D+E);
5209
+ x.fillStyle = C;
5210
+ C ? x.fill() : x.stroke();
5211
+ };
5212
+ const color = (c=0, l=0) =>
5213
+ hsl([.98,.3,.57,.14][c%4]-10,.8,[0,.3,.5,.8,.9][l]);
5214
+ const alpha = wave(1,1,t);
5215
+ const p = percent(alpha, .1, .5);
5216
+
5217
+ // setup
5218
+ x.translate(w/2,h/2);
5219
+ const size = min(6, min(w,h)/99); // fit to screen
5220
+ x.scale(size,size);
5221
+ x.translate(-40,-35);
5222
+ x.lineJoin = x.lineCap = 'round';
5223
+ x.lineWidth = 1+p;
5224
+
5225
+ // drawing effect
5226
+ const p2 = percent(alpha,.1,1);
5227
+ x.setLineDash([99*p2,99]);
5228
+
5229
+ // cab top
5230
+ rect(7,17,18,-8,color(2,2));
5231
+ rect(7,9,18,4,color(2,3));
5232
+ rect(25,9,8,8,color(2,1));
5233
+ rect(25,9,-18,8);
5234
+ rect(25,9,8,8);
5235
+
5236
+ // cab
5237
+ rect(25,17,7,22,color());
5238
+ rect(11,40,14,-23,color(1,1));
5239
+ rect(11,17,14,17,color(1,2));
5240
+ rect(11,17,14,9,color(1,3));
5241
+ rect(15,31,6,-9,color(2,2));
5242
+ circle(15,23,5,0,PI/2,color(2,4),1);
5243
+ rect(25,17,-14,23);
5244
+ rect(21,22,-6,9);
5245
+
5246
+ // little stack
5247
+ rect(37,14,9,6,color(3,2));
5248
+ rect(37,14,4,6,color(3,3));
5249
+ rect(37,14,9,6);
5250
+
5251
+ // big stack
5252
+ rect(50,20,10,-10,color(0,1));
5253
+ rect(50,20,6,-10,color(0,2));
5254
+ rect(50,20,3,-10,color(0,3));
5255
+ rect(50,10,10,10);
5256
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
5257
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
5258
+ circle(55,2,11.4,.5,PI-.5);
5259
+ rect(45,7,20,-7,color(0,2));
5260
+ rect(45,0,20,3,color(0,3));
5261
+ rect(45,0,20,7);
5262
+
5263
+ // engine
5264
+ for (let i=5; i--;)
5265
+ {
5266
+ // stagger radius to fix slight seam
5267
+ circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
5268
+ circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
5269
+ circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
5270
+ }
5271
+
5272
+ // engine outline
5273
+ circle(36,30,10,PI/2,PI*3/2);
5274
+ circle(47,30,10,PI/2,PI*3/2);
5275
+ circle(60,30,10);
5276
+ line(36,20,60,20);
5277
+
5278
+ // engine front light
5279
+ circle(60,30,4,PI,3*PI,color(3,2));
5280
+ circle(60,30,4,PI,2*PI,color(3,3));
5281
+ circle(60,30,4,PI,3*PI);
5282
+
5283
+ // front brush
5284
+ for (let i=6; i--;)
5285
+ {
5286
+ x.beginPath();
5287
+ x.lineTo(53,54);
5288
+ x.lineTo(53,40);
5289
+ x.lineTo(53+(1+i*2.9)*p,40);
5290
+ x.lineTo(53+(4+i*3.5)*p,54);
5291
+ x.fillStyle = color(0,i%2+2);
5292
+ x.fill() || i%2 && x.stroke();
5293
+ }
5294
+
5295
+ // wheels
5296
+ rect(6,40,5,5);
5297
+ rect(6,40,5,5,color());
5298
+ rect(15,54,38,-14,color());
5299
+ for (let i=3; i--;)
5300
+ for (let j=2; j--;)
5301
+ {
5302
+ circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
5303
+ x.stroke();
5304
+ circle(15*i+15,47,j?7:1,0,PI,color(i,2));
5305
+ x.stroke();
5306
+ }
5307
+ line(6,40,68,40); // center
5308
+ line(77,54,4,54); // bottom
5309
+
5310
+ // draw engine name
5311
+ const s = engineName;
5312
+ x.font = '900 16px arial';
5313
+ x.textAlign = 'center';
5314
+ x.textBaseline = 'top';
5315
+ x.lineWidth = 1+p*3;
5316
+ let w2 = 0;
5317
+ for (let i=0; i<s.length; ++i)
5318
+ w2 += x.measureText(s[i]).width;
5319
+ for (let j=2; j--;)
5320
+ for (let i=0, X=41-w2/2; i<s.length; ++i)
5321
+ {
5322
+ x.fillStyle = color(i,2);
5323
+ const w = x.measureText(s[i]).width;
5324
+ x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
5325
+ X += w;
5326
+ }
5327
+
5328
+ x.restore();
5125
5329
  }