diff --git a/README.md b/README.md index 6479886..b61ed9f 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,6 @@ The communication between the Linux client and the Android app are unencrypted U ## Tweaking -Many aspects of Yoke behavior can be changed easily - ave a look at `yoke/assets/joypad`, `bin/yoke` and `yoke/service.py`. +Many aspects of Yoke behavior can be changed easily - have a look at `yoke/assets/joypad`, `bin/yoke` and `yoke/service.py`. ![Thumbstick](media/thumbstick.gif) diff --git a/devel/app-debug.apk b/devel/app-debug.apk index 7a1e696..45fc93c 100755 Binary files a/devel/app-debug.apk and b/devel/app-debug.apk differ diff --git a/yoke/assets/joypad/base.css b/yoke/assets/joypad/base.css index 66cae88..8df5948 100644 --- a/yoke/assets/joypad/base.css +++ b/yoke/assets/joypad/base.css @@ -110,11 +110,24 @@ div { background-size: 100% 100%; } +/* Joysticks */ +.joystick { background-image: url("img/joystick.svg"); background-color: #bbb; } +.circle { + background-color: black; + width: 10px; + height: 10px; + border-radius: 100%; +} + +/* Buttons */ +.button { background-color: #bbb; } +.pressed { filter: brightness(70%); } /* seq 1 16 | xargs -I xx echo "#bxx { background-image: url('img/xx.svg'); }" */ -#b1 { background-image: url('img/1.svg'); } -#b2 { background-image: url('img/2.svg'); } -#b3 { background-image: url('img/3.svg'); } -#b4 { background-image: url('img/4.svg'); } +/* Some edits done by hand */ +#b1 { background-image: url('img/1.svg'); background-color: #00d; } +#b2 { background-image: url('img/2.svg'); background-color: #e00; } +#b3 { background-image: url('img/3.svg'); background-color: #dd0; } +#b4 { background-image: url('img/4.svg'); background-color: #0d0; } #b5 { background-image: url('img/5.svg'); } #b6 { background-image: url('img/6.svg'); } #b7 { background-image: url('img/7.svg'); } @@ -127,9 +140,27 @@ div { #b14 { background-image: url('img/14.svg'); } #b15 { background-image: url('img/15.svg'); } #b16 { background-image: url('img/16.svg'); } - +#bg { background-image: url('img/g.svg'); background-color: #444; } +#bs { background-image: url('img/s.svg'); background-color: #444; } +#bm { background-image: url('img/m.svg'); background-color: #444; } /* printf "du\ndl\ndd\ndr" | xargs -I xx echo "#xx { background-image: url('img/xx.svg'); }" */ #du { background-image: url('img/du.svg'); } #dl { background-image: url('img/dl.svg'); } #dd { background-image: url('img/dd.svg'); } #dr { background-image: url('img/dr.svg'); } + +/* Analog buttons */ +#a1 {background-color: #66f;} +#a2 {background-color: #f33;} +#a3 {background-color: #ff2;} +#a4 {background-color: #2f2;} + +/* Motion controls */ +.motion {background-color: #ddd;} + +/* Pedals */ +.pedal {background-color: #444;} + +/* Knobs */ +.knob { } +.knobcircle {background-color: #888;} diff --git a/yoke/assets/joypad/base.js b/yoke/assets/joypad/base.js index b15b1b5..7d510da 100644 --- a/yoke/assets/joypad/base.js +++ b/yoke/assets/joypad/base.js @@ -201,16 +201,12 @@ function mnemonics(a, b) { function truncate(f, id, pattern) { var truncated = false; f = f.map(function(val) { - if (val < 0) { truncated = true; return 0; } - else if (val > 1) { truncated = true; return 1; } + if (val < 0.000001) { truncated = true; return 0.000001; } + else if (val > 0.999999) { truncated = true; return 0.999999; } else { return val; } }); if (VIBRATE_ON_PAD_BOUNDARY && pattern) { - if (truncated) { - queueForVibration(id, pattern); - } else { - unqueueForVibration(id); - } + truncated ? queueForVibration(id, pattern) : unqueueForVibration(id); } return f; } @@ -252,9 +248,16 @@ function Control(type, id, updateStateCallback) { this._state = 0; this.kernelEvent = ''; } +Control.prototype.getBoundingClientRect = function() { + this._offset = this.element.getBoundingClientRect(); + this._offset.semiwidth = this._offset.width / 2; + this._offset.semiheight = this._offset.height / 2; + this._offset.xCenter = this._offset.x + this._offset.semiwidth; + this._offset.yCenter = this._offset.y + this._offset.semiheight; +}; Control.prototype.onAttached = function() {}; Control.prototype.state = function() { - return this._state.toString(); + return Math.floor(256 * this._state); }; function Joystick(id, updateStateCallback) { @@ -262,7 +265,6 @@ function Joystick(id, updateStateCallback) { this._state = [0.5, 0.5]; this.quadrant = -2; this._locking = (id[0] == 's'); - this._offset = {}; this._circle = document.createElement('div'); this._circle.className = 'circle'; this.element.appendChild(this._circle); @@ -270,7 +272,6 @@ function Joystick(id, updateStateCallback) { } Joystick.prototype = Object.create(Control.prototype); Joystick.prototype.onAttached = function() { - this._offset = this.element.getBoundingClientRect(); this._updateCircle(); this.element.addEventListener('touchmove', this.onTouch.bind(this), false); this.element.addEventListener('touchstart', this.onTouchStart.bind(this), false); @@ -311,7 +312,12 @@ Joystick.prototype.onTouchEnd = function() { window.navigator.vibrate(VIBRATION_MILLISECONDS_OUT); }; Joystick.prototype._updateCircle = function() { - this._circle.style.transform = 'translate(-50%, -50%) translate(' + (this._offset.x + this._offset.width * this._state[0]) + 'px, ' + (this._offset.y + this._offset.height * this._state[1]) + 'px)'; + this._circle.style.transform = 'translate(-50%, -50%) translate(' + + (this._offset.x + this._offset.width * this._state[0]) + 'px, ' + + (this._offset.y + this._offset.height * this._state[1]) + 'px)'; +}; +Joystick.prototype.state = function() { + return this._state.map(function(val) {return Math.floor(256 * val);}); }; function Motion(id, updateStateCallback) { @@ -347,8 +353,8 @@ function Motion(id, updateStateCallback) { Motion.prototype = Object.create(Control.prototype); Motion.prototype._normalize = function(f) { f *= ACCELERATION_CONSTANT; - if (f < -0.5) { f = -0.5; } - if (f > 0.5) { f = 0.5; } + if (f < -0.499999) { f = -0.499999; } + if (f > 0.499999) { f = 0.499999; } return f + 0.5; }; Motion.prototype.onAttached = function() {}; @@ -365,18 +371,16 @@ Motion.prototype.onDeviceOrientation = function(ev) { motionSensor.updateStateCallback(); }; Motion.prototype.state = function() { - return motionState[this._mask].toString(); + return Math.floor(256 * motionState[this._mask]); }; function Pedal(id, updateStateCallback) { Control.call(this, 'button', id, updateStateCallback); this._state = 0; - this._offset = {}; axes += 1; } Pedal.prototype = Object.create(Control.prototype); Pedal.prototype.onAttached = function() { - this._offset = this.element.getBoundingClientRect(); this.element.addEventListener('touchstart', this.onTouchStart.bind(this), false); this.element.addEventListener('touchmove', this.onTouchMove.bind(this), false); this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); @@ -415,30 +419,22 @@ function AnalogButton(id, updateStateCallback) { this.onTouchMoveParticular = function() {}; Control.call(this, 'button', id, updateStateCallback); this._state = 0; - this._offset = {}; axes += 1; } AnalogButton.prototype = Object.create(Control.prototype); AnalogButton.prototype.onAttached = function() { - this._offset = this.element.getBoundingClientRect(); this.element.addEventListener('touchstart', this.onTouchStart.bind(this), false); this.element.addEventListener('touchmove', this.onTouchMove.bind(this), false); this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); if (this._offset.width > this._offset.height) { - this._offset.width /= 2; - this._offset.x += this._offset.width; this.onTouchMoveParticular = function(ev) { var pos = ev.targetTouches[0]; - return truncate([1 - Math.abs(this._offset.x - pos.pageX) / this._offset.width]); - + return truncate([1 - Math.abs(this._offset.xCenter - pos.pageX) / this._offset.semiwidth]); }; } else { - this._offset.height /= 2; - this._offset.y += this._offset.height; this.onTouchMoveParticular = function(ev) { var pos = ev.targetTouches[0]; - return truncate([1 - Math.abs(this._offset.y - pos.pageY) / this._offset.height]); - + return truncate([1 - Math.abs(this._offset.yCenter - pos.pageY) / this._offset.semiheight]); }; } }; @@ -470,7 +466,6 @@ AnalogButton.prototype.onTouchEnd = function() { function Knob(id, updateStateCallback) { Control.call(this, 'knob', id, updateStateCallback); this._state = 0; - this._offset = {}; this._knobcircle = document.createElement('div'); this._knobcircle.className = 'knobcircle'; this.element.appendChild(this._knobcircle); @@ -481,23 +476,20 @@ function Knob(id, updateStateCallback) { } Knob.prototype = Object.create(Control.prototype); Knob.prototype.onAttached = function() { - // First approximation to the knob coordinates. - this._offset = this.element.getBoundingClientRect(); // Centering the knob within the boundary. - var minDimension = Math.min(this._offset.width, this._offset.height); - if (minDimension == this._offset.width) { - this._knobcircle.style.top = this._offset.y + (this._offset.height - this._offset.width) / 2 + 'px'; + if (this._offset.width < this._offset.height) { + this._offset.y += this._offset.semiheight - this._offset.semiwidth; this._offset.height = this._offset.width; + this._offset.semiheight = this._offset.semiwidth; } else { - this._knobcircle.style.left = this._offset.x + (this._offset.width - this._offset.height) / 2 + 'px'; + this._offset.x += this._offset.semiwidth - this._offset.semiheight; this._offset.width = this._offset.height; + this._offset.semiwidth = this._offset.semiheight; } + this._knobcircle.style.top = this._offset.y + 'px'; + this._knobcircle.style.left = this._offset.x + 'px'; this._knobcircle.style.height = this._offset.width + 'px'; this._knobcircle.style.width = this._offset.height + 'px'; - // Calculating the exact center. - this._offset = this._knobcircle.getBoundingClientRect(); - this._offset.x += this._offset.width / 2; - this._offset.y += this._offset.height / 2; this._updateCircles(); this.quadrant = 0; this.element.addEventListener('touchmove', this.onTouch.bind(this), false); @@ -506,7 +498,7 @@ Knob.prototype.onAttached = function() { }; Knob.prototype.onTouch = function(ev) { var pos = ev.targetTouches[0]; - this._state = Math.atan2(pos.pageY - this._offset.y, pos.pageX - this._offset.x) / 2 / Math.PI + 0.5; + this._state = Math.atan2(pos.pageY - this._offset.yCenter, pos.pageX - this._offset.xCenter) / 2 / Math.PI + 0.5; this.updateStateCallback(); var currentQuadrant = Math.floor(this._state * 16); if (VIBRATE_ON_QUADRANT_BOUNDARY && this.quadrant != currentQuadrant) { @@ -531,7 +523,7 @@ Knob.prototype._updateCircles = function() { function Button(id, updateStateCallback) { Control.call(this, 'button', id, updateStateCallback); - this._state = 0; + this._state = false; buttons += 1; } Button.prototype = Object.create(Control.prototype); @@ -541,17 +533,20 @@ Button.prototype.onAttached = function() { }; Button.prototype.onTouchStart = function(ev) { ev.preventDefault(); // Android Webview delays the vibration without this. - this._state = 1; + this._state = true; this.updateStateCallback(); this.element.classList.add('pressed'); window.navigator.vibrate(VIBRATION_MILLISECONDS_IN); }; Button.prototype.onTouchEnd = function() { - this._state = 0; + this._state = false; this.updateStateCallback(); this.element.classList.remove('pressed'); window.navigator.vibrate(VIBRATION_MILLISECONDS_OUT); }; +Button.prototype.state = function() { + return (this._state ? 1 : 0); +}; function Dummy(id, updateStateCallback) { Control.call(this, 'dummy', 'dum', updateStateCallback); @@ -583,6 +578,7 @@ function Joypad() { }, this); this._controls.forEach(function(control) { this.element.appendChild(control.element); + control.getBoundingClientRect(); control.onAttached(); }, this); if (axes == 0 && buttons == 0) { @@ -614,11 +610,6 @@ function Joypad() { Joypad.prototype.updateState = function() { var state = this._controls.map(function(control) { return control.state(); }).join(','); - // We are reducing float precision to avoid getting UDP messages cut in half. - // We are re-splitting the string since some control.state() above return strings - // (e.g. because [0.5, 0.5].toString() == '0.5,0.5') - state = state.split(',').map(function(x) { return x.substr(0, 6); }).join(','); - // Within the Yoke webview, sends the joypad state. // Outside the Yoke webview, window.Yoke.update_vals() is redefined to have no effect. // This prevents JavaScript exceptions, and wastes less CPU time when in Yoke: diff --git a/yoke/assets/joypad/gamepad.css b/yoke/assets/joypad/gamepad.css index 5218e0e..c12bc61 100644 --- a/yoke/assets/joypad/gamepad.css +++ b/yoke/assets/joypad/gamepad.css @@ -32,7 +32,8 @@ * g for SELECT button, * m for the branded button (HOME or equivalent), * 1, 2, 3, 4, 5, 6, 7, 8, 9, 10... for the rest. - * d_ for D-PAD, where _ is one of these letters: + * a_ for analog button/trigger, where _ is a number; + * d_ for D-pad, where _ is one of these letters: * u, for the key UP, * d, for the key DOWN, * l, for the key LEFT, @@ -40,26 +41,3 @@ * dbg for debug messages; * a period for empty space. */ } - -.joystick { background-image: url("img/joystick.svg"); background-color: #bbb; } -.circle { - background-color: black; - width: 10px; - height: 10px; - border-radius: 100%; -} - -.motion {background-color: #ddd;} - -.pedal {background-color: #444;} - -.knob { } -.knobcircle {background-color: #888;} - -.button { background-color: #bbb; } -.pressed { filter: brightness(70%); } -#b1 {background-color: #22e;} -#b2 {background-color: #e00;} -#b3 {background-color: #dd0;} -#b4 {background-color: #0d0;} -#bg, #bs, #bm {background-color: #444;} diff --git a/yoke/assets/joypad/img/g.svg b/yoke/assets/joypad/img/g.svg new file mode 100644 index 0000000..b641c1e --- /dev/null +++ b/yoke/assets/joypad/img/g.svg @@ -0,0 +1,4 @@ + + + G + diff --git a/yoke/assets/joypad/img/m.svg b/yoke/assets/joypad/img/m.svg new file mode 100644 index 0000000..23a4f7e --- /dev/null +++ b/yoke/assets/joypad/img/m.svg @@ -0,0 +1,4 @@ + + + M + diff --git a/yoke/assets/joypad/img/s.svg b/yoke/assets/joypad/img/s.svg new file mode 100644 index 0000000..7d6904b --- /dev/null +++ b/yoke/assets/joypad/img/s.svg @@ -0,0 +1,4 @@ + + + + diff --git a/yoke/assets/joypad/racing.css b/yoke/assets/joypad/racing.css index 8c32be0..3769916 100644 --- a/yoke/assets/joypad/racing.css +++ b/yoke/assets/joypad/racing.css @@ -32,7 +32,8 @@ * g for SELECT button, * m for the branded button (HOME or equivalent), * 1, 2, 3, 4, 5, 6, 7, 8, 9, 10... for the rest. - * d_ for D-PAD, where _ is one of these letters: + * a_ for analog button/trigger, where _ is a number; + * d_ for D-pad, where _ is one of these letters: * u, for the key UP, * d, for the key DOWN, * l, for the key LEFT, @@ -40,26 +41,3 @@ * dbg for debug messages; * a period for empty space. */ } - -.joystick { background-image: url("img/joystick.svg"); background-color: #bbb; } -.circle { - background-color: black; - width: 10px; - height: 10px; - border-radius: 100%; -} - -.motion {background-color: #ddd;} - -.pedal {background-color: #444;} - -.knob { } -.knobcircle {background-color: #888;} - -.button { background-color: #bbb; } -.pressed { filter: brightness(70%); } -#b1 {background-color: #22e;} -#b2 {background-color: #e00;} -#b3 {background-color: #dd0;} -#b4 {background-color: #0d0;} -#bg, #bs, #bm {background-color: #444;} diff --git a/yoke/assets/joypad/testing.css b/yoke/assets/joypad/testing.css index e5b93c3..3c0e623 100644 --- a/yoke/assets/joypad/testing.css +++ b/yoke/assets/joypad/testing.css @@ -33,7 +33,7 @@ * m for the branded button (HOME or equivalent), * 1, 2, 3, 4, 5, 6, 7, 8, 9, 10... for the rest; * a_ for analog button/trigger, where _ is a number; - * d_ for D-PAD, where _ is one of these letters: + * d_ for D-pad, where _ is one of these letters: * u, for the key UP, * d, for the key DOWN, * l, for the key LEFT, @@ -41,31 +41,3 @@ * dbg for debug messages; * a period for empty space. */ } - -.joystick { background-image: url("img/joystick.svg"); background-color: #bbb; } -.circle { - background-color: black; - width: 10px; - height: 10px; - border-radius: 100%; -} - -.motion {background-color: #ddd;} - -.pedal {background-color: #444;} - -.knob { } -.knobcircle {background-color: #888;} - -.button { background-color: #bbb; } -.pressed { filter: brightness(70%); } -#b1 {background-color: #22e;} -#b2 {background-color: #e00;} -#b3 {background-color: #dd0;} -#b4 {background-color: #0d0;} -#bg, #bs, #bm {background-color: #444;} - -#a1 {background-color: #66f;} -#a2 {background-color: #f33;} -#a3 {background-color: #ff2;} -#a4 {background-color: #2f2;} diff --git a/yoke/service.py b/yoke/service.py index 2b27c01..f001496 100644 --- a/yoke/service.py +++ b/yoke/service.py @@ -52,8 +52,6 @@ def get_ip_address(): ) - - ABS_EVENTS = [getattr(EVENTS, n) for n in dir(EVENTS) if n.startswith("ABS_")] class Device: @@ -77,8 +75,6 @@ def __init__(self, id=1, name="Yoke", events=GAMEPAD_EVENTS): def emit(self, d, v): if d not in self.events: raise AttributeError("Event {} has not been registered.".format(d)) - if d in ABS_EVENTS: - v = (v+1)/2 * 255 self.device.emit(d, int(v), syn=False) def flush(self): @@ -109,7 +105,7 @@ def emit(self, d, v): if d in range(1, 8+1): self.device.set_button(d, v) else: - self.device.set_axis(d, int((v+1)/2 * 32768)) + self.device.set_axis(d, int(v * 32767 / 255)) def flush(self): pass def close(self): @@ -175,23 +171,11 @@ def __init__(self, dev, iface='auto', port=0, client_path=DEFAULT_CLIENT_PATH): self.client_path = client_path def make_events(self, values): - """returns a (event_code, value) tuple for each value in values - values are in (-1, 1) and should be returned in (-1, 1) - """ raise NotImplementedError() def preprocess(self, message): - _, *v, _ = message.split(b',') # first and last value is nothing - _, *v = v # first real value (from accelerometer) is not important yet - _, _, *v = v # ignore 2 values from accelerometer in Android code - # TODO: remove when removing corresponding Android code - v = [float(m) for m in v] - v = ( # TODO: normalize from [0, 1] to [-1, 1] on JS side - v[0] * 2 - 1, - v[1] * 2 - 1, - v[2] * 2 - 1, - v[3] * 2 - 1, - ) + tuple(v[4:]) + v = message.split(b',') + v = tuple([int(m) for m in v]) if len(v) < len(GAMEPAD_EVENTS): # Before reducing float precision, sometimes UDP messages were getting cut in half. # Keeping the code just in case.