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.release.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 - Release Mode
|
|
@@ -1383,9 +1405,15 @@ class Vector2
|
|
|
1383
1405
|
{
|
|
1384
1406
|
this.x = x;
|
|
1385
1407
|
this.y = y;
|
|
1408
|
+
ASSERT_VECTOR2_VALID(this);
|
|
1386
1409
|
return this;
|
|
1387
1410
|
}
|
|
1388
1411
|
|
|
1412
|
+
/** Sets this vector from another vector and returns self
|
|
1413
|
+
* @param {Vector2} v - other vector
|
|
1414
|
+
* @return {Vector2} */
|
|
1415
|
+
setFrom(v) { return this.set(v.x, v.y); }
|
|
1416
|
+
|
|
1389
1417
|
/** Returns a new vector that is a copy of this
|
|
1390
1418
|
* @return {Vector2} */
|
|
1391
1419
|
copy() { return new Vector2(this.x, this.y); }
|
|
@@ -1448,7 +1476,7 @@ class Vector2
|
|
|
1448
1476
|
clampLength(length=1)
|
|
1449
1477
|
{
|
|
1450
1478
|
const l = this.length();
|
|
1451
|
-
return l > length ? this.scale(length/l) : this;
|
|
1479
|
+
return l > length ? this.scale(length/l) : this.copy();
|
|
1452
1480
|
}
|
|
1453
1481
|
|
|
1454
1482
|
/** Returns the dot product of this and the vector passed in
|
|
@@ -1535,6 +1563,10 @@ class Vector2
|
|
|
1535
1563
|
* @return {number} */
|
|
1536
1564
|
area() { return abs(this.x * this.y); }
|
|
1537
1565
|
|
|
1566
|
+
/** Returns true if this vector is (0,0)
|
|
1567
|
+
* @return {boolean} */
|
|
1568
|
+
isZero() { return !this.x && !this.y; }
|
|
1569
|
+
|
|
1538
1570
|
/** Returns a new vector that is p percent between this and the vector passed in
|
|
1539
1571
|
* @param {Vector2} v - other vector
|
|
1540
1572
|
* @param {number} percent
|
|
@@ -1645,9 +1677,15 @@ class Color
|
|
|
1645
1677
|
this.g = g;
|
|
1646
1678
|
this.b = b;
|
|
1647
1679
|
this.a = a;
|
|
1680
|
+
ASSERT_COLOR_VALID(this);
|
|
1648
1681
|
return this;
|
|
1649
1682
|
}
|
|
1650
1683
|
|
|
1684
|
+
/** Sets this color from another color and returns self
|
|
1685
|
+
* @param {Color} c - other color
|
|
1686
|
+
* @return {Color} */
|
|
1687
|
+
setFrom(c) { return this.set(c.r, c.g, c.b, c.a); }
|
|
1688
|
+
|
|
1651
1689
|
/** Returns a new color that is a copy of this
|
|
1652
1690
|
* @return {Color} */
|
|
1653
1691
|
copy() { return new Color(this.r, this.g, this.b, this.a); }
|
|
@@ -2023,6 +2061,20 @@ let canvasClearColor = CLEAR_BLACK;
|
|
|
2023
2061
|
* @memberof Settings */
|
|
2024
2062
|
let canvasMaxSize = vec2(1920, 1080);
|
|
2025
2063
|
|
|
2064
|
+
/** Minimum aspect ratio of the canvas (width/height), unused if 0
|
|
2065
|
+
* Can be used with canvasMaxAspect to limit aspect ratio
|
|
2066
|
+
* @type {number}
|
|
2067
|
+
* @default
|
|
2068
|
+
* @memberof Settings */
|
|
2069
|
+
let canvasMinAspect = 0;
|
|
2070
|
+
|
|
2071
|
+
/** Maximum aspect ratio of the canvas (width/height), unused if 0
|
|
2072
|
+
* Can be used with canvasMinAspect to limit aspect ratio
|
|
2073
|
+
* @type {number}
|
|
2074
|
+
* @default
|
|
2075
|
+
* @memberof Settings */
|
|
2076
|
+
let canvasMaxAspect = 0;
|
|
2077
|
+
|
|
2026
2078
|
/** Fixed size of the canvas, if enabled canvas size never changes
|
|
2027
2079
|
* - you may also need to set mainCanvasSize if using screen space coords in startup
|
|
2028
2080
|
* @type {Vector2}
|
|
@@ -2199,17 +2251,17 @@ let touchGamepadEnable = false;
|
|
|
2199
2251
|
* @memberof Settings */
|
|
2200
2252
|
let touchGamepadCenterButton = true;
|
|
2201
2253
|
|
|
2202
|
-
/**
|
|
2203
|
-
* @type {
|
|
2254
|
+
/** Number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
|
|
2255
|
+
* @type {number}
|
|
2204
2256
|
* @default
|
|
2205
2257
|
* @memberof Settings */
|
|
2206
|
-
let
|
|
2258
|
+
let touchGamepadButtonCount = 4;
|
|
2207
2259
|
|
|
2208
|
-
/**
|
|
2209
|
-
* @type {
|
|
2260
|
+
/** True if touch gamepad should be analog stick or false to use if 8 way dpad
|
|
2261
|
+
* @type {boolean}
|
|
2210
2262
|
* @default
|
|
2211
2263
|
* @memberof Settings */
|
|
2212
|
-
let
|
|
2264
|
+
let touchGamepadAnalog = true;
|
|
2213
2265
|
|
|
2214
2266
|
/** Size of virtual gamepad for touch devices in pixels
|
|
2215
2267
|
* @type {number}
|
|
@@ -2319,6 +2371,16 @@ function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
|
|
|
2319
2371
|
* @memberof Settings */
|
|
2320
2372
|
function setCanvasMaxSize(size) { canvasMaxSize = size.copy(); }
|
|
2321
2373
|
|
|
2374
|
+
/** Set minimum aspect ratio of the canvas (width/height), unused if 0
|
|
2375
|
+
* @param {number} aspect
|
|
2376
|
+
* @memberof Settings */
|
|
2377
|
+
function setCanvasMinAspect(aspect) { canvasMinAspect = aspect; }
|
|
2378
|
+
|
|
2379
|
+
/** Set maximum aspect ratio of the canvas (width/height), unused if 0
|
|
2380
|
+
* @param {number} aspect
|
|
2381
|
+
* @memberof Settings */
|
|
2382
|
+
function setCanvasMaxAspect(aspect) { canvasMaxAspect = aspect; }
|
|
2383
|
+
|
|
2322
2384
|
/** Set fixed size of the canvas
|
|
2323
2385
|
* @param {Vector2} size
|
|
2324
2386
|
* @memberof Settings */
|
|
@@ -2475,6 +2537,11 @@ function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
|
|
|
2475
2537
|
* @memberof Settings */
|
|
2476
2538
|
function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
|
|
2477
2539
|
|
|
2540
|
+
/** Set number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
|
|
2541
|
+
* @param {number} count
|
|
2542
|
+
* @memberof Settings */
|
|
2543
|
+
function setTouchGamepadButtonCount(count) { touchGamepadButtonCount = count; }
|
|
2544
|
+
|
|
2478
2545
|
/** Set if touch gamepad should be analog stick or 8 way dpad
|
|
2479
2546
|
* @param {boolean} analog
|
|
2480
2547
|
* @memberof Settings */
|
|
@@ -2683,7 +2750,7 @@ class EngineObject
|
|
|
2683
2750
|
child.updateTransforms();
|
|
2684
2751
|
}
|
|
2685
2752
|
|
|
2686
|
-
/** Update the object physics, called automatically by engine once each frame */
|
|
2753
|
+
/** Update the object physics, called automatically by engine once each frame. Can be overridden to stop or change how physics works for an object. */
|
|
2687
2754
|
updatePhysics()
|
|
2688
2755
|
{
|
|
2689
2756
|
// child objects do not have physics
|
|
@@ -2716,7 +2783,7 @@ class EngineObject
|
|
|
2716
2783
|
if (!enablePhysicsSolver || !this.mass) // don't do collision for static objects
|
|
2717
2784
|
return;
|
|
2718
2785
|
|
|
2719
|
-
const
|
|
2786
|
+
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
2720
2787
|
if (this.groundObject)
|
|
2721
2788
|
{
|
|
2722
2789
|
// apply friction in local space of ground object
|
|
@@ -2772,10 +2839,10 @@ class EngineObject
|
|
|
2772
2839
|
{
|
|
2773
2840
|
// push outside object collision
|
|
2774
2841
|
this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
|
|
2775
|
-
if ((o.groundObject &&
|
|
2842
|
+
if ((o.groundObject && wasFalling) || !o.mass)
|
|
2776
2843
|
{
|
|
2777
2844
|
// set ground object if landed on something
|
|
2778
|
-
if (
|
|
2845
|
+
if (wasFalling)
|
|
2779
2846
|
this.groundObject = o;
|
|
2780
2847
|
|
|
2781
2848
|
// bounce if other object is fixed or grounded
|
|
@@ -2862,12 +2929,15 @@ class EngineObject
|
|
|
2862
2929
|
const restitution = max(this.restitution, hitLayer.restitution);
|
|
2863
2930
|
this.velocity.y *= -restitution;
|
|
2864
2931
|
|
|
2865
|
-
if (
|
|
2932
|
+
if (wasFalling)
|
|
2866
2933
|
{
|
|
2867
|
-
// adjust position to slightly
|
|
2934
|
+
// adjust position to slightly away from nearest tile
|
|
2868
2935
|
// this prevents gap between object and ground
|
|
2869
2936
|
const epsilon = .0001;
|
|
2870
|
-
|
|
2937
|
+
const offset = this.size.y/2 + epsilon;
|
|
2938
|
+
this.pos.y = gravity.y < 0 ?
|
|
2939
|
+
floor(oldPos.y-this.size.y/2) + offset :
|
|
2940
|
+
ceil( oldPos.y+this.size.y/2) - offset;
|
|
2871
2941
|
|
|
2872
2942
|
// set ground object for tile collision
|
|
2873
2943
|
this.groundObject = hitLayer;
|
|
@@ -3036,21 +3106,20 @@ class EngineObject
|
|
|
3036
3106
|
* @return {string} */
|
|
3037
3107
|
toString()
|
|
3038
3108
|
{
|
|
3039
|
-
if (debug)
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
}
|
|
3109
|
+
if (!debug) return;
|
|
3110
|
+
|
|
3111
|
+
let text = 'type = ' + this.constructor.name;
|
|
3112
|
+
if (this.pos.x || this.pos.y)
|
|
3113
|
+
text += '\npos = ' + this.pos;
|
|
3114
|
+
if (this.velocity.x || this.velocity.y)
|
|
3115
|
+
text += '\nvelocity = ' + this.velocity;
|
|
3116
|
+
if (this.size.x || this.size.y)
|
|
3117
|
+
text += '\nsize = ' + this.size;
|
|
3118
|
+
if (this.angle)
|
|
3119
|
+
text += '\nangle = ' + this.angle.toFixed(3);
|
|
3120
|
+
if (this.color)
|
|
3121
|
+
text += '\ncolor = ' + this.color;
|
|
3122
|
+
return text;
|
|
3054
3123
|
}
|
|
3055
3124
|
|
|
3056
3125
|
/** Render debug info for this object */
|
|
@@ -3219,7 +3288,7 @@ class TileInfo
|
|
|
3219
3288
|
this.padding = padding;
|
|
3220
3289
|
/** @property {TextureInfo} - The texture info for this tile */
|
|
3221
3290
|
this.textureInfo = textureInfos[this.textureIndex];
|
|
3222
|
-
/** @property {
|
|
3291
|
+
/** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
|
|
3223
3292
|
this.bleedScale = bleedScale;
|
|
3224
3293
|
}
|
|
3225
3294
|
|
|
@@ -3706,7 +3775,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
3706
3775
|
|
|
3707
3776
|
/** Draw text on main canvas in world space
|
|
3708
3777
|
* Automatically splits new lines into rows
|
|
3709
|
-
* @param {string} text
|
|
3778
|
+
* @param {string|number} text
|
|
3710
3779
|
* @param {Vector2} pos
|
|
3711
3780
|
* @param {number} [size]
|
|
3712
3781
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -3733,7 +3802,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
|
|
|
3733
3802
|
|
|
3734
3803
|
/** Draw text on overlay canvas in world space
|
|
3735
3804
|
* Automatically splits new lines into rows
|
|
3736
|
-
* @param {string} text
|
|
3805
|
+
* @param {string|number} text
|
|
3737
3806
|
* @param {Vector2} pos
|
|
3738
3807
|
* @param {number} [size]
|
|
3739
3808
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -3752,7 +3821,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
3752
3821
|
|
|
3753
3822
|
/** Draw text on overlay canvas in screen space
|
|
3754
3823
|
* Automatically splits new lines into rows
|
|
3755
|
-
* @param {string} text
|
|
3824
|
+
* @param {string|number} text
|
|
3756
3825
|
* @param {Vector2} pos
|
|
3757
3826
|
* @param {number} [size]
|
|
3758
3827
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -3917,11 +3986,28 @@ function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
|
|
|
3917
3986
|
];
|
|
3918
3987
|
}
|
|
3919
3988
|
|
|
3920
|
-
/** Get the camera
|
|
3989
|
+
/** Get the size of the camera window in world space
|
|
3921
3990
|
* @return {Vector2}
|
|
3922
3991
|
* @memberof Draw */
|
|
3923
3992
|
function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
3924
3993
|
|
|
3994
|
+
/** Check if a point or circle is on screen
|
|
3995
|
+
* If size is a Vector2, uses the largest dimension as diameter
|
|
3996
|
+
* This can be used to cull offscreen objects from render or update
|
|
3997
|
+
* @param {Vector2} pos - world space position
|
|
3998
|
+
* @param {Vector2|number} size - world space size or diameter
|
|
3999
|
+
* @return {boolean}
|
|
4000
|
+
* @memberof Draw */
|
|
4001
|
+
function isOnScreen(pos, size=0)
|
|
4002
|
+
{
|
|
4003
|
+
pos = worldToScreen(pos);
|
|
4004
|
+
if (size instanceof Vector2)
|
|
4005
|
+
size = max(size.x, size.y); // use largest dimension
|
|
4006
|
+
size *= cameraScale/2;
|
|
4007
|
+
return pos.x + size > 0 && pos.x - size < mainCanvasSize.x &&
|
|
4008
|
+
pos.y + size > 0 && pos.y - size < mainCanvasSize.y;
|
|
4009
|
+
}
|
|
4010
|
+
|
|
3925
4011
|
/** Enable normal or additive blend mode
|
|
3926
4012
|
* @param {boolean} [additive]
|
|
3927
4013
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -4085,7 +4171,7 @@ class FontImage
|
|
|
4085
4171
|
}
|
|
4086
4172
|
|
|
4087
4173
|
/** Draw text in world space using the image font
|
|
4088
|
-
* @param {string} text
|
|
4174
|
+
* @param {string|number} text
|
|
4089
4175
|
* @param {Vector2} pos
|
|
4090
4176
|
* @param {number} [scale=.25]
|
|
4091
4177
|
* @param {boolean} [center]
|
|
@@ -4097,7 +4183,7 @@ class FontImage
|
|
|
4097
4183
|
}
|
|
4098
4184
|
|
|
4099
4185
|
/** Draw text on overlay canvas in world space using the image font
|
|
4100
|
-
* @param {string} text
|
|
4186
|
+
* @param {string|number} text
|
|
4101
4187
|
* @param {Vector2} pos
|
|
4102
4188
|
* @param {number} [scale]
|
|
4103
4189
|
* @param {boolean} [center]
|
|
@@ -4106,7 +4192,7 @@ class FontImage
|
|
|
4106
4192
|
{ this.drawText(text, pos, scale, center, overlayContext); }
|
|
4107
4193
|
|
|
4108
4194
|
/** Draw text on overlay canvas in screen space using the image font
|
|
4109
|
-
* @param {string} text
|
|
4195
|
+
* @param {string|number} text
|
|
4110
4196
|
* @param {Vector2} pos
|
|
4111
4197
|
* @param {number} [scale]
|
|
4112
4198
|
* @param {boolean} [center]
|
|
@@ -4150,6 +4236,85 @@ class FontImage
|
|
|
4150
4236
|
* @namespace Input
|
|
4151
4237
|
*/
|
|
4152
4238
|
|
|
4239
|
+
/** Mouse pos in world space
|
|
4240
|
+
* @type {Vector2}
|
|
4241
|
+
* @memberof Input */
|
|
4242
|
+
let mousePos = vec2();
|
|
4243
|
+
|
|
4244
|
+
/** Mouse pos in screen space
|
|
4245
|
+
* @type {Vector2}
|
|
4246
|
+
* @memberof Input */
|
|
4247
|
+
let mousePosScreen = vec2();
|
|
4248
|
+
|
|
4249
|
+
/** Mouse movement delta in world space
|
|
4250
|
+
* @type {Vector2}
|
|
4251
|
+
* @memberof Input */
|
|
4252
|
+
let mouseDelta = vec2();
|
|
4253
|
+
|
|
4254
|
+
/** Mouse movement delta in screen space
|
|
4255
|
+
* @type {Vector2}
|
|
4256
|
+
* @memberof Input */
|
|
4257
|
+
let mouseDeltaScreen = vec2();
|
|
4258
|
+
|
|
4259
|
+
/** Mouse wheel delta this frame
|
|
4260
|
+
* @type {number}
|
|
4261
|
+
* @memberof Input */
|
|
4262
|
+
let mouseWheel = 0;
|
|
4263
|
+
|
|
4264
|
+
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4265
|
+
* @type {boolean}
|
|
4266
|
+
* @memberof Input */
|
|
4267
|
+
let mouseInWindow = true;
|
|
4268
|
+
|
|
4269
|
+
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
4270
|
+
* @type {boolean}
|
|
4271
|
+
* @memberof Input */
|
|
4272
|
+
let isUsingGamepad = false;
|
|
4273
|
+
|
|
4274
|
+
/** Prevents input continuing to the default browser handling (true by default)
|
|
4275
|
+
* @type {boolean}
|
|
4276
|
+
* @memberof Input */
|
|
4277
|
+
let inputPreventDefault = true;
|
|
4278
|
+
|
|
4279
|
+
/** Primary gamepad index, automatically set to first gamepad with input
|
|
4280
|
+
* @type {number}
|
|
4281
|
+
* @memberof Input */
|
|
4282
|
+
let gamepadPrimary = 0;
|
|
4283
|
+
|
|
4284
|
+
/** Prevents input continuing to the default browser handling
|
|
4285
|
+
* This is useful to disable for html menus so the browser can handle input normally
|
|
4286
|
+
* @param {boolean} preventDefault
|
|
4287
|
+
* @memberof Input */
|
|
4288
|
+
function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
|
|
4289
|
+
|
|
4290
|
+
/** Clears an input key state
|
|
4291
|
+
* @param {string|number} key
|
|
4292
|
+
* @param {number} [device]
|
|
4293
|
+
* @param {boolean} [clearDown=true]
|
|
4294
|
+
* @param {boolean} [clearPressed=true]
|
|
4295
|
+
* @param {boolean} [clearReleased=true]
|
|
4296
|
+
* @memberof Input */
|
|
4297
|
+
function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
|
|
4298
|
+
{
|
|
4299
|
+
if (!inputData[device])
|
|
4300
|
+
return;
|
|
4301
|
+
inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
/** Clears all input
|
|
4305
|
+
* @memberof Input */
|
|
4306
|
+
function inputClear()
|
|
4307
|
+
{
|
|
4308
|
+
inputData.length = 0;
|
|
4309
|
+
inputData[0] = [];
|
|
4310
|
+
touchGamepadButtons.length = 0;
|
|
4311
|
+
touchGamepadSticks.length = 0;
|
|
4312
|
+
gamepadStickData.length = 0;
|
|
4313
|
+
gamepadDpadData.length = 0;
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4317
|
+
|
|
4153
4318
|
/** Returns true if device key is down
|
|
4154
4319
|
* @param {string|number} key
|
|
4155
4320
|
* @param {number} [device]
|
|
@@ -4157,9 +4322,9 @@ class FontImage
|
|
|
4157
4322
|
* @memberof Input */
|
|
4158
4323
|
function keyIsDown(key, device=0)
|
|
4159
4324
|
{
|
|
4160
|
-
ASSERT(key
|
|
4325
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4161
4326
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4162
|
-
return
|
|
4327
|
+
return !!(inputData[device]?.[key] & 1);
|
|
4163
4328
|
}
|
|
4164
4329
|
|
|
4165
4330
|
/** Returns true if device key was pressed this frame
|
|
@@ -4169,9 +4334,9 @@ function keyIsDown(key, device=0)
|
|
|
4169
4334
|
* @memberof Input */
|
|
4170
4335
|
function keyWasPressed(key, device=0)
|
|
4171
4336
|
{
|
|
4172
|
-
ASSERT(key
|
|
4337
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4173
4338
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4174
|
-
return
|
|
4339
|
+
return !!(inputData[device]?.[key] & 2);
|
|
4175
4340
|
}
|
|
4176
4341
|
|
|
4177
4342
|
/** Returns true if device key was released this frame
|
|
@@ -4181,172 +4346,192 @@ function keyWasPressed(key, device=0)
|
|
|
4181
4346
|
* @memberof Input */
|
|
4182
4347
|
function keyWasReleased(key, device=0)
|
|
4183
4348
|
{
|
|
4184
|
-
ASSERT(key
|
|
4349
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4185
4350
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4186
|
-
return
|
|
4351
|
+
return !!(inputData[device]?.[key] & 4);
|
|
4187
4352
|
}
|
|
4188
4353
|
|
|
4189
4354
|
/** Returns input vector from arrow keys or WASD if enabled
|
|
4355
|
+
* @param {string} [up]
|
|
4356
|
+
* @param {string} [down]
|
|
4357
|
+
* @param {string} [left]
|
|
4358
|
+
* @param {string} [right]
|
|
4190
4359
|
* @return {Vector2}
|
|
4191
4360
|
* @memberof Input */
|
|
4192
4361
|
function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
|
|
4193
4362
|
{
|
|
4363
|
+
ASSERT(isString(up), 'up key must be a string');
|
|
4364
|
+
ASSERT(isString(down), 'down key must be a string');
|
|
4365
|
+
ASSERT(isString(left), 'left key must be a string');
|
|
4366
|
+
ASSERT(isString(right), 'right key must be a string');
|
|
4194
4367
|
const k = (key)=> keyIsDown(key) ? 1 : 0;
|
|
4195
4368
|
return vec2(k(right) - k(left), k(up) - k(down));
|
|
4196
4369
|
}
|
|
4197
4370
|
|
|
4198
|
-
/** Clears all input
|
|
4199
|
-
* @memberof Input */
|
|
4200
|
-
function inputClear() { inputData = [[]]; touchGamepadButtons = []; }
|
|
4201
|
-
|
|
4202
|
-
/** Clears an input key state
|
|
4203
|
-
* @param {string|number} key
|
|
4204
|
-
* @param {number} [device]
|
|
4205
|
-
* @param {boolean} [clearDown=true]
|
|
4206
|
-
* @param {boolean} [clearPressed=true]
|
|
4207
|
-
* @param {boolean} [clearReleased=true]
|
|
4208
|
-
* @memberof Input */
|
|
4209
|
-
function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
|
|
4210
|
-
{
|
|
4211
|
-
if (!inputData[device])
|
|
4212
|
-
return;
|
|
4213
|
-
inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
|
|
4214
|
-
}
|
|
4215
|
-
|
|
4216
4371
|
/** Returns true if mouse button is down
|
|
4217
4372
|
* @function
|
|
4218
4373
|
* @param {number} button
|
|
4219
4374
|
* @return {boolean}
|
|
4220
4375
|
* @memberof Input */
|
|
4221
|
-
function mouseIsDown(button)
|
|
4376
|
+
function mouseIsDown(button)
|
|
4377
|
+
{
|
|
4378
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
4379
|
+
return keyIsDown(button);
|
|
4380
|
+
}
|
|
4222
4381
|
|
|
4223
4382
|
/** Returns true if mouse button was pressed
|
|
4224
4383
|
* @function
|
|
4225
4384
|
* @param {number} button
|
|
4226
4385
|
* @return {boolean}
|
|
4227
4386
|
* @memberof Input */
|
|
4228
|
-
function mouseWasPressed(button)
|
|
4387
|
+
function mouseWasPressed(button)
|
|
4388
|
+
{
|
|
4389
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
4390
|
+
return keyWasPressed(button);
|
|
4391
|
+
}
|
|
4229
4392
|
|
|
4230
4393
|
/** Returns true if mouse button was released
|
|
4231
4394
|
* @function
|
|
4232
4395
|
* @param {number} button
|
|
4233
4396
|
* @return {boolean}
|
|
4234
4397
|
* @memberof Input */
|
|
4235
|
-
function mouseWasReleased(button)
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
let mousePos = vec2();
|
|
4241
|
-
|
|
4242
|
-
/** Mouse pos in screen space
|
|
4243
|
-
* @type {Vector2}
|
|
4244
|
-
* @memberof Input */
|
|
4245
|
-
let mousePosScreen = vec2();
|
|
4246
|
-
|
|
4247
|
-
/** Mouse movement delta in world space
|
|
4248
|
-
* @type {Vector2}
|
|
4249
|
-
* @memberof Input */
|
|
4250
|
-
let mouseDelta = vec2();
|
|
4251
|
-
|
|
4252
|
-
/** Mouse movement delta in screen space
|
|
4253
|
-
* @type {Vector2}
|
|
4254
|
-
* @memberof Input */
|
|
4255
|
-
let mouseDeltaScreen = vec2();
|
|
4256
|
-
|
|
4257
|
-
/** Mouse wheel delta this frame
|
|
4258
|
-
* @type {number}
|
|
4259
|
-
* @memberof Input */
|
|
4260
|
-
let mouseWheel = 0;
|
|
4261
|
-
|
|
4262
|
-
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4263
|
-
* @type {boolean}
|
|
4264
|
-
* @memberof Input */
|
|
4265
|
-
let mouseInWindow = true;
|
|
4266
|
-
|
|
4267
|
-
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
4268
|
-
* @type {boolean}
|
|
4269
|
-
* @memberof Input */
|
|
4270
|
-
let isUsingGamepad = false;
|
|
4271
|
-
|
|
4272
|
-
/** Prevents input continuing to the default browser handling (true by default)
|
|
4273
|
-
* @type {boolean}
|
|
4274
|
-
* @memberof Input */
|
|
4275
|
-
let inputPreventDefault = true;
|
|
4276
|
-
|
|
4277
|
-
/** Prevents input continuing to the default browser handling
|
|
4278
|
-
* This is useful to disable for html menus so the browser can handle input normally
|
|
4279
|
-
* @param {boolean} preventDefault
|
|
4280
|
-
* @memberof Input */
|
|
4281
|
-
function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
|
|
4398
|
+
function mouseWasReleased(button)
|
|
4399
|
+
{
|
|
4400
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
4401
|
+
return keyWasReleased(button);
|
|
4402
|
+
}
|
|
4282
4403
|
|
|
4283
4404
|
/** Returns true if gamepad button is down
|
|
4284
4405
|
* @param {number} button
|
|
4285
4406
|
* @param {number} [gamepad]
|
|
4286
4407
|
* @return {boolean}
|
|
4287
4408
|
* @memberof Input */
|
|
4288
|
-
function gamepadIsDown(button, gamepad=
|
|
4289
|
-
{
|
|
4409
|
+
function gamepadIsDown(button, gamepad=gamepadPrimary)
|
|
4410
|
+
{
|
|
4411
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
4412
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4413
|
+
return keyIsDown(button, gamepad+1);
|
|
4414
|
+
}
|
|
4290
4415
|
|
|
4291
4416
|
/** Returns true if gamepad button was pressed
|
|
4292
4417
|
* @param {number} button
|
|
4293
4418
|
* @param {number} [gamepad]
|
|
4294
4419
|
* @return {boolean}
|
|
4295
4420
|
* @memberof Input */
|
|
4296
|
-
function gamepadWasPressed(button, gamepad=
|
|
4297
|
-
{
|
|
4421
|
+
function gamepadWasPressed(button, gamepad=gamepadPrimary)
|
|
4422
|
+
{
|
|
4423
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
4424
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4425
|
+
return keyWasPressed(button, gamepad+1);
|
|
4426
|
+
}
|
|
4298
4427
|
|
|
4299
4428
|
/** Returns true if gamepad button was released
|
|
4300
4429
|
* @param {number} button
|
|
4301
4430
|
* @param {number} [gamepad]
|
|
4302
4431
|
* @return {boolean}
|
|
4303
4432
|
* @memberof Input */
|
|
4304
|
-
function gamepadWasReleased(button, gamepad=
|
|
4305
|
-
{
|
|
4433
|
+
function gamepadWasReleased(button, gamepad=gamepadPrimary)
|
|
4434
|
+
{
|
|
4435
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
4436
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4437
|
+
return keyWasReleased(button, gamepad+1);
|
|
4438
|
+
}
|
|
4306
4439
|
|
|
4307
4440
|
/** Returns gamepad stick value
|
|
4308
4441
|
* @param {number} stick
|
|
4309
4442
|
* @param {number} [gamepad]
|
|
4310
4443
|
* @return {Vector2}
|
|
4311
4444
|
* @memberof Input */
|
|
4312
|
-
function gamepadStick(stick, gamepad=
|
|
4313
|
-
{
|
|
4445
|
+
function gamepadStick(stick, gamepad=gamepadPrimary)
|
|
4446
|
+
{
|
|
4447
|
+
ASSERT(isNumber(stick), 'stick must be a number');
|
|
4448
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4449
|
+
return gamepadStickData[gamepad]?.[stick] ?? vec2();
|
|
4450
|
+
}
|
|
4314
4451
|
|
|
4315
|
-
|
|
4316
|
-
|
|
4452
|
+
/** Returns gamepad dpad value
|
|
4453
|
+
* @param {number} [gamepad]
|
|
4454
|
+
* @return {Vector2}
|
|
4455
|
+
* @memberof Input */
|
|
4456
|
+
function gamepadDpad(gamepad=gamepadPrimary)
|
|
4457
|
+
{
|
|
4458
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4459
|
+
return gamepadDpadData[gamepad] ?? vec2();
|
|
4460
|
+
}
|
|
4317
4461
|
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4462
|
+
/** Returns true if passed in gamepad is connected
|
|
4463
|
+
* @param {number} [gamepad]
|
|
4464
|
+
* @return {boolean}
|
|
4465
|
+
* @memberof Input */
|
|
4466
|
+
function gamepadConnected(gamepad=gamepadPrimary)
|
|
4467
|
+
{
|
|
4468
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4469
|
+
return !!inputData[gamepad+1];
|
|
4470
|
+
}
|
|
4321
4471
|
|
|
4322
|
-
|
|
4472
|
+
/** Returns how many control sticks the passed in gamepad has
|
|
4473
|
+
* @param {number} [gamepad]
|
|
4474
|
+
* @return {number}
|
|
4475
|
+
* @memberof Input */
|
|
4476
|
+
function gamepadStickCount(gamepad=gamepadPrimary)
|
|
4323
4477
|
{
|
|
4324
|
-
|
|
4478
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4479
|
+
return gamepadStickData[gamepad]?.length ?? 0;
|
|
4480
|
+
}
|
|
4325
4481
|
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4482
|
+
/** True if a touch device has been detected
|
|
4483
|
+
* @memberof Input */
|
|
4484
|
+
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
4329
4485
|
|
|
4330
|
-
|
|
4331
|
-
mousePos = screenToWorld(mousePosScreen);
|
|
4332
|
-
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
4486
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4333
4487
|
|
|
4334
|
-
|
|
4335
|
-
|
|
4488
|
+
/** Pulse the vibration hardware if it exists
|
|
4489
|
+
* @param {number|Array} [pattern] - single value in ms or vibration interval array
|
|
4490
|
+
* @memberof Input */
|
|
4491
|
+
function vibrate(pattern=100)
|
|
4492
|
+
{
|
|
4493
|
+
ASSERT(isNumber(pattern) || isArray(pattern), 'pattern must be a number or array');
|
|
4494
|
+
vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern);
|
|
4336
4495
|
}
|
|
4337
4496
|
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4497
|
+
/** Cancel any ongoing vibration
|
|
4498
|
+
* @memberof Input */
|
|
4499
|
+
function vibrateStop() { vibrate(0); }
|
|
4341
4500
|
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4501
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4502
|
+
// Pointer Lock
|
|
4503
|
+
|
|
4504
|
+
/** Request to lock the pointer, does not work on touch devices
|
|
4505
|
+
* @memberof Input */
|
|
4506
|
+
function pointerLockRequest()
|
|
4507
|
+
{ !isTouchDevice && mainCanvas.requestPointerLock?.(); }
|
|
4508
|
+
|
|
4509
|
+
/** Request to unlock the pointer
|
|
4510
|
+
* @memberof Input */
|
|
4511
|
+
function pointerLockExit()
|
|
4512
|
+
{ document.exitPointerLock?.(); }
|
|
4513
|
+
|
|
4514
|
+
/** Check if pointer is locked (true if locked)
|
|
4515
|
+
* @return {boolean}
|
|
4516
|
+
* @memberof Input */
|
|
4517
|
+
function pointerLockIsActive()
|
|
4518
|
+
{ return document.pointerLockElement === mainCanvas; }
|
|
4519
|
+
|
|
4520
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4521
|
+
// Input variables used by engine
|
|
4522
|
+
|
|
4523
|
+
// input uses bit field for each key: 1=isDown, 2=wasPressed, 4=wasReleased
|
|
4524
|
+
// mouse and keyboard stored in device 0, gamepads stored in devices > 0
|
|
4525
|
+
const inputData = [[]];
|
|
4526
|
+
|
|
4527
|
+
// gamepad internal variables
|
|
4528
|
+
const gamepadStickData = [], gamepadDpadData = [], gamepadHadInput = [];
|
|
4529
|
+
|
|
4530
|
+
// touch gamepad internal variables
|
|
4531
|
+
const touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadSticks = [];
|
|
4532
|
+
|
|
4533
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4534
|
+
// Input system functions used by engine
|
|
4350
4535
|
|
|
4351
4536
|
function inputInit()
|
|
4352
4537
|
{
|
|
@@ -4376,6 +4561,18 @@ function inputInit()
|
|
|
4376
4561
|
if (inputWASDEmulateDirection)
|
|
4377
4562
|
inputData[0][remapKey(e.code)] = 3;
|
|
4378
4563
|
}
|
|
4564
|
+
|
|
4565
|
+
// prevent arrow key from moving the page
|
|
4566
|
+
const preventDefaultKeys =
|
|
4567
|
+
[
|
|
4568
|
+
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', // scrolling
|
|
4569
|
+
'Space', // page down scroll
|
|
4570
|
+
'Tab', // focus navigation
|
|
4571
|
+
'Backspace', // browser back
|
|
4572
|
+
];
|
|
4573
|
+
if (preventDefaultKeys.includes(e.code))
|
|
4574
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
4575
|
+
e.preventDefault();
|
|
4379
4576
|
}
|
|
4380
4577
|
function onKeyUp(e)
|
|
4381
4578
|
{
|
|
@@ -4407,7 +4604,9 @@ function inputInit()
|
|
|
4407
4604
|
const mousePosScreenLast = mousePosScreen;
|
|
4408
4605
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
4409
4606
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
4410
|
-
|
|
4607
|
+
|
|
4608
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
4609
|
+
e.preventDefault();
|
|
4411
4610
|
}
|
|
4412
4611
|
function onMouseUp(e)
|
|
4413
4612
|
{
|
|
@@ -4431,344 +4630,388 @@ function inputInit()
|
|
|
4431
4630
|
function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
|
|
4432
4631
|
function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
|
|
4433
4632
|
function onBlur() { inputClear(); } // reset input when focus is lost
|
|
4434
|
-
}
|
|
4435
|
-
|
|
4436
|
-
// convert a mouse or touch event position to screen space
|
|
4437
|
-
function mouseEventToScreen(mousePos)
|
|
4438
|
-
{
|
|
4439
|
-
const rect = mainCanvas.getBoundingClientRect();
|
|
4440
|
-
const px = percent(mousePos.x, rect.left, rect.right);
|
|
4441
|
-
const py = percent(mousePos.y, rect.top, rect.bottom);
|
|
4442
|
-
return vec2(px*mainCanvas.width, py*mainCanvas.height);
|
|
4443
|
-
}
|
|
4444
|
-
|
|
4445
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
4446
|
-
// Gamepad input
|
|
4447
4633
|
|
|
4448
|
-
//
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
// gamepads are updated by engine every frame automatically
|
|
4452
|
-
function gamepadsUpdate()
|
|
4453
|
-
{
|
|
4454
|
-
const applyDeadZones = (v)=>
|
|
4634
|
+
// enable touch input mouse passthrough
|
|
4635
|
+
function touchInputInit()
|
|
4455
4636
|
{
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
4461
|
-
}
|
|
4637
|
+
// add non passive touch event listeners
|
|
4638
|
+
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
4639
|
+
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
4640
|
+
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
4462
4641
|
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
if (!touchGamepadTimer.isSet())
|
|
4467
|
-
return;
|
|
4468
|
-
|
|
4469
|
-
// read virtual analog stick
|
|
4470
|
-
const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
|
|
4471
|
-
sticks[0] = vec2();
|
|
4472
|
-
if (touchGamepadAnalog)
|
|
4473
|
-
sticks[0] = applyDeadZones(touchGamepadStick);
|
|
4474
|
-
else if (touchGamepadStick.lengthSquared() > .3)
|
|
4642
|
+
// handle all touch events the same way
|
|
4643
|
+
let wasTouching;
|
|
4644
|
+
function handleTouch(e)
|
|
4475
4645
|
{
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
sticks[0].y = -round(touchGamepadStick.y);
|
|
4479
|
-
sticks[0] = sticks[0].clampLength();
|
|
4480
|
-
}
|
|
4646
|
+
if (!touchInputEnable)
|
|
4647
|
+
return;
|
|
4481
4648
|
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
{
|
|
4486
|
-
const wasDown = gamepadIsDown(i,0);
|
|
4487
|
-
data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
4488
|
-
}
|
|
4649
|
+
// route touch to gamepad
|
|
4650
|
+
if (touchGamepadEnable)
|
|
4651
|
+
handleTouchGamepad(e);
|
|
4489
4652
|
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4653
|
+
// fix stalled audio requiring user interaction
|
|
4654
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
4655
|
+
audioContext.resume();
|
|
4493
4656
|
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4657
|
+
// check if touching and pass to mouse events
|
|
4658
|
+
const touching = e.touches.length;
|
|
4659
|
+
const button = 0; // all touches are left mouse button
|
|
4660
|
+
if (touching)
|
|
4661
|
+
{
|
|
4662
|
+
// set event pos and pass it along
|
|
4663
|
+
const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
|
|
4664
|
+
const mousePosScreenLast = mousePosScreen;
|
|
4665
|
+
mousePosScreen = mouseEventToScreen(pos);
|
|
4666
|
+
if (wasTouching)
|
|
4667
|
+
{
|
|
4668
|
+
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
4669
|
+
isUsingGamepad = touchGamepadEnable;
|
|
4670
|
+
}
|
|
4671
|
+
else
|
|
4672
|
+
inputData[0][button] = 3;
|
|
4673
|
+
}
|
|
4674
|
+
else if (wasTouching)
|
|
4675
|
+
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
4497
4676
|
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
return;
|
|
4677
|
+
// set was touching
|
|
4678
|
+
wasTouching = touching;
|
|
4501
4679
|
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
|
|
4680
|
+
// prevent default handling like copy, magnifier lens, and scrolling
|
|
4681
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
4682
|
+
e.preventDefault();
|
|
4683
|
+
|
|
4684
|
+
// must return true so the document will get focus
|
|
4685
|
+
return true;
|
|
4686
|
+
}
|
|
4510
4687
|
|
|
4511
|
-
|
|
4688
|
+
// special handling for virtual gamepad mode
|
|
4689
|
+
function handleTouchGamepad(e)
|
|
4512
4690
|
{
|
|
4513
|
-
//
|
|
4514
|
-
|
|
4515
|
-
|
|
4691
|
+
// clear touch gamepad input
|
|
4692
|
+
touchGamepadSticks.length = 0;
|
|
4693
|
+
touchGamepadSticks[0] = vec2();
|
|
4694
|
+
touchGamepadSticks[1] = vec2();
|
|
4695
|
+
touchGamepadButtons.length = 0;
|
|
4696
|
+
isUsingGamepad = true;
|
|
4516
4697
|
|
|
4517
|
-
|
|
4518
|
-
|
|
4698
|
+
const touching = e.touches.length;
|
|
4699
|
+
if (touching)
|
|
4519
4700
|
{
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4701
|
+
touchGamepadTimer.set();
|
|
4702
|
+
if (touchGamepadCenterButton && !wasTouching && paused)
|
|
4703
|
+
{
|
|
4704
|
+
// touch anywhere to press start when paused
|
|
4705
|
+
touchGamepadButtons[9] = 1;
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4526
4708
|
}
|
|
4527
4709
|
|
|
4528
|
-
if
|
|
4710
|
+
// don't process touch gamepad if paused
|
|
4711
|
+
if (paused)
|
|
4712
|
+
return;
|
|
4713
|
+
|
|
4714
|
+
// get center of left and right sides
|
|
4715
|
+
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4716
|
+
const buttonCenter = touchGamepadButtonCenter();
|
|
4717
|
+
const startCenter = mainCanvasSize.scale(.5);
|
|
4718
|
+
|
|
4719
|
+
// check each touch point
|
|
4720
|
+
for (const touch of e.touches)
|
|
4529
4721
|
{
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4722
|
+
const touchPos = mouseEventToScreen(vec2(touch.clientX, touch.clientY));
|
|
4723
|
+
if (stickCenter.distance(touchPos) < touchGamepadSize)
|
|
4724
|
+
{
|
|
4725
|
+
// virtual analog stick
|
|
4726
|
+
const delta = touchPos.subtract(stickCenter);
|
|
4727
|
+
touchGamepadSticks[0] = delta.scale(2/touchGamepadSize).clampLength();
|
|
4728
|
+
}
|
|
4729
|
+
else if (buttonCenter.distance(touchPos) < touchGamepadSize)
|
|
4730
|
+
{
|
|
4731
|
+
if (touchGamepadButtonCount === 1)
|
|
4732
|
+
{
|
|
4733
|
+
// virtual right analog stick
|
|
4734
|
+
const delta = touchPos.subtract(buttonCenter);
|
|
4735
|
+
touchGamepadSticks[1] = delta.scale(2/touchGamepadSize).clampLength();
|
|
4736
|
+
}
|
|
4737
|
+
// virtual face buttons
|
|
4738
|
+
let button = buttonCenter.subtract(touchPos).direction();
|
|
4739
|
+
button = mod(button+2, 4);
|
|
4740
|
+
if (touchGamepadButtonCount === 1)
|
|
4741
|
+
button = 0;
|
|
4742
|
+
else if (touchGamepadButtonCount === 2)
|
|
4743
|
+
{
|
|
4744
|
+
const delta = buttonCenter.subtract(touchPos);
|
|
4745
|
+
button = -delta.x < delta.y ? 1 : 0;
|
|
4746
|
+
}
|
|
4747
|
+
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
4748
|
+
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
4749
|
+
if (button < touchGamepadButtonCount)
|
|
4750
|
+
touchGamepadButtons[button] = 1;
|
|
4751
|
+
}
|
|
4752
|
+
else if (touchGamepadCenterButton &&
|
|
4753
|
+
startCenter.distance(touchPos) < touchGamepadSize)
|
|
4754
|
+
{
|
|
4755
|
+
// virtual start button in center
|
|
4756
|
+
touchGamepadButtons[9] = 1;
|
|
4757
|
+
}
|
|
4536
4758
|
}
|
|
4537
|
-
|
|
4538
|
-
// disable touch gamepad if using real gamepad
|
|
4539
|
-
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
4540
4759
|
}
|
|
4541
4760
|
}
|
|
4542
|
-
}
|
|
4543
|
-
|
|
4544
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
4545
|
-
|
|
4546
|
-
/** Pulse the vibration hardware if it exists
|
|
4547
|
-
* @param {number|Array} [pattern] - single value in ms or vibration interval array
|
|
4548
|
-
* @memberof Input */
|
|
4549
|
-
function vibrate(pattern=100)
|
|
4550
|
-
{ vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
|
|
4551
4761
|
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
* @memberof Input */
|
|
4561
|
-
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
4562
|
-
|
|
4563
|
-
// touch gamepad internal variables
|
|
4564
|
-
let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
|
|
4565
|
-
|
|
4566
|
-
function touchGamepadButtonCenter()
|
|
4567
|
-
{
|
|
4568
|
-
// draw right face buttons
|
|
4569
|
-
const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4570
|
-
if (touchGamepadButtonCount <= 2)
|
|
4571
|
-
center.x += touchGamepadSize/2;
|
|
4572
|
-
return center;
|
|
4762
|
+
// convert a mouse or touch event position to screen space
|
|
4763
|
+
function mouseEventToScreen(mousePos)
|
|
4764
|
+
{
|
|
4765
|
+
const rect = mainCanvas.getBoundingClientRect();
|
|
4766
|
+
const px = percent(mousePos.x, rect.left, rect.right);
|
|
4767
|
+
const py = percent(mousePos.y, rect.top, rect.bottom);
|
|
4768
|
+
return vec2(px*mainCanvas.width, py*mainCanvas.height);
|
|
4769
|
+
}
|
|
4573
4770
|
}
|
|
4574
4771
|
|
|
4575
|
-
|
|
4576
|
-
function touchInputInit()
|
|
4772
|
+
function inputUpdate()
|
|
4577
4773
|
{
|
|
4578
|
-
|
|
4579
|
-
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
4580
|
-
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
4581
|
-
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
4774
|
+
if (headlessMode) return;
|
|
4582
4775
|
|
|
4583
|
-
//
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
{
|
|
4587
|
-
if (!touchInputEnable)
|
|
4588
|
-
return;
|
|
4776
|
+
// clear input when lost focus (prevent stuck keys)
|
|
4777
|
+
if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
|
|
4778
|
+
inputClear();
|
|
4589
4779
|
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4780
|
+
// update mouse world space position and delta
|
|
4781
|
+
mousePos = screenToWorld(mousePosScreen);
|
|
4782
|
+
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
4593
4783
|
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4784
|
+
// update gamepads if enabled
|
|
4785
|
+
gamepadsUpdate();
|
|
4786
|
+
|
|
4787
|
+
// gamepads are updated by engine every frame automatically
|
|
4788
|
+
function gamepadsUpdate()
|
|
4789
|
+
{
|
|
4790
|
+
const applyDeadZones = (v)=>
|
|
4791
|
+
{
|
|
4792
|
+
const min=.3, max=.8;
|
|
4793
|
+
const deadZone = (v)=>
|
|
4794
|
+
v > min ? percent(v, min, max) :
|
|
4795
|
+
v < -min ? -percent(-v, min, max) : 0;
|
|
4796
|
+
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
4797
|
+
}
|
|
4597
4798
|
|
|
4598
|
-
//
|
|
4599
|
-
|
|
4600
|
-
const button = 0; // all touches are left mouse button
|
|
4601
|
-
if (touching)
|
|
4799
|
+
// update touch gamepad if enabled
|
|
4800
|
+
if (touchGamepadEnable && isTouchDevice)
|
|
4602
4801
|
{
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4802
|
+
if (!touchGamepadTimer.isSet())
|
|
4803
|
+
return;
|
|
4804
|
+
|
|
4805
|
+
// read virtual analog stick
|
|
4806
|
+
gamepadPrimary = 0; // touch gamepad uses index 0
|
|
4807
|
+
const sticks = gamepadStickData[0] ?? (gamepadStickData[0] = []);
|
|
4808
|
+
const dpad = gamepadDpadData[0] ?? (gamepadDpadData[0] = vec2());
|
|
4809
|
+
sticks[0] = vec2();
|
|
4810
|
+
dpad.set();
|
|
4811
|
+
const leftTouchStick = touchGamepadSticks[0] ?? vec2();
|
|
4812
|
+
if (touchGamepadAnalog)
|
|
4813
|
+
sticks[0] = applyDeadZones(leftTouchStick);
|
|
4814
|
+
else if (leftTouchStick.lengthSquared() > .3)
|
|
4608
4815
|
{
|
|
4609
|
-
|
|
4610
|
-
|
|
4816
|
+
// convert to 8 way dpad
|
|
4817
|
+
const x = clamp(round(leftTouchStick.x), -1, 1);
|
|
4818
|
+
const y = clamp(round(leftTouchStick.y), -1, 1);
|
|
4819
|
+
dpad.set(x, -y);
|
|
4820
|
+
sticks[0] = dpad.clampLength(); // clamp to circle
|
|
4821
|
+
}
|
|
4822
|
+
if (touchGamepadButtonCount === 1)
|
|
4823
|
+
{
|
|
4824
|
+
const rightTouchStick = touchGamepadSticks[1] ?? vec2();
|
|
4825
|
+
sticks[1] = applyDeadZones(rightTouchStick);
|
|
4611
4826
|
}
|
|
4612
|
-
else
|
|
4613
|
-
inputData[0][button] = 3;
|
|
4614
|
-
}
|
|
4615
|
-
else if (wasTouching)
|
|
4616
|
-
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
4617
4827
|
|
|
4618
|
-
|
|
4619
|
-
|
|
4828
|
+
// read virtual gamepad buttons
|
|
4829
|
+
const data = inputData[1] ?? (inputData[1] = []);
|
|
4830
|
+
for (let i=10; i--;)
|
|
4831
|
+
{
|
|
4832
|
+
const wasDown = gamepadIsDown(i,0);
|
|
4833
|
+
data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
4834
|
+
}
|
|
4620
4835
|
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4836
|
+
// disable normal gamepads when touch gamepad is active
|
|
4837
|
+
return;
|
|
4838
|
+
}
|
|
4624
4839
|
|
|
4625
|
-
//
|
|
4626
|
-
|
|
4627
|
-
|
|
4840
|
+
// return if gamepads are disabled or not supported
|
|
4841
|
+
if (!gamepadsEnable || !navigator || !navigator.getGamepads)
|
|
4842
|
+
return;
|
|
4628
4843
|
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
// clear touch gamepad input
|
|
4633
|
-
touchGamepadStick = vec2();
|
|
4634
|
-
touchGamepadButtons = [];
|
|
4635
|
-
isUsingGamepad = true;
|
|
4844
|
+
// only poll gamepads when focused or in debug mode
|
|
4845
|
+
if (!debug && !document.hasFocus())
|
|
4846
|
+
return;
|
|
4636
4847
|
|
|
4637
|
-
|
|
4638
|
-
|
|
4848
|
+
// poll gamepads
|
|
4849
|
+
const maxGamepads = 8;
|
|
4850
|
+
const gamepads = navigator.getGamepads();
|
|
4851
|
+
const gamepadCount = min(maxGamepads, gamepads.length)
|
|
4852
|
+
for (let i=0; i<gamepadCount; ++i)
|
|
4639
4853
|
{
|
|
4640
|
-
|
|
4641
|
-
|
|
4854
|
+
// get or create gamepad data
|
|
4855
|
+
const gamepad = gamepads[i];
|
|
4856
|
+
if (!gamepad)
|
|
4642
4857
|
{
|
|
4643
|
-
//
|
|
4644
|
-
|
|
4645
|
-
|
|
4858
|
+
// clear gamepad data if not connected
|
|
4859
|
+
inputData[i+1] = undefined;
|
|
4860
|
+
gamepadStickData[i] = undefined;
|
|
4861
|
+
gamepadDpadData[i] = undefined;
|
|
4862
|
+
gamepadHadInput[i] = undefined;
|
|
4863
|
+
continue;
|
|
4646
4864
|
}
|
|
4647
|
-
}
|
|
4648
4865
|
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
const startCenter = mainCanvasSize.scale(.5);
|
|
4866
|
+
const data = inputData[i+1] ?? (inputData[i+1] = []);
|
|
4867
|
+
const sticks = gamepadStickData[i] ?? (gamepadStickData[i] = []);
|
|
4868
|
+
const dpad = gamepadDpadData[i] ?? (gamepadDpadData[i] = vec2());
|
|
4653
4869
|
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4870
|
+
// read analog sticks
|
|
4871
|
+
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
4872
|
+
sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
|
|
4873
|
+
|
|
4874
|
+
// read buttons
|
|
4875
|
+
let hadInput = false;
|
|
4876
|
+
for (let j = gamepad.buttons.length; j--;)
|
|
4877
|
+
{
|
|
4878
|
+
const button = gamepad.buttons[j];
|
|
4879
|
+
const wasDown = gamepadIsDown(j,i);
|
|
4880
|
+
data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
4881
|
+
|
|
4882
|
+
// check for any input on this gamepad, analog must be full press
|
|
4883
|
+
if (button.pressed)
|
|
4884
|
+
if (!button.value || button.value > .9)
|
|
4885
|
+
hadInput = true;
|
|
4886
|
+
}
|
|
4887
|
+
|
|
4888
|
+
// set new primary gamepad if current is not connected
|
|
4889
|
+
if (hadInput)
|
|
4659
4890
|
{
|
|
4660
|
-
|
|
4661
|
-
|
|
4891
|
+
gamepadHadInput[i] = true;
|
|
4892
|
+
if (!gamepadHadInput[gamepadPrimary])
|
|
4893
|
+
gamepadPrimary = i;
|
|
4894
|
+
isUsingGamepad ||= (gamepadPrimary === i);
|
|
4662
4895
|
}
|
|
4663
|
-
|
|
4896
|
+
|
|
4897
|
+
if (gamepad.mapping === 'standard')
|
|
4664
4898
|
{
|
|
4665
|
-
//
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
button = 0;
|
|
4670
|
-
else if (touchGamepadButtonCount === 2)
|
|
4671
|
-
{
|
|
4672
|
-
const delta = buttonCenter.subtract(touchPos);
|
|
4673
|
-
button = -delta.x < delta.y ? 1 : 0;
|
|
4674
|
-
}
|
|
4675
|
-
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
4676
|
-
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
4677
|
-
if (button < touchGamepadButtonCount)
|
|
4678
|
-
touchGamepadButtons[button] = 1;
|
|
4899
|
+
// get dpad buttons (standard mapping)
|
|
4900
|
+
dpad.set(
|
|
4901
|
+
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
4902
|
+
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
4679
4903
|
}
|
|
4680
|
-
else if (
|
|
4681
|
-
startCenter.distance(touchPos) < touchGamepadSize)
|
|
4904
|
+
else if (gamepad.axes && gamepad.axes.length >= 2)
|
|
4682
4905
|
{
|
|
4683
|
-
//
|
|
4684
|
-
|
|
4906
|
+
// digital style dpad from axes
|
|
4907
|
+
const x = clamp(round(gamepad.axes[0]), -1, 1);
|
|
4908
|
+
const y = clamp(round(gamepad.axes[1]), -1, 1);
|
|
4909
|
+
dpad.set(x, -y);
|
|
4685
4910
|
}
|
|
4911
|
+
|
|
4912
|
+
// copy dpad to left analog stick when pressed
|
|
4913
|
+
if (gamepadDirectionEmulateStick && !dpad.isZero())
|
|
4914
|
+
sticks[0] = dpad.clampLength();
|
|
4686
4915
|
}
|
|
4916
|
+
|
|
4917
|
+
// disable touch gamepad if using real gamepad
|
|
4918
|
+
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
4687
4919
|
}
|
|
4688
4920
|
}
|
|
4689
4921
|
|
|
4690
|
-
|
|
4691
|
-
function touchGamepadRender()
|
|
4922
|
+
function inputUpdatePost()
|
|
4692
4923
|
{
|
|
4693
|
-
if (
|
|
4694
|
-
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
4695
|
-
return;
|
|
4924
|
+
if (headlessMode) return;
|
|
4696
4925
|
|
|
4697
|
-
//
|
|
4698
|
-
const
|
|
4699
|
-
|
|
4700
|
-
|
|
4926
|
+
// clear input to prepare for next frame
|
|
4927
|
+
for (const deviceInputData of inputData)
|
|
4928
|
+
for (const i in deviceInputData)
|
|
4929
|
+
deviceInputData[i] &= 1;
|
|
4930
|
+
mouseWheel = 0;
|
|
4931
|
+
mouseDelta = vec2();
|
|
4932
|
+
mouseDeltaScreen = vec2();
|
|
4933
|
+
}
|
|
4701
4934
|
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
4706
|
-
context.strokeStyle = '#fff';
|
|
4707
|
-
context.lineWidth = 3;
|
|
4935
|
+
function inputRender()
|
|
4936
|
+
{
|
|
4937
|
+
touchGamepadRender();
|
|
4708
4938
|
|
|
4709
|
-
|
|
4710
|
-
context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
|
|
4711
|
-
context.beginPath();
|
|
4712
|
-
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4713
|
-
if (touchGamepadAnalog) // draw circle shaped gamepad
|
|
4939
|
+
function touchGamepadRender()
|
|
4714
4940
|
{
|
|
4715
|
-
|
|
4941
|
+
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
4942
|
+
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
4943
|
+
return;
|
|
4944
|
+
|
|
4945
|
+
// fade off when not touching or paused
|
|
4946
|
+
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
4947
|
+
if (!alpha || paused)
|
|
4948
|
+
return;
|
|
4949
|
+
|
|
4950
|
+
// setup the canvas
|
|
4951
|
+
const context = overlayContext;
|
|
4952
|
+
context.save();
|
|
4953
|
+
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
4954
|
+
context.strokeStyle = '#fff';
|
|
4955
|
+
context.lineWidth = 3;
|
|
4956
|
+
|
|
4957
|
+
// draw left analog stick
|
|
4958
|
+
const leftTouchStick = touchGamepadSticks[0] ?? vec2();
|
|
4959
|
+
context.fillStyle = leftTouchStick.lengthSquared() > 0 ? '#fff' : '#000';
|
|
4960
|
+
context.beginPath();
|
|
4961
|
+
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4962
|
+
if (touchGamepadAnalog)
|
|
4963
|
+
{
|
|
4964
|
+
// draw circle shaped gamepad
|
|
4965
|
+
context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
|
|
4966
|
+
}
|
|
4967
|
+
else
|
|
4968
|
+
{
|
|
4969
|
+
// draw cross shaped gamepad
|
|
4970
|
+
for (let i=10; --i;)
|
|
4971
|
+
{
|
|
4972
|
+
const angle = i*PI/4;
|
|
4973
|
+
context.arc(stickCenter.x, stickCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
|
|
4974
|
+
i%2 && context.arc(stickCenter.x, stickCenter.y, touchGamepadSize*.33, angle, angle);
|
|
4975
|
+
}
|
|
4976
|
+
}
|
|
4716
4977
|
context.fill();
|
|
4717
4978
|
context.stroke();
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
{
|
|
4721
|
-
for (let i=10; i--;)
|
|
4979
|
+
|
|
4980
|
+
// draw right face buttons
|
|
4722
4981
|
{
|
|
4723
|
-
const
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
i
|
|
4982
|
+
const buttonCenter = touchGamepadButtonCenter();
|
|
4983
|
+
const buttonSize = touchGamepadButtonCount > 1 ?
|
|
4984
|
+
touchGamepadSize/4 : touchGamepadSize/2;
|
|
4985
|
+
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
4986
|
+
{
|
|
4987
|
+
const j = mod(i-1, 4);
|
|
4988
|
+
let button = touchGamepadButtonCount > 2 ?
|
|
4989
|
+
j : min(j, touchGamepadButtonCount-1);
|
|
4990
|
+
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
4991
|
+
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
4992
|
+
const pos = touchGamepadButtonCount < 2 ? buttonCenter :
|
|
4993
|
+
buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
|
|
4994
|
+
context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
|
|
4995
|
+
context.beginPath();
|
|
4996
|
+
context.arc(pos.x, pos.y, buttonSize, 0,9);
|
|
4997
|
+
context.fill();
|
|
4998
|
+
context.stroke();
|
|
4999
|
+
}
|
|
4727
5000
|
}
|
|
4728
|
-
context.stroke();
|
|
4729
|
-
}
|
|
4730
5001
|
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
|
|
4734
|
-
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
4735
|
-
{
|
|
4736
|
-
const j = mod(i-1, 4);
|
|
4737
|
-
let button = touchGamepadButtonCount > 2 ?
|
|
4738
|
-
j : min(j, touchGamepadButtonCount-1);
|
|
4739
|
-
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
4740
|
-
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
4741
|
-
const pos = buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
|
|
4742
|
-
context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
|
|
4743
|
-
context.beginPath();
|
|
4744
|
-
context.arc(pos.x, pos.y, buttonSize, 0,9);
|
|
4745
|
-
context.fill();
|
|
4746
|
-
context.stroke();
|
|
5002
|
+
// set canvas back to normal
|
|
5003
|
+
context.restore();
|
|
4747
5004
|
}
|
|
4748
|
-
|
|
4749
|
-
// set canvas back to normal
|
|
4750
|
-
context.restore();
|
|
4751
5005
|
}
|
|
4752
5006
|
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
/** Request to lock the pointer, does not work on touch devices
|
|
4757
|
-
* @memberof Input */
|
|
4758
|
-
function pointerLockRequest()
|
|
5007
|
+
// center position for right tocuh pad face buttons
|
|
5008
|
+
function touchGamepadButtonCenter()
|
|
4759
5009
|
{
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
* @memberof Input */
|
|
4766
|
-
function pointerLockExit() { document.exitPointerLock && document.exitPointerLock(); }
|
|
4767
|
-
|
|
4768
|
-
/** Check if pointer is locked (true if locked)
|
|
4769
|
-
* @return {boolean}
|
|
4770
|
-
* @memberof Input */
|
|
4771
|
-
function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
|
|
5010
|
+
const center = mainCanvasSize.subtract(vec2(touchGamepadSize));
|
|
5011
|
+
if (touchGamepadButtonCount === 2)
|
|
5012
|
+
center.x += touchGamepadSize/2;
|
|
5013
|
+
return center;
|
|
5014
|
+
}
|
|
4772
5015
|
/**
|
|
4773
5016
|
* LittleJS Audio System
|
|
4774
5017
|
* - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
|
|
@@ -5508,7 +5751,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
|
|
|
5508
5751
|
}
|
|
5509
5752
|
}
|
|
5510
5753
|
|
|
5511
|
-
/** Return the exact position of the
|
|
5754
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
5512
5755
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
5513
5756
|
* @param {Vector2} posStart
|
|
5514
5757
|
* @param {Vector2} posEnd
|
|
@@ -5672,8 +5915,7 @@ class CanvasLayer extends EngineObject
|
|
|
5672
5915
|
/** Destroy this canvas layer */
|
|
5673
5916
|
destroy()
|
|
5674
5917
|
{
|
|
5675
|
-
if (this.destroyed)
|
|
5676
|
-
return;
|
|
5918
|
+
if (this.destroyed) return;
|
|
5677
5919
|
|
|
5678
5920
|
this.textureInfo.destroyWebGLTexture();
|
|
5679
5921
|
super.destroy();
|
|
@@ -6082,7 +6324,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6082
6324
|
return false;
|
|
6083
6325
|
}
|
|
6084
6326
|
|
|
6085
|
-
/** Return the exact position of the
|
|
6327
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
6086
6328
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
6087
6329
|
* @param {Vector2} posStart
|
|
6088
6330
|
* @param {Vector2} posEnd
|
|
@@ -6225,9 +6467,9 @@ class ParticleEmitter extends EngineObject
|
|
|
6225
6467
|
/** @property {Color} - Color at start of life 2, randomized between start colors */
|
|
6226
6468
|
this.colorStartB = colorStartB.copy();
|
|
6227
6469
|
/** @property {Color} - Color at end of life 1, randomized between end colors */
|
|
6228
|
-
this.colorEndA
|
|
6470
|
+
this.colorEndA = colorEndA.copy();
|
|
6229
6471
|
/** @property {Color} - Color at end of life 2, randomized between end colors */
|
|
6230
|
-
this.colorEndB
|
|
6472
|
+
this.colorEndB = colorEndB.copy();
|
|
6231
6473
|
/** @property {boolean} - Should color be randomized linearly or across each component */
|
|
6232
6474
|
this.randomColorLinear = randomColorLinear;
|
|
6233
6475
|
|
|
@@ -6311,8 +6553,10 @@ class ParticleEmitter extends EngineObject
|
|
|
6311
6553
|
if (debugParticles)
|
|
6312
6554
|
{
|
|
6313
6555
|
// show emitter bounds
|
|
6314
|
-
|
|
6315
|
-
|
|
6556
|
+
if (typeof this.emitSize === 'number')
|
|
6557
|
+
debugCircle(this.pos, this.emitSize/2, '#0f0');
|
|
6558
|
+
else
|
|
6559
|
+
debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle);
|
|
6316
6560
|
}
|
|
6317
6561
|
}
|
|
6318
6562
|
|
|
@@ -6322,8 +6566,8 @@ class ParticleEmitter extends EngineObject
|
|
|
6322
6566
|
{
|
|
6323
6567
|
// spawn a particle
|
|
6324
6568
|
let pos = typeof this.emitSize === 'number' ? // check if number was used
|
|
6325
|
-
randInCircle(this.emitSize/2)
|
|
6326
|
-
: vec2(rand(-.5,.5), rand(-.5,.5))
|
|
6569
|
+
randInCircle(this.emitSize/2) // circle emitter
|
|
6570
|
+
: vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
|
|
6327
6571
|
.multiply(this.emitSize).rotate(this.angle)
|
|
6328
6572
|
let angle = rand(this.particleConeAngle, -this.particleConeAngle);
|
|
6329
6573
|
if (!this.localSpace)
|
|
@@ -8027,11 +8271,14 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
8027
8271
|
/**
|
|
8028
8272
|
* LittleJS User Interface Plugin
|
|
8029
8273
|
* - call new UISystemPlugin() to setup the UI system
|
|
8274
|
+
* - Gamepad and keyboard navigation support
|
|
8030
8275
|
* - Nested Menus
|
|
8031
8276
|
* - Text
|
|
8032
8277
|
* - Buttons
|
|
8033
8278
|
* - Checkboxes
|
|
8034
8279
|
* - Images
|
|
8280
|
+
* - Scrollbars
|
|
8281
|
+
* - Video
|
|
8035
8282
|
* @namespace UISystem
|
|
8036
8283
|
*/
|
|
8037
8284
|
|
|
@@ -8042,6 +8289,20 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
8042
8289
|
* @memberof UISystem */
|
|
8043
8290
|
let uiSystem;
|
|
8044
8291
|
|
|
8292
|
+
/** Enable UI system debug drawing
|
|
8293
|
+
* 0=off, 1=normal, 2=show invisible
|
|
8294
|
+
* @type {number}
|
|
8295
|
+
* @default
|
|
8296
|
+
* @memberof UISystem */
|
|
8297
|
+
let uiDebug = 0;
|
|
8298
|
+
|
|
8299
|
+
/** Enable UI system debug drawing
|
|
8300
|
+
* 0=off, 1=normal, 2=show invisible
|
|
8301
|
+
* @param {number|boolean} enable
|
|
8302
|
+
* @memberof UISystem */
|
|
8303
|
+
function uiSetDebug(debugMode)
|
|
8304
|
+
{ uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
|
|
8305
|
+
|
|
8045
8306
|
///////////////////////////////////////////////////////////////////////////////
|
|
8046
8307
|
/**
|
|
8047
8308
|
* UI System Global Object
|
|
@@ -8080,7 +8341,7 @@ class UISystemPlugin
|
|
|
8080
8341
|
/** @property {number} - Default rounded rect corner radius for UI elements */
|
|
8081
8342
|
this.defaultCornerRadius = 0;
|
|
8082
8343
|
/** @property {number} - Default scale to use for fitting text to object */
|
|
8083
|
-
this.
|
|
8344
|
+
this.defaultTextFitScale = .8;
|
|
8084
8345
|
/** @property {string} - Default font for UI elements */
|
|
8085
8346
|
this.defaultFont = fontDefault;
|
|
8086
8347
|
/** @property {Sound} - Default sound when interactive UI element is pressed */
|
|
@@ -8095,6 +8356,21 @@ class UISystemPlugin
|
|
|
8095
8356
|
this.defaultShadowBlur = 5;
|
|
8096
8357
|
/** @property {Vector2} - Offset of shadow blur */
|
|
8097
8358
|
this.defaultShadowOffset = vec2(5);
|
|
8359
|
+
/** @property {number} - If set ui coords will be renormalized to this canvas height */
|
|
8360
|
+
this.nativeHeight = 0;
|
|
8361
|
+
|
|
8362
|
+
// navigation properties
|
|
8363
|
+
/** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
|
|
8364
|
+
this.navigationObject = undefined;
|
|
8365
|
+
/** @property {Timer} - Cooldown timer for navigation inputs */
|
|
8366
|
+
this.navigationTimer = new Timer(undefined, true);
|
|
8367
|
+
/** @property {number} - Time between navigation inputs in seconds */
|
|
8368
|
+
this.navigationDelay = .2;
|
|
8369
|
+
/** @property {boolean} - should the navigation be horizontal, vertical, or both? */
|
|
8370
|
+
this.navigationDirection = 1;
|
|
8371
|
+
/** @property {boolean} - True if user last used navigation instead of mouse */
|
|
8372
|
+
this.navigationMode = false;
|
|
8373
|
+
|
|
8098
8374
|
// system state
|
|
8099
8375
|
/** @property {Array<UIObject>} - List of all UI elements */
|
|
8100
8376
|
this.uiObjects = [];
|
|
@@ -8106,50 +8382,118 @@ class UISystemPlugin
|
|
|
8106
8382
|
this.hoverObject = undefined;
|
|
8107
8383
|
/** @property {UIObject} - Hover object at start of update */
|
|
8108
8384
|
this.lastHoverObject = undefined;
|
|
8109
|
-
/** @property {
|
|
8110
|
-
this.
|
|
8385
|
+
/** @property {UIObject} - Current confirm menu being shown */
|
|
8386
|
+
this.confirmDialog = undefined;
|
|
8111
8387
|
|
|
8112
8388
|
engineAddPlugin(uiUpdate, uiRender);
|
|
8113
8389
|
|
|
8390
|
+
// set object position in parent space
|
|
8391
|
+
function updateTransforms(o)
|
|
8392
|
+
{
|
|
8393
|
+
if (!o.parent) return;
|
|
8394
|
+
o.pos.x = o.localPos.x + o.parent.pos.x;
|
|
8395
|
+
o.pos.y = o.localPos.y + o.parent.pos.y;
|
|
8396
|
+
}
|
|
8397
|
+
|
|
8114
8398
|
// setup recursive update and render
|
|
8115
8399
|
// update in reverse order to detect mouse enter/leave
|
|
8116
8400
|
function uiUpdate()
|
|
8117
8401
|
{
|
|
8118
|
-
|
|
8402
|
+
if (uiSystem.activeObject && !uiSystem.activeObject.visible)
|
|
8403
|
+
uiSystem.activeObject = undefined;
|
|
8404
|
+
|
|
8405
|
+
// reset hover object at start of update
|
|
8406
|
+
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
8407
|
+
uiSystem.hoverObject = undefined;
|
|
8408
|
+
|
|
8409
|
+
if (mouseWasPressed(0))
|
|
8119
8410
|
{
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
updateInvisibleObject(c);
|
|
8123
|
-
o.updateInvisible();
|
|
8411
|
+
uiSystem.navigationMode = false;
|
|
8412
|
+
uiSystem.navigationObject = undefined;
|
|
8124
8413
|
}
|
|
8125
|
-
|
|
8414
|
+
|
|
8415
|
+
// navigation with gamepad/keyboard
|
|
8416
|
+
const navigableObjects = uiSystem.getNavigableObjects();
|
|
8417
|
+
if (!navigableObjects.length)
|
|
8418
|
+
uiSystem.navigationObject = undefined;
|
|
8419
|
+
else
|
|
8126
8420
|
{
|
|
8127
|
-
if
|
|
8421
|
+
// unselect object if it is no longer navigable
|
|
8422
|
+
if (!navigableObjects.includes(uiSystem.navigationObject))
|
|
8423
|
+
uiSystem.navigationObject = undefined;
|
|
8424
|
+
|
|
8425
|
+
if (!isTouchDevice)
|
|
8426
|
+
if (uiSystem.navigationMode && !uiSystem.navigationObject)
|
|
8128
8427
|
{
|
|
8129
|
-
//
|
|
8130
|
-
|
|
8131
|
-
o.pos = o.localPos.add(o.parent.pos);
|
|
8132
|
-
// update in reverse order to detect mouse enter/leave
|
|
8133
|
-
for (let i=o.children.length; i--;)
|
|
8134
|
-
updateObject(o.children[i]);
|
|
8135
|
-
o.update();
|
|
8428
|
+
// select first auto focus object
|
|
8429
|
+
uiSystem.navigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
|
|
8136
8430
|
}
|
|
8137
|
-
|
|
8138
|
-
|
|
8431
|
+
|
|
8432
|
+
// navigate with dpad or left stick
|
|
8433
|
+
if (!uiSystem.navigationTimer.active())
|
|
8434
|
+
{
|
|
8435
|
+
// navigate through list with gamepad or keyboard
|
|
8436
|
+
const direction = sign(uiSystem.getNavigationDirection());
|
|
8437
|
+
if (direction)
|
|
8438
|
+
{
|
|
8439
|
+
let newNavigationObject;
|
|
8440
|
+
if (!uiSystem.navigationObject)
|
|
8441
|
+
{
|
|
8442
|
+
// use auto select object
|
|
8443
|
+
newNavigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
|
|
8444
|
+
|
|
8445
|
+
if (!newNavigationObject)
|
|
8446
|
+
{
|
|
8447
|
+
// try first or last object
|
|
8448
|
+
const newIndex = direction > 0 ? 0 : navigableObjects.length-1;
|
|
8449
|
+
newNavigationObject = navigableObjects[newIndex];
|
|
8450
|
+
}
|
|
8451
|
+
}
|
|
8452
|
+
else
|
|
8453
|
+
{
|
|
8454
|
+
const currentIndex = navigableObjects.indexOf(uiSystem.navigationObject);
|
|
8455
|
+
const newIndex = mod(currentIndex + direction, navigableObjects.length);
|
|
8456
|
+
newNavigationObject = navigableObjects[newIndex];
|
|
8457
|
+
}
|
|
8458
|
+
|
|
8459
|
+
if (uiSystem.navigationObject !== newNavigationObject)
|
|
8460
|
+
{
|
|
8461
|
+
uiSystem.navigationMode = true;
|
|
8462
|
+
uiSystem.hoverObject = undefined;
|
|
8463
|
+
uiSystem.navigationObject = newNavigationObject;
|
|
8464
|
+
uiSystem.navigationTimer.set(uiSystem.navigationDelay);
|
|
8465
|
+
newNavigationObject.soundPress &&
|
|
8466
|
+
newNavigationObject.soundPress.play();
|
|
8467
|
+
}
|
|
8468
|
+
}
|
|
8469
|
+
}
|
|
8470
|
+
|
|
8471
|
+
// activate the navigation object when pressed
|
|
8472
|
+
if (uiSystem.navigationObject)
|
|
8473
|
+
if (uiSystem.getNavigationWasPressed())
|
|
8474
|
+
uiSystem.navigationObject.navigatePressed();
|
|
8139
8475
|
}
|
|
8140
|
-
// reset hover object at start of update
|
|
8141
|
-
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
8142
|
-
uiSystem.hoverObject = undefined;
|
|
8143
8476
|
|
|
8144
8477
|
// update in reverse order so topmost objects get priority
|
|
8145
8478
|
for (let i = uiSystem.uiObjects.length; i--;)
|
|
8146
8479
|
{
|
|
8147
8480
|
const o = uiSystem.uiObjects[i];
|
|
8148
|
-
o.parent || updateObject(o)
|
|
8481
|
+
o.parent || updateObject(o);
|
|
8149
8482
|
}
|
|
8150
8483
|
|
|
8151
8484
|
// remove destroyed objects
|
|
8152
8485
|
uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
|
|
8486
|
+
|
|
8487
|
+
function updateObject(o)
|
|
8488
|
+
{
|
|
8489
|
+
if (!o.visible) return;
|
|
8490
|
+
|
|
8491
|
+
// update in reverse order to detect mouse enter/leave
|
|
8492
|
+
updateTransforms(o);
|
|
8493
|
+
for (let i=o.children.length; i--;)
|
|
8494
|
+
updateObject(o.children[i]);
|
|
8495
|
+
o.update();
|
|
8496
|
+
}
|
|
8153
8497
|
}
|
|
8154
8498
|
function uiRender()
|
|
8155
8499
|
{
|
|
@@ -8166,15 +8510,29 @@ class UISystemPlugin
|
|
|
8166
8510
|
|
|
8167
8511
|
function renderObject(o)
|
|
8168
8512
|
{
|
|
8169
|
-
if (!o.visible)
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8513
|
+
if (!o.visible) return;
|
|
8514
|
+
|
|
8515
|
+
// render object and children
|
|
8516
|
+
updateTransforms(o);
|
|
8173
8517
|
o.render();
|
|
8174
8518
|
for (const c of o.children)
|
|
8175
8519
|
renderObject(c);
|
|
8176
8520
|
}
|
|
8177
8521
|
uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
|
|
8522
|
+
|
|
8523
|
+
if (uiDebug > 0)
|
|
8524
|
+
{
|
|
8525
|
+
// debug render all objects
|
|
8526
|
+
function renderDebug(o, visible=true)
|
|
8527
|
+
{
|
|
8528
|
+
visible &&= !!o.visible;
|
|
8529
|
+
updateTransforms(o);
|
|
8530
|
+
o.renderDebug(visible);
|
|
8531
|
+
for (const c of o.children)
|
|
8532
|
+
renderDebug(c, visible);
|
|
8533
|
+
}
|
|
8534
|
+
uiSystem.uiObjects.forEach(o=> o.parent || renderDebug(o));
|
|
8535
|
+
}
|
|
8178
8536
|
context.restore();
|
|
8179
8537
|
}
|
|
8180
8538
|
}
|
|
@@ -8227,8 +8585,8 @@ class UISystemPlugin
|
|
|
8227
8585
|
else
|
|
8228
8586
|
context.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
|
|
8229
8587
|
context.fill();
|
|
8230
|
-
context.shadowColor = '#0000'
|
|
8231
|
-
if (lineWidth)
|
|
8588
|
+
context.shadowColor = '#0000';
|
|
8589
|
+
if (lineWidth && lineColor.a > 0)
|
|
8232
8590
|
{
|
|
8233
8591
|
context.strokeStyle = lineColor.toString();
|
|
8234
8592
|
context.lineWidth = lineWidth;
|
|
@@ -8263,10 +8621,24 @@ class UISystemPlugin
|
|
|
8263
8621
|
* @param {TileInfo} tileInfo
|
|
8264
8622
|
* @param {Color} [color=uiSystem.defaultColor]
|
|
8265
8623
|
* @param {number} [angle]
|
|
8266
|
-
* @param {boolean} [mirror]
|
|
8267
|
-
|
|
8624
|
+
* @param {boolean} [mirror]
|
|
8625
|
+
* @param {Color} [shadowColor]
|
|
8626
|
+
* @param {number} [shadowBlur]
|
|
8627
|
+
* @param {Color} [shadowOffset] */
|
|
8628
|
+
drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
|
|
8268
8629
|
{
|
|
8269
|
-
|
|
8630
|
+
const context = uiSystem.uiContext;
|
|
8631
|
+
if (shadowBlur || shadowOffset.x || shadowOffset.y)
|
|
8632
|
+
if (shadowColor.a > 0)
|
|
8633
|
+
{
|
|
8634
|
+
// setup shadow
|
|
8635
|
+
context.shadowColor = shadowColor.toString();
|
|
8636
|
+
context.shadowBlur = shadowBlur;
|
|
8637
|
+
context.shadowOffsetX = shadowOffset.x;
|
|
8638
|
+
context.shadowOffsetY = shadowOffset.y;
|
|
8639
|
+
}
|
|
8640
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, CLEAR_BLACK, false, true, context);
|
|
8641
|
+
context.shadowColor = '#0000';
|
|
8270
8642
|
}
|
|
8271
8643
|
|
|
8272
8644
|
/** Draw text to the UI context
|
|
@@ -8281,12 +8653,27 @@ class UISystemPlugin
|
|
|
8281
8653
|
* @param {string} [fontStyle]
|
|
8282
8654
|
* @param {boolean} [applyMaxWidth=true]
|
|
8283
8655
|
* @param {Vector2} [textShadow]
|
|
8284
|
-
|
|
8285
|
-
|
|
8656
|
+
* @param {Color} [shadowColor]
|
|
8657
|
+
* @param {number} [shadowBlur]
|
|
8658
|
+
* @param {Color} [shadowOffset] */
|
|
8659
|
+
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())
|
|
8286
8660
|
{
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8661
|
+
const context = uiSystem.uiContext;
|
|
8662
|
+
if (shadowColor.a > 0)
|
|
8663
|
+
{
|
|
8664
|
+
if (textShadow)
|
|
8665
|
+
drawTextScreen(text, pos.add(textShadow), size.y, shadowColor, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, context);
|
|
8666
|
+
if (shadowBlur || shadowOffset.x || shadowOffset.y)
|
|
8667
|
+
{
|
|
8668
|
+
// setup shadow
|
|
8669
|
+
context.shadowColor = shadowColor.toString();
|
|
8670
|
+
context.shadowBlur = shadowBlur;
|
|
8671
|
+
context.shadowOffsetX = shadowOffset.x;
|
|
8672
|
+
context.shadowOffsetY = shadowOffset.y;
|
|
8673
|
+
}
|
|
8674
|
+
}
|
|
8675
|
+
drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, context);
|
|
8676
|
+
context.shadowColor = '#0000';
|
|
8290
8677
|
}
|
|
8291
8678
|
|
|
8292
8679
|
/**
|
|
@@ -8300,7 +8687,7 @@ class UISystemPlugin
|
|
|
8300
8687
|
* @param {DragAndDropCallback} [onDrop] - when a file is dropped
|
|
8301
8688
|
* @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
|
|
8302
8689
|
* @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
|
|
8303
|
-
* @param {DragAndDropCallback} [onDragOver] -
|
|
8690
|
+
* @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
|
|
8304
8691
|
setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
|
|
8305
8692
|
{
|
|
8306
8693
|
function setCallback(callback, listenerType)
|
|
@@ -8316,8 +8703,7 @@ class UISystemPlugin
|
|
|
8316
8703
|
|
|
8317
8704
|
/** Convert a screen space position to native UI position
|
|
8318
8705
|
* @param {Vector2} pos
|
|
8319
|
-
* @return {Vector2}
|
|
8320
|
-
*/
|
|
8706
|
+
* @return {Vector2} */
|
|
8321
8707
|
screenToNative(pos)
|
|
8322
8708
|
{
|
|
8323
8709
|
if (!uiSystem.nativeHeight)
|
|
@@ -8340,6 +8726,157 @@ class UISystemPlugin
|
|
|
8340
8726
|
for (const o of this.uiObjects)
|
|
8341
8727
|
o.parent || o.destroy();
|
|
8342
8728
|
this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
|
|
8729
|
+
this.activeObject = undefined;
|
|
8730
|
+
this.hoverObject = undefined;
|
|
8731
|
+
this.lastHoverObject = undefined;
|
|
8732
|
+
}
|
|
8733
|
+
|
|
8734
|
+
/** Get all navigable UI objects sorted by navigationIndex
|
|
8735
|
+
* @return {Array<UIObject>} */
|
|
8736
|
+
getNavigableObjects()
|
|
8737
|
+
{
|
|
8738
|
+
function getNavigableRecursive(o)
|
|
8739
|
+
{
|
|
8740
|
+
if (!o.visible || o.disabled)
|
|
8741
|
+
return; // skip children if parent is invisible or disabled
|
|
8742
|
+
|
|
8743
|
+
if (o.isInteractive() && o.navigationIndex !== undefined)
|
|
8744
|
+
objects.push(o);
|
|
8745
|
+
for (let i=o.children.length; i--;)
|
|
8746
|
+
getNavigableRecursive(o.children[i]);
|
|
8747
|
+
}
|
|
8748
|
+
|
|
8749
|
+
// get all the valid navigable objects recursively
|
|
8750
|
+
let objects = [];
|
|
8751
|
+
for (let i = uiSystem.uiObjects.length; i--;)
|
|
8752
|
+
{
|
|
8753
|
+
const o = uiSystem.uiObjects[i];
|
|
8754
|
+
if (uiSystem.confirmDialog && o !== uiSystem.confirmDialog)
|
|
8755
|
+
continue;
|
|
8756
|
+
o.parent || getNavigableRecursive(o);
|
|
8757
|
+
}
|
|
8758
|
+
|
|
8759
|
+
// sort by navigationIndex (lower numbers first)
|
|
8760
|
+
objects.sort((a, b)=> a.navigationIndex - b.navigationIndex);
|
|
8761
|
+
return objects;
|
|
8762
|
+
}
|
|
8763
|
+
|
|
8764
|
+
/** Get navigation direction from gamepad or keyboard
|
|
8765
|
+
* @return {number} */
|
|
8766
|
+
getNavigationDirection()
|
|
8767
|
+
{
|
|
8768
|
+
const vertical = uiSystem.navigationDirection === 1;
|
|
8769
|
+
const both = uiSystem.navigationDirection === 2;
|
|
8770
|
+
if (isUsingGamepad)
|
|
8771
|
+
{
|
|
8772
|
+
const stick = gamepadStick(0, gamepadPrimary);
|
|
8773
|
+
const dpad = gamepadDpad(gamepadPrimary);
|
|
8774
|
+
if (both)
|
|
8775
|
+
return -(stick.y || dpad.y) || (stick.x || dpad.x);
|
|
8776
|
+
return vertical ? -(stick.y || dpad.y) : (stick.x || dpad.x);
|
|
8777
|
+
}
|
|
8778
|
+
const up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight';
|
|
8779
|
+
if (both)
|
|
8780
|
+
{
|
|
8781
|
+
return keyIsDown(up) || keyIsDown(left) ? -1 :
|
|
8782
|
+
keyIsDown(down) || keyIsDown(right) ? 1 : 0;
|
|
8783
|
+
}
|
|
8784
|
+
const back = vertical ? up : left;
|
|
8785
|
+
const forward = vertical ? down : right;
|
|
8786
|
+
return keyIsDown(back) ? -1 : keyIsDown(forward) ? 1 : 0;
|
|
8787
|
+
}
|
|
8788
|
+
|
|
8789
|
+
/** Get other axis navigation direction from gamepad or keyboard
|
|
8790
|
+
* @return {Vector2} */
|
|
8791
|
+
getNavigationOtherDirection()
|
|
8792
|
+
{
|
|
8793
|
+
if (uiSystem.navigationDirection === 2)
|
|
8794
|
+
return 0; // other direction disabled
|
|
8795
|
+
|
|
8796
|
+
const vertical = uiSystem.navigationDirection === 1;
|
|
8797
|
+
if (isUsingGamepad)
|
|
8798
|
+
{
|
|
8799
|
+
const stick = gamepadStick(0, gamepadPrimary);
|
|
8800
|
+
const dpad = gamepadDpad(gamepadPrimary);
|
|
8801
|
+
return !vertical ? (stick.y || dpad.y) : (stick.x || dpad.x);
|
|
8802
|
+
}
|
|
8803
|
+
const back = !vertical ? 'ArrowUp' : 'ArrowLeft';
|
|
8804
|
+
const forward = !vertical ? 'ArrowDown' : 'ArrowRight';
|
|
8805
|
+
return keyIsDown(back) ? -1 : keyIsDown(forward) ? 1 : 0;
|
|
8806
|
+
}
|
|
8807
|
+
|
|
8808
|
+
/** Get if navigation button was pressed from gamepad or keyboard
|
|
8809
|
+
* @return {boolean} */
|
|
8810
|
+
getNavigationWasPressed()
|
|
8811
|
+
{
|
|
8812
|
+
return isUsingGamepad ? gamepadWasPressed(0, gamepadPrimary) :
|
|
8813
|
+
keyWasPressed('Space') || keyWasPressed('Enter');
|
|
8814
|
+
}
|
|
8815
|
+
|
|
8816
|
+
/** Show a confirmation dialog with Yes/No buttons
|
|
8817
|
+
* Centers the dialog on the screen with darkened background
|
|
8818
|
+
* @param {string} [text] - The message to display
|
|
8819
|
+
* @param {Function} [yesCallback] - Called when Yes is clicked
|
|
8820
|
+
* @param {Function} [noCallback] - Called when No is clicked
|
|
8821
|
+
* @param {Vector2} [size] - Size of the confirmation dialog
|
|
8822
|
+
* @param {string} [exitKey] - Key that can exit the menu
|
|
8823
|
+
* @return {UIObject} The confirmation menu object
|
|
8824
|
+
*/
|
|
8825
|
+
showConfirmDialog(text='Are you sure?', yesCallback, noCallback, size=vec2(500,250), exitKey='Escape')
|
|
8826
|
+
{
|
|
8827
|
+
ASSERT(!uiSystem.confirmDialog);
|
|
8828
|
+
|
|
8829
|
+
const savedNavigationDirection = uiSystem.navigationDirection;
|
|
8830
|
+
|
|
8831
|
+
// allow both axies for navigation
|
|
8832
|
+
uiSystem.navigationDirection = 2;
|
|
8833
|
+
|
|
8834
|
+
// confirm menu
|
|
8835
|
+
const confirmMenu = new UIObject(vec2(), size);
|
|
8836
|
+
uiSystem.confirmDialog = confirmMenu;
|
|
8837
|
+
confirmMenu.onRender = ()=>
|
|
8838
|
+
{
|
|
8839
|
+
confirmMenu.pos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
|
|
8840
|
+
const backgroundColor = hsl(0,0,0,.7);
|
|
8841
|
+
uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
|
|
8842
|
+
}
|
|
8843
|
+
confirmMenu.onUpdate = ()=>
|
|
8844
|
+
{
|
|
8845
|
+
if (keyWasPressed(exitKey))
|
|
8846
|
+
closeMenu();
|
|
8847
|
+
}
|
|
8848
|
+
confirmMenu.isMouseOverlapping = ()=> true; // always hover
|
|
8849
|
+
|
|
8850
|
+
// title text
|
|
8851
|
+
const gap = 50;
|
|
8852
|
+
const textTitle = new UIText(vec2(0,-50), vec2(size.x-gap,70), text);
|
|
8853
|
+
confirmMenu.addChild(textTitle);
|
|
8854
|
+
|
|
8855
|
+
// yes button
|
|
8856
|
+
const buttonYes = new UIButton(vec2(-80,50), vec2(120,70), 'Yes');
|
|
8857
|
+
buttonYes.textHeight = 40;
|
|
8858
|
+
buttonYes.navigationIndex = 1;
|
|
8859
|
+
buttonYes.hoverColor = hsl(0,1,.5);
|
|
8860
|
+
buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
|
|
8861
|
+
confirmMenu.addChild(buttonYes);
|
|
8862
|
+
|
|
8863
|
+
// no button
|
|
8864
|
+
const buttonNo = new UIButton(vec2(80,50), vec2(120,70), 'No');
|
|
8865
|
+
buttonNo.textHeight = 40;
|
|
8866
|
+
buttonNo.navigationIndex = 2;
|
|
8867
|
+
buttonNo.navigationAutoSelect = true;
|
|
8868
|
+
buttonNo.onClick = ()=> { closeMenu(); noCallback && noCallback(); };
|
|
8869
|
+
confirmMenu.addChild(buttonNo);
|
|
8870
|
+
|
|
8871
|
+
// close menu and return to normal navigation
|
|
8872
|
+
function closeMenu()
|
|
8873
|
+
{
|
|
8874
|
+
ASSERT(uiSystem.confirmDialog === confirmMenu);
|
|
8875
|
+
confirmMenu.destroy();
|
|
8876
|
+
uiSystem.confirmDialog = undefined;
|
|
8877
|
+
uiSystem.navigationDirection = savedNavigationDirection;
|
|
8878
|
+
inputClear();
|
|
8879
|
+
}
|
|
8343
8880
|
}
|
|
8344
8881
|
}
|
|
8345
8882
|
|
|
@@ -8395,7 +8932,13 @@ class UIObject
|
|
|
8395
8932
|
/** @property {number} - Override for text height */
|
|
8396
8933
|
this.textHeight = undefined;
|
|
8397
8934
|
/** @property {number} - Scale text to fit in the object */
|
|
8398
|
-
this.
|
|
8935
|
+
this.textFitScale = uiSystem.defaultTextFitScale;
|
|
8936
|
+
/** @property {Vector2} - How much to offset the text shadow or undefined */
|
|
8937
|
+
this.textShadow = undefined;
|
|
8938
|
+
/** @property {number} - Color for text line drawing */
|
|
8939
|
+
this.textLineColor = uiSystem.defaultLineColor.copy();
|
|
8940
|
+
/** @property {number} - Width for text line drawing */
|
|
8941
|
+
this.textLineWidth = 0;
|
|
8399
8942
|
/** @property {boolean} - Should this object be drawn */
|
|
8400
8943
|
this.visible = true;
|
|
8401
8944
|
/** @property {Array<UIObject>} - A list of this object's children */
|
|
@@ -8421,16 +8964,17 @@ class UIObject
|
|
|
8421
8964
|
/** @property {number} - Size of shadow blur */
|
|
8422
8965
|
this.shadowBlur = uiSystem.defaultShadowBlur;
|
|
8423
8966
|
/** @property {Vector2} - Offset of shadow blur */
|
|
8424
|
-
this.shadowOffset = uiSystem.defaultShadowOffset
|
|
8425
|
-
|
|
8967
|
+
this.shadowOffset = uiSystem.defaultShadowOffset?.copy();
|
|
8968
|
+
/** @property {number} - Optional navigation order index, lower values are selected first */
|
|
8969
|
+
this.navigationIndex = undefined;
|
|
8970
|
+
/** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
|
|
8971
|
+
this.navigationAutoSelect = false;
|
|
8426
8972
|
|
|
8427
|
-
|
|
8428
|
-
this.textShadow = undefined;
|
|
8973
|
+
uiSystem.uiObjects.push(this);
|
|
8429
8974
|
}
|
|
8430
8975
|
|
|
8431
8976
|
/** Add a child UIObject to this object
|
|
8432
|
-
* @param {UIObject} child
|
|
8433
|
-
*/
|
|
8977
|
+
* @param {UIObject} child */
|
|
8434
8978
|
addChild(child)
|
|
8435
8979
|
{
|
|
8436
8980
|
ASSERT(!child.parent && !this.children.includes(child));
|
|
@@ -8439,8 +8983,7 @@ class UIObject
|
|
|
8439
8983
|
}
|
|
8440
8984
|
|
|
8441
8985
|
/** Remove a child UIObject from this object
|
|
8442
|
-
* @param {UIObject} child
|
|
8443
|
-
*/
|
|
8986
|
+
* @param {UIObject} child */
|
|
8444
8987
|
removeChild(child)
|
|
8445
8988
|
{
|
|
8446
8989
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
@@ -8448,7 +8991,6 @@ class UIObject
|
|
|
8448
8991
|
child.parent = undefined;
|
|
8449
8992
|
}
|
|
8450
8993
|
|
|
8451
|
-
|
|
8452
8994
|
/** Destroy this object, destroy its children, detach its parent, and mark it for removal */
|
|
8453
8995
|
destroy()
|
|
8454
8996
|
{
|
|
@@ -8464,9 +9006,9 @@ class UIObject
|
|
|
8464
9006
|
child.destroy();
|
|
8465
9007
|
}
|
|
8466
9008
|
}
|
|
9009
|
+
|
|
8467
9010
|
/** Check if the mouse is overlapping a box in screen space
|
|
8468
|
-
* @return {boolean} - True if overlapping
|
|
8469
|
-
*/
|
|
9011
|
+
* @return {boolean} - True if overlapping */
|
|
8470
9012
|
isMouseOverlapping()
|
|
8471
9013
|
{
|
|
8472
9014
|
if (!mouseInWindow) return false;
|
|
@@ -8483,11 +9025,16 @@ class UIObject
|
|
|
8483
9025
|
// call the custom update callback
|
|
8484
9026
|
this.onUpdate();
|
|
8485
9027
|
|
|
9028
|
+
// unset active if disabled
|
|
9029
|
+
if (this.disabled && this == uiSystem.activeObject)
|
|
9030
|
+
uiSystem.activeObject = undefined;
|
|
9031
|
+
|
|
8486
9032
|
const wasHover = uiSystem.lastHoverObject === this;
|
|
8487
9033
|
const isActive = this.isActiveObject();
|
|
8488
9034
|
const mouseDown = mouseIsDown(0);
|
|
8489
9035
|
const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
|
|
8490
9036
|
if (this.canBeHover)
|
|
9037
|
+
if (!uiSystem.navigationMode) // no mouse hover in navigation mode
|
|
8491
9038
|
if (mousePress || isActive || (!mouseDown && !isTouchDevice))
|
|
8492
9039
|
if (!uiSystem.hoverObject && this.isMouseOverlapping())
|
|
8493
9040
|
uiSystem.hoverObject = this;
|
|
@@ -8501,8 +9048,7 @@ class UIObject
|
|
|
8501
9048
|
{
|
|
8502
9049
|
if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
|
|
8503
9050
|
this.onPress();
|
|
8504
|
-
|
|
8505
|
-
this.soundPress.play();
|
|
9051
|
+
this.soundPress && this.soundPress.play();
|
|
8506
9052
|
if (uiSystem.activeObject && !isActive)
|
|
8507
9053
|
uiSystem.activeObject.onRelease();
|
|
8508
9054
|
uiSystem.activeObject = this;
|
|
@@ -8511,10 +9057,10 @@ class UIObject
|
|
|
8511
9057
|
if (!mouseDown && this.isActiveObject() && this.interactive)
|
|
8512
9058
|
{
|
|
8513
9059
|
this.onClick();
|
|
8514
|
-
|
|
8515
|
-
this.soundClick.play();
|
|
9060
|
+
this.soundClick && this.soundClick.play();
|
|
8516
9061
|
}
|
|
8517
9062
|
}
|
|
9063
|
+
|
|
8518
9064
|
// clear mouse was pressed state even when disabled
|
|
8519
9065
|
mousePress && inputClearKey(0,0,0,1,0);
|
|
8520
9066
|
}
|
|
@@ -8522,8 +9068,7 @@ class UIObject
|
|
|
8522
9068
|
if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
|
|
8523
9069
|
{
|
|
8524
9070
|
this.onRelease();
|
|
8525
|
-
|
|
8526
|
-
this.soundRelease.play();
|
|
9071
|
+
this.soundRelease && this.soundRelease.play();
|
|
8527
9072
|
uiSystem.activeObject = undefined;
|
|
8528
9073
|
}
|
|
8529
9074
|
|
|
@@ -8535,29 +9080,40 @@ class UIObject
|
|
|
8535
9080
|
/** Render the object, called automatically by plugin once each frame */
|
|
8536
9081
|
render()
|
|
8537
9082
|
{
|
|
8538
|
-
|
|
9083
|
+
// call the custom render callback
|
|
9084
|
+
this.onRender();
|
|
8539
9085
|
|
|
8540
|
-
|
|
8541
|
-
const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
|
|
8542
|
-
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
8543
|
-
}
|
|
9086
|
+
if (!this.size.x || !this.size.y) return;
|
|
8544
9087
|
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
9088
|
+
const isNavigationObject = this.isNavigationObject();
|
|
9089
|
+
const lineColor = isNavigationObject ? this.color :
|
|
9090
|
+
this.interactive && this.isActiveObject() && !this.disabled ?
|
|
9091
|
+
this.color : this.lineColor;
|
|
9092
|
+
const color = isNavigationObject ? this.hoverColor :
|
|
9093
|
+
this.disabled ? this.disabledColor :
|
|
9094
|
+
this.interactive ?
|
|
9095
|
+
this.isHoverObject() ? this.hoverColor :
|
|
9096
|
+
this.isActiveObject() ? this.activeColor || this.color :
|
|
9097
|
+
this.color : this.color;
|
|
9098
|
+
const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
|
|
9099
|
+
|
|
9100
|
+
uiSystem.drawRect(this.pos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
8551
9101
|
}
|
|
8552
9102
|
|
|
8553
9103
|
/** Get the size for text with overrides and scale
|
|
8554
|
-
* @return {Vector2}
|
|
8555
|
-
*/
|
|
9104
|
+
* @return {Vector2} */
|
|
8556
9105
|
getTextSize()
|
|
8557
9106
|
{
|
|
8558
9107
|
return vec2(
|
|
8559
|
-
this.textWidth || this.
|
|
8560
|
-
this.textHeight || this.
|
|
9108
|
+
this.textWidth || this.textFitScale * this.size.x,
|
|
9109
|
+
this.textHeight || this.textFitScale * this.size.y);
|
|
9110
|
+
}
|
|
9111
|
+
|
|
9112
|
+
/** Called when the navigation button is pressed on this object */
|
|
9113
|
+
navigatePressed()
|
|
9114
|
+
{
|
|
9115
|
+
this.onClick();
|
|
9116
|
+
this.soundClick && this.soundClick.play();
|
|
8561
9117
|
}
|
|
8562
9118
|
|
|
8563
9119
|
/** @return {boolean} - Is the mouse hovering over this element */
|
|
@@ -8566,9 +9122,51 @@ class UIObject
|
|
|
8566
9122
|
/** @return {boolean} - Is the mouse held onto this element */
|
|
8567
9123
|
isActiveObject() { return uiSystem.activeObject === this; }
|
|
8568
9124
|
|
|
8569
|
-
/**
|
|
9125
|
+
/** @return {boolean} - Is the gamepad or keyboard navigation object */
|
|
9126
|
+
isNavigationObject() { return uiSystem.navigationObject === this; }
|
|
9127
|
+
|
|
9128
|
+
/** @return {boolean} - Can it be interacted with */
|
|
9129
|
+
isInteractive() { return this.interactive && this.visible && !this.disabled;}
|
|
9130
|
+
|
|
9131
|
+
/** Returns string containing info about this object for debugging
|
|
9132
|
+
* @return {string} */
|
|
9133
|
+
toString()
|
|
9134
|
+
{
|
|
9135
|
+
if (!debug) return;
|
|
9136
|
+
|
|
9137
|
+
let text = 'type = ' + this.constructor.name;
|
|
9138
|
+
if (this.text)
|
|
9139
|
+
text += '\ntext = ' + this.text;
|
|
9140
|
+
if (this.pos.x || this.pos.y)
|
|
9141
|
+
text += '\npos = ' + this.pos;
|
|
9142
|
+
if (this.localPos.x || this.localPos.y)
|
|
9143
|
+
text += '\localPos = ' + this.localPos;
|
|
9144
|
+
if (this.size.x || this.size.y)
|
|
9145
|
+
text += '\nsize = ' + this.size;
|
|
9146
|
+
if (this.color)
|
|
9147
|
+
text += '\ncolor = ' + this.color;
|
|
9148
|
+
return text;
|
|
9149
|
+
}
|
|
9150
|
+
|
|
9151
|
+
/** Called if uiDebug is enabled
|
|
9152
|
+
* @param {boolean} visible */
|
|
9153
|
+
renderDebug(visible=true)
|
|
9154
|
+
{
|
|
9155
|
+
// apply color based on state
|
|
9156
|
+
const color =
|
|
9157
|
+
!visible ? GREEN :
|
|
9158
|
+
this.isHoverObject() ? YELLOW :
|
|
9159
|
+
this.disabled ? PURPLE :
|
|
9160
|
+
this.interactive ? RED : BLUE;
|
|
9161
|
+
uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);
|
|
9162
|
+
}
|
|
9163
|
+
|
|
9164
|
+
/** Called each frame before object updates */
|
|
8570
9165
|
onUpdate() {}
|
|
8571
9166
|
|
|
9167
|
+
/** Called each frame before object renders */
|
|
9168
|
+
onRender() {}
|
|
9169
|
+
|
|
8572
9170
|
/** Called when the mouse enters the object */
|
|
8573
9171
|
onEnter() {}
|
|
8574
9172
|
|
|
@@ -8616,16 +9214,25 @@ class UIText extends UIObject
|
|
|
8616
9214
|
this.align = align;
|
|
8617
9215
|
this.font = font;
|
|
8618
9216
|
|
|
8619
|
-
// make text not outlined by default
|
|
8620
|
-
this.lineWidth = 0;
|
|
8621
9217
|
// text can not be a hover object by default
|
|
8622
9218
|
this.canBeHover = false;
|
|
9219
|
+
|
|
9220
|
+
// no background by default
|
|
9221
|
+
this.color = CLEAR_BLACK;
|
|
9222
|
+
this.shadowColor = CLEAR_BLACK;
|
|
9223
|
+
this.gradientColor = undefined;
|
|
9224
|
+
this.lineWidth = 0;
|
|
9225
|
+
|
|
9226
|
+
// use max fit scale by default
|
|
9227
|
+
this.textFitScale = 1;
|
|
8623
9228
|
}
|
|
8624
9229
|
render()
|
|
8625
9230
|
{
|
|
8626
|
-
|
|
9231
|
+
super.render();
|
|
9232
|
+
|
|
9233
|
+
// render the text
|
|
8627
9234
|
const textSize = this.getTextSize();
|
|
8628
|
-
uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.
|
|
9235
|
+
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);
|
|
8629
9236
|
}
|
|
8630
9237
|
}
|
|
8631
9238
|
|
|
@@ -8661,10 +9268,13 @@ class UITile extends UIObject
|
|
|
8661
9268
|
this.mirror = mirror;
|
|
8662
9269
|
// set properties
|
|
8663
9270
|
this.color = color.copy();
|
|
9271
|
+
|
|
9272
|
+
// no shadow by default
|
|
9273
|
+
this.shadowColor = CLEAR_BLACK;
|
|
8664
9274
|
}
|
|
8665
9275
|
render()
|
|
8666
9276
|
{
|
|
8667
|
-
uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror);
|
|
9277
|
+
uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
8668
9278
|
}
|
|
8669
9279
|
}
|
|
8670
9280
|
|
|
@@ -8689,6 +9299,9 @@ class UIButton extends UIObject
|
|
|
8689
9299
|
ASSERT(isString(text), 'ui button must be a string');
|
|
8690
9300
|
ASSERT(isColor(color), 'ui button color must be a color');
|
|
8691
9301
|
|
|
9302
|
+
/** @property {Vector2} - Text offset for the button */
|
|
9303
|
+
this.textOffset = vec2();
|
|
9304
|
+
|
|
8692
9305
|
// set properties
|
|
8693
9306
|
this.text = text;
|
|
8694
9307
|
this.color = color.copy();
|
|
@@ -8700,8 +9313,8 @@ class UIButton extends UIObject
|
|
|
8700
9313
|
|
|
8701
9314
|
// draw the text scaled to fit
|
|
8702
9315
|
const textSize = this.getTextSize();
|
|
8703
|
-
uiSystem.drawText(this.text, this.pos, textSize,
|
|
8704
|
-
this.textColor,
|
|
9316
|
+
uiSystem.drawText(this.text, this.pos.add(this.textOffset), textSize,
|
|
9317
|
+
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
8705
9318
|
}
|
|
8706
9319
|
}
|
|
8707
9320
|
|
|
@@ -8755,7 +9368,7 @@ class UICheckbox extends UIObject
|
|
|
8755
9368
|
const textSize = this.getTextSize();
|
|
8756
9369
|
const pos = this.pos.add(vec2(this.size.x,0));
|
|
8757
9370
|
uiSystem.drawText(this.text, pos, textSize,
|
|
8758
|
-
this.textColor,
|
|
9371
|
+
this.textColor, this.textLineWidth, this.textLineColor, 'left', this.font, this.fontStyle, false, this.textShadow);
|
|
8759
9372
|
}
|
|
8760
9373
|
}
|
|
8761
9374
|
|
|
@@ -8797,7 +9410,11 @@ class UIScrollbar extends UIObject
|
|
|
8797
9410
|
update()
|
|
8798
9411
|
{
|
|
8799
9412
|
super.update();
|
|
8800
|
-
if (this.
|
|
9413
|
+
if (!this.interactive)
|
|
9414
|
+
return;
|
|
9415
|
+
|
|
9416
|
+
const oldValue = this.value;
|
|
9417
|
+
if (this.isActiveObject())
|
|
8801
9418
|
{
|
|
8802
9419
|
// handle horizontal or vertical scrollbar
|
|
8803
9420
|
const isHorizontal = this.size.x > this.size.y;
|
|
@@ -8809,14 +9426,19 @@ class UIScrollbar extends UIObject
|
|
|
8809
9426
|
const handleWidth = barSize - handleSize;
|
|
8810
9427
|
const p1 = centerPos - handleWidth/2;
|
|
8811
9428
|
const p2 = centerPos + handleWidth/2;
|
|
8812
|
-
const oldValue = this.value;
|
|
8813
|
-
|
|
8814
9429
|
const p = uiSystem.screenToNative(mousePosScreen);
|
|
8815
9430
|
this.value = isHorizontal ?
|
|
8816
9431
|
percent(p.x, p1, p2) :
|
|
8817
9432
|
percent(p.y, p2, p1);
|
|
8818
|
-
this.value === oldValue || this.onChange();
|
|
8819
9433
|
}
|
|
9434
|
+
else if (this.isNavigationObject())
|
|
9435
|
+
{
|
|
9436
|
+
// gamepad/keyboard navigation adjustment
|
|
9437
|
+
const direction = uiSystem.getNavigationOtherDirection();
|
|
9438
|
+
if (!uiSystem.navigationTimer.active())
|
|
9439
|
+
this.value = clamp(this.value + direction*.01);
|
|
9440
|
+
}
|
|
9441
|
+
this.value === oldValue || this.onChange();
|
|
8820
9442
|
}
|
|
8821
9443
|
render()
|
|
8822
9444
|
{
|
|
@@ -8841,7 +9463,14 @@ class UIScrollbar extends UIObject
|
|
|
8841
9463
|
// draw the text scaled to fit on the scrollbar
|
|
8842
9464
|
const textSize = this.getTextSize();
|
|
8843
9465
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
8844
|
-
this.textColor,
|
|
9466
|
+
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
9467
|
+
}
|
|
9468
|
+
navigatePressed()
|
|
9469
|
+
{
|
|
9470
|
+
// toggle value between 0 and 1
|
|
9471
|
+
this.value = this.value ? 0 : 1;
|
|
9472
|
+
this.onRelease();
|
|
9473
|
+
super.navigatePressed();
|
|
8845
9474
|
}
|
|
8846
9475
|
}
|
|
8847
9476
|
|
|
@@ -8875,7 +9504,7 @@ class UIVideo extends UIObject
|
|
|
8875
9504
|
this.color = BLACK; // default to black background
|
|
8876
9505
|
this.cornerRadius = 0; // default to no corner radius
|
|
8877
9506
|
|
|
8878
|
-
/** @property {
|
|
9507
|
+
/** @property {number} - The video volume */
|
|
8879
9508
|
this.volume = volume;
|
|
8880
9509
|
|
|
8881
9510
|
// create video element
|
|
@@ -8908,7 +9537,7 @@ class UIVideo extends UIObject
|
|
|
8908
9537
|
|
|
8909
9538
|
/** Check if video is currently loading
|
|
8910
9539
|
* @return {boolean} */
|
|
8911
|
-
|
|
9540
|
+
isLoading()
|
|
8912
9541
|
{ return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
|
|
8913
9542
|
|
|
8914
9543
|
/** Check if video is currently paused
|
|
@@ -8918,7 +9547,7 @@ class UIVideo extends UIObject
|
|
|
8918
9547
|
/** Check if video is currently playing
|
|
8919
9548
|
* @return {boolean} */
|
|
8920
9549
|
isPlaying()
|
|
8921
|
-
{ return !this.isPaused() && !this.hasEnded() && !this.
|
|
9550
|
+
{ return !this.isPaused() && !this.hasEnded() && !this.isLoading(); }
|
|
8922
9551
|
|
|
8923
9552
|
/** Check if video has ended playing
|
|
8924
9553
|
* @return {boolean} */
|
|
@@ -8967,7 +9596,7 @@ class UIVideo extends UIObject
|
|
|
8967
9596
|
{
|
|
8968
9597
|
super.render();
|
|
8969
9598
|
|
|
8970
|
-
if (this.
|
|
9599
|
+
if (this.isLoading())
|
|
8971
9600
|
return;
|
|
8972
9601
|
const context = uiSystem.uiContext;
|
|
8973
9602
|
const s = this.size;
|
|
@@ -10751,7 +11380,7 @@ class Box2dPlugin
|
|
|
10751
11380
|
* @param {Vector2} v */
|
|
10752
11381
|
vec2dTo(v)
|
|
10753
11382
|
{
|
|
10754
|
-
ASSERT(v
|
|
11383
|
+
ASSERT(isVector2(v));
|
|
10755
11384
|
return new box2d.instance.b2Vec2(v.x, v.y);
|
|
10756
11385
|
}
|
|
10757
11386
|
|