littlejsengine 1.9.11 → 1.10.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 (76) hide show
  1. package/README.md +9 -13
  2. package/dist/littlejs.d.ts +129 -10
  3. package/dist/littlejs.esm.js +220 -75
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +188 -73
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +187 -72
  8. package/examples/box2d/game.js +4 -1
  9. package/examples/box2d/index.html +7 -6
  10. package/examples/breakout/game.js +1 -1
  11. package/examples/breakout/index.html +5 -4
  12. package/examples/breakoutTutorial/README.md +9 -7
  13. package/examples/breakoutTutorial/game.js +6 -6
  14. package/examples/breakoutTutorial/index.html +4 -3
  15. package/examples/electron/game.js +1 -1
  16. package/examples/electron/index.html +4 -3
  17. package/examples/empty/game.js +1 -1
  18. package/examples/empty/index.html +2 -1
  19. package/examples/htmlMenu/game.js +67 -0
  20. package/examples/htmlMenu/index.html +56 -0
  21. package/examples/htmlMenu/tiles.png +0 -0
  22. package/examples/index.html +33 -0
  23. package/examples/js13k/build/index.html +1 -0
  24. package/examples/js13k/build/index.js +1 -0
  25. package/examples/js13k/build/tiles.png +0 -0
  26. package/examples/js13k/game - Copy.zip +0 -0
  27. package/examples/js13k/game.js +1 -1
  28. package/examples/js13k/game.zip +0 -0
  29. package/examples/js13k/index.html +17 -14
  30. package/examples/module/game.js +1 -1
  31. package/examples/module/index.html +3 -2
  32. package/examples/particles/index.html +4 -3
  33. package/examples/platformer/game.js +7 -5
  34. package/examples/platformer/gameCharacter.js +12 -11
  35. package/examples/platformer/gameEffects.js +3 -4
  36. package/examples/platformer/gameLevel.js +18 -34
  37. package/examples/platformer/gameObjects.js +9 -15
  38. package/examples/platformer/index.html +10 -9
  39. package/examples/puzzle/game.js +1 -1
  40. package/examples/puzzle/index.html +4 -3
  41. package/examples/starter/build/index.html +2 -0
  42. package/examples/starter/build/index.js +1 -0
  43. package/examples/starter/build/tiles.png +0 -0
  44. package/examples/starter/game.js +4 -4
  45. package/examples/starter/game.zip +0 -0
  46. package/examples/starter/index.html +15 -14
  47. package/examples/stress/index.html +3 -2
  48. package/examples/typescript/build/build/littlejs.esm.js +4327 -0
  49. package/examples/typescript/build/dist/littlejs.esm.js +4425 -0
  50. package/examples/typescript/build/examples/typescript/build.js +24 -0
  51. package/examples/typescript/build/examples/typescript/game.js +100 -0
  52. package/examples/typescript/build/examples/typescript/test/build/littlejs.esm.js +3934 -0
  53. package/examples/typescript/build/examples/typescript/test/examples/typescript/build.js +86 -0
  54. package/examples/typescript/build/examples/typescript/test/examples/typescript/game.js +92 -0
  55. package/examples/typescript/game.js +1 -1
  56. package/examples/typescript/game.ts +1 -1
  57. package/examples/typescript/index.html +3 -2
  58. package/examples/uiSystem/game.js +134 -0
  59. package/examples/uiSystem/index.html +25 -0
  60. package/examples/uiSystem/tiles.png +0 -0
  61. package/jsconfig.json +11 -0
  62. package/package.json +1 -1
  63. package/plugins/box2d.js +2 -1
  64. package/plugins/postProcess.js +14 -8
  65. package/plugins/uiSystem.js +243 -0
  66. package/src/engine.js +42 -22
  67. package/src/engineAudio.js +10 -13
  68. package/src/engineDebug.js +1 -1
  69. package/src/engineDraw.js +102 -27
  70. package/src/engineExport.js +32 -2
  71. package/src/engineInput.js +1 -3
  72. package/src/engineObject.js +1 -0
  73. package/src/engineSettings.js +1 -1
  74. package/src/engineTileLayer.js +2 -1
  75. package/src/engineUtilities.js +2 -1
  76. package/src/engineWebGL.js +26 -4
@@ -0,0 +1,243 @@
1
+ /**
2
+ * LittleJS User Interface Plugin
3
+ * - Nested Menus
4
+ * - Text
5
+ * - Buttons
6
+ * - Checkboxes
7
+ * - Images
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ ///////////////////////////////////////////////////////////////////////////////
13
+
14
+ // ui defaults
15
+ let uiDefaultColor = WHITE;
16
+ let uiDefaultLineColor = BLACK;
17
+ let uiDefaultTextColor = BLACK;
18
+ let uiDefaultButtonColor = hsl(0,0,.5);
19
+ let uiDefaultHoverColor = hsl(0,0,.7);
20
+ let uiDefaultLineWidth = 4;
21
+ let uiDefaultFont = 'arial';
22
+
23
+ // ui system
24
+ let uiObjects = [];
25
+ let uiContext;
26
+
27
+ function initUISystem(context=overlayContext)
28
+ {
29
+ uiContext = context;
30
+ engineAddPlugin(uiUpdate, uiRender);
31
+
32
+ // setup recursive update and render
33
+ function uiUpdate()
34
+ {
35
+ function updateObject(o)
36
+ {
37
+ if (!o.visible)
38
+ return;
39
+ if (o.parent)
40
+ o.pos = o.localPos.add(o.parent.pos);
41
+ o.update();
42
+ for(const c of o.children)
43
+ updateObject(c)
44
+ }
45
+ uiObjects.forEach(o=> o.parent || updateObject(o));
46
+ }
47
+ function uiRender()
48
+ {
49
+ function renderObject(o)
50
+ {
51
+ if (!o.visible)
52
+ return;
53
+ o.render();
54
+ for(const c of o.children)
55
+ renderObject(c)
56
+ }
57
+ uiObjects.forEach(o=> o.parent || renderObject(o));
58
+ }
59
+ }
60
+
61
+ function drawUIRect(pos, size, color=uiDefaultColor, lineWidth=uiDefaultLineWidth, lineColor=uiDefaultLineColor)
62
+ {
63
+ uiContext.fillStyle = color.toString();
64
+ uiContext.beginPath();
65
+ uiContext.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
66
+ uiContext.fill();
67
+ if (lineWidth)
68
+ {
69
+ uiContext.strokeStyle = lineColor.toString();
70
+ uiContext.lineWidth = lineWidth;
71
+ uiContext.stroke();
72
+ }
73
+ }
74
+
75
+ function drawUILine(posA, posB, thickness=uiDefaultLineWidth, color=uiDefaultLineColor)
76
+ {
77
+ uiContext.strokeStyle = color.toString();
78
+ uiContext.lineWidth = thickness;
79
+ uiContext.beginPath();
80
+ uiContext.lineTo(posA.x, posA.y);
81
+ uiContext.lineTo(posB.x, posB.y);
82
+ uiContext.stroke();
83
+ }
84
+
85
+ function drawUITile(pos, size, tileInfo, color=uiDefaultColor, angle=0, mirror=false, additiveColor=BLACK)
86
+ {
87
+ drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, false, true, uiContext);
88
+ }
89
+
90
+ function drawUIText(text, pos, size, color=uiDefaultColor, lineWidth=uiDefaultLineWidth, lineColor=uiDefaultLineColor, align='center', font=uiDefaultFont)
91
+ {
92
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, uiContext, size.x);
93
+ }
94
+
95
+ ///////////////////////////////////////////////////////////////////////////////
96
+
97
+ class UIObject
98
+ {
99
+ constructor(localPos, size=vec2())
100
+ {
101
+ this.localPos = localPos.copy();
102
+ this.pos = localPos.copy();
103
+ this.size = size.copy();
104
+ this.color = uiDefaultColor;
105
+ this.lineColor = uiDefaultLineColor;
106
+ this.textColor = uiDefaultTextColor;
107
+ this.hoverColor = uiDefaultHoverColor;
108
+ this.lineWidth = uiDefaultLineWidth;
109
+ this.font = uiDefaultFont;
110
+ this.visible = true;
111
+ this.children = [];
112
+ this.parent = null;
113
+ uiObjects.push(this);
114
+ }
115
+
116
+ addChild(child)
117
+ {
118
+ ASSERT(!child.parent && !this.children.includes(child));
119
+ this.children.push(child);
120
+ child.parent = this;
121
+ }
122
+
123
+ removeChild(child)
124
+ {
125
+ ASSERT(child.parent == this && this.children.includes(child));
126
+ this.children.splice(this.children.indexOf(child), 1);
127
+ child.parent = 0;
128
+ }
129
+
130
+ update()
131
+ {
132
+ // track mouse input
133
+ const mouseWasOver = this.mouseIsOver;
134
+ this.mouseIsOver = isOverlapping(this.pos, this.size, mousePosScreen);
135
+ if (this.mouseIsOver && !mouseWasOver)
136
+ this.onEnter();
137
+ if (!this.mouseIsOver && mouseWasOver)
138
+ this.onLeave();
139
+ if (mouseWasPressed(0) && this.mouseIsOver)
140
+ {
141
+ this.mouseIsHeld = true;
142
+ this.onPress();
143
+ }
144
+ else if (this.mouseIsHeld && !mouseIsDown(0))
145
+ {
146
+ this.mouseIsHeld = false;
147
+ if (this.mouseIsOver)
148
+ this.onClick();
149
+ }
150
+ }
151
+ render()
152
+ {
153
+ if (this.size.x && this.size.y)
154
+ drawUIRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor);
155
+ }
156
+
157
+ // callback functions
158
+ onEnter() {}
159
+ onLeave() {}
160
+ onPress() {}
161
+ onClick() {}
162
+ }
163
+
164
+ ///////////////////////////////////////////////////////////////////////////////
165
+
166
+ class UIText extends UIObject
167
+ {
168
+ constructor(pos, size, text, align='center', font=fontDefault)
169
+ {
170
+ super(pos, size);
171
+
172
+ this.text = text;
173
+ this.align = align;
174
+ this.font = font;
175
+ this.lineWidth = 0;
176
+ }
177
+ render()
178
+ {
179
+ drawUIText(this.text, this.pos, this.size, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
180
+ }
181
+ }
182
+
183
+ ///////////////////////////////////////////////////////////////////////////////
184
+
185
+ class UITile extends UIObject
186
+ {
187
+ constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false, additiveColor=BLACK)
188
+ {
189
+ super(pos, size);
190
+
191
+ this.tileInfo = tileInfo;
192
+ this.color = color;
193
+ this.angle = angle;
194
+ this.mirror = mirror;
195
+ this.additiveColor = additiveColor;
196
+ }
197
+ render()
198
+ {
199
+ drawUITile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
200
+ }
201
+ }
202
+
203
+ ///////////////////////////////////////////////////////////////////////////////
204
+
205
+ class UIButton extends UIObject
206
+ {
207
+ constructor(pos, size, text)
208
+ {
209
+ super(pos, size);
210
+ this.text = text;
211
+ this.color = uiDefaultButtonColor;
212
+ }
213
+ render()
214
+ {
215
+ const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
216
+ const color = this.mouseIsOver? this.hoverColor : this.color;
217
+ drawUIRect(this.pos, this.size, color, this.lineWidth, lineColor);
218
+ drawTextScreen(this.text, this.pos, this.size.y*.8,
219
+ this.textColor, undefined, undefined, this.align, this.font, uiContext);
220
+ }
221
+ }
222
+
223
+ ///////////////////////////////////////////////////////////////////////////////
224
+
225
+ class UICheckbox extends UIObject
226
+ {
227
+ constructor(pos, size, checked=false)
228
+ {
229
+ super(pos, size);
230
+ this.checked = checked;
231
+ }
232
+ onClick() { this.checked = !this.checked; }
233
+ render()
234
+ {
235
+ drawUIRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor);
236
+ if (this.checked)
237
+ {
238
+ // draw an X if checked
239
+ drawUILine(this.pos.add(this.size.multiply(vec2(-.5,-.5))), this.pos.add(this.size.multiply(vec2(.5,.5))), this.lineWidth, this.lineColor);
240
+ drawUILine(this.pos.add(this.size.multiply(vec2(-.5,.5))), this.pos.add(this.size.multiply(vec2(.5,-.5))), this.lineWidth, this.lineColor);
241
+ }
242
+ }
243
+ }
package/src/engine.js CHANGED
@@ -30,7 +30,7 @@ const engineName = 'LittleJS';
30
30
  * @type {String}
31
31
  * @default
32
32
  * @memberof Engine */
33
- const engineVersion = '1.9.11';
33
+ const engineVersion = '1.10.4';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {Number}
@@ -74,7 +74,6 @@ let timeReal = 0;
74
74
  * @default false
75
75
  * @memberof Engine */
76
76
  let paused = false;
77
-
78
77
  /** Set if game is paused
79
78
  * @param {Boolean} isPaused
80
79
  * @memberof Engine */
@@ -107,9 +106,10 @@ function engineAddPlugin(updateFunction, renderFunction)
107
106
  * @param {Function} gameUpdatePost - Called after physics and objects are updated, setup camera and prepare for render
108
107
  * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
109
108
  * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
110
- * @param {Array} [imageSources=['tiles.png']] - Image to load
109
+ * @param {Array} [imageSources=[]] - Image to load
110
+ * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default
111
111
  * @memberof Engine */
112
- function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=['tiles.png'])
112
+ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
113
113
  {
114
114
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
115
115
 
@@ -151,6 +151,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
151
151
  for (const o of engineObjects)
152
152
  o.parent || o.updateTransforms();
153
153
  inputUpdate();
154
+ pluginUpdateList.forEach(f=>f());
154
155
  debugUpdate();
155
156
  gameUpdatePost();
156
157
  inputUpdatePost();
@@ -266,16 +267,21 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
266
267
  }
267
268
 
268
269
  // setup html
269
- const styleBody =
270
+ const styleRoot =
270
271
  'margin:0;overflow:hidden;' + // fill the window
272
+ 'width:100vw;height:100vh;' + // fill the window
273
+ 'display:flex;' + // use flexbox
274
+ 'align-items:center;' + // horizontal center
275
+ (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
276
+ 'justify-content:center;' + // vertical center
271
277
  'background:#000;' + // set background color
272
278
  'user-select:none;' + // prevent hold to select
273
279
  '-webkit-user-select:none;' + // compatibility for ios
274
280
  (!touchInputEnable ? '' : // no touch css setttings
275
281
  'touch-action:none;' + // prevent mobile pinch to resize
276
282
  '-webkit-touch-callout:none');// compatibility for ios
277
- document.body.style.cssText = styleBody;
278
- document.body.appendChild(mainCanvas = document.createElement('canvas'));
283
+ rootElement.style.cssText = styleRoot;
284
+ rootElement.appendChild(mainCanvas = document.createElement('canvas'));
279
285
  mainContext = mainCanvas.getContext('2d');
280
286
 
281
287
  // init stuff and start engine
@@ -285,13 +291,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
285
291
  glInit();
286
292
 
287
293
  // create overlay canvas for hud to appear above gl canvas
288
- document.body.appendChild(overlayCanvas = document.createElement('canvas'));
294
+ rootElement.appendChild(overlayCanvas = document.createElement('canvas'));
289
295
  overlayContext = overlayCanvas.getContext('2d');
290
296
 
291
297
  // set canvas style
292
- const styleCanvas = 'position:absolute;' + // position
293
- 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center
294
- (glCanvas||mainCanvas).style.cssText = mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
298
+ const styleCanvas = 'position:absolute'; // allow canvases to overlap
299
+ mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
300
+ if (glCanvas)
301
+ glCanvas.style.cssText = styleCanvas;
295
302
  updateCanvas();
296
303
 
297
304
  // create promises for loading images
@@ -308,19 +315,32 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
308
315
  })
309
316
  );
310
317
 
311
- // draw splash screen
312
- showSplashScreen && promises.push(new Promise(resolve =>
318
+ if (!imageSources.length)
313
319
  {
314
- let t = 0;
315
- console.log(`${engineName} Engine v${engineVersion}`);
316
- updateSplash();
317
- function updateSplash()
320
+ // no images to load
321
+ promises.push(new Promise(resolve =>
318
322
  {
319
- clearInput();
320
- drawEngineSplashScreen(t+=.01);
321
- t>1 ? resolve() : setTimeout(updateSplash, 16);
322
- }
323
- }));
323
+ textureInfos[0] = new TextureInfo(new Image);
324
+ resolve();
325
+ }));
326
+ }
327
+
328
+ if (showSplashScreen)
329
+ {
330
+ // draw splash screen
331
+ promises.push(new Promise(resolve =>
332
+ {
333
+ let t = 0;
334
+ console.log(`${engineName} Engine v${engineVersion}`);
335
+ updateSplash();
336
+ function updateSplash()
337
+ {
338
+ clearInput();
339
+ drawEngineSplashScreen(t+=.01);
340
+ t>1 ? resolve() : setTimeout(updateSplash, 16);
341
+ }
342
+ }));
343
+ }
324
344
 
325
345
  // load all of the images
326
346
  Promise.all(promises).then(startEngine);
@@ -320,17 +320,6 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
320
320
  {
321
321
  if (!soundEnable || headlessMode) return;
322
322
 
323
- // prevent sounds from building up if they can't be played
324
- if (audioContext.state != 'running')
325
- {
326
- // fix stalled audio
327
- audioContext.resume().then(()=>
328
- playSamples(sampleChannels, volume, rate, pan, loop, sampleRate, gainNode));
329
-
330
- // prevent suspended sounds from building up
331
- return;
332
- }
333
-
334
323
  // create buffer and source
335
324
  const channelCount = sampleChannels.length;
336
325
  const sampleLength = sampleChannels[0].length;
@@ -352,8 +341,16 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
352
341
  const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
353
342
  source.connect(pannerNode).connect(gainNode);
354
343
 
355
- // play and return sound
356
- source.start();
344
+ // play the sound
345
+ if (audioContext.state != 'running')
346
+ {
347
+ // fix stalled audio and play
348
+ audioContext.resume().then(()=>source.start());
349
+ }
350
+ else
351
+ source.start();
352
+
353
+ // return sound
357
354
  return source;
358
355
  }
359
356
 
@@ -124,7 +124,6 @@ function debugPoint(pos, color, time, angle)
124
124
  * @memberof Debug */
125
125
  function debugLine(posA, posB, color, thickness=.1, time)
126
126
  {
127
- ASSERT(typeof color == 'string', 'pass in css color strings');
128
127
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
129
128
  const size = vec2(thickness, halfDelta.length()*2);
130
129
  debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
@@ -200,6 +199,7 @@ function debugShowErrors()
200
199
 
201
200
  const showError = (message)=>
202
201
  {
202
+ // replace entire page with error message
203
203
  document.body.style.backgroundColor = '#111';
204
204
  document.body.innerHTML = `<pre style=color:#f00;font-size:50px>` + message;
205
205
  }
package/src/engineDraw.js CHANGED
@@ -58,12 +58,13 @@ let drawCount;
58
58
  ///////////////////////////////////////////////////////////////////////////////
59
59
 
60
60
  /**
61
- * Create a tile info object
61
+ * Create a tile info object using a grid based system
62
62
  * - This can take vecs or floats for easier use and conversion
63
63
  * - If an index is passed in, the tile size and index will determine the position
64
- * @param {(Number|Vector2)} [pos=(0,0)] - Top left corner of tile in pixels or index
64
+ * @param {(Number|Vector2)} [pos=0] - Index of tile in sheet
65
65
  * @param {(Number|Vector2)} [size=tileSizeDefault] - Size of tile in pixels
66
66
  * @param {Number} [textureIndex] - Texture index to use
67
+ * @param {Number} [padding] - How many pixels padding around tiles
67
68
  * @return {TileInfo}
68
69
  * @example
69
70
  * tile(2) // a tile at index 2 using the default tile size of 16
@@ -72,7 +73,7 @@ let drawCount;
72
73
  * tile(vec2(4,8), vec2(30,10)) // a tile at pixel location (4,8) with a size of (30,10)
73
74
  * @memberof Draw
74
75
  */
75
- function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
76
+ function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0)
76
77
  {
77
78
  if (headlessMode)
78
79
  return new TileInfo;
@@ -84,17 +85,17 @@ function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
84
85
  size = vec2(size);
85
86
  }
86
87
 
87
- // if pos is a number, use it as a tile index
88
+ // use pos as a tile index
89
+ const textureInfo = textureInfos[textureIndex];
90
+ ASSERT(textureInfo, 'Texture not loaded');
91
+ const sizePadded = size.add(vec2(padding*2));
92
+ const cols = textureInfo.size.x / sizePadded.x |0;
88
93
  if (typeof pos === 'number')
89
- {
90
- const textureInfo = textureInfos[textureIndex];
91
- ASSERT(textureInfo, 'Texture not loaded');
92
- const cols = textureInfo.size.x / size.x |0;
93
- pos = vec2((pos%cols)*size.x, (pos/cols|0)*size.y);
94
- }
94
+ pos = vec2(pos%cols, pos/cols|0);
95
+ pos = vec2(pos.x*sizePadded.x+padding, pos.y*sizePadded.y+padding);
95
96
 
96
97
  // return a tile info object
97
- return new TileInfo(pos, size, textureIndex);
98
+ return new TileInfo(pos, size, textureIndex, padding);
98
99
  }
99
100
 
100
101
  /**
@@ -106,8 +107,9 @@ class TileInfo
106
107
  * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
107
108
  * @param {Vector2} [size=tileSizeDefault] - Size of tile in pixels
108
109
  * @param {Number} [textureIndex] - Texture index to use
110
+ * @param {Number} [padding] - How many pixels padding around tiles
109
111
  */
110
- constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0)
112
+ constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0)
111
113
  {
112
114
  /** @property {Vector2} - Top left corner of tile in pixels */
113
115
  this.pos = pos.copy();
@@ -115,6 +117,8 @@ class TileInfo
115
117
  this.size = size.copy();
116
118
  /** @property {Number} - Texture index to use */
117
119
  this.textureIndex = textureIndex;
120
+ /** @property {Number} - How many pixels padding around tiles */
121
+ this.padding = padding;
118
122
  }
119
123
 
120
124
  /** Returns a copy of this tile offset by a vector
@@ -131,7 +135,7 @@ class TileInfo
131
135
  frame(frame)
132
136
  {
133
137
  ASSERT(typeof frame == 'number');
134
- return this.offset(vec2(frame*this.size.x, 0));
138
+ return this.offset(vec2(frame*(this.size.x+this.padding*2), 0));
135
139
  }
136
140
 
137
141
  /** Returns the texture info for this tile
@@ -156,8 +160,6 @@ class TextureInfo
156
160
  this.size = vec2(image.width, image.height);
157
161
  /** @property {WebGLTexture} - webgl texture */
158
162
  this.glTexture = glEnable && glCreateTexture(image);
159
- /** @property {Vector2} - size to adjust tile to fix bleeding */
160
- this.fixBleedSize = vec2(tileFixBleedScale).divide(this.size);
161
163
  }
162
164
  }
163
165
 
@@ -226,11 +228,12 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
226
228
  if (textureInfo)
227
229
  {
228
230
  // calculate uvs and render
229
- const x = tileInfo.pos.x / textureInfo.size.x;
230
- const y = tileInfo.pos.y / textureInfo.size.y;
231
- const w = tileInfo.size.x / textureInfo.size.x;
232
- const h = tileInfo.size.y / textureInfo.size.y;
233
- const tileImageFixBleed = textureInfo.fixBleedSize;
231
+ const sizeInverse = vec2(1).divide(textureInfo.size);
232
+ const x = tileInfo.pos.x * sizeInverse.x;
233
+ const y = tileInfo.pos.y * sizeInverse.y;
234
+ const w = tileInfo.size.x * sizeInverse.x;
235
+ const h = tileInfo.size.y * sizeInverse.y;
236
+ const tileImageFixBleed = sizeInverse.scale(tileFixBleedScale);
234
237
  glSetTexture(textureInfo.glTexture);
235
238
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
236
239
  x + tileImageFixBleed.x, y + tileImageFixBleed.y,
@@ -247,6 +250,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
247
250
  {
248
251
  // normal canvas 2D rendering method (slower)
249
252
  showWatermark && ++drawCount;
253
+ size = vec2(size.x, -size.y); // fix upside down sprites
250
254
  drawCanvas2D(pos, size, angle, mirror, (context)=>
251
255
  {
252
256
  if (textureInfo)
@@ -300,6 +304,74 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, contex
300
304
  drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace, context);
301
305
  }
302
306
 
307
+ /** Draw colored polygon using passed in points
308
+ * @param {Array} points - Array of Vector2 points
309
+ * @param {Color} [color=(1,1,1,1)]
310
+ * @param {Number} [lineWidth=0]
311
+ * @param {Color} [lineColor=(0,0,0,1)]
312
+ * @param {Boolean} [screenSpace=false]
313
+ * @param {CanvasRenderingContext2D} [context=mainContext]
314
+ * @memberof Draw */
315
+ function drawPoly(points, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
316
+ {
317
+ context.fillStyle = color.toString();
318
+ context.beginPath();
319
+ for (const point of screenSpace ? points : points.map(worldToScreen))
320
+ context.lineTo(point.x, point.y);
321
+ context.closePath();
322
+ context.fill();
323
+ if (lineWidth)
324
+ {
325
+ context.strokeStyle = lineColor.toString();
326
+ context.lineWidth = screenSpace ? lineWidth : lineWidth*cameraScale;
327
+ context.stroke();
328
+ }
329
+ }
330
+
331
+ /** Draw colored ellipse using passed in point
332
+ * @param {Vector2} pos
333
+ * @param {Number} [width=1]
334
+ * @param {Number} [height=1]
335
+ * @param {Number} [angle=0]
336
+ * @param {Color} [color=(1,1,1,1)]
337
+ * @param {Number} [lineWidth=0]
338
+ * @param {Color} [lineColor=(0,0,0,1)]
339
+ * @param {Boolean} [screenSpace=false]
340
+ * @param {CanvasRenderingContext2D} [context=mainContext]
341
+ * @memberof Draw */
342
+ function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
343
+ {
344
+ if (!screenSpace)
345
+ {
346
+ pos = worldToScreen(pos);
347
+ width *= cameraScale;
348
+ height *= cameraScale;
349
+ lineWidth *= cameraScale;
350
+ }
351
+ context.fillStyle = color.toString();
352
+ context.beginPath();
353
+ context.ellipse(pos.x, pos.y, width, height, angle, 0, 9);
354
+ context.fill();
355
+ if (lineWidth)
356
+ {
357
+ context.strokeStyle = lineColor.toString();
358
+ context.lineWidth = lineWidth;
359
+ context.stroke();
360
+ }
361
+ }
362
+
363
+ /** Draw colored circle using passed in point
364
+ * @param {Vector2} pos
365
+ * @param {Number} [radius=1]
366
+ * @param {Color} [color=(1,1,1,1)]
367
+ * @param {Number} [lineWidth=0]
368
+ * @param {Color} [lineColor=(0,0,0,1)]
369
+ * @param {Boolean} [screenSpace=false]
370
+ * @param {CanvasRenderingContext2D} [context=mainContext]
371
+ * @memberof Draw */
372
+ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
373
+ { drawEllipse(pos, radius, radius, 0, color, lineWidth, lineColor, screenSpace, context); }
374
+
303
375
  /** Draw directly to a 2d canvas context in world space
304
376
  * @param {Vector2} pos
305
377
  * @param {Vector2} size
@@ -354,10 +426,11 @@ function setBlendMode(additive, useWebGL=glEnable, context)
354
426
  * @param {CanvasTextAlign} [textAlign='center']
355
427
  * @param {String} [font=fontDefault]
356
428
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
429
+ * @param {Number} [maxWidth]
357
430
  * @memberof Draw */
358
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context)
431
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context, maxWidth)
359
432
  {
360
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, context);
433
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, context, maxWidth);
361
434
  }
362
435
 
363
436
  /** Draw text on overlay canvas in screen space
@@ -371,8 +444,9 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
371
444
  * @param {CanvasTextAlign} [textAlign]
372
445
  * @param {String} [font=fontDefault]
373
446
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
447
+ * @param {Number} [maxWidth]
374
448
  * @memberof Draw */
375
- function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
449
+ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext, maxWidth=undefined)
376
450
  {
377
451
  context.fillStyle = color.toString();
378
452
  context.lineWidth = lineWidth;
@@ -385,8 +459,8 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
385
459
  pos = pos.copy();
386
460
  (text+'').split('\n').forEach(line=>
387
461
  {
388
- lineWidth && context.strokeText(line, pos.x, pos.y);
389
- context.fillText(line, pos.x, pos.y);
462
+ lineWidth && context.strokeText(line, pos.x, pos.y, maxWidth);
463
+ context.fillText(line, pos.x, pos.y, maxWidth);
390
464
  pos.y += size;
391
465
  });
392
466
  }
@@ -489,11 +563,12 @@ function isFullscreen() { return !!document.fullscreenElement; }
489
563
  * @memberof Draw */
490
564
  function toggleFullscreen()
491
565
  {
566
+ const rootElement = mainCanvas.parentElement;
492
567
  if (isFullscreen())
493
568
  {
494
569
  if (document.exitFullscreen)
495
570
  document.exitFullscreen();
496
571
  }
497
- else if (document.body.requestFullscreen)
498
- document.body.requestFullscreen();
572
+ else if (rootElement.requestFullscreen)
573
+ rootElement.requestFullscreen();
499
574
  }