diff --git a/src/components/action-to-event.js b/src/components/action-to-event.js
index ee6f397445fea8d556f268ca81ea978f366df6d0..8bcd22e43dcbc30759f7ec3ec17692f0bf75a161 100644
--- a/src/components/action-to-event.js
+++ b/src/components/action-to-event.js
@@ -8,7 +8,7 @@ AFRAME.registerComponent("action-to-event", {
 
   tick() {
     const userinput = AFRAME.scenes[0].systems.userinput;
-    if (userinput.readFrameValueAtPath(this.data.path)) {
+    if (userinput.get(this.data.path)) {
       this.el.emit(this.data.event);
     }
   }
diff --git a/src/components/camera-tool.js b/src/components/camera-tool.js
index eacfaf8f443a0131cef4cd189f0a05e5c9a58a20..9882eb88f481126ce63b9269f7a3dda44cbbfaab 100644
--- a/src/components/camera-tool.js
+++ b/src/components/camera-tool.js
@@ -110,7 +110,7 @@ AFRAME.registerComponent("camera-tool", {
     const grabber = this.el.components.grabbable.grabbers[0];
     if (grabber && !!pathsMap[grabber.id]) {
       const paths = pathsMap[grabber.id];
-      if (AFRAME.scenes[0].systems.userinput.readFrameValueAtPath(paths.takeSnapshot)) {
+      if (AFRAME.scenes[0].systems.userinput.get(paths.takeSnapshot)) {
         this.takeSnapshotNextTick = true;
       }
     }
diff --git a/src/components/character-controller.js b/src/components/character-controller.js
index 910123ba8bb2f5e54e64037ac478a1ac2e57e577..083f47e195e7453136c958ecad62762be882c614 100644
--- a/src/components/character-controller.js
+++ b/src/components/character-controller.js
@@ -45,8 +45,6 @@ AFRAME.registerComponent("character-controller", {
     const eventSrc = this.el.sceneEl;
     eventSrc.addEventListener("move", this.setAccelerationInput);
     eventSrc.addEventListener("rotateY", this.setAngularVelocity);
-    eventSrc.addEventListener("snap_rotate_left", this.snapRotateLeft);
-    eventSrc.addEventListener("snap_rotate_right", this.snapRotateRight);
     eventSrc.addEventListener("teleported", this.handleTeleport);
   },
 
@@ -54,8 +52,6 @@ AFRAME.registerComponent("character-controller", {
     const eventSrc = this.el.sceneEl;
     eventSrc.removeEventListener("move", this.setAccelerationInput);
     eventSrc.removeEventListener("rotateY", this.setAngularVelocity);
-    eventSrc.removeEventListener("snap_rotate_left", this.snapRotateLeft);
-    eventSrc.removeEventListener("snap_rotate_right", this.snapRotateRight);
     eventSrc.removeEventListener("teleported", this.handleTeleport);
     this.reset();
   },
@@ -78,10 +74,12 @@ AFRAME.registerComponent("character-controller", {
 
   snapRotateLeft: function() {
     this.pendingSnapRotationMatrix.copy(this.leftRotationMatrix);
+    this.el.emit("snap_rotate_left");
   },
 
   snapRotateRight: function() {
     this.pendingSnapRotationMatrix.copy(this.rightRotationMatrix);
+    this.el.emit("snap_rotate_right");
   },
 
   handleTeleport: function(event) {
@@ -119,13 +117,13 @@ AFRAME.registerComponent("character-controller", {
       root.updateMatrix();
 
       const userinput = AFRAME.scenes[0].systems.userinput;
-      if (userinput.readFrameValueAtPath(paths.actions.snapRotateLeft)) {
+      if (userinput.get(paths.actions.snapRotateLeft)) {
         this.snapRotateLeft();
       }
-      if (userinput.readFrameValueAtPath(paths.actions.snapRotateRight)) {
+      if (userinput.get(paths.actions.snapRotateRight)) {
         this.snapRotateRight();
       }
-      const acc = userinput.readFrameValueAtPath(paths.actions.characterAcceleration);
+      const acc = userinput.get(paths.actions.characterAcceleration);
       if (acc) {
         this.accelerationInput.set(
           this.accelerationInput.x + acc[0],
@@ -145,7 +143,7 @@ AFRAME.registerComponent("character-controller", {
       this.updateVelocity(deltaSeconds);
       this.accelerationInput.set(0, 0, 0);
 
-      const boost = userinput.readFrameValueAtPath(paths.actions.boost) ? 2 : 1;
+      const boost = userinput.get(paths.actions.boost) ? 2 : 1;
       move.makeTranslation(
         this.velocity.x * distance * boost,
         this.velocity.y * distance * boost,
diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js
index c5113dba822b23944026cff06b41497deecf55f0..1b095fc05948d88e38cbbcebd8b3141046ffb69f 100644
--- a/src/components/cursor-controller.js
+++ b/src/components/cursor-controller.js
@@ -94,8 +94,8 @@ AFRAME.registerComponent("cursor-controller", {
       }
 
       const userinput = AFRAME.scenes[0].systems.userinput;
-      const cursorPose = userinput.readFrameValueAtPath(paths.actions.cursor.pose);
-      const rightHandPose = userinput.readFrameValueAtPath(paths.actions.rightHand.pose);
+      const cursorPose = userinput.get(paths.actions.cursor.pose);
+      const rightHandPose = userinput.get(paths.actions.rightHand.pose);
 
       this.data.cursor.object3D.visible = this.enabled && !!cursorPose;
       this.el.setAttribute("line", "visible", this.enabled && !!rightHandPose);
@@ -119,7 +119,7 @@ AFRAME.registerComponent("cursor-controller", {
 
       const { cursor, near, far, camera, cursorColorHovered, cursorColorUnhovered } = this.data;
 
-      const cursorModDelta = userinput.readFrameValueAtPath(paths.actions.cursor.modDelta);
+      const cursorModDelta = userinput.get(paths.actions.cursor.modDelta);
       if (isGrabbing && cursorModDelta) {
         this.distance = THREE.Math.clamp(this.distance - cursorModDelta, near, far);
       }
diff --git a/src/components/hand-controls2.js b/src/components/hand-controls2.js
index 534f796c45c304097768d5e486cac12db4d76664..3a97888b172687838581fc720f516e50ceb6b248 100644
--- a/src/components/hand-controls2.js
+++ b/src/components/hand-controls2.js
@@ -106,10 +106,10 @@ AFRAME.registerComponent("hand-controls2", {
     const hand = this.data;
     const userinput = AFRAME.scenes[0].systems.userinput;
     const subpath = hand === "left" ? paths.actions.leftHand : paths.actions.rightHand;
-    const hasPose = userinput.readFrameValueAtPath(subpath.pose);
-    const thumb = userinput.readFrameValueAtPath(subpath.thumb);
-    const index = userinput.readFrameValueAtPath(subpath.index);
-    const middleRingPinky = userinput.readFrameValueAtPath(subpath.middleRingPinky);
+    const hasPose = userinput.get(subpath.pose);
+    const thumb = userinput.get(subpath.thumb);
+    const index = userinput.get(subpath.index);
+    const middleRingPinky = userinput.get(subpath.middleRingPinky);
     const pose = this.poseForFingers(thumb, index, middleRingPinky);
     if (pose !== this.pose) {
       this.el.emit("hand-pose", { previous: this.pose, current: pose });
diff --git a/src/components/pinch-to-move.js b/src/components/pinch-to-move.js
index 28dc43cb5d2584532bf549fc3b08aaf01c1ce953..bc5c57a7631e8070c4eaafba13f52c0ceb757bca 100644
--- a/src/components/pinch-to-move.js
+++ b/src/components/pinch-to-move.js
@@ -9,7 +9,7 @@ AFRAME.registerComponent("pinch-to-move", {
   },
   tick() {
     const userinput = AFRAME.scenes[0].systems.userinput;
-    const pinch = userinput.readFrameValueAtPath(paths.device.touchscreen.pinchDelta);
+    const pinch = userinput.get(paths.device.touchscreen.pinchDelta);
     if (pinch) {
       this.axis[1] = pinch * this.data.speed;
       this.el.emit("move", { axis: this.axis });
diff --git a/src/components/pitch-yaw-rotator.js b/src/components/pitch-yaw-rotator.js
index 297bf4f81b5105ca229a5d34bdf527b2389725bb..a2c2ebdead1d580c4480e2578e59c918289941f0 100644
--- a/src/components/pitch-yaw-rotator.js
+++ b/src/components/pitch-yaw-rotator.js
@@ -36,7 +36,7 @@ AFRAME.registerComponent("pitch-yaw-rotator", {
 
   tick() {
     const userinput = AFRAME.scenes[0].systems.userinput;
-    const cameraDelta = userinput.readFrameValueAtPath(paths.actions.cameraDelta);
+    const cameraDelta = userinput.get(paths.actions.cameraDelta);
     let lookX = this.pendingXRotation;
     let lookY = 0;
     if (cameraDelta) {
diff --git a/src/components/super-networked-interactable.js b/src/components/super-networked-interactable.js
index 4c96f2452f9945c0b9bf29be259c487b8d665c3d..1fbbaa37c6f0786b91f9d8cc109df19e0716829d 100644
--- a/src/components/super-networked-interactable.js
+++ b/src/components/super-networked-interactable.js
@@ -98,6 +98,6 @@ AFRAME.registerComponent("super-networked-interactable", {
     if (!(grabber && pathsMap[grabber.id])) return;
 
     const userinput = AFRAME.scenes[0].systems.userinput;
-    this._changeScale(userinput.readFrameValueAtPath(pathsMap[grabber.id].scaleGrabbedGrabbable));
+    this._changeScale(userinput.get(pathsMap[grabber.id].scaleGrabbedGrabbable));
   }
 });
diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js
index a786639e1b828e34d1daf36bedd10e500d275ed1..7ff4e1189f589d9d026ba63e920dfc5d47a78760 100644
--- a/src/components/super-spawner.js
+++ b/src/components/super-spawner.js
@@ -116,8 +116,8 @@ AFRAME.registerComponent("super-spawner", {
 
   async onSpawnEvent() {
     const userinput = AFRAME.scenes[0].systems.userinput;
-    const leftPose = userinput.readFrameValueAtPath(paths.actions.leftHand.pose);
-    const rightPose = userinput.readFrameValueAtPath(paths.actions.rightHand.pose);
+    const leftPose = userinput.get(paths.actions.leftHand.pose);
+    const rightPose = userinput.get(paths.actions.rightHand.pose);
     const controllerCount = leftPose && rightPose ? 2 : leftPose || rightPose ? 1 : 0;
     const using6DOF = controllerCount > 1 && this.el.sceneEl.is("vr-mode");
     const hand = using6DOF ? this.data.superHand : this.data.cursorSuperHand;
diff --git a/src/components/tools/pen.js b/src/components/tools/pen.js
index dd52ca4ab14030ddf064a9d5ae9f0ef9a7d4456a..f09e63101f4366440a06ba4d60481999edaba6b8 100644
--- a/src/components/tools/pen.js
+++ b/src/components/tools/pen.js
@@ -103,20 +103,20 @@ AFRAME.registerComponent("pen", {
     const userinput = AFRAME.scenes[0].systems.userinput;
     if (grabber && pathsMap[grabber.id]) {
       const paths = pathsMap[grabber.id];
-      if (userinput.readFrameValueAtPath(paths.startDrawing)) {
+      if (userinput.get(paths.startDrawing)) {
         this._startDraw();
       }
-      if (userinput.readFrameValueAtPath(paths.stopDrawing)) {
+      if (userinput.get(paths.stopDrawing)) {
         this._endDraw();
       }
-      const penScaleMod = userinput.readFrameValueAtPath(paths.scalePenTip);
+      const penScaleMod = userinput.get(paths.scalePenTip);
       if (penScaleMod) {
         this._changeRadius(penScaleMod);
       }
-      if (userinput.readFrameValueAtPath(paths.penNextColor)) {
+      if (userinput.get(paths.penNextColor)) {
         this._changeColor(1);
       }
-      if (userinput.readFrameValueAtPath(paths.penPrevColor)) {
+      if (userinput.get(paths.penPrevColor)) {
         this._changeColor(-1);
       }
     }
diff --git a/src/systems/userinput/bindings/keyboard-mouse-user.js b/src/systems/userinput/bindings/keyboard-mouse-user.js
index 1f067d4447effc86541b93d26fb32ffaba5e471b..12e3b3e4f64b814c06427030a7b5e9be7d6aaf0a 100644
--- a/src/systems/userinput/bindings/keyboard-mouse-user.js
+++ b/src/systems/userinput/bindings/keyboard-mouse-user.js
@@ -3,6 +3,8 @@ import { sets } from "../sets";
 import { xforms } from "./xforms";
 
 const wasd_vec2 = "/var/mouse-and-keyboard/wasd_vec2";
+const keyboardCharacterAcceleration = "/var/mouse-and-keyboard/keyboardCharacterAcceleration";
+const arrows_vec2 = "/var/mouse-and-keyboard/arrows_vec2";
 const dropWithRMB = "/vars/mouse-and-keyboard/drop_with_RMB";
 const dropWithEsc = "/vars/mouse-and-keyboard/drop_with_esc";
 
@@ -10,7 +12,9 @@ const dropWithRMBorEscBindings = [
   {
     src: { value: paths.device.mouse.buttonRight },
     dest: { value: dropWithRMB },
-    xform: xforms.falling
+    xform: xforms.falling,
+    root: "rmb",
+    priority: 200
   },
   {
     src: { value: paths.device.keyboard.key("Escape") },
@@ -35,6 +39,16 @@ export const keyboardMouseUserBindings = {
       },
       xform: xforms.rising
     },
+    {
+      src: {
+        w: paths.device.keyboard.key("arrowup"),
+        a: paths.device.keyboard.key("arrowleft"),
+        s: paths.device.keyboard.key("arrowdown"),
+        d: paths.device.keyboard.key("arrowright")
+      },
+      dest: { vec2: arrows_vec2 },
+      xform: xforms.wasd_to_vec2
+    },
     {
       src: {
         w: paths.device.keyboard.key("w"),
@@ -46,9 +60,17 @@ export const keyboardMouseUserBindings = {
       xform: xforms.wasd_to_vec2
     },
     {
-      src: { value: wasd_vec2 },
+      src: {
+        first: wasd_vec2,
+        second: arrows_vec2
+      },
+      dest: { value: keyboardCharacterAcceleration },
+      xform: xforms.max_vec2
+    },
+    {
+      src: { value: keyboardCharacterAcceleration },
       dest: { value: paths.actions.characterAcceleration },
-      xform: xforms.copy
+      xform: xforms.normalize_vec2
     },
     {
       src: { value: paths.device.keyboard.key("shift") },
@@ -87,7 +109,7 @@ export const keyboardMouseUserBindings = {
     {
       src: { value: "/var/smartMouseCamDeltaX" },
       dest: { value: "/var/smartMouseCamDeltaXScaled" },
-      xform: xforms.scale(-0.06)
+      xform: xforms.scale(-0.2)
     },
     {
       src: { value: "/var/smartMouseCamDeltaY" },
@@ -116,6 +138,26 @@ export const keyboardMouseUserBindings = {
         value: paths.actions.logDebugFrame
       },
       xform: xforms.rising
+    },
+    {
+      src: {
+        value: paths.device.mouse.buttonRight
+      },
+      dest: {
+        value: paths.actions.startGazeTeleport
+      },
+      xform: xforms.rising,
+      root: "rmb",
+      priority: 100
+    },
+    {
+      src: {
+        value: paths.device.mouse.buttonRight
+      },
+      dest: {
+        value: paths.actions.stopGazeTeleport
+      },
+      xform: xforms.falling
     }
   ],
 
diff --git a/src/systems/userinput/bindings/oculus-touch-user.js b/src/systems/userinput/bindings/oculus-touch-user.js
index d386016c27497b4c3faeae1fcc3d413f3a0476fd..1d557129c05e1d7ec718dbadfb08c7c22543b84c 100644
--- a/src/systems/userinput/bindings/oculus-touch-user.js
+++ b/src/systems/userinput/bindings/oculus-touch-user.js
@@ -47,6 +47,9 @@ const leftJoyY = `${name}left/joyY`;
 const leftJoyYCursorMod = `${name}left/joyYCursorMod`;
 const oculusTouchCharacterAcceleration = `${name}characterAcceleration`;
 const keyboardCharacterAcceleration = "/var/keyboard/characterAcceleration";
+const characterAcceleration = "/var/oculus-touch/nonNormalizedCharacterAcceleration";
+const wasd_vec2 = "/var/keyboard/wasd_vec2";
+const arrows_vec2 = "/var/keyboard/arrows_vec2";
 const keyboardBoost = "/var/keyboard-oculus/boost";
 const rightBoost = "/var/right-oculus/boost";
 const leftBoost = "/var/left-oculus/boost";
@@ -224,6 +227,16 @@ export const oculusTouchUserBindings = {
       dest: { value: oculusTouchCharacterAcceleration },
       xform: xforms.compose_vec2
     },
+    {
+      src: {
+        w: paths.device.keyboard.key("arrowup"),
+        a: paths.device.keyboard.key("arrowleft"),
+        s: paths.device.keyboard.key("arrowdown"),
+        d: paths.device.keyboard.key("arrowright")
+      },
+      dest: { vec2: arrows_vec2 },
+      xform: xforms.wasd_to_vec2
+    },
     {
       src: {
         w: paths.device.keyboard.key("w"),
@@ -231,18 +244,31 @@ export const oculusTouchUserBindings = {
         s: paths.device.keyboard.key("s"),
         d: paths.device.keyboard.key("d")
       },
-      dest: { vec2: keyboardCharacterAcceleration },
+      dest: { vec2: wasd_vec2 },
       xform: xforms.wasd_to_vec2
     },
+    {
+      src: {
+        first: wasd_vec2,
+        second: arrows_vec2
+      },
+      dest: { value: keyboardCharacterAcceleration },
+      xform: xforms.max_vec2
+    },
     {
       src: {
         first: oculusTouchCharacterAcceleration,
         second: keyboardCharacterAcceleration
       },
       dest: {
-        value: paths.actions.characterAcceleration
+        value: characterAcceleration
       },
-      xform: xforms.add_vec2
+      xform: xforms.max_vec2
+    },
+    {
+      src: { value: characterAcceleration },
+      dest: { value: paths.actions.characterAcceleration },
+      xform: xforms.normalize_vec2
     },
     {
       src: { value: paths.device.keyboard.key("shift") },
diff --git a/src/systems/userinput/bindings/vive-user.js b/src/systems/userinput/bindings/vive-user.js
index 0e20233c7ff606ee29914e0d5293173404302491..39954438401c372a7070d876f8b04991b198817a 100644
--- a/src/systems/userinput/bindings/vive-user.js
+++ b/src/systems/userinput/bindings/vive-user.js
@@ -26,6 +26,7 @@ const lTriggerRisingGrab = v("right/trigger/rising/grab");
 const lGripRisingGrab = v("right/grab/rising/grab");
 const lTouchpadRising = v("left/touchpad/rising");
 const lCharacterAcceleration = v("left/characterAcceleration");
+const characterAcceleration = v("nonNormalizedCharacterAcceleration");
 const lGripFalling = v("left/grip/falling");
 const lGripRising = v("left/grip/rising");
 const leftBoost = v("left/boost");
@@ -69,6 +70,8 @@ const k = name => {
 const keyboardSnapRight = k("snap-right");
 const keyboardSnapLeft = k("snap-left");
 const keyboardCharacterAcceleration = k("characterAcceleration");
+const wasd_vec2 = k("wasd_vec2");
+const arrows_vec2 = k("arrows_vec2");
 const keyboardBoost = k("boost");
 
 const teleportLeft = [
@@ -314,6 +317,16 @@ export const viveUserBindings = {
       dest: { value: lCharacterAcceleration },
       xform: xforms.copyIfTrue
     },
+    {
+      src: {
+        w: paths.device.keyboard.key("arrowup"),
+        a: paths.device.keyboard.key("arrowleft"),
+        s: paths.device.keyboard.key("arrowdown"),
+        d: paths.device.keyboard.key("arrowright")
+      },
+      dest: { vec2: arrows_vec2 },
+      xform: xforms.wasd_to_vec2
+    },
     {
       src: {
         w: paths.device.keyboard.key("w"),
@@ -321,18 +334,31 @@ export const viveUserBindings = {
         s: paths.device.keyboard.key("s"),
         d: paths.device.keyboard.key("d")
       },
-      dest: { vec2: keyboardCharacterAcceleration },
+      dest: { vec2: wasd_vec2 },
       xform: xforms.wasd_to_vec2
     },
+    {
+      src: {
+        first: wasd_vec2,
+        second: arrows_vec2
+      },
+      dest: { value: keyboardCharacterAcceleration },
+      xform: xforms.max_vec2
+    },
     {
       src: {
         first: lCharacterAcceleration,
         second: keyboardCharacterAcceleration
       },
       dest: {
-        value: paths.actions.characterAcceleration
+        value: characterAcceleration
       },
-      xform: xforms.add_vec2
+      xform: xforms.max_vec2
+    },
+    {
+      src: { value: characterAcceleration },
+      dest: { value: paths.actions.characterAcceleration },
+      xform: xforms.normalize_vec2
     },
     {
       src: { value: paths.device.keyboard.key("shift") },
diff --git a/src/systems/userinput/bindings/xforms.js b/src/systems/userinput/bindings/xforms.js
index 444134675a842316f0f5b52f55ed8ff30083ca15..0ab4cdf5226a3840a26475cf5cd93390b7723a22 100644
--- a/src/systems/userinput/bindings/xforms.js
+++ b/src/systems/userinput/bindings/xforms.js
@@ -104,6 +104,29 @@ export const xforms = {
       frame[dest.value] = first;
     }
   },
+  max_vec2: function(frame, src, dest) {
+    const first = frame[src.first];
+    const second = frame[src.second];
+    if (first && second) {
+      frame[dest.value] =
+        first[0] * first[0] + first[1] * first[1] > second[0] * second[0] + second[1] * second[1] ? first : second;
+    } else if (second) {
+      frame[dest.value] = second;
+    } else if (first) {
+      frame[dest.value] = first;
+    }
+  },
+  normalize_vec2: function(frame, src, dest) {
+    const vec2 = frame[src.value];
+    if (vec2) {
+      if (vec2[0] === 0 && vec2[0] === 0) {
+        frame[dest.value] = vec2;
+      } else {
+        const l = Math.sqrt(vec2[0] * vec2[0] + vec2[1] * vec2[1]);
+        frame[dest.value] = [vec2[0] / l, vec2[1] / l];
+      }
+    }
+  },
   any: function(frame, src, dest) {
     for (const path in src) {
       if (frame[src[path]]) {
diff --git a/src/systems/userinput/devices/app-aware-mouse.js b/src/systems/userinput/devices/app-aware-mouse.js
index 4afd124e7006b86869309aaa3626a756b7f20292..66e006d44c56a98c07a39a498cc68581c4d0e6b9 100644
--- a/src/systems/userinput/devices/app-aware-mouse.js
+++ b/src/systems/userinput/devices/app-aware-mouse.js
@@ -30,13 +30,6 @@ export class AppAwareMouseDevice {
       this.camera = document.querySelector("#player-camera").components.camera.camera;
     }
 
-    const coords = frame[paths.device.mouse.coords];
-    const isCursorGrabbing = this.cursorController.data.cursor.components["super-hands"].state.has("grab-start");
-    if (isCursorGrabbing) {
-      frame[paths.device.smartMouse.cursorPose] = calculateCursorPose(this.camera, coords);
-      return;
-    }
-
     const buttonLeft = frame[paths.device.mouse.buttonLeft];
     if (buttonLeft && !this.prevButtonLeft) {
       const rawIntersections = [];
@@ -54,8 +47,9 @@ export class AppAwareMouseDevice {
 
     if (!this.clickedOnAnything && buttonLeft) {
       frame[paths.device.smartMouse.cameraDelta] = frame[paths.device.mouse.movementXY];
-    } else {
-      frame[paths.device.smartMouse.cursorPose] = calculateCursorPose(this.camera, coords);
     }
+
+    const coords = frame[paths.device.mouse.coords];
+    frame[paths.device.smartMouse.cursorPose] = calculateCursorPose(this.camera, coords);
   }
 }
diff --git a/src/systems/userinput/devices/mouse.js b/src/systems/userinput/devices/mouse.js
index 0ab7a3ac2aea0ce0ec44ad55fb5804348443bcae..3252fb7372a53406da0992b8b3445a9f854dd065 100644
--- a/src/systems/userinput/devices/mouse.js
+++ b/src/systems/userinput/devices/mouse.js
@@ -18,24 +18,19 @@ export class MouseDevice {
 
     const queueEvent = this.events.push.bind(this.events);
     const canvas = document.querySelector("canvas");
-    ["mousedown", "mouseup", "mousemove", "wheel"].map(x => canvas.addEventListener(x, queueEvent));
-    ["mouseout", "blur"].map(x => document.addEventListener(x, queueEvent));
+    ["mousedown", "wheel"].map(x => canvas.addEventListener(x, queueEvent));
+    ["mousemove", "mouseup"].map(x => window.addEventListener(x, queueEvent));
+    document.addEventListener("wheel", e => {
+      e.preventDefault();
+    });
   }
 
   process(event) {
     if (event.type === "wheel") {
-      this.wheel += event.deltaY / modeMod[event.deltaMode];
+      this.wheel += (event.deltaX + event.deltaY) / modeMod[event.deltaMode];
       return;
     }
-    if (event.type === "mouseout" || event.type === "blur") {
-      this.coords[0] = 0;
-      this.coords[1] = 0;
-      this.movementXY[0] = 0;
-      this.movementXY[1] = 0;
-      this.buttonLeft = false;
-      this.buttonRight = false;
-      this.wheel = 0;
-    }
+
     const left = event.button === 0;
     const right = event.button === 2;
     this.coords[0] = (event.clientX / window.innerWidth) * 2 - 1;
diff --git a/src/systems/userinput/resolve-action-sets.js b/src/systems/userinput/resolve-action-sets.js
index d1375f1b02df1c6f3d56463824a13600383fea6b..4341064a1085ba55f3584d4a3a5e00bcd5a8d6d7 100644
--- a/src/systems/userinput/resolve-action-sets.js
+++ b/src/systems/userinput/resolve-action-sets.js
@@ -136,30 +136,30 @@ export function updateActionSetsBasedOnSuperhands() {
     !cursorHoveringOnUI;
 
   const userinput = AFRAME.scenes[0].systems.userinput;
-  userinput.toggleActive(sets.leftHandHoveringOnInteractable, leftHandHoveringOnInteractable);
-  userinput.toggleActive(sets.leftHandHoveringOnPen, leftHandHoveringOnPen);
-  userinput.toggleActive(sets.leftHandHoveringOnCamera, leftHandHoveringOnCamera);
-  userinput.toggleActive(sets.leftHandHoveringOnNothing, leftHandHoveringOnNothing);
-  userinput.toggleActive(sets.leftHandHoldingPen, leftHandHoldingPen);
-  userinput.toggleActive(sets.leftHandHoldingInteractable, leftHandHoldingInteractable);
-  userinput.toggleActive(sets.leftHandHoldingCamera, leftHandHoldingCamera);
-  userinput.toggleActive(sets.leftHandTeleporting, leftHandTeleporting);
+  userinput.toggleSet(sets.leftHandHoveringOnInteractable, leftHandHoveringOnInteractable);
+  userinput.toggleSet(sets.leftHandHoveringOnPen, leftHandHoveringOnPen);
+  userinput.toggleSet(sets.leftHandHoveringOnCamera, leftHandHoveringOnCamera);
+  userinput.toggleSet(sets.leftHandHoveringOnNothing, leftHandHoveringOnNothing);
+  userinput.toggleSet(sets.leftHandHoldingPen, leftHandHoldingPen);
+  userinput.toggleSet(sets.leftHandHoldingInteractable, leftHandHoldingInteractable);
+  userinput.toggleSet(sets.leftHandHoldingCamera, leftHandHoldingCamera);
+  userinput.toggleSet(sets.leftHandTeleporting, leftHandTeleporting);
 
-  userinput.toggleActive(sets.rightHandHoveringOnInteractable, rightHandHoveringOnInteractable);
-  userinput.toggleActive(sets.rightHandHoveringOnPen, rightHandHoveringOnPen);
-  userinput.toggleActive(sets.rightHandHoveringOnNothing, rightHandHoveringOnNothing);
-  userinput.toggleActive(sets.rightHandHoveringOnCamera, rightHandHoveringOnCamera);
-  userinput.toggleActive(sets.rightHandHoldingPen, rightHandHoldingPen);
-  userinput.toggleActive(sets.rightHandHoldingInteractable, rightHandHoldingInteractable);
-  userinput.toggleActive(sets.rightHandTeleporting, rightHandTeleporting);
-  userinput.toggleActive(sets.rightHandHoldingCamera, rightHandHoldingCamera);
+  userinput.toggleSet(sets.rightHandHoveringOnInteractable, rightHandHoveringOnInteractable);
+  userinput.toggleSet(sets.rightHandHoveringOnPen, rightHandHoveringOnPen);
+  userinput.toggleSet(sets.rightHandHoveringOnNothing, rightHandHoveringOnNothing);
+  userinput.toggleSet(sets.rightHandHoveringOnCamera, rightHandHoveringOnCamera);
+  userinput.toggleSet(sets.rightHandHoldingPen, rightHandHoldingPen);
+  userinput.toggleSet(sets.rightHandHoldingInteractable, rightHandHoldingInteractable);
+  userinput.toggleSet(sets.rightHandTeleporting, rightHandTeleporting);
+  userinput.toggleSet(sets.rightHandHoldingCamera, rightHandHoldingCamera);
 
-  userinput.toggleActive(sets.cursorHoveringOnPen, cursorHoveringOnPen);
-  userinput.toggleActive(sets.cursorHoveringOnCamera, cursorHoveringOnCamera);
-  userinput.toggleActive(sets.cursorHoveringOnInteractable, cursorHoveringOnInteractable);
-  userinput.toggleActive(sets.cursorHoveringOnUI, cursorHoveringOnUI);
-  userinput.toggleActive(sets.cursorHoveringOnNothing, cursorHoveringOnNothing);
-  userinput.toggleActive(sets.cursorHoldingPen, cursorHoldingPen);
-  userinput.toggleActive(sets.cursorHoldingCamera, cursorHoldingCamera);
-  userinput.toggleActive(sets.cursorHoldingInteractable, cursorHoldingInteractable);
+  userinput.toggleSet(sets.cursorHoveringOnPen, cursorHoveringOnPen);
+  userinput.toggleSet(sets.cursorHoveringOnCamera, cursorHoveringOnCamera);
+  userinput.toggleSet(sets.cursorHoveringOnInteractable, cursorHoveringOnInteractable);
+  userinput.toggleSet(sets.cursorHoveringOnUI, cursorHoveringOnUI);
+  userinput.toggleSet(sets.cursorHoveringOnNothing, cursorHoveringOnNothing);
+  userinput.toggleSet(sets.cursorHoldingPen, cursorHoldingPen);
+  userinput.toggleSet(sets.cursorHoldingCamera, cursorHoldingCamera);
+  userinput.toggleSet(sets.cursorHoldingInteractable, cursorHoldingInteractable);
 }
diff --git a/src/systems/userinput/userinput.js b/src/systems/userinput/userinput.js
index 2ffbdca79db0e2d07d464f32ea320fc8cb31c329..8442cd73a763cf2fa8881e04267fb68d653ac0f1 100644
--- a/src/systems/userinput/userinput.js
+++ b/src/systems/userinput/userinput.js
@@ -26,29 +26,27 @@ import { updateActionSetsBasedOnSuperhands } from "./resolve-action-sets";
 import { GamepadDevice } from "./devices/gamepad";
 import { gamepadBindings } from "./bindings/generic-gamepad";
 
-const prioritizedBindings = new Map();
+const priorityMap = new Map();
 function prioritizeBindings(registeredMappings, activeSets) {
   const activeBindings = new Set();
-  prioritizedBindings.clear();
+  priorityMap.clear();
   for (const mapping of registeredMappings) {
     for (const setName in mapping) {
       if (!activeSets.has(setName) || !mapping[setName]) continue;
       for (const binding of mapping[setName]) {
         const { root, priority } = binding;
+        const prevBinding = priorityMap.get(root);
         if (!root || !priority) {
           activeBindings.add(binding);
-        } else if (!prioritizedBindings.has(root)) {
+        } else if (!prevBinding) {
           activeBindings.add(binding);
-          prioritizedBindings.set(root, binding);
-        } else {
-          const prevPriority = prioritizedBindings.get(root).priority;
-          if (priority > prevPriority) {
-            activeBindings.delete(prioritizedBindings.get(root));
-            activeBindings.add(binding);
-            prioritizedBindings.set(root, binding);
-          } else if (prevPriority === priority) {
-            console.error("equal priorities on same root", binding, prioritizedBindings.get(root));
-          }
+          priorityMap.set(root, binding);
+        } else if (priority > prevBinding.priority) {
+          activeBindings.delete(priorityMap.get(root));
+          activeBindings.add(binding);
+          priorityMap.set(root, binding);
+        } else if (prevBinding.priority === priority) {
+          console.error("equal priorities on same root", binding, priorityMap.get(root));
         }
       }
     }
@@ -57,11 +55,11 @@ function prioritizeBindings(registeredMappings, activeSets) {
 }
 
 AFRAME.registerSystem("userinput", {
-  readFrameValueAtPath(path) {
+  get(path) {
     return this.frame && this.frame[path];
   },
 
-  toggleActive(set, value) {
+  toggleSet(set, value) {
     this.pendingSetChanges.push({ set, value });
   },
 
diff --git a/src/systems/userinput/userinput.md b/src/systems/userinput/userinput.md
index d3701d5c93dd34a4be3828108d29a7a5c094c2a0..b8ceb54539665f0130863a6018b187bf0562a8aa 100644
--- a/src/systems/userinput/userinput.md
+++ b/src/systems/userinput/userinput.md
@@ -1,19 +1,4 @@
 
-# Table of Contents
-
-1.  [The userinput system](#org6030eab)
-    1.  [Overview](#org2da9acd)
-    2.  [Terms and Conventions](#org4721ce9)
-        1.  [path](#orgd62cc68)
-        2.  [action](#orgb8066a6)
-        3.  [frame](#org15eafde)
-        4.  [device](#orgea2f123)
-        5.  [binding](#org47c9c20)
-        6.  [xforms](#org876e7b0)
-        7.  [set](#orgbe4669b)
-        8.  [priority and root](#orgdd3c0c5)
-
-
 <a id="org6030eab"></a>
 
 # The userinput system
@@ -25,7 +10,7 @@ The userinput system is a module that manages mappings from device state changes
 
 ## Overview
 
-The userinput system happens to be an `aframe` `system`; its `tick` is called once a frame within the `aframe` `scene`'s `tick`. When the userinput system `tick` happens, it is responsible for creating a map called the frame. The keys of the frame are called "paths". The values stored in the frame can be any type, but are usually one of: bool, number, vec2, vec3, vec4, pose. On each tick, each connected `device` writes "raw" input values to known "device paths" within the frame. Configuration units called `bindings` are then applied to transform "raw" input values to app-specific "actions". The userinput system exposes the state of a given `action` in the current frame via `readFrameValueAtPath`. The `bindings` that are applied to transform input to "actions" must be `available`, `active`, and `prioritized`.
+The userinput system happens to be an `aframe` `system`; its `tick` is called once a frame within the `aframe` `scene`'s `tick`. When the userinput system `tick` happens, it is responsible for creating a map called the frame. The keys of the frame are called "paths". The values stored in the frame can be any type, but are usually one of: bool, number, vec2, vec3, vec4, pose. On each tick, each connected `device` writes "raw" input values to known "device paths" within the frame. Configuration units called `bindings` are then applied to transform "raw" input values to app-specific "actions". The userinput system exposes the state of a given `action` in the current frame via `get`. The `bindings` that are applied to transform input to "actions" must be `available`, `active`, and `prioritized`.
 
 1.  A `binding` is made `available` when the userinput system detects a change to the user's device configuration that matches certain criteria. A touchscreen user only has `availableBindings` related to touchscreen input. A mouse-and-keyboard user only has `availableBindings` related to mouse-and-keyboard input. An oculus/vive user has `bindings` related to mouse, keyboard, and oculus/vive controllers.
 
@@ -70,14 +55,14 @@ A path is used as a key when writing or querying the state a user input frame. P
 A path used by app code when reading a user input frame.
 
     const userinput = AFRAME.scenes[0].systems.userinput;
-    if (userinput.readFrameValueAtPath("/actions/rightHandGrab")) {
+    if (userinput.get("/actions/rightHandGrab")) {
       this.startInteraction();
     }
 
 The value in the frame can be of any type, but we have tried to keep it to simple types like bool, number, vec2, vec3, and pose.
 
     const userinput = AFRAME.scenes[0].systems.userinput;
-    const acceleration = userinput.readFrameValueAtPath("/actions/characterAcceleration");
+    const acceleration = userinput.get("/actions/characterAcceleration");
     this.updateVelocity( this.velocity, acceleration || zero );
     this.move( this.velocity );