littlejsengine 1.15.9 → 1.16.2

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 (54) hide show
  1. package/dist/littlejs.d.ts +154 -158
  2. package/dist/littlejs.esm.js +573 -570
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +553 -553
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +392 -396
  7. package/examples/box2d/game.js +1 -1
  8. package/examples/box2d/gameObjects.js +5 -5
  9. package/examples/breakout/game.js +3 -3
  10. package/examples/electron/game.js +1 -2
  11. package/examples/electron/index.html +2 -2
  12. package/examples/htmlMenu/game.js +0 -1
  13. package/examples/index.html +1 -1
  14. package/examples/logo.png +0 -0
  15. package/examples/logo2.png +0 -0
  16. package/examples/module/game.js +1 -2
  17. package/examples/particles/index.html +1 -1
  18. package/examples/platformer/game.js +4 -4
  19. package/examples/platformer/gameEffects.js +1 -1
  20. package/examples/puzzle/game.js +2 -2
  21. package/examples/shorts/base.html +4 -3
  22. package/examples/shorts/box2dPool.js +2 -2
  23. package/examples/shorts/empty.js +1 -1
  24. package/examples/shorts/{systemFont.js → fontImage.js} +3 -3
  25. package/examples/shorts/fps.js +1 -1
  26. package/examples/shorts/helloWorld.js +2 -2
  27. package/examples/shorts/nineSlice.js +2 -2
  28. package/examples/shorts/slidingPuzzle.js +1 -1
  29. package/examples/starter/game.js +1 -2
  30. package/examples/starter/index.html +3 -2
  31. package/examples/stress/index.html +1 -1
  32. package/examples/typescript/game.js +1 -2
  33. package/examples/typescript/game.ts +1 -2
  34. package/package.json +1 -1
  35. package/plugins/box2d.js +8 -8
  36. package/plugins/drawUtilities.js +6 -6
  37. package/plugins/postProcess.js +13 -20
  38. package/plugins/uiSystem.js +16 -4
  39. package/reference.md +9 -11
  40. package/src/engine.js +40 -54
  41. package/src/engineAudio.js +5 -7
  42. package/src/engineBuild.js +1 -0
  43. package/src/engineDebug.js +163 -161
  44. package/src/engineDraw.js +108 -130
  45. package/src/engineExport.js +20 -17
  46. package/src/engineInput.js +4 -7
  47. package/src/engineMath.js +1201 -0
  48. package/src/engineMedals.js +7 -7
  49. package/src/engineObject.js +5 -4
  50. package/src/engineRelease.js +2 -4
  51. package/src/engineSettings.js +22 -32
  52. package/src/engineTileLayer.js +9 -10
  53. package/src/engineUtilities.js +34 -1187
  54. package/src/engineWebGL.js +13 -15
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.15.9';
36
+ const engineVersion = '1.16.2';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -180,8 +180,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
180
180
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
181
181
 
182
182
  // disable smoothing for pixel art
183
- overlayContext.imageSmoothingEnabled =
184
- mainContext.imageSmoothingEnabled = !tilesPixelated;
183
+ mainContext.imageSmoothingEnabled = !tilesPixelated;
185
184
 
186
185
  // setup gl rendering if enabled
187
186
  glPreRender();
@@ -193,7 +192,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
193
192
  // update time keeping
194
193
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
195
194
  frameTimeLastMS = frameTimeMS;
196
- if (debug || showWatermark)
195
+ if (debug || debugWatermark)
197
196
  averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
198
197
  const debugSpeedUp = debug && keyIsDown('Equal'); // +
199
198
  const debugSpeedDown = debug && keyIsDown('Minus'); // -
@@ -221,6 +220,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
221
220
  debugUpdate();
222
221
  gameUpdatePost();
223
222
  inputUpdatePost();
223
+ if (debugVideoCaptureIsActive())
224
+ renderFrame();
224
225
  }
225
226
  else
226
227
  {
@@ -271,33 +272,20 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
271
272
  if (!wasUpdated)
272
273
  updateCanvas();
273
274
 
274
- // render sort then render while removing destroyed objects
275
+ // render the game and objects
275
276
  enginePreRender();
276
277
  gameRender();
277
278
  engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
278
279
  for (const o of engineObjects)
279
280
  o.destroyed || o.render();
281
+
282
+ // post rendering
280
283
  gameRenderPost();
281
284
  pluginList.forEach(plugin=>plugin.render?.());
282
285
  inputRender();
283
286
  debugRender();
284
287
  glFlush();
285
- debugVideoCaptureUpdate();
286
-
287
- if (showWatermark && !debugVideoCaptureIsActive())
288
- {
289
- // update fps display
290
- overlayContext.textAlign = 'right';
291
- overlayContext.textBaseline = 'top';
292
- overlayContext.font = '1em monospace';
293
- overlayContext.fillStyle = '#000';
294
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
295
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
296
- + (glEnable ? ' GL' : ' 2D') ;
297
- overlayContext.fillText(text, mainCanvas.width-3, 3);
298
- overlayContext.fillStyle = '#fff';
299
- overlayContext.fillText(text, mainCanvas.width-2, 2);
300
- }
288
+ debugRenderPost();
301
289
  drawCount = 0;
302
290
  }
303
291
  }
@@ -316,8 +304,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
316
304
  const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
317
305
  const w = innerAspect < fixedAspect ? '100%' : '';
318
306
  const h = innerAspect < fixedAspect ? '' : '100%';
319
- overlayCanvas.style.width = mainCanvas.style.width = w;
320
- overlayCanvas.style.height = mainCanvas.style.height = h;
307
+ mainCanvas.style.width = w;
308
+ mainCanvas.style.height = h;
321
309
  if (glCanvas)
322
310
  {
323
311
  glCanvas.style.width = w;
@@ -332,6 +320,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
332
320
 
333
321
  // responsive aspect ratio with native resolution
334
322
  const innerAspect = innerWidth / innerHeight;
323
+ ASSERT(canvasMinAspect <= canvasMaxAspect);
335
324
  if (canvasMaxAspect && innerAspect > canvasMaxAspect)
336
325
  {
337
326
  // full height
@@ -346,12 +335,12 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
346
335
  }
347
336
  }
348
337
 
349
- // clear main and overlay canvas and set size
350
- overlayCanvas.width = mainCanvas.width = mainCanvasSize.x;
351
- overlayCanvas.height = mainCanvas.height = mainCanvasSize.y;
338
+ // clear main canvas and set size
339
+ mainCanvas.width = mainCanvasSize.x;
340
+ mainCanvas.height = mainCanvasSize.y;
352
341
 
353
342
  // apply the clear color to main canvas
354
- if (canvasClearColor.a > 0)
343
+ if (canvasClearColor.a > 0 && !glEnable)
355
344
  {
356
345
  mainContext.fillStyle = canvasClearColor.toString();
357
346
  mainContext.fillRect(0, 0, mainCanvasSize.x, mainCanvasSize.y);
@@ -359,19 +348,15 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
359
348
  }
360
349
 
361
350
  // set default line join and cap
362
- const lineJoin = 'round', lineCap = 'round';
363
- mainContext.lineJoin = overlayContext.lineJoin = lineJoin;
364
- mainContext.lineCap = overlayContext.lineCap = lineCap;
351
+ mainContext.lineJoin = 'round';
352
+ mainContext.lineCap = 'round';
365
353
  }
354
+
355
+ // skip setup if headless
356
+ if (headlessMode) return startEngine();
366
357
 
367
- // wait for gameInit to load
368
- async function startEngine()
369
- {
370
- await gameInit();
371
- engineUpdate();
372
- }
373
- if (headlessMode)
374
- return startEngine();
358
+ // setup webgl
359
+ glInit(rootElement);
375
360
 
376
361
  // setup html
377
362
  const styleRoot =
@@ -383,36 +368,30 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
383
368
  'touch-action:none;' + // prevent mobile pinch to resize
384
369
  '-webkit-touch-callout:none'; // compatibility for ios
385
370
  rootElement.style.cssText = styleRoot;
386
- drawCanvas = mainCanvas = document.createElement('canvas');
387
- rootElement.appendChild(mainCanvas);
371
+ mainCanvas = rootElement.appendChild(document.createElement('canvas'));
388
372
  drawContext = mainContext = mainCanvas.getContext('2d');
389
373
 
390
374
  // init stuff and start engine
391
375
  inputInit();
392
376
  audioInit();
393
377
  debugInit();
394
- glInit();
395
-
396
- // create overlay canvas for hud to appear above gl canvas
397
- overlayCanvas = document.createElement('canvas')
398
- rootElement.appendChild(overlayCanvas);
399
- overlayContext = overlayCanvas.getContext('2d');
400
378
 
401
379
  // setup canvases
402
380
  // transform way is still more reliable then flexbox or grid
403
381
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
404
382
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
405
- mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
383
+ mainCanvas.style.cssText = styleCanvas;
406
384
  if (glCanvas)
407
385
  glCanvas.style.cssText = styleCanvas;
408
386
  setCanvasPixelated(canvasPixelated);
409
- setOverlayCanvasPixelated(overlayCanvasPixelated);
410
387
  updateCanvas();
411
388
  glPreRender();
412
389
 
413
- // create offscreen canvas for image processing
414
- workCanvas = new OffscreenCanvas(256, 256);
415
- workContext = workCanvas.getContext('2d', { willReadFrequently: true });
390
+ // create offscreen canvases for image processing
391
+ workCanvas = new OffscreenCanvas(64, 64);
392
+ workContext = workCanvas.getContext('2d');
393
+ workReadCanvas = new OffscreenCanvas(64, 64);
394
+ workReadContext = workReadCanvas.getContext('2d', { willReadFrequently: true });
416
395
 
417
396
  // create promises for loading images
418
397
  const promises = imageSources.map((src, textureIndex)=>
@@ -464,13 +443,20 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
464
443
  await Promise.all(promises);
465
444
  return startEngine();
466
445
 
446
+ async function startEngine()
447
+ {
448
+ // wait for gameInit to load
449
+ await gameInit();
450
+ engineUpdate();
451
+ }
452
+
467
453
  ///////////////////////////////////////////////////////////////////////////
468
454
  // LittleJS Splash Screen
469
455
  function drawEngineSplashScreen(t)
470
456
  {
471
- const x = overlayContext;
472
- const w = overlayCanvas.width = innerWidth;
473
- const h = overlayCanvas.height = innerHeight;
457
+ const x = mainContext;
458
+ const w = mainCanvas.width = innerWidth;
459
+ const h = mainCanvas.height = innerHeight;
474
460
 
475
461
  {
476
462
  // background
@@ -768,7 +754,7 @@ const debugPointSize = .5;
768
754
  * @type {boolean}
769
755
  * @default
770
756
  * @memberof Debug */
771
- let showWatermark = true;
757
+ let debugWatermark = true;
772
758
 
773
759
  /** Key code used to toggle debug mode, Esc by default
774
760
  * @type {string}
@@ -783,7 +769,7 @@ let debugKey = 'Escape';
783
769
  let debugOverlay = false;
784
770
 
785
771
  // Engine internal variables not exposed to documentation
786
- let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugMedals = false, debugTakeScreenshot, downloadLink, debugCanvas;
772
+ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugMedals = false, debugTakeScreenshot;
787
773
 
788
774
  ///////////////////////////////////////////////////////////////////////////////
789
775
  // Debug helper functions
@@ -970,46 +956,6 @@ function debugClear() { debugPrimitives = []; }
970
956
  * @memberof Debug */
971
957
  function debugScreenshot() { debugTakeScreenshot = 1; }
972
958
 
973
- /** Save a canvas to disk
974
- * @param {HTMLCanvasElement|OffscreenCanvas} canvas
975
- * @param {string} [filename]
976
- * @param {string} [type]
977
- * @memberof Debug */
978
- function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
979
- {
980
- if (canvas instanceof OffscreenCanvas)
981
- {
982
- // copy to temporary canvas and save
983
- if (!debugCanvas)
984
- debugCanvas = document.createElement('canvas');
985
- debugCanvas.width = canvas.width;
986
- debugCanvas.height = canvas.height;
987
- debugCanvas.getContext('2d').drawImage(canvas, 0, 0);
988
- debugSaveDataURL(debugCanvas.toDataURL(type), filename);
989
- }
990
- else
991
- debugSaveDataURL(canvas.toDataURL(type), filename);
992
- }
993
-
994
- /** Save a text file to disk
995
- * @param {string} text
996
- * @param {string} [filename]
997
- * @param {string} [type]
998
- * @memberof Debug */
999
- function debugSaveText(text, filename='text', type='text/plain')
1000
- { debugSaveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
1001
-
1002
- /** Save a data url to disk
1003
- * @param {string} dataURL
1004
- * @param {string} filename
1005
- * @memberof Debug */
1006
- function debugSaveDataURL(dataURL, filename)
1007
- {
1008
- downloadLink.download = filename;
1009
- downloadLink.href = dataURL;
1010
- downloadLink.click();
1011
- }
1012
-
1013
959
  /** Breaks on all asserts/errors, hides the canvas, and shows message in plain text
1014
960
  * This is a good function to call at the start of your game to catch all errors
1015
961
  * In release builds this function has no effect
@@ -1045,21 +991,18 @@ function debugShowErrors()
1045
991
 
1046
992
  function debugInit()
1047
993
  {
1048
- // create link for saving screenshots
1049
- downloadLink = document.createElement('a');
1050
994
  }
1051
995
 
1052
996
  function debugUpdate()
1053
997
  {
1054
- if (!debug)
1055
- return;
998
+ if (!debug) return;
1056
999
 
1057
1000
  if (keyWasPressed(debugKey)) // Esc
1058
1001
  debugOverlay = !debugOverlay;
1059
1002
  if (debugOverlay)
1060
1003
  {
1061
1004
  if (keyWasPressed('Digit0'))
1062
- showWatermark = !showWatermark;
1005
+ debugWatermark = !debugWatermark;
1063
1006
  if (keyWasPressed('Digit1'))
1064
1007
  debugPhysics = !debugPhysics, debugParticles = false;
1065
1008
  if (keyWasPressed('Digit2'))
@@ -1070,9 +1013,15 @@ function debugUpdate()
1070
1013
  debugRaycast = !debugRaycast;
1071
1014
  if (keyWasPressed('Digit5'))
1072
1015
  debugScreenshot();
1073
- if (keyWasPressed('Digit6'))
1074
- debugVideoCaptureIsActive() ? debugVideoCaptureStop() : debugVideoCaptureStart();
1075
1016
  }
1017
+ if (debugVideoCaptureIsActive())
1018
+ {
1019
+ // control to stop video capture
1020
+ if (!debugOverlay || keyWasPressed('Digit6'))
1021
+ debugVideoCaptureStop();
1022
+ }
1023
+ else if (debugOverlay && keyWasPressed('Digit6'))
1024
+ debugVideoCaptureStart();
1076
1025
  }
1077
1026
 
1078
1027
  function debugRender()
@@ -1087,13 +1036,11 @@ function debugRender()
1087
1036
  {
1088
1037
  // combine canvases, remove alpha and save
1089
1038
  combineCanvases();
1090
- const w = mainCanvas.width, h = mainCanvas.height;
1091
- overlayContext.fillRect(0,0,w,h);
1092
- overlayContext.drawImage(mainCanvas, 0, 0);
1093
- debugSaveCanvas(overlayCanvas);
1039
+ saveCanvas(mainCanvas);
1094
1040
  debugTakeScreenshot = 0;
1095
1041
  }
1096
1042
 
1043
+ const debugContext = mainContext;
1097
1044
  if (debugGamepads && gamepadsEnable)
1098
1045
  {
1099
1046
  // draw gamepads
@@ -1141,9 +1088,6 @@ function debugRender()
1141
1088
  let debugObject;
1142
1089
  if (debugOverlay)
1143
1090
  {
1144
- const saveContext = mainContext;
1145
- mainContext = overlayContext;
1146
-
1147
1091
  // draw red rectangle around screen
1148
1092
  const cameraSize = getCameraSize();
1149
1093
  debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008');
@@ -1175,15 +1119,14 @@ function debugRender()
1175
1119
  // show floored tile pick for tile collision
1176
1120
  drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(1,1,0,.5));
1177
1121
  }
1178
- mainContext = saveContext;
1179
1122
  }
1180
1123
 
1181
1124
  {
1182
1125
  // draw debug primitives
1183
- overlayContext.lineWidth = 2;
1126
+ debugContext.lineWidth = 2;
1184
1127
  debugPrimitives.forEach(p=>
1185
1128
  {
1186
- overlayContext.save();
1129
+ debugContext.save();
1187
1130
 
1188
1131
  // create canvas transform from world space to screen space
1189
1132
  // without scaling because we want consistent pixel sizes
@@ -1194,55 +1137,56 @@ function debugRender()
1194
1137
  scale = cameraScale;
1195
1138
  angle -= cameraAngle;
1196
1139
  }
1197
- overlayContext.translate(pos.x|0, pos.y|0);
1198
- overlayContext.rotate(angle);
1199
- overlayContext.scale(1, p.text ? 1 : -1);
1200
- overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
1140
+ debugContext.translate(pos.x|0, pos.y|0);
1141
+ debugContext.rotate(angle);
1142
+ debugContext.scale(1, p.text ? 1 : -1);
1143
+ debugContext.fillStyle = p.color;
1144
+ debugContext.strokeStyle = p.color;
1201
1145
  if (p.text !== undefined)
1202
1146
  {
1203
- overlayContext.font = p.size*scale + 'px '+ p.font;
1204
- overlayContext.textAlign = 'center';
1205
- overlayContext.textBaseline = 'middle';
1206
- overlayContext.fillText(p.text, 0, 0);
1147
+ debugContext.font = p.size*scale + 'px '+ p.font;
1148
+ debugContext.textAlign = 'center';
1149
+ debugContext.textBaseline = 'middle';
1150
+ debugContext.fillText(p.text, 0, 0);
1207
1151
  }
1208
1152
  else if (p.points !== undefined)
1209
1153
  {
1210
1154
  // poly
1211
- overlayContext.beginPath();
1155
+ debugContext.beginPath();
1212
1156
  for (const point of p.points)
1213
1157
  {
1214
1158
  const p2 = point.scale(scale).floor();
1215
- overlayContext.lineTo(p2.x, p2.y);
1159
+ debugContext.lineTo(p2.x, p2.y);
1216
1160
  }
1217
- overlayContext.closePath();
1218
- p.fill && overlayContext.fill();
1219
- overlayContext.stroke();
1161
+ debugContext.closePath();
1162
+ p.fill && debugContext.fill();
1163
+ debugContext.stroke();
1220
1164
  }
1221
1165
  else if (p.size === 0 || (p.size.x === 0 && p.size.y === 0))
1222
1166
  {
1223
1167
  // point
1224
1168
  const pointSize = debugPointSize * scale;
1225
- overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
1226
- overlayContext.fillRect(-1, -pointSize/2, 3, pointSize);
1169
+ debugContext.fillRect(-pointSize/2, -1, pointSize, 3);
1170
+ debugContext.fillRect(-1, -pointSize/2, 3, pointSize);
1227
1171
  }
1228
1172
  else if (p.size.x !== undefined)
1229
1173
  {
1230
1174
  // rect
1231
1175
  const s = p.size.scale(scale).floor();
1232
1176
  const w = s.x, h = s.y;
1233
- p.fill && overlayContext.fillRect(-w/2|0, -h/2|0, w, h);
1234
- overlayContext.strokeRect(-w/2|0, -h/2|0, w, h);
1177
+ p.fill && debugContext.fillRect(-w/2|0, -h/2|0, w, h);
1178
+ debugContext.strokeRect(-w/2|0, -h/2|0, w, h);
1235
1179
  }
1236
1180
  else
1237
1181
  {
1238
1182
  // circle
1239
- overlayContext.beginPath();
1240
- overlayContext.arc(0, 0, p.size*scale/2, 0, 9);
1241
- p.fill && overlayContext.fill();
1242
- overlayContext.stroke();
1183
+ debugContext.beginPath();
1184
+ debugContext.arc(0, 0, p.size*scale/2, 0, 9);
1185
+ p.fill && debugContext.fill();
1186
+ debugContext.stroke();
1243
1187
  }
1244
1188
 
1245
- overlayContext.restore();
1189
+ debugContext.restore();
1246
1190
  });
1247
1191
 
1248
1192
  // remove expired primitives
@@ -1251,55 +1195,53 @@ function debugRender()
1251
1195
 
1252
1196
  if (debugObject)
1253
1197
  {
1254
- const saveContext = mainContext;
1255
- mainContext = overlayContext;
1256
1198
  const raycastHitPos = tileCollisionRaycast(debugObject.pos, mousePos);
1257
1199
  raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3));
1258
1200
  drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5));
1259
1201
 
1260
- const debugText = 'mouse pos = ' + mousePos +
1261
- '\nmouse collision = ' + tileCollisionGetData(mousePos) +
1262
- '\n\n--- object info ---\n' +
1263
- debugObject.toString();
1202
+ let debugText = 'mouse pos = ' + mousePos;
1203
+ if (tileCollisionLayers.length)
1204
+ debugText += '\nmouse collision = ' + tileCollisionGetData(mousePos);
1205
+ debugText += '\n\n--- object info ---\n';
1206
+ debugText += debugObject.toString();
1264
1207
  drawTextScreen(debugText, mousePosScreen, 24, rgb(), .05, undefined, 'center', 'monospace');
1265
- mainContext = saveContext;
1266
1208
  }
1267
1209
 
1268
1210
  {
1269
1211
  // draw debug overlay
1270
1212
  const fontSize = 20;
1271
1213
  const lineHeight = fontSize * 1.2 | 0;
1272
- overlayContext.save();
1273
- overlayContext.fillStyle = '#fff';
1274
- overlayContext.textAlign = 'left';
1275
- overlayContext.textBaseline = 'top';
1276
- overlayContext.font = fontSize + 'px monospace';
1277
- overlayContext.shadowColor = '#000';
1278
- overlayContext.shadowBlur = 9;
1214
+ debugContext.save();
1215
+ debugContext.fillStyle = '#fff';
1216
+ debugContext.textAlign = 'left';
1217
+ debugContext.textBaseline = 'top';
1218
+ debugContext.font = fontSize + 'px monospace';
1219
+ debugContext.shadowColor = '#000';
1220
+ debugContext.shadowBlur = 9;
1279
1221
 
1280
1222
  let x = 9, y = 0, h = lineHeight;
1281
1223
  if (debugOverlay)
1282
1224
  {
1283
- overlayContext.fillText(`${engineName} v${engineVersion}`, x, y += h/2 );
1284
- overlayContext.fillText('Time: ' + formatTime(time), x, y += h);
1285
- overlayContext.fillText('FPS: ' + averageFPS.toFixed(1) + (glEnable?' WebGL':' Canvas2D'),
1225
+ debugContext.fillText(`${engineName} v${engineVersion}`, x, y += h/2 );
1226
+ debugContext.fillText('Time: ' + formatTime(time), x, y += h);
1227
+ debugContext.fillText('FPS: ' + averageFPS.toFixed(1) + (glEnable?' WebGL':' Canvas2D'),
1286
1228
  x, y += h);
1287
- overlayContext.fillText('Objects: ' + engineObjects.length, x, y += h);
1288
- overlayContext.fillText('Draw Count: ' + drawCount, x, y += h);
1289
- overlayContext.fillText('---------', x, y += h);
1290
- overlayContext.fillStyle = '#f00';
1291
- overlayContext.fillText('ESC: Debug Overlay', x, y += h);
1292
- overlayContext.fillStyle = debugPhysics ? '#f00' : '#fff';
1293
- overlayContext.fillText('1: Debug Physics', x, y += h);
1294
- overlayContext.fillStyle = debugParticles ? '#f00' : '#fff';
1295
- overlayContext.fillText('2: Debug Particles', x, y += h);
1296
- overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
1297
- overlayContext.fillText('3: Debug Gamepads', x, y += h);
1298
- overlayContext.fillStyle = debugRaycast ? '#f00' : '#fff';
1299
- overlayContext.fillText('4: Debug Raycasts', x, y += h);
1300
- overlayContext.fillStyle = '#fff';
1301
- overlayContext.fillText('5: Save Screenshot', x, y += h);
1302
- overlayContext.fillText('6: Toggle Video Capture', x, y += h);
1229
+ debugContext.fillText('Objects: ' + engineObjects.length, x, y += h);
1230
+ debugContext.fillText('Draw Count: ' + drawCount, x, y += h);
1231
+ debugContext.fillText('---------', x, y += h);
1232
+ debugContext.fillStyle = '#f00';
1233
+ debugContext.fillText('ESC: Debug Overlay', x, y += h);
1234
+ debugContext.fillStyle = debugPhysics ? '#f00' : '#fff';
1235
+ debugContext.fillText('1: Debug Physics', x, y += h);
1236
+ debugContext.fillStyle = debugParticles ? '#f00' : '#fff';
1237
+ debugContext.fillText('2: Debug Particles', x, y += h);
1238
+ debugContext.fillStyle = debugGamepads ? '#f00' : '#fff';
1239
+ debugContext.fillText('3: Debug Gamepads', x, y += h);
1240
+ debugContext.fillStyle = debugRaycast ? '#f00' : '#fff';
1241
+ debugContext.fillText('4: Debug Raycasts', x, y += h);
1242
+ debugContext.fillStyle = '#fff';
1243
+ debugContext.fillText('5: Save Screenshot', x, y += h);
1244
+ debugContext.fillText('6: Toggle Video Capture', x, y += h);
1303
1245
 
1304
1246
  let keysPressed = '';
1305
1247
  let mousePressed = '';
@@ -1312,8 +1254,8 @@ function debugRender()
1312
1254
  else if (keyIsDown(i, 0))
1313
1255
  keysPressed += i + ' ' ;
1314
1256
  }
1315
- mousePressed && overlayContext.fillText('Mouse: ' + mousePressed, x, y += h);
1316
- keysPressed && overlayContext.fillText('Keys: ' + keysPressed, x, y += h);
1257
+ mousePressed && debugContext.fillText('Mouse: ' + mousePressed, x, y += h);
1258
+ keysPressed && debugContext.fillText('Keys: ' + keysPressed, x, y += h);
1317
1259
 
1318
1260
  // show gamepad buttons
1319
1261
  for (let i = 1; i < inputData.length; i++)
@@ -1325,26 +1267,49 @@ function debugRender()
1325
1267
  if (keyIsDown(j, i))
1326
1268
  buttonsPressed += j + ' ' ;
1327
1269
  }
1328
- buttonsPressed && overlayContext.fillText(`Gamepad ${i-1}: ` + buttonsPressed, x, y += h);
1270
+ buttonsPressed && debugContext.fillText(`Gamepad ${i-1}: ` + buttonsPressed, x, y += h);
1329
1271
  }
1330
1272
  }
1331
1273
  else
1332
1274
  {
1333
- overlayContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h);
1334
- overlayContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
1335
- overlayContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
1336
- overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
1275
+ debugContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h);
1276
+ debugContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
1277
+ debugContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
1278
+ debugContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
1337
1279
  }
1338
1280
 
1339
- overlayContext.restore();
1281
+ debugContext.restore();
1340
1282
  }
1341
1283
  }
1342
1284
 
1285
+ function debugRenderPost()
1286
+ {
1287
+ if (debugVideoCaptureIsActive())
1288
+ {
1289
+ debugVideoCaptureUpdate();
1290
+ return;
1291
+ }
1292
+
1293
+ if (!debugWatermark) return;
1294
+
1295
+ // update fps display
1296
+ mainContext.textAlign = 'right';
1297
+ mainContext.textBaseline = 'top';
1298
+ mainContext.font = '1em monospace';
1299
+ mainContext.fillStyle = '#000';
1300
+ const text = engineName + ' v' + engineVersion + ' / '
1301
+ + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
1302
+ + (glEnable ? ' GL' : ' 2D') ;
1303
+ mainContext.fillText(text, mainCanvas.width-3, 3);
1304
+ mainContext.fillStyle = '#fff';
1305
+ mainContext.fillText(text, mainCanvas.width-2, 2);
1306
+ }
1307
+
1343
1308
  ///////////////////////////////////////////////////////////////////////////////
1344
1309
  // video capture - records video and audio at 60 fps using MediaRecorder API
1345
1310
 
1346
1311
  // internal variables used to capture video
1347
- let debugVideoCapture, debugVideoCaptureTrack, debugVideoCaptureIcon, debugVideoCaptureTimer;
1312
+ let debugVideoCapture, debugVideoCaptureIcon;
1348
1313
 
1349
1314
  /** Check if video capture is active
1350
1315
  * @memberof Debug */
@@ -1354,80 +1319,103 @@ function debugVideoCaptureIsActive() { return !!debugVideoCapture; }
1354
1319
  * @memberof Debug */
1355
1320
  function debugVideoCaptureStart()
1356
1321
  {
1357
- if (debugVideoCaptureIsActive())
1358
- return; // already recording
1322
+ ASSERT(!debugVideoCaptureIsActive(), 'Already capturing video!');
1359
1323
 
1360
- // captureStream passing in 0 to only capture when requestFrame() is called
1324
+ if (!debugVideoCaptureIcon)
1325
+ {
1326
+ // create recording icon to show it is capturing video
1327
+ debugVideoCaptureIcon = document.createElement('div');
1328
+ debugVideoCaptureIcon.style.position = 'absolute';
1329
+ debugVideoCaptureIcon.style.padding = '9px';
1330
+ debugVideoCaptureIcon.style.color = '#f00';
1331
+ debugVideoCaptureIcon.style.font = '50px monospace';
1332
+ document.body.appendChild(debugVideoCaptureIcon);
1333
+ }
1334
+ // show recording icon
1335
+ debugVideoCaptureIcon.textContent = '';
1336
+ debugVideoCaptureIcon.style.display = '';
1337
+
1338
+ // setup captureStream to capture manually by passing 0
1361
1339
  const stream = mainCanvas.captureStream(0);
1340
+ const videoTrack = stream.getVideoTracks()[0];
1341
+ const captureTimer = new Timer(0, true);
1362
1342
  const chunks = [];
1363
- debugVideoCaptureTrack = stream.getVideoTracks()[0];
1364
- if (debugVideoCaptureTrack.applyConstraints)
1365
- debugVideoCaptureTrack.applyConstraints({frameRate:60}); // force 60 fps
1366
- debugVideoCapture = new MediaRecorder(stream, {mimeType:'video/webm;codecs=vp8'});
1367
- debugVideoCapture.ondataavailable = (e)=> chunks.push(e.data);
1368
- debugVideoCapture.onstop = ()=>
1343
+ videoTrack.applyConstraints({frameRate:frameRate});
1344
+
1345
+ // set up the media recorder
1346
+ const mediaRecorder = new MediaRecorder(stream,
1347
+ {mimeType:'video/webm;codecs=vp8'});
1348
+ mediaRecorder.ondataavailable = (e)=> chunks.push(e.data);
1349
+ mediaRecorder.onstop = ()=>
1369
1350
  {
1370
1351
  const blob = new Blob(chunks, {type: 'video/webm'});
1371
1352
  const url = URL.createObjectURL(blob);
1372
- downloadLink.download = 'capture.webm';
1373
- downloadLink.href = url;
1374
- downloadLink.click();
1375
- URL.revokeObjectURL(url);
1353
+ saveDataURL(url, 'capture.webm', 1e3);
1376
1354
  };
1377
1355
 
1378
- if (audioMasterGain)
1356
+ let audioStreamDestination, silentAudioSource;
1357
+ if (soundEnable)
1379
1358
  {
1359
+ // create silent audio source
1360
+ // fixes issue where video can not start recording without audio
1361
+ silentAudioSource = new ConstantSourceNode(audioContext, { offset: 0 });
1362
+ silentAudioSource.connect(audioMasterGain);
1363
+ silentAudioSource.start();
1364
+
1380
1365
  // connect to audio master gain node
1381
- const audioStreamDestination = audioContext.createMediaStreamDestination();
1366
+ audioStreamDestination = audioContext.createMediaStreamDestination();
1382
1367
  audioMasterGain.connect(audioStreamDestination);
1383
1368
  for (const track of audioStreamDestination.stream.getAudioTracks())
1384
1369
  stream.addTrack(track); // add audio tracks to capture stream
1385
1370
  }
1386
1371
 
1387
1372
  // start recording
1373
+ try { mediaRecorder.start(); }
1374
+ catch(e)
1375
+ {
1376
+ LOG('Video capture not supported in this browser!');
1377
+ silentAudioSource?.stop();
1378
+ return;
1379
+ }
1380
+
1388
1381
  LOG('Video capture started.');
1389
- debugVideoCapture.start();
1390
- debugVideoCaptureTimer = new Timer(0);
1391
1382
 
1392
- if (!debugVideoCaptureIcon)
1383
+ // save debug video info
1384
+ debugVideoCapture =
1393
1385
  {
1394
- // create recording icon to show it is capturing video
1395
- debugVideoCaptureIcon = document.createElement('div');
1396
- debugVideoCaptureIcon.style.position = 'absolute';
1397
- debugVideoCaptureIcon.style.padding = '9px';
1398
- debugVideoCaptureIcon.style.color = '#f00';
1399
- debugVideoCaptureIcon.style.font = '50px monospace';
1400
- document.body.appendChild(debugVideoCaptureIcon);
1401
- }
1402
- // show recording icon
1403
- debugVideoCaptureIcon.textContent = '';
1404
- debugVideoCaptureIcon.style.display = '';
1386
+ mediaRecorder,
1387
+ captureTimer,
1388
+ videoTrack,
1389
+ silentAudioSource,
1390
+ audioStreamDestination
1391
+ };
1405
1392
  }
1406
1393
 
1407
1394
  /** Stop capturing video and save to disk
1408
1395
  * @memberof Debug */
1409
1396
  function debugVideoCaptureStop()
1410
1397
  {
1411
- if (!debugVideoCaptureIsActive())
1412
- return; // not recording
1398
+ ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
1413
1399
 
1414
1400
  // stop recording
1415
- LOG(`Video capture ended. ${debugVideoCaptureTimer.get().toFixed(2)} seconds recorded.`);
1416
- debugVideoCapture.stop();
1417
- debugVideoCapture = 0;
1401
+ LOG(`Video capture ended. ${debugVideoCapture.captureTimer.get().toFixed(2)} seconds recorded.`);
1418
1402
  debugVideoCaptureIcon.style.display = 'none';
1403
+ debugVideoCapture.silentAudioSource?.stop();
1404
+ debugVideoCapture.mediaRecorder?.stop();
1405
+ debugVideoCapture.videoTrack?.stop();
1406
+ debugVideoCapture = undefined;
1419
1407
  }
1420
1408
 
1421
1409
  // update video capture, called automatically by engine
1422
1410
  function debugVideoCaptureUpdate()
1423
1411
  {
1424
- if (!debugVideoCaptureIsActive())
1425
- return; // not recording
1412
+ ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
1426
1413
 
1427
1414
  // save the video frame
1428
1415
  combineCanvases();
1429
- debugVideoCaptureTrack.requestFrame();
1430
- debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
1416
+ debugVideoCapture.videoTrack.requestFrame();
1417
+ debugVideoCaptureIcon.textContent = '● REC '
1418
+ + formatTime(debugVideoCapture.captureTimer);
1431
1419
  }
1432
1420
 
1433
1421
  ///////////////////////////////////////////////////////////////////////////////
@@ -1466,99 +1454,99 @@ function debugProtectConstant(obj)
1466
1454
  * - Color - holds a rgba color with some math functions
1467
1455
  * - Timer - tracks time automatically
1468
1456
  * - RandomGenerator - seeded random number generator
1469
- * @namespace Utilities
1457
+ * @namespace Math
1470
1458
  */
1471
1459
 
1472
1460
  /** The value of PI
1473
1461
  * @type {number}
1474
1462
  * @default Math.PI
1475
- * @memberof Utilities */
1463
+ * @memberof Math */
1476
1464
  const PI = Math.PI;
1477
1465
 
1478
1466
  /** Returns absolute value of value passed in
1479
1467
  * @param {number} value
1480
1468
  * @return {number}
1481
- * @memberof Utilities */
1469
+ * @memberof Math */
1482
1470
  const abs = Math.abs;
1483
1471
 
1484
1472
  /** Returns floored value of value passed in
1485
1473
  * @param {number} value
1486
1474
  * @return {number}
1487
- * @memberof Utilities */
1475
+ * @memberof Math */
1488
1476
  const floor = Math.floor;
1489
1477
 
1490
1478
  /** Returns ceiled value of value passed in
1491
1479
  * @param {number} value
1492
1480
  * @return {number}
1493
- * @memberof Utilities */
1481
+ * @memberof Math */
1494
1482
  const ceil = Math.ceil;
1495
1483
 
1496
1484
  /** Returns rounded value passed in
1497
1485
  * @param {number} value
1498
1486
  * @return {number}
1499
- * @memberof Utilities */
1487
+ * @memberof Math */
1500
1488
  const round = Math.round;
1501
1489
 
1502
1490
  /** Returns lowest value passed in
1503
1491
  * @param {...number} values
1504
1492
  * @return {number}
1505
- * @memberof Utilities */
1493
+ * @memberof Math */
1506
1494
  const min = Math.min;
1507
1495
 
1508
1496
  /** Returns highest value passed in
1509
1497
  * @param {...number} values
1510
1498
  * @return {number}
1511
- * @memberof Utilities */
1499
+ * @memberof Math */
1512
1500
  const max = Math.max;
1513
1501
 
1514
1502
  /** Returns the sign of value passed in
1515
1503
  * @param {number} value
1516
1504
  * @return {number}
1517
- * @memberof Utilities */
1505
+ * @memberof Math */
1518
1506
  const sign = Math.sign;
1519
1507
 
1520
1508
  /** Returns hypotenuse of values passed in
1521
1509
  * @param {...number} values
1522
1510
  * @return {number}
1523
- * @memberof Utilities */
1511
+ * @memberof Math */
1524
1512
  const hypot = Math.hypot;
1525
1513
 
1526
1514
  /** Returns log2 of value passed in
1527
1515
  * @param {number} value
1528
1516
  * @return {number}
1529
- * @memberof Utilities */
1517
+ * @memberof Math */
1530
1518
  const log2 = Math.log2;
1531
1519
 
1532
1520
  /** Returns sin of value passed in
1533
1521
  * @param {number} value
1534
1522
  * @return {number}
1535
- * @memberof Utilities */
1523
+ * @memberof Math */
1536
1524
  const sin = Math.sin;
1537
1525
 
1538
1526
  /** Returns cos of value passed in
1539
1527
  * @param {number} value
1540
1528
  * @return {number}
1541
- * @memberof Utilities */
1529
+ * @memberof Math */
1542
1530
  const cos = Math.cos;
1543
1531
 
1544
1532
  /** Returns tan of value passed in
1545
1533
  * @param {number} value
1546
1534
  * @return {number}
1547
- * @memberof Utilities */
1535
+ * @memberof Math */
1548
1536
  const tan = Math.tan;
1549
1537
 
1550
1538
  /** Returns atan2 of values passed in
1551
1539
  * @param {number} y
1552
1540
  * @param {number} x
1553
1541
  * @return {number}
1554
- * @memberof Utilities */
1542
+ * @memberof Math */
1555
1543
  const atan2 = Math.atan2;
1556
1544
 
1557
1545
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
1558
1546
  * @param {number} dividend
1559
1547
  * @param {number} [divisor]
1560
1548
  * @return {number}
1561
- * @memberof Utilities */
1549
+ * @memberof Math */
1562
1550
  function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % divisor; }
1563
1551
 
1564
1552
  /** Clamps the value between max and min
@@ -1566,7 +1554,7 @@ function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % di
1566
1554
  * @param {number} [min]
1567
1555
  * @param {number} [max]
1568
1556
  * @return {number}
1569
- * @memberof Utilities */
1557
+ * @memberof Math */
1570
1558
  function clamp(value, min=0, max=1) { return value < min ? min : value > max ? max : value; }
1571
1559
 
1572
1560
  /** Returns what percentage the value is between valueA and valueB
@@ -1574,7 +1562,7 @@ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? m
1574
1562
  * @param {number} valueA
1575
1563
  * @param {number} valueB
1576
1564
  * @return {number}
1577
- * @memberof Utilities */
1565
+ * @memberof Math */
1578
1566
  function percent(value, valueA, valueB)
1579
1567
  { return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
1580
1568
 
@@ -1583,13 +1571,9 @@ function percent(value, valueA, valueB)
1583
1571
  * @param {number} valueB
1584
1572
  * @param {number} percent
1585
1573
  * @return {number}
1586
- * @memberof Utilities */
1574
+ * @memberof Math */
1587
1575
  function lerp(valueA, valueB, percent)
1588
- {
1589
- if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
1590
- console.warn('lerp() parameter order changed! use lerp(start, end, p)');
1591
- return valueA + clamp(percent) * (valueB-valueA);
1592
- }
1576
+ { return valueA + clamp(percent) * (valueB-valueA); }
1593
1577
 
1594
1578
  /** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
1595
1579
  * A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
@@ -1599,7 +1583,7 @@ function lerp(valueA, valueB, percent)
1599
1583
  * @param {number} lerpA
1600
1584
  * @param {number} lerpB
1601
1585
  * @return {number}
1602
- * @memberof Utilities */
1586
+ * @memberof Math */
1603
1587
  function percentLerp(value, percentA, percentB, lerpA, lerpB)
1604
1588
  { return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
1605
1589
 
@@ -1608,7 +1592,7 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
1608
1592
  * @param {number} valueB
1609
1593
  * @param {number} [wrapSize]
1610
1594
  * @return {number}
1611
- * @memberof Utilities */
1595
+ * @memberof Math */
1612
1596
  function distanceWrap(valueA, valueB, wrapSize=1)
1613
1597
  { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
1614
1598
 
@@ -1618,19 +1602,15 @@ function distanceWrap(valueA, valueB, wrapSize=1)
1618
1602
  * @param {number} percent
1619
1603
  * @param {number} [wrapSize]
1620
1604
  * @return {number}
1621
- * @memberof Utilities */
1605
+ * @memberof Math */
1622
1606
  function lerpWrap(valueA, valueB, percent, wrapSize=1)
1623
- {
1624
- if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
1625
- console.warn('lerpWrap() parameter order changed! use lerpWrap(start, end, p)');
1626
- return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize);
1627
- }
1607
+ { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
1628
1608
 
1629
1609
  /** Returns signed wrapped distance between the two angles passed in
1630
1610
  * @param {number} angleA
1631
1611
  * @param {number} angleB
1632
1612
  * @return {number}
1633
- * @memberof Utilities */
1613
+ * @memberof Math */
1634
1614
  function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
1635
1615
 
1636
1616
  /** Linearly interpolates between the angles passed in with wrapping
@@ -1638,25 +1618,25 @@ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*P
1638
1618
  * @param {number} angleB
1639
1619
  * @param {number} percent
1640
1620
  * @return {number}
1641
- * @memberof Utilities */
1621
+ * @memberof Math */
1642
1622
  function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
1643
1623
 
1644
1624
  /** Applies smoothstep function to the percentage value
1645
1625
  * @param {number} percent
1646
1626
  * @return {number}
1647
- * @memberof Utilities */
1627
+ * @memberof Math */
1648
1628
  function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
1649
1629
 
1650
1630
  /** Checks if the value passed in is a power of two
1651
1631
  * @param {number} value
1652
1632
  * @return {boolean}
1653
- * @memberof Utilities */
1633
+ * @memberof Math */
1654
1634
  function isPowerOfTwo(value) { return !(value & (value - 1)); }
1655
1635
 
1656
1636
  /** Returns the nearest power of two not less than the value
1657
1637
  * @param {number} value
1658
1638
  * @return {number}
1659
- * @memberof Utilities */
1639
+ * @memberof Math */
1660
1640
  function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
1661
1641
 
1662
1642
  /** Returns true if two axis aligned bounding boxes are overlapping
@@ -1666,7 +1646,7 @@ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
1666
1646
  * @param {Vector2} posB - Center of box B
1667
1647
  * @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
1668
1648
  * @return {boolean} - True if overlapping
1669
- * @memberof Utilities */
1649
+ * @memberof Math */
1670
1650
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
1671
1651
  {
1672
1652
  const dx = (posA.x - posB.x)*2;
@@ -1682,7 +1662,7 @@ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
1682
1662
  * @param {Vector2} pos - Center of box
1683
1663
  * @param {Vector2} size - Size of box
1684
1664
  * @return {boolean} - True if intersecting
1685
- * @memberof Utilities */
1665
+ * @memberof Math */
1686
1666
  function isIntersecting(start, end, pos, size)
1687
1667
  {
1688
1668
  // Liang-Barsky algorithm
@@ -1723,52 +1703,29 @@ function isIntersecting(start, end, pos, size)
1723
1703
  * @param {number} [t=time] - Value to use for time of the wave
1724
1704
  * @param {number} [offset] - Value to use for time offset of the wave
1725
1705
  * @return {number} - Value waving between 0 and amplitude
1726
- * @memberof Utilities */
1706
+ * @memberof Math */
1727
1707
  function wave(frequency=1, amplitude=1, t=time, offset=0)
1728
1708
  { return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
1729
1709
 
1730
- /** Formats seconds to mm:ss style for display purposes
1731
- * @param {number} t - time in seconds
1732
- * @return {string}
1733
- * @memberof Utilities */
1734
- function formatTime(t)
1735
- {
1736
- const sign = t < 0 ? '-' : '';
1737
- t = abs(t)|0;
1738
- return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
1739
- }
1740
-
1741
- /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
1742
- * @param {string} url - URL of JSON file
1743
- * @return {Promise<object>}
1744
- * @memberof Utilities */
1745
- async function fetchJSON(url)
1746
- {
1747
- const response = await fetch(url);
1748
- if (!response.ok)
1749
- throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1750
- return response.json();
1751
- }
1752
-
1753
1710
  /**
1754
1711
  * Check if object is a valid number, not NaN or undefined, but it may be infinite
1755
1712
  * @param {any} n
1756
1713
  * @return {boolean}
1757
- * @memberof Utilities */
1714
+ * @memberof Math */
1758
1715
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1759
1716
 
1760
1717
  /**
1761
1718
  * Check if object is a valid string or can be converted to one
1762
1719
  * @param {any} s
1763
1720
  * @return {boolean}
1764
- * @memberof Utilities */
1721
+ * @memberof Math */
1765
1722
  function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1766
1723
 
1767
1724
  /**
1768
1725
  * Check if object is an array
1769
1726
  * @param {any} a
1770
1727
  * @return {boolean}
1771
- * @memberof Utilities */
1728
+ * @memberof Math */
1772
1729
  function isArray(a) { return Array.isArray(a); }
1773
1730
 
1774
1731
  /**
@@ -1784,7 +1741,7 @@ function isArray(a) { return Array.isArray(a); }
1784
1741
  * @param {LineTestFunction} testFunction - Check if colliding
1785
1742
  * @param {Vector2} [normal] - Optional vector to store the normal
1786
1743
  * @return {Vector2|undefined} - Position of the collision or undefined if none found
1787
- * @memberof Utilities */
1744
+ * @memberof Math */
1788
1745
  function lineTest(posStart, posEnd, testFunction, normal)
1789
1746
  {
1790
1747
  ASSERT(isVector2(posStart), 'posStart must be a vec2');
@@ -1796,8 +1753,7 @@ function lineTest(posStart, posEnd, testFunction, normal)
1796
1753
  const dx = posEnd.x - posStart.x;
1797
1754
  const dy = posEnd.y - posStart.y;
1798
1755
  const totalLength = hypot(dx, dy);
1799
- if (!totalLength)
1800
- return;
1756
+ if (!totalLength) return;
1801
1757
 
1802
1758
  // current integer cell we are in
1803
1759
  const pos = posStart.floor();
@@ -2031,14 +1987,14 @@ class RandomGenerator
2031
1987
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
2032
1988
  * a = vec2(5); // set a to (5, 5)
2033
1989
  * b = vec2(); // set b to (0, 0)
2034
- * @memberof Utilities */
1990
+ * @memberof Math */
2035
1991
  function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
2036
1992
 
2037
1993
  /**
2038
1994
  * Check if object is a valid Vector2
2039
1995
  * @param {any} v
2040
1996
  * @return {boolean}
2041
- * @memberof Utilities */
1997
+ * @memberof Math */
2042
1998
  function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
2043
1999
 
2044
2000
  // vector2 asserts
@@ -2288,7 +2244,7 @@ class Vector2
2288
2244
  * @param {number} [b=1] - blue
2289
2245
  * @param {number} [a=1] - alpha
2290
2246
  * @return {Color}
2291
- * @memberof Utilities
2247
+ * @memberof Math
2292
2248
  */
2293
2249
  function rgb(r, g, b, a) { return new Color(r, g, b, a); }
2294
2250
 
@@ -2299,14 +2255,14 @@ function rgb(r, g, b, a) { return new Color(r, g, b, a); }
2299
2255
  * @param {number} [l=1] - lightness
2300
2256
  * @param {number} [a=1] - alpha
2301
2257
  * @return {Color}
2302
- * @memberof Utilities */
2258
+ * @memberof Math */
2303
2259
  function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
2304
2260
 
2305
2261
  /**
2306
2262
  * Check if object is a valid Color
2307
2263
  * @param {any} c
2308
2264
  * @return {boolean}
2309
- * @memberof Utilities */
2265
+ * @memberof Math */
2310
2266
  function isColor(c) { return c instanceof Color && c.isValid(); }
2311
2267
 
2312
2268
  // color asserts
@@ -2487,7 +2443,7 @@ class Color
2487
2443
  toString(useAlpha = true)
2488
2444
  {
2489
2445
  if (debug && !this.isValid())
2490
- return `#000`;
2446
+ return '#000';
2491
2447
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
2492
2448
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
2493
2449
  }
@@ -2544,67 +2500,67 @@ class Color
2544
2500
 
2545
2501
  /** Color - White #ffffff
2546
2502
  * @type {Color}
2547
- * @memberof Utilities */
2503
+ * @memberof Math */
2548
2504
  const WHITE = debugProtectConstant(rgb());
2549
2505
 
2550
2506
  /** Color - Clear White #757474ff with 0 alpha
2551
2507
  * @type {Color}
2552
- * @memberof Utilities */
2508
+ * @memberof Math */
2553
2509
  const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
2554
2510
 
2555
2511
  /** Color - Black #000000
2556
2512
  * @type {Color}
2557
- * @memberof Utilities */
2513
+ * @memberof Math */
2558
2514
  const BLACK = debugProtectConstant(rgb(0,0,0));
2559
2515
 
2560
2516
  /** Color - Clear Black #000000 with 0 alpha
2561
2517
  * @type {Color}
2562
- * @memberof Utilities */
2518
+ * @memberof Math */
2563
2519
  const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
2564
2520
 
2565
2521
  /** Color - Gray #808080
2566
2522
  * @type {Color}
2567
- * @memberof Utilities */
2523
+ * @memberof Math */
2568
2524
  const GRAY = debugProtectConstant(rgb(.5,.5,.5));
2569
2525
 
2570
2526
  /** Color - Red #ff0000
2571
2527
  * @type {Color}
2572
- * @memberof Utilities */
2528
+ * @memberof Math */
2573
2529
  const RED = debugProtectConstant(rgb(1,0,0));
2574
2530
 
2575
2531
  /** Color - Orange #ff8000
2576
2532
  * @type {Color}
2577
- * @memberof Utilities */
2533
+ * @memberof Math */
2578
2534
  const ORANGE = debugProtectConstant(rgb(1,.5,0));
2579
2535
 
2580
2536
  /** Color - Yellow #ffff00
2581
2537
  * @type {Color}
2582
- * @memberof Utilities */
2538
+ * @memberof Math */
2583
2539
  const YELLOW = debugProtectConstant(rgb(1,1,0));
2584
2540
 
2585
2541
  /** Color - Green #00ff00
2586
2542
  * @type {Color}
2587
- * @memberof Utilities */
2543
+ * @memberof Math */
2588
2544
  const GREEN = debugProtectConstant(rgb(0,1,0));
2589
2545
 
2590
2546
  /** Color - Cyan #00ffff
2591
2547
  * @type {Color}
2592
- * @memberof Utilities */
2548
+ * @memberof Math */
2593
2549
  const CYAN = debugProtectConstant(rgb(0,1,1));
2594
2550
 
2595
2551
  /** Color - Blue #0000ff
2596
2552
  * @type {Color}
2597
- * @memberof Utilities */
2553
+ * @memberof Math */
2598
2554
  const BLUE = debugProtectConstant(rgb(0,0,1));
2599
2555
 
2600
2556
  /** Color - Purple #8000ff
2601
2557
  * @type {Color}
2602
- * @memberof Utilities */
2558
+ * @memberof Math */
2603
2559
  const PURPLE = debugProtectConstant(rgb(.5,0,1));
2604
2560
 
2605
2561
  /** Color - Magenta #ff00ff
2606
2562
  * @type {Color}
2607
- * @memberof Utilities */
2563
+ * @memberof Math */
2608
2564
  const MAGENTA = debugProtectConstant(rgb(1,0,1));
2609
2565
 
2610
2566
  ///////////////////////////////////////////////////////////////////////////////
@@ -2690,6 +2646,84 @@ class Timer
2690
2646
  * @return {number} */
2691
2647
  valueOf() { return this.get(); }
2692
2648
  }
2649
+ /**
2650
+ * LittleJS Utility Classes and Functions
2651
+ * - General purpose math library
2652
+ * - Vector2 - fast, simple, easy 2D vector class
2653
+ * - Color - holds a rgba color with some math functions
2654
+ * - Timer - tracks time automatically
2655
+ * - RandomGenerator - seeded random number generator
2656
+ * @namespace Utilities
2657
+ */
2658
+
2659
+ /** Formats seconds to mm:ss style for display purposes
2660
+ * @param {number} t - time in seconds
2661
+ * @return {string}
2662
+ * @memberof Utilities */
2663
+ function formatTime(t)
2664
+ {
2665
+ const sign = t < 0 ? '-' : '';
2666
+ t = abs(t)|0;
2667
+ return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
2668
+ }
2669
+
2670
+ /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
2671
+ * @param {string} url - URL of JSON file
2672
+ * @return {Promise<object>}
2673
+ * @memberof Utilities */
2674
+ async function fetchJSON(url)
2675
+ {
2676
+ const response = await fetch(url);
2677
+ if (!response.ok)
2678
+ throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
2679
+ return response.json();
2680
+ }
2681
+
2682
+ ///////////////////////////////////////////////////////////////////////////////
2683
+
2684
+ /** Save a text file to disk
2685
+ * @param {string} text
2686
+ * @param {string} [filename]
2687
+ * @param {string} [type]
2688
+ * @memberof Utilities */
2689
+ function saveText(text, filename='text', type='text/plain')
2690
+ { saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
2691
+
2692
+ /** Save a canvas to disk
2693
+ * @param {HTMLCanvasElement|OffscreenCanvas} canvas
2694
+ * @param {string} [filename]
2695
+ * @param {string} [type]
2696
+ * @memberof Utilities */
2697
+ function saveCanvas(canvas, filename='screenshot', type='image/png')
2698
+ {
2699
+ if (canvas instanceof OffscreenCanvas)
2700
+ {
2701
+ // copy to temporary canvas and save
2702
+ const saveCanvas = document.createElement('canvas');
2703
+ saveCanvas.width = canvas.width;
2704
+ saveCanvas.height = canvas.height;
2705
+ saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
2706
+ saveDataURL(saveCanvas.toDataURL(type), filename);
2707
+ }
2708
+ else
2709
+ saveDataURL(canvas.toDataURL(type), filename);
2710
+ }
2711
+
2712
+ /** Save a data url to disk
2713
+ * @param {string} url
2714
+ * @param {string} [filename]
2715
+ * @param {number} [revokeTime] - how long before revoking the url
2716
+ * @memberof Utilities */
2717
+ function saveDataURL(url, filename='download', revokeTime)
2718
+ {
2719
+ // create link for saving screenshots
2720
+ const link = document.createElement('a');
2721
+ link.download = filename;
2722
+ link.href = url;
2723
+ link.click();
2724
+ if (revokeTime !== undefined)
2725
+ setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
2726
+ }
2693
2727
  /**
2694
2728
  * LittleJS Engine Settings
2695
2729
  * - All settings for the engine are here
@@ -2727,7 +2761,7 @@ let cameraScale = 32;
2727
2761
  * @memberof Settings */
2728
2762
  let canvasColorTiles = true;
2729
2763
 
2730
- /** Color to clear the canvas to before render
2764
+ /** Color to clear the canvas to before render, does not clear if alpha is 0
2731
2765
  * @type {Color}
2732
2766
  * @memberof Draw */
2733
2767
  let canvasClearColor = CLEAR_BLACK;
@@ -2764,15 +2798,7 @@ let canvasFixedSize = vec2();
2764
2798
  * @type {boolean}
2765
2799
  * @default
2766
2800
  * @memberof Settings */
2767
- let canvasPixelated = true;
2768
-
2769
- /** Use nearest canvas scaling for more pixelated look
2770
- * - If enabled sets css image-rendering:pixelated
2771
- * - This defaults to false because text looks better with smoothing
2772
- * @type {boolean}
2773
- * @default
2774
- * @memberof Settings */
2775
- let overlayCanvasPixelated = false;
2801
+ let canvasPixelated = false;
2776
2802
 
2777
2803
  /** Disables texture filtering for crisper pixel art
2778
2804
  * @type {boolean}
@@ -2820,13 +2846,19 @@ let glCircleSides = 32;
2820
2846
  * @type {Vector2}
2821
2847
  * @default Vector2(16,16)
2822
2848
  * @memberof Settings */
2823
- let tileSizeDefault = vec2(16);
2849
+ let tileDefaultSize = vec2(16);
2824
2850
 
2825
- /** How many pixels smaller to draw tiles to prevent bleeding from neighbors
2851
+ /** Default padding pixels around tiles
2826
2852
  * @type {number}
2827
2853
  * @default
2828
2854
  * @memberof Settings */
2829
- let tileFixBleedScale = 0;
2855
+ let tileDefaultPadding = 0;
2856
+
2857
+ /** Default amount of pixels smaller to draw tiles to prevent neighbor bleeding
2858
+ * @type {number}
2859
+ * @default
2860
+ * @memberof Settings */
2861
+ let tileDefaultBleed = 0;
2830
2862
 
2831
2863
  ///////////////////////////////////////////////////////////////////////////////
2832
2864
  // Object settings
@@ -3038,7 +3070,7 @@ function setCameraScale(scale) { cameraScale = scale; }
3038
3070
  * @memberof Settings */
3039
3071
  function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
3040
3072
 
3041
- /** Set color to clear the canvas to before render
3073
+ /** Set color to clear the canvas to before render, does not clear if alpha is 0
3042
3074
  * @param {Color} color
3043
3075
  * @memberof Settings */
3044
3076
  function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
@@ -3064,7 +3096,6 @@ function setCanvasMaxAspect(aspect) { canvasMaxAspect = aspect; }
3064
3096
  function setCanvasFixedSize(size) { canvasFixedSize = size.copy(); }
3065
3097
 
3066
3098
  /** Use nearest scaling algorithm for canvas for more pixelated look
3067
- * - If enabled sets css image-rendering:pixelated
3068
3099
  * @param {boolean} pixelated
3069
3100
  * @memberof Settings */
3070
3101
  function setCanvasPixelated(pixelated)
@@ -3076,18 +3107,6 @@ function setCanvasPixelated(pixelated)
3076
3107
  glCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
3077
3108
  }
3078
3109
 
3079
- /** Use nearest scaling algorithm for canvas for more pixelated look
3080
- * - If enabled sets css image-rendering:pixelated
3081
- * - This defaults to false because text looks better with smoothing
3082
- * @param {boolean} pixelated
3083
- * @memberof Settings */
3084
- function setOverlayCanvasPixelated(pixelated)
3085
- {
3086
- overlayCanvasPixelated = pixelated;
3087
- if (overlayCanvas)
3088
- overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
3089
- }
3090
-
3091
3110
  /** Disables texture filtering for crisper pixel art
3092
3111
  * @param {boolean} pixelated
3093
3112
  * @memberof Settings */
@@ -3131,12 +3150,17 @@ function setGLCircleSides(sides) { glCircleSides = sides; }
3131
3150
  /** Set default size of tiles in pixels
3132
3151
  * @param {Vector2} size
3133
3152
  * @memberof Settings */
3134
- function setTileSizeDefault(size) { tileSizeDefault = size.copy(); }
3153
+ function setTileDefaultSize(size) { tileDefaultSize = size.copy(); }
3135
3154
 
3136
- /** Set to prevent tile bleeding from neighbors in pixels
3137
- * @param {number} scale
3155
+ /** Default padding pixels around tiles
3156
+ * @param {number} padding
3138
3157
  * @memberof Settings */
3139
- function setTileFixBleedScale(scale) { tileFixBleedScale = scale; }
3158
+ function setTileDefaultPadding(padding) { tileDefaultPadding = padding; }
3159
+
3160
+ /** Default amount of pixels smaller to draw tiles to prevent neighbor bleeding
3161
+ * @param {number} bleed
3162
+ * @memberof Settings */
3163
+ function setTileDefaultBleed(bleed) { tileDefaultBleed = bleed; }
3140
3164
 
3141
3165
  /** Set if collisions between objects are enabled
3142
3166
  * @param {boolean} enable
@@ -3287,7 +3311,7 @@ function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUn
3287
3311
  /** Set if watermark with FPS should be shown
3288
3312
  * @param {boolean} show
3289
3313
  * @memberof Debug */
3290
- function setShowWatermark(show) { showWatermark = show; }
3314
+ function setDebugWatermark(show) { debugWatermark = show; }
3291
3315
 
3292
3316
  /** Set key code used to toggle debug mode, Esc by default
3293
3317
  * @param {string} key
@@ -3723,10 +3747,11 @@ class EngineObject
3723
3747
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
3724
3748
  getMirrorSign() { return this.mirror ? -1 : 1; }
3725
3749
 
3726
- /** Attaches a child to this with a given local transform
3750
+ /** Attaches a child to this with a local transform, returns child for chaining
3727
3751
  * @param {EngineObject} child
3728
3752
  * @param {Vector2} [localPos=(0,0)]
3729
- * @param {number} [localAngle] */
3753
+ * @param {number} [localAngle]
3754
+ * @return {EngineObject} The child object added */
3730
3755
  addChild(child, localPos=vec2(), localAngle=0)
3731
3756
  {
3732
3757
  ASSERT(!child.parent && !this.children.includes(child));
@@ -3736,6 +3761,7 @@ class EngineObject
3736
3761
  child.parent = this;
3737
3762
  child.localPos = localPos.copy();
3738
3763
  child.localAngle = localAngle;
3764
+ return child;
3739
3765
  }
3740
3766
 
3741
3767
  /** Removes a child from this one
@@ -3802,8 +3828,7 @@ class EngineObject
3802
3828
  /** Render debug info for this object */
3803
3829
  renderDebugInfo()
3804
3830
  {
3805
- if (!debug)
3806
- return;
3831
+ if (!debug) return;
3807
3832
 
3808
3833
  // show object info for debugging
3809
3834
  const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
@@ -3826,7 +3851,6 @@ class EngineObject
3826
3851
  * There are 3 canvas/contexts available to draw to...
3827
3852
  * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
3828
3853
  * glCanvas - Used by the accelerated WebGL batch rendering system.
3829
- * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
3830
3854
  *
3831
3855
  * The WebGL rendering system is very fast with some caveats...
3832
3856
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
@@ -3846,21 +3870,6 @@ let mainCanvas;
3846
3870
  * @memberof Draw */
3847
3871
  let mainContext;
3848
3872
 
3849
- /** A canvas that appears on top of everything the same size as mainCanvas
3850
- * @type {HTMLCanvasElement}
3851
- * @memberof Draw */
3852
- let overlayCanvas;
3853
-
3854
- /** 2d context for overlayCanvas
3855
- * @type {CanvasRenderingContext2D}
3856
- * @memberof Draw */
3857
- let overlayContext;
3858
-
3859
- /** The default canvas to use for drawing, usually mainCanvas
3860
- * @type {HTMLCanvasElement|OffscreenCanvas}
3861
- * @memberof Draw */
3862
- let drawCanvas;
3863
-
3864
3873
  /** The default 2d context to use for drawing, usually mainContext
3865
3874
  * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
3866
3875
  * @memberof Draw */
@@ -3876,6 +3885,16 @@ let workCanvas;
3876
3885
  * @memberof Draw */
3877
3886
  let workContext;
3878
3887
 
3888
+ /** Offscreen canvas with willReadFrequently that can be used for image processing
3889
+ * @type {OffscreenCanvas}
3890
+ * @memberof Draw */
3891
+ let workReadCanvas;
3892
+
3893
+ /** Offscreen canvas with willReadFrequently that can be used for image processing
3894
+ * @type {OffscreenCanvasRenderingContext2D}
3895
+ * @memberof Draw */
3896
+ let workReadContext;
3897
+
3879
3898
  /** The size of the main canvas (and other secondary canvases)
3880
3899
  * @type {Vector2}
3881
3900
  * @memberof Draw */
@@ -3898,7 +3917,7 @@ let drawCount;
3898
3917
  * - This can take vecs or floats for easier use and conversion
3899
3918
  * - If an index is passed in, the tile size and index will determine the position
3900
3919
  * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
3901
- * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
3920
+ * @param {Vector2|number} [size] - Size of tile in pixels
3902
3921
  * @param {number} [textureIndex] - Texture index to use
3903
3922
  * @param {number} [padding] - How many pixels padding around tiles
3904
3923
  * @return {TileInfo}
@@ -3908,7 +3927,7 @@ let drawCount;
3908
3927
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
3909
3928
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
3910
3929
  * @memberof Draw */
3911
- function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3930
+ function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding)
3912
3931
  {
3913
3932
  if (headlessMode)
3914
3933
  return new TileInfo;
@@ -3947,13 +3966,13 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3947
3966
  class TileInfo
3948
3967
  {
3949
3968
  /** Create a tile info object
3950
- * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3951
- * @param {Vector2} [size=tileSizeDefault] - Size of tile in pixels
3952
- * @param {number} [textureIndex] - Texture index to use
3953
- * @param {number} [padding] - How many pixels padding around tiles
3954
- * @param {number} [bleedScale] - How many pixels smaller to draw tiles
3969
+ * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3970
+ * @param {Vector2} [size] - Size of tile in pixels
3971
+ * @param {number} [textureIndex] - Texture index to use
3972
+ * @param {number} [padding] - How many pixels padding around tiles
3973
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3955
3974
  */
3956
- constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0, bleedScale=tileFixBleedScale)
3975
+ constructor(pos=vec2(), size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3957
3976
  {
3958
3977
  /** @property {Vector2} - Top left corner of tile in pixels */
3959
3978
  this.pos = pos.copy();
@@ -3966,7 +3985,7 @@ class TileInfo
3966
3985
  /** @property {TextureInfo} - The texture info for this tile */
3967
3986
  this.textureInfo = textureInfos[this.textureIndex];
3968
3987
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
3969
- this.bleedScale = bleedScale;
3988
+ this.bleed = bleed;
3970
3989
  }
3971
3990
 
3972
3991
  /** Returns a copy of this tile offset by a vector
@@ -3974,7 +3993,7 @@ class TileInfo
3974
3993
  * @return {TileInfo}
3975
3994
  */
3976
3995
  offset(offset)
3977
- { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleedScale); }
3996
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleed); }
3978
3997
 
3979
3998
  /** Returns a copy of this tile offset by a number of animation frames
3980
3999
  * @param {number} frame - Offset to apply in animation frames
@@ -3997,7 +4016,7 @@ class TileInfo
3997
4016
  this.size = textureInfo.size.copy();
3998
4017
  this.textureInfo = textureInfo;
3999
4018
  // do not use padding or bleed
4000
- this.bleedScale = this.padding = 0;
4019
+ this.bleed = this.padding = 0;
4001
4020
  return this;
4002
4021
  }
4003
4022
  }
@@ -4063,7 +4082,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
4063
4082
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4064
4083
 
4065
4084
  const textureInfo = tileInfo && tileInfo.textureInfo;
4066
- const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
4085
+ const bleed = tileInfo?.bleed ?? 0;
4067
4086
  if (useWebGL && glEnable)
4068
4087
  {
4069
4088
  ASSERT(!!glContext, 'WebGL is not enabled!');
@@ -4078,13 +4097,13 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
4078
4097
  const w = tileInfo.size.x * sizeInverse.x;
4079
4098
  const h = tileInfo.size.y * sizeInverse.y;
4080
4099
  glSetTexture(textureInfo.glTexture);
4081
- if (bleedScale)
4100
+ if (bleed)
4082
4101
  {
4083
- const tileImageFixBleedX = sizeInverse.x*bleedScale;
4084
- const tileImageFixBleedY = sizeInverse.y*bleedScale;
4102
+ const bleedX = sizeInverse.x*bleed;
4103
+ const bleedY = sizeInverse.y*bleed;
4085
4104
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
4086
- x + tileImageFixBleedX, y + tileImageFixBleedY,
4087
- x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
4105
+ x + bleedX, y + bleedY,
4106
+ x - bleedX + w, y - bleedY + h,
4088
4107
  color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
4089
4108
  }
4090
4109
  else
@@ -4112,7 +4131,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
4112
4131
  // calculate uvs and render
4113
4132
  const x = tileInfo.pos.x, y = tileInfo.pos.y;
4114
4133
  const w = tileInfo.size.x, h = tileInfo.size.y;
4115
- drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor, bleedScale);
4134
+ drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor, bleed);
4116
4135
  }
4117
4136
  else
4118
4137
  {
@@ -4438,7 +4457,11 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
4438
4457
  ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
4439
4458
 
4440
4459
  if (!screenSpace)
4441
- [pos, size, angle] = worldToScreenTransform(pos, size, angle);
4460
+ {
4461
+ pos = worldToScreen(pos);
4462
+ size = size.scale(cameraScale);
4463
+ angle -= cameraAngle;
4464
+ }
4442
4465
  context.save();
4443
4466
  context.translate(pos.x+.5, pos.y+.5);
4444
4467
  context.rotate(angle);
@@ -4477,26 +4500,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
4477
4500
  drawTextScreen(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
4478
4501
  }
4479
4502
 
4480
- /** Draw text on overlay canvas in world space
4481
- * Automatically splits new lines into rows
4482
- * @param {string|number} text
4483
- * @param {Vector2} pos
4484
- * @param {number} [size]
4485
- * @param {Color} [color=(1,1,1,1)]
4486
- * @param {number} [lineWidth]
4487
- * @param {Color} [lineColor=(0,0,0,1)]
4488
- * @param {CanvasTextAlign} [textAlign='center']
4489
- * @param {string} [font=fontDefault]
4490
- * @param {string} [fontStyle]
4491
- * @param {number} [maxWidth]
4492
- * @param {number} [angle]
4493
- * @memberof Draw */
4494
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
4495
- {
4496
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
4497
- }
4498
-
4499
- /** Draw text on overlay canvas in screen space
4503
+ /** Draw text in screen space
4500
4504
  * Automatically splits new lines into rows
4501
4505
  * @param {string|number} text
4502
4506
  * @param {Vector2} pos
@@ -4509,9 +4513,9 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
4509
4513
  * @param {string} [fontStyle]
4510
4514
  * @param {number} [maxWidth]
4511
4515
  * @param {number} [angle]
4512
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
4516
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4513
4517
  * @memberof Draw */
4514
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
4518
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
4515
4519
  {
4516
4520
  ASSERT(isString(text), 'text must be a string');
4517
4521
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4555,16 +4559,16 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4555
4559
  * @memberof Draw */
4556
4560
  function screenToWorld(screenPos)
4557
4561
  {
4562
+ ASSERT(isVector2(screenPos), 'screenPos must be a vec2');
4563
+
4558
4564
  let x = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
4559
4565
  let y = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
4560
4566
  if (cameraAngle)
4561
4567
  {
4562
4568
  // apply camera rotation
4563
4569
  const c = cos(-cameraAngle), s = sin(-cameraAngle);
4564
- const rotatedX = x * c - y * s;
4565
- const rotatedY = x * s + y * c;
4566
- x = rotatedX;
4567
- y = rotatedY;
4570
+ const xr = x * c - y * s, yr = x * s + y * c;
4571
+ x = xr; y = yr;
4568
4572
  }
4569
4573
  return new Vector2(x + cameraPos.x, y + cameraPos.y);
4570
4574
  }
@@ -4575,16 +4579,16 @@ function screenToWorld(screenPos)
4575
4579
  * @memberof Draw */
4576
4580
  function worldToScreen(worldPos)
4577
4581
  {
4582
+ ASSERT(isVector2(worldPos), 'worldPos must be a vec2');
4583
+
4578
4584
  let x = worldPos.x - cameraPos.x;
4579
4585
  let y = worldPos.y - cameraPos.y;
4580
4586
  if (cameraAngle)
4581
4587
  {
4582
4588
  // apply inverse camera rotation
4583
4589
  const c = cos(cameraAngle), s = sin(cameraAngle);
4584
- const rotatedX = x * c - y * s;
4585
- const rotatedY = x * s + y * c;
4586
- x = rotatedX;
4587
- y = rotatedY;
4590
+ const xr = x * c - y * s, yr = x * s + y * c;
4591
+ x = xr; y = yr;
4588
4592
  }
4589
4593
  return new Vector2
4590
4594
  (
@@ -4599,16 +4603,16 @@ function worldToScreen(worldPos)
4599
4603
  * @memberof Draw */
4600
4604
  function screenToWorldDelta(screenDelta)
4601
4605
  {
4606
+ ASSERT(isVector2(screenDelta), 'screenDelta must be a vec2');
4607
+
4602
4608
  let x = screenDelta.x / cameraScale;
4603
4609
  let y = screenDelta.y / -cameraScale;
4604
4610
  if (cameraAngle)
4605
4611
  {
4606
4612
  // apply camera rotation
4607
4613
  const c = cos(-cameraAngle), s = sin(-cameraAngle);
4608
- const rotatedX = x * c - y * s;
4609
- const rotatedY = x * s + y * c;
4610
- x = rotatedX;
4611
- y = rotatedY;
4614
+ const xr = x * c - y * s, yr = x * s + y * c;
4615
+ x = xr; y = yr;
4612
4616
  }
4613
4617
  return new Vector2(x, y);
4614
4618
  }
@@ -4619,16 +4623,16 @@ function screenToWorldDelta(screenDelta)
4619
4623
  * @memberof Draw */
4620
4624
  function worldToScreenDelta(worldDelta)
4621
4625
  {
4626
+ ASSERT(isVector2(worldDelta), 'worldDelta must be a vec2');
4627
+
4622
4628
  let x = worldDelta.x;
4623
4629
  let y = worldDelta.y;
4624
4630
  if (cameraAngle)
4625
4631
  {
4626
4632
  // apply inverse camera rotation
4627
4633
  const c = cos(cameraAngle), s = sin(cameraAngle);
4628
- const rotatedX = x * c - y * s;
4629
- const rotatedY = x * s + y * c;
4630
- x = rotatedX;
4631
- y = rotatedY;
4634
+ const xr = x * c - y * s, yr = x * s + y * c;
4635
+ x = xr; y = yr;
4632
4636
  }
4633
4637
  return new Vector2(x * cameraScale, y * -cameraScale);
4634
4638
  }
@@ -4641,6 +4645,10 @@ function worldToScreenDelta(worldDelta)
4641
4645
  * @memberof Draw */
4642
4646
  function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
4643
4647
  {
4648
+ ASSERT(isVector2(screenPos), 'screenPos must be a vec2');
4649
+ ASSERT(isVector2(screenSize), 'screenSize must be a vec2');
4650
+ ASSERT(isNumber(screenAngle), 'screenAngle must be a number');
4651
+
4644
4652
  return [
4645
4653
  screenToWorld(screenPos),
4646
4654
  screenSize.scale(1/cameraScale),
@@ -4648,28 +4656,13 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
4648
4656
  ];
4649
4657
  }
4650
4658
 
4651
- /** Convert world space transform to screen space
4652
- * @param {Vector2} worldPos
4653
- * @param {Vector2} worldSize
4654
- * @param {number} [worldAngle]
4655
- * @return {[Vector2, Vector2, number]} - [pos, size, angle]
4656
- * @memberof Draw */
4657
- function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
4658
- {
4659
- return [
4660
- worldToScreen(worldPos),
4661
- worldSize.scale(cameraScale),
4662
- worldAngle - cameraAngle
4663
- ];
4664
- }
4665
-
4666
4659
  /** Get the size of the camera window in world space
4667
4660
  * @return {Vector2}
4668
4661
  * @memberof Draw */
4669
4662
  function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
4670
4663
 
4671
- /** Check if a point or circle is on screen
4672
- * If size is a Vector2, uses the largest dimension as diameter
4664
+ /** Check if a box, point, or circle is on screen with a circle test
4665
+ * If size is a Vector2, uses the length as diameter
4673
4666
  * This can be used to cull offscreen objects from render or update
4674
4667
  * @param {Vector2} pos - world space position
4675
4668
  * @param {Vector2|number} size - world space size or diameter
@@ -4677,12 +4670,30 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
4677
4670
  * @memberof Draw */
4678
4671
  function isOnScreen(pos, size=0)
4679
4672
  {
4680
- pos = worldToScreen(pos);
4673
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4674
+ ASSERT(isVector2(size) || isNumber(size), 'size must be a vec2 or number');
4675
+
4676
+ // optimized circle on screen test
4677
+ // pos = worldToScreen(pos);
4678
+ let x = pos.x - cameraPos.x;
4679
+ let y = pos.y - cameraPos.y;
4680
+ if (cameraAngle)
4681
+ {
4682
+ // apply inverse camera rotation
4683
+ const c = cos(cameraAngle), s = sin(cameraAngle);
4684
+ const xr = x * c - y * s, yr = x * s + y * c;
4685
+ x = xr; y = yr;
4686
+ }
4687
+ x *= cameraScale*2; y *= -cameraScale*2;
4688
+
4681
4689
  if (size instanceof Vector2)
4682
- size = max(size.x, size.y); // use largest dimension
4683
- size *= cameraScale/2;
4684
- return pos.x + size > 0 && pos.x - size < mainCanvasSize.x &&
4685
- pos.y + size > 0 && pos.y - size < mainCanvasSize.y;
4690
+ size = size.length(); // use length of vector as diameter
4691
+ size *= cameraScale;
4692
+
4693
+ // check against screen bounds
4694
+ const w = mainCanvasSize.x, h = mainCanvasSize.y;
4695
+ return x + size > -w && x - size < w &&
4696
+ y + size > -h && y - size < h;
4686
4697
  }
4687
4698
 
4688
4699
  /** Enable normal or additive blend mode
@@ -4696,18 +4707,19 @@ function setBlendMode(additive=false, context)
4696
4707
  context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
4697
4708
  }
4698
4709
 
4699
- /** Combines all LittleJS canvases onto the main canvas and clears them
4700
- * This is necessary for things like saving a screenshot
4710
+ /** Combines LittleJS canvases onto the main canvas
4711
+ * This is necessary for things like screenshots and video
4701
4712
  * @memberof Draw */
4702
4713
  function combineCanvases()
4703
4714
  {
4704
- // combine canvases
4705
- glCopyToContext(mainContext);
4706
- mainContext.drawImage(overlayCanvas, 0, 0);
4707
-
4708
- // clear canvases
4709
- glClearCanvas();
4710
- overlayCanvas.width |= 0;
4715
+ const w = mainCanvasSize.x, h = mainCanvasSize.y;
4716
+ workCanvas.width = w;
4717
+ workCanvas.height = h;
4718
+ workContext.fillRect(0,0,w,h); // remove background alpha
4719
+ glCopyToContext(workContext);
4720
+ workContext.drawImage(mainCanvas, 0, 0);
4721
+ mainCanvas.width |= 0;
4722
+ mainContext.drawImage(workCanvas, 0, 0);
4711
4723
  }
4712
4724
 
4713
4725
  /** Helper function to draw an image with color and additive color applied
@@ -4724,18 +4736,18 @@ function combineCanvases()
4724
4736
  * @param {number} dHeight
4725
4737
  * @param {Color} color
4726
4738
  * @param {Color} [additiveColor]
4727
- * @param {number} [bleedScale] - How much to shrink the source, used to fix bleeding
4739
+ * @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding
4728
4740
  * @memberof Draw */
4729
- function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleedScale=0)
4741
+ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
4730
4742
  {
4731
4743
  function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
4732
4744
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
4733
- const sx2 = bleedScale;
4734
- const sy2 = bleedScale;
4745
+ const sx2 = bleed;
4746
+ const sy2 = bleed;
4735
4747
  sWidth = max(1,sWidth|0);
4736
4748
  sHeight = max(1,sHeight|0);
4737
- const sWidth2 = sWidth - 2*bleedScale;
4738
- const sHeight2 = sHeight - 2*bleedScale;
4749
+ const sWidth2 = sWidth - 2*bleed;
4750
+ const sHeight2 = sHeight - 2*bleed;
4739
4751
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
4740
4752
  {
4741
4753
  // white texture with no additive alpha, no need to tint
@@ -4746,12 +4758,12 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4746
4758
  else
4747
4759
  {
4748
4760
  // copy to offscreen canvas
4749
- workCanvas.width = sWidth;
4750
- workCanvas.height = sHeight;
4751
- workContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
4761
+ workReadCanvas.width = sWidth;
4762
+ workReadCanvas.height = sHeight;
4763
+ workReadContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
4752
4764
 
4753
4765
  // tint image using offscreen work context
4754
- const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
4766
+ const imageData = workReadContext.getImageData(0, 0, sWidth, sHeight);
4755
4767
  const data = imageData.data;
4756
4768
  if (additiveColor && !isBlack(additiveColor))
4757
4769
  {
@@ -4760,8 +4772,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4760
4772
  const colorAdd = [additiveColor.r * 255, additiveColor.g * 255, additiveColor.b * 255, additiveColor.a * 255];
4761
4773
  for (let i = 0; i < data.length; ++i)
4762
4774
  data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
4763
- workContext.putImageData(imageData, 0, 0);
4764
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4775
+ workReadContext.putImageData(imageData, 0, 0);
4776
+ context.drawImage(workReadCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4765
4777
  }
4766
4778
  else
4767
4779
  {
@@ -4772,9 +4784,9 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4772
4784
  data[i+1] *= color.g;
4773
4785
  data[i+2] *= color.b;
4774
4786
  }
4775
- workContext.putImageData(imageData, 0, 0);
4787
+ workReadContext.putImageData(imageData, 0, 0);
4776
4788
  context.globalAlpha = color.a;
4777
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4789
+ context.drawImage(workReadCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4778
4790
  context.globalAlpha = 1;
4779
4791
  }
4780
4792
  }
@@ -4836,7 +4848,7 @@ class FontImage
4836
4848
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
4837
4849
  {
4838
4850
  // load default font image
4839
- if (!engineFontImage)
4851
+ if (!image && !engineFontImage)
4840
4852
  {
4841
4853
  engineFontImage = new Image;
4842
4854
  engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
@@ -4859,23 +4871,14 @@ class FontImage
4859
4871
  this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
4860
4872
  }
4861
4873
 
4862
- /** Draw text on overlay canvas in world space using the image font
4863
- * @param {string|number} text
4864
- * @param {Vector2} pos
4865
- * @param {number} [scale]
4866
- * @param {boolean} [center]
4867
- */
4868
- drawTextOverlay(text, pos, scale=4, center)
4869
- { this.drawText(text, pos, scale, center, overlayContext); }
4870
-
4871
- /** Draw text on overlay canvas in screen space using the image font
4874
+ /** Draw text in screen space using the image font
4872
4875
  * @param {string|number} text
4873
4876
  * @param {Vector2} pos
4874
4877
  * @param {number} [scale]
4875
4878
  * @param {boolean} [center]
4876
4879
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4877
4880
  */
4878
- drawTextScreen(text, pos, scale=4, center=true, context=overlayContext)
4881
+ drawTextScreen(text, pos, scale=4, center=true, context=drawContext)
4879
4882
  {
4880
4883
  context.save();
4881
4884
  const size = this.tileSize;
@@ -5268,8 +5271,7 @@ function inputInit()
5268
5271
  }
5269
5272
  function onMouseDown(e)
5270
5273
  {
5271
- if (isTouchDevice && touchInputEnable)
5272
- return;
5274
+ if (isTouchDevice && touchInputEnable) return;
5273
5275
 
5274
5276
  // fix stalled audio requiring user interaction
5275
5277
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
@@ -5320,8 +5322,7 @@ function inputInit()
5320
5322
  let wasTouching;
5321
5323
  function handleTouch(e)
5322
5324
  {
5323
- if (!touchInputEnable)
5324
- return;
5325
+ if (!touchInputEnable) return;
5325
5326
 
5326
5327
  // route touch to gamepad
5327
5328
  if (touchGamepadEnable)
@@ -5385,8 +5386,7 @@ function inputInit()
5385
5386
  }
5386
5387
 
5387
5388
  // don't process touch gamepad if paused
5388
- if (paused)
5389
- return;
5389
+ if (paused) return;
5390
5390
 
5391
5391
  // get center of left and right sides
5392
5392
  const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
@@ -5625,7 +5625,7 @@ function inputRender()
5625
5625
  return;
5626
5626
 
5627
5627
  // setup the canvas
5628
- const context = overlayContext;
5628
+ const context = mainContext;
5629
5629
  context.save();
5630
5630
  context.globalAlpha = alpha*touchGamepadAlpha;
5631
5631
  context.strokeStyle = '#fff';
@@ -5867,12 +5867,12 @@ class Sound
5867
5867
  ///////////////////////////////////////////////////////////////////////////////
5868
5868
 
5869
5869
  /**
5870
- * Sound Wave Object - Stores a wave sound for later use and can be played positionally
5871
- * - this can be used to play wave, mp3, and ogg files
5870
+ * Sound Wave Object - Loads and stores an audio file for later use
5871
+ * - this can be used to load and play wave, mp3, and ogg files
5872
5872
  * @extends Sound
5873
5873
  * @memberof Audio
5874
5874
  * @example
5875
- * // create a sound
5875
+ * // load an audio asset file
5876
5876
  * const sound_example = new SoundWave('sound.mp3');
5877
5877
  *
5878
5878
  * // play the sound
@@ -6072,8 +6072,7 @@ class SoundInstance
6072
6072
  /** Pause this sound instance */
6073
6073
  pause()
6074
6074
  {
6075
- if (this.isPaused())
6076
- return;
6075
+ if (this.isPaused()) return;
6077
6076
 
6078
6077
  // save current time and stop sound
6079
6078
  this.pausedTime = this.getCurrentTime();
@@ -6085,8 +6084,7 @@ class SoundInstance
6085
6084
  /** Unpauses this sound instance */
6086
6085
  resume()
6087
6086
  {
6088
- if (!this.isPaused())
6089
- return;
6087
+ if (!this.isPaused()) return;
6090
6088
 
6091
6089
  // restart sound from paused time
6092
6090
  this.start(this.pausedTime);
@@ -6824,26 +6822,26 @@ class TileLayer extends CanvasLayer
6824
6822
  if (!this.context) return;
6825
6823
 
6826
6824
  // save current render settings
6827
- /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number]} */
6828
- this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale];
6825
+ /** @type {[CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6826
+ this.savedRenderSettings = [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
6829
6827
 
6830
6828
  // set the draw canvas and context to this layer
6831
6829
  // use camera settings to match this layer's canvas
6832
- drawCanvas = this.canvas;
6833
6830
  drawContext = this.context;
6834
6831
  cameraPos = this.size.scale(.5);
6835
6832
  const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
6836
6833
  cameraScale = tileSize.x;
6834
+ canvasClearColor = CLEAR_BLACK;
6837
6835
  mainCanvasSize = this.size.multiply(tileSize);
6838
6836
  if (clear)
6839
6837
  {
6840
6838
  // clear and set size
6841
- drawCanvas.width = mainCanvasSize.x;
6842
- drawCanvas.height = mainCanvasSize.y;
6839
+ this.canvas.width = mainCanvasSize.x;
6840
+ this.canvas.height = mainCanvasSize.y;
6843
6841
  }
6844
6842
 
6845
6843
  // disable smoothing for pixel art
6846
- this.context.imageSmoothingEnabled = !tilesPixelated;
6844
+ drawContext.imageSmoothingEnabled = !tilesPixelated;
6847
6845
 
6848
6846
  // setup gl rendering if enabled
6849
6847
  glPreRender();
@@ -6856,10 +6854,10 @@ class TileLayer extends CanvasLayer
6856
6854
 
6857
6855
  ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6858
6856
  glCopyToContext(drawContext);
6859
- //debugSaveCanvas(this.canvas);
6857
+ //saveCanvas(this.canvas);
6860
6858
 
6861
6859
  // set stuff back to normal
6862
- [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale] = this.savedRenderSettings;
6860
+ [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6863
6861
  }
6864
6862
 
6865
6863
  /** Draw the tile at a given position in the tile grid
@@ -6927,8 +6925,7 @@ class TileCollisionLayer extends TileLayer
6927
6925
  /** Destroy this tile layer */
6928
6926
  destroy()
6929
6927
  {
6930
- if (this.destroyed)
6931
- return;
6928
+ if (this.destroyed) return;
6932
6929
 
6933
6930
  // remove from collision layers array and destroy
6934
6931
  const index = tileCollisionLayers.indexOf(this);
@@ -7465,10 +7462,11 @@ function medalsInit(saveName)
7465
7462
 
7466
7463
  // engine automatically renders medals
7467
7464
  engineAddPlugin(undefined, medalsRender);
7465
+
7466
+ // plugin functions
7468
7467
  function medalsRender()
7469
7468
  {
7470
- if (!medalsDisplayQueue.length)
7471
- return;
7469
+ if (!medalsDisplayQueue.length) return;
7472
7470
 
7473
7471
  // update first medal in queue
7474
7472
  const medal = medalsDisplayQueue[0];
@@ -7558,8 +7556,7 @@ class Medal
7558
7556
  /** Unlocks a medal if not already unlocked */
7559
7557
  unlock()
7560
7558
  {
7561
- if (medalsPreventUnlock || this.unlocked)
7562
- return;
7559
+ if (medalsPreventUnlock || this.unlocked) return;
7563
7560
 
7564
7561
  // save the medal
7565
7562
  ASSERT(medalsSaveName, 'save name must be set');
@@ -7572,10 +7569,10 @@ class Medal
7572
7569
  */
7573
7570
  render(hidePercent=0)
7574
7571
  {
7575
- const context = overlayContext;
7572
+ const context = mainContext;
7576
7573
  const width = min(medalDisplaySize.x, mainCanvas.width);
7577
7574
  const height = medalDisplaySize.y;
7578
- const x = overlayCanvas.width - width;
7575
+ const x = mainCanvas.width - width;
7579
7576
  const y = -height*hidePercent;
7580
7577
  const backgroundColor = hsl(0,0,.9);
7581
7578
 
@@ -7616,7 +7613,7 @@ class Medal
7616
7613
  {
7617
7614
  // draw the image or icon
7618
7615
  if (this.image)
7619
- overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
7616
+ mainContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
7620
7617
  else
7621
7618
  drawTextScreen(this.icon, pos, size*.7, BLACK);
7622
7619
  }
@@ -7637,7 +7634,7 @@ class Medal
7637
7634
  * @namespace WebGL
7638
7635
  */
7639
7636
 
7640
- /** The WebGL canvas which appears above the main canvas and below the overlay canvas
7637
+ /** The WebGL canvas which appears below the main canvas
7641
7638
  * @type {HTMLCanvasElement}
7642
7639
  * @memberof WebGL */
7643
7640
  let glCanvas;
@@ -7667,7 +7664,7 @@ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE |
7667
7664
  ///////////////////////////////////////////////////////////////////////////////
7668
7665
 
7669
7666
  // Initialize WebGL, called automatically by the engine
7670
- function glInit()
7667
+ function glInit(rootElement)
7671
7668
  {
7672
7669
  // keep set of texture infos so they can be restored if context is lost
7673
7670
  glTextureInfos = new Set;
@@ -7691,8 +7688,7 @@ function glInit()
7691
7688
  return;
7692
7689
  }
7693
7690
 
7694
- // attach the WebGL canvas
7695
- const rootElement = mainCanvas.parentElement;
7691
+ // attach the WebGL canvas;
7696
7692
  rootElement.appendChild(glCanvas);
7697
7693
 
7698
7694
  // startup webgl
@@ -7793,8 +7789,7 @@ function glInit()
7793
7789
 
7794
7790
  function glSetInstancedMode()
7795
7791
  {
7796
- if (!glPolyMode)
7797
- return;
7792
+ if (!glPolyMode) return;
7798
7793
 
7799
7794
  // setup instanced mode
7800
7795
  glFlush();
@@ -7827,8 +7822,7 @@ function glSetInstancedMode()
7827
7822
 
7828
7823
  function glSetPolyMode()
7829
7824
  {
7830
- if (glPolyMode)
7831
- return;
7825
+ if (glPolyMode) return;
7832
7826
 
7833
7827
  // setup poly mode
7834
7828
  glFlush();
@@ -7907,9 +7901,12 @@ function glClearCanvas()
7907
7901
  if (!glContext) return;
7908
7902
 
7909
7903
  // clear and set to same size as main canvas
7910
- glCanvas.width = drawCanvas.width;
7911
- glCanvas.height = drawCanvas.height;
7904
+ glCanvas.width = mainCanvasSize.x;
7905
+ glCanvas.height = mainCanvasSize.y;
7912
7906
  glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
7907
+ const color = canvasClearColor;
7908
+ if (color.a > 0)
7909
+ glContext.clearColor(color.r, color.g, color.b, color.a);
7913
7910
  glContext.clear(glContext.COLOR_BUFFER_BIT);
7914
7911
  }
7915
7912
 
@@ -7921,8 +7918,7 @@ function glClearCanvas()
7921
7918
  function glSetTexture(texture, wrap=false)
7922
7919
  {
7923
7920
  // must flush cache with the old texture to set a new one
7924
- if (!glContext || texture === glActiveTexture)
7925
- return;
7921
+ if (!glContext || texture === glActiveTexture) return;
7926
7922
 
7927
7923
  glFlush();
7928
7924
  glActiveTexture = texture;
@@ -8018,6 +8014,7 @@ function glCreateTexture(image)
8018
8014
  function glDeleteTexture(texture)
8019
8015
  {
8020
8016
  if (!glContext) return;
8017
+
8021
8018
  glContext.deleteTexture(texture);
8022
8019
  }
8023
8020
 
@@ -8102,8 +8099,7 @@ function glFlush()
8102
8099
  * @memberof WebGL */
8103
8100
  function glCopyToContext(context)
8104
8101
  {
8105
- if (!glEnable || !glContext)
8106
- return;
8102
+ if (!glEnable || !glContext) return;
8107
8103
 
8108
8104
  glFlush();
8109
8105
  context.drawImage(glCanvas, 0, 0);
@@ -8139,7 +8135,6 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
8139
8135
  glFlush();
8140
8136
  glSetInstancedMode();
8141
8137
 
8142
- glPolyMode = false;
8143
8138
  let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
8144
8139
  glPositionData[offset++] = x;
8145
8140
  glPositionData[offset++] = y;
@@ -8660,13 +8655,12 @@ class PostProcessPlugin
8660
8655
  {
8661
8656
  /** Create global post processing shader
8662
8657
  * @param {string} shaderCode
8663
- * @param {boolean} [includeOverlay]
8664
8658
  * @param {boolean} [includeMainCanvas]
8665
8659
  * @example
8666
8660
  * // create the post process plugin object
8667
8661
  * new PostProcessPlugin(shaderCode);
8668
8662
  */
8669
- constructor(shaderCode, includeOverlay=false, includeMainCanvas=true)
8663
+ constructor(shaderCode, includeMainCanvas=true)
8670
8664
  {
8671
8665
  ASSERT(!postProcess, 'Post process already initialized');
8672
8666
  postProcess = this;
@@ -8738,17 +8732,19 @@ class PostProcessPlugin
8738
8732
  // clear out the buffer
8739
8733
  glFlush();
8740
8734
 
8741
- if (includeMainCanvas || includeOverlay)
8735
+ // setup texture
8736
+ glContext.activeTexture(glContext.TEXTURE0);
8737
+ glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
8738
+ if (includeMainCanvas)
8742
8739
  {
8743
- // copy WebGL to the main canvas
8744
- mainContext.drawImage(glCanvas, 0, 0);
8745
-
8746
- if (includeOverlay)
8747
- {
8748
- // copy overlay canvas so it will be included in post processing
8749
- mainContext.drawImage(overlayCanvas, 0, 0);
8750
- overlayCanvas.width |= 0; // clear overlay canvas
8751
- }
8740
+ // copy main canvas to work canvas
8741
+ workCanvas.width = mainCanvasSize.x;
8742
+ workCanvas.height = mainCanvasSize.y;
8743
+ glCopyToContext(workContext);
8744
+ workContext.drawImage(mainCanvas, 0, 0);
8745
+
8746
+ // copy work canvas to texture
8747
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
8752
8748
  }
8753
8749
 
8754
8750
  // setup shader program to draw a quad
@@ -8757,14 +8753,6 @@ class PostProcessPlugin
8757
8753
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
8758
8754
  glContext.disable(glContext.BLEND);
8759
8755
 
8760
- // set textures, pass in the 2d canvas and gl canvas in separate texture channels
8761
- glContext.activeTexture(glContext.TEXTURE0);
8762
- glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
8763
- if (includeMainCanvas || includeOverlay)
8764
- {
8765
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
8766
- }
8767
-
8768
8756
  // set vertex position attribute
8769
8757
  const vertexByteStride = 8;
8770
8758
  const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
@@ -8993,12 +8981,14 @@ class UISystemPlugin
8993
8981
  * // create the ui plugin object
8994
8982
  * new UISystemPlugin;
8995
8983
  */
8996
- constructor(context=overlayContext)
8984
+ constructor(context=mainContext)
8997
8985
  {
8998
8986
  ASSERT(!uiSystem, 'UI system already initialized');
8999
8987
  uiSystem = this;
9000
8988
 
9001
8989
  // default settings
8990
+ /** @property {boolean} - Activate when mouse is pressed down instead of clicked */
8991
+ this.activateOnPress = false;
9002
8992
  /** @property {Color} - Default fill color for UI elements */
9003
8993
  this.defaultColor = WHITE;
9004
8994
  /** @property {Color} - Default outline color for UI elements */
@@ -9650,13 +9640,15 @@ class UIObject
9650
9640
  uiSystem.uiObjects.push(this);
9651
9641
  }
9652
9642
 
9653
- /** Add a child UIObject to this object
9654
- * @param {UIObject} child */
9643
+ /** Add a child UIObject to this object, returns child for chaining
9644
+ * @param {UIObject} child
9645
+ * @return {UIObject} The child object added */
9655
9646
  addChild(child)
9656
9647
  {
9657
9648
  ASSERT(!child.parent && !this.children.includes(child));
9658
9649
  this.children.push(child);
9659
9650
  child.parent = this;
9651
+ return child;
9660
9652
  }
9661
9653
 
9662
9654
  /** Remove a child UIObject from this object
@@ -9703,7 +9695,7 @@ class UIObject
9703
9695
  this.onUpdate();
9704
9696
 
9705
9697
  // unset active if disabled
9706
- if (this.disabled && this == uiSystem.activeObject)
9698
+ if (this.disabled && this === uiSystem.activeObject)
9707
9699
  uiSystem.activeObject = undefined;
9708
9700
 
9709
9701
  const wasHover = uiSystem.lastHoverObject === this;
@@ -9729,8 +9721,16 @@ class UIObject
9729
9721
  if (uiSystem.activeObject && !isActive)
9730
9722
  uiSystem.activeObject.onRelease();
9731
9723
  uiSystem.activeObject = this;
9724
+
9725
+ if (uiSystem.activateOnPress)
9726
+ {
9727
+ this.onClick();
9728
+ if (!this.soundPress && this.soundClick)
9729
+ this.soundClick.play();
9730
+ }
9732
9731
  }
9733
9732
  }
9733
+ if (!uiSystem.activateOnPress)
9734
9734
  if (!mouseDown && this.isActiveObject() && this.interactive)
9735
9735
  {
9736
9736
  this.onClick();
@@ -12170,33 +12170,33 @@ async function box2dInit()
12170
12170
  color = getDebugColor(color);
12171
12171
  point1 = box2d.vec2FromPointer(point1);
12172
12172
  point2 = box2d.vec2FromPointer(point2);
12173
- drawLine(point1, point2, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
12173
+ drawLine(point1, point2, debugLineWidth, color, vec2(), 0, false);
12174
12174
  };
12175
12175
  debugDraw.DrawPolygon = function(vertices, vertexCount, color)
12176
12176
  {
12177
12177
  color = getDebugColor(color);
12178
12178
  const points = getPointsList(vertices, vertexCount);
12179
- drawPoly(points, CLEAR_WHITE, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
12179
+ drawPoly(points, CLEAR_WHITE, debugLineWidth, color, vec2(), 0, false);
12180
12180
  };
12181
12181
  debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
12182
12182
  {
12183
12183
  color = getDebugColor(color);
12184
12184
  const points = getPointsList(vertices, vertexCount);
12185
- drawPoly(points, color, 0, color, vec2(), 0, false, false, overlayContext);
12185
+ drawPoly(points, color, 0, color, vec2(), 0, false);
12186
12186
  };
12187
12187
  debugDraw.DrawCircle = function(center, radius, color)
12188
12188
  {
12189
12189
  color = getDebugColor(color);
12190
12190
  center = box2d.vec2FromPointer(center);
12191
- drawCircle(center, radius*2, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
12191
+ drawCircle(center, radius*2, CLEAR_WHITE, debugLineWidth, color, false);
12192
12192
  };
12193
12193
  debugDraw.DrawSolidCircle = function(center, radius, axis, color)
12194
12194
  {
12195
12195
  color = getDebugColor(color);
12196
12196
  center = box2d.vec2FromPointer(center);
12197
12197
  axis = box2d.vec2FromPointer(axis).scale(radius);
12198
- drawCircle(center, radius*2, color, debugLineWidth, color, false, false, overlayContext);
12199
- drawLine(vec2(), axis, debugLineWidth, color, center, 0, false, false, overlayContext);
12198
+ drawCircle(center, radius*2, color, debugLineWidth, color, false);
12199
+ drawLine(vec2(), axis, debugLineWidth, color, center, 0, false);
12200
12200
  };
12201
12201
  debugDraw.DrawTransform = function(transform)
12202
12202
  {
@@ -12205,8 +12205,8 @@ async function box2dInit()
12205
12205
  const angle = -transform.get_q().GetAngle();
12206
12206
  const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
12207
12207
  const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
12208
- drawLine(vec2(), p1, debugLineWidth, c1, pos, angle, false, false, overlayContext);
12209
- drawLine(vec2(), p2, debugLineWidth, c2, pos, angle, false, false, overlayContext);
12208
+ drawLine(vec2(), p1, debugLineWidth, c1, pos, angle, false);
12209
+ drawLine(vec2(), p2, debugLineWidth, c2, pos, angle, false);
12210
12210
  }
12211
12211
 
12212
12212
  debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
@@ -12226,8 +12226,8 @@ async function box2dInit()
12226
12226
 
12227
12227
  ///////////////////////////////////////////////////////////////////////////////
12228
12228
 
12229
- /** Draw a scalable nine-slice UI element to the overlay canvas in screen space
12230
- * This function can not apply color because it draws using the overlay 2d context
12229
+ /** Draw a scalable nine-slice UI element to the main canvas in screen space
12230
+ * This function can not apply color because it draws using the 2d context
12231
12231
  * @param {Vector2} pos - Screen space position
12232
12232
  * @param {Vector2} size - Screen space size
12233
12233
  * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
@@ -12237,7 +12237,7 @@ async function box2dInit()
12237
12237
  * @memberof DrawUtilities */
12238
12238
  function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
12239
12239
  {
12240
- drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
12240
+ drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true);
12241
12241
  }
12242
12242
 
12243
12243
  /** Draw a scalable nine-slice UI element in world space
@@ -12286,8 +12286,8 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
12286
12286
  }
12287
12287
  }
12288
12288
 
12289
- /** Draw a scalable three-slice UI element to the overlay canvas in screen space
12290
- * This function can not apply color because it draws using the overlay 2d context
12289
+ /** Draw a scalable three-slice UI element to the main canvas in screen space
12290
+ * This function can not apply color because it draws using the 2d context
12291
12291
  * @param {Vector2} pos - Screen space position
12292
12292
  * @param {Vector2} size - Screen space size
12293
12293
  * @param {TileInfo} startTile - Starting tile for the three-slice pattern
@@ -12297,7 +12297,7 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
12297
12297
  * @memberof DrawUtilities */
12298
12298
  function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
12299
12299
  {
12300
- drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
12300
+ drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true);
12301
12301
  }
12302
12302
 
12303
12303
  /** Draw a scalable three-slice UI element in world space
@@ -12377,7 +12377,7 @@ export
12377
12377
  // Globals
12378
12378
  debug,
12379
12379
  debugOverlay,
12380
- showWatermark,
12380
+ debugWatermark,
12381
12381
 
12382
12382
  // Debug
12383
12383
  ASSERT,
@@ -12391,13 +12391,10 @@ export
12391
12391
  debugText,
12392
12392
  debugClear,
12393
12393
  debugScreenshot,
12394
- debugSaveCanvas,
12395
- debugSaveText,
12396
- debugSaveDataURL,
12397
12394
  debugShowErrors,
12398
- debugVideoCaptureIsActive,
12399
12395
  debugVideoCaptureStart,
12400
12396
  debugVideoCaptureStop,
12397
+ debugVideoCaptureIsActive,
12401
12398
 
12402
12399
  // Settings
12403
12400
  cameraPos,
@@ -12410,13 +12407,13 @@ export
12410
12407
  canvasMaxAspect,
12411
12408
  canvasFixedSize,
12412
12409
  canvasPixelated,
12413
- overlayCanvasPixelated,
12414
12410
  tilesPixelated,
12415
12411
  fontDefault,
12416
12412
  showSplashScreen,
12417
12413
  headlessMode,
12418
- tileSizeDefault,
12419
- tileFixBleedScale,
12414
+ tileDefaultSize,
12415
+ tileDefaultPadding,
12416
+ tileDefaultBleed,
12420
12417
  enablePhysicsSolver,
12421
12418
  objectDefaultMass,
12422
12419
  objectDefaultDamping,
@@ -12455,14 +12452,14 @@ export
12455
12452
  setCanvasMaxAspect,
12456
12453
  setCanvasFixedSize,
12457
12454
  setCanvasPixelated,
12458
- setOverlayCanvasPixelated,
12459
12455
  setTilesPixelated,
12460
12456
  setFontDefault,
12461
12457
  setShowSplashScreen,
12462
12458
  setHeadlessMode,
12463
12459
  setGLEnable,
12464
- setTileSizeDefault,
12465
- setTileFixBleedScale,
12460
+ setTileDefaultSize,
12461
+ setTileDefaultPadding,
12462
+ setTileDefaultBleed,
12466
12463
  setEnablePhysicsSolver,
12467
12464
  setObjectDefaultMass,
12468
12465
  setObjectDefaultDamping,
@@ -12491,10 +12488,10 @@ export
12491
12488
  setMedalDisplaySlideTime,
12492
12489
  setMedalDisplaySize,
12493
12490
  setMedalsPreventUnlock,
12494
- setShowWatermark,
12491
+ setDebugWatermark,
12495
12492
  setDebugKey,
12496
12493
 
12497
- // Utilities
12494
+ // Math
12498
12495
  PI,
12499
12496
  abs,
12500
12497
  floor,
@@ -12522,8 +12519,13 @@ export
12522
12519
  isOverlapping,
12523
12520
  isIntersecting,
12524
12521
  wave,
12522
+
12523
+ // Utilities
12525
12524
  formatTime,
12526
12525
  fetchJSON,
12526
+ saveText,
12527
+ saveCanvas,
12528
+ saveDataURL,
12527
12529
 
12528
12530
  // Random
12529
12531
  rand,
@@ -12569,10 +12571,11 @@ export
12569
12571
  TextureInfo,
12570
12572
  mainCanvas,
12571
12573
  mainContext,
12572
- drawCanvas,
12573
12574
  drawContext,
12574
- overlayCanvas,
12575
- overlayContext,
12575
+ workCanvas,
12576
+ workContext,
12577
+ workReadCanvas,
12578
+ workReadContext,
12576
12579
  mainCanvasSize,
12577
12580
  textureInfos,
12578
12581
  drawCount,
@@ -12580,6 +12583,7 @@ export
12580
12583
  worldToScreen,
12581
12584
  screenToWorldDelta,
12582
12585
  worldToScreenDelta,
12586
+ screenToWorldTransform,
12583
12587
  drawTile,
12584
12588
  drawRect,
12585
12589
  drawRectGradient,
@@ -12590,7 +12594,6 @@ export
12590
12594
  drawCircle,
12591
12595
  drawCanvas2D,
12592
12596
  drawText,
12593
- drawTextOverlay,
12594
12597
  drawTextScreen,
12595
12598
  setBlendMode,
12596
12599
  combineCanvases,