littlejsengine 1.11.10 → 1.11.17

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 (50) hide show
  1. package/dist/littlejs.d.ts +1311 -81
  2. package/dist/littlejs.esm.js +3135 -299
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +291 -293
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +193 -288
  7. package/examples/box2d/game.js +7 -10
  8. package/examples/box2d/gameObjects.js +33 -33
  9. package/examples/box2d/index.html +6 -6
  10. package/examples/box2d/scenes.js +28 -28
  11. package/examples/breakout/game.js +1 -4
  12. package/examples/breakout/index.html +4 -4
  13. package/examples/breakoutTutorial/index.html +2 -2
  14. package/examples/htmlMenu/game.js +14 -7
  15. package/examples/htmlMenu/index.html +3 -12
  16. package/examples/module/index.html +1 -1
  17. package/examples/particles/index.html +1 -1
  18. package/examples/platformer/game.js +0 -3
  19. package/examples/platformer/index.html +8 -8
  20. package/examples/puzzle/game.js +0 -3
  21. package/examples/puzzle/index.html +2 -2
  22. package/examples/shorts/base.html +1 -1
  23. package/examples/starter/build.js +15 -5
  24. package/examples/starter/index.html +13 -13
  25. package/examples/stress/index.html +3 -2
  26. package/examples/uiSystem/game.js +1 -7
  27. package/examples/uiSystem/index.html +3 -3
  28. package/package.json +3 -3
  29. package/plugins/box2d.js +1552 -640
  30. package/plugins/{Box2D_v2.3.1_min.wasm.js → box2d.wasm.js} +1 -1
  31. package/plugins/newgrounds.js +13 -9
  32. package/plugins/pluginExport.js +51 -0
  33. package/plugins/postProcess.js +94 -93
  34. package/plugins/uiSystem.js +145 -161
  35. package/plugins/zzfxm.js +163 -0
  36. package/src/engine.js +15 -20
  37. package/src/engineAudio.js +74 -204
  38. package/src/engineBuild.js +21 -3
  39. package/src/engineDebug.js +104 -6
  40. package/src/engineDraw.js +27 -14
  41. package/src/engineExport.js +12 -6
  42. package/src/engineInput.js +10 -5
  43. package/src/engineMedals.js +20 -10
  44. package/src/engineObject.js +3 -1
  45. package/src/engineParticles.js +6 -1
  46. package/src/engineRelease.js +6 -1
  47. package/src/engineSettings.js +7 -14
  48. package/src/engineUtilities.js +5 -11
  49. package/src/engineWebGL.js +19 -7
  50. /package/plugins/{Box2D_v2.3.1_min.wasm.wasm → box2d.wasm.wasm} +0 -0
@@ -37,7 +37,12 @@ function debugClear (){}
37
37
  function debugScreenshot (){}
38
38
  function debugSaveCanvas (){}
39
39
  function debugSaveText (){}
40
- function debugSaveDataURL(){}
40
+ function debugSaveDataURL(){}
41
+ function debugShowErrors(){}
42
+ function debugVideoCaptureIsActive(){ return false; }
43
+ function debugVideoCaptureStart (){}
44
+ function debugVideoCaptureStop (){}
45
+ function debugVideoCaptureUpdate(){}
41
46
  /**
42
47
  * LittleJS Utility Classes and Functions
43
48
  * - General purpose math library
@@ -131,7 +136,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
131
136
  * @returns {number}
132
137
  * @memberof Utilities */
133
138
  function lerpWrap(percent, valueA, valueB, wrapSize=1)
134
- { return valueB + clamp(percent) * distanceWrap(valueA, valueB, wrapSize); }
139
+ { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
135
140
 
136
141
  /** Returns signed wrapped distance between the two angles passed in
137
142
  * @param {number} angleA
@@ -329,23 +334,17 @@ class RandomGenerator
329
334
  ///////////////////////////////////////////////////////////////////////////////
330
335
 
331
336
  /**
332
- * Create a 2d vector, can take another Vector2 to copy, 2 scalars, or 1 scalar
333
- * @param {Vector2|number} [x]
334
- * @param {number} [y]
337
+ * Create a 2d vector, can take 1 or 2 scalar values
338
+ * @param {number} [x]
339
+ * @param {number} [y] - if y is undefined, x is used for both
335
340
  * @return {Vector2}
336
341
  * @example
337
342
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
338
- * let b = vec2(a); // copy a into b
339
343
  * a = vec2(5); // set a to (5, 5)
340
344
  * b = vec2(); // set b to (0, 0)
341
345
  * @memberof Utilities
342
346
  */
343
- function vec2(x=0, y)
344
- {
345
- return typeof x == 'number' ?
346
- new Vector2(x, y == undefined? x : y) :
347
- new Vector2(x.x, x.y);
348
- }
347
+ function vec2(x=0, y) { return new Vector2(x, y == undefined? x : y); }
349
348
 
350
349
  /**
351
350
  * Check if object is a valid Vector2
@@ -1030,6 +1029,7 @@ let fontDefault = 'arial';
1030
1029
  let showSplashScreen = false;
1031
1030
 
1032
1031
  /** Disables all rendering, audio, and input for servers
1032
+ * - Must be set before startup to take effect
1033
1033
  * @type {boolean}
1034
1034
  * @default
1035
1035
  * @memberof Settings */
@@ -1039,12 +1039,14 @@ let headlessMode = false;
1039
1039
  // WebGL settings
1040
1040
 
1041
1041
  /** Enable webgl rendering, webgl can be disabled and removed from build (with some features disabled)
1042
+ * - Must be set before startup to take effect
1042
1043
  * @type {boolean}
1043
1044
  * @default
1044
1045
  * @memberof Settings */
1045
1046
  let glEnable = true;
1046
1047
 
1047
1048
  /** Fixes slow rendering in some browsers by not compositing the WebGL canvas
1049
+ * - Must be set before startup to take effect
1048
1050
  * @type {boolean}
1049
1051
  * @default
1050
1052
  * @memberof Settings */
@@ -1145,6 +1147,7 @@ let inputWASDEmulateDirection = true;
1145
1147
 
1146
1148
  /** True if touch input is enabled for mobile devices
1147
1149
  * - Touch events will be routed to mouse events
1150
+ * - Must be set before startup to take effect
1148
1151
  * @type {boolean}
1149
1152
  * @default
1150
1153
  * @memberof Settings */
@@ -1152,7 +1155,7 @@ let touchInputEnable = true;
1152
1155
 
1153
1156
  /** True if touch gamepad should appear on mobile devices
1154
1157
  * - Supports left analog stick, 4 face buttons and start button (button 9)
1155
- * - Must be set by end of gameInit to be activated
1158
+ * - Must be set before startup to take effect
1156
1159
  * @type {boolean}
1157
1160
  * @default
1158
1161
  * @memberof Settings */
@@ -1230,12 +1233,6 @@ let medalDisplaySlideTime = .5;
1230
1233
  * @memberof Settings */
1231
1234
  let medalDisplaySize = vec2(640, 80);
1232
1235
 
1233
- /** Size of icon in medal display
1234
- * @type {number}
1235
- * @default
1236
- * @memberof Settings */
1237
- let medalDisplayIconSize = 50;
1238
-
1239
1236
  /** Set to stop medals from being unlockable (like if cheats are enabled)
1240
1237
  * @type {boolean}
1241
1238
  * @default
@@ -1411,8 +1408,8 @@ function setSoundEnable(enable) { soundEnable = enable; }
1411
1408
  function setSoundVolume(volume)
1412
1409
  {
1413
1410
  soundVolume = volume;
1414
- if (soundEnable && !headlessMode && audioGainNode)
1415
- audioGainNode.gain.value = volume; // update gain immediately
1411
+ if (soundEnable && !headlessMode && audioMasterGain)
1412
+ audioMasterGain.gain.value = volume; // update gain immediately
1416
1413
  }
1417
1414
 
1418
1415
  /** Set default range where sound no longer plays
@@ -1440,11 +1437,6 @@ function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
1440
1437
  * @memberof Settings */
1441
1438
  function setMedalDisplaySize(size) { medalDisplaySize = size; }
1442
1439
 
1443
- /** Set size of icon in medal display
1444
- * @param {number} size
1445
- * @memberof Settings */
1446
- function setMedalDisplayIconSize(size) { medalDisplayIconSize = size; }
1447
-
1448
1440
  /** Set to stop medals from being unlockable
1449
1441
  * @param {boolean} preventUnlock
1450
1442
  * @memberof Settings */
@@ -1634,7 +1626,8 @@ class EngineObject
1634
1626
  if (this.groundObject)
1635
1627
  {
1636
1628
  // apply friction in local space of ground object
1637
- const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
1629
+ const groundSpeed = this.groundObject != this && this.groundObject.velocity ?
1630
+ this.groundObject.velocity.x : 0;
1638
1631
  this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
1639
1632
  this.groundObject = undefined;
1640
1633
  //debugOverlay && debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
@@ -1761,6 +1754,7 @@ class EngineObject
1761
1754
  this.pos.y = (oldPos.y-this.size.y/2|0)+this.size.y/2+epsilon;
1762
1755
 
1763
1756
  // set ground object to self for tile collision
1757
+ // TODO: rework system so tile collision is its own object
1764
1758
  this.groundObject = this;
1765
1759
  }
1766
1760
  else
@@ -2081,6 +2075,8 @@ class TextureInfo
2081
2075
  this.image = image;
2082
2076
  /** @property {Vector2} - size of the image */
2083
2077
  this.size = vec2(image.width, image.height);
2078
+ /** @property {Vector2} - inverse of the size, cached for rendering */
2079
+ this.sizeInverse = vec2(1/image.width, 1/image.height);
2084
2080
  /** @property {WebGLTexture} - webgl texture */
2085
2081
  this.glTexture = glEnable && glCreateTexture(image);
2086
2082
  }
@@ -2126,19 +2122,19 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
2126
2122
  * @param {Color} [color=(1,1,1,1)] - Color to modulate with
2127
2123
  * @param {number} [angle] - Angle to rotate by
2128
2124
  * @param {boolean} [mirror] - If true image is flipped along the Y axis
2129
- * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
2125
+ * @param {Color} [additiveColor] - Additive color to be applied if any
2130
2126
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
2131
2127
  * @param {boolean} [screenSpace=false] - If true the pos and size are in screen space
2132
2128
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2133
2129
  * @memberof Draw */
2134
2130
  function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2135
- angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
2131
+ angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
2136
2132
  {
2137
2133
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2138
2134
  ASSERT(typeof tileInfo !== 'number' || !tileInfo,
2139
2135
  'this is an old style calls, to fix replace it with tile(tileIndex, tileSize)');
2140
2136
  ASSERT(isVector2(pos) && isVector2(size));
2141
- ASSERT(isColor(color) && isColor(additiveColor));
2137
+ ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)));
2142
2138
 
2143
2139
  const textureInfo = tileInfo && tileInfo.getTextureInfo();
2144
2140
  if (useWebGL)
@@ -2149,21 +2145,30 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2149
2145
  pos = screenToWorld(pos);
2150
2146
  size = size.scale(1/cameraScale);
2151
2147
  }
2152
-
2153
2148
  if (textureInfo)
2154
2149
  {
2155
2150
  // calculate uvs and render
2156
- const sizeInverse = vec2(1).divide(textureInfo.size);
2151
+ const sizeInverse = textureInfo.sizeInverse;
2157
2152
  const x = tileInfo.pos.x * sizeInverse.x;
2158
2153
  const y = tileInfo.pos.y * sizeInverse.y;
2159
2154
  const w = tileInfo.size.x * sizeInverse.x;
2160
2155
  const h = tileInfo.size.y * sizeInverse.y;
2161
- const tileImageFixBleed = sizeInverse.scale(tileFixBleedScale);
2162
2156
  glSetTexture(textureInfo.glTexture);
2163
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2164
- x + tileImageFixBleed.x, y + tileImageFixBleed.y,
2165
- x - tileImageFixBleed.x + w, y - tileImageFixBleed.y + h,
2166
- color.rgbaInt(), additiveColor.rgbaInt());
2157
+ if (tileFixBleedScale)
2158
+ {
2159
+ const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
2160
+ const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
2161
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2162
+ x + tileImageFixBleedX, y + tileImageFixBleedY,
2163
+ x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
2164
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2165
+ }
2166
+ else
2167
+ {
2168
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2169
+ x, y, x + w, y + h,
2170
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2171
+ }
2167
2172
  }
2168
2173
  else
2169
2174
  {
@@ -2375,16 +2380,15 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
2375
2380
  function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
2376
2381
  {
2377
2382
  context.fillStyle = color.toString();
2378
- context.lineWidth = lineWidth;
2379
2383
  context.strokeStyle = lineColor.toString();
2384
+ context.lineWidth = lineWidth;
2380
2385
  context.textAlign = textAlign;
2381
2386
  context.font = size + 'px '+ font;
2382
2387
  context.textBaseline = 'middle';
2383
2388
  context.lineJoin = 'round';
2384
2389
 
2385
- pos = pos.copy();
2386
-
2387
2390
  const lines = (text+'').split('\n');
2391
+ pos = pos.copy();
2388
2392
  pos.y -= (lines.length-1) * size/2; // center text vertically
2389
2393
  lines.forEach(line=>
2390
2394
  {
@@ -2454,7 +2458,10 @@ class FontImage
2454
2458
  {
2455
2459
  // load default font image
2456
2460
  if (!engineFontImage)
2457
- (engineFontImage = new Image).src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
2461
+ {
2462
+ engineFontImage = new Image;
2463
+ engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
2464
+ }
2458
2465
 
2459
2466
  this.image = image || engineFontImage;
2460
2467
  this.tileSize = tileSize;
@@ -2640,10 +2647,16 @@ let mouseWheel = 0;
2640
2647
  * @memberof Input */
2641
2648
  let isUsingGamepad = false;
2642
2649
 
2643
- /** Prevents input continuing to the default browser handling (false by default)
2650
+ /** Prevents input continuing to the default browser handling (true by default)
2644
2651
  * @type {boolean}
2645
2652
  * @memberof Input */
2646
- let preventDefaultInput = false;
2653
+ let inputPreventDefault = true;
2654
+
2655
+ /** Prevents input continuing to the default browser handling
2656
+ * This is useful to disable for html menus so the browser can handle input normally
2657
+ * @param {boolean} preventDefault
2658
+ * @memberof Input */
2659
+ function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
2647
2660
 
2648
2661
  /** Returns true if gamepad button is down
2649
2662
  * @param {number} button
@@ -2723,7 +2736,6 @@ function inputInit()
2723
2736
  if (inputWASDEmulateDirection)
2724
2737
  inputData[0][remapKey(e.code)] = 3;
2725
2738
  }
2726
- preventDefaultInput && e.preventDefault();
2727
2739
  }
2728
2740
 
2729
2741
  onkeyup = (e)=>
@@ -2753,7 +2765,7 @@ function inputInit()
2753
2765
  isUsingGamepad = false;
2754
2766
  inputData[0][e.button] = 3;
2755
2767
  mousePosScreen = mouseEventToScreen(e);
2756
- e.button && e.preventDefault();
2768
+ inputPreventDefault && e.button && e.preventDefault();
2757
2769
  }
2758
2770
  onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2759
2771
  onmousemove = (e)=> mousePosScreen = mouseEventToScreen(e);
@@ -2937,7 +2949,7 @@ function touchInputInit()
2937
2949
  wasTouching = touching;
2938
2950
 
2939
2951
  // prevent default handling like copy and magnifier lens
2940
- if (document.hasFocus()) // allow document to get focus
2952
+ if (inputPreventDefault && document.hasFocus()) // allow document to get focus
2941
2953
  e.preventDefault();
2942
2954
 
2943
2955
  // must return true so the document will get focus
@@ -3080,16 +3092,15 @@ let audioContext = new AudioContext;
3080
3092
  /** Master gain node for all audio to pass through
3081
3093
  * @type {GainNode}
3082
3094
  * @memberof Audio */
3083
- let audioGainNode;
3095
+ let audioMasterGain;
3084
3096
 
3085
3097
  function audioInit()
3086
3098
  {
3087
3099
  if (!soundEnable || headlessMode) return;
3088
3100
 
3089
- // (createGain is more widely supported then GainNode constructor)
3090
- audioGainNode = audioContext.createGain();
3091
- audioGainNode.connect(audioContext.destination);
3092
- audioGainNode.gain.value = soundVolume; // set starting value
3101
+ audioMasterGain = audioContext.createGain();
3102
+ audioMasterGain.connect(audioContext.destination);
3103
+ audioMasterGain.gain.value = soundVolume; // set starting value
3093
3104
  }
3094
3105
 
3095
3106
  ///////////////////////////////////////////////////////////////////////////////
@@ -3218,6 +3229,8 @@ class Sound
3218
3229
  isLoading() { return !this.sampleChannels; }
3219
3230
  }
3220
3231
 
3232
+ ///////////////////////////////////////////////////////////////////////////////
3233
+
3221
3234
  /**
3222
3235
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
3223
3236
  * - this can be used to play wave, mp3, and ogg files
@@ -3269,59 +3282,7 @@ function playAudioFile(filename, volume=1, loop=false)
3269
3282
  return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
3270
3283
  }
3271
3284
 
3272
- /**
3273
- * Music Object - Stores a zzfx music track for later use
3274
- *
3275
- * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
3276
- * @example
3277
- * // create some music
3278
- * const music_example = new Music(
3279
- * [
3280
- * [ // instruments
3281
- * [,0,400] // simple note
3282
- * ],
3283
- * [ // patterns
3284
- * [ // pattern 1
3285
- * [ // channel 0
3286
- * 0, -1, // instrument 0, left speaker
3287
- * 1, 0, 9, 1 // channel notes
3288
- * ],
3289
- * [ // channel 1
3290
- * 0, 1, // instrument 0, right speaker
3291
- * 0, 12, 17, -1 // channel notes
3292
- * ]
3293
- * ],
3294
- * ],
3295
- * [0, 0, 0, 0], // sequence, play pattern 0 four times
3296
- * 90 // BPM
3297
- * ]);
3298
- *
3299
- * // play the music
3300
- * music_example.play();
3301
- */
3302
- class Music extends Sound
3303
- {
3304
- /** Create a music object and cache the zzfx music samples for later use
3305
- * @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters
3306
- */
3307
- constructor(zzfxMusic)
3308
- {
3309
- super(undefined);
3310
-
3311
- if (!soundEnable || headlessMode) return;
3312
- this.randomness = 0;
3313
- this.sampleChannels = zzfxM(...zzfxMusic);
3314
- this.sampleRate = zzfxR;
3315
- }
3316
-
3317
- /** Play the music
3318
- * @param {number} [volume=1] - How much to scale volume by
3319
- * @param {boolean} [loop] - True if the music should loop
3320
- * @return {AudioBufferSourceNode} - The audio source node
3321
- */
3322
- playMusic(volume, loop=false)
3323
- { return super.play(undefined, volume, 1, 1, loop); }
3324
- }
3285
+ ///////////////////////////////////////////////////////////////////////////////
3325
3286
 
3326
3287
  /** Speak text with passed in settings
3327
3288
  * @param {string} text - The text to speak
@@ -3393,7 +3354,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3393
3354
  // create and connect gain node
3394
3355
  gainNode = gainNode || audioContext.createGain();
3395
3356
  gainNode.gain.value = volume;
3396
- gainNode.connect(audioGainNode);
3357
+ gainNode.connect(audioMasterGain);
3397
3358
 
3398
3359
  // connect source to stereo panner and gain
3399
3360
  const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
@@ -3413,7 +3374,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3413
3374
  }
3414
3375
 
3415
3376
  ///////////////////////////////////////////////////////////////////////////////
3416
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.1 by Frank Force
3377
+ // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
3417
3378
 
3418
3379
  /** Generate and play a ZzFX sound
3419
3380
  *
@@ -3455,21 +3416,45 @@ const zzfxR = 44100;
3455
3416
  */
3456
3417
  function zzfxG
3457
3418
  (
3458
- // parameters
3459
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3460
- release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3461
- pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3462
- bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
3419
+ volume = 1,
3420
+ randomness = .05,
3421
+ frequency = 220,
3422
+ attack = 0,
3423
+ sustain = 0,
3424
+ release = .1,
3425
+ shape = 0,
3426
+ shapeCurve = 1,
3427
+ slide = 0,
3428
+ deltaSlide = 0,
3429
+ pitchJump = 0,
3430
+ pitchJumpTime = 0,
3431
+ repeatTime = 0,
3432
+ noise = 0,
3433
+ modulation = 0,
3434
+ bitCrush = 0,
3435
+ delay = 0,
3436
+ sustainVolume = 1,
3437
+ decay = 0,
3438
+ tremolo = 0,
3439
+ filter = 0
3463
3440
  )
3464
3441
  {
3465
- // LJS Note: ZZFX modded so randomness is handled by Sound class
3466
-
3467
3442
  // init parameters
3468
- let PI2 = PI*2, sampleRate = zzfxR,
3443
+ let sampleRate = zzfxR,
3444
+ PI2 = PI*2,
3469
3445
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3470
3446
  startFrequency = frequency *=
3471
- rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3472
- b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3447
+ (1 + rand(randomness,-randomness)) * PI2 / sampleRate,
3448
+ modOffset = 0, // modulation offset
3449
+ repeat = 0, // repeat offset
3450
+ crush = 0, // bit crush offset
3451
+ jump = 1, // pitch jump timer
3452
+ length, // sample length
3453
+ b = [], // sample buffer
3454
+ t = 0, // sample time
3455
+ i = 0, // sample index
3456
+ s = 0, // sample value
3457
+ f, // wave frequency
3473
3458
 
3474
3459
  // biquad LP/HP filter
3475
3460
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
@@ -3479,35 +3464,37 @@ function zzfxG
3479
3464
  b1 = -(sign(filter) + cos) / a0, b2 = b0,
3480
3465
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
3481
3466
 
3482
- // scale by sample rate
3483
- attack = attack * sampleRate + 9; // minimum attack to prevent pop
3484
- decay *= sampleRate;
3485
- sustain *= sampleRate;
3486
- release *= sampleRate;
3487
- delay *= sampleRate;
3488
- deltaSlide *= 500 * PI2 / sampleRate**3;
3489
- modulation *= PI2 / sampleRate;
3490
- pitchJump *= PI2 / sampleRate;
3491
- pitchJumpTime *= sampleRate;
3492
- repeatTime = repeatTime * sampleRate | 0;
3467
+ // scale by sample rate
3468
+ const minAttack = 9; // prevent pop if attack is 0
3469
+ attack = attack * sampleRate || minAttack;
3470
+ decay *= sampleRate;
3471
+ sustain *= sampleRate;
3472
+ release *= sampleRate;
3473
+ delay *= sampleRate;
3474
+ deltaSlide *= 500 * PI2 / sampleRate**3;
3475
+ modulation *= PI2 / sampleRate;
3476
+ pitchJump *= PI2 / sampleRate;
3477
+ pitchJumpTime *= sampleRate;
3478
+ repeatTime = repeatTime * sampleRate | 0;
3493
3479
 
3494
3480
  // generate waveform
3495
3481
  for(length = attack + decay + sustain + release + delay | 0;
3496
- i < length; b[i++] = s * volume) // sample
3482
+ i < length; b[i++] = s * volume) // sample
3497
3483
  {
3498
- if (!(++c%(bitCrush*100|0))) // bit crush
3484
+ if (!(++crush%(bitCrush*100|0))) // bit crush
3499
3485
  {
3500
- s = shape? shape>1? shape>2? shape>3? // wave shape
3501
- Math.sin(t**3) : // 4 noise
3502
- clamp(Math.tan(t),1,-1): // 3 tan
3503
- 1-(2*t/PI2%2+2)%2: // 2 saw
3504
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
3505
- Math.sin(t); // 0 sin
3486
+ s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
3487
+ (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
3488
+ Math.sin(t**3) : // 4 noise
3489
+ Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
3490
+ 1-(2*t/PI2%2+2)%2: // 2 saw
3491
+ 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
3492
+ Math.sin(t); // 0 sin
3506
3493
 
3507
3494
  s = (repeatTime ?
3508
3495
  1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
3509
3496
  : 1) *
3510
- sign(s)*(abs(s)**shapeCurve) * // curve
3497
+ (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
3511
3498
  (i < attack ? i/attack : // attack
3512
3499
  i < attack + decay ? // decay
3513
3500
  1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
@@ -3522,136 +3509,32 @@ function zzfxG
3522
3509
  (i<length-delay? 1 : (length-i)/delay) * // release delay
3523
3510
  b[i-delay|0]/2/volume) : s; // sample delay
3524
3511
 
3525
- if (filter) // apply filter
3512
+ if (filter) // apply filter
3526
3513
  s = y1 = b2*x2 + b1*(x2=x1) + b0*(x1=s) - a2*y2 - a1*(y2=y1);
3527
3514
  }
3528
3515
 
3529
3516
  f = (frequency += slide += deltaSlide) *// frequency
3530
- Math.cos(modulation*tm++); // modulation
3517
+ Math.cos(modulation*modOffset++); // modulation
3531
3518
  t += f + f*noise*Math.sin(i**5); // noise
3532
3519
 
3533
- if (j && ++j > pitchJumpTime) // pitch jump
3520
+ if (jump && ++jump > pitchJumpTime) // pitch jump
3534
3521
  {
3535
3522
  frequency += pitchJump; // apply pitch jump
3536
3523
  startFrequency += pitchJump; // also apply to start
3537
- j = 0; // stop pitch jump time
3524
+ jump = 0; // stop pitch jump time
3538
3525
  }
3539
3526
 
3540
- if (repeatTime && !(++r % repeatTime)) // repeat
3527
+ if (repeatTime && !(++repeat % repeatTime)) // repeat
3541
3528
  {
3542
- frequency = startFrequency; // reset frequency
3543
- slide = startSlide; // reset slide
3544
- j = j || 1; // reset pitch jump time
3529
+ frequency = startFrequency; // reset frequency
3530
+ slide = startSlide; // reset slide
3531
+ jump ||= 1; // reset pitch jump time
3545
3532
  }
3546
3533
  }
3547
3534
 
3548
- return b;
3535
+ return b; // return sample buffer
3549
3536
  }
3550
-
3551
- ///////////////////////////////////////////////////////////////////////////////
3552
- // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
3553
-
3554
- /** Generate samples for a ZzFM song with given parameters
3555
- * @param {Array} instruments - Array of ZzFX sound parameters
3556
- * @param {Array} patterns - Array of pattern data
3557
- * @param {Array} sequence - Array of pattern indexes
3558
- * @param {number} [BPM] - Playback speed of the song in BPM
3559
- * @return {Array} - Left and right channel sample data
3560
- * @memberof Audio */
3561
- function zzfxM(instruments, patterns, sequence, BPM = 125)
3562
- {
3563
- let i, j, k;
3564
- let instrumentParameters;
3565
- let note;
3566
- let sample;
3567
- let patternChannel;
3568
- let notFirstBeat;
3569
- let stop;
3570
- let instrument;
3571
- let attenuation;
3572
- let outSampleOffset;
3573
- let isSequenceEnd;
3574
- let sampleOffset = 0;
3575
- let nextSampleOffset;
3576
- let sampleBuffer = [];
3577
- let leftChannelBuffer = [];
3578
- let rightChannelBuffer = [];
3579
- let channelIndex = 0;
3580
- let panning = 0;
3581
- let hasMore = 1;
3582
- let sampleCache = {};
3583
- let beatLength = zzfxR / BPM * 60 >> 2;
3584
-
3585
- // for each channel in order until there are no more
3586
- for (; hasMore; channelIndex++) {
3587
-
3588
- // reset current values
3589
- sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
3590
-
3591
- // for each pattern in sequence
3592
- sequence.forEach((patternIndex, sequenceIndex) => {
3593
- // get pattern for current channel, use empty 1 note pattern if none found
3594
- patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
3595
-
3596
- // check if there are more channels
3597
- hasMore |= patterns[patternIndex][channelIndex]&&1;
3598
-
3599
- // get next offset, use the length of first channel
3600
- nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
3601
- // for each beat in pattern, plus one extra if end of sequence
3602
- isSequenceEnd = sequenceIndex == sequence.length - 1;
3603
- for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
3604
-
3605
- // <channel-note>
3606
- note = patternChannel[i];
3607
-
3608
- // stop if end, different instrument or new note
3609
- stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
3610
- instrument != (patternChannel[0] || 0) || note | 0;
3611
-
3612
- // fill buffer with samples for previous beat, most cpu intensive part
3613
- for (j = 0; j < beatLength && notFirstBeat;
3614
-
3615
- // fade off attenuation at end of beat if stopping note, prevents clicking
3616
- j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
3617
- ) {
3618
- // copy sample to stereo buffers with panning
3619
- sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
3620
- leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
3621
- rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
3622
- }
3623
-
3624
- // set up for next note
3625
- if (note) {
3626
- // set attenuation
3627
- attenuation = note % 1;
3628
- panning = patternChannel[1] || 0;
3629
- if (note |= 0) {
3630
- // get cached sample
3631
- sampleBuffer = sampleCache[
3632
- [
3633
- instrument = patternChannel[sampleOffset = 0] || 0,
3634
- note
3635
- ]
3636
- ] = sampleCache[[instrument, note]] || (
3637
- // add sample to cache
3638
- instrumentParameters = [...instruments[instrument]],
3639
- instrumentParameters[2] *= 2 ** ((note - 12) / 12),
3640
-
3641
- // allow negative values to stop notes
3642
- note > 0 ? zzfxG(...instrumentParameters) : []
3643
- );
3644
- }
3645
- }
3646
- }
3647
-
3648
- // update the sample offset
3649
- outSampleOffset = nextSampleOffset;
3650
- });
3651
- }
3652
-
3653
- return [leftChannelBuffer, rightChannelBuffer];
3654
- }
3537
+
3655
3538
  /**
3656
3539
  * LittleJS Tile Layer System
3657
3540
  * - Caches arrays of tiles to off screen canvas for fast rendering
@@ -4208,7 +4091,12 @@ class ParticleEmitter extends EngineObject
4208
4091
  else
4209
4092
  this.destroy();
4210
4093
 
4211
- debugParticles && debugRect(this.pos, vec2(this.emitSize), '#0f0', 0, this.angle);
4094
+ if (debugParticles)
4095
+ {
4096
+ // show emitter bounds
4097
+ const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
4098
+ debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
4099
+ }
4212
4100
  }
4213
4101
 
4214
4102
  /** Spawn one particle
@@ -4513,8 +4401,9 @@ class Medal
4513
4401
  {
4514
4402
  const context = overlayContext;
4515
4403
  const width = min(medalDisplaySize.x, mainCanvas.width);
4404
+ const height = medalDisplaySize.y;
4516
4405
  const x = overlayCanvas.width - width;
4517
- const y = -medalDisplaySize.y*hidePercent;
4406
+ const y = -height*hidePercent;
4518
4407
 
4519
4408
  // draw containing rect and clip to that region
4520
4409
  context.save();
@@ -4522,25 +4411,34 @@ class Medal
4522
4411
  context.fillStyle = new Color(.9,.9,.9).toString();
4523
4412
  context.strokeStyle = new Color(0,0,0).toString();
4524
4413
  context.lineWidth = 3;
4525
- context.rect(x, y, width, medalDisplaySize.y);
4414
+ context.rect(x, y, width, height);
4526
4415
  context.fill();
4527
4416
  context.stroke();
4528
4417
  context.clip();
4529
4418
 
4530
- // draw the icon and text
4531
- this.renderIcon(vec2(x+15+medalDisplayIconSize/2, y+medalDisplaySize.y/2));
4532
- const pos = vec2(x+medalDisplayIconSize+30, y+28);
4533
- drawTextScreen(this.name, pos, 38, new Color(0,0,0), 0, undefined, 'left');
4534
- pos.y += 32;
4535
- drawTextScreen(this.description, pos, 24, new Color(0,0,0), 0, undefined, 'left');
4419
+ // draw the icon
4420
+ const gap = vec2(.1, .05).scale(height);
4421
+ const medalDisplayIconSize = height - 2*gap.x;
4422
+ this.renderIcon(vec2(x + gap.x + medalDisplayIconSize/2, y + height/2), medalDisplayIconSize);
4423
+
4424
+ // draw the name
4425
+ const nameSize = height*.5;
4426
+ const descriptionSize = height*.3;
4427
+ const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
4428
+ const textWidth = width - medalDisplayIconSize - 3*gap.x;
4429
+ drawTextScreen(this.name, pos, nameSize, new Color(0,0,0), 0, undefined, 'left', undefined, textWidth);
4430
+
4431
+ // draw the description
4432
+ pos.y = y + height - gap.y*2 - descriptionSize/2;
4433
+ drawTextScreen(this.description, pos, descriptionSize, new Color(0,0,0), 0, undefined, 'left', undefined, textWidth);
4536
4434
  context.restore();
4537
4435
  }
4538
4436
 
4539
4437
  /** Render the icon for a medal
4540
4438
  * @param {Vector2} pos - Screen space position
4541
- * @param {Number} [size=medalDisplayIconSize] - Screen space size
4439
+ * @param {Number} size - Screen space size
4542
4440
  */
4543
- renderIcon(pos, size=medalDisplayIconSize)
4441
+ renderIcon(pos, size)
4544
4442
  {
4545
4443
  // draw the image or icon
4546
4444
  if (this.image)
@@ -4660,12 +4558,12 @@ function glPreRender()
4660
4558
 
4661
4559
  // set vertex attributes
4662
4560
  let offset = glAdditive = glBatchAdditive = 0;
4663
- let initVertexAttribArray = (name, type, typeSize, size)=>
4561
+ const initVertexAttribArray = (name, type, typeSize, size)=>
4664
4562
  {
4665
4563
  const location = glContext.getAttribLocation(glShader, name);
4666
4564
  const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
4667
4565
  const divisor = typeSize && 1; // only if not geometry
4668
- const normalize = typeSize==1; // only if color
4566
+ const normalize = typeSize == 1; // only if color
4669
4567
  glContext.enableVertexAttribArray(location);
4670
4568
  glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
4671
4569
  glContext.vertexAttribDivisor(location, divisor);
@@ -4764,14 +4662,14 @@ function glCreateTexture(image)
4764
4662
  const texture = glContext.createTexture();
4765
4663
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
4766
4664
  if (image && image.width)
4767
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
4665
+ glSetTextureData(texture, image);
4768
4666
  else
4769
4667
  {
4770
4668
  // create a white texture
4771
4669
  const whitePixel = new Uint8Array([255, 255, 255, 255]);
4772
4670
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
4773
4671
  }
4774
-
4672
+
4775
4673
  // use point filtering for pixelated rendering
4776
4674
  const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
4777
4675
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
@@ -4779,6 +4677,18 @@ function glCreateTexture(image)
4779
4677
  return texture;
4780
4678
  }
4781
4679
 
4680
+ /** Set WebGL texture data from an image
4681
+ * @param {WebGLTexture} texture
4682
+ * @param {HTMLImageElement} image
4683
+ * @memberof WebGL */
4684
+ function glSetTextureData(texture, image)
4685
+ {
4686
+ // build the texture
4687
+ ASSERT(!!image && image.width > 0, 'Invalid image data.');
4688
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
4689
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
4690
+ }
4691
+
4782
4692
  /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
4783
4693
  * @memberof WebGL */
4784
4694
  function glFlush()
@@ -4832,10 +4742,10 @@ function glSetAntialias(antialias=true)
4832
4742
  * @param {Number} uv0Y
4833
4743
  * @param {Number} uv1X
4834
4744
  * @param {Number} uv1Y
4835
- * @param {Number} rgba
4836
- * @param {Number} [rgbaAdditive=0]
4745
+ * @param {Number} [rgba=-1] - white is -1
4746
+ * @param {Number} [rgbaAdditive=0] - black is 0
4837
4747
  * @memberof WebGL */
4838
- function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
4748
+ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=-1, rgbaAdditive=0)
4839
4749
  {
4840
4750
  ASSERT(typeof rgba == 'number' && typeof rgbaAdditive == 'number', 'invalid color');
4841
4751
 
@@ -4888,7 +4798,7 @@ const engineName = 'LittleJS';
4888
4798
  * @type {string}
4889
4799
  * @default
4890
4800
  * @memberof Engine */
4891
- const engineVersion = '1.11.10';
4801
+ const engineVersion = '1.11.17';
4892
4802
 
4893
4803
  /** Frames per second to update
4894
4804
  * @type {number}
@@ -4976,16 +4886,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4976
4886
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
4977
4887
 
4978
4888
  // allow passing in empty functions
4979
- if (!gameInit)
4980
- gameInit = ()=>{};
4981
- if (!gameUpdate)
4982
- gameUpdate = ()=>{};
4983
- if (!gameUpdatePost)
4984
- gameUpdatePost = ()=>{};
4985
- if (!gameRender)
4986
- gameRender = ()=>{};
4987
- if (!gameRenderPost)
4988
- gameRenderPost = ()=>{};
4889
+ gameInit ||= ()=>{};
4890
+ gameUpdate ||= ()=>{};
4891
+ gameUpdatePost ||= ()=>{};
4892
+ gameRender ||= ()=>{};
4893
+ gameRenderPost ||= ()=>{};
4989
4894
 
4990
4895
  // Called automatically by engine to setup render system
4991
4896
  function enginePreRender()
@@ -5012,11 +4917,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5012
4917
  const debugSpeedUp = debug && keyIsDown('Equal'); // +
5013
4918
  const debugSpeedDown = debug && keyIsDown('Minus'); // -
5014
4919
  if (debug) // +/- to speed/slow time
5015
- frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1;
4920
+ frameTimeDeltaMS *= debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
5016
4921
  timeReal += frameTimeDeltaMS / 1e3;
5017
4922
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
5018
4923
  if (!debugSpeedUp)
5019
- frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
4924
+ frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
4925
+ if (debug && debugVideoCaptureIsActive())
4926
+ frameTimeBufferMS = 0; // disable time smoothing when capturing video
5020
4927
 
5021
4928
  updateCanvas();
5022
4929
 
@@ -5095,6 +5002,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5095
5002
  }
5096
5003
  }
5097
5004
 
5005
+ debugVideoCaptureUpdate();
5098
5006
  requestAnimationFrame(engineUpdate);
5099
5007
  }
5100
5008
 
@@ -5142,11 +5050,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5142
5050
 
5143
5051
  // setup html
5144
5052
  const styleRoot =
5145
- 'margin:0;overflow:hidden;' + // fill the window
5146
- 'width:100vw;height:100vh;' + // fill the window
5147
- 'display:flex;' + // use flexbox
5148
- 'align-items:center;' + // horizontal center
5149
- 'justify-content:center;' + // vertical center
5053
+ 'margin:0;' + // fill the window
5150
5054
  'background:#000;' + // set background color
5151
5055
  (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
5152
5056
  'user-select:none;' + // prevent hold to select
@@ -5169,7 +5073,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5169
5073
  overlayContext = overlayCanvas.getContext('2d');
5170
5074
 
5171
5075
  // set canvas style
5172
- const styleCanvas = 'position:absolute'; // allow canvases to overlap
5076
+ const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
5077
+ 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
5173
5078
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
5174
5079
  if (glCanvas)
5175
5080
  glCanvas.style.cssText = styleCanvas;
@@ -5180,12 +5085,12 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5180
5085
  new Promise(resolve =>
5181
5086
  {
5182
5087
  const image = new Image;
5183
- image.crossOrigin = 'anonymous';
5184
5088
  image.onerror = image.onload = ()=>
5185
5089
  {
5186
5090
  textureInfos[textureIndex] = new TextureInfo(image);
5187
5091
  resolve();
5188
5092
  }
5093
+ image.crossOrigin = 'anonymous';
5189
5094
  image.src = src;
5190
5095
  })
5191
5096
  );