elation-engine
Version:
WebGL/WebVR engine written in Javascript
1,364 lines (1,296 loc) • 54.7 kB
JavaScript
/**
* Context-aware input mapper
*
* Handles bindings and contexts for various types of inputs in a generic way.
* Works in two layers, first by mapping physical inputs (mouse, keyboard,
* gamepad, accelerometer, etc) to generalized event names (mouse_button_1,
* mouse_drag_x, keyboard_w, gamepad_0_axis_1, etc), and then mapping these
* to context-specific actions ("move_forward", "jump", "fire", etc).
*
* Contexts
* --------
* Contexts let you define groups of actions for different situations, which
* can be activated or deactivated programatically (eg, when a player enters
* or exits a vehicle). When activating contexts, an object can be passed which
* is then available as 'this' within the action callbacks.
*
* Commands
* --------
* Commands define a set of actions which an object exposes. These commands
* can be mapped to keyboard, mouse, or gamepad keys and axes using Bindings.
*
* Example:
*
* var myApplication = new SuperCoolGame();
* var controls = new elation.engine.system.controls();
*
* controls.addCommands("default", {
* "menu": function(ev) { this.showMenu(); },
* "screenshot": function(ev) { this.screenshot(); }
* });
* controls.addCommands("player", {
* "move_forward": function(ev) { this.move(0, 0, -ev.data); },
* "move_back": function(ev) { this.move(0, 0, ev.data); },
* "move_left": function(ev) { this.move(-ev.data, 0, 0); },
* "move_right": function(ev) { this.move(ev.data, 0, 0); },
* "pitch": function(ev) { this.pitch(ev.data); }
* "turn": function(ev) { this.turn(ev.data); }
* "jump": function(ev) { this.jump(ev.data); }
* });
* controls.activateContext("default", myApplication);
* controls.activateContext("player", myApplication.player);
*
*
*
* Bindings
* --------
* Bindings define virtual identifiers for all input events, which can then
* be mapped to context-specific Commands. In most cases, ev.data contains
* a floating point value from 0..1 for buttons or -1..1 for axes
*
* - keyboard: keyboard_<#letter>, keyboard_space, keyboard_delete, etc.
* - mouse: mouse_pos mouse_x mouse_y mouse_drag_x mouse_button_<#button>
* - gamepad: gamepad_<#stick>_axis_<#axis>
*
* Example:
*
* controls.addBindings("default", {
* "keyboard_esc": "menu",
* "gamepad_0_button_10": "menu" // first gamepad start button
* };
* controls.addBindings("player", {
* "keyboard_w": "move_forward",
* "keyboard_a": "move_left",
* "keyboard_s": "move_back",
* "keyboard_d": "move_right",
* "mouse_x": "turn",
* "mouse_y": "pitch",
* "gamepad_0_axis_0": "move_right",
* "gamepad_0_axis_1": "move_forward",
* "gamepad_0_axis_2": "turn",
* "gamepad_0_axis_3": "pitch"
* "gamepad_0_button_1": "jump"
* });
**/
elation.requireCSS('engine.systems.controls');
elation.require(['ui.window', 'ui.panel', 'ui.toggle', 'ui.slider', 'ui.label', 'ui.list', 'ui.tabbedcontent'], function() {
elation.extend("engine.systems.controls", function(args) {
elation.implement(this, elation.engine.systems.system);
this.contexts = {};
this.activecontexts = [];
this.bindings = {};
this.state = {};
this.contexttargets = {};
this.contextstates = {};
this.changes = [];
this.gamepads = [];
this.virtualgamepads = [];
this.viewport = [];
this.hmdframes = [];
this.firstclick = true;
this.mousedowntime = 0;
this.settings = {
mouse: {
sensitivity: 100,
invertY: false,
invertX: false
},
keyboard: {
turnspeed: 1,
lookspeed: 1
},
gamepad: {
sensitivity: 1,
deadzone: 0.2
},
touchpad: {
emulateGamepad: false
},
hmd: {
},
leapmotion: {
enabled: false,
mount: 'VR'
}
};
this.capturekeys = [
'keyboard_f1',
'keyboard_f6',
//'keyboard_tab',
];
this.initialized = false;
// FIXME - this is a temporary workaround for a Windows/Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=781182
// It could however be extended as a feature, to provide configuurable mouse input curves (filtering + acceleration)
this.mousesmooth = new function() {
this.x = 0;
this.y = 0;
this.filtermax = 300;
this.filtermin = 80;
this.filtermult = 5;
this.history = {
x: [],
y: []
};
this.idx = 0;
this.frames = 1;
this.add = function(x, y) {
this.history.x[this.idx] = x;
this.history.y[this.idx] = y;
this.removeOutlier();
this.idx = (this.idx + 1) % this.frames;
//this.average();
}
this.average = function() {
var x = 0,
y = 0;
var len = this.history.x.length;
for (var i = 0; i < len; i++) {
x += this.history.x[i];
y += this.history.y[i];
}
this.x = x / len;
this.y = y / len;
}
this.removeOutlier = function() {
var idx1 = this.idx,
idx2 = (this.idx + this.frames - 1) % this.frames,
idx3 = (this.idx + this.frames - 2) % this.frames;
var x1 = this.history.x[idx1],
y1 = this.history.y[idx1],
x2 = this.history.x[idx2],
y2 = this.history.y[idx2],
x3 = this.history.x[idx3],
y3 = this.history.y[idx3];
// Compare the last 3 values. If our middle value is a big spike, throw it out! Otherwise, we'll use the middle value as our current value
// This introduces one frame of mouse latency, but works around a Windows Creator's Update bug with Chrome
if (Math.abs(x2) > this.filtermin && Math.abs(x2 / x1) > this.filtermult && Math.abs(x2 / x3) > this.filtermult) {
this.x = x1;
this.y = y1;
console.log('MOUSE SKIP', x1, x2, x3);
} else {
this.x = x2;
this.y = y2;
}
}
};
this.system_attach = function(ev) {
console.log('INIT: controls');
if (this.loadonstart) {
for (var k in this.loadonstart) {
this.addContext(k, this.loadonstart[k]);
}
}
}
this.engine_frame = function(ev) {
if (!this.initialized) {
this.initcontrols();
}
//console.log("FRAME: controls");
this.update(ev.delta);
}
this.engine_stop = function(ev) {
console.log('SHUTDOWN: controls');
}
this.initcontrols = function() {
if (!this.container) this.container = (this.engine.systems.render ? this.engine.systems.render.renderer.domElement : window);
elation.events.add(this.container, "mousedown,mousemove,mouseup,mousewheel,click,DOMMouseScroll,gesturestart,gesturechange,gestureend", this);
//elation.events.add(this.container, "touchstart,touchmove,touchend, this);
elation.events.add(window, "keydown,keyup,webkitGamepadConnected,webkitgamepaddisconnected,MozGamepadConnected,MozGamepadDisconnected,gamepadconnected,gamepaddisconnected,focus", this);
//elation.events.add(window, "deviceorientation,devicemotion", this);
elation.events.add(document, "pointerlockchange,webkitpointerlockchange,mozpointerlockchange", elation.bind(this, this.pointerLockChange));
elation.events.add(document, "pointerlockerror,webkitpointerlockerror,mozpointerlockerror", elation.bind(this, this.pointerLockError));
if (args) {
this.addContexts(args);
}
this.initialized = true;
}
this.addCommands = function(context, commands) {
this.contexts[context] = commands;
}
this.addContexts = function(contexts) {
for (var k in contexts) {
this.addContext(k, contexts[k]);
}
}
this.addContext = function(context, contextargs) {
var commands = {};
var bindings = {};
var states = {};
for (var k in contextargs) {
var newbindings = contextargs[k][0].split(',');
for (var i = 0; i < newbindings.length; i++) {
bindings[newbindings[i]] = k;
}
commands[k] = contextargs[k][1];
states[k] = 0;
}
this.addCommands(context, commands);
this.addBindings(context, bindings);
this.contextstates[context] = states;
console.log("[controls] added control context: " + context);
// FIXME - context state object should be a JS class, with reset() as a member function
states._reset = function() {
for (var k in this) {
if (typeof this[k] != 'function') {
this[k] = 0;
}
}
}.bind(states);
return states;
}
this.activateContext = function(context, target) {
if (this.activecontexts.indexOf(context) == -1) {
console.log('[controls] activate control context ' + context);
this.activecontexts.unshift(context);
}
if (target) {
this.contexttargets[context] = target;
}
}
this.deactivateContext = function(context) {
var i = this.activecontexts.indexOf(context);
if (i != -1) {
console.log('[controls] deactivate control context ' + context);
this.activecontexts.splice(i, 1);
if (this.contexttargets[context]) {
delete this.contexttargets[context];
}
}
}
this.addBindings = function(context, bindings) {
if (!this.bindings[context]) {
this.bindings[context] = {};
}
for (var k in bindings) {
this.bindings[context][k] = bindings[k];
}
}
this.update = function(t) {
this.pollGamepads();
//this.pollHMDs();
var processed = {};
if (this.changes.length > 0) {
var now = new Date().getTime();
for (var i = 0; i < this.changes.length; i++) {
if (processed[this.changes[i]]) {
continue;
}
var firedev = elation.events.fire({
type: 'control_change',
element: this,
data: {
name: this.changes[i],
value: this.state[this.changes[i]]
}
});
//console.log('fired!', firedev);
for (var j = 0; j < this.activecontexts.length; j++) {
var context = this.activecontexts[j];
var contextstate = this.contextstates[context] || {};
if (this.bindings[context] && this.bindings[context][this.changes[i]]) {
var action = this.bindings[context][this.changes[i]];
if (this.contexts[context][action]) {
contextstate[action] = this.state[this.changes[i]];
//var ev = {timeStamp: now, type: this.changes[i], value: this.state[this.changes[i]], data: contextstate};
var ev = {timeStamp: now, type: action, value: this.state[this.changes[i]], data: contextstate};
//console.log('call it', this.changes[i], this.bindings[context][this.changes[i]], this.state[this.changes[i]]);
if (this.contexttargets[context]) {
ev.target = this.contexttargets[context];
this.contexts[context][action].call(ev.data, ev);
} else {
this.contexts[context][action](ev);
}
break; // Event was handled, no need to check other active contexts
} else {
console.log('Unknown action "' + action + '" in context "' + context + '"');
}
}
}
processed[this.changes[i]] = true;
}
this.changes = [];
}
if (this.state['mouse_delta_x'] != 0 || this.state['mouse_delta_y'] != 0) {
this.state['mouse_delta'] = [0, 0];
this.state['mouse_delta_x'] = 0;
this.state['mouse_delta_y'] = 0;
this.changes.push('mouse_delta_x');
this.changes.push('mouse_delta_y');
this.changes.push('mouse_delta');
}
}
this.getBindingName = function(type, id, subid) {
var codes = {
keyboard: {
8: 'backspace',
9: 'tab',
13: 'enter',
16: 'shift',
17: 'ctrl',
18: 'alt',
20: 'capslock',
27: 'esc',
32: 'space',
33: 'pgup',
34: 'pgdn',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'insert',
46: 'delete',
91: 'meta',
92: 'rightmeta',
106: 'numpad_asterisk',
107: 'numpad_plus',
110: 'numpad_period',
111: 'numpad_slash',
144: 'numlock',
173: 'volume_mute',
174: 'volume_down',
175: 'volume_up',
176: 'media_next',
177: 'media_prev',
178: 'media_stop',
179: 'media_playpause',
186: 'semicolon',
187: 'equals',
188: 'comma',
189: 'minus',
190: 'period',
191: 'slash',
192: 'backtick',
220: 'backslash',
221: 'rightsquarebracket',
219: 'leftsquarebracket',
222: 'apostrophe',
// firefox-specific
0: 'meta',
59: 'semicolon',
61: 'equals',
109: 'minus',
},
}
var bindname = type + (!elation.utils.isEmpty(subid) ? '_' + subid + '_' : '_unknown_') + id;
switch (type) {
case 'keyboard':
var basename = type + '_' + (!elation.utils.isEmpty(subid) ? subid + '_' : '');
if (codes[type][id]) {
// map the numeric code to a string, skipping the subid if it's redundant
bindname = type + '_' + (!elation.utils.isEmpty(subid) && subid !== codes[type][id] ? subid + '_' : '') + codes[type][id];
} else if (id >= 65 && id <= 90) {
bindname = basename + String.fromCharCode(id).toLowerCase();
} else if (id >= 48 && id <= 57) {
bindname = basename + (id - 48);
} else if (id >= 96 && id <= 105) {
bindname = basename + 'numpad_' + (id - 96);
} else if (id >= 112 && id <= 123) {
bindname = basename + 'f' + (id - 111);
} else {
console.log('Unknown key pressed: ' + bindname);
}
break;
case 'gamepad':
bindname = type + '_' + id + '_' + subid;
break;
}
return bindname;
}
this.pollGamepads = function() {
this.updateConnectedGamepads();
if (this.gamepads.length > 0) {
for (var i = 0; i < this.gamepads.length; i++) {
if (this.gamepads[i] != null) {
var gamepad = this.gamepads[i];
for (var a = 0; a < gamepad.axes.length; a+=2) {
var bindname_x = this.getBindingName('gamepad', i, 'axis_' + a);
var bindname_y = this.getBindingName('gamepad', i, 'axis_' + (a+1));
var bindname_any_x = this.getBindingName('gamepad', 'any', 'axis_' + a);
var bindname_any_y = this.getBindingName('gamepad', 'any', 'axis_' + (a+1));
// FIXME - Vive hack
var axisscale = 1;
if (gamepad.id == 'OpenVR Gamepad') {
axisscale = -1;
}
var values = this.deadzone(gamepad.axes[a], axisscale * gamepad.axes[a+1]);
if (this.state[bindname_x] != values[0]) {
this.changes.push(bindname_x);
this.state[bindname_x] = values[0];
this.state[bindname_x + '_full'] = THREE.MathUtils.mapLinear(gamepad.axes[a], -1, 1, 0, 1);
this.changes.push(bindname_any_x);
this.state[bindname_any_x] = values[0];
this.state[bindname_any_x + '_full'] = THREE.MathUtils.mapLinear(gamepad.axes[a], -1, 1, 0, 1);
}
if (this.state[bindname_y] != values[1]) {
this.changes.push(bindname_y);
this.state[bindname_y] = values[1];
this.state[bindname_y + '_full'] = THREE.MathUtils.mapLinear(gamepad.axes[a+1], -1, 1, 0, 1);
this.changes.push(bindname_any_y);
this.state[bindname_any_y] = values[1];
this.state[bindname_any_y + '_full'] = THREE.MathUtils.mapLinear(gamepad.axes[a+1], -1, 1, 0, 1);
}
}
for (var b = 0; b < gamepad.buttons.length; b++) {
var bindname = this.getBindingName('gamepad', i, 'button_' + b);
var bindname_any = this.getBindingName('gamepad', 'any', 'button_' + b);
if (this.state[bindname] != gamepad.buttons[b].value) {
this.changes.push(bindname);
this.state[bindname] = gamepad.buttons[b].value;
this.changes.push(bindname_any);
this.state[bindname_any] = gamepad.buttons[b].value;
}
}
}
}
}
}
this.updateConnectedGamepads = function() {
var func = navigator.getGamepads || navigator.webkitGetGamepads;
if (typeof func == 'function') {
var realgamepads = func.call(navigator),
virtualgamepads = this.virtualgamepads;
for (var i = 0; i < realgamepads.length; i++) {
this.gamepads[i] = realgamepads[i];
}
for (var i = 0; i < virtualgamepads.length; i++) {
this.gamepads[realgamepads.length + i] = virtualgamepads[i];
}
//console.log(this.gamepads);
}
}
this.addVirtualGamepad = function(gamepad) {
this.virtualgamepads.push(gamepad);
}
this.removeVirtualGamepad = function(gamepad) {
var idx = this.virtualgamepads.indexOf(gamepad);
if (idx) {
this.virtualgamepads.splice(idx, 1);
}
}
this.getGamepads = function() {
var gamepads = [];
for (var i = 0; i < this.gamepads.length; i++) {
if (this.gamepads[i]) {
gamepads.push(this.gamepads[i]);
}
}
return gamepads;
}
this.pollHMDs = function() {
if (typeof this.hmds == 'undefined') {
this.updateConnectedHMDs();
} else if (this.hmds && this.hmds.length > 0) {
for (var i = 0; i < this.hmds.length; i++) {
var hmd = this.hmds[i];
if (typeof VRDisplay != 'undefined' && hmd instanceof VRDisplay) {
var framedata = this.hmdframes[i];
var pose = false;
if (hmd.getFrameData && hmd.getFrameData(framedata)) {
pose = framedata.pose;
} else if (hmd.getPose) {
pose = hmd.getPose();
}
if (pose) {
var hmdstate = pose;
var bindname = "hmd_" + i;
this.changes.push(bindname);
this.state[bindname] = hmdstate;
}
} else {
var hmdstate = this.hmds[i].getState();
var realhmdstate = {
position: [0,0,0],
orientation: [0,0,0,1],
linearVelocity: [0,0,0],
linearAcceleration: [0,0,0],
angularVelocity: [0,0,0],
angularAcceleration: [0,0,0],
}
if (hmdstate.hasPosition) {
realhmdstate.position = [hmdstate.position.x, hmdstate.position.y, hmdstate.position.z];
}
if (hmdstate.hasOrientation) {
realhmdstate.orientation = [hmdstate.orientation.x, hmdstate.orientation.y, hmdstate.orientation.z, hmdstate.orientation.w];
}
realhmdstate.linearVelocity = [hmdstate.linearVelocity.x, hmdstate.linearVelocity.y, hmdstate.linearVelocity.z];
realhmdstate.linearAcceleration = [hmdstate.linearAcceleration.x, hmdstate.linearAcceleration.y, hmdstate.linearAcceleration.z];
realhmdstate.angularVelocity = [hmdstate.angularVelocity.x, hmdstate.angularVelocity.y, hmdstate.angularVelocity.z];
realhmdstate.angularAcceleration = [hmdstate.angularAcceleration.x, hmdstate.angularAcceleration.y, hmdstate.angularAcceleration.z];
var bindname = "hmd_" + i;
this.changes.push(bindname);
this.state[bindname] = realhmdstate;
}
}
}
}
this.updateConnectedHMDs = function() {
this.hmds = false;
if (typeof navigator.getVRDisplays == 'function') {
navigator.getVRDisplays().then(elation.bind(this, this.processConnectedHMDs));
} else if (typeof navigator.getVRDevices == 'function') {
navigator.getVRDevices().then(elation.bind(this, this.processConnectedHMDs));
}
}
this.processConnectedHMDs = function(hmds) {
if (hmds.length > 0) {
this.hmds = [];
for (var i = 0; i < hmds.length; i++) {
// We only care about position sensors
if ((typeof PositionSensorVRDevice != 'undefined' && hmds[i] instanceof PositionSensorVRDevice) ||
(typeof VRDisplay != 'undefined' && hmds[i] instanceof VRDisplay)) {
this.hmds.push(hmds[i]);
if (typeof VRFrameData !== 'undefined') {
this.hmdframes[i] = new VRFrameData();
}
}
}
}
}
this.calibrateHMDs = function() {
if (this.hmds) {
for (var i = 0; i < this.hmds.length; i++) {
if (this.hmds[i].resetPose) {
this.hmds[i].resetPose();
} else if (this.hmds[i].resetSensor) {
this.hmds[i].resetSensor();
}
}
}
}
this.getPointerLockElement = function() {
var el = document.pointerLockElement || document.mozPointerLockElement || document.webkitPointerLockElement;
return el;
}
this.enablePointerLock = function(enable) {
this.pointerLockEnabled = enable;
if (!this.pointerLockEnabled && this.pointerLockActive) {
this.releasePointerLock();
}
}
this.requestPointerLock = function() {
if (this.pointerLockEnabled && !this.pointerLockActive) {
var domel = this.engine.systems.render.renderer.domElement;
if (!domel.requestPointerLock) {
domel.requestPointerLock = domel.requestPointerLock || domel.mozRequestPointerLock || domel.webkitRequestPointerLock;
}
if (domel.requestPointerLock) {
domel.requestPointerLock();
return true;
}
}
return false;
}
this.releasePointerLock = function() {
this.pointerLockActive = false;
var lock = this.getPointerLockElement();
if (lock) {
document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock || document.webkitExitPointerLock;
document.exitPointerLock();
}
}
this.pointerLockChange = function(ev) {
var lock = this.getPointerLockElement();
if (lock && !this.pointerLockActive) {
this.pointerLockActive = true;
this.state['pointerlock'] = this.pointerLockActive;
this.changes.push('pointerlock');
} else if (!lock && this.pointerLockActive) {
this.pointerLockActive = false;
this.state['pointerlock'] = this.pointerLockActive;
this.changes.push('pointerlock');
}
}
this.pointerLockError = function(ev) {
console.error('[controls] Pointer lock error');
this.pointerLockChange(ev);
}
this.getMousePosition = function(ev) {
var width = this.container.offsetWidth || this.container.innerWidth,
height = this.container.offsetHeight || this.container.innerHeight,
top = this.container.offsetTop || 0,
left = this.container.offsetLeft || 0;
var relpos = [ev.clientX - left, ev.clientY - top];
//console.log(relpos, [ev.clientX, ev.clientY], this.container, [width, height], [top, left]);
var ret = [(relpos[0] / width - .5) * 2, (relpos[1] / height - .5) * 2];
return ret;
}
this.getMouseDelta = function(ev) {
var width = this.container.offsetWidth || this.container.innerWidth,
height = this.container.offsetHeight || this.container.innerHeight;
var scaleX = this.settings.mouse.sensitivity * (this.settings.mouse.invertX ? -1 : 1),
scaleY = this.settings.mouse.sensitivity * (this.settings.mouse.invertY ? -1 : 1),
movementX = elation.utils.any(ev.movementX, ev.mozMovementX),
movementY = elation.utils.any(ev.movementY, ev.mozMovementY);
this.mousesmooth.add(movementX, movementY);
var deltas = [
scaleX * this.mousesmooth.x / width,
scaleY * this.mousesmooth.y / height
];
return deltas;
}
this.getKeyboardModifiers = function(ev) {
var ret = "";
var modifiers = {'shiftKey': 'shift', 'altKey': 'alt', 'ctrlKey': 'ctrl'};
for (var k in modifiers) {
if (ev[k]) {
ret += (ret.length > 0 ? "_" : "") + modifiers[k];
}
}
if (ret != "")
return ret;
return "nomod";
}
this.mousedown = function(ev, skiplock) {
this.cancelclick = false;
this.mousedowntime = performance.now();
if (this.firstclick) {
if (!this.engine.systems.sound.canPlaySound) {
this.engine.systems.sound.enableSound();
}
this.firstclick = false;
}
var bindid = "mouse_button_" + ev.button;
if (!this.state[bindid]) {
this.state[bindid] = 1;
this.changes.push(bindid);
}
//elation.events.add(window, "mousemove,mouseup", this);
}
this.mousemove = function(ev) {
var mpos = this.getMousePosition(ev);
var deltas = this.getMouseDelta(ev);
var status = {mouse_pos: false, mouse_delta: false, mouse_x: false, mouse_y: false};
if (!this.state["mouse_pos"]) {
status["mouse_pos"] = true;
status["mouse_x"] = true;
status["mouse_y"] = true;
} else {
if (this.state["mouse_pos"][0] != mpos[0]) {
status["mouse_pos"] = true;
status["mouse_x"] = true;
}
if (this.state["mouse_pos"][1] != mpos[1]) {
status["mouse_pos"] = true;
status["mouse_y"] = true;
}
}
status["mouse_delta"] = (Math.abs(deltas[0]) != 0 || Math.abs(deltas[1]) != 0);
if (status["mouse_pos"]) {
if (status["mouse_x"]) {
this.state["mouse_x"] = mpos[0];
this.changes.push("mouse_x");
if (this.state["mouse_button_0"]) {
this.state["mouse_drag_x"] = this.state["mouse_x"];
this.changes.push("mouse_drag_x");
}
}
this.state["mouse_pos"] = mpos;
this.state["mouse_delta"] = [this.state["mouse_delta_x"], this.state["mouse_delta_y"]];
this.changes.push("mouse_pos");
this.changes.push("mouse_delta");
if (status["mouse_y"]) {
this.state["mouse_y"] = mpos[1];
this.changes.push("mouse_y");
if (this.state["mouse_button_0"]) {
this.state["mouse_drag_y"] = this.state["mouse_y"];
this.changes.push("mouse_drag_y");
}
}
if (this.state["mouse_button_0"]) {
this.state["mouse_drag"] = this.state["mouse_pos"];
this.state["mouse_drag_delta"] = [this.state["mouse_drag_delta_x"], this.state["mouse_drag_delta_y"]];
this.changes.push("mouse_drag");
this.changes.push("mouse_drag_delta");
}
}
if (status["mouse_delta"]) {
this.state["mouse_delta_x"] = (this.state["mouse_delta_x"] ? this.state["mouse_delta_x"] + deltas[0] : deltas[0]);
this.state["mouse_delta_y"] = (this.state["mouse_delta_y"] ? this.state["mouse_delta_y"] + deltas[1] : deltas[1]);
this.state["mouse_delta"] = [this.state["mouse_delta_x"], this.state["mouse_delta_y"]];
this.changes.push("mouse_delta_x");
this.changes.push("mouse_delta_y");
this.changes.push("mouse_delta");
if (this.state["mouse_button_0"]) {
this.state["mouse_drag_x"] = this.state["mouse_x"];
this.state["mouse_drag_y"] = this.state["mouse_y"];
this.state["mouse_drag_delta_x"] = this.state["mouse_delta_x"];
this.state["mouse_drag_delta_y"] = this.state["mouse_delta_y"];
this.changes.push("mouse_drag_x");
this.changes.push("mouse_drag_y");
this.changes.push("mouse_drag_delta_x");
this.changes.push("mouse_drag_delta_y");
}
}
}
this.mouseup = function(ev) {
if (this.cancelclick) {
ev.stopPropagation();
ev.preventDefault();
return;
}
var bindid = "mouse_button_" + ev.button;
//elation.events.remove(window, "mousemove", this);
if (this.state[bindid]) {
this.state[bindid] = 0;
this.changes.push(bindid);
if (bindid = "mouse_button_0") {
this.state['mouse_drag_x'] = 0;
this.state['mouse_drag_y'] = 0;
this.changes.push("mouse_drag_x");
this.changes.push("mouse_drag_y");
}
}
if (ev.button === 0 && !this.getPointerLockElement() && performance.now() - this.mousedowntime <= 500) {
if (this.requestPointerLock()) {
//this.cancelclick = true;
//ev.stopPropagation();
//ev.preventDefault();
}
}
}
this.click = function(ev) {
if (this.cancelclick) {
ev.stopPropagation();
ev.preventDefault();
return;
}
}
this.DOMMouseScroll = function(ev) {
this.mousewheel(ev);
}
this.mousewheel = function(ev) {
var delta = Math.max(-1, Math.min(1, (ev.wheelDelta || -ev.detail)));
var bindid = "mouse_wheel_" + (delta < 0 ? "down" : "up");;
this.state[bindid] = 1;
this.changes.push(bindid);
//ev.preventDefault();
if (this.mousewheeltimer) {
clearTimeout(this.mousewheeltimer);
}
this.mousewheeltimer = setTimeout(() => {
this.state[bindid] = 0;
this.changes.push(bindid);
}, 10);
//ev.preventDefault();
}
this.keydown = function(ev) {
// Send key events for both keyboard_<key> and keyboard_<modname>_<key>
var mods = this.getKeyboardModifiers(ev);
var keynamemod = this.getBindingName("keyboard", ev.keyCode, mods);
keyname = this.getBindingName("keyboard", ev.keyCode);
if (!this.state[keynamemod]) {
this.changes.push(keynamemod);
}
this.state[keynamemod] = 1;
if (mods != 'alt') {
if (!this.state[keyname]) {
this.changes.push(keyname);
}
this.state[keyname] = 1;
}
if (this.capturekeys.indexOf(keyname) != -1 ||
this.capturekeys.indexOf(keynamemod) != -1) {
ev.preventDefault();
}
}
this.keyup = function(ev) {
// Send key events for both keyboard_<key> and keyboard_<modname>_<key>
var keyname = this.getBindingName("keyboard", ev.keyCode);
var keynamemod = this.getBindingName("keyboard", ev.keyCode, this.getKeyboardModifiers(ev));
this.state[keyname] = 0;
this.state[keynamemod] = 0;
this.changes.push(keyname);
this.changes.push(keynamemod);
}
this.touchstart = function(ev) {
return;
var newev = {
button: 0,
type: 'mousedown',
screenX: ev.touches[0].screenX,
screenY: ev.touches[0].screenY,
pageX: ev.touches[0].pageX,
pageY: ev.touches[0].pageY,
clientX: ev.touches[0].clientX,
clientY: ev.touches[0].clientY,
stopPropagation: elation.bind(ev, ev.stopPropagation),
preventDefault: elation.bind(ev, ev.preventDefault),
};
this.lasttouchpos = [newev.clientX, newev.clientY];
this.mousedown(newev, true);
//ev.preventDefault();
}
this.touchmove = function(ev) {
return;
//if (ev.touches.length == 1) {
var newev = {
type: 'mousemove',
screenX: ev.touches[0].screenX,
screenY: ev.touches[0].screenY,
pageX: ev.touches[0].pageX,
pageY: ev.touches[0].pageY,
clientX: ev.touches[0].clientX,
clientY: ev.touches[0].clientY,
stopPropagation: elation.bind(ev, ev.stopPropagation),
preventDefault: elation.bind(ev, ev.preventDefault),
};
newev.movementX = (this.lasttouchpos[0] - newev.clientX) / devicePixelRatio;
newev.movementY = (this.lasttouchpos[1] - newev.clientY) / devicePixelRatio;
this.lasttouchpos = [newev.clientX, newev.clientY];
this.mousemove(newev);
//} else {
// ev.preventDefault();
//}
}
this.touchend = function(ev) {
if (ev.touches.length == 0) {
var newev = {
button: 0,
type: 'mouseup',
/*
screenX: ev.touches[0].screenX,
screenY: ev.touches[0].screenY,
pageX: ev.touches[0].pageX,
pageY: ev.touches[0].pageY,
clientX: ev.touches[0].clientX,
clientY: ev.touches[0].clientY,
*/
stopPropagation: elation.bind(ev, ev.stopPropagation),
preventDefault: elation.bind(ev, ev.preventDefault),
};
this.mouseup(newev);
}
}
this.gesturestart = function(ev) {
console.log('do a gesture', ev);
ev.preventDefault();
}
this.gesturechange = function(ev) {
console.log('change a gesture', ev);
}
this.gestureend = function(ev) {
console.log('end a gesture', ev);
}
this.deviceorientation = function(ev) {
console.log('deviceorientation:', [ev.alpha, ev.beta, ev.gamma]);
var deg2rad = Math.PI/180;
var radval = [ev.alpha * deg2rad, ev.beta * deg2rad, ev.gamma * deg2rad, window.orientation * deg2rad];
this.state['orientation'] = {
alpha: radval[0],
beta : radval[1] * Math.sin(radval[3]) + radval[2] * Math.cos(radval[3]),
gamma: radval[2] * Math.sin(radval[3]) + radval[1] * Math.cos(radval[3])
};
this.changes.push('orientation');
}
this.devicemotion = function(ev) {
//console.log('devicemotion:', ev.acceleration, ev.rotationRate);
}
/* Gamepad handlers */
this.webkitGamepadconnected = function(ev) {
this.gamepadconnected(ev);
}
this.webkitgamepaddisconnected = function(ev) {
this.gamepaddisconnected(ev);
}
this.MozGamepadConnected = function(ev) {
this.gamepadconnected(ev);
}
this.MozGamepadDisconnected = function(ev) {
this.gamepaddisconnected(ev);
}
this.webkitGamepadConnected = function(ev) {
gamepadconnected(ev);
}
this.gamepadconnected = function(ev) {
for (var i = 0; i < this.gamepads.length; i++) {
if (this.gamepads[i] == null) {
this.gamepads[i] = ev.gamepad;
console.log('replace previously-connected gamepad ' + i + ':', ev);
break;
}
}
if (i == this.gamepads.length) {
this.gamepads.push(ev.gamepad);
console.log('add new gamepad ' + i + ':', ev);
}
}
this.gamepaddisconnected = function(ev) {
for (var i = 0; i < this.gamepads.length; i++) {
if (this.gamepads[i] == ev.gamepad) {
console.log('remove gamepad ' + i + ':', ev);
this.gamepads[i] = null;
}
}
}
this.showviewer = function() {
var viewerwindow = elation.ui.window({title: 'Control Viewer', append: document.body});
var viewer = elation.engine.systems.controls.gamepadviewer({controlsystem: this, gamepad: this.gamepads[0]});
viewerwindow.setcontent(viewer)
}
this.deadzone = function(x, y) {
var deadzone = this.settings.gamepad.deadzone;
var magnitude = Math.sqrt(x*x + y*y);
var adjusted = magnitude;
if (magnitude > deadzone) {
if (magnitude > 1) magnitude = 1;
adjusted = (magnitude - deadzone) / (1 - deadzone);
} else {
adjusted = 0;
}
return [x * adjusted, y * adjusted];
//return (Math.abs(value) < this.settings.gamepad.deadzone ? 0 : value);
}
this.enableKeyboardCapture = function(key) {
let idx = this.capturekeys.indexOf(key);
if (idx == -1) {
this.capturekeys.push(key);
}
}
this.disableKeyboardCapture = function(key) {
let idx = this.capturekeys.indexOf(key);
if (idx == -1) {
this.capturekeys.splice(idx, 1);
}
}
this.focus = function(ev) {
if (this.state['keyboard_alt']) this.state['keyboard_alt'] = 0; // fix sticky alt key on alt tab
}
});
if (0) {
elation.component.add('engine.systems.controls.config', function() {
this.init = function() {
this.controlsystem = this.args.controlsystem;
this.create();
}
this.create = function() {
var columns = elation.ui.panel_horizontal({
append: this,
classname: 'controls_columns',
});
var controltypes = elation.ui.panel_vertical({
append: columns,
classname: 'controls_types',
});
var mousecontrols = elation.ui.panel({
append: controltypes,
classname: 'engine_config_section controls_mouse',
});
var gamepadcontrols = elation.ui.panel({
append: controltypes,
classname: 'engine_config_section controls_gamepad',
});
var keyboardcontrols = elation.ui.panel({
append: controltypes,
classname: 'engine_config_section controls_keyboard',
});
var leapmotioncontrols = elation.ui.panel({
append: controltypes,
classname: 'engine_config_section controls_leapmotion',
});
var label = elation.ui.labeldivider({
append: mousecontrols,
label: 'Mouse'
});
var sensitivity = elation.ui.slider({
append: mousecontrols,
min: 0,
max: 500,
snap: 1,
label: 'Sensitivity',
classname: 'controls_mouse_sensitivity',
handle: {
name: 'handle_one',
value: this.controlsystem.settings.mouse.sensitivity,
bindvar: [this.controlsystem.settings.mouse, 'sensitivity'],
},
events: {
ui_slider_change: elation.bind(this, this.fireSettingsChangeEvent)
}
});
var invertY = elation.ui.toggle({
append: mousecontrols,
classname: 'controls_mouse_inverty',
label: 'Invert Y',
bindvar: [this.controlsystem.settings.mouse, 'invertY'],
events: {
toggle: elation.bind(this, this.fireSettingsChangeEvent)
}
});
label = elation.ui.labeldivider({
append: gamepadcontrols,
label: 'Gamepad'
});
var gamepads = this.controlsystem.getGamepads();
/*
if (gamepads.length == 0) {
elation.ui.content({ append: this, content: 'No gamepads connected'});
} else {
elation.ui.list({ append: this, items: gamepads, attrs: { label: 'id'}});
for (var i = 0; i < gamepads.length; i++) {
if (gamepads[i]) {
elation.engine.systems.controls.gamepadviewer({ append: this, gamepadnum: i, controlsystem: this.controlsystem });
}
}
}
*/
elation.engine.systems.controls.gamepadviewer({ append: gamepadcontrols, gamepadnum: 0, controlsystem: this.controlsystem });
label = elation.ui.labeldivider({
append: keyboardcontrols,
label: 'Keyboard'
});
/*
var turnspeed = elation.ui.slider({
append: this,
min: 0,
max: 10,
snap: .1,
handle: [
{
name: 'handle_two',
value: this.player.turnSpeed,
labelprefix: 'Turn Speed:',
bindvar: [this.player, 'turnSpeed']
}
]
});
*/
//elation.ui.content({ append: keyboardcontrols, content: '(TODO - build keybinding UI)'});
var leaplabel = elation.ui.labeldivider({
append: leapmotioncontrols,
label: 'Leap Motion'
});
var leapenabled = elation.ui.toggle({
append: leapmotioncontrols,
classname: 'controls_leapmotion_enabled',
label: 'Enabled',
bindvar: [this.controlsystem.settings.leapmotion, 'enabled'],
events: {
toggle: elation.bind(this, this.fireSettingsChangeEvent)
}
});
var leapmount = elation.ui.select({
append: leapmotioncontrols,
classname: 'controls_leapmotion_mount',
label: 'Mount',
items: ['VR', 'Desktop'],
bindvar: [this.controlsystem.settings.leapmotion, 'mount'],
events: {
ui_select_change: elation.bind(this, this.fireSettingsChangeEvent)
}
});
var bindingpanel = elation.engine.systems.controls.bindingviewer({
append: columns,
controlsystem: this.controlsystem
});
}
this.fireSettingsChangeEvent = function() {
console.log('settings changed!');
elation.events.fire({element: this.controlsystem, type: 'settings_change'});
}
}, elation.ui.panel);
elation.component.add('engine.systems.controls.gamepadviewer', function() {
this.init = function() {
this.controlsystem = this.args.controlsystem;
this.gamepadnum = this.args.gamepadnum;
this.gamepad = this.args.gamepad || this.controlsystem.gamepads[this.gamepadnum] || false;
if (this.gamepad && this.gamepadnum == undefined) {
this.gamepadnum = this.controlsystem.gamepads.indexOf(this.gamepad);
}
this.sticks = [];
this.buttons = [];
this.addclass('controls_gamepadviewer');
if (!this.gamepad) return;
var controls = {
'axis_0_horizontal': [ 'gamepad_' + this.gamepadnum + '_axis_0', elation.bind(this, this.update) ],
'axis_0_vertical': [ 'gamepad_' + this.gamepadnum + '_axis_1', elation.bind(this, this.update) ],
};
this.sticks[0] = elation.engine.systems.controls.axisviewer({stick: 'left', append: this});
var buttonparents = {
10: this.sticks[0].stickend,
}
if (this.gamepad.axes.length > 2) {
controls.axis_1_horizontal = [ 'gamepad_' + this.gamepadnum + '_axis_2', elation.bind(this, this.update) ];
controls.axis_1_vertical = [ 'gamepad_' + this.gamepadnum + '_axis_3', elation.bind(this, this.update) ];
this.sticks[1] = elation.engine.systems.controls.axisviewer({stick: 'right', append: this});
buttonparents[11] = this.sticks[1].stickend;
}
for (var i = 0; i < this.gamepad.buttons.length; i++) {
controls['button_' + i] = ['gamepad_' + this.gamepadnum + '_button_' + i, elation.bind(this, this.update) ];
var buttonparent = buttonparents[i] || this;
this.buttons[i] = elation.engine.systems.controls.buttonviewer({label: i+1, button: this.gamepad.buttons[i], append: buttonparent, buttontype: (i >= 4 && i < 8 ? 'shoulder' : 'normal') });
}
this.controlstate = this.controlsystem.addContext('control_viewer', controls);
this.controlsystem.activateContext('control_viewer');
}
this.update = function(ev) {
this.gamepad = this.args.gamepad || this.controlsystem.gamepads[this.args.gamepadnum] || false;
//console.log('got a control update', ev, this.controlstate);
var point_left = {
x: this.controlstate.axis_0_horizontal,
y: this.controlstate.axis_0_vertical,
};
this.sticks[0].updatepoint(point_left);
if (this.sticks[1]) {
var point_right = {
x: this.controlstate.axis_1_horizontal,
y: this.controlstate.axis_1_vertical,
};
this.sticks[1].updatepoint(point_right);
}
for (var i = 0; i < this.buttons.length; i++) {
this.buttons[i].updatebutton(this.gamepad.buttons[i]);
}
}
}, elation.ui.base);
elation.component.add('engine.systems.controls.axisviewer', function() {
this.init = function() {
this.addclass('controls_gamepad_stick');
this.addclass('controls_gamepad_stick_' + this.args.stick);
this.size = this.args.size || 60;
this.canvas = elation.html.create({tag: 'canvas', append: this});
this.canvas.width = this.canvas.height = this.size;
this.ctx = this.canvas.getContext('2d');
this.point = { x: 0, y: 0 };
this.stickend = elation.html.create({tag: 'div', classname: 'controls_gamepad_stick_end', append: this});
this.refresh();
}
this.render = function() {
this.clear();
this.drawaxes();
this.drawmarker(this.point);
}
this.clear = function() {
this.canvas.width = this.canvas.height = this.size;
}
this.drawaxes = function() {
var ctx = this.ctx;
ctx.beginPath();
ctx.moveTo(0, this.size / 2);
ctx.lineTo(this.size, this.size / 2);
ctx.moveTo(this.size / 2, 0);
ctx.lineTo(this.size / 2, this.size);
ctx.closePath();
ctx.strokeStyle = 'rgba(255,255,255,.5)';
ctx.stroke();
}
this.drawmarker = function() {
var pointsize = 6,
halfpointsize = pointsize / 2;
var ctx = this.ctx;
ctx.strokeStyle = 'rgba(255,0,0,1)';
ctx.fillStyle = 'rgba(255,0,0,.5)';
var point = [(this.point.x / 2 + .5), (this.point.y / 2 + .5)],
scaledpoint = [point[0] * this.size, point[1] * this.size];
/*
var len = Math.sqrt(this.point.x * this.point.x + this.point.y * this.point.y);
if (len < 1) len = 1;
var rpoint = [(this.point.x / (2 * len) + .5), (this.point.y / (2 * len) + .5)];
var point = [(rpoint[0] / len) * this.size, (rpoint[1] / len) * this.size];
console.log(rpoint, point, len);
*/
ctx.fillRect(scaledpoint[0] - halfpointsize, scaledpoint[1] - halfpointsize, pointsize, pointsize);
ctx.strokeRect(scaledpoint[0] - halfpointsize, scaledpoint[1] - halfpointsize, pointsize, pointsize);
var sticksize = 50,
halfsticksize = sticksize / 2,
movescale = .35;
var stickpos = [
(this.size * (((movescale * this.point.x) / 2 + .5)) - halfsticksize),
(this.size * (((movescale * this.point.y) / 2 + .5)) - halfsticksize)
];
/*
this.stickend.style.left = stickpos[0] + 'px';
this.stickend.style.top = stickpos[1] + 'px';
*/
this.stickend.style.transform = 'translate(' + stickpos[0] + 'px, ' + stickpos[1] + 'px)';
}
this.updatepoint = function(point) {
this.point.x = point.x;
this.point.y = point.y;
this.refresh();
}
}, elation.ui.base);
elation.component.add('engine.systems.controls.buttonviewer', function() {
this.init = function() {
this.button = this.args.button;
this.addclass('controls_gamepad_button');
this.addclass('controls_gamepad_button_' + this.args.label);
if (this.args.buttontype) {
this.addclass('controls_gamepad_button_' + this.args.buttontype);
}
this.container.innerHTML = this.args.label;
console.log('new button', this.button);
}
this.render = function() {
if (this.button.pressed && !this.hasclass('state_pressed')) {
this.addclass('state_pressed');
} else if (!this.button.pressed && this.hasclass('state_pressed')) {
this.removeclass('state_pressed');
}
}
this.updatebutton = function(button) {
this.button = button;
this.refresh();
}
}, elation.ui.base);
elation.component.add('engine.systems.controls.bindingviewer', function() {
this.init = function() {
this.addclass('controls_bindings');
this.controlsystem = this.args.controlsystem;
this.tabs = elation.ui.tabs({
append: this,
classname: 'controls_binding_contexts',
items: Object.keys(this.controlsystem.contexts),
events: {
ui_tabs_change: elation.bind(this, this.updateBindingList)
}
});
this.bindings = elation.ui.list({
append: this,
classname: 'controls_binding_list',
attrs: {
itemcomponent: 'engine.systems.controls.binding'
}
});
this.footer = elation.ui.panel_horizontal({
append: this,
classname: 'controls_binding_footer'
});
this.footerlabel = elation.ui.labeldivider({
append: this.footer,
label: '',
});
this.clearbutton = elation.ui.button({
append: this.footer,
label: 'Clear',
events: {
click: elation.bind(this, this.clearBindings)
}
});
this.savedconfigs = elation.ui.select({
append: this.footer,
label: 'Load',
items: ['default', 'thinger', 'whatsit']
});
this.bindings.setItems([]);
elation.events.add(this.bindings, 'ui_list_select', elation.bind(this, this.rebind));
}
this.updateBindingList = function(ev) {
if (ev && ev.data) {
var tab = ev.data;
this.context = tab.name;
}
var bindings = this.controlsystem.bindings[this.context];
var actions = this.controlsystem.contexts[this.context];
//console.log('set it!', this.context, bindings);
var actionmap = {};
for (var binding in bindings) {
var action = bindings[binding];
var item = actionmap[action];
if (item) {
item.bindings.push(binding);
} else {
actionmap[action] = {
action: action,
bindings: [binding]