littlejsengine 1.15.2 → 1.15.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.
Files changed (42) hide show
  1. package/dist/littlejs.d.ts +190 -63
  2. package/dist/littlejs.esm.js +1267 -605
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +1261 -604
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +1179 -552
  7. package/examples/electron/index.html +2 -2
  8. package/examples/index.html +4 -3
  9. package/examples/particles/index.html +22 -6
  10. package/examples/shorts/base.html +2 -1
  11. package/examples/shorts/box2dCar.js +1 -1
  12. package/examples/shorts/box2dPool.js +9 -10
  13. package/examples/shorts/empty.js +1 -1
  14. package/examples/shorts/input.js +30 -25
  15. package/examples/shorts/music.js +1 -0
  16. package/examples/shorts/musicPlayer.js +1 -0
  17. package/examples/shorts/sequencer.js +2 -0
  18. package/examples/shorts/sound.js +1 -0
  19. package/examples/shorts/tileLayer.js +2 -2
  20. package/examples/shorts/tiles.png +0 -0
  21. package/examples/shorts/timers.js +1 -0
  22. package/examples/shorts/uiSystem.js +26 -14
  23. package/examples/shorts/videoPlayer.js +2 -1
  24. package/examples/starter/index.html +3 -2
  25. package/examples/uiSystem/game.js +28 -15
  26. package/package.json +1 -1
  27. package/plugins/box2d.js +1 -1
  28. package/plugins/desktop.ini +2 -0
  29. package/plugins/pluginExport.js +2 -0
  30. package/plugins/uiSystem.js +503 -91
  31. package/src/engine.js +2 -169
  32. package/src/engineAudio.js +0 -1
  33. package/src/engineBuild.js +1 -0
  34. package/src/engineDebug.js +84 -54
  35. package/src/engineDraw.js +81 -40
  36. package/src/engineExport.js +4 -1
  37. package/src/engineInput.js +526 -381
  38. package/src/engineObject.js +27 -23
  39. package/src/engineSettings.js +11 -6
  40. package/src/engineSplash.js +173 -0
  41. package/src/engineTileLayer.js +3 -3
  42. package/src/engineUtilities.js +17 -1
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * LittleJS User Interface Plugin
3
3
  * - call new UISystemPlugin() to setup the UI system
4
+ * - Gamepad and keyboard navigation support
4
5
  * - Nested Menus
5
6
  * - Text
6
7
  * - Buttons
7
8
  * - Checkboxes
8
9
  * - Images
10
+ * - Scrollbars
11
+ * - Video
9
12
  * @namespace UISystem
10
13
  */
11
14
 
@@ -18,6 +21,20 @@
18
21
  * @memberof UISystem */
19
22
  let uiSystem;
20
23
 
24
+ /** Enable UI system debug drawing
25
+ * 0=off, 1=normal, 2=show invisible
26
+ * @type {number}
27
+ * @default
28
+ * @memberof UISystem */
29
+ let uiDebug = 0;
30
+
31
+ /** Enable UI system debug drawing
32
+ * 0=off, 1=normal, 2=show invisible
33
+ * @param {number|boolean} enable
34
+ * @memberof UISystem */
35
+ function uiSetDebug(debugMode)
36
+ { uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
37
+
21
38
  ///////////////////////////////////////////////////////////////////////////////
22
39
  /**
23
40
  * UI System Global Object
@@ -36,6 +53,7 @@ class UISystemPlugin
36
53
  ASSERT(!uiSystem, 'UI system already initialized');
37
54
  uiSystem = this;
38
55
 
56
+ // default settings
39
57
  /** @property {Color} - Default fill color for UI elements */
40
58
  this.defaultColor = WHITE;
41
59
  /** @property {Color} - Default outline color for UI elements */
@@ -55,7 +73,7 @@ class UISystemPlugin
55
73
  /** @property {number} - Default rounded rect corner radius for UI elements */
56
74
  this.defaultCornerRadius = 0;
57
75
  /** @property {number} - Default scale to use for fitting text to object */
58
- this.defaultTextScale = .8;
76
+ this.defaultTextFitScale = .8;
59
77
  /** @property {string} - Default font for UI elements */
60
78
  this.defaultFont = fontDefault;
61
79
  /** @property {Sound} - Default sound when interactive UI element is pressed */
@@ -64,6 +82,28 @@ class UISystemPlugin
64
82
  this.defaultSoundRelease = undefined;
65
83
  /** @property {Sound} - Default sound when interactive UI element is clicked */
66
84
  this.defaultSoundClick = undefined;
85
+ /** @property {Color} - Color for shadow */
86
+ this.defaultShadowColor = CLEAR_BLACK;
87
+ /** @property {number} - Size of shadow blur */
88
+ this.defaultShadowBlur = 5;
89
+ /** @property {Vector2} - Offset of shadow blur */
90
+ this.defaultShadowOffset = vec2(5);
91
+ /** @property {number} - If set ui coords will be renormalized to this canvas height */
92
+ this.nativeHeight = 0;
93
+
94
+ // navigation properties
95
+ /** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
96
+ this.navigationObject = undefined;
97
+ /** @property {Timer} - Cooldown timer for navigation inputs */
98
+ this.navigationTimer = new Timer(undefined, true);
99
+ /** @property {number} - Time between navigation inputs in seconds */
100
+ this.navigationDelay = .2;
101
+ /** @property {boolean} - should the navigation be horizontal, vertical, or both? */
102
+ this.navigationDirection = 1;
103
+ /** @property {boolean} - True if user last used navigation instead of mouse */
104
+ this.navigationMode = false;
105
+
106
+ // system state
67
107
  /** @property {Array<UIObject>} - List of all UI elements */
68
108
  this.uiObjects = [];
69
109
  /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
@@ -74,50 +114,118 @@ class UISystemPlugin
74
114
  this.hoverObject = undefined;
75
115
  /** @property {UIObject} - Hover object at start of update */
76
116
  this.lastHoverObject = undefined;
77
- /** @property {number} - If set ui coords will be renormalized to this canvas height */
78
- this.nativeHeight = 0;
117
+ /** @property {UIObject} - Current confirm menu being shown */
118
+ this.confirmDialog = undefined;
79
119
 
80
120
  engineAddPlugin(uiUpdate, uiRender);
81
121
 
122
+ // set object position in parent space
123
+ function updateTransforms(o)
124
+ {
125
+ if (!o.parent) return;
126
+ o.pos.x = o.localPos.x + o.parent.pos.x;
127
+ o.pos.y = o.localPos.y + o.parent.pos.y;
128
+ }
129
+
82
130
  // setup recursive update and render
83
131
  // update in reverse order to detect mouse enter/leave
84
132
  function uiUpdate()
85
133
  {
86
- function updateInvisibleObject(o)
134
+ if (uiSystem.activeObject && !uiSystem.activeObject.visible)
135
+ uiSystem.activeObject = undefined;
136
+
137
+ // reset hover object at start of update
138
+ uiSystem.lastHoverObject = uiSystem.hoverObject;
139
+ uiSystem.hoverObject = undefined;
140
+
141
+ if (mouseWasPressed(0))
87
142
  {
88
- // update invisible objects
89
- for (const c of o.children)
90
- updateInvisibleObject(c);
91
- o.updateInvisible();
143
+ uiSystem.navigationMode = false;
144
+ uiSystem.navigationObject = undefined;
92
145
  }
93
- function updateObject(o)
146
+
147
+ // navigation with gamepad/keyboard
148
+ const navigableObjects = uiSystem.getNavigableObjects();
149
+ if (!navigableObjects.length)
150
+ uiSystem.navigationObject = undefined;
151
+ else
94
152
  {
95
- if (o.visible)
153
+ // unselect object if it is no longer navigable
154
+ if (!navigableObjects.includes(uiSystem.navigationObject))
155
+ uiSystem.navigationObject = undefined;
156
+
157
+ if (!isTouchDevice)
158
+ if (uiSystem.navigationMode && !uiSystem.navigationObject)
96
159
  {
97
- // set position in parent space
98
- if (o.parent)
99
- o.pos = o.localPos.add(o.parent.pos);
100
- // update in reverse order to detect mouse enter/leave
101
- for (let i=o.children.length; i--;)
102
- updateObject(o.children[i]);
103
- o.update();
160
+ // select first auto focus object
161
+ uiSystem.navigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
162
+ }
163
+
164
+ // navigate with dpad or left stick
165
+ if (!uiSystem.navigationTimer.active())
166
+ {
167
+ // navigate through list with gamepad or keyboard
168
+ const direction = sign(uiSystem.getNavigationDirection());
169
+ if (direction)
170
+ {
171
+ let newNavigationObject;
172
+ if (!uiSystem.navigationObject)
173
+ {
174
+ // use auto select object
175
+ newNavigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
176
+
177
+ if (!newNavigationObject)
178
+ {
179
+ // try first or last object
180
+ const newIndex = direction > 0 ? 0 : navigableObjects.length-1;
181
+ newNavigationObject = navigableObjects[newIndex];
182
+ }
183
+ }
184
+ else
185
+ {
186
+ const currentIndex = navigableObjects.indexOf(uiSystem.navigationObject);
187
+ const newIndex = mod(currentIndex + direction, navigableObjects.length);
188
+ newNavigationObject = navigableObjects[newIndex];
189
+ }
190
+
191
+ if (uiSystem.navigationObject !== newNavigationObject)
192
+ {
193
+ uiSystem.navigationMode = true;
194
+ uiSystem.hoverObject = undefined;
195
+ uiSystem.navigationObject = newNavigationObject;
196
+ uiSystem.navigationTimer.set(uiSystem.navigationDelay);
197
+ newNavigationObject.soundPress &&
198
+ newNavigationObject.soundPress.play();
199
+ }
200
+ }
104
201
  }
105
- else
106
- updateInvisibleObject(o);
202
+
203
+ // activate the navigation object when pressed
204
+ if (uiSystem.navigationObject)
205
+ if (uiSystem.getNavigationWasPressed())
206
+ uiSystem.navigationObject.navigatePressed();
107
207
  }
108
- // reset hover object at start of update
109
- uiSystem.lastHoverObject = uiSystem.hoverObject;
110
- uiSystem.hoverObject = undefined;
111
208
 
112
209
  // update in reverse order so topmost objects get priority
113
210
  for (let i = uiSystem.uiObjects.length; i--;)
114
211
  {
115
212
  const o = uiSystem.uiObjects[i];
116
- o.parent || updateObject(o)
213
+ o.parent || updateObject(o);
117
214
  }
118
215
 
119
216
  // remove destroyed objects
120
217
  uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
218
+
219
+ function updateObject(o)
220
+ {
221
+ if (!o.visible) return;
222
+
223
+ // update in reverse order to detect mouse enter/leave
224
+ updateTransforms(o);
225
+ for (let i=o.children.length; i--;)
226
+ updateObject(o.children[i]);
227
+ o.update();
228
+ }
121
229
  }
122
230
  function uiRender()
123
231
  {
@@ -134,15 +242,29 @@ class UISystemPlugin
134
242
 
135
243
  function renderObject(o)
136
244
  {
137
- if (!o.visible)
138
- return;
139
- if (o.parent)
140
- o.pos = o.localPos.add(o.parent.pos);
245
+ if (!o.visible) return;
246
+
247
+ // render object and children
248
+ updateTransforms(o);
141
249
  o.render();
142
250
  for (const c of o.children)
143
251
  renderObject(c);
144
252
  }
145
253
  uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
254
+
255
+ if (uiDebug > 0)
256
+ {
257
+ // debug render all objects
258
+ function renderDebug(o, visible=true)
259
+ {
260
+ visible &&= !!o.visible;
261
+ updateTransforms(o);
262
+ o.renderDebug(visible);
263
+ for (const c of o.children)
264
+ renderDebug(c, visible);
265
+ }
266
+ uiSystem.uiObjects.forEach(o=> o.parent || renderDebug(o));
267
+ }
146
268
  context.restore();
147
269
  }
148
270
  }
@@ -154,8 +276,11 @@ class UISystemPlugin
154
276
  * @param {number} [lineWidth]
155
277
  * @param {Color} [lineColor]
156
278
  * @param {number} [cornerRadius]
157
- * @param {Color} [gradientColor] */
158
- drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
279
+ * @param {Color} [gradientColor]
280
+ * @param {Color} [shadowColor]
281
+ * @param {number} [shadowBlur]
282
+ * @param {Color} [shadowOffset] */
283
+ drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
159
284
  {
160
285
  ASSERT(isVector2(pos), 'pos must be a vec2');
161
286
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -177,13 +302,23 @@ class UISystemPlugin
177
302
  }
178
303
  else
179
304
  context.fillStyle = color.toString();
305
+ if (shadowBlur || shadowOffset.x || shadowOffset.y)
306
+ if (shadowColor.a > 0)
307
+ {
308
+ // setup shadow
309
+ context.shadowColor = shadowColor.toString();
310
+ context.shadowBlur = shadowBlur;
311
+ context.shadowOffsetX = shadowOffset.x;
312
+ context.shadowOffsetY = shadowOffset.y;
313
+ }
180
314
  context.beginPath();
181
315
  if (cornerRadius && context['roundRect'])
182
316
  context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
183
317
  else
184
318
  context.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
185
319
  context.fill();
186
- if (lineWidth)
320
+ context.shadowColor = '#0000';
321
+ if (lineWidth && lineColor.a > 0)
187
322
  {
188
323
  context.strokeStyle = lineColor.toString();
189
324
  context.lineWidth = lineWidth;
@@ -218,10 +353,24 @@ class UISystemPlugin
218
353
  * @param {TileInfo} tileInfo
219
354
  * @param {Color} [color=uiSystem.defaultColor]
220
355
  * @param {number} [angle]
221
- * @param {boolean} [mirror] */
222
- drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false)
356
+ * @param {boolean} [mirror]
357
+ * @param {Color} [shadowColor]
358
+ * @param {number} [shadowBlur]
359
+ * @param {Color} [shadowOffset] */
360
+ drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
223
361
  {
224
- drawTile(pos, size, tileInfo, color, angle, mirror, CLEAR_BLACK, false, true, uiSystem.uiContext);
362
+ const context = uiSystem.uiContext;
363
+ if (shadowBlur || shadowOffset.x || shadowOffset.y)
364
+ if (shadowColor.a > 0)
365
+ {
366
+ // setup shadow
367
+ context.shadowColor = shadowColor.toString();
368
+ context.shadowBlur = shadowBlur;
369
+ context.shadowOffsetX = shadowOffset.x;
370
+ context.shadowOffsetY = shadowOffset.y;
371
+ }
372
+ drawTile(pos, size, tileInfo, color, angle, mirror, CLEAR_BLACK, false, true, context);
373
+ context.shadowColor = '#0000';
225
374
  }
226
375
 
227
376
  /** Draw text to the UI context
@@ -236,12 +385,27 @@ class UISystemPlugin
236
385
  * @param {string} [fontStyle]
237
386
  * @param {boolean} [applyMaxWidth=true]
238
387
  * @param {Vector2} [textShadow]
239
- */
240
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined)
388
+ * @param {Color} [shadowColor]
389
+ * @param {number} [shadowBlur]
390
+ * @param {Color} [shadowOffset] */
391
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
241
392
  {
242
- if (textShadow)
243
- drawTextScreen(text, pos.add(textShadow), size.y, BLACK, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
244
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
393
+ const context = uiSystem.uiContext;
394
+ if (shadowColor.a > 0)
395
+ {
396
+ if (textShadow)
397
+ drawTextScreen(text, pos.add(textShadow), size.y, shadowColor, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, context);
398
+ if (shadowBlur || shadowOffset.x || shadowOffset.y)
399
+ {
400
+ // setup shadow
401
+ context.shadowColor = shadowColor.toString();
402
+ context.shadowBlur = shadowBlur;
403
+ context.shadowOffsetX = shadowOffset.x;
404
+ context.shadowOffsetY = shadowOffset.y;
405
+ }
406
+ }
407
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, context);
408
+ context.shadowColor = '#0000';
245
409
  }
246
410
 
247
411
  /**
@@ -255,7 +419,7 @@ class UISystemPlugin
255
419
  * @param {DragAndDropCallback} [onDrop] - when a file is dropped
256
420
  * @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
257
421
  * @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
258
- * @param {DragAndDropCallback} [onDragOver] - continously when dragging over */
422
+ * @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
259
423
  setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
260
424
  {
261
425
  function setCallback(callback, listenerType)
@@ -271,8 +435,7 @@ class UISystemPlugin
271
435
 
272
436
  /** Convert a screen space position to native UI position
273
437
  * @param {Vector2} pos
274
- * @return {Vector2}
275
- */
438
+ * @return {Vector2} */
276
439
  screenToNative(pos)
277
440
  {
278
441
  if (!uiSystem.nativeHeight)
@@ -295,6 +458,157 @@ class UISystemPlugin
295
458
  for (const o of this.uiObjects)
296
459
  o.parent || o.destroy();
297
460
  this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
461
+ this.activeObject = undefined;
462
+ this.hoverObject = undefined;
463
+ this.lastHoverObject = undefined;
464
+ }
465
+
466
+ /** Get all navigable UI objects sorted by navigationIndex
467
+ * @return {Array<UIObject>} */
468
+ getNavigableObjects()
469
+ {
470
+ function getNavigableRecursive(o)
471
+ {
472
+ if (!o.visible || o.disabled)
473
+ return; // skip children if parent is invisible or disabled
474
+
475
+ if (o.isInteractive() && o.navigationIndex !== undefined)
476
+ objects.push(o);
477
+ for (let i=o.children.length; i--;)
478
+ getNavigableRecursive(o.children[i]);
479
+ }
480
+
481
+ // get all the valid navigable objects recursively
482
+ let objects = [];
483
+ for (let i = uiSystem.uiObjects.length; i--;)
484
+ {
485
+ const o = uiSystem.uiObjects[i];
486
+ if (uiSystem.confirmDialog && o !== uiSystem.confirmDialog)
487
+ continue;
488
+ o.parent || getNavigableRecursive(o);
489
+ }
490
+
491
+ // sort by navigationIndex (lower numbers first)
492
+ objects.sort((a, b)=> a.navigationIndex - b.navigationIndex);
493
+ return objects;
494
+ }
495
+
496
+ /** Get navigation direction from gamepad or keyboard
497
+ * @return {number} */
498
+ getNavigationDirection()
499
+ {
500
+ const vertical = uiSystem.navigationDirection === 1;
501
+ const both = uiSystem.navigationDirection === 2;
502
+ if (isUsingGamepad)
503
+ {
504
+ const stick = gamepadStick(0, gamepadPrimary);
505
+ const dpad = gamepadDpad(gamepadPrimary);
506
+ if (both)
507
+ return -(stick.y || dpad.y) || (stick.x || dpad.x);
508
+ return vertical ? -(stick.y || dpad.y) : (stick.x || dpad.x);
509
+ }
510
+ const up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight';
511
+ if (both)
512
+ {
513
+ return keyIsDown(up) || keyIsDown(left) ? -1 :
514
+ keyIsDown(down) || keyIsDown(right) ? 1 : 0;
515
+ }
516
+ const back = vertical ? up : left;
517
+ const forward = vertical ? down : right;
518
+ return keyIsDown(back) ? -1 : keyIsDown(forward) ? 1 : 0;
519
+ }
520
+
521
+ /** Get other axis navigation direction from gamepad or keyboard
522
+ * @return {Vector2} */
523
+ getNavigationOtherDirection()
524
+ {
525
+ if (uiSystem.navigationDirection === 2)
526
+ return 0; // other direction disabled
527
+
528
+ const vertical = uiSystem.navigationDirection === 1;
529
+ if (isUsingGamepad)
530
+ {
531
+ const stick = gamepadStick(0, gamepadPrimary);
532
+ const dpad = gamepadDpad(gamepadPrimary);
533
+ return !vertical ? (stick.y || dpad.y) : (stick.x || dpad.x);
534
+ }
535
+ const back = !vertical ? 'ArrowUp' : 'ArrowLeft';
536
+ const forward = !vertical ? 'ArrowDown' : 'ArrowRight';
537
+ return keyIsDown(back) ? -1 : keyIsDown(forward) ? 1 : 0;
538
+ }
539
+
540
+ /** Get if navigation button was pressed from gamepad or keyboard
541
+ * @return {boolean} */
542
+ getNavigationWasPressed()
543
+ {
544
+ return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
545
+ keyWasPressed('Space') || keyWasPressed('Enter');
546
+ }
547
+
548
+ /** Show a confirmation dialog with Yes/No buttons
549
+ * Centers the dialog on the screen with darkened background
550
+ * @param {string} [text] - The message to display
551
+ * @param {Function} [yesCallback] - Called when Yes is clicked
552
+ * @param {Function} [noCallback] - Called when No is clicked
553
+ * @param {Vector2} [size] - Size of the confirmation dialog
554
+ * @param {string} [exitKey] - Key that can exit the menu
555
+ * @return {UIObject} The confirmation menu object
556
+ */
557
+ showConfirmDialog(text='Are you sure?', yesCallback, noCallback, size=vec2(500,250), exitKey='Escape')
558
+ {
559
+ ASSERT(!uiSystem.confirmDialog);
560
+
561
+ const savedNavigationDirection = uiSystem.navigationDirection;
562
+
563
+ // allow both axies for navigation
564
+ uiSystem.navigationDirection = 2;
565
+
566
+ // confirm menu
567
+ const confirmMenu = new UIObject(vec2(), size);
568
+ uiSystem.confirmDialog = confirmMenu;
569
+ confirmMenu.onRender = ()=>
570
+ {
571
+ confirmMenu.pos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
572
+ const backgroundColor = hsl(0,0,0,.7);
573
+ uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
574
+ }
575
+ confirmMenu.onUpdate = ()=>
576
+ {
577
+ if (keyWasPressed(exitKey))
578
+ closeMenu();
579
+ }
580
+ confirmMenu.isMouseOverlapping = ()=> true; // always hover
581
+
582
+ // title text
583
+ const gap = 50;
584
+ const textTitle = new UIText(vec2(0,-50), vec2(size.x-gap,70), text);
585
+ confirmMenu.addChild(textTitle);
586
+
587
+ // yes button
588
+ const buttonYes = new UIButton(vec2(-80,50), vec2(120,70), 'Yes');
589
+ buttonYes.textHeight = 40;
590
+ buttonYes.navigationIndex = 1;
591
+ buttonYes.hoverColor = hsl(0,1,.5);
592
+ buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
593
+ confirmMenu.addChild(buttonYes);
594
+
595
+ // no button
596
+ const buttonNo = new UIButton(vec2(80,50), vec2(120,70), 'No');
597
+ buttonNo.textHeight = 40;
598
+ buttonNo.navigationIndex = 2;
599
+ buttonNo.navigationAutoSelect = true;
600
+ buttonNo.onClick = ()=> { closeMenu(); noCallback && noCallback(); };
601
+ confirmMenu.addChild(buttonNo);
602
+
603
+ // close menu and return to normal navigation
604
+ function closeMenu()
605
+ {
606
+ ASSERT(uiSystem.confirmDialog === confirmMenu);
607
+ confirmMenu.destroy();
608
+ uiSystem.confirmDialog = undefined;
609
+ uiSystem.navigationDirection = savedNavigationDirection;
610
+ inputClear();
611
+ }
298
612
  }
299
613
  }
300
614
 
@@ -350,7 +664,13 @@ class UIObject
350
664
  /** @property {number} - Override for text height */
351
665
  this.textHeight = undefined;
352
666
  /** @property {number} - Scale text to fit in the object */
353
- this.textScale = uiSystem.defaultTextScale;
667
+ this.textFitScale = uiSystem.defaultTextFitScale;
668
+ /** @property {Vector2} - How much to offset the text shadow or undefined */
669
+ this.textShadow = undefined;
670
+ /** @property {number} - Color for text line drawing */
671
+ this.textLineColor = uiSystem.defaultLineColor.copy();
672
+ /** @property {number} - Width for text line drawing */
673
+ this.textLineWidth = 0;
354
674
  /** @property {boolean} - Should this object be drawn */
355
675
  this.visible = true;
356
676
  /** @property {Array<UIObject>} - A list of this object's children */
@@ -371,15 +691,22 @@ class UIObject
371
691
  this.dragActivate = false;
372
692
  /** @property {boolean} - True if this can be a hover object */
373
693
  this.canBeHover = true;
374
- uiSystem.uiObjects.push(this);
694
+ /** @property {Color} - Color for shadow, undefined if no shadow */
695
+ this.shadowColor = uiSystem.defaultShadowColor?.copy();
696
+ /** @property {number} - Size of shadow blur */
697
+ this.shadowBlur = uiSystem.defaultShadowBlur;
698
+ /** @property {Vector2} - Offset of shadow blur */
699
+ this.shadowOffset = uiSystem.defaultShadowOffset?.copy();
700
+ /** @property {number} - Optional navigation order index, lower values are selected first */
701
+ this.navigationIndex = undefined;
702
+ /** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
703
+ this.navigationAutoSelect = false;
375
704
 
376
- /** @property {Vector2} - How much to offset the text shadow or undefined */
377
- this.textShadow = undefined;
705
+ uiSystem.uiObjects.push(this);
378
706
  }
379
707
 
380
708
  /** Add a child UIObject to this object
381
- * @param {UIObject} child
382
- */
709
+ * @param {UIObject} child */
383
710
  addChild(child)
384
711
  {
385
712
  ASSERT(!child.parent && !this.children.includes(child));
@@ -388,8 +715,7 @@ class UIObject
388
715
  }
389
716
 
390
717
  /** Remove a child UIObject from this object
391
- * @param {UIObject} child
392
- */
718
+ * @param {UIObject} child */
393
719
  removeChild(child)
394
720
  {
395
721
  ASSERT(child.parent === this && this.children.includes(child));
@@ -397,7 +723,6 @@ class UIObject
397
723
  child.parent = undefined;
398
724
  }
399
725
 
400
-
401
726
  /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
402
727
  destroy()
403
728
  {
@@ -413,9 +738,9 @@ class UIObject
413
738
  child.destroy();
414
739
  }
415
740
  }
741
+
416
742
  /** Check if the mouse is overlapping a box in screen space
417
- * @return {boolean} - True if overlapping
418
- */
743
+ * @return {boolean} - True if overlapping */
419
744
  isMouseOverlapping()
420
745
  {
421
746
  if (!mouseInWindow) return false;
@@ -432,11 +757,16 @@ class UIObject
432
757
  // call the custom update callback
433
758
  this.onUpdate();
434
759
 
760
+ // unset active if disabled
761
+ if (this.disabled && this == uiSystem.activeObject)
762
+ uiSystem.activeObject = undefined;
763
+
435
764
  const wasHover = uiSystem.lastHoverObject === this;
436
765
  const isActive = this.isActiveObject();
437
766
  const mouseDown = mouseIsDown(0);
438
767
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
439
768
  if (this.canBeHover)
769
+ if (!uiSystem.navigationMode) // no mouse hover in navigation mode
440
770
  if (mousePress || isActive || (!mouseDown && !isTouchDevice))
441
771
  if (!uiSystem.hoverObject && this.isMouseOverlapping())
442
772
  uiSystem.hoverObject = this;
@@ -450,8 +780,7 @@ class UIObject
450
780
  {
451
781
  if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
452
782
  this.onPress();
453
- if (this.soundPress)
454
- this.soundPress.play();
783
+ this.soundPress && this.soundPress.play();
455
784
  if (uiSystem.activeObject && !isActive)
456
785
  uiSystem.activeObject.onRelease();
457
786
  uiSystem.activeObject = this;
@@ -460,10 +789,10 @@ class UIObject
460
789
  if (!mouseDown && this.isActiveObject() && this.interactive)
461
790
  {
462
791
  this.onClick();
463
- if (this.soundClick)
464
- this.soundClick.play();
792
+ this.soundClick && this.soundClick.play();
465
793
  }
466
794
  }
795
+
467
796
  // clear mouse was pressed state even when disabled
468
797
  mousePress && inputClearKey(0,0,0,1,0);
469
798
  }
@@ -471,8 +800,7 @@ class UIObject
471
800
  if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
472
801
  {
473
802
  this.onRelease();
474
- if (this.soundRelease)
475
- this.soundRelease.play();
803
+ this.soundRelease && this.soundRelease.play();
476
804
  uiSystem.activeObject = undefined;
477
805
  }
478
806
 
@@ -484,29 +812,40 @@ class UIObject
484
812
  /** Render the object, called automatically by plugin once each frame */
485
813
  render()
486
814
  {
487
- if (!this.size.x || !this.size.y) return;
815
+ // call the custom render callback
816
+ this.onRender();
488
817
 
489
- const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
490
- const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
491
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
492
- }
818
+ if (!this.size.x || !this.size.y) return;
493
819
 
494
- /** Special update when object is not visible */
495
- updateInvisible()
496
- {
497
- // reset input state when not visible
498
- if (this.isActiveObject())
499
- uiSystem.activeObject = undefined;
820
+ const isNavigationObject = this.isNavigationObject();
821
+ const lineColor = isNavigationObject ? this.color :
822
+ this.interactive && this.isActiveObject() && !this.disabled ?
823
+ this.color : this.lineColor;
824
+ const color = isNavigationObject ? this.hoverColor :
825
+ this.disabled ? this.disabledColor :
826
+ this.interactive ?
827
+ this.isHoverObject() ? this.hoverColor :
828
+ this.isActiveObject() ? this.activeColor || this.color :
829
+ this.color : this.color;
830
+ const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
831
+
832
+ uiSystem.drawRect(this.pos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
500
833
  }
501
834
 
502
835
  /** Get the size for text with overrides and scale
503
- * @return {Vector2}
504
- */
836
+ * @return {Vector2} */
505
837
  getTextSize()
506
838
  {
507
839
  return vec2(
508
- this.textWidth || this.textScale * this.size.x,
509
- this.textHeight || this.textScale * this.size.y);
840
+ this.textWidth || this.textFitScale * this.size.x,
841
+ this.textHeight || this.textFitScale * this.size.y);
842
+ }
843
+
844
+ /** Called when the navigation button is pressed on this object */
845
+ navigatePressed()
846
+ {
847
+ this.onClick();
848
+ this.soundClick && this.soundClick.play();
510
849
  }
511
850
 
512
851
  /** @return {boolean} - Is the mouse hovering over this element */
@@ -515,9 +854,51 @@ class UIObject
515
854
  /** @return {boolean} - Is the mouse held onto this element */
516
855
  isActiveObject() { return uiSystem.activeObject === this; }
517
856
 
518
- /** Called each frame when object updates */
857
+ /** @return {boolean} - Is the gamepad or keyboard navigation object */
858
+ isNavigationObject() { return uiSystem.navigationObject === this; }
859
+
860
+ /** @return {boolean} - Can it be interacted with */
861
+ isInteractive() { return this.interactive && this.visible && !this.disabled;}
862
+
863
+ /** Returns string containing info about this object for debugging
864
+ * @return {string} */
865
+ toString()
866
+ {
867
+ if (!debug) return;
868
+
869
+ let text = 'type = ' + this.constructor.name;
870
+ if (this.text)
871
+ text += '\ntext = ' + this.text;
872
+ if (this.pos.x || this.pos.y)
873
+ text += '\npos = ' + this.pos;
874
+ if (this.localPos.x || this.localPos.y)
875
+ text += '\localPos = ' + this.localPos;
876
+ if (this.size.x || this.size.y)
877
+ text += '\nsize = ' + this.size;
878
+ if (this.color)
879
+ text += '\ncolor = ' + this.color;
880
+ return text;
881
+ }
882
+
883
+ /** Called if uiDebug is enabled
884
+ * @param {boolean} visible */
885
+ renderDebug(visible=true)
886
+ {
887
+ // apply color based on state
888
+ const color =
889
+ !visible ? GREEN :
890
+ this.isHoverObject() ? YELLOW :
891
+ this.disabled ? PURPLE :
892
+ this.interactive ? RED : BLUE;
893
+ uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);
894
+ }
895
+
896
+ /** Called each frame before object updates */
519
897
  onUpdate() {}
520
898
 
899
+ /** Called each frame before object renders */
900
+ onRender() {}
901
+
521
902
  /** Called when the mouse enters the object */
522
903
  onEnter() {}
523
904
 
@@ -565,16 +946,25 @@ class UIText extends UIObject
565
946
  this.align = align;
566
947
  this.font = font;
567
948
 
568
- // make text not outlined by default
569
- this.lineWidth = 0;
570
949
  // text can not be a hover object by default
571
950
  this.canBeHover = false;
951
+
952
+ // no background by default
953
+ this.color = CLEAR_BLACK;
954
+ this.shadowColor = CLEAR_BLACK;
955
+ this.gradientColor = undefined;
956
+ this.lineWidth = 0;
957
+
958
+ // use max fit scale by default
959
+ this.textFitScale = 1;
572
960
  }
573
961
  render()
574
962
  {
575
- // only render the text
963
+ super.render();
964
+
965
+ // render the text
576
966
  const textSize = this.getTextSize();
577
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
967
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow, this.shadowColor, this.shadowBlur, this.shadowOffset);
578
968
  }
579
969
  }
580
970
 
@@ -610,10 +1000,13 @@ class UITile extends UIObject
610
1000
  this.mirror = mirror;
611
1001
  // set properties
612
1002
  this.color = color.copy();
1003
+
1004
+ // no shadow by default
1005
+ this.shadowColor = CLEAR_BLACK;
613
1006
  }
614
1007
  render()
615
1008
  {
616
- uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror);
1009
+ uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor, this.shadowBlur, this.shadowOffset);
617
1010
  }
618
1011
  }
619
1012
 
@@ -638,6 +1031,9 @@ class UIButton extends UIObject
638
1031
  ASSERT(isString(text), 'ui button must be a string');
639
1032
  ASSERT(isColor(color), 'ui button color must be a color');
640
1033
 
1034
+ /** @property {Vector2} - Text offset for the button */
1035
+ this.textOffset = vec2();
1036
+
641
1037
  // set properties
642
1038
  this.text = text;
643
1039
  this.color = color.copy();
@@ -649,8 +1045,8 @@ class UIButton extends UIObject
649
1045
 
650
1046
  // draw the text scaled to fit
651
1047
  const textSize = this.getTextSize();
652
- uiSystem.drawText(this.text, this.pos, textSize,
653
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
1048
+ uiSystem.drawText(this.text, this.pos.add(this.textOffset), textSize,
1049
+ this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
654
1050
  }
655
1051
  }
656
1052
 
@@ -704,7 +1100,7 @@ class UICheckbox extends UIObject
704
1100
  const textSize = this.getTextSize();
705
1101
  const pos = this.pos.add(vec2(this.size.x,0));
706
1102
  uiSystem.drawText(this.text, pos, textSize,
707
- this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false, this.textShadow);
1103
+ this.textColor, this.textLineWidth, this.textLineColor, 'left', this.font, this.fontStyle, false, this.textShadow);
708
1104
  }
709
1105
  }
710
1106
 
@@ -746,7 +1142,11 @@ class UIScrollbar extends UIObject
746
1142
  update()
747
1143
  {
748
1144
  super.update();
749
- if (this.isActiveObject() && this.interactive)
1145
+ if (!this.interactive)
1146
+ return;
1147
+
1148
+ const oldValue = this.value;
1149
+ if (this.isActiveObject())
750
1150
  {
751
1151
  // handle horizontal or vertical scrollbar
752
1152
  const isHorizontal = this.size.x > this.size.y;
@@ -758,14 +1158,19 @@ class UIScrollbar extends UIObject
758
1158
  const handleWidth = barSize - handleSize;
759
1159
  const p1 = centerPos - handleWidth/2;
760
1160
  const p2 = centerPos + handleWidth/2;
761
- const oldValue = this.value;
762
-
763
1161
  const p = uiSystem.screenToNative(mousePosScreen);
764
1162
  this.value = isHorizontal ?
765
1163
  percent(p.x, p1, p2) :
766
1164
  percent(p.y, p2, p1);
767
- this.value === oldValue || this.onChange();
768
1165
  }
1166
+ else if (this.isNavigationObject())
1167
+ {
1168
+ // gamepad/keyboard navigation adjustment
1169
+ const direction = uiSystem.getNavigationOtherDirection();
1170
+ if (!uiSystem.navigationTimer.active())
1171
+ this.value = clamp(this.value + direction*.01);
1172
+ }
1173
+ this.value === oldValue || this.onChange();
769
1174
  }
770
1175
  render()
771
1176
  {
@@ -790,7 +1195,14 @@ class UIScrollbar extends UIObject
790
1195
  // draw the text scaled to fit on the scrollbar
791
1196
  const textSize = this.getTextSize();
792
1197
  uiSystem.drawText(this.text, this.pos, textSize,
793
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
1198
+ this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
1199
+ }
1200
+ navigatePressed()
1201
+ {
1202
+ // toggle value between 0 and 1
1203
+ this.value = this.value ? 0 : 1;
1204
+ this.onRelease();
1205
+ super.navigatePressed();
794
1206
  }
795
1207
  }
796
1208
 
@@ -824,7 +1236,7 @@ class UIVideo extends UIObject
824
1236
  this.color = BLACK; // default to black background
825
1237
  this.cornerRadius = 0; // default to no corner radius
826
1238
 
827
- /** @property {float} - The video volume */
1239
+ /** @property {number} - The video volume */
828
1240
  this.volume = volume;
829
1241
 
830
1242
  // create video element
@@ -857,7 +1269,7 @@ class UIVideo extends UIObject
857
1269
 
858
1270
  /** Check if video is currently loading
859
1271
  * @return {boolean} */
860
- isLoadng()
1272
+ isLoading()
861
1273
  { return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
862
1274
 
863
1275
  /** Check if video is currently paused
@@ -867,7 +1279,7 @@ class UIVideo extends UIObject
867
1279
  /** Check if video is currently playing
868
1280
  * @return {boolean} */
869
1281
  isPlaying()
870
- { return !this.isPaused() && !this.hasEnded() && !this.isLoadng(); }
1282
+ { return !this.isPaused() && !this.hasEnded() && !this.isLoading(); }
871
1283
 
872
1284
  /** Check if video has ended playing
873
1285
  * @return {boolean} */
@@ -916,7 +1328,7 @@ class UIVideo extends UIObject
916
1328
  {
917
1329
  super.render();
918
1330
 
919
- if (this.isLoadng())
1331
+ if (this.isLoading())
920
1332
  return;
921
1333
  const context = uiSystem.uiContext;
922
1334
  const s = this.size;