littlejsengine 1.9.7 → 1.9.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 (48) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +13 -8
  3. package/dist/littlejs.d.ts +75 -69
  4. package/dist/littlejs.esm.js +313 -354
  5. package/dist/littlejs.esm.min.js +1 -1
  6. package/dist/littlejs.js +307 -350
  7. package/dist/littlejs.min.js +1 -1
  8. package/dist/littlejs.release.js +244 -319
  9. package/examples/box2d/game.js +137 -0
  10. package/examples/box2d/index.html +12 -0
  11. package/examples/box2d/scenes.js +412 -0
  12. package/examples/box2d/tiles.png +0 -0
  13. package/examples/breakout/game.js +3 -3
  14. package/examples/breakout/index.html +4 -3
  15. package/examples/breakoutTutorial/index.html +2 -2
  16. package/examples/electron/build.js +1 -1
  17. package/examples/electron/index.html +2 -2
  18. package/examples/js13k/build.js +19 -1
  19. package/examples/js13k/index.html +13 -13
  20. package/examples/module/index.html +1 -1
  21. package/examples/particles/index.html +1 -1
  22. package/examples/platformer/gameEffects.js +2 -2
  23. package/examples/platformer/gameLevel.js +0 -1
  24. package/examples/platformer/index.html +8 -8
  25. package/examples/puzzle/index.html +2 -2
  26. package/examples/starter/build.js +1 -1
  27. package/examples/starter/index.html +13 -13
  28. package/examples/stress/index.html +1 -1
  29. package/examples/typescript/index.html +1 -1
  30. package/package.json +1 -1
  31. package/plugins/Box2D_v2.3.1_min.wasm.js +630 -0
  32. package/plugins/Box2D_v2.3.1_min.wasm.wasm +0 -0
  33. package/plugins/box2d.js +879 -0
  34. package/plugins/newgrounds.js +169 -0
  35. package/plugins/postProcess.js +102 -0
  36. package/src/engine.js +48 -29
  37. package/src/engineAudio.js +20 -12
  38. package/src/engineBuild.js +1 -1
  39. package/src/engineDebug.js +68 -33
  40. package/src/engineDraw.js +1 -1
  41. package/src/engineExport.js +6 -4
  42. package/src/engineInput.js +7 -4
  43. package/src/engineMedals.js +40 -171
  44. package/src/engineObject.js +33 -2
  45. package/src/engineRelease.js +5 -2
  46. package/src/engineSettings.js +14 -2
  47. package/src/engineUtilities.js +75 -2
  48. package/src/engineWebGL.js +1 -94
package/dist/littlejs.js CHANGED
@@ -80,6 +80,20 @@ function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
80
80
  debugPrimitives.push({pos, size:vec2(size), color, time:new Timer(time), angle, fill});
81
81
  }
82
82
 
83
+ /** Draw a debug poly in world space
84
+ * @param {Vector2} pos
85
+ * @param {Array} points
86
+ * @param {String} [color]
87
+ * @param {Number} [time]
88
+ * @param {Number} [angle]
89
+ * @param {Boolean} [fill]
90
+ * @memberof Debug */
91
+ function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
92
+ {
93
+ ASSERT(typeof color == 'string', 'pass in css color strings');
94
+ debugPrimitives.push({pos, points, color, time:new Timer(time), angle, fill});
95
+ }
96
+
83
97
  /** Draw a debug circle in world space
84
98
  * @param {Vector2} pos
85
99
  * @param {Number} [radius]
@@ -89,7 +103,7 @@ function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
89
103
  * @memberof Debug */
90
104
  function debugCircle(pos, radius=0, color='#fff', time=0, fill=false)
91
105
  {
92
- ASSERT(typeof color == 'string', 'pass in css color strings');
106
+ ASSERT(typeof color == 'string', 'pass in css color strings');
93
107
  debugPrimitives.push({pos, size:radius, color, time:new Timer(time), angle:0, fill});
94
108
  }
95
109
 
@@ -99,7 +113,11 @@ function debugCircle(pos, radius=0, color='#fff', time=0, fill=false)
99
113
  * @param {Number} [time]
100
114
  * @param {Number} [angle]
101
115
  * @memberof Debug */
102
- function debugPoint(pos, color, time, angle) {debugRect(pos, undefined, color, time, angle);}
116
+ function debugPoint(pos, color, time, angle)
117
+ {
118
+ ASSERT(typeof color == 'string', 'pass in css color strings');
119
+ debugRect(pos, undefined, color, time, angle);
120
+ }
103
121
 
104
122
  /** Draw a debug line in world space
105
123
  * @param {Vector2} posA
@@ -110,19 +128,20 @@ function debugPoint(pos, color, time, angle) {debugRect(pos, undefined, color, t
110
128
  * @memberof Debug */
111
129
  function debugLine(posA, posB, color, thickness=.1, time)
112
130
  {
131
+ ASSERT(typeof color == 'string', 'pass in css color strings');
113
132
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
114
133
  const size = vec2(thickness, halfDelta.length()*2);
115
134
  debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
116
135
  }
117
136
 
118
- /** Draw a debug axis aligned bounding box in world space
137
+ /** Draw a debug combined axis aligned bounding box in world space
119
138
  * @param {Vector2} pA - position A
120
139
  * @param {Vector2} sA - size A
121
140
  * @param {Vector2} pB - position B
122
141
  * @param {Vector2} sB - size B
123
142
  * @param {String} [color]
124
143
  * @memberof Debug */
125
- function debugAABB(pA, sA, pB, sB, color)
144
+ function debugOverlap(pA, sA, pB, sB, color)
126
145
  {
127
146
  const minPos = vec2(min(pA.x - sA.x/2, pB.x - sB.x/2), min(pA.y - sA.y/2, pB.y - sB.y/2));
128
147
  const maxPos = vec2(max(pA.x + sA.x/2, pB.x + sB.x/2), max(pA.y + sA.y/2, pB.y + sB.y/2));
@@ -140,7 +159,7 @@ function debugAABB(pA, sA, pB, sB, color)
140
159
  * @memberof Debug */
141
160
  function debugText(text, pos, size=1, color='#fff', time=0, angle=0, font='monospace')
142
161
  {
143
- ASSERT(typeof color == 'string', 'pass in css color strings');
162
+ ASSERT(typeof color == 'string', 'pass in css color strings');
144
163
  debugPrimitives.push({text, pos, size, color, time:new Timer(time), angle, font});
145
164
  }
146
165
 
@@ -239,7 +258,7 @@ function debugRender()
239
258
  const stickScale = 1;
240
259
  const buttonScale = .2;
241
260
  const centerPos = cameraPos;
242
- const sticks = stickData[i];
261
+ const sticks = gamepadStickData[i];
243
262
  for (let j = sticks.length; j--;)
244
263
  {
245
264
  const drawPos = centerPos.add(vec2(j*stickScale*2, i*stickScale*3));
@@ -259,6 +278,7 @@ function debugRender()
259
278
  }
260
279
  }
261
280
 
281
+ let debugObject;
262
282
  if (debugOverlay)
263
283
  {
264
284
  const saveContext = mainContext;
@@ -269,11 +289,13 @@ function debugRender()
269
289
  debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008');
270
290
 
271
291
  // mouse pick
272
- let bestDistance = Infinity, bestObject;
292
+ let bestDistance = Infinity;
273
293
  for (const o of engineObjects)
274
294
  {
275
295
  if (o.canvas || o.destroyed)
276
296
  continue;
297
+
298
+ o.renderDebugInfo();
277
299
  if (!o.size.x || !o.size.y)
278
300
  continue;
279
301
 
@@ -281,33 +303,15 @@ function debugRender()
281
303
  if (distance < bestDistance)
282
304
  {
283
305
  bestDistance = distance;
284
- bestObject = o;
306
+ debugObject = o;
285
307
  }
286
-
287
- // show object info
288
- const size = vec2(max(o.size.x, .2), max(o.size.y, .2));
289
- const color1 = new Color(o.collideTiles?1:0, o.collideSolidObjects?1:0, o.isSolid?1:0, o.parent?.2:.5);
290
- const color2 = o.parent ? new Color(1,1,1,.5) : new Color(0,0,0,.8);
291
- drawRect(o.pos, size, color1, o.angle, false);
292
- drawRect(o.pos, size.scale(.8), color2, o.angle, false);
293
- o.parent && drawLine(o.pos, o.parent.pos, .1, new Color(0,0,1,.5), false);
294
- }
295
-
296
- if (bestObject)
297
- {
298
- const raycastHitPos = tileCollisionRaycast(bestObject.pos, mousePos);
299
- raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), new Color(0,1,1,.3));
300
- drawRect(mousePos.floor().add(vec2(.5)), vec2(1), new Color(0,0,1,.5), 0, false);
301
- drawLine(mousePos, bestObject.pos, .1, raycastHitPos ? new Color(1,0,0,.5) : new Color(0,1,0,.5), false);
302
-
303
- const debugText = 'mouse pos = ' + mousePos +
304
- '\nmouse collision = ' + getTileCollisionData(mousePos) +
305
- '\n\n--- object info ---\n' +
306
- bestObject.toString();
307
- drawTextScreen(debugText, mousePosScreen, 24, new Color, .05, undefined, 'center', 'monospace');
308
308
  }
309
309
 
310
- glCopyToContext(mainContext = saveContext);
310
+ if (tileCollisionSize.x > 0 && tileCollisionSize.y > 0)
311
+ drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(0,0,1,.5), 0, false);
312
+ mainContext = saveContext;
313
+
314
+ //glCopyToContext(mainContext = saveContext);
311
315
  }
312
316
 
313
317
  {
@@ -322,6 +326,7 @@ function debugRender()
322
326
  const pos = worldToScreen(p.pos);
323
327
  overlayContext.translate(pos.x|0, pos.y|0);
324
328
  overlayContext.rotate(p.angle);
329
+ overlayContext.scale(1, -1);
325
330
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
326
331
 
327
332
  if (p.text != undefined)
@@ -331,7 +336,20 @@ function debugRender()
331
336
  overlayContext.textBaseline = 'middle';
332
337
  overlayContext.fillText(p.text, 0, 0);
333
338
  }
334
- else if (p.size == 0 || p.size.x === 0 && p.size.y === 0 )
339
+ else if (p.points != undefined)
340
+ {
341
+ // poly
342
+ overlayContext.beginPath();
343
+ for (const point of p.points)
344
+ {
345
+ const p2 = point.scale(cameraScale).floor();
346
+ overlayContext.lineTo(p2.x, p2.y);
347
+ }
348
+ overlayContext.closePath();
349
+ p.fill && overlayContext.fill();
350
+ overlayContext.stroke();
351
+ }
352
+ else if (p.size == 0 || p.size.x === 0 && p.size.y === 0)
335
353
  {
336
354
  // point
337
355
  overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
@@ -340,7 +358,8 @@ function debugRender()
340
358
  else if (p.size.x != undefined)
341
359
  {
342
360
  // rect
343
- const w = p.size.x*cameraScale|0, h = p.size.y*cameraScale|0;
361
+ const s = p.size.scale(cameraScale).floor();
362
+ const w = s.x, h = s.y;
344
363
  p.fill && overlayContext.fillRect(-w/2|0, -h/2|0, w, h);
345
364
  overlayContext.strokeRect(-w/2|0, -h/2|0, w, h);
346
365
  }
@@ -359,6 +378,22 @@ function debugRender()
359
378
  // remove expired primitives
360
379
  debugPrimitives = debugPrimitives.filter(r=>r.time<0);
361
380
  }
381
+
382
+ if (debugObject)
383
+ {
384
+ const saveContext = mainContext;
385
+ mainContext = overlayContext;
386
+ const raycastHitPos = tileCollisionRaycast(debugObject.pos, mousePos);
387
+ raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3));
388
+ drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5), false);
389
+
390
+ const debugText = 'mouse pos = ' + mousePos +
391
+ '\nmouse collision = ' + getTileCollisionData(mousePos) +
392
+ '\n\n--- object info ---\n' +
393
+ debugObject.toString();
394
+ drawTextScreen(debugText, mousePosScreen, 24, rgb(), .05, undefined, 'center', 'monospace');
395
+ mainContext = saveContext;
396
+ }
362
397
 
363
398
  {
364
399
  // draw debug overlay
@@ -721,7 +756,7 @@ class RandomGenerator
721
756
  */
722
757
  function vec2(x=0, y)
723
758
  {
724
- return typeof x === 'number' ?
759
+ return typeof x == 'number' ?
725
760
  new Vector2(x, y == undefined? x : y) :
726
761
  new Vector2(x.x, x.y);
727
762
  }
@@ -750,13 +785,19 @@ class Vector2
750
785
  * @param {Number} [y] - Y axis location */
751
786
  constructor(x=0, y=0)
752
787
  {
753
- ASSERT(typeof x === 'number' && typeof y === 'number');
788
+ ASSERT(typeof x == 'number' && typeof y == 'number');
754
789
  /** @property {Number} - X axis location */
755
790
  this.x = x;
756
791
  /** @property {Number} - Y axis location */
757
792
  this.y = y;
758
793
  }
759
794
 
795
+ /** Sets values of this vector and returns self
796
+ * @param {Number} [x] - X axis location
797
+ * @param {Number} [y] - Y axis location
798
+ * @return {Vector2} */
799
+ set(x=0, y=0) { this.x=x; this.y=y; return this; }
800
+
760
801
  /** Returns a new vector that is a copy of this
761
802
  * @return {Vector2} */
762
803
  copy() { return new Vector2(this.x, this.y); }
@@ -1008,6 +1049,15 @@ class Color
1008
1049
  this.a = a;
1009
1050
  }
1010
1051
 
1052
+ /** Sets values of this color and returns self
1053
+ * @param {Number} [r] - red
1054
+ * @param {Number} [g] - green
1055
+ * @param {Number} [b] - blue
1056
+ * @param {Number} [a] - alpha
1057
+ * @return {Color} */
1058
+ set(r=1, g=1, b=1, a=1)
1059
+ { this.r=r; this.g=g; this.b=b; this.a=a; return this; }
1060
+
1011
1061
  /** Returns a new color that is a copy of this
1012
1062
  * @return {Color} */
1013
1063
  copy() { return new Color(this.r, this.g, this.b, this.a); }
@@ -1169,6 +1219,64 @@ class Color
1169
1219
  }
1170
1220
  }
1171
1221
 
1222
+ ///////////////////////////////////////////////////////////////////////////////
1223
+ // default colors
1224
+
1225
+ /** Color - White
1226
+ * @type {Color}
1227
+ * @memberof Utilities */
1228
+ const WHITE = rgb();
1229
+
1230
+ /** Color - Black
1231
+ * @type {Color}
1232
+ * @memberof Utilities */
1233
+ const BLACK = rgb(0,0,0);
1234
+
1235
+ /** Color - Gray
1236
+ * @type {Color}
1237
+ * @memberof Utilities */
1238
+ const GRAY = rgb(.5,.5,.5);
1239
+
1240
+ /** Color - Red
1241
+ * @type {Color}
1242
+ * @memberof Utilities */
1243
+ const RED = rgb(1,0,0);
1244
+
1245
+ /** Color - Orange
1246
+ * @type {Color}
1247
+ * @memberof Utilities */
1248
+ const ORANGE = rgb(1,.5,0);
1249
+
1250
+ /** Color - Yellow
1251
+ * @type {Color}
1252
+ * @memberof Utilities */
1253
+ const YELLOW = rgb(1,1,0);
1254
+
1255
+ /** Color - Green
1256
+ * @type {Color}
1257
+ * @memberof Utilities */
1258
+ const GREEN = rgb(0,1,0);
1259
+
1260
+ /** Color - Cyan
1261
+ * @type {Color}
1262
+ * @memberof Utilities */
1263
+ const CYAN = rgb(0,1,1);
1264
+
1265
+ /** Color - Blue
1266
+ * @type {Color}
1267
+ * @memberof Utilities */
1268
+ const BLUE = rgb(0,0,1);
1269
+
1270
+ /** Color - Purple
1271
+ * @type {Color}
1272
+ * @memberof Utilities */
1273
+ const PURPLE = rgb(.5,0,1);
1274
+
1275
+ /** Color - Magenta
1276
+ * @type {Color}
1277
+ * @memberof Utilities */
1278
+ const MAGENTA = rgb(1,0,1);
1279
+
1172
1280
  ///////////////////////////////////////////////////////////////////////////////
1173
1281
 
1174
1282
  /**
@@ -1249,9 +1357,9 @@ let cameraScale = 32;
1249
1357
 
1250
1358
  /** The max size of the canvas, centered if window is larger
1251
1359
  * @type {Vector2}
1252
- * @default Vector2(1920,1200)
1360
+ * @default Vector2(1920,1080)
1253
1361
  * @memberof Settings */
1254
- let canvasMaxSize = vec2(1920, 1200);
1362
+ let canvasMaxSize = vec2(1920, 1080);
1255
1363
 
1256
1364
  /** Fixed size of the canvas, if enabled canvas size never changes
1257
1365
  * - you may also need to set mainCanvasSize if using screen space coords in startup
@@ -1392,6 +1500,13 @@ let gamepadDirectionEmulateStick = true;
1392
1500
  * @memberof Settings */
1393
1501
  let inputWASDEmulateDirection = true;
1394
1502
 
1503
+ /** True if touch input is enabled for mobile devices
1504
+ * - Touch events will be routed to mouse events
1505
+ * @type {Boolean}
1506
+ * @default
1507
+ * @memberof Settings */
1508
+ let touchInputEnable = true;
1509
+
1395
1510
  /** True if touch gamepad should appear on mobile devices
1396
1511
  * - Supports left analog stick, 4 face buttons and start button (button 9)
1397
1512
  * - Must be set by end of gameInit to be activated
@@ -1607,6 +1722,11 @@ function setGamepadDirectionEmulateStick(enable) { gamepadDirectionEmulateStick
1607
1722
  * @memberof Settings */
1608
1723
  function setInputWASDEmulateDirection(enable) { inputWASDEmulateDirection = enable; }
1609
1724
 
1725
+ /** Set if touch input is allowed
1726
+ * @param {Boolean} enable
1727
+ * @memberof Settings */
1728
+ function setTouchInputEnable(enable) { touchInputEnable = enable; }
1729
+
1610
1730
  /** Set if touch gamepad should appear on mobile devices
1611
1731
  * @param {Boolean} enable
1612
1732
  * @memberof Settings */
@@ -1898,7 +2018,7 @@ class EngineObject
1898
2018
  if (o.mass) // push away if not fixed
1899
2019
  o.velocity = o.velocity.subtract(velocity);
1900
2020
 
1901
- debugOverlay && debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f00');
2021
+ debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
1902
2022
  continue;
1903
2023
  }
1904
2024
 
@@ -1960,7 +2080,7 @@ class EngineObject
1960
2080
  else // bounce if other object is fixed
1961
2081
  this.velocity.x *= -elasticity;
1962
2082
  }
1963
- debugOverlay && debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f0f');
2083
+ debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
1964
2084
  }
1965
2085
  }
1966
2086
  if (this.collideTiles)
@@ -2021,6 +2141,22 @@ class EngineObject
2021
2141
  for (const child of this.children)
2022
2142
  child.destroy(child.parent = 0);
2023
2143
  }
2144
+
2145
+ /** Convert from local space to world space
2146
+ * @param {Vector2} pos - local space point */
2147
+ localToWorld(pos) { return this.pos.add(pos.rotate(-this.angle)); }
2148
+
2149
+ /** Convert from world space to local space
2150
+ * @param {Vector2} pos - world space point */
2151
+ worldToLocal(pos) { return pos.subtract(this.pos).rotate(this.angle); }
2152
+
2153
+ /** Convert from local space to world space for a vector (rotation only)
2154
+ * @param {Vector2} vec - local space vector */
2155
+ localToWorldVector(vec) { return vec.rotate(this.angle); }
2156
+
2157
+ /** Convert from world space to local space for a vector (rotation only)
2158
+ * @param {Vector2} vec - world space vector */
2159
+ worldToLocalVector(vec) { return vec.rotate(-this.angle); }
2024
2160
 
2025
2161
  /** Called to check if a tile collision should be resolved
2026
2162
  * @param {Number} tileData - the value of the tile at the position
@@ -2107,6 +2243,21 @@ class EngineObject
2107
2243
  return text;
2108
2244
  }
2109
2245
  }
2246
+
2247
+ /** Render debug info for this object */
2248
+ renderDebugInfo()
2249
+ {
2250
+ if (debug)
2251
+ {
2252
+ // show object info for debugging
2253
+ const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
2254
+ const color1 = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, this.parent?.2:.5);
2255
+ const color2 = this.parent ? rgb(1,1,1,.5) : rgb(0,0,0,.8);
2256
+ drawRect(this.pos, size, color1, this.angle, false);
2257
+ drawRect(this.pos, size.scale(.8), color2, this.angle, false);
2258
+ this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(0,0,1,.5), false);
2259
+ }
2260
+ }
2110
2261
  }
2111
2262
  /**
2112
2263
  * LittleJS Drawing System
@@ -2430,7 +2581,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
2430
2581
  context.save();
2431
2582
  context.translate(pos.x+.5, pos.y+.5);
2432
2583
  context.rotate(angle);
2433
- context.scale(mirror ? -size.x : size.x, size.y);
2584
+ context.scale(mirror ? -size.x : size.x, -size.y);
2434
2585
  drawFunction(context);
2435
2586
  context.restore();
2436
2587
  }
@@ -2612,6 +2763,7 @@ function toggleFullscreen()
2612
2763
  * - Tracks keyboard down, pressed, and released
2613
2764
  * - Tracks mouse buttons, position, and wheel
2614
2765
  * - Tracks multiple analog gamepads
2766
+ * - Touch input is handled as mouse input
2615
2767
  * - Virtual gamepad for touch devices
2616
2768
  * @namespace Input
2617
2769
  */
@@ -2745,7 +2897,8 @@ function inputUpdate()
2745
2897
  if (headlessMode) return;
2746
2898
 
2747
2899
  // clear input when lost focus (prevent stuck keys)
2748
- isTouchDevice || document.hasFocus() || clearInput();
2900
+ if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
2901
+ clearInput();
2749
2902
 
2750
2903
  // update mouse world space position
2751
2904
  mousePos = screenToWorld(mousePosScreen);
@@ -2817,7 +2970,7 @@ function inputInit()
2817
2970
  oncontextmenu = (e)=> false; // prevent right click menu
2818
2971
 
2819
2972
  // init touch input
2820
- if (isTouchDevice)
2973
+ if (isTouchDevice && touchInputEnable && !headlessMode)
2821
2974
  touchInputInit();
2822
2975
  }
2823
2976
 
@@ -2945,12 +3098,12 @@ function vibrateStop() { vibrate(0); }
2945
3098
 
2946
3099
  /** True if a touch device has been detected
2947
3100
  * @memberof Input */
2948
- const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
3101
+ const isTouchDevice = window.ontouchstart !== undefined;
2949
3102
 
2950
3103
  // touch gamepad internal variables
2951
3104
  let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2952
3105
 
2953
- // try to enable touch mouse
3106
+ // enable touch input mouse passthrough
2954
3107
  function touchInputInit()
2955
3108
  {
2956
3109
  // add non passive touch event listeners
@@ -3059,6 +3212,7 @@ function touchInputInit()
3059
3212
  // render the touch gamepad, called automatically by the engine
3060
3213
  function touchGamepadRender()
3061
3214
  {
3215
+ if (!touchInputEnable || !isTouchDevice || headlessMode) return;
3062
3216
  if (!touchGamepadEnable || !touchGamepadTimer.isSet())
3063
3217
  return;
3064
3218
 
@@ -3226,7 +3380,17 @@ class Sound
3226
3380
 
3227
3381
  // play the sound
3228
3382
  const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
3229
- return this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate);
3383
+ this.gainNode = audioContext.createGain();
3384
+ return this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
3385
+ }
3386
+
3387
+ /** Set the sound volume
3388
+ * @param {Number} [volume] - How much to scale volume by
3389
+ */
3390
+ setVolume(volume=1)
3391
+ {
3392
+ if (this.gainNode)
3393
+ this.gainNode.gain.value = volume;
3230
3394
  }
3231
3395
 
3232
3396
  /** Stop the last instance of this sound that was played */
@@ -3414,15 +3578,16 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
3414
3578
  let audioSuspended = false;
3415
3579
 
3416
3580
  /** Play cached audio samples with given settings
3417
- * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
3418
- * @param {Number} [volume] - How much to scale volume by
3419
- * @param {Number} [rate] - The playback rate to use
3420
- * @param {Number} [pan] - How much to apply stereo panning
3421
- * @param {Boolean} [loop] - True if the sound should loop when it reaches the end
3422
- * @param {Number} [sampleRate=44100] - Sample rate for the sound
3581
+ * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
3582
+ * @param {Number} [volume] - How much to scale volume by
3583
+ * @param {Number} [rate] - The playback rate to use
3584
+ * @param {Number} [pan] - How much to apply stereo panning
3585
+ * @param {Boolean} [loop] - True if the sound should loop when it reaches the end
3586
+ * @param {Number} [sampleRate=44100] - Sample rate for the sound
3587
+ * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
3423
3588
  * @return {AudioBufferSourceNode} - The audio node of the sound played
3424
3589
  * @memberof Audio */
3425
- function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
3590
+ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR, gainNode)
3426
3591
  {
3427
3592
  if (!soundEnable || headlessMode) return;
3428
3593
 
@@ -3448,11 +3613,8 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3448
3613
  source.playbackRate.value = rate;
3449
3614
  source.loop = loop;
3450
3615
 
3451
- // set master gain volume
3452
- setSoundVolume(soundVolume);
3453
-
3454
3616
  // create and connect gain node
3455
- const gainNode = audioContext.createGain();
3617
+ gainNode = gainNode || audioContext.createGain();
3456
3618
  gainNode.gain.value = volume;
3457
3619
  gainNode.connect(audioGainNode);
3458
3620
 
@@ -4433,9 +4595,9 @@ class Particle extends EngineObject
4433
4595
 
4434
4596
 
4435
4597
  /** List of all medals
4436
- * @type {Array}
4598
+ * @type {Object}
4437
4599
  * @memberof Medals */
4438
- const medals = [];
4600
+ const medals = {};
4439
4601
 
4440
4602
  // Engine internal variables not exposed to documentation
4441
4603
  let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
@@ -4451,9 +4613,46 @@ function medalsInit(saveName)
4451
4613
  {
4452
4614
  // check if medals are unlocked
4453
4615
  medalsSaveName = saveName;
4454
- debugMedals || medals.forEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
4616
+ if (!debugMedals)
4617
+ medalsForEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
4618
+
4619
+ // engine automatically renders medals
4620
+ engineAddPlugin(undefined, medalsRender);
4621
+ function medalsRender()
4622
+ {
4623
+ if (!medalsDisplayQueue.length)
4624
+ return;
4625
+
4626
+ // update first medal in queue
4627
+ const medal = medalsDisplayQueue[0];
4628
+ const time = timeReal - medalsDisplayTimeLast;
4629
+ if (!medalsDisplayTimeLast)
4630
+ medalsDisplayTimeLast = timeReal;
4631
+ else if (time > medalDisplayTime)
4632
+ {
4633
+ medalsDisplayTimeLast = 0;
4634
+ medalsDisplayQueue.shift();
4635
+ }
4636
+ else
4637
+ {
4638
+ // slide on/off medals
4639
+ const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
4640
+ const hidePercent =
4641
+ time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
4642
+ time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
4643
+ medal.render(hidePercent);
4644
+ }
4645
+ }
4455
4646
  }
4456
4647
 
4648
+ /** Calls a function for each medal
4649
+ * @param {Function} callback
4650
+ * @memberof Medals */
4651
+ function medalsForEach(callback)
4652
+ { Object.values(medals).forEach(medal=>callback(medal)); }
4653
+
4654
+ ///////////////////////////////////////////////////////////////////////////////
4655
+
4457
4656
  /**
4458
4657
  * Medal - Tracks an unlockable medal
4459
4658
  * @example
@@ -4498,7 +4697,6 @@ class Medal
4498
4697
  ASSERT(medalsSaveName, 'save name must be set');
4499
4698
  localStorage[this.storageKey()] = this.unlocked = 1;
4500
4699
  medalsDisplayQueue.push(this);
4501
- newgrounds && newgrounds.unlockMedal(this.id);
4502
4700
  }
4503
4701
 
4504
4702
  /** Render a medal
@@ -4546,173 +4744,6 @@ class Medal
4546
4744
 
4547
4745
  // Get local storage key used by the medal
4548
4746
  storageKey() { return medalsSaveName + '_' + this.id; }
4549
- }
4550
-
4551
- // engine automatically renders medals
4552
- function medalsRender()
4553
- {
4554
- if (!medalsDisplayQueue.length)
4555
- return;
4556
-
4557
- // update first medal in queue
4558
- const medal = medalsDisplayQueue[0];
4559
- const time = timeReal - medalsDisplayTimeLast;
4560
- if (!medalsDisplayTimeLast)
4561
- medalsDisplayTimeLast = timeReal;
4562
- else if (time > medalDisplayTime)
4563
- {
4564
- medalsDisplayTimeLast = 0;
4565
- medalsDisplayQueue.shift();
4566
- }
4567
- else
4568
- {
4569
- // slide on/off medals
4570
- const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
4571
- const hidePercent =
4572
- time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
4573
- time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
4574
- medal.render(hidePercent);
4575
- }
4576
- }
4577
-
4578
- ///////////////////////////////////////////////////////////////////////////////
4579
-
4580
- // global Newgrounds object
4581
- let newgrounds;
4582
-
4583
- /** This can used to enable Newgrounds functionality
4584
- * @param {Number} app_id - The newgrounds App ID
4585
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
4586
- * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher
4587
- * @memberof Medals */
4588
- function newgroundsInit(app_id, cipher, cryptoJS)
4589
- { newgrounds = new Newgrounds(app_id, cipher, cryptoJS); }
4590
-
4591
- /**
4592
- * Newgrounds API wrapper object
4593
- * @example
4594
- * // create a newgrounds object, replace the app id with your own
4595
- * const app_id = '53123:1ZuSTQ9l';
4596
- * newgrounds = new Newgrounds(app_id);
4597
- */
4598
- class Newgrounds
4599
- {
4600
- /** Create a newgrounds object
4601
- * @param {Number} app_id - The newgrounds App ID
4602
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
4603
- * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher */
4604
- constructor(app_id, cipher, cryptoJS)
4605
- {
4606
- ASSERT(!newgrounds && app_id>0, 'there can only be one newgrounds object');
4607
- ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
4608
-
4609
- this.app_id = app_id;
4610
- this.cipher = cipher;
4611
- this.cryptoJS = cryptoJS;
4612
- this.host = location ? location.hostname : '';
4613
-
4614
- // get session id from url search params
4615
- const url = new URL(location.href);
4616
- this.session_id = url.searchParams.get('ngio_session_id');
4617
-
4618
- if (!this.session_id)
4619
- return; // only use newgrounds when logged in
4620
-
4621
- // get medals
4622
- const medalsResult = this.call('Medal.getList');
4623
- this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
4624
- debugMedals && console.log(this.medals);
4625
- for (const newgroundsMedal of this.medals)
4626
- {
4627
- const medal = medals[newgroundsMedal['id']];
4628
- if (medal)
4629
- {
4630
- // copy newgrounds medal data
4631
- medal.image = new Image;
4632
- medal.image.src = newgroundsMedal['icon'];
4633
- medal.name = newgroundsMedal['name'];
4634
- medal.description = newgroundsMedal['description'];
4635
- medal.unlocked = newgroundsMedal['unlocked'];
4636
- medal.difficulty = newgroundsMedal['difficulty'];
4637
- medal.value = newgroundsMedal['value'];
4638
-
4639
- if (medal.value)
4640
- medal.description = medal.description + ' (' + medal.value + ')';
4641
- }
4642
- }
4643
-
4644
- // get scoreboards
4645
- const scoreboardResult = this.call('ScoreBoard.getBoards');
4646
- this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
4647
- debugMedals && console.log(this.scoreboards);
4648
-
4649
- const keepAliveMS = 5 * 60 * 1e3;
4650
- setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
4651
- }
4652
-
4653
- /** Send message to unlock a medal by id
4654
- * @param {Number} id - The medal id */
4655
- unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
4656
-
4657
- /** Send message to post score
4658
- * @param {Number} id - The scoreboard id
4659
- * @param {Number} value - The score value */
4660
- postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
4661
-
4662
- /** Get scores from a scoreboard
4663
- * @param {Number} id - The scoreboard id
4664
- * @param {String} [user] - A user's id or name
4665
- * @param {Number} [social] - If true, only social scores will be loaded
4666
- * @param {Number} [skip] - Number of scores to skip before start
4667
- * @param {Number} [limit] - Number of scores to include in the list
4668
- * @return {Object} - The response JSON object
4669
- */
4670
- getScores(id, user, social=0, skip=0, limit=10)
4671
- { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
4672
-
4673
- /** Send message to log a view */
4674
- logView() { return this.call('App.logView', {'host':this.host}, true); }
4675
-
4676
- /** Send a message to call a component of the Newgrounds API
4677
- * @param {String} component - Name of the component
4678
- * @param {Object} [parameters] - Parameters to use for call
4679
- * @param {Boolean} [async] - If true, don't wait for response before continuing
4680
- * @return {Object} - The response JSON object
4681
- */
4682
- call(component, parameters, async=false)
4683
- {
4684
- const call = {'component':component, 'parameters':parameters};
4685
- if (this.cipher)
4686
- {
4687
- // encrypt using AES-128 Base64 with cryptoJS
4688
- const cryptoJS = this.cryptoJS;
4689
- const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
4690
- const iv = cryptoJS['lib']['WordArray']['random'](16);
4691
- const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
4692
- call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
4693
- call['parameters'] = 0;
4694
- }
4695
-
4696
- // build the input object
4697
- const input =
4698
- {
4699
- 'app_id': this.app_id,
4700
- 'session_id': this.session_id,
4701
- 'call': call
4702
- };
4703
-
4704
- // build post data
4705
- const formData = new FormData();
4706
- formData.append('input', JSON.stringify(input));
4707
-
4708
- // send post data
4709
- const xmlHttp = new XMLHttpRequest();
4710
- const url = 'https://newgrounds.io/gateway_v3.php';
4711
- xmlHttp.open('POST', url, !debugMedals && async);
4712
- xmlHttp.send(formData);
4713
- debugMedals && console.log(xmlHttp.responseText);
4714
- return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
4715
- }
4716
4747
  }
4717
4748
  /**
4718
4749
  * LittleJS WebGL Interface
@@ -4803,7 +4834,7 @@ function glPreRender()
4803
4834
 
4804
4835
  // clear and set to same size as main canvas
4805
4836
  glContext.viewport(0, 0, glCanvas.width=mainCanvas.width, glCanvas.height=mainCanvas.height);
4806
- //glContext.clear(gl_COLOR_BUFFER_BIT); // auto cleared when size is set
4837
+ glContext.clear(gl_COLOR_BUFFER_BIT);
4807
4838
 
4808
4839
  // set up the shader
4809
4840
  glContext.useProgram(glShader);
@@ -4986,99 +5017,6 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdd
4986
5017
  glInstanceCount++;
4987
5018
  }
4988
5019
 
4989
- ///////////////////////////////////////////////////////////////////////////////
4990
- // post processing - can be enabled to pass other canvases through a final shader
4991
-
4992
- let glPostShader, glPostTexture, glPostIncludeOverlay;
4993
-
4994
- /** Set up a post processing shader
4995
- * @param {String} shaderCode
4996
- * @param {Boolean} includeOverlay
4997
- * @memberof WebGL */
4998
- function glInitPostProcess(shaderCode, includeOverlay=false)
4999
- {
5000
- ASSERT(!glPostShader, 'can only have 1 post effects shader');
5001
- if (headlessMode) return;
5002
- if (!shaderCode) // default shader pass through
5003
- shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
5004
-
5005
- // create the shader
5006
- glPostShader = glCreateProgram(
5007
- '#version 300 es\n' + // specify GLSL ES version
5008
- 'precision highp float;'+ // use highp for better accuracy
5009
- 'in vec2 p;'+ // position
5010
- 'void main(){'+ // shader entry point
5011
- 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
5012
- '}' // end of shader
5013
- ,
5014
- '#version 300 es\n' + // specify GLSL ES version
5015
- 'precision highp float;'+ // use highp for better accuracy
5016
- 'uniform sampler2D iChannel0;'+ // input texture
5017
- 'uniform vec3 iResolution;'+ // size of output texture
5018
- 'uniform float iTime;'+ // time
5019
- 'out vec4 c;'+ // out color
5020
- '\n' + shaderCode + '\n'+ // insert custom shader code
5021
- 'void main(){'+ // shader entry point
5022
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
5023
- 'c.a=1.;'+ // always use full alpha
5024
- '}' // end of shader
5025
- );
5026
-
5027
- // create buffer and texture
5028
- glPostTexture = glCreateTexture(undefined);
5029
- glPostIncludeOverlay = includeOverlay;
5030
-
5031
- // hide the original 2d canvas
5032
- mainCanvas.style.visibility = 'hidden';
5033
- if (glPostIncludeOverlay)
5034
- overlayCanvas.style.visibility = 'hidden';
5035
- }
5036
-
5037
- // Render the post processing shader, called automatically by the engine
5038
- function glRenderPostProcess()
5039
- {
5040
- if (!glPostShader || headlessMode) return;
5041
-
5042
- // prepare to render post process shader
5043
- if (glEnable)
5044
- {
5045
- glFlush(); // clear out the buffer
5046
- mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
5047
- }
5048
- else
5049
- {
5050
- // set the viewport
5051
- glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
5052
- }
5053
-
5054
- // copy overlay canvas so it will be included in post processing
5055
- glPostIncludeOverlay && mainContext.drawImage(overlayCanvas, 0, 0);
5056
-
5057
- // setup shader program to draw one triangle
5058
- glContext.useProgram(glPostShader);
5059
- glContext.bindBuffer(gl_ARRAY_BUFFER, glGeometryBuffer);
5060
- glContext.pixelStorei(gl_UNPACK_FLIP_Y_WEBGL, 1);
5061
- glContext.disable(gl_BLEND);
5062
-
5063
- // set textures, pass in the 2d canvas and gl canvas in separate texture channels
5064
- glContext.activeTexture(gl_TEXTURE0);
5065
- glContext.bindTexture(gl_TEXTURE_2D, glPostTexture);
5066
- glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, mainCanvas);
5067
-
5068
- // set vertex position attribute
5069
- const vertexByteStride = 8;
5070
- const pLocation = glContext.getAttribLocation(glPostShader, 'p');
5071
- glContext.enableVertexAttribArray(pLocation);
5072
- glContext.vertexAttribPointer(pLocation, 2, gl_FLOAT, false, vertexByteStride, 0);
5073
-
5074
- // set uniforms and draw
5075
- const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
5076
- glContext.uniform1i(uniformLocation('iChannel0'), 0);
5077
- glContext.uniform1f(uniformLocation('iTime'), time);
5078
- glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
5079
- glContext.drawArrays(gl_TRIANGLE_STRIP, 0, 4);
5080
- }
5081
-
5082
5020
  ///////////////////////////////////////////////////////////////////////////////
5083
5021
  // store gl constants as integers so their name doesn't use space in minifed
5084
5022
  const
@@ -5143,7 +5081,7 @@ const engineName = 'LittleJS';
5143
5081
  * @type {String}
5144
5082
  * @default
5145
5083
  * @memberof Engine */
5146
- const engineVersion = '1.9.7';
5084
+ const engineVersion = '1.9.9';
5147
5085
 
5148
5086
  /** Frames per second to update
5149
5087
  * @type {Number}
@@ -5197,6 +5135,22 @@ function setPaused(isPaused) { paused = isPaused; }
5197
5135
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
5198
5136
 
5199
5137
  ///////////////////////////////////////////////////////////////////////////////
5138
+ // plugin hooks
5139
+
5140
+ const pluginUpdateList = [], pluginRenderList = [];
5141
+
5142
+ /** Add a new update function for a plugin
5143
+ * @param {Function} [updateFunction]
5144
+ * @param {Function} [renderFunction]
5145
+ * @memberof Engine */
5146
+ function engineAddPlugin(updateFunction, renderFunction)
5147
+ {
5148
+ updateFunction && pluginUpdateList.push(updateFunction);
5149
+ renderFunction && pluginRenderList.push(renderFunction);
5150
+ }
5151
+
5152
+ ///////////////////////////////////////////////////////////////////////////////
5153
+ // Main engine functions
5200
5154
 
5201
5155
  /** Startup LittleJS engine with your callback functions
5202
5156
  * @param {Function} gameInit - Called once after the engine starts up, setup the game
@@ -5272,6 +5226,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5272
5226
  // update game and objects
5273
5227
  inputUpdate();
5274
5228
  gameUpdate();
5229
+ pluginUpdateList.forEach(f=>f());
5275
5230
  engineObjectsUpdate();
5276
5231
 
5277
5232
  // do post update
@@ -5293,8 +5248,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5293
5248
  for (const o of engineObjects)
5294
5249
  o.destroyed || o.render();
5295
5250
  gameRenderPost();
5296
- glRenderPostProcess();
5297
- medalsRender();
5251
+ pluginRenderList.forEach(f=>f());
5298
5252
  touchGamepadRender();
5299
5253
  debugRender();
5300
5254
  glCopyToContext(mainContext);
@@ -5366,10 +5320,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5366
5320
  const styleBody =
5367
5321
  'margin:0;overflow:hidden;' + // fill the window
5368
5322
  'background:#000;' + // set background color
5369
- 'touch-action:none;' + // prevent mobile pinch to resize
5370
- 'user-select:none;' + // prevent mobile hold to select
5323
+ 'user-select:none;' + // prevent hold to select
5371
5324
  '-webkit-user-select:none;' + // compatibility for ios
5372
- '-webkit-touch-callout:none'; // compatibility for ios
5325
+ (!touchInputEnable ? '' : // no touch css setttings
5326
+ 'touch-action:none;' + // prevent mobile pinch to resize
5327
+ '-webkit-touch-callout:none');// compatibility for ios
5373
5328
  document.body.style.cssText = styleBody;
5374
5329
  document.body.appendChild(mainCanvas = document.createElement('canvas'));
5375
5330
  mainContext = mainCanvas.getContext('2d');
@@ -5584,21 +5539,23 @@ function drawEngineSplashScreen(t)
5584
5539
  x.setLineDash([99*p2,99]);
5585
5540
 
5586
5541
  // cab top
5587
- rect(7,17,18,-8,color(2,2));
5588
- rect(7,9,18,4,color(2,3));
5589
- rect(25,9,8,8,color(2,1));
5590
- rect(25,9,-18,8);
5591
- rect(25,9,8,8);
5542
+ rect(7,16,18,-8,color(2,2));
5543
+ rect(7,8,18,4,color(2,3));
5544
+ rect(25,8,8,8,color(2,1));
5545
+ rect(25,8,-18,8);
5546
+ rect(25,8,8,8);
5592
5547
 
5593
5548
  // cab
5594
- rect(25,17,7,22,color());
5595
- rect(11,40,14,-23,color(1,1));
5596
- rect(11,17,14,17,color(1,2));
5597
- rect(11,17,14,9,color(1,3));
5598
- rect(15,31,6,-9,color(2,2));
5599
- circle(15,23,5,0,PI/2,color(2,4),1);
5600
- rect(25,17,-14,23);
5601
- rect(21,22,-6,9);
5549
+ rect(25,16,7,23,color());
5550
+ rect(11,39,14,-23,color(1,1));
5551
+ rect(11,16,14,18,color(1,2));
5552
+ rect(11,16,14,8,color(1,3));
5553
+ rect(25,16,-14,24);
5554
+
5555
+ // cab window
5556
+ rect(15,29,6,-9,color(2,2));
5557
+ circle(15,21,5,0,PI/2,color(2,4),1);
5558
+ rect(21,21,-6,9);
5602
5559
 
5603
5560
  // little stack
5604
5561
  rect(37,14,9,6,color(3,2));
@@ -5606,16 +5563,16 @@ function drawEngineSplashScreen(t)
5606
5563
  rect(37,14,9,6);
5607
5564
 
5608
5565
  // big stack
5609
- rect(50,20,10,-8,color(0,1))
5610
- rect(50,20,6.5,-8,color(0,2))
5611
- rect(50,20,3.5,-8,color(0,3))
5612
- rect(50,20,10,-8)
5613
- circle(55,2,11.4,.5,PI-.5,color(3,3))
5614
- circle(55,2,11.4,.5,PI/2,color(3,2),1)
5615
- circle(55,2,11.4,.5,PI-.5)
5616
- rect(45,7,20,-7,color(0,2))
5617
- rect(45,-1,20,4,color(0,3))
5618
- rect(45,-1,20,8)
5566
+ rect(50,20,10,-8,color(0,1));
5567
+ rect(50,20,6.5,-8,color(0,2));
5568
+ rect(50,20,3.5,-8,color(0,3));
5569
+ rect(50,20,10,-8);
5570
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
5571
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
5572
+ circle(55,2,11.4,.5,PI-.5);
5573
+ rect(45,7,20,-7,color(0,2));
5574
+ rect(45,-1,20,4,color(0,3));
5575
+ rect(45,-1,20,8);
5619
5576
 
5620
5577
  // engine
5621
5578
  for (let i=5; i--;)