diff --git a/yoke/assets/joypad/base.css b/yoke/assets/joypad/base.css index 38d4b8a..9a76e18 100644 --- a/yoke/assets/joypad/base.css +++ b/yoke/assets/joypad/base.css @@ -1,94 +1,79 @@ -/* 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: 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. + * 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. + * Some variables only are defined for one specific type of control (like D-pads.) */ /* Joysticks */ .joystick { + --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%; +} +.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; +} +.joystick > .inner.ellipse { + border-radius: 100%; } .circle { background-color: black; @@ -101,7 +86,7 @@ div { top: 0px; } .joystick.thumb > .circle { background-color: #444; } -.joystick.pressed { +.joystick > .inner.pressed { background-color: hsl(196, 77%, 33%); border: 3px hsl(188, 100%, 30%) solid; } @@ -111,6 +96,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 +110,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 +121,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 +141,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,37 +157,98 @@ div { #a16 { background-image: url('img/analog.svg#a16'); } /* D-pad */ -.dpad { background-image: url('img/dp.svg'); } -.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 { + --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 > .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 { 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 { } -.knobcircle { +.knob { + --shape: circle; +} +.knob > .inner { + background-image: url('img/knob.svg'); position: absolute; - border-radius: 100%; transform: rotate(0); - background-color: #888; } -.knobcircle > .circle { - position: absolute; - top: 50%; - left: 90%; - 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..e0b8bab 100644 --- a/yoke/assets/joypad/base.js +++ b/yoke/assets/joypad/base.js @@ -1,50 +1,12 @@ 'use strict'; - -// 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; -// These 2 options are recommended for testing in non-kiosk/non-embedded browsers: +// DEBUG SETTINGS: +// These 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: -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') { @@ -64,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, @@ -99,6 +75,54 @@ function truncate(val) { (val > 0x7fff) ? 0x7fff : Math.floor(val); } +function parseTime(c) { + if (c.endsWith('ms')) { + 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 { + return undefined; + } +} + +function parsePercentage(c) { + if (c.endsWith('%')) { + 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) { + 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) { + switch (c) { + case 'yes': + case 'true': + return true; break; + case 'none': + case 'no': + case 'false': + return false; break; + default: + return undefined; break; + } +} + var serializer = new TextDecoder('iso-8859-1'); // HAPTIC FEEDBACK MIXING: @@ -143,7 +167,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; @@ -152,10 +176,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; @@ -165,6 +190,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); @@ -181,6 +207,13 @@ function Joystick(id, updateStateCallback) { this.state = [0, 0]; this.checkThumbButton = function() {}; } + 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'); + } + this.circle = document.createElement('div'); this.circle.className = 'circle'; this.element.appendChild(this.circle); @@ -188,22 +221,62 @@ 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.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); 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])); } - this.offset.factorX = 0x4000 / this.offset.halfWidth; - this.offset.factorY = 0x4000 / this.offset.halfHeight; +}; +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.inner.classList.add('ellipse'); + this.onTouchMove = this.onTouchMoveCircle; + this.updateCircle = this.updateCircleCircle; + } + 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); }; // This contant defines the limits of all the octants in analytic geometry: 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); @@ -218,27 +291,71 @@ 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; } 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); - window.navigator.vibrate(VIBRATION_MILLISECONDS_IN); + window.navigator.vibrate(this.vibrateOnTouch); }; Joystick.prototype.onTouchEnd = function(ev) { if (ev.targetTouches.length == 0) { @@ -251,7 +368,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.inner.classList.remove('pressed'); this.oldButtonState = 0; } this.updateStateCallback(); @@ -266,17 +383,17 @@ 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); - (this.state[2] == 0) ? this.element.classList.remove('pressed') : this.element.classList.add('pressed'); + window.navigator.vibrate(this.vibrateOnClick); + (this.state[2] == 0) ? this.inner.classList.remove('pressed') : this.inner.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); - (this.state[2] == 0) ? this.element.classList.remove('pressed') : this.element.classList.add('pressed'); + window.navigator.vibrate(this.vibrateOnClick); + (this.state[2] == 0) ? this.inner.classList.remove('pressed') : this.inner.classList.add('pressed'); this.oldButtonState = this.state[2]; } }; @@ -285,6 +402,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); @@ -310,11 +439,13 @@ 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) { - 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(); @@ -355,9 +486,14 @@ Pedal.prototype.onAttached = function() { this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); this.element.addEventListener('touchcancel', this.onTouchEnd.bind(this), false); }; +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) { 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'); }; @@ -367,7 +503,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); } @@ -377,11 +513,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); } @@ -401,7 +537,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); @@ -417,12 +553,18 @@ AnalogButton.prototype.onAttached = function() { 'scaleY(', this.offset.height, ')' ].join(''); }; +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);}); +}; 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); @@ -438,7 +580,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); @@ -461,7 +603,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); } }; @@ -477,21 +619,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); @@ -499,6 +637,11 @@ Knob.prototype.onAttached = function() { this.element.addEventListener('touchend', this.onTouchEnd.bind(this), false); 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); +}; Knob.prototype.onTouchMove = function(ev) { var pos = ev.targetTouches[0]; // The knob now increments the state proportionally to the turned angle. @@ -509,8 +652,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(); @@ -520,14 +663,14 @@ 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(); 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) { @@ -535,7 +678,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); @@ -569,6 +712,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() { @@ -576,15 +722,26 @@ 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 - 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.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.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. @@ -616,7 +773,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() { @@ -630,16 +787,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) { @@ -719,6 +876,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(); @@ -827,11 +985,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; } } }; +Control.prototype.readVariable = function (key, vartype) { + var output = getComputedStyle(this.element).getPropertyValue(key) + .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; + } +}; // BASE CODE: 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 + + + + 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 @@ + + + + + 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 @@ + + + + + + + + + + + + + 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)