littlejsengine 1.19.3 → 1.21.0
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.
- package/FAQ.md +841 -820
- package/README.md +29 -25
- package/dist/littlejs.d.ts +2857 -955
- package/dist/littlejs.esm.js +33428 -20995
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +33386 -20996
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +27841 -20422
- package/package.json +10 -5
- package/plugins/audioEffects.js +28 -24
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +786 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -332
- package/plugins/math3d.js +148 -27
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +25 -0
- package/plugins/postProcess.js +96 -24
- package/plugins/render3d.js +2067 -1632
- package/plugins/render3dDebug.js +110 -0
- package/plugins/render3dEditor.js +1812 -0
- package/plugins/render3dExtras.js +1948 -0
- package/plugins/render3dLevel.js +189 -0
- package/plugins/render3dVoxels.js +514 -0
- package/plugins/sceneSystem.js +58 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +265 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +378 -97
- package/src/engineAudio.js +231 -127
- package/src/engineBuild.mjs +292 -256
- package/src/engineDebug.js +967 -743
- package/src/engineDraw.js +391 -198
- package/src/engineEditor.js +2452 -0
- package/src/engineExport.js +18 -0
- package/src/engineInput.js +1466 -1293
- package/src/engineLogo.js +19 -0
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +197 -82
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +79 -45
- package/src/engineSettings.js +115 -29
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +19 -13
- package/src/engineWebGL.js +239 -82
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
package/plugins/uiSystem.js
CHANGED
|
@@ -1,1688 +1,2025 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LittleJS User Interface Plugin
|
|
3
|
-
* - call new UISystemPlugin() to setup the UI system
|
|
4
|
-
* - Gamepad and keyboard navigation support
|
|
5
|
-
* - Nested Menus
|
|
6
|
-
* - Text
|
|
7
|
-
* - Buttons
|
|
8
|
-
* - Checkboxes
|
|
9
|
-
* - Images
|
|
10
|
-
* - Sliders
|
|
11
|
-
* - Video
|
|
12
|
-
* @namespace UISystem
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
'use strict';
|
|
16
|
-
|
|
17
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
18
|
-
|
|
19
|
-
/** Global UI system plugin object
|
|
20
|
-
* @type {UISystemPlugin}
|
|
21
|
-
* @memberof UISystem */
|
|
22
|
-
let uiSystem;
|
|
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} debugMode
|
|
34
|
-
* @memberof UISystem */
|
|
35
|
-
function uiSetDebug(debugMode)
|
|
36
|
-
{ uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
|
|
37
|
-
|
|
38
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
39
|
-
/**
|
|
40
|
-
* UI System Global Object
|
|
41
|
-
* @memberof UISystem
|
|
42
|
-
*/
|
|
43
|
-
class UISystemPlugin
|
|
44
|
-
{
|
|
45
|
-
/** Create the global UI system object
|
|
46
|
-
* @param {CanvasRenderingContext2D} [context]
|
|
47
|
-
* @example
|
|
48
|
-
* // create the ui plugin object
|
|
49
|
-
* new UISystemPlugin;
|
|
50
|
-
*/
|
|
51
|
-
constructor(context=mainContext)
|
|
52
|
-
{
|
|
53
|
-
ASSERT(!uiSystem, 'UI system already initialized');
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
/** @property {number} - Default
|
|
76
|
-
this.
|
|
77
|
-
/** @property {number} - Default
|
|
78
|
-
this.
|
|
79
|
-
/** @property {
|
|
80
|
-
this.
|
|
81
|
-
/** @property {
|
|
82
|
-
this.
|
|
83
|
-
/** @property {Sound} - Default sound when interactive UI element is
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.
|
|
89
|
-
/** @property {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this.
|
|
105
|
-
/** @property {
|
|
106
|
-
this.
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
/** @property {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
context.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
* @
|
|
516
|
-
|
|
517
|
-
{
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
//
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
//
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
if (
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
this.
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
this.
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
this.
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
this.
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
this.
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
{
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
uiSystem.
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
inputClearKey(0,0);
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if (this.
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
this.
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
*/
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
/**
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
{
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
this.text
|
|
1233
|
-
|
|
1234
|
-
this.
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
* @param {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
{
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
{
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
this.
|
|
1331
|
-
/** @
|
|
1332
|
-
this.
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
this.
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
uiSystem.
|
|
1406
|
-
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
this.
|
|
1457
|
-
|
|
1458
|
-
this.
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
/**
|
|
1517
|
-
* @
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
* @param {
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
this.
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
* @param {
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS User Interface Plugin
|
|
3
|
+
* - call new UISystemPlugin() to setup the UI system
|
|
4
|
+
* - Gamepad and keyboard navigation support
|
|
5
|
+
* - Nested Menus
|
|
6
|
+
* - Text
|
|
7
|
+
* - Buttons
|
|
8
|
+
* - Checkboxes
|
|
9
|
+
* - Images
|
|
10
|
+
* - Sliders
|
|
11
|
+
* - Video
|
|
12
|
+
* @namespace UISystem
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
18
|
+
|
|
19
|
+
/** Global UI system plugin object
|
|
20
|
+
* @type {UISystemPlugin}
|
|
21
|
+
* @memberof UISystem */
|
|
22
|
+
let uiSystem;
|
|
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} debugMode
|
|
34
|
+
* @memberof UISystem */
|
|
35
|
+
function uiSetDebug(debugMode)
|
|
36
|
+
{ uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
|
|
37
|
+
|
|
38
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
39
|
+
/**
|
|
40
|
+
* UI System Global Object
|
|
41
|
+
* @memberof UISystem
|
|
42
|
+
*/
|
|
43
|
+
class UISystemPlugin
|
|
44
|
+
{
|
|
45
|
+
/** Create the global UI system object
|
|
46
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
47
|
+
* @example
|
|
48
|
+
* // create the ui plugin object
|
|
49
|
+
* new UISystemPlugin;
|
|
50
|
+
*/
|
|
51
|
+
constructor(context=mainContext)
|
|
52
|
+
{
|
|
53
|
+
ASSERT(!uiSystem, 'UI system already initialized');
|
|
54
|
+
ASSERT(context || headlessMode, 'create the UISystemPlugin after engineInit, in gameInit');
|
|
55
|
+
uiSystem = this;
|
|
56
|
+
|
|
57
|
+
// default settings
|
|
58
|
+
/** @property {boolean} - Activate when mouse is pressed down instead of clicked */
|
|
59
|
+
this.activateOnPress = false;
|
|
60
|
+
/** @property {Color} - Default fill color for UI elements */
|
|
61
|
+
this.defaultColor = WHITE;
|
|
62
|
+
/** @property {Color} - Default outline color for UI elements */
|
|
63
|
+
this.defaultLineColor = BLACK;
|
|
64
|
+
/** @property {Color} - Default text color for UI elements */
|
|
65
|
+
this.defaultTextColor = BLACK;
|
|
66
|
+
/** @property {Color} - Default button color for UI elements */
|
|
67
|
+
this.defaultButtonColor = hsl(0,0,.7);
|
|
68
|
+
/** @property {Color} - Default hover color for UI elements */
|
|
69
|
+
this.defaultHoverColor = hsl(0,0,.9);
|
|
70
|
+
/** @property {Color} - Default color for disabled UI elements */
|
|
71
|
+
this.defaultDisabledColor = hsl(0,0,.3);
|
|
72
|
+
/** @property {Color|undefined} - Uses a gradient fill combined with color
|
|
73
|
+
* @type {Color|undefined} */
|
|
74
|
+
this.defaultGradientColor = undefined;
|
|
75
|
+
/** @property {number} - Default line width for UI elements */
|
|
76
|
+
this.defaultLineWidth = 4;
|
|
77
|
+
/** @property {number} - Default rounded rect corner radius for UI elements */
|
|
78
|
+
this.defaultCornerRadius = 0;
|
|
79
|
+
/** @property {number} - Default scale to use for fitting text to object */
|
|
80
|
+
this.defaultTextFitScale = .8;
|
|
81
|
+
/** @property {string} - Default font for UI elements */
|
|
82
|
+
this.defaultFont = fontDefault;
|
|
83
|
+
/** @property {Sound|undefined} - Default sound when interactive UI element is pressed
|
|
84
|
+
* @type {Sound|undefined} */
|
|
85
|
+
this.defaultSoundPress = undefined;
|
|
86
|
+
/** @property {Sound|undefined} - Default sound when interactive UI element is released
|
|
87
|
+
* @type {Sound|undefined} */
|
|
88
|
+
this.defaultSoundRelease = undefined;
|
|
89
|
+
/** @property {Sound|undefined} - Default sound when interactive UI element is clicked
|
|
90
|
+
* @type {Sound|undefined} */
|
|
91
|
+
this.defaultSoundClick = undefined;
|
|
92
|
+
/** @property {Color} - Color for shadow */
|
|
93
|
+
this.defaultShadowColor = CLEAR_BLACK;
|
|
94
|
+
/** @property {number} - Size of shadow blur */
|
|
95
|
+
this.defaultShadowBlur = 5;
|
|
96
|
+
/** @property {Vector2} - Offset of shadow blur */
|
|
97
|
+
this.defaultShadowOffset = vec2(5);
|
|
98
|
+
/** @property {TileSlice|undefined} - Style to draw UI elements with in place of their rectangle, tinted by
|
|
99
|
+
* their color, undefined for rectangles; needs the drawUtilities plugin
|
|
100
|
+
* @type {TileSlice|undefined} */
|
|
101
|
+
this.defaultSlice = undefined;
|
|
102
|
+
/** @property {TileSlice|undefined} - Style to draw slider handles with, undefined for the slider's own slice
|
|
103
|
+
* @type {TileSlice|undefined} */
|
|
104
|
+
this.defaultHandleSlice = undefined;
|
|
105
|
+
/** @property {number} - If set ui coords will be renormalized to this canvas height */
|
|
106
|
+
this.nativeHeight = 0;
|
|
107
|
+
|
|
108
|
+
// navigation properties
|
|
109
|
+
/** @property {UIObject|undefined} - Object currently selected by navigation (gamepad or keyboard)
|
|
110
|
+
* @type {UIObject|undefined} */
|
|
111
|
+
this.navigationObject = undefined;
|
|
112
|
+
/** @property {Timer} - Cool down timer for navigation inputs */
|
|
113
|
+
this.navigationTimer = new Timer(undefined, true);
|
|
114
|
+
/** @property {number} - Time between navigation inputs in seconds */
|
|
115
|
+
this.navigationDelay = .2;
|
|
116
|
+
/** @property {number} - Which way keys and gamepads move the selection: 0 horizontal, 1 vertical, 2 both */
|
|
117
|
+
this.navigationDirection = 1;
|
|
118
|
+
/** @property {boolean} - True if user last used navigation instead of mouse */
|
|
119
|
+
this.navigationMode = false;
|
|
120
|
+
|
|
121
|
+
// system state
|
|
122
|
+
/** @property {Array<UIObject>} - List of all UI elements
|
|
123
|
+
* @type {Array<UIObject>} */
|
|
124
|
+
this.uiObjects = [];
|
|
125
|
+
/** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
|
|
126
|
+
this.uiContext = context;
|
|
127
|
+
/** @property {UIObject|undefined} - Object user is currently interacting with
|
|
128
|
+
* @type {UIObject|undefined} */
|
|
129
|
+
this.activeObject = undefined;
|
|
130
|
+
/** @property {UIObject|undefined} - Top most object user is over
|
|
131
|
+
* @type {UIObject|undefined} */
|
|
132
|
+
this.hoverObject = undefined;
|
|
133
|
+
/** @property {UIObject|undefined} - Hover object at start of update
|
|
134
|
+
* @type {UIObject|undefined} */
|
|
135
|
+
this.lastHoverObject = undefined;
|
|
136
|
+
/** @property {UIObject|undefined} - Current confirm menu being shown
|
|
137
|
+
* @type {UIObject|undefined} */
|
|
138
|
+
this.confirmDialog = undefined;
|
|
139
|
+
/** @private
|
|
140
|
+
* @type {UIObject|undefined} */
|
|
141
|
+
this._keyInputObject = undefined;
|
|
142
|
+
/** @private
|
|
143
|
+
* @type {Array<Array<any>>|undefined} */
|
|
144
|
+
this._dragListeners = undefined;
|
|
145
|
+
/** @private */
|
|
146
|
+
this._onKeyDown = (e) =>
|
|
147
|
+
{
|
|
148
|
+
// a field that was hidden, disabled or destroyed since it took focus, or is behind a confirm dialog opened
|
|
149
|
+
// since, does not take the key, the game gets it, and the next UI update ends the edit, so it ends one
|
|
150
|
+
// way whichever comes first
|
|
151
|
+
const o = this._keyInputObject;
|
|
152
|
+
if (!o || !uiObjectIsUsable(o) || uiObjectIsBehindDialog(o)) return;
|
|
153
|
+
|
|
154
|
+
// the field has the key, the game's input never sees it; browser shortcuts still work,
|
|
155
|
+
// and only the keys a field uses lose their default, so F5, F11, F12 and the like still work
|
|
156
|
+
e.stopPropagation();
|
|
157
|
+
const key = e.key || '';
|
|
158
|
+
if (!e.ctrlKey && !e.metaKey && !e.altKey)
|
|
159
|
+
if (key.length === 1 ||
|
|
160
|
+
/^(Backspace|Delete|Tab|Arrow(Up|Down|Left|Right)|Home|End|Enter|Escape)$/.test(key))
|
|
161
|
+
e.preventDefault(); // no scrolling, find as you type or going back
|
|
162
|
+
e.type === 'keydown' && o.onKeyDown(e);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
engineAddPlugin(uiUpdate, uiRender);
|
|
166
|
+
|
|
167
|
+
// set object position based on anchor target (parent box, or canvas for roots),
|
|
168
|
+
// self-pivot, and localPos offset
|
|
169
|
+
function updateTransforms(o)
|
|
170
|
+
{
|
|
171
|
+
let targetPos, targetSize;
|
|
172
|
+
if (o.parent)
|
|
173
|
+
{
|
|
174
|
+
targetPos = o.parent.nativePos;
|
|
175
|
+
targetSize = o.parent.size;
|
|
176
|
+
}
|
|
177
|
+
else
|
|
178
|
+
{
|
|
179
|
+
// anchor to canvas in native coords (handles nativeHeight if set)
|
|
180
|
+
targetPos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
|
|
181
|
+
targetSize = uiSystem.nativeHeight
|
|
182
|
+
? vec2(mainCanvasSize.x * uiSystem.nativeHeight / mainCanvasSize.y,
|
|
183
|
+
uiSystem.nativeHeight)
|
|
184
|
+
: mainCanvasSize;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const a = o.anchor;
|
|
188
|
+
o.nativePos = targetPos
|
|
189
|
+
.add(targetSize.multiply(a).scale(.5)) // anchor point on target
|
|
190
|
+
.subtract(o.size.multiply(a).scale(.5)) // pivot shift on self
|
|
191
|
+
.add(o.localPos); // user offset
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// setup recursive update and render
|
|
195
|
+
// update in reverse order to detect mouse enter/leave
|
|
196
|
+
function uiUpdate()
|
|
197
|
+
{
|
|
198
|
+
// a held or focused object that can no longer be used, itself or through a parent, lets go,
|
|
199
|
+
// and one that was hidden or disabled is still released, a destroyed one stays silent
|
|
200
|
+
const activeObject = uiSystem.activeObject;
|
|
201
|
+
if (activeObject && !uiObjectIsUsable(activeObject))
|
|
202
|
+
{
|
|
203
|
+
// a text field being edited is active without being held, it was released when the press ended
|
|
204
|
+
uiSystem.activeObject = undefined;
|
|
205
|
+
activeObject.destroyed || activeObject === uiSystem.keyInputObject || activeObject.onRelease();
|
|
206
|
+
}
|
|
207
|
+
// an edit whose field can no longer be used, or is behind a confirm dialog, ends as if it were finished,
|
|
208
|
+
// so onChange keeps the text
|
|
209
|
+
const keyInputObject = uiSystem.keyInputObject;
|
|
210
|
+
if (keyInputObject && (!uiObjectIsUsable(keyInputObject) || uiObjectIsBehindDialog(keyInputObject)))
|
|
211
|
+
{
|
|
212
|
+
if (keyInputObject instanceof UITextInput && !keyInputObject.destroyed)
|
|
213
|
+
keyInputObject.stopEditing();
|
|
214
|
+
else
|
|
215
|
+
uiSystem.keyInputObject = undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// a click off the field being edited ends the edit before anything updates, so the click goes on to
|
|
219
|
+
// what it lands on, another field or a button, as in a web form
|
|
220
|
+
const editing = uiSystem.keyInputObject;
|
|
221
|
+
const clickedOff = editing instanceof UITextInput && mouseWasPressed(0) && !editing.isMouseOverlapping();
|
|
222
|
+
clickedOff && editing.stopEditing();
|
|
223
|
+
|
|
224
|
+
// reset hover object at start of update
|
|
225
|
+
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
226
|
+
uiSystem.hoverObject = undefined;
|
|
227
|
+
|
|
228
|
+
// a hidden object is not updated, so the one whose hover ends by hiding it, itself or through a parent,
|
|
229
|
+
// is left here; a disabled one still updates and handles its own leave
|
|
230
|
+
const lastHoverObject = uiSystem.lastHoverObject;
|
|
231
|
+
if (lastHoverObject && !lastHoverObject.destroyed && uiObjectIsHidden(lastHoverObject))
|
|
232
|
+
{
|
|
233
|
+
uiSystem.lastHoverObject = undefined;
|
|
234
|
+
lastHoverObject.onLeave();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (mouseWasPressed(0))
|
|
238
|
+
{
|
|
239
|
+
// exit navigation mode on mouse press
|
|
240
|
+
uiSystem.navigationMode = false;
|
|
241
|
+
uiSystem.navigationObject = undefined;
|
|
242
|
+
}
|
|
243
|
+
if (uiSystem.keyInputObject)
|
|
244
|
+
{
|
|
245
|
+
// handle text input, navigation keeps its place for when the edit ends, and the hover stays where it
|
|
246
|
+
// was, so an edit started by navigation does not end with an onLeave that had no onEnter
|
|
247
|
+
uiSystem.activeObject = uiSystem.keyInputObject;
|
|
248
|
+
uiSystem.hoverObject = uiSystem.lastHoverObject;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// navigation with gamepad/keyboard
|
|
252
|
+
const navigableObjects = uiSystem.getNavigableObjects();
|
|
253
|
+
if (!navigableObjects.length)
|
|
254
|
+
uiSystem.navigationObject = undefined;
|
|
255
|
+
else if (!uiSystem.keyInputObject)
|
|
256
|
+
{
|
|
257
|
+
// unselect object if it is no longer navigable
|
|
258
|
+
if (!navigableObjects.includes(uiSystem.navigationObject))
|
|
259
|
+
uiSystem.navigationObject = undefined;
|
|
260
|
+
|
|
261
|
+
if (!isTouchDevice)
|
|
262
|
+
if (uiSystem.navigationMode && !uiSystem.navigationObject)
|
|
263
|
+
{
|
|
264
|
+
// select first auto focus object
|
|
265
|
+
uiSystem.navigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// navigate with dpad or left stick
|
|
269
|
+
if (!uiSystem.navigationTimer.active())
|
|
270
|
+
{
|
|
271
|
+
// navigate through list with gamepad or keyboard
|
|
272
|
+
const direction = sign(uiSystem.getNavigationDirection());
|
|
273
|
+
if (direction)
|
|
274
|
+
{
|
|
275
|
+
let newNavigationObject;
|
|
276
|
+
if (!uiSystem.navigationObject)
|
|
277
|
+
{
|
|
278
|
+
// use auto select object
|
|
279
|
+
newNavigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
|
|
280
|
+
|
|
281
|
+
if (!newNavigationObject)
|
|
282
|
+
{
|
|
283
|
+
// try first or last object
|
|
284
|
+
const newIndex = direction > 0 ? 0 : navigableObjects.length-1;
|
|
285
|
+
newNavigationObject = navigableObjects[newIndex];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else
|
|
289
|
+
{
|
|
290
|
+
const currentIndex = navigableObjects.indexOf(uiSystem.navigationObject);
|
|
291
|
+
const newIndex = mod(currentIndex + direction, navigableObjects.length);
|
|
292
|
+
newNavigationObject = navigableObjects[newIndex];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (uiSystem.navigationObject !== newNavigationObject)
|
|
296
|
+
{
|
|
297
|
+
uiSystem.navigationMode = true;
|
|
298
|
+
uiSystem.hoverObject = undefined;
|
|
299
|
+
uiSystem.navigationObject = newNavigationObject;
|
|
300
|
+
uiSystem.navigationTimer.set(uiSystem.navigationDelay);
|
|
301
|
+
newNavigationObject.soundPress &&
|
|
302
|
+
newNavigationObject.soundPress.play();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// activate the navigation object when pressed, the press is used up as a mouse click is
|
|
308
|
+
if (uiSystem.navigationObject)
|
|
309
|
+
if (uiSystem.getNavigationWasPressed())
|
|
310
|
+
{
|
|
311
|
+
uiSystem.navigationObject.navigatePressed();
|
|
312
|
+
if (isUsingGamepad)
|
|
313
|
+
inputClearKey(0, gamepadPrimary+1, false, true, false);
|
|
314
|
+
else
|
|
315
|
+
{
|
|
316
|
+
inputClearKey('Space', 0, false, true, false);
|
|
317
|
+
inputClearKey('Enter', 0, false, true, false);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// update in reverse order so topmost objects get priority, from the list as it was
|
|
323
|
+
// since a callback may call destroyObjects, which swaps in a shorter one
|
|
324
|
+
// a confirm dialog is modal, so it goes first however late it was made, and UI made after it opened
|
|
325
|
+
// can not take the mouse through it
|
|
326
|
+
const uiObjects = uiSystem.uiObjects, dialog = uiSystem.confirmDialog;
|
|
327
|
+
dialog && updateObject(dialog);
|
|
328
|
+
for (let i = uiObjects.length; i--;)
|
|
329
|
+
{
|
|
330
|
+
const o = uiObjects[i];
|
|
331
|
+
o.parent || o === dialog || updateObject(o);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// a click off the field that no UI object took is used up, the game does not see the click that ended it
|
|
335
|
+
clickedOff && mouseWasPressed(0) && inputClearKey(0, 0);
|
|
336
|
+
|
|
337
|
+
// remove destroyed objects
|
|
338
|
+
uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
|
|
339
|
+
|
|
340
|
+
function updateObject(o)
|
|
341
|
+
{
|
|
342
|
+
if (o.destroyed || !o.visible) return;
|
|
343
|
+
|
|
344
|
+
// update in reverse order to detect mouse enter/leave, from a copy since a child may destroy
|
|
345
|
+
// siblings mid-update (e.g. dialog close) and the ones after it would shift under the loop
|
|
346
|
+
updateTransforms(o);
|
|
347
|
+
const children = o.children.slice();
|
|
348
|
+
for (let i=children.length; i--;)
|
|
349
|
+
updateObject(children[i]);
|
|
350
|
+
if (!o.destroyed)
|
|
351
|
+
o.update();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function uiRender()
|
|
355
|
+
{
|
|
356
|
+
const context = uiSystem.uiContext;
|
|
357
|
+
context.save();
|
|
358
|
+
if (uiSystem.nativeHeight)
|
|
359
|
+
{
|
|
360
|
+
// convert to native height
|
|
361
|
+
const s = mainCanvasSize.y / uiSystem.nativeHeight;
|
|
362
|
+
context.translate(-s*mainCanvasSize.x/2,0);
|
|
363
|
+
context.scale(s,s);
|
|
364
|
+
context.translate(mainCanvasSize.x/2/s,0);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function renderObject(o)
|
|
368
|
+
{
|
|
369
|
+
// a destroyed object stays in the list until the next update, but is gone now
|
|
370
|
+
if (o.destroyed || !o.visible) return;
|
|
371
|
+
|
|
372
|
+
// render object and children
|
|
373
|
+
updateTransforms(o);
|
|
374
|
+
o.render();
|
|
375
|
+
for (const c of o.children)
|
|
376
|
+
renderObject(c);
|
|
377
|
+
}
|
|
378
|
+
// a confirm dialog is drawn over everything, UI made after it opened too
|
|
379
|
+
const dialog = uiSystem.confirmDialog;
|
|
380
|
+
uiSystem.uiObjects.forEach(o=> o.parent || o === dialog || renderObject(o));
|
|
381
|
+
dialog && renderObject(dialog);
|
|
382
|
+
|
|
383
|
+
if (uiDebug > 0)
|
|
384
|
+
{
|
|
385
|
+
// debug render all objects
|
|
386
|
+
function renderDebug(o, visible=true)
|
|
387
|
+
{
|
|
388
|
+
visible &&= !!o.visible;
|
|
389
|
+
if (o.destroyed || !visible && uiDebug < 2)
|
|
390
|
+
return; // only mode 2 shows the invisible ones
|
|
391
|
+
updateTransforms(o);
|
|
392
|
+
o.renderDebug(visible);
|
|
393
|
+
for (const c of o.children)
|
|
394
|
+
renderDebug(c, visible);
|
|
395
|
+
}
|
|
396
|
+
uiSystem.uiObjects.forEach(o=> o.parent || o === dialog || renderDebug(o));
|
|
397
|
+
dialog && renderDebug(dialog);
|
|
398
|
+
}
|
|
399
|
+
context.restore();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Draw a rectangle to the UI context
|
|
404
|
+
* @param {Vector2} pos
|
|
405
|
+
* @param {Vector2} size
|
|
406
|
+
* @param {Color} [color]
|
|
407
|
+
* @param {number} [lineWidth]
|
|
408
|
+
* @param {Color} [lineColor]
|
|
409
|
+
* @param {number} [cornerRadius]
|
|
410
|
+
* @param {Color} [gradientColor]
|
|
411
|
+
* @param {Color} [shadowColor]
|
|
412
|
+
* @param {number} [shadowBlur]
|
|
413
|
+
* @param {Vector2} [shadowOffset] */
|
|
414
|
+
drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
|
|
415
|
+
{
|
|
416
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
417
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
418
|
+
ASSERT(isColor(color), 'color must be a color');
|
|
419
|
+
ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
|
|
420
|
+
ASSERT(isColor(lineColor), 'lineColor must be a color');
|
|
421
|
+
ASSERT(isNumber(cornerRadius), 'cornerRadius must be a number');
|
|
422
|
+
|
|
423
|
+
const context = uiSystem.uiContext;
|
|
424
|
+
if (gradientColor)
|
|
425
|
+
{
|
|
426
|
+
const g = context.createLinearGradient(
|
|
427
|
+
pos.x, pos.y-size.y/2, pos.x, pos.y+size.y/2);
|
|
428
|
+
const c = color.toString();
|
|
429
|
+
g.addColorStop(0, c);
|
|
430
|
+
g.addColorStop(.5, gradientColor.toString());
|
|
431
|
+
g.addColorStop(1, c);
|
|
432
|
+
context.fillStyle = g;
|
|
433
|
+
}
|
|
434
|
+
else
|
|
435
|
+
context.fillStyle = color.toString();
|
|
436
|
+
uiSetShadow(context, shadowColor, shadowBlur, shadowOffset);
|
|
437
|
+
context.beginPath();
|
|
438
|
+
if (cornerRadius && context['roundRect'])
|
|
439
|
+
context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
|
|
440
|
+
else
|
|
441
|
+
context.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
|
|
442
|
+
context.fill();
|
|
443
|
+
context.shadowColor = '#0000';
|
|
444
|
+
if (lineWidth && lineColor.a > 0)
|
|
445
|
+
{
|
|
446
|
+
context.strokeStyle = lineColor.toString();
|
|
447
|
+
context.lineWidth = lineWidth;
|
|
448
|
+
context.stroke();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Draw a TileSlice to the UI context, in place of a rectangle
|
|
453
|
+
* @param {TileSlice} slice
|
|
454
|
+
* @param {Vector2} pos
|
|
455
|
+
* @param {Vector2} size
|
|
456
|
+
* @param {Color} [color] */
|
|
457
|
+
drawSlice(slice, pos, size, color=WHITE)
|
|
458
|
+
{
|
|
459
|
+
ASSERT(typeof TileSlice === 'function' && slice instanceof TileSlice, 'slice must be a TileSlice, from the drawUtilities plugin');
|
|
460
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
461
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
462
|
+
ASSERT(isColor(color), 'color must be a color');
|
|
463
|
+
if (color.a > 0) // a clear one, like a UIText's, costs a tint of each piece for nothing
|
|
464
|
+
slice.drawScreen(pos, size, color, undefined, 0, false, uiSystem.uiContext);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Draw a line to the UI context
|
|
468
|
+
* @param {Vector2} posA
|
|
469
|
+
* @param {Vector2} posB
|
|
470
|
+
* @param {number} [lineWidth=uiSystem.defaultLineWidth]
|
|
471
|
+
* @param {Color} [lineColor=uiSystem.defaultLineColor] */
|
|
472
|
+
drawLine(posA, posB, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
|
|
473
|
+
{
|
|
474
|
+
ASSERT(isVector2(posA), 'posA must be a vec2');
|
|
475
|
+
ASSERT(isVector2(posB), 'posB must be a vec2');
|
|
476
|
+
ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
|
|
477
|
+
ASSERT(isColor(lineColor), 'lineColor must be a color');
|
|
478
|
+
|
|
479
|
+
const context = uiSystem.uiContext;
|
|
480
|
+
context.strokeStyle = lineColor.toString();
|
|
481
|
+
context.lineWidth = lineWidth;
|
|
482
|
+
context.beginPath();
|
|
483
|
+
context.lineTo(posA.x, posA.y);
|
|
484
|
+
context.lineTo(posB.x, posB.y);
|
|
485
|
+
context.stroke();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Draw a tile to the UI context
|
|
489
|
+
* @param {Vector2} pos
|
|
490
|
+
* @param {Vector2} size
|
|
491
|
+
* @param {TileInfo} tileInfo
|
|
492
|
+
* @param {Color} [color=uiSystem.defaultColor]
|
|
493
|
+
* @param {number} [angle]
|
|
494
|
+
* @param {boolean} [mirror]
|
|
495
|
+
* @param {Color} [shadowColor]
|
|
496
|
+
* @param {number} [shadowBlur]
|
|
497
|
+
* @param {Vector2} [shadowOffset] */
|
|
498
|
+
drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
|
|
499
|
+
{
|
|
500
|
+
const context = uiSystem.uiContext;
|
|
501
|
+
uiSetShadow(context, shadowColor, shadowBlur, shadowOffset);
|
|
502
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, CLEAR_BLACK, false, true, context);
|
|
503
|
+
context.shadowColor = '#0000';
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Draw text to the UI context
|
|
507
|
+
* @param {string} text
|
|
508
|
+
* @param {Vector2} pos
|
|
509
|
+
* @param {Vector2} size
|
|
510
|
+
* @param {Color} [color=uiSystem.defaultColor]
|
|
511
|
+
* @param {number} [lineWidth=uiSystem.defaultLineWidth]
|
|
512
|
+
* @param {Color} [lineColor=uiSystem.defaultLineColor]
|
|
513
|
+
* @param {'left'|'center'|'right'} [align]
|
|
514
|
+
* @param {string} [font=uiSystem.defaultFont]
|
|
515
|
+
* @param {string} [fontStyle]
|
|
516
|
+
* @param {boolean} [applyMaxWidth=true]
|
|
517
|
+
* @param {Vector2} [textShadow]
|
|
518
|
+
* @param {Color} [shadowColor]
|
|
519
|
+
* @param {number} [shadowBlur]
|
|
520
|
+
* @param {Vector2} [shadowOffset] */
|
|
521
|
+
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())
|
|
522
|
+
{
|
|
523
|
+
const context = uiSystem.uiContext;
|
|
524
|
+
if (textShadow && shadowColor.a > 0)
|
|
525
|
+
drawTextScreen(text, pos.add(textShadow), size.y, shadowColor, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, context);
|
|
526
|
+
uiSetShadow(context, shadowColor, shadowBlur, shadowOffset);
|
|
527
|
+
drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, context);
|
|
528
|
+
context.shadowColor = '#0000';
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* @callback DragAndDropCallback - Callback for drag and drop events
|
|
533
|
+
* @param {DragEvent} event - The drag event
|
|
534
|
+
* @memberof UISystem
|
|
535
|
+
*/
|
|
536
|
+
|
|
537
|
+
/** Setup drag and drop event handlers
|
|
538
|
+
* Automatically prevents defaults and calls the given functions
|
|
539
|
+
* @param {DragAndDropCallback} [onDrop] - when a file is dropped
|
|
540
|
+
* @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
|
|
541
|
+
* @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
|
|
542
|
+
* @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
|
|
543
|
+
setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
|
|
544
|
+
{
|
|
545
|
+
// remove any prior listeners so repeated setup calls don't stack
|
|
546
|
+
if (this._dragListeners)
|
|
547
|
+
for (const [type, listener] of this._dragListeners)
|
|
548
|
+
document.removeEventListener(type, listener);
|
|
549
|
+
this._dragListeners = [];
|
|
550
|
+
const setCallback = (callback, listenerType, when=()=> true)=>
|
|
551
|
+
{
|
|
552
|
+
const listener = (e)=> { e.preventDefault(); when() && callback && callback(e); };
|
|
553
|
+
document.addEventListener(listenerType, listener);
|
|
554
|
+
this._dragListeners.push([listenerType, listener]);
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// every element the drag crosses sends its own enter and leave, and a move between two ends with a leave,
|
|
558
|
+
// so they are counted, and only the first enter and the last leave are the window's
|
|
559
|
+
let depth = 0;
|
|
560
|
+
setCallback(onDrop, 'drop', ()=> { depth = 0; return true; });
|
|
561
|
+
setCallback(onDragEnter, 'dragenter', ()=> !depth++);
|
|
562
|
+
setCallback(onDragLeave, 'dragleave', ()=> !!depth && !--depth);
|
|
563
|
+
setCallback(onDragOver, 'dragover');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Convert a screen space position to native UI position
|
|
567
|
+
* @param {Vector2} pos
|
|
568
|
+
* @return {Vector2} */
|
|
569
|
+
screenToNative(pos)
|
|
570
|
+
{
|
|
571
|
+
if (!uiSystem.nativeHeight)
|
|
572
|
+
return pos;
|
|
573
|
+
|
|
574
|
+
const s = mainCanvasSize.y / uiSystem.nativeHeight;
|
|
575
|
+
const sInv = 1/s;
|
|
576
|
+
const p = pos.copy();
|
|
577
|
+
p.x += s*mainCanvasSize.x/2;
|
|
578
|
+
p.x *= sInv;
|
|
579
|
+
p.y *= sInv;
|
|
580
|
+
p.x -= sInv*mainCanvasSize.x/2;
|
|
581
|
+
return p;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Object to send keyboard input to (typically a UITextInput), which keeps the keys from the game while set.
|
|
585
|
+
* The keyboard listeners are only attached while this is set,
|
|
586
|
+
* so games that never use text input pay no event-handling cost.
|
|
587
|
+
* To end typing in a field, call its stopEditing(), which also fires its onChange and release sound
|
|
588
|
+
* @type {UIObject|undefined} */
|
|
589
|
+
get keyInputObject() { return this._keyInputObject; }
|
|
590
|
+
set keyInputObject(obj)
|
|
591
|
+
{
|
|
592
|
+
const had = !!this._keyInputObject;
|
|
593
|
+
this._keyInputObject = obj;
|
|
594
|
+
// listen on the window as the event comes down, before the engine's input on the document can see it
|
|
595
|
+
if (!had && obj)
|
|
596
|
+
{
|
|
597
|
+
addEventListener('keydown', this._onKeyDown, true);
|
|
598
|
+
addEventListener('keyup', this._onKeyDown, true);
|
|
599
|
+
inputClearKeyboard(); // keys held when editing starts let go, or they would stay down
|
|
600
|
+
|
|
601
|
+
// a press that starts the edit, as with activateOnPress, is let go now, the updates skip its release
|
|
602
|
+
// while the edit goes on; a click on release already had its release
|
|
603
|
+
const held = this.activeObject;
|
|
604
|
+
if (held && mouseIsDown(0))
|
|
605
|
+
{
|
|
606
|
+
this.activeObject = undefined;
|
|
607
|
+
held.onRelease();
|
|
608
|
+
held.soundRelease && held.soundRelease.play();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
else if (had && !obj)
|
|
612
|
+
{
|
|
613
|
+
removeEventListener('keydown', this._onKeyDown, true);
|
|
614
|
+
removeEventListener('keyup', this._onKeyDown, true);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Destroy and remove all objects
|
|
619
|
+
* @memberof UISystem */
|
|
620
|
+
destroyObjects()
|
|
621
|
+
{
|
|
622
|
+
for (const o of this.uiObjects)
|
|
623
|
+
o.parent || o.destroy();
|
|
624
|
+
this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
|
|
625
|
+
this.activeObject = undefined;
|
|
626
|
+
this.hoverObject = undefined;
|
|
627
|
+
this.lastHoverObject = undefined;
|
|
628
|
+
this.keyInputObject = undefined;
|
|
629
|
+
this.confirmDialog = undefined;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Get all navigable UI objects sorted by navigationIndex
|
|
633
|
+
* @return {Array<UIObject>} */
|
|
634
|
+
getNavigableObjects()
|
|
635
|
+
{
|
|
636
|
+
function getNavigableRecursive(o)
|
|
637
|
+
{
|
|
638
|
+
if (o.destroyed || !o.visible || o.disabled)
|
|
639
|
+
return; // skip children if parent is destroyed, invisible or disabled
|
|
640
|
+
|
|
641
|
+
if (o.isInteractive() && o.navigationIndex !== undefined)
|
|
642
|
+
objects.push(o);
|
|
643
|
+
for (const child of o.children)
|
|
644
|
+
getNavigableRecursive(child);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// get all the valid navigable objects recursively
|
|
648
|
+
// while the confirm dialog is open only its buttons can be navigated
|
|
649
|
+
const objects = [];
|
|
650
|
+
if (uiSystem.confirmDialog)
|
|
651
|
+
getNavigableRecursive(uiSystem.confirmDialog);
|
|
652
|
+
else for (const o of uiSystem.uiObjects)
|
|
653
|
+
o.parent || getNavigableRecursive(o);
|
|
654
|
+
|
|
655
|
+
// sort by navigationIndex (lower numbers first), ties keep creation and child order
|
|
656
|
+
objects.sort((a, b)=> a.navigationIndex - b.navigationIndex);
|
|
657
|
+
return objects;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** Check if the mouse is over a visible UI object that can be hovered, or anywhere while the
|
|
661
|
+
* confirm dialog is open, so a game can leave world clicks on the UI alone, on touch too.
|
|
662
|
+
* The UI uses up a click before objects update and gameUpdatePost, so read world clicks there,
|
|
663
|
+
* or check this in gameUpdate, which runs first. Positions are from the last UI update.
|
|
664
|
+
* @return {boolean} */
|
|
665
|
+
isMouseOverUI()
|
|
666
|
+
{
|
|
667
|
+
function isOverRecursive(o)
|
|
668
|
+
{
|
|
669
|
+
if (o.destroyed || !o.visible)
|
|
670
|
+
return false; // a hidden parent hides its children
|
|
671
|
+
if (o.canBeHover && o.isMouseOverlapping())
|
|
672
|
+
return true;
|
|
673
|
+
return o.children.some(isOverRecursive);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// a click while a text field is being edited ends the edit, the UI takes it
|
|
677
|
+
if (uiSystem.keyInputObject)
|
|
678
|
+
return true;
|
|
679
|
+
|
|
680
|
+
// while the confirm dialog is open it blocks everything else
|
|
681
|
+
if (uiSystem.confirmDialog)
|
|
682
|
+
return isOverRecursive(uiSystem.confirmDialog);
|
|
683
|
+
return uiSystem.uiObjects.some(o=> !o.parent && isOverRecursive(o));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** Get navigation direction from gamepad or keyboard
|
|
687
|
+
* @return {number} */
|
|
688
|
+
getNavigationDirection()
|
|
689
|
+
{
|
|
690
|
+
const vertical = uiSystem.navigationDirection === 1;
|
|
691
|
+
const both = uiSystem.navigationDirection === 2;
|
|
692
|
+
if (isUsingGamepad)
|
|
693
|
+
{
|
|
694
|
+
const stick = gamepadStick(0, gamepadPrimary);
|
|
695
|
+
const dpad = gamepadDpad(gamepadPrimary);
|
|
696
|
+
if (both)
|
|
697
|
+
return -(stick.y || dpad.y) || (stick.x || dpad.x);
|
|
698
|
+
return vertical ? -(stick.y || dpad.y) : (stick.x || dpad.x);
|
|
699
|
+
}
|
|
700
|
+
const up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight';
|
|
701
|
+
if (both)
|
|
702
|
+
{
|
|
703
|
+
return keyIsDown(up) || keyIsDown(left) ? -1 :
|
|
704
|
+
keyIsDown(down) || keyIsDown(right) ? 1 : 0;
|
|
705
|
+
}
|
|
706
|
+
const back = vertical ? up : left;
|
|
707
|
+
const forward = vertical ? down : right;
|
|
708
|
+
return keyIsDown(back) ? -1 : keyIsDown(forward) ? 1 : 0;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Get other axis navigation direction from gamepad or keyboard
|
|
712
|
+
* @return {number} */
|
|
713
|
+
getNavigationOtherDirection()
|
|
714
|
+
{
|
|
715
|
+
if (uiSystem.navigationDirection === 2)
|
|
716
|
+
return 0; // other direction disabled
|
|
717
|
+
|
|
718
|
+
const vertical = uiSystem.navigationDirection === 1;
|
|
719
|
+
if (isUsingGamepad)
|
|
720
|
+
{
|
|
721
|
+
const stick = gamepadStick(0, gamepadPrimary);
|
|
722
|
+
const dpad = gamepadDpad(gamepadPrimary);
|
|
723
|
+
return !vertical ? (stick.y || dpad.y) : (stick.x || dpad.x);
|
|
724
|
+
}
|
|
725
|
+
// up is positive, as it is on the gamepad
|
|
726
|
+
if (!vertical)
|
|
727
|
+
return keyIsDown('ArrowUp') ? 1 : keyIsDown('ArrowDown') ? -1 : 0;
|
|
728
|
+
return keyIsDown('ArrowLeft') ? -1 : keyIsDown('ArrowRight') ? 1 : 0;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** Get if navigation button was pressed from gamepad or keyboard
|
|
732
|
+
* @return {boolean} */
|
|
733
|
+
getNavigationWasPressed()
|
|
734
|
+
{
|
|
735
|
+
return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
|
|
736
|
+
keyWasPressed('Space') || keyWasPressed('Enter');
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** Show a confirmation dialog with Yes/No buttons
|
|
740
|
+
* Centers the dialog on the screen with darkened background
|
|
741
|
+
* @param {string} [text] - The message to display
|
|
742
|
+
* @param {Function} [yesCallback] - Called when Yes is clicked
|
|
743
|
+
* @param {Function} [noCallback] - Called when No is clicked, or the exit key or gamepad B closes it
|
|
744
|
+
* @param {Vector2} [size] - Size of the confirmation dialog, the title and buttons are placed by it
|
|
745
|
+
* @param {string} [exitKey] - Key that closes the menu as No, gamepad B (button 1) does too
|
|
746
|
+
* @return {UIObject} The confirmation menu object
|
|
747
|
+
*/
|
|
748
|
+
showConfirmDialog(text='Are you sure?', yesCallback, noCallback, size=vec2(500,250), exitKey='Escape')
|
|
749
|
+
{
|
|
750
|
+
ASSERT(!uiSystem.confirmDialog, 'a confirm dialog is already open, check uiSystem.confirmDialog');
|
|
751
|
+
|
|
752
|
+
const savedNavigationDirection = uiSystem.navigationDirection;
|
|
753
|
+
const savedNavigationObject = uiSystem.navigationObject;
|
|
754
|
+
|
|
755
|
+
// allow both axes for navigation
|
|
756
|
+
uiSystem.navigationDirection = 2;
|
|
757
|
+
|
|
758
|
+
// confirm menu
|
|
759
|
+
const confirmMenu = new UIObject(vec2(), size);
|
|
760
|
+
uiSystem.confirmDialog = confirmMenu;
|
|
761
|
+
confirmMenu.onRender = ()=>
|
|
762
|
+
{
|
|
763
|
+
const backgroundColor = hsl(0,0,0,.7);
|
|
764
|
+
uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
|
|
765
|
+
}
|
|
766
|
+
let opened = false;
|
|
767
|
+
confirmMenu.onUpdate = ()=>
|
|
768
|
+
{
|
|
769
|
+
// not the press that opened it, a game may open it on the same back button, so its first update is skipped
|
|
770
|
+
// (not by frame number, which does not advance while paused, where a confirm is usually shown)
|
|
771
|
+
if (opened && (keyWasPressed(exitKey) || gamepadWasPressed(1)))
|
|
772
|
+
{
|
|
773
|
+
closeMenu(); // the exit key or gamepad B answers no
|
|
774
|
+
noCallback && noCallback();
|
|
775
|
+
}
|
|
776
|
+
opened = true;
|
|
777
|
+
}
|
|
778
|
+
confirmMenu.isMouseOverlapping = ()=> true; // always hover
|
|
779
|
+
|
|
780
|
+
// the title and buttons are laid out for the default 500 by 250 and scaled to the dialog's size, so a
|
|
781
|
+
// small or narrow one still fits them
|
|
782
|
+
const scaleX = size.x/500, scaleY = size.y/250;
|
|
783
|
+
const gap = 50, y = size.y/5;
|
|
784
|
+
const textTitle = new UIText(vec2(0,-y), vec2(size.x-gap, 70*scaleY), text);
|
|
785
|
+
confirmMenu.addChild(textTitle);
|
|
786
|
+
|
|
787
|
+
// yes button
|
|
788
|
+
const buttonSize = vec2(120*scaleX, 70*scaleY);
|
|
789
|
+
const buttonYes = new UIButton(vec2(-80*scaleX,y), buttonSize, 'Yes');
|
|
790
|
+
buttonYes.textHeight = 40*scaleY;
|
|
791
|
+
buttonYes.navigationIndex = 1;
|
|
792
|
+
buttonYes.hoverColor = hsl(0,1,.5);
|
|
793
|
+
buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
|
|
794
|
+
confirmMenu.addChild(buttonYes);
|
|
795
|
+
|
|
796
|
+
// no button
|
|
797
|
+
const buttonNo = new UIButton(vec2(80*scaleX,y), buttonSize, 'No');
|
|
798
|
+
buttonNo.textHeight = 40*scaleY;
|
|
799
|
+
buttonNo.navigationIndex = 2;
|
|
800
|
+
buttonNo.navigationAutoSelect = true;
|
|
801
|
+
buttonNo.onClick = ()=> { closeMenu(); noCallback && noCallback(); };
|
|
802
|
+
confirmMenu.addChild(buttonNo);
|
|
803
|
+
|
|
804
|
+
// return to normal navigation however the menu goes, by its buttons,
|
|
805
|
+
// destroyObjects or a call to its destroy, with the item that opened it selected again
|
|
806
|
+
const destroy = confirmMenu.destroy.bind(confirmMenu);
|
|
807
|
+
confirmMenu.destroy = ()=>
|
|
808
|
+
{
|
|
809
|
+
const closing = uiSystem.confirmDialog === confirmMenu;
|
|
810
|
+
if (closing)
|
|
811
|
+
{
|
|
812
|
+
uiSystem.confirmDialog = undefined;
|
|
813
|
+
uiSystem.navigationDirection = savedNavigationDirection;
|
|
814
|
+
}
|
|
815
|
+
destroy(); // its buttons go, and the one selected clears the selection
|
|
816
|
+
if (closing && savedNavigationObject && !savedNavigationObject.destroyed)
|
|
817
|
+
uiSystem.navigationObject = savedNavigationObject;
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
// close menu and clear the input that closed it
|
|
821
|
+
function closeMenu()
|
|
822
|
+
{
|
|
823
|
+
ASSERT(uiSystem.confirmDialog === confirmMenu);
|
|
824
|
+
confirmMenu.destroy();
|
|
825
|
+
inputClear();
|
|
826
|
+
}
|
|
827
|
+
return confirmMenu;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// whether a UI object can still be used: it and every parent visible, enabled and not destroyed
|
|
832
|
+
function uiObjectIsUsable(o)
|
|
833
|
+
{
|
|
834
|
+
for (; o; o = o.parent)
|
|
835
|
+
if (o.destroyed || !o.visible || o.disabled)
|
|
836
|
+
return false;
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// set the canvas shadow for the next draw, when it has a color and a blur or an offset; the caller clears it
|
|
841
|
+
function uiSetShadow(context, shadowColor, shadowBlur, shadowOffset)
|
|
842
|
+
{
|
|
843
|
+
if (shadowColor.a > 0 && (shadowBlur || shadowOffset.x || shadowOffset.y))
|
|
844
|
+
{
|
|
845
|
+
context.shadowColor = shadowColor.toString();
|
|
846
|
+
context.shadowBlur = shadowBlur;
|
|
847
|
+
context.shadowOffsetX = shadowOffset.x;
|
|
848
|
+
context.shadowOffsetY = shadowOffset.y;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// whether an open confirm dialog shuts a UI object out, it is modal: all but the dialog and what is in it
|
|
853
|
+
function uiObjectIsBehindDialog(o)
|
|
854
|
+
{
|
|
855
|
+
const dialog = uiSystem.confirmDialog;
|
|
856
|
+
if (!dialog)
|
|
857
|
+
return false;
|
|
858
|
+
for (; o; o = o.parent)
|
|
859
|
+
if (o === dialog)
|
|
860
|
+
return false;
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// whether a UI object is disabled, itself or through a parent
|
|
865
|
+
function uiObjectIsDisabled(o)
|
|
866
|
+
{
|
|
867
|
+
for (; o; o = o.parent)
|
|
868
|
+
if (o.disabled)
|
|
869
|
+
return true;
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// whether a UI object is hidden, itself or through a parent
|
|
874
|
+
function uiObjectIsHidden(o)
|
|
875
|
+
{
|
|
876
|
+
for (; o; o = o.parent)
|
|
877
|
+
if (!o.visible)
|
|
878
|
+
return true;
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
883
|
+
/**
|
|
884
|
+
* UI Object - Base level object for all UI elements
|
|
885
|
+
* @memberof UISystem */
|
|
886
|
+
class UIObject
|
|
887
|
+
{
|
|
888
|
+
/** Create a UIObject
|
|
889
|
+
* @param {Vector2} [pos=vec2()]
|
|
890
|
+
* @param {Vector2} [size=vec2()]
|
|
891
|
+
*/
|
|
892
|
+
constructor(pos=vec2(), size=vec2())
|
|
893
|
+
{
|
|
894
|
+
ASSERT(uiSystem, 'create the UISystemPlugin before UI objects');
|
|
895
|
+
ASSERT(isVector2(pos), 'ui object pos must be a vec2');
|
|
896
|
+
ASSERT(isVector2(size), 'ui object size must be a vec2');
|
|
897
|
+
|
|
898
|
+
/** @property {Vector2} - Position you set: an offset from this object's
|
|
899
|
+
* anchor point (the parent box, or the canvas for roots). This is the
|
|
900
|
+
* input that controls placement — set this, not nativePos. */
|
|
901
|
+
this.localPos = pos.copy();
|
|
902
|
+
/** @property {Vector2} - Resolved position in native UI space, recomputed
|
|
903
|
+
* every frame from localPos + anchor (and nativeHeight, if set). This is a
|
|
904
|
+
* derived output used for drawing and hit-testing; assigning to it has no
|
|
905
|
+
* effect since it is overwritten each frame. Set localPos instead. */
|
|
906
|
+
this.nativePos = pos.copy();
|
|
907
|
+
/** @property {Vector2} - Screen space size of the object */
|
|
908
|
+
this.size = size.copy();
|
|
909
|
+
/** @property {Color} - Color of the object */
|
|
910
|
+
this.color = uiSystem.defaultColor.copy();
|
|
911
|
+
/** @property {Color|undefined} - Color of the object when active, uses hoverColor if undefined
|
|
912
|
+
* @type {Color|undefined} */
|
|
913
|
+
this.activeColor = undefined;
|
|
914
|
+
/** @property {string|undefined} - Text for this ui object
|
|
915
|
+
* @type {string|undefined} */
|
|
916
|
+
this.text = undefined;
|
|
917
|
+
/** @property {Color} - Color when disabled */
|
|
918
|
+
this.disabledColor = uiSystem.defaultDisabledColor.copy();
|
|
919
|
+
/** @property {boolean} - Is this object disabled? */
|
|
920
|
+
this.disabled = false;
|
|
921
|
+
/** @property {Color} - Color for text */
|
|
922
|
+
this.textColor = uiSystem.defaultTextColor.copy();
|
|
923
|
+
/** @property {Color} - Color used when hovering over the object */
|
|
924
|
+
this.hoverColor = uiSystem.defaultHoverColor.copy();
|
|
925
|
+
/** @property {Color} - Color for line drawing */
|
|
926
|
+
this.lineColor = uiSystem.defaultLineColor.copy();
|
|
927
|
+
/** @property {Color|undefined} - Uses a gradient fill combined with color
|
|
928
|
+
* @type {Color|undefined} */
|
|
929
|
+
this.gradientColor = uiSystem.defaultGradientColor ? uiSystem.defaultGradientColor.copy() : undefined;
|
|
930
|
+
/** @property {number} - Width for line drawing */
|
|
931
|
+
this.lineWidth = uiSystem.defaultLineWidth;
|
|
932
|
+
/** @property {number} - Corner radius for rounded rects */
|
|
933
|
+
this.cornerRadius = uiSystem.defaultCornerRadius;
|
|
934
|
+
/** @property {TileSlice|undefined} - Style to draw with in place of the rectangle, tinted by the color for its
|
|
935
|
+
* state; its art has the frame, so the outline, corner radius and shadow are not drawn
|
|
936
|
+
* @type {TileSlice|undefined} */
|
|
937
|
+
this.slice = uiSystem.defaultSlice;
|
|
938
|
+
/** @property {string} - Font for this object */
|
|
939
|
+
this.font = uiSystem.defaultFont;
|
|
940
|
+
/** @property {string|undefined} - Font style for this object or undefined
|
|
941
|
+
* @type {string|undefined} */
|
|
942
|
+
this.fontStyle = undefined;
|
|
943
|
+
/** @property {number|undefined} - Override for text width
|
|
944
|
+
* @type {number|undefined} */
|
|
945
|
+
this.textWidth = undefined;
|
|
946
|
+
/** @property {number|undefined} - Override for text height
|
|
947
|
+
* @type {number|undefined} */
|
|
948
|
+
this.textHeight = undefined;
|
|
949
|
+
/** @property {number} - Scale text to fit in the object */
|
|
950
|
+
this.textFitScale = uiSystem.defaultTextFitScale;
|
|
951
|
+
/** @property {Vector2|undefined} - How much to offset the text shadow or undefined.
|
|
952
|
+
* UIText draws it in its shadowColor, which is clear by default, so set that too;
|
|
953
|
+
* the blurred shadow then shows as well unless shadowBlur and shadowOffset are zero.
|
|
954
|
+
* The other widgets draw it in black
|
|
955
|
+
* @type {Vector2|undefined} */
|
|
956
|
+
this.textShadow = undefined;
|
|
957
|
+
/** @property {Color} - Color for text line drawing */
|
|
958
|
+
this.textLineColor = uiSystem.defaultLineColor.copy();
|
|
959
|
+
/** @property {number} - Width for text line drawing */
|
|
960
|
+
this.textLineWidth = 0;
|
|
961
|
+
/** @property {boolean} - Should this object be drawn */
|
|
962
|
+
this.visible = true;
|
|
963
|
+
/** @property {Array<UIObject>} - A list of this object's children
|
|
964
|
+
* @type {Array<UIObject>} */
|
|
965
|
+
this.children = [];
|
|
966
|
+
/** @property {UIObject|undefined} - This object's parent, position is in parent space
|
|
967
|
+
* @type {UIObject|undefined} */
|
|
968
|
+
this.parent = undefined;
|
|
969
|
+
/** @property {number} - Added size to make small buttons easier to touch on mobile devices */
|
|
970
|
+
this.extraTouchSize = 0;
|
|
971
|
+
/** @property {Sound|undefined} - Sound when interactive element is pressed
|
|
972
|
+
* @type {Sound|undefined} */
|
|
973
|
+
this.soundPress = uiSystem.defaultSoundPress;
|
|
974
|
+
/** @property {Sound|undefined} - Sound when interactive element is released
|
|
975
|
+
* @type {Sound|undefined} */
|
|
976
|
+
this.soundRelease = uiSystem.defaultSoundRelease;
|
|
977
|
+
/** @property {Sound|undefined} - Sound when interactive element is clicked
|
|
978
|
+
* @type {Sound|undefined} */
|
|
979
|
+
this.soundClick = uiSystem.defaultSoundClick;
|
|
980
|
+
/** @property {boolean} - Is this element interactive */
|
|
981
|
+
this.interactive = false;
|
|
982
|
+
/** @property {boolean} - Activate when dragged over with mouse held down */
|
|
983
|
+
this.dragActivate = false;
|
|
984
|
+
/** @property {boolean} - True if this can be a hover object */
|
|
985
|
+
this.canBeHover = true;
|
|
986
|
+
/** @property {Color} - Color for shadow */
|
|
987
|
+
this.shadowColor = uiSystem.defaultShadowColor?.copy();
|
|
988
|
+
/** @property {number} - Size of shadow blur */
|
|
989
|
+
this.shadowBlur = uiSystem.defaultShadowBlur;
|
|
990
|
+
/** @property {Vector2} - Offset of shadow blur */
|
|
991
|
+
this.shadowOffset = uiSystem.defaultShadowOffset?.copy();
|
|
992
|
+
/** @property {number|undefined} - Optional navigation order index, lower values are selected first
|
|
993
|
+
* @type {number|undefined} */
|
|
994
|
+
this.navigationIndex = undefined;
|
|
995
|
+
/** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
|
|
996
|
+
this.navigationAutoSelect = false;
|
|
997
|
+
/** @property {Vector2} - Where on parent (or canvas if no parent) this object is anchored.
|
|
998
|
+
* Components in [-1, 1]: (0,0)=center, (-1,-1)=top-left, (1,1)=bottom-right.
|
|
999
|
+
* Also acts as self-pivot — e.g. (1,-1) puts your top-right corner at the anchor point. */
|
|
1000
|
+
this.anchor = vec2();
|
|
1001
|
+
/** @property {'left'|'center'|'right'} - Horizontal text alignment: left, center, or right
|
|
1002
|
+
* @type {'left'|'center'|'right'} */
|
|
1003
|
+
this.align = 'center';
|
|
1004
|
+
/** @property {boolean} - Has this object been destroyed? */
|
|
1005
|
+
this.destroyed = false;
|
|
1006
|
+
|
|
1007
|
+
uiSystem.uiObjects.push(this);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/** Add a child UIObject to this object, returns child for chaining
|
|
1011
|
+
* @param {UIObject} child
|
|
1012
|
+
* @return {UIObject} The child object added */
|
|
1013
|
+
addChild(child)
|
|
1014
|
+
{
|
|
1015
|
+
ASSERT(!child.parent && !this.children.includes(child), 'child already has a parent, removeChild it first');
|
|
1016
|
+
this.children.push(child);
|
|
1017
|
+
child.parent = this;
|
|
1018
|
+
return child;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/** Remove a child UIObject from this object
|
|
1022
|
+
* @param {UIObject} child */
|
|
1023
|
+
removeChild(child)
|
|
1024
|
+
{
|
|
1025
|
+
ASSERT(child.parent === this && this.children.includes(child));
|
|
1026
|
+
this.children.splice(this.children.indexOf(child), 1);
|
|
1027
|
+
child.parent = undefined;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/** Destroy this object, destroy its children, detach its parent, and mark it for removal */
|
|
1031
|
+
destroy()
|
|
1032
|
+
{
|
|
1033
|
+
if (this.destroyed)
|
|
1034
|
+
return;
|
|
1035
|
+
|
|
1036
|
+
// clear ui-system references that point at this object so events
|
|
1037
|
+
// don't keep firing against a destroyed target (especially the
|
|
1038
|
+
// keydown listener attached for keyInputObject)
|
|
1039
|
+
if (uiSystem.activeObject === this) uiSystem.activeObject = undefined;
|
|
1040
|
+
if (uiSystem.hoverObject === this) uiSystem.hoverObject = undefined;
|
|
1041
|
+
if (uiSystem.lastHoverObject === this) uiSystem.lastHoverObject = undefined;
|
|
1042
|
+
if (uiSystem.navigationObject === this) uiSystem.navigationObject = undefined;
|
|
1043
|
+
if (uiSystem.keyInputObject === this) uiSystem.keyInputObject = undefined;
|
|
1044
|
+
|
|
1045
|
+
// disconnect from parent and destroy children
|
|
1046
|
+
this.destroyed = true;
|
|
1047
|
+
this.parent?.removeChild(this);
|
|
1048
|
+
for (const child of this.children)
|
|
1049
|
+
{
|
|
1050
|
+
child.parent = undefined;
|
|
1051
|
+
child.destroy();
|
|
1052
|
+
}
|
|
1053
|
+
// clear references so destroyed children can be GC'd
|
|
1054
|
+
this.children.length = 0;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/** Check if the mouse is overlapping this ui object
|
|
1058
|
+
* @return {boolean} - True if overlapping */
|
|
1059
|
+
isMouseOverlapping()
|
|
1060
|
+
{
|
|
1061
|
+
if (!mouseInWindow) return false;
|
|
1062
|
+
|
|
1063
|
+
const size = !isTouchDevice ? this.size :
|
|
1064
|
+
this.size.add(vec2(this.extraTouchSize || 0));
|
|
1065
|
+
const pos = uiSystem.screenToNative(mousePosScreen);
|
|
1066
|
+
return isOverlapping(this.nativePos, size, pos);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/** Update the object, called automatically by plugin once each frame
|
|
1070
|
+
* @return {void} */
|
|
1071
|
+
update()
|
|
1072
|
+
{
|
|
1073
|
+
// call the custom update callback, which may destroy this object
|
|
1074
|
+
this.onUpdate();
|
|
1075
|
+
if (this.destroyed) return;
|
|
1076
|
+
|
|
1077
|
+
// unset active if disabled, itself or through a parent, and let go of it
|
|
1078
|
+
const disabled = uiObjectIsDisabled(this);
|
|
1079
|
+
if (disabled)
|
|
1080
|
+
{
|
|
1081
|
+
if (this === uiSystem.activeObject)
|
|
1082
|
+
{
|
|
1083
|
+
uiSystem.activeObject = undefined;
|
|
1084
|
+
if (this !== uiSystem.keyInputObject) // a field being edited was released when its press ended
|
|
1085
|
+
{
|
|
1086
|
+
this.onRelease();
|
|
1087
|
+
if (this.destroyed) return;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
// a field being edited is ended by the next UI update, through stopEditing, so onChange keeps its text
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (uiSystem.keyInputObject)
|
|
1094
|
+
return;
|
|
1095
|
+
|
|
1096
|
+
const wasHover = uiSystem.lastHoverObject === this;
|
|
1097
|
+
const isActive = this.isActiveObject();
|
|
1098
|
+
const mouseDown = mouseIsDown(0);
|
|
1099
|
+
// a press and release in one frame is a press too, as it is for a button that is not drag activated
|
|
1100
|
+
const mousePress = mouseWasPressed(0) || this.dragActivate && mouseDown;
|
|
1101
|
+
if (this.canBeHover)
|
|
1102
|
+
if (!uiSystem.navigationMode) // no mouse hover in navigation mode
|
|
1103
|
+
if (mousePress || isActive || (!mouseDown && !isTouchDevice))
|
|
1104
|
+
if (!uiSystem.hoverObject && this.isMouseOverlapping())
|
|
1105
|
+
uiSystem.hoverObject = this;
|
|
1106
|
+
if (this.isHoverObject())
|
|
1107
|
+
{
|
|
1108
|
+
if (!disabled)
|
|
1109
|
+
{
|
|
1110
|
+
if (mousePress)
|
|
1111
|
+
{
|
|
1112
|
+
if (this.interactive)
|
|
1113
|
+
{
|
|
1114
|
+
// drag activate sees the held mouse as a press every frame, only a new one counts
|
|
1115
|
+
const newPress = !this.dragActivate || !wasHover || mouseWasPressed(0);
|
|
1116
|
+
if (newPress)
|
|
1117
|
+
{
|
|
1118
|
+
this.onPress();
|
|
1119
|
+
if (this.destroyed) // the press took it away, and the press is used up
|
|
1120
|
+
return void inputClearKey(0, 0, false, true, false);
|
|
1121
|
+
this.soundPress && this.soundPress.play();
|
|
1122
|
+
if (uiSystem.activeObject && !isActive)
|
|
1123
|
+
uiSystem.activeObject.onRelease();
|
|
1124
|
+
}
|
|
1125
|
+
if (uiSystem.keyInputObject && uiSystem.keyInputObject !== this)
|
|
1126
|
+
{
|
|
1127
|
+
// its onPress started an edit elsewhere, the updates skip its release while it goes on
|
|
1128
|
+
this.onRelease();
|
|
1129
|
+
this.soundRelease && this.soundRelease.play();
|
|
1130
|
+
}
|
|
1131
|
+
else
|
|
1132
|
+
uiSystem.activeObject = this;
|
|
1133
|
+
|
|
1134
|
+
if (newPress && uiSystem.activateOnPress)
|
|
1135
|
+
this.click(!this.soundPress);
|
|
1136
|
+
if (this.destroyed)
|
|
1137
|
+
return void inputClearKey(0, 0, false, true, false);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (!uiSystem.activateOnPress)
|
|
1141
|
+
if (!mouseDown && this.isActiveObject() && this.interactive)
|
|
1142
|
+
this.click();
|
|
1143
|
+
if (this.destroyed) return;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// clear mouse was pressed state even when disabled
|
|
1147
|
+
mousePress && inputClearKey(0, 0, false, true, false);
|
|
1148
|
+
}
|
|
1149
|
+
if (isActive)
|
|
1150
|
+
if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
|
|
1151
|
+
{
|
|
1152
|
+
this.onRelease();
|
|
1153
|
+
this.soundRelease && this.soundRelease.play();
|
|
1154
|
+
uiSystem.activeObject = undefined;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// call enter/leave events
|
|
1158
|
+
if (this.isHoverObject() !== wasHover)
|
|
1159
|
+
this.isHoverObject() ? this.onEnter() : this.onLeave();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/** Render the object, called automatically by plugin once each frame */
|
|
1163
|
+
render()
|
|
1164
|
+
{
|
|
1165
|
+
// call the custom render callback
|
|
1166
|
+
this.onRender();
|
|
1167
|
+
|
|
1168
|
+
if (!this.size.x || !this.size.y) return;
|
|
1169
|
+
|
|
1170
|
+
const isNavigationObject = this.isNavigationObject();
|
|
1171
|
+
const disabled = this.interactive ? uiObjectIsDisabled(this) : this.disabled;
|
|
1172
|
+
const lineColor = isNavigationObject ? this.color :
|
|
1173
|
+
this.interactive && this.isActiveObject() && !disabled ?
|
|
1174
|
+
this.color : this.lineColor;
|
|
1175
|
+
const color = isNavigationObject ? this.hoverColor :
|
|
1176
|
+
disabled ? this.disabledColor :
|
|
1177
|
+
this.interactive ?
|
|
1178
|
+
this.isActiveObject() ? this.activeColor || this.hoverColor :
|
|
1179
|
+
this.isHoverObject() ? this.hoverColor :
|
|
1180
|
+
this.color : this.color;
|
|
1181
|
+
const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
|
|
1182
|
+
|
|
1183
|
+
if (this.slice)
|
|
1184
|
+
uiSystem.drawSlice(this.slice, this.nativePos, this.size, color);
|
|
1185
|
+
else
|
|
1186
|
+
uiSystem.drawRect(this.nativePos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor || CLEAR_BLACK, this.shadowBlur, this.shadowOffset);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/** Get the size for text with overrides and scale
|
|
1190
|
+
* @return {Vector2} */
|
|
1191
|
+
getTextSize()
|
|
1192
|
+
{
|
|
1193
|
+
// text fitted to the size shares its height between its lines, a set textHeight is the height of each line
|
|
1194
|
+
const lines = this.textHeight ? 1 : (this.text + '').split('\n').length;
|
|
1195
|
+
return vec2(
|
|
1196
|
+
this.textWidth || this.textFitScale * this.size.x,
|
|
1197
|
+
this.textHeight || this.textFitScale * this.size.y / lines);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/** Get where the text is drawn, the center, or the edge of the text area its align puts it against
|
|
1201
|
+
* @param {Vector2} textSize - From getTextSize
|
|
1202
|
+
* @return {Vector2} */
|
|
1203
|
+
getTextPos(textSize)
|
|
1204
|
+
{
|
|
1205
|
+
const side = this.align === 'left' ? -1 : this.align === 'right' ? 1 : 0;
|
|
1206
|
+
return this.nativePos.add(vec2(side * textSize.x / 2, 0));
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/** Called when the navigation button is pressed on this object */
|
|
1210
|
+
navigatePressed() { this.click(); }
|
|
1211
|
+
|
|
1212
|
+
/** @return {boolean} - Is the mouse hovering over this element */
|
|
1213
|
+
isHoverObject() { return uiSystem.hoverObject === this; }
|
|
1214
|
+
|
|
1215
|
+
/** @return {boolean} - Is the mouse held onto this element */
|
|
1216
|
+
isActiveObject() { return uiSystem.activeObject === this; }
|
|
1217
|
+
|
|
1218
|
+
/** @return {boolean} - Is the gamepad or keyboard navigation object */
|
|
1219
|
+
isNavigationObject() { return uiSystem.navigationObject === this; }
|
|
1220
|
+
|
|
1221
|
+
/** @return {boolean} - Is this object in keyboard input mode */
|
|
1222
|
+
isKeyInputObject() { return uiSystem.keyInputObject === this; }
|
|
1223
|
+
|
|
1224
|
+
/** @return {boolean} - Can it be interacted with, it and every parent visible and enabled */
|
|
1225
|
+
isInteractive() { return this.interactive && uiObjectIsUsable(this); }
|
|
1226
|
+
|
|
1227
|
+
/** Returns string containing info about this object for debugging
|
|
1228
|
+
* @return {string} */
|
|
1229
|
+
toString()
|
|
1230
|
+
{
|
|
1231
|
+
let text = 'type = ' + this.constructor.name;
|
|
1232
|
+
if (this.text)
|
|
1233
|
+
text += '\ntext = ' + this.text;
|
|
1234
|
+
if (this.nativePos.x || this.nativePos.y)
|
|
1235
|
+
text += '\nnativePos = ' + this.nativePos;
|
|
1236
|
+
if (this.localPos.x || this.localPos.y)
|
|
1237
|
+
text += '\nlocalPos = ' + this.localPos;
|
|
1238
|
+
if (this.size.x || this.size.y)
|
|
1239
|
+
text += '\nsize = ' + this.size;
|
|
1240
|
+
if (this.color)
|
|
1241
|
+
text += '\ncolor = ' + this.color;
|
|
1242
|
+
return text;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/** Called if uiDebug is enabled
|
|
1246
|
+
* @param {boolean} [visible] */
|
|
1247
|
+
renderDebug(visible=true)
|
|
1248
|
+
{
|
|
1249
|
+
// apply color based on state
|
|
1250
|
+
const color =
|
|
1251
|
+
!visible ? GREEN :
|
|
1252
|
+
this.isHoverObject() ? YELLOW :
|
|
1253
|
+
uiObjectIsDisabled(this) ? PURPLE :
|
|
1254
|
+
this.interactive ? RED : BLUE;
|
|
1255
|
+
uiSystem.drawRect(this.nativePos, this.size, CLEAR_BLACK, 4, color);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/** Internal function called when object is clicked
|
|
1259
|
+
* @param {boolean} [playSound] */
|
|
1260
|
+
click(playSound=true)
|
|
1261
|
+
{
|
|
1262
|
+
this.onClick();
|
|
1263
|
+
if (playSound && this.soundClick)
|
|
1264
|
+
this.soundClick.play();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/** Called each frame before object updates */
|
|
1268
|
+
onUpdate() {}
|
|
1269
|
+
|
|
1270
|
+
/** Called each frame before object renders */
|
|
1271
|
+
onRender() {}
|
|
1272
|
+
|
|
1273
|
+
/** Called when the mouse enters the object */
|
|
1274
|
+
onEnter() {}
|
|
1275
|
+
|
|
1276
|
+
/** Called when the mouse leaves the object */
|
|
1277
|
+
onLeave() {}
|
|
1278
|
+
|
|
1279
|
+
/** Called when the mouse is pressed while over the object */
|
|
1280
|
+
onPress() {}
|
|
1281
|
+
|
|
1282
|
+
/** Called when a press on it ends, wherever the mouse is let go, or when it is disabled or hidden while held, or
|
|
1283
|
+
* an edit takes the press; onClick is the one for a press let go over it */
|
|
1284
|
+
onRelease() {}
|
|
1285
|
+
|
|
1286
|
+
/** Called when user clicks on this object */
|
|
1287
|
+
onClick() {}
|
|
1288
|
+
|
|
1289
|
+
/** Called when the state of this object changes */
|
|
1290
|
+
onChange() {}
|
|
1291
|
+
|
|
1292
|
+
/** Called with each key while this is the keyInputObject
|
|
1293
|
+
* @param {KeyboardEvent} e */
|
|
1294
|
+
onKeyDown(e) {}
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1298
|
+
/**
|
|
1299
|
+
* UIText - A UI object that displays text
|
|
1300
|
+
* @extends UIObject
|
|
1301
|
+
* @memberof UISystem
|
|
1302
|
+
*/
|
|
1303
|
+
class UIText extends UIObject
|
|
1304
|
+
{
|
|
1305
|
+
/** Create a UIText object
|
|
1306
|
+
* @param {Vector2} [pos]
|
|
1307
|
+
* @param {Vector2} [size]
|
|
1308
|
+
* @param {string} [text]
|
|
1309
|
+
* @param {'left'|'center'|'right'} [align]
|
|
1310
|
+
* @param {string} [font=uiSystem.defaultFont]
|
|
1311
|
+
*/
|
|
1312
|
+
constructor(pos, size, text='', align='center', font=uiSystem.defaultFont)
|
|
1313
|
+
{
|
|
1314
|
+
super(pos, size);
|
|
1315
|
+
|
|
1316
|
+
ASSERT(isStringLike(text), 'ui text must be a string');
|
|
1317
|
+
ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
|
|
1318
|
+
ASSERT(isStringLike(font), 'ui text font must be a string');
|
|
1319
|
+
|
|
1320
|
+
// set properties
|
|
1321
|
+
this.text = text;
|
|
1322
|
+
this.align = align;
|
|
1323
|
+
this.font = font;
|
|
1324
|
+
|
|
1325
|
+
// text can not be a hover object by default
|
|
1326
|
+
this.canBeHover = false;
|
|
1327
|
+
|
|
1328
|
+
// no background by default
|
|
1329
|
+
this.color = CLEAR_BLACK.copy();
|
|
1330
|
+
this.shadowColor = CLEAR_BLACK.copy();
|
|
1331
|
+
/** @type {Color|undefined} */
|
|
1332
|
+
this.gradientColor = undefined;
|
|
1333
|
+
this.lineWidth = 0;
|
|
1334
|
+
|
|
1335
|
+
// use max fit scale by default
|
|
1336
|
+
this.textFitScale = 1;
|
|
1337
|
+
}
|
|
1338
|
+
render()
|
|
1339
|
+
{
|
|
1340
|
+
super.render();
|
|
1341
|
+
|
|
1342
|
+
// render the text
|
|
1343
|
+
const textSize = this.getTextSize();
|
|
1344
|
+
uiSystem.drawText(this.text, this.getTextPos(textSize), textSize, this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow, this.shadowColor || CLEAR_BLACK, this.shadowBlur, this.shadowOffset);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1349
|
+
/**
|
|
1350
|
+
* UITextInput - An editable text input field
|
|
1351
|
+
* - A simple text entry field that supports basic editing
|
|
1352
|
+
* - Suitable for short text input like names or numbers
|
|
1353
|
+
* - Reads a physical keyboard: no on-screen keyboard opens on touch devices, and IME composition and paste do not type,
|
|
1354
|
+
* use an HTML input element for those
|
|
1355
|
+
* @extends UIObject
|
|
1356
|
+
* @memberof UISystem
|
|
1357
|
+
*/
|
|
1358
|
+
class UITextInput extends UIObject
|
|
1359
|
+
{
|
|
1360
|
+
/** Create a UITextInput object
|
|
1361
|
+
* @param {Vector2} [pos]
|
|
1362
|
+
* @param {Vector2} [size]
|
|
1363
|
+
* @param {string} [text]
|
|
1364
|
+
*/
|
|
1365
|
+
constructor(pos, size, text='')
|
|
1366
|
+
{
|
|
1367
|
+
super(pos, size);
|
|
1368
|
+
|
|
1369
|
+
ASSERT(isStringLike(text), 'ui text must be a string');
|
|
1370
|
+
|
|
1371
|
+
/** @property {number} - Max length of input (0 = no limit) */
|
|
1372
|
+
this.maxLength = 0;
|
|
1373
|
+
|
|
1374
|
+
// set properties, as a string, which typing adds to
|
|
1375
|
+
this.text = text + '';
|
|
1376
|
+
this.interactive = true;
|
|
1377
|
+
this.canBeHover = true;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/** Start editing the text, called when it is clicked
|
|
1381
|
+
* @param {boolean} [playSound] */
|
|
1382
|
+
click(playSound=true)
|
|
1383
|
+
{
|
|
1384
|
+
// an edit in another field ends first, with its onChange, like a Tab to the next field
|
|
1385
|
+
const editing = uiSystem.keyInputObject;
|
|
1386
|
+
if (editing !== this && editing instanceof UITextInput)
|
|
1387
|
+
editing.stopEditing();
|
|
1388
|
+
|
|
1389
|
+
// start editing the text, the gamepad press that started it is used up so it does not stop it too
|
|
1390
|
+
uiSystem.keyInputObject = this;
|
|
1391
|
+
inputClearKey(0, gamepadPrimary+1, false, true, false);
|
|
1392
|
+
this.onClick();
|
|
1393
|
+
playSound && this.soundClick && this.soundClick.play();
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/** Stop editing the text */
|
|
1397
|
+
stopEditing()
|
|
1398
|
+
{
|
|
1399
|
+
if (!this.isKeyInputObject())
|
|
1400
|
+
return;
|
|
1401
|
+
|
|
1402
|
+
if (this.soundRelease)
|
|
1403
|
+
this.soundRelease.play();
|
|
1404
|
+
uiSystem.activeObject = undefined;
|
|
1405
|
+
uiSystem.keyInputObject = undefined;
|
|
1406
|
+
this.onChange();
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/** Key down event handler if this object is being edited
|
|
1410
|
+
* @param {KeyboardEvent} e */
|
|
1411
|
+
onKeyDown(e)
|
|
1412
|
+
{
|
|
1413
|
+
// named keys by key, so numpad Enter works as Enter
|
|
1414
|
+
const code = e.code, key = e.key;
|
|
1415
|
+
if (e.repeat && (key === 'Enter' || code === 'Space'))
|
|
1416
|
+
return; // a key held when editing began repeats, it should not type or stop editing
|
|
1417
|
+
this.text += ''; // a game may have set a number
|
|
1418
|
+
if (key === 'Backspace')
|
|
1419
|
+
this.text = [...this.text].slice(0, -1).join(''); // a whole character, an emoji is two code units
|
|
1420
|
+
else if (key === 'Enter' || key === 'Escape')
|
|
1421
|
+
this.stopEditing();
|
|
1422
|
+
else if (key.length === 1) // printable characters
|
|
1423
|
+
{
|
|
1424
|
+
// ctrl and cmd shortcuts do not type, but AltGr reports ctrl and alt and types characters like @
|
|
1425
|
+
if ((e.ctrlKey || e.metaKey) && !e.getModifierState?.('AltGraph'))
|
|
1426
|
+
return;
|
|
1427
|
+
if (!this.maxLength || this.text.length < this.maxLength)
|
|
1428
|
+
this.text += key;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
update()
|
|
1433
|
+
{
|
|
1434
|
+
super.update();
|
|
1435
|
+
|
|
1436
|
+
if (!this.isKeyInputObject())
|
|
1437
|
+
return;
|
|
1438
|
+
|
|
1439
|
+
// the gamepad's press stops editing and is used up, a click off the field is handled by the UI update
|
|
1440
|
+
if (gamepadWasPressed(0, gamepadPrimary))
|
|
1441
|
+
{
|
|
1442
|
+
this.stopEditing();
|
|
1443
|
+
inputClearKey(0, gamepadPrimary+1, false, true, false);
|
|
1444
|
+
}
|
|
1445
|
+
else if (mouseWasPressed(0))
|
|
1446
|
+
inputClearKey(0, 0, false, true, false); // a click inside is used up like any click on the UI
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
render()
|
|
1450
|
+
{
|
|
1451
|
+
super.render();
|
|
1452
|
+
|
|
1453
|
+
// draw the text scaled to fit
|
|
1454
|
+
const textSize = this.getTextSize();
|
|
1455
|
+
let text = this.text;
|
|
1456
|
+
if (this.isKeyInputObject()) // add a cursor to end of text
|
|
1457
|
+
text += timeReal%1 < .5 ? '█' : '░';
|
|
1458
|
+
uiSystem.drawText(text, this.getTextPos(textSize), textSize,
|
|
1459
|
+
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1464
|
+
/**
|
|
1465
|
+
* UITile - A UI object that displays a tile image
|
|
1466
|
+
* @extends UIObject
|
|
1467
|
+
* @memberof UISystem
|
|
1468
|
+
*/
|
|
1469
|
+
class UITile extends UIObject
|
|
1470
|
+
{
|
|
1471
|
+
/** Create a UITile object
|
|
1472
|
+
* @param {Vector2} pos
|
|
1473
|
+
* @param {Vector2} size
|
|
1474
|
+
* @param {TileInfo} tileInfo
|
|
1475
|
+
* @param {Color} [color=WHITE]
|
|
1476
|
+
* @param {number} [angle]
|
|
1477
|
+
* @param {boolean} [mirror]
|
|
1478
|
+
*/
|
|
1479
|
+
constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
|
|
1480
|
+
{
|
|
1481
|
+
super(pos, size);
|
|
1482
|
+
|
|
1483
|
+
ASSERT(tileInfo instanceof TileInfo, 'ui tile tileInfo must be a TileInfo');
|
|
1484
|
+
ASSERT(isColor(color), 'ui tile color must be a color');
|
|
1485
|
+
ASSERT(isNumber(angle), 'ui tile angle must be a number');
|
|
1486
|
+
|
|
1487
|
+
/** @property {TileInfo} - Tile image to use */
|
|
1488
|
+
this.tileInfo = tileInfo;
|
|
1489
|
+
/** @property {number} - Angle to rotate in radians */
|
|
1490
|
+
this.angle = angle;
|
|
1491
|
+
/** @property {boolean} - Should it be mirrored? */
|
|
1492
|
+
this.mirror = mirror;
|
|
1493
|
+
// set properties
|
|
1494
|
+
this.color = color.copy();
|
|
1495
|
+
|
|
1496
|
+
// no shadow by default
|
|
1497
|
+
this.shadowColor = CLEAR_BLACK.copy();
|
|
1498
|
+
}
|
|
1499
|
+
render()
|
|
1500
|
+
{
|
|
1501
|
+
// call the custom render callback
|
|
1502
|
+
this.onRender();
|
|
1503
|
+
|
|
1504
|
+
uiSystem.drawTile(this.nativePos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor || CLEAR_BLACK, this.shadowBlur, this.shadowOffset);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1509
|
+
/**
|
|
1510
|
+
* UIButton - A UI object that acts as a button
|
|
1511
|
+
* @extends UIObject
|
|
1512
|
+
* @memberof UISystem
|
|
1513
|
+
*/
|
|
1514
|
+
class UIButton extends UIObject
|
|
1515
|
+
{
|
|
1516
|
+
/** Create a UIButton object
|
|
1517
|
+
* @param {Vector2} [pos]
|
|
1518
|
+
* @param {Vector2} [size]
|
|
1519
|
+
* @param {string} [text]
|
|
1520
|
+
* @param {Color} [color=uiSystem.defaultButtonColor]
|
|
1521
|
+
*/
|
|
1522
|
+
constructor(pos, size, text='', color=uiSystem.defaultButtonColor)
|
|
1523
|
+
{
|
|
1524
|
+
super(pos, size);
|
|
1525
|
+
|
|
1526
|
+
ASSERT(isStringLike(text), 'ui button must be a string');
|
|
1527
|
+
ASSERT(isColor(color), 'ui button color must be a color');
|
|
1528
|
+
|
|
1529
|
+
/** @property {Vector2} - Text offset for the button */
|
|
1530
|
+
this.textOffset = vec2();
|
|
1531
|
+
|
|
1532
|
+
// set properties
|
|
1533
|
+
this.text = text;
|
|
1534
|
+
this.color = color.copy();
|
|
1535
|
+
this.interactive = true;
|
|
1536
|
+
}
|
|
1537
|
+
render()
|
|
1538
|
+
{
|
|
1539
|
+
super.render();
|
|
1540
|
+
|
|
1541
|
+
// draw the text scaled to fit
|
|
1542
|
+
const textSize = this.getTextSize();
|
|
1543
|
+
uiSystem.drawText(this.text, this.getTextPos(textSize).add(this.textOffset), textSize,
|
|
1544
|
+
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1549
|
+
/**
|
|
1550
|
+
* UICheckbox - A UI object that acts as a checkbox
|
|
1551
|
+
* @extends UIObject
|
|
1552
|
+
* @memberof UISystem
|
|
1553
|
+
*/
|
|
1554
|
+
class UICheckbox extends UIObject
|
|
1555
|
+
{
|
|
1556
|
+
/** Create a UICheckbox object
|
|
1557
|
+
* @param {Vector2} [pos]
|
|
1558
|
+
* @param {Vector2} [size]
|
|
1559
|
+
* @param {boolean} [checked]
|
|
1560
|
+
* @param {string} [text]
|
|
1561
|
+
* @param {Color} [color=uiSystem.defaultButtonColor]
|
|
1562
|
+
*/
|
|
1563
|
+
constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
|
|
1564
|
+
{
|
|
1565
|
+
super(pos, size);
|
|
1566
|
+
|
|
1567
|
+
ASSERT(isStringLike(text), 'ui checkbox must be a string');
|
|
1568
|
+
ASSERT(isColor(color), 'ui checkbox color must be a color');
|
|
1569
|
+
|
|
1570
|
+
/** @property {boolean} - Is the checkbox currently checked? */
|
|
1571
|
+
this.checked = checked;
|
|
1572
|
+
// set properties
|
|
1573
|
+
this.text = text;
|
|
1574
|
+
this.color = color.copy();
|
|
1575
|
+
this.interactive = true;
|
|
1576
|
+
}
|
|
1577
|
+
/** Toggle the checkbox, called when it is clicked
|
|
1578
|
+
* @param {boolean} [playSound] */
|
|
1579
|
+
click(playSound=true)
|
|
1580
|
+
{
|
|
1581
|
+
this.checked = !this.checked;
|
|
1582
|
+
super.click(playSound); // the click callback and sound, as every UI object has
|
|
1583
|
+
this.onChange();
|
|
1584
|
+
}
|
|
1585
|
+
render()
|
|
1586
|
+
{
|
|
1587
|
+
super.render();
|
|
1588
|
+
if (this.checked)
|
|
1589
|
+
{
|
|
1590
|
+
const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
|
|
1591
|
+
const length = lerp(1, 2**.5/2, p) / 2;
|
|
1592
|
+
let s = this.size.scale(length);
|
|
1593
|
+
uiSystem.drawLine(this.nativePos.add(s.multiply(vec2(-1))), this.nativePos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
|
|
1594
|
+
uiSystem.drawLine(this.nativePos.add(s.multiply(vec2(-1,1))), this.nativePos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// draw the text next to the checkbox
|
|
1598
|
+
const textSize = this.getTextSize();
|
|
1599
|
+
const pos = this.nativePos.add(vec2(this.size.x,0));
|
|
1600
|
+
uiSystem.drawText(this.text, pos, textSize,
|
|
1601
|
+
this.textColor, this.textLineWidth, this.textLineColor, 'left', this.font, this.fontStyle, false, this.textShadow);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1606
|
+
/**
|
|
1607
|
+
* UISlider - A UI object that acts as a slider or scrollbar
|
|
1608
|
+
* @extends UIObject
|
|
1609
|
+
* @memberof UISystem
|
|
1610
|
+
*/
|
|
1611
|
+
class UISlider extends UIObject
|
|
1612
|
+
{
|
|
1613
|
+
/** Create a UISlider object
|
|
1614
|
+
* @param {Vector2} [pos]
|
|
1615
|
+
* @param {Vector2} [size]
|
|
1616
|
+
* @param {number} [value]
|
|
1617
|
+
* @param {string} [text]
|
|
1618
|
+
* @param {Color} [color=uiSystem.defaultButtonColor]
|
|
1619
|
+
* @param {Color} [handleColor=WHITE]
|
|
1620
|
+
*/
|
|
1621
|
+
constructor(pos, size, value=.5, text='', color=uiSystem.defaultButtonColor, handleColor=WHITE)
|
|
1622
|
+
{
|
|
1623
|
+
super(pos, size);
|
|
1624
|
+
|
|
1625
|
+
ASSERT(isNumber(value), 'ui slider value must be a number');
|
|
1626
|
+
ASSERT(isStringLike(text), 'ui slider must be a string');
|
|
1627
|
+
ASSERT(isColor(color), 'ui slider color must be a color');
|
|
1628
|
+
ASSERT(isColor(handleColor), 'ui slider handleColor must be a color');
|
|
1629
|
+
|
|
1630
|
+
/** @property {number} - Current percentage value of this slider 0-1 */
|
|
1631
|
+
this.value = value;
|
|
1632
|
+
/** @property {Color} - Color for the handle part of the slider */
|
|
1633
|
+
this.handleColor = handleColor.copy();
|
|
1634
|
+
/** @property {TileSlice|undefined} - Style to draw the handle, or the fill, with; undefined for the slider's own
|
|
1635
|
+
* slice, or a rectangle when it has none
|
|
1636
|
+
* @type {TileSlice|undefined} */
|
|
1637
|
+
this.handleSlice = uiSystem.defaultHandleSlice;
|
|
1638
|
+
/** @property {boolean} - Should it fill up like a progress bar? */
|
|
1639
|
+
this.fillMode = false;
|
|
1640
|
+
|
|
1641
|
+
// set properties
|
|
1642
|
+
this.text = text;
|
|
1643
|
+
this.color = color.copy();
|
|
1644
|
+
this.interactive = true;
|
|
1645
|
+
}
|
|
1646
|
+
update()
|
|
1647
|
+
{
|
|
1648
|
+
super.update();
|
|
1649
|
+
if (!this.interactive)
|
|
1650
|
+
return;
|
|
1651
|
+
|
|
1652
|
+
const oldValue = this.value;
|
|
1653
|
+
if (this.isActiveObject())
|
|
1654
|
+
{
|
|
1655
|
+
// handle horizontal or vertical slider
|
|
1656
|
+
const isHorizontal = this.size.x > this.size.y;
|
|
1657
|
+
const handleSize = isHorizontal ? this.size.y : this.size.x;
|
|
1658
|
+
const barSize = isHorizontal ? this.size.x : this.size.y;
|
|
1659
|
+
const centerPos = isHorizontal ? this.nativePos.x : this.nativePos.y;
|
|
1660
|
+
|
|
1661
|
+
// check if value changed
|
|
1662
|
+
const handleWidth = barSize - handleSize;
|
|
1663
|
+
const p1 = centerPos - handleWidth/2;
|
|
1664
|
+
const p2 = centerPos + handleWidth/2;
|
|
1665
|
+
const p = uiSystem.screenToNative(mousePosScreen);
|
|
1666
|
+
this.value = isHorizontal ?
|
|
1667
|
+
percent(p.x, p1, p2) :
|
|
1668
|
+
percent(p.y, p2, p1);
|
|
1669
|
+
}
|
|
1670
|
+
else if (this.isNavigationObject())
|
|
1671
|
+
{
|
|
1672
|
+
// gamepad/keyboard navigation adjustment
|
|
1673
|
+
const direction = uiSystem.getNavigationOtherDirection();
|
|
1674
|
+
if (!uiSystem.navigationTimer.active())
|
|
1675
|
+
this.value = clamp(this.value + direction*.01);
|
|
1676
|
+
}
|
|
1677
|
+
this.value === oldValue || this.onChange();
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/** Draw the handle, or the fill of a fill mode slider, with the handle slice, the slider's own, or a rectangle
|
|
1681
|
+
* @param {Vector2} pos
|
|
1682
|
+
* @param {Vector2} size
|
|
1683
|
+
* @param {Color} color */
|
|
1684
|
+
drawHandle(pos, size, color)
|
|
1685
|
+
{
|
|
1686
|
+
const slice = this.handleSlice || this.slice;
|
|
1687
|
+
if (slice)
|
|
1688
|
+
uiSystem.drawSlice(slice, pos, size, color);
|
|
1689
|
+
else
|
|
1690
|
+
uiSystem.drawRect(pos, size, color, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
|
|
1691
|
+
}
|
|
1692
|
+
render()
|
|
1693
|
+
{
|
|
1694
|
+
super.render();
|
|
1695
|
+
|
|
1696
|
+
// handle horizontal or vertical slider
|
|
1697
|
+
const isHorizontal = this.size.x > this.size.y;
|
|
1698
|
+
const barWidth = isHorizontal ? this.size.x : this.size.y;
|
|
1699
|
+
const handleWidth = isHorizontal ? this.size.y : this.size.x;
|
|
1700
|
+
const color = uiObjectIsDisabled(this) ? this.disabledColor : this.handleColor;
|
|
1701
|
+
if (this.fillMode)
|
|
1702
|
+
{
|
|
1703
|
+
// draw progress bar
|
|
1704
|
+
const minWidth = min(handleWidth, this.cornerRadius * 2);
|
|
1705
|
+
const progressWidth = lerp(minWidth, barWidth, this.value);
|
|
1706
|
+
const p = (progressWidth - barWidth) * (isHorizontal ? .5 : -.5);
|
|
1707
|
+
const pos = this.nativePos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
|
|
1708
|
+
const drawSize = isHorizontal ?
|
|
1709
|
+
vec2(progressWidth, this.size.y) : vec2(this.size.x, progressWidth);
|
|
1710
|
+
this.drawHandle(pos, drawSize, color);
|
|
1711
|
+
}
|
|
1712
|
+
else
|
|
1713
|
+
{
|
|
1714
|
+
// draw the slider handle
|
|
1715
|
+
const value = clamp(isHorizontal ? this.value : 1 - this.value);
|
|
1716
|
+
const p = (barWidth - handleWidth) * (value - .5);
|
|
1717
|
+
const pos = this.nativePos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
|
|
1718
|
+
const drawSize = vec2(handleWidth);
|
|
1719
|
+
this.drawHandle(pos, drawSize, color);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// draw the text scaled to fit on the slider
|
|
1723
|
+
const textSize = this.getTextSize();
|
|
1724
|
+
uiSystem.drawText(this.text, this.getTextPos(textSize), textSize,
|
|
1725
|
+
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
1726
|
+
}
|
|
1727
|
+
navigatePressed()
|
|
1728
|
+
{
|
|
1729
|
+
// toggle value between 0 and 1
|
|
1730
|
+
this.value = this.value ? 0 : 1;
|
|
1731
|
+
this.onChange();
|
|
1732
|
+
this.onRelease();
|
|
1733
|
+
super.navigatePressed();
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1738
|
+
/**
|
|
1739
|
+
* UIVideo - A UI object that plays video
|
|
1740
|
+
* @extends UIObject
|
|
1741
|
+
* @example
|
|
1742
|
+
* // Create a video player UI object
|
|
1743
|
+
* const video = new UIVideo(vec2(400, 300), vec2(320, 240), 'video.mp4', true);
|
|
1744
|
+
* video.play();
|
|
1745
|
+
* @memberof UISystem
|
|
1746
|
+
*/
|
|
1747
|
+
class UIVideo extends UIObject
|
|
1748
|
+
{
|
|
1749
|
+
/** Create a video player UI object
|
|
1750
|
+
* @param {Vector2} pos
|
|
1751
|
+
* @param {Vector2} size
|
|
1752
|
+
* @param {string} src - Video file path or URL
|
|
1753
|
+
* @param {boolean} [autoplay=false] - Start playing immediately?
|
|
1754
|
+
* @param {boolean} [loop=false] - Loop the video?
|
|
1755
|
+
* @param {number} [volume=1] - Volume percent scaled by global volume (0-1)
|
|
1756
|
+
*/
|
|
1757
|
+
constructor(pos, size, src, autoplay=false, loop=false, volume=1)
|
|
1758
|
+
{
|
|
1759
|
+
super(pos, size || vec2());
|
|
1760
|
+
|
|
1761
|
+
ASSERT(isStringLike(src), 'video src must be a string');
|
|
1762
|
+
ASSERT(isNumber(volume), 'video volume must be a number');
|
|
1763
|
+
|
|
1764
|
+
this.color = BLACK.copy(); // default to black background
|
|
1765
|
+
this.cornerRadius = 0; // default to no corner radius
|
|
1766
|
+
|
|
1767
|
+
/** @property {number} - The video volume */
|
|
1768
|
+
this.volume = volume;
|
|
1769
|
+
|
|
1770
|
+
// create video element
|
|
1771
|
+
/** @property {HTMLVideoElement} - The video player */
|
|
1772
|
+
this.video = document.createElement('video');
|
|
1773
|
+
this.video.loop = loop;
|
|
1774
|
+
this.video.volume = clamp(volume * soundVolume);
|
|
1775
|
+
this.video.muted = !soundEnable;
|
|
1776
|
+
/** @private */
|
|
1777
|
+
this._soundEnabled = soundEnable; // the sound setting the mute last followed
|
|
1778
|
+
this.video.style.display = 'none';
|
|
1779
|
+
this.video.playsInline = true; // an iPhone would play it fullscreen and block its muted autoplay
|
|
1780
|
+
this.video.src = src;
|
|
1781
|
+
document.body.appendChild(this.video);
|
|
1782
|
+
autoplay && this.play();
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
/** Play or resume the video
|
|
1786
|
+
* @return {Promise<boolean>} Resolves true once playback starts, false if the browser refused it */
|
|
1787
|
+
async play()
|
|
1788
|
+
{
|
|
1789
|
+
// try to play the video, catch any errors (autoplay may be blocked)
|
|
1790
|
+
try { await this.video.play(); return true; }
|
|
1791
|
+
catch(e) { return false; }
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/** Pause the video */
|
|
1795
|
+
pause() { this.video.pause(); }
|
|
1796
|
+
|
|
1797
|
+
/** Stop and reset the video */
|
|
1798
|
+
stop() { this.video.pause(); this.video.currentTime = 0; }
|
|
1799
|
+
|
|
1800
|
+
/** Check if video is currently loading
|
|
1801
|
+
* @return {boolean} */
|
|
1802
|
+
isLoading()
|
|
1803
|
+
{ return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
|
|
1804
|
+
|
|
1805
|
+
/** Check if video is currently paused
|
|
1806
|
+
* @return {boolean} */
|
|
1807
|
+
isPaused() { return this.video.paused; }
|
|
1808
|
+
|
|
1809
|
+
/** Check if video is currently playing
|
|
1810
|
+
* @return {boolean} */
|
|
1811
|
+
isPlaying()
|
|
1812
|
+
{ return !this.isPaused() && !this.hasEnded() && !this.isLoading(); }
|
|
1813
|
+
|
|
1814
|
+
/** Check if video has ended playing
|
|
1815
|
+
* @return {boolean} */
|
|
1816
|
+
hasEnded() { return this.video.ended; }
|
|
1817
|
+
|
|
1818
|
+
/** Set volume (0-1)
|
|
1819
|
+
* @param {number} volume - Volume level (0-1) */
|
|
1820
|
+
setVolume(volume)
|
|
1821
|
+
{
|
|
1822
|
+
this.volume = volume;
|
|
1823
|
+
this.video.volume = clamp(volume * soundVolume);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/** Set playback speed
|
|
1827
|
+
* @param {number} rate - Playback rate multiplier */
|
|
1828
|
+
setPlaybackRate(rate) { this.video.playbackRate = rate; }
|
|
1829
|
+
|
|
1830
|
+
/** Get current time in seconds
|
|
1831
|
+
* @return {number} Current playback time */
|
|
1832
|
+
getCurrentTime() { return this.video.currentTime || 0; }
|
|
1833
|
+
|
|
1834
|
+
/** Get duration in seconds
|
|
1835
|
+
* @return {number} Total video duration */
|
|
1836
|
+
getDuration() { return this.video.duration || 0; }
|
|
1837
|
+
|
|
1838
|
+
/** Get the native video dimensions
|
|
1839
|
+
* @return {Vector2} Video dimensions (may be 0,0 if metadata not loaded) */
|
|
1840
|
+
getVideoSize()
|
|
1841
|
+
{ return vec2(this.video.videoWidth, this.video.videoHeight); }
|
|
1842
|
+
|
|
1843
|
+
/** Seek to time in seconds
|
|
1844
|
+
* @param {number} time - Time in seconds to seek to */
|
|
1845
|
+
setTime(time)
|
|
1846
|
+
{
|
|
1847
|
+
// before the metadata loads there is no duration to clamp to, and the time set is where it starts
|
|
1848
|
+
const duration = this.video.duration;
|
|
1849
|
+
this.video.currentTime = duration ? clamp(time, 0, duration) : max(time, 0);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
update()
|
|
1853
|
+
{
|
|
1854
|
+
super.update();
|
|
1855
|
+
|
|
1856
|
+
// update volume based on global sound volume, and the mute when sound is turned on or off,
|
|
1857
|
+
// so a game's own mute stays until then
|
|
1858
|
+
this.video.volume = clamp(this.volume * soundVolume);
|
|
1859
|
+
if (this._soundEnabled !== soundEnable)
|
|
1860
|
+
this.video.muted = !(this._soundEnabled = soundEnable);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
/** Render video to UI canvas */
|
|
1864
|
+
render()
|
|
1865
|
+
{
|
|
1866
|
+
super.render();
|
|
1867
|
+
|
|
1868
|
+
if (this.isLoading())
|
|
1869
|
+
return;
|
|
1870
|
+
const context = uiSystem.uiContext;
|
|
1871
|
+
const s = this.size;
|
|
1872
|
+
context.save();
|
|
1873
|
+
context.translate(this.nativePos.x, this.nativePos.y);
|
|
1874
|
+
context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
|
|
1875
|
+
context.restore();
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
/** Clean up video on destroy */
|
|
1879
|
+
destroy()
|
|
1880
|
+
{
|
|
1881
|
+
if (this.destroyed)
|
|
1882
|
+
return;
|
|
1883
|
+
|
|
1884
|
+
this.video.pause();
|
|
1885
|
+
this.video.remove();
|
|
1886
|
+
super.destroy();
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1891
|
+
/**
|
|
1892
|
+
* UILayout - A container that auto-arranges children in a vertical list, horizontal list, or grid
|
|
1893
|
+
* - Set columns to 1 for a vertical list (default)
|
|
1894
|
+
* - Set columns to children.length for a horizontal list
|
|
1895
|
+
* - Set columns to N (1 < N < children.length) for a grid with N columns
|
|
1896
|
+
* - Per-child sizing: each row's height = max child.size.y in that row, each column's width = max child.size.x in that column
|
|
1897
|
+
* - Children are positioned centered in their cell
|
|
1898
|
+
* - Container auto-sizes to fit children plus padding
|
|
1899
|
+
* - Hidden children take no cell, call relayout after showing or hiding one
|
|
1900
|
+
* @extends UIObject
|
|
1901
|
+
* @memberof UISystem
|
|
1902
|
+
*/
|
|
1903
|
+
class UILayout extends UIObject
|
|
1904
|
+
{
|
|
1905
|
+
/** Create a UILayout container that auto-arranges children
|
|
1906
|
+
* @param {Vector2} [pos]
|
|
1907
|
+
* @param {number} [columns=1] - Number of columns (1 = vertical list)
|
|
1908
|
+
* @param {number} [gap=10] - Space between children
|
|
1909
|
+
* @param {number} [padding=10] - Space between container border and children
|
|
1910
|
+
* @param {boolean} [transparent=false] - If true, draws no background, outline, or shadow
|
|
1911
|
+
*/
|
|
1912
|
+
constructor(pos, columns=1, gap=10, padding=10, transparent=false)
|
|
1913
|
+
{
|
|
1914
|
+
super(pos);
|
|
1915
|
+
|
|
1916
|
+
ASSERT(isNumber(columns) && columns >= 1, 'ui layout columns must be a number >= 1');
|
|
1917
|
+
ASSERT(isNumber(gap), 'ui layout gap must be a number');
|
|
1918
|
+
ASSERT(isNumber(padding), 'ui layout padding must be a number');
|
|
1919
|
+
|
|
1920
|
+
/** @property {number} - Number of columns in the layout */
|
|
1921
|
+
this.columns = columns;
|
|
1922
|
+
/** @property {number} - Space between children */
|
|
1923
|
+
this.gap = gap;
|
|
1924
|
+
/** @property {number} - Space between container border and children */
|
|
1925
|
+
this.padding = padding;
|
|
1926
|
+
|
|
1927
|
+
if (transparent)
|
|
1928
|
+
{
|
|
1929
|
+
// pure positioning helper - skip background, outline, and shadow,
|
|
1930
|
+
// and leave clicks in its gaps and padding to the game
|
|
1931
|
+
this.color = CLEAR_BLACK.copy();
|
|
1932
|
+
this.gradientColor = undefined;
|
|
1933
|
+
this.lineWidth = 0;
|
|
1934
|
+
this.shadowColor = CLEAR_BLACK.copy();
|
|
1935
|
+
this.canBeHover = false;
|
|
1936
|
+
}
|
|
1937
|
+
this.relayout();
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
/** Add a child UIObject and re-layout
|
|
1941
|
+
* @param {UIObject} child
|
|
1942
|
+
* @return {UIObject} The child object added */
|
|
1943
|
+
addChild(child)
|
|
1944
|
+
{
|
|
1945
|
+
super.addChild(child);
|
|
1946
|
+
this.relayout();
|
|
1947
|
+
return child;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
/** Remove a child UIObject and re-layout
|
|
1951
|
+
* @param {UIObject} child */
|
|
1952
|
+
removeChild(child)
|
|
1953
|
+
{
|
|
1954
|
+
super.removeChild(child);
|
|
1955
|
+
this.relayout();
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
/** Recompute child positions and container size based on per-child sizes.
|
|
1959
|
+
* Called automatically by addChild and removeChild. Call manually if you
|
|
1960
|
+
* mutate a child's size, show or hide one, or change columns, gap, or padding. */
|
|
1961
|
+
relayout()
|
|
1962
|
+
{
|
|
1963
|
+
// a hidden child leaves no gap, like a Continue button hidden when there is nothing to continue
|
|
1964
|
+
const children = this.children.filter(child=> child.visible);
|
|
1965
|
+
const n = children.length;
|
|
1966
|
+
if (!n)
|
|
1967
|
+
{
|
|
1968
|
+
this.size = vec2(this.padding * 2);
|
|
1969
|
+
this.parent instanceof UILayout && this.parent.relayout(); // a layout it is in takes its new size
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// whole columns, at least one, and no empty ones, they would push it off center
|
|
1974
|
+
const cols = clamp(floor(this.columns), 1, n);
|
|
1975
|
+
const rows = ceil(n / cols);
|
|
1976
|
+
const colWidths = new Array(cols).fill(0);
|
|
1977
|
+
const rowHeights = new Array(rows).fill(0);
|
|
1978
|
+
|
|
1979
|
+
// first pass: compute column widths and row heights from child sizes
|
|
1980
|
+
for (let i = 0; i < n; ++i)
|
|
1981
|
+
{
|
|
1982
|
+
const col = i % cols;
|
|
1983
|
+
const row = floor(i / cols);
|
|
1984
|
+
const child = children[i];
|
|
1985
|
+
colWidths[col] = max(colWidths[col], child.size.x);
|
|
1986
|
+
rowHeights[row] = max(rowHeights[row], child.size.y);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// total content size (sum of column widths/row heights plus gaps between them)
|
|
1990
|
+
let contentWidth = this.gap * (cols - 1);
|
|
1991
|
+
for (const w of colWidths) contentWidth += w;
|
|
1992
|
+
let contentHeight = this.gap * (rows - 1);
|
|
1993
|
+
for (const h of rowHeights) contentHeight += h;
|
|
1994
|
+
|
|
1995
|
+
// cumulative column/row offsets so positioning is O(n) not O(n^2)
|
|
1996
|
+
const colOffsets = new Array(cols);
|
|
1997
|
+
let xAcc = 0;
|
|
1998
|
+
for (let c = 0; c < cols; ++c)
|
|
1999
|
+
{
|
|
2000
|
+
colOffsets[c] = xAcc;
|
|
2001
|
+
xAcc += colWidths[c];
|
|
2002
|
+
}
|
|
2003
|
+
const rowOffsets = new Array(rows);
|
|
2004
|
+
let yAcc = 0;
|
|
2005
|
+
for (let r = 0; r < rows; ++r)
|
|
2006
|
+
{
|
|
2007
|
+
rowOffsets[r] = yAcc;
|
|
2008
|
+
yAcc += rowHeights[r];
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// second pass: position each child centered in its cell
|
|
2012
|
+
for (let i = 0; i < n; ++i)
|
|
2013
|
+
{
|
|
2014
|
+
const col = i % cols;
|
|
2015
|
+
const row = floor(i / cols);
|
|
2016
|
+
const x = -contentWidth/2 + colOffsets[col] + this.gap * col + colWidths[col] / 2;
|
|
2017
|
+
const y = -contentHeight/2 + rowOffsets[row] + this.gap * row + rowHeights[row] / 2;
|
|
2018
|
+
children[i].localPos = vec2(x, y);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// container size = content + padding on all sides
|
|
2022
|
+
this.size = vec2(contentWidth + this.padding * 2, contentHeight + this.padding * 2);
|
|
2023
|
+
this.parent instanceof UILayout && this.parent.relayout(); // a layout it is in takes its new size
|
|
2024
|
+
}
|
|
1688
2025
|
}
|