littlejsengine 1.14.23 → 1.14.28

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 (49) hide show
  1. package/dist/littlejs.d.ts +99 -20
  2. package/dist/littlejs.esm.js +378 -130
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +373 -130
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +336 -130
  7. package/examples/box2d/gameObjects.js +6 -10
  8. package/examples/box2d/scenes.js +2 -2
  9. package/examples/breakout/gameObjects.js +1 -1
  10. package/examples/electron/index.html +2 -2
  11. package/examples/index.html +10 -7
  12. package/examples/platformer/gameCharacter.js +3 -2
  13. package/examples/platformer/gameObjects.js +3 -8
  14. package/examples/shorts/base.html +1 -1
  15. package/examples/shorts/box2d.js +2 -2
  16. package/examples/shorts/box2dCar.js +18 -7
  17. package/examples/shorts/box2dPool.js +108 -0
  18. package/examples/shorts/debugDraw.js +2 -3
  19. package/examples/shorts/flappyGame.js +1 -0
  20. package/examples/shorts/fps.js +1 -1
  21. package/examples/shorts/hillGlideGame.js +0 -2
  22. package/examples/shorts/input.js +59 -0
  23. package/examples/shorts/landerGame.js +1 -3
  24. package/examples/shorts/medals.js +15 -9
  25. package/examples/shorts/music.js +15 -17
  26. package/examples/shorts/musicPlayer.js +24 -24
  27. package/examples/shorts/platformer.js +0 -2
  28. package/examples/shorts/slidingPuzzle.js +1 -1
  29. package/examples/shorts/spaceGame.js +0 -2
  30. package/examples/shorts/spriteAtlas.js +0 -2
  31. package/examples/shorts/tiltedView.js +0 -3
  32. package/examples/shorts/timers.js +1 -2
  33. package/examples/shorts/topDown.js +0 -2
  34. package/examples/starter/index.html +2 -2
  35. package/package.json +1 -1
  36. package/plugins/box2d.js +170 -50
  37. package/plugins/pluginExport.js +2 -0
  38. package/plugins/uiSystem.js +55 -27
  39. package/src/engine.js +17 -14
  40. package/src/engineAudio.js +14 -2
  41. package/src/engineDebug.js +37 -0
  42. package/src/engineDraw.js +20 -12
  43. package/src/engineExport.js +3 -0
  44. package/src/engineInput.js +24 -12
  45. package/src/engineMedals.js +2 -2
  46. package/src/engineObject.js +24 -4
  47. package/src/engineParticles.js +1 -6
  48. package/src/engineTileLayer.js +2 -0
  49. package/src/engineUtilities.js +7 -1
@@ -145,12 +145,12 @@ class UISystemPlugin
145
145
  /** Draw a rectangle to the UI context
146
146
  * @param {Vector2} pos
147
147
  * @param {Vector2} size
148
- * @param {Color} [color=uiSystem.defaultColor]
149
- * @param {number} [lineWidth=uiSystem.defaultLineWidth]
150
- * @param {Color} [lineColor=uiSystem.defaultLineColor]
151
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius]
152
- * @param {Color} [gradientColor=uiSystem.defaultGradientColor] */
153
- drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius, gradientColor=uiSystem.defaultGradientColor)
148
+ * @param {Color} [color]
149
+ * @param {number} [lineWidth]
150
+ * @param {Color} [lineColor]
151
+ * @param {number} [cornerRadius]
152
+ * @param {Color} [gradientColor] */
153
+ drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
154
154
  {
155
155
  ASSERT(isVector2(pos), 'pos must be a vec2');
156
156
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -229,10 +229,14 @@ class UISystemPlugin
229
229
  * @param {string} [align]
230
230
  * @param {string} [font=uiSystem.defaultFont]
231
231
  * @param {string} [fontStyle]
232
- * @param {boolean} [applyMaxWidth=true] */
233
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true)
232
+ * @param {boolean} [applyMaxWidth=true]
233
+ * @param {Vector2} [textShadow]
234
+ */
235
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined)
234
236
  {
235
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
237
+ if (textShadow)
238
+ drawTextScreen(text, pos.add(textShadow), size.y, BLACK, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
239
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
236
240
  }
237
241
 
238
242
  /**
@@ -259,6 +263,25 @@ class UISystemPlugin
259
263
  setCallback(onDragLeave, 'dragleave');
260
264
  setCallback(onDragOver, 'dragover');
261
265
  }
266
+
267
+ /** Convert a screen space position to native UI position
268
+ * @param {Vector2} pos
269
+ * @return {Vector2}
270
+ */
271
+ screenToNative(pos)
272
+ {
273
+ if (!uiSystem.nativeHeight)
274
+ return pos;
275
+
276
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
277
+ const sInv = 1/s;
278
+ const p = pos.copy();
279
+ p.x += s*mainCanvasSize.x/2;
280
+ p.x *= sInv;
281
+ p.y *= sInv;
282
+ p.x -= sInv*mainCanvasSize.x/2;
283
+ return p;
284
+ }
262
285
  }
263
286
 
264
287
  ///////////////////////////////////////////////////////////////////////////////
@@ -335,6 +358,9 @@ class UIObject
335
358
  /** @property {boolean} - True if this can be a hover object */
336
359
  this.canBeHover = true;
337
360
  uiSystem.uiObjects.push(this);
361
+
362
+ /** @property {Vector2} - How much to offset the text shadow or undefined */
363
+ this.textShadow = undefined;
338
364
  }
339
365
 
340
366
  /** Add a child UIObject to this object
@@ -362,24 +388,20 @@ class UIObject
362
388
  */
363
389
  isMouseOverlapping()
364
390
  {
391
+ if (!mouseInWindow) return false;
392
+
365
393
  const size = !isTouchDevice ? this.size :
366
394
  this.size.add(vec2(this.extraTouchSize || 0));
367
- if (!uiSystem.nativeHeight)
368
- return isOverlapping(this.pos, size, mousePosScreen);
369
-
370
- const s = mainCanvasSize.y / uiSystem.nativeHeight;
371
- const sInv = 1/s;
372
- let pos = mousePosScreen.copy();
373
- pos.x += s*mainCanvasSize.x/2;
374
- pos.x *= sInv;
375
- pos.y *= sInv;
376
- pos.x -= sInv*mainCanvasSize.x/2;
395
+ const pos = uiSystem.screenToNative(mousePosScreen);
377
396
  return isOverlapping(this.pos, size, pos);
378
397
  }
379
398
 
380
399
  /** Update the object, called automatically by plugin once each frame */
381
400
  update()
382
401
  {
402
+ // call the custom update callback
403
+ this.onUpdate();
404
+
383
405
  const wasHover = uiSystem.lastHoverObject === this;
384
406
  const isActive = this.isActiveObject();
385
407
  const mouseDown = mouseIsDown(0);
@@ -436,7 +458,7 @@ class UIObject
436
458
 
437
459
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
438
460
  const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
439
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
461
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
440
462
  }
441
463
 
442
464
  /** Special update when object is not visible */
@@ -463,6 +485,9 @@ class UIObject
463
485
  /** @return {boolean} - Is the mouse held onto this element */
464
486
  isActiveObject() { return uiSystem.activeObject === this; }
465
487
 
488
+ /** Called each frame when object updates */
489
+ onUpdate() {}
490
+
466
491
  /** Called when the mouse enters the object */
467
492
  onEnter() {}
468
493
 
@@ -517,8 +542,9 @@ class UIText extends UIObject
517
542
  }
518
543
  render()
519
544
  {
545
+ // only render the text
520
546
  const textSize = this.getTextSize();
521
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
547
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
522
548
  }
523
549
  }
524
550
 
@@ -594,7 +620,7 @@ class UIButton extends UIObject
594
620
  // draw the text scaled to fit
595
621
  const textSize = this.getTextSize();
596
622
  uiSystem.drawText(this.text, this.pos, textSize,
597
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
623
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
598
624
  }
599
625
  }
600
626
 
@@ -648,7 +674,7 @@ class UICheckbox extends UIObject
648
674
  const textSize = this.getTextSize();
649
675
  const pos = this.pos.add(vec2(this.size.x,0));
650
676
  uiSystem.drawText(this.text, pos, textSize,
651
- this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
677
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false, this.textShadow);
652
678
  }
653
679
  }
654
680
 
@@ -703,9 +729,11 @@ class UIScrollbar extends UIObject
703
729
  const p1 = centerPos - handleWidth/2;
704
730
  const p2 = centerPos + handleWidth/2;
705
731
  const oldValue = this.value;
732
+
733
+ const p = uiSystem.screenToNative(mousePosScreen);
706
734
  this.value = isHorizontal ?
707
- percent(mousePosScreen.x, p1, p2) :
708
- percent(mousePosScreen.y, p2, p1);
735
+ percent(p.x, p1, p2) :
736
+ percent(p.y, p2, p1);
709
737
  this.value === oldValue || this.onChange();
710
738
  }
711
739
  }
@@ -727,11 +755,11 @@ class UIScrollbar extends UIObject
727
755
  vec2(lerp(p1, p2, this.value), this.pos.y) :
728
756
  vec2(this.pos.x, lerp(p2, p1, this.value))
729
757
  const handleColor = this.disabled ? this.disabledColor : this.handleColor;
730
- uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
758
+ uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
731
759
 
732
760
  // draw the text scaled to fit on the scrollbar
733
761
  const textSize = this.getTextSize();
734
762
  uiSystem.drawText(this.text, this.pos, textSize,
735
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
763
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
736
764
  }
737
765
  }
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.14.24';
33
+ const engineVersion = '1.15.0';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {number}
@@ -161,7 +161,7 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
161
161
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
162
162
  {
163
163
  ASSERT(!mainContext, 'engine already initialized');
164
- ASSERT(Array.isArray(imageSources), 'pass in images as array');
164
+ ASSERT(isArray(imageSources), 'pass in images as array');
165
165
 
166
166
  // allow passing in empty functions
167
167
  gameInit ||= ()=>{};
@@ -453,21 +453,24 @@ function engineObjectsUpdate()
453
453
  // recursive object update
454
454
  function updateObject(o)
455
455
  {
456
- if (!o.destroyed)
457
- {
458
- o.update();
459
- for (const child of o.children)
460
- updateObject(child);
461
- }
456
+ if (o.destroyed)
457
+ return;
458
+
459
+ o.update();
460
+ for (const child of o.children)
461
+ updateObject(child);
462
462
  }
463
463
  for (const o of engineObjects)
464
464
  {
465
+ if (o.parent)
466
+ continue;
467
+
465
468
  // update top level objects
466
- if (!o.parent)
467
- {
468
- updateObject(o);
469
- o.updateTransforms();
470
- }
469
+ o.update();
470
+ o.updatePhysics();
471
+ for (const child of o.children)
472
+ updateObject(child);
473
+ o.updateTransforms();
471
474
  }
472
475
 
473
476
  // remove destroyed objects
@@ -500,7 +503,7 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
500
503
  else if (size instanceof Vector2) // bounding box test
501
504
  {
502
505
  for (const o of objects)
503
- isOverlapping(pos, size, o.pos, o.size) && collectedObjects.push(o);
506
+ o.isOverlapping(pos, size) && collectedObjects.push(o);
504
507
  }
505
508
  else // circle test
506
509
  {
@@ -66,6 +66,10 @@ class Sound
66
66
  {
67
67
  if (!soundEnable || headlessMode) return;
68
68
 
69
+ ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
70
+ ASSERT(isNumber(range), 'range must be a number');
71
+ ASSERT(isNumber(taper), 'taper must be a number');
72
+
69
73
  /** @property {number} - World space max range of sound */
70
74
  this.range = range;
71
75
  /** @property {number} - At what percentage of range should it start tapering */
@@ -104,6 +108,12 @@ class Sound
104
108
  */
105
109
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
106
110
  {
111
+ ASSERT(!pos || isVector2(pos), 'pos must be a vec2');
112
+ ASSERT(isNumber(volume), 'volume must be a number');
113
+ ASSERT(isNumber(pitch), 'pitch must be a number');
114
+ ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
115
+
116
+
107
117
  if (!soundEnable || headlessMode) return;
108
118
  if (!this.sampleChannels) return;
109
119
 
@@ -149,6 +159,7 @@ class Sound
149
159
  */
150
160
  playNote(semitoneOffset=0, pos, volume)
151
161
  {
162
+ ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
152
163
  const pitch = getNoteFrequency(semitoneOffset, 1);
153
164
  return this.play(pos, volume, pitch, 0);
154
165
  }
@@ -157,7 +168,7 @@ class Sound
157
168
  * @return {number} - How long the sound is in seconds (undefined if loading)
158
169
  */
159
170
  getDuration()
160
- { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
171
+ { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
161
172
 
162
173
  /** Check if sound is loaded, for sounds fetched from a url
163
174
  * @return {boolean} - True if sound is loaded and ready to play
@@ -199,6 +210,7 @@ class SoundWave extends Sound
199
210
  super(undefined, range, taper);
200
211
  if (!soundEnable || headlessMode) return;
201
212
  ASSERT(!filename || isString(filename), 'filename must be a string');
213
+ ASSERT(isNumber(randomness), 'randomness must be a number');
202
214
 
203
215
  /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
204
216
  this.onloadCallback = onloadCallback;
@@ -415,7 +427,7 @@ class SoundInstance
415
427
  /** Get the total duration of this sound
416
428
  * @return {number} - Total duration in seconds
417
429
  */
418
- getDuration() { return this.sound.getDuration() / this.rate; }
430
+ getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
419
431
 
420
432
  /** Get source of this sound instance
421
433
  * @return {AudioBufferSourceNode}
@@ -73,6 +73,12 @@ function LOG(...output) { console.log(...output); }
73
73
  * @memberof Debug */
74
74
  function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
75
75
  {
76
+ ASSERT(isVector2(pos), 'pos must be a vec2');
77
+ ASSERT(isVector2(size), 'size must be a vec2');
78
+ ASSERT(isString(color) || isColor(color), 'color is invalid');
79
+ ASSERT(isNumber(time), 'time must be a number');
80
+ ASSERT(isNumber(angle), 'angle must be a number');
81
+
76
82
  if (typeof size === 'number')
77
83
  size = vec2(size); // allow passing in floats
78
84
  if (isColor(color))
@@ -93,6 +99,12 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
93
99
  * @memberof Debug */
94
100
  function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
95
101
  {
102
+ ASSERT(isVector2(pos), 'pos must be a vec2');
103
+ ASSERT(isArray(points), 'points must be an array');
104
+ ASSERT(isString(color) || isColor(color), 'color is invalid');
105
+ ASSERT(isNumber(time), 'time must be a number');
106
+ ASSERT(isNumber(angle), 'angle must be a number');
107
+
96
108
  if (isColor(color))
97
109
  color = color.toString();
98
110
  pos = pos.copy();
@@ -110,6 +122,11 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
110
122
  * @memberof Debug */
111
123
  function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
112
124
  {
125
+ ASSERT(isVector2(pos), 'pos must be a vec2');
126
+ ASSERT(isNumber(size), 'size must be a number');
127
+ ASSERT(isString(color) || isColor(color), 'color is invalid');
128
+ ASSERT(isNumber(time), 'time must be a number');
129
+
113
130
  if (isColor(color))
114
131
  color = color.toString();
115
132
  pos = pos.copy();
@@ -135,6 +152,10 @@ function debugPoint(pos, color, time, angle)
135
152
  * @memberof Debug */
136
153
  function debugLine(posA, posB, color, width=.1, time)
137
154
  {
155
+ ASSERT(isVector2(posA), 'posA must be a vec2');
156
+ ASSERT(isVector2(posB), 'posB must be sa vec2');
157
+ ASSERT(isNumber(width), 'width must be a number');
158
+
138
159
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
139
160
  const size = vec2(width, halfDelta.length()*2);
140
161
  debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
@@ -149,6 +170,11 @@ function debugLine(posA, posB, color, width=.1, time)
149
170
  * @memberof Debug */
150
171
  function debugOverlap(posA, sizeA, posB, sizeB, color)
151
172
  {
173
+ ASSERT(isVector2(posA), 'posA must be a vec2');
174
+ ASSERT(isVector2(posB), 'posB must be a vec2');
175
+ ASSERT(isVector2(sizeA), 'sizeA must be a vec2');
176
+ ASSERT(isVector2(sizeB), 'sizeB must be a vec2');
177
+
152
178
  const minPos = vec2(
153
179
  min(posA.x - sizeA.x/2, posB.x - sizeB.x/2),
154
180
  min(posA.y - sizeA.y/2, posB.y - sizeB.y/2)
@@ -171,6 +197,14 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
171
197
  * @memberof Debug */
172
198
  function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace')
173
199
  {
200
+ ASSERT(isString(text), 'text must be a string');
201
+ ASSERT(isVector2(pos), 'pos must be a vec2');
202
+ ASSERT(isNumber(size), 'size must be a number');
203
+ ASSERT(isString(color) || isColor(color), 'color is invalid');
204
+ ASSERT(isNumber(time), 'time must be a number');
205
+ ASSERT(isNumber(angle), 'angle must be a number');
206
+ ASSERT(isString(font), 'font must be a string');
207
+
174
208
  if (isColor(color))
175
209
  color = color.toString();
176
210
  pos = pos.copy();
@@ -624,6 +658,9 @@ function debugVideoCaptureUpdate()
624
658
  debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
625
659
  }
626
660
 
661
+ ///////////////////////////////////////////////////////////////////////////////
662
+ // debug utility functions
663
+
627
664
  // make color constants immutable with debug assertions
628
665
  function debugProtectConstant(obj)
629
666
  {
package/src/engineDraw.js CHANGED
@@ -403,7 +403,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
403
403
  * @memberof Draw */
404
404
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
405
405
  {
406
- ASSERT(Array.isArray(points), 'points must be an array');
406
+ ASSERT(isArray(points), 'points must be an array');
407
407
  ASSERT(isNumber(width), 'width must be a number');
408
408
  ASSERT(isColor(color), 'color is invalid');
409
409
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -509,7 +509,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
509
509
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
510
510
  {
511
511
  ASSERT(isVector2(pos), 'pos must be a vec2');
512
- ASSERT(Array.isArray(points), 'points must be an array');
512
+ ASSERT(isArray(points), 'points must be an array');
513
513
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
514
514
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
515
515
  ASSERT(isNumber(angle), 'angle must be a number');
@@ -661,11 +661,12 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
661
661
  * @param {string} [font=fontDefault]
662
662
  * @param {string} [fontStyle]
663
663
  * @param {number} [maxWidth]
664
+ * @param {number} [angle]
664
665
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
665
666
  * @memberof Draw */
666
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
667
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
667
668
  {
668
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
669
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
669
670
  }
670
671
 
671
672
  /** Draw text on overlay canvas in world space
@@ -680,10 +681,11 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
680
681
  * @param {string} [font=fontDefault]
681
682
  * @param {string} [fontStyle]
682
683
  * @param {number} [maxWidth]
684
+ * @param {number} [angle]
683
685
  * @memberof Draw */
684
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
686
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
685
687
  {
686
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
688
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
687
689
  }
688
690
 
689
691
  /** Draw text on overlay canvas in screen space
@@ -698,9 +700,10 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
698
700
  * @param {string} [font=fontDefault]
699
701
  * @param {string} [fontStyle]
700
702
  * @param {number} [maxWidth]
703
+ * @param {number} [angle]
701
704
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
702
705
  * @memberof Draw */
703
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
706
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
704
707
  {
705
708
  ASSERT(isString(text), 'text must be a string');
706
709
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -711,6 +714,7 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
711
714
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
712
715
  ASSERT(isString(font), 'font must be a string');
713
716
  ASSERT(isString(fontStyle), 'fontStyle must be a string');
717
+ ASSERT(isNumber(angle), 'angle must be a number');
714
718
 
715
719
  context.fillStyle = color.toString();
716
720
  context.strokeStyle = lineColor.toString();
@@ -720,14 +724,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
720
724
  context.textBaseline = 'middle';
721
725
 
722
726
  const lines = (text+'').split('\n');
723
- let posY = pos.y;
724
- posY -= (lines.length-1) * size/2; // center text vertically
727
+ const posY = pos.y - (lines.length-1) * size/2; // center vertically
728
+ context.save();
729
+ context.translate(pos.x, posY);
730
+ context.rotate(-angle);
731
+ let yOffset = 0;
725
732
  lines.forEach(line=>
726
733
  {
727
- lineWidth && context.strokeText(line, pos.x, posY, maxWidth);
728
- context.fillText(line, pos.x, posY, maxWidth);
729
- posY += size;
734
+ lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
735
+ context.fillText(line, 0, yOffset, maxWidth);
736
+ yOffset += size;
730
737
  });
738
+ context.restore();
731
739
  }
732
740
 
733
741
  ///////////////////////////////////////////////////////////////////////////////
@@ -173,6 +173,7 @@ export
173
173
  // Random
174
174
  rand,
175
175
  randInt,
176
+ randBool,
176
177
  randSign,
177
178
  randInCircle,
178
179
  randVec2,
@@ -190,6 +191,7 @@ export
190
191
  isVector2,
191
192
  isNumber,
192
193
  isString,
194
+ isArray,
193
195
 
194
196
  // Default Colors
195
197
  WHITE,
@@ -290,6 +292,7 @@ export
290
292
  mouseDelta,
291
293
  mouseDeltaScreen,
292
294
  mouseWheel,
295
+ mouseInWindow,
293
296
  isUsingGamepad,
294
297
  inputPreventDefault,
295
298
  setInputPreventDefault,
@@ -119,6 +119,11 @@ let mouseDeltaScreen = vec2();
119
119
  * @memberof Input */
120
120
  let mouseWheel = 0;
121
121
 
122
+ /** True if mouse was inside the document window, set to false when mouse leaves
123
+ * @type {boolean}
124
+ * @memberof Input */
125
+ let mouseInWindow = true;
126
+
122
127
  /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
123
128
  * @type {boolean}
124
129
  * @memberof Input */
@@ -213,6 +218,7 @@ function inputInit()
213
218
  document.addEventListener('mousedown', onMouseDown);
214
219
  document.addEventListener('mouseup', onMouseUp);
215
220
  document.addEventListener('mousemove', onMouseMove);
221
+ document.addEventListener('mouseleave', onMouseLeave);
216
222
  document.addEventListener('wheel', onMouseWheel);
217
223
  document.addEventListener('contextmenu', onContextMenu);
218
224
  document.addEventListener('blur', onBlur);
@@ -237,14 +243,14 @@ function inputInit()
237
243
  if (inputWASDEmulateDirection)
238
244
  inputData[0][remapKey(e.code)] = 4;
239
245
  }
240
- function remapKey(c)
246
+ function remapKey(k)
241
247
  {
242
248
  // handle remapping wasd keys to directions
243
249
  return inputWASDEmulateDirection ?
244
- c === 'KeyW' ? 'ArrowUp' :
245
- c === 'KeyS' ? 'ArrowDown' :
246
- c === 'KeyA' ? 'ArrowLeft' :
247
- c === 'KeyD' ? 'ArrowRight' : c : c;
250
+ k === 'KeyW' ? 'ArrowUp' :
251
+ k === 'KeyS' ? 'ArrowDown' :
252
+ k === 'KeyA' ? 'ArrowLeft' :
253
+ k === 'KeyD' ? 'ArrowRight' : k : k;
248
254
  }
249
255
  function onMouseDown(e)
250
256
  {
@@ -258,7 +264,7 @@ function inputInit()
258
264
  isUsingGamepad = false;
259
265
  inputData[0][e.button] = 3;
260
266
 
261
- let mousePosScreenLast = mousePosScreen;
267
+ const mousePosScreenLast = mousePosScreen;
262
268
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
263
269
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
264
270
  inputPreventDefault && e.button && e.preventDefault();
@@ -271,10 +277,17 @@ function inputInit()
271
277
  }
272
278
  function onMouseMove(e)
273
279
  {
274
- let mousePosScreenLast = mousePosScreen;
280
+ mouseInWindow = true;
281
+ const mousePosScreenLast = mousePosScreen;
275
282
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
276
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
283
+
284
+ // when pointer is locked use movementX/Y for delta
285
+ const movement = pointerLockIsActive() ?
286
+ vec2(e.movementX, e.movementY) :
287
+ mousePosScreen.subtract(mousePosScreenLast);
288
+ mouseDeltaScreen = mouseDeltaScreen.add(movement);
277
289
  }
290
+ function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
278
291
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
279
292
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
280
293
  function onBlur() { inputClear(); } // reset input when focus is lost
@@ -413,7 +426,7 @@ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick =
413
426
  function touchGamepadButtonCenter()
414
427
  {
415
428
  // draw right face buttons
416
- let center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
429
+ const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
417
430
  if (touchGamepadButtonCount <= 2)
418
431
  center.x += touchGamepadSize/2;
419
432
  return center;
@@ -423,14 +436,13 @@ function touchGamepadButtonCenter()
423
436
  function touchInputInit()
424
437
  {
425
438
  // add non passive touch event listeners
426
- let handleTouch = handleTouchDefault;
427
439
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
428
440
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
429
441
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
430
442
 
431
443
  // handle all touch events the same way
432
444
  let wasTouching;
433
- function handleTouchDefault(e)
445
+ function handleTouch(e)
434
446
  {
435
447
  if (!touchInputEnable)
436
448
  return;
@@ -581,7 +593,7 @@ function touchGamepadRender()
581
593
  const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
582
594
  for (let i=0; i<touchGamepadButtonCount; i++)
583
595
  {
584
- let j = mod(i-1, 4);
596
+ const j = mod(i-1, 4);
585
597
  let button = touchGamepadButtonCount > 2 ?
586
598
  j : min(j, touchGamepadButtonCount-1);
587
599
  // fix button locations (swap 2 and 3 to match gamepad layout)
@@ -167,11 +167,11 @@ class Medal
167
167
  const descriptionSize = height*.3;
168
168
  const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
169
169
  const textWidth = width - medalDisplayIconSize - 3*gap.x;
170
- drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
170
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
171
171
 
172
172
  // draw the description
173
173
  pos.y = y + height - gap.y*2 - descriptionSize/2;
174
- drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
174
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
175
175
  context.restore();
176
176
  }
177
177