littlejsengine 1.13.1 → 1.13.4

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 (66) hide show
  1. package/README.md +15 -12
  2. package/dist/littlejs.d.ts +94 -74
  3. package/dist/littlejs.esm.js +422 -306
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +416 -306
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +414 -304
  8. package/examples/box2d/game.js +1 -1
  9. package/examples/box2d/gameObjects.js +1 -1
  10. package/examples/box2d/scenes.js +1 -1
  11. package/examples/breakoutTutorial/README.md +44 -41
  12. package/examples/breakoutTutorial/game.js +2 -2
  13. package/examples/electron/build.bat +7 -0
  14. package/examples/electron/build.js +124 -0
  15. package/examples/electron/electron.js +35 -0
  16. package/examples/electron/game.js +140 -0
  17. package/examples/electron/index.html +13 -0
  18. package/examples/electron/package.json +12 -0
  19. package/examples/electron/tiles.png +0 -0
  20. package/examples/empty/game.js +1 -0
  21. package/examples/htmlMenu/game.js +1 -1
  22. package/examples/index.html +154 -93
  23. package/examples/module/game.js +5 -2
  24. package/examples/particles/index.html +12 -3
  25. package/examples/platformer/gameEffects.js +3 -3
  26. package/examples/shorts/base.html +1 -1
  27. package/examples/shorts/blending.js +9 -5
  28. package/examples/shorts/box2d.js +1 -1
  29. package/examples/shorts/box2dCar.js +1 -1
  30. package/examples/shorts/clock.js +1 -1
  31. package/examples/shorts/colors.js +2 -2
  32. package/examples/shorts/maze.js +1 -1
  33. package/examples/shorts/nineSlice.js +9 -9
  34. package/examples/shorts/piano.js +2 -2
  35. package/examples/shorts/raycasting.js +2 -2
  36. package/examples/shorts/shapes.js +9 -9
  37. package/examples/shorts/slidingPuzzle.js +2 -2
  38. package/examples/shorts/starfield.js +5 -7
  39. package/examples/shorts/systemFont.js +1 -1
  40. package/examples/shorts/uiSystem.js +1 -0
  41. package/examples/starter/build.js +9 -8
  42. package/examples/starter/game.js +5 -2
  43. package/examples/starter/index.html +2 -2
  44. package/examples/stress/index.html +5 -5
  45. package/examples/style.css +5 -2
  46. package/examples/typescript/build.js +1 -1
  47. package/examples/typescript/game.js +2 -2
  48. package/examples/typescript/game.ts +5 -2
  49. package/examples/uiSystem/game.js +2 -1
  50. package/package.json +2 -2
  51. package/plugins/box2d.js +19 -94
  52. package/plugins/drawUtilities.js +24 -18
  53. package/plugins/postProcess.js +7 -0
  54. package/plugins/uiSystem.js +4 -4
  55. package/src/engine.js +7 -2
  56. package/src/engineAudio.js +1 -1
  57. package/src/engineDebug.js +2 -2
  58. package/src/engineDraw.js +243 -150
  59. package/src/engineExport.js +6 -0
  60. package/src/engineInput.js +2 -2
  61. package/src/engineMedals.js +4 -4
  62. package/src/engineObject.js +3 -3
  63. package/src/engineParticles.js +8 -1
  64. package/src/engineSettings.js +55 -8
  65. package/src/engineUtilities.js +7 -7
  66. package/src/engineWebGL.js +25 -5
package/dist/littlejs.js CHANGED
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.13.1';
36
+ const engineVersion = '1.13.4';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -300,7 +300,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
300
300
  'margin:0;' + // fill the window
301
301
  'overflow:hidden;' + // no scroll bars
302
302
  'background:#000;' + // set background color
303
- (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
304
303
  'user-select:none;' + // prevent hold to select
305
304
  '-webkit-user-select:none;' + // compatibility for ios
306
305
  (!touchInputEnable ? '' : // no touch css settings
@@ -328,7 +327,13 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
328
327
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
329
328
  if (glCanvas)
330
329
  glCanvas.style.cssText = styleCanvas;
330
+ setCanvasPixelated(canvasPixelated);
331
+ setOverlayCanvasPixelated(overlayCanvasPixelated);
331
332
  updateCanvas();
333
+
334
+ // create offscreen canvas for image processing
335
+ workCanvas = new OffscreenCanvas(256, 256);
336
+ workContext = workCanvas.getContext('2d', { willReadFrequently: true });
332
337
 
333
338
  // create promises for loading images
334
339
  const promises = imageSources.map((src, textureIndex)=>
@@ -1120,7 +1125,7 @@ function debugRender()
1120
1125
  overlayContext.fillText('6: Toggle Video Capture', x, y += h);
1121
1126
 
1122
1127
  let keysPressed = '';
1123
- for(const i in inputData[0])
1128
+ for (const i in inputData[0])
1124
1129
  {
1125
1130
  if (keyIsDown(i, 0))
1126
1131
  keysPressed += i + ' ' ;
@@ -1129,7 +1134,7 @@ function debugRender()
1129
1134
 
1130
1135
  let buttonsPressed = '';
1131
1136
  if (inputData[1])
1132
- for(const i in inputData[1])
1137
+ for (const i in inputData[1])
1133
1138
  {
1134
1139
  if (keyIsDown(i, 1))
1135
1140
  buttonsPressed += i + ' ' ;
@@ -1370,11 +1375,12 @@ function isPowerOfTwo(value) { return !(value & (value - 1)); }
1370
1375
  * @memberof Utilities */
1371
1376
  function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
1372
1377
 
1373
- /** Returns true if two axis aligned bounding boxes are overlapping
1378
+ /** Returns true if two axis aligned bounding boxes are overlapping
1379
+ * this can be used for simple collision detection between objects
1374
1380
  * @param {Vector2} posA - Center of box A
1375
1381
  * @param {Vector2} sizeA - Size of box A
1376
1382
  * @param {Vector2} posB - Center of box B
1377
- * @param {Vector2} [sizeB=(0,0)] - Size of box B, a point if undefined
1383
+ * @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
1378
1384
  * @return {boolean} - True if overlapping
1379
1385
  * @memberof Utilities */
1380
1386
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
@@ -1428,10 +1434,11 @@ function isIntersecting(start, end, pos, size)
1428
1434
  * @param {number} [frequency] - Frequency of the wave in Hz
1429
1435
  * @param {number} [amplitude] - Amplitude (max height) of the wave
1430
1436
  * @param {number} [t=time] - Value to use for time of the wave
1437
+ * @param {number} [offset] - Value to use for time offset of the wave
1431
1438
  * @return {number} - Value waving between 0 and amplitude
1432
1439
  * @memberof Utilities */
1433
- function wave(frequency=1, amplitude=1, t=time)
1434
- { return amplitude/2 * (1 - Math.cos(t*frequency*2*PI)); }
1440
+ function wave(frequency=1, amplitude=1, t=time, offset=0)
1441
+ { return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
1435
1442
 
1436
1443
  /** Formats seconds to mm:ss style for display purposes
1437
1444
  * @param {number} t - time in seconds
@@ -2088,9 +2095,7 @@ class Color
2088
2095
  /** Checks if this is a valid color
2089
2096
  * @return {boolean} */
2090
2097
  isValid()
2091
- {
2092
- return isNumber(this.r) && isNumber(this.g) && isNumber(this.b) && isNumber(this.a);
2093
- }
2098
+ { return isNumber(this.r) && isNumber(this.g) && isNumber(this.b) && isNumber(this.a); }
2094
2099
  }
2095
2100
 
2096
2101
  ///////////////////////////////////////////////////////////////////////////////
@@ -2253,6 +2258,13 @@ let cameraScale = 32;
2253
2258
  ///////////////////////////////////////////////////////////////////////////////
2254
2259
  // Display settings
2255
2260
 
2261
+ /** Enable applying color to tiles when using canvas2d
2262
+ * - This is slower but should be the same as webgl rendering
2263
+ * @type {boolean}
2264
+ * @default
2265
+ * @memberof Settings */
2266
+ let canvasColorTiles = true;
2267
+
2256
2268
  /** The max size of the canvas, centered if window is larger
2257
2269
  * @type {Vector2}
2258
2270
  * @default Vector2(1920,1080)
@@ -2266,14 +2278,21 @@ let canvasMaxSize = vec2(1920, 1080);
2266
2278
  * @memberof Settings */
2267
2279
  let canvasFixedSize = vec2();
2268
2280
 
2269
- /** Use nearest neighbor canvas scaling for more pixelated look
2270
- * - Must be set before startup to take effect
2281
+ /** Use nearest canvas scaling for more pixelated look
2271
2282
  * - If enabled sets css image-rendering:pixelated
2272
2283
  * @type {boolean}
2273
2284
  * @default
2274
2285
  * @memberof Settings */
2275
2286
  let canvasPixelated = true;
2276
2287
 
2288
+ /** Use nearest canvas scaling for more pixelated look
2289
+ * - If enabled sets css image-rendering:pixelated
2290
+ * - This defaults to false because text looks better with smoothing
2291
+ * @type {boolean}
2292
+ * @default
2293
+ * @memberof Settings */
2294
+ let overlayCanvasPixelated = false;
2295
+
2277
2296
  /** Disables texture filtering for crisper pixel art
2278
2297
  * @type {boolean}
2279
2298
  * @default
@@ -2514,6 +2533,14 @@ function setCameraAngle(angle) { cameraAngle = angle; }
2514
2533
  * @memberof Settings */
2515
2534
  function setCameraScale(scale) { cameraScale = scale; }
2516
2535
 
2536
+ /** Set if tiles should be colorized when using canvas2d
2537
+ * This can be slower but results should look nearly identical to webgl rendering
2538
+ * It can be enabled/disabled at any time
2539
+ * Optimized for performance, and will use faster method if color is white or untextured
2540
+ * @param {boolean} colorTiles
2541
+ * @memberof Settings */
2542
+ function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
2543
+
2517
2544
  /** Set max size of the canvas
2518
2545
  * @param {Vector2} size
2519
2546
  * @memberof Settings */
@@ -2524,10 +2551,30 @@ function setCanvasMaxSize(size) { canvasMaxSize = size; }
2524
2551
  * @memberof Settings */
2525
2552
  function setCanvasFixedSize(size) { canvasFixedSize = size; }
2526
2553
 
2527
- /** Use nearest neighbor scaling algorithm for canvas for more pixelated look
2554
+ /** Use nearest scaling algorithm for canvas for more pixelated look
2555
+ * - If enabled sets css image-rendering:pixelated
2556
+ * @param {boolean} pixelated
2557
+ * @memberof Settings */
2558
+ function setCanvasPixelated(pixelated)
2559
+ {
2560
+ canvasPixelated = pixelated;
2561
+ if (mainCanvas)
2562
+ mainCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2563
+ if (glCanvas)
2564
+ glCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2565
+ }
2566
+
2567
+ /** Use nearest scaling algorithm for canvas for more pixelated look
2568
+ * - If enabled sets css image-rendering:pixelated
2569
+ * - This defaults to false because text looks better with smoothing
2528
2570
  * @param {boolean} pixelated
2529
2571
  * @memberof Settings */
2530
- function setCanvasPixelated(pixelated) { canvasPixelated = pixelated; }
2572
+ function setOverlayCanvasPixelated(pixelated)
2573
+ {
2574
+ overlayCanvasPixelated = pixelated;
2575
+ if (overlayCanvas)
2576
+ overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2577
+ }
2531
2578
 
2532
2579
  /** Disables texture filtering for crisper pixel art
2533
2580
  * @param {boolean} pixelated
@@ -2552,7 +2599,12 @@ function setHeadlessMode(headless) { headlessMode = headless; }
2552
2599
  /** Set if webgl rendering is enabled
2553
2600
  * @param {boolean} enable
2554
2601
  * @memberof Settings */
2555
- function setGLEnable(enable) { glEnable = enable; }
2602
+ function setGLEnable(enable)
2603
+ {
2604
+ glEnable = enable;
2605
+ if (glCanvas) // hide glCanvas if webgl is disabled
2606
+ glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2607
+ }
2556
2608
 
2557
2609
  /** Set default size of tiles in pixels
2558
2610
  * @param {Vector2} size
@@ -2916,7 +2968,7 @@ class EngineObject
2916
2968
  if (o.mass) // push away if not fixed
2917
2969
  o.velocity = o.velocity.subtract(velocity);
2918
2970
 
2919
- debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
2971
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
2920
2972
  continue;
2921
2973
  }
2922
2974
 
@@ -2978,7 +3030,7 @@ class EngineObject
2978
3030
  else // bounce if other object is fixed
2979
3031
  this.velocity.x *= -restitution;
2980
3032
  }
2981
- debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
3033
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
2982
3034
  }
2983
3035
  }
2984
3036
  if (this.collideTiles)
@@ -3023,7 +3075,7 @@ class EngineObject
3023
3075
  this.pos.x = oldPos.x;
3024
3076
  this.velocity.x *= -this.restitution;
3025
3077
  }
3026
- debugOverlay && debugPhysics && debugRect(this.pos, this.size, '#f00');
3078
+ debugPhysics && debugRect(this.pos, this.size, '#f00');
3027
3079
  }
3028
3080
  }
3029
3081
  }
@@ -3225,6 +3277,16 @@ let drawCanvas;
3225
3277
  * @memberof Draw */
3226
3278
  let drawContext;
3227
3279
 
3280
+ /** Offscreen canvas that can be used for image processing
3281
+ * @type {OffscreenCanvas}
3282
+ * @memberof Draw */
3283
+ let workCanvas;
3284
+
3285
+ /** Offscreen canvas that can be used for image processing
3286
+ * @type {OffscreenCanvasRenderingContext2D}
3287
+ * @memberof Draw */
3288
+ let workContext;
3289
+
3228
3290
  /** The size of the main canvas (and other secondary canvases)
3229
3291
  * @type {Vector2}
3230
3292
  * @memberof Draw */
@@ -3244,10 +3306,10 @@ let drawCount;
3244
3306
  * Create a tile info object using a grid based system
3245
3307
  * - This can take vecs or floats for easier use and conversion
3246
3308
  * - If an index is passed in, the tile size and index will determine the position
3247
- * @param {Vector2|number} [pos=0] - Index of tile in sheet
3309
+ * @param {Vector2|number} [pos=0] - Index of tile in sheet
3248
3310
  * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
3249
- * @param {number} [textureIndex] - Texture index to use
3250
- * @param {number} [padding] - How many pixels padding around tiles
3311
+ * @param {number} [textureIndex] - Texture index to use
3312
+ * @param {number} [padding] - How many pixels padding around tiles
3251
3313
  * @return {TileInfo}
3252
3314
  * @example
3253
3315
  * tile(2) // a tile at index 2 using the default tile size of 16
@@ -3374,55 +3436,7 @@ class TextureInfo
3374
3436
  }
3375
3437
 
3376
3438
  ///////////////////////////////////////////////////////////////////////////////
3377
-
3378
- /** Convert from screen to world space coordinates
3379
- * @param {Vector2} screenPos
3380
- * @return {Vector2}
3381
- * @memberof Draw */
3382
- function screenToWorld(screenPos)
3383
- {
3384
- let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3385
- let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3386
- if (cameraAngle)
3387
- {
3388
- // apply camera rotation
3389
- const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
3390
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
3391
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
3392
- cameraPosRelativeX = rotatedX;
3393
- cameraPosRelativeY = rotatedY;
3394
- }
3395
- return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
3396
- }
3397
-
3398
- /** Convert from world to screen space coordinates
3399
- * @param {Vector2} worldPos
3400
- * @return {Vector2}
3401
- * @memberof Draw */
3402
- function worldToScreen(worldPos)
3403
- {
3404
- let cameraPosRelativeX = worldPos.x - cameraPos.x;
3405
- let cameraPosRelativeY = worldPos.y - cameraPos.y;
3406
- if (cameraAngle)
3407
- {
3408
- // apply inverse camera rotation
3409
- const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
3410
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
3411
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
3412
- cameraPosRelativeX = rotatedX;
3413
- cameraPosRelativeY = rotatedY;
3414
- }
3415
- return new Vector2
3416
- (
3417
- cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
3418
- cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
3419
- );
3420
- }
3421
-
3422
- /** Get the camera's visible area in world space
3423
- * @return {Vector2}
3424
- * @memberof Draw */
3425
- function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
3439
+ // Drawing functions
3426
3440
 
3427
3441
  /** Draw textured tile centered in world space, with color applied if using WebGL
3428
3442
  * @param {Vector2} pos - Center of the tile in world space
@@ -3445,6 +3459,9 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
3445
3459
  ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
3446
3460
  ASSERT(isNumber(angle), 'drawTile angle should be a number');
3447
3461
 
3462
+ if (color.a <= 0 && (!additiveColor || additiveColor.a <= 0) || !size.x || !size.y)
3463
+ return; // completely invisible, skip render
3464
+
3448
3465
  const textureInfo = tileInfo && tileInfo.textureInfo;
3449
3466
  if (useWebGL)
3450
3467
  {
@@ -3495,18 +3512,15 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
3495
3512
  if (textureInfo)
3496
3513
  {
3497
3514
  // calculate uvs and render
3498
- const x = tileInfo.pos.x + tileFixBleedScale;
3499
- const y = tileInfo.pos.y + tileFixBleedScale;
3500
- const w = tileInfo.size.x - 2*tileFixBleedScale;
3501
- const h = tileInfo.size.y - 2*tileFixBleedScale;
3502
- context.globalAlpha = color.a; // only alpha is supported
3503
- context.drawImage(textureInfo.image, x, y, w, h, -.5, -.5, 1, 1);
3504
- context.globalAlpha = 1; // set back to full alpha
3515
+ const x = tileInfo.pos.x, y = tileInfo.pos.y;
3516
+ const w = tileInfo.size.x, h = tileInfo.size.y;
3517
+ drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor);
3505
3518
  }
3506
3519
  else
3507
3520
  {
3508
- // if no tile info, force untextured
3509
- context.fillStyle = color.toString();
3521
+ // if no tile info, use untextured rect
3522
+ const c = additiveColor ? color.add(additiveColor) : color;
3523
+ context.fillStyle = c.toString();
3510
3524
  context.fillRect(-.5, -.5, 1, 1);
3511
3525
  }
3512
3526
  }, screenSpace, context);
@@ -3519,7 +3533,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
3519
3533
  * @param {Color} [color=(1,1,1,1)]
3520
3534
  * @param {number} [angle]
3521
3535
  * @param {boolean} [useWebGL=glEnable]
3522
- * @param {boolean} [screenSpace=false]
3536
+ * @param {boolean} [screenSpace]
3523
3537
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3524
3538
  * @memberof Draw */
3525
3539
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
@@ -3532,73 +3546,90 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3532
3546
  * @param {Vector2} posB
3533
3547
  * @param {number} [thickness]
3534
3548
  * @param {Color} [color=(1,1,1,1)]
3549
+ * @param {Vector2} [pos=(0,0)] - Offset to apply
3550
+ * @param {number} [angle] - Angle to rotate by
3535
3551
  * @param {boolean} [useWebGL=glEnable]
3536
- * @param {boolean} [screenSpace=false]
3552
+ * @param {boolean} [screenSpace]
3537
3553
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3538
3554
  * @memberof Draw */
3539
- function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
3555
+ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
3540
3556
  {
3541
3557
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
3542
3558
  const size = vec2(thickness, halfDelta.length()*2);
3543
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace, context);
3559
+ pos = pos.add(posA.add(halfDelta));
3560
+ angle += halfDelta.angle();
3561
+ drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
3544
3562
  }
3545
3563
 
3546
3564
  /** Draw colored polygon using passed in points
3547
- * @param {Array<Vector2>} points - Array of Vector2 points
3565
+ * @param {Array<Vector2>} points - Array of Vector2 points
3548
3566
  * @param {Color} [color=(1,1,1,1)]
3549
- * @param {number} [lineWidth=0]
3567
+ * @param {number} [lineWidth]
3550
3568
  * @param {Color} [lineColor=(0,0,0,1)]
3551
- * @param {boolean} [screenSpace=false]
3569
+ * @param {Vector2} [pos=(0,0)] - Offset to apply
3570
+ * @param {number} [angle] - Angle to rotate by
3571
+ * @param {boolean} [useWebGL] - Webgl not supported
3572
+ * @param {boolean} [screenSpace]
3552
3573
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3553
3574
  * @memberof Draw */
3554
- function drawPoly(points, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=drawContext)
3575
+ function drawPoly(points, color=new Color, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=false, screenSpace=false, context=drawContext)
3555
3576
  {
3556
- ASSERT(isColor(color) && isColor(lineColor));
3557
- context.fillStyle = color.toString();
3558
- context.beginPath();
3559
- for (const point of screenSpace ? points : points.map(worldToScreen))
3560
- context.lineTo(point.x, point.y);
3561
- context.closePath();
3562
- context.fill();
3563
- if (lineWidth)
3577
+ ASSERT(isVector2(pos) && pos.isValid(), 'drawPoly pos should be a vec2');
3578
+ ASSERT(Array.isArray(points), 'drawPoly points should be an array');
3579
+ ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
3580
+ ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
3581
+ ASSERT(isNumber(angle), 'drawPoly angle should be a number');
3582
+ ASSERT(!useWebGL, 'drawPoly webgl not supported');
3583
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
3564
3584
  {
3565
- context.strokeStyle = lineColor.toString();
3566
- context.lineWidth = screenSpace ? lineWidth : lineWidth*cameraScale;
3567
- context.stroke();
3568
- }
3585
+ context.beginPath();
3586
+ for (const point of points)
3587
+ context.lineTo(point.x, point.y);
3588
+ context.closePath();
3589
+ context.fillStyle = color.toString();
3590
+ context.fill();
3591
+ if (lineWidth)
3592
+ {
3593
+ context.strokeStyle = lineColor.toString();
3594
+ context.lineWidth = lineWidth;
3595
+ context.stroke();
3596
+ }
3597
+ }, screenSpace, context);
3569
3598
  }
3570
3599
 
3571
3600
  /** Draw colored ellipse using passed in point
3572
3601
  * @param {Vector2} pos
3573
- * @param {number} [width=1]
3574
- * @param {number} [height=1]
3575
- * @param {number} [angle=0]
3602
+ * @param {Vector2} [size=(1,1)]
3576
3603
  * @param {Color} [color=(1,1,1,1)]
3577
- * @param {number} [lineWidth=0]
3604
+ * @param {number} [angle]
3605
+ * @param {number} [lineWidth]
3578
3606
  * @param {Color} [lineColor=(0,0,0,1)]
3579
- * @param {boolean} [screenSpace=false]
3607
+ * @param {boolean} [useWebGL] - Webgl not supported
3608
+ * @param {boolean} [screenSpace]
3580
3609
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3581
3610
  * @memberof Draw */
3582
- function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=drawContext)
3611
+ function drawEllipse(pos, size=vec2(1), color=new Color, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace=false, context=drawContext)
3583
3612
  {
3584
- ASSERT(isColor(color) && isColor(lineColor));
3585
- if (!screenSpace)
3586
- {
3587
- pos = worldToScreen(pos);
3588
- width *= cameraScale;
3589
- height *= cameraScale;
3590
- lineWidth *= cameraScale;
3591
- }
3592
- context.fillStyle = color.toString();
3593
- context.beginPath();
3594
- context.ellipse(pos.x, pos.y, width, height, angle, 0, 9);
3595
- context.fill();
3596
- if (lineWidth)
3613
+ ASSERT(isVector2(pos) && pos.isValid(), 'drawEllipse pos should be a vec2');
3614
+ ASSERT(isVector2(size) && size.isValid(), 'drawEllipse size should be a vec2');
3615
+ ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
3616
+ ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
3617
+ ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
3618
+ ASSERT(lineWidth>=0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3619
+ ASSERT(!useWebGL, 'drawEllipse webgl not supported');
3620
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
3597
3621
  {
3598
- context.strokeStyle = lineColor.toString();
3599
- context.lineWidth = lineWidth;
3600
- context.stroke();
3601
- }
3622
+ context.beginPath();
3623
+ context.ellipse(0, 0, size.y, size.x, 0, 0, 9);
3624
+ context.fillStyle = color.toString();
3625
+ context.fill();
3626
+ if (lineWidth)
3627
+ {
3628
+ context.strokeStyle = lineColor.toString();
3629
+ context.lineWidth = lineWidth;
3630
+ context.stroke();
3631
+ }
3632
+ }, screenSpace, context);
3602
3633
  }
3603
3634
 
3604
3635
  /** Draw colored circle using passed in point
@@ -3607,11 +3638,12 @@ function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth
3607
3638
  * @param {Color} [color=(1,1,1,1)]
3608
3639
  * @param {number} [lineWidth=0]
3609
3640
  * @param {Color} [lineColor=(0,0,0,1)]
3641
+ * @param {boolean} [useWebGL] - Webgl not supported
3610
3642
  * @param {boolean} [screenSpace=false]
3611
3643
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3612
3644
  * @memberof Draw */
3613
- function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=drawContext)
3614
- { drawEllipse(pos, radius, radius, 0, color, lineWidth, lineColor, screenSpace, context); }
3645
+ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace, context=drawContext)
3646
+ { drawEllipse(pos, vec2(radius), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context); }
3615
3647
 
3616
3648
  /** Draw directly to a 2d canvas context in world space
3617
3649
  * @param {Vector2} pos
@@ -3638,6 +3670,9 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3638
3670
  context.restore();
3639
3671
  }
3640
3672
 
3673
+ ///////////////////////////////////////////////////////////////////////////////
3674
+ // Text Drawing Functions
3675
+
3641
3676
  /** Draw text on main canvas in world space
3642
3677
  * Automatically splits new lines into rows
3643
3678
  * @param {string} text
@@ -3686,7 +3721,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3686
3721
  * @param {number} [maxWidth]
3687
3722
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3688
3723
  * @memberof Draw */
3689
- function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
3724
+ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
3690
3725
  {
3691
3726
  context.fillStyle = color.toString();
3692
3727
  context.strokeStyle = lineColor.toString();
@@ -3707,22 +3742,67 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
3707
3742
  });
3708
3743
  }
3709
3744
 
3745
+ ///////////////////////////////////////////////////////////////////////////////
3746
+ // Drawing utilities
3747
+
3748
+ /** Convert from screen to world space coordinates
3749
+ * @param {Vector2} screenPos
3750
+ * @return {Vector2}
3751
+ * @memberof Draw */
3752
+ function screenToWorld(screenPos)
3753
+ {
3754
+ let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3755
+ let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3756
+ if (cameraAngle)
3757
+ {
3758
+ // apply camera rotation
3759
+ const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
3760
+ const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
3761
+ const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
3762
+ cameraPosRelativeX = rotatedX;
3763
+ cameraPosRelativeY = rotatedY;
3764
+ }
3765
+ return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
3766
+ }
3767
+
3768
+ /** Convert from world to screen space coordinates
3769
+ * @param {Vector2} worldPos
3770
+ * @return {Vector2}
3771
+ * @memberof Draw */
3772
+ function worldToScreen(worldPos)
3773
+ {
3774
+ let cameraPosRelativeX = worldPos.x - cameraPos.x;
3775
+ let cameraPosRelativeY = worldPos.y - cameraPos.y;
3776
+ if (cameraAngle)
3777
+ {
3778
+ // apply inverse camera rotation
3779
+ const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
3780
+ const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
3781
+ const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
3782
+ cameraPosRelativeX = rotatedX;
3783
+ cameraPosRelativeY = rotatedY;
3784
+ }
3785
+ return new Vector2
3786
+ (
3787
+ cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
3788
+ cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
3789
+ );
3790
+ }
3791
+
3792
+ /** Get the camera's visible area in world space
3793
+ * @return {Vector2}
3794
+ * @memberof Draw */
3795
+ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
3796
+
3710
3797
  /** Enable normal or additive blend mode
3711
3798
  * @param {boolean} [additive]
3712
- * @param {boolean} [useWebGL=glEnable]
3713
3799
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3714
3800
  * @memberof Draw */
3715
- function setBlendMode(additive=false, useWebGL=glEnable, context)
3801
+ function setBlendMode(additive=false, context)
3716
3802
  {
3717
- ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3718
- if (useWebGL)
3719
- glAdditive = additive;
3720
- else
3721
- {
3722
- if (!context)
3723
- context = drawContext;
3724
- context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
3725
- }
3803
+ glAdditive = additive;
3804
+ context ||= drawContext;
3805
+ context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
3726
3806
  }
3727
3807
 
3728
3808
  /** Combines all LittleJS canvases onto the main canvas and clears them
@@ -3739,6 +3819,102 @@ function combineCanvases()
3739
3819
  overlayCanvas.width |= 0;
3740
3820
  }
3741
3821
 
3822
+ /** Helper function to draw an image with color and additive color applied
3823
+ * This is slower then normal drawImage when color is applied
3824
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
3825
+ * @param {HTMLImageElement|OffscreenCanvas} image
3826
+ * @param {number} sx
3827
+ * @param {number} sy
3828
+ * @param {number} sWidth
3829
+ * @param {number} sHeight
3830
+ * @param {number} dx
3831
+ * @param {number} dy
3832
+ * @param {number} dWidth
3833
+ * @param {number} dHeight
3834
+ * @param {Color} color
3835
+ * @param {Color} [additiveColor]
3836
+ * @memberof Draw */
3837
+ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor)
3838
+ {
3839
+ function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
3840
+ function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
3841
+ const sx2 = tileFixBleedScale;
3842
+ const sy2 = tileFixBleedScale;
3843
+ const sWidth2 = sWidth - 2*tileFixBleedScale;
3844
+ const sHeight2 = sHeight - 2*tileFixBleedScale;
3845
+ if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
3846
+ {
3847
+ // white texture with no additive alpha, no need to tint
3848
+ context.globalAlpha = color.a;
3849
+ context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3850
+ context.globalAlpha = 1;
3851
+ }
3852
+ else
3853
+ {
3854
+ // copy to offscreen canvas
3855
+ workCanvas.width = sWidth;
3856
+ workCanvas.height = sHeight;
3857
+ workContext.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
3858
+
3859
+ // tint image using offscreen work context
3860
+ const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
3861
+ const data = imageData.data;
3862
+ if (additiveColor && !isBlack(additiveColor))
3863
+ {
3864
+ // slower path with additive color
3865
+ const colorMultiply = [color.r, color.g, color.b, color.a];
3866
+ const colorAdd = [additiveColor.r * 255, additiveColor.g * 255, additiveColor.b * 255, additiveColor.a * 255];
3867
+ for (let i = 0; i < data.length; ++i)
3868
+ data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
3869
+ workContext.putImageData(imageData, 0, 0);
3870
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3871
+ }
3872
+ else
3873
+ {
3874
+ // faster path with no additive color
3875
+ for (let i = 0; i < data.length; i+=4)
3876
+ {
3877
+ data[i ] *= color.r;
3878
+ data[i+1] *= color.g;
3879
+ data[i+2] *= color.b;
3880
+ }
3881
+ workContext.putImageData(imageData, 0, 0);
3882
+ context.globalAlpha = color.a;
3883
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3884
+ context.globalAlpha = 1;
3885
+ }
3886
+ }
3887
+ }
3888
+
3889
+
3890
+ /** Returns true if fullscreen mode is active
3891
+ * @return {boolean}
3892
+ * @memberof Draw */
3893
+ function isFullscreen() { return !!document.fullscreenElement; }
3894
+
3895
+ /** Toggle fullscreen mode
3896
+ * @memberof Draw */
3897
+ function toggleFullscreen()
3898
+ {
3899
+ const rootElement = mainCanvas.parentElement;
3900
+ if (isFullscreen())
3901
+ {
3902
+ if (document.exitFullscreen)
3903
+ document.exitFullscreen();
3904
+ }
3905
+ else if (rootElement.requestFullscreen)
3906
+ rootElement.requestFullscreen();
3907
+ }
3908
+
3909
+ /** Set the cursor style
3910
+ * @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
3911
+ * @memberof Draw */
3912
+ function setCursor(cursorStyle = 'auto')
3913
+ {
3914
+ const rootElement = mainCanvas.parentElement;
3915
+ rootElement.style.cursor = cursorStyle;
3916
+ }
3917
+
3742
3918
  ///////////////////////////////////////////////////////////////////////////////
3743
3919
 
3744
3920
  let engineFontImage;
@@ -3806,7 +3982,7 @@ class FontImage
3806
3982
  (text+'').split('\n').forEach((line, i)=>
3807
3983
  {
3808
3984
  const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
3809
- for(let j=line.length; j--;)
3985
+ for (let j=line.length; j--;)
3810
3986
  {
3811
3987
  // draw each character
3812
3988
  let charCode = line[j].charCodeAt(0);
@@ -3825,37 +4001,6 @@ class FontImage
3825
4001
 
3826
4002
  context.restore();
3827
4003
  }
3828
- }
3829
-
3830
- ///////////////////////////////////////////////////////////////////////////////
3831
- // Display functions
3832
-
3833
- /** Returns true if fullscreen mode is active
3834
- * @return {boolean}
3835
- * @memberof Draw */
3836
- function isFullscreen() { return !!document.fullscreenElement; }
3837
-
3838
- /** Toggle fullscreen mode
3839
- * @memberof Draw */
3840
- function toggleFullscreen()
3841
- {
3842
- const rootElement = mainCanvas.parentElement;
3843
- if (isFullscreen())
3844
- {
3845
- if (document.exitFullscreen)
3846
- document.exitFullscreen();
3847
- }
3848
- else if (rootElement.requestFullscreen)
3849
- rootElement.requestFullscreen();
3850
- }
3851
-
3852
- /** Set the cursor style
3853
- * @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
3854
- * @memberof Draw */
3855
- function setCursor(cursorStyle = 'auto')
3856
- {
3857
- const rootElement = mainCanvas.parentElement;
3858
- rootElement.style.cursor = cursorStyle;
3859
4004
  }
3860
4005
  /**
3861
4006
  * LittleJS Input System
@@ -4407,7 +4552,7 @@ function touchGamepadRender()
4407
4552
  }
4408
4553
  else // draw cross shaped gamepad
4409
4554
  {
4410
- for(let i=10; i--;)
4555
+ for (let i=10; i--;)
4411
4556
  {
4412
4557
  const angle = i*PI/4;
4413
4558
  context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
@@ -4451,7 +4596,7 @@ function pointerLockExit() { document.exitPointerLock && document.exitPointerLoc
4451
4596
  /** Check if pointer is locked (true if locked)
4452
4597
  * @return {boolean}
4453
4598
  * @memberof Input */
4454
- function pointerLockIsActive() { return document.pointerLockElement == mainCanvas }
4599
+ function pointerLockIsActive() { return document.pointerLockElement == mainCanvas; }
4455
4600
  /**
4456
4601
  * LittleJS Audio System
4457
4602
  * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
@@ -4875,7 +5020,7 @@ function zzfxG
4875
5020
  repeatTime = repeatTime * sampleRate | 0;
4876
5021
 
4877
5022
  // generate waveform
4878
- for(length = attack + decay + sustain + release + delay | 0;
5023
+ for (length = attack + decay + sustain + release + delay | 0;
4879
5024
  i < length; b[i++] = s * volume) // sample
4880
5025
  {
4881
5026
  if (!(++crush%(bitCrush*100|0))) // bit crush
@@ -5768,10 +5913,17 @@ class ParticleEmitter extends EngineObject
5768
5913
  const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
5769
5914
  particle.velocity = vec2().setAngle(velocityAngle, speed);
5770
5915
  particle.angleVelocity = angleSpeed;
5916
+ if (!this.localSpace)
5917
+ {
5918
+ // apply emitter velocity to particle
5919
+ particle.velocity.x += this.velocity.x;
5920
+ particle.velocity.y += this.velocity.y;
5921
+ particle.angleVelocity += this.angleVelocity;
5922
+ }
5771
5923
  particle.fadeRate = this.fadeRate;
5772
5924
  particle.damping = this.damping;
5773
5925
  particle.angleDamping = this.angleDamping;
5774
- particle.restitution = this.restitution;
5926
+ particle.restitution = this.restitution;
5775
5927
  particle.friction = this.friction;
5776
5928
  particle.gravityScale = this.gravityScale;
5777
5929
  particle.collideTiles = this.collideTiles;
@@ -6058,7 +6210,7 @@ class Medal
6058
6210
  context.save();
6059
6211
  context.beginPath();
6060
6212
  context.fillStyle = new Color(.9,.9,.9).toString();
6061
- context.strokeStyle = new Color(0,0,0).toString();
6213
+ context.strokeStyle = BLACK.toString();
6062
6214
  context.lineWidth = 3;
6063
6215
  context.rect(x, y, width, height);
6064
6216
  context.fill();
@@ -6075,11 +6227,11 @@ class Medal
6075
6227
  const descriptionSize = height*.3;
6076
6228
  const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
6077
6229
  const textWidth = width - medalDisplayIconSize - 3*gap.x;
6078
- drawTextScreen(this.name, pos, nameSize, new Color(0,0,0), 0, undefined, 'left', undefined, textWidth);
6230
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
6079
6231
 
6080
6232
  // draw the description
6081
6233
  pos.y = y + height - gap.y*2 - descriptionSize/2;
6082
- drawTextScreen(this.description, pos, descriptionSize, new Color(0,0,0), 0, undefined, 'left', undefined, textWidth);
6234
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
6083
6235
  context.restore();
6084
6236
  }
6085
6237
 
@@ -6093,7 +6245,7 @@ class Medal
6093
6245
  if (this.image)
6094
6246
  overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
6095
6247
  else
6096
- drawTextScreen(this.icon, pos, size*.7, new Color(0,0,0));
6248
+ drawTextScreen(this.icon, pos, size*.7, BLACK);
6097
6249
  }
6098
6250
 
6099
6251
  // Get local storage key used by the medal
@@ -6102,12 +6254,13 @@ class Medal
6102
6254
  /**
6103
6255
  * LittleJS WebGL Interface
6104
6256
  * - All webgl used by the engine is wrapped up here
6257
+ * - Will fall back to 2D canvas rendering if webgl is not supported
6105
6258
  * - For normal stuff you won't need to see or call anything in this file
6106
6259
  * - For advanced stuff there are helper functions to create shaders, textures, etc
6107
6260
  * - Can be disabled with glEnable to revert to 2D canvas rendering
6108
6261
  * - Batches sprite rendering on GPU for incredibly fast performance
6109
6262
  * - Sprite transform math is done in the shader where possible
6110
- * - Supports shadertoy style post processing shaders
6263
+ * - Supports shadertoy style post processing shaders via plugin
6111
6264
  * @namespace WebGL
6112
6265
  */
6113
6266
 
@@ -6146,6 +6299,14 @@ function glInit()
6146
6299
  glCanvas = document.createElement('canvas');
6147
6300
  glContext = glCanvas.getContext('webgl2', {antialias:glAntialias});
6148
6301
 
6302
+ if (!glContext)
6303
+ {
6304
+ console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
6305
+ glCanvas = glContext = undefined;
6306
+ glEnable = false;
6307
+ return;
6308
+ }
6309
+
6149
6310
  // create the webgl canvas
6150
6311
  const rootElement = mainCanvas.parentElement;
6151
6312
  rootElement.appendChild(glCanvas);
@@ -6195,7 +6356,7 @@ function glInit()
6195
6356
  // Also used by tile layer rendering when redrawing tiles
6196
6357
  function glPreRender()
6197
6358
  {
6198
- if (!glEnable || headlessMode) return;
6359
+ if (!glEnable || !glContext) return;
6199
6360
 
6200
6361
  // set up the shader and canvas
6201
6362
  glClearCanvas();
@@ -6246,6 +6407,8 @@ function glPreRender()
6246
6407
  * @memberof WebGL */
6247
6408
  function glClearCanvas()
6248
6409
  {
6410
+ if (!glContext) return;
6411
+
6249
6412
  // clear and set to same size as main canvas
6250
6413
  glContext.viewport(0, 0, glCanvas.width=drawCanvas.width, glCanvas.height=drawCanvas.height);
6251
6414
  glContext.clear(glContext.COLOR_BUFFER_BIT);
@@ -6259,7 +6422,7 @@ function glClearCanvas()
6259
6422
  function glSetTexture(texture, wrap=false)
6260
6423
  {
6261
6424
  // must flush cache with the old texture to set a new one
6262
- if (headlessMode || texture == glActiveTexture)
6425
+ if (!glContext || texture == glActiveTexture)
6263
6426
  return;
6264
6427
 
6265
6428
  glFlush();
@@ -6278,6 +6441,8 @@ function glSetTexture(texture, wrap=false)
6278
6441
  * @memberof WebGL */
6279
6442
  function glCompileShader(source, type)
6280
6443
  {
6444
+ if (!glContext) return;
6445
+
6281
6446
  // build the shader
6282
6447
  const shader = glContext.createShader(type);
6283
6448
  glContext.shaderSource(shader, source);
@@ -6296,6 +6461,8 @@ function glCompileShader(source, type)
6296
6461
  * @memberof WebGL */
6297
6462
  function glCreateProgram(vsSource, fsSource)
6298
6463
  {
6464
+ if (!glContext) return;
6465
+
6299
6466
  // build the program
6300
6467
  const program = glContext.createProgram();
6301
6468
  glContext.attachShader(program, glCompileShader(vsSource, glContext.VERTEX_SHADER));
@@ -6314,6 +6481,8 @@ function glCreateProgram(vsSource, fsSource)
6314
6481
  * @memberof WebGL */
6315
6482
  function glCreateTexture(image)
6316
6483
  {
6484
+ if (!glContext) return;
6485
+
6317
6486
  // build the texture
6318
6487
  const texture = glContext.createTexture();
6319
6488
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
@@ -6349,6 +6518,7 @@ function glCreateTexture(image)
6349
6518
  * @memberof WebGL */
6350
6519
  function glDeleteTexture(texture)
6351
6520
  {
6521
+ if (!glContext) return;
6352
6522
  glContext.deleteTexture(texture);
6353
6523
  }
6354
6524
 
@@ -6358,6 +6528,8 @@ function glDeleteTexture(texture)
6358
6528
  * @memberof WebGL */
6359
6529
  function glSetTextureData(texture, image)
6360
6530
  {
6531
+ if (!glContext) return;
6532
+
6361
6533
  // build the texture
6362
6534
  ASSERT(!!image && image.width > 0, 'Invalid image data.');
6363
6535
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
@@ -6368,7 +6540,7 @@ function glSetTextureData(texture, image)
6368
6540
  * @memberof WebGL */
6369
6541
  function glFlush()
6370
6542
  {
6371
- if (!glEnable || !glInstanceCount) return;
6543
+ if (!glEnable || !glContext || !glInstanceCount) return;
6372
6544
 
6373
6545
  const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
6374
6546
  glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
@@ -6388,7 +6560,7 @@ function glFlush()
6388
6560
  * @memberof WebGL */
6389
6561
  function glCopyToContext(context)
6390
6562
  {
6391
- if (!glEnable)
6563
+ if (!glEnable || !glContext)
6392
6564
  return;
6393
6565
 
6394
6566
  glFlush();
@@ -6643,6 +6815,13 @@ class PostProcessPlugin
6643
6815
  postProcess = this;
6644
6816
 
6645
6817
  if (headlessMode) return;
6818
+
6819
+ if (!glEnable)
6820
+ {
6821
+ console.warn('PostProcessPlugin: WebGL not enabled!');
6822
+ return;
6823
+ }
6824
+
6646
6825
  if (!shaderCode) // default shader pass through
6647
6826
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
6648
6827
 
@@ -6952,7 +7131,7 @@ class UISystemPlugin
6952
7131
 
6953
7132
  function updateInvisible(o)
6954
7133
  {
6955
- for(const c of o.children)
7134
+ for (const c of o.children)
6956
7135
  updateInvisible(c);
6957
7136
  o.updateInvisible();
6958
7137
  }
@@ -6968,7 +7147,7 @@ class UISystemPlugin
6968
7147
  if (o.parent)
6969
7148
  o.pos = o.localPos.add(o.parent.pos);
6970
7149
  // update in reverse order to detect mouse enter/leave
6971
- for(let i=o.children.length; i--;)
7150
+ for (let i=o.children.length; i--;)
6972
7151
  updateObject(o.children[i]);
6973
7152
  o.update();
6974
7153
  }
@@ -6986,7 +7165,7 @@ class UISystemPlugin
6986
7165
  if (o.parent)
6987
7166
  o.pos = o.localPos.add(o.parent.pos);
6988
7167
  o.render();
6989
- for(const c of o.children)
7168
+ for (const c of o.children)
6990
7169
  renderObject(c);
6991
7170
  }
6992
7171
  uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
@@ -7043,7 +7222,7 @@ class UISystemPlugin
7043
7222
  * @param {boolean} [mirror] */
7044
7223
  drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false)
7045
7224
  {
7046
- drawTile(pos, size, tileInfo, color, angle, mirror, BLACK, false, true, uiSystem.uiContext);
7225
+ drawTile(pos, size, tileInfo, color, angle, mirror, CLEAR_BLACK, false, true, uiSystem.uiContext);
7047
7226
  }
7048
7227
 
7049
7228
  /** Draw text to the UI context
@@ -7493,7 +7672,7 @@ class Box2dObject extends EngineObject
7493
7672
  bodyDef.set_angle(-angle);
7494
7673
  this.body = box2d.world.CreateBody(bodyDef);
7495
7674
  this.body.object = this;
7496
- this.outlineColor = BLACK;
7675
+ this.lineColor = BLACK;
7497
7676
  }
7498
7677
 
7499
7678
  /** Destroy this object and it's physics body */
@@ -7520,7 +7699,7 @@ class Box2dObject extends EngineObject
7520
7699
  if (this.tileInfo)
7521
7700
  super.render();
7522
7701
  else
7523
- this.drawFixtures(this.color, this.outlineColor, this.lineWidth, mainContext);
7702
+ this.drawFixtures(this.color, this.lineColor, this.lineWidth, mainContext);
7524
7703
  }
7525
7704
 
7526
7705
  /** Render debug info */
@@ -7534,13 +7713,13 @@ class Box2dObject extends EngineObject
7534
7713
 
7535
7714
  /** Draws all this object's fixtures
7536
7715
  * @param {Color} [color]
7537
- * @param {Color} [outlineColor]
7716
+ * @param {Color} [lineColor]
7538
7717
  * @param {number} [lineWidth]
7539
7718
  * @param {CanvasRenderingContext2D} [context] */
7540
- drawFixtures(color=WHITE, outlineColor, lineWidth=.1, context)
7719
+ drawFixtures(color=WHITE, lineColor, lineWidth=.1, context)
7541
7720
  {
7542
7721
  this.getFixtureList().forEach(fixture=>
7543
- box2d.drawFixture(fixture, this.pos, this.angle, color, outlineColor, lineWidth, context));
7722
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context));
7544
7723
  }
7545
7724
 
7546
7725
  ///////////////////////////////////////////////////////////////////////////////
@@ -9021,10 +9200,10 @@ class Box2dPlugin
9021
9200
  * @param {Vector2} pos
9022
9201
  * @param {number} angle
9023
9202
  * @param {Color} [color]
9024
- * @param {Color} [outlineColor]
9203
+ * @param {Color} [lineColor]
9025
9204
  * @param {number} [lineWidth]
9026
9205
  * @param {CanvasRenderingContext2D} [context] */
9027
- drawFixture(fixture, pos, angle, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=drawContext)
9206
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context=drawContext)
9028
9207
  {
9029
9208
  const shape = box2d.castObjectType(fixture.GetShape());
9030
9209
  switch (shape.GetType())
@@ -9034,101 +9213,25 @@ class Box2dPlugin
9034
9213
  let points = [];
9035
9214
  for (let i=shape.GetVertexCount(); i--;)
9036
9215
  points.push(box2d.vec2From(shape.GetVertex(i)));
9037
- box2d.drawPoly(pos, angle, points, color, outlineColor, lineWidth, context);
9216
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, false, false, context);
9038
9217
  break;
9039
9218
  }
9040
9219
  case box2d.instance.b2Shape.e_circle:
9041
9220
  {
9042
9221
  const radius = shape.get_m_radius();
9043
- box2d.drawCircle(pos, radius, color, outlineColor, lineWidth, context);
9222
+ drawCircle(pos, radius, color, lineWidth, lineColor, false, false, context);
9044
9223
  break;
9045
9224
  }
9046
9225
  case box2d.instance.b2Shape.e_edge:
9047
9226
  {
9048
9227
  const v1 = box2d.vec2From(shape.get_m_vertex1());
9049
9228
  const v2 = box2d.vec2From(shape.get_m_vertex2());
9050
- box2d.drawLine(pos, angle, v1, v2, color, lineWidth, context);
9229
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle, false, false, context);
9051
9230
  break;
9052
9231
  }
9053
9232
  }
9054
9233
  }
9055
9234
 
9056
- /** draws a circle
9057
- * @param {Vector2} pos
9058
- * @param {number} radius
9059
- * @param {Color} [color]
9060
- * @param {Color} [outlineColor]
9061
- * @param {number} [lineWidth]
9062
- * @param {CanvasRenderingContext2D} [context] */
9063
- drawCircle(pos, radius, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=drawContext)
9064
- {
9065
- drawCanvas2D(pos, vec2(1), 0, 0, context=>
9066
- {
9067
- context.beginPath();
9068
- context.arc(0, 0, radius, 0, 9);
9069
- box2d.drawFillStroke(color, outlineColor, lineWidth, context);
9070
- }, 0, context);
9071
- }
9072
-
9073
- /** draws a polygon
9074
- * @param {Vector2} pos
9075
- * @param {number} angle
9076
- * @param {Array<Vector2>} points
9077
- * @param {Color} [color]
9078
- * @param {Color} [outlineColor]
9079
- * @param {number} [lineWidth]
9080
- * @param {CanvasRenderingContext2D} [context] */
9081
- drawPoly(pos, angle, points, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=drawContext)
9082
- {
9083
- drawCanvas2D(pos, vec2(1), angle, 0, context=>
9084
- {
9085
- context.beginPath();
9086
- points.forEach(p=>context.lineTo(p.x, p.y));
9087
- context.closePath();
9088
- box2d.drawFillStroke(color, outlineColor, lineWidth, context);
9089
- }, 0, context);
9090
- }
9091
-
9092
- /** draws a line
9093
- * @param {Vector2} pos
9094
- * @param {number} angle
9095
- * @param {Vector2} posA
9096
- * @param {Vector2} posB
9097
- * @param {Color} [color]
9098
- * @param {number} [lineWidth]
9099
- * @param {CanvasRenderingContext2D} [context] */
9100
- drawLine(pos, angle, posA, posB, color=WHITE, lineWidth=.1, context=drawContext)
9101
- {
9102
- drawCanvas2D(pos, vec2(1), angle, 0, context=>
9103
- {
9104
- context.beginPath();
9105
- context.lineTo(posA.x, posA.y);
9106
- context.lineTo(posB.x, posB.y);
9107
- box2d.drawFillStroke(0, color, lineWidth, context);
9108
- }, 0, context);
9109
- }
9110
-
9111
- /** performs a fill or stroke as a helper to the other draw functions
9112
- * @param {Color} [color]
9113
- * @param {Color} [outlineColor]
9114
- * @param {number} [lineWidth]
9115
- * @param {CanvasRenderingContext2D} [context] */
9116
- drawFillStroke(color=WHITE, outlineColor=BLACK, lineWidth=.1, context=drawContext)
9117
- {
9118
- if (color)
9119
- {
9120
- context.fillStyle = color.toString();
9121
- context.fill();
9122
- }
9123
- if (outlineColor && lineWidth)
9124
- {
9125
- context.lineWidth = lineWidth;
9126
- context.lineJoin = context.lineCap = 'round';
9127
- context.strokeStyle = outlineColor.toString();
9128
- context.stroke();
9129
- }
9130
- }
9131
-
9132
9235
  ///////////////////////////////////////////////////////////////////////////////
9133
9236
  // helper functions
9134
9237
 
@@ -9229,6 +9332,7 @@ async function box2dInit()
9229
9332
  function setupDebugDraw()
9230
9333
  {
9231
9334
  // setup debug draw
9335
+ const debugLineWidth = .1;
9232
9336
  const debugDraw = new box2d.instance.JSDraw();
9233
9337
  const box2dColor = (c)=> new Color(c.get_r(), c.get_g(), c.get_b());
9234
9338
  const box2dColorPointer = (c)=>
@@ -9246,33 +9350,33 @@ async function box2dInit()
9246
9350
  color = getDebugColor(color);
9247
9351
  point1 = box2d.vec2FromPointer(point1);
9248
9352
  point2 = box2d.vec2FromPointer(point2);
9249
- box2d.drawLine(vec2(), 0, point1, point2, color, undefined, overlayContext);
9353
+ drawLine(point1, point2, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
9250
9354
  };
9251
9355
  debugDraw.DrawPolygon = function(vertices, vertexCount, color)
9252
9356
  {
9253
9357
  color = getDebugColor(color);
9254
9358
  const points = getPointsList(vertices, vertexCount);
9255
- box2d.drawPoly(vec2(), 0, points, undefined, color, undefined, overlayContext);
9359
+ drawPoly(points, CLEAR_WHITE, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
9256
9360
  };
9257
9361
  debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
9258
9362
  {
9259
9363
  color = getDebugColor(color);
9260
9364
  const points = getPointsList(vertices, vertexCount);
9261
- box2d.drawPoly(vec2(), 0, points, color, color, undefined, overlayContext);
9365
+ drawPoly(points, color, 0, color, vec2(), 0, false, false, overlayContext);
9262
9366
  };
9263
9367
  debugDraw.DrawCircle = function(center, radius, color)
9264
9368
  {
9265
9369
  color = getDebugColor(color);
9266
9370
  center = box2d.vec2FromPointer(center);
9267
- box2d.drawCircle(center, radius, undefined, color, undefined, overlayContext);
9371
+ drawCircle(center, radius, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
9268
9372
  };
9269
9373
  debugDraw.DrawSolidCircle = function(center, radius, axis, color)
9270
9374
  {
9271
9375
  color = getDebugColor(color);
9272
9376
  center = box2d.vec2FromPointer(center);
9273
9377
  axis = box2d.vec2FromPointer(axis).scale(radius);
9274
- box2d.drawCircle(center, radius, color, color, undefined, overlayContext);
9275
- box2d.drawLine(center, 0, vec2(), axis, color, undefined, overlayContext);
9378
+ drawCircle(center, radius, color, debugLineWidth, color, false, false, overlayContext);
9379
+ drawLine(vec2(), axis, debugLineWidth, color, center, 0, false, false, overlayContext);
9276
9380
  };
9277
9381
  debugDraw.DrawTransform = function(transform)
9278
9382
  {
@@ -9281,8 +9385,8 @@ async function box2dInit()
9281
9385
  const angle = -transform.get_q().GetAngle();
9282
9386
  const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
9283
9387
  const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
9284
- box2d.drawLine(pos, angle, vec2(), p1, c1, undefined, overlayContext);
9285
- box2d.drawLine(pos, angle, vec2(), p2, c2, undefined, overlayContext);
9388
+ drawLine(vec2(), p1, debugLineWidth, c1, pos, angle, false, false, overlayContext);
9389
+ drawLine(vec2(), p2, debugLineWidth, c2, pos, angle, false, false, overlayContext);
9286
9390
  }
9287
9391
 
9288
9392
  debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
@@ -9307,12 +9411,13 @@ async function box2dInit()
9307
9411
  * @param {Vector2} pos - Screen space position
9308
9412
  * @param {Vector2} size - Screen space size
9309
9413
  * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
9310
- * @param {number} [borderSize=1] - Width of the border sections
9311
- * @param {number} [extraSpace=.01] - Extra spacing adjustment
9414
+ * @param {number} [borderSize] - Width of the border sections
9415
+ * @param {number} [extraSpace] - Extra spacing adjustment
9416
+ * @param {number} [angle] - Angle to rotate by
9312
9417
  * @memberof DrawUtilities */
9313
- function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=1)
9418
+ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
9314
9419
  {
9315
- drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, false, true, overlayContext);
9420
+ drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
9316
9421
  }
9317
9422
 
9318
9423
  /** Draw a scalable nine-slice UI element in world space
@@ -9321,14 +9426,15 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=1)
9321
9426
  * @param {Vector2} size - World space size
9322
9427
  * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
9323
9428
  * @param {Color} [color] - Color to modulate with
9324
- * @param {number} [borderSize=1] - Width of the border sections
9429
+ * @param {number} [borderSize] - Width of the border sections
9325
9430
  * @param {Color} [additiveColor] - Additive color
9326
- * @param {number} [extraSpace=.01] - Extra spacing adjustment
9431
+ * @param {number} [extraSpace] - Extra spacing adjustment
9432
+ * @param {number} [angle] - Angle to rotate by
9327
9433
  * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
9328
9434
  * @param {boolean} [screenSpace] - Use screen space coordinates
9329
9435
  * @param {CanvasRenderingContext2D} [context] - Canvas context to use
9330
9436
  * @memberof DrawUtilities */
9331
- function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.01, useWebGL=glEnable, screenSpace, context)
9437
+ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
9332
9438
  {
9333
9439
  // setup nine slice tiles
9334
9440
  const centerTile = startTile.offset(startTile.size);
@@ -9336,26 +9442,27 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
9336
9442
  const cornerSize = vec2(borderSize);
9337
9443
  const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5));
9338
9444
  const flip = screenSpace ? -1 : 1;
9445
+ const rotateAngle = screenSpace ? -angle : angle;
9339
9446
 
9340
9447
  // center
9341
- drawTile(pos, centerSize, centerTile, color, 0, false, additiveColor, useWebGL, screenSpace, context);
9342
- for(let i=4; i--;)
9448
+ drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
9449
+ for (let i=4; i--;)
9343
9450
  {
9344
9451
  // sides
9345
9452
  const horizontal = i%2;
9346
9453
  const sidePos = cornerOffset.multiply(vec2(horizontal?i==1?1:-1:0, horizontal?0:i?-1:1));
9347
9454
  const sideSize = vec2(horizontal ? borderSize : centerSize.x, horizontal ? centerSize.y : borderSize);
9348
9455
  const sideTile = centerTile.offset(startTile.size.multiply(vec2(i==1?1:i==3?-1:0,i==0?-flip:i==2?flip:0)))
9349
- drawTile(pos.add(sidePos), sideSize, sideTile, color, 0, false, additiveColor, useWebGL, screenSpace, context);
9456
+ drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
9350
9457
  }
9351
- for(let i=4; i--;)
9458
+ for (let i=4; i--;)
9352
9459
  {
9353
9460
  // corners
9354
9461
  const flipX = i>1;
9355
9462
  const flipY = i && i<3;
9356
9463
  const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-1:1));
9357
9464
  const cornerTile = centerTile.offset(startTile.size.multiply(vec2(flipX?-1:1,flipY?flip:-flip)));
9358
- drawTile(pos.add(cornerPos), cornerSize, cornerTile, color, 0, false, additiveColor, useWebGL, screenSpace, context);
9465
+ drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
9359
9466
  }
9360
9467
  }
9361
9468
 
@@ -9364,12 +9471,13 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
9364
9471
  * @param {Vector2} pos - Screen space position
9365
9472
  * @param {Vector2} size - Screen space size
9366
9473
  * @param {TileInfo} startTile - Starting tile for the three-slice pattern
9367
- * @param {number} [borderSize=1] - Width of the border sections
9368
- * @param {number} [extraSpace=.01] - Extra spacing adjustment
9474
+ * @param {number} [borderSize] - Width of the border sections
9475
+ * @param {number} [extraSpace] - Extra spacing adjustment
9476
+ * @param {number} [angle] - Angle to rotate by
9369
9477
  * @memberof DrawUtilities */
9370
- function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=1)
9478
+ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
9371
9479
  {
9372
- drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, false, true, overlayContext);
9480
+ drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
9373
9481
  }
9374
9482
 
9375
9483
  /** Draw a scalable three-slice UI element in world space
@@ -9378,14 +9486,15 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=1)
9378
9486
  * @param {Vector2} size - World space size
9379
9487
  * @param {TileInfo} startTile - Starting tile for the three-slice pattern
9380
9488
  * @param {Color} [color] - Color to modulate with
9381
- * @param {number} [borderSize=1] - Width of the border sections
9489
+ * @param {number} [borderSize] - Width of the border sections
9382
9490
  * @param {Color} [additiveColor] - Additive color
9383
- * @param {number} [extraSpace=.01] - Extra spacing adjustment
9491
+ * @param {number} [extraSpace] - Extra spacing adjustment
9492
+ * @param {number} [angle] - Angle to rotate by
9384
9493
  * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
9385
9494
  * @param {boolean} [screenSpace] - Use screen space coordinates
9386
9495
  * @param {CanvasRenderingContext2D} [context] - Canvas context to use
9387
9496
  * @memberof DrawUtilities */
9388
- function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.01, useWebGL=glEnable, screenSpace, context)
9497
+ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
9389
9498
  {
9390
9499
  // setup three slice tiles
9391
9500
  const cornerTile = startTile.frame(0);
@@ -9395,25 +9504,26 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
9395
9504
  const cornerSize = vec2(borderSize);
9396
9505
  const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5));
9397
9506
  const flip = screenSpace ? -1 : 1;
9507
+ const rotateAngle = screenSpace ? -angle : angle;
9398
9508
 
9399
9509
  // center
9400
- drawTile(pos, centerSize, centerTile, color, 0, false, additiveColor, useWebGL, screenSpace, context);
9401
- for(let i=4; i--;)
9510
+ drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
9511
+ for (let i=4; i--;)
9402
9512
  {
9403
9513
  // sides
9404
- const angle = i*PI/2;
9514
+ const a = angle + i*PI/2;
9405
9515
  const horizontal = i%2;
9406
9516
  const sidePos = cornerOffset.multiply(vec2(horizontal?i==1?1:-1:0, horizontal?0:i?-flip:flip));
9407
9517
  const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize);
9408
- drawTile(pos.add(sidePos), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
9518
+ drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
9409
9519
  }
9410
- for(let i=4; i--;)
9520
+ for (let i=4; i--;)
9411
9521
  {
9412
9522
  // corners
9413
- const angle = i*PI/2;
9523
+ const a = angle + i*PI/2;
9414
9524
  const flipX = !i || i>2;
9415
9525
  const flipY = i>1;
9416
9526
  const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-flip:flip));
9417
- drawTile(pos.add(cornerPos), cornerSize, cornerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
9527
+ drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
9418
9528
  }
9419
9529
  }