diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js index e147c6bf4e5e7097b532e42a5d955a27ff30ddc5..0947f60406f4f3d651b9134939c115d142a61013 100644 --- a/src/components/cursor-controller.js +++ b/src/components/cursor-controller.js @@ -3,383 +3,190 @@ const TARGET_TYPE_INTERACTABLE = 2; const TARGET_TYPE_UI = 4; const TARGET_TYPE_INTERACTABLE_OR_UI = TARGET_TYPE_INTERACTABLE | TARGET_TYPE_UI; -/** - * Controls virtual cursor behavior in various modalities to affect teleportation, interatables and UI. - * @namespace user-input - * @component cursor-controller - */ AFRAME.registerComponent("cursor-controller", { dependencies: ["raycaster", "line"], schema: { cursor: { type: "selector" }, camera: { type: "selector" }, - playerRig: { type: "selector" }, - gazeTeleportControls: { type: "selector" }, - physicalHandSelector: { type: "string" }, - handedness: { default: "right", oneOf: ["right", "left"] }, maxDistance: { default: 3 }, - minDistance: { default: 0.5 }, + minDistance: { default: 0 }, cursorColorHovered: { default: "#2F80ED" }, cursorColorUnhovered: { default: "#FFFFFF" }, - primaryDown: { default: "action_primary_down" }, - primaryUp: { default: "action_primary_up" }, - grabEvent: { default: "action_grab" }, - releaseEvent: { default: "action_release" } + rayObject: { type: "selector" }, + useMousePos: { default: true }, + drawLine: { default: false } }, init: function() { + this.enabled = true; this.inVR = false; this.isMobile = AFRAME.utils.device.isMobile(); - this.hasPointingDevice = false; this.currentTargetType = TARGET_TYPE_NONE; - this.grabStarting = false; this.currentDistance = this.data.maxDistance; this.currentDistanceMod = 0; this.mousePos = new THREE.Vector2(); - this.controller = null; - this.controllerQueue = []; this.wasCursorHovered = false; - this.wasPhysicalHandGrabbing = false; this.origin = new THREE.Vector3(); this.direction = new THREE.Vector3(); this.controllerQuaternion = new THREE.Quaternion(); this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered }); - window.APP.touchEventsHandler.registerCursor(this); - window.APP.touchEventsHandler.registerPinchEmitter(this.el); - this.handleTouchStart = this.handleTouchStart.bind(this); - this.handleTouchMove = this.handleTouchMove.bind(this); - this.handleTouchEnd = this.handleTouchEnd.bind(this); + this.forceCursorUpdate = this.forceCursorUpdate.bind(this); + this.startInteraction = this.startInteraction.bind(this); + this.moveCursor = this.moveCursor.bind(this); + this.endInteraction = this.endInteraction.bind(this); + this.changeDistanceMod = this.changeDistanceMod.bind(this); + this.setRaycasterWithMousePos = this.setRaycasterWithMousePos.bind(this); + this.updateDistanceAndTargetType = this.updateDistanceAndTargetType.bind(this); - window.APP.mouseEventsHandler.registerCursor(this); - this.handleMouseDown = this.handleMouseDown.bind(this); - this.handleMouseMove = this.handleMouseMove.bind(this); - this.handleMouseUp = this.handleMouseUp.bind(this); - this.handleMouseWheel = this.handleMouseWheel.bind(this); - - this._handleEnterVR = this._handleEnterVR.bind(this); - this._handleExitVR = this._handleExitVR.bind(this); - this._handlePrimaryDown = this._handlePrimaryDown.bind(this); - this._handlePrimaryUp = this._handlePrimaryUp.bind(this); - this._handleModelLoaded = this._handleModelLoaded.bind(this); this._handleCursorLoaded = this._handleCursorLoaded.bind(this); - this._handleControllerConnected = this._handleControllerConnected.bind(this); - this._handleControllerDisconnected = this._handleControllerDisconnected.bind(this); - this.data.cursor.addEventListener("loaded", this._handleCursorLoaded); }, - remove: function() { - this.data.cursor.removeEventListener("loaded", this._handleCursorLoaded); + enable: function() { + this.enabled = true; }, - update: function(oldData) { - if (oldData.physicalHandSelector !== this.data.physicalHandSelector) { - this._handleModelLoaded(); - } - - if (oldData.handedness !== this.data.handedness) { - //TODO - } + disable: function() { + this.enabled = false; + this.setCursorVisibility(false); }, - play: function() { - window.addEventListener("enter-vr", this._handleEnterVR); - window.addEventListener("exit-vr", this._handleExitVR); - - this.data.playerRig.addEventListener(this.data.primaryDown, this._handlePrimaryDown); - this.data.playerRig.addEventListener(this.data.primaryUp, this._handlePrimaryUp); - this.data.playerRig.addEventListener(this.data.grabEvent, this._handlePrimaryDown); - this.data.playerRig.addEventListener(this.data.releaseEvent, this._handlePrimaryUp); - this.data.playerRig.addEventListener("cardboardbuttondown", this._handlePrimaryDown); - this.data.playerRig.addEventListener("cardboardbuttonup", this._handlePrimaryUp); - this.data.playerRig.addEventListener("model-loaded", this._handleModelLoaded); - - this.el.sceneEl.addEventListener("controllerconnected", this._handleControllerConnected); - this.el.sceneEl.addEventListener("controllerdisconnected", this._handleControllerDisconnected); + update: function() { + if (this.data.rayObject) { + this.rayObject = this.data.rayObject.object3D; + } }, - pause: function() { - window.removeEventListener("enter-vr", this._handleEnterVR); - window.removeEventListener("exit-vr", this._handleExitVR); - - this.data.playerRig.removeEventListener(this.data.primaryDown, this._handlePrimaryDown); - this.data.playerRig.removeEventListener(this.data.primaryUp, this._handlePrimaryUp); - this.data.playerRig.removeEventListener(this.data.grabEvent, this._handlePrimaryDown); - this.data.playerRig.removeEventListener(this.data.releaseEvent, this._handlePrimaryUp); - this.data.playerRig.removeEventListener("cardboardbuttondown", this._handlePrimaryDown); - this.data.playerRig.removeEventListener("cardboardbuttonup", this._handlePrimaryUp); + tick: (() => { + const rayObjectRotation = new THREE.Quaternion(); - this.data.playerRig.removeEventListener("model-loaded", this._handleModelLoaded); + return function() { + if (!this.enabled) { + return; + } - this.el.sceneEl.removeEventListener("controllerconnected", this._handleControllerConnected); - this.el.sceneEl.removeEventListener("controllerdisconnected", this._handleControllerDisconnected); - }, + if (this.data.useMousePos) { + this.setRaycasterWithMousePos(); + } else { + //this.rayObject.updateMatrixWorld(); + rayObjectRotation.setFromRotationMatrix(this.rayObject.matrixWorld); + this.direction + .set(0, 0, 1) + .applyQuaternion(rayObjectRotation) + .normalize(); + this.origin.setFromMatrixPosition(this.rayObject.matrixWorld); + } + this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction }); + + const isGrabbing = this.data.cursor.components["super-hands"].state.has("grab-start"); + if (isGrabbing) { + const distance = Math.min( + this.data.maxDistance, + Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod) + ); + this.direction.multiplyScalar(distance); + this.data.cursor.object3D.position.addVectors(this.origin, this.direction); + } else { + this.currentDistanceMod = 0; + this.updateDistanceAndTargetType(); + + const isTarget = this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI); + if (isTarget && !this.wasCursorHovered) { + this.wasCursorHovered = true; + this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered }); + } else if (!isTarget && this.wasCursorHovered) { + this.wasCursorHovered = false; + this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered }); + } + } - tick: function() { - //handle physical hand - if (this.physicalHand) { - const state = this.physicalHand.components["super-hands"].state; - const isPhysicalHandGrabbing = state.has("grab-start") || state.has("hover-start"); - if (this.wasPhysicalHandGrabbing != isPhysicalHandGrabbing) { - this._setCursorVisibility(!isPhysicalHandGrabbing); - this.currentTargetType = TARGET_TYPE_NONE; + if (this.data.drawLine) { + this.el.setAttribute("line", { start: this.origin.clone(), end: this.data.cursor.object3D.position.clone() }); } - this.wasPhysicalHandGrabbing = isPhysicalHandGrabbing; - if (isPhysicalHandGrabbing) return; - } + }; + })(), - //set raycaster origin/direction + setRaycasterWithMousePos() { const camera = this.data.camera.components.camera.camera; - if (!this.inVR) { - //mouse cursor mode - const raycaster = this.el.components.raycaster.raycaster; - raycaster.setFromCamera(this.mousePos, camera); - this.origin.copy(raycaster.ray.origin); - this.direction.copy(raycaster.ray.direction); - } else if ((this.inVR || this.isMobile) && !this.hasPointingDevice) { - //gaze cursor mode - camera.getWorldPosition(this.origin); - camera.getWorldDirection(this.direction); - } else if (this.controller != null) { - //3d cursor mode - this.controller.object3D.getWorldPosition(this.origin); - this.controller.object3D.getWorldQuaternion(this.controllerQuaternion); - this.direction - .set(0, 0, -1) - .applyQuaternion(this.controllerQuaternion) - .normalize(); - } - - this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction }); + const raycaster = this.el.components.raycaster.raycaster; + raycaster.setFromCamera(this.mousePos, camera); + this.origin.copy(raycaster.ray.origin); + this.direction.copy(raycaster.ray.direction); + this.el.setAttribute("raycaster", { origin: raycaster.ray.origin, direction: raycaster.ray.direction }); + }, + updateDistanceAndTargetType() { let intersection = null; - - //update cursor position - if (!this._isGrabbing()) { - const intersections = this.el.components.raycaster.intersections; - if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) { - intersection = intersections[0]; - this.data.cursor.object3D.position.copy(intersection.point); - this.currentDistance = intersections[0].distance; - } else { - this.currentDistance = this.data.maxDistance; - } - this.currentDistanceMod = 0; - } - - if (this._isGrabbing() || !intersection) { - const max = Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod); - const distance = Math.min(max, this.data.maxDistance); - this.currentDistanceMod = this.currentDistance - distance; - this.direction.multiplyScalar(distance); + const intersections = this.el.components.raycaster.intersections; + if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) { + intersection = intersections[0]; + this.data.cursor.object3D.position.copy(intersection.point); + this.currentDistance = intersections[0].distance; + } else { + this.currentDistance = this.data.maxDistance; + this.direction.multiplyScalar(this.currentDistance); this.data.cursor.object3D.position.addVectors(this.origin, this.direction); } - //update currentTargetType - if (this._isGrabbing() && !intersection) { - this.currentTargetType = TARGET_TYPE_INTERACTABLE; - } else if (intersection) { - if (intersection.object.el.matches(".interactable, .interactable *")) { - this.currentTargetType = TARGET_TYPE_INTERACTABLE; - } else if (intersection.object.el.matches(".ui, .ui *")) { - this.currentTargetType = TARGET_TYPE_UI; - } - } else { + if (!intersection) { this.currentTargetType = TARGET_TYPE_NONE; - } - - //update cursor material - const isTarget = this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI); - if ((this._isGrabbing() || isTarget) && !this.wasCursorHovered) { - this.wasCursorHovered = true; - this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered }); - } else if (!this._isGrabbing() && !isTarget && this.wasCursorHovered) { - this.wasCursorHovered = false; - this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered }); - } - - //update line - if (this.hasPointingDevice) { - this.el.setAttribute("line", { start: this.origin.clone(), end: this.data.cursor.object3D.position.clone() }); + } else if (intersection.object.el.matches(".interactable, .interactable *")) { + this.currentTargetType = TARGET_TYPE_INTERACTABLE; + } else if (intersection.object.el.matches(".ui, .ui *")) { + this.currentTargetType = TARGET_TYPE_UI; } }, - _isGrabbing() { - return this.data.cursor.components["super-hands"].state.has("grab-start"); - }, - _isTargetOfType: function(mask) { return (this.currentTargetType & mask) === this.currentTargetType; }, - _setCursorVisibility(visible) { + setCursorVisibility(visible) { this.data.cursor.setAttribute("visible", visible); - this.el.setAttribute("line", { visible: visible && this.hasPointingDevice }); - }, - - _startTeleport: function() { - if (this.controller != null) { - this.controller.emit("cursor-teleport_down", {}); - } else if (this.inVR) { - this.data.gazeTeleportControls.emit("cursor-teleport_down", {}); - } - this._setCursorVisibility(false); - }, - - _endTeleport: function() { - if (this.controller != null) { - this.controller.emit("cursor-teleport_up", {}); - } else if (this.inVR) { - this.data.gazeTeleportControls.emit("cursor-teleport_up", {}); - } - this._setCursorVisibility(true); + this.el.setAttribute("line", { visible: visible && this.data.drawLine }); }, - handleTouchStart: function(touch) { - // Update the ray and cursor positions - const raycasterComp = this.el.components.raycaster; - const raycaster = raycasterComp.raycaster; - const camera = this.data.camera.components.camera.camera; - const cursor = this.data.cursor; - this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1); - raycaster.setFromCamera(this.mousePos, camera); - this.el.setAttribute("raycaster", { origin: raycaster.ray.origin, direction: raycaster.ray.direction }); - raycasterComp.checkIntersections(); - const intersections = raycasterComp.intersections; - if (intersections.length === 0 || intersections[0].distance >= this.data.maxDistance) { - return; - } - cursor.object3D.position.copy(intersections[0].point); - // Cursor position must be synced to physics before constraint is created - cursor.components["static-body"].syncToPhysics(); - cursor.emit("cursor-grab", {}); - return true; - }, - - handleTouchMove: function(touch) { - this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1); - }, - - handleTouchEnd: function() { - this.data.cursor.emit("cursor-release", {}); + forceCursorUpdate: function() { + this.setRaycasterWithMousePos(); + this.el.components.raycaster.checkIntersections(); + this.updateDistanceAndTargetType(); + this.data.cursor.components["static-body"].syncToPhysics(); }, - handleMouseDown: function() { + startInteraction: function() { if (this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI)) { this.data.cursor.emit("cursor-grab", {}); return true; - } else if (this.inVR || this.isMobile) { - this._startTeleport(); - return; } return false; }, - handleMouseMove: function(e) { - this.mousePos.set(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1); + moveCursor: function(x, y) { + this.mousePos.set(x, y); }, - handleMouseUp: function() { + endInteraction: function() { this.data.cursor.emit("cursor-release", {}); - this._endTeleport(); - }, - - handleMouseWheel: function(e) { - if (this._isGrabbing()) { - switch (e.deltaMode) { - case e.DOM_DELTA_PIXEL: - this.currentDistanceMod += e.deltaY / 500; - break; - case e.DOM_DELTA_LINE: - this.currentDistanceMod += e.deltaY / 10; - break; - case e.DOM_DELTA_PAGE: - this.currentDistanceMod += e.deltaY / 2; - break; - } - } - }, - - _handleEnterVR: function() { - this.inVR = true; - this._updateController(); - }, - - _handleExitVR: function() { - this.inVR = false; - this._updateController(); - }, - - _handlePrimaryDown: function(e) { - if (e.target === this.controller || e.target === this.data.playerRig) { - const isInteractable = this._isTargetOfType(TARGET_TYPE_INTERACTABLE) && !this.grabStarting; - if (isInteractable || this._isTargetOfType(TARGET_TYPE_UI)) { - this.grabStarting = true; - this.data.cursor.emit("cursor-grab", e.detail); - } else if (e.type !== this.data.grabEvent) { - this._startTeleport(); - } - } }, - _handlePrimaryUp: function(e) { - if (e.target === this.controller || e.target === this.data.playerRig) { - this.grabStarting = false; - if (this._isGrabbing() || this._isTargetOfType(TARGET_TYPE_UI)) { - this.data.cursor.emit("cursor-release", e.detail); - } else if (e.type !== this.data.releaseEvent) { - this._endTeleport(); - } + changeDistanceMod: function(delta) { + const { minDistance, maxDistance } = this.data; + const targetDistanceMod = this.currentDistanceMod + delta; + const moddedDistance = this.currentDistance - targetDistanceMod; + if (moddedDistance > maxDistance || moddedDistance < minDistance) { + return; } - }, - - _handleModelLoaded: function() { - this.physicalHand = this.data.playerRig.querySelector(this.data.physicalHandSelector); + this.currentDistanceMod = targetDistanceMod; }, _handleCursorLoaded: function() { this.data.cursor.object3DMap.mesh.renderOrder = window.APP.RENDER_ORDER.CURSOR; + this.data.cursor.removeEventListener("loaded", this._handleCursorLoaded); }, - _handleControllerConnected: function(e) { - const data = { - controller: e.target, - handedness: e.detail.component.data.hand - }; - - if (data.handedness === this.data.handedness) { - this.controllerQueue.unshift(data); - } else { - this.controllerQueue.push(data); - } - - this._updateController(); - }, - - _handleControllerDisconnected: function(e) { - for (let i = 0; i < this.controllerQueue.length; i++) { - if (e.target === this.controllerQueue[i].controller) { - this.controllerQueue.splice(i, 1); - this._updateController(); - return; - } - } - }, - - _updateController: function() { - this.hasPointingDevice = this.controllerQueue.length > 0 && this.inVR; - - this._setCursorVisibility(this.hasPointingDevice || this.isMobile); - - if (this.hasPointingDevice) { - const controllerData = this.controllerQueue[0]; - const hand = controllerData.handedness; - this.el.setAttribute("cursor-controller", { physicalHandSelector: `#player-${hand}-controller` }); - this.controller = controllerData.controller; - } else { - this.controller = null; - } + remove: function() { + this.data.cursor.removeEventListener("loaded", this._handleCursorLoaded); } }); diff --git a/src/components/input-configurator.js b/src/components/input-configurator.js new file mode 100644 index 0000000000000000000000000000000000000000..a5bcc0cd6cdd72a0105c7ea1d0161368b4fd4cd1 --- /dev/null +++ b/src/components/input-configurator.js @@ -0,0 +1,171 @@ +import TouchEventsHandler from "../utils/touch-events-handler.js"; +import MouseEventsHandler from "../utils/mouse-events-handler.js"; +import GearVRMouseEventsHandler from "../utils/gearvr-mouse-events-handler.js"; +import ActionEventHandler from "../utils/action-event-handler.js"; + +AFRAME.registerComponent("input-configurator", { + schema: { + cursorController: { type: "selector" }, + gazeTeleporter: { type: "selector" }, + camera: { type: "selector" }, + playerRig: { type: "selector" }, + leftController: { type: "selector" }, + rightController: { type: "selector" }, + leftControllerRayObject: { type: "string" }, + rightControllerRayObject: { type: "string" }, + gazeCursorRayObject: { type: "string" } + }, + + init() { + this.inVR = this.el.sceneEl.is("vr-mode"); + this.isMobile = AFRAME.utils.device.isMobile(); + this.eventHandlers = []; + this.controllerQueue = []; + this.hasPointingDevice = false; + this.cursor = this.data.cursorController.components["cursor-controller"]; + this.gazeTeleporter = this.data.gazeTeleporter.components["teleport-controls"]; + this.cameraController = this.data.camera.components["pitch-yaw-rotator"]; + this.playerRig = this.data.playerRig; + this.handedness = "right"; + + this.onEnterVR = this.onEnterVR.bind(this); + this.onExitVR = this.onExitVR.bind(this); + this.tearDown = this.tearDown.bind(this); + this.configureInput = this.configureInput.bind(this); + this.addLookOnMobile = this.addLookOnMobile.bind(this); + this.handleControllerConnected = this.handleControllerConnected.bind(this); + this.handleControllerDisconnected = this.handleControllerDisconnected.bind(this); + + this.configureInput(); + }, + + play() { + this.el.sceneEl.addEventListener("controllerconnected", this.handleControllerConnected); + this.el.sceneEl.addEventListener("controllerdisconnected", this.handleControllerDisconnected); + this.el.sceneEl.addEventListener("enter-vr", this.onEnterVR); + this.el.sceneEl.addEventListener("exit-vr", this.onExitVR); + }, + + pause() { + this.el.sceneEl.removeEventListener("controllerconnected", this.handleControllerConnected); + this.el.sceneEl.removeEventListener("controllerdisconnected", this.handleControllerDisconnected); + this.el.sceneEl.removeEventListener("enter-vr", this.onEnterVR); + this.el.sceneEl.removeEventListener("exit-vr", this.onExitVR); + }, + + onEnterVR() { + this.inVR = true; + this.tearDown(); + this.configureInput(); + this.updateController(); + }, + + onExitVR() { + this.inVR = false; + this.tearDown(); + this.configureInput(); + this.updateController(); + }, + + tearDown() { + this.eventHandlers.forEach(h => h.tearDown()); + this.eventHandlers = []; + this.actionEventHandler = null; + if (this.lookOnMobile) { + this.lookOnMobile.el.removeComponent("look-on-mobile"); + this.lookOnMobile = null; + } + this.cursorRequiresManagement = false; + }, + + addLookOnMobile() { + const onAdded = e => { + if (e.detail.name !== "look-on-mobile") return; + this.lookOnMobile = this.el.sceneEl.components["look-on-mobile"]; + }; + this.el.sceneEl.addEventListener("componentinitialized", onAdded); + // This adds look-on-mobile to the scene + this.el.sceneEl.setAttribute("look-on-mobile", "camera", this.data.camera); + }, + + configureInput() { + this.actionEventHandler = new ActionEventHandler(this.el.sceneEl, this.cursor); + this.eventHandlers.push(this.actionEventHandler); + + this.cursor.el.setAttribute("cursor-controller", "useMousePos", !this.inVR); + + if (this.inVR) { + this.cameraController.pause(); + this.cursorRequiresManagement = true; + this.cursor.el.setAttribute("cursor-controller", "minDistance", 0); + if (this.isMobile) { + this.eventHandlers.push(new GearVRMouseEventsHandler(this.cursor, this.gazeTeleporter)); + } else { + this.eventHandlers.push(new MouseEventsHandler(this.cursor, this.cameraController)); + } + } else { + this.cameraController.play(); + if (this.isMobile) { + this.eventHandlers.push(new TouchEventsHandler(this.cursor, this.cameraController, this.cursor.el)); + this.addLookOnMobile(); + } else { + this.eventHandlers.push(new MouseEventsHandler(this.cursor, this.cameraController)); + this.cursor.el.setAttribute("cursor-controller", "minDistance", 0.3); + } + } + }, + + tick() { + if (this.cursorRequiresManagement && this.controller) { + this.actionEventHandler.manageCursorEnabled(); + } + }, + + handleControllerConnected: function(e) { + const data = { + controller: e.target, + handedness: e.detail.component.data.hand + }; + + if (data.handedness === this.handedness) { + this.controllerQueue.unshift(data); + } else { + this.controllerQueue.push(data); + } + + this.updateController(); + }, + + handleControllerDisconnected: function(e) { + for (let i = 0; i < this.controllerQueue.length; i++) { + if (e.target === this.controllerQueue[i].controller) { + this.controllerQueue.splice(i, 1); + this.updateController(); + return; + } + } + }, + + updateController: function() { + this.hasPointingDevice = this.controllerQueue.length > 0 && this.inVR; + this.cursor.el.setAttribute("cursor-controller", "drawLine", this.hasPointingDevice); + + this.cursor.setCursorVisibility(true); + + if (this.hasPointingDevice) { + const controllerData = this.controllerQueue[0]; + const hand = controllerData.handedness; + this.controller = controllerData.controller; + this.cursor.el.setAttribute("cursor-controller", { + rayObject: hand === "left" ? this.data.leftControllerRayObject : this.data.rightControllerRayObject + }); + } else { + this.controller = null; + this.cursor.el.setAttribute("cursor-controller", { rayObject: this.data.gazeCursorRayObject }); + } + + if (this.actionEventHandler) { + this.actionEventHandler.setHandThatAlsoDrivesCursor(this.controller); + } + } +}); diff --git a/src/components/look-on-mobile.js b/src/components/look-on-mobile.js index 7c99a261c5886f3652ca4b6c1655e2ad1057b0e5..768df1acdf3073ba65192abc0e5250d9b8e58769 100644 --- a/src/components/look-on-mobile.js +++ b/src/components/look-on-mobile.js @@ -1,5 +1,4 @@ const PolyfillControls = AFRAME.utils.device.PolyfillControls; -const PI_4 = Math.PI / 4; const TWOPI = Math.PI * 2; const abs = Math.abs; @@ -29,9 +28,9 @@ const average = a => { AFRAME.registerComponent("look-on-mobile", { schema: { - enabled: { default: false }, horizontalLookSpeedRatio: { default: 0.4 }, // motion applied to camera / motion of polyfill object - verticalLookSpeedRatio: { default: 0.4 } // motion applied to camera / motion of polyfill object + verticalLookSpeedRatio: { default: 0.4 }, // motion applied to camera / motion of polyfill object + camera: { type: "selector" } }, init() { @@ -49,35 +48,29 @@ AFRAME.registerComponent("look-on-mobile", { 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 * 0.8; + + update() { + this.cameraController = this.data.camera.components["pitch-yaw-rotator"]; }, - registerLookControls(lookControls) { - this.lookControls = lookControls; - this.lookControls.data.enabled = false; - this.lookControls.polyfillControls.update = () => {}; + onRotateX(e) { + this.pendingLookX = e.detail.value; }, - tick(t, dt) { - if (!this.data.enabled) return; - const scene = this.el.sceneEl; + tick() { 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 dX = difference(hmdEuler.x, this.prevX); - const dY = difference(hmdEuler.y, this.prevY); + const dX = THREE.Math.RAD2DEG * difference(hmdEuler.x, this.prevX); + const dY = THREE.Math.RAD2DEG * 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); @@ -89,11 +82,11 @@ AFRAME.registerComponent("look-on-mobile", { this.dYBuffer.splice(0, 1); } - 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)); + const deltaYaw = average(this.dYBuffer) * horizontalLookSpeedRatio; + const deltaPitch = average(this.dXBuffer) * verticalLookSpeedRatio + this.pendingLookX; + + this.cameraController.look(deltaPitch, deltaYaw); - this.lookControls.updateOrientation(); this.prevX = hmdEuler.x; this.prevY = hmdEuler.y; this.pendingLookX = 0; diff --git a/src/components/pitch-yaw-rotator.js b/src/components/pitch-yaw-rotator.js new file mode 100644 index 0000000000000000000000000000000000000000..87f4e646959ead44cec4b9223a52535081244240 --- /dev/null +++ b/src/components/pitch-yaw-rotator.js @@ -0,0 +1,30 @@ +const degToRad = THREE.Math.degToRad; +AFRAME.registerComponent("pitch-yaw-rotator", { + schema: { + minPitch: { default: -50 }, + maxPitch: { default: 50 } + }, + + init() { + this.pitch = 0; + this.yaw = 0; + this.rotation = { x: 0, y: 0, z: 0 }; + }, + + look(deltaPitch, deltaYaw) { + const { minPitch, maxPitch } = this.data; + this.pitch += deltaPitch; + this.pitch = Math.max(minPitch, Math.min(maxPitch, this.pitch)); + this.yaw += deltaYaw; + }, + + tick() { + this.rotation.x = this.pitch; + this.rotation.y = this.yaw; + + // Update rotation of object3D the same way the rotation component of aframe does, + // skipping the work that would be done if we used this.el.setAttribute("rotation", this.rotation); + this.el.object3D.rotation.set(degToRad(this.rotation.x), degToRad(this.rotation.y), degToRad(this.rotation.z)); + this.el.object3D.rotation.order = "YXZ"; + } +}); diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js index 083f81db766a810876b563a77b8de18544daf912..08491ec0ea05f253c720b50e078c5895bd3f188f 100644 --- a/src/components/super-spawner.js +++ b/src/components/super-spawner.js @@ -10,7 +10,7 @@ AFRAME.registerComponent("super-spawner", { spawnPosition: { type: "vec3" }, useCustomSpawnRotation: { default: false }, spawnRotation: { type: "vec4" }, - events: { default: ["cursor-grab", "action_grab"] }, + events: { default: ["cursor-grab", "hand_grab"] }, spawnCooldown: { default: 1 } }, diff --git a/src/hub.html b/src/hub.html index 9abac20147fc7731b96ec018b972449d21d8cd6b..24f7b5e8009fb064de253eaa0e9b7a71126b3674 100644 --- a/src/hub.html +++ b/src/hub.html @@ -42,7 +42,16 @@ personal-space-bubble="debug: false;" vr-mode-ui="enabled: false" pinch-to-move - look-on-mobile + input-configurator=" + gazeCursorRayObject: #player-camera-reverse-z; + cursorController: #cursor-controller; + gazeTeleporter: #gaze-teleport; + camera: #player-camera; + playerRig: #player-rig; + leftController: #player-left-controller; + leftControllerRayObject: #player-left-controller-reverse-z; + rightController: #player-right-controller; + rightControllerRayObject: #player-right-controller-reverse-z;" > <a-assets> @@ -184,13 +193,13 @@ ></a-entity> </template> - <a-mixin id="super-hands" + <a-mixin id="controller-super-hands" super-hands=" colliderEvent: collisions; colliderEventProperty: els; colliderEndEvent: collisions; colliderEndEventProperty: clearedEls; - grabStartButtons: action_grab; grabEndButtons: action_release; - stretchStartButtons: action_grab; stretchEndButtons: action_release; - dragDropStartButtons: action_grab; dragDropEndButtons: action_release;" + grabStartButtons: hand_grab; grabEndButtons: hand_release; + stretchStartButtons: hand_grab; stretchEndButtons: hand_release; + dragDropStartButtons: hand_grab; dragDropEndButtons: hand_release;" collision-filter="collisionForces: false" physics-collider ></a-mixin> @@ -203,10 +212,7 @@ id="cursor-controller" cursor-controller=" cursor: #cursor; - camera: #player-camera; - playerRig: #player-rig; - physicalHandSelector: #player-right-controller; - gazeTeleportControls: #gaze-teleport;" + camera: #player-camera; " raycaster="objects: .collidable, .interactable, .ui; far: 3;" line="visible: false; color: white; opacity: 0.2;" ></a-entity> @@ -263,6 +269,7 @@ camera position="0 1.6 0" personal-space-bubble="radius: 0.4" + pitch-yaw-rotator > <a-entity id="gaze-teleport" @@ -270,13 +277,14 @@ teleport-controls=" cameraRig: #player-rig; teleportOrigin: #player-camera; - button: cursor-teleport_; + button: gaze-teleport_; collisionEntities: [nav-mesh]; drawIncrementally: true; incrementalDrawMs: 600; hitOpacity: 0.3; missOpacity: 0.2;" ></a-entity> + <a-entity id="player-camera-reverse-z" rotation="0 180 0"></a-entity> </a-entity> <a-entity @@ -295,9 +303,10 @@ missOpacity: 0.2;" haptic-feedback body="type: static; shape: none;" - mixin="super-hands" + mixin="controller-super-hands" controls-shape-offset > + <a-entity id="player-left-controller-reverse-z" rotation="0 180 0"></a-entity> </a-entity> <a-entity @@ -316,9 +325,11 @@ missOpacity: 0.2;" haptic-feedback body="type: static; shape: none;" - mixin="super-hands" + mixin="controller-super-hands" controls-shape-offset - ></a-entity> + > + <a-entity id="player-right-controller-reverse-z" rotation="0 180 0"></a-entity> + </a-entity> <a-entity gltf-model-plus="inflate: true;" class="model"> diff --git a/src/hub.js b/src/hub.js index 410febf04af8acf4825126bc657ab6d14bb461b9..76b202b3d3fa535334d699fe44d8e62e5ba80f44 100644 --- a/src/hub.js +++ b/src/hub.js @@ -64,6 +64,8 @@ import "./components/scene-shadow"; import "./components/avatar-replay"; import "./components/pinch-to-move"; import "./components/look-on-mobile"; +import "./components/pitch-yaw-rotator"; +import "./components/input-configurator"; import ReactDOM from "react-dom"; import React from "react"; @@ -123,10 +125,6 @@ import registerTelemetry from "./telemetry"; import { getAvailableVREntryTypes, VR_DEVICE_AVAILABILITY } from "./utils/vr-caps-detect.js"; import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js"; -import TouchEventsHandler from "./utils/touch-events-handler.js"; -import MouseEventsHandler from "./utils/mouse-events-handler.js"; -window.APP.touchEventsHandler = new TouchEventsHandler(); -window.APP.mouseEventsHandler = new MouseEventsHandler(); function qsTruthy(param) { const val = qs[param]; @@ -230,6 +228,7 @@ const onReady = async () => { const enterScene = async (mediaStream, enterInVR, hubId) => { const scene = document.querySelector("a-scene"); + scene.style.cursor = "none"; scene.renderer.sortObjects = true; const playerRig = document.querySelector("#player-rig"); document.querySelector("canvas").classList.remove("blurred"); @@ -241,24 +240,6 @@ const onReady = async () => { AFRAME.registerInputActions(inGameActions, "default"); - const camera = document.querySelector("#player-camera"); - const registerLookControls = e => { - if (e.detail.name !== "look-controls") return; - camera.removeEventListener("componentinitialized", registerLookControls); - - window.APP.touchEventsHandler.registerLookControls(camera.components["look-controls"]); - scene.components["look-on-mobile"].registerLookControls(camera.components["look-controls"]); - scene.setAttribute("look-on-mobile", "enabled", true); - - window.APP.mouseEventsHandler.registerLookControls(camera.components["look-controls"]); - window.APP.mouseEventsHandler.setInverseMouseLook(qsTruthy("invertMouseLook")); - }; - camera.addEventListener("componentinitialized", registerLookControls); - camera.setAttribute("look-controls", { - touchEnabled: false, - hmdEnabled: false - }); - scene.setAttribute("networked-scene", { room: hubId, serverURL: process.env.JANUS_SERVER diff --git a/src/input-mappings.js b/src/input-mappings.js index a1c4c06a2d7cf1f8442b81fee5cfa25821af90b5..ecef8335bdd9a4223c93f7a22deb5f4549c0f384 100644 --- a/src/input-mappings.js +++ b/src/input-mappings.js @@ -83,7 +83,7 @@ const config = { thumbsticktouchend: "thumb_up", triggerdown: ["action_grab", "index_down"], triggerup: ["action_release", "index_up"], - "axismove.reverseY": { left: "move" }, + "axismove.reverseY": { left: "move", right: "move_duck" }, abuttondown: "action_primary_down", abuttonup: "action_primary_up" }, @@ -107,7 +107,7 @@ const config = { trackpadtouchend: "thumb_up", triggerdown: ["action_grab", "index_down"], triggerup: ["action_release", "index_up"], - axisMoveWithDeadzone: { left: "move" } + axisMoveWithDeadzone: { left: "move", right: "move_duck" } }, "daydream-controls": { trackpad_dpad4_pressed_west_down: "snap_rotate_left", diff --git a/src/utils/action-event-handler.js b/src/utils/action-event-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..1884a7313bf88bc345eff106856c270403d2a74f --- /dev/null +++ b/src/utils/action-event-handler.js @@ -0,0 +1,161 @@ +export default class ActionEventHandler { + constructor(scene, cursor) { + this.scene = scene; + this.cursor = cursor; + this.isCursorInteracting = false; + this.isTeleporting = false; + this.handThatAlsoDrivesCursor = null; + this.hovered = false; + + this.addEventListeners = this.addEventListeners.bind(this); + this.tearDown = this.tearDown.bind(this); + this.onPrimaryDown = this.onPrimaryDown.bind(this); + this.onPrimaryUp = this.onPrimaryUp.bind(this); + this.onGrab = this.onGrab.bind(this); + this.onRelease = this.onRelease.bind(this); + this.onCardboardButtonDown = this.onCardboardButtonDown.bind(this); + this.onCardboardButtonUp = this.onCardboardButtonUp.bind(this); + this.onMoveDuck = this.onMoveDuck.bind(this); + this.manageCursorEnabled = this.manageCursorEnabled.bind(this); + this.addEventListeners(); + } + + addEventListeners() { + this.scene.addEventListener("action_primary_down", this.onPrimaryDown); + this.scene.addEventListener("action_primary_up", this.onPrimaryUp); + this.scene.addEventListener("action_grab", this.onGrab); + this.scene.addEventListener("action_release", this.onRelease); + this.scene.addEventListener("move_duck", this.onMoveDuck); + this.scene.addEventListener("cardboardbuttondown", this.onCardboardButtonDown); // TODO: These should be actions + this.scene.addEventListener("cardboardbuttonup", this.onCardboardButtonUp); + } + + tearDown() { + this.scene.removeEventListener("action_primary_down", this.onPrimaryDown); + this.scene.removeEventListener("action_primary_up", this.onPrimaryUp); + this.scene.removeEventListener("action_grab", this.onGrab); + this.scene.removeEventListener("action_release", this.onRelease); + this.scene.removeEventListener("move_duck", this.onMoveDuck); + this.scene.removeEventListener("cardboardbuttondown", this.onCardboardButtonDown); + this.scene.removeEventListener("cardboardbuttonup", this.onCardboardButtonUp); + } + + onMoveDuck(e) { + this.cursor.changeDistanceMod(-e.detail.axis[1] / 8); + } + + setHandThatAlsoDrivesCursor(handThatAlsoDrivesCursor) { + this.handThatAlsoDrivesCursor = handThatAlsoDrivesCursor; + } + + onGrab(e) { + if (this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target) { + if (this.isCursorInteracting) { + return; + } else if (e.target.components["super-hands"].state.has("hover-start")) { + e.target.emit("hand_grab"); + return; + } else { + this.isCursorInteracting = this.cursor.startInteraction(); + return; + } + } else { + e.target.emit("hand_grab"); + return; + } + } + + onRelease(e) { + if (this.isCursorInteracting && this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target) { + this.isCursorInteracting = false; + this.cursor.endInteraction(); + } else { + e.target.emit("hand_release"); + } + } + + onPrimaryDown(e) { + if (this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target) { + if (this.isCursorInteracting) { + return; + } else if (e.target.components["super-hands"].state.has("hover-start")) { + e.target.emit("hand_grab"); + return; + } else { + this.isCursorInteracting = this.cursor.startInteraction(); + if (this.isCursorInteracting) return; + } + } + + this.cursor.setCursorVisibility(false); + const button = e.target.components["teleport-controls"].data.button; + e.target.emit(button + "down"); + this.isTeleporting = true; + } + + onPrimaryUp(e) { + const isCursorHand = this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target; + if (this.isCursorInteracting && isCursorHand) { + this.isCursorInteracting = false; + this.cursor.endInteraction(); + return; + } + + const state = e.target.components["super-hands"].state; + if (state.has("grab-start")) { + e.target.emit("hand_release"); + return; + } + + if (isCursorHand) { + this.cursor.setCursorVisibility(!state.has("hover-start")); + } + const button = e.target.components["teleport-controls"].data.button; + e.target.emit(button + "up"); + this.isTeleporting = false; + } + + onCardboardButtonDown(e) { + this.isCursorInteracting = this.cursor.startInteraction(); + if (this.isCursorInteracting) { + return; + } + + this.cursor.setCursorVisibility(false); + + const gazeTeleport = e.target.querySelector("#gaze-teleport"); + const button = gazeTeleport.components["teleport-controls"].data.button; + gazeTeleport.emit(button + "down"); + this.isTeleporting = true; + } + + onCardboardButtonUp(e) { + if (this.isCursorInteracting) { + this.isCursorInteracting = false; + this.cursor.endInteraction(); + return; + } + + this.cursor.setCursorVisibility(true); + + const gazeTeleport = e.target.querySelector("#gaze-teleport"); + const button = gazeTeleport.components["teleport-controls"].data.button; + gazeTeleport.emit(button + "up"); + this.isTeleporting = false; + } + + manageCursorEnabled() { + const handState = this.handThatAlsoDrivesCursor.components["super-hands"].state; + const handHoveredThisFrame = !this.hovered && handState.has("hover-start") && !this.isCursorInteracting; + const handStoppedHoveringThisFrame = + this.hovered === true && !handState.has("hover-start") && !handState.has("grab-start"); + if (handHoveredThisFrame) { + this.hovered = true; + this.cursor.disable(); + } else if (handStoppedHoveringThisFrame) { + this.hovered = false; + this.cursor.enable(); + this.cursor.setCursorVisibility(!this.isTeleporting); + } + } +} diff --git a/src/utils/gearvr-mouse-events-handler.js b/src/utils/gearvr-mouse-events-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..90ee3c8538bf912592b89a6051c243b406a1039f --- /dev/null +++ b/src/utils/gearvr-mouse-events-handler.js @@ -0,0 +1,52 @@ +export default class GearVRMouseEventsHandler { + constructor(cursor, gazeTeleporter) { + this.cursor = cursor; + this.gazeTeleporter = gazeTeleporter; + this.isMouseDownHandledByCursor = false; + this.isMouseDownHandledByGazeTeleporter = false; + + this.addEventListeners = this.addEventListeners.bind(this); + this.tearDown = this.tearDown.bind(this); + this.onMouseDown = this.onMouseDown.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + this.addEventListeners(); + } + + addEventListeners() { + document.addEventListener("mousedown", this.onMouseDown); + document.addEventListener("mouseup", this.onMouseUp); + } + + tearDown() { + document.removeEventListener("mousedown", this.onMouseDown); + document.removeEventListener("mouseup", this.onMouseUp); + } + + onMouseDown() { + this.isMouseDownHandledByCursor = this.cursor.startInteraction(); + if (this.isMouseDownHandledByCursor) { + return; + } + + this.cursor.setCursorVisibility(false); + + const button = this.gazeTeleporter.data.button; + this.gazeTeleporter.el.emit(button + "down"); + this.isMouseDownHandledByGazeTeleporter = true; + } + + onMouseUp() { + if (this.isMouseDownHandledByCursor) { + this.cursor.endInteraction(); + this.isMouseDownHandledByCursor = false; + } + + this.cursor.setCursorVisibility(true); + + if (this.isMouseDownHandledByGazeTeleporter) { + const button = this.gazeTeleporter.data.button; + this.gazeTeleporter.el.emit(button + "up"); + this.isMouseDownHandledByGazeTeleporter = false; + } + } +} diff --git a/src/utils/mouse-events-handler.js b/src/utils/mouse-events-handler.js index 425b16d5262c62af8f5c710d1b7e4945520973fb..b034006a1fafa9756cc94928288622a7b022bb07 100644 --- a/src/utils/mouse-events-handler.js +++ b/src/utils/mouse-events-handler.js @@ -1,52 +1,38 @@ -const HORIZONTAL_LOOK_SPEED = 0.0035; -const VERTICAL_LOOK_SPEED = 0.0021; -const PI_4 = Math.PI / 4; +// TODO: Make look speed adjustable by the user +const HORIZONTAL_LOOK_SPEED = 0.1; +const VERTICAL_LOOK_SPEED = 0.06; export default class MouseEventsHandler { - constructor() { - this.cursor = null; - this.lookControls = null; - this.isMouseDown = false; - this.isMouseDownHandledByCursor = false; + constructor(cursor, cameraController) { + this.cursor = cursor; + this.cameraController = cameraController; + this.isLeftButtonDown = false; + this.isLeftButtonHandledByCursor = false; this.isPointerLocked = false; - this.dXBuffer = []; - this.dYBuffer = []; - this.registerCursor = this.registerCursor.bind(this); - this.registerLookControls = this.registerLookControls.bind(this); - this.isReady = this.isReady.bind(this); this.addEventListeners = this.addEventListeners.bind(this); this.onMouseDown = this.onMouseDown.bind(this); + this.onLeftButtonDown = this.onLeftButtonDown.bind(this); + this.onRightButtonDown = this.onRightButtonDown.bind(this); + this.tearDown = this.tearDown.bind(this); this.onMouseMove = this.onMouseMove.bind(this); this.onMouseUp = this.onMouseUp.bind(this); this.onMouseWheel = this.onMouseWheel.bind(this); this.look = this.look.bind(this); - document.addEventListener("contextmenu", function(e) { - e.preventDefault(); - }); - } - - setInverseMouseLook(invert) { - this.invertMouseLook = invert; - } - - registerCursor(cursor) { - this.cursor = cursor; - if (this.isReady()) { - this.addEventListeners(); - } + this.addEventListeners(); } - registerLookControls(lookControls) { - this.lookControls = lookControls; - if (this.isReady()) { - this.addEventListeners(); - } + tearDown() { + document.removeEventListener("mousedown", this.onMouseDown); + document.removeEventListener("mousemove", this.onMouseMove); + document.removeEventListener("mouseup", this.onMouseUp); + document.removeEventListener("wheel", this.onMouseWheel); + document.removeEventListener("contextmenu", this.onContextMenu); } - isReady() { - return this.cursor && this.lookControls; + setInverseMouseLook(invert) { + this.invertMouseLook = invert; } addEventListeners() { @@ -54,61 +40,76 @@ export default class MouseEventsHandler { document.addEventListener("mousemove", this.onMouseMove); document.addEventListener("mouseup", this.onMouseUp); document.addEventListener("wheel", this.onMouseWheel); + document.addEventListener("contextmenu", this.onContextMenu); + } + + onContextMenu(e) { + e.preventDefault(); } onMouseDown(e) { const isLeftButton = e.button === 0; + const isRightButton = e.button === 2; if (isLeftButton) { - this.isMouseDownHandledByCursor = this.cursor.handleMouseDown(); - this.isMouseDown = true; + this.onLeftButtonDown(); + } else if (isRightButton) { + this.onRightButtonDown(); + } + } + + onLeftButtonDown() { + this.isLeftButtonDown = true; + this.isLeftButtonHandledByCursor = this.cursor.startInteraction(); + } + + onRightButtonDown() { + if (this.isPointerLocked) { + document.exitPointerLock(); + this.isPointerLocked = false; } else { - if (this.isPointerLocked) { - document.exitPointerLock(); - this.isPointerLocked = false; - } else { - document.body.requestPointerLock(); - this.isPointerLocked = true; - } + document.body.requestPointerLock(); + this.isPointerLocked = true; } } onMouseWheel(e) { - this.cursor.handleMouseWheel(e); + switch (e.deltaMode) { + case e.DOM_DELTA_PIXEL: + this.cursor.changeDistanceMod(e.deltaY / 500); + break; + case e.DOM_DELTA_LINE: + this.cursor.changeDistanceMod(e.deltaY / 10); + break; + case e.DOM_DELTA_PAGE: + this.cursor.changeDistanceMod(e.deltaY / 2); + break; + } } onMouseMove(e) { - const shouldLook = (this.isMouseDown && !this.isMouseDownHandledByCursor) || this.isPointerLocked; + const shouldLook = this.isPointerLocked || (this.isLeftButtonDown && !this.isLeftButtonHandledByCursor); if (shouldLook) { this.look(e); } - this.cursor.handleMouseMove(e); - } - - look(e) { - const movementX = e.movementX; - const movementY = e.movementY; - - const sign = this.invertMouseLook ? 1 : -1; - this.lookControls.yawObject.rotation.y += sign * movementX * HORIZONTAL_LOOK_SPEED; - this.lookControls.pitchObject.rotation.x += sign * movementY * VERTICAL_LOOK_SPEED; - this.lookControls.pitchObject.rotation.x = Math.max( - -PI_4, - Math.min(PI_4, this.lookControls.pitchObject.rotation.x) - ); - this.lookControls.updateOrientation(); + this.cursor.moveCursor(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1); } onMouseUp(e) { const isLeftButton = e.button === 0; - if (isLeftButton) { - if (this.isMouseDownHandledByCursor) { - this.cursor.handleMouseUp(); - } - this.isMouseDownHandledByCursor = false; - this.isMouseDown = false; - this.dXBuffer = []; - this.dYBuffer = []; + if (!isLeftButton) return; + + if (this.isLeftButtonHandledByCursor) { + this.cursor.endInteraction(); } + this.isLeftButtonHandledByCursor = false; + this.isLeftButtonDown = false; + } + + look(e) { + const sign = this.invertMouseLook ? 1 : -1; + const deltaPitch = e.movementY * VERTICAL_LOOK_SPEED * sign; + const deltaYaw = e.movementX * HORIZONTAL_LOOK_SPEED * sign; + this.cameraController.look(deltaPitch, deltaYaw); } } diff --git a/src/utils/touch-events-handler.js b/src/utils/touch-events-handler.js index b652dc9b8024274684a9f95fc9c9548a93420658..91b9bc25ba0e2ec11428219a3f248e7ec052dbfe 100644 --- a/src/utils/touch-events-handler.js +++ b/src/utils/touch-events-handler.js @@ -1,13 +1,12 @@ const VIRTUAL_JOYSTICK_HEIGHT = 0.8; -const HORIZONTAL_LOOK_SPEED = 0.006; -const VERTICAL_LOOK_SPEED = 0.003; -const PI_4 = Math.PI / 4; +const HORIZONTAL_LOOK_SPEED = 0.35; +const VERTICAL_LOOK_SPEED = 0.18; export default class TouchEventsHandler { - constructor() { - this.cursor = null; - this.lookControls = null; - this.pinchEmitter = null; + constructor(cursor, cameraController, pinchEmitter) { + this.cursor = cursor; + this.cameraController = cameraController; + this.pinchEmitter = pinchEmitter; this.touches = []; this.touchReservedForCursor = null; this.touchesReservedForPinch = []; @@ -16,9 +15,6 @@ export default class TouchEventsHandler { this.pinchTouchId1 = -1; this.pinchTouchId2 = -1; - this.registerCursor = this.registerCursor.bind(this); - this.registerLookControls = this.registerLookControls.bind(this); - this.isReady = this.isReady.bind(this); this.addEventListeners = this.addEventListeners.bind(this); this.handleTouchStart = this.handleTouchStart.bind(this); this.singleTouchStart = this.singleTouchStart.bind(this); @@ -28,31 +24,9 @@ export default class TouchEventsHandler { this.singleTouchEnd = this.singleTouchEnd.bind(this); this.pinch = this.pinch.bind(this); this.look = this.look.bind(this); - } - - registerCursor(cursor) { - this.cursor = cursor; - if (this.isReady()) { - this.addEventListeners(); - } - } - - registerLookControls(lookControls) { - this.lookControls = lookControls; - if (this.isReady()) { - this.addEventListeners(); - } - } - - registerPinchEmitter(pinchEmitter) { - this.pinchEmitter = pinchEmitter; - if (this.isReady()) { - this.addEventListeners(); - } - } + this.tearDown = this.tearDown.bind(this); - isReady() { - return this.cursor && this.lookControls && this.pinchEmitter; + this.addEventListeners(); } addEventListeners() { @@ -62,7 +36,16 @@ export default class TouchEventsHandler { document.addEventListener("touchcancel", this.handleTouchEnd); } + tearDown() { + document.removeEventListener("touchstart", this.handleTouchStart); + document.removeEventListener("touchmove", this.handleTouchMove); + document.removeEventListener("touchend", this.handleTouchEnd); + document.removeEventListener("touchcancel", this.handleTouchEnd); + } + handleTouchStart(e) { + this.cursor.setCursorVisibility(false); + Array.prototype.forEach.call(e.changedTouches, this.singleTouchStart); } @@ -70,8 +53,12 @@ export default class TouchEventsHandler { if (touch.clientY / window.innerHeight >= VIRTUAL_JOYSTICK_HEIGHT) { return; } - if (!this.touchReservedForCursor && this.cursor.handleTouchStart(touch)) { - this.touchReservedForCursor = touch; + if (!this.touchReservedForCursor) { + this.cursor.moveCursor(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1); + this.cursor.forceCursorUpdate(); + if (this.cursor.startInteraction()) { + this.touchReservedForCursor = touch; + } } this.touches.push(touch); } @@ -86,7 +73,7 @@ export default class TouchEventsHandler { singleTouchMove(touch) { if (this.touchReservedForCursor && touch.identifier === this.touchReservedForCursor.identifier) { - this.cursor.handleTouchMove(touch); + this.cursor.moveCursor(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1); return; } if (touch.clientY / window.innerHeight >= VIRTUAL_JOYSTICK_HEIGHT) return; @@ -114,7 +101,10 @@ export default class TouchEventsHandler { } if (touch.identifier === this.touchReservedForLookControls.identifier) { if (!this.touchReservedForCursor) { - this.cursor.handleTouchMove(touch); + this.cursor.moveCursor( + touch.clientX / window.innerWidth * 2 - 1, + -(touch.clientY / window.innerHeight) * 2 + 1 + ); } this.look(this.touchReservedForLookControls, touch); this.touchReservedForLookControls = touch; @@ -133,15 +123,9 @@ export default class TouchEventsHandler { } look(prevTouch, touch) { - 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.pitchObject.rotation.x = Math.max( - -PI_4, - Math.min(PI_4, this.lookControls.pitchObject.rotation.x) - ); + const deltaPitch = (touch.clientY - prevTouch.clientY) * VERTICAL_LOOK_SPEED; + const deltaYaw = (touch.clientX - prevTouch.clientX) * HORIZONTAL_LOOK_SPEED; + this.cameraController.look(deltaPitch, deltaYaw); } handleTouchEnd(e) { @@ -156,7 +140,7 @@ export default class TouchEventsHandler { this.touches.splice(touchIndex, 1); if (this.touchReservedForCursor && touch.identifier === this.touchReservedForCursor.identifier) { - this.cursor.handleTouchEnd(touch); + this.cursor.endInteraction(touch); this.touchReservedForCursor = null; return; }