littlejsengine 1.9.11 → 1.10.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 (53) hide show
  1. package/README.md +4 -0
  2. package/dist/littlejs.d.ts +80 -9
  3. package/dist/littlejs.esm.js +193 -71
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +175 -69
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +174 -68
  8. package/examples/box2d/game.js +1 -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/index.html +4 -3
  13. package/examples/electron/game.js +1 -1
  14. package/examples/electron/index.html +4 -3
  15. package/examples/empty/game.js +1 -1
  16. package/examples/empty/index.html +2 -1
  17. package/examples/htmlMenu/game.js +67 -0
  18. package/examples/htmlMenu/index.html +56 -0
  19. package/examples/htmlMenu/tiles.png +0 -0
  20. package/examples/index.html +33 -0
  21. package/examples/js13k/game.js +1 -1
  22. package/examples/js13k/index.html +17 -14
  23. package/examples/module/game.js +1 -1
  24. package/examples/module/index.html +3 -2
  25. package/examples/particles/index.html +4 -3
  26. package/examples/platformer/game.js +1 -1
  27. package/examples/platformer/gameLevel.js +2 -2
  28. package/examples/platformer/gameObjects.js +0 -1
  29. package/examples/platformer/index.html +10 -9
  30. package/examples/puzzle/game.js +1 -1
  31. package/examples/puzzle/index.html +4 -3
  32. package/examples/starter/game.js +4 -4
  33. package/examples/starter/index.html +15 -14
  34. package/examples/stress/index.html +3 -2
  35. package/examples/typescript/game.js +1 -1
  36. package/examples/typescript/index.html +3 -2
  37. package/examples/uiSystem/game.js +134 -0
  38. package/examples/uiSystem/index.html +25 -0
  39. package/examples/uiSystem/tiles.png +0 -0
  40. package/jsconfig.json +11 -0
  41. package/package.json +1 -1
  42. package/plugins/box2d.js +2 -1
  43. package/plugins/postProcess.js +6 -7
  44. package/plugins/uiSystem.js +243 -0
  45. package/src/engine.js +48 -20
  46. package/src/engineAudio.js +10 -13
  47. package/src/engineDebug.js +1 -1
  48. package/src/engineDraw.js +101 -27
  49. package/src/engineExport.js +18 -2
  50. package/src/engineInput.js +2 -2
  51. package/src/engineSettings.js +1 -1
  52. package/src/engineUtilities.js +1 -1
  53. package/src/engineWebGL.js +11 -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.2';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {Number}
@@ -75,6 +75,12 @@ let timeReal = 0;
75
75
  * @memberof Engine */
76
76
  let paused = false;
77
77
 
78
+ /** The root element that engine is attached to
79
+ * @type {HTMLElement}
80
+ * @default document.body
81
+ * @memberof Engine */
82
+ let engineRoot;
83
+
78
84
  /** Set if game is paused
79
85
  * @param {Boolean} isPaused
80
86
  * @memberof Engine */
@@ -108,8 +114,9 @@ function engineAddPlugin(updateFunction, renderFunction)
108
114
  * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
109
115
  * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
110
116
  * @param {Array} [imageSources=['tiles.png']] - Image to load
117
+ * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default
111
118
  * @memberof Engine */
112
- function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=['tiles.png'])
119
+ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
113
120
  {
114
121
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
115
122
 
@@ -151,6 +158,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
151
158
  for (const o of engineObjects)
152
159
  o.parent || o.updateTransforms();
153
160
  inputUpdate();
161
+ pluginUpdateList.forEach(f=>f());
154
162
  debugUpdate();
155
163
  gameUpdatePost();
156
164
  inputUpdatePost();
@@ -266,16 +274,22 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
266
274
  }
267
275
 
268
276
  // setup html
269
- const styleBody =
277
+ const styleRoot =
270
278
  'margin:0;overflow:hidden;' + // fill the window
279
+ 'width:100vw;height:100vh;' + // fill the window
280
+ 'display:flex;' + // use flexbox
281
+ 'align-items:center;' + // horizontal center
282
+ (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
283
+ 'justify-content:center;' + // vertical center
271
284
  'background:#000;' + // set background color
272
285
  'user-select:none;' + // prevent hold to select
273
286
  '-webkit-user-select:none;' + // compatibility for ios
274
287
  (!touchInputEnable ? '' : // no touch css setttings
275
288
  'touch-action:none;' + // prevent mobile pinch to resize
276
289
  '-webkit-touch-callout:none');// compatibility for ios
277
- document.body.style.cssText = styleBody;
278
- document.body.appendChild(mainCanvas = document.createElement('canvas'));
290
+ engineRoot = rootElement;
291
+ engineRoot.style.cssText = styleRoot;
292
+ engineRoot.appendChild(mainCanvas = document.createElement('canvas'));
279
293
  mainContext = mainCanvas.getContext('2d');
280
294
 
281
295
  // init stuff and start engine
@@ -285,13 +299,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
285
299
  glInit();
286
300
 
287
301
  // create overlay canvas for hud to appear above gl canvas
288
- document.body.appendChild(overlayCanvas = document.createElement('canvas'));
302
+ engineRoot.appendChild(overlayCanvas = document.createElement('canvas'));
289
303
  overlayContext = overlayCanvas.getContext('2d');
290
304
 
291
305
  // 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;
306
+ const styleCanvas = 'position:absolute'; // allow canvases to overlap
307
+ mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
308
+ if (glCanvas)
309
+ glCanvas.style.cssText = styleCanvas;
295
310
  updateCanvas();
296
311
 
297
312
  // create promises for loading images
@@ -308,19 +323,32 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
308
323
  })
309
324
  );
310
325
 
311
- // draw splash screen
312
- showSplashScreen && promises.push(new Promise(resolve =>
326
+ if (!imageSources.length)
313
327
  {
314
- let t = 0;
315
- console.log(`${engineName} Engine v${engineVersion}`);
316
- updateSplash();
317
- function updateSplash()
328
+ // no images to load
329
+ promises.push(new Promise(resolve =>
318
330
  {
319
- clearInput();
320
- drawEngineSplashScreen(t+=.01);
321
- t>1 ? resolve() : setTimeout(updateSplash, 16);
322
- }
323
- }));
331
+ textureInfos[0] = new TextureInfo(new Image);
332
+ resolve();
333
+ }));
334
+ }
335
+
336
+ if (showSplashScreen)
337
+ {
338
+ // draw splash screen
339
+ promises.push(new Promise(resolve =>
340
+ {
341
+ let t = 0;
342
+ console.log(`${engineName} Engine v${engineVersion}`);
343
+ updateSplash();
344
+ function updateSplash()
345
+ {
346
+ clearInput();
347
+ drawEngineSplashScreen(t+=.01);
348
+ t>1 ? resolve() : setTimeout(updateSplash, 16);
349
+ }
350
+ }));
351
+ }
324
352
 
325
353
  // load all of the images
326
354
  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)
@@ -284,6 +288,74 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
284
288
  drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
285
289
  }
286
290
 
291
+ /** Draw colored polygon using passed in points
292
+ * @param {Array} points - Array of Vector2 points
293
+ * @param {Color} [color=(1,1,1,1)]
294
+ * @param {Number} [lineWidth=0]
295
+ * @param {Color} [lineColor=(0,0,0,1)]
296
+ * @param {Boolean} [screenSpace=false]
297
+ * @param {CanvasRenderingContext2D} [context=mainContext]
298
+ * @memberof Draw */
299
+ function drawPoly(points, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
300
+ {
301
+ context.fillStyle = color.toString();
302
+ context.beginPath();
303
+ for (const point of screenSpace ? points : points.map(worldToScreen))
304
+ context.lineTo(point.x, point.y);
305
+ context.closePath();
306
+ context.fill();
307
+ if (lineWidth)
308
+ {
309
+ context.strokeStyle = lineColor.toString();
310
+ context.lineWidth = screenSpace ? lineWidth : lineWidth*cameraScale;
311
+ context.stroke();
312
+ }
313
+ }
314
+
315
+ /** Draw colored ellipse using passed in point
316
+ * @param {Vector2} pos
317
+ * @param {Number} [width=1]
318
+ * @param {Number} [height=1]
319
+ * @param {Number} [angle=0]
320
+ * @param {Color} [color=(1,1,1,1)]
321
+ * @param {Number} [lineWidth=0]
322
+ * @param {Color} [lineColor=(0,0,0,1)]
323
+ * @param {Boolean} [screenSpace=false]
324
+ * @param {CanvasRenderingContext2D} [context=mainContext]
325
+ * @memberof Draw */
326
+ function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
327
+ {
328
+ if (!screenSpace)
329
+ {
330
+ pos = worldToScreen(pos);
331
+ width *= cameraScale;
332
+ height *= cameraScale;
333
+ lineWidth *= cameraScale;
334
+ }
335
+ context.fillStyle = color.toString();
336
+ context.beginPath();
337
+ context.ellipse(pos.x, pos.y, width, height, angle, 0, 9);
338
+ context.fill();
339
+ if (lineWidth)
340
+ {
341
+ context.strokeStyle = lineColor.toString();
342
+ context.lineWidth = lineWidth;
343
+ context.stroke();
344
+ }
345
+ }
346
+
347
+ /** Draw colored circle using passed in point
348
+ * @param {Vector2} pos
349
+ * @param {Number} [radius=1]
350
+ * @param {Color} [color=(1,1,1,1)]
351
+ * @param {Number} [lineWidth=0]
352
+ * @param {Color} [lineColor=(0,0,0,1)]
353
+ * @param {Boolean} [screenSpace=false]
354
+ * @param {CanvasRenderingContext2D} [context=mainContext]
355
+ * @memberof Draw */
356
+ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
357
+ { drawEllipse(pos, radius, radius, 0, color, lineWidth, lineColor, screenSpace, context); }
358
+
287
359
  /** Draw colored line between two points
288
360
  * @param {Vector2} posA
289
361
  * @param {Vector2} posB
@@ -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
  }
@@ -494,6 +568,6 @@ function toggleFullscreen()
494
568
  if (document.exitFullscreen)
495
569
  document.exitFullscreen();
496
570
  }
497
- else if (document.body.requestFullscreen)
498
- document.body.requestFullscreen();
571
+ else if (engineRoot.requestFullscreen)
572
+ engineRoot.requestFullscreen();
499
573
  }