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/src/engineDraw.js CHANGED
@@ -52,6 +52,16 @@ let drawCanvas;
52
52
  * @memberof Draw */
53
53
  let drawContext;
54
54
 
55
+ /** Offscreen canvas that can be used for image processing
56
+ * @type {OffscreenCanvas}
57
+ * @memberof Draw */
58
+ let workCanvas;
59
+
60
+ /** Offscreen canvas that can be used for image processing
61
+ * @type {OffscreenCanvasRenderingContext2D}
62
+ * @memberof Draw */
63
+ let workContext;
64
+
55
65
  /** The size of the main canvas (and other secondary canvases)
56
66
  * @type {Vector2}
57
67
  * @memberof Draw */
@@ -71,10 +81,10 @@ let drawCount;
71
81
  * Create a tile info object using a grid based system
72
82
  * - This can take vecs or floats for easier use and conversion
73
83
  * - If an index is passed in, the tile size and index will determine the position
74
- * @param {Vector2|number} [pos=0] - Index of tile in sheet
84
+ * @param {Vector2|number} [pos=0] - Index of tile in sheet
75
85
  * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
76
- * @param {number} [textureIndex] - Texture index to use
77
- * @param {number} [padding] - How many pixels padding around tiles
86
+ * @param {number} [textureIndex] - Texture index to use
87
+ * @param {number} [padding] - How many pixels padding around tiles
78
88
  * @return {TileInfo}
79
89
  * @example
80
90
  * tile(2) // a tile at index 2 using the default tile size of 16
@@ -201,55 +211,7 @@ class TextureInfo
201
211
  }
202
212
 
203
213
  ///////////////////////////////////////////////////////////////////////////////
204
-
205
- /** Convert from screen to world space coordinates
206
- * @param {Vector2} screenPos
207
- * @return {Vector2}
208
- * @memberof Draw */
209
- function screenToWorld(screenPos)
210
- {
211
- let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
212
- let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
213
- if (cameraAngle)
214
- {
215
- // apply camera rotation
216
- const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
217
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
218
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
219
- cameraPosRelativeX = rotatedX;
220
- cameraPosRelativeY = rotatedY;
221
- }
222
- return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
223
- }
224
-
225
- /** Convert from world to screen space coordinates
226
- * @param {Vector2} worldPos
227
- * @return {Vector2}
228
- * @memberof Draw */
229
- function worldToScreen(worldPos)
230
- {
231
- let cameraPosRelativeX = worldPos.x - cameraPos.x;
232
- let cameraPosRelativeY = worldPos.y - cameraPos.y;
233
- if (cameraAngle)
234
- {
235
- // apply inverse camera rotation
236
- const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
237
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
238
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
239
- cameraPosRelativeX = rotatedX;
240
- cameraPosRelativeY = rotatedY;
241
- }
242
- return new Vector2
243
- (
244
- cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
245
- cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
246
- );
247
- }
248
-
249
- /** Get the camera's visible area in world space
250
- * @return {Vector2}
251
- * @memberof Draw */
252
- function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
214
+ // Drawing functions
253
215
 
254
216
  /** Draw textured tile centered in world space, with color applied if using WebGL
255
217
  * @param {Vector2} pos - Center of the tile in world space
@@ -272,6 +234,9 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
272
234
  ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
273
235
  ASSERT(isNumber(angle), 'drawTile angle should be a number');
274
236
 
237
+ if (color.a <= 0 && (!additiveColor || additiveColor.a <= 0) || !size.x || !size.y)
238
+ return; // completely invisible, skip render
239
+
275
240
  const textureInfo = tileInfo && tileInfo.textureInfo;
276
241
  if (useWebGL)
277
242
  {
@@ -322,18 +287,15 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
322
287
  if (textureInfo)
323
288
  {
324
289
  // calculate uvs and render
325
- const x = tileInfo.pos.x + tileFixBleedScale;
326
- const y = tileInfo.pos.y + tileFixBleedScale;
327
- const w = tileInfo.size.x - 2*tileFixBleedScale;
328
- const h = tileInfo.size.y - 2*tileFixBleedScale;
329
- context.globalAlpha = color.a; // only alpha is supported
330
- context.drawImage(textureInfo.image, x, y, w, h, -.5, -.5, 1, 1);
331
- context.globalAlpha = 1; // set back to full alpha
290
+ const x = tileInfo.pos.x, y = tileInfo.pos.y;
291
+ const w = tileInfo.size.x, h = tileInfo.size.y;
292
+ drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor);
332
293
  }
333
294
  else
334
295
  {
335
- // if no tile info, force untextured
336
- context.fillStyle = color.toString();
296
+ // if no tile info, use untextured rect
297
+ const c = additiveColor ? color.add(additiveColor) : color;
298
+ context.fillStyle = c.toString();
337
299
  context.fillRect(-.5, -.5, 1, 1);
338
300
  }
339
301
  }, screenSpace, context);
@@ -346,7 +308,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
346
308
  * @param {Color} [color=(1,1,1,1)]
347
309
  * @param {number} [angle]
348
310
  * @param {boolean} [useWebGL=glEnable]
349
- * @param {boolean} [screenSpace=false]
311
+ * @param {boolean} [screenSpace]
350
312
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
351
313
  * @memberof Draw */
352
314
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
@@ -359,73 +321,90 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
359
321
  * @param {Vector2} posB
360
322
  * @param {number} [thickness]
361
323
  * @param {Color} [color=(1,1,1,1)]
324
+ * @param {Vector2} [pos=(0,0)] - Offset to apply
325
+ * @param {number} [angle] - Angle to rotate by
362
326
  * @param {boolean} [useWebGL=glEnable]
363
- * @param {boolean} [screenSpace=false]
327
+ * @param {boolean} [screenSpace]
364
328
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
365
329
  * @memberof Draw */
366
- function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
330
+ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
367
331
  {
368
332
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
369
333
  const size = vec2(thickness, halfDelta.length()*2);
370
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace, context);
334
+ pos = pos.add(posA.add(halfDelta));
335
+ angle += halfDelta.angle();
336
+ drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
371
337
  }
372
338
 
373
339
  /** Draw colored polygon using passed in points
374
- * @param {Array<Vector2>} points - Array of Vector2 points
340
+ * @param {Array<Vector2>} points - Array of Vector2 points
375
341
  * @param {Color} [color=(1,1,1,1)]
376
- * @param {number} [lineWidth=0]
342
+ * @param {number} [lineWidth]
377
343
  * @param {Color} [lineColor=(0,0,0,1)]
378
- * @param {boolean} [screenSpace=false]
344
+ * @param {Vector2} [pos=(0,0)] - Offset to apply
345
+ * @param {number} [angle] - Angle to rotate by
346
+ * @param {boolean} [useWebGL] - Webgl not supported
347
+ * @param {boolean} [screenSpace]
379
348
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
380
349
  * @memberof Draw */
381
- function drawPoly(points, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=drawContext)
350
+ function drawPoly(points, color=new Color, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=false, screenSpace=false, context=drawContext)
382
351
  {
383
- ASSERT(isColor(color) && isColor(lineColor));
384
- context.fillStyle = color.toString();
385
- context.beginPath();
386
- for (const point of screenSpace ? points : points.map(worldToScreen))
387
- context.lineTo(point.x, point.y);
388
- context.closePath();
389
- context.fill();
390
- if (lineWidth)
352
+ ASSERT(isVector2(pos) && pos.isValid(), 'drawPoly pos should be a vec2');
353
+ ASSERT(Array.isArray(points), 'drawPoly points should be an array');
354
+ ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
355
+ ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
356
+ ASSERT(isNumber(angle), 'drawPoly angle should be a number');
357
+ ASSERT(!useWebGL, 'drawPoly webgl not supported');
358
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
391
359
  {
392
- context.strokeStyle = lineColor.toString();
393
- context.lineWidth = screenSpace ? lineWidth : lineWidth*cameraScale;
394
- context.stroke();
395
- }
360
+ context.beginPath();
361
+ for (const point of points)
362
+ context.lineTo(point.x, point.y);
363
+ context.closePath();
364
+ context.fillStyle = color.toString();
365
+ context.fill();
366
+ if (lineWidth)
367
+ {
368
+ context.strokeStyle = lineColor.toString();
369
+ context.lineWidth = lineWidth;
370
+ context.stroke();
371
+ }
372
+ }, screenSpace, context);
396
373
  }
397
374
 
398
375
  /** Draw colored ellipse using passed in point
399
376
  * @param {Vector2} pos
400
- * @param {number} [width=1]
401
- * @param {number} [height=1]
402
- * @param {number} [angle=0]
377
+ * @param {Vector2} [size=(1,1)]
403
378
  * @param {Color} [color=(1,1,1,1)]
404
- * @param {number} [lineWidth=0]
379
+ * @param {number} [angle]
380
+ * @param {number} [lineWidth]
405
381
  * @param {Color} [lineColor=(0,0,0,1)]
406
- * @param {boolean} [screenSpace=false]
382
+ * @param {boolean} [useWebGL] - Webgl not supported
383
+ * @param {boolean} [screenSpace]
407
384
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
408
385
  * @memberof Draw */
409
- function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=drawContext)
386
+ function drawEllipse(pos, size=vec2(1), color=new Color, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace=false, context=drawContext)
410
387
  {
411
- ASSERT(isColor(color) && isColor(lineColor));
412
- if (!screenSpace)
413
- {
414
- pos = worldToScreen(pos);
415
- width *= cameraScale;
416
- height *= cameraScale;
417
- lineWidth *= cameraScale;
418
- }
419
- context.fillStyle = color.toString();
420
- context.beginPath();
421
- context.ellipse(pos.x, pos.y, width, height, angle, 0, 9);
422
- context.fill();
423
- if (lineWidth)
388
+ ASSERT(isVector2(pos) && pos.isValid(), 'drawEllipse pos should be a vec2');
389
+ ASSERT(isVector2(size) && size.isValid(), 'drawEllipse size should be a vec2');
390
+ ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
391
+ ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
392
+ ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
393
+ ASSERT(lineWidth>=0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
394
+ ASSERT(!useWebGL, 'drawEllipse webgl not supported');
395
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
424
396
  {
425
- context.strokeStyle = lineColor.toString();
426
- context.lineWidth = lineWidth;
427
- context.stroke();
428
- }
397
+ context.beginPath();
398
+ context.ellipse(0, 0, size.y, size.x, 0, 0, 9);
399
+ context.fillStyle = color.toString();
400
+ context.fill();
401
+ if (lineWidth)
402
+ {
403
+ context.strokeStyle = lineColor.toString();
404
+ context.lineWidth = lineWidth;
405
+ context.stroke();
406
+ }
407
+ }, screenSpace, context);
429
408
  }
430
409
 
431
410
  /** Draw colored circle using passed in point
@@ -434,11 +413,12 @@ function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth
434
413
  * @param {Color} [color=(1,1,1,1)]
435
414
  * @param {number} [lineWidth=0]
436
415
  * @param {Color} [lineColor=(0,0,0,1)]
416
+ * @param {boolean} [useWebGL] - Webgl not supported
437
417
  * @param {boolean} [screenSpace=false]
438
418
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
439
419
  * @memberof Draw */
440
- function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=drawContext)
441
- { drawEllipse(pos, radius, radius, 0, color, lineWidth, lineColor, screenSpace, context); }
420
+ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace, context=drawContext)
421
+ { drawEllipse(pos, vec2(radius), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context); }
442
422
 
443
423
  /** Draw directly to a 2d canvas context in world space
444
424
  * @param {Vector2} pos
@@ -465,6 +445,9 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
465
445
  context.restore();
466
446
  }
467
447
 
448
+ ///////////////////////////////////////////////////////////////////////////////
449
+ // Text Drawing Functions
450
+
468
451
  /** Draw text on main canvas in world space
469
452
  * Automatically splits new lines into rows
470
453
  * @param {string} text
@@ -513,7 +496,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
513
496
  * @param {number} [maxWidth]
514
497
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
515
498
  * @memberof Draw */
516
- 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)
499
+ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
517
500
  {
518
501
  context.fillStyle = color.toString();
519
502
  context.strokeStyle = lineColor.toString();
@@ -534,22 +517,67 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
534
517
  });
535
518
  }
536
519
 
520
+ ///////////////////////////////////////////////////////////////////////////////
521
+ // Drawing utilities
522
+
523
+ /** Convert from screen to world space coordinates
524
+ * @param {Vector2} screenPos
525
+ * @return {Vector2}
526
+ * @memberof Draw */
527
+ function screenToWorld(screenPos)
528
+ {
529
+ let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
530
+ let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
531
+ if (cameraAngle)
532
+ {
533
+ // apply camera rotation
534
+ const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
535
+ const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
536
+ const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
537
+ cameraPosRelativeX = rotatedX;
538
+ cameraPosRelativeY = rotatedY;
539
+ }
540
+ return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
541
+ }
542
+
543
+ /** Convert from world to screen space coordinates
544
+ * @param {Vector2} worldPos
545
+ * @return {Vector2}
546
+ * @memberof Draw */
547
+ function worldToScreen(worldPos)
548
+ {
549
+ let cameraPosRelativeX = worldPos.x - cameraPos.x;
550
+ let cameraPosRelativeY = worldPos.y - cameraPos.y;
551
+ if (cameraAngle)
552
+ {
553
+ // apply inverse camera rotation
554
+ const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
555
+ const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
556
+ const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
557
+ cameraPosRelativeX = rotatedX;
558
+ cameraPosRelativeY = rotatedY;
559
+ }
560
+ return new Vector2
561
+ (
562
+ cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
563
+ cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
564
+ );
565
+ }
566
+
567
+ /** Get the camera's visible area in world space
568
+ * @return {Vector2}
569
+ * @memberof Draw */
570
+ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
571
+
537
572
  /** Enable normal or additive blend mode
538
573
  * @param {boolean} [additive]
539
- * @param {boolean} [useWebGL=glEnable]
540
574
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
541
575
  * @memberof Draw */
542
- function setBlendMode(additive=false, useWebGL=glEnable, context)
576
+ function setBlendMode(additive=false, context)
543
577
  {
544
- ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
545
- if (useWebGL)
546
- glAdditive = additive;
547
- else
548
- {
549
- if (!context)
550
- context = drawContext;
551
- context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
552
- }
578
+ glAdditive = additive;
579
+ context ||= drawContext;
580
+ context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
553
581
  }
554
582
 
555
583
  /** Combines all LittleJS canvases onto the main canvas and clears them
@@ -566,6 +594,102 @@ function combineCanvases()
566
594
  overlayCanvas.width |= 0;
567
595
  }
568
596
 
597
+ /** Helper function to draw an image with color and additive color applied
598
+ * This is slower then normal drawImage when color is applied
599
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
600
+ * @param {HTMLImageElement|OffscreenCanvas} image
601
+ * @param {number} sx
602
+ * @param {number} sy
603
+ * @param {number} sWidth
604
+ * @param {number} sHeight
605
+ * @param {number} dx
606
+ * @param {number} dy
607
+ * @param {number} dWidth
608
+ * @param {number} dHeight
609
+ * @param {Color} color
610
+ * @param {Color} [additiveColor]
611
+ * @memberof Draw */
612
+ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor)
613
+ {
614
+ function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
615
+ function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
616
+ const sx2 = tileFixBleedScale;
617
+ const sy2 = tileFixBleedScale;
618
+ const sWidth2 = sWidth - 2*tileFixBleedScale;
619
+ const sHeight2 = sHeight - 2*tileFixBleedScale;
620
+ if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
621
+ {
622
+ // white texture with no additive alpha, no need to tint
623
+ context.globalAlpha = color.a;
624
+ context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
625
+ context.globalAlpha = 1;
626
+ }
627
+ else
628
+ {
629
+ // copy to offscreen canvas
630
+ workCanvas.width = sWidth;
631
+ workCanvas.height = sHeight;
632
+ workContext.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
633
+
634
+ // tint image using offscreen work context
635
+ const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
636
+ const data = imageData.data;
637
+ if (additiveColor && !isBlack(additiveColor))
638
+ {
639
+ // slower path with additive color
640
+ const colorMultiply = [color.r, color.g, color.b, color.a];
641
+ const colorAdd = [additiveColor.r * 255, additiveColor.g * 255, additiveColor.b * 255, additiveColor.a * 255];
642
+ for (let i = 0; i < data.length; ++i)
643
+ data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
644
+ workContext.putImageData(imageData, 0, 0);
645
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
646
+ }
647
+ else
648
+ {
649
+ // faster path with no additive color
650
+ for (let i = 0; i < data.length; i+=4)
651
+ {
652
+ data[i ] *= color.r;
653
+ data[i+1] *= color.g;
654
+ data[i+2] *= color.b;
655
+ }
656
+ workContext.putImageData(imageData, 0, 0);
657
+ context.globalAlpha = color.a;
658
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
659
+ context.globalAlpha = 1;
660
+ }
661
+ }
662
+ }
663
+
664
+
665
+ /** Returns true if fullscreen mode is active
666
+ * @return {boolean}
667
+ * @memberof Draw */
668
+ function isFullscreen() { return !!document.fullscreenElement; }
669
+
670
+ /** Toggle fullscreen mode
671
+ * @memberof Draw */
672
+ function toggleFullscreen()
673
+ {
674
+ const rootElement = mainCanvas.parentElement;
675
+ if (isFullscreen())
676
+ {
677
+ if (document.exitFullscreen)
678
+ document.exitFullscreen();
679
+ }
680
+ else if (rootElement.requestFullscreen)
681
+ rootElement.requestFullscreen();
682
+ }
683
+
684
+ /** Set the cursor style
685
+ * @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
686
+ * @memberof Draw */
687
+ function setCursor(cursorStyle = 'auto')
688
+ {
689
+ const rootElement = mainCanvas.parentElement;
690
+ rootElement.style.cursor = cursorStyle;
691
+ }
692
+
569
693
  ///////////////////////////////////////////////////////////////////////////////
570
694
 
571
695
  let engineFontImage;
@@ -633,7 +757,7 @@ class FontImage
633
757
  (text+'').split('\n').forEach((line, i)=>
634
758
  {
635
759
  const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
636
- for(let j=line.length; j--;)
760
+ for (let j=line.length; j--;)
637
761
  {
638
762
  // draw each character
639
763
  let charCode = line[j].charCodeAt(0);
@@ -652,35 +776,4 @@ class FontImage
652
776
 
653
777
  context.restore();
654
778
  }
655
- }
656
-
657
- ///////////////////////////////////////////////////////////////////////////////
658
- // Display functions
659
-
660
- /** Returns true if fullscreen mode is active
661
- * @return {boolean}
662
- * @memberof Draw */
663
- function isFullscreen() { return !!document.fullscreenElement; }
664
-
665
- /** Toggle fullscreen mode
666
- * @memberof Draw */
667
- function toggleFullscreen()
668
- {
669
- const rootElement = mainCanvas.parentElement;
670
- if (isFullscreen())
671
- {
672
- if (document.exitFullscreen)
673
- document.exitFullscreen();
674
- }
675
- else if (rootElement.requestFullscreen)
676
- rootElement.requestFullscreen();
677
- }
678
-
679
- /** Set the cursor style
680
- * @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
681
- * @memberof Draw */
682
- function setCursor(cursorStyle = 'auto')
683
- {
684
- const rootElement = mainCanvas.parentElement;
685
- rootElement.style.cursor = cursorStyle;
686
779
  }
@@ -52,9 +52,11 @@ export
52
52
  cameraPos,
53
53
  cameraAngle,
54
54
  cameraScale,
55
+ canvasColorTiles,
55
56
  canvasMaxSize,
56
57
  canvasFixedSize,
57
58
  canvasPixelated,
59
+ overlayCanvasPixelated,
58
60
  tilesPixelated,
59
61
  fontDefault,
60
62
  showSplashScreen,
@@ -91,9 +93,11 @@ export
91
93
  setCameraPos,
92
94
  setCameraAngle,
93
95
  setCameraScale,
96
+ setCanvasColorTiles,
94
97
  setCanvasMaxSize,
95
98
  setCanvasFixedSize,
96
99
  setCanvasPixelated,
100
+ setOverlayCanvasPixelated,
97
101
  setTilesPixelated,
98
102
  setFontDefault,
99
103
  setShowSplashScreen,
@@ -169,6 +173,8 @@ export
169
173
  rgb,
170
174
  hsl,
171
175
  isColor,
176
+ isVector2,
177
+ isNumber,
172
178
 
173
179
  // Default Colors
174
180
  WHITE,
@@ -550,7 +550,7 @@ function touchGamepadRender()
550
550
  }
551
551
  else // draw cross shaped gamepad
552
552
  {
553
- for(let i=10; i--;)
553
+ for (let i=10; i--;)
554
554
  {
555
555
  const angle = i*PI/4;
556
556
  context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
@@ -594,4 +594,4 @@ function pointerLockExit() { document.exitPointerLock && document.exitPointerLoc
594
594
  /** Check if pointer is locked (true if locked)
595
595
  * @return {boolean}
596
596
  * @memberof Input */
597
- function pointerLockIsActive() { return document.pointerLockElement == mainCanvas }
597
+ function pointerLockIsActive() { return document.pointerLockElement == mainCanvas; }
@@ -142,7 +142,7 @@ class Medal
142
142
  context.save();
143
143
  context.beginPath();
144
144
  context.fillStyle = new Color(.9,.9,.9).toString();
145
- context.strokeStyle = new Color(0,0,0).toString();
145
+ context.strokeStyle = BLACK.toString();
146
146
  context.lineWidth = 3;
147
147
  context.rect(x, y, width, height);
148
148
  context.fill();
@@ -159,11 +159,11 @@ class Medal
159
159
  const descriptionSize = height*.3;
160
160
  const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
161
161
  const textWidth = width - medalDisplayIconSize - 3*gap.x;
162
- drawTextScreen(this.name, pos, nameSize, new Color(0,0,0), 0, undefined, 'left', undefined, textWidth);
162
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
163
163
 
164
164
  // draw the description
165
165
  pos.y = y + height - gap.y*2 - descriptionSize/2;
166
- drawTextScreen(this.description, pos, descriptionSize, new Color(0,0,0), 0, undefined, 'left', undefined, textWidth);
166
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
167
167
  context.restore();
168
168
  }
169
169
 
@@ -177,7 +177,7 @@ class Medal
177
177
  if (this.image)
178
178
  overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
179
179
  else
180
- drawTextScreen(this.icon, pos, size*.7, new Color(0,0,0));
180
+ drawTextScreen(this.icon, pos, size*.7, BLACK);
181
181
  }
182
182
 
183
183
  // Get local storage key used by the medal
@@ -208,7 +208,7 @@ class EngineObject
208
208
  if (o.mass) // push away if not fixed
209
209
  o.velocity = o.velocity.subtract(velocity);
210
210
 
211
- debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
211
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
212
212
  continue;
213
213
  }
214
214
 
@@ -270,7 +270,7 @@ class EngineObject
270
270
  else // bounce if other object is fixed
271
271
  this.velocity.x *= -restitution;
272
272
  }
273
- debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
273
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
274
274
  }
275
275
  }
276
276
  if (this.collideTiles)
@@ -315,7 +315,7 @@ class EngineObject
315
315
  this.pos.x = oldPos.x;
316
316
  this.velocity.x *= -this.restitution;
317
317
  }
318
- debugOverlay && debugPhysics && debugRect(this.pos, this.size, '#f00');
318
+ debugPhysics && debugRect(this.pos, this.size, '#f00');
319
319
  }
320
320
  }
321
321
  }
@@ -209,10 +209,17 @@ class ParticleEmitter extends EngineObject
209
209
  const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
210
210
  particle.velocity = vec2().setAngle(velocityAngle, speed);
211
211
  particle.angleVelocity = angleSpeed;
212
+ if (!this.localSpace)
213
+ {
214
+ // apply emitter velocity to particle
215
+ particle.velocity.x += this.velocity.x;
216
+ particle.velocity.y += this.velocity.y;
217
+ particle.angleVelocity += this.angleVelocity;
218
+ }
212
219
  particle.fadeRate = this.fadeRate;
213
220
  particle.damping = this.damping;
214
221
  particle.angleDamping = this.angleDamping;
215
- particle.restitution = this.restitution;
222
+ particle.restitution = this.restitution;
216
223
  particle.friction = this.friction;
217
224
  particle.gravityScale = this.gravityScale;
218
225
  particle.collideTiles = this.collideTiles;