littlejsengine 1.8.5 → 1.8.8

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 (40) hide show
  1. package/README.md +13 -3
  2. package/build/littlejs.d.ts +22 -15
  3. package/build/littlejs.esm.js +255 -55
  4. package/build/littlejs.esm.min.js +1 -1
  5. package/build/littlejs.js +253 -55
  6. package/build/littlejs.min.js +1 -1
  7. package/build/littlejs.release.js +253 -55
  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/gamePlayer.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 +3 -0
  24. package/examples/starter/index.html +13 -13
  25. package/examples/stress/index.html +3 -1
  26. package/examples/typescript/game.js +3 -1
  27. package/examples/typescript/game.ts +3 -0
  28. package/examples/typescript/index.html +1 -1
  29. package/package.json +1 -1
  30. package/src/engine.js +186 -9
  31. package/src/engineAudio.js +1 -1
  32. package/src/engineDraw.js +1 -1
  33. package/src/engineExport.js +2 -0
  34. package/src/engineInput.js +24 -14
  35. package/src/engineObject.js +1 -1
  36. package/src/engineParticles.js +1 -1
  37. package/src/engineSettings.js +11 -0
  38. package/src/engineTileLayer.js +8 -8
  39. package/src/engineUtilities.js +5 -6
  40. package/src/engineWebGL.js +15 -14
package/build/littlejs.js CHANGED
@@ -819,7 +819,7 @@ class Vector2
819
819
  arrayCheck(arraySize) { return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; }
820
820
 
821
821
  /** Returns this vector expressed as a string
822
- * @param {float} digits - precision to display
822
+ * @param {Number} digits - precision to display
823
823
  * @return {String} */
824
824
  toString(digits=3)
825
825
  { if (debug) { return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`; }}
@@ -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 */
@@ -1599,7 +1609,7 @@ class EngineObject
1599
1609
  // init other internal object stuff
1600
1610
  this.spawnTime = time;
1601
1611
  this.children = [];
1602
- this.collideTiles = 1;
1612
+ this.collideTiles = false;
1603
1613
 
1604
1614
  // add to list of objects
1605
1615
  engineObjects.push(this);
@@ -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
  ///////////////////////////////////////////////////////////////////////////////
@@ -2547,23 +2557,33 @@ function inputUpdatePost()
2547
2557
  ///////////////////////////////////////////////////////////////////////////////
2548
2558
  // Keyboard event handlers
2549
2559
 
2550
- onkeydown = (e)=>
2551
2560
  {
2552
- if (debug && e.target != document.body) return;
2553
- e.repeat || (inputData[isUsingGamepad = 0][remapKey(e.which)] = 3);
2554
- preventDefaultInput && e.preventDefault();
2555
- }
2561
+ onkeydown = (e)=>
2562
+ {
2563
+ if (debug && e.target != document.body) return;
2564
+ if (!e.repeat)
2565
+ {
2566
+ inputData[isUsingGamepad = 0][e.which] = 3;
2567
+ if (inputWASDEmulateDirection)
2568
+ inputData[0][remapKey(e.which)] = 3;
2569
+ }
2570
+ preventDefaultInput && e.preventDefault();
2571
+ }
2556
2572
 
2557
- onkeyup = (e)=>
2558
- {
2559
- if (debug && e.target != document.body) return;
2560
- inputData[0][remapKey(e.which)] = 4;
2561
- }
2573
+ onkeyup = (e)=>
2574
+ {
2575
+ if (debug && e.target != document.body) return;
2576
+ inputData[0][e.which] = 4;
2577
+ if (inputWASDEmulateDirection)
2578
+ inputData[0][remapKey(e.which)] = 4;
2579
+ }
2562
2580
 
2563
- function remapKey(c)
2564
- {
2565
- return inputWASDEmulateDirection ?
2566
- c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
2581
+ // handle remapping wasd keys to directions
2582
+ function remapKey(c)
2583
+ {
2584
+ return inputWASDEmulateDirection ?
2585
+ c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
2586
+ }
2567
2587
  }
2568
2588
 
2569
2589
  ///////////////////////////////////////////////////////////////////////////////
@@ -3062,7 +3082,7 @@ class Music extends Sound
3062
3082
  * @param {Boolean} [loop=1] - True if the music should loop
3063
3083
  * @return {AudioBufferSourceNode} - The audio source node
3064
3084
  */
3065
- play(volume, loop = 1)
3085
+ playMusic(volume, loop = 1)
3066
3086
  { return super.play(0, volume, 1, 1, loop); }
3067
3087
  }
3068
3088
 
@@ -3709,10 +3729,10 @@ constructor(pos, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderO
3709
3729
  /** Draw directly to the 2D canvas in world space (bipass webgl)
3710
3730
  * @param {Vector2} pos
3711
3731
  * @param {Vector2} size
3712
- * @param {Number} [angle=0]
3713
- * @param {Boolean} [mirror=0]
3732
+ * @param {Number} angle
3733
+ * @param {Boolean} mirror
3714
3734
  * @param {Function} drawFunction */
3715
- drawCanvas2D(pos, size, angle=0, mirror, drawFunction)
3735
+ drawCanvas2D(pos, size, angle, mirror, drawFunction)
3716
3736
  {
3717
3737
  const context = this.context;
3718
3738
  context.save();
@@ -3726,12 +3746,12 @@ constructor(pos, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderO
3726
3746
  }
3727
3747
 
3728
3748
  /** Draw a tile directly onto the layer canvas
3729
- * @param {Vector2} pos
3730
- * @param {Vector2} [size=Vector2(1,1)]
3749
+ * @param {Vector2} pos
3750
+ * @param {Vector2} [size=Vector2(1,1)]
3731
3751
  * @param {TileInfo} [tileInfo]
3732
- * @param {Color} [color=Color()]
3733
- * @param {Number} [angle=0]
3734
- * @param {Boolean} [mirror=0] */
3752
+ * @param {Color} [color=Color()]
3753
+ * @param {Number} [angle=0]
3754
+ * @param {Boolean} [mirror=0] */
3735
3755
  drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle, mirror)
3736
3756
  {
3737
3757
  this.drawCanvas2D(pos, size, angle, mirror, (context)=>
@@ -3894,7 +3914,7 @@ class ParticleEmitter extends EngineObject
3894
3914
  this.fadeRate = fadeRate;
3895
3915
  /** @property {Number} - Apply extra randomness percent */
3896
3916
  this.randomness = randomness;
3897
- /** @property {Number} - Do particles collide against tiles */
3917
+ /** @property {Boolean} - Do particles collide against tiles */
3898
3918
  this.collideTiles = collideTiles;
3899
3919
  /** @property {Number} - Should particles use addtive blend */
3900
3920
  this.additive = additive;
@@ -4382,7 +4402,7 @@ let glCanvas;
4382
4402
  let glContext;
4383
4403
 
4384
4404
  // WebGL internal variables not exposed to documentation
4385
- let glActiveTexture, glShader, glArrayBuffer, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
4405
+ let glActiveTexture, glShader, glArrayBuffer, glVertexData, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
4386
4406
 
4387
4407
  ///////////////////////////////////////////////////////////////////////////////
4388
4408
 
@@ -4419,10 +4439,10 @@ function glInit()
4419
4439
  );
4420
4440
 
4421
4441
  // init buffers
4422
- const vertexData = new ArrayBuffer(gl_VERTEX_BUFFER_SIZE);
4442
+ glVertexData = new ArrayBuffer(gl_VERTEX_BUFFER_SIZE);
4443
+ glPositionData = new Float32Array(glVertexData);
4444
+ glColorData = new Uint32Array(glVertexData);
4423
4445
  glArrayBuffer = glContext.createBuffer();
4424
- glPositionData = new Float32Array(vertexData);
4425
- glColorData = new Uint32Array(vertexData);
4426
4446
  glBatchCount = 0;
4427
4447
  }
4428
4448
 
@@ -4450,7 +4470,7 @@ function glPreRender()
4450
4470
  glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
4451
4471
  offset += size*typeSize;
4452
4472
  }
4453
- initVertexAttribArray('p', gl_FLOAT, 4, 4); // position & texture coords
4473
+ initVertexAttribArray('p', gl_FLOAT, 4, 4); // position & texture
4454
4474
  initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
4455
4475
  initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
4456
4476
 
@@ -4520,7 +4540,7 @@ function glCreateProgram(vsSource, fsSource)
4520
4540
  return program;
4521
4541
  }
4522
4542
 
4523
- /** Create WebGL texture from an image and set the texture settings
4543
+ /** Create WebGL texture from an image and init the texture settings
4524
4544
  * @param {Image} image
4525
4545
  * @return {WebGLTexture}
4526
4546
  * @memberof WebGL */
@@ -4529,7 +4549,8 @@ function glCreateTexture(image)
4529
4549
  // build the texture
4530
4550
  const texture = glContext.createTexture();
4531
4551
  glContext.bindTexture(gl_TEXTURE_2D, texture);
4532
- image && image.width && glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
4552
+ if (image)
4553
+ glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
4533
4554
 
4534
4555
  // use point filtering for pixelated rendering
4535
4556
  const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
@@ -4537,6 +4558,7 @@ function glCreateTexture(image)
4537
4558
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
4538
4559
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE);
4539
4560
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE);
4561
+
4540
4562
  return texture;
4541
4563
  }
4542
4564
 
@@ -4551,8 +4573,7 @@ function glFlush()
4551
4573
  glContext.enable(gl_BLEND);
4552
4574
 
4553
4575
  // draw all the sprites in the batch and reset the buffer
4554
- glContext.bufferSubData(gl_ARRAY_BUFFER, 0,
4555
- glPositionData.subarray(0, glBatchCount * gl_INDICIES_PER_VERT));
4576
+ glContext.bufferSubData(gl_ARRAY_BUFFER, 0, glVertexData);
4556
4577
  glContext.drawArrays(gl_TRIANGLE_STRIP, 0, glBatchCount);
4557
4578
  glBatchCount = 0;
4558
4579
  glBatchAdditive = glAdditive;
@@ -4607,11 +4628,11 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdd
4607
4628
  // setup 2 triangle strip quad
4608
4629
  for(let i = vertCount, offset = glBatchCount * gl_INDICIES_PER_VERT; i--;)
4609
4630
  {
4610
- let j = clamp(i-1, 0, 3)*4; // degenerate tri at ends
4611
- glPositionData[offset++] = positionData[j++];
4612
- glPositionData[offset++] = positionData[j++];
4613
- glPositionData[offset++] = positionData[j++];
4614
- glPositionData[offset++] = positionData[j++];
4631
+ const j = clamp(i-1, 0, 3)*4; // degenerate tri at ends
4632
+ glPositionData[offset++] = positionData[j+0];
4633
+ glPositionData[offset++] = positionData[j+1];
4634
+ glPositionData[offset++] = positionData[j+2];
4635
+ glPositionData[offset++] = positionData[j+3];
4615
4636
  glColorData[offset++] = rgba;
4616
4637
  glColorData[offset++] = rgbaAdditive;
4617
4638
  }
@@ -4812,7 +4833,7 @@ const engineName = 'LittleJS';
4812
4833
  * @type {String}
4813
4834
  * @default
4814
4835
  * @memberof Engine */
4815
- const engineVersion = '1.8.5';
4836
+ const engineVersion = '1.8.8';
4816
4837
 
4817
4838
  /** Frames per second to update objects
4818
4839
  * @type {Number}
@@ -4995,7 +5016,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4995
5016
  }
4996
5017
 
4997
5018
  // setup html
4998
- const styleBody =
5019
+ const styleBody =
4999
5020
  'margin:0;overflow:hidden;' + // fill the window
5000
5021
  'background:#000;' + // set background color
5001
5022
  'touch-action:none;' + // prevent mobile pinch to resize
@@ -5016,14 +5037,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5016
5037
 
5017
5038
  // set canvas style
5018
5039
  const styleCanvas =
5019
- 'position:absolute;' + // position
5020
- 'top:50%;left:50%;transform:translate(-50%,-50%);' + // center
5021
- (canvasPixelated?'image-rendering:pixelated':''); // pixelated rendering
5040
+ 'position:absolute;' + // position
5041
+ 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center
5022
5042
  (glCanvas||mainCanvas).style = mainCanvas.style = overlayCanvas.style = styleCanvas;
5023
5043
 
5024
- // load all of the images
5025
- Promise.all(imageSources.map((src, textureIndex)=>
5026
- new Promise((resolve, reject)=>
5044
+ // create promises for loading images
5045
+ const promises = imageSources.map((src, textureIndex)=>
5046
+ new Promise(resolve =>
5027
5047
  {
5028
5048
  const image = new Image;
5029
5049
  image.onerror = image.onload = ()=>
@@ -5033,7 +5053,24 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5033
5053
  }
5034
5054
  image.src = src;
5035
5055
  })
5036
- )).then(()=>
5056
+ )
5057
+
5058
+ // draw splash screen
5059
+ showSplashScreen && promises.push(new Promise(resolve =>
5060
+ {
5061
+ let t = 0;
5062
+ console.log(`LittleJS Engine v${engineVersion}`);
5063
+ updateSplash();
5064
+ function updateSplash()
5065
+ {
5066
+ clearInput();
5067
+ drawEngineSplashScreen(t+=.01);
5068
+ t>1 ? resolve() : setTimeout(updateSplash,16);
5069
+ }
5070
+ }));
5071
+
5072
+ // load all of the images
5073
+ Promise.all(promises).then(()=>
5037
5074
  {
5038
5075
  // start the engine
5039
5076
  gameInit();
@@ -5111,4 +5148,165 @@ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObject
5111
5148
  for (const o of objects)
5112
5149
  pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
5113
5150
  }
5151
+ }
5152
+
5153
+ ///////////////////////////////////////////////////////////////////////////////
5154
+ // LittleJS splash screen and logo
5155
+
5156
+ function drawEngineSplashScreen(t)
5157
+ {
5158
+ // background
5159
+ const grayscale = 0;
5160
+ const x = mainContext;
5161
+ const w = mainCanvas.width = innerWidth;
5162
+ const h = mainCanvas.height = innerHeight;
5163
+ const p3 = percent(t, 1, .8);
5164
+ const p4 = percent(t, 0, .5);
5165
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,Math.hypot(w,h)*.7);
5166
+ g.addColorStop(0,hsl(0,0,lerp(p4,0,p3/2),p3));
5167
+ g.addColorStop(1,hsl(0,0,0,p3));
5168
+ x.save();
5169
+ x.fillStyle = g;
5170
+ x.fillRect(0,0,w,h);
5171
+
5172
+ // logo - fade in and out
5173
+ const rect = (X, Y, W, H, C)=>
5174
+ {
5175
+ x.beginPath();
5176
+ x.rect(X,Y,W,C?H*p:H);
5177
+ x.fillStyle = C;
5178
+ C ? x.fill() : x.stroke();
5179
+ };
5180
+ const line = (X, Y, Z, W)=>
5181
+ {
5182
+ x.beginPath();
5183
+ x.lineTo(X,Y);
5184
+ x.lineTo(Z,W);
5185
+ x.stroke();
5186
+ };
5187
+ const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
5188
+ {
5189
+ const D = (A+B)/2, E = p*(B-A)/2;
5190
+ x.beginPath();
5191
+ F && x.lineTo(X,Y);
5192
+ x.arc(X,Y,R,D-E,D+E);
5193
+ x.fillStyle = C;
5194
+ C ? x.fill() : x.stroke();
5195
+ };
5196
+ const color = (c=0, l=0) =>
5197
+ hsl([.98,.3,.57,.14][c%4]-10,grayscale?0:.8,[0,.3,.5,.8,.9][l]);
5198
+ const alpha = wave(1,1,t);
5199
+ const p = percent(alpha, .1, .5);
5200
+
5201
+ // setup
5202
+ x.translate(w/2,h/2);
5203
+ const size = min(6, min(w,h)/99); // fit to screen
5204
+ x.scale(size,size);
5205
+ x.translate(-40,-35);
5206
+ x.lineJoin = x.lineCap = 'round';
5207
+ x.lineWidth = 1+p;
5208
+
5209
+ // drawing effect
5210
+ const p2 = percent(alpha,.1,1);
5211
+ x.setLineDash([99*p2,99]);
5212
+
5213
+ // cab top
5214
+ rect(7,17,18,-8,color(2,2));
5215
+ rect(7,9,18,4,color(2,3));
5216
+ rect(25,9,8,8,color(2,1));
5217
+ rect(25,9,-18,8);
5218
+ rect(25,9,8,8);
5219
+
5220
+ // cab
5221
+ rect(25,17,7,22,color());
5222
+ rect(11,40,14,-23,color(1,1));
5223
+ rect(11,17,14,17,color(1,2));
5224
+ rect(11,17,14,9,color(1,3));
5225
+ rect(15,31,6,-9,color(2,2));
5226
+ circle(15,23,5,0,PI/2,color(2,4),1);
5227
+ rect(25,17,-14,23);
5228
+ rect(21,22,-6,9);
5229
+
5230
+ // little stack
5231
+ rect(37,14,9,6,color(3,2));
5232
+ rect(37,14,4,6,color(3,3));
5233
+ rect(37,14,9,6);
5234
+
5235
+ // big stack
5236
+ rect(50,20,10,-10,color(0,1));
5237
+ rect(50,20,6,-10,color(0,2));
5238
+ rect(50,20,3,-10,color(0,3));
5239
+ rect(50,10,10,10);
5240
+ circle(55,2,11,.5,PI-.5,color(3,3));
5241
+ circle(55,2,11,.5,PI/2,color(3,2),1);
5242
+ circle(55,2,11,.5,PI-.5);
5243
+ rect(45,7,20,-7,color(0,2));
5244
+ rect(45,0,20,3,color(0,3));
5245
+ rect(45,0,20,7);
5246
+
5247
+ // engine
5248
+ for (let i=5; i--;)
5249
+ {
5250
+ // stagger radius to fix slight seam
5251
+ circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
5252
+ circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
5253
+ circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
5254
+ }
5255
+
5256
+ // engine outline
5257
+ circle(36,30,10,PI/2,PI*3/2);
5258
+ circle(47,30,10,PI/2,PI*3/2);
5259
+ circle(60,30,10);
5260
+ line(36,20,60,20);
5261
+
5262
+ // engine front light
5263
+ circle(60,30,4,PI,3*PI,color(3,2));
5264
+ circle(60,30,4,PI,2*PI,color(3,3));
5265
+ circle(60,30,4,PI,3*PI);
5266
+
5267
+ // front brush
5268
+ for (let i=6; i--;)
5269
+ {
5270
+ x.beginPath();
5271
+ x.lineTo(53,54);
5272
+ x.lineTo(53,40);
5273
+ x.lineTo(53+(1+i*2.9)*p,40);
5274
+ x.lineTo(53+(4+i*3.5)*p,54);
5275
+ x.fillStyle = color(0,i%2+2);
5276
+ x.fill() || i%2 && x.stroke();
5277
+ }
5278
+
5279
+ // wheels
5280
+ rect(5,40,9,6,color());
5281
+ rect(15,54,38,-14,color())
5282
+ for (let i=3; i--;)
5283
+ for (let j=2; j--;)
5284
+ {
5285
+ circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
5286
+ x.stroke();
5287
+ circle(15*i+15,47,j?7:1,0,PI,color(i,2));
5288
+ x.stroke();
5289
+ }
5290
+ line(6,40,68,40) // center
5291
+ line(77,54,4,54) // bottom
5292
+
5293
+ // text
5294
+ const s = engineName;
5295
+ x.font = '900 16px arial';
5296
+ x.textAlign = 'center';
5297
+ x.textBaseline = 'top';
5298
+ x.lineWidth = 1+p*3
5299
+ let w2 = 0;
5300
+ for (let i=0; i<s.length; ++i)
5301
+ w2 += x.measureText(s[i]).width;
5302
+ for (let j=2; j--;)
5303
+ for (let i=0, X=41-w2/2; i<s.length; ++i)
5304
+ {
5305
+ x.fillStyle = color(i,grayscale?3:2);
5306
+ const w = x.measureText(s[i]).width;
5307
+ x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
5308
+ X += w;
5309
+ }
5310
+
5311
+ x.restore();
5114
5312
  }