littlejsengine 1.18.7 → 1.18.8

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.
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.18.7';
38
+ const engineVersion = '1.18.8';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -900,13 +900,15 @@ function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
900
900
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
901
901
 
902
902
  /**
903
- * Check if object can be converted to a string (has a toString method)
903
+ * Check if a value is stringifiable i.e. it has a toString that returns
904
+ * a string. Use this for ASSERTs and inputs that will be coerced to text;
905
+ * use `typeof x === 'string'` inline if you need strict-string semantics.
904
906
  * - Returns true for strings, numbers, and most objects
905
907
  * - Returns false for null and undefined
906
908
  * @param {any} s
907
909
  * @return {boolean}
908
910
  * @memberof Math */
909
- function isString(s) { return s != null && typeof s?.toString() === 'string'; }
911
+ function isStringLike(s) { return s != null && typeof s?.toString() === 'string'; }
910
912
 
911
913
  /**
912
914
  * Check if object is an array
@@ -1650,7 +1652,7 @@ class Color
1650
1652
  * @return {Color} */
1651
1653
  setHex(hex)
1652
1654
  {
1653
- ASSERT(isString(hex), 'Color hex code must be a string');
1655
+ ASSERT(isStringLike(hex), 'Color hex code must be a string');
1654
1656
  ASSERT(hex[0] === '#', 'Color hex code must start with #');
1655
1657
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
1656
1658
 
@@ -1914,8 +1916,8 @@ function saveCanvas(canvas, filename='screenshot', type='image/png')
1914
1916
  * @memberof Utilities */
1915
1917
  function saveDataURL(url, filename='download', revokeTime)
1916
1918
  {
1917
- ASSERT(isString(url), 'saveDataURL requires url string');
1918
- ASSERT(isString(filename), 'saveDataURL requires filename string');
1919
+ ASSERT(isStringLike(url), 'saveDataURL requires url string');
1920
+ ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
1919
1921
 
1920
1922
  // create link for saving screenshots
1921
1923
  const link = document.createElement('a');
@@ -1933,8 +1935,8 @@ function saveDataURL(url, filename='download', revokeTime)
1933
1935
  * @memberof Utilities */
1934
1936
  function shareURL(title, url, callback)
1935
1937
  {
1936
- ASSERT(isString(title), 'shareURL requires title string');
1937
- ASSERT(isString(url), 'shareURL requires url string');
1938
+ ASSERT(isStringLike(title), 'shareURL requires title string');
1939
+ ASSERT(isStringLike(url), 'shareURL requires url string');
1938
1940
  navigator.share?.({title, url}).then(()=>callback?.());
1939
1941
  }
1940
1942
 
@@ -1947,7 +1949,7 @@ function shareURL(title, url, callback)
1947
1949
  * @memberof Utilities */
1948
1950
  function readSaveData(saveName, defaultSaveData)
1949
1951
  {
1950
- ASSERT(isString(saveName), 'loadData requires saveName string');
1952
+ ASSERT(isStringLike(saveName), 'loadData requires saveName string');
1951
1953
 
1952
1954
  // replace undefined values with defaults; tolerate corrupt JSON
1953
1955
  const data = localStorage[saveName];
@@ -1966,7 +1968,7 @@ function readSaveData(saveName, defaultSaveData)
1966
1968
  * @memberof Utilities */
1967
1969
  function writeSaveData(saveName, saveData)
1968
1970
  {
1969
- ASSERT(isString(saveName), 'saveData requires saveName string');
1971
+ ASSERT(isStringLike(saveName), 'saveData requires saveName string');
1970
1972
  localStorage[saveName] = JSON.stringify(saveData);
1971
1973
  }
1972
1974
  /**
@@ -3912,15 +3914,15 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3912
3914
  * @memberof Draw */
3913
3915
  function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
3914
3916
  {
3915
- ASSERT(isString(text), 'text must be a string');
3917
+ ASSERT(isStringLike(text), 'text must be a string');
3916
3918
  ASSERT(isVector2(pos), 'pos must be a vec2');
3917
3919
  ASSERT(isNumber(size), 'size must be a number');
3918
3920
  ASSERT(isColor(color), 'color must be a color');
3919
3921
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3920
3922
  ASSERT(isColor(lineColor), 'lineColor must be a color');
3921
3923
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
3922
- ASSERT(isString(font), 'font must be a string');
3923
- ASSERT(isString(fontStyle), 'fontStyle must be a string');
3924
+ ASSERT(isStringLike(font), 'font must be a string');
3925
+ ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
3924
3926
  ASSERT(isNumber(angle), 'angle must be a number');
3925
3927
 
3926
3928
  context.fillStyle = color.toString();
@@ -3957,7 +3959,7 @@ async function loadTexture(textureIndex, src)
3957
3959
  {
3958
3960
  ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
3959
3961
  ASSERT(!textureInfos[textureIndex], 'textureIndex is already loaded!');
3960
- ASSERT(!src || isString(src), 'image src must be a string');
3962
+ ASSERT(!src || isStringLike(src), 'image src must be a string');
3961
3963
 
3962
3964
  const image = new Image;
3963
3965
  if (src)
@@ -4345,7 +4347,7 @@ class FontImage
4345
4347
  */
4346
4348
  drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
4347
4349
  {
4348
- ASSERT(isString(text), 'text must be a string');
4350
+ ASSERT(isStringLike(text), 'text must be a string');
4349
4351
  ASSERT(isVector2(pos), 'pos must be a vec2');
4350
4352
  ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
4351
4353
  ASSERT(isColor(color), 'color must be a color');
@@ -4506,7 +4508,7 @@ function inputClear()
4506
4508
  * @memberof Input */
4507
4509
  function keyIsDown(key, device=0)
4508
4510
  {
4509
- ASSERT(isString(key), 'key must be a number or string');
4511
+ ASSERT(isStringLike(key), 'key must be a number or string');
4510
4512
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
4511
4513
  return !!(inputData[device]?.[key] & 1);
4512
4514
  }
@@ -4518,7 +4520,7 @@ function keyIsDown(key, device=0)
4518
4520
  * @memberof Input */
4519
4521
  function keyWasPressed(key, device=0)
4520
4522
  {
4521
- ASSERT(isString(key), 'key must be a number or string');
4523
+ ASSERT(isStringLike(key), 'key must be a number or string');
4522
4524
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
4523
4525
  return !!(inputData[device]?.[key] & 2);
4524
4526
  }
@@ -4530,7 +4532,7 @@ function keyWasPressed(key, device=0)
4530
4532
  * @memberof Input */
4531
4533
  function keyWasReleased(key, device=0)
4532
4534
  {
4533
- ASSERT(isString(key), 'key must be a number or string');
4535
+ ASSERT(isStringLike(key), 'key must be a number or string');
4534
4536
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
4535
4537
  return !!(inputData[device]?.[key] & 4);
4536
4538
  }
@@ -4544,10 +4546,10 @@ function keyWasReleased(key, device=0)
4544
4546
  * @memberof Input */
4545
4547
  function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
4546
4548
  {
4547
- ASSERT(isString(up), 'up key must be a string');
4548
- ASSERT(isString(down), 'down key must be a string');
4549
- ASSERT(isString(left), 'left key must be a string');
4550
- ASSERT(isString(right), 'right key must be a string');
4549
+ ASSERT(isStringLike(up), 'up key must be a string');
4550
+ ASSERT(isStringLike(down), 'down key must be a string');
4551
+ ASSERT(isStringLike(left), 'left key must be a string');
4552
+ ASSERT(isStringLike(right), 'right key must be a string');
4551
4553
  const k = (key)=> keyIsDown(key) ? 1 : 0;
4552
4554
  return vec2(k(right) - k(left), k(up) - k(down));
4553
4555
  }
@@ -5265,12 +5267,10 @@ function touchGamepadButtonCenter()
5265
5267
  * @namespace Audio
5266
5268
  */
5267
5269
 
5268
- /** Audio context used by the engine. Created lazily in audioInit() to avoid
5269
- * browser autoplay warnings about constructing an AudioContext before any
5270
- * user gesture.
5270
+ /** Audio context used by the engine
5271
5271
  * @type {AudioContext}
5272
5272
  * @memberof Audio */
5273
- let audioContext;
5273
+ let audioContext = new AudioContext;
5274
5274
 
5275
5275
  /** Master gain node for all audio to pass through
5276
5276
  * @type {GainNode}
@@ -5286,13 +5286,12 @@ const audioDefaultSampleRate = 44100;
5286
5286
  * @return {boolean} - True if the audio context is running
5287
5287
  * @memberof Audio */
5288
5288
  function audioIsRunning()
5289
- { return audioContext?.state === 'running'; }
5289
+ { return audioContext.state === 'running'; }
5290
5290
 
5291
5291
  function audioInit()
5292
5292
  {
5293
5293
  if (!soundEnable || headlessMode) return;
5294
5294
 
5295
- audioContext = new AudioContext;
5296
5295
  audioMasterGain = audioContext.createGain();
5297
5296
  audioMasterGain.connect(audioContext.destination);
5298
5297
  audioMasterGain.gain.value = soundVolume; // set starting value
@@ -5338,7 +5337,7 @@ class Sound
5338
5337
  {
5339
5338
  if (!soundEnable || headlessMode) return;
5340
5339
 
5341
- ASSERT(!asset || isArray(asset) || isString(asset), 'asset must be a file name or zzfx array');
5340
+ ASSERT(!asset || isArray(asset) || isStringLike(asset), 'asset must be a file name or zzfx array');
5342
5341
  ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
5343
5342
  ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
5344
5343
  ASSERT(isNumber(range), 'range must be a number');
@@ -9097,12 +9096,31 @@ class UISystemPlugin
9097
9096
 
9098
9097
  engineAddPlugin(uiUpdate, uiRender);
9099
9098
 
9100
- // set object position in parent space
9099
+ // set object position based on anchor target (parent box, or canvas for roots),
9100
+ // self-pivot, and localPos offset
9101
9101
  function updateTransforms(o)
9102
9102
  {
9103
- if (!o.parent) return;
9104
- o.pos.x = o.localPos.x + o.parent.pos.x;
9105
- o.pos.y = o.localPos.y + o.parent.pos.y;
9103
+ let targetPos, targetSize;
9104
+ if (o.parent)
9105
+ {
9106
+ targetPos = o.parent.pos;
9107
+ targetSize = o.parent.size;
9108
+ }
9109
+ else
9110
+ {
9111
+ // anchor to canvas in native coords (handles nativeHeight if set)
9112
+ targetPos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
9113
+ targetSize = uiSystem.nativeHeight
9114
+ ? vec2(mainCanvasSize.x * uiSystem.nativeHeight / mainCanvasSize.y,
9115
+ uiSystem.nativeHeight)
9116
+ : mainCanvasSize;
9117
+ }
9118
+
9119
+ const a = o.anchor;
9120
+ o.pos = targetPos
9121
+ .add(targetSize.multiply(a).scale(.5)) // anchor point on target
9122
+ .subtract(o.size.multiply(a).scale(.5)) // pivot shift on self
9123
+ .add(o.localPos); // user offset
9106
9124
  }
9107
9125
 
9108
9126
  // setup recursive update and render
@@ -9569,9 +9587,8 @@ class UISystemPlugin
9569
9587
  // confirm menu
9570
9588
  const confirmMenu = new UIObject(vec2(), size);
9571
9589
  uiSystem.confirmDialog = confirmMenu;
9572
- confirmMenu.onRender = ()=>
9590
+ confirmMenu.onRender = ()=>
9573
9591
  {
9574
- confirmMenu.pos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
9575
9592
  const backgroundColor = hsl(0,0,0,.7);
9576
9593
  uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
9577
9594
  }
@@ -9704,7 +9721,11 @@ class UIObject
9704
9721
  this.navigationIndex = undefined;
9705
9722
  /** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
9706
9723
  this.navigationAutoSelect = false;
9707
-
9724
+ /** @property {Vector2} - Where on parent (or canvas if no parent) this object is anchored.
9725
+ * Components in [-1, 1]: (0,0)=center, (-1,-1)=top-left, (1,1)=bottom-right.
9726
+ * Also acts as self-pivot — e.g. (1,-1) puts your top-right corner at the anchor point. */
9727
+ this.anchor = vec2();
9728
+
9708
9729
  uiSystem.uiObjects.push(this);
9709
9730
  }
9710
9731
 
@@ -9957,9 +9978,9 @@ class UIText extends UIObject
9957
9978
  {
9958
9979
  super(pos, size);
9959
9980
 
9960
- ASSERT(isString(text), 'ui text must be a string');
9981
+ ASSERT(isStringLike(text), 'ui text must be a string');
9961
9982
  ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
9962
- ASSERT(isString(font), 'ui text font must be a string');
9983
+ ASSERT(isStringLike(font), 'ui text font must be a string');
9963
9984
 
9964
9985
  // set properties
9965
9986
  this.text = text;
@@ -10007,7 +10028,7 @@ class UITextInput extends UIObject
10007
10028
  {
10008
10029
  super(pos, size);
10009
10030
 
10010
- ASSERT(isString(text), 'ui text must be a string');
10031
+ ASSERT(isStringLike(text), 'ui text must be a string');
10011
10032
 
10012
10033
  /** @property {number} - Max length of input (0 = no limit) */
10013
10034
  this.maxLength = 0;
@@ -10144,7 +10165,7 @@ class UIButton extends UIObject
10144
10165
  {
10145
10166
  super(pos, size);
10146
10167
 
10147
- ASSERT(isString(text), 'ui button must be a string');
10168
+ ASSERT(isStringLike(text), 'ui button must be a string');
10148
10169
  ASSERT(isColor(color), 'ui button color must be a color');
10149
10170
 
10150
10171
  /** @property {Vector2} - Text offset for the button */
@@ -10185,7 +10206,7 @@ class UICheckbox extends UIObject
10185
10206
  {
10186
10207
  super(pos, size);
10187
10208
 
10188
- ASSERT(isString(text), 'ui checkbox must be a string');
10209
+ ASSERT(isStringLike(text), 'ui checkbox must be a string');
10189
10210
  ASSERT(isColor(color), 'ui checkbox color must be a color');
10190
10211
 
10191
10212
  /** @property {boolean} - Current percentage value of this slider 0-1 */
@@ -10242,7 +10263,7 @@ class UISlider extends UIObject
10242
10263
  super(pos, size);
10243
10264
 
10244
10265
  ASSERT(isNumber(value), 'ui slider value must be a number');
10245
- ASSERT(isString(text), 'ui slider must be a string');
10266
+ ASSERT(isStringLike(text), 'ui slider must be a string');
10246
10267
  ASSERT(isColor(color), 'ui slider color must be a color');
10247
10268
  ASSERT(isColor(handleColor), 'ui slider handleColor must be a color');
10248
10269
 
@@ -10360,7 +10381,7 @@ class UIVideo extends UIObject
10360
10381
  {
10361
10382
  super(pos, size || vec2());
10362
10383
 
10363
- ASSERT(isString(src), 'video src must be a string');
10384
+ ASSERT(isStringLike(src), 'video src must be a string');
10364
10385
  ASSERT(isNumber(volume), 'video volume must be a number');
10365
10386
 
10366
10387
  this.color = BLACK; // default to black background
@@ -13210,7 +13231,7 @@ const Ease =
13210
13231
  function tweenProperty(target, propertyPath, start, end, duration = 1, options = {})
13211
13232
  {
13212
13233
  ASSERT(target != null && typeof target === 'object', 'tweenProperty target must be an object');
13213
- ASSERT(isString(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
13234
+ ASSERT(isStringLike(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
13214
13235
 
13215
13236
  const parts = propertyPath.split('.');
13216
13237
  const lastKey = parts.pop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.18.7",
3
+ "version": "1.18.8",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
@@ -409,7 +409,7 @@ const Ease =
409
409
  function tweenProperty(target, propertyPath, start, end, duration = 1, options = {})
410
410
  {
411
411
  ASSERT(target != null && typeof target === 'object', 'tweenProperty target must be an object');
412
- ASSERT(isString(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
412
+ ASSERT(isStringLike(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
413
413
 
414
414
  const parts = propertyPath.split('.');
415
415
  const lastKey = parts.pop();
@@ -125,12 +125,31 @@ class UISystemPlugin
125
125
 
126
126
  engineAddPlugin(uiUpdate, uiRender);
127
127
 
128
- // set object position in parent space
128
+ // set object position based on anchor target (parent box, or canvas for roots),
129
+ // self-pivot, and localPos offset
129
130
  function updateTransforms(o)
130
131
  {
131
- if (!o.parent) return;
132
- o.pos.x = o.localPos.x + o.parent.pos.x;
133
- o.pos.y = o.localPos.y + o.parent.pos.y;
132
+ let targetPos, targetSize;
133
+ if (o.parent)
134
+ {
135
+ targetPos = o.parent.pos;
136
+ targetSize = o.parent.size;
137
+ }
138
+ else
139
+ {
140
+ // anchor to canvas in native coords (handles nativeHeight if set)
141
+ targetPos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
142
+ targetSize = uiSystem.nativeHeight
143
+ ? vec2(mainCanvasSize.x * uiSystem.nativeHeight / mainCanvasSize.y,
144
+ uiSystem.nativeHeight)
145
+ : mainCanvasSize;
146
+ }
147
+
148
+ const a = o.anchor;
149
+ o.pos = targetPos
150
+ .add(targetSize.multiply(a).scale(.5)) // anchor point on target
151
+ .subtract(o.size.multiply(a).scale(.5)) // pivot shift on self
152
+ .add(o.localPos); // user offset
134
153
  }
135
154
 
136
155
  // setup recursive update and render
@@ -597,9 +616,8 @@ class UISystemPlugin
597
616
  // confirm menu
598
617
  const confirmMenu = new UIObject(vec2(), size);
599
618
  uiSystem.confirmDialog = confirmMenu;
600
- confirmMenu.onRender = ()=>
619
+ confirmMenu.onRender = ()=>
601
620
  {
602
- confirmMenu.pos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
603
621
  const backgroundColor = hsl(0,0,0,.7);
604
622
  uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
605
623
  }
@@ -732,7 +750,11 @@ class UIObject
732
750
  this.navigationIndex = undefined;
733
751
  /** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
734
752
  this.navigationAutoSelect = false;
735
-
753
+ /** @property {Vector2} - Where on parent (or canvas if no parent) this object is anchored.
754
+ * Components in [-1, 1]: (0,0)=center, (-1,-1)=top-left, (1,1)=bottom-right.
755
+ * Also acts as self-pivot — e.g. (1,-1) puts your top-right corner at the anchor point. */
756
+ this.anchor = vec2();
757
+
736
758
  uiSystem.uiObjects.push(this);
737
759
  }
738
760
 
@@ -985,9 +1007,9 @@ class UIText extends UIObject
985
1007
  {
986
1008
  super(pos, size);
987
1009
 
988
- ASSERT(isString(text), 'ui text must be a string');
1010
+ ASSERT(isStringLike(text), 'ui text must be a string');
989
1011
  ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
990
- ASSERT(isString(font), 'ui text font must be a string');
1012
+ ASSERT(isStringLike(font), 'ui text font must be a string');
991
1013
 
992
1014
  // set properties
993
1015
  this.text = text;
@@ -1035,7 +1057,7 @@ class UITextInput extends UIObject
1035
1057
  {
1036
1058
  super(pos, size);
1037
1059
 
1038
- ASSERT(isString(text), 'ui text must be a string');
1060
+ ASSERT(isStringLike(text), 'ui text must be a string');
1039
1061
 
1040
1062
  /** @property {number} - Max length of input (0 = no limit) */
1041
1063
  this.maxLength = 0;
@@ -1172,7 +1194,7 @@ class UIButton extends UIObject
1172
1194
  {
1173
1195
  super(pos, size);
1174
1196
 
1175
- ASSERT(isString(text), 'ui button must be a string');
1197
+ ASSERT(isStringLike(text), 'ui button must be a string');
1176
1198
  ASSERT(isColor(color), 'ui button color must be a color');
1177
1199
 
1178
1200
  /** @property {Vector2} - Text offset for the button */
@@ -1213,7 +1235,7 @@ class UICheckbox extends UIObject
1213
1235
  {
1214
1236
  super(pos, size);
1215
1237
 
1216
- ASSERT(isString(text), 'ui checkbox must be a string');
1238
+ ASSERT(isStringLike(text), 'ui checkbox must be a string');
1217
1239
  ASSERT(isColor(color), 'ui checkbox color must be a color');
1218
1240
 
1219
1241
  /** @property {boolean} - Current percentage value of this slider 0-1 */
@@ -1270,7 +1292,7 @@ class UISlider extends UIObject
1270
1292
  super(pos, size);
1271
1293
 
1272
1294
  ASSERT(isNumber(value), 'ui slider value must be a number');
1273
- ASSERT(isString(text), 'ui slider must be a string');
1295
+ ASSERT(isStringLike(text), 'ui slider must be a string');
1274
1296
  ASSERT(isColor(color), 'ui slider color must be a color');
1275
1297
  ASSERT(isColor(handleColor), 'ui slider handleColor must be a color');
1276
1298
 
@@ -1388,7 +1410,7 @@ class UIVideo extends UIObject
1388
1410
  {
1389
1411
  super(pos, size || vec2());
1390
1412
 
1391
- ASSERT(isString(src), 'video src must be a string');
1413
+ ASSERT(isStringLike(src), 'video src must be a string');
1392
1414
  ASSERT(isNumber(volume), 'video volume must be a number');
1393
1415
 
1394
1416
  this.color = BLACK; // default to black background
package/src/engine.js CHANGED
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
32
32
  * @type {string}
33
33
  * @default
34
34
  * @memberof Engine */
35
- const engineVersion = '1.18.7';
35
+ const engineVersion = '1.18.8';
36
36
 
37
37
  /** Frames per second to update
38
38
  * @type {number}
@@ -14,12 +14,10 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- /** Audio context used by the engine. Created lazily in audioInit() to avoid
18
- * browser autoplay warnings about constructing an AudioContext before any
19
- * user gesture.
17
+ /** Audio context used by the engine
20
18
  * @type {AudioContext}
21
19
  * @memberof Audio */
22
- let audioContext;
20
+ let audioContext = new AudioContext;
23
21
 
24
22
  /** Master gain node for all audio to pass through
25
23
  * @type {GainNode}
@@ -35,13 +33,12 @@ const audioDefaultSampleRate = 44100;
35
33
  * @return {boolean} - True if the audio context is running
36
34
  * @memberof Audio */
37
35
  function audioIsRunning()
38
- { return audioContext?.state === 'running'; }
36
+ { return audioContext.state === 'running'; }
39
37
 
40
38
  function audioInit()
41
39
  {
42
40
  if (!soundEnable || headlessMode) return;
43
41
 
44
- audioContext = new AudioContext;
45
42
  audioMasterGain = audioContext.createGain();
46
43
  audioMasterGain.connect(audioContext.destination);
47
44
  audioMasterGain.gain.value = soundVolume; // set starting value
@@ -87,7 +84,7 @@ class Sound
87
84
  {
88
85
  if (!soundEnable || headlessMode) return;
89
86
 
90
- ASSERT(!asset || isArray(asset) || isString(asset), 'asset must be a file name or zzfx array');
87
+ ASSERT(!asset || isArray(asset) || isStringLike(asset), 'asset must be a file name or zzfx array');
91
88
  ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
92
89
  ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
93
90
  ASSERT(isNumber(range), 'range must be a number');
@@ -79,7 +79,7 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, s
79
79
  {
80
80
  ASSERT(isVector2(pos), 'pos must be a vec2');
81
81
  ASSERT(isVector2(size), 'size must be a vec2');
82
- ASSERT(isString(color) || isColor(color), 'color is invalid');
82
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
83
83
  ASSERT(isNumber(time), 'time must be a number');
84
84
  ASSERT(isNumber(angle), 'angle must be a number');
85
85
 
@@ -106,7 +106,7 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false, screen
106
106
  {
107
107
  ASSERT(isVector2(pos), 'pos must be a vec2');
108
108
  ASSERT(isArray(points), 'points must be an array');
109
- ASSERT(isString(color) || isColor(color), 'color is invalid');
109
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
110
110
  ASSERT(isNumber(time), 'time must be a number');
111
111
  ASSERT(isNumber(angle), 'angle must be a number');
112
112
 
@@ -130,7 +130,7 @@ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false, screenSpace=f
130
130
  {
131
131
  ASSERT(isVector2(pos), 'pos must be a vec2');
132
132
  ASSERT(isNumber(size), 'size must be a number');
133
- ASSERT(isString(color) || isColor(color), 'color is invalid');
133
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
134
134
  ASSERT(isNumber(time), 'time must be a number');
135
135
 
136
136
  if (isColor(color))
@@ -208,13 +208,13 @@ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
208
208
  * @memberof Debug */
209
209
  function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace', screenSpace=false)
210
210
  {
211
- ASSERT(isString(text), 'text must be a string');
211
+ ASSERT(isStringLike(text), 'text must be a string');
212
212
  ASSERT(isVector2(pos), 'pos must be a vec2');
213
213
  ASSERT(isNumber(size), 'size must be a number');
214
- ASSERT(isString(color) || isColor(color), 'color is invalid');
214
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
215
215
  ASSERT(isNumber(time), 'time must be a number');
216
216
  ASSERT(isNumber(angle), 'angle must be a number');
217
- ASSERT(isString(font), 'font must be a string');
217
+ ASSERT(isStringLike(font), 'font must be a string');
218
218
 
219
219
  if (isColor(color))
220
220
  color = color.toString();
package/src/engineDraw.js CHANGED
@@ -810,15 +810,15 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
810
810
  * @memberof Draw */
811
811
  function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
812
812
  {
813
- ASSERT(isString(text), 'text must be a string');
813
+ ASSERT(isStringLike(text), 'text must be a string');
814
814
  ASSERT(isVector2(pos), 'pos must be a vec2');
815
815
  ASSERT(isNumber(size), 'size must be a number');
816
816
  ASSERT(isColor(color), 'color must be a color');
817
817
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
818
818
  ASSERT(isColor(lineColor), 'lineColor must be a color');
819
819
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
820
- ASSERT(isString(font), 'font must be a string');
821
- ASSERT(isString(fontStyle), 'fontStyle must be a string');
820
+ ASSERT(isStringLike(font), 'font must be a string');
821
+ ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
822
822
  ASSERT(isNumber(angle), 'angle must be a number');
823
823
 
824
824
  context.fillStyle = color.toString();
@@ -855,7 +855,7 @@ async function loadTexture(textureIndex, src)
855
855
  {
856
856
  ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
857
857
  ASSERT(!textureInfos[textureIndex], 'textureIndex is already loaded!');
858
- ASSERT(!src || isString(src), 'image src must be a string');
858
+ ASSERT(!src || isStringLike(src), 'image src must be a string');
859
859
 
860
860
  const image = new Image;
861
861
  if (src)
@@ -1243,7 +1243,7 @@ class FontImage
1243
1243
  */
1244
1244
  drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
1245
1245
  {
1246
- ASSERT(isString(text), 'text must be a string');
1246
+ ASSERT(isStringLike(text), 'text must be a string');
1247
1247
  ASSERT(isVector2(pos), 'pos must be a vec2');
1248
1248
  ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
1249
1249
  ASSERT(isColor(color), 'color must be a color');
@@ -208,7 +208,7 @@ export
208
208
  isColor,
209
209
  isVector2,
210
210
  isNumber,
211
- isString,
211
+ isStringLike,
212
212
  isArray,
213
213
 
214
214
  // Default Colors
@@ -103,7 +103,7 @@ function inputClear()
103
103
  * @memberof Input */
104
104
  function keyIsDown(key, device=0)
105
105
  {
106
- ASSERT(isString(key), 'key must be a number or string');
106
+ ASSERT(isStringLike(key), 'key must be a number or string');
107
107
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
108
108
  return !!(inputData[device]?.[key] & 1);
109
109
  }
@@ -115,7 +115,7 @@ function keyIsDown(key, device=0)
115
115
  * @memberof Input */
116
116
  function keyWasPressed(key, device=0)
117
117
  {
118
- ASSERT(isString(key), 'key must be a number or string');
118
+ ASSERT(isStringLike(key), 'key must be a number or string');
119
119
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
120
120
  return !!(inputData[device]?.[key] & 2);
121
121
  }
@@ -127,7 +127,7 @@ function keyWasPressed(key, device=0)
127
127
  * @memberof Input */
128
128
  function keyWasReleased(key, device=0)
129
129
  {
130
- ASSERT(isString(key), 'key must be a number or string');
130
+ ASSERT(isStringLike(key), 'key must be a number or string');
131
131
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
132
132
  return !!(inputData[device]?.[key] & 4);
133
133
  }
@@ -141,10 +141,10 @@ function keyWasReleased(key, device=0)
141
141
  * @memberof Input */
142
142
  function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
143
143
  {
144
- ASSERT(isString(up), 'up key must be a string');
145
- ASSERT(isString(down), 'down key must be a string');
146
- ASSERT(isString(left), 'left key must be a string');
147
- ASSERT(isString(right), 'right key must be a string');
144
+ ASSERT(isStringLike(up), 'up key must be a string');
145
+ ASSERT(isStringLike(down), 'down key must be a string');
146
+ ASSERT(isStringLike(left), 'left key must be a string');
147
+ ASSERT(isStringLike(right), 'right key must be a string');
148
148
  const k = (key)=> keyIsDown(key) ? 1 : 0;
149
149
  return vec2(k(right) - k(left), k(up) - k(down));
150
150
  }
package/src/engineMath.js CHANGED
@@ -292,13 +292,15 @@ function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
292
292
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
293
293
 
294
294
  /**
295
- * Check if object can be converted to a string (has a toString method)
295
+ * Check if a value is stringifiable i.e. it has a toString that returns
296
+ * a string. Use this for ASSERTs and inputs that will be coerced to text;
297
+ * use `typeof x === 'string'` inline if you need strict-string semantics.
296
298
  * - Returns true for strings, numbers, and most objects
297
299
  * - Returns false for null and undefined
298
300
  * @param {any} s
299
301
  * @return {boolean}
300
302
  * @memberof Math */
301
- function isString(s) { return s != null && typeof s?.toString() === 'string'; }
303
+ function isStringLike(s) { return s != null && typeof s?.toString() === 'string'; }
302
304
 
303
305
  /**
304
306
  * Check if object is an array
@@ -1042,7 +1044,7 @@ class Color
1042
1044
  * @return {Color} */
1043
1045
  setHex(hex)
1044
1046
  {
1045
- ASSERT(isString(hex), 'Color hex code must be a string');
1047
+ ASSERT(isStringLike(hex), 'Color hex code must be a string');
1046
1048
  ASSERT(hex[0] === '#', 'Color hex code must start with #');
1047
1049
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
1048
1050