@gg-web-engine/mobile-controls 0.0.0-stage → 0.0.78
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/README.md +151 -2
- package/dist/controls/touch-axis-control.d.ts +38 -0
- package/dist/controls/touch-axis-control.js +91 -0
- package/dist/controls/touch-button.d.ts +49 -0
- package/dist/controls/touch-button.js +111 -0
- package/dist/controls/touch-control.d.ts +76 -0
- package/dist/controls/touch-control.js +151 -0
- package/dist/controls/touch-dpad.d.ts +27 -0
- package/dist/controls/touch-dpad.js +61 -0
- package/dist/controls/touch-look-area.d.ts +35 -0
- package/dist/controls/touch-look-area.js +61 -0
- package/dist/controls/touch-stick.d.ts +37 -0
- package/dist/controls/touch-stick.js +71 -0
- package/dist/icons.d.ts +19 -0
- package/dist/icons.js +20 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/inputs/tilt.input.d.ts +40 -0
- package/dist/inputs/tilt.input.js +106 -0
- package/dist/layouts/car.layout.d.ts +31 -0
- package/dist/layouts/car.layout.js +99 -0
- package/dist/layouts/character-2d.layout.d.ts +15 -0
- package/dist/layouts/character-2d.layout.js +60 -0
- package/dist/layouts/character.layout.d.ts +25 -0
- package/dist/layouts/character.layout.js +87 -0
- package/dist/layouts/free-camera.layout.d.ts +16 -0
- package/dist/layouts/free-camera.layout.js +73 -0
- package/dist/mobile-controls-layout.d.ts +54 -0
- package/dist/mobile-controls-layout.js +47 -0
- package/dist/mobile-controls.entity.d.ts +112 -0
- package/dist/mobile-controls.entity.js +274 -0
- package/dist/styles.d.ts +16 -0
- package/dist/styles.js +158 -0
- package/package.json +68 -4
- package/src/controls/touch-axis-control.ts +105 -0
- package/src/controls/touch-button.ts +136 -0
- package/src/controls/touch-control.ts +204 -0
- package/src/controls/touch-dpad.ts +78 -0
- package/src/controls/touch-look-area.ts +78 -0
- package/src/controls/touch-stick.ts +100 -0
- package/src/icons.ts +23 -0
- package/src/index.ts +15 -0
- package/src/inputs/tilt.input.ts +128 -0
- package/src/layouts/car.layout.ts +139 -0
- package/src/layouts/character-2d.layout.ts +85 -0
- package/src/layouts/character.layout.ts +125 -0
- package/src/layouts/free-camera.layout.ts +103 -0
- package/src/mobile-controls-layout.ts +86 -0
- package/src/mobile-controls.entity.ts +350 -0
- package/src/styles.ts +160 -0
- package/test/helpers.ts +38 -0
- package/test/mobile-controls.spec.ts +204 -0
- package/test/tilt.input.spec.ts +95 -0
- package/test/touch-axis-controls.spec.ts +172 -0
- package/test/touch-button.spec.ts +162 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TouchControlOptions } from './touch-control';
|
|
2
|
+
import { TouchAxisControl } from './touch-axis-control';
|
|
3
|
+
export type TouchDPadOptions = TouchControlOptions & {
|
|
4
|
+
/** Whether two neighboring directions can be held at once. `true` by default. */
|
|
5
|
+
diagonals?: boolean;
|
|
6
|
+
/** What each arm shows: markup or a DOM node. Arrows by default. */
|
|
7
|
+
content?: {
|
|
8
|
+
up?: string | Node;
|
|
9
|
+
down?: string | Node;
|
|
10
|
+
left?: string | Node;
|
|
11
|
+
right?: string | Node;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* An on-screen directional pad: four arms read as one control, so a thumb slides from one direction
|
|
16
|
+
* to another without lifting. Each axis of its value is -1, 0 or 1.
|
|
17
|
+
*/
|
|
18
|
+
export declare class TouchDPad extends TouchAxisControl {
|
|
19
|
+
readonly diagonals: boolean;
|
|
20
|
+
private readonly arms;
|
|
21
|
+
constructor(options?: TouchDPadOptions);
|
|
22
|
+
protected onPointerStart(event: PointerEvent): void;
|
|
23
|
+
protected onPointerMove(event: PointerEvent): void;
|
|
24
|
+
protected onPointerEnd(): void;
|
|
25
|
+
private update;
|
|
26
|
+
private apply;
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Pnt2 } from '@gg-web-engine/core';
|
|
2
|
+
import { TouchAxisControl } from './touch-axis-control';
|
|
3
|
+
import { MobileControlsIcons } from '../icons';
|
|
4
|
+
const ARMS = ['up', 'down', 'left', 'right'];
|
|
5
|
+
/**
|
|
6
|
+
* An on-screen directional pad: four arms read as one control, so a thumb slides from one direction
|
|
7
|
+
* to another without lifting. Each axis of its value is -1, 0 or 1.
|
|
8
|
+
*/
|
|
9
|
+
export class TouchDPad extends TouchAxisControl {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
var _a, _b, _c;
|
|
12
|
+
super('dpad', options);
|
|
13
|
+
this.diagonals = (_a = options.diagonals) !== null && _a !== void 0 ? _a : true;
|
|
14
|
+
this.arms = {};
|
|
15
|
+
for (const arm of ARMS) {
|
|
16
|
+
const element = document.createElement('div');
|
|
17
|
+
element.className = `gg-mc-dpad-arm gg-mc-dpad-arm--${arm}`;
|
|
18
|
+
const content = (_c = (_b = options.content) === null || _b === void 0 ? void 0 : _b[arm]) !== null && _c !== void 0 ? _c : MobileControlsIcons[arm];
|
|
19
|
+
if (typeof content === 'string') {
|
|
20
|
+
element.innerHTML = content;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
element.appendChild(content);
|
|
24
|
+
}
|
|
25
|
+
this.arms[arm] = element;
|
|
26
|
+
this.element.appendChild(element);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
onPointerStart(event) {
|
|
30
|
+
this.update(event);
|
|
31
|
+
}
|
|
32
|
+
onPointerMove(event) {
|
|
33
|
+
this.update(event);
|
|
34
|
+
}
|
|
35
|
+
onPointerEnd() {
|
|
36
|
+
this.apply(0, 0);
|
|
37
|
+
}
|
|
38
|
+
update(event) {
|
|
39
|
+
const rect = this.element.getBoundingClientRect();
|
|
40
|
+
const radius = rect.width / 2 || 50;
|
|
41
|
+
const offset = {
|
|
42
|
+
x: (event.clientX - (rect.left + rect.width / 2)) / radius,
|
|
43
|
+
y: (event.clientY - (rect.top + rect.height / 2)) / radius,
|
|
44
|
+
};
|
|
45
|
+
if (Pnt2.len(offset) < 0.18) {
|
|
46
|
+
this.apply(0, 0);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// split the circle into 8 sectors (4 without diagonals) and take the one the pointer is in
|
|
50
|
+
const sectors = this.diagonals ? 8 : 4;
|
|
51
|
+
const angle = (Math.round(Math.atan2(-offset.y, offset.x) / ((Math.PI * 2) / sectors)) * Math.PI * 2) / sectors;
|
|
52
|
+
this.apply(Math.round(Math.cos(angle)), Math.round(Math.sin(angle)));
|
|
53
|
+
}
|
|
54
|
+
apply(x, y) {
|
|
55
|
+
this.arms.up.classList.toggle('gg-mc-active', y > 0);
|
|
56
|
+
this.arms.down.classList.toggle('gg-mc-active', y < 0);
|
|
57
|
+
this.arms.left.classList.toggle('gg-mc-active', x < 0);
|
|
58
|
+
this.arms.right.classList.toggle('gg-mc-active', x > 0);
|
|
59
|
+
this.setValue({ x: x === 0 ? 0 : x, y: y === 0 ? 0 : y });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { MouseInput, Point2 } from '@gg-web-engine/core';
|
|
3
|
+
import { TouchControl, TouchControlOptions } from './touch-control';
|
|
4
|
+
export type TouchLookAreaOptions = TouchControlOptions & {
|
|
5
|
+
/** How far, in pixels, a touch may move and still count as a tap. 10 by default. */
|
|
6
|
+
tapDistance?: number;
|
|
7
|
+
/** How long, in milliseconds, a touch may last and still count as a tap. 250 by default. */
|
|
8
|
+
tapDuration?: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* An invisible area that reports how a finger drags across it - the touch counterpart of moving the
|
|
12
|
+
* mouse to look around. Fills the whole overlay unless placed; put it before the other controls, so
|
|
13
|
+
* they stay on top of it.
|
|
14
|
+
*/
|
|
15
|
+
export declare class TouchLookArea extends TouchControl {
|
|
16
|
+
private readonly _delta$;
|
|
17
|
+
private readonly _tap$;
|
|
18
|
+
private readonly tapDistance;
|
|
19
|
+
private readonly tapDuration;
|
|
20
|
+
private last;
|
|
21
|
+
private start;
|
|
22
|
+
/** The movement of the dragging finger since its previous report, in pixels. */
|
|
23
|
+
get delta$(): Observable<Point2>;
|
|
24
|
+
/** A short touch that did not turn into a drag, with its client position. */
|
|
25
|
+
get tap$(): Observable<Point2>;
|
|
26
|
+
constructor(options?: TouchLookAreaOptions);
|
|
27
|
+
/**
|
|
28
|
+
* Makes dragging over the area turn a view the way moving the mouse does: every drag is reported
|
|
29
|
+
* by `mouse` as a movement, multiplied by `sensitivity`.
|
|
30
|
+
*/
|
|
31
|
+
bindMouse(mouse: MouseInput, sensitivity?: number): this;
|
|
32
|
+
protected onPointerStart(event: PointerEvent): void;
|
|
33
|
+
protected onPointerMove(event: PointerEvent): void;
|
|
34
|
+
protected onPointerEnd(event: PointerEvent | null): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Subject, takeUntil } from 'rxjs';
|
|
2
|
+
import { TouchControl } from './touch-control';
|
|
3
|
+
/**
|
|
4
|
+
* An invisible area that reports how a finger drags across it - the touch counterpart of moving the
|
|
5
|
+
* mouse to look around. Fills the whole overlay unless placed; put it before the other controls, so
|
|
6
|
+
* they stay on top of it.
|
|
7
|
+
*/
|
|
8
|
+
export class TouchLookArea extends TouchControl {
|
|
9
|
+
/** The movement of the dragging finger since its previous report, in pixels. */
|
|
10
|
+
get delta$() {
|
|
11
|
+
return this._delta$.pipe(takeUntil(this.disposed$));
|
|
12
|
+
}
|
|
13
|
+
/** A short touch that did not turn into a drag, with its client position. */
|
|
14
|
+
get tap$() {
|
|
15
|
+
return this._tap$.pipe(takeUntil(this.disposed$));
|
|
16
|
+
}
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
var _a, _b;
|
|
19
|
+
super('look-area', options);
|
|
20
|
+
this._delta$ = new Subject();
|
|
21
|
+
this._tap$ = new Subject();
|
|
22
|
+
this.last = { x: 0, y: 0 };
|
|
23
|
+
this.start = null;
|
|
24
|
+
this.tapDistance = (_a = options.tapDistance) !== null && _a !== void 0 ? _a : 10;
|
|
25
|
+
this.tapDuration = (_b = options.tapDuration) !== null && _b !== void 0 ? _b : 250;
|
|
26
|
+
if (!options.placement) {
|
|
27
|
+
this.element.style.inset = '0';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Makes dragging over the area turn a view the way moving the mouse does: every drag is reported
|
|
32
|
+
* by `mouse` as a movement, multiplied by `sensitivity`.
|
|
33
|
+
*/
|
|
34
|
+
bindMouse(mouse, sensitivity = 1) {
|
|
35
|
+
this.delta$.subscribe(delta => mouse.emulateMove({ x: delta.x * sensitivity, y: delta.y * sensitivity }));
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
onPointerStart(event) {
|
|
39
|
+
this.last = { x: event.clientX, y: event.clientY };
|
|
40
|
+
this.start = { x: event.clientX, y: event.clientY, time: event.timeStamp, moved: false };
|
|
41
|
+
}
|
|
42
|
+
onPointerMove(event) {
|
|
43
|
+
const delta = { x: event.clientX - this.last.x, y: event.clientY - this.last.y };
|
|
44
|
+
this.last = { x: event.clientX, y: event.clientY };
|
|
45
|
+
if (this.start && Math.hypot(event.clientX - this.start.x, event.clientY - this.start.y) > this.tapDistance) {
|
|
46
|
+
this.start.moved = true;
|
|
47
|
+
}
|
|
48
|
+
if (delta.x !== 0 || delta.y !== 0) {
|
|
49
|
+
this._delta$.next(delta);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
onPointerEnd(event) {
|
|
53
|
+
const start = this.start;
|
|
54
|
+
this.start = null;
|
|
55
|
+
if (event && event.type === 'pointerup' && start && !start.moved) {
|
|
56
|
+
if (event.timeStamp - start.time <= this.tapDuration) {
|
|
57
|
+
this._tap$.next({ x: event.clientX, y: event.clientY });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { TouchControlOptions } from './touch-control';
|
|
2
|
+
import { TouchAxisControl } from './touch-axis-control';
|
|
3
|
+
export type TouchStickOptions = TouchControlOptions & {
|
|
4
|
+
/**
|
|
5
|
+
* `'fixed'` (default): the stick stays where it is placed. `'floating'`: the control is a touch
|
|
6
|
+
* zone (give it a `placement` with a size), and the stick appears under the finger wherever the
|
|
7
|
+
* zone is touched, so the thumb never has to find it.
|
|
8
|
+
*/
|
|
9
|
+
mode?: 'fixed' | 'floating';
|
|
10
|
+
/** Which axes the stick moves along. `'both'` by default. */
|
|
11
|
+
axes?: 'both' | 'x' | 'y';
|
|
12
|
+
/** The fraction of the travel around the center that reads as zero. 0.12 by default. */
|
|
13
|
+
deadzone?: number;
|
|
14
|
+
/**
|
|
15
|
+
* How far the knob travels from the center at full deflection, in pixels. By default 36% of the
|
|
16
|
+
* rendered width of the base.
|
|
17
|
+
*/
|
|
18
|
+
travel?: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* An on-screen analog stick.
|
|
22
|
+
*/
|
|
23
|
+
export declare class TouchStick extends TouchAxisControl {
|
|
24
|
+
readonly mode: 'fixed' | 'floating';
|
|
25
|
+
readonly axes: 'both' | 'x' | 'y';
|
|
26
|
+
readonly deadzone: number;
|
|
27
|
+
private readonly base;
|
|
28
|
+
private readonly knob;
|
|
29
|
+
private readonly travelOption;
|
|
30
|
+
private center;
|
|
31
|
+
private travel;
|
|
32
|
+
constructor(options?: TouchStickOptions);
|
|
33
|
+
protected onPointerStart(event: PointerEvent): void;
|
|
34
|
+
protected onPointerMove(event: PointerEvent): void;
|
|
35
|
+
protected onPointerEnd(): void;
|
|
36
|
+
private update;
|
|
37
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Pnt2 } from '@gg-web-engine/core';
|
|
2
|
+
import { TouchAxisControl } from './touch-axis-control';
|
|
3
|
+
/**
|
|
4
|
+
* An on-screen analog stick.
|
|
5
|
+
*/
|
|
6
|
+
export class TouchStick extends TouchAxisControl {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
var _a;
|
|
9
|
+
super('stick', options);
|
|
10
|
+
this.center = Pnt2.O;
|
|
11
|
+
this.travel = 1;
|
|
12
|
+
this.mode = options.mode || 'fixed';
|
|
13
|
+
this.axes = options.axes || 'both';
|
|
14
|
+
this.deadzone = (_a = options.deadzone) !== null && _a !== void 0 ? _a : 0.12;
|
|
15
|
+
this.travelOption = options.travel;
|
|
16
|
+
this.element.classList.add(`gg-mc-stick--${this.mode}`);
|
|
17
|
+
this.base = document.createElement('div');
|
|
18
|
+
this.base.className = 'gg-mc-stick-base';
|
|
19
|
+
this.knob = document.createElement('div');
|
|
20
|
+
this.knob.className = 'gg-mc-stick-knob';
|
|
21
|
+
this.base.appendChild(this.knob);
|
|
22
|
+
this.element.appendChild(this.base);
|
|
23
|
+
}
|
|
24
|
+
onPointerStart(event) {
|
|
25
|
+
var _a;
|
|
26
|
+
this.element.classList.add('gg-mc-active');
|
|
27
|
+
if (this.mode === 'floating') {
|
|
28
|
+
const zone = this.element.getBoundingClientRect();
|
|
29
|
+
this.base.style.left = `${event.clientX - zone.left}px`;
|
|
30
|
+
this.base.style.top = `${event.clientY - zone.top}px`;
|
|
31
|
+
this.center = { x: event.clientX, y: event.clientY };
|
|
32
|
+
}
|
|
33
|
+
const rect = this.base.getBoundingClientRect();
|
|
34
|
+
if (this.mode === 'fixed') {
|
|
35
|
+
this.center = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
36
|
+
}
|
|
37
|
+
this.travel = (_a = this.travelOption) !== null && _a !== void 0 ? _a : (rect.width * 0.36 || 50);
|
|
38
|
+
this.update(event);
|
|
39
|
+
}
|
|
40
|
+
onPointerMove(event) {
|
|
41
|
+
this.update(event);
|
|
42
|
+
}
|
|
43
|
+
onPointerEnd() {
|
|
44
|
+
this.element.classList.remove('gg-mc-active');
|
|
45
|
+
this.base.style.left = '';
|
|
46
|
+
this.base.style.top = '';
|
|
47
|
+
this.knob.style.transform = '';
|
|
48
|
+
this.setValue(Pnt2.O);
|
|
49
|
+
}
|
|
50
|
+
update(event) {
|
|
51
|
+
let offset = { x: (event.clientX - this.center.x) / this.travel, y: (event.clientY - this.center.y) / this.travel };
|
|
52
|
+
if (this.axes === 'x') {
|
|
53
|
+
offset.y = 0;
|
|
54
|
+
}
|
|
55
|
+
else if (this.axes === 'y') {
|
|
56
|
+
offset.x = 0;
|
|
57
|
+
}
|
|
58
|
+
const length = Pnt2.len(offset);
|
|
59
|
+
if (length > 1) {
|
|
60
|
+
offset = Pnt2.scalarMult(offset, 1 / length);
|
|
61
|
+
}
|
|
62
|
+
this.knob.style.transform = `translate(-50%, -50%) translate(${offset.x * this.travel}px, ${offset.y * this.travel}px)`;
|
|
63
|
+
if (length <= this.deadzone) {
|
|
64
|
+
this.setValue(Pnt2.O);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// rescaled so the value starts from 0 right outside the deadzone instead of jumping to it
|
|
68
|
+
const scale = (Math.min(1, length) - this.deadzone) / (1 - this.deadzone) / Math.min(1, length);
|
|
69
|
+
this.setValue({ x: offset.x * scale, y: offset.y === 0 ? 0 : -offset.y * scale });
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/icons.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in icons, as inline SVG markup drawn with `currentColor`. Pass any of them (or any other
|
|
3
|
+
* markup, or a DOM node) as a `TouchButton`'s `content`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MobileControlsIcons: {
|
|
6
|
+
left: string;
|
|
7
|
+
right: string;
|
|
8
|
+
up: string;
|
|
9
|
+
down: string;
|
|
10
|
+
accelerate: string;
|
|
11
|
+
brake: string;
|
|
12
|
+
handbrake: string;
|
|
13
|
+
plus: string;
|
|
14
|
+
minus: string;
|
|
15
|
+
jump: string;
|
|
16
|
+
crouch: string;
|
|
17
|
+
run: string;
|
|
18
|
+
view: string;
|
|
19
|
+
};
|
package/dist/icons.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const svg = (body) => `<svg viewBox="0 0 24 24" aria-hidden="true">${body}</svg>`;
|
|
2
|
+
/**
|
|
3
|
+
* The built-in icons, as inline SVG markup drawn with `currentColor`. Pass any of them (or any other
|
|
4
|
+
* markup, or a DOM node) as a `TouchButton`'s `content`.
|
|
5
|
+
*/
|
|
6
|
+
export const MobileControlsIcons = {
|
|
7
|
+
left: svg('<path d="M15 5l-7 7 7 7"/>'),
|
|
8
|
+
right: svg('<path d="M9 5l7 7-7 7"/>'),
|
|
9
|
+
up: svg('<path d="M5 15l7-7 7 7"/>'),
|
|
10
|
+
down: svg('<path d="M5 9l7 7 7-7"/>'),
|
|
11
|
+
accelerate: svg('<path d="M6 12l6-6 6 6M6 19l6-6 6 6"/>'),
|
|
12
|
+
brake: svg('<path d="M6 5l6 6 6-6M6 12l6 6 6-6"/>'),
|
|
13
|
+
handbrake: svg('<circle cx="12" cy="12" r="6"/><path d="M4.5 6a10 10 0 0 0 0 12M19.5 6a10 10 0 0 1 0 12M10.5 15V9h1.75a1.75 1.75 0 0 1 0 3.5H10.5"/>'),
|
|
14
|
+
plus: svg('<path d="M12 5v14M5 12h14"/>'),
|
|
15
|
+
minus: svg('<path d="M5 12h14"/>'),
|
|
16
|
+
jump: svg('<path d="M12 16V4M7 9l5-5 5 5M5 20h14"/>'),
|
|
17
|
+
crouch: svg('<path d="M12 4v12M7 11l5 5 5-5M5 20h14"/>'),
|
|
18
|
+
run: svg('<path d="M5 6l6 6-6 6M13 6l6 6-6 6"/>'),
|
|
19
|
+
view: svg('<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/>'),
|
|
20
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './controls/touch-control';
|
|
2
|
+
export * from './controls/touch-button';
|
|
3
|
+
export * from './controls/touch-axis-control';
|
|
4
|
+
export * from './controls/touch-stick';
|
|
5
|
+
export * from './controls/touch-dpad';
|
|
6
|
+
export * from './controls/touch-look-area';
|
|
7
|
+
export * from './inputs/tilt.input';
|
|
8
|
+
export * from './layouts/car.layout';
|
|
9
|
+
export * from './layouts/character.layout';
|
|
10
|
+
export * from './layouts/character-2d.layout';
|
|
11
|
+
export * from './layouts/free-camera.layout';
|
|
12
|
+
export * from './icons';
|
|
13
|
+
export * from './styles';
|
|
14
|
+
export * from './mobile-controls-layout';
|
|
15
|
+
export * from './mobile-controls.entity';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './controls/touch-control';
|
|
2
|
+
export * from './controls/touch-button';
|
|
3
|
+
export * from './controls/touch-axis-control';
|
|
4
|
+
export * from './controls/touch-stick';
|
|
5
|
+
export * from './controls/touch-dpad';
|
|
6
|
+
export * from './controls/touch-look-area';
|
|
7
|
+
export * from './inputs/tilt.input';
|
|
8
|
+
export * from './layouts/car.layout';
|
|
9
|
+
export * from './layouts/character.layout';
|
|
10
|
+
export * from './layouts/character-2d.layout';
|
|
11
|
+
export * from './layouts/free-camera.layout';
|
|
12
|
+
export * from './icons';
|
|
13
|
+
export * from './styles';
|
|
14
|
+
export * from './mobile-controls-layout';
|
|
15
|
+
export * from './mobile-controls.entity';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { IInput } from '@gg-web-engine/core';
|
|
3
|
+
export type TiltInputOptions = {
|
|
4
|
+
/** The tilt, in degrees, that reads as full deflection. 30 by default. */
|
|
5
|
+
maxAngle: number;
|
|
6
|
+
/** The tilt, in degrees, around level that reads as zero. 2 by default. */
|
|
7
|
+
deadzone: number;
|
|
8
|
+
/** Flips the sign of the output. `false` by default. */
|
|
9
|
+
invert: boolean;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* An input reading how the device is tilted sideways, like a steering wheel: `value$` goes from -1
|
|
13
|
+
* (the left edge of the screen is the lower one) to 1 (the right edge is), whichever way the screen
|
|
14
|
+
* is currently rotated and whether the device is held upright or lies flat.
|
|
15
|
+
*
|
|
16
|
+
* Orientation data is available to pages served over https (or from localhost) only; anywhere else
|
|
17
|
+
* `permission$` reports `'denied'`.
|
|
18
|
+
*
|
|
19
|
+
* Some browsers (iOS Safari) only hand out orientation data after the user allowed it in a prompt
|
|
20
|
+
* that has to be opened from a user gesture. `start()` takes care of that: when a permission is
|
|
21
|
+
* needed, it asks on the next tap anywhere on the page and starts reporting once granted.
|
|
22
|
+
* `permission$` tells how that went, for an app that wants to explain itself first or to fall back
|
|
23
|
+
* to another control when denied.
|
|
24
|
+
*/
|
|
25
|
+
export declare class TiltInput extends IInput {
|
|
26
|
+
/** Whether the browser reports device orientation at all. */
|
|
27
|
+
static isSupported(): boolean;
|
|
28
|
+
readonly options: TiltInputOptions;
|
|
29
|
+
private readonly _value$;
|
|
30
|
+
private readonly _permission$;
|
|
31
|
+
/** Emits the current value on subscription and then every change. */
|
|
32
|
+
get value$(): Observable<number>;
|
|
33
|
+
get value(): number;
|
|
34
|
+
get permission$(): Observable<'unknown' | 'granted' | 'denied'>;
|
|
35
|
+
constructor(options?: Partial<TiltInputOptions>);
|
|
36
|
+
protected startInternal(): void;
|
|
37
|
+
protected stopInternal(): void;
|
|
38
|
+
private requestPermission;
|
|
39
|
+
private handleOrientation;
|
|
40
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { BehaviorSubject } from 'rxjs';
|
|
2
|
+
import { distinctUntilChanged } from 'rxjs/operators';
|
|
3
|
+
import { IInput } from '@gg-web-engine/core';
|
|
4
|
+
const DEFAULT_OPTIONS = { maxAngle: 30, deadzone: 2, invert: false };
|
|
5
|
+
/**
|
|
6
|
+
* An input reading how the device is tilted sideways, like a steering wheel: `value$` goes from -1
|
|
7
|
+
* (the left edge of the screen is the lower one) to 1 (the right edge is), whichever way the screen
|
|
8
|
+
* is currently rotated and whether the device is held upright or lies flat.
|
|
9
|
+
*
|
|
10
|
+
* Orientation data is available to pages served over https (or from localhost) only; anywhere else
|
|
11
|
+
* `permission$` reports `'denied'`.
|
|
12
|
+
*
|
|
13
|
+
* Some browsers (iOS Safari) only hand out orientation data after the user allowed it in a prompt
|
|
14
|
+
* that has to be opened from a user gesture. `start()` takes care of that: when a permission is
|
|
15
|
+
* needed, it asks on the next tap anywhere on the page and starts reporting once granted.
|
|
16
|
+
* `permission$` tells how that went, for an app that wants to explain itself first or to fall back
|
|
17
|
+
* to another control when denied.
|
|
18
|
+
*/
|
|
19
|
+
export class TiltInput extends IInput {
|
|
20
|
+
/** Whether the browser reports device orientation at all. */
|
|
21
|
+
static isSupported() {
|
|
22
|
+
return typeof window !== 'undefined' && 'DeviceOrientationEvent' in window;
|
|
23
|
+
}
|
|
24
|
+
/** Emits the current value on subscription and then every change. */
|
|
25
|
+
get value$() {
|
|
26
|
+
return this._value$.pipe(distinctUntilChanged());
|
|
27
|
+
}
|
|
28
|
+
get value() {
|
|
29
|
+
return this._value$.getValue();
|
|
30
|
+
}
|
|
31
|
+
get permission$() {
|
|
32
|
+
return this._permission$.pipe(distinctUntilChanged());
|
|
33
|
+
}
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
super();
|
|
36
|
+
this._value$ = new BehaviorSubject(0);
|
|
37
|
+
this._permission$ = new BehaviorSubject('unknown');
|
|
38
|
+
this.options = Object.assign(Object.assign({}, DEFAULT_OPTIONS), options);
|
|
39
|
+
this.handleOrientation = this.handleOrientation.bind(this);
|
|
40
|
+
this.requestPermission = this.requestPermission.bind(this);
|
|
41
|
+
}
|
|
42
|
+
startInternal() {
|
|
43
|
+
if (!TiltInput.isSupported()) {
|
|
44
|
+
this._permission$.next('denied');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (window.isSecureContext === false) {
|
|
48
|
+
// browsers hand out orientation data to pages served over https (or from localhost) only, and
|
|
49
|
+
// on other pages stay silent instead of failing
|
|
50
|
+
console.warn('[TiltInput] device orientation is not available: the page is not served over https');
|
|
51
|
+
this._permission$.next('denied');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const needsPermission = typeof DeviceOrientationEvent.requestPermission === 'function';
|
|
55
|
+
if (needsPermission && this._permission$.getValue() !== 'granted') {
|
|
56
|
+
// the prompt only opens from a handler of a finished gesture, not from a touch going down
|
|
57
|
+
window.addEventListener('touchend', this.requestPermission);
|
|
58
|
+
window.addEventListener('click', this.requestPermission);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this._permission$.next('granted');
|
|
62
|
+
window.addEventListener('deviceorientation', this.handleOrientation);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
stopInternal() {
|
|
66
|
+
window.removeEventListener('touchend', this.requestPermission);
|
|
67
|
+
window.removeEventListener('click', this.requestPermission);
|
|
68
|
+
window.removeEventListener('deviceorientation', this.handleOrientation);
|
|
69
|
+
this._value$.next(0);
|
|
70
|
+
}
|
|
71
|
+
requestPermission() {
|
|
72
|
+
window.removeEventListener('touchend', this.requestPermission);
|
|
73
|
+
window.removeEventListener('click', this.requestPermission);
|
|
74
|
+
DeviceOrientationEvent
|
|
75
|
+
.requestPermission()
|
|
76
|
+
.then((state) => {
|
|
77
|
+
this._permission$.next(state === 'granted' ? 'granted' : 'denied');
|
|
78
|
+
if (state === 'granted' && this.running) {
|
|
79
|
+
window.addEventListener('deviceorientation', this.handleOrientation);
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
.catch(() => this._permission$.next('denied'));
|
|
83
|
+
}
|
|
84
|
+
handleOrientation(event) {
|
|
85
|
+
var _a, _b, _c;
|
|
86
|
+
if (event.beta === null || event.gamma === null) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const beta = (event.beta * Math.PI) / 180;
|
|
90
|
+
const gamma = (event.gamma * Math.PI) / 180;
|
|
91
|
+
// the direction of gravity within the plane of the screen, in the device's own (portrait) axes:
|
|
92
|
+
// x towards its right edge, y towards its top edge
|
|
93
|
+
const gravityX = Math.cos(beta) * Math.sin(gamma);
|
|
94
|
+
const gravityY = -Math.sin(beta);
|
|
95
|
+
// how much of it points towards the right edge of the screen as currently rotated
|
|
96
|
+
const screenAngle = (((_c = (_b = (_a = screen.orientation) === null || _a === void 0 ? void 0 : _a.angle) !== null && _b !== void 0 ? _b : window.orientation) !== null && _c !== void 0 ? _c : 0) * Math.PI) / 180;
|
|
97
|
+
const right = gravityX * Math.cos(screenAngle) - gravityY * Math.sin(screenAngle);
|
|
98
|
+
const degrees = (Math.asin(Math.max(-1, Math.min(1, right))) * 180) / Math.PI;
|
|
99
|
+
const { maxAngle, deadzone, invert } = this.options;
|
|
100
|
+
let value = 0;
|
|
101
|
+
if (Math.abs(degrees) > deadzone) {
|
|
102
|
+
value = Math.sign(degrees) * Math.min(1, (Math.abs(degrees) - deadzone) / (maxAngle - deadzone));
|
|
103
|
+
}
|
|
104
|
+
this._value$.next(invert && value !== 0 ? -value : value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CarHandlingController, GgCarHandlingController } from '@gg-web-engine/core';
|
|
2
|
+
import { TiltInputOptions } from '../inputs/tilt.input';
|
|
3
|
+
import { LayoutCustomization, MobileControlsLayoutFactory } from '../mobile-controls-layout';
|
|
4
|
+
export type CarLayoutControlId = 'steer-left' | 'steer-right' | 'steer-stick' | 'accelerate' | 'brake' | 'handbrake' | 'gear-up' | 'gear-down';
|
|
5
|
+
/** The controllers `carLayout` builds controls for. */
|
|
6
|
+
export type CarLayoutController = GgCarHandlingController | CarHandlingController;
|
|
7
|
+
export type CarLayoutOptions = LayoutCustomization<CarLayoutController, CarLayoutControlId> & {
|
|
8
|
+
/**
|
|
9
|
+
* How the car is steered: `'buttons'` (default) - a left and a right button, `'stick'` - an analog
|
|
10
|
+
* stick moving sideways, `'tilt'` - by tilting the device like a steering wheel (see `TiltInput`).
|
|
11
|
+
*/
|
|
12
|
+
steering?: 'buttons' | 'stick' | 'tilt';
|
|
13
|
+
/** Options of the `TiltInput` behind `steering: 'tilt'`. */
|
|
14
|
+
tilt?: Partial<TiltInputOptions>;
|
|
15
|
+
/** Whether there is a handbrake button (`GgCarHandlingController` only). `true` by default. */
|
|
16
|
+
handbrake?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Whether there are gear up/down buttons (`GgCarHandlingController` only). `'auto'`
|
|
19
|
+
* (default) shows them when the driver has to
|
|
20
|
+
* shift: gear switching is enabled on the controller, and the car has a manual gearbox or the
|
|
21
|
+
* controller does not pick reverse by itself (`autoReverse`).
|
|
22
|
+
*/
|
|
23
|
+
gears?: boolean | 'auto';
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The built-in layout for the car controllers: steering on the left, pedals on the right. For a
|
|
27
|
+
* `GgCarHandlingController` also the handbrake and the gears; a bare
|
|
28
|
+
* `CarHandlingController` (the steering/throttle half, usable with any vehicle) gets the
|
|
29
|
+
* steering and the pedals alone.
|
|
30
|
+
*/
|
|
31
|
+
export declare function carLayout(options?: CarLayoutOptions): MobileControlsLayoutFactory<CarLayoutController>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { GgCarHandlingController } from '@gg-web-engine/core';
|
|
2
|
+
import { TouchButton } from '../controls/touch-button';
|
|
3
|
+
import { TouchStick } from '../controls/touch-stick';
|
|
4
|
+
import { MobileControlsIcons } from '../icons';
|
|
5
|
+
import { TiltInput } from '../inputs/tilt.input';
|
|
6
|
+
import { LayoutBuilder } from '../mobile-controls-layout';
|
|
7
|
+
/**
|
|
8
|
+
* The built-in layout for the car controllers: steering on the left, pedals on the right. For a
|
|
9
|
+
* `GgCarHandlingController` also the handbrake and the gears; a bare
|
|
10
|
+
* `CarHandlingController` (the steering/throttle half, usable with any vehicle) gets the
|
|
11
|
+
* steering and the pedals alone.
|
|
12
|
+
*/
|
|
13
|
+
export function carLayout(options = {}) {
|
|
14
|
+
return (controller, context) => {
|
|
15
|
+
var _a, _b, _c;
|
|
16
|
+
if (controller.parent instanceof GgCarHandlingController) {
|
|
17
|
+
// the half of a `GgCarHandlingController`, whose own layout already covers it
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const b = new LayoutBuilder(options);
|
|
21
|
+
const ggCar = controller instanceof GgCarHandlingController ? controller : null;
|
|
22
|
+
const direction = (ggCar ? ggCar.carHandlingInput : controller).directionsInput;
|
|
23
|
+
const steering = options.steering || 'buttons';
|
|
24
|
+
if (steering === 'buttons') {
|
|
25
|
+
b.add('steer-left', () => new TouchButton({
|
|
26
|
+
id: 'steer-left',
|
|
27
|
+
label: 'Steer left',
|
|
28
|
+
content: b.icon('steer-left', MobileControlsIcons.left),
|
|
29
|
+
placement: b.placement('steer-left', { left: 4, bottom: 4, width: 10, height: 10 }),
|
|
30
|
+
}).bindDirection(direction, { x: -1 }));
|
|
31
|
+
b.add('steer-right', () => new TouchButton({
|
|
32
|
+
id: 'steer-right',
|
|
33
|
+
label: 'Steer right',
|
|
34
|
+
content: b.icon('steer-right', MobileControlsIcons.right),
|
|
35
|
+
placement: b.placement('steer-right', { left: 16, bottom: 4, width: 10, height: 10 }),
|
|
36
|
+
}).bindDirection(direction, { x: 1 }));
|
|
37
|
+
}
|
|
38
|
+
else if (steering === 'stick') {
|
|
39
|
+
b.add('steer-stick', () => new TouchStick({
|
|
40
|
+
id: 'steer-stick',
|
|
41
|
+
label: 'Steering',
|
|
42
|
+
axes: 'x',
|
|
43
|
+
placement: b.placement('steer-stick', { left: 4, bottom: 3 }),
|
|
44
|
+
}).bindDirection(direction));
|
|
45
|
+
}
|
|
46
|
+
b.add('accelerate', () => new TouchButton({
|
|
47
|
+
id: 'accelerate',
|
|
48
|
+
label: 'Accelerate',
|
|
49
|
+
content: b.icon('accelerate', MobileControlsIcons.accelerate),
|
|
50
|
+
placement: b.placement('accelerate', { right: 4, bottom: 4, width: 10, height: 10 }),
|
|
51
|
+
}).bindDirection(direction, { y: 1 }));
|
|
52
|
+
b.add('brake', () => new TouchButton({
|
|
53
|
+
id: 'brake',
|
|
54
|
+
label: 'Brake',
|
|
55
|
+
content: b.icon('brake', MobileControlsIcons.brake),
|
|
56
|
+
placement: b.placement('brake', { right: 16, bottom: 4, width: 10, height: 10 }),
|
|
57
|
+
}).bindDirection(direction, { y: -1 }));
|
|
58
|
+
if (ggCar && ((_a = options.handbrake) !== null && _a !== void 0 ? _a : true)) {
|
|
59
|
+
b.add('handbrake', () => new TouchButton({
|
|
60
|
+
id: 'handbrake',
|
|
61
|
+
label: 'Handbrake',
|
|
62
|
+
content: b.icon('handbrake', MobileControlsIcons.handbrake),
|
|
63
|
+
placement: b.placement('handbrake', { right: 5.5, bottom: 16, width: 7, height: 7 }),
|
|
64
|
+
}).bindKey(ggCar.keyboard, ggCar.options.handbrakeKey));
|
|
65
|
+
}
|
|
66
|
+
let gears = (_b = options.gears) !== null && _b !== void 0 ? _b : 'auto';
|
|
67
|
+
if (ggCar && gears === 'auto') {
|
|
68
|
+
const isAutoGearbox = !!((_c = ggCar.car) === null || _c === void 0 ? void 0 : _c.carProperties.transmission.isAuto);
|
|
69
|
+
gears = ggCar.switchingGearsEnabled && !(isAutoGearbox && ggCar.options.autoReverse);
|
|
70
|
+
}
|
|
71
|
+
if (ggCar && gears) {
|
|
72
|
+
b.add('gear-down', () => new TouchButton({
|
|
73
|
+
id: 'gear-down',
|
|
74
|
+
label: 'Gear down',
|
|
75
|
+
content: b.icon('gear-down', MobileControlsIcons.minus),
|
|
76
|
+
placement: b.placement('gear-down', { left: 5.5, bottom: 16, width: 7, height: 7 }),
|
|
77
|
+
}).bindKey(ggCar.keyboard, ggCar.options.gearUpDownKeys[1]));
|
|
78
|
+
b.add('gear-up', () => new TouchButton({
|
|
79
|
+
id: 'gear-up',
|
|
80
|
+
label: 'Gear up',
|
|
81
|
+
content: b.icon('gear-up', MobileControlsIcons.plus),
|
|
82
|
+
placement: b.placement('gear-up', { left: 17.5, bottom: 16, width: 7, height: 7 }),
|
|
83
|
+
}).bindKey(ggCar.keyboard, ggCar.options.gearUpDownKeys[0]));
|
|
84
|
+
}
|
|
85
|
+
if (steering === 'tilt') {
|
|
86
|
+
const tilt = new TiltInput(options.tilt);
|
|
87
|
+
const subscription = tilt.value$.subscribe(x => direction.setAnalogDirection(tilt, x === 0 ? null : { x }));
|
|
88
|
+
tilt.start();
|
|
89
|
+
b.result.push({
|
|
90
|
+
dispose: () => {
|
|
91
|
+
subscription.unsubscribe();
|
|
92
|
+
direction.setAnalogDirection(tilt, null);
|
|
93
|
+
tilt.stop();
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return b.finish(controller, context);
|
|
98
|
+
};
|
|
99
|
+
}
|