littlejsengine 1.15.3 → 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.
@@ -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
@@ -56,7 +73,7 @@ class UISystemPlugin
56
73
  /** @property {number} - Default rounded rect corner radius for UI elements */
57
74
  this.defaultCornerRadius = 0;
58
75
  /** @property {number} - Default scale to use for fitting text to object */
59
- this.defaultTextScale = .8;
76
+ this.defaultTextFitScale = .8;
60
77
  /** @property {string} - Default font for UI elements */
61
78
  this.defaultFont = fontDefault;
62
79
  /** @property {Sound} - Default sound when interactive UI element is pressed */
@@ -71,6 +88,21 @@ class UISystemPlugin
71
88
  this.defaultShadowBlur = 5;
72
89
  /** @property {Vector2} - Offset of shadow blur */
73
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
+
74
106
  // system state
75
107
  /** @property {Array<UIObject>} - List of all UI elements */
76
108
  this.uiObjects = [];
@@ -82,50 +114,118 @@ class UISystemPlugin
82
114
  this.hoverObject = undefined;
83
115
  /** @property {UIObject} - Hover object at start of update */
84
116
  this.lastHoverObject = undefined;
85
- /** @property {number} - If set ui coords will be renormalized to this canvas height */
86
- this.nativeHeight = 0;
117
+ /** @property {UIObject} - Current confirm menu being shown */
118
+ this.confirmDialog = undefined;
87
119
 
88
120
  engineAddPlugin(uiUpdate, uiRender);
89
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
+
90
130
  // setup recursive update and render
91
131
  // update in reverse order to detect mouse enter/leave
92
132
  function uiUpdate()
93
133
  {
94
- 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))
95
142
  {
96
- // update invisible objects
97
- for (const c of o.children)
98
- updateInvisibleObject(c);
99
- o.updateInvisible();
143
+ uiSystem.navigationMode = false;
144
+ uiSystem.navigationObject = undefined;
100
145
  }
101
- function updateObject(o)
146
+
147
+ // navigation with gamepad/keyboard
148
+ const navigableObjects = uiSystem.getNavigableObjects();
149
+ if (!navigableObjects.length)
150
+ uiSystem.navigationObject = undefined;
151
+ else
102
152
  {
103
- 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)
104
159
  {
105
- // set position in parent space
106
- if (o.parent)
107
- o.pos = o.localPos.add(o.parent.pos);
108
- // update in reverse order to detect mouse enter/leave
109
- for (let i=o.children.length; i--;)
110
- updateObject(o.children[i]);
111
- 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
+ }
112
201
  }
113
- else
114
- updateInvisibleObject(o);
202
+
203
+ // activate the navigation object when pressed
204
+ if (uiSystem.navigationObject)
205
+ if (uiSystem.getNavigationWasPressed())
206
+ uiSystem.navigationObject.navigatePressed();
115
207
  }
116
- // reset hover object at start of update
117
- uiSystem.lastHoverObject = uiSystem.hoverObject;
118
- uiSystem.hoverObject = undefined;
119
208
 
120
209
  // update in reverse order so topmost objects get priority
121
210
  for (let i = uiSystem.uiObjects.length; i--;)
122
211
  {
123
212
  const o = uiSystem.uiObjects[i];
124
- o.parent || updateObject(o)
213
+ o.parent || updateObject(o);
125
214
  }
126
215
 
127
216
  // remove destroyed objects
128
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
+ }
129
229
  }
130
230
  function uiRender()
131
231
  {
@@ -142,15 +242,29 @@ class UISystemPlugin
142
242
 
143
243
  function renderObject(o)
144
244
  {
145
- if (!o.visible)
146
- return;
147
- if (o.parent)
148
- o.pos = o.localPos.add(o.parent.pos);
245
+ if (!o.visible) return;
246
+
247
+ // render object and children
248
+ updateTransforms(o);
149
249
  o.render();
150
250
  for (const c of o.children)
151
251
  renderObject(c);
152
252
  }
153
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
+ }
154
268
  context.restore();
155
269
  }
156
270
  }
@@ -203,8 +317,8 @@ class UISystemPlugin
203
317
  else
204
318
  context.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
205
319
  context.fill();
206
- context.shadowColor = '#0000'
207
- if (lineWidth)
320
+ context.shadowColor = '#0000';
321
+ if (lineWidth && lineColor.a > 0)
208
322
  {
209
323
  context.strokeStyle = lineColor.toString();
210
324
  context.lineWidth = lineWidth;
@@ -239,10 +353,24 @@ class UISystemPlugin
239
353
  * @param {TileInfo} tileInfo
240
354
  * @param {Color} [color=uiSystem.defaultColor]
241
355
  * @param {number} [angle]
242
- * @param {boolean} [mirror] */
243
- 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())
244
361
  {
245
- 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';
246
374
  }
247
375
 
248
376
  /** Draw text to the UI context
@@ -257,12 +385,27 @@ class UISystemPlugin
257
385
  * @param {string} [fontStyle]
258
386
  * @param {boolean} [applyMaxWidth=true]
259
387
  * @param {Vector2} [textShadow]
260
- */
261
- 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())
262
392
  {
263
- if (textShadow)
264
- drawTextScreen(text, pos.add(textShadow), size.y, BLACK, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
265
- 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';
266
409
  }
267
410
 
268
411
  /**
@@ -276,7 +419,7 @@ class UISystemPlugin
276
419
  * @param {DragAndDropCallback} [onDrop] - when a file is dropped
277
420
  * @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
278
421
  * @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
279
- * @param {DragAndDropCallback} [onDragOver] - continously when dragging over */
422
+ * @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
280
423
  setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
281
424
  {
282
425
  function setCallback(callback, listenerType)
@@ -292,8 +435,7 @@ class UISystemPlugin
292
435
 
293
436
  /** Convert a screen space position to native UI position
294
437
  * @param {Vector2} pos
295
- * @return {Vector2}
296
- */
438
+ * @return {Vector2} */
297
439
  screenToNative(pos)
298
440
  {
299
441
  if (!uiSystem.nativeHeight)
@@ -316,6 +458,157 @@ class UISystemPlugin
316
458
  for (const o of this.uiObjects)
317
459
  o.parent || o.destroy();
318
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
+ }
319
612
  }
320
613
  }
321
614
 
@@ -371,7 +664,13 @@ class UIObject
371
664
  /** @property {number} - Override for text height */
372
665
  this.textHeight = undefined;
373
666
  /** @property {number} - Scale text to fit in the object */
374
- 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;
375
674
  /** @property {boolean} - Should this object be drawn */
376
675
  this.visible = true;
377
676
  /** @property {Array<UIObject>} - A list of this object's children */
@@ -397,16 +696,17 @@ class UIObject
397
696
  /** @property {number} - Size of shadow blur */
398
697
  this.shadowBlur = uiSystem.defaultShadowBlur;
399
698
  /** @property {Vector2} - Offset of shadow blur */
400
- this.shadowOffset = uiSystem.defaultShadowOffset.copy();
401
- uiSystem.uiObjects.push(this);
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;
402
704
 
403
- /** @property {Vector2} - How much to offset the text shadow or undefined */
404
- this.textShadow = undefined;
705
+ uiSystem.uiObjects.push(this);
405
706
  }
406
707
 
407
708
  /** Add a child UIObject to this object
408
- * @param {UIObject} child
409
- */
709
+ * @param {UIObject} child */
410
710
  addChild(child)
411
711
  {
412
712
  ASSERT(!child.parent && !this.children.includes(child));
@@ -415,8 +715,7 @@ class UIObject
415
715
  }
416
716
 
417
717
  /** Remove a child UIObject from this object
418
- * @param {UIObject} child
419
- */
718
+ * @param {UIObject} child */
420
719
  removeChild(child)
421
720
  {
422
721
  ASSERT(child.parent === this && this.children.includes(child));
@@ -424,7 +723,6 @@ class UIObject
424
723
  child.parent = undefined;
425
724
  }
426
725
 
427
-
428
726
  /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
429
727
  destroy()
430
728
  {
@@ -440,9 +738,9 @@ class UIObject
440
738
  child.destroy();
441
739
  }
442
740
  }
741
+
443
742
  /** Check if the mouse is overlapping a box in screen space
444
- * @return {boolean} - True if overlapping
445
- */
743
+ * @return {boolean} - True if overlapping */
446
744
  isMouseOverlapping()
447
745
  {
448
746
  if (!mouseInWindow) return false;
@@ -459,11 +757,16 @@ class UIObject
459
757
  // call the custom update callback
460
758
  this.onUpdate();
461
759
 
760
+ // unset active if disabled
761
+ if (this.disabled && this == uiSystem.activeObject)
762
+ uiSystem.activeObject = undefined;
763
+
462
764
  const wasHover = uiSystem.lastHoverObject === this;
463
765
  const isActive = this.isActiveObject();
464
766
  const mouseDown = mouseIsDown(0);
465
767
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
466
768
  if (this.canBeHover)
769
+ if (!uiSystem.navigationMode) // no mouse hover in navigation mode
467
770
  if (mousePress || isActive || (!mouseDown && !isTouchDevice))
468
771
  if (!uiSystem.hoverObject && this.isMouseOverlapping())
469
772
  uiSystem.hoverObject = this;
@@ -477,8 +780,7 @@ class UIObject
477
780
  {
478
781
  if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
479
782
  this.onPress();
480
- if (this.soundPress)
481
- this.soundPress.play();
783
+ this.soundPress && this.soundPress.play();
482
784
  if (uiSystem.activeObject && !isActive)
483
785
  uiSystem.activeObject.onRelease();
484
786
  uiSystem.activeObject = this;
@@ -487,10 +789,10 @@ class UIObject
487
789
  if (!mouseDown && this.isActiveObject() && this.interactive)
488
790
  {
489
791
  this.onClick();
490
- if (this.soundClick)
491
- this.soundClick.play();
792
+ this.soundClick && this.soundClick.play();
492
793
  }
493
794
  }
795
+
494
796
  // clear mouse was pressed state even when disabled
495
797
  mousePress && inputClearKey(0,0,0,1,0);
496
798
  }
@@ -498,8 +800,7 @@ class UIObject
498
800
  if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
499
801
  {
500
802
  this.onRelease();
501
- if (this.soundRelease)
502
- this.soundRelease.play();
803
+ this.soundRelease && this.soundRelease.play();
503
804
  uiSystem.activeObject = undefined;
504
805
  }
505
806
 
@@ -511,29 +812,40 @@ class UIObject
511
812
  /** Render the object, called automatically by plugin once each frame */
512
813
  render()
513
814
  {
514
- if (!this.size.x || !this.size.y) return;
815
+ // call the custom render callback
816
+ this.onRender();
515
817
 
516
- const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
517
- const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
518
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
519
- }
818
+ if (!this.size.x || !this.size.y) return;
520
819
 
521
- /** Special update when object is not visible */
522
- updateInvisible()
523
- {
524
- // reset input state when not visible
525
- if (this.isActiveObject())
526
- 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);
527
833
  }
528
834
 
529
835
  /** Get the size for text with overrides and scale
530
- * @return {Vector2}
531
- */
836
+ * @return {Vector2} */
532
837
  getTextSize()
533
838
  {
534
839
  return vec2(
535
- this.textWidth || this.textScale * this.size.x,
536
- 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();
537
849
  }
538
850
 
539
851
  /** @return {boolean} - Is the mouse hovering over this element */
@@ -542,9 +854,51 @@ class UIObject
542
854
  /** @return {boolean} - Is the mouse held onto this element */
543
855
  isActiveObject() { return uiSystem.activeObject === this; }
544
856
 
545
- /** 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 */
546
897
  onUpdate() {}
547
898
 
899
+ /** Called each frame before object renders */
900
+ onRender() {}
901
+
548
902
  /** Called when the mouse enters the object */
549
903
  onEnter() {}
550
904
 
@@ -592,16 +946,25 @@ class UIText extends UIObject
592
946
  this.align = align;
593
947
  this.font = font;
594
948
 
595
- // make text not outlined by default
596
- this.lineWidth = 0;
597
949
  // text can not be a hover object by default
598
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;
599
960
  }
600
961
  render()
601
962
  {
602
- // only render the text
963
+ super.render();
964
+
965
+ // render the text
603
966
  const textSize = this.getTextSize();
604
- 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);
605
968
  }
606
969
  }
607
970
 
@@ -637,10 +1000,13 @@ class UITile extends UIObject
637
1000
  this.mirror = mirror;
638
1001
  // set properties
639
1002
  this.color = color.copy();
1003
+
1004
+ // no shadow by default
1005
+ this.shadowColor = CLEAR_BLACK;
640
1006
  }
641
1007
  render()
642
1008
  {
643
- 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);
644
1010
  }
645
1011
  }
646
1012
 
@@ -665,6 +1031,9 @@ class UIButton extends UIObject
665
1031
  ASSERT(isString(text), 'ui button must be a string');
666
1032
  ASSERT(isColor(color), 'ui button color must be a color');
667
1033
 
1034
+ /** @property {Vector2} - Text offset for the button */
1035
+ this.textOffset = vec2();
1036
+
668
1037
  // set properties
669
1038
  this.text = text;
670
1039
  this.color = color.copy();
@@ -676,8 +1045,8 @@ class UIButton extends UIObject
676
1045
 
677
1046
  // draw the text scaled to fit
678
1047
  const textSize = this.getTextSize();
679
- uiSystem.drawText(this.text, this.pos, textSize,
680
- 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);
681
1050
  }
682
1051
  }
683
1052
 
@@ -731,7 +1100,7 @@ class UICheckbox extends UIObject
731
1100
  const textSize = this.getTextSize();
732
1101
  const pos = this.pos.add(vec2(this.size.x,0));
733
1102
  uiSystem.drawText(this.text, pos, textSize,
734
- 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);
735
1104
  }
736
1105
  }
737
1106
 
@@ -773,7 +1142,11 @@ class UIScrollbar extends UIObject
773
1142
  update()
774
1143
  {
775
1144
  super.update();
776
- if (this.isActiveObject() && this.interactive)
1145
+ if (!this.interactive)
1146
+ return;
1147
+
1148
+ const oldValue = this.value;
1149
+ if (this.isActiveObject())
777
1150
  {
778
1151
  // handle horizontal or vertical scrollbar
779
1152
  const isHorizontal = this.size.x > this.size.y;
@@ -785,14 +1158,19 @@ class UIScrollbar extends UIObject
785
1158
  const handleWidth = barSize - handleSize;
786
1159
  const p1 = centerPos - handleWidth/2;
787
1160
  const p2 = centerPos + handleWidth/2;
788
- const oldValue = this.value;
789
-
790
1161
  const p = uiSystem.screenToNative(mousePosScreen);
791
1162
  this.value = isHorizontal ?
792
1163
  percent(p.x, p1, p2) :
793
1164
  percent(p.y, p2, p1);
794
- this.value === oldValue || this.onChange();
795
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();
796
1174
  }
797
1175
  render()
798
1176
  {
@@ -817,7 +1195,14 @@ class UIScrollbar extends UIObject
817
1195
  // draw the text scaled to fit on the scrollbar
818
1196
  const textSize = this.getTextSize();
819
1197
  uiSystem.drawText(this.text, this.pos, textSize,
820
- 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();
821
1206
  }
822
1207
  }
823
1208
 
@@ -851,7 +1236,7 @@ class UIVideo extends UIObject
851
1236
  this.color = BLACK; // default to black background
852
1237
  this.cornerRadius = 0; // default to no corner radius
853
1238
 
854
- /** @property {float} - The video volume */
1239
+ /** @property {number} - The video volume */
855
1240
  this.volume = volume;
856
1241
 
857
1242
  // create video element
@@ -884,7 +1269,7 @@ class UIVideo extends UIObject
884
1269
 
885
1270
  /** Check if video is currently loading
886
1271
  * @return {boolean} */
887
- isLoadng()
1272
+ isLoading()
888
1273
  { return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
889
1274
 
890
1275
  /** Check if video is currently paused
@@ -894,7 +1279,7 @@ class UIVideo extends UIObject
894
1279
  /** Check if video is currently playing
895
1280
  * @return {boolean} */
896
1281
  isPlaying()
897
- { return !this.isPaused() && !this.hasEnded() && !this.isLoadng(); }
1282
+ { return !this.isPaused() && !this.hasEnded() && !this.isLoading(); }
898
1283
 
899
1284
  /** Check if video has ended playing
900
1285
  * @return {boolean} */
@@ -943,7 +1328,7 @@ class UIVideo extends UIObject
943
1328
  {
944
1329
  super.render();
945
1330
 
946
- if (this.isLoadng())
1331
+ if (this.isLoading())
947
1332
  return;
948
1333
  const context = uiSystem.uiContext;
949
1334
  const s = this.size;