├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── .gitkeep ├── aframe-mouse-cursor-component.js └── aframe-mouse-cursor-component.min.js ├── examples ├── basic │ └── index.html ├── common.css ├── embedded │ └── index.html ├── example.gif ├── index.html └── main.js ├── index.js ├── package.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["es2015"] } 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .sw[ponm] 2 | examples/build.js 3 | examples/node_modules/ 4 | gh-pages 5 | node_modules/ 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mayo Tobita 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A-Frame Mouse Cursor Component 2 | 3 | > **This feature is now available in A-Frame v0.6.1 by setting ``.** 4 | 5 | This is similar to `cursor` component besides the mouse behaves as cursor. 6 | 7 | For detail, please check [cursor page](https://aframe.io/docs/components/cursor.html). 8 | 9 | **[DEMO](https://mayognaise.github.io/aframe-mouse-cursor-component/index.html)** 10 | 11 | ![example](examples/example.gif) 12 | 13 | ## Properties 14 | 15 | There is no property. 16 | 17 | 18 | ## States 19 | 20 | The `mouse-cursor` will add states to the cursor entity on certain events. 21 | 22 | **There is no `hovering` or `hovered` state for mobile.** 23 | 24 | | State Name | Description | 25 | | -------- | ----------- | 26 | | hovering | Added when the cursor is hovering over another entity. | 27 | 28 | The cursor will add states to the target entity on certain events. 29 | 30 | | State Name | Description | 31 | | -------- | ----------- | 32 | | hovered | Added when target entity is being hovered by the cursor. | 33 | 34 | 35 | ## Events 36 | 37 | **There is no `mouseenter` or `mouseleave` events but `click` event for mobile.** 38 | 39 | | Event Name | Description | 40 | | -------- | ----------- | 41 | | click | Triggered when an entity is clicked. | 42 | | mouseenter | Triggered on mouseenter of the canvas. | 43 | | mouseleave | Triggered on mouseleave of the canvas. | 44 | | mousedown | Triggered on mousedown of the canvas. | 45 | | mouseup | Triggered on mouseup of the canvas. | 46 | 47 | For events, please check [demo page](https://mayognaise.github.io/aframe-mouse-cursor-component/basic/index.html). 48 | 49 | 50 | ## Usage 51 | 52 | **The `mouse-cursor` component is usually used alongside the [camera component][components-camera].** 53 | 54 | ### Browser Installation 55 | 56 | Install and use by directly including the [browser files](dist): 57 | 58 | ```html 59 | 60 | My A-Frame Scene 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | ``` 71 | 72 | ### NPM Installation 73 | 74 | Install via NPM: 75 | 76 | ```bash 77 | npm i -D aframe-mouse-cursor-component 78 | ``` 79 | 80 | Then register and use. 81 | 82 | ```js 83 | import 'aframe' 84 | import 'aframe-mouse-cursor-component' 85 | ``` 86 | 87 | ### Contributions 88 | 89 | If you want to work on this component, take a fork of this branch, and submit a PR back. 90 | 91 | * To dev, run `npm run dev` in your terminal, and check your code at `http://localhost:8000` 92 | * To build (prior to PR) run `npm run build` 93 | 94 | 95 | ## Change log 96 | 97 | ### 0.5.1 98 | 99 | - Add ‘mousedown’ and ‘mouseup’ event 100 | 101 | ### 0.5.0 102 | 103 | - Now works accurately with scenes embedded in page 104 | 105 | ### 0.2.1 106 | 107 | - Now mouse cursor works in stereo mode on both desktop/mobile 108 | - `click` event won't be fired when mouse moves a lot after mouse down 109 | 110 | 111 | -------------------------------------------------------------------------------- /dist/.gitkeep: -------------------------------------------------------------------------------- 1 | `npm run dist` to generate browser files. 2 | -------------------------------------------------------------------------------- /dist/aframe-mouse-cursor-component.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) 10 | /******/ return installedModules[moduleId].exports; 11 | 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ exports: {}, 15 | /******/ id: moduleId, 16 | /******/ loaded: false 17 | /******/ }; 18 | 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | 22 | /******/ // Flag the module as loaded 23 | /******/ module.loaded = true; 24 | 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | 29 | 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | 36 | /******/ // __webpack_public_path__ 37 | /******/ __webpack_require__.p = ""; 38 | 39 | /******/ // Load entry module and return exports 40 | /******/ return __webpack_require__(0); 41 | /******/ }) 42 | /************************************************************************/ 43 | /******/ ([ 44 | /* 0 */ 45 | /***/ (function(module, exports, __webpack_require__) { 46 | 47 | 'use strict'; 48 | 49 | var _lodash = __webpack_require__(1); 50 | 51 | var _lodash2 = _interopRequireDefault(_lodash); 52 | 53 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 54 | 55 | if (typeof AFRAME === 'undefined') { 56 | throw 'mouse-cursor Component attempted to register before AFRAME was available.'; 57 | } 58 | 59 | var IS_VR_AVAILABLE = AFRAME.utils.device.isMobile() || window.hasNonPolyfillWebVRSupport; 60 | 61 | /** 62 | * Mouse Cursor Component for A-Frame. 63 | */ 64 | AFRAME.registerComponent('mouse-cursor', { 65 | schema: {}, 66 | 67 | /** 68 | * Called once when component is attached. Generally for initial setup. 69 | * @protected 70 | */ 71 | init: function init() { 72 | this._raycaster = new THREE.Raycaster(); 73 | this._mouse = new THREE.Vector2(); 74 | this._isMobile = this.el.sceneEl.isMobile; 75 | this._isStereo = false; 76 | this._active = false; 77 | this._isDown = false; 78 | this._intersectedEl = null; 79 | this._attachEventListeners(); 80 | this._canvasSize = false; 81 | /* bind functions */ 82 | this.__getCanvasPos = this._getCanvasPos.bind(this); 83 | this.__getCanvasPos = this._getCanvasPos.bind(this); 84 | this.__onEnterVR = this._onEnterVR.bind(this); 85 | this.__onExitVR = this._onExitVR.bind(this); 86 | this.__onDown = this._onDown.bind(this); 87 | this.__onClick = this._onClick.bind(this); 88 | this.__onMouseMove = this._onMouseMove.bind(this); 89 | this.__onRelease = this._onRelease.bind(this); 90 | this.__onTouchMove = this._onTouchMove.bind(this); 91 | this.__onComponentChanged = this._onComponentChanged.bind(this); 92 | }, 93 | 94 | 95 | /** 96 | * Called when component is attached and when component data changes. 97 | * Generally modifies the entity based on the data. 98 | * @protected 99 | */ 100 | update: function update(oldData) {}, 101 | 102 | 103 | /** 104 | * Called when a component is removed (e.g., via removeAttribute). 105 | * Generally undoes all modifications to the entity. 106 | * @protected 107 | */ 108 | remove: function remove() { 109 | this._removeEventListeners(); 110 | this._raycaster = null; 111 | }, 112 | 113 | 114 | /** 115 | * Called on each scene tick. 116 | * @protected 117 | */ 118 | // tick (t) { }, 119 | 120 | /** 121 | * Called when entity pauses. 122 | * Use to stop or remove any dynamic or background behavior such as events. 123 | * @protected 124 | */ 125 | pause: function pause() { 126 | this._active = false; 127 | }, 128 | 129 | 130 | /** 131 | * Called when entity resumes. 132 | * Use to continue or add any dynamic or background behavior such as events. 133 | * @protected 134 | */ 135 | play: function play() { 136 | this._active = true; 137 | }, 138 | 139 | 140 | /*============================== 141 | = events = 142 | ==============================*/ 143 | 144 | /** 145 | * @private 146 | */ 147 | _attachEventListeners: function _attachEventListeners() { 148 | var el = this.el; 149 | var sceneEl = el.sceneEl; 150 | var canvas = sceneEl.canvas; 151 | /* if canvas doesn't exist, listen for canvas to load. */ 152 | 153 | if (!canvas) { 154 | el.sceneEl.addEventListener('render-target-loaded', this._attachEventListeners.bind(this)); 155 | return; 156 | } 157 | 158 | window.addEventListener('resize', this.__getCanvasPos); 159 | document.addEventListener('scroll', this.__getCanvasPos); 160 | /* update _canvas in case scene is embedded */ 161 | this._getCanvasPos(); 162 | 163 | /* scene */ 164 | sceneEl.addEventListener('enter-vr', this.__onEnterVR); 165 | sceneEl.addEventListener('exit-vr', this.__onExitVR); 166 | 167 | /* Mouse events */ 168 | canvas.addEventListener('mousedown', this.__onDown); 169 | canvas.addEventListener('mousemove', this.__onMouseMove); 170 | canvas.addEventListener('mouseup', this.__onRelease); 171 | canvas.addEventListener('mouseout', this.__onRelease); 172 | 173 | /* Touch events */ 174 | canvas.addEventListener('touchstart', this.__onDown); 175 | canvas.addEventListener('touchmove', this.__onTouchMove); 176 | canvas.addEventListener('touchend', this.__onRelease); 177 | 178 | /* Click event */ 179 | canvas.addEventListener('click', this.__onClick); 180 | 181 | /* Element component change */ 182 | el.addEventListener('componentchanged', this.__onComponentChanged); 183 | }, 184 | 185 | 186 | /** 187 | * @private 188 | */ 189 | _removeEventListeners: function _removeEventListeners() { 190 | var el = this.el; 191 | var sceneEl = el.sceneEl; 192 | var canvas = sceneEl.canvas; 193 | 194 | if (!canvas) { 195 | return; 196 | } 197 | 198 | window.removeEventListener('resize', this.__getCanvasPos); 199 | document.removeEventListener('scroll', this.__getCanvasPos); 200 | 201 | /* scene */ 202 | sceneEl.removeEventListener('enter-vr', this.__onEnterVR); 203 | sceneEl.removeEventListener('exit-vr', this.__onExitVR); 204 | 205 | /* Mouse events */ 206 | canvas.removeEventListener('mousedown', this.__onDown); 207 | canvas.removeEventListener('mousemove', this.__onMouseMove); 208 | canvas.removeEventListener('mouseup', this.__onRelease); 209 | canvas.removeEventListener('mouseout', this.__onRelease); 210 | 211 | /* Touch events */ 212 | canvas.removeEventListener('touchstart', this.__onDown); 213 | canvas.removeEventListener('touchmove', this.__onTouchMove); 214 | canvas.removeEventListener('touchend', this.__onRelease); 215 | 216 | /* Click event */ 217 | canvas.removeEventListener('click', this.__onClick); 218 | 219 | /* Element component change */ 220 | el.removeEventListener('componentchanged', this.__onComponentChanged); 221 | }, 222 | 223 | 224 | /** 225 | * Check if the mouse cursor is active 226 | * @private 227 | */ 228 | _isActive: function _isActive() { 229 | return !!(this._active || this._raycaster); 230 | }, 231 | 232 | 233 | /** 234 | * @private 235 | */ 236 | _onDown: function _onDown(evt) { 237 | if (!this._isActive()) { 238 | return; 239 | } 240 | 241 | this._isDown = true; 242 | 243 | this._updateMouse(evt); 244 | this._updateIntersectObject(); 245 | 246 | if (!this._isMobile) { 247 | this._setInitMousePosition(evt); 248 | } 249 | if (this._intersectedEl) { 250 | this._emit('mousedown'); 251 | } 252 | }, 253 | 254 | 255 | /** 256 | * @private 257 | */ 258 | _onClick: function _onClick(evt) { 259 | if (!this._isActive()) { 260 | return; 261 | } 262 | 263 | this._updateMouse(evt); 264 | this._updateIntersectObject(); 265 | 266 | if (this._intersectedEl) { 267 | this._emit('click'); 268 | } 269 | }, 270 | 271 | 272 | /** 273 | * @private 274 | */ 275 | _onRelease: function _onRelease() { 276 | if (!this._isActive()) { 277 | return; 278 | } 279 | 280 | /* check if mouse position has updated */ 281 | if (this._defMousePosition) { 282 | var defX = Math.abs(this._initMousePosition.x - this._defMousePosition.x); 283 | var defY = Math.abs(this._initMousePosition.y - this._defMousePosition.y); 284 | var def = Math.max(defX, defY); 285 | if (def > 0.04) { 286 | /* mouse has moved too much to recognize as click. */ 287 | this._isDown = false; 288 | } 289 | } 290 | 291 | if (this._isDown && this._intersectedEl) { 292 | this._emit('mouseup'); 293 | } 294 | this._isDown = false; 295 | this._resetMousePosition(); 296 | }, 297 | 298 | 299 | /** 300 | * @private 301 | */ 302 | _onMouseMove: function _onMouseMove(evt) { 303 | if (!this._isActive()) { 304 | return; 305 | } 306 | 307 | this._updateMouse(evt); 308 | this._updateIntersectObject(); 309 | 310 | if (this._isDown) { 311 | this._setMousePosition(evt); 312 | } 313 | }, 314 | 315 | 316 | /** 317 | * @private 318 | */ 319 | _onTouchMove: function _onTouchMove(evt) { 320 | if (!this._isActive()) { 321 | return; 322 | } 323 | 324 | this._isDown = false; 325 | }, 326 | 327 | 328 | /** 329 | * @private 330 | */ 331 | _onEnterVR: function _onEnterVR() { 332 | if (IS_VR_AVAILABLE) { 333 | this._isStereo = true; 334 | } 335 | this._getCanvasPos(); 336 | }, 337 | 338 | 339 | /** 340 | * @private 341 | */ 342 | _onExitVR: function _onExitVR() { 343 | this._isStereo = false; 344 | this._getCanvasPos(); 345 | }, 346 | 347 | 348 | /** 349 | * @private 350 | */ 351 | _onComponentChanged: function _onComponentChanged(evt) { 352 | if (evt.detail.name === 'position') { 353 | this._updateIntersectObject(); 354 | } 355 | }, 356 | 357 | 358 | /*============================= 359 | = mouse = 360 | =============================*/ 361 | 362 | /** 363 | * Get mouse position from size of canvas element 364 | * @private 365 | */ 366 | _getPosition: function _getPosition(evt) { 367 | var _canvasSize = this._canvasSize, 368 | w = _canvasSize.width, 369 | h = _canvasSize.height, 370 | offsetW = _canvasSize.left, 371 | offsetH = _canvasSize.top; 372 | 373 | 374 | var cx = void 0, 375 | cy = void 0; 376 | if (this._isMobile) { 377 | var touches = evt.touches; 378 | 379 | if (!touches || touches.length !== 1) { 380 | return; 381 | } 382 | var touch = touches[0]; 383 | cx = touch.clientX; 384 | cy = touch.clientY; 385 | } else { 386 | cx = evt.clientX; 387 | cy = evt.clientY; 388 | } 389 | 390 | /* account for the offset if scene is embedded */ 391 | cx = cx - offsetW; 392 | cy = cy - offsetH; 393 | 394 | if (this._isStereo) { 395 | cx = cx % (w / 2) * 2; 396 | } 397 | 398 | var x = cx / w * 2 - 1; 399 | var y = -(cy / h) * 2 + 1; 400 | 401 | return { x: x, y: y }; 402 | }, 403 | 404 | 405 | /** 406 | * Update mouse 407 | * @private 408 | */ 409 | _updateMouse: function _updateMouse(evt) { 410 | var pos = this._getPosition(evt); 411 | if (!pos) { 412 | return; 413 | } 414 | 415 | this._mouse.x = pos.x; 416 | this._mouse.y = pos.y; 417 | }, 418 | 419 | 420 | /** 421 | * Update mouse position 422 | * @private 423 | */ 424 | _setMousePosition: function _setMousePosition(evt) { 425 | this._defMousePosition = this._getPosition(evt); 426 | }, 427 | 428 | 429 | /** 430 | * Update initial mouse position 431 | * @private 432 | */ 433 | _setInitMousePosition: function _setInitMousePosition(evt) { 434 | this._initMousePosition = this._getPosition(evt); 435 | }, 436 | _resetMousePosition: function _resetMousePosition() { 437 | this._initMousePosition = this._defMousePosition = null; 438 | }, 439 | 440 | 441 | /*====================================== 442 | = scene children = 443 | ======================================*/ 444 | 445 | /** 446 | * @private 447 | */ 448 | _getCanvasPos: function _getCanvasPos() { 449 | this._canvasSize = this.el.sceneEl.canvas.getBoundingClientRect(); // update _canvas in case scene is embedded 450 | }, 451 | 452 | 453 | /** 454 | * Get non group object3D 455 | * @private 456 | */ 457 | _getChildren: function _getChildren(object3D) { 458 | var _this = this; 459 | 460 | return object3D.children.map(function (obj) { 461 | return obj.type === 'Group' ? _this._getChildren(obj) : obj; 462 | }); 463 | }, 464 | 465 | 466 | /** 467 | * Get all non group object3D 468 | * @private 469 | */ 470 | _getAllChildren: function _getAllChildren() { 471 | var children = this._getChildren(this.el.sceneEl.object3D); 472 | return (0, _lodash2.default)(children); 473 | }, 474 | 475 | 476 | /*==================================== 477 | = intersection = 478 | ====================================*/ 479 | 480 | /** 481 | * Update intersect element with cursor 482 | * @private 483 | */ 484 | _updateIntersectObject: function _updateIntersectObject() { 485 | var _raycaster = this._raycaster, 486 | el = this.el, 487 | _mouse = this._mouse; 488 | var scene = el.sceneEl.object3D; 489 | 490 | var camera = this.el.getObject3D('camera'); 491 | this._getAllChildren(); 492 | /* find intersections */ 493 | // _raycaster.setFromCamera(_mouse, camera) /* this somehow gets error so did the below */ 494 | _raycaster.ray.origin.setFromMatrixPosition(camera.matrixWorld); 495 | _raycaster.ray.direction.set(_mouse.x, _mouse.y, 0.5).unproject(camera).sub(_raycaster.ray.origin).normalize(); 496 | 497 | /* get objects intersected between mouse and camera */ 498 | var children = this._getAllChildren(); 499 | var intersects = _raycaster.intersectObjects(children); 500 | 501 | if (intersects.length > 0) { 502 | /* get the closest three obj */ 503 | var obj = void 0; 504 | intersects.every(function (item) { 505 | if (item.object.parent.visible === true) { 506 | obj = item.object; 507 | return false; 508 | } else { 509 | return true; 510 | } 511 | }); 512 | if (!obj) { 513 | this._clearIntersectObject(); 514 | return; 515 | } 516 | /* get the entity */ 517 | var _el = obj.parent.el; 518 | /* only updates if the object is not the activated object */ 519 | 520 | if (this._intersectedEl === _el) { 521 | return; 522 | } 523 | this._clearIntersectObject(); 524 | /* apply new object as intersected */ 525 | this._setIntersectObject(_el); 526 | } else { 527 | this._clearIntersectObject(); 528 | } 529 | }, 530 | 531 | 532 | /** 533 | * Set intersect element 534 | * @private 535 | * @param {AEntity} el `a-entity` element 536 | */ 537 | _setIntersectObject: function _setIntersectObject(el) { 538 | this._intersectedEl = el; 539 | if (this._isMobile) { 540 | return; 541 | } 542 | el.addState('hovered'); 543 | el.emit('mouseenter'); 544 | this.el.addState('hovering'); 545 | }, 546 | 547 | 548 | /** 549 | * Clear intersect element 550 | * @private 551 | */ 552 | _clearIntersectObject: function _clearIntersectObject() { 553 | var el = this._intersectedEl; 554 | 555 | if (el && !this._isMobile) { 556 | el.removeState('hovered'); 557 | el.emit('mouseleave'); 558 | this.el.removeState('hovering'); 559 | } 560 | 561 | this._intersectedEl = null; 562 | }, 563 | 564 | 565 | /*=============================== 566 | = emitter = 567 | ===============================*/ 568 | 569 | /** 570 | * @private 571 | */ 572 | _emit: function _emit(evt) { 573 | var _intersectedEl = this._intersectedEl; 574 | 575 | this.el.emit(evt, { target: _intersectedEl }); 576 | if (_intersectedEl) { 577 | _intersectedEl.emit(evt); 578 | } 579 | } 580 | }); 581 | 582 | /***/ }), 583 | /* 1 */ 584 | /***/ (function(module, exports) { 585 | 586 | /* WEBPACK VAR INJECTION */(function(global) {/** 587 | * lodash (Custom Build) 588 | * Build: `lodash modularize exports="npm" -o ./` 589 | * Copyright jQuery Foundation and other contributors 590 | * Released under MIT license 591 | * Based on Underscore.js 1.8.3 592 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 593 | */ 594 | 595 | /** Used as references for various `Number` constants. */ 596 | var INFINITY = 1 / 0, 597 | MAX_SAFE_INTEGER = 9007199254740991; 598 | 599 | /** `Object#toString` result references. */ 600 | var argsTag = '[object Arguments]', 601 | funcTag = '[object Function]', 602 | genTag = '[object GeneratorFunction]'; 603 | 604 | /** Detect free variable `global` from Node.js. */ 605 | var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; 606 | 607 | /** Detect free variable `self`. */ 608 | var freeSelf = typeof self == 'object' && self && self.Object === Object && self; 609 | 610 | /** Used as a reference to the global object. */ 611 | var root = freeGlobal || freeSelf || Function('return this')(); 612 | 613 | /** 614 | * Appends the elements of `values` to `array`. 615 | * 616 | * @private 617 | * @param {Array} array The array to modify. 618 | * @param {Array} values The values to append. 619 | * @returns {Array} Returns `array`. 620 | */ 621 | function arrayPush(array, values) { 622 | var index = -1, 623 | length = values.length, 624 | offset = array.length; 625 | 626 | while (++index < length) { 627 | array[offset + index] = values[index]; 628 | } 629 | return array; 630 | } 631 | 632 | /** Used for built-in method references. */ 633 | var objectProto = Object.prototype; 634 | 635 | /** Used to check objects for own properties. */ 636 | var hasOwnProperty = objectProto.hasOwnProperty; 637 | 638 | /** 639 | * Used to resolve the 640 | * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) 641 | * of values. 642 | */ 643 | var objectToString = objectProto.toString; 644 | 645 | /** Built-in value references. */ 646 | var Symbol = root.Symbol, 647 | propertyIsEnumerable = objectProto.propertyIsEnumerable, 648 | spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined; 649 | 650 | /** 651 | * The base implementation of `_.flatten` with support for restricting flattening. 652 | * 653 | * @private 654 | * @param {Array} array The array to flatten. 655 | * @param {number} depth The maximum recursion depth. 656 | * @param {boolean} [predicate=isFlattenable] The function invoked per iteration. 657 | * @param {boolean} [isStrict] Restrict to values that pass `predicate` checks. 658 | * @param {Array} [result=[]] The initial result value. 659 | * @returns {Array} Returns the new flattened array. 660 | */ 661 | function baseFlatten(array, depth, predicate, isStrict, result) { 662 | var index = -1, 663 | length = array.length; 664 | 665 | predicate || (predicate = isFlattenable); 666 | result || (result = []); 667 | 668 | while (++index < length) { 669 | var value = array[index]; 670 | if (depth > 0 && predicate(value)) { 671 | if (depth > 1) { 672 | // Recursively flatten arrays (susceptible to call stack limits). 673 | baseFlatten(value, depth - 1, predicate, isStrict, result); 674 | } else { 675 | arrayPush(result, value); 676 | } 677 | } else if (!isStrict) { 678 | result[result.length] = value; 679 | } 680 | } 681 | return result; 682 | } 683 | 684 | /** 685 | * Checks if `value` is a flattenable `arguments` object or array. 686 | * 687 | * @private 688 | * @param {*} value The value to check. 689 | * @returns {boolean} Returns `true` if `value` is flattenable, else `false`. 690 | */ 691 | function isFlattenable(value) { 692 | return isArray(value) || isArguments(value) || 693 | !!(spreadableSymbol && value && value[spreadableSymbol]); 694 | } 695 | 696 | /** 697 | * Recursively flattens `array`. 698 | * 699 | * @static 700 | * @memberOf _ 701 | * @since 3.0.0 702 | * @category Array 703 | * @param {Array} array The array to flatten. 704 | * @returns {Array} Returns the new flattened array. 705 | * @example 706 | * 707 | * _.flattenDeep([1, [2, [3, [4]], 5]]); 708 | * // => [1, 2, 3, 4, 5] 709 | */ 710 | function flattenDeep(array) { 711 | var length = array ? array.length : 0; 712 | return length ? baseFlatten(array, INFINITY) : []; 713 | } 714 | 715 | /** 716 | * Checks if `value` is likely an `arguments` object. 717 | * 718 | * @static 719 | * @memberOf _ 720 | * @since 0.1.0 721 | * @category Lang 722 | * @param {*} value The value to check. 723 | * @returns {boolean} Returns `true` if `value` is an `arguments` object, 724 | * else `false`. 725 | * @example 726 | * 727 | * _.isArguments(function() { return arguments; }()); 728 | * // => true 729 | * 730 | * _.isArguments([1, 2, 3]); 731 | * // => false 732 | */ 733 | function isArguments(value) { 734 | // Safari 8.1 makes `arguments.callee` enumerable in strict mode. 735 | return isArrayLikeObject(value) && hasOwnProperty.call(value, 'callee') && 736 | (!propertyIsEnumerable.call(value, 'callee') || objectToString.call(value) == argsTag); 737 | } 738 | 739 | /** 740 | * Checks if `value` is classified as an `Array` object. 741 | * 742 | * @static 743 | * @memberOf _ 744 | * @since 0.1.0 745 | * @category Lang 746 | * @param {*} value The value to check. 747 | * @returns {boolean} Returns `true` if `value` is an array, else `false`. 748 | * @example 749 | * 750 | * _.isArray([1, 2, 3]); 751 | * // => true 752 | * 753 | * _.isArray(document.body.children); 754 | * // => false 755 | * 756 | * _.isArray('abc'); 757 | * // => false 758 | * 759 | * _.isArray(_.noop); 760 | * // => false 761 | */ 762 | var isArray = Array.isArray; 763 | 764 | /** 765 | * Checks if `value` is array-like. A value is considered array-like if it's 766 | * not a function and has a `value.length` that's an integer greater than or 767 | * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. 768 | * 769 | * @static 770 | * @memberOf _ 771 | * @since 4.0.0 772 | * @category Lang 773 | * @param {*} value The value to check. 774 | * @returns {boolean} Returns `true` if `value` is array-like, else `false`. 775 | * @example 776 | * 777 | * _.isArrayLike([1, 2, 3]); 778 | * // => true 779 | * 780 | * _.isArrayLike(document.body.children); 781 | * // => true 782 | * 783 | * _.isArrayLike('abc'); 784 | * // => true 785 | * 786 | * _.isArrayLike(_.noop); 787 | * // => false 788 | */ 789 | function isArrayLike(value) { 790 | return value != null && isLength(value.length) && !isFunction(value); 791 | } 792 | 793 | /** 794 | * This method is like `_.isArrayLike` except that it also checks if `value` 795 | * is an object. 796 | * 797 | * @static 798 | * @memberOf _ 799 | * @since 4.0.0 800 | * @category Lang 801 | * @param {*} value The value to check. 802 | * @returns {boolean} Returns `true` if `value` is an array-like object, 803 | * else `false`. 804 | * @example 805 | * 806 | * _.isArrayLikeObject([1, 2, 3]); 807 | * // => true 808 | * 809 | * _.isArrayLikeObject(document.body.children); 810 | * // => true 811 | * 812 | * _.isArrayLikeObject('abc'); 813 | * // => false 814 | * 815 | * _.isArrayLikeObject(_.noop); 816 | * // => false 817 | */ 818 | function isArrayLikeObject(value) { 819 | return isObjectLike(value) && isArrayLike(value); 820 | } 821 | 822 | /** 823 | * Checks if `value` is classified as a `Function` object. 824 | * 825 | * @static 826 | * @memberOf _ 827 | * @since 0.1.0 828 | * @category Lang 829 | * @param {*} value The value to check. 830 | * @returns {boolean} Returns `true` if `value` is a function, else `false`. 831 | * @example 832 | * 833 | * _.isFunction(_); 834 | * // => true 835 | * 836 | * _.isFunction(/abc/); 837 | * // => false 838 | */ 839 | function isFunction(value) { 840 | // The use of `Object#toString` avoids issues with the `typeof` operator 841 | // in Safari 8-9 which returns 'object' for typed array and other constructors. 842 | var tag = isObject(value) ? objectToString.call(value) : ''; 843 | return tag == funcTag || tag == genTag; 844 | } 845 | 846 | /** 847 | * Checks if `value` is a valid array-like length. 848 | * 849 | * **Note:** This method is loosely based on 850 | * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). 851 | * 852 | * @static 853 | * @memberOf _ 854 | * @since 4.0.0 855 | * @category Lang 856 | * @param {*} value The value to check. 857 | * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. 858 | * @example 859 | * 860 | * _.isLength(3); 861 | * // => true 862 | * 863 | * _.isLength(Number.MIN_VALUE); 864 | * // => false 865 | * 866 | * _.isLength(Infinity); 867 | * // => false 868 | * 869 | * _.isLength('3'); 870 | * // => false 871 | */ 872 | function isLength(value) { 873 | return typeof value == 'number' && 874 | value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; 875 | } 876 | 877 | /** 878 | * Checks if `value` is the 879 | * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) 880 | * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) 881 | * 882 | * @static 883 | * @memberOf _ 884 | * @since 0.1.0 885 | * @category Lang 886 | * @param {*} value The value to check. 887 | * @returns {boolean} Returns `true` if `value` is an object, else `false`. 888 | * @example 889 | * 890 | * _.isObject({}); 891 | * // => true 892 | * 893 | * _.isObject([1, 2, 3]); 894 | * // => true 895 | * 896 | * _.isObject(_.noop); 897 | * // => true 898 | * 899 | * _.isObject(null); 900 | * // => false 901 | */ 902 | function isObject(value) { 903 | var type = typeof value; 904 | return !!value && (type == 'object' || type == 'function'); 905 | } 906 | 907 | /** 908 | * Checks if `value` is object-like. A value is object-like if it's not `null` 909 | * and has a `typeof` result of "object". 910 | * 911 | * @static 912 | * @memberOf _ 913 | * @since 4.0.0 914 | * @category Lang 915 | * @param {*} value The value to check. 916 | * @returns {boolean} Returns `true` if `value` is object-like, else `false`. 917 | * @example 918 | * 919 | * _.isObjectLike({}); 920 | * // => true 921 | * 922 | * _.isObjectLike([1, 2, 3]); 923 | * // => true 924 | * 925 | * _.isObjectLike(_.noop); 926 | * // => false 927 | * 928 | * _.isObjectLike(null); 929 | * // => false 930 | */ 931 | function isObjectLike(value) { 932 | return !!value && typeof value == 'object'; 933 | } 934 | 935 | module.exports = flattenDeep; 936 | 937 | /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) 938 | 939 | /***/ }) 940 | /******/ ]); -------------------------------------------------------------------------------- /dist/aframe-mouse-cursor-component.min.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(n){if(i[n])return i[n].exports;var s=i[n]={exports:{},id:n,loaded:!1};return e[n].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var i={};return t.m=e,t.c=i,t.p="",t(0)}([function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}var s=i(1),o=n(s);if("undefined"==typeof AFRAME)throw"mouse-cursor Component attempted to register before AFRAME was available.";var r=AFRAME.utils.device.isMobile()||window.hasNonPolyfillWebVRSupport;AFRAME.registerComponent("mouse-cursor",{schema:{},init:function(){this._raycaster=new THREE.Raycaster,this._mouse=new THREE.Vector2,this._isMobile=this.el.sceneEl.isMobile,this._isStereo=!1,this._active=!1,this._isDown=!1,this._intersectedEl=null,this._attachEventListeners(),this._canvasSize=!1,this.__getCanvasPos=this._getCanvasPos.bind(this),this.__getCanvasPos=this._getCanvasPos.bind(this),this.__onEnterVR=this._onEnterVR.bind(this),this.__onExitVR=this._onExitVR.bind(this),this.__onDown=this._onDown.bind(this),this.__onClick=this._onClick.bind(this),this.__onMouseMove=this._onMouseMove.bind(this),this.__onRelease=this._onRelease.bind(this),this.__onTouchMove=this._onTouchMove.bind(this),this.__onComponentChanged=this._onComponentChanged.bind(this)},update:function(e){},remove:function(){this._removeEventListeners(),this._raycaster=null},pause:function(){this._active=!1},play:function(){this._active=!0},_attachEventListeners:function(){var e=this.el,t=e.sceneEl,i=t.canvas;return i?(window.addEventListener("resize",this.__getCanvasPos),document.addEventListener("scroll",this.__getCanvasPos),this._getCanvasPos(),t.addEventListener("enter-vr",this.__onEnterVR),t.addEventListener("exit-vr",this.__onExitVR),i.addEventListener("mousedown",this.__onDown),i.addEventListener("mousemove",this.__onMouseMove),i.addEventListener("mouseup",this.__onRelease),i.addEventListener("mouseout",this.__onRelease),i.addEventListener("touchstart",this.__onDown),i.addEventListener("touchmove",this.__onTouchMove),i.addEventListener("touchend",this.__onRelease),i.addEventListener("click",this.__onClick),void e.addEventListener("componentchanged",this.__onComponentChanged)):void e.sceneEl.addEventListener("render-target-loaded",this._attachEventListeners.bind(this))},_removeEventListeners:function(){var e=this.el,t=e.sceneEl,i=t.canvas;i&&(window.removeEventListener("resize",this.__getCanvasPos),document.removeEventListener("scroll",this.__getCanvasPos),t.removeEventListener("enter-vr",this.__onEnterVR),t.removeEventListener("exit-vr",this.__onExitVR),i.removeEventListener("mousedown",this.__onDown),i.removeEventListener("mousemove",this.__onMouseMove),i.removeEventListener("mouseup",this.__onRelease),i.removeEventListener("mouseout",this.__onRelease),i.removeEventListener("touchstart",this.__onDown),i.removeEventListener("touchmove",this.__onTouchMove),i.removeEventListener("touchend",this.__onRelease),i.removeEventListener("click",this.__onClick),e.removeEventListener("componentchanged",this.__onComponentChanged))},_isActive:function(){return!(!this._active&&!this._raycaster)},_onDown:function(e){this._isActive()&&(this._isDown=!0,this._updateMouse(e),this._updateIntersectObject(),this._isMobile||this._setInitMousePosition(e),this._intersectedEl&&this._emit("mousedown"))},_onClick:function(e){this._isActive()&&(this._updateMouse(e),this._updateIntersectObject(),this._intersectedEl&&this._emit("click"))},_onRelease:function(){if(this._isActive()){if(this._defMousePosition){var e=Math.abs(this._initMousePosition.x-this._defMousePosition.x),t=Math.abs(this._initMousePosition.y-this._defMousePosition.y),i=Math.max(e,t);i>.04&&(this._isDown=!1)}this._isDown&&this._intersectedEl&&this._emit("mouseup"),this._isDown=!1,this._resetMousePosition()}},_onMouseMove:function(e){this._isActive()&&(this._updateMouse(e),this._updateIntersectObject(),this._isDown&&this._setMousePosition(e))},_onTouchMove:function(e){this._isActive()&&(this._isDown=!1)},_onEnterVR:function(){r&&(this._isStereo=!0),this._getCanvasPos()},_onExitVR:function(){this._isStereo=!1,this._getCanvasPos()},_onComponentChanged:function(e){"position"===e.detail.name&&this._updateIntersectObject()},_getPosition:function(e){var t=this._canvasSize,i=t.width,n=t.height,s=t.left,o=t.top,r=void 0,h=void 0;if(this._isMobile){var c=e.touches;if(!c||1!==c.length)return;var _=c[0];r=_.clientX,h=_.clientY}else r=e.clientX,h=e.clientY;r-=s,h-=o,this._isStereo&&(r=r%(i/2)*2);var a=r/i*2-1,u=2*-(h/n)+1;return{x:a,y:u}},_updateMouse:function(e){var t=this._getPosition(e);t&&(this._mouse.x=t.x,this._mouse.y=t.y)},_setMousePosition:function(e){this._defMousePosition=this._getPosition(e)},_setInitMousePosition:function(e){this._initMousePosition=this._getPosition(e)},_resetMousePosition:function(){this._initMousePosition=this._defMousePosition=null},_getCanvasPos:function(){this._canvasSize=this.el.sceneEl.canvas.getBoundingClientRect()},_getChildren:function(e){var t=this;return e.children.map(function(e){return"Group"===e.type?t._getChildren(e):e})},_getAllChildren:function(){var e=this._getChildren(this.el.sceneEl.object3D);return(0,o.default)(e)},_updateIntersectObject:function(){var e=this._raycaster,t=this.el,i=this._mouse,n=(t.sceneEl.object3D,this.el.getObject3D("camera"));this._getAllChildren(),e.ray.origin.setFromMatrixPosition(n.matrixWorld),e.ray.direction.set(i.x,i.y,.5).unproject(n).sub(e.ray.origin).normalize();var s=this._getAllChildren(),o=e.intersectObjects(s);if(o.length>0){var r=void 0;if(o.every(function(e){return e.object.parent.visible!==!0||(r=e.object,!1)}),!r)return void this._clearIntersectObject();var h=r.parent.el;if(this._intersectedEl===h)return;this._clearIntersectObject(),this._setIntersectObject(h)}else this._clearIntersectObject()},_setIntersectObject:function(e){this._intersectedEl=e,this._isMobile||(e.addState("hovered"),e.emit("mouseenter"),this.el.addState("hovering"))},_clearIntersectObject:function(){var e=this._intersectedEl;e&&!this._isMobile&&(e.removeState("hovered"),e.emit("mouseleave"),this.el.removeState("hovering")),this._intersectedEl=null},_emit:function(e){var t=this._intersectedEl;this.el.emit(e,{target:t}),t&&t.emit(e)}})},function(e,t){(function(t){function i(e,t){for(var i=-1,n=t.length,s=e.length;++i0&&o(a)?t>1?n(a,t-1,o,r,h):i(h,a):r||(h[h.length]=a)}return h}function s(e){return w(e)||r(e)||!!(y&&e&&e[y])}function o(e){var t=e?e.length:0;return t?n(e,v):[]}function r(e){return c(e)&&C.call(e,"callee")&&(!j.call(e,"callee")||P.call(e)==f)}function h(e){return null!=e&&a(e.length)&&!_(e)}function c(e){return l(e)&&h(e)}function _(e){var t=u(e)?P.call(e):"";return t==m||t==E}function a(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=d}function u(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function l(e){return!!e&&"object"==typeof e}var v=1/0,d=9007199254740991,f="[object Arguments]",m="[object Function]",E="[object GeneratorFunction]",b="object"==typeof t&&t&&t.Object===Object&&t,p="object"==typeof self&&self&&self.Object===Object&&self,g=b||p||Function("return this")(),M=Object.prototype,C=M.hasOwnProperty,P=M.toString,L=g.Symbol,j=M.propertyIsEnumerable,y=L?L.isConcatSpreadable:void 0,w=Array.isArray;e.exports=o}).call(t,function(){return this}())}]); -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Mouse Cursor Component - Basic 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/common.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: inherit !important; 3 | font-family: monospace; 4 | } 5 | .buttons { 6 | position: absolute; 7 | z-index: 2; 8 | } 9 | .buttons a { 10 | display: inline-block; 11 | border: none; 12 | padding: 1em; 13 | margin: 1em 0 0 1em; 14 | background: gray; 15 | color: white; 16 | font: 14px monospace; 17 | text-decoration: none; 18 | } 19 | .buttons a:active { 20 | background: #333; 21 | } 22 | .spacer { 23 | position: relative; 24 | pointer-events: none; 25 | height: 100%; 26 | } 27 | .spacer2 { 28 | position: relative; 29 | pointer-events: none; 30 | height: 1px; 31 | } 32 | -------------------------------------------------------------------------------- /examples/embedded/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Mouse Cursor Component - Embedded 6 | 7 | 8 | 14 | 15 | 16 |

My awesome embedded VR scene

17 |

You should be able to embed your scene in a blog post, and still have the mouse cursor work.

18 | 19 |

Now I'm just filling this space deliberately, to make the scene somewhere in the page...

20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayognaise/aframe-mouse-cursor-component/3146426bc5ee45d979222a6453f4053fe6e9900a/examples/example.gif -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Mouse Cursor Component 6 | 7 | 27 | 28 | 29 |

A-Frame Mouse Cursor Component

30 | Basic 31 | Embedded 32 | 33 |
34 |
35 | Fork me on GitHub 36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | import 'aframe' 2 | import 'aframe-event-set-component' 3 | import '../index' -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import flattenDeep from 'lodash.flattendeep' 2 | 3 | if (typeof AFRAME === 'undefined') { 4 | throw 'mouse-cursor Component attempted to register before AFRAME was available.' 5 | } 6 | 7 | const IS_VR_AVAILABLE = AFRAME.utils.device.isMobile() || window.hasNonPolyfillWebVRSupport 8 | 9 | /** 10 | * Mouse Cursor Component for A-Frame. 11 | */ 12 | AFRAME.registerComponent('mouse-cursor', { 13 | schema: { }, 14 | 15 | /** 16 | * Called once when component is attached. Generally for initial setup. 17 | * @protected 18 | */ 19 | init () { 20 | this._raycaster = new THREE.Raycaster() 21 | this._mouse = new THREE.Vector2() 22 | this._isMobile = this.el.sceneEl.isMobile 23 | this._isStereo = false 24 | this._active = false 25 | this._isDown = false 26 | this._intersectedEl = null 27 | this._attachEventListeners() 28 | this._canvasSize = false 29 | /* bind functions */ 30 | this.__getCanvasPos = this._getCanvasPos.bind(this) 31 | this.__getCanvasPos = this._getCanvasPos.bind(this) 32 | this.__onEnterVR = this._onEnterVR.bind(this) 33 | this.__onExitVR = this._onExitVR.bind(this) 34 | this.__onDown = this._onDown.bind(this) 35 | this.__onClick = this._onClick.bind(this) 36 | this.__onMouseMove = this._onMouseMove.bind(this) 37 | this.__onRelease = this._onRelease.bind(this) 38 | this.__onTouchMove = this._onTouchMove.bind(this) 39 | this.__onComponentChanged = this._onComponentChanged.bind(this) 40 | }, 41 | 42 | /** 43 | * Called when component is attached and when component data changes. 44 | * Generally modifies the entity based on the data. 45 | * @protected 46 | */ 47 | update (oldData) { 48 | }, 49 | 50 | /** 51 | * Called when a component is removed (e.g., via removeAttribute). 52 | * Generally undoes all modifications to the entity. 53 | * @protected 54 | */ 55 | remove () { 56 | this._removeEventListeners() 57 | this._raycaster = null 58 | }, 59 | 60 | /** 61 | * Called on each scene tick. 62 | * @protected 63 | */ 64 | // tick (t) { }, 65 | 66 | /** 67 | * Called when entity pauses. 68 | * Use to stop or remove any dynamic or background behavior such as events. 69 | * @protected 70 | */ 71 | pause () { 72 | this._active = false 73 | }, 74 | 75 | /** 76 | * Called when entity resumes. 77 | * Use to continue or add any dynamic or background behavior such as events. 78 | * @protected 79 | */ 80 | play () { 81 | this._active = true 82 | }, 83 | 84 | /*============================== 85 | = events = 86 | ==============================*/ 87 | 88 | /** 89 | * @private 90 | */ 91 | _attachEventListeners () { 92 | const { el } = this 93 | const { sceneEl } = el 94 | const { canvas } = sceneEl 95 | /* if canvas doesn't exist, listen for canvas to load. */ 96 | if (!canvas) { 97 | el.sceneEl.addEventListener('render-target-loaded', this._attachEventListeners.bind(this)) 98 | return 99 | } 100 | 101 | window.addEventListener('resize', this.__getCanvasPos) 102 | document.addEventListener('scroll', this.__getCanvasPos) 103 | /* update _canvas in case scene is embedded */ 104 | this._getCanvasPos() 105 | 106 | /* scene */ 107 | sceneEl.addEventListener('enter-vr', this.__onEnterVR) 108 | sceneEl.addEventListener('exit-vr', this.__onExitVR) 109 | 110 | /* Mouse events */ 111 | canvas.addEventListener('mousedown', this.__onDown) 112 | canvas.addEventListener('mousemove', this.__onMouseMove) 113 | canvas.addEventListener('mouseup', this.__onRelease) 114 | canvas.addEventListener('mouseout', this.__onRelease) 115 | 116 | /* Touch events */ 117 | canvas.addEventListener('touchstart', this.__onDown) 118 | canvas.addEventListener('touchmove', this.__onTouchMove) 119 | canvas.addEventListener('touchend', this.__onRelease) 120 | 121 | /* Click event */ 122 | canvas.addEventListener('click', this.__onClick) 123 | 124 | /* Element component change */ 125 | el.addEventListener('componentchanged', this.__onComponentChanged) 126 | 127 | }, 128 | 129 | /** 130 | * @private 131 | */ 132 | _removeEventListeners () { 133 | const { el } = this 134 | const { sceneEl } = el 135 | const { canvas } = sceneEl 136 | if (!canvas) { return } 137 | 138 | window.removeEventListener('resize', this.__getCanvasPos) 139 | document.removeEventListener('scroll', this.__getCanvasPos) 140 | 141 | /* scene */ 142 | sceneEl.removeEventListener('enter-vr', this.__onEnterVR) 143 | sceneEl.removeEventListener('exit-vr', this.__onExitVR) 144 | 145 | 146 | /* Mouse events */ 147 | canvas.removeEventListener('mousedown', this.__onDown) 148 | canvas.removeEventListener('mousemove', this.__onMouseMove) 149 | canvas.removeEventListener('mouseup', this.__onRelease) 150 | canvas.removeEventListener('mouseout', this.__onRelease) 151 | 152 | /* Touch events */ 153 | canvas.removeEventListener('touchstart', this.__onDown) 154 | canvas.removeEventListener('touchmove', this.__onTouchMove) 155 | canvas.removeEventListener('touchend', this.__onRelease) 156 | 157 | /* Click event */ 158 | canvas.removeEventListener('click', this.__onClick) 159 | 160 | /* Element component change */ 161 | el.removeEventListener('componentchanged', this.__onComponentChanged) 162 | 163 | }, 164 | 165 | /** 166 | * Check if the mouse cursor is active 167 | * @private 168 | */ 169 | _isActive () { 170 | return !!(this._active || this._raycaster) 171 | }, 172 | 173 | /** 174 | * @private 175 | */ 176 | _onDown (evt) { 177 | if (!this._isActive()) { return } 178 | 179 | this._isDown = true 180 | 181 | this._updateMouse(evt) 182 | this._updateIntersectObject() 183 | 184 | if (!this._isMobile) { 185 | this._setInitMousePosition(evt) 186 | } 187 | if (this._intersectedEl) { 188 | this._emit('mousedown') 189 | } 190 | }, 191 | 192 | /** 193 | * @private 194 | */ 195 | _onClick (evt) { 196 | if (!this._isActive()) { return } 197 | 198 | this._updateMouse(evt) 199 | this._updateIntersectObject() 200 | 201 | if (this._intersectedEl) { 202 | this._emit('click') 203 | } 204 | }, 205 | 206 | /** 207 | * @private 208 | */ 209 | _onRelease () { 210 | if (!this._isActive()) { return } 211 | 212 | /* check if mouse position has updated */ 213 | if (this._defMousePosition) { 214 | const defX = Math.abs(this._initMousePosition.x - this._defMousePosition.x) 215 | const defY = Math.abs(this._initMousePosition.y - this._defMousePosition.y) 216 | const def = Math.max(defX, defY) 217 | if (def > 0.04) { 218 | /* mouse has moved too much to recognize as click. */ 219 | this._isDown = false 220 | } 221 | } 222 | 223 | if (this._isDown && this._intersectedEl) { 224 | this._emit('mouseup') 225 | } 226 | this._isDown = false 227 | this._resetMousePosition() 228 | }, 229 | 230 | /** 231 | * @private 232 | */ 233 | _onMouseMove (evt) { 234 | if (!this._isActive()) { return } 235 | 236 | this._updateMouse(evt) 237 | this._updateIntersectObject() 238 | 239 | if (this._isDown) { 240 | this._setMousePosition(evt) 241 | } 242 | }, 243 | 244 | /** 245 | * @private 246 | */ 247 | _onTouchMove (evt) { 248 | if (!this._isActive()) { return } 249 | 250 | this._isDown = false 251 | }, 252 | 253 | /** 254 | * @private 255 | */ 256 | _onEnterVR () { 257 | if (IS_VR_AVAILABLE) { 258 | this._isStereo = true 259 | } 260 | this._getCanvasPos() 261 | }, 262 | 263 | /** 264 | * @private 265 | */ 266 | _onExitVR () { 267 | this._isStereo = false 268 | this._getCanvasPos() 269 | 270 | }, 271 | 272 | /** 273 | * @private 274 | */ 275 | _onComponentChanged (evt) { 276 | if (evt.detail.name === 'position') { 277 | this._updateIntersectObject() 278 | } 279 | }, 280 | 281 | 282 | /*============================= 283 | = mouse = 284 | =============================*/ 285 | 286 | 287 | /** 288 | * Get mouse position from size of canvas element 289 | * @private 290 | */ 291 | _getPosition (evt) { 292 | const { width: w, height: h, left : offsetW, top : offsetH } = this._canvasSize 293 | 294 | let cx, cy 295 | if (this._isMobile) { 296 | const { touches } = evt 297 | if (!touches || touches.length !== 1) { return } 298 | const touch = touches[0] 299 | cx = touch.clientX 300 | cy = touch.clientY 301 | } 302 | else { 303 | cx = evt.clientX 304 | cy = evt.clientY 305 | } 306 | 307 | /* account for the offset if scene is embedded */ 308 | cx = cx - offsetW 309 | cy = cy - offsetH 310 | 311 | if (this._isStereo) { 312 | cx = (cx % (w/2)) * 2 313 | } 314 | 315 | const x = (cx / w) * 2 - 1 316 | const y = - (cy / h) * 2 + 1 317 | 318 | return { x, y } 319 | 320 | }, 321 | 322 | /** 323 | * Update mouse 324 | * @private 325 | */ 326 | _updateMouse (evt) { 327 | const pos = this._getPosition(evt) 328 | if (!pos) { return } 329 | 330 | this._mouse.x = pos.x 331 | this._mouse.y = pos.y 332 | }, 333 | 334 | 335 | /** 336 | * Update mouse position 337 | * @private 338 | */ 339 | _setMousePosition (evt) { 340 | this._defMousePosition = this._getPosition(evt) 341 | }, 342 | 343 | /** 344 | * Update initial mouse position 345 | * @private 346 | */ 347 | _setInitMousePosition (evt) { 348 | this._initMousePosition = this._getPosition(evt) 349 | }, 350 | 351 | _resetMousePosition () { 352 | this._initMousePosition = this._defMousePosition = null 353 | }, 354 | 355 | 356 | /*====================================== 357 | = scene children = 358 | ======================================*/ 359 | 360 | 361 | /** 362 | * @private 363 | */ 364 | _getCanvasPos () { 365 | this._canvasSize = this.el.sceneEl.canvas.getBoundingClientRect() // update _canvas in case scene is embedded 366 | }, 367 | 368 | /** 369 | * Get non group object3D 370 | * @private 371 | */ 372 | _getChildren (object3D) { 373 | return object3D.children.map(obj => (obj.type === 'Group')? this._getChildren(obj) : obj) 374 | }, 375 | 376 | /** 377 | * Get all non group object3D 378 | * @private 379 | */ 380 | _getAllChildren () { 381 | const children = this._getChildren(this.el.sceneEl.object3D) 382 | return flattenDeep(children) 383 | }, 384 | 385 | /*==================================== 386 | = intersection = 387 | ====================================*/ 388 | 389 | /** 390 | * Update intersect element with cursor 391 | * @private 392 | */ 393 | _updateIntersectObject () { 394 | const { _raycaster, el, _mouse } = this 395 | const { object3D: scene } = el.sceneEl 396 | const camera = this.el.getObject3D('camera') 397 | this._getAllChildren() 398 | /* find intersections */ 399 | // _raycaster.setFromCamera(_mouse, camera) /* this somehow gets error so did the below */ 400 | _raycaster.ray.origin.setFromMatrixPosition(camera.matrixWorld) 401 | _raycaster.ray.direction.set(_mouse.x, _mouse.y, 0.5).unproject(camera).sub(_raycaster.ray.origin).normalize() 402 | 403 | /* get objects intersected between mouse and camera */ 404 | const children = this._getAllChildren() 405 | const intersects = _raycaster.intersectObjects(children) 406 | 407 | if (intersects.length > 0) { 408 | /* get the closest three obj */ 409 | let obj 410 | intersects.every(item => { 411 | if (item.object.parent.visible === true) { 412 | obj = item.object 413 | return false 414 | } 415 | else { 416 | return true 417 | } 418 | }) 419 | if (!obj) { 420 | this._clearIntersectObject() 421 | return 422 | } 423 | /* get the entity */ 424 | const { el } = obj.parent 425 | /* only updates if the object is not the activated object */ 426 | if (this._intersectedEl === el) { return } 427 | this._clearIntersectObject() 428 | /* apply new object as intersected */ 429 | this._setIntersectObject(el) 430 | } 431 | else { 432 | this._clearIntersectObject() 433 | } 434 | }, 435 | 436 | /** 437 | * Set intersect element 438 | * @private 439 | * @param {AEntity} el `a-entity` element 440 | */ 441 | _setIntersectObject (el) { 442 | this._intersectedEl = el 443 | if (this._isMobile) { return } 444 | el.addState('hovered') 445 | el.emit('mouseenter') 446 | this.el.addState('hovering') 447 | 448 | }, 449 | 450 | /** 451 | * Clear intersect element 452 | * @private 453 | */ 454 | _clearIntersectObject () { 455 | const { _intersectedEl: el } = this 456 | if (el && !this._isMobile) { 457 | el.removeState('hovered') 458 | el.emit('mouseleave') 459 | this.el.removeState('hovering') 460 | } 461 | 462 | this._intersectedEl = null 463 | }, 464 | 465 | 466 | 467 | /*=============================== 468 | = emitter = 469 | ===============================*/ 470 | 471 | /** 472 | * @private 473 | */ 474 | _emit (evt) { 475 | const { _intersectedEl } = this 476 | this.el.emit(evt, { target: _intersectedEl }) 477 | if (_intersectedEl) { _intersectedEl.emit(evt) } 478 | }, 479 | 480 | }) 481 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-mouse-cursor-component", 3 | "version": "0.5.3", 4 | "description": "A-Frame Mouse Cursor Component for A-Frame VR.", 5 | "main": "dist/aframe-mouse-cursor-component.js", 6 | "scripts": { 7 | "build": "webpack -p examples/main.js examples/build.js", 8 | "dev": "budo examples/main.js:build.js --dir examples --port 8000 --live -- -t babelify", 9 | "dist": "webpack index.js dist/aframe-mouse-cursor-component.js && webpack -p index.js dist/aframe-mouse-cursor-component.min.js", 10 | "postpublish": "npm run dist", 11 | "preghpages": "npm run build && rm -rf gh-pages && cp -r examples gh-pages", 12 | "ghpages": "npm run preghpages && ghpages -p gh-pages" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/mayognaise/aframe-mouse-cursor-component.git" 17 | }, 18 | "keywords": [ 19 | "aframe", 20 | "aframe-component", 21 | "layout", 22 | "aframe-vr", 23 | "vr", 24 | "aframe-layout", 25 | "mozvr", 26 | "webvr" 27 | ], 28 | "author": "Mayo Tobita ", 29 | "contributors": [{ 30 | "name" : "Leigh Garland", 31 | "email" : "leigh@studiozero.co", 32 | "url" : "http://studiozero.co" 33 | }], 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/mayognaise/aframe-mouse-cursor-component/issues" 37 | }, 38 | "homepage": "https://github.com/mayognaise/aframe-mouse-cursor-component#readme", 39 | "devDependencies": { 40 | "aframe": "^0.5.0", 41 | "aframe-event-set-component": "^3.0.0", 42 | "babel-core": "^6.7.6", 43 | "babel-loader": "^6.2.4", 44 | "babel-preset-es2015": "^6.6.0", 45 | "babelify": "^7.2.0", 46 | "browserify": "^13.0.0", 47 | "budo": "^8.2.1", 48 | "ghpages": "0.0.3", 49 | "lodash.flattendeep": "^4.4.0", 50 | "webpack": "^1.12.15" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | loaders: [ 4 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } 5 | ] 6 | } 7 | } --------------------------------------------------------------------------------