diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js
index 6f2662fd4e6c698d9e26e1f19096f3f716266fcb..71f50fde5a83d6c01a0739744a724107f7c9293f 100644
--- a/src/components/cursor-controller.js
+++ b/src/components/cursor-controller.js
@@ -25,6 +25,9 @@ AFRAME.registerComponent("cursor-controller", {
   init: function() {
     this.inVR = false;
     this.isMobile = AFRAME.utils.device.isMobile();
+    if (this.isMobile) {
+      this._setCursorVisibility(false);
+    }
     this.hasPointingDevice = false;
     this.currentTargetType = TARGET_TYPE_NONE;
     this.grabStarting = false;
@@ -268,6 +271,7 @@ AFRAME.registerComponent("cursor-controller", {
     // Cursor position must be synced to physics before constraint is created
     cursor.components["static-body"].syncToPhysics();
     cursor.emit("cursor-grab", {});
+    this._setCursorVisibility(false);
     return true;
   },
 
@@ -282,6 +286,7 @@ AFRAME.registerComponent("cursor-controller", {
     if (!this.isMobile || this.hasPointingDevice) return;
 
     this.data.cursor.emit("cursor-release", {});
+    this._setCursorVisibility(false);
   },
 
   _handleMouseDown: function() {
diff --git a/src/components/look-on-mobile.js b/src/components/look-on-mobile.js
index 61f014bcbafa0e752e548b075b06f5d691c2eb21..53670eb8fa96185d4be311f4f78e16e41a8dc6f3 100644
--- a/src/components/look-on-mobile.js
+++ b/src/components/look-on-mobile.js
@@ -1,5 +1,32 @@
 const PolyfillControls = AFRAME.utils.device.PolyfillControls;
 const PI_4 = Math.PI / 4;
+const PI_2 = Math.PI / 2;
+const TWOPI = Math.PI * 2;
+
+const abs = Math.abs;
+// Input: two numbers between [-Math.PI, Math.PI]
+// Output: difference between them, where -Math.PI === Math.PI
+const difference = (curr, prev) => {
+  const a = curr - prev;
+  const b = curr + TWOPI - prev;
+  const c = curr - (prev + TWOPI);
+  if (abs(a) < abs(b)) {
+    if (abs(a) < abs(c)) {
+      return a;
+    }
+  }
+  if (abs(b) < abs(c)) {
+    return b;
+  }
+
+  return c;
+};
+
+const average = a => {
+  let sum = 0;
+  a.forEach(n => (sum += n));
+  return sum / a.length;
+};
 
 AFRAME.registerComponent("look-on-mobile", {
   schema: {
@@ -12,21 +39,25 @@ AFRAME.registerComponent("look-on-mobile", {
     this.hmdEuler = new THREE.Euler();
     this.prevX = this.hmdEuler.x;
     this.prevY = this.hmdEuler.y;
-    this.polyfillObject = new THREE.Object3D();
-    this.polyfillControls = new PolyfillControls(this.polyfillObject);
     this.ticks = 0;
     this.pendingLookX = 0;
     this.onRotateX = this.onRotateX.bind(this);
+    this.dXBuffer = [];
+    this.dYBuffer = [];
   },
 
   play() {
     this.el.addEventListener("rotateX", this.onRotateX);
+    this.polyfillObject = new THREE.Object3D();
+    this.polyfillControls = new PolyfillControls(this.polyfillObject);
   },
   pause() {
     this.el.removeEventListener("rotateX", this.onRotateX);
+    this.polyfillControls = null;
+    this.polyfillObject = null;
   },
   onRotateX(e) {
-    this.pendingLookX = e.detail.value * 2;
+    this.pendingLookX = e.detail.value * 0.8;
   },
 
   registerLookControls(lookControls) {
@@ -41,17 +72,27 @@ AFRAME.registerComponent("look-on-mobile", {
     const hmdEuler = this.hmdEuler;
     const pitchObject = this.lookControls.pitchObject;
     const yawObject = this.lookControls.yawObject;
+    const joystick = this.pendingLookX * dt / 1000;
     const { horizontalLookSpeedRatio, verticalLookSpeedRatio } = this.data;
     if (scene.is("vr-mode") && scene.checkHeadsetConnected()) return;
     this.polyfillControls.update();
     hmdEuler.setFromQuaternion(this.polyfillObject.quaternion, "YXZ");
 
-    const joystick = this.pendingLookX * dt / 1000;
-    const dX = joystick + hmdEuler.x - this.prevX;
-    const dY = hmdEuler.y - this.prevY;
+    const dX = difference(hmdEuler.x, this.prevX);
+    const dY = difference(hmdEuler.y, this.prevY);
+
+    this.dXBuffer.push(Math.abs(dX) < 0.001 ? 0 : dX);
+    this.dYBuffer.push(Math.abs(dY) < 0.001 ? 0 : dY);
+
+    if (this.dXBuffer.length > 5) {
+      this.dXBuffer.splice(0, 1);
+    }
+    if (this.dYBuffer.length > 5) {
+      this.dYBuffer.splice(0, 1);
+    }
 
-    yawObject.rotation.y += dY * horizontalLookSpeedRatio;
-    pitchObject.rotation.x += dX * verticalLookSpeedRatio;
+    yawObject.rotation.y += average(this.dYBuffer) * horizontalLookSpeedRatio;
+    pitchObject.rotation.x += average(this.dXBuffer) * verticalLookSpeedRatio + joystick;
     pitchObject.rotation.x = Math.max(-PI_4, Math.min(PI_4, pitchObject.rotation.x));
 
     this.lookControls.updateOrientation();
diff --git a/src/components/pinch-to-move.js b/src/components/pinch-to-move.js
index 9b8e1c1d446258d56d7c7c3fdce6551a37aaab7e..79e51f0dab313dc36facf3a37ac2a326053ac0e7 100644
--- a/src/components/pinch-to-move.js
+++ b/src/components/pinch-to-move.js
@@ -1,6 +1,6 @@
 AFRAME.registerComponent("pinch-to-move", {
   schema: {
-    speed: { default: 0.35 }
+    speed: { default: 0.25 }
   },
   init() {
     this.onPinch = this.onPinch.bind(this);
diff --git a/src/utils/touch-events-handler.js b/src/utils/touch-events-handler.js
index 423a40aeaa373b9408d966a8cedaea542ef83cdd..b652dc9b8024274684a9f95fc9c9548a93420658 100644
--- a/src/utils/touch-events-handler.js
+++ b/src/utils/touch-events-handler.js
@@ -1,5 +1,5 @@
 const VIRTUAL_JOYSTICK_HEIGHT = 0.8;
-const HORIZONTAL_LOOK_SPEED = 0.005;
+const HORIZONTAL_LOOK_SPEED = 0.006;
 const VERTICAL_LOOK_SPEED = 0.003;
 const PI_4 = Math.PI / 4;
 
@@ -63,11 +63,13 @@ export default class TouchEventsHandler {
   }
 
   handleTouchStart(e) {
-    Array.prototype.forEach.call(e.touches, this.singleTouchStart);
+    Array.prototype.forEach.call(e.changedTouches, this.singleTouchStart);
   }
 
   singleTouchStart(touch) {
-    if (touch.clientY / window.innerHeight >= VIRTUAL_JOYSTICK_HEIGHT) return;
+    if (touch.clientY / window.innerHeight >= VIRTUAL_JOYSTICK_HEIGHT) {
+      return;
+    }
     if (!this.touchReservedForCursor && this.cursor.handleTouchStart(touch)) {
       this.touchReservedForCursor = touch;
     }
@@ -88,7 +90,9 @@ export default class TouchEventsHandler {
       return;
     }
     if (touch.clientY / window.innerHeight >= VIRTUAL_JOYSTICK_HEIGHT) return;
-    if (!this.touches.some(t => touch.identifier === t.identifier)) return;
+    if (!this.touches.some(t => touch.identifier === t.identifier)) {
+      return;
+    }
 
     let pinchIndex = this.touchesReservedForPinch.findIndex(t => touch.identifier === t.identifier);
     if (pinchIndex !== -1) {
@@ -132,8 +136,8 @@ export default class TouchEventsHandler {
     const dX = touch.clientX - prevTouch.clientX;
     const dY = touch.clientY - prevTouch.clientY;
 
-    this.lookControls.yawObject.rotation.y -= dX * HORIZONTAL_LOOK_SPEED;
-    this.lookControls.pitchObject.rotation.x -= dY * VERTICAL_LOOK_SPEED;
+    this.lookControls.yawObject.rotation.y += dX * HORIZONTAL_LOOK_SPEED;
+    this.lookControls.pitchObject.rotation.x += dY * VERTICAL_LOOK_SPEED;
     this.lookControls.pitchObject.rotation.x = Math.max(
       -PI_4,
       Math.min(PI_4, this.lookControls.pitchObject.rotation.x)
@@ -145,16 +149,18 @@ export default class TouchEventsHandler {
   }
 
   singleTouchEnd(touch) {
+    const touchIndex = this.touches.findIndex(t => touch.identifier === t.identifier);
+    if (touchIndex === -1) {
+      return;
+    }
+    this.touches.splice(touchIndex, 1);
+
     if (this.touchReservedForCursor && touch.identifier === this.touchReservedForCursor.identifier) {
       this.cursor.handleTouchEnd(touch);
       this.touchReservedForCursor = null;
       return;
     }
 
-    const touchIndex = this.touches.findIndex(t => touch.identifier === t.identifier);
-    if (touchIndex === -1) return;
-    this.touches.splice(touchIndex, 1);
-
     const pinchIndex = this.touchesReservedForPinch.findIndex(t => touch.identifier === t.identifier);
     if (pinchIndex !== -1) {
       this.touchesReservedForPinch.splice(pinchIndex, 1);