From 0e5a4b514f50718124d0816c3ec421e8bfd400f1 Mon Sep 17 00:00:00 2001 From: Medape Date: Wed, 5 Jan 2022 21:05:31 +0100 Subject: [PATCH 1/8] Yoke can now read numeric variables from CSS This commit adds CSS variables and a codebase that for now, is unused. In a future commit, most variables will be read from the CSS instead of hardcoded in the JavaScript. This allows users to both fine-tune Yoke more easily, and also to fix settings per layout and per control (for example, the first button from a given layout might need extra force to be pushed). CSS blocks have also been moved around for the user's convenience. Critical parameters that users should not touch are now at the bottom of the file. Parameters at the top will be overridden by parameters at the bottom, following normal CSS priority rules. No examples are given for this, but parameters bound to any specific control by ID (e.g. "#j1") would be given a higher priority. --- yoke/assets/joypad/base.css | 211 ++++++++++++++++++++---------------- yoke/assets/joypad/base.js | 58 +++++++++- 2 files changed, 173 insertions(+), 96 deletions(-) diff --git a/yoke/assets/joypad/base.css b/yoke/assets/joypad/base.css index 38d4b8a..d1c1250 100644 --- a/yoke/assets/joypad/base.css +++ b/yoke/assets/joypad/base.css @@ -1,94 +1,54 @@ -/* Basic properties */ - -html { - touch-action: none; - user-select: none; - -webkit-user-select: none; -} - -body { - background: #0099CC; - margin: 0; - padding: 0; - overflow: hidden; - text-align: center; - vertical-align: middle; -} - -#joypad { - display: none; - width: 100vw; - height: 100vh; -} - -#calibration { - display: none; - position: fixed; - z-index: 10; - border-radius: 10px; - padding: 3vh 3vw 3vh; - filter: drop-shadow(0px 0px 5px black); - background: #DDDDDD; - width: 80vw; - height: 74vh; - top: 6vw; - left: 7vw; -} - -#force { - position: absolute; - background: #008000; - width: 70vw; - height: 11px; - transform-origin: left; -} - -#calibrationOk, #calibrationNo { width: 48%; padding: 4vh 0vh 4vh; } - -#menu { - width: 100vw; - height: 100vh; - display: flex; - flex-flow: row wrap; - justify-content: space-evenly; -} - -#menu > * { - flex: 1 1 auto; - background: #fff6; - border-radius: 10px; - margin: 3px; - padding-top: 30vh; - text-align: center; - vertical-align: middle; -} - -.dismiss { - margin-top: 5vh; - font-size: 80%; -} - -#dbg { - vertical-align: middle; - overflow: hidden; - background: #000; - color: #0f0; - font-family: monospace; - white-space: pre-wrap; - z-index: 2; -} - -div { - background-size: 100% 100%; -} - -.control { z-index: 2; } +/** CONTROLS **/ + +.control { /* Do not modify this line: */ z-index: 2; + + /* Default Yoke configuration follows. + * Times can be given as milliseconds (8ms) or seconds (8s). + * Boolean options can be given as yes/true or no/none/false. + * Numbers can be given as-is, or as percentages. + * Most options may be disabled setting all values to 0/no/none/false. */ + + /* The area that a joystick or knob covers is divided in 8 octants. + * Yoke can vibrate when the finger changes octants. */ + --vibrate-on-octant-edge: 20ms; + /* Vibration pattern when the finger is over the joystick edge. + * The first value is the vibration time, + * the second value is the relaxation time. */ + --vibrate-on-edge: 9ms 11ms; + /* If `yes` or `true`, the vibration becomes stronger the + * farther the joystick is pushed beyond its borders. */ + --vibrate-proportionally-to-distance: no; + /* Tactile feedback when a control "clicks", for example: + * thumbstick is pressed down to simulate L3/R3 button; + * a button or a D-pad leg is clicked. + * (Yoke never vibrates on finger release to avoid crosstalk). */ + --vibrate-on-click: 40ms; + /* Tactile feedback when a knob or joystick is touched: */ + --vibrate-on-touch: 40ms; + /* Width and height of a central deadzone: + * (round or square, depending on the shape of the control itself). */ + --center-deadzone: 9% 9%; + /* Calibration for pressure-sensitive controls. + * The first value is the minimum pressure that will engage it, + * while the second value is the pressure that will saturate it. + * The meaning for joysticks is slightly different; see below. */ + --force-thresholds: 10% 40%; +} + +/* Variables after this line override the defaults above for specific controls. + * Some variables only are defined for one specific type of control (like D-pads.) */ /* Joysticks */ .joystick { background-image: url("img/joystick.svg"); background-color: hsl(196, 77%, 55%); border: 3px hsl(188, 100%, 50%) solid; + --vibrate-on-click: 45ms; + /* Calibration for finger pressure. + * On joysticks, the first value has no effect yet. + * The second value is only used on joysticks with a L3/R3 button below, + * and determines at which pressure is it considered pressed. */ + --force-thresholds: 10% 20%; } .circle { background-color: black; @@ -111,6 +71,13 @@ div { z-index: 1; border-width: 2px; border-radius: 4px; + /* Tactile feedback. Applies to analog and digital buttons alike: */ + --vibrate-on-click: 40ms; + /* Distance that the hitbox of a button covers beyond its apparent size. + * This allows a single finger to press more than one button. + * First value controls left and right overshoot, + * second values controls top and bottom overshoot. */ + --overshoot-hitbox: 7px 7px; } .analogbutton { background-color: #ccc; } .button { background-color: #bbb; } @@ -118,7 +85,7 @@ div { .analogbutton.pressed, .button.pressed { filter: brightness(70%); border-style: inset; } -.buttonhitbox { +.button > .hitbox, .analogbutton > .hitbox { position: absolute; top: 0px; left: 0px; @@ -129,7 +96,7 @@ div { } #b1 { background-image: url('img/button.svg#b1'); background-color: #00c; border-color: #00c; } #b2 { background-image: url('img/button.svg#b2'); background-color: #e00; border-color: #e00; } -#b3 { background-image: url('img/button.svg#b3'); background-color: #dd0; border-color: #dd0; } +#b3 { background-image: url('img/button.svg#b3'); background-color: #cc0; border-color: #cc0; } #b4 { background-image: url('img/button.svg#b4'); background-color: #0c0; border-color: #0c0; } #b5 { background-image: url('img/button.svg#b5'); background-color: #a0a; border-color: #a0a; } #b6 { background-image: url('img/button.svg#b6'); background-color: #c70; border-color: #c70; } @@ -149,7 +116,7 @@ div { #a1 { background-image: url('img/analog.svg#a1'); background-color: #66f; border-color: #66f; } #a2 { background-image: url('img/analog.svg#a2'); background-color: #f33; border-color: #f33; } -#a3 { background-image: url('img/analog.svg#a3'); background-color: #ff2; border-color: #ff2; } +#a3 { background-image: url('img/analog.svg#a3'); background-color: #ee2; border-color: #ee2; } #a4 { background-image: url('img/analog.svg#a4'); background-color: #2f2; border-color: #2f2; } #a5 { background-image: url('img/analog.svg#a5'); } #a6 { background-image: url('img/analog.svg#a6'); } @@ -165,7 +132,14 @@ div { #a16 { background-image: url('img/analog.svg#a16'); } /* D-pad */ -.dpad { background-image: url('img/dp.svg'); } +.dpad { + background-image: url('img/dp.svg'); + --vibrate-on-click: 35ms; + /* Length and width of a D-pad button hitbox, given as percentages of the + * length and width of the whole D-pad. + * Buttons should overlap slightly to allow for easy diagonal movement. */ + --button-hitbox: 40% 50%; +} .dpad.u { background-image: url('img/dp.svg#u'); } .dpad.ur { background-image: url('img/dp.svg#ur'); } .dpad.r { background-image: url('img/dp.svg#r'); } @@ -177,11 +151,18 @@ div { .dpad.all { background-image: url('img/dp.svg#all'); } /* Motion controls */ -.motion { background-color: #ddd; } +.motion { + background-color: #ddd; + /* Constant normalization factor for acceleration. Indirectly sets the + * maximum acceleration Yoke can detect. */ + --normalization-constant: 0.025; +} .motiontrinket { width: 100%; height: 100%; background-image: url('img/motiontrinket.svg'); } /* Pedals */ -.pedal { background-color: #333; } +#pa { background-image: url('img/pedal.svg#accelerator') } +#pb { background-image: url('img/pedal.svg#brake') } +#pt { background-image: url('img/pedal.svg#accelerator') } /* Knobs */ .knob { } @@ -199,3 +180,51 @@ div { transform: translate(-50%, -50%); } +/** GENERAL STYLE **/ +body { + background: #0099CC; + margin: 0; + padding: 0; + overflow: hidden; + text-align: center; + vertical-align: middle; +} +#menu { + width: 100vw; + height: 100vh; + display: flex; + flex-flow: row wrap; + justify-content: space-evenly; +} +#menu > * { + flex: 1 1 auto; + background: #fff6; + border-radius: 10px; + margin: 3px; + padding-top: 30vh; + text-align: center; + vertical-align: middle; +} +#dbg { + vertical-align: middle; + overflow: hidden; + background: #000; + color: #0f0; + font-family: monospace; + white-space: pre-wrap; + z-index: 2; +} + +/** BASIC PROPERTIES -- DO NOT MODIFY **/ +html { + touch-action: none; + user-select: none; + -webkit-user-select: none; +} +div { + background-size: 100% 100%; +} +#joypad { + display: none; width: 100vw; height: 100vh; +} + diff --git a/yoke/assets/joypad/base.js b/yoke/assets/joypad/base.js index 7381cdb..64b5811 100644 --- a/yoke/assets/joypad/base.js +++ b/yoke/assets/joypad/base.js @@ -1,4 +1,8 @@ 'use strict'; +// DEBUG SETTINGS: +// These 2 options are recommended for testing in non-kiosk/non-embedded browsers: +var WAIT_FOR_FULLSCREEN = false; +var DEBUG_NO_CONSOLE_SPAM = true; // SETTINGS: // Octant refers to the 8 sectors in which the area of a joystick is divided. @@ -7,9 +11,6 @@ var VIBRATE_ON_OCTANT_BOUNDARY = true; var VIBRATE_ON_PAD_BOUNDARY = true; var VIBRATE_PROPORTIONALLY_TO_DISTANCE = false; -// These 2 options are recommended for testing in non-kiosk/non-embedded browsers: -var WAIT_FOR_FULLSCREEN = false; -var DEBUG_NO_CONSOLE_SPAM = true; // Duration of vibration when clicking a button (onTouchStart): var VIBRATION_MILLISECONDS_IN = 40; // Duration of vibration when changing octants in a joystick: @@ -99,6 +100,44 @@ function truncate(val) { (val > 0x7fff) ? 0x7fff : Math.floor(val); } +function parseTime(c) { + if (c.endsWith('ms')) { + return parseFloat(c) / 1000; + } else if (c.endsWith('s') || parseFloat(c).toString() == c) { + return parseFloat(c); + } else { + return undefined; + } +} + +function parsePercentage(c) { + if (c.endsWith('%')) { + return parseFloat(c) / 100; + } else if (parseFloat(c).toString() == c) { + return parseFloat(c); + } else { + return undefined; + } +} + +function parseDistance(c) { + return (c.endsWith('px') || parseFloat(c).toString() == c) ? parseFloat(c) : undefined; +} + +function parseBoolean(c) { + switch (c) { + case 'yes': + case 'true': + return true; break; + case 'no': + case 'none': + case 'false': + return false; break; + default: + return undefined; break; + } +} + var serializer = new TextDecoder('iso-8859-1'); // HAPTIC FEEDBACK MIXING: @@ -401,7 +440,7 @@ function AnalogButton(id, updateStateCallback) { this.state = 0; this.oldState = 0; this.hitbox = document.createElement('div'); - this.hitbox.className = 'buttonhitbox'; + this.hitbox.className = 'hitbox'; this.element.appendChild(this.hitbox); } AnalogButton.prototype = Object.create(Control.prototype); @@ -535,7 +574,7 @@ function Button(id, updateStateCallback) { this.state = 0; this.oldState = 0; this.hitbox = document.createElement('div'); - this.hitbox.className = 'buttonhitbox'; + this.hitbox.className = 'hitbox'; this.element.appendChild(this.hitbox); } Button.prototype = Object.create(Control.prototype); @@ -832,6 +871,15 @@ Joypad.prototype.mnemonics = function(id, callback) { } } }; +Joypad.prototype.readVariable = function (key, vartype) { + var output = getComputedStyle(this.element).getPropertyValue(key) + .trim().replace(/\s+/g, ' ').toLocaleLowerCase('en-US').split(' ').map(vartype); + if (output.reduce(function (acc, cur) {return (acc || typeof cur === 'undefined');}, false)) { + window.Yoke.alert('Value for variable `' + key + '` could not be parsed. Unexpected behavior may occur.'); + } + return (output.length == 1) ? output[0] : output; +}; +Control.prototype.readVariable = Joypad.prototype.readVariable; // BASE CODE: From 9123b31e133eac54ac6d6e37648b792f9f2a78dc Mon Sep 17 00:00:00 2001 From: Medape Date: Thu, 6 Jan 2022 03:19:15 +0100 Subject: [PATCH 2/8] Read options from CSS, delete hardcoded values from JS --- yoke/assets/joypad/base.js | 159 ++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 83 deletions(-) diff --git a/yoke/assets/joypad/base.js b/yoke/assets/joypad/base.js index 64b5811..cbcdd1d 100644 --- a/yoke/assets/joypad/base.js +++ b/yoke/assets/joypad/base.js @@ -1,51 +1,12 @@ 'use strict'; // DEBUG SETTINGS: -// These 2 options are recommended for testing in non-kiosk/non-embedded browsers: +// These options are recommended for testing in non-kiosk/non-embedded browsers: var WAIT_FOR_FULLSCREEN = false; var DEBUG_NO_CONSOLE_SPAM = true; - -// SETTINGS: -// Octant refers to the 8 sectors in which the area of a joystick is divided. -// These 8 sectors are more or less aligned with the cardinal directions, -// and allow one to feel when one is changing directions without looking. -var VIBRATE_ON_OCTANT_BOUNDARY = true; -var VIBRATE_ON_PAD_BOUNDARY = true; -var VIBRATE_PROPORTIONALLY_TO_DISTANCE = false; -// Duration of vibration when clicking a button (onTouchStart): -var VIBRATION_MILLISECONDS_IN = 40; -// Duration of vibration when changing octants in a joystick: -var VIBRATION_MILLISECONDS_OVER = 20; -// Duration of vibration when forcing a control over the maximum or the minimum: -var VIBRATION_MILLISECONDS_SATURATION = [9, 11]; -// Duration of vibration when pushing a thumb joystick down or releasing it: -var VIBRATION_MILLISECONDS_THUMB = 45; -// Duration of vibration when clicking a D-Pad button (this.state change): -var VIBRATION_MILLISECONDS_DPAD = 35; -// Length, relative to the D-pad, of the hitbox of a D-pad leg, from border to center: -var DPAD_BUTTON_LENGTH = 0.4; -// Length, relative to the D-pad, of the hitbox of a D-pad leg, measured perpendicularly to the length: -var DPAD_BUTTON_WIDTH = 0.5; -// Pixels between apparent and real (oversized) hitbox of a button, to the left (and the right): -var BUTTON_OVERSHOOT_WIDTH = 7; -// Pixels between apparent and real (oversized) hitbox of a button, upwards (and downwards): -var BUTTON_OVERSHOOT_HEIGHT = 7; -// To normalize values from accelerometers: -var ACCELERATION_CONSTANT = 0.025; -// Multiplier for analog buttons in non-force-detecting mode. -// (Simulated force is multiplied by this number, and truncated in case of saturation.) -// The dead zone length/width, relative to the analog button hitbox length/width, -// will be 1 - (1 / ANALOG_BUTTON_DEADZONE_CONSTANT). -var ANALOG_BUTTON_DEADZONE_CONSTANT = 1.10; // If true, the controller will check for finger pressure detection. // If false, it will assume the screen cannot measure this, and load // alternate control methods (like number of fingers and distance to center). var PRESSURE_DETECTION_ENABLED = true; -// Pressure-sensitive controls will not be responsive to finger pressures lower than this: -var MINIMUM_FORCE = 0.1; -// Pressure-sensitive controls will not respond differently if finger pressures are stronger than this: -var MAXIMUM_FORCE = 0.4; -// This threshold is, for the moment, only used for the button under thumbstick -var THRESHOLD_FORCE = 0.2; // HELPER FUNCTIONS: if (typeof window.Yoke === 'undefined') { @@ -105,6 +66,8 @@ function parseTime(c) { return parseFloat(c) / 1000; } else if (c.endsWith('s') || parseFloat(c).toString() == c) { return parseFloat(c); + } else if (c == 'none' || c == 'no' || c == 'false') { + return 0; } else { return undefined; } @@ -115,13 +78,21 @@ function parsePercentage(c) { return parseFloat(c) / 100; } else if (parseFloat(c).toString() == c) { return parseFloat(c); + } else if (c == 'none' || c == 'no' || c == 'false') { + return 0; } else { return undefined; } } function parseDistance(c) { - return (c.endsWith('px') || parseFloat(c).toString() == c) ? parseFloat(c) : undefined; + if (c.endsWith('px') || parseFloat(c).toString() == c) { + return parseFloat(c); + } else if (c == 'none' || c == 'no' || c == 'false') { + return 0; + } else { + return undefined; + } } function parseBoolean(c) { @@ -129,8 +100,8 @@ function parseBoolean(c) { case 'yes': case 'true': return true; break; - case 'no': case 'none': + case 'no': case 'false': return false; break; default: @@ -191,10 +162,11 @@ Control.prototype.getBoundingClientRect = function() { this.offset.width = this.offset.height; } } else if (this.shape == 'overshoot') { - this.offset.x -= BUTTON_OVERSHOOT_WIDTH; - this.offset.y -= BUTTON_OVERSHOOT_HEIGHT; - this.offset.width += 2 * BUTTON_OVERSHOOT_WIDTH; - this.offset.height += 2 * BUTTON_OVERSHOOT_HEIGHT; + this.offset.overshootHitbox = this.readVariable('--overshoot-hitbox', parseDistance); + this.offset.x -= this.offset.overshootHitbox[0]; + this.offset.y -= this.offset.overshootHitbox[1]; + this.offset.width += 2 * this.offset.overshootHitbox[0]; + this.offset.height += 2 * this.offset.overshootHitbox[1]; } this.offset.halfWidth = this.offset.width / 2; this.offset.halfHeight = this.offset.height / 2; @@ -236,6 +208,13 @@ Joystick.prototype.onAttached = function() { } this.offset.factorX = 0x4000 / this.offset.halfWidth; this.offset.factorY = 0x4000 / this.offset.halfHeight; + this.vibrateOnClick = this.readVariable('--vibrate-on-click', parseTime) * 1000; + this.vibrateOnTouch = this.readVariable('--vibrate-on-touch', parseTime) * 1000; + this.vibrateOnEdge = this.readVariable('--vibrate-on-edge', parseTime) + .map(function (c) {return c * 1000;}); + this.vibrateOnOctantEdge = this.readVariable('--vibrate-on-octant-edge', parseTime) * 1000; + this.vibrateProportionallyToDistance = this.readVariable('--vibrate-proportionally-to-distance', parseBoolean); + this.forceThresholds = this.readVariable('--force-thresholds', parsePercentage); }; // This contant defines the limits of all the octants in analytic geometry: Joystick.prototype.EIGHTH_OF_RADIAN = 1 / Math.sin(Math.PI / 8); @@ -257,17 +236,17 @@ Joystick.prototype.onTouchMove = function(ev) { ((this.state[0] > this.state[1] * -this.EIGHTH_OF_RADIAN) ? 2 : 0) + ((this.state[0] * -this.EIGHTH_OF_RADIAN > this.state[1]) ? 4 : 0) + ((this.state[0] > this.state[1] * this.EIGHTH_OF_RADIAN) ? 8 : 0); - if (VIBRATE_ON_OCTANT_BOUNDARY && this.octant != -2 && this.octant != currentOctant) { - window.navigator.vibrate(VIBRATION_MILLISECONDS_OVER); + if (this.vibrateOnOctantEdge && this.octant != -2 && this.octant != currentOctant) { + window.navigator.vibrate(this.vibrateOnOctantEdge); } this.octant = currentOctant; } else { - if (VIBRATE_ON_PAD_BOUNDARY) { + if (this.vibrateOnEdge[0]) { queueForVibration(this.element.id, [ - VIBRATE_PROPORTIONALLY_TO_DISTANCE - ? distance / 0x4000 * VIBRATION_MILLISECONDS_SATURATION[0] - : VIBRATION_MILLISECONDS_SATURATION[0], - VIBRATION_MILLISECONDS_SATURATION[1] + this.vibrateProportionallyToDistance + ? distance / 0x4000 * this.vibrateOnEdge[0] + : this.vibrateOnEdge[0], + this.vibrateOnEdge[1] ]); } this.octant = -2; @@ -277,7 +256,7 @@ Joystick.prototype.onTouchMove = function(ev) { Joystick.prototype.onTouchStart = function(ev) { ev.preventDefault(); // Android Webview delays the vibration without this. this.onTouchMove(ev); - window.navigator.vibrate(VIBRATION_MILLISECONDS_IN); + window.navigator.vibrate(this.vibrateOnTouch); }; Joystick.prototype.onTouchEnd = function(ev) { if (ev.targetTouches.length == 0) { @@ -305,16 +284,16 @@ Joystick.prototype.checkThumbButton = function(ev) { this.state[2] = (ev.targetTouches.length > 1) ? 1 : 0; this.stateBuffer.setUint8(4, this.state[2]); if (this.oldButtonState != this.state[2]) { - window.navigator.vibrate(VIBRATION_MILLISECONDS_THUMB); + window.navigator.vibrate(this.vibrateOnClick); (this.state[2] == 0) ? this.element.classList.remove('pressed') : this.element.classList.add('pressed'); this.oldButtonState = this.state[2]; } }; Joystick.prototype.checkThumbButtonForce = function(ev) { - this.state[2] = (ev.targetTouches[0].force > THRESHOLD_FORCE) ? 1 : 0; + this.state[2] = (ev.targetTouches[0].force > this.forceThresholds[1]) ? 1 : 0; this.stateBuffer.setUint8(4, this.state[2]); if (this.oldButtonState != this.state[2]) { - window.navigator.vibrate(VIBRATION_MILLISECONDS_THUMB); + window.navigator.vibrate(this.vibrateOnClick); (this.state[2] == 0) ? this.element.classList.remove('pressed') : this.element.classList.add('pressed'); this.oldButtonState = this.state[2]; } @@ -349,11 +328,13 @@ function Motion(id, updateStateCallback) { this.element.appendChild(this.trinket); } Motion.prototype = Object.create(Control.prototype); -Motion.prototype.onAttached = function() {}; +Motion.prototype.onAttached = function() { + this.normalizationConstant = this.readVariable('--normalization-constant', parsePercentage); +}; Motion.prototype.onDeviceMotion = function(ev) { - motionState[0] = ev.accelerationIncludingGravity.x * ACCELERATION_CONSTANT; - motionState[1] = ev.accelerationIncludingGravity.y * ACCELERATION_CONSTANT; - motionState[2] = ev.accelerationIncludingGravity.z * ACCELERATION_CONSTANT; + motionState[0] = ev.accelerationIncludingGravity.x * this.normalizationConstant; + motionState[1] = ev.accelerationIncludingGravity.y * this.normalizationConstant; + motionState[2] = ev.accelerationIncludingGravity.z * this.normalizationConstant; joypad.controls.deviceMotion.forEach(function(c) { c.stateBuffer.setUint16(0, truncate(0x4000 * motionState[c.mask] + 0x4000), false); c.updateTrinket(); @@ -393,10 +374,14 @@ Pedal.prototype.onAttached = function() { this.element.addEventListener('touchmove', this.onTouchMove.bind(this), false); this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); this.element.addEventListener('touchcancel', this.onTouchEnd.bind(this), false); + this.vibrateOnEdge = this.readVariable('--vibrate-on-edge', parseTime) + .map(function (c) {return c * 1000;}); + this.vibrateOnTouch = this.readVariable('--vibrate-on-touch', parseTime) * 1000; + this.forceThresholds = this.readVariable('--force-thresholds', parsePercentage); }; Pedal.prototype.onTouchStart = function(ev) { ev.preventDefault(); // Android Webview delays the vibration without this. - window.navigator.vibrate(VIBRATION_MILLISECONDS_IN); + window.navigator.vibrate(this.vibrateOnTouch); this.onTouchMove(ev); this.element.classList.add('pressed'); }; @@ -406,7 +391,7 @@ Pedal.prototype.onTouchMove = function(ev) { var pos = ev.targetTouches[0]; this.state = (this.offset.y - pos.pageY) / this.offset.height + 1; if (this.state > 1) { - queueForVibration(this.element.id, VIBRATION_MILLISECONDS_SATURATION); + queueForVibration(this.element.id, this.vibrateOnEdge); } else { unqueueForVibration(this.element.id); } @@ -416,11 +401,11 @@ Pedal.prototype.onTouchMove = function(ev) { Pedal.prototype.onTouchMoveForce = function(ev) { // This is the replacement handler, which uses touch pressure. // Overwriting the handler once is much faster than checking - // MINIMUM_FORCE and MAXIMUM_FORCE at every updateStateCallback: + // minimum and maximum force at every updateStateCallback: var pos = ev.targetTouches[0]; - this.state = (pos.force - MINIMUM_FORCE) / (MAXIMUM_FORCE - MINIMUM_FORCE); + this.state = (pos.force - this.forceThresholds[0]) / (this.forceThresholds[1] - this.forceThresholds[0]); if (this.state > 1) { - queueForVibration(this.element.id, VIBRATION_MILLISECONDS_SATURATION); + queueForVibration(this.element.id, this.vibrateOnEdge); } else { unqueueForVibration(this.element.id); } @@ -455,13 +440,17 @@ AnalogButton.prototype.onAttached = function() { 'scaleX(', this.offset.width, ') ', 'scaleY(', this.offset.height, ')' ].join(''); + this.vibrateOnClick = this.readVariable('--vibrate-on-click', parseTime) * 1000; + this.forceThresholds = this.readVariable('--force-thresholds', parsePercentage); + this.centerDeadzone = this.readVariable('--center-deadzone', parsePercentage) + .map(function (c) {return 1 / (1 - c);}); }; AnalogButton.prototype.processTouches = function(ev) { this.state = 0; for (var touch of ev.touches) { - this.state = Math.max(this.state, ANALOG_BUTTON_DEADZONE_CONSTANT * Math.min( - 1 - Math.abs((touch.pageY - this.offset.yCenter) / this.offset.halfHeight), - 1 - Math.abs((touch.pageX - this.offset.xCenter) / this.offset.halfWidth) + this.state = Math.max(this.state, Math.min( + this.centerDeadzone[1] * (1 - Math.abs((touch.pageY - this.offset.yCenter) / this.offset.halfHeight)), + this.centerDeadzone[0] * (1 - Math.abs((touch.pageX - this.offset.xCenter) / this.offset.halfWidth)) )); } this.stateBuffer.setUint16(0, truncate(this.state * 0x8000), false); @@ -477,7 +466,7 @@ AnalogButton.prototype.processTouchesForce = function(ev) { for (var touch of ev.touches) { if (touch.pageX > this.offset.x && touch.pageX < this.offset.xMax && touch.pageY > this.offset.y && touch.pageY < this.offset.yMax) { - this.state = Math.max(this.state, (touch.force - MINIMUM_FORCE) / (MAXIMUM_FORCE - MINIMUM_FORCE)); + this.state = Math.max(this.state, (touch.force - this.forceThresholds[0]) / (this.forceThresholds[1] - this.forceThresholds[0])); } } this.stateBuffer.setUint16(0, truncate(this.state * 0x8000), false); @@ -500,7 +489,7 @@ AnalogButton.prototype.onTouchMove = function(ev) { }).reduce(function(acc, cur) {return acc || cur;}, false); this.updateStateCallback(); if (stateChanged) { - window.navigator.vibrate(VIBRATION_MILLISECONDS_IN); + window.navigator.vibrate(this.vibrateOnClick); } }; @@ -537,6 +526,8 @@ Knob.prototype.onAttached = function() { this.element.addEventListener('touchstart', this.onTouchStart.bind(this), false); this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); this.element.addEventListener('touchcancel', this.onTouchEnd.bind(this), false); + this.vibrateOnTouch = this.readVariable('--vibrate-on-touch', parseTime) * 1000; + this.vibrateOnOctantEdge = this.readVariable('--vibrate-on-octant-edge', parseTime); }; Knob.prototype.onTouchMove = function(ev) { var pos = ev.targetTouches[0]; @@ -548,8 +539,8 @@ Knob.prototype.onTouchMove = function(ev) { this.stateBuffer.setUint16(0, truncate(this.state * 0x8000), false); this.updateStateCallback(); var currentOctant = Math.floor(this.state * 8); - if (VIBRATE_ON_OCTANT_BOUNDARY && this.octant != currentOctant) { - window.navigator.vibrate(VIBRATION_MILLISECONDS_OVER); + if (this.vibrateOnOctantEdge && this.octant != currentOctant) { + window.navigator.vibrate(this.vibrateOnOctantEdge); } this.octant = currentOctant; this.updateCircles(); @@ -559,7 +550,7 @@ Knob.prototype.onTouchStart = function(ev) { var pos = ev.targetTouches[0]; this.initState = this.state - (Math.atan2(pos.pageY - this.offset.yCenter, pos.pageX - this.offset.xCenter) / (2 * Math.PI)) + 1; - window.navigator.vibrate(VIBRATION_MILLISECONDS_IN); + window.navigator.vibrate(this.vibrateOnTouch); }; Knob.prototype.onTouchEnd = function() { this.updateStateCallback(); @@ -616,14 +607,16 @@ DPad.prototype.onAttached = function() { this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); this.element.addEventListener('touchcancel', this.onTouchEnd.bind(this), false); // Precalculate the borders of the buttons: - this.offset.x1 = this.offset.xCenter - DPAD_BUTTON_WIDTH * this.offset.halfWidth; - this.offset.x2 = this.offset.xCenter + DPAD_BUTTON_WIDTH * this.offset.halfWidth; - this.offset.up_y = this.offset.y + DPAD_BUTTON_LENGTH * this.offset.height; - this.offset.down_y = this.offset.yMax - DPAD_BUTTON_LENGTH * this.offset.height; - this.offset.y1 = this.offset.yCenter - DPAD_BUTTON_WIDTH * this.offset.halfHeight; - this.offset.y2 = this.offset.yCenter + DPAD_BUTTON_WIDTH * this.offset.halfHeight; - this.offset.left_x = this.offset.x + DPAD_BUTTON_LENGTH * this.offset.width; - this.offset.right_x = this.offset.xMax - DPAD_BUTTON_LENGTH * this.offset.width; + this.offset.buttonHitbox = this.readVariable('--button-hitbox', parsePercentage); + this.offset.x1 = this.offset.xCenter - this.offset.buttonHitbox[1] * this.offset.halfWidth; + this.offset.x2 = this.offset.xCenter + this.offset.buttonHitbox[1] * this.offset.halfWidth; + this.offset.up_y = this.offset.y + this.offset.buttonHitbox[0] * this.offset.height; + this.offset.down_y = this.offset.yMax - this.offset.buttonHitbox[0] * this.offset.height; + this.offset.y1 = this.offset.yCenter - this.offset.buttonHitbox[1] * this.offset.halfHeight; + this.offset.y2 = this.offset.yCenter + this.offset.buttonHitbox[1] * this.offset.halfHeight; + this.offset.left_x = this.offset.x + this.offset.buttonHitbox[0] * this.offset.width; + this.offset.right_x = this.offset.xMax - this.offset.buttonHitbox[0] * this.offset.width; + this.vibrateOnClick = this.readVariable('--vibrate-on-click', parseTime) * 1000; }; DPad.prototype.onTouchStart = function(ev) { ev.preventDefault(); // Android Webview delays the vibration without this. @@ -655,7 +648,7 @@ DPad.prototype.onTouchMove = function(ev) { var currentState = this.stateBuffer.reduce(function(acc, cur) {return (acc << 1) + cur;}, 0); if (currentState != this.oldState) { this.oldState = currentState; this.updateButtons(currentState); - window.navigator.vibrate(VIBRATION_MILLISECONDS_DPAD); + window.navigator.vibrate(this.vibrateOnClick); } }; DPad.prototype.onTouchEnd = function() { From 891ca49b54884a44ce4b4b8b5873e3a832d5683f Mon Sep 17 00:00:00 2001 From: Medape Date: Thu, 6 Jan 2022 17:50:54 +0100 Subject: [PATCH 3/8] Add support for joystick shapes and deadzones Joysticks can now be elliptical, square, round or rectangular. A central deadzone is also supported, which depends on the joystick shape: rectangular joysticks have axial deadzones (evaluated separately for each component), while round joysticks have a single radial deadzone. Both deadzones are scaled, meaning that output grows gradually from the edge of the deadzone to the edge of the joystick. --- yoke/assets/joypad/base.css | 26 ++++- yoke/assets/joypad/base.js | 205 +++++++++++++++++++++++++++++------- 2 files changed, 186 insertions(+), 45 deletions(-) diff --git a/yoke/assets/joypad/base.css b/yoke/assets/joypad/base.css index d1c1250..8037b41 100644 --- a/yoke/assets/joypad/base.css +++ b/yoke/assets/joypad/base.css @@ -27,7 +27,7 @@ --vibrate-on-touch: 40ms; /* Width and height of a central deadzone: * (round or square, depending on the shape of the control itself). */ - --center-deadzone: 9% 9%; + --center-deadzone: 10% 10%; /* Calibration for pressure-sensitive controls. * The first value is the minimum pressure that will engage it, * while the second value is the pressure that will saturate it. @@ -40,15 +40,31 @@ /* Joysticks */ .joystick { - background-image: url("img/joystick.svg"); - background-color: hsl(196, 77%, 55%); - border: 3px hsl(188, 100%, 50%) solid; --vibrate-on-click: 45ms; /* Calibration for finger pressure. * On joysticks, the first value has no effect yet. * The second value is only used on joysticks with a L3/R3 button below, * and determines at which pressure is it considered pressed. */ --force-thresholds: 10% 20%; + /* Determines the shape of both the joystick itself and the central deadzone, if any. + * If circle or ellipse, specify only one percentage (from center to edge). + * If rectangle or square, specify the percentage per axis */ + --shape: rectangle; + --center-deadzone: 10% 10%; +} +.joystick.ellipse, .joystick.thumb { + --shape: circle; + --center-deadzone: 15%; +} +.joysticksquare { + background-image: url("img/joystick.svg"); + background-color: hsl(196, 77%, 55%); + border: 3px hsl(188, 100%, 50%) solid; + position: absolute; + box-sizing: border-box; +} +.joysticksquare.ellipse { + border-radius: 100%; } .circle { background-color: black; @@ -61,7 +77,7 @@ top: 0px; } .joystick.thumb > .circle { background-color: #444; } -.joystick.pressed { +.joysticksquare.pressed { background-color: hsl(196, 77%, 33%); border: 3px hsl(188, 100%, 30%) solid; } diff --git a/yoke/assets/joypad/base.js b/yoke/assets/joypad/base.js index cbcdd1d..9923635 100644 --- a/yoke/assets/joypad/base.js +++ b/yoke/assets/joypad/base.js @@ -63,9 +63,9 @@ function truncate(val) { function parseTime(c) { if (c.endsWith('ms')) { - return parseFloat(c) / 1000; - } else if (c.endsWith('s') || parseFloat(c).toString() == c) { return parseFloat(c); + } else if (c.endsWith('s') || parseFloat(c).toString() == c) { + return parseFloat(c) * 1000; } else if (c == 'none' || c == 'no' || c == 'false') { return 0; } else { @@ -153,7 +153,7 @@ function Control(type, id, updateStateCallback) { Control.prototype.shape = 'rectangle'; Control.prototype.getBoundingClientRect = function() { this.offset = this.element.getBoundingClientRect(); - if (this.shape == 'square') { + if (this.shape == 'square' || this.shape == 'circle') { if (this.offset.width < this.offset.height) { this.offset.y += (this.offset.height - this.offset.width) / 2; this.offset.height = this.offset.width; @@ -176,6 +176,7 @@ Control.prototype.getBoundingClientRect = function() { this.offset.yMax = this.offset.y + this.offset.height; }; Control.prototype.onAttached = function() {}; +Control.prototype.readConfig = function() {}; Control.prototype.setBufferView = function(cursor, buffer) { this.stateBuffer = new DataView(buffer, cursor, 2); this.stateBuffer.setUint16(0, 0x0000 + cursor, false); @@ -192,6 +193,13 @@ function Joystick(id, updateStateCallback) { this.state = [0, 0]; this.checkThumbButton = function() {}; } + this.joystickSquare = document.createElement('div'); + this.joystickSquare.className = 'joysticksquare'; + this.element.appendChild(this.joystickSquare); + if (this.element.id[0] == 't') { + this.element.classList.add('thumb'); + } + this.circle = document.createElement('div'); this.circle.className = 'circle'; this.element.appendChild(this.circle); @@ -199,20 +207,41 @@ function Joystick(id, updateStateCallback) { Joystick.prototype = Object.create(Control.prototype); Joystick.prototype.onAttached = function() { this.updateCircle(this.offset.xCenter, this.offset.yCenter); + // Resizing the joystick to fit whatever shape is specified. + this.joystickSquare.style.top = this.offset.y + 'px'; + this.joystickSquare.style.left = this.offset.x + 'px'; + this.joystickSquare.style.height = this.offset.height + 'px'; + this.joystickSquare.style.width = this.offset.width + 'px'; this.element.addEventListener('touchmove', this.onTouchMove.bind(this), false); this.element.addEventListener('touchstart', this.onTouchStart.bind(this), false); this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); this.element.addEventListener('touchcancel', this.onTouchEnd.bind(this), false); - if (this.element.id[0] == 't') { - this.element.classList.add('thumb'); + if (this.shape == 'circle' || this.shape == 'ellipse') { + this.offset.factorX = 0x4000 / this.offset.halfWidth; + this.offset.factorY = 0x4000 / this.offset.halfHeight; + this.offset.factorR = 0x4000 / (0x4000 - this.centerDeadzone); + } else { + this.offset.xDeadLow = this.offset.xCenter - this.offset.halfWidth * this.centerDeadzone[0]; + this.offset.xDeadHigh = this.offset.xCenter + this.offset.halfWidth * this.centerDeadzone[0]; + this.offset.yDeadLow = this.offset.yCenter - this.offset.halfHeight * this.centerDeadzone[1]; + this.offset.yDeadHigh = this.offset.yCenter + this.offset.halfHeight * this.centerDeadzone[1]; + this.offset.factorX = 0x4000 / (this.offset.halfWidth * (1 - this.centerDeadzone[0])); + this.offset.factorY = 0x4000 / (this.offset.halfHeight * (1 - this.centerDeadzone[1])); + } +}; +Joystick.prototype.readConfig = function() { + this.centerDeadzone = this.readVariable('--center-deadzone', parsePercentage); + this.shape = this.readVariable('--shape', 'circle|ellipse|rectangle|square'); + if (this.shape == 'circle' || this.shape == 'ellipse') { + this.centerDeadzone = this.centerDeadzone * 0x4000; + this.joystickSquare.classList.add('ellipse'); + this.onTouchMove = this.onTouchMoveCircle; + this.updateCircle = this.updateCircleCircle; } - this.offset.factorX = 0x4000 / this.offset.halfWidth; - this.offset.factorY = 0x4000 / this.offset.halfHeight; - this.vibrateOnClick = this.readVariable('--vibrate-on-click', parseTime) * 1000; - this.vibrateOnTouch = this.readVariable('--vibrate-on-touch', parseTime) * 1000; - this.vibrateOnEdge = this.readVariable('--vibrate-on-edge', parseTime) - .map(function (c) {return c * 1000;}); - this.vibrateOnOctantEdge = this.readVariable('--vibrate-on-octant-edge', parseTime) * 1000; + this.vibrateOnClick = this.readVariable('--vibrate-on-click', parseTime); + this.vibrateOnTouch = this.readVariable('--vibrate-on-touch', parseTime); + this.vibrateOnEdge = this.readVariable('--vibrate-on-edge', parseTime); + this.vibrateOnOctantEdge = this.readVariable('--vibrate-on-octant-edge', parseTime); this.vibrateProportionallyToDistance = this.readVariable('--vibrate-proportionally-to-distance', parseBoolean); this.forceThresholds = this.readVariable('--force-thresholds', parsePercentage); }; @@ -220,8 +249,20 @@ Joystick.prototype.onAttached = function() { Joystick.prototype.EIGHTH_OF_RADIAN = 1 / Math.sin(Math.PI / 8); Joystick.prototype.onTouchMove = function(ev) { var pos = ev.targetTouches[0]; - this.state[0] = (pos.pageX - this.offset.xCenter) * this.offset.factorX; - this.state[1] = (pos.pageY - this.offset.yCenter) * this.offset.factorY; + if (pos.pageX < this.offset.xDeadLow) { + this.state[0] = (pos.pageX - this.offset.xDeadLow) * this.offset.factorX; + } else if (pos.pageX > this.offset.xDeadHigh) { + this.state[0] = (pos.pageX - this.offset.xDeadHigh) * this.offset.factorX; + } else { + this.state[0] = 0; + } + if (pos.pageY < this.offset.yDeadLow) { + this.state[1] = (pos.pageY - this.offset.yDeadLow) * this.offset.factorY; + } else if (pos.pageY > this.offset.yDeadHigh) { + this.state[1] = (pos.pageY - this.offset.yDeadHigh) * this.offset.factorY; + } else { + this.state[1] = 0; + } this.stateBuffer.setUint16(0, truncate(this.state[0] + 0x4000), false); this.stateBuffer.setUint16(2, truncate(this.state[1] + 0x4000), false); this.checkThumbButton(ev); @@ -253,6 +294,50 @@ Joystick.prototype.onTouchMove = function(ev) { } this.updateCircle(pos.pageX, pos.pageY); }; +Joystick.prototype.onTouchMoveCircle = function(ev) { + var pos = ev.targetTouches[0]; + this.state[0] = (pos.pageX - this.offset.xCenter) * this.offset.factorX; + this.state[1] = (pos.pageY - this.offset.yCenter) * this.offset.factorY; + this.checkThumbButton(ev); + var distance = Math.hypot(this.state[0], this.state[1]); + if (distance < this.centerDeadzone) { + this.state[0] = 0; + this.state[1] = 0; + this.octant = -2; + } else if (distance < 0x4000) { + var newDistance = (distance - this.centerDeadzone) * this.offset.factorR; + this.state[0] *= newDistance / distance; + this.state[1] *= newDistance / distance; + unqueueForVibration(this.element.id); + // Instead of calculating an accurate angle, we hardcode the limits of each octant using analytic geometry: + // the lines that divide the sector are x/sin(π/8)=y, etc. + var currentOctant = + ((this.state[0] * this.EIGHTH_OF_RADIAN > this.state[1]) ? 1 : 0) + + ((this.state[0] > this.state[1] * -this.EIGHTH_OF_RADIAN) ? 2 : 0) + + ((this.state[0] * -this.EIGHTH_OF_RADIAN > this.state[1]) ? 4 : 0) + + ((this.state[0] > this.state[1] * this.EIGHTH_OF_RADIAN) ? 8 : 0); + if (this.vibrateOnOctantEdge && this.octant != -2 && this.octant != currentOctant) { + window.navigator.vibrate(this.vibrateOnOctantEdge); + } + this.octant = currentOctant; + } else { + if (this.vibrateOnEdge[0]) { + queueForVibration(this.element.id, [ + this.vibrateProportionallyToDistance + ? distance / 0x4000 * this.vibrateOnEdge[0] + : this.vibrateOnEdge[0], + this.vibrateOnEdge[1] + ]); + } + this.state[0] *= 0x4000 / distance; + this.state[1] *= 0x4000 / distance; + this.octant = -2; + } + this.stateBuffer.setUint16(0, truncate(this.state[0] + 0x4000), false); + this.stateBuffer.setUint16(2, truncate(this.state[1] + 0x4000), false); + this.updateStateCallback(); + this.updateCircle(pos.pageX, pos.pageY, distance); +}; Joystick.prototype.onTouchStart = function(ev) { ev.preventDefault(); // Android Webview delays the vibration without this. this.onTouchMove(ev); @@ -269,7 +354,7 @@ Joystick.prototype.onTouchEnd = function(ev) { if (this.state.length == 3) { this.state[2] = 0; this.stateBuffer.setUint8(4, 0); - this.element.classList.remove('pressed'); + this.joystickSquare.classList.remove('pressed'); this.oldButtonState = 0; } this.updateStateCallback(); @@ -285,7 +370,7 @@ Joystick.prototype.checkThumbButton = function(ev) { this.stateBuffer.setUint8(4, this.state[2]); if (this.oldButtonState != this.state[2]) { window.navigator.vibrate(this.vibrateOnClick); - (this.state[2] == 0) ? this.element.classList.remove('pressed') : this.element.classList.add('pressed'); + (this.state[2] == 0) ? this.joystickSquare.classList.remove('pressed') : this.joystickSquare.classList.add('pressed'); this.oldButtonState = this.state[2]; } }; @@ -294,7 +379,7 @@ Joystick.prototype.checkThumbButtonForce = function(ev) { this.stateBuffer.setUint8(4, this.state[2]); if (this.oldButtonState != this.state[2]) { window.navigator.vibrate(this.vibrateOnClick); - (this.state[2] == 0) ? this.element.classList.remove('pressed') : this.element.classList.add('pressed'); + (this.state[2] == 0) ? this.joystickSquare.classList.remove('pressed') : this.joystickSquare.classList.add('pressed'); this.oldButtonState = this.state[2]; } }; @@ -303,6 +388,18 @@ Joystick.prototype.updateCircle = function(x, y) { Math.max(Math.min(x, this.offset.xMax), this.offset.x) + 'px, ' + Math.max(Math.min(y, this.offset.yMax), this.offset.y) + 'px)'; }; +Joystick.prototype.updateCircleCircle = function(x, y, distance) { + if (distance > 0x4000) { + this.circle.style.transform = 'translate(-50%, -50%) translate(' + + ((x - this.offset.xCenter) * 0x4000 / distance + this.offset.xCenter) + 'px, ' + + ((y - this.offset.yCenter) * 0x4000 / distance + this.offset.yCenter) + 'px)'; + } else { + // also runs if distance is undefined: + this.circle.style.transform = 'translate(-50%, -50%) translate(' + + x + 'px, ' + + y + 'px)'; + } +}; Joystick.prototype.setBufferView = function(cursor, buffer) { if (this.element.id[0] == 't') { this.stateBuffer = new DataView(buffer, cursor, 5); @@ -328,7 +425,7 @@ function Motion(id, updateStateCallback) { this.element.appendChild(this.trinket); } Motion.prototype = Object.create(Control.prototype); -Motion.prototype.onAttached = function() { +Motion.prototype.readConfig = function() { this.normalizationConstant = this.readVariable('--normalization-constant', parsePercentage); }; Motion.prototype.onDeviceMotion = function(ev) { @@ -374,9 +471,10 @@ Pedal.prototype.onAttached = function() { this.element.addEventListener('touchmove', this.onTouchMove.bind(this), false); this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); this.element.addEventListener('touchcancel', this.onTouchEnd.bind(this), false); - this.vibrateOnEdge = this.readVariable('--vibrate-on-edge', parseTime) - .map(function (c) {return c * 1000;}); - this.vibrateOnTouch = this.readVariable('--vibrate-on-touch', parseTime) * 1000; +}; +Pedal.prototype.readConfig = function() { + this.vibrateOnEdge = this.readVariable('--vibrate-on-edge', parseTime); + this.vibrateOnTouch = this.readVariable('--vibrate-on-touch', parseTime); this.forceThresholds = this.readVariable('--force-thresholds', parsePercentage); }; Pedal.prototype.onTouchStart = function(ev) { @@ -440,7 +538,9 @@ AnalogButton.prototype.onAttached = function() { 'scaleX(', this.offset.width, ') ', 'scaleY(', this.offset.height, ')' ].join(''); - this.vibrateOnClick = this.readVariable('--vibrate-on-click', parseTime) * 1000; +}; +AnalogButton.prototype.readConfig = function() { + this.vibrateOnClick = this.readVariable('--vibrate-on-click', parseTime); this.forceThresholds = this.readVariable('--force-thresholds', parsePercentage); this.centerDeadzone = this.readVariable('--center-deadzone', parsePercentage) .map(function (c) {return 1 / (1 - c);}); @@ -526,7 +626,9 @@ Knob.prototype.onAttached = function() { this.element.addEventListener('touchstart', this.onTouchStart.bind(this), false); this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); this.element.addEventListener('touchcancel', this.onTouchEnd.bind(this), false); - this.vibrateOnTouch = this.readVariable('--vibrate-on-touch', parseTime) * 1000; +}; +Knob.prototype.readConfig = function() { + this.vibrateOnTouch = this.readVariable('--vibrate-on-touch', parseTime); this.vibrateOnOctantEdge = this.readVariable('--vibrate-on-octant-edge', parseTime); }; Knob.prototype.onTouchMove = function(ev) { @@ -607,16 +709,18 @@ DPad.prototype.onAttached = function() { this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); this.element.addEventListener('touchcancel', this.onTouchEnd.bind(this), false); // Precalculate the borders of the buttons: - this.offset.buttonHitbox = this.readVariable('--button-hitbox', parsePercentage); - this.offset.x1 = this.offset.xCenter - this.offset.buttonHitbox[1] * this.offset.halfWidth; - this.offset.x2 = this.offset.xCenter + this.offset.buttonHitbox[1] * this.offset.halfWidth; - this.offset.up_y = this.offset.y + this.offset.buttonHitbox[0] * this.offset.height; - this.offset.down_y = this.offset.yMax - this.offset.buttonHitbox[0] * this.offset.height; - this.offset.y1 = this.offset.yCenter - this.offset.buttonHitbox[1] * this.offset.halfHeight; - this.offset.y2 = this.offset.yCenter + this.offset.buttonHitbox[1] * this.offset.halfHeight; - this.offset.left_x = this.offset.x + this.offset.buttonHitbox[0] * this.offset.width; - this.offset.right_x = this.offset.xMax - this.offset.buttonHitbox[0] * this.offset.width; - this.vibrateOnClick = this.readVariable('--vibrate-on-click', parseTime) * 1000; + this.offset.x1 = this.offset.xCenter - this.buttonHitbox[1] * this.offset.halfWidth; + this.offset.x2 = this.offset.xCenter + this.buttonHitbox[1] * this.offset.halfWidth; + this.offset.up_y = this.offset.y + this.buttonHitbox[0] * this.offset.height; + this.offset.down_y = this.offset.yMax - this.buttonHitbox[0] * this.offset.height; + this.offset.y1 = this.offset.yCenter - this.buttonHitbox[1] * this.offset.halfHeight; + this.offset.y2 = this.offset.yCenter + this.buttonHitbox[1] * this.offset.halfHeight; + this.offset.left_x = this.offset.x + this.buttonHitbox[0] * this.offset.width; + this.offset.right_x = this.offset.xMax - this.buttonHitbox[0] * this.offset.width; + this.vibrateOnClick = this.readVariable('--vibrate-on-click', parseTime); +}; +DPad.prototype.readConfig = function() { + this.buttonHitbox = this.readVariable('--button-hitbox', parsePercentage); }; DPad.prototype.onTouchStart = function(ev) { ev.preventDefault(); // Android Webview delays the vibration without this. @@ -751,6 +855,7 @@ function Joypad() { var cursor = 1; this.controls.byNumID.forEach(function(c) { this.element.appendChild(c.element); + c.readConfig(); cursor = c.setBufferView(cursor, this.stateBuffer); c.getBoundingClientRect(); c.onAttached(); @@ -859,20 +964,40 @@ Joypad.prototype.mnemonics = function(id, callback) { return new DPad(id, callback); } default: - window.Yoke.alert('Unrecognised control `' + id + '` at user.css.'); + window.Yoke.alert('Unrecognized control `' + id + '` at user.css.'); return null; } } }; -Joypad.prototype.readVariable = function (key, vartype) { +Control.prototype.readVariable = function (key, vartype) { var output = getComputedStyle(this.element).getPropertyValue(key) - .trim().replace(/\s+/g, ' ').toLocaleLowerCase('en-US').split(' ').map(vartype); - if (output.reduce(function (acc, cur) {return (acc || typeof cur === 'undefined');}, false)) { - window.Yoke.alert('Value for variable `' + key + '` could not be parsed. Unexpected behavior may occur.'); + .trim().replace(/\s+/g, ' ').toLocaleLowerCase('en-US').split(' '); + switch (typeof vartype) { + case 'undefined': + return output; + break; + case 'string': + output = output[0]; + if (vartype.split('|').indexOf(output) > -1) { + return output; + } else { + window.Yoke.alert('Value for variable `' + key + '` was unexpected. Unexpected behavior may occur.'); + return undefined; + } + break; + case 'function': + output = output.map(vartype); + if (output.reduce(function (acc, cur) {return (acc || typeof cur === 'undefined');}, false)) { + window.Yoke.alert('Value for variable `' + key + '` could not be parsed. Unexpected behavior may occur.'); + } + return (output.length == 1) ? output[0] : output; + break; + default: + window.Yoke.alert('Method to parse variable `' + key + '` not specified.'); + return undefined; + break; } - return (output.length == 1) ? output[0] : output; }; -Control.prototype.readVariable = Joypad.prototype.readVariable; // BASE CODE: From dad3ce78804055267dabeed2ac4955b78f0c4e11 Mon Sep 17 00:00:00 2001 From: Medape Date: Thu, 6 Jan 2022 21:07:01 +0100 Subject: [PATCH 4/8] Image for pedals was yet to be committed --- yoke/assets/joypad/img/pedal.svg | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 yoke/assets/joypad/img/pedal.svg diff --git a/yoke/assets/joypad/img/pedal.svg b/yoke/assets/joypad/img/pedal.svg new file mode 100644 index 0000000..d700bab --- /dev/null +++ b/yoke/assets/joypad/img/pedal.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + From 06466ac13f6aabfba998bf0811d14920a2d4a724 Mon Sep 17 00:00:00 2001 From: Medape Date: Fri, 7 Jan 2022 01:37:29 +0100 Subject: [PATCH 5/8] Regularize shapes across controls - D-pad and knobs are compatible now with all four shapes - D-pads, knobs, and joysticks use similar CSS and JS - Knobs have now an image (made with Inkscape and manually trimmed down), - D-pads use now CSS filters instead of hardcoded shadows --- yoke/assets/joypad/base.css | 56 ++++++++++++++----------- yoke/assets/joypad/base.js | 73 ++++++++++++++++++--------------- yoke/assets/joypad/img/dp.svg | 40 ------------------ yoke/assets/joypad/img/knob.svg | 15 +++++++ 4 files changed, 87 insertions(+), 97 deletions(-) create mode 100644 yoke/assets/joypad/img/knob.svg diff --git a/yoke/assets/joypad/base.css b/yoke/assets/joypad/base.css index 8037b41..9a76e18 100644 --- a/yoke/assets/joypad/base.css +++ b/yoke/assets/joypad/base.css @@ -33,6 +33,15 @@ * while the second value is the pressure that will saturate it. * The meaning for joysticks is slightly different; see below. */ --force-thresholds: 10% 40%; + /* The controller always is centered in the area assigned to it. + * Some controls can have different shapes: + * - rectangle (covers the whole area); + * - square (covers the maximum square that fits in the area) + * - ellipse (whose axes match the area's width and height) + * - circle (covers the maximum circle that fits in the area) + * Not all of the controllers accept all shapes. Some of them will + * be rectangular in spite of this setting: */ + --shape: rectangle; } /* Variables after this line override the defaults above for specific controls. @@ -56,14 +65,14 @@ --shape: circle; --center-deadzone: 15%; } -.joysticksquare { +.joystick > .inner { background-image: url("img/joystick.svg"); background-color: hsl(196, 77%, 55%); border: 3px hsl(188, 100%, 50%) solid; position: absolute; box-sizing: border-box; } -.joysticksquare.ellipse { +.joystick > .inner.ellipse { border-radius: 100%; } .circle { @@ -77,7 +86,7 @@ top: 0px; } .joystick.thumb > .circle { background-color: #444; } -.joysticksquare.pressed { +.joystick > .inner.pressed { background-color: hsl(196, 77%, 33%); border: 3px hsl(188, 100%, 30%) solid; } @@ -149,22 +158,27 @@ /* D-pad */ .dpad { - background-image: url('img/dp.svg'); --vibrate-on-click: 35ms; /* Length and width of a D-pad button hitbox, given as percentages of the * length and width of the whole D-pad. * Buttons should overlap slightly to allow for easy diagonal movement. */ --button-hitbox: 40% 50%; + --shape: square; +} +.dpad > .inner { + background-image: url('img/dp.svg'); + position: absolute; } -.dpad.u { background-image: url('img/dp.svg#u'); } -.dpad.ur { background-image: url('img/dp.svg#ur'); } -.dpad.r { background-image: url('img/dp.svg#r'); } -.dpad.dr { background-image: url('img/dp.svg#dr'); } -.dpad.d { background-image: url('img/dp.svg#d'); } -.dpad.dl { background-image: url('img/dp.svg#dl'); } -.dpad.l { background-image: url('img/dp.svg#l'); } -.dpad.ul { background-image: url('img/dp.svg#ul'); } -.dpad.all { background-image: url('img/dp.svg#all'); } +.dpad > .inner:not(.none) { filter: brightness(85%); } +.dpad > .inner.u { transform: scale(97%) rotateX( 15deg); } +.dpad > .inner.ur { transform: scale(97%) rotate3d( 1, 1, 0, 15deg); } +.dpad > .inner.r { transform: scale(97%) rotateY( 15deg); } +.dpad > .inner.dr { transform: scale(97%) rotate3d( 1, -1, 0, 15deg); } +.dpad > .inner.d { transform: scale(97%) rotateX(-15deg); } +.dpad > .inner.dl { transform: scale(97%) rotate3d(-1, -1, 0, 15deg); } +.dpad > .inner.l { transform: scale(97%) rotateY(-15deg); } +.dpad > .inner.ul { transform: scale(97%) rotate3d(-1, 1, 0, 15deg); } +.dpad > .inner.all { transform: scale(97%); } /* Motion controls */ .motion { @@ -181,19 +195,13 @@ #pt { background-image: url('img/pedal.svg#accelerator') } /* Knobs */ -.knob { } -.knobcircle { - position: absolute; - border-radius: 100%; - transform: rotate(0); - background-color: #888; +.knob { + --shape: circle; } - -.knobcircle > .circle { +.knob > .inner { + background-image: url('img/knob.svg'); position: absolute; - top: 50%; - left: 90%; - transform: translate(-50%, -50%); + transform: rotate(0); } /** GENERAL STYLE **/ diff --git a/yoke/assets/joypad/base.js b/yoke/assets/joypad/base.js index 9923635..cf98e96 100644 --- a/yoke/assets/joypad/base.js +++ b/yoke/assets/joypad/base.js @@ -193,9 +193,9 @@ function Joystick(id, updateStateCallback) { this.state = [0, 0]; this.checkThumbButton = function() {}; } - this.joystickSquare = document.createElement('div'); - this.joystickSquare.className = 'joysticksquare'; - this.element.appendChild(this.joystickSquare); + this.inner = document.createElement('div'); + this.inner.className = 'inner'; + this.element.appendChild(this.inner); if (this.element.id[0] == 't') { this.element.classList.add('thumb'); } @@ -208,10 +208,10 @@ Joystick.prototype = Object.create(Control.prototype); Joystick.prototype.onAttached = function() { this.updateCircle(this.offset.xCenter, this.offset.yCenter); // Resizing the joystick to fit whatever shape is specified. - this.joystickSquare.style.top = this.offset.y + 'px'; - this.joystickSquare.style.left = this.offset.x + 'px'; - this.joystickSquare.style.height = this.offset.height + 'px'; - this.joystickSquare.style.width = this.offset.width + 'px'; + this.inner.style.top = this.offset.y + 'px'; + this.inner.style.left = this.offset.x + 'px'; + this.inner.style.height = this.offset.height + 'px'; + this.inner.style.width = this.offset.width + 'px'; this.element.addEventListener('touchmove', this.onTouchMove.bind(this), false); this.element.addEventListener('touchstart', this.onTouchStart.bind(this), false); this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); @@ -234,7 +234,7 @@ Joystick.prototype.readConfig = function() { this.shape = this.readVariable('--shape', 'circle|ellipse|rectangle|square'); if (this.shape == 'circle' || this.shape == 'ellipse') { this.centerDeadzone = this.centerDeadzone * 0x4000; - this.joystickSquare.classList.add('ellipse'); + this.inner.classList.add('ellipse'); this.onTouchMove = this.onTouchMoveCircle; this.updateCircle = this.updateCircleCircle; } @@ -354,7 +354,7 @@ Joystick.prototype.onTouchEnd = function(ev) { if (this.state.length == 3) { this.state[2] = 0; this.stateBuffer.setUint8(4, 0); - this.joystickSquare.classList.remove('pressed'); + this.inner.classList.remove('pressed'); this.oldButtonState = 0; } this.updateStateCallback(); @@ -370,7 +370,7 @@ Joystick.prototype.checkThumbButton = function(ev) { this.stateBuffer.setUint8(4, this.state[2]); if (this.oldButtonState != this.state[2]) { window.navigator.vibrate(this.vibrateOnClick); - (this.state[2] == 0) ? this.joystickSquare.classList.remove('pressed') : this.joystickSquare.classList.add('pressed'); + (this.state[2] == 0) ? this.inner.classList.remove('pressed') : this.inner.classList.add('pressed'); this.oldButtonState = this.state[2]; } }; @@ -379,7 +379,7 @@ Joystick.prototype.checkThumbButtonForce = function(ev) { this.stateBuffer.setUint8(4, this.state[2]); if (this.oldButtonState != this.state[2]) { window.navigator.vibrate(this.vibrateOnClick); - (this.state[2] == 0) ? this.joystickSquare.classList.remove('pressed') : this.joystickSquare.classList.add('pressed'); + (this.state[2] == 0) ? this.inner.classList.remove('pressed') : this.inner.classList.add('pressed'); this.oldButtonState = this.state[2]; } }; @@ -605,21 +605,17 @@ function Knob(id, updateStateCallback) { this.state = 0.5; this.initState = 0.5; // state at onTouchStart this.initTransform = ''; // style.transform - this.knobCircle = document.createElement('div'); - this.knobCircle.className = 'knobcircle'; - this.element.appendChild(this.knobCircle); - this.circle = document.createElement('div'); - this.circle.className = 'circle'; - this.knobCircle.appendChild(this.circle); + this.inner = document.createElement('div'); + this.inner.className = 'inner'; + this.element.appendChild(this.inner); } Knob.prototype = Object.create(Control.prototype); -Knob.prototype.shape = 'square'; Knob.prototype.onAttached = function() { // Centering the knob within the boundary. - this.knobCircle.style.top = this.offset.y + 'px'; - this.knobCircle.style.left = this.offset.x + 'px'; - this.knobCircle.style.height = this.offset.height + 'px'; - this.knobCircle.style.width = this.offset.width + 'px'; + this.inner.style.top = this.offset.y + 'px'; + this.inner.style.left = this.offset.x + 'px'; + this.inner.style.height = this.offset.height + 'px'; + this.inner.style.width = this.offset.width + 'px'; this.updateCircles(); this.octant = 0; this.element.addEventListener('touchmove', this.onTouchMove.bind(this), false); @@ -628,6 +624,7 @@ Knob.prototype.onAttached = function() { this.element.addEventListener('touchcancel', this.onTouchEnd.bind(this), false); }; Knob.prototype.readConfig = function() { + this.shape = this.readVariable('--shape', 'circle|ellipse|rectangle|square'); this.vibrateOnTouch = this.readVariable('--vibrate-on-touch', parseTime); this.vibrateOnOctantEdge = this.readVariable('--vibrate-on-octant-edge', parseTime); }; @@ -659,7 +656,7 @@ Knob.prototype.onTouchEnd = function() { this.updateCircles(); }; Knob.prototype.updateCircles = function() { - this.knobCircle.style.transform = 'rotate(' + ((this.state + 0.25) * 360) + 'deg)'; + this.inner.style.transform = 'rotate(' + (this.state * 360) + 'deg)'; }; function Button(id, updateStateCallback) { @@ -701,6 +698,9 @@ function DPad(id, updateStateCallback) { Control.call(this, 'dpad', id, updateStateCallback); this.state = [0, 0, 0, 0]; this.oldState = -1; + this.inner = document.createElement('div'); + this.inner.className = 'inner'; + this.element.appendChild(this.inner); } DPad.prototype = Object.create(Control.prototype); DPad.prototype.onAttached = function() { @@ -708,6 +708,11 @@ DPad.prototype.onAttached = function() { this.element.addEventListener('touchmove', this.onTouchMove.bind(this), false); this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); this.element.addEventListener('touchcancel', this.onTouchEnd.bind(this), false); + // Centering the knob within the boundary. + this.inner.style.top = this.offset.y + 'px'; + this.inner.style.left = this.offset.x + 'px'; + this.inner.style.height = this.offset.height + 'px'; + this.inner.style.width = this.offset.width + 'px'; // Precalculate the borders of the buttons: this.offset.x1 = this.offset.xCenter - this.buttonHitbox[1] * this.offset.halfWidth; this.offset.x2 = this.offset.xCenter + this.buttonHitbox[1] * this.offset.halfWidth; @@ -720,7 +725,9 @@ DPad.prototype.onAttached = function() { this.vibrateOnClick = this.readVariable('--vibrate-on-click', parseTime); }; DPad.prototype.readConfig = function() { + this.shape = this.readVariable('--shape', 'circle|ellipse|rectangle|square'); this.buttonHitbox = this.readVariable('--button-hitbox', parsePercentage); + this.vibrateOnClick = this.readVariable('--vibrate-on-click', parseTime); }; DPad.prototype.onTouchStart = function(ev) { ev.preventDefault(); // Android Webview delays the vibration without this. @@ -766,16 +773,16 @@ DPad.prototype.onTouchEnd = function() { }; DPad.prototype.updateButtons = function(state) { switch (state) { - case 0: this.element.className = 'control dpad'; break; - case 1: this.element.className = 'control dpad r'; break; - case 2: this.element.className = 'control dpad d'; break; - case 4: this.element.className = 'control dpad l'; break; - case 8: this.element.className = 'control dpad u'; break; - case 3: this.element.className = 'control dpad dr'; break; - case 6: this.element.className = 'control dpad dl'; break; - case 9: this.element.className = 'control dpad ur'; break; - case 12: this.element.className = 'control dpad ul'; break; - default: this.element.className = 'control dpad all'; break; + case 0: this.inner.className = 'inner none'; break; + case 1: this.inner.className = 'inner r'; break; + case 2: this.inner.className = 'inner d'; break; + case 4: this.inner.className = 'inner l'; break; + case 8: this.inner.className = 'inner u'; break; + case 3: this.inner.className = 'inner dr'; break; + case 6: this.inner.className = 'inner dl'; break; + case 9: this.inner.className = 'inner ur'; break; + case 12: this.inner.className = 'inner ul'; break; + default: this.inner.className = 'inner all'; break; } }; DPad.prototype.setBufferView = function(cursor, buffer) { diff --git a/yoke/assets/joypad/img/dp.svg b/yoke/assets/joypad/img/dp.svg index 03918a0..0af375b 100644 --- a/yoke/assets/joypad/img/dp.svg +++ b/yoke/assets/joypad/img/dp.svg @@ -1,52 +1,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/yoke/assets/joypad/img/knob.svg b/yoke/assets/joypad/img/knob.svg new file mode 100644 index 0000000..cae5962 --- /dev/null +++ b/yoke/assets/joypad/img/knob.svg @@ -0,0 +1,15 @@ + + + + + From 0c691ff1eb70daccc5381ab7ff68d20c4258053c Mon Sep 17 00:00:00 2001 From: Medape Date: Sat, 8 Jan 2022 13:44:19 +0100 Subject: [PATCH 6/8] Add more options for button labels Also, remove bg.svg, bm.svg, bs.svg, which have been superseded by button.svg#bg, #bm and #bs. --- yoke/assets/joypad/img/analog.svg | 14 +++++++++++--- yoke/assets/joypad/img/bg.svg | 4 ---- yoke/assets/joypad/img/bm.svg | 4 ---- yoke/assets/joypad/img/bs.svg | 4 ---- yoke/assets/joypad/img/button.svg | 18 ++++++++++++++++-- 5 files changed, 27 insertions(+), 17 deletions(-) delete mode 100644 yoke/assets/joypad/img/bg.svg delete mode 100644 yoke/assets/joypad/img/bm.svg delete mode 100644 yoke/assets/joypad/img/bs.svg diff --git a/yoke/assets/joypad/img/analog.svg b/yoke/assets/joypad/img/analog.svg index a1123ae..3c17fc7 100644 --- a/yoke/assets/joypad/img/analog.svg +++ b/yoke/assets/joypad/img/analog.svg @@ -1,20 +1,24 @@ - + G M @@ -40,4 +44,8 @@ 14 15 16 + LT + RT + L2 + R2 diff --git a/yoke/assets/joypad/img/bg.svg b/yoke/assets/joypad/img/bg.svg deleted file mode 100644 index 713d262..0000000 --- a/yoke/assets/joypad/img/bg.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - G - diff --git a/yoke/assets/joypad/img/bm.svg b/yoke/assets/joypad/img/bm.svg deleted file mode 100644 index 9fb3eed..0000000 --- a/yoke/assets/joypad/img/bm.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - M - diff --git a/yoke/assets/joypad/img/bs.svg b/yoke/assets/joypad/img/bs.svg deleted file mode 100644 index 7632b4d..0000000 --- a/yoke/assets/joypad/img/bs.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/yoke/assets/joypad/img/button.svg b/yoke/assets/joypad/img/button.svg index 9854d44..95d27d5 100644 --- a/yoke/assets/joypad/img/button.svg +++ b/yoke/assets/joypad/img/button.svg @@ -1,5 +1,9 @@ - + G M @@ -40,4 +44,14 @@ 14 15 16 + LB + RB + L1 + L2 + R1 + R2 + + + + From 2492aa244073de8a61c4f2a42b3408b5e5d9de90 Mon Sep 17 00:00:00 2001 From: Medape Date: Sat, 8 Jan 2022 18:46:51 +0100 Subject: [PATCH 7/8] CChange sorting method for controls Several programs on Linux (and all programs in Windows, due to a design decision on Yoke) ignore the semantics of every button and only look at the order in which they are reported. Because of this, the JavaScript side now reorders controls to simplify most configurations (face buttons should go first, then in-game menu buttons, then thumbstick buttons, and then special buttons like the branded key to open the system menu, which is even less used in this scenario). --- yoke/assets/joypad/base.js | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/yoke/assets/joypad/base.js b/yoke/assets/joypad/base.js index cf98e96..e0b8bab 100644 --- a/yoke/assets/joypad/base.js +++ b/yoke/assets/joypad/base.js @@ -26,24 +26,38 @@ function unique(value, index, self) { return self.indexOf(value) === index; } function categories(a, b) { // Custom algorithm to sort control mnemonics. + // Order is important to simplify configuration in systems that only consider the order of buttons, + // and not their meaning (udev code). var ids = [a, b]; var sortScores = ids.slice(); - // sortScores contains arbitrary numbers. The lower-ranking controls are attached earlier. + // sortScores contains arbitrary numbers. Lower-ranking controls are attached earlier. + // sortScores ending in "00000" are fine-tuned later. ids.forEach(function(id, i) { if (id == 'dbg') { sortScores[i] = 999998; } else { sortScores[i] = 999997; switch (id[0]) { + case 'b': + // First face buttons and triggers (1xxxxx), + // then in-game menu buttons (19999x), + // then thumbstick buttons (2xxxxx), + // then out-game menu buttons (HOME, MODE) (3xxxxx). + switch (id) { + case 'bg': sortScores[i] = 199991; break; + case 'bs': sortScores[i] = 199992; break; + case 'bm': sortScores[i] = 300001; break; + default: sortScores[i] = 100000; break; + } + break; // 's' is a locking joystick, 'j' - non-locking, 't' - thumbstick with L3/R3 button - case 's': case 'j': case 't': sortScores[i] = 100000; break; - case 'm': sortScores[i] = 200000; break; - case 'p': sortScores[i] = 300000; break; - case 'k': sortScores[i] = 400000; break; - case 'a': sortScores[i] = 500000; break; - case 'b': sortScores[i] = 600000; break; - case 'd': sortScores[i] = 700000; break; + case 's': case 'j': case 't': sortScores[i] = 200000; break; + case 'm': sortScores[i] = 400000; break; + case 'p': sortScores[i] = 500000; break; + case 'a': sortScores[i] = 600000; break; + case 'k': sortScores[i] = 700000; break; + case 'd': sortScores[i] = 800000; break; default: sortScores[i] = 999999999; break; } - if (sortScores[i] < 999990) { + if (sortScores[i] % 100000 == 0) { // read: if sortScore is not fine-tuned // This line should sort controls in the same category by id length, // and after that, by the ASCII codes in the id tag // This shortcut reorders non-negative integers at the end of a mnemonic correctly, From 1363da5b6daaa936060b40cc6f01dddadc3a003d Mon Sep 17 00:00:00 2001 From: Medape Date: Sat, 8 Jan 2022 18:47:53 +0100 Subject: [PATCH 8/8] Remove confusing duplicate code from vjoydevice.py --- yoke/vjoy/vjoydevice.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/yoke/vjoy/vjoydevice.py b/yoke/vjoy/vjoydevice.py index 43f6191..f937350 100644 --- a/yoke/vjoy/vjoydevice.py +++ b/yoke/vjoy/vjoydevice.py @@ -36,30 +36,5 @@ def __init__(self, id=None): self.lib.vJoyEnabled() self.lib.AcquireVJD(id) - def set_button(self, id, on): - self.buttons |= (on << id) - def set_axis(self, id, v): - self.axes[id] = ((v << 7) | (v >> 1)) + 1 - def flush(self, axes, buttons): - # Struct JOYSTICK_POSITION_V2's definition can be found at - # https://github.com/shauleiz/vJoy/blob/2c9a6f14967083d29f5a294b8f5ac65d3d42ac87/SDK/inc/public.h#L203 - # It's basically: - # 1 BYTE for device ID - # 3 unused LONGs - # 8 LONGs for axes - # 7 unused LONGs - # 1 LONGs for buttons - # 4 DWORDs for hats - # 3 LONGs for buttons - self.lib.UpdateVJD(self.id, self.outStruct.pack( - self.id, # 1 BYTE for device ID - 0, 0, 0, # 3 unused LONGs - *axes, # 8 LONGs for axes and 7 unused LONGs - buttons & 0xffffffff, # 1 LONG for buttons - 0, 0, 0, 0, # 4 DWORDs for hats - (buttons >> 32) & 0xffffffff, - (buttons >> 64) & 0xffffffff, - (buttons >> 96) & 0xffffffff # 3 LONGs for buttons - )) def close(self): return self.lib.RelinquishVJD(self.id)