playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
527 lines (526 loc) • 13.4 kB
JavaScript
import { EventHandler } from "../../core/event-handler.js";
import { PAD_FACE_1, PAD_FACE_2, PAD_FACE_3, PAD_FACE_4, PAD_L_SHOULDER_1, PAD_R_SHOULDER_1, PAD_L_SHOULDER_2, PAD_R_SHOULDER_2, PAD_SELECT, PAD_START, PAD_L_STICK_BUTTON, PAD_R_STICK_BUTTON, PAD_UP, PAD_DOWN, PAD_LEFT, PAD_RIGHT, PAD_VENDOR, XRPAD_TRIGGER, XRPAD_SQUEEZE, XRPAD_TOUCHPAD_BUTTON, XRPAD_STICK_BUTTON, XRPAD_A, XRPAD_B, PAD_L_STICK_X, PAD_L_STICK_Y, PAD_R_STICK_X, PAD_R_STICK_Y, XRPAD_TOUCHPAD_X, XRPAD_TOUCHPAD_Y, XRPAD_STICK_X, XRPAD_STICK_Y } from "./constants.js";
import { math } from "../../core/math/math.js";
import { platform } from "../../core/platform.js";
const dummyArray = Object.freeze([]);
let getGamepads = function() {
return dummyArray;
};
if (typeof navigator !== "undefined") {
getGamepads = (navigator.getGamepads || navigator.webkitGetGamepads || getGamepads).bind(navigator);
}
const MAPS_INDEXES = {
buttons: {
PAD_FACE_1,
PAD_FACE_2,
PAD_FACE_3,
PAD_FACE_4,
PAD_L_SHOULDER_1,
PAD_R_SHOULDER_1,
PAD_L_SHOULDER_2,
PAD_R_SHOULDER_2,
PAD_SELECT,
PAD_START,
PAD_L_STICK_BUTTON,
PAD_R_STICK_BUTTON,
PAD_UP,
PAD_DOWN,
PAD_LEFT,
PAD_RIGHT,
PAD_VENDOR,
XRPAD_TRIGGER,
XRPAD_SQUEEZE,
XRPAD_TOUCHPAD_BUTTON,
XRPAD_STICK_BUTTON,
XRPAD_A,
XRPAD_B
},
axes: {
PAD_L_STICK_X,
PAD_L_STICK_Y,
PAD_R_STICK_X,
PAD_R_STICK_Y,
XRPAD_TOUCHPAD_X,
XRPAD_TOUCHPAD_Y,
XRPAD_STICK_X,
XRPAD_STICK_Y
}
};
const MAPS = {
DEFAULT: {
buttons: [
// Face buttons
"PAD_FACE_1",
"PAD_FACE_2",
"PAD_FACE_3",
"PAD_FACE_4",
// Shoulder buttons
"PAD_L_SHOULDER_1",
"PAD_R_SHOULDER_1",
"PAD_L_SHOULDER_2",
"PAD_R_SHOULDER_2",
// Other buttons
"PAD_SELECT",
"PAD_START",
"PAD_L_STICK_BUTTON",
"PAD_R_STICK_BUTTON",
// D Pad
"PAD_UP",
"PAD_DOWN",
"PAD_LEFT",
"PAD_RIGHT",
// Vendor specific button
"PAD_VENDOR"
],
axes: [
// Analog Sticks
"PAD_L_STICK_X",
"PAD_L_STICK_Y",
"PAD_R_STICK_X",
"PAD_R_STICK_Y"
]
},
DEFAULT_DUAL: {
buttons: [
// Face buttons
"PAD_FACE_1",
"PAD_FACE_2",
"PAD_FACE_3",
"PAD_FACE_4",
// Shoulder buttons
"PAD_L_SHOULDER_1",
"PAD_R_SHOULDER_1",
"PAD_L_SHOULDER_2",
"PAD_R_SHOULDER_2",
// Other buttons
"PAD_SELECT",
"PAD_START",
"PAD_L_STICK_BUTTON",
"PAD_R_STICK_BUTTON",
// Vendor specific button
"PAD_VENDOR"
],
axes: [
// Analog Sticks
"PAD_L_STICK_X",
"PAD_L_STICK_Y",
"PAD_R_STICK_X",
"PAD_R_STICK_Y"
],
synthesizedButtons: {
PAD_UP: { axis: 0, min: 0, max: 1 },
PAD_DOWN: { axis: 0, min: -1, max: 0 },
PAD_LEFT: { axis: 0, min: -1, max: 0 },
PAD_RIGHT: { axis: 0, min: 0, max: 1 }
}
},
PS3: {
buttons: [
// X, O, TRI, SQ
"PAD_FACE_1",
"PAD_FACE_2",
"PAD_FACE_4",
"PAD_FACE_3",
// Shoulder buttons
"PAD_L_SHOULDER_1",
"PAD_R_SHOULDER_1",
"PAD_L_SHOULDER_2",
"PAD_R_SHOULDER_2",
// Other buttons
"PAD_SELECT",
"PAD_START",
"PAD_L_STICK_BUTTON",
"PAD_R_STICK_BUTTON",
// D Pad
"PAD_UP",
"PAD_DOWN",
"PAD_LEFT",
"PAD_RIGHT",
"PAD_VENDOR"
],
axes: [
// Analog Sticks
"PAD_L_STICK_X",
"PAD_L_STICK_Y",
"PAD_R_STICK_X",
"PAD_R_STICK_Y"
],
mapping: "standard"
},
DEFAULT_XR: {
buttons: [
// Back buttons
"XRPAD_TRIGGER",
"XRPAD_SQUEEZE",
// Axes buttons
"XRPAD_TOUCHPAD_BUTTON",
"XRPAD_STICK_BUTTON",
// Face buttons
"XRPAD_A",
"XRPAD_B"
],
axes: [
// Analog Sticks
"XRPAD_TOUCHPAD_X",
"XRPAD_TOUCHPAD_Y",
"XRPAD_STICK_X",
"XRPAD_STICK_Y"
],
mapping: "xr-standard"
}
};
const PRODUCT_CODES = {
"Product: 0268": "PS3"
};
const custom_maps = {};
function getMap(pad) {
const custom = custom_maps[pad.id];
if (custom) {
return custom;
}
for (const code in PRODUCT_CODES) {
if (pad.id.indexOf(code) !== -1) {
const product = PRODUCT_CODES[code];
if (!pad.mapping) {
const raw = MAPS[`RAW_${product}`];
if (raw) {
return raw;
}
}
return MAPS[product];
}
}
if (pad.mapping === "xr-standard") {
return MAPS.DEFAULT_XR;
}
const defaultmap = MAPS.DEFAULT;
const map = pad.buttons.length < defaultmap.buttons.length ? MAPS.DEFAULT_DUAL : defaultmap;
map.mapping = pad.mapping;
return map;
}
let deadZone = 0.25;
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
class GamePadButton {
value = 0;
pressed = false;
touched = false;
wasPressed = false;
wasReleased = false;
wasTouched = false;
constructor(current, previous) {
if (typeof current === "number") {
this.value = current;
this.pressed = current === 1;
this.touched = current > 0;
} else {
this.value = current.value;
this.pressed = current.pressed;
this.touched = current.touched ?? current.value > 0;
}
if (previous) {
if (typeof previous === "number") {
this.wasPressed = previous !== 1 && this.pressed;
this.wasReleased = previous === 1 && !this.pressed;
this.wasTouched = previous === 0 && this.touched;
} else {
this.wasPressed = !previous.pressed && this.pressed;
this.wasReleased = previous.pressed && !this.pressed;
this.wasTouched = !(previous.touched ?? previous.value > 0) && this.touched;
}
}
}
update(button) {
const { value, pressed } = button;
const touched = button.touched ?? value > 0;
this.wasPressed = !this.pressed && pressed;
this.wasReleased = this.pressed && !pressed;
this.wasTouched = !this.touched && touched;
this.value = value;
this.pressed = pressed;
this.touched = touched;
}
}
const dummyButton = Object.freeze(new GamePadButton(0));
class GamePad {
_compiledMapping = {
buttons: [],
axes: []
};
id;
index;
_buttons;
_axes;
_previousAxes;
mapping;
map;
hand;
pad;
constructor(gamepad, map) {
this.id = gamepad.id;
this.index = gamepad.index;
this._buttons = gamepad.buttons.map((b) => new GamePadButton(b));
this._axes = [...gamepad.axes];
this._previousAxes = [...gamepad.axes];
this.mapping = map.mapping;
this.map = map;
this.hand = gamepad.hand || "none";
this.pad = gamepad;
this._compileMapping();
}
get connected() {
return this.pad.connected;
}
_compileMapping() {
const { axes, buttons } = this._compiledMapping;
const axesIndexes = MAPS_INDEXES.axes;
const buttonsIndexes = MAPS_INDEXES.buttons;
axes.length = 0;
buttons.length = 0;
const axesMap = this.map.axes;
if (axesMap) {
this.map.axes.forEach((axis, i) => {
axes[axesIndexes[axis]] = () => this.pad.axes[i] || 0;
});
}
for (let i = 0, l = axes.length; i < l; i++) {
if (!axes[i]) {
axes[i] = () => 0;
}
}
const buttonsMap = this.map.buttons;
if (buttonsMap) {
buttonsMap.forEach((button, i) => {
buttons[buttonsIndexes[button]] = () => this._buttons[i] || dummyButton;
});
}
const synthesizedButtonsMap = this.map.synthesizedButtons;
if (synthesizedButtonsMap) {
Object.entries(synthesizedButtonsMap).forEach((button) => {
const { axis, max, min } = button[1];
buttons[buttonsIndexes[button[0]]] = () => new GamePadButton(
Math.abs(math.clamp(this._axes[axis] ?? 0, min, max)),
Math.abs(math.clamp(this._previousAxes[axis] ?? 0, min, max))
);
});
}
for (let i = 0, l = buttons.length; i < l; i++) {
if (!buttons[i]) {
buttons[i] = () => dummyButton;
}
}
}
update(gamepad) {
this.pad = gamepad;
const previousAxes = this._previousAxes;
const axes = this._axes;
previousAxes.length = 0;
previousAxes.push(...axes);
axes.length = 0;
axes.push(...gamepad.axes);
const buttons = this._buttons;
for (let i = 0, l = buttons.length; i < l; i++) {
buttons[i].update(gamepad.buttons[i]);
}
return this;
}
updateMap(map) {
map.mapping = "custom";
custom_maps[this.id] = map;
this.map = map;
this.mapping = "custom";
this._compileMapping();
}
resetMap() {
if (custom_maps[this.id]) {
delete custom_maps[this.id];
const map = getMap(this.pad);
this.map = map;
this.mapping = map.mapping;
this._compileMapping();
}
}
get axes() {
return this._compiledMapping.axes.map((a) => a());
}
get buttons() {
return this._compiledMapping.buttons.map((b) => b());
}
async pulse(intensity, duration, options) {
const actuators = this.pad.vibrationActuator ? [this.pad.vibrationActuator] : this.pad.hapticActuators || dummyArray;
if (actuators.length) {
const startDelay = options?.startDelay ?? 0;
const strongMagnitude = options?.strongMagnitude ?? intensity;
const weakMagnitude = options?.weakMagnitude ?? intensity;
const results = await Promise.all(
actuators.map(async (actuator) => {
if (!actuator) {
return true;
}
if (actuator.playEffect) {
return actuator.playEffect(actuator.type, {
duration,
startDelay,
strongMagnitude,
weakMagnitude
});
} else if (actuator.pulse) {
await sleep(startDelay);
return actuator.pulse(intensity, duration);
}
return false;
})
);
return results.some((r) => r === true || r === "complete");
}
return false;
}
getButton(index) {
const button = this._compiledMapping.buttons[index];
return button ? button() : dummyButton;
}
isPressed(button) {
return this.getButton(button).pressed;
}
wasPressed(button) {
return this.getButton(button).wasPressed;
}
wasReleased(button) {
return this.getButton(button).wasReleased;
}
isTouched(button) {
return this.getButton(button).touched;
}
wasTouched(button) {
return this.getButton(button).wasTouched;
}
getValue(button) {
return this.getButton(button).value;
}
getAxis(axis) {
const a = this.axes[axis];
return a && Math.abs(a) > deadZone ? a : 0;
}
}
class GamePads extends EventHandler {
static EVENT_GAMEPADCONNECTED = "gamepadconnected";
static EVENT_GAMEPADDISCONNECTED = "gamepaddisconnected";
gamepadsSupported;
current = [];
_previous = [];
_ongamepadconnectedHandler;
_ongamepaddisconnectedHandler;
constructor() {
super();
this.gamepadsSupported = platform.gamepads;
this._ongamepadconnectedHandler = this._ongamepadconnected.bind(this);
this._ongamepaddisconnectedHandler = this._ongamepaddisconnected.bind(this);
window.addEventListener("gamepadconnected", this._ongamepadconnectedHandler, false);
window.addEventListener("gamepaddisconnected", this._ongamepaddisconnectedHandler, false);
this.poll();
}
set deadZone(value) {
deadZone = value;
}
get deadZone() {
return deadZone;
}
get previous() {
const current = this.current;
for (let i = 0, l = current.length; i < l; i++) {
const buttons = current[i]._buttons;
if (!this._previous[i]) {
this._previous[i] = [];
}
for (let j = 0, m = buttons.length; j < m; j++) {
const button = buttons[i];
this.previous[i][j] = button ? !button.wasPressed && button.pressed || button.wasReleased : false;
}
}
this._previous.length = this.current.length;
return this._previous;
}
_ongamepadconnected(event) {
const pad = new GamePad(event.gamepad, this.getMap(event.gamepad));
const current = this.current;
let padIndex = current.findIndex((gp) => gp.index === pad.index);
while (padIndex !== -1) {
current.splice(padIndex, 1);
padIndex = current.findIndex((gp) => gp.index === pad.index);
}
current.push(pad);
this.fire("gamepadconnected", pad);
}
_ongamepaddisconnected(event) {
const current = this.current;
const padIndex = current.findIndex((gp) => gp.index === event.gamepad.index);
if (padIndex !== -1) {
this.fire("gamepaddisconnected", current[padIndex]);
current.splice(padIndex, 1);
}
}
update() {
this.poll();
}
poll(pads = []) {
if (pads.length > 0) {
pads.length = 0;
}
const padDevices = getGamepads();
for (let i = 0, len = padDevices.length; i < len; i++) {
if (padDevices[i]) {
const pad = this.findByIndex(padDevices[i].index);
if (pad) {
pads.push(pad.update(padDevices[i]));
} else {
const nPad = new GamePad(padDevices[i], this.getMap(padDevices[i]));
this.current.push(nPad);
pads.push(nPad);
}
}
}
return pads;
}
destroy() {
window.removeEventListener("gamepadconnected", this._ongamepadconnectedHandler, false);
window.removeEventListener("gamepaddisconnected", this._ongamepaddisconnectedHandler, false);
}
getMap(pad) {
return getMap(pad);
}
isPressed(orderIndex, button) {
return this.current[orderIndex]?.isPressed(button) || false;
}
wasPressed(orderIndex, button) {
return this.current[orderIndex]?.wasPressed(button) || false;
}
wasReleased(orderIndex, button) {
return this.current[orderIndex]?.wasReleased(button) || false;
}
getAxis(orderIndex, axis) {
return this.current[orderIndex]?.getAxis(axis) || 0;
}
pulse(orderIndex, intensity, duration, options) {
const pad = this.current[orderIndex];
return pad ? pad.pulse(intensity, duration, options) : Promise.resolve(false);
}
pulseAll(intensity, duration, options) {
return Promise.all(
this.current.map((pad) => pad.pulse(intensity, duration, options))
);
}
findById(id) {
return this.current.find((gp) => gp && gp.id === id) || null;
}
findByIndex(index) {
return this.current.find((gp) => gp && gp.index === index) || null;
}
}
export {
GamePad,
GamePadButton,
GamePads
};