littlejsengine 1.13.1 → 1.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +15 -11
  2. package/copyToGithub.bat +28 -0
  3. package/dist/littlejs.d.ts +430 -304
  4. package/dist/littlejs.esm.js +1745 -982
  5. package/dist/littlejs.esm.min.js +1 -1
  6. package/dist/littlejs.js +1716 -965
  7. package/dist/littlejs.min.js +1 -1
  8. package/dist/littlejs.release.js +1673 -922
  9. package/examples/box2d/game.js +2 -2
  10. package/examples/box2d/gameObjects.js +2 -2
  11. package/examples/box2d/scenes.js +1 -1
  12. package/examples/breakout/game.js +30 -25
  13. package/examples/breakoutTutorial/README.md +44 -41
  14. package/examples/breakoutTutorial/game.js +2 -2
  15. package/examples/electron/build.bat +7 -0
  16. package/examples/electron/build.js +124 -0
  17. package/examples/electron/electron.js +35 -0
  18. package/examples/electron/game.js +140 -0
  19. package/examples/electron/index.html +13 -0
  20. package/examples/electron/package.json +12 -0
  21. package/examples/electron/tiles.png +0 -0
  22. package/examples/empty/game.js +1 -0
  23. package/examples/htmlMenu/game.js +1 -1
  24. package/examples/index.html +332 -160
  25. package/examples/module/game.js +5 -2
  26. package/examples/particles/index.html +12 -3
  27. package/examples/platformer/gameEffects.js +20 -19
  28. package/examples/platformer/gameLevel.js +6 -1
  29. package/examples/shorts/base.html +1 -1
  30. package/examples/shorts/blending.js +9 -5
  31. package/examples/shorts/box2d.js +1 -1
  32. package/examples/shorts/box2dCar.js +1 -1
  33. package/examples/shorts/clock.js +1 -1
  34. package/examples/shorts/colors.js +2 -2
  35. package/examples/shorts/flappyGame.js +2 -1
  36. package/examples/shorts/helloWorld.js +1 -1
  37. package/examples/shorts/maze.js +1 -1
  38. package/examples/shorts/music.js +106 -0
  39. package/examples/shorts/nineSlice.js +9 -9
  40. package/examples/shorts/parallax.js +71 -0
  41. package/examples/shorts/piano.js +9 -10
  42. package/examples/shorts/postProcess.js +25 -8
  43. package/examples/shorts/raycasting.js +2 -2
  44. package/examples/shorts/shapes.js +7 -15
  45. package/examples/shorts/slidingPuzzle.js +2 -2
  46. package/examples/shorts/song.mp3 +0 -0
  47. package/examples/shorts/starfield.js +5 -7
  48. package/examples/shorts/systemFont.js +1 -1
  49. package/examples/shorts/uiSystem.js +16 -8
  50. package/examples/starter/build.js +9 -8
  51. package/examples/starter/game.js +5 -2
  52. package/examples/starter/index.html +2 -2
  53. package/examples/stress/index.html +13 -8
  54. package/examples/style.css +19 -6
  55. package/examples/typescript/build.js +1 -1
  56. package/examples/typescript/game.js +2 -2
  57. package/examples/typescript/game.ts +5 -2
  58. package/examples/uiSystem/game.js +13 -12
  59. package/package.json +2 -2
  60. package/plugins/box2d.js +24 -97
  61. package/plugins/drawUtilities.js +29 -23
  62. package/plugins/newgrounds.js +6 -6
  63. package/plugins/pluginExport.js +1 -1
  64. package/plugins/postProcess.js +7 -0
  65. package/plugins/uiSystem.js +106 -82
  66. package/plugins/zzfxm.js +10 -10
  67. package/reference.md +90 -54
  68. package/src/engine.js +28 -23
  69. package/src/engineAudio.js +285 -110
  70. package/src/engineBuild.js +9 -9
  71. package/src/engineDebug.js +28 -28
  72. package/src/engineDraw.js +346 -202
  73. package/src/engineExport.js +28 -16
  74. package/src/engineInput.js +58 -68
  75. package/src/engineMedals.js +29 -29
  76. package/src/engineObject.js +28 -25
  77. package/src/engineParticles.js +67 -60
  78. package/src/engineRelease.js +1 -1
  79. package/src/engineSettings.js +70 -16
  80. package/src/engineTileLayer.js +34 -34
  81. package/src/engineUtilities.js +69 -62
  82. package/src/engineWebGL.js +467 -65
  83. /package/examples/shorts/{playSound.js → sound.js} +0 -0
package/src/engineDraw.js CHANGED
@@ -1,21 +1,21 @@
1
- /**
1
+ /**
2
2
  * LittleJS Drawing System
3
3
  * - Hybrid system with both Canvas2D and WebGL available
4
4
  * - Super fast tile sheet rendering with WebGL
5
5
  * - Can apply rotation, mirror, color and additive color
6
6
  * - Font rendering system with built in engine font
7
7
  * - Many useful utility functions
8
- *
8
+ *
9
9
  * LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
10
10
  * There are 3 canvas/contexts available to draw to...
11
11
  * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
12
12
  * glCanvas - Used by the accelerated WebGL batch rendering system.
13
13
  * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
14
- *
14
+ *
15
15
  * The WebGL rendering system is very fast with some caveats...
16
16
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
17
17
  * - Group additive rendering together using renderOrder to mitigate this issue
18
- *
18
+ *
19
19
  * The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
20
20
  * @namespace Draw
21
21
  */
@@ -52,7 +52,17 @@ let drawCanvas;
52
52
  * @memberof Draw */
53
53
  let drawContext;
54
54
 
55
- /** The size of the main canvas (and other secondary canvases)
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
+
65
+ /** The size of the main canvas (and other secondary canvases)
56
66
  * @type {Vector2}
57
67
  * @memberof Draw */
58
68
  let mainCanvasSize = vec2();
@@ -67,29 +77,28 @@ let drawCount;
67
77
 
68
78
  ///////////////////////////////////////////////////////////////////////////////
69
79
 
70
- /**
80
+ /**
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
81
91
  * tile(5, 8) // a tile at index 5 using a tile size of 8
82
92
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
83
93
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
84
- * @memberof Draw
85
- */
94
+ * @memberof Draw */
86
95
  function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
87
96
  {
88
97
  if (headlessMode)
89
98
  return new TileInfo;
90
99
 
91
100
  // if size is a number, make it a vector
92
- if (typeof size == 'number')
101
+ if (typeof size === 'number')
93
102
  {
94
103
  ASSERT(size > 0);
95
104
  size = new Vector2(size, size);
@@ -98,24 +107,24 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
98
107
  // create tile info object
99
108
  const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
100
109
 
101
- // use get the pos of the tile
110
+ // get the position of the tile
102
111
  const textureInfo = textureInfos[textureIndex];
103
112
  ASSERT(!!textureInfo, 'Texture not loaded');
104
113
  const sizePaddedX = size.x + padding*2;
105
114
  const sizePaddedY = size.y + padding*2;
106
- if (typeof pos == 'number')
115
+ if (typeof pos === 'number')
107
116
  {
108
117
  const cols = textureInfo.size.x / sizePaddedX |0;
109
- ASSERT(cols>0, 'Tile size is too big for texture');
118
+ ASSERT(cols > 0, 'Tile size is too big for texture');
110
119
  const posX = pos % cols, posY = (pos / cols) |0;
111
120
  tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
112
121
  }
113
122
  else
114
123
  tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
115
- return tileInfo;
124
+ return tileInfo;
116
125
  }
117
126
 
118
- /**
127
+ /**
119
128
  * Tile Info - Stores info about how to draw a tile
120
129
  */
121
130
  class TileInfo
@@ -153,14 +162,14 @@ class TileInfo
153
162
  */
154
163
  frame(frame)
155
164
  {
156
- ASSERT(typeof frame == 'number');
165
+ ASSERT(typeof frame === 'number');
157
166
  return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
158
167
  }
159
168
 
160
169
  /**
161
170
  * Set this tile to use a full image
162
171
  * @param {HTMLImageElement|OffscreenCanvas} image
163
- * @param {WebGLTexture} [glTexture] - webgl texture
172
+ * @param {WebGLTexture} [glTexture] - WebGL texture
164
173
  * @return {TileInfo}
165
174
  */
166
175
  setFullImage(image, glTexture)
@@ -178,7 +187,7 @@ class TextureInfo
178
187
  /**
179
188
  * Create a TextureInfo, called automatically by the engine
180
189
  * @param {HTMLImageElement|OffscreenCanvas} image
181
- * @param {WebGLTexture} [glTexture] - webgl texture
190
+ * @param {WebGLTexture} [glTexture] - WebGL texture
182
191
  */
183
192
  constructor(image, glTexture)
184
193
  {
@@ -188,7 +197,7 @@ class TextureInfo
188
197
  this.size = vec2(image.width, image.height);
189
198
  /** @property {Vector2} - inverse of the size, cached for rendering */
190
199
  this.sizeInverse = vec2(1/image.width, 1/image.height);
191
- /** @property {WebGLTexture} - webgl texture */
200
+ /** @property {WebGLTexture} - WebGL texture */
192
201
  this.glTexture = glTexture;
193
202
  }
194
203
 
@@ -201,76 +210,28 @@ class TextureInfo
201
210
  }
202
211
 
203
212
  ///////////////////////////////////////////////////////////////////////////////
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); }
213
+ // Drawing functions
253
214
 
254
215
  /** Draw textured tile centered in world space, with color applied if using WebGL
255
- * @param {Vector2} pos - Center of the tile in world space
256
- * @param {Vector2} [size=(1,1)] - Size of the tile in world space
257
- * @param {TileInfo}[tileInfo] - Tile info to use, untextured if undefined
258
- * @param {Color} [color=(1,1,1,1)] - Color to modulate with
259
- * @param {number} [angle] - Angle to rotate by
260
- * @param {boolean} [mirror] - If true image is flipped along the Y axis
261
- * @param {Color} [additiveColor] - Additive color to be applied if any
262
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
263
- * @param {boolean} [screenSpace=false] - If true the pos and size are in screen space
216
+ * @param {Vector2} pos - Center of the tile in world space
217
+ * @param {Vector2} [size=(1,1)] - Size of the tile in world space
218
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
219
+ * @param {Color} [color=(1,1,1,1)] - Color to modulate with
220
+ * @param {number} [angle] - Angle to rotate by
221
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
222
+ * @param {Color} [additiveColor] - Additive color to be applied if any
223
+ * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
224
+ * @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
264
225
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
265
226
  * @memberof Draw */
266
- function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
227
+ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
267
228
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
268
229
  {
269
- ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
270
230
  ASSERT(isVector2(pos) && pos.isValid(), 'drawTile pos should be a vec2');
271
231
  ASSERT(isVector2(size) && size.isValid(), 'drawTile size should be a vec2');
272
232
  ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
273
233
  ASSERT(isNumber(angle), 'drawTile angle should be a number');
234
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
274
235
 
275
236
  const textureInfo = tileInfo && tileInfo.textureInfo;
276
237
  if (useWebGL)
@@ -294,22 +255,22 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
294
255
  {
295
256
  const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
296
257
  const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
297
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
298
- x + tileImageFixBleedX, y + tileImageFixBleedY,
299
- x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
300
- color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
258
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
259
+ x + tileImageFixBleedX, y + tileImageFixBleedY,
260
+ x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
261
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
301
262
  }
302
263
  else
303
264
  {
304
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
305
- x, y, x + w, y + h,
306
- color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
265
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
266
+ x, y, x + w, y + h,
267
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
307
268
  }
308
269
  }
309
270
  else
310
271
  {
311
272
  // if no tile info, force untextured
312
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
273
+ glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
313
274
  }
314
275
  }
315
276
  else
@@ -322,18 +283,15 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
322
283
  if (textureInfo)
323
284
  {
324
285
  // 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
286
+ const x = tileInfo.pos.x, y = tileInfo.pos.y;
287
+ const w = tileInfo.size.x, h = tileInfo.size.y;
288
+ drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor);
332
289
  }
333
290
  else
334
291
  {
335
- // if no tile info, force untextured
336
- context.fillStyle = color.toString();
292
+ // if no tile info, use untextured rect
293
+ const c = additiveColor ? color.add(additiveColor) : color;
294
+ context.fillStyle = c.toString();
337
295
  context.fillRect(-.5, -.5, 1, 1);
338
296
  }
339
297
  }, screenSpace, context);
@@ -346,12 +304,12 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
346
304
  * @param {Color} [color=(1,1,1,1)]
347
305
  * @param {number} [angle]
348
306
  * @param {boolean} [useWebGL=glEnable]
349
- * @param {boolean} [screenSpace=false]
307
+ * @param {boolean} [screenSpace]
350
308
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
351
309
  * @memberof Draw */
352
310
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
353
- {
354
- drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
311
+ {
312
+ drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
355
313
  }
356
314
 
357
315
  /** Draw colored line between two points
@@ -359,72 +317,138 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
359
317
  * @param {Vector2} posB
360
318
  * @param {number} [thickness]
361
319
  * @param {Color} [color=(1,1,1,1)]
320
+ * @param {Vector2} [pos=(0,0)] - Offset to apply
321
+ * @param {number} [angle] - Angle to rotate by
362
322
  * @param {boolean} [useWebGL=glEnable]
363
- * @param {boolean} [screenSpace=false]
323
+ * @param {boolean} [screenSpace]
364
324
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
365
325
  * @memberof Draw */
366
- function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
326
+ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
367
327
  {
368
328
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
369
329
  const size = vec2(thickness, halfDelta.length()*2);
370
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace, context);
330
+ pos = pos.add(posA.add(halfDelta));
331
+ angle += halfDelta.angle();
332
+ drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
333
+ }
334
+
335
+ /** Draw colored regular polygon using passed in number of sides
336
+ * @param {Vector2} pos
337
+ * @param {Vector2} [size=(1,1)]
338
+ * @param {number} [sides]
339
+ * @param {Color} [color=(1,1,1,1)]
340
+ * @param {number} [angle]
341
+ * @param {number} [lineWidth]
342
+ * @param {Color} [lineColor=(0,0,0,1)]
343
+ * @param {boolean} [useWebGL=glEnable]
344
+ * @param {boolean} [screenSpace]
345
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
346
+ * @memberof Draw */
347
+ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
348
+ {
349
+ // build regular polygon points
350
+ const points = [];
351
+ for (let i=sides; i--;)
352
+ {
353
+ const a = (i/sides)*PI*2;
354
+ points.push(vec2(Math.sin(a)*size.x, Math.cos(a)*size.y));
355
+ }
356
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
371
357
  }
372
358
 
373
359
  /** Draw colored polygon using passed in points
374
- * @param {Array<Vector2>} points - Array of Vector2 points
360
+ * @param {Array<Vector2>} points - Array of Vector2 points
375
361
  * @param {Color} [color=(1,1,1,1)]
376
- * @param {number} [lineWidth=0]
362
+ * @param {number} [lineWidth]
377
363
  * @param {Color} [lineColor=(0,0,0,1)]
378
- * @param {boolean} [screenSpace=false]
379
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
364
+ * @param {Vector2} [pos=(0,0)] - Offset to apply
365
+ * @param {number} [angle] - Angle to rotate by
366
+ * @param {boolean} [useWebGL=glEnable]
367
+ * @param {boolean} [screenSpace]
368
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
380
369
  * @memberof Draw */
381
- function drawPoly(points, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=drawContext)
370
+ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
382
371
  {
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)
372
+ ASSERT(isVector2(pos) && pos.isValid(), 'drawPoly pos should be a vec2');
373
+ ASSERT(Array.isArray(points), 'drawPoly points should be an array');
374
+ ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
375
+ ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
376
+ ASSERT(isNumber(angle), 'drawPoly angle should be a number');
377
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
378
+ if (useWebGL)
391
379
  {
392
- context.strokeStyle = lineColor.toString();
393
- context.lineWidth = screenSpace ? lineWidth : lineWidth*cameraScale;
394
- context.stroke();
380
+ let scale = 1;
381
+ if (screenSpace)
382
+ {
383
+ // convert to world space
384
+ pos = screenToWorld(pos);
385
+ scale = 1/cameraScale;
386
+ }
387
+ glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
388
+ if (lineWidth > 0)
389
+ glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, scale, scale, angle);
390
+ }
391
+ else
392
+ {
393
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
394
+ {
395
+ context.fillStyle = color.toString();
396
+ context.beginPath();
397
+ for (const point of points)
398
+ context.lineTo(point.x, point.y);
399
+ context.closePath();
400
+ context.fill();
401
+ if (lineWidth)
402
+ {
403
+ context.strokeStyle = lineColor.toString();
404
+ context.lineWidth = lineWidth;
405
+ context.stroke();
406
+ }
407
+ }, screenSpace, context);
395
408
  }
396
409
  }
397
410
 
398
411
  /** Draw colored ellipse using passed in point
399
412
  * @param {Vector2} pos
400
- * @param {number} [width=1]
401
- * @param {number} [height=1]
402
- * @param {number} [angle=0]
413
+ * @param {Vector2} [size=(1,1)]
403
414
  * @param {Color} [color=(1,1,1,1)]
404
- * @param {number} [lineWidth=0]
415
+ * @param {number} [angle]
416
+ * @param {number} [lineWidth]
405
417
  * @param {Color} [lineColor=(0,0,0,1)]
406
- * @param {boolean} [screenSpace=false]
407
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
418
+ * @param {boolean} [useWebGL=glEnable]
419
+ * @param {boolean} [screenSpace]
420
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
408
421
  * @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)
422
+ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
410
423
  {
411
- ASSERT(isColor(color) && isColor(lineColor));
412
- if (!screenSpace)
424
+ ASSERT(isVector2(pos) && pos.isValid(), 'drawEllipse pos should be a vec2');
425
+ ASSERT(isVector2(size) && size.isValid(), 'drawEllipse size should be a vec2');
426
+ ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
427
+ ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
428
+ ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
429
+ ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
430
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
431
+ if (useWebGL)
413
432
  {
414
- pos = worldToScreen(pos);
415
- width *= cameraScale;
416
- height *= cameraScale;
417
- lineWidth *= cameraScale;
433
+ // draw as a regular polygon
434
+ const sides = glCircleSides;
435
+ drawRegularPoly(pos, size, sides, color, lineWidth, lineColor, angle, useWebGL, screenSpace, context);
418
436
  }
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)
437
+ else
424
438
  {
425
- context.strokeStyle = lineColor.toString();
426
- context.lineWidth = lineWidth;
427
- context.stroke();
439
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
440
+ {
441
+ context.fillStyle = color.toString();
442
+ context.beginPath();
443
+ context.ellipse(0, 0, size.x, size.y, 0, 0, 9);
444
+ context.fill();
445
+ if (lineWidth)
446
+ {
447
+ context.strokeStyle = lineColor.toString();
448
+ context.lineWidth = lineWidth;
449
+ context.stroke();
450
+ }
451
+ }, screenSpace, context);
428
452
  }
429
453
  }
430
454
 
@@ -434,11 +458,12 @@ function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth
434
458
  * @param {Color} [color=(1,1,1,1)]
435
459
  * @param {number} [lineWidth=0]
436
460
  * @param {Color} [lineColor=(0,0,0,1)]
437
- * @param {boolean} [screenSpace=false]
438
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
461
+ * @param {boolean} [useWebGL=glEnable]
462
+ * @param {boolean} [screenSpace]
463
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
439
464
  * @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); }
465
+ function drawCircle(pos, radius=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
466
+ { drawEllipse(pos, vec2(radius), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context); }
442
467
 
443
468
  /** Draw directly to a 2d canvas context in world space
444
469
  * @param {Vector2} pos
@@ -446,7 +471,7 @@ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new C
446
471
  * @param {number} angle
447
472
  * @param {boolean} [mirror]
448
473
  * @param {Function} [drawFunction]
449
- * @param {boolean} [screenSpace=false]
474
+ * @param {boolean} [screenSpace=false]
450
475
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
451
476
  * @memberof Draw */
452
477
  function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
@@ -465,6 +490,9 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
465
490
  context.restore();
466
491
  }
467
492
 
493
+ ///////////////////////////////////////////////////////////////////////////////
494
+ // Text Drawing Functions
495
+
468
496
  /** Draw text on main canvas in world space
469
497
  * Automatically splits new lines into rows
470
498
  * @param {string} text
@@ -513,7 +541,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
513
541
  * @param {number} [maxWidth]
514
542
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
515
543
  * @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)
544
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
517
545
  {
518
546
  context.fillStyle = color.toString();
519
547
  context.strokeStyle = lineColor.toString();
@@ -534,22 +562,67 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
534
562
  });
535
563
  }
536
564
 
565
+ ///////////////////////////////////////////////////////////////////////////////
566
+ // Drawing utilities
567
+
568
+ /** Convert from screen to world space coordinates
569
+ * @param {Vector2} screenPos
570
+ * @return {Vector2}
571
+ * @memberof Draw */
572
+ function screenToWorld(screenPos)
573
+ {
574
+ let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
575
+ let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
576
+ if (cameraAngle)
577
+ {
578
+ // apply camera rotation
579
+ const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
580
+ const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
581
+ const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
582
+ cameraPosRelativeX = rotatedX;
583
+ cameraPosRelativeY = rotatedY;
584
+ }
585
+ return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
586
+ }
587
+
588
+ /** Convert from world to screen space coordinates
589
+ * @param {Vector2} worldPos
590
+ * @return {Vector2}
591
+ * @memberof Draw */
592
+ function worldToScreen(worldPos)
593
+ {
594
+ let cameraPosRelativeX = worldPos.x - cameraPos.x;
595
+ let cameraPosRelativeY = worldPos.y - cameraPos.y;
596
+ if (cameraAngle)
597
+ {
598
+ // apply inverse camera rotation
599
+ const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
600
+ const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
601
+ const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
602
+ cameraPosRelativeX = rotatedX;
603
+ cameraPosRelativeY = rotatedY;
604
+ }
605
+ return new Vector2
606
+ (
607
+ cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
608
+ cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
609
+ );
610
+ }
611
+
612
+ /** Get the camera's visible area in world space
613
+ * @return {Vector2}
614
+ * @memberof Draw */
615
+ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
616
+
537
617
  /** Enable normal or additive blend mode
538
618
  * @param {boolean} [additive]
539
- * @param {boolean} [useWebGL=glEnable]
540
619
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
541
620
  * @memberof Draw */
542
- function setBlendMode(additive=false, useWebGL=glEnable, context)
621
+ function setBlendMode(additive=false, context)
543
622
  {
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
- }
623
+ glAdditive = additive;
624
+ context ||= drawContext;
625
+ context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
553
626
  }
554
627
 
555
628
  /** Combines all LittleJS canvases onto the main canvas and clears them
@@ -566,11 +639,107 @@ function combineCanvases()
566
639
  overlayCanvas.width |= 0;
567
640
  }
568
641
 
642
+ /** Helper function to draw an image with color and additive color applied
643
+ * This is slower then normal drawImage when color is applied
644
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
645
+ * @param {HTMLImageElement|OffscreenCanvas} image
646
+ * @param {number} sx
647
+ * @param {number} sy
648
+ * @param {number} sWidth
649
+ * @param {number} sHeight
650
+ * @param {number} dx
651
+ * @param {number} dy
652
+ * @param {number} dWidth
653
+ * @param {number} dHeight
654
+ * @param {Color} color
655
+ * @param {Color} [additiveColor]
656
+ * @memberof Draw */
657
+ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor)
658
+ {
659
+ function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
660
+ function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
661
+ const sx2 = tileFixBleedScale;
662
+ const sy2 = tileFixBleedScale;
663
+ const sWidth2 = sWidth - 2*tileFixBleedScale;
664
+ const sHeight2 = sHeight - 2*tileFixBleedScale;
665
+ if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
666
+ {
667
+ // white texture with no additive alpha, no need to tint
668
+ context.globalAlpha = color.a;
669
+ context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
670
+ context.globalAlpha = 1;
671
+ }
672
+ else
673
+ {
674
+ // copy to offscreen canvas
675
+ workCanvas.width = sWidth;
676
+ workCanvas.height = sHeight;
677
+ workContext.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
678
+
679
+ // tint image using offscreen work context
680
+ const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
681
+ const data = imageData.data;
682
+ if (additiveColor && !isBlack(additiveColor))
683
+ {
684
+ // slower path with additive color
685
+ const colorMultiply = [color.r, color.g, color.b, color.a];
686
+ const colorAdd = [additiveColor.r * 255, additiveColor.g * 255, additiveColor.b * 255, additiveColor.a * 255];
687
+ for (let i = 0; i < data.length; ++i)
688
+ data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
689
+ workContext.putImageData(imageData, 0, 0);
690
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
691
+ }
692
+ else
693
+ {
694
+ // faster path with no additive color
695
+ for (let i = 0; i < data.length; i+=4)
696
+ {
697
+ data[i ] *= color.r;
698
+ data[i+1] *= color.g;
699
+ data[i+2] *= color.b;
700
+ }
701
+ workContext.putImageData(imageData, 0, 0);
702
+ context.globalAlpha = color.a;
703
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
704
+ context.globalAlpha = 1;
705
+ }
706
+ }
707
+ }
708
+
709
+
710
+ /** Returns true if fullscreen mode is active
711
+ * @return {boolean}
712
+ * @memberof Draw */
713
+ function isFullscreen() { return !!document.fullscreenElement; }
714
+
715
+ /** Toggle fullscreen mode
716
+ * @memberof Draw */
717
+ function toggleFullscreen()
718
+ {
719
+ const rootElement = mainCanvas.parentElement;
720
+ if (isFullscreen())
721
+ {
722
+ if (document.exitFullscreen)
723
+ document.exitFullscreen();
724
+ }
725
+ else if (rootElement.requestFullscreen)
726
+ rootElement.requestFullscreen();
727
+ }
728
+
729
+ /** Set the cursor style
730
+ * @param {string} [cursorStyle] - CSS cursor style (auto, none, crosshair, etc)
731
+ * @memberof Draw */
732
+ function setCursor(cursorStyle = 'auto')
733
+ {
734
+ const rootElement = mainCanvas.parentElement;
735
+ rootElement.style.cursor = cursorStyle;
736
+ }
737
+
569
738
  ///////////////////////////////////////////////////////////////////////////////
570
739
 
571
740
  let engineFontImage;
572
741
 
573
- /**
742
+ /**
574
743
  * Font Image Object - Draw text on a 2D canvas by using characters in an image
575
744
  * - 96 characters (from space to tilde) are stored in an image
576
745
  * - Uses a default 8x8 font if none is supplied
@@ -578,7 +747,7 @@ let engineFontImage;
578
747
  * @example
579
748
  * // use built in font
580
749
  * const font = new FontImage;
581
- *
750
+ *
582
751
  * // draw text
583
752
  * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
584
753
  */
@@ -588,7 +757,6 @@ class FontImage
588
757
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
589
758
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
590
759
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
591
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
592
760
  */
593
761
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
594
762
  {
@@ -602,7 +770,6 @@ class FontImage
602
770
  this.image = image || engineFontImage;
603
771
  this.tileSize = tileSize;
604
772
  this.paddingSize = paddingSize;
605
- this.context = context;
606
773
  }
607
774
 
608
775
  /** Draw text in world space using the image font
@@ -610,30 +777,39 @@ class FontImage
610
777
  * @param {Vector2} pos
611
778
  * @param {number} [scale=.25]
612
779
  * @param {boolean} [center]
780
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}[context=drawContext]
613
781
  */
614
- drawText(text, pos, scale=1, center)
782
+ drawText(text, pos, scale=1, center, context=drawContext)
615
783
  {
616
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
784
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
617
785
  }
618
786
 
619
- /** Draw text in screen space using the image font
787
+ /** Draw text on overlay canvas in world space using the image font
788
+ * @param {string} text
789
+ * @param {Vector2} pos
790
+ * @param {number} [scale]
791
+ * @param {boolean} [center]
792
+ */
793
+ drawTextOverlay(text, pos, scale=4, center)
794
+ { this.drawText(text, pos, scale, center, overlayContext); }
795
+
796
+ /** Draw text on overlay canvas in screen space using the image font
620
797
  * @param {string} text
621
798
  * @param {Vector2} pos
622
799
  * @param {number} [scale]
623
800
  * @param {boolean} [center]
801
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
624
802
  */
625
- drawTextScreen(text, pos, scale=4, center)
803
+ drawTextScreen(text, pos, scale=4, center, context=overlayContext)
626
804
  {
627
- const context = this.context;
628
805
  context.save();
629
-
630
806
  const size = this.tileSize;
631
807
  const drawSize = size.add(this.paddingSize).scale(scale);
632
808
  const cols = this.image.width / this.tileSize.x |0;
633
809
  (text+'').split('\n').forEach((line, i)=>
634
810
  {
635
811
  const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
636
- for(let j=line.length; j--;)
812
+ for (let j=line.length; j--;)
637
813
  {
638
814
  // draw each character
639
815
  let charCode = line[j].charCodeAt(0);
@@ -645,42 +821,10 @@ class FontImage
645
821
  const x = tile % cols;
646
822
  const y = tile / cols |0;
647
823
  const drawPos = pos.add(vec2(j,i).multiply(drawSize));
648
- context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
824
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
649
825
  drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
650
826
  }
651
827
  });
652
-
653
828
  context.restore();
654
829
  }
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
830
  }