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.
@@ -1473,13 +1473,15 @@ declare module "littlejsengine" {
1473
1473
  * @memberof Math */
1474
1474
  export function isNumber(n: any): boolean;
1475
1475
  /**
1476
- * Check if object can be converted to a string (has a toString method)
1476
+ * Check if a value is stringifiable i.e. it has a toString that returns
1477
+ * a string. Use this for ASSERTs and inputs that will be coerced to text;
1478
+ * use `typeof x === 'string'` inline if you need strict-string semantics.
1477
1479
  * - Returns true for strings, numbers, and most objects
1478
1480
  * - Returns false for null and undefined
1479
1481
  * @param {any} s
1480
1482
  * @return {boolean}
1481
1483
  * @memberof Math */
1482
- export function isString(s: any): boolean;
1484
+ export function isStringLike(s: any): boolean;
1483
1485
  /**
1484
1486
  * Check if object is an array
1485
1487
  * @param {any} a
@@ -2301,9 +2303,7 @@ declare module "littlejsengine" {
2301
2303
  * - Web Audio API integration with master gain control
2302
2304
  * @namespace Audio
2303
2305
  */
2304
- /** Audio context used by the engine. Created lazily in audioInit() to avoid
2305
- * browser autoplay warnings about constructing an AudioContext before any
2306
- * user gesture.
2306
+ /** Audio context used by the engine
2307
2307
  * @type {AudioContext}
2308
2308
  * @memberof Audio */
2309
2309
  export let audioContext: AudioContext;
@@ -3738,6 +3738,10 @@ declare module "littlejsengine" {
3738
3738
  navigationIndex: any;
3739
3739
  /** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
3740
3740
  navigationAutoSelect: boolean;
3741
+ /** @property {Vector2} - Where on parent (or canvas if no parent) this object is anchored.
3742
+ * Components in [-1, 1]: (0,0)=center, (-1,-1)=top-left, (1,1)=bottom-right.
3743
+ * Also acts as self-pivot — e.g. (1,-1) puts your top-right corner at the anchor point. */
3744
+ anchor: Vector2;
3741
3745
  /** Add a child UIObject to this object, returns child for chaining
3742
3746
  * @param {UIObject} child
3743
3747
  * @return {UIObject} The child object added */
@@ -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}
@@ -645,7 +645,7 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, s
645
645
  {
646
646
  ASSERT(isVector2(pos), 'pos must be a vec2');
647
647
  ASSERT(isVector2(size), 'size must be a vec2');
648
- ASSERT(isString(color) || isColor(color), 'color is invalid');
648
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
649
649
  ASSERT(isNumber(time), 'time must be a number');
650
650
  ASSERT(isNumber(angle), 'angle must be a number');
651
651
 
@@ -672,7 +672,7 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false, screen
672
672
  {
673
673
  ASSERT(isVector2(pos), 'pos must be a vec2');
674
674
  ASSERT(isArray(points), 'points must be an array');
675
- ASSERT(isString(color) || isColor(color), 'color is invalid');
675
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
676
676
  ASSERT(isNumber(time), 'time must be a number');
677
677
  ASSERT(isNumber(angle), 'angle must be a number');
678
678
 
@@ -696,7 +696,7 @@ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false, screenSpace=f
696
696
  {
697
697
  ASSERT(isVector2(pos), 'pos must be a vec2');
698
698
  ASSERT(isNumber(size), 'size must be a number');
699
- ASSERT(isString(color) || isColor(color), 'color is invalid');
699
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
700
700
  ASSERT(isNumber(time), 'time must be a number');
701
701
 
702
702
  if (isColor(color))
@@ -774,13 +774,13 @@ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
774
774
  * @memberof Debug */
775
775
  function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace', screenSpace=false)
776
776
  {
777
- ASSERT(isString(text), 'text must be a string');
777
+ ASSERT(isStringLike(text), 'text must be a string');
778
778
  ASSERT(isVector2(pos), 'pos must be a vec2');
779
779
  ASSERT(isNumber(size), 'size must be a number');
780
- ASSERT(isString(color) || isColor(color), 'color is invalid');
780
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
781
781
  ASSERT(isNumber(time), 'time must be a number');
782
782
  ASSERT(isNumber(angle), 'angle must be a number');
783
- ASSERT(isString(font), 'font must be a string');
783
+ ASSERT(isStringLike(font), 'font must be a string');
784
784
 
785
785
  if (isColor(color))
786
786
  color = color.toString();
@@ -1580,13 +1580,15 @@ function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
1580
1580
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1581
1581
 
1582
1582
  /**
1583
- * Check if object can be converted to a string (has a toString method)
1583
+ * Check if a value is stringifiable i.e. it has a toString that returns
1584
+ * a string. Use this for ASSERTs and inputs that will be coerced to text;
1585
+ * use `typeof x === 'string'` inline if you need strict-string semantics.
1584
1586
  * - Returns true for strings, numbers, and most objects
1585
1587
  * - Returns false for null and undefined
1586
1588
  * @param {any} s
1587
1589
  * @return {boolean}
1588
1590
  * @memberof Math */
1589
- function isString(s) { return s != null && typeof s?.toString() === 'string'; }
1591
+ function isStringLike(s) { return s != null && typeof s?.toString() === 'string'; }
1590
1592
 
1591
1593
  /**
1592
1594
  * Check if object is an array
@@ -2330,7 +2332,7 @@ class Color
2330
2332
  * @return {Color} */
2331
2333
  setHex(hex)
2332
2334
  {
2333
- ASSERT(isString(hex), 'Color hex code must be a string');
2335
+ ASSERT(isStringLike(hex), 'Color hex code must be a string');
2334
2336
  ASSERT(hex[0] === '#', 'Color hex code must start with #');
2335
2337
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
2336
2338
 
@@ -2594,8 +2596,8 @@ function saveCanvas(canvas, filename='screenshot', type='image/png')
2594
2596
  * @memberof Utilities */
2595
2597
  function saveDataURL(url, filename='download', revokeTime)
2596
2598
  {
2597
- ASSERT(isString(url), 'saveDataURL requires url string');
2598
- ASSERT(isString(filename), 'saveDataURL requires filename string');
2599
+ ASSERT(isStringLike(url), 'saveDataURL requires url string');
2600
+ ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
2599
2601
 
2600
2602
  // create link for saving screenshots
2601
2603
  const link = document.createElement('a');
@@ -2613,8 +2615,8 @@ function saveDataURL(url, filename='download', revokeTime)
2613
2615
  * @memberof Utilities */
2614
2616
  function shareURL(title, url, callback)
2615
2617
  {
2616
- ASSERT(isString(title), 'shareURL requires title string');
2617
- ASSERT(isString(url), 'shareURL requires url string');
2618
+ ASSERT(isStringLike(title), 'shareURL requires title string');
2619
+ ASSERT(isStringLike(url), 'shareURL requires url string');
2618
2620
  navigator.share?.({title, url}).then(()=>callback?.());
2619
2621
  }
2620
2622
 
@@ -2627,7 +2629,7 @@ function shareURL(title, url, callback)
2627
2629
  * @memberof Utilities */
2628
2630
  function readSaveData(saveName, defaultSaveData)
2629
2631
  {
2630
- ASSERT(isString(saveName), 'loadData requires saveName string');
2632
+ ASSERT(isStringLike(saveName), 'loadData requires saveName string');
2631
2633
 
2632
2634
  // replace undefined values with defaults; tolerate corrupt JSON
2633
2635
  const data = localStorage[saveName];
@@ -2646,7 +2648,7 @@ function readSaveData(saveName, defaultSaveData)
2646
2648
  * @memberof Utilities */
2647
2649
  function writeSaveData(saveName, saveData)
2648
2650
  {
2649
- ASSERT(isString(saveName), 'saveData requires saveName string');
2651
+ ASSERT(isStringLike(saveName), 'saveData requires saveName string');
2650
2652
  localStorage[saveName] = JSON.stringify(saveData);
2651
2653
  }
2652
2654
  /**
@@ -4592,15 +4594,15 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
4592
4594
  * @memberof Draw */
4593
4595
  function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
4594
4596
  {
4595
- ASSERT(isString(text), 'text must be a string');
4597
+ ASSERT(isStringLike(text), 'text must be a string');
4596
4598
  ASSERT(isVector2(pos), 'pos must be a vec2');
4597
4599
  ASSERT(isNumber(size), 'size must be a number');
4598
4600
  ASSERT(isColor(color), 'color must be a color');
4599
4601
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
4600
4602
  ASSERT(isColor(lineColor), 'lineColor must be a color');
4601
4603
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
4602
- ASSERT(isString(font), 'font must be a string');
4603
- ASSERT(isString(fontStyle), 'fontStyle must be a string');
4604
+ ASSERT(isStringLike(font), 'font must be a string');
4605
+ ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
4604
4606
  ASSERT(isNumber(angle), 'angle must be a number');
4605
4607
 
4606
4608
  context.fillStyle = color.toString();
@@ -4637,7 +4639,7 @@ async function loadTexture(textureIndex, src)
4637
4639
  {
4638
4640
  ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
4639
4641
  ASSERT(!textureInfos[textureIndex], 'textureIndex is already loaded!');
4640
- ASSERT(!src || isString(src), 'image src must be a string');
4642
+ ASSERT(!src || isStringLike(src), 'image src must be a string');
4641
4643
 
4642
4644
  const image = new Image;
4643
4645
  if (src)
@@ -5025,7 +5027,7 @@ class FontImage
5025
5027
  */
5026
5028
  drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
5027
5029
  {
5028
- ASSERT(isString(text), 'text must be a string');
5030
+ ASSERT(isStringLike(text), 'text must be a string');
5029
5031
  ASSERT(isVector2(pos), 'pos must be a vec2');
5030
5032
  ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
5031
5033
  ASSERT(isColor(color), 'color must be a color');
@@ -5186,7 +5188,7 @@ function inputClear()
5186
5188
  * @memberof Input */
5187
5189
  function keyIsDown(key, device=0)
5188
5190
  {
5189
- ASSERT(isString(key), 'key must be a number or string');
5191
+ ASSERT(isStringLike(key), 'key must be a number or string');
5190
5192
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
5191
5193
  return !!(inputData[device]?.[key] & 1);
5192
5194
  }
@@ -5198,7 +5200,7 @@ function keyIsDown(key, device=0)
5198
5200
  * @memberof Input */
5199
5201
  function keyWasPressed(key, device=0)
5200
5202
  {
5201
- ASSERT(isString(key), 'key must be a number or string');
5203
+ ASSERT(isStringLike(key), 'key must be a number or string');
5202
5204
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
5203
5205
  return !!(inputData[device]?.[key] & 2);
5204
5206
  }
@@ -5210,7 +5212,7 @@ function keyWasPressed(key, device=0)
5210
5212
  * @memberof Input */
5211
5213
  function keyWasReleased(key, device=0)
5212
5214
  {
5213
- ASSERT(isString(key), 'key must be a number or string');
5215
+ ASSERT(isStringLike(key), 'key must be a number or string');
5214
5216
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
5215
5217
  return !!(inputData[device]?.[key] & 4);
5216
5218
  }
@@ -5224,10 +5226,10 @@ function keyWasReleased(key, device=0)
5224
5226
  * @memberof Input */
5225
5227
  function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
5226
5228
  {
5227
- ASSERT(isString(up), 'up key must be a string');
5228
- ASSERT(isString(down), 'down key must be a string');
5229
- ASSERT(isString(left), 'left key must be a string');
5230
- ASSERT(isString(right), 'right key must be a string');
5229
+ ASSERT(isStringLike(up), 'up key must be a string');
5230
+ ASSERT(isStringLike(down), 'down key must be a string');
5231
+ ASSERT(isStringLike(left), 'left key must be a string');
5232
+ ASSERT(isStringLike(right), 'right key must be a string');
5231
5233
  const k = (key)=> keyIsDown(key) ? 1 : 0;
5232
5234
  return vec2(k(right) - k(left), k(up) - k(down));
5233
5235
  }
@@ -5945,12 +5947,10 @@ function touchGamepadButtonCenter()
5945
5947
  * @namespace Audio
5946
5948
  */
5947
5949
 
5948
- /** Audio context used by the engine. Created lazily in audioInit() to avoid
5949
- * browser autoplay warnings about constructing an AudioContext before any
5950
- * user gesture.
5950
+ /** Audio context used by the engine
5951
5951
  * @type {AudioContext}
5952
5952
  * @memberof Audio */
5953
- let audioContext;
5953
+ let audioContext = new AudioContext;
5954
5954
 
5955
5955
  /** Master gain node for all audio to pass through
5956
5956
  * @type {GainNode}
@@ -5966,13 +5966,12 @@ const audioDefaultSampleRate = 44100;
5966
5966
  * @return {boolean} - True if the audio context is running
5967
5967
  * @memberof Audio */
5968
5968
  function audioIsRunning()
5969
- { return audioContext?.state === 'running'; }
5969
+ { return audioContext.state === 'running'; }
5970
5970
 
5971
5971
  function audioInit()
5972
5972
  {
5973
5973
  if (!soundEnable || headlessMode) return;
5974
5974
 
5975
- audioContext = new AudioContext;
5976
5975
  audioMasterGain = audioContext.createGain();
5977
5976
  audioMasterGain.connect(audioContext.destination);
5978
5977
  audioMasterGain.gain.value = soundVolume; // set starting value
@@ -6018,7 +6017,7 @@ class Sound
6018
6017
  {
6019
6018
  if (!soundEnable || headlessMode) return;
6020
6019
 
6021
- ASSERT(!asset || isArray(asset) || isString(asset), 'asset must be a file name or zzfx array');
6020
+ ASSERT(!asset || isArray(asset) || isStringLike(asset), 'asset must be a file name or zzfx array');
6022
6021
  ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
6023
6022
  ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
6024
6023
  ASSERT(isNumber(range), 'range must be a number');
@@ -9777,12 +9776,31 @@ class UISystemPlugin
9777
9776
 
9778
9777
  engineAddPlugin(uiUpdate, uiRender);
9779
9778
 
9780
- // set object position in parent space
9779
+ // set object position based on anchor target (parent box, or canvas for roots),
9780
+ // self-pivot, and localPos offset
9781
9781
  function updateTransforms(o)
9782
9782
  {
9783
- if (!o.parent) return;
9784
- o.pos.x = o.localPos.x + o.parent.pos.x;
9785
- o.pos.y = o.localPos.y + o.parent.pos.y;
9783
+ let targetPos, targetSize;
9784
+ if (o.parent)
9785
+ {
9786
+ targetPos = o.parent.pos;
9787
+ targetSize = o.parent.size;
9788
+ }
9789
+ else
9790
+ {
9791
+ // anchor to canvas in native coords (handles nativeHeight if set)
9792
+ targetPos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
9793
+ targetSize = uiSystem.nativeHeight
9794
+ ? vec2(mainCanvasSize.x * uiSystem.nativeHeight / mainCanvasSize.y,
9795
+ uiSystem.nativeHeight)
9796
+ : mainCanvasSize;
9797
+ }
9798
+
9799
+ const a = o.anchor;
9800
+ o.pos = targetPos
9801
+ .add(targetSize.multiply(a).scale(.5)) // anchor point on target
9802
+ .subtract(o.size.multiply(a).scale(.5)) // pivot shift on self
9803
+ .add(o.localPos); // user offset
9786
9804
  }
9787
9805
 
9788
9806
  // setup recursive update and render
@@ -10249,9 +10267,8 @@ class UISystemPlugin
10249
10267
  // confirm menu
10250
10268
  const confirmMenu = new UIObject(vec2(), size);
10251
10269
  uiSystem.confirmDialog = confirmMenu;
10252
- confirmMenu.onRender = ()=>
10270
+ confirmMenu.onRender = ()=>
10253
10271
  {
10254
- confirmMenu.pos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
10255
10272
  const backgroundColor = hsl(0,0,0,.7);
10256
10273
  uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
10257
10274
  }
@@ -10384,7 +10401,11 @@ class UIObject
10384
10401
  this.navigationIndex = undefined;
10385
10402
  /** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
10386
10403
  this.navigationAutoSelect = false;
10387
-
10404
+ /** @property {Vector2} - Where on parent (or canvas if no parent) this object is anchored.
10405
+ * Components in [-1, 1]: (0,0)=center, (-1,-1)=top-left, (1,1)=bottom-right.
10406
+ * Also acts as self-pivot — e.g. (1,-1) puts your top-right corner at the anchor point. */
10407
+ this.anchor = vec2();
10408
+
10388
10409
  uiSystem.uiObjects.push(this);
10389
10410
  }
10390
10411
 
@@ -10637,9 +10658,9 @@ class UIText extends UIObject
10637
10658
  {
10638
10659
  super(pos, size);
10639
10660
 
10640
- ASSERT(isString(text), 'ui text must be a string');
10661
+ ASSERT(isStringLike(text), 'ui text must be a string');
10641
10662
  ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
10642
- ASSERT(isString(font), 'ui text font must be a string');
10663
+ ASSERT(isStringLike(font), 'ui text font must be a string');
10643
10664
 
10644
10665
  // set properties
10645
10666
  this.text = text;
@@ -10687,7 +10708,7 @@ class UITextInput extends UIObject
10687
10708
  {
10688
10709
  super(pos, size);
10689
10710
 
10690
- ASSERT(isString(text), 'ui text must be a string');
10711
+ ASSERT(isStringLike(text), 'ui text must be a string');
10691
10712
 
10692
10713
  /** @property {number} - Max length of input (0 = no limit) */
10693
10714
  this.maxLength = 0;
@@ -10824,7 +10845,7 @@ class UIButton extends UIObject
10824
10845
  {
10825
10846
  super(pos, size);
10826
10847
 
10827
- ASSERT(isString(text), 'ui button must be a string');
10848
+ ASSERT(isStringLike(text), 'ui button must be a string');
10828
10849
  ASSERT(isColor(color), 'ui button color must be a color');
10829
10850
 
10830
10851
  /** @property {Vector2} - Text offset for the button */
@@ -10865,7 +10886,7 @@ class UICheckbox extends UIObject
10865
10886
  {
10866
10887
  super(pos, size);
10867
10888
 
10868
- ASSERT(isString(text), 'ui checkbox must be a string');
10889
+ ASSERT(isStringLike(text), 'ui checkbox must be a string');
10869
10890
  ASSERT(isColor(color), 'ui checkbox color must be a color');
10870
10891
 
10871
10892
  /** @property {boolean} - Current percentage value of this slider 0-1 */
@@ -10922,7 +10943,7 @@ class UISlider extends UIObject
10922
10943
  super(pos, size);
10923
10944
 
10924
10945
  ASSERT(isNumber(value), 'ui slider value must be a number');
10925
- ASSERT(isString(text), 'ui slider must be a string');
10946
+ ASSERT(isStringLike(text), 'ui slider must be a string');
10926
10947
  ASSERT(isColor(color), 'ui slider color must be a color');
10927
10948
  ASSERT(isColor(handleColor), 'ui slider handleColor must be a color');
10928
10949
 
@@ -11040,7 +11061,7 @@ class UIVideo extends UIObject
11040
11061
  {
11041
11062
  super(pos, size || vec2());
11042
11063
 
11043
- ASSERT(isString(src), 'video src must be a string');
11064
+ ASSERT(isStringLike(src), 'video src must be a string');
11044
11065
  ASSERT(isNumber(volume), 'video volume must be a number');
11045
11066
 
11046
11067
  this.color = BLACK; // default to black background
@@ -13890,7 +13911,7 @@ const Ease =
13890
13911
  function tweenProperty(target, propertyPath, start, end, duration = 1, options = {})
13891
13912
  {
13892
13913
  ASSERT(target != null && typeof target === 'object', 'tweenProperty target must be an object');
13893
- ASSERT(isString(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
13914
+ ASSERT(isStringLike(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
13894
13915
 
13895
13916
  const parts = propertyPath.split('.');
13896
13917
  const lastKey = parts.pop();
@@ -14957,7 +14978,7 @@ export
14957
14978
  isColor,
14958
14979
  isVector2,
14959
14980
  isNumber,
14960
- isString,
14981
+ isStringLike,
14961
14982
  isArray,
14962
14983
 
14963
14984
  // Default Colors