littlejsengine 1.15.3 → 1.15.9
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/dist/littlejs.d.ts +178 -55
- package/dist/littlejs.esm.js +1400 -746
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +1390 -745
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +1343 -714
- package/examples/box2d/game.js +1 -1
- package/examples/electron/index.html +2 -2
- package/examples/index.html +4 -3
- package/examples/particles/index.html +22 -6
- package/examples/platformer/game.js +7 -0
- package/examples/shorts/base.html +1 -1
- package/examples/shorts/box2dPool.js +9 -10
- package/examples/shorts/empty.js +1 -1
- package/examples/shorts/input.js +30 -25
- package/examples/shorts/tileLayer.js +2 -2
- package/examples/shorts/tiles.png +0 -0
- package/examples/shorts/uiSystem.js +26 -15
- package/examples/starter/index.html +2 -2
- package/examples/uiSystem/game.js +26 -17
- package/package.json +1 -1
- package/plugins/box2d.js +1 -1
- package/plugins/desktop.ini +2 -0
- package/plugins/pluginExport.js +2 -0
- package/plugins/uiSystem.js +476 -91
- package/src/engine.js +215 -193
- package/src/engineDebug.js +48 -32
- package/src/engineDraw.js +25 -8
- package/src/engineExport.js +8 -1
- package/src/engineInput.js +537 -380
- package/src/engineObject.js +24 -22
- package/src/engineParticles.js +8 -6
- package/src/engineSettings.js +35 -6
- package/src/engineTileLayer.js +3 -4
- package/src/engineUtilities.js +17 -1
package/dist/littlejs.esm.js
CHANGED
|
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
|
|
|
33
33
|
* @type {string}
|
|
34
34
|
* @default
|
|
35
35
|
* @memberof Engine */
|
|
36
|
-
const engineVersion = '1.15.
|
|
36
|
+
const engineVersion = '1.15.9';
|
|
37
37
|
|
|
38
38
|
/** Frames per second to update
|
|
39
39
|
* @type {number}
|
|
@@ -279,7 +279,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
279
279
|
o.destroyed || o.render();
|
|
280
280
|
gameRenderPost();
|
|
281
281
|
pluginList.forEach(plugin=>plugin.render?.());
|
|
282
|
-
|
|
282
|
+
inputRender();
|
|
283
283
|
debugRender();
|
|
284
284
|
glFlush();
|
|
285
285
|
debugVideoCaptureUpdate();
|
|
@@ -308,23 +308,48 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
308
308
|
|
|
309
309
|
if (canvasFixedSize.x)
|
|
310
310
|
{
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
311
|
+
// set canvas fixed size
|
|
312
|
+
mainCanvasSize = canvasFixedSize.copy();
|
|
313
|
+
|
|
314
|
+
// fit to window using css width and height
|
|
315
|
+
const innerAspect = innerWidth / innerHeight;
|
|
316
|
+
const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
|
|
317
|
+
const w = innerAspect < fixedAspect ? '100%' : '';
|
|
318
|
+
const h = innerAspect < fixedAspect ? '' : '100%';
|
|
319
|
+
overlayCanvas.style.width = mainCanvas.style.width = w;
|
|
320
|
+
overlayCanvas.style.height = mainCanvas.style.height = h;
|
|
321
|
+
if (glCanvas)
|
|
322
|
+
{
|
|
323
|
+
glCanvas.style.width = w;
|
|
324
|
+
glCanvas.style.height = h;
|
|
325
|
+
}
|
|
320
326
|
}
|
|
321
327
|
else
|
|
322
328
|
{
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
329
|
+
// get main canvas size based on window size
|
|
330
|
+
mainCanvasSize.x = min(innerWidth, canvasMaxSize.x);
|
|
331
|
+
mainCanvasSize.y = min(innerHeight, canvasMaxSize.y);
|
|
332
|
+
|
|
333
|
+
// responsive aspect ratio with native resolution
|
|
334
|
+
const innerAspect = innerWidth / innerHeight;
|
|
335
|
+
if (canvasMaxAspect && innerAspect > canvasMaxAspect)
|
|
336
|
+
{
|
|
337
|
+
// full height
|
|
338
|
+
const w = mainCanvasSize.y * canvasMaxAspect | 0;
|
|
339
|
+
mainCanvasSize.x = min(w, canvasMaxSize.x);
|
|
340
|
+
}
|
|
341
|
+
else if (innerAspect < canvasMinAspect)
|
|
342
|
+
{
|
|
343
|
+
// full width
|
|
344
|
+
const h = mainCanvasSize.x / canvasMinAspect | 0;
|
|
345
|
+
mainCanvasSize.y = min(h, canvasMaxSize.y);
|
|
346
|
+
}
|
|
326
347
|
}
|
|
327
348
|
|
|
349
|
+
// clear main and overlay canvas and set size
|
|
350
|
+
overlayCanvas.width = mainCanvas.width = mainCanvasSize.x;
|
|
351
|
+
overlayCanvas.height = mainCanvas.height = mainCanvasSize.y;
|
|
352
|
+
|
|
328
353
|
// apply the clear color to main canvas
|
|
329
354
|
if (canvasClearColor.a > 0)
|
|
330
355
|
{
|
|
@@ -333,13 +358,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
333
358
|
mainContext.fillStyle = BLACK.toString();
|
|
334
359
|
}
|
|
335
360
|
|
|
336
|
-
// clear overlay canvas and set size
|
|
337
|
-
overlayCanvas.width = mainCanvas.width;
|
|
338
|
-
overlayCanvas.height = mainCanvas.height;
|
|
339
|
-
|
|
340
|
-
// save canvas size
|
|
341
|
-
mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
|
|
342
|
-
|
|
343
361
|
// set default line join and cap
|
|
344
362
|
const lineJoin = 'round', lineCap = 'round';
|
|
345
363
|
mainContext.lineJoin = overlayContext.lineJoin = lineJoin;
|
|
@@ -363,7 +381,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
363
381
|
'user-select:none;' + // prevent hold to select
|
|
364
382
|
'-webkit-user-select:none;' + // compatibility for ios
|
|
365
383
|
'touch-action:none;' + // prevent mobile pinch to resize
|
|
366
|
-
'-webkit-touch-callout:none'
|
|
384
|
+
'-webkit-touch-callout:none'; // compatibility for ios
|
|
367
385
|
rootElement.style.cssText = styleRoot;
|
|
368
386
|
drawCanvas = mainCanvas = document.createElement('canvas');
|
|
369
387
|
rootElement.appendChild(mainCanvas);
|
|
@@ -380,7 +398,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
380
398
|
rootElement.appendChild(overlayCanvas);
|
|
381
399
|
overlayContext = overlayCanvas.getContext('2d');
|
|
382
400
|
|
|
383
|
-
//
|
|
401
|
+
// setup canvases
|
|
402
|
+
// transform way is still more reliable then flexbox or grid
|
|
384
403
|
const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
|
|
385
404
|
'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
|
|
386
405
|
mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
|
|
@@ -444,9 +463,176 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
444
463
|
// wait for all the promises to finish
|
|
445
464
|
await Promise.all(promises);
|
|
446
465
|
return startEngine();
|
|
466
|
+
|
|
467
|
+
///////////////////////////////////////////////////////////////////////////
|
|
468
|
+
// LittleJS Splash Screen
|
|
469
|
+
function drawEngineSplashScreen(t)
|
|
470
|
+
{
|
|
471
|
+
const x = overlayContext;
|
|
472
|
+
const w = overlayCanvas.width = innerWidth;
|
|
473
|
+
const h = overlayCanvas.height = innerHeight;
|
|
474
|
+
|
|
475
|
+
{
|
|
476
|
+
// background
|
|
477
|
+
const p3 = percent(t, 1, .8);
|
|
478
|
+
const p4 = percent(t, 0, .5);
|
|
479
|
+
const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
|
|
480
|
+
g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
|
|
481
|
+
g.addColorStop(1,hsl(0,0,0,p3).toString());
|
|
482
|
+
x.save();
|
|
483
|
+
x.fillStyle = g;
|
|
484
|
+
x.fillRect(0,0,w,h);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// draw LittleJS logo...
|
|
488
|
+
const rect = (X, Y, W, H, C)=>
|
|
489
|
+
{
|
|
490
|
+
x.beginPath();
|
|
491
|
+
x.rect(X,Y,W,C?H*p:H);
|
|
492
|
+
x.fillStyle = C;
|
|
493
|
+
C ? x.fill() : x.stroke();
|
|
494
|
+
};
|
|
495
|
+
const line = (X, Y, Z, W)=>
|
|
496
|
+
{
|
|
497
|
+
x.beginPath();
|
|
498
|
+
x.lineTo(X,Y);
|
|
499
|
+
x.lineTo(Z,W);
|
|
500
|
+
x.stroke();
|
|
501
|
+
};
|
|
502
|
+
const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
|
|
503
|
+
{
|
|
504
|
+
const D = (A+B)/2, E = p*(B-A)/2;
|
|
505
|
+
x.beginPath();
|
|
506
|
+
F && x.lineTo(X,Y);
|
|
507
|
+
x.arc(X,Y,R,D-E,D+E);
|
|
508
|
+
x.fillStyle = C;
|
|
509
|
+
C ? x.fill() : x.stroke();
|
|
510
|
+
};
|
|
511
|
+
const color = (c=0, l=0) =>
|
|
512
|
+
hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();
|
|
513
|
+
const alpha = wave(1,1,t);
|
|
514
|
+
const p = percent(alpha, .1, .5);
|
|
515
|
+
|
|
516
|
+
// setup
|
|
517
|
+
x.translate(w/2,h/2);
|
|
518
|
+
const size = min(6, min(w,h)/99); // fit to screen
|
|
519
|
+
x.scale(size,size);
|
|
520
|
+
x.translate(-40,-35);
|
|
521
|
+
x.lineJoin = x.lineCap = 'round';
|
|
522
|
+
x.lineWidth = .1 + p*1.9;
|
|
523
|
+
|
|
524
|
+
// drawing effect
|
|
525
|
+
const p2 = percent(alpha,.1,1);
|
|
526
|
+
x.setLineDash([99*p2,99]);
|
|
527
|
+
|
|
528
|
+
// cab top
|
|
529
|
+
rect(7,16,18,-8,color(2,2));
|
|
530
|
+
rect(7,8,18,4,color(2,3));
|
|
531
|
+
rect(25,8,8,8,color(2,1));
|
|
532
|
+
rect(25,8,-18,8);
|
|
533
|
+
rect(25,8,8,8);
|
|
534
|
+
|
|
535
|
+
// cab
|
|
536
|
+
rect(25,16,7,23,color());
|
|
537
|
+
rect(11,39,14,-23,color(1,1));
|
|
538
|
+
rect(11,16,14,18,color(1,2));
|
|
539
|
+
rect(11,16,14,8,color(1,3));
|
|
540
|
+
rect(25,16,-14,24);
|
|
541
|
+
|
|
542
|
+
// cab window
|
|
543
|
+
rect(15,29,6,-9,color(2,2));
|
|
544
|
+
circle(15,21,5,0,PI/2,color(2,4),1);
|
|
545
|
+
rect(21,21,-6,9);
|
|
546
|
+
|
|
547
|
+
// little stack
|
|
548
|
+
rect(37,14,9,6,color(3,2));
|
|
549
|
+
rect(37,14,4.5,6,color(3,3));
|
|
550
|
+
rect(37,14,9,6);
|
|
551
|
+
|
|
552
|
+
// big stack
|
|
553
|
+
rect(50,20,10,-8,color(0,1));
|
|
554
|
+
rect(50,20,6.5,-8,color(0,2));
|
|
555
|
+
rect(50,20,3.5,-8,color(0,3));
|
|
556
|
+
rect(50,20,10,-8);
|
|
557
|
+
circle(55,2,11.4,.5,PI-.5,color(3,3));
|
|
558
|
+
circle(55,2,11.4,.5,PI/2,color(3,2),1);
|
|
559
|
+
circle(55,2,11.4,.5,PI-.5);
|
|
560
|
+
rect(45,7,20,-7,color(0,2));
|
|
561
|
+
rect(45,-1,20,4,color(0,3));
|
|
562
|
+
rect(45,-1,20,8);
|
|
563
|
+
|
|
564
|
+
// engine
|
|
565
|
+
for (let i=5; i--;)
|
|
566
|
+
{
|
|
567
|
+
// stagger radius to fix slight seam
|
|
568
|
+
circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
|
|
569
|
+
circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
|
|
570
|
+
circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// engine outline
|
|
574
|
+
circle(36,30,10,PI/2,PI*3/2);
|
|
575
|
+
circle(48,30,10,PI/2,PI*3/2);
|
|
576
|
+
circle(60,30,10);
|
|
577
|
+
line(36,20,60,20);
|
|
578
|
+
|
|
579
|
+
// engine front light
|
|
580
|
+
circle(60,30,4,PI,3*PI,color(3,2));
|
|
581
|
+
circle(60,30,4,PI,2*PI,color(3,3));
|
|
582
|
+
circle(60,30,4,PI,3*PI);
|
|
583
|
+
|
|
584
|
+
// front brush
|
|
585
|
+
for (let i=6; i--;)
|
|
586
|
+
{
|
|
587
|
+
x.beginPath();
|
|
588
|
+
x.lineTo(53,54);
|
|
589
|
+
x.lineTo(53,40);
|
|
590
|
+
x.lineTo(53+(1+i*2.9)*p,40);
|
|
591
|
+
x.lineTo(53+(4+i*3.5)*p,54);
|
|
592
|
+
x.fillStyle = color(0,i%2+2);
|
|
593
|
+
x.fill();
|
|
594
|
+
i%2 && x.stroke();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// wheels
|
|
598
|
+
rect(6,40,5,5);
|
|
599
|
+
rect(6,40,5,5,color());
|
|
600
|
+
rect(15,54,38,-14,color());
|
|
601
|
+
for (let i=3; i--;)
|
|
602
|
+
for (let j=2; j--;)
|
|
603
|
+
{
|
|
604
|
+
circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
|
|
605
|
+
x.stroke();
|
|
606
|
+
circle(15*i+15,47,j?7:1,0,PI,color(i,2));
|
|
607
|
+
x.stroke();
|
|
608
|
+
}
|
|
609
|
+
line(6,40,68,40); // center
|
|
610
|
+
line(77,54,4,54); // bottom
|
|
611
|
+
|
|
612
|
+
// draw engine name
|
|
613
|
+
const s = engineName;
|
|
614
|
+
x.font = '900 16px arial';
|
|
615
|
+
x.textAlign = 'center';
|
|
616
|
+
x.textBaseline = 'top';
|
|
617
|
+
x.lineWidth = .1+p*3.9;
|
|
618
|
+
let w2 = 0;
|
|
619
|
+
for (let i=0; i<s.length; ++i)
|
|
620
|
+
w2 += x.measureText(s[i]).width;
|
|
621
|
+
for (let j=2; j--;)
|
|
622
|
+
for (let i=0, X=41-w2/2; i<s.length; ++i)
|
|
623
|
+
{
|
|
624
|
+
x.fillStyle = color(i,2);
|
|
625
|
+
const w = x.measureText(s[i]).width;
|
|
626
|
+
x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
|
|
627
|
+
X += w;
|
|
628
|
+
}
|
|
629
|
+
x.restore();
|
|
630
|
+
}
|
|
631
|
+
///////////////////////////////////////////////////////////////////////////
|
|
447
632
|
}
|
|
448
633
|
|
|
449
634
|
/** Update each engine object, remove destroyed objects, and update time
|
|
635
|
+
* can be called manually if objects need to be updated outside of main loop
|
|
450
636
|
* @memberof Engine */
|
|
451
637
|
function engineObjectsUpdate()
|
|
452
638
|
{
|
|
@@ -498,18 +684,21 @@ function engineObjectsDestroy()
|
|
|
498
684
|
function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
499
685
|
{
|
|
500
686
|
const collectedObjects = [];
|
|
501
|
-
if (!pos)
|
|
687
|
+
if (!pos)
|
|
502
688
|
{
|
|
689
|
+
// all objects
|
|
503
690
|
for (const o of objects)
|
|
504
691
|
collectedObjects.push(o);
|
|
505
692
|
}
|
|
506
|
-
else if (size instanceof Vector2)
|
|
693
|
+
else if (size instanceof Vector2)
|
|
507
694
|
{
|
|
695
|
+
// bounding box test
|
|
508
696
|
for (const o of objects)
|
|
509
697
|
o.isOverlapping(pos, size) && collectedObjects.push(o);
|
|
510
698
|
}
|
|
511
|
-
else
|
|
699
|
+
else
|
|
512
700
|
{
|
|
701
|
+
// circle test
|
|
513
702
|
const sizeSquared = size*size;
|
|
514
703
|
for (const o of objects)
|
|
515
704
|
pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
|
|
@@ -552,173 +741,6 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
|
|
|
552
741
|
|
|
553
742
|
debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
|
|
554
743
|
return hitObjects;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
558
|
-
// LittleJS splash screen and logo
|
|
559
|
-
|
|
560
|
-
function drawEngineSplashScreen(t)
|
|
561
|
-
{
|
|
562
|
-
const x = overlayContext;
|
|
563
|
-
const w = overlayCanvas.width = innerWidth;
|
|
564
|
-
const h = overlayCanvas.height = innerHeight;
|
|
565
|
-
|
|
566
|
-
{
|
|
567
|
-
// background
|
|
568
|
-
const p3 = percent(t, 1, .8);
|
|
569
|
-
const p4 = percent(t, 0, .5);
|
|
570
|
-
const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
|
|
571
|
-
g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
|
|
572
|
-
g.addColorStop(1,hsl(0,0,0,p3).toString());
|
|
573
|
-
x.save();
|
|
574
|
-
x.fillStyle = g;
|
|
575
|
-
x.fillRect(0,0,w,h);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// draw LittleJS logo...
|
|
579
|
-
const rect = (X, Y, W, H, C)=>
|
|
580
|
-
{
|
|
581
|
-
x.beginPath();
|
|
582
|
-
x.rect(X,Y,W,C?H*p:H);
|
|
583
|
-
x.fillStyle = C;
|
|
584
|
-
C ? x.fill() : x.stroke();
|
|
585
|
-
};
|
|
586
|
-
const line = (X, Y, Z, W)=>
|
|
587
|
-
{
|
|
588
|
-
x.beginPath();
|
|
589
|
-
x.lineTo(X,Y);
|
|
590
|
-
x.lineTo(Z,W);
|
|
591
|
-
x.stroke();
|
|
592
|
-
};
|
|
593
|
-
const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
|
|
594
|
-
{
|
|
595
|
-
const D = (A+B)/2, E = p*(B-A)/2;
|
|
596
|
-
x.beginPath();
|
|
597
|
-
F && x.lineTo(X,Y);
|
|
598
|
-
x.arc(X,Y,R,D-E,D+E);
|
|
599
|
-
x.fillStyle = C;
|
|
600
|
-
C ? x.fill() : x.stroke();
|
|
601
|
-
};
|
|
602
|
-
const color = (c=0, l=0) =>
|
|
603
|
-
hsl([.98,.3,.57,.14][c%4]-10,.8,[0,.3,.5,.8,.9][l]).toString();
|
|
604
|
-
const alpha = wave(1,1,t);
|
|
605
|
-
const p = percent(alpha, .1, .5);
|
|
606
|
-
|
|
607
|
-
// setup
|
|
608
|
-
x.translate(w/2,h/2);
|
|
609
|
-
const size = min(6, min(w,h)/99); // fit to screen
|
|
610
|
-
x.scale(size,size);
|
|
611
|
-
x.translate(-40,-35);
|
|
612
|
-
x.lineJoin = x.lineCap = 'round';
|
|
613
|
-
x.lineWidth = .1 + p*1.9;
|
|
614
|
-
|
|
615
|
-
// drawing effect
|
|
616
|
-
const p2 = percent(alpha,.1,1);
|
|
617
|
-
x.setLineDash([99*p2,99]);
|
|
618
|
-
|
|
619
|
-
// cab top
|
|
620
|
-
rect(7,16,18,-8,color(2,2));
|
|
621
|
-
rect(7,8,18,4,color(2,3));
|
|
622
|
-
rect(25,8,8,8,color(2,1));
|
|
623
|
-
rect(25,8,-18,8);
|
|
624
|
-
rect(25,8,8,8);
|
|
625
|
-
|
|
626
|
-
// cab
|
|
627
|
-
rect(25,16,7,23,color());
|
|
628
|
-
rect(11,39,14,-23,color(1,1));
|
|
629
|
-
rect(11,16,14,18,color(1,2));
|
|
630
|
-
rect(11,16,14,8,color(1,3));
|
|
631
|
-
rect(25,16,-14,24);
|
|
632
|
-
|
|
633
|
-
// cab window
|
|
634
|
-
rect(15,29,6,-9,color(2,2));
|
|
635
|
-
circle(15,21,5,0,PI/2,color(2,4),1);
|
|
636
|
-
rect(21,21,-6,9);
|
|
637
|
-
|
|
638
|
-
// little stack
|
|
639
|
-
rect(37,14,9,6,color(3,2));
|
|
640
|
-
rect(37,14,4.5,6,color(3,3));
|
|
641
|
-
rect(37,14,9,6);
|
|
642
|
-
|
|
643
|
-
// big stack
|
|
644
|
-
rect(50,20,10,-8,color(0,1));
|
|
645
|
-
rect(50,20,6.5,-8,color(0,2));
|
|
646
|
-
rect(50,20,3.5,-8,color(0,3));
|
|
647
|
-
rect(50,20,10,-8);
|
|
648
|
-
circle(55,2,11.4,.5,PI-.5,color(3,3));
|
|
649
|
-
circle(55,2,11.4,.5,PI/2,color(3,2),1);
|
|
650
|
-
circle(55,2,11.4,.5,PI-.5);
|
|
651
|
-
rect(45,7,20,-7,color(0,2));
|
|
652
|
-
rect(45,-1,20,4,color(0,3));
|
|
653
|
-
rect(45,-1,20,8);
|
|
654
|
-
|
|
655
|
-
// engine
|
|
656
|
-
for (let i=5; i--;)
|
|
657
|
-
{
|
|
658
|
-
// stagger radius to fix slight seam
|
|
659
|
-
circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
|
|
660
|
-
circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
|
|
661
|
-
circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// engine outline
|
|
665
|
-
circle(36,30,10,PI/2,PI*3/2);
|
|
666
|
-
circle(48,30,10,PI/2,PI*3/2);
|
|
667
|
-
circle(60,30,10);
|
|
668
|
-
line(36,20,60,20);
|
|
669
|
-
|
|
670
|
-
// engine front light
|
|
671
|
-
circle(60,30,4,PI,3*PI,color(3,2));
|
|
672
|
-
circle(60,30,4,PI,2*PI,color(3,3));
|
|
673
|
-
circle(60,30,4,PI,3*PI);
|
|
674
|
-
|
|
675
|
-
// front brush
|
|
676
|
-
for (let i=6; i--;)
|
|
677
|
-
{
|
|
678
|
-
x.beginPath();
|
|
679
|
-
x.lineTo(53,54);
|
|
680
|
-
x.lineTo(53,40);
|
|
681
|
-
x.lineTo(53+(1+i*2.9)*p,40);
|
|
682
|
-
x.lineTo(53+(4+i*3.5)*p,54);
|
|
683
|
-
x.fillStyle = color(0,i%2+2);
|
|
684
|
-
x.fill();
|
|
685
|
-
i%2 && x.stroke();
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// wheels
|
|
689
|
-
rect(6,40,5,5);
|
|
690
|
-
rect(6,40,5,5,color());
|
|
691
|
-
rect(15,54,38,-14,color());
|
|
692
|
-
for (let i=3; i--;)
|
|
693
|
-
for (let j=2; j--;)
|
|
694
|
-
{
|
|
695
|
-
circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
|
|
696
|
-
x.stroke();
|
|
697
|
-
circle(15*i+15,47,j?7:1,0,PI,color(i,2));
|
|
698
|
-
x.stroke();
|
|
699
|
-
}
|
|
700
|
-
line(6,40,68,40); // center
|
|
701
|
-
line(77,54,4,54); // bottom
|
|
702
|
-
|
|
703
|
-
// draw engine name
|
|
704
|
-
const s = engineName;
|
|
705
|
-
x.font = '900 16px arial';
|
|
706
|
-
x.textAlign = 'center';
|
|
707
|
-
x.textBaseline = 'top';
|
|
708
|
-
x.lineWidth = .1+p*3.9;
|
|
709
|
-
let w2 = 0;
|
|
710
|
-
for (let i=0; i<s.length; ++i)
|
|
711
|
-
w2 += x.measureText(s[i]).width;
|
|
712
|
-
for (let j=2; j--;)
|
|
713
|
-
for (let i=0, X=41-w2/2; i<s.length; ++i)
|
|
714
|
-
{
|
|
715
|
-
x.fillStyle = color(i,2);
|
|
716
|
-
const w = x.measureText(s[i]).width;
|
|
717
|
-
x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
|
|
718
|
-
X += w;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
x.restore();
|
|
722
744
|
}
|
|
723
745
|
/**
|
|
724
746
|
* LittleJS Debug System
|
|
@@ -914,7 +936,7 @@ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
|
|
|
914
936
|
}
|
|
915
937
|
|
|
916
938
|
/** Draw a debug axis aligned bounding box in world space
|
|
917
|
-
* @param {string} text
|
|
939
|
+
* @param {string|number} text
|
|
918
940
|
* @param {Vector2} pos
|
|
919
941
|
* @param {number} [size]
|
|
920
942
|
* @param {Color|string} [color]
|
|
@@ -1072,34 +1094,46 @@ function debugRender()
|
|
|
1072
1094
|
debugTakeScreenshot = 0;
|
|
1073
1095
|
}
|
|
1074
1096
|
|
|
1075
|
-
if (debugGamepads && gamepadsEnable
|
|
1097
|
+
if (debugGamepads && gamepadsEnable)
|
|
1076
1098
|
{
|
|
1077
|
-
//
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1099
|
+
// draw gamepads
|
|
1100
|
+
const maxGamepads = 8;
|
|
1101
|
+
let gamepadConnectedCount = 0;
|
|
1102
|
+
for (let i = 0; i < maxGamepads; i++)
|
|
1103
|
+
gamepadConnected(i) && gamepadConnectedCount++;
|
|
1104
|
+
|
|
1105
|
+
for (let i = 0; i < maxGamepads; i++)
|
|
1080
1106
|
{
|
|
1081
|
-
|
|
1082
|
-
|
|
1107
|
+
if (!gamepadConnected(i))
|
|
1108
|
+
continue;
|
|
1109
|
+
|
|
1110
|
+
const stickScale = 1;
|
|
1111
|
+
const buttonScale = .2;
|
|
1112
|
+
const cornerPos = cameraPos.add(vec2(-stickScale*2, ((gamepadConnectedCount-1)/2-i)*stickScale*3));
|
|
1113
|
+
debugText(i, cornerPos.add(vec2(-stickScale, stickScale)), 1);
|
|
1114
|
+
if (i === gamepadPrimary)
|
|
1115
|
+
debugText('Main', cornerPos.add(vec2(-stickScale*2, 0)),1, '#0f0');
|
|
1116
|
+
|
|
1117
|
+
// read analog sticks
|
|
1118
|
+
const stickCount = gamepadStickData[i].length;
|
|
1119
|
+
for (let j = 0; j < stickCount; j++)
|
|
1083
1120
|
{
|
|
1084
|
-
const
|
|
1085
|
-
const
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
debugCircle(drawPos, buttonScale*2, pressed ? '#f00' : '#fff7', 0, true);
|
|
1101
|
-
debugText(''+j, drawPos, .2);
|
|
1102
|
-
}
|
|
1121
|
+
const stick = gamepadStick(j, i);
|
|
1122
|
+
const drawPos = cornerPos.add(vec2(j*stickScale*2, 0));
|
|
1123
|
+
const stickPos = drawPos.add(stick.scale(stickScale));
|
|
1124
|
+
debugCircle(drawPos, stickScale*2, '#fff7',0,true);
|
|
1125
|
+
debugLine(drawPos, stickPos, '#f00');
|
|
1126
|
+
debugText(j, drawPos, .3);
|
|
1127
|
+
debugPoint(stickPos, '#f00');
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const buttonCount = inputData[i+1].length;
|
|
1131
|
+
for (let j = 0; j < buttonCount; j++)
|
|
1132
|
+
{
|
|
1133
|
+
const drawPos = cornerPos.add(vec2(j*buttonScale*2, -stickScale-buttonScale*2));
|
|
1134
|
+
const pressed = gamepadIsDown(j, i);
|
|
1135
|
+
debugCircle(drawPos, buttonScale*2, pressed ? '#f00' : '#fff7', 0, true);
|
|
1136
|
+
debugText(j, drawPos, .3);
|
|
1103
1137
|
}
|
|
1104
1138
|
}
|
|
1105
1139
|
}
|
|
@@ -1281,14 +1315,18 @@ function debugRender()
|
|
|
1281
1315
|
mousePressed && overlayContext.fillText('Mouse: ' + mousePressed, x, y += h);
|
|
1282
1316
|
keysPressed && overlayContext.fillText('Keys: ' + keysPressed, x, y += h);
|
|
1283
1317
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
for (const i in inputData[1])
|
|
1318
|
+
// show gamepad buttons
|
|
1319
|
+
for (let i = 1; i < inputData.length; i++)
|
|
1287
1320
|
{
|
|
1288
|
-
|
|
1289
|
-
|
|
1321
|
+
let buttonsPressed = '';
|
|
1322
|
+
if (inputData[i])
|
|
1323
|
+
for (const j in inputData[i])
|
|
1324
|
+
{
|
|
1325
|
+
if (keyIsDown(j, i))
|
|
1326
|
+
buttonsPressed += j + ' ' ;
|
|
1327
|
+
}
|
|
1328
|
+
buttonsPressed && overlayContext.fillText(`Gamepad ${i-1}: ` + buttonsPressed, x, y += h);
|
|
1290
1329
|
}
|
|
1291
|
-
buttonsPressed && overlayContext.fillText('Gamepad: ' + buttonsPressed, x, y += h);
|
|
1292
1330
|
}
|
|
1293
1331
|
else
|
|
1294
1332
|
{
|
|
@@ -2044,9 +2082,15 @@ class Vector2
|
|
|
2044
2082
|
{
|
|
2045
2083
|
this.x = x;
|
|
2046
2084
|
this.y = y;
|
|
2085
|
+
ASSERT_VECTOR2_VALID(this);
|
|
2047
2086
|
return this;
|
|
2048
2087
|
}
|
|
2049
2088
|
|
|
2089
|
+
/** Sets this vector from another vector and returns self
|
|
2090
|
+
* @param {Vector2} v - other vector
|
|
2091
|
+
* @return {Vector2} */
|
|
2092
|
+
setFrom(v) { return this.set(v.x, v.y); }
|
|
2093
|
+
|
|
2050
2094
|
/** Returns a new vector that is a copy of this
|
|
2051
2095
|
* @return {Vector2} */
|
|
2052
2096
|
copy() { return new Vector2(this.x, this.y); }
|
|
@@ -2109,7 +2153,7 @@ class Vector2
|
|
|
2109
2153
|
clampLength(length=1)
|
|
2110
2154
|
{
|
|
2111
2155
|
const l = this.length();
|
|
2112
|
-
return l > length ? this.scale(length/l) : this;
|
|
2156
|
+
return l > length ? this.scale(length/l) : this.copy();
|
|
2113
2157
|
}
|
|
2114
2158
|
|
|
2115
2159
|
/** Returns the dot product of this and the vector passed in
|
|
@@ -2196,6 +2240,10 @@ class Vector2
|
|
|
2196
2240
|
* @return {number} */
|
|
2197
2241
|
area() { return abs(this.x * this.y); }
|
|
2198
2242
|
|
|
2243
|
+
/** Returns true if this vector is (0,0)
|
|
2244
|
+
* @return {boolean} */
|
|
2245
|
+
isZero() { return !this.x && !this.y; }
|
|
2246
|
+
|
|
2199
2247
|
/** Returns a new vector that is p percent between this and the vector passed in
|
|
2200
2248
|
* @param {Vector2} v - other vector
|
|
2201
2249
|
* @param {number} percent
|
|
@@ -2306,9 +2354,15 @@ class Color
|
|
|
2306
2354
|
this.g = g;
|
|
2307
2355
|
this.b = b;
|
|
2308
2356
|
this.a = a;
|
|
2357
|
+
ASSERT_COLOR_VALID(this);
|
|
2309
2358
|
return this;
|
|
2310
2359
|
}
|
|
2311
2360
|
|
|
2361
|
+
/** Sets this color from another color and returns self
|
|
2362
|
+
* @param {Color} c - other color
|
|
2363
|
+
* @return {Color} */
|
|
2364
|
+
setFrom(c) { return this.set(c.r, c.g, c.b, c.a); }
|
|
2365
|
+
|
|
2312
2366
|
/** Returns a new color that is a copy of this
|
|
2313
2367
|
* @return {Color} */
|
|
2314
2368
|
copy() { return new Color(this.r, this.g, this.b, this.a); }
|
|
@@ -2684,6 +2738,20 @@ let canvasClearColor = CLEAR_BLACK;
|
|
|
2684
2738
|
* @memberof Settings */
|
|
2685
2739
|
let canvasMaxSize = vec2(1920, 1080);
|
|
2686
2740
|
|
|
2741
|
+
/** Minimum aspect ratio of the canvas (width/height), unused if 0
|
|
2742
|
+
* Can be used with canvasMaxAspect to limit aspect ratio
|
|
2743
|
+
* @type {number}
|
|
2744
|
+
* @default
|
|
2745
|
+
* @memberof Settings */
|
|
2746
|
+
let canvasMinAspect = 0;
|
|
2747
|
+
|
|
2748
|
+
/** Maximum aspect ratio of the canvas (width/height), unused if 0
|
|
2749
|
+
* Can be used with canvasMinAspect to limit aspect ratio
|
|
2750
|
+
* @type {number}
|
|
2751
|
+
* @default
|
|
2752
|
+
* @memberof Settings */
|
|
2753
|
+
let canvasMaxAspect = 0;
|
|
2754
|
+
|
|
2687
2755
|
/** Fixed size of the canvas, if enabled canvas size never changes
|
|
2688
2756
|
* - you may also need to set mainCanvasSize if using screen space coords in startup
|
|
2689
2757
|
* @type {Vector2}
|
|
@@ -2860,17 +2928,17 @@ let touchGamepadEnable = false;
|
|
|
2860
2928
|
* @memberof Settings */
|
|
2861
2929
|
let touchGamepadCenterButton = true;
|
|
2862
2930
|
|
|
2863
|
-
/**
|
|
2864
|
-
* @type {
|
|
2931
|
+
/** Number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
|
|
2932
|
+
* @type {number}
|
|
2865
2933
|
* @default
|
|
2866
2934
|
* @memberof Settings */
|
|
2867
|
-
let
|
|
2935
|
+
let touchGamepadButtonCount = 4;
|
|
2868
2936
|
|
|
2869
|
-
/**
|
|
2870
|
-
* @type {
|
|
2937
|
+
/** True if touch gamepad should be analog stick or false to use if 8 way dpad
|
|
2938
|
+
* @type {boolean}
|
|
2871
2939
|
* @default
|
|
2872
2940
|
* @memberof Settings */
|
|
2873
|
-
let
|
|
2941
|
+
let touchGamepadAnalog = true;
|
|
2874
2942
|
|
|
2875
2943
|
/** Size of virtual gamepad for touch devices in pixels
|
|
2876
2944
|
* @type {number}
|
|
@@ -2980,6 +3048,16 @@ function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
|
|
|
2980
3048
|
* @memberof Settings */
|
|
2981
3049
|
function setCanvasMaxSize(size) { canvasMaxSize = size.copy(); }
|
|
2982
3050
|
|
|
3051
|
+
/** Set minimum aspect ratio of the canvas (width/height), unused if 0
|
|
3052
|
+
* @param {number} aspect
|
|
3053
|
+
* @memberof Settings */
|
|
3054
|
+
function setCanvasMinAspect(aspect) { canvasMinAspect = aspect; }
|
|
3055
|
+
|
|
3056
|
+
/** Set maximum aspect ratio of the canvas (width/height), unused if 0
|
|
3057
|
+
* @param {number} aspect
|
|
3058
|
+
* @memberof Settings */
|
|
3059
|
+
function setCanvasMaxAspect(aspect) { canvasMaxAspect = aspect; }
|
|
3060
|
+
|
|
2983
3061
|
/** Set fixed size of the canvas
|
|
2984
3062
|
* @param {Vector2} size
|
|
2985
3063
|
* @memberof Settings */
|
|
@@ -3136,6 +3214,11 @@ function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
|
|
|
3136
3214
|
* @memberof Settings */
|
|
3137
3215
|
function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
|
|
3138
3216
|
|
|
3217
|
+
/** Set number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
|
|
3218
|
+
* @param {number} count
|
|
3219
|
+
* @memberof Settings */
|
|
3220
|
+
function setTouchGamepadButtonCount(count) { touchGamepadButtonCount = count; }
|
|
3221
|
+
|
|
3139
3222
|
/** Set if touch gamepad should be analog stick or 8 way dpad
|
|
3140
3223
|
* @param {boolean} analog
|
|
3141
3224
|
* @memberof Settings */
|
|
@@ -3344,7 +3427,7 @@ class EngineObject
|
|
|
3344
3427
|
child.updateTransforms();
|
|
3345
3428
|
}
|
|
3346
3429
|
|
|
3347
|
-
/** Update the object physics, called automatically by engine once each frame */
|
|
3430
|
+
/** Update the object physics, called automatically by engine once each frame. Can be overridden to stop or change how physics works for an object. */
|
|
3348
3431
|
updatePhysics()
|
|
3349
3432
|
{
|
|
3350
3433
|
// child objects do not have physics
|
|
@@ -3377,7 +3460,7 @@ class EngineObject
|
|
|
3377
3460
|
if (!enablePhysicsSolver || !this.mass) // don't do collision for static objects
|
|
3378
3461
|
return;
|
|
3379
3462
|
|
|
3380
|
-
const
|
|
3463
|
+
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
3381
3464
|
if (this.groundObject)
|
|
3382
3465
|
{
|
|
3383
3466
|
// apply friction in local space of ground object
|
|
@@ -3433,10 +3516,10 @@ class EngineObject
|
|
|
3433
3516
|
{
|
|
3434
3517
|
// push outside object collision
|
|
3435
3518
|
this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
|
|
3436
|
-
if ((o.groundObject &&
|
|
3519
|
+
if ((o.groundObject && wasFalling) || !o.mass)
|
|
3437
3520
|
{
|
|
3438
3521
|
// set ground object if landed on something
|
|
3439
|
-
if (
|
|
3522
|
+
if (wasFalling)
|
|
3440
3523
|
this.groundObject = o;
|
|
3441
3524
|
|
|
3442
3525
|
// bounce if other object is fixed or grounded
|
|
@@ -3523,12 +3606,15 @@ class EngineObject
|
|
|
3523
3606
|
const restitution = max(this.restitution, hitLayer.restitution);
|
|
3524
3607
|
this.velocity.y *= -restitution;
|
|
3525
3608
|
|
|
3526
|
-
if (
|
|
3609
|
+
if (wasFalling)
|
|
3527
3610
|
{
|
|
3528
|
-
// adjust position to slightly
|
|
3611
|
+
// adjust position to slightly away from nearest tile
|
|
3529
3612
|
// this prevents gap between object and ground
|
|
3530
3613
|
const epsilon = .0001;
|
|
3531
|
-
|
|
3614
|
+
const offset = this.size.y/2 + epsilon;
|
|
3615
|
+
this.pos.y = gravity.y < 0 ?
|
|
3616
|
+
floor(oldPos.y-this.size.y/2) + offset :
|
|
3617
|
+
ceil( oldPos.y+this.size.y/2) - offset;
|
|
3532
3618
|
|
|
3533
3619
|
// set ground object for tile collision
|
|
3534
3620
|
this.groundObject = hitLayer;
|
|
@@ -3697,21 +3783,20 @@ class EngineObject
|
|
|
3697
3783
|
* @return {string} */
|
|
3698
3784
|
toString()
|
|
3699
3785
|
{
|
|
3700
|
-
if (debug)
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
}
|
|
3786
|
+
if (!debug) return;
|
|
3787
|
+
|
|
3788
|
+
let text = 'type = ' + this.constructor.name;
|
|
3789
|
+
if (this.pos.x || this.pos.y)
|
|
3790
|
+
text += '\npos = ' + this.pos;
|
|
3791
|
+
if (this.velocity.x || this.velocity.y)
|
|
3792
|
+
text += '\nvelocity = ' + this.velocity;
|
|
3793
|
+
if (this.size.x || this.size.y)
|
|
3794
|
+
text += '\nsize = ' + this.size;
|
|
3795
|
+
if (this.angle)
|
|
3796
|
+
text += '\nangle = ' + this.angle.toFixed(3);
|
|
3797
|
+
if (this.color)
|
|
3798
|
+
text += '\ncolor = ' + this.color;
|
|
3799
|
+
return text;
|
|
3715
3800
|
}
|
|
3716
3801
|
|
|
3717
3802
|
/** Render debug info for this object */
|
|
@@ -3880,7 +3965,7 @@ class TileInfo
|
|
|
3880
3965
|
this.padding = padding;
|
|
3881
3966
|
/** @property {TextureInfo} - The texture info for this tile */
|
|
3882
3967
|
this.textureInfo = textureInfos[this.textureIndex];
|
|
3883
|
-
/** @property {
|
|
3968
|
+
/** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
|
|
3884
3969
|
this.bleedScale = bleedScale;
|
|
3885
3970
|
}
|
|
3886
3971
|
|
|
@@ -4367,7 +4452,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
4367
4452
|
|
|
4368
4453
|
/** Draw text on main canvas in world space
|
|
4369
4454
|
* Automatically splits new lines into rows
|
|
4370
|
-
* @param {string} text
|
|
4455
|
+
* @param {string|number} text
|
|
4371
4456
|
* @param {Vector2} pos
|
|
4372
4457
|
* @param {number} [size]
|
|
4373
4458
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -4394,7 +4479,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
|
|
|
4394
4479
|
|
|
4395
4480
|
/** Draw text on overlay canvas in world space
|
|
4396
4481
|
* Automatically splits new lines into rows
|
|
4397
|
-
* @param {string} text
|
|
4482
|
+
* @param {string|number} text
|
|
4398
4483
|
* @param {Vector2} pos
|
|
4399
4484
|
* @param {number} [size]
|
|
4400
4485
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -4413,7 +4498,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
4413
4498
|
|
|
4414
4499
|
/** Draw text on overlay canvas in screen space
|
|
4415
4500
|
* Automatically splits new lines into rows
|
|
4416
|
-
* @param {string} text
|
|
4501
|
+
* @param {string|number} text
|
|
4417
4502
|
* @param {Vector2} pos
|
|
4418
4503
|
* @param {number} [size]
|
|
4419
4504
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -4578,11 +4663,28 @@ function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
|
|
|
4578
4663
|
];
|
|
4579
4664
|
}
|
|
4580
4665
|
|
|
4581
|
-
/** Get the camera
|
|
4666
|
+
/** Get the size of the camera window in world space
|
|
4582
4667
|
* @return {Vector2}
|
|
4583
4668
|
* @memberof Draw */
|
|
4584
4669
|
function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
4585
4670
|
|
|
4671
|
+
/** Check if a point or circle is on screen
|
|
4672
|
+
* If size is a Vector2, uses the largest dimension as diameter
|
|
4673
|
+
* This can be used to cull offscreen objects from render or update
|
|
4674
|
+
* @param {Vector2} pos - world space position
|
|
4675
|
+
* @param {Vector2|number} size - world space size or diameter
|
|
4676
|
+
* @return {boolean}
|
|
4677
|
+
* @memberof Draw */
|
|
4678
|
+
function isOnScreen(pos, size=0)
|
|
4679
|
+
{
|
|
4680
|
+
pos = worldToScreen(pos);
|
|
4681
|
+
if (size instanceof Vector2)
|
|
4682
|
+
size = max(size.x, size.y); // use largest dimension
|
|
4683
|
+
size *= cameraScale/2;
|
|
4684
|
+
return pos.x + size > 0 && pos.x - size < mainCanvasSize.x &&
|
|
4685
|
+
pos.y + size > 0 && pos.y - size < mainCanvasSize.y;
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4586
4688
|
/** Enable normal or additive blend mode
|
|
4587
4689
|
* @param {boolean} [additive]
|
|
4588
4690
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -4746,7 +4848,7 @@ class FontImage
|
|
|
4746
4848
|
}
|
|
4747
4849
|
|
|
4748
4850
|
/** Draw text in world space using the image font
|
|
4749
|
-
* @param {string} text
|
|
4851
|
+
* @param {string|number} text
|
|
4750
4852
|
* @param {Vector2} pos
|
|
4751
4853
|
* @param {number} [scale=.25]
|
|
4752
4854
|
* @param {boolean} [center]
|
|
@@ -4758,7 +4860,7 @@ class FontImage
|
|
|
4758
4860
|
}
|
|
4759
4861
|
|
|
4760
4862
|
/** Draw text on overlay canvas in world space using the image font
|
|
4761
|
-
* @param {string} text
|
|
4863
|
+
* @param {string|number} text
|
|
4762
4864
|
* @param {Vector2} pos
|
|
4763
4865
|
* @param {number} [scale]
|
|
4764
4866
|
* @param {boolean} [center]
|
|
@@ -4767,7 +4869,7 @@ class FontImage
|
|
|
4767
4869
|
{ this.drawText(text, pos, scale, center, overlayContext); }
|
|
4768
4870
|
|
|
4769
4871
|
/** Draw text on overlay canvas in screen space using the image font
|
|
4770
|
-
* @param {string} text
|
|
4872
|
+
* @param {string|number} text
|
|
4771
4873
|
* @param {Vector2} pos
|
|
4772
4874
|
* @param {number} [scale]
|
|
4773
4875
|
* @param {boolean} [center]
|
|
@@ -4811,6 +4913,85 @@ class FontImage
|
|
|
4811
4913
|
* @namespace Input
|
|
4812
4914
|
*/
|
|
4813
4915
|
|
|
4916
|
+
/** Mouse pos in world space
|
|
4917
|
+
* @type {Vector2}
|
|
4918
|
+
* @memberof Input */
|
|
4919
|
+
let mousePos = vec2();
|
|
4920
|
+
|
|
4921
|
+
/** Mouse pos in screen space
|
|
4922
|
+
* @type {Vector2}
|
|
4923
|
+
* @memberof Input */
|
|
4924
|
+
let mousePosScreen = vec2();
|
|
4925
|
+
|
|
4926
|
+
/** Mouse movement delta in world space
|
|
4927
|
+
* @type {Vector2}
|
|
4928
|
+
* @memberof Input */
|
|
4929
|
+
let mouseDelta = vec2();
|
|
4930
|
+
|
|
4931
|
+
/** Mouse movement delta in screen space
|
|
4932
|
+
* @type {Vector2}
|
|
4933
|
+
* @memberof Input */
|
|
4934
|
+
let mouseDeltaScreen = vec2();
|
|
4935
|
+
|
|
4936
|
+
/** Mouse wheel delta this frame
|
|
4937
|
+
* @type {number}
|
|
4938
|
+
* @memberof Input */
|
|
4939
|
+
let mouseWheel = 0;
|
|
4940
|
+
|
|
4941
|
+
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4942
|
+
* @type {boolean}
|
|
4943
|
+
* @memberof Input */
|
|
4944
|
+
let mouseInWindow = true;
|
|
4945
|
+
|
|
4946
|
+
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
4947
|
+
* @type {boolean}
|
|
4948
|
+
* @memberof Input */
|
|
4949
|
+
let isUsingGamepad = false;
|
|
4950
|
+
|
|
4951
|
+
/** Prevents input continuing to the default browser handling (true by default)
|
|
4952
|
+
* @type {boolean}
|
|
4953
|
+
* @memberof Input */
|
|
4954
|
+
let inputPreventDefault = true;
|
|
4955
|
+
|
|
4956
|
+
/** Primary gamepad index, automatically set to first gamepad with input
|
|
4957
|
+
* @type {number}
|
|
4958
|
+
* @memberof Input */
|
|
4959
|
+
let gamepadPrimary = 0;
|
|
4960
|
+
|
|
4961
|
+
/** Prevents input continuing to the default browser handling
|
|
4962
|
+
* This is useful to disable for html menus so the browser can handle input normally
|
|
4963
|
+
* @param {boolean} preventDefault
|
|
4964
|
+
* @memberof Input */
|
|
4965
|
+
function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
|
|
4966
|
+
|
|
4967
|
+
/** Clears an input key state
|
|
4968
|
+
* @param {string|number} key
|
|
4969
|
+
* @param {number} [device]
|
|
4970
|
+
* @param {boolean} [clearDown=true]
|
|
4971
|
+
* @param {boolean} [clearPressed=true]
|
|
4972
|
+
* @param {boolean} [clearReleased=true]
|
|
4973
|
+
* @memberof Input */
|
|
4974
|
+
function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
|
|
4975
|
+
{
|
|
4976
|
+
if (!inputData[device])
|
|
4977
|
+
return;
|
|
4978
|
+
inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
|
|
4979
|
+
}
|
|
4980
|
+
|
|
4981
|
+
/** Clears all input
|
|
4982
|
+
* @memberof Input */
|
|
4983
|
+
function inputClear()
|
|
4984
|
+
{
|
|
4985
|
+
inputData.length = 0;
|
|
4986
|
+
inputData[0] = [];
|
|
4987
|
+
touchGamepadButtons.length = 0;
|
|
4988
|
+
touchGamepadSticks.length = 0;
|
|
4989
|
+
gamepadStickData.length = 0;
|
|
4990
|
+
gamepadDpadData.length = 0;
|
|
4991
|
+
}
|
|
4992
|
+
|
|
4993
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4994
|
+
|
|
4814
4995
|
/** Returns true if device key is down
|
|
4815
4996
|
* @param {string|number} key
|
|
4816
4997
|
* @param {number} [device]
|
|
@@ -4818,9 +4999,9 @@ class FontImage
|
|
|
4818
4999
|
* @memberof Input */
|
|
4819
5000
|
function keyIsDown(key, device=0)
|
|
4820
5001
|
{
|
|
4821
|
-
ASSERT(key
|
|
5002
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4822
5003
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4823
|
-
return
|
|
5004
|
+
return !!(inputData[device]?.[key] & 1);
|
|
4824
5005
|
}
|
|
4825
5006
|
|
|
4826
5007
|
/** Returns true if device key was pressed this frame
|
|
@@ -4830,9 +5011,9 @@ function keyIsDown(key, device=0)
|
|
|
4830
5011
|
* @memberof Input */
|
|
4831
5012
|
function keyWasPressed(key, device=0)
|
|
4832
5013
|
{
|
|
4833
|
-
ASSERT(key
|
|
5014
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4834
5015
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4835
|
-
return
|
|
5016
|
+
return !!(inputData[device]?.[key] & 2);
|
|
4836
5017
|
}
|
|
4837
5018
|
|
|
4838
5019
|
/** Returns true if device key was released this frame
|
|
@@ -4842,173 +5023,193 @@ function keyWasPressed(key, device=0)
|
|
|
4842
5023
|
* @memberof Input */
|
|
4843
5024
|
function keyWasReleased(key, device=0)
|
|
4844
5025
|
{
|
|
4845
|
-
ASSERT(key
|
|
5026
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4846
5027
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4847
|
-
return
|
|
5028
|
+
return !!(inputData[device]?.[key] & 4);
|
|
4848
5029
|
}
|
|
4849
5030
|
|
|
4850
5031
|
/** Returns input vector from arrow keys or WASD if enabled
|
|
5032
|
+
* @param {string} [up]
|
|
5033
|
+
* @param {string} [down]
|
|
5034
|
+
* @param {string} [left]
|
|
5035
|
+
* @param {string} [right]
|
|
4851
5036
|
* @return {Vector2}
|
|
4852
5037
|
* @memberof Input */
|
|
4853
5038
|
function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
|
|
4854
5039
|
{
|
|
5040
|
+
ASSERT(isString(up), 'up key must be a string');
|
|
5041
|
+
ASSERT(isString(down), 'down key must be a string');
|
|
5042
|
+
ASSERT(isString(left), 'left key must be a string');
|
|
5043
|
+
ASSERT(isString(right), 'right key must be a string');
|
|
4855
5044
|
const k = (key)=> keyIsDown(key) ? 1 : 0;
|
|
4856
5045
|
return vec2(k(right) - k(left), k(up) - k(down));
|
|
4857
5046
|
}
|
|
4858
5047
|
|
|
4859
|
-
/** Clears all input
|
|
4860
|
-
* @memberof Input */
|
|
4861
|
-
function inputClear() { inputData = [[]]; touchGamepadButtons = []; }
|
|
4862
|
-
|
|
4863
|
-
/** Clears an input key state
|
|
4864
|
-
* @param {string|number} key
|
|
4865
|
-
* @param {number} [device]
|
|
4866
|
-
* @param {boolean} [clearDown=true]
|
|
4867
|
-
* @param {boolean} [clearPressed=true]
|
|
4868
|
-
* @param {boolean} [clearReleased=true]
|
|
4869
|
-
* @memberof Input */
|
|
4870
|
-
function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
|
|
4871
|
-
{
|
|
4872
|
-
if (!inputData[device])
|
|
4873
|
-
return;
|
|
4874
|
-
inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
|
|
4875
|
-
}
|
|
4876
|
-
|
|
4877
5048
|
/** Returns true if mouse button is down
|
|
4878
5049
|
* @function
|
|
4879
5050
|
* @param {number} button
|
|
4880
5051
|
* @return {boolean}
|
|
4881
5052
|
* @memberof Input */
|
|
4882
|
-
function mouseIsDown(button)
|
|
5053
|
+
function mouseIsDown(button)
|
|
5054
|
+
{
|
|
5055
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
5056
|
+
return keyIsDown(button);
|
|
5057
|
+
}
|
|
4883
5058
|
|
|
4884
5059
|
/** Returns true if mouse button was pressed
|
|
4885
5060
|
* @function
|
|
4886
5061
|
* @param {number} button
|
|
4887
5062
|
* @return {boolean}
|
|
4888
5063
|
* @memberof Input */
|
|
4889
|
-
function mouseWasPressed(button)
|
|
5064
|
+
function mouseWasPressed(button)
|
|
5065
|
+
{
|
|
5066
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
5067
|
+
return keyWasPressed(button);
|
|
5068
|
+
}
|
|
4890
5069
|
|
|
4891
5070
|
/** Returns true if mouse button was released
|
|
4892
5071
|
* @function
|
|
4893
5072
|
* @param {number} button
|
|
4894
5073
|
* @return {boolean}
|
|
4895
5074
|
* @memberof Input */
|
|
4896
|
-
function mouseWasReleased(button)
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
let mousePos = vec2();
|
|
4902
|
-
|
|
4903
|
-
/** Mouse pos in screen space
|
|
4904
|
-
* @type {Vector2}
|
|
4905
|
-
* @memberof Input */
|
|
4906
|
-
let mousePosScreen = vec2();
|
|
4907
|
-
|
|
4908
|
-
/** Mouse movement delta in world space
|
|
4909
|
-
* @type {Vector2}
|
|
4910
|
-
* @memberof Input */
|
|
4911
|
-
let mouseDelta = vec2();
|
|
4912
|
-
|
|
4913
|
-
/** Mouse movement delta in screen space
|
|
4914
|
-
* @type {Vector2}
|
|
4915
|
-
* @memberof Input */
|
|
4916
|
-
let mouseDeltaScreen = vec2();
|
|
4917
|
-
|
|
4918
|
-
/** Mouse wheel delta this frame
|
|
4919
|
-
* @type {number}
|
|
4920
|
-
* @memberof Input */
|
|
4921
|
-
let mouseWheel = 0;
|
|
4922
|
-
|
|
4923
|
-
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4924
|
-
* @type {boolean}
|
|
4925
|
-
* @memberof Input */
|
|
4926
|
-
let mouseInWindow = true;
|
|
4927
|
-
|
|
4928
|
-
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
4929
|
-
* @type {boolean}
|
|
4930
|
-
* @memberof Input */
|
|
4931
|
-
let isUsingGamepad = false;
|
|
4932
|
-
|
|
4933
|
-
/** Prevents input continuing to the default browser handling (true by default)
|
|
4934
|
-
* @type {boolean}
|
|
4935
|
-
* @memberof Input */
|
|
4936
|
-
let inputPreventDefault = true;
|
|
4937
|
-
|
|
4938
|
-
/** Prevents input continuing to the default browser handling
|
|
4939
|
-
* This is useful to disable for html menus so the browser can handle input normally
|
|
4940
|
-
* @param {boolean} preventDefault
|
|
4941
|
-
* @memberof Input */
|
|
4942
|
-
function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
|
|
5075
|
+
function mouseWasReleased(button)
|
|
5076
|
+
{
|
|
5077
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
5078
|
+
return keyWasReleased(button);
|
|
5079
|
+
}
|
|
4943
5080
|
|
|
4944
5081
|
/** Returns true if gamepad button is down
|
|
4945
5082
|
* @param {number} button
|
|
4946
5083
|
* @param {number} [gamepad]
|
|
4947
5084
|
* @return {boolean}
|
|
4948
5085
|
* @memberof Input */
|
|
4949
|
-
function gamepadIsDown(button, gamepad=
|
|
4950
|
-
{
|
|
5086
|
+
function gamepadIsDown(button, gamepad=gamepadPrimary)
|
|
5087
|
+
{
|
|
5088
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
5089
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5090
|
+
return keyIsDown(button, gamepad+1);
|
|
5091
|
+
}
|
|
4951
5092
|
|
|
4952
5093
|
/** Returns true if gamepad button was pressed
|
|
4953
5094
|
* @param {number} button
|
|
4954
5095
|
* @param {number} [gamepad]
|
|
4955
5096
|
* @return {boolean}
|
|
4956
5097
|
* @memberof Input */
|
|
4957
|
-
function gamepadWasPressed(button, gamepad=
|
|
4958
|
-
{
|
|
5098
|
+
function gamepadWasPressed(button, gamepad=gamepadPrimary)
|
|
5099
|
+
{
|
|
5100
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
5101
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5102
|
+
return keyWasPressed(button, gamepad+1);
|
|
5103
|
+
}
|
|
4959
5104
|
|
|
4960
5105
|
/** Returns true if gamepad button was released
|
|
4961
5106
|
* @param {number} button
|
|
4962
5107
|
* @param {number} [gamepad]
|
|
4963
5108
|
* @return {boolean}
|
|
4964
5109
|
* @memberof Input */
|
|
4965
|
-
function gamepadWasReleased(button, gamepad=
|
|
4966
|
-
{
|
|
5110
|
+
function gamepadWasReleased(button, gamepad=gamepadPrimary)
|
|
5111
|
+
{
|
|
5112
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
5113
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5114
|
+
return keyWasReleased(button, gamepad+1);
|
|
5115
|
+
}
|
|
4967
5116
|
|
|
4968
5117
|
/** Returns gamepad stick value
|
|
4969
5118
|
* @param {number} stick
|
|
4970
5119
|
* @param {number} [gamepad]
|
|
4971
5120
|
* @return {Vector2}
|
|
4972
5121
|
* @memberof Input */
|
|
4973
|
-
function gamepadStick(stick, gamepad=
|
|
4974
|
-
{ return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
|
|
4975
|
-
|
|
4976
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
4977
|
-
// Input system functions called automatically by engine
|
|
4978
|
-
|
|
4979
|
-
// input is stored as a bit field for each key: 1 = isDown, 2 = wasPressed, 4 = wasReleased
|
|
4980
|
-
// mouse and keyboard are stored together in device 0, gamepads are in devices > 0
|
|
4981
|
-
let inputData = [[]];
|
|
4982
|
-
|
|
4983
|
-
function inputUpdate()
|
|
5122
|
+
function gamepadStick(stick, gamepad=gamepadPrimary)
|
|
4984
5123
|
{
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
inputClear();
|
|
5124
|
+
ASSERT(isNumber(stick), 'stick must be a number');
|
|
5125
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5126
|
+
return gamepadStickData[gamepad]?.[stick] ?? vec2();
|
|
5127
|
+
}
|
|
4990
5128
|
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
5129
|
+
/** Returns gamepad dpad value
|
|
5130
|
+
* @param {number} [gamepad]
|
|
5131
|
+
* @return {Vector2}
|
|
5132
|
+
* @memberof Input */
|
|
5133
|
+
function gamepadDpad(gamepad=gamepadPrimary)
|
|
5134
|
+
{
|
|
5135
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5136
|
+
return gamepadDpadData[gamepad] ?? vec2();
|
|
5137
|
+
}
|
|
4994
5138
|
|
|
4995
|
-
|
|
4996
|
-
|
|
5139
|
+
/** Returns true if passed in gamepad is connected
|
|
5140
|
+
* @param {number} [gamepad]
|
|
5141
|
+
* @return {boolean}
|
|
5142
|
+
* @memberof Input */
|
|
5143
|
+
function gamepadConnected(gamepad=gamepadPrimary)
|
|
5144
|
+
{
|
|
5145
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5146
|
+
return !!inputData[gamepad+1];
|
|
4997
5147
|
}
|
|
4998
5148
|
|
|
4999
|
-
|
|
5149
|
+
/** Returns how many control sticks the passed in gamepad has
|
|
5150
|
+
* @param {number} [gamepad]
|
|
5151
|
+
* @return {number}
|
|
5152
|
+
* @memberof Input */
|
|
5153
|
+
function gamepadStickCount(gamepad=gamepadPrimary)
|
|
5000
5154
|
{
|
|
5001
|
-
|
|
5155
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5156
|
+
return gamepadStickData[gamepad]?.length ?? 0;
|
|
5157
|
+
}
|
|
5002
5158
|
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5159
|
+
/** True if a touch device has been detected
|
|
5160
|
+
* @memberof Input */
|
|
5161
|
+
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
5162
|
+
|
|
5163
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5164
|
+
|
|
5165
|
+
/** Pulse the vibration hardware if it exists
|
|
5166
|
+
* @param {number|Array} [pattern] - single value in ms or vibration interval array
|
|
5167
|
+
* @memberof Input */
|
|
5168
|
+
function vibrate(pattern=100)
|
|
5169
|
+
{
|
|
5170
|
+
ASSERT(isNumber(pattern) || isArray(pattern), 'pattern must be a number or array');
|
|
5171
|
+
vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern);
|
|
5010
5172
|
}
|
|
5011
5173
|
|
|
5174
|
+
/** Cancel any ongoing vibration
|
|
5175
|
+
* @memberof Input */
|
|
5176
|
+
function vibrateStop() { vibrate(0); }
|
|
5177
|
+
|
|
5178
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5179
|
+
// Pointer Lock
|
|
5180
|
+
|
|
5181
|
+
/** Request to lock the pointer, does not work on touch devices
|
|
5182
|
+
* @memberof Input */
|
|
5183
|
+
function pointerLockRequest()
|
|
5184
|
+
{ !isTouchDevice && mainCanvas.requestPointerLock?.(); }
|
|
5185
|
+
|
|
5186
|
+
/** Request to unlock the pointer
|
|
5187
|
+
* @memberof Input */
|
|
5188
|
+
function pointerLockExit()
|
|
5189
|
+
{ document.exitPointerLock?.(); }
|
|
5190
|
+
|
|
5191
|
+
/** Check if pointer is locked (true if locked)
|
|
5192
|
+
* @return {boolean}
|
|
5193
|
+
* @memberof Input */
|
|
5194
|
+
function pointerLockIsActive()
|
|
5195
|
+
{ return document.pointerLockElement === mainCanvas; }
|
|
5196
|
+
|
|
5197
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5198
|
+
// Input variables used by engine
|
|
5199
|
+
|
|
5200
|
+
// input uses bit field for each key: 1=isDown, 2=wasPressed, 4=wasReleased
|
|
5201
|
+
// mouse and keyboard stored in device 0, gamepads stored in devices > 0
|
|
5202
|
+
const inputData = [[]];
|
|
5203
|
+
|
|
5204
|
+
// gamepad internal variables
|
|
5205
|
+
const gamepadStickData = [], gamepadDpadData = [], gamepadHadInput = [];
|
|
5206
|
+
|
|
5207
|
+
// touch gamepad internal variables
|
|
5208
|
+
const touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadSticks = [];
|
|
5209
|
+
|
|
5210
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5211
|
+
// Input system functions used by engine
|
|
5212
|
+
|
|
5012
5213
|
function inputInit()
|
|
5013
5214
|
{
|
|
5014
5215
|
if (headlessMode) return;
|
|
@@ -5037,6 +5238,18 @@ function inputInit()
|
|
|
5037
5238
|
if (inputWASDEmulateDirection)
|
|
5038
5239
|
inputData[0][remapKey(e.code)] = 3;
|
|
5039
5240
|
}
|
|
5241
|
+
|
|
5242
|
+
// prevent arrow key from moving the page
|
|
5243
|
+
const preventDefaultKeys =
|
|
5244
|
+
[
|
|
5245
|
+
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', // scrolling
|
|
5246
|
+
'Space', // page down scroll
|
|
5247
|
+
'Tab', // focus navigation
|
|
5248
|
+
'Backspace', // browser back
|
|
5249
|
+
];
|
|
5250
|
+
if (preventDefaultKeys.includes(e.code))
|
|
5251
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
5252
|
+
e.preventDefault();
|
|
5040
5253
|
}
|
|
5041
5254
|
function onKeyUp(e)
|
|
5042
5255
|
{
|
|
@@ -5068,7 +5281,9 @@ function inputInit()
|
|
|
5068
5281
|
const mousePosScreenLast = mousePosScreen;
|
|
5069
5282
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
5070
5283
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
5071
|
-
|
|
5284
|
+
|
|
5285
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
5286
|
+
e.preventDefault();
|
|
5072
5287
|
}
|
|
5073
5288
|
function onMouseUp(e)
|
|
5074
5289
|
{
|
|
@@ -5092,344 +5307,388 @@ function inputInit()
|
|
|
5092
5307
|
function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
|
|
5093
5308
|
function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
|
|
5094
5309
|
function onBlur() { inputClear(); } // reset input when focus is lost
|
|
5095
|
-
}
|
|
5096
5310
|
|
|
5097
|
-
//
|
|
5098
|
-
function
|
|
5099
|
-
{
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
}
|
|
5311
|
+
// enable touch input mouse passthrough
|
|
5312
|
+
function touchInputInit()
|
|
5313
|
+
{
|
|
5314
|
+
// add non passive touch event listeners
|
|
5315
|
+
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
5316
|
+
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
5317
|
+
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
5105
5318
|
|
|
5106
|
-
|
|
5107
|
-
|
|
5319
|
+
// handle all touch events the same way
|
|
5320
|
+
let wasTouching;
|
|
5321
|
+
function handleTouch(e)
|
|
5322
|
+
{
|
|
5323
|
+
if (!touchInputEnable)
|
|
5324
|
+
return;
|
|
5108
5325
|
|
|
5109
|
-
//
|
|
5110
|
-
|
|
5326
|
+
// route touch to gamepad
|
|
5327
|
+
if (touchGamepadEnable)
|
|
5328
|
+
handleTouchGamepad(e);
|
|
5111
5329
|
|
|
5112
|
-
//
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
const applyDeadZones = (v)=>
|
|
5116
|
-
{
|
|
5117
|
-
const min=.3, max=.8;
|
|
5118
|
-
const deadZone = (v)=>
|
|
5119
|
-
v > min ? percent(v, min, max) :
|
|
5120
|
-
v < -min ? -percent(-v, min, max) : 0;
|
|
5121
|
-
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
5122
|
-
}
|
|
5330
|
+
// fix stalled audio requiring user interaction
|
|
5331
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
5332
|
+
audioContext.resume();
|
|
5123
5333
|
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5334
|
+
// check if touching and pass to mouse events
|
|
5335
|
+
const touching = e.touches.length;
|
|
5336
|
+
const button = 0; // all touches are left mouse button
|
|
5337
|
+
if (touching)
|
|
5338
|
+
{
|
|
5339
|
+
// set event pos and pass it along
|
|
5340
|
+
const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
|
|
5341
|
+
const mousePosScreenLast = mousePosScreen;
|
|
5342
|
+
mousePosScreen = mouseEventToScreen(pos);
|
|
5343
|
+
if (wasTouching)
|
|
5344
|
+
{
|
|
5345
|
+
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
5346
|
+
isUsingGamepad = touchGamepadEnable;
|
|
5347
|
+
}
|
|
5348
|
+
else
|
|
5349
|
+
inputData[0][button] = 3;
|
|
5350
|
+
}
|
|
5351
|
+
else if (wasTouching)
|
|
5352
|
+
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
5129
5353
|
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
//
|
|
5138
|
-
|
|
5139
|
-
sticks[0].y = -round(touchGamepadStick.y);
|
|
5140
|
-
sticks[0] = sticks[0].clampLength();
|
|
5354
|
+
// set was touching
|
|
5355
|
+
wasTouching = touching;
|
|
5356
|
+
|
|
5357
|
+
// prevent default handling like copy, magnifier lens, and scrolling
|
|
5358
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
5359
|
+
e.preventDefault();
|
|
5360
|
+
|
|
5361
|
+
// must return true so the document will get focus
|
|
5362
|
+
return true;
|
|
5141
5363
|
}
|
|
5142
5364
|
|
|
5143
|
-
//
|
|
5144
|
-
|
|
5145
|
-
for (let i=10; i--;)
|
|
5365
|
+
// special handling for virtual gamepad mode
|
|
5366
|
+
function handleTouchGamepad(e)
|
|
5146
5367
|
{
|
|
5147
|
-
|
|
5148
|
-
|
|
5368
|
+
// clear touch gamepad input
|
|
5369
|
+
touchGamepadSticks.length = 0;
|
|
5370
|
+
touchGamepadSticks[0] = vec2();
|
|
5371
|
+
touchGamepadSticks[1] = vec2();
|
|
5372
|
+
touchGamepadButtons.length = 0;
|
|
5373
|
+
isUsingGamepad = true;
|
|
5374
|
+
|
|
5375
|
+
const touching = e.touches.length;
|
|
5376
|
+
if (touching)
|
|
5377
|
+
{
|
|
5378
|
+
touchGamepadTimer.set();
|
|
5379
|
+
if (touchGamepadCenterButton && !wasTouching && paused)
|
|
5380
|
+
{
|
|
5381
|
+
// touch anywhere to press start when paused
|
|
5382
|
+
touchGamepadButtons[9] = 1;
|
|
5383
|
+
return;
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
|
|
5387
|
+
// don't process touch gamepad if paused
|
|
5388
|
+
if (paused)
|
|
5389
|
+
return;
|
|
5390
|
+
|
|
5391
|
+
// get center of left and right sides
|
|
5392
|
+
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
5393
|
+
const buttonCenter = touchGamepadButtonCenter();
|
|
5394
|
+
const startCenter = mainCanvasSize.scale(.5);
|
|
5395
|
+
|
|
5396
|
+
// check each touch point
|
|
5397
|
+
for (const touch of e.touches)
|
|
5398
|
+
{
|
|
5399
|
+
const touchPos = mouseEventToScreen(vec2(touch.clientX, touch.clientY));
|
|
5400
|
+
if (stickCenter.distance(touchPos) < touchGamepadSize)
|
|
5401
|
+
{
|
|
5402
|
+
// virtual analog stick
|
|
5403
|
+
const delta = touchPos.subtract(stickCenter);
|
|
5404
|
+
touchGamepadSticks[0] = delta.scale(2/touchGamepadSize).clampLength();
|
|
5405
|
+
}
|
|
5406
|
+
else if (buttonCenter.distance(touchPos) < touchGamepadSize)
|
|
5407
|
+
{
|
|
5408
|
+
if (touchGamepadButtonCount === 1)
|
|
5409
|
+
{
|
|
5410
|
+
// virtual right analog stick
|
|
5411
|
+
const delta = touchPos.subtract(buttonCenter);
|
|
5412
|
+
touchGamepadSticks[1] = delta.scale(2/touchGamepadSize).clampLength();
|
|
5413
|
+
}
|
|
5414
|
+
// virtual face buttons
|
|
5415
|
+
let button = buttonCenter.subtract(touchPos).direction();
|
|
5416
|
+
button = mod(button+2, 4);
|
|
5417
|
+
if (touchGamepadButtonCount === 1)
|
|
5418
|
+
button = 0;
|
|
5419
|
+
else if (touchGamepadButtonCount === 2)
|
|
5420
|
+
{
|
|
5421
|
+
const delta = buttonCenter.subtract(touchPos);
|
|
5422
|
+
button = -delta.x < delta.y ? 1 : 0;
|
|
5423
|
+
}
|
|
5424
|
+
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
5425
|
+
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
5426
|
+
if (button < touchGamepadButtonCount)
|
|
5427
|
+
touchGamepadButtons[button] = 1;
|
|
5428
|
+
}
|
|
5429
|
+
else if (touchGamepadCenterButton &&
|
|
5430
|
+
startCenter.distance(touchPos) < touchGamepadSize)
|
|
5431
|
+
{
|
|
5432
|
+
// virtual start button in center
|
|
5433
|
+
touchGamepadButtons[9] = 1;
|
|
5434
|
+
}
|
|
5435
|
+
}
|
|
5149
5436
|
}
|
|
5437
|
+
}
|
|
5150
5438
|
|
|
5151
|
-
|
|
5152
|
-
|
|
5439
|
+
// convert a mouse or touch event position to screen space
|
|
5440
|
+
function mouseEventToScreen(mousePos)
|
|
5441
|
+
{
|
|
5442
|
+
const rect = mainCanvas.getBoundingClientRect();
|
|
5443
|
+
const px = percent(mousePos.x, rect.left, rect.right);
|
|
5444
|
+
const py = percent(mousePos.y, rect.top, rect.bottom);
|
|
5445
|
+
return vec2(px*mainCanvas.width, py*mainCanvas.height);
|
|
5153
5446
|
}
|
|
5447
|
+
}
|
|
5154
5448
|
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5449
|
+
function inputUpdate()
|
|
5450
|
+
{
|
|
5451
|
+
if (headlessMode) return;
|
|
5158
5452
|
|
|
5159
|
-
//
|
|
5160
|
-
if (!
|
|
5161
|
-
|
|
5453
|
+
// clear input when lost focus (prevent stuck keys)
|
|
5454
|
+
if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
|
|
5455
|
+
inputClear();
|
|
5162
5456
|
|
|
5163
|
-
//
|
|
5164
|
-
|
|
5165
|
-
|
|
5457
|
+
// update mouse world space position and delta
|
|
5458
|
+
mousePos = screenToWorld(mousePosScreen);
|
|
5459
|
+
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
5460
|
+
|
|
5461
|
+
// update gamepads if enabled
|
|
5462
|
+
gamepadsUpdate();
|
|
5463
|
+
|
|
5464
|
+
// gamepads are updated by engine every frame automatically
|
|
5465
|
+
function gamepadsUpdate()
|
|
5166
5466
|
{
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5467
|
+
const applyDeadZones = (v)=>
|
|
5468
|
+
{
|
|
5469
|
+
const min=.3, max=.8;
|
|
5470
|
+
const deadZone = (v)=>
|
|
5471
|
+
v > min ? percent(v, min, max) :
|
|
5472
|
+
v < -min ? -percent(-v, min, max) : 0;
|
|
5473
|
+
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
5474
|
+
}
|
|
5475
|
+
|
|
5476
|
+
// update touch gamepad if enabled
|
|
5477
|
+
if (touchGamepadEnable && isTouchDevice)
|
|
5478
|
+
{
|
|
5479
|
+
if (!touchGamepadTimer.isSet())
|
|
5480
|
+
return;
|
|
5481
|
+
|
|
5482
|
+
// read virtual analog stick
|
|
5483
|
+
gamepadPrimary = 0; // touch gamepad uses index 0
|
|
5484
|
+
const sticks = gamepadStickData[0] ?? (gamepadStickData[0] = []);
|
|
5485
|
+
const dpad = gamepadDpadData[0] ?? (gamepadDpadData[0] = vec2());
|
|
5486
|
+
sticks[0] = vec2();
|
|
5487
|
+
dpad.set();
|
|
5488
|
+
const leftTouchStick = touchGamepadSticks[0] ?? vec2();
|
|
5489
|
+
if (touchGamepadAnalog)
|
|
5490
|
+
sticks[0] = applyDeadZones(leftTouchStick);
|
|
5491
|
+
else if (leftTouchStick.lengthSquared() > .3)
|
|
5492
|
+
{
|
|
5493
|
+
// convert to 8 way dpad
|
|
5494
|
+
const x = clamp(round(leftTouchStick.x), -1, 1);
|
|
5495
|
+
const y = clamp(round(leftTouchStick.y), -1, 1);
|
|
5496
|
+
dpad.set(x, -y);
|
|
5497
|
+
sticks[0] = dpad.clampLength(); // clamp to circle
|
|
5498
|
+
}
|
|
5499
|
+
if (touchGamepadButtonCount === 1)
|
|
5500
|
+
{
|
|
5501
|
+
const rightTouchStick = touchGamepadSticks[1] ?? vec2();
|
|
5502
|
+
sticks[1] = applyDeadZones(rightTouchStick);
|
|
5503
|
+
}
|
|
5171
5504
|
|
|
5172
|
-
|
|
5505
|
+
// read virtual gamepad buttons
|
|
5506
|
+
const data = inputData[1] ?? (inputData[1] = []);
|
|
5507
|
+
for (let i=10; i--;)
|
|
5508
|
+
{
|
|
5509
|
+
const wasDown = gamepadIsDown(i,0);
|
|
5510
|
+
data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
5511
|
+
}
|
|
5512
|
+
|
|
5513
|
+
// disable normal gamepads when touch gamepad is active
|
|
5514
|
+
return;
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5517
|
+
// return if gamepads are disabled or not supported
|
|
5518
|
+
if (!gamepadsEnable || !navigator || !navigator.getGamepads)
|
|
5519
|
+
return;
|
|
5520
|
+
|
|
5521
|
+
// only poll gamepads when focused or in debug mode
|
|
5522
|
+
if (!debug && !document.hasFocus())
|
|
5523
|
+
return;
|
|
5524
|
+
|
|
5525
|
+
// poll gamepads
|
|
5526
|
+
const maxGamepads = 8;
|
|
5527
|
+
const gamepads = navigator.getGamepads();
|
|
5528
|
+
const gamepadCount = min(maxGamepads, gamepads.length)
|
|
5529
|
+
for (let i=0; i<gamepadCount; ++i)
|
|
5173
5530
|
{
|
|
5531
|
+
// get or create gamepad data
|
|
5532
|
+
const gamepad = gamepads[i];
|
|
5533
|
+
if (!gamepad)
|
|
5534
|
+
{
|
|
5535
|
+
// clear gamepad data if not connected
|
|
5536
|
+
inputData[i+1] = undefined;
|
|
5537
|
+
gamepadStickData[i] = undefined;
|
|
5538
|
+
gamepadDpadData[i] = undefined;
|
|
5539
|
+
gamepadHadInput[i] = undefined;
|
|
5540
|
+
continue;
|
|
5541
|
+
}
|
|
5542
|
+
|
|
5543
|
+
const data = inputData[i+1] ?? (inputData[i+1] = []);
|
|
5544
|
+
const sticks = gamepadStickData[i] ?? (gamepadStickData[i] = []);
|
|
5545
|
+
const dpad = gamepadDpadData[i] ?? (gamepadDpadData[i] = vec2());
|
|
5546
|
+
|
|
5174
5547
|
// read analog sticks
|
|
5175
5548
|
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
5176
5549
|
sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
|
|
5177
5550
|
|
|
5178
5551
|
// read buttons
|
|
5552
|
+
let hadInput = false;
|
|
5179
5553
|
for (let j = gamepad.buttons.length; j--;)
|
|
5180
5554
|
{
|
|
5181
5555
|
const button = gamepad.buttons[j];
|
|
5182
5556
|
const wasDown = gamepadIsDown(j,i);
|
|
5183
5557
|
data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5558
|
+
|
|
5559
|
+
// check for any input on this gamepad, analog must be full press
|
|
5560
|
+
if (button.pressed)
|
|
5561
|
+
if (!button.value || button.value > .9)
|
|
5562
|
+
hadInput = true;
|
|
5563
|
+
}
|
|
5564
|
+
|
|
5565
|
+
// set new primary gamepad if current is not connected
|
|
5566
|
+
if (hadInput)
|
|
5567
|
+
{
|
|
5568
|
+
gamepadHadInput[i] = true;
|
|
5569
|
+
if (!gamepadHadInput[gamepadPrimary])
|
|
5570
|
+
gamepadPrimary = i;
|
|
5571
|
+
isUsingGamepad ||= (gamepadPrimary === i);
|
|
5187
5572
|
}
|
|
5188
5573
|
|
|
5189
|
-
if (
|
|
5574
|
+
if (gamepad.mapping === 'standard')
|
|
5190
5575
|
{
|
|
5191
|
-
//
|
|
5192
|
-
|
|
5576
|
+
// get dpad buttons (standard mapping)
|
|
5577
|
+
dpad.set(
|
|
5193
5578
|
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
5194
5579
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
5195
|
-
|
|
5196
|
-
|
|
5580
|
+
}
|
|
5581
|
+
else if (gamepad.axes && gamepad.axes.length >= 2)
|
|
5582
|
+
{
|
|
5583
|
+
// digital style dpad from axes
|
|
5584
|
+
const x = clamp(round(gamepad.axes[0]), -1, 1);
|
|
5585
|
+
const y = clamp(round(gamepad.axes[1]), -1, 1);
|
|
5586
|
+
dpad.set(x, -y);
|
|
5197
5587
|
}
|
|
5198
5588
|
|
|
5199
|
-
//
|
|
5200
|
-
|
|
5589
|
+
// copy dpad to left analog stick when pressed
|
|
5590
|
+
if (gamepadDirectionEmulateStick && !dpad.isZero())
|
|
5591
|
+
sticks[0] = dpad.clampLength();
|
|
5201
5592
|
}
|
|
5593
|
+
|
|
5594
|
+
// disable touch gamepad if using real gamepad
|
|
5595
|
+
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
5202
5596
|
}
|
|
5203
5597
|
}
|
|
5204
5598
|
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
/** Pulse the vibration hardware if it exists
|
|
5208
|
-
* @param {number|Array} [pattern] - single value in ms or vibration interval array
|
|
5209
|
-
* @memberof Input */
|
|
5210
|
-
function vibrate(pattern=100)
|
|
5211
|
-
{ vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
|
|
5212
|
-
|
|
5213
|
-
/** Cancel any ongoing vibration
|
|
5214
|
-
* @memberof Input */
|
|
5215
|
-
function vibrateStop() { vibrate(0); }
|
|
5216
|
-
|
|
5217
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5218
|
-
// Touch input & virtual on screen gamepad
|
|
5219
|
-
|
|
5220
|
-
/** True if a touch device has been detected
|
|
5221
|
-
* @memberof Input */
|
|
5222
|
-
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
5223
|
-
|
|
5224
|
-
// touch gamepad internal variables
|
|
5225
|
-
let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
|
|
5226
|
-
|
|
5227
|
-
function touchGamepadButtonCenter()
|
|
5599
|
+
function inputUpdatePost()
|
|
5228
5600
|
{
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5601
|
+
if (headlessMode) return;
|
|
5602
|
+
|
|
5603
|
+
// clear input to prepare for next frame
|
|
5604
|
+
for (const deviceInputData of inputData)
|
|
5605
|
+
for (const i in deviceInputData)
|
|
5606
|
+
deviceInputData[i] &= 1;
|
|
5607
|
+
mouseWheel = 0;
|
|
5608
|
+
mouseDelta = vec2();
|
|
5609
|
+
mouseDeltaScreen = vec2();
|
|
5234
5610
|
}
|
|
5235
5611
|
|
|
5236
|
-
|
|
5237
|
-
function touchInputInit()
|
|
5612
|
+
function inputRender()
|
|
5238
5613
|
{
|
|
5239
|
-
|
|
5240
|
-
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
5241
|
-
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
5242
|
-
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
5614
|
+
touchGamepadRender();
|
|
5243
5615
|
|
|
5244
|
-
|
|
5245
|
-
let wasTouching;
|
|
5246
|
-
function handleTouch(e)
|
|
5616
|
+
function touchGamepadRender()
|
|
5247
5617
|
{
|
|
5248
|
-
if (!touchInputEnable)
|
|
5618
|
+
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
5619
|
+
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
5249
5620
|
return;
|
|
5250
5621
|
|
|
5251
|
-
//
|
|
5252
|
-
|
|
5253
|
-
|
|
5622
|
+
// fade off when not touching or paused
|
|
5623
|
+
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
5624
|
+
if (!alpha || paused)
|
|
5625
|
+
return;
|
|
5254
5626
|
|
|
5255
|
-
//
|
|
5256
|
-
|
|
5257
|
-
|
|
5627
|
+
// setup the canvas
|
|
5628
|
+
const context = overlayContext;
|
|
5629
|
+
context.save();
|
|
5630
|
+
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
5631
|
+
context.strokeStyle = '#fff';
|
|
5632
|
+
context.lineWidth = 3;
|
|
5258
5633
|
|
|
5259
|
-
//
|
|
5260
|
-
const
|
|
5261
|
-
|
|
5262
|
-
|
|
5634
|
+
// draw left analog stick
|
|
5635
|
+
const leftTouchStick = touchGamepadSticks[0] ?? vec2();
|
|
5636
|
+
context.fillStyle = leftTouchStick.lengthSquared() > 0 ? '#fff' : '#000';
|
|
5637
|
+
context.beginPath();
|
|
5638
|
+
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
5639
|
+
if (touchGamepadAnalog)
|
|
5263
5640
|
{
|
|
5264
|
-
//
|
|
5265
|
-
|
|
5266
|
-
const mousePosScreenLast = mousePosScreen;
|
|
5267
|
-
mousePosScreen = mouseEventToScreen(pos);
|
|
5268
|
-
if (wasTouching)
|
|
5269
|
-
{
|
|
5270
|
-
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
5271
|
-
isUsingGamepad = touchGamepadEnable;
|
|
5272
|
-
}
|
|
5273
|
-
else
|
|
5274
|
-
inputData[0][button] = 3;
|
|
5641
|
+
// draw circle shaped gamepad
|
|
5642
|
+
context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
|
|
5275
5643
|
}
|
|
5276
|
-
else
|
|
5277
|
-
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
5278
|
-
|
|
5279
|
-
// set was touching
|
|
5280
|
-
wasTouching = touching;
|
|
5281
|
-
|
|
5282
|
-
// prevent default handling like copy, magnifier lens, and scrolling
|
|
5283
|
-
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
5284
|
-
e.preventDefault();
|
|
5285
|
-
|
|
5286
|
-
// must return true so the document will get focus
|
|
5287
|
-
return true;
|
|
5288
|
-
}
|
|
5289
|
-
|
|
5290
|
-
// special handling for virtual gamepad mode
|
|
5291
|
-
function handleTouchGamepad(e)
|
|
5292
|
-
{
|
|
5293
|
-
// clear touch gamepad input
|
|
5294
|
-
touchGamepadStick = vec2();
|
|
5295
|
-
touchGamepadButtons = [];
|
|
5296
|
-
isUsingGamepad = true;
|
|
5297
|
-
|
|
5298
|
-
const touching = e.touches.length;
|
|
5299
|
-
if (touching)
|
|
5644
|
+
else
|
|
5300
5645
|
{
|
|
5301
|
-
|
|
5302
|
-
|
|
5646
|
+
// draw cross shaped gamepad
|
|
5647
|
+
for (let i=10; --i;)
|
|
5303
5648
|
{
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5649
|
+
const angle = i*PI/4;
|
|
5650
|
+
context.arc(stickCenter.x, stickCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
|
|
5651
|
+
i%2 && context.arc(stickCenter.x, stickCenter.y, touchGamepadSize*.33, angle, angle);
|
|
5307
5652
|
}
|
|
5308
5653
|
}
|
|
5654
|
+
context.fill();
|
|
5655
|
+
context.stroke();
|
|
5309
5656
|
|
|
5310
|
-
//
|
|
5311
|
-
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
5312
|
-
const buttonCenter = touchGamepadButtonCenter();
|
|
5313
|
-
const startCenter = mainCanvasSize.scale(.5);
|
|
5314
|
-
|
|
5315
|
-
// check each touch point
|
|
5316
|
-
for (const touch of e.touches)
|
|
5657
|
+
// draw right face buttons
|
|
5317
5658
|
{
|
|
5318
|
-
const
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
|
|
5323
|
-
}
|
|
5324
|
-
else if (buttonCenter.distance(touchPos) < touchGamepadSize)
|
|
5659
|
+
const buttonCenter = touchGamepadButtonCenter();
|
|
5660
|
+
const buttonSize = touchGamepadButtonCount > 1 ?
|
|
5661
|
+
touchGamepadSize/4 : touchGamepadSize/2;
|
|
5662
|
+
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
5325
5663
|
{
|
|
5326
|
-
|
|
5327
|
-
let button =
|
|
5328
|
-
|
|
5329
|
-
if (touchGamepadButtonCount === 1)
|
|
5330
|
-
button = 0;
|
|
5331
|
-
else if (touchGamepadButtonCount === 2)
|
|
5332
|
-
{
|
|
5333
|
-
const delta = buttonCenter.subtract(touchPos);
|
|
5334
|
-
button = -delta.x < delta.y ? 1 : 0;
|
|
5335
|
-
}
|
|
5664
|
+
const j = mod(i-1, 4);
|
|
5665
|
+
let button = touchGamepadButtonCount > 2 ?
|
|
5666
|
+
j : min(j, touchGamepadButtonCount-1);
|
|
5336
5667
|
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
5337
5668
|
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
touchGamepadButtons[9] = 1;
|
|
5669
|
+
const pos = touchGamepadButtonCount < 2 ? buttonCenter :
|
|
5670
|
+
buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
|
|
5671
|
+
context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
|
|
5672
|
+
context.beginPath();
|
|
5673
|
+
context.arc(pos.x, pos.y, buttonSize, 0,9);
|
|
5674
|
+
context.fill();
|
|
5675
|
+
context.stroke();
|
|
5346
5676
|
}
|
|
5347
5677
|
}
|
|
5348
|
-
}
|
|
5349
|
-
}
|
|
5350
5678
|
|
|
5351
|
-
//
|
|
5352
|
-
|
|
5353
|
-
{
|
|
5354
|
-
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
5355
|
-
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
5356
|
-
return;
|
|
5357
|
-
|
|
5358
|
-
// fade off when not touching or paused
|
|
5359
|
-
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
5360
|
-
if (!alpha || paused)
|
|
5361
|
-
return;
|
|
5362
|
-
|
|
5363
|
-
// setup the canvas
|
|
5364
|
-
const context = overlayContext;
|
|
5365
|
-
context.save();
|
|
5366
|
-
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
5367
|
-
context.strokeStyle = '#fff';
|
|
5368
|
-
context.lineWidth = 3;
|
|
5369
|
-
|
|
5370
|
-
// draw left analog stick
|
|
5371
|
-
context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
|
|
5372
|
-
context.beginPath();
|
|
5373
|
-
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
5374
|
-
if (touchGamepadAnalog) // draw circle shaped gamepad
|
|
5375
|
-
{
|
|
5376
|
-
context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
|
|
5377
|
-
context.fill();
|
|
5378
|
-
context.stroke();
|
|
5379
|
-
}
|
|
5380
|
-
else // draw cross shaped gamepad
|
|
5381
|
-
{
|
|
5382
|
-
for (let i=10; i--;)
|
|
5383
|
-
{
|
|
5384
|
-
const angle = i*PI/4;
|
|
5385
|
-
context.arc(stickCenter.x, stickCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
|
|
5386
|
-
i%2 && context.arc(stickCenter.x, stickCenter.y, touchGamepadSize*.33, angle, angle);
|
|
5387
|
-
i===1 && context.fill();
|
|
5388
|
-
}
|
|
5389
|
-
context.stroke();
|
|
5390
|
-
}
|
|
5391
|
-
|
|
5392
|
-
// draw right face buttons
|
|
5393
|
-
const buttonCenter = touchGamepadButtonCenter();
|
|
5394
|
-
const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
|
|
5395
|
-
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
5396
|
-
{
|
|
5397
|
-
const j = mod(i-1, 4);
|
|
5398
|
-
let button = touchGamepadButtonCount > 2 ?
|
|
5399
|
-
j : min(j, touchGamepadButtonCount-1);
|
|
5400
|
-
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
5401
|
-
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
5402
|
-
const pos = buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
|
|
5403
|
-
context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
|
|
5404
|
-
context.beginPath();
|
|
5405
|
-
context.arc(pos.x, pos.y, buttonSize, 0,9);
|
|
5406
|
-
context.fill();
|
|
5407
|
-
context.stroke();
|
|
5679
|
+
// set canvas back to normal
|
|
5680
|
+
context.restore();
|
|
5408
5681
|
}
|
|
5409
|
-
|
|
5410
|
-
// set canvas back to normal
|
|
5411
|
-
context.restore();
|
|
5412
5682
|
}
|
|
5413
5683
|
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
/** Request to lock the pointer, does not work on touch devices
|
|
5418
|
-
* @memberof Input */
|
|
5419
|
-
function pointerLockRequest()
|
|
5684
|
+
// center position for right tocuh pad face buttons
|
|
5685
|
+
function touchGamepadButtonCenter()
|
|
5420
5686
|
{
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
* @memberof Input */
|
|
5427
|
-
function pointerLockExit() { document.exitPointerLock && document.exitPointerLock(); }
|
|
5428
|
-
|
|
5429
|
-
/** Check if pointer is locked (true if locked)
|
|
5430
|
-
* @return {boolean}
|
|
5431
|
-
* @memberof Input */
|
|
5432
|
-
function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
|
|
5687
|
+
const center = mainCanvasSize.subtract(vec2(touchGamepadSize));
|
|
5688
|
+
if (touchGamepadButtonCount === 2)
|
|
5689
|
+
center.x += touchGamepadSize/2;
|
|
5690
|
+
return center;
|
|
5691
|
+
}
|
|
5433
5692
|
/**
|
|
5434
5693
|
* LittleJS Audio System
|
|
5435
5694
|
* - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
|
|
@@ -6169,7 +6428,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
|
|
|
6169
6428
|
}
|
|
6170
6429
|
}
|
|
6171
6430
|
|
|
6172
|
-
/** Return the exact position of the
|
|
6431
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
6173
6432
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
6174
6433
|
* @param {Vector2} posStart
|
|
6175
6434
|
* @param {Vector2} posEnd
|
|
@@ -6333,8 +6592,7 @@ class CanvasLayer extends EngineObject
|
|
|
6333
6592
|
/** Destroy this canvas layer */
|
|
6334
6593
|
destroy()
|
|
6335
6594
|
{
|
|
6336
|
-
if (this.destroyed)
|
|
6337
|
-
return;
|
|
6595
|
+
if (this.destroyed) return;
|
|
6338
6596
|
|
|
6339
6597
|
this.textureInfo.destroyWebGLTexture();
|
|
6340
6598
|
super.destroy();
|
|
@@ -6743,7 +7001,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6743
7001
|
return false;
|
|
6744
7002
|
}
|
|
6745
7003
|
|
|
6746
|
-
/** Return the exact position of the
|
|
7004
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
6747
7005
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
6748
7006
|
* @param {Vector2} posStart
|
|
6749
7007
|
* @param {Vector2} posEnd
|
|
@@ -6886,9 +7144,9 @@ class ParticleEmitter extends EngineObject
|
|
|
6886
7144
|
/** @property {Color} - Color at start of life 2, randomized between start colors */
|
|
6887
7145
|
this.colorStartB = colorStartB.copy();
|
|
6888
7146
|
/** @property {Color} - Color at end of life 1, randomized between end colors */
|
|
6889
|
-
this.colorEndA
|
|
7147
|
+
this.colorEndA = colorEndA.copy();
|
|
6890
7148
|
/** @property {Color} - Color at end of life 2, randomized between end colors */
|
|
6891
|
-
this.colorEndB
|
|
7149
|
+
this.colorEndB = colorEndB.copy();
|
|
6892
7150
|
/** @property {boolean} - Should color be randomized linearly or across each component */
|
|
6893
7151
|
this.randomColorLinear = randomColorLinear;
|
|
6894
7152
|
|
|
@@ -6972,8 +7230,10 @@ class ParticleEmitter extends EngineObject
|
|
|
6972
7230
|
if (debugParticles)
|
|
6973
7231
|
{
|
|
6974
7232
|
// show emitter bounds
|
|
6975
|
-
|
|
6976
|
-
|
|
7233
|
+
if (typeof this.emitSize === 'number')
|
|
7234
|
+
debugCircle(this.pos, this.emitSize/2, '#0f0');
|
|
7235
|
+
else
|
|
7236
|
+
debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle);
|
|
6977
7237
|
}
|
|
6978
7238
|
}
|
|
6979
7239
|
|
|
@@ -6983,8 +7243,8 @@ class ParticleEmitter extends EngineObject
|
|
|
6983
7243
|
{
|
|
6984
7244
|
// spawn a particle
|
|
6985
7245
|
let pos = typeof this.emitSize === 'number' ? // check if number was used
|
|
6986
|
-
randInCircle(this.emitSize/2)
|
|
6987
|
-
: vec2(rand(-.5,.5), rand(-.5,.5))
|
|
7246
|
+
randInCircle(this.emitSize/2) // circle emitter
|
|
7247
|
+
: vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
|
|
6988
7248
|
.multiply(this.emitSize).rotate(this.angle)
|
|
6989
7249
|
let angle = rand(this.particleConeAngle, -this.particleConeAngle);
|
|
6990
7250
|
if (!this.localSpace)
|
|
@@ -8688,11 +8948,14 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
8688
8948
|
/**
|
|
8689
8949
|
* LittleJS User Interface Plugin
|
|
8690
8950
|
* - call new UISystemPlugin() to setup the UI system
|
|
8951
|
+
* - Gamepad and keyboard navigation support
|
|
8691
8952
|
* - Nested Menus
|
|
8692
8953
|
* - Text
|
|
8693
8954
|
* - Buttons
|
|
8694
8955
|
* - Checkboxes
|
|
8695
8956
|
* - Images
|
|
8957
|
+
* - Scrollbars
|
|
8958
|
+
* - Video
|
|
8696
8959
|
* @namespace UISystem
|
|
8697
8960
|
*/
|
|
8698
8961
|
|
|
@@ -8703,6 +8966,20 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
8703
8966
|
* @memberof UISystem */
|
|
8704
8967
|
let uiSystem;
|
|
8705
8968
|
|
|
8969
|
+
/** Enable UI system debug drawing
|
|
8970
|
+
* 0=off, 1=normal, 2=show invisible
|
|
8971
|
+
* @type {number}
|
|
8972
|
+
* @default
|
|
8973
|
+
* @memberof UISystem */
|
|
8974
|
+
let uiDebug = 0;
|
|
8975
|
+
|
|
8976
|
+
/** Enable UI system debug drawing
|
|
8977
|
+
* 0=off, 1=normal, 2=show invisible
|
|
8978
|
+
* @param {number|boolean} enable
|
|
8979
|
+
* @memberof UISystem */
|
|
8980
|
+
function uiSetDebug(debugMode)
|
|
8981
|
+
{ uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
|
|
8982
|
+
|
|
8706
8983
|
///////////////////////////////////////////////////////////////////////////////
|
|
8707
8984
|
/**
|
|
8708
8985
|
* UI System Global Object
|
|
@@ -8741,7 +9018,7 @@ class UISystemPlugin
|
|
|
8741
9018
|
/** @property {number} - Default rounded rect corner radius for UI elements */
|
|
8742
9019
|
this.defaultCornerRadius = 0;
|
|
8743
9020
|
/** @property {number} - Default scale to use for fitting text to object */
|
|
8744
|
-
this.
|
|
9021
|
+
this.defaultTextFitScale = .8;
|
|
8745
9022
|
/** @property {string} - Default font for UI elements */
|
|
8746
9023
|
this.defaultFont = fontDefault;
|
|
8747
9024
|
/** @property {Sound} - Default sound when interactive UI element is pressed */
|
|
@@ -8756,6 +9033,21 @@ class UISystemPlugin
|
|
|
8756
9033
|
this.defaultShadowBlur = 5;
|
|
8757
9034
|
/** @property {Vector2} - Offset of shadow blur */
|
|
8758
9035
|
this.defaultShadowOffset = vec2(5);
|
|
9036
|
+
/** @property {number} - If set ui coords will be renormalized to this canvas height */
|
|
9037
|
+
this.nativeHeight = 0;
|
|
9038
|
+
|
|
9039
|
+
// navigation properties
|
|
9040
|
+
/** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
|
|
9041
|
+
this.navigationObject = undefined;
|
|
9042
|
+
/** @property {Timer} - Cooldown timer for navigation inputs */
|
|
9043
|
+
this.navigationTimer = new Timer(undefined, true);
|
|
9044
|
+
/** @property {number} - Time between navigation inputs in seconds */
|
|
9045
|
+
this.navigationDelay = .2;
|
|
9046
|
+
/** @property {boolean} - should the navigation be horizontal, vertical, or both? */
|
|
9047
|
+
this.navigationDirection = 1;
|
|
9048
|
+
/** @property {boolean} - True if user last used navigation instead of mouse */
|
|
9049
|
+
this.navigationMode = false;
|
|
9050
|
+
|
|
8759
9051
|
// system state
|
|
8760
9052
|
/** @property {Array<UIObject>} - List of all UI elements */
|
|
8761
9053
|
this.uiObjects = [];
|
|
@@ -8767,50 +9059,118 @@ class UISystemPlugin
|
|
|
8767
9059
|
this.hoverObject = undefined;
|
|
8768
9060
|
/** @property {UIObject} - Hover object at start of update */
|
|
8769
9061
|
this.lastHoverObject = undefined;
|
|
8770
|
-
/** @property {
|
|
8771
|
-
this.
|
|
9062
|
+
/** @property {UIObject} - Current confirm menu being shown */
|
|
9063
|
+
this.confirmDialog = undefined;
|
|
8772
9064
|
|
|
8773
9065
|
engineAddPlugin(uiUpdate, uiRender);
|
|
8774
9066
|
|
|
9067
|
+
// set object position in parent space
|
|
9068
|
+
function updateTransforms(o)
|
|
9069
|
+
{
|
|
9070
|
+
if (!o.parent) return;
|
|
9071
|
+
o.pos.x = o.localPos.x + o.parent.pos.x;
|
|
9072
|
+
o.pos.y = o.localPos.y + o.parent.pos.y;
|
|
9073
|
+
}
|
|
9074
|
+
|
|
8775
9075
|
// setup recursive update and render
|
|
8776
9076
|
// update in reverse order to detect mouse enter/leave
|
|
8777
9077
|
function uiUpdate()
|
|
8778
9078
|
{
|
|
8779
|
-
|
|
9079
|
+
if (uiSystem.activeObject && !uiSystem.activeObject.visible)
|
|
9080
|
+
uiSystem.activeObject = undefined;
|
|
9081
|
+
|
|
9082
|
+
// reset hover object at start of update
|
|
9083
|
+
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
9084
|
+
uiSystem.hoverObject = undefined;
|
|
9085
|
+
|
|
9086
|
+
if (mouseWasPressed(0))
|
|
8780
9087
|
{
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
updateInvisibleObject(c);
|
|
8784
|
-
o.updateInvisible();
|
|
9088
|
+
uiSystem.navigationMode = false;
|
|
9089
|
+
uiSystem.navigationObject = undefined;
|
|
8785
9090
|
}
|
|
8786
|
-
|
|
9091
|
+
|
|
9092
|
+
// navigation with gamepad/keyboard
|
|
9093
|
+
const navigableObjects = uiSystem.getNavigableObjects();
|
|
9094
|
+
if (!navigableObjects.length)
|
|
9095
|
+
uiSystem.navigationObject = undefined;
|
|
9096
|
+
else
|
|
8787
9097
|
{
|
|
8788
|
-
if
|
|
9098
|
+
// unselect object if it is no longer navigable
|
|
9099
|
+
if (!navigableObjects.includes(uiSystem.navigationObject))
|
|
9100
|
+
uiSystem.navigationObject = undefined;
|
|
9101
|
+
|
|
9102
|
+
if (!isTouchDevice)
|
|
9103
|
+
if (uiSystem.navigationMode && !uiSystem.navigationObject)
|
|
8789
9104
|
{
|
|
8790
|
-
//
|
|
8791
|
-
|
|
8792
|
-
o.pos = o.localPos.add(o.parent.pos);
|
|
8793
|
-
// update in reverse order to detect mouse enter/leave
|
|
8794
|
-
for (let i=o.children.length; i--;)
|
|
8795
|
-
updateObject(o.children[i]);
|
|
8796
|
-
o.update();
|
|
9105
|
+
// select first auto focus object
|
|
9106
|
+
uiSystem.navigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
|
|
8797
9107
|
}
|
|
8798
|
-
|
|
8799
|
-
|
|
9108
|
+
|
|
9109
|
+
// navigate with dpad or left stick
|
|
9110
|
+
if (!uiSystem.navigationTimer.active())
|
|
9111
|
+
{
|
|
9112
|
+
// navigate through list with gamepad or keyboard
|
|
9113
|
+
const direction = sign(uiSystem.getNavigationDirection());
|
|
9114
|
+
if (direction)
|
|
9115
|
+
{
|
|
9116
|
+
let newNavigationObject;
|
|
9117
|
+
if (!uiSystem.navigationObject)
|
|
9118
|
+
{
|
|
9119
|
+
// use auto select object
|
|
9120
|
+
newNavigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
|
|
9121
|
+
|
|
9122
|
+
if (!newNavigationObject)
|
|
9123
|
+
{
|
|
9124
|
+
// try first or last object
|
|
9125
|
+
const newIndex = direction > 0 ? 0 : navigableObjects.length-1;
|
|
9126
|
+
newNavigationObject = navigableObjects[newIndex];
|
|
9127
|
+
}
|
|
9128
|
+
}
|
|
9129
|
+
else
|
|
9130
|
+
{
|
|
9131
|
+
const currentIndex = navigableObjects.indexOf(uiSystem.navigationObject);
|
|
9132
|
+
const newIndex = mod(currentIndex + direction, navigableObjects.length);
|
|
9133
|
+
newNavigationObject = navigableObjects[newIndex];
|
|
9134
|
+
}
|
|
9135
|
+
|
|
9136
|
+
if (uiSystem.navigationObject !== newNavigationObject)
|
|
9137
|
+
{
|
|
9138
|
+
uiSystem.navigationMode = true;
|
|
9139
|
+
uiSystem.hoverObject = undefined;
|
|
9140
|
+
uiSystem.navigationObject = newNavigationObject;
|
|
9141
|
+
uiSystem.navigationTimer.set(uiSystem.navigationDelay);
|
|
9142
|
+
newNavigationObject.soundPress &&
|
|
9143
|
+
newNavigationObject.soundPress.play();
|
|
9144
|
+
}
|
|
9145
|
+
}
|
|
9146
|
+
}
|
|
9147
|
+
|
|
9148
|
+
// activate the navigation object when pressed
|
|
9149
|
+
if (uiSystem.navigationObject)
|
|
9150
|
+
if (uiSystem.getNavigationWasPressed())
|
|
9151
|
+
uiSystem.navigationObject.navigatePressed();
|
|
8800
9152
|
}
|
|
8801
|
-
// reset hover object at start of update
|
|
8802
|
-
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
8803
|
-
uiSystem.hoverObject = undefined;
|
|
8804
9153
|
|
|
8805
9154
|
// update in reverse order so topmost objects get priority
|
|
8806
9155
|
for (let i = uiSystem.uiObjects.length; i--;)
|
|
8807
9156
|
{
|
|
8808
9157
|
const o = uiSystem.uiObjects[i];
|
|
8809
|
-
o.parent || updateObject(o)
|
|
9158
|
+
o.parent || updateObject(o);
|
|
8810
9159
|
}
|
|
8811
9160
|
|
|
8812
9161
|
// remove destroyed objects
|
|
8813
9162
|
uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
|
|
9163
|
+
|
|
9164
|
+
function updateObject(o)
|
|
9165
|
+
{
|
|
9166
|
+
if (!o.visible) return;
|
|
9167
|
+
|
|
9168
|
+
// update in reverse order to detect mouse enter/leave
|
|
9169
|
+
updateTransforms(o);
|
|
9170
|
+
for (let i=o.children.length; i--;)
|
|
9171
|
+
updateObject(o.children[i]);
|
|
9172
|
+
o.update();
|
|
9173
|
+
}
|
|
8814
9174
|
}
|
|
8815
9175
|
function uiRender()
|
|
8816
9176
|
{
|
|
@@ -8827,15 +9187,29 @@ class UISystemPlugin
|
|
|
8827
9187
|
|
|
8828
9188
|
function renderObject(o)
|
|
8829
9189
|
{
|
|
8830
|
-
if (!o.visible)
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
9190
|
+
if (!o.visible) return;
|
|
9191
|
+
|
|
9192
|
+
// render object and children
|
|
9193
|
+
updateTransforms(o);
|
|
8834
9194
|
o.render();
|
|
8835
9195
|
for (const c of o.children)
|
|
8836
9196
|
renderObject(c);
|
|
8837
9197
|
}
|
|
8838
9198
|
uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
|
|
9199
|
+
|
|
9200
|
+
if (uiDebug > 0)
|
|
9201
|
+
{
|
|
9202
|
+
// debug render all objects
|
|
9203
|
+
function renderDebug(o, visible=true)
|
|
9204
|
+
{
|
|
9205
|
+
visible &&= !!o.visible;
|
|
9206
|
+
updateTransforms(o);
|
|
9207
|
+
o.renderDebug(visible);
|
|
9208
|
+
for (const c of o.children)
|
|
9209
|
+
renderDebug(c, visible);
|
|
9210
|
+
}
|
|
9211
|
+
uiSystem.uiObjects.forEach(o=> o.parent || renderDebug(o));
|
|
9212
|
+
}
|
|
8839
9213
|
context.restore();
|
|
8840
9214
|
}
|
|
8841
9215
|
}
|
|
@@ -8888,8 +9262,8 @@ class UISystemPlugin
|
|
|
8888
9262
|
else
|
|
8889
9263
|
context.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
|
|
8890
9264
|
context.fill();
|
|
8891
|
-
context.shadowColor = '#0000'
|
|
8892
|
-
if (lineWidth)
|
|
9265
|
+
context.shadowColor = '#0000';
|
|
9266
|
+
if (lineWidth && lineColor.a > 0)
|
|
8893
9267
|
{
|
|
8894
9268
|
context.strokeStyle = lineColor.toString();
|
|
8895
9269
|
context.lineWidth = lineWidth;
|
|
@@ -8924,10 +9298,24 @@ class UISystemPlugin
|
|
|
8924
9298
|
* @param {TileInfo} tileInfo
|
|
8925
9299
|
* @param {Color} [color=uiSystem.defaultColor]
|
|
8926
9300
|
* @param {number} [angle]
|
|
8927
|
-
* @param {boolean} [mirror]
|
|
8928
|
-
|
|
9301
|
+
* @param {boolean} [mirror]
|
|
9302
|
+
* @param {Color} [shadowColor]
|
|
9303
|
+
* @param {number} [shadowBlur]
|
|
9304
|
+
* @param {Color} [shadowOffset] */
|
|
9305
|
+
drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
|
|
8929
9306
|
{
|
|
8930
|
-
|
|
9307
|
+
const context = uiSystem.uiContext;
|
|
9308
|
+
if (shadowBlur || shadowOffset.x || shadowOffset.y)
|
|
9309
|
+
if (shadowColor.a > 0)
|
|
9310
|
+
{
|
|
9311
|
+
// setup shadow
|
|
9312
|
+
context.shadowColor = shadowColor.toString();
|
|
9313
|
+
context.shadowBlur = shadowBlur;
|
|
9314
|
+
context.shadowOffsetX = shadowOffset.x;
|
|
9315
|
+
context.shadowOffsetY = shadowOffset.y;
|
|
9316
|
+
}
|
|
9317
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, CLEAR_BLACK, false, true, context);
|
|
9318
|
+
context.shadowColor = '#0000';
|
|
8931
9319
|
}
|
|
8932
9320
|
|
|
8933
9321
|
/** Draw text to the UI context
|
|
@@ -8942,12 +9330,27 @@ class UISystemPlugin
|
|
|
8942
9330
|
* @param {string} [fontStyle]
|
|
8943
9331
|
* @param {boolean} [applyMaxWidth=true]
|
|
8944
9332
|
* @param {Vector2} [textShadow]
|
|
8945
|
-
|
|
8946
|
-
|
|
9333
|
+
* @param {Color} [shadowColor]
|
|
9334
|
+
* @param {number} [shadowBlur]
|
|
9335
|
+
* @param {Color} [shadowOffset] */
|
|
9336
|
+
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())
|
|
8947
9337
|
{
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
9338
|
+
const context = uiSystem.uiContext;
|
|
9339
|
+
if (shadowColor.a > 0)
|
|
9340
|
+
{
|
|
9341
|
+
if (textShadow)
|
|
9342
|
+
drawTextScreen(text, pos.add(textShadow), size.y, shadowColor, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, context);
|
|
9343
|
+
if (shadowBlur || shadowOffset.x || shadowOffset.y)
|
|
9344
|
+
{
|
|
9345
|
+
// setup shadow
|
|
9346
|
+
context.shadowColor = shadowColor.toString();
|
|
9347
|
+
context.shadowBlur = shadowBlur;
|
|
9348
|
+
context.shadowOffsetX = shadowOffset.x;
|
|
9349
|
+
context.shadowOffsetY = shadowOffset.y;
|
|
9350
|
+
}
|
|
9351
|
+
}
|
|
9352
|
+
drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, context);
|
|
9353
|
+
context.shadowColor = '#0000';
|
|
8951
9354
|
}
|
|
8952
9355
|
|
|
8953
9356
|
/**
|
|
@@ -8961,7 +9364,7 @@ class UISystemPlugin
|
|
|
8961
9364
|
* @param {DragAndDropCallback} [onDrop] - when a file is dropped
|
|
8962
9365
|
* @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
|
|
8963
9366
|
* @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
|
|
8964
|
-
* @param {DragAndDropCallback} [onDragOver] -
|
|
9367
|
+
* @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
|
|
8965
9368
|
setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
|
|
8966
9369
|
{
|
|
8967
9370
|
function setCallback(callback, listenerType)
|
|
@@ -8977,8 +9380,7 @@ class UISystemPlugin
|
|
|
8977
9380
|
|
|
8978
9381
|
/** Convert a screen space position to native UI position
|
|
8979
9382
|
* @param {Vector2} pos
|
|
8980
|
-
* @return {Vector2}
|
|
8981
|
-
*/
|
|
9383
|
+
* @return {Vector2} */
|
|
8982
9384
|
screenToNative(pos)
|
|
8983
9385
|
{
|
|
8984
9386
|
if (!uiSystem.nativeHeight)
|
|
@@ -9001,6 +9403,157 @@ class UISystemPlugin
|
|
|
9001
9403
|
for (const o of this.uiObjects)
|
|
9002
9404
|
o.parent || o.destroy();
|
|
9003
9405
|
this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
|
|
9406
|
+
this.activeObject = undefined;
|
|
9407
|
+
this.hoverObject = undefined;
|
|
9408
|
+
this.lastHoverObject = undefined;
|
|
9409
|
+
}
|
|
9410
|
+
|
|
9411
|
+
/** Get all navigable UI objects sorted by navigationIndex
|
|
9412
|
+
* @return {Array<UIObject>} */
|
|
9413
|
+
getNavigableObjects()
|
|
9414
|
+
{
|
|
9415
|
+
function getNavigableRecursive(o)
|
|
9416
|
+
{
|
|
9417
|
+
if (!o.visible || o.disabled)
|
|
9418
|
+
return; // skip children if parent is invisible or disabled
|
|
9419
|
+
|
|
9420
|
+
if (o.isInteractive() && o.navigationIndex !== undefined)
|
|
9421
|
+
objects.push(o);
|
|
9422
|
+
for (let i=o.children.length; i--;)
|
|
9423
|
+
getNavigableRecursive(o.children[i]);
|
|
9424
|
+
}
|
|
9425
|
+
|
|
9426
|
+
// get all the valid navigable objects recursively
|
|
9427
|
+
let objects = [];
|
|
9428
|
+
for (let i = uiSystem.uiObjects.length; i--;)
|
|
9429
|
+
{
|
|
9430
|
+
const o = uiSystem.uiObjects[i];
|
|
9431
|
+
if (uiSystem.confirmDialog && o !== uiSystem.confirmDialog)
|
|
9432
|
+
continue;
|
|
9433
|
+
o.parent || getNavigableRecursive(o);
|
|
9434
|
+
}
|
|
9435
|
+
|
|
9436
|
+
// sort by navigationIndex (lower numbers first)
|
|
9437
|
+
objects.sort((a, b)=> a.navigationIndex - b.navigationIndex);
|
|
9438
|
+
return objects;
|
|
9439
|
+
}
|
|
9440
|
+
|
|
9441
|
+
/** Get navigation direction from gamepad or keyboard
|
|
9442
|
+
* @return {number} */
|
|
9443
|
+
getNavigationDirection()
|
|
9444
|
+
{
|
|
9445
|
+
const vertical = uiSystem.navigationDirection === 1;
|
|
9446
|
+
const both = uiSystem.navigationDirection === 2;
|
|
9447
|
+
if (isUsingGamepad)
|
|
9448
|
+
{
|
|
9449
|
+
const stick = gamepadStick(0, gamepadPrimary);
|
|
9450
|
+
const dpad = gamepadDpad(gamepadPrimary);
|
|
9451
|
+
if (both)
|
|
9452
|
+
return -(stick.y || dpad.y) || (stick.x || dpad.x);
|
|
9453
|
+
return vertical ? -(stick.y || dpad.y) : (stick.x || dpad.x);
|
|
9454
|
+
}
|
|
9455
|
+
const up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight';
|
|
9456
|
+
if (both)
|
|
9457
|
+
{
|
|
9458
|
+
return keyIsDown(up) || keyIsDown(left) ? -1 :
|
|
9459
|
+
keyIsDown(down) || keyIsDown(right) ? 1 : 0;
|
|
9460
|
+
}
|
|
9461
|
+
const back = vertical ? up : left;
|
|
9462
|
+
const forward = vertical ? down : right;
|
|
9463
|
+
return keyIsDown(back) ? -1 : keyIsDown(forward) ? 1 : 0;
|
|
9464
|
+
}
|
|
9465
|
+
|
|
9466
|
+
/** Get other axis navigation direction from gamepad or keyboard
|
|
9467
|
+
* @return {Vector2} */
|
|
9468
|
+
getNavigationOtherDirection()
|
|
9469
|
+
{
|
|
9470
|
+
if (uiSystem.navigationDirection === 2)
|
|
9471
|
+
return 0; // other direction disabled
|
|
9472
|
+
|
|
9473
|
+
const vertical = uiSystem.navigationDirection === 1;
|
|
9474
|
+
if (isUsingGamepad)
|
|
9475
|
+
{
|
|
9476
|
+
const stick = gamepadStick(0, gamepadPrimary);
|
|
9477
|
+
const dpad = gamepadDpad(gamepadPrimary);
|
|
9478
|
+
return !vertical ? (stick.y || dpad.y) : (stick.x || dpad.x);
|
|
9479
|
+
}
|
|
9480
|
+
const back = !vertical ? 'ArrowUp' : 'ArrowLeft';
|
|
9481
|
+
const forward = !vertical ? 'ArrowDown' : 'ArrowRight';
|
|
9482
|
+
return keyIsDown(back) ? -1 : keyIsDown(forward) ? 1 : 0;
|
|
9483
|
+
}
|
|
9484
|
+
|
|
9485
|
+
/** Get if navigation button was pressed from gamepad or keyboard
|
|
9486
|
+
* @return {boolean} */
|
|
9487
|
+
getNavigationWasPressed()
|
|
9488
|
+
{
|
|
9489
|
+
return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
|
|
9490
|
+
keyWasPressed('Space') || keyWasPressed('Enter');
|
|
9491
|
+
}
|
|
9492
|
+
|
|
9493
|
+
/** Show a confirmation dialog with Yes/No buttons
|
|
9494
|
+
* Centers the dialog on the screen with darkened background
|
|
9495
|
+
* @param {string} [text] - The message to display
|
|
9496
|
+
* @param {Function} [yesCallback] - Called when Yes is clicked
|
|
9497
|
+
* @param {Function} [noCallback] - Called when No is clicked
|
|
9498
|
+
* @param {Vector2} [size] - Size of the confirmation dialog
|
|
9499
|
+
* @param {string} [exitKey] - Key that can exit the menu
|
|
9500
|
+
* @return {UIObject} The confirmation menu object
|
|
9501
|
+
*/
|
|
9502
|
+
showConfirmDialog(text='Are you sure?', yesCallback, noCallback, size=vec2(500,250), exitKey='Escape')
|
|
9503
|
+
{
|
|
9504
|
+
ASSERT(!uiSystem.confirmDialog);
|
|
9505
|
+
|
|
9506
|
+
const savedNavigationDirection = uiSystem.navigationDirection;
|
|
9507
|
+
|
|
9508
|
+
// allow both axies for navigation
|
|
9509
|
+
uiSystem.navigationDirection = 2;
|
|
9510
|
+
|
|
9511
|
+
// confirm menu
|
|
9512
|
+
const confirmMenu = new UIObject(vec2(), size);
|
|
9513
|
+
uiSystem.confirmDialog = confirmMenu;
|
|
9514
|
+
confirmMenu.onRender = ()=>
|
|
9515
|
+
{
|
|
9516
|
+
confirmMenu.pos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
|
|
9517
|
+
const backgroundColor = hsl(0,0,0,.7);
|
|
9518
|
+
uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
|
|
9519
|
+
}
|
|
9520
|
+
confirmMenu.onUpdate = ()=>
|
|
9521
|
+
{
|
|
9522
|
+
if (keyWasPressed(exitKey))
|
|
9523
|
+
closeMenu();
|
|
9524
|
+
}
|
|
9525
|
+
confirmMenu.isMouseOverlapping = ()=> true; // always hover
|
|
9526
|
+
|
|
9527
|
+
// title text
|
|
9528
|
+
const gap = 50;
|
|
9529
|
+
const textTitle = new UIText(vec2(0,-50), vec2(size.x-gap,70), text);
|
|
9530
|
+
confirmMenu.addChild(textTitle);
|
|
9531
|
+
|
|
9532
|
+
// yes button
|
|
9533
|
+
const buttonYes = new UIButton(vec2(-80,50), vec2(120,70), 'Yes');
|
|
9534
|
+
buttonYes.textHeight = 40;
|
|
9535
|
+
buttonYes.navigationIndex = 1;
|
|
9536
|
+
buttonYes.hoverColor = hsl(0,1,.5);
|
|
9537
|
+
buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
|
|
9538
|
+
confirmMenu.addChild(buttonYes);
|
|
9539
|
+
|
|
9540
|
+
// no button
|
|
9541
|
+
const buttonNo = new UIButton(vec2(80,50), vec2(120,70), 'No');
|
|
9542
|
+
buttonNo.textHeight = 40;
|
|
9543
|
+
buttonNo.navigationIndex = 2;
|
|
9544
|
+
buttonNo.navigationAutoSelect = true;
|
|
9545
|
+
buttonNo.onClick = ()=> { closeMenu(); noCallback && noCallback(); };
|
|
9546
|
+
confirmMenu.addChild(buttonNo);
|
|
9547
|
+
|
|
9548
|
+
// close menu and return to normal navigation
|
|
9549
|
+
function closeMenu()
|
|
9550
|
+
{
|
|
9551
|
+
ASSERT(uiSystem.confirmDialog === confirmMenu);
|
|
9552
|
+
confirmMenu.destroy();
|
|
9553
|
+
uiSystem.confirmDialog = undefined;
|
|
9554
|
+
uiSystem.navigationDirection = savedNavigationDirection;
|
|
9555
|
+
inputClear();
|
|
9556
|
+
}
|
|
9004
9557
|
}
|
|
9005
9558
|
}
|
|
9006
9559
|
|
|
@@ -9056,7 +9609,13 @@ class UIObject
|
|
|
9056
9609
|
/** @property {number} - Override for text height */
|
|
9057
9610
|
this.textHeight = undefined;
|
|
9058
9611
|
/** @property {number} - Scale text to fit in the object */
|
|
9059
|
-
this.
|
|
9612
|
+
this.textFitScale = uiSystem.defaultTextFitScale;
|
|
9613
|
+
/** @property {Vector2} - How much to offset the text shadow or undefined */
|
|
9614
|
+
this.textShadow = undefined;
|
|
9615
|
+
/** @property {number} - Color for text line drawing */
|
|
9616
|
+
this.textLineColor = uiSystem.defaultLineColor.copy();
|
|
9617
|
+
/** @property {number} - Width for text line drawing */
|
|
9618
|
+
this.textLineWidth = 0;
|
|
9060
9619
|
/** @property {boolean} - Should this object be drawn */
|
|
9061
9620
|
this.visible = true;
|
|
9062
9621
|
/** @property {Array<UIObject>} - A list of this object's children */
|
|
@@ -9082,16 +9641,17 @@ class UIObject
|
|
|
9082
9641
|
/** @property {number} - Size of shadow blur */
|
|
9083
9642
|
this.shadowBlur = uiSystem.defaultShadowBlur;
|
|
9084
9643
|
/** @property {Vector2} - Offset of shadow blur */
|
|
9085
|
-
this.shadowOffset = uiSystem.defaultShadowOffset
|
|
9086
|
-
|
|
9644
|
+
this.shadowOffset = uiSystem.defaultShadowOffset?.copy();
|
|
9645
|
+
/** @property {number} - Optional navigation order index, lower values are selected first */
|
|
9646
|
+
this.navigationIndex = undefined;
|
|
9647
|
+
/** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
|
|
9648
|
+
this.navigationAutoSelect = false;
|
|
9087
9649
|
|
|
9088
|
-
|
|
9089
|
-
this.textShadow = undefined;
|
|
9650
|
+
uiSystem.uiObjects.push(this);
|
|
9090
9651
|
}
|
|
9091
9652
|
|
|
9092
9653
|
/** Add a child UIObject to this object
|
|
9093
|
-
* @param {UIObject} child
|
|
9094
|
-
*/
|
|
9654
|
+
* @param {UIObject} child */
|
|
9095
9655
|
addChild(child)
|
|
9096
9656
|
{
|
|
9097
9657
|
ASSERT(!child.parent && !this.children.includes(child));
|
|
@@ -9100,8 +9660,7 @@ class UIObject
|
|
|
9100
9660
|
}
|
|
9101
9661
|
|
|
9102
9662
|
/** Remove a child UIObject from this object
|
|
9103
|
-
* @param {UIObject} child
|
|
9104
|
-
*/
|
|
9663
|
+
* @param {UIObject} child */
|
|
9105
9664
|
removeChild(child)
|
|
9106
9665
|
{
|
|
9107
9666
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
@@ -9109,7 +9668,6 @@ class UIObject
|
|
|
9109
9668
|
child.parent = undefined;
|
|
9110
9669
|
}
|
|
9111
9670
|
|
|
9112
|
-
|
|
9113
9671
|
/** Destroy this object, destroy its children, detach its parent, and mark it for removal */
|
|
9114
9672
|
destroy()
|
|
9115
9673
|
{
|
|
@@ -9125,9 +9683,9 @@ class UIObject
|
|
|
9125
9683
|
child.destroy();
|
|
9126
9684
|
}
|
|
9127
9685
|
}
|
|
9686
|
+
|
|
9128
9687
|
/** Check if the mouse is overlapping a box in screen space
|
|
9129
|
-
* @return {boolean} - True if overlapping
|
|
9130
|
-
*/
|
|
9688
|
+
* @return {boolean} - True if overlapping */
|
|
9131
9689
|
isMouseOverlapping()
|
|
9132
9690
|
{
|
|
9133
9691
|
if (!mouseInWindow) return false;
|
|
@@ -9144,11 +9702,16 @@ class UIObject
|
|
|
9144
9702
|
// call the custom update callback
|
|
9145
9703
|
this.onUpdate();
|
|
9146
9704
|
|
|
9705
|
+
// unset active if disabled
|
|
9706
|
+
if (this.disabled && this == uiSystem.activeObject)
|
|
9707
|
+
uiSystem.activeObject = undefined;
|
|
9708
|
+
|
|
9147
9709
|
const wasHover = uiSystem.lastHoverObject === this;
|
|
9148
9710
|
const isActive = this.isActiveObject();
|
|
9149
9711
|
const mouseDown = mouseIsDown(0);
|
|
9150
9712
|
const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
|
|
9151
9713
|
if (this.canBeHover)
|
|
9714
|
+
if (!uiSystem.navigationMode) // no mouse hover in navigation mode
|
|
9152
9715
|
if (mousePress || isActive || (!mouseDown && !isTouchDevice))
|
|
9153
9716
|
if (!uiSystem.hoverObject && this.isMouseOverlapping())
|
|
9154
9717
|
uiSystem.hoverObject = this;
|
|
@@ -9162,8 +9725,7 @@ class UIObject
|
|
|
9162
9725
|
{
|
|
9163
9726
|
if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
|
|
9164
9727
|
this.onPress();
|
|
9165
|
-
|
|
9166
|
-
this.soundPress.play();
|
|
9728
|
+
this.soundPress && this.soundPress.play();
|
|
9167
9729
|
if (uiSystem.activeObject && !isActive)
|
|
9168
9730
|
uiSystem.activeObject.onRelease();
|
|
9169
9731
|
uiSystem.activeObject = this;
|
|
@@ -9172,10 +9734,10 @@ class UIObject
|
|
|
9172
9734
|
if (!mouseDown && this.isActiveObject() && this.interactive)
|
|
9173
9735
|
{
|
|
9174
9736
|
this.onClick();
|
|
9175
|
-
|
|
9176
|
-
this.soundClick.play();
|
|
9737
|
+
this.soundClick && this.soundClick.play();
|
|
9177
9738
|
}
|
|
9178
9739
|
}
|
|
9740
|
+
|
|
9179
9741
|
// clear mouse was pressed state even when disabled
|
|
9180
9742
|
mousePress && inputClearKey(0,0,0,1,0);
|
|
9181
9743
|
}
|
|
@@ -9183,8 +9745,7 @@ class UIObject
|
|
|
9183
9745
|
if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
|
|
9184
9746
|
{
|
|
9185
9747
|
this.onRelease();
|
|
9186
|
-
|
|
9187
|
-
this.soundRelease.play();
|
|
9748
|
+
this.soundRelease && this.soundRelease.play();
|
|
9188
9749
|
uiSystem.activeObject = undefined;
|
|
9189
9750
|
}
|
|
9190
9751
|
|
|
@@ -9196,29 +9757,40 @@ class UIObject
|
|
|
9196
9757
|
/** Render the object, called automatically by plugin once each frame */
|
|
9197
9758
|
render()
|
|
9198
9759
|
{
|
|
9199
|
-
|
|
9760
|
+
// call the custom render callback
|
|
9761
|
+
this.onRender();
|
|
9200
9762
|
|
|
9201
|
-
|
|
9202
|
-
const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
|
|
9203
|
-
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
9204
|
-
}
|
|
9763
|
+
if (!this.size.x || !this.size.y) return;
|
|
9205
9764
|
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9765
|
+
const isNavigationObject = this.isNavigationObject();
|
|
9766
|
+
const lineColor = isNavigationObject ? this.color :
|
|
9767
|
+
this.interactive && this.isActiveObject() && !this.disabled ?
|
|
9768
|
+
this.color : this.lineColor;
|
|
9769
|
+
const color = isNavigationObject ? this.hoverColor :
|
|
9770
|
+
this.disabled ? this.disabledColor :
|
|
9771
|
+
this.interactive ?
|
|
9772
|
+
this.isHoverObject() ? this.hoverColor :
|
|
9773
|
+
this.isActiveObject() ? this.activeColor || this.color :
|
|
9774
|
+
this.color : this.color;
|
|
9775
|
+
const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
|
|
9776
|
+
|
|
9777
|
+
uiSystem.drawRect(this.pos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
9212
9778
|
}
|
|
9213
9779
|
|
|
9214
9780
|
/** Get the size for text with overrides and scale
|
|
9215
|
-
* @return {Vector2}
|
|
9216
|
-
*/
|
|
9781
|
+
* @return {Vector2} */
|
|
9217
9782
|
getTextSize()
|
|
9218
9783
|
{
|
|
9219
9784
|
return vec2(
|
|
9220
|
-
this.textWidth || this.
|
|
9221
|
-
this.textHeight || this.
|
|
9785
|
+
this.textWidth || this.textFitScale * this.size.x,
|
|
9786
|
+
this.textHeight || this.textFitScale * this.size.y);
|
|
9787
|
+
}
|
|
9788
|
+
|
|
9789
|
+
/** Called when the navigation button is pressed on this object */
|
|
9790
|
+
navigatePressed()
|
|
9791
|
+
{
|
|
9792
|
+
this.onClick();
|
|
9793
|
+
this.soundClick && this.soundClick.play();
|
|
9222
9794
|
}
|
|
9223
9795
|
|
|
9224
9796
|
/** @return {boolean} - Is the mouse hovering over this element */
|
|
@@ -9227,9 +9799,51 @@ class UIObject
|
|
|
9227
9799
|
/** @return {boolean} - Is the mouse held onto this element */
|
|
9228
9800
|
isActiveObject() { return uiSystem.activeObject === this; }
|
|
9229
9801
|
|
|
9230
|
-
/**
|
|
9802
|
+
/** @return {boolean} - Is the gamepad or keyboard navigation object */
|
|
9803
|
+
isNavigationObject() { return uiSystem.navigationObject === this; }
|
|
9804
|
+
|
|
9805
|
+
/** @return {boolean} - Can it be interacted with */
|
|
9806
|
+
isInteractive() { return this.interactive && this.visible && !this.disabled;}
|
|
9807
|
+
|
|
9808
|
+
/** Returns string containing info about this object for debugging
|
|
9809
|
+
* @return {string} */
|
|
9810
|
+
toString()
|
|
9811
|
+
{
|
|
9812
|
+
if (!debug) return;
|
|
9813
|
+
|
|
9814
|
+
let text = 'type = ' + this.constructor.name;
|
|
9815
|
+
if (this.text)
|
|
9816
|
+
text += '\ntext = ' + this.text;
|
|
9817
|
+
if (this.pos.x || this.pos.y)
|
|
9818
|
+
text += '\npos = ' + this.pos;
|
|
9819
|
+
if (this.localPos.x || this.localPos.y)
|
|
9820
|
+
text += '\localPos = ' + this.localPos;
|
|
9821
|
+
if (this.size.x || this.size.y)
|
|
9822
|
+
text += '\nsize = ' + this.size;
|
|
9823
|
+
if (this.color)
|
|
9824
|
+
text += '\ncolor = ' + this.color;
|
|
9825
|
+
return text;
|
|
9826
|
+
}
|
|
9827
|
+
|
|
9828
|
+
/** Called if uiDebug is enabled
|
|
9829
|
+
* @param {boolean} visible */
|
|
9830
|
+
renderDebug(visible=true)
|
|
9831
|
+
{
|
|
9832
|
+
// apply color based on state
|
|
9833
|
+
const color =
|
|
9834
|
+
!visible ? GREEN :
|
|
9835
|
+
this.isHoverObject() ? YELLOW :
|
|
9836
|
+
this.disabled ? PURPLE :
|
|
9837
|
+
this.interactive ? RED : BLUE;
|
|
9838
|
+
uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);
|
|
9839
|
+
}
|
|
9840
|
+
|
|
9841
|
+
/** Called each frame before object updates */
|
|
9231
9842
|
onUpdate() {}
|
|
9232
9843
|
|
|
9844
|
+
/** Called each frame before object renders */
|
|
9845
|
+
onRender() {}
|
|
9846
|
+
|
|
9233
9847
|
/** Called when the mouse enters the object */
|
|
9234
9848
|
onEnter() {}
|
|
9235
9849
|
|
|
@@ -9277,16 +9891,25 @@ class UIText extends UIObject
|
|
|
9277
9891
|
this.align = align;
|
|
9278
9892
|
this.font = font;
|
|
9279
9893
|
|
|
9280
|
-
// make text not outlined by default
|
|
9281
|
-
this.lineWidth = 0;
|
|
9282
9894
|
// text can not be a hover object by default
|
|
9283
9895
|
this.canBeHover = false;
|
|
9896
|
+
|
|
9897
|
+
// no background by default
|
|
9898
|
+
this.color = CLEAR_BLACK;
|
|
9899
|
+
this.shadowColor = CLEAR_BLACK;
|
|
9900
|
+
this.gradientColor = undefined;
|
|
9901
|
+
this.lineWidth = 0;
|
|
9902
|
+
|
|
9903
|
+
// use max fit scale by default
|
|
9904
|
+
this.textFitScale = 1;
|
|
9284
9905
|
}
|
|
9285
9906
|
render()
|
|
9286
9907
|
{
|
|
9287
|
-
|
|
9908
|
+
super.render();
|
|
9909
|
+
|
|
9910
|
+
// render the text
|
|
9288
9911
|
const textSize = this.getTextSize();
|
|
9289
|
-
uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.
|
|
9912
|
+
uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
9290
9913
|
}
|
|
9291
9914
|
}
|
|
9292
9915
|
|
|
@@ -9322,10 +9945,13 @@ class UITile extends UIObject
|
|
|
9322
9945
|
this.mirror = mirror;
|
|
9323
9946
|
// set properties
|
|
9324
9947
|
this.color = color.copy();
|
|
9948
|
+
|
|
9949
|
+
// no shadow by default
|
|
9950
|
+
this.shadowColor = CLEAR_BLACK;
|
|
9325
9951
|
}
|
|
9326
9952
|
render()
|
|
9327
9953
|
{
|
|
9328
|
-
uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror);
|
|
9954
|
+
uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
9329
9955
|
}
|
|
9330
9956
|
}
|
|
9331
9957
|
|
|
@@ -9350,6 +9976,9 @@ class UIButton extends UIObject
|
|
|
9350
9976
|
ASSERT(isString(text), 'ui button must be a string');
|
|
9351
9977
|
ASSERT(isColor(color), 'ui button color must be a color');
|
|
9352
9978
|
|
|
9979
|
+
/** @property {Vector2} - Text offset for the button */
|
|
9980
|
+
this.textOffset = vec2();
|
|
9981
|
+
|
|
9353
9982
|
// set properties
|
|
9354
9983
|
this.text = text;
|
|
9355
9984
|
this.color = color.copy();
|
|
@@ -9361,8 +9990,8 @@ class UIButton extends UIObject
|
|
|
9361
9990
|
|
|
9362
9991
|
// draw the text scaled to fit
|
|
9363
9992
|
const textSize = this.getTextSize();
|
|
9364
|
-
uiSystem.drawText(this.text, this.pos, textSize,
|
|
9365
|
-
this.textColor,
|
|
9993
|
+
uiSystem.drawText(this.text, this.pos.add(this.textOffset), textSize,
|
|
9994
|
+
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
9366
9995
|
}
|
|
9367
9996
|
}
|
|
9368
9997
|
|
|
@@ -9416,7 +10045,7 @@ class UICheckbox extends UIObject
|
|
|
9416
10045
|
const textSize = this.getTextSize();
|
|
9417
10046
|
const pos = this.pos.add(vec2(this.size.x,0));
|
|
9418
10047
|
uiSystem.drawText(this.text, pos, textSize,
|
|
9419
|
-
this.textColor,
|
|
10048
|
+
this.textColor, this.textLineWidth, this.textLineColor, 'left', this.font, this.fontStyle, false, this.textShadow);
|
|
9420
10049
|
}
|
|
9421
10050
|
}
|
|
9422
10051
|
|
|
@@ -9458,7 +10087,11 @@ class UIScrollbar extends UIObject
|
|
|
9458
10087
|
update()
|
|
9459
10088
|
{
|
|
9460
10089
|
super.update();
|
|
9461
|
-
if (this.
|
|
10090
|
+
if (!this.interactive)
|
|
10091
|
+
return;
|
|
10092
|
+
|
|
10093
|
+
const oldValue = this.value;
|
|
10094
|
+
if (this.isActiveObject())
|
|
9462
10095
|
{
|
|
9463
10096
|
// handle horizontal or vertical scrollbar
|
|
9464
10097
|
const isHorizontal = this.size.x > this.size.y;
|
|
@@ -9470,14 +10103,19 @@ class UIScrollbar extends UIObject
|
|
|
9470
10103
|
const handleWidth = barSize - handleSize;
|
|
9471
10104
|
const p1 = centerPos - handleWidth/2;
|
|
9472
10105
|
const p2 = centerPos + handleWidth/2;
|
|
9473
|
-
const oldValue = this.value;
|
|
9474
|
-
|
|
9475
10106
|
const p = uiSystem.screenToNative(mousePosScreen);
|
|
9476
10107
|
this.value = isHorizontal ?
|
|
9477
10108
|
percent(p.x, p1, p2) :
|
|
9478
10109
|
percent(p.y, p2, p1);
|
|
9479
|
-
this.value === oldValue || this.onChange();
|
|
9480
10110
|
}
|
|
10111
|
+
else if (this.isNavigationObject())
|
|
10112
|
+
{
|
|
10113
|
+
// gamepad/keyboard navigation adjustment
|
|
10114
|
+
const direction = uiSystem.getNavigationOtherDirection();
|
|
10115
|
+
if (!uiSystem.navigationTimer.active())
|
|
10116
|
+
this.value = clamp(this.value + direction*.01);
|
|
10117
|
+
}
|
|
10118
|
+
this.value === oldValue || this.onChange();
|
|
9481
10119
|
}
|
|
9482
10120
|
render()
|
|
9483
10121
|
{
|
|
@@ -9502,7 +10140,14 @@ class UIScrollbar extends UIObject
|
|
|
9502
10140
|
// draw the text scaled to fit on the scrollbar
|
|
9503
10141
|
const textSize = this.getTextSize();
|
|
9504
10142
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
9505
|
-
this.textColor,
|
|
10143
|
+
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
10144
|
+
}
|
|
10145
|
+
navigatePressed()
|
|
10146
|
+
{
|
|
10147
|
+
// toggle value between 0 and 1
|
|
10148
|
+
this.value = this.value ? 0 : 1;
|
|
10149
|
+
this.onRelease();
|
|
10150
|
+
super.navigatePressed();
|
|
9506
10151
|
}
|
|
9507
10152
|
}
|
|
9508
10153
|
|
|
@@ -9536,7 +10181,7 @@ class UIVideo extends UIObject
|
|
|
9536
10181
|
this.color = BLACK; // default to black background
|
|
9537
10182
|
this.cornerRadius = 0; // default to no corner radius
|
|
9538
10183
|
|
|
9539
|
-
/** @property {
|
|
10184
|
+
/** @property {number} - The video volume */
|
|
9540
10185
|
this.volume = volume;
|
|
9541
10186
|
|
|
9542
10187
|
// create video element
|
|
@@ -9569,7 +10214,7 @@ class UIVideo extends UIObject
|
|
|
9569
10214
|
|
|
9570
10215
|
/** Check if video is currently loading
|
|
9571
10216
|
* @return {boolean} */
|
|
9572
|
-
|
|
10217
|
+
isLoading()
|
|
9573
10218
|
{ return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
|
|
9574
10219
|
|
|
9575
10220
|
/** Check if video is currently paused
|
|
@@ -9579,7 +10224,7 @@ class UIVideo extends UIObject
|
|
|
9579
10224
|
/** Check if video is currently playing
|
|
9580
10225
|
* @return {boolean} */
|
|
9581
10226
|
isPlaying()
|
|
9582
|
-
{ return !this.isPaused() && !this.hasEnded() && !this.
|
|
10227
|
+
{ return !this.isPaused() && !this.hasEnded() && !this.isLoading(); }
|
|
9583
10228
|
|
|
9584
10229
|
/** Check if video has ended playing
|
|
9585
10230
|
* @return {boolean} */
|
|
@@ -9628,7 +10273,7 @@ class UIVideo extends UIObject
|
|
|
9628
10273
|
{
|
|
9629
10274
|
super.render();
|
|
9630
10275
|
|
|
9631
|
-
if (this.
|
|
10276
|
+
if (this.isLoading())
|
|
9632
10277
|
return;
|
|
9633
10278
|
const context = uiSystem.uiContext;
|
|
9634
10279
|
const s = this.size;
|
|
@@ -11412,7 +12057,7 @@ class Box2dPlugin
|
|
|
11412
12057
|
* @param {Vector2} v */
|
|
11413
12058
|
vec2dTo(v)
|
|
11414
12059
|
{
|
|
11415
|
-
ASSERT(v
|
|
12060
|
+
ASSERT(isVector2(v));
|
|
11416
12061
|
return new box2d.instance.b2Vec2(v.x, v.y);
|
|
11417
12062
|
}
|
|
11418
12063
|
|
|
@@ -11761,6 +12406,8 @@ export
|
|
|
11761
12406
|
canvasColorTiles,
|
|
11762
12407
|
canvasClearColor,
|
|
11763
12408
|
canvasMaxSize,
|
|
12409
|
+
canvasMinAspect,
|
|
12410
|
+
canvasMaxAspect,
|
|
11764
12411
|
canvasFixedSize,
|
|
11765
12412
|
canvasPixelated,
|
|
11766
12413
|
overlayCanvasPixelated,
|
|
@@ -11804,6 +12451,8 @@ export
|
|
|
11804
12451
|
setCanvasColorTiles,
|
|
11805
12452
|
setCanvasClearColor,
|
|
11806
12453
|
setCanvasMaxSize,
|
|
12454
|
+
setCanvasMinAspect,
|
|
12455
|
+
setCanvasMaxAspect,
|
|
11807
12456
|
setCanvasFixedSize,
|
|
11808
12457
|
setCanvasPixelated,
|
|
11809
12458
|
setOverlayCanvasPixelated,
|
|
@@ -11829,6 +12478,7 @@ export
|
|
|
11829
12478
|
setInputWASDEmulateDirection,
|
|
11830
12479
|
setTouchGamepadEnable,
|
|
11831
12480
|
setTouchGamepadCenterButton,
|
|
12481
|
+
setTouchGamepadButtonCount,
|
|
11832
12482
|
setTouchGamepadAnalog,
|
|
11833
12483
|
setTouchGamepadSize,
|
|
11834
12484
|
setTouchGamepadAlpha,
|
|
@@ -12000,12 +12650,14 @@ export
|
|
|
12000
12650
|
mouseInWindow,
|
|
12001
12651
|
isUsingGamepad,
|
|
12002
12652
|
inputPreventDefault,
|
|
12653
|
+
gamepadPrimary,
|
|
12003
12654
|
setInputPreventDefault,
|
|
12004
12655
|
gamepadIsDown,
|
|
12005
12656
|
gamepadWasPressed,
|
|
12006
12657
|
gamepadWasReleased,
|
|
12007
12658
|
gamepadStick,
|
|
12008
|
-
|
|
12659
|
+
gamepadDpad,
|
|
12660
|
+
gamepadConnected,
|
|
12009
12661
|
vibrate,
|
|
12010
12662
|
vibrateStop,
|
|
12011
12663
|
isTouchDevice,
|
|
@@ -12071,6 +12723,8 @@ export
|
|
|
12071
12723
|
|
|
12072
12724
|
// UI System
|
|
12073
12725
|
uiSystem,
|
|
12726
|
+
uiDebug,
|
|
12727
|
+
uiSetDebug,
|
|
12074
12728
|
UISystemPlugin,
|
|
12075
12729
|
UIObject,
|
|
12076
12730
|
UIText,
|