littlejsengine 1.12.6 → 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 (102) hide show
  1. package/README.md +15 -12
  2. package/dist/littlejs.d.ts +466 -236
  3. package/dist/littlejs.esm.js +6126 -5370
  4. package/dist/littlejs.esm.min.js +4 -1
  5. package/dist/littlejs.js +6093 -5359
  6. package/dist/littlejs.min.js +4 -1
  7. package/dist/littlejs.release.js +5614 -4907
  8. package/examples/box2d/game.js +2 -2
  9. package/examples/box2d/gameObjects.js +8 -7
  10. package/examples/box2d/index.html +1 -1
  11. package/examples/box2d/scenes.js +8 -8
  12. package/examples/breakout/game.js +1 -1
  13. package/examples/breakout/gameObjects.js +2 -2
  14. package/examples/breakout/index.html +1 -1
  15. package/examples/breakoutTutorial/README.md +46 -43
  16. package/examples/breakoutTutorial/game.js +3 -3
  17. package/examples/breakoutTutorial/index.html +1 -1
  18. package/examples/electron/build.bat +7 -0
  19. package/examples/electron/build.js +124 -0
  20. package/examples/electron/electron.js +35 -0
  21. package/examples/electron/game.js +140 -0
  22. package/examples/electron/index.html +13 -0
  23. package/examples/electron/package.json +12 -0
  24. package/examples/electron/tiles.png +0 -0
  25. package/examples/empty/game.js +1 -0
  26. package/examples/empty/index.html +1 -1
  27. package/examples/htmlMenu/game.js +1 -1
  28. package/examples/htmlMenu/index.html +1 -1
  29. package/examples/index.html +486 -182
  30. package/examples/module/game.js +6 -3
  31. package/examples/module/index.html +1 -1
  32. package/examples/particles/index.html +12 -3
  33. package/examples/platformer/game.js +4 -4
  34. package/examples/platformer/gameCharacter.js +6 -6
  35. package/examples/platformer/gameEffects.js +74 -57
  36. package/examples/platformer/gameLevel.js +61 -70
  37. package/examples/platformer/gameObjects.js +4 -4
  38. package/examples/platformer/index.html +1 -1
  39. package/examples/puzzle/index.html +1 -1
  40. package/examples/shorts/base.html +23 -3
  41. package/examples/shorts/blending.js +9 -5
  42. package/examples/shorts/box2d.js +1 -1
  43. package/examples/shorts/box2dCar.js +9 -13
  44. package/examples/shorts/clock.js +1 -1
  45. package/examples/shorts/colors.js +2 -2
  46. package/examples/shorts/flappyGame.js +52 -0
  47. package/examples/shorts/helloWorld.js +6 -3
  48. package/examples/shorts/hillGlideGame.js +56 -0
  49. package/examples/shorts/landerGame.js +32 -0
  50. package/examples/shorts/maze.js +47 -0
  51. package/examples/shorts/medals.js +42 -0
  52. package/examples/shorts/nineSlice.js +21 -0
  53. package/examples/shorts/piano.js +52 -0
  54. package/examples/shorts/platformer.js +24 -20
  55. package/examples/shorts/{pong.js → pongGame.js} +1 -1
  56. package/examples/shorts/raycasting.js +71 -0
  57. package/examples/shorts/shapes.js +9 -9
  58. package/examples/shorts/slidingPuzzle.js +42 -0
  59. package/examples/shorts/spaceGame.js +56 -0
  60. package/examples/shorts/starfield.js +15 -0
  61. package/examples/shorts/systemFont.js +1 -1
  62. package/examples/shorts/tileLayer.js +3 -5
  63. package/examples/shorts/tiles.png +0 -0
  64. package/examples/shorts/tiltedView.js +8 -7
  65. package/examples/shorts/topDown.js +18 -26
  66. package/examples/shorts/uiSystem.js +5 -7
  67. package/examples/starter/build.bat +6 -1
  68. package/examples/starter/build.js +9 -8
  69. package/examples/starter/game.js +7 -4
  70. package/examples/starter/index.html +23 -13
  71. package/examples/stress/index.html +7 -8
  72. package/examples/style.css +139 -0
  73. package/examples/typescript/build.js +2 -3
  74. package/examples/typescript/game.js +4 -4
  75. package/examples/typescript/game.ts +6 -3
  76. package/examples/typescript/index.html +1 -1
  77. package/examples/uiSystem/game.js +10 -25
  78. package/examples/uiSystem/index.html +1 -1
  79. package/package.json +2 -2
  80. package/plugins/box2d.js +22 -97
  81. package/plugins/drawUtilities.js +132 -0
  82. package/plugins/newgrounds.js +1 -1
  83. package/plugins/pluginExport.js +7 -1
  84. package/plugins/postProcess.js +9 -2
  85. package/plugins/uiSystem.js +155 -67
  86. package/plugins/zzfxm.js +1 -1
  87. package/reference.md +10 -10
  88. package/src/engine.js +56 -30
  89. package/src/engineAudio.js +15 -6
  90. package/src/engineBuild.bat +6 -1
  91. package/src/engineBuild.js +20 -5
  92. package/src/engineDebug.js +55 -28
  93. package/src/engineDraw.js +321 -177
  94. package/src/engineExport.js +27 -9
  95. package/src/engineInput.js +110 -38
  96. package/src/engineMedals.js +4 -4
  97. package/src/engineObject.js +71 -70
  98. package/src/engineParticles.js +33 -9
  99. package/src/engineSettings.js +69 -23
  100. package/src/engineTileLayer.js +312 -153
  101. package/src/engineUtilities.js +187 -135
  102. package/src/engineWebGL.js +70 -39
package/src/engineDraw.js CHANGED
@@ -42,6 +42,26 @@ let overlayCanvas;
42
42
  * @memberof Draw */
43
43
  let overlayContext;
44
44
 
45
+ /** The default canvas to use for drawing, usually mainCanvas
46
+ * @type {HTMLCanvasElement|OffscreenCanvas}
47
+ * @memberof Draw */
48
+ let drawCanvas;
49
+
50
+ /** The default 2d context to use for drawing, usually mainContext
51
+ * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
52
+ * @memberof Draw */
53
+ let drawContext;
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
+
45
65
  /** The size of the main canvas (and other secondary canvases)
46
66
  * @type {Vector2}
47
67
  * @memberof Draw */
@@ -61,10 +81,10 @@ let drawCount;
61
81
  * Create a tile info object using a grid based system
62
82
  * - This can take vecs or floats for easier use and conversion
63
83
  * - If an index is passed in, the tile size and index will determine the position
64
- * @param {Vector2|number} [pos=0] - Index of tile in sheet
84
+ * @param {Vector2|number} [pos=0] - Index of tile in sheet
65
85
  * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
66
- * @param {number} [textureIndex] - Texture index to use
67
- * @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
68
88
  * @return {TileInfo}
69
89
  * @example
70
90
  * tile(2) // a tile at index 2 using the default tile size of 16
@@ -73,31 +93,36 @@ let drawCount;
73
93
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
74
94
  * @memberof Draw
75
95
  */
76
- function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0)
96
+ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
77
97
  {
78
98
  if (headlessMode)
79
99
  return new TileInfo;
80
100
 
81
101
  // if size is a number, make it a vector
82
- if (typeof size === 'number')
102
+ if (typeof size == 'number')
83
103
  {
84
104
  ASSERT(size > 0);
85
- size = vec2(size);
105
+ size = new Vector2(size, size);
86
106
  }
87
107
 
88
- // use pos as a tile index
108
+ // create tile info object
109
+ const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
110
+
111
+ // use get the pos of the tile
89
112
  const textureInfo = textureInfos[textureIndex];
90
113
  ASSERT(!!textureInfo, 'Texture not loaded');
91
- const sizePadded = size.add(vec2(padding*2));
92
- if (typeof pos === 'number')
114
+ const sizePaddedX = size.x + padding*2;
115
+ const sizePaddedY = size.y + padding*2;
116
+ if (typeof pos == 'number')
93
117
  {
94
- const cols = textureInfo.size.x / sizePadded.x |0;
95
- pos = cols>0 ? vec2(pos%cols, pos/cols|0) : vec2();
118
+ const cols = textureInfo.size.x / sizePaddedX |0;
119
+ ASSERT(cols>0, 'Tile size is too big for texture');
120
+ const posX = pos % cols, posY = (pos / cols) |0;
121
+ tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
96
122
  }
97
- pos = vec2(pos.x*sizePadded.x+padding, pos.y*sizePadded.y+padding);
98
-
99
- // return a tile info object
100
- return new TileInfo(pos, size, textureIndex, padding);
123
+ else
124
+ tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
125
+ return tileInfo;
101
126
  }
102
127
 
103
128
  /**
@@ -121,6 +146,8 @@ class TileInfo
121
146
  this.textureIndex = textureIndex;
122
147
  /** @property {number} - How many pixels padding around tiles */
123
148
  this.padding = padding;
149
+ /** @property {TextureInfo} - The texture info for this tile */
150
+ this.textureInfo = textureInfos[this.textureIndex];
124
151
  }
125
152
 
126
153
  /** Returns a copy of this tile offset by a vector
@@ -137,14 +164,22 @@ class TileInfo
137
164
  frame(frame)
138
165
  {
139
166
  ASSERT(typeof frame == 'number');
140
- return this.offset(vec2(frame*(this.size.x+this.padding*2), 0));
167
+ return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
141
168
  }
142
169
 
143
- /** Returns the texture info for this tile
144
- * @return {TextureInfo}
145
- */
146
- getTextureInfo()
147
- { return textureInfos[this.textureIndex]; }
170
+ /**
171
+ * Set this tile to use a full image
172
+ * @param {HTMLImageElement|OffscreenCanvas} image
173
+ * @param {WebGLTexture} [glTexture] - webgl texture
174
+ * @return {TileInfo}
175
+ */
176
+ setFullImage(image, glTexture)
177
+ {
178
+ this.pos = new Vector2;
179
+ this.size = new Vector2(image.width, image.height);
180
+ this.textureInfo = new TextureInfo(image, glTexture);
181
+ return this;
182
+ }
148
183
  }
149
184
 
150
185
  /** Texture Info - Stores info about each texture */
@@ -152,9 +187,10 @@ class TextureInfo
152
187
  {
153
188
  /**
154
189
  * Create a TextureInfo, called automatically by the engine
155
- * @param {HTMLImageElement} image
190
+ * @param {HTMLImageElement|OffscreenCanvas} image
191
+ * @param {WebGLTexture} [glTexture] - webgl texture
156
192
  */
157
- constructor(image)
193
+ constructor(image, glTexture)
158
194
  {
159
195
  /** @property {HTMLImageElement} - image source */
160
196
  this.image = image;
@@ -163,42 +199,19 @@ class TextureInfo
163
199
  /** @property {Vector2} - inverse of the size, cached for rendering */
164
200
  this.sizeInverse = vec2(1/image.width, 1/image.height);
165
201
  /** @property {WebGLTexture} - webgl texture */
166
- this.glTexture = glEnable && glCreateTexture(image);
202
+ this.glTexture = glTexture;
167
203
  }
168
- }
169
-
170
- ///////////////////////////////////////////////////////////////////////////////
171
-
172
- /** Convert from screen to world space coordinates
173
- * @param {Vector2} screenPos
174
- * @return {Vector2}
175
- * @memberof Draw */
176
- function screenToWorld(screenPos)
177
- {
178
- return new Vector2
179
- (
180
- (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale + cameraPos.x,
181
- (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale + cameraPos.y
182
- );
183
- }
184
204
 
185
- /** Convert from world to screen space coordinates
186
- * @param {Vector2} worldPos
187
- * @return {Vector2}
188
- * @memberof Draw */
189
- function worldToScreen(worldPos)
190
- {
191
- return new Vector2
192
- (
193
- (worldPos.x - cameraPos.x) * cameraScale + mainCanvasSize.x/2 - .5,
194
- (worldPos.y - cameraPos.y) * -cameraScale + mainCanvasSize.y/2 - .5
195
- );
205
+ createWebGLTexture()
206
+ {
207
+ ASSERT(!this.glTexture);
208
+ if (glEnable)
209
+ this.glTexture = glCreateTexture(this.image);
210
+ }
196
211
  }
197
212
 
198
- /** Get the camera's visible area in world space
199
- * @return {Vector2}
200
- * @memberof Draw */
201
- function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
213
+ ///////////////////////////////////////////////////////////////////////////////
214
+ // Drawing functions
202
215
 
203
216
  /** Draw textured tile centered in world space, with color applied if using WebGL
204
217
  * @param {Vector2} pos - Center of the tile in world space
@@ -212,16 +225,19 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
212
225
  * @param {boolean} [screenSpace=false] - If true the pos and size are in screen space
213
226
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
214
227
  * @memberof Draw */
215
- function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
228
+ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
216
229
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
217
230
  {
218
231
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
219
- ASSERT(typeof tileInfo !== 'number' || !tileInfo,
220
- 'this is an old style calls, to fix replace it with tile(tileIndex, tileSize)');
221
- ASSERT(isVector2(pos) && isVector2(size));
222
- ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)));
232
+ ASSERT(isVector2(pos) && pos.isValid(), 'drawTile pos should be a vec2');
233
+ ASSERT(isVector2(size) && size.isValid(), 'drawTile size should be a vec2');
234
+ ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
235
+ ASSERT(isNumber(angle), 'drawTile angle should be a number');
223
236
 
224
- const textureInfo = tileInfo && tileInfo.getTextureInfo();
237
+ if (color.a <= 0 && (!additiveColor || additiveColor.a <= 0) || !size.x || !size.y)
238
+ return; // completely invisible, skip render
239
+
240
+ const textureInfo = tileInfo && tileInfo.textureInfo;
225
241
  if (useWebGL)
226
242
  {
227
243
  if (screenSpace)
@@ -265,24 +281,21 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
265
281
  {
266
282
  // normal canvas 2D rendering method (slower)
267
283
  showWatermark && ++drawCount;
268
- size = vec2(size.x, -size.y); // fix upside down sprites
284
+ size = new Vector2(size.x, -size.y); // fix upside down sprites
269
285
  drawCanvas2D(pos, size, angle, mirror, (context)=>
270
286
  {
271
287
  if (textureInfo)
272
288
  {
273
289
  // calculate uvs and render
274
- const x = tileInfo.pos.x + tileFixBleedScale;
275
- const y = tileInfo.pos.y + tileFixBleedScale;
276
- const w = tileInfo.size.x - 2*tileFixBleedScale;
277
- const h = tileInfo.size.y - 2*tileFixBleedScale;
278
- context.globalAlpha = color.a; // only alpha is supported
279
- context.drawImage(textureInfo.image, x, y, w, h, -.5, -.5, 1, 1);
280
- 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);
281
293
  }
282
294
  else
283
295
  {
284
- // if no tile info, force untextured
285
- 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();
286
299
  context.fillRect(-.5, -.5, 1, 1);
287
300
  }
288
301
  }, screenSpace, context);
@@ -295,7 +308,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
295
308
  * @param {Color} [color=(1,1,1,1)]
296
309
  * @param {number} [angle]
297
310
  * @param {boolean} [useWebGL=glEnable]
298
- * @param {boolean} [screenSpace=false]
311
+ * @param {boolean} [screenSpace]
299
312
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
300
313
  * @memberof Draw */
301
314
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
@@ -308,73 +321,90 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
308
321
  * @param {Vector2} posB
309
322
  * @param {number} [thickness]
310
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
311
326
  * @param {boolean} [useWebGL=glEnable]
312
- * @param {boolean} [screenSpace=false]
327
+ * @param {boolean} [screenSpace]
313
328
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
314
329
  * @memberof Draw */
315
- 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)
316
331
  {
317
332
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
318
333
  const size = vec2(thickness, halfDelta.length()*2);
319
- 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);
320
337
  }
321
338
 
322
339
  /** Draw colored polygon using passed in points
323
- * @param {Array<Vector2>} points - Array of Vector2 points
340
+ * @param {Array<Vector2>} points - Array of Vector2 points
324
341
  * @param {Color} [color=(1,1,1,1)]
325
- * @param {number} [lineWidth=0]
342
+ * @param {number} [lineWidth]
326
343
  * @param {Color} [lineColor=(0,0,0,1)]
327
- * @param {boolean} [screenSpace=false]
328
- * @param {CanvasRenderingContext2D} [context=mainContext]
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]
348
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
329
349
  * @memberof Draw */
330
- function drawPoly(points, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
350
+ function drawPoly(points, color=new Color, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=false, screenSpace=false, context=drawContext)
331
351
  {
332
- ASSERT(isColor(color) && isColor(lineColor));
333
- context.fillStyle = color.toString();
334
- context.beginPath();
335
- for (const point of screenSpace ? points : points.map(worldToScreen))
336
- context.lineTo(point.x, point.y);
337
- context.closePath();
338
- context.fill();
339
- 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=>
340
359
  {
341
- context.strokeStyle = lineColor.toString();
342
- context.lineWidth = screenSpace ? lineWidth : lineWidth*cameraScale;
343
- context.stroke();
344
- }
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);
345
373
  }
346
374
 
347
375
  /** Draw colored ellipse using passed in point
348
376
  * @param {Vector2} pos
349
- * @param {number} [width=1]
350
- * @param {number} [height=1]
351
- * @param {number} [angle=0]
377
+ * @param {Vector2} [size=(1,1)]
352
378
  * @param {Color} [color=(1,1,1,1)]
353
- * @param {number} [lineWidth=0]
379
+ * @param {number} [angle]
380
+ * @param {number} [lineWidth]
354
381
  * @param {Color} [lineColor=(0,0,0,1)]
355
- * @param {boolean} [screenSpace=false]
356
- * @param {CanvasRenderingContext2D} [context=mainContext]
382
+ * @param {boolean} [useWebGL] - Webgl not supported
383
+ * @param {boolean} [screenSpace]
384
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
357
385
  * @memberof Draw */
358
- function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
386
+ function drawEllipse(pos, size=vec2(1), color=new Color, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace=false, context=drawContext)
359
387
  {
360
- ASSERT(isColor(color) && isColor(lineColor));
361
- if (!screenSpace)
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=>
362
396
  {
363
- pos = worldToScreen(pos);
364
- width *= cameraScale;
365
- height *= cameraScale;
366
- lineWidth *= cameraScale;
367
- }
368
- context.fillStyle = color.toString();
369
- context.beginPath();
370
- context.ellipse(pos.x, pos.y, width, height, angle, 0, 9);
371
- context.fill();
372
- if (lineWidth)
373
- {
374
- context.strokeStyle = lineColor.toString();
375
- context.lineWidth = lineWidth;
376
- context.stroke();
377
- }
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);
378
408
  }
379
409
 
380
410
  /** Draw colored circle using passed in point
@@ -383,22 +413,23 @@ function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth
383
413
  * @param {Color} [color=(1,1,1,1)]
384
414
  * @param {number} [lineWidth=0]
385
415
  * @param {Color} [lineColor=(0,0,0,1)]
416
+ * @param {boolean} [useWebGL] - Webgl not supported
386
417
  * @param {boolean} [screenSpace=false]
387
- * @param {CanvasRenderingContext2D} [context=mainContext]
418
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
388
419
  * @memberof Draw */
389
- function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
390
- { 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); }
391
422
 
392
423
  /** Draw directly to a 2d canvas context in world space
393
424
  * @param {Vector2} pos
394
425
  * @param {Vector2} size
395
426
  * @param {number} angle
396
- * @param {boolean} mirror
397
- * @param {Function} drawFunction
427
+ * @param {boolean} [mirror]
428
+ * @param {Function} [drawFunction]
398
429
  * @param {boolean} [screenSpace=false]
399
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
430
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
400
431
  * @memberof Draw */
401
- function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, context=mainContext)
432
+ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
402
433
  {
403
434
  if (!screenSpace)
404
435
  {
@@ -408,12 +439,15 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
408
439
  }
409
440
  context.save();
410
441
  context.translate(pos.x+.5, pos.y+.5);
411
- context.rotate(angle);
442
+ context.rotate(angle-cameraAngle);
412
443
  context.scale(mirror ? -size.x : size.x, -size.y);
413
444
  drawFunction(context);
414
445
  context.restore();
415
446
  }
416
447
 
448
+ ///////////////////////////////////////////////////////////////////////////////
449
+ // Text Drawing Functions
450
+
417
451
  /** Draw text on main canvas in world space
418
452
  * Automatically splits new lines into rows
419
453
  * @param {string} text
@@ -425,9 +459,9 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
425
459
  * @param {CanvasTextAlign} [textAlign='center']
426
460
  * @param {string} [font=fontDefault]
427
461
  * @param {number} [maxWidth]
428
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
462
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
429
463
  * @memberof Draw */
430
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth, context=mainContext)
464
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth, context=drawContext)
431
465
  {
432
466
  drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, maxWidth, context);
433
467
  }
@@ -462,7 +496,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
462
496
  * @param {number} [maxWidth]
463
497
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
464
498
  * @memberof Draw */
465
- 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)
466
500
  {
467
501
  context.fillStyle = color.toString();
468
502
  context.strokeStyle = lineColor.toString();
@@ -473,32 +507,77 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
473
507
  context.lineJoin = 'round';
474
508
 
475
509
  const lines = (text+'').split('\n');
476
- pos = pos.copy();
477
- pos.y -= (lines.length-1) * size/2; // center text vertically
510
+ let posY = pos.y;
511
+ posY -= (lines.length-1) * size/2; // center text vertically
478
512
  lines.forEach(line=>
479
513
  {
480
- lineWidth && context.strokeText(line, pos.x, pos.y, maxWidth);
481
- context.fillText(line, pos.x, pos.y, maxWidth);
482
- pos.y += size;
514
+ lineWidth && context.strokeText(line, pos.x, posY, maxWidth);
515
+ context.fillText(line, pos.x, posY, maxWidth);
516
+ posY += size;
483
517
  });
484
518
  }
485
519
 
486
- /** Enable normal or additive blend mode
487
- * @param {boolean} [additive]
488
- * @param {boolean} [useWebGL=glEnable]
489
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
520
+ ///////////////////////////////////////////////////////////////////////////////
521
+ // Drawing utilities
522
+
523
+ /** Convert from screen to world space coordinates
524
+ * @param {Vector2} screenPos
525
+ * @return {Vector2}
490
526
  * @memberof Draw */
491
- function setBlendMode(additive, useWebGL=glEnable, context)
527
+ function screenToWorld(screenPos)
492
528
  {
493
- ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
494
- if (useWebGL)
495
- glAdditive = additive;
496
- else
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)
497
552
  {
498
- if (!context)
499
- context = mainContext;
500
- context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
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;
501
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
+
572
+ /** Enable normal or additive blend mode
573
+ * @param {boolean} [additive]
574
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
575
+ * @memberof Draw */
576
+ function setBlendMode(additive=false, context)
577
+ {
578
+ glAdditive = additive;
579
+ context ||= drawContext;
580
+ context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
502
581
  }
503
582
 
504
583
  /** Combines all LittleJS canvases onto the main canvas and clears them
@@ -507,7 +586,7 @@ function setBlendMode(additive, useWebGL=glEnable, context)
507
586
  function combineCanvases()
508
587
  {
509
588
  // combine canvases
510
- glCopyToContext(mainContext, true);
589
+ glCopyToContext(mainContext);
511
590
  mainContext.drawImage(overlayCanvas, 0, 0);
512
591
 
513
592
  // clear canvases
@@ -515,6 +594,102 @@ function combineCanvases()
515
594
  overlayCanvas.width |= 0;
516
595
  }
517
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
+
518
693
  ///////////////////////////////////////////////////////////////////////////////
519
694
 
520
695
  let engineFontImage;
@@ -582,7 +757,7 @@ class FontImage
582
757
  (text+'').split('\n').forEach((line, i)=>
583
758
  {
584
759
  const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
585
- for(let j=line.length; j--;)
760
+ for (let j=line.length; j--;)
586
761
  {
587
762
  // draw each character
588
763
  let charCode = line[j].charCodeAt(0);
@@ -601,35 +776,4 @@ class FontImage
601
776
 
602
777
  context.restore();
603
778
  }
604
- }
605
-
606
- ///////////////////////////////////////////////////////////////////////////////
607
- // Display functions
608
-
609
- /** Returns true if fullscreen mode is active
610
- * @return {boolean}
611
- * @memberof Draw */
612
- function isFullscreen() { return !!document.fullscreenElement; }
613
-
614
- /** Toggle fullscreen mode
615
- * @memberof Draw */
616
- function toggleFullscreen()
617
- {
618
- const rootElement = mainCanvas.parentElement;
619
- if (isFullscreen())
620
- {
621
- if (document.exitFullscreen)
622
- document.exitFullscreen();
623
- }
624
- else if (rootElement.requestFullscreen)
625
- rootElement.requestFullscreen();
626
- }
627
-
628
- /** Set the cursor style
629
- * @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
630
- * @memberof Draw */
631
- function setCursor(cursorStyle = 'auto')
632
- {
633
- const rootElement = mainCanvas.parentElement;
634
- rootElement.style.cursor = cursorStyle;
635
779
  }