├── .gitignore ├── README.md ├── bower.json ├── examples ├── basic.html ├── depth.html ├── fuse.html ├── gazeable.html ├── groups.html ├── img │ └── interactivepatterns_displayreticle.png ├── js │ └── lib │ │ ├── VRControls.js │ │ ├── VREffect.js │ │ ├── device-info-test.js │ │ ├── three.js │ │ ├── webvr-manager.js │ │ └── webvr-polyfill.js ├── proximity.html └── visibility.html ├── index.html ├── package.json └── reticulum.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile ~/.gitignore_global 6 | 7 | # Ignore bundler config 8 | /.bundle 9 | 10 | # Ignore the build directory 11 | **/build 12 | **/tmp 13 | 14 | # Ignore TDS 15 | **/typings 16 | 17 | # Ignore Sass' cache 18 | /.sass-cache 19 | 20 | # Ignore .DS_store file 21 | .DS_Store 22 | 23 | # Ignore Bower files 24 | /bower_components 25 | 26 | # Ignore Node files 27 | **/node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Reticulum 2 | 3 | > A simple gaze interaction manager for VR with Three.js. [See examples](https://skezo.github.io/Reticulum/) 4 | 5 |  6 | 7 | ##Purpose 8 | Reticulum attempts to follow Google's interactive pattern for the [display reticle](http://www.google.com/design/spec-vr/interactive-patterns/display-reticle.html). It creates the illusion of depth by projecting spatially onto targeted objects while maintaining a fixed size so that it is easy to see at all times. 9 | 10 | 11 | ### Features: 12 | - Avoids double vision and depth issues by projecting spatially onto targeted objects 13 | - Gaze and click events for targeted objects `onGazeOver`, `onGazeOut`, `onGazeLong` and `onGazeClick` 14 | - Set different fuze durations for targeted objects 15 | - Built in [fuse support](http://www.google.com/design/spec-vr/interactive-patterns/controls.html#controls-fuse-buttons) 16 | - Display the reticle only when the camera can see a targeted object 17 | - Works in the browser with Three.js (r73) 18 | 19 | 20 | ### 1. Getting Started 21 | 22 | Load Three.js and include the Reticulum.js file. You might also want to use the [Web VR boilerplate](https://github.com/borismus/webvr-boilerplate): 23 | 24 | ```html 25 | 26 | 27 | 28 | ``` 29 | 30 | ### 2. Initiate and set options 31 | 32 | Call the Reticulum initializer function and set your options. Options can be set globally or per targeted object. 33 | 34 | **Note:** You must define the `camera`... it is required. 35 | 36 | ```javascript 37 | Reticulum.init(camera, { 38 | proximity: false, 39 | clickevents: true, 40 | near: null, //near factor of the raycaster (shouldn't be negative and should be smaller than the far property) 41 | far: null, //far factor of the raycaster (shouldn't be negative and should be larger than the near property) 42 | reticle: { 43 | visible: true, 44 | restPoint: 1000, //Defines the reticle's resting point when no object has been targeted 45 | color: 0xcc0000, 46 | innerRadius: 0.0001, 47 | outerRadius: 0.003, 48 | hover: { 49 | color: 0xcc0000, 50 | innerRadius: 0.02, 51 | outerRadius: 0.024, 52 | speed: 5, 53 | vibrate: 50 //Set to 0 or [] to disable 54 | } 55 | }, 56 | fuse: { 57 | visible: true, 58 | duration: 2.5, 59 | color: 0x00fff6, 60 | innerRadius: 0.045, 61 | outerRadius: 0.06, 62 | vibrate: 100, //Set to 0 or [] to disable 63 | clickCancelFuse: false //If users clicks on targeted object fuse is canceled 64 | } 65 | }); 66 | ``` 67 | 68 | ### 3. Define targeted objects and options 69 | 70 | Add the three.js objects you want to be targeted objects. Override global options by setting local ones. 71 | 72 | ```javascript 73 | 74 | Reticulum.add( object, { 75 | clickCancelFuse: true, // Overrides global setting for fuse's clickCancelFuse 76 | reticleHoverColor: 0x00fff6, // Overrides global reticle hover color 77 | fuseVisible: true, // Overrides global fuse visibility 78 | fuseDuration: 1.5, // Overrides global fuse duration 79 | fuseColor: 0xcc0000, // Overrides global fuse color 80 | onGazeOver: function(){ 81 | // do something when user targets object 82 | this.material.emissive.setHex( 0xffcc00 ); 83 | }, 84 | onGazeOut: function(){ 85 | // do something when user moves reticle off targeted object 86 | this.material.emissive.setHex( 0xcc0000 ); 87 | }, 88 | onGazeLong: function(){ 89 | // do something user targetes object for specific time 90 | this.material.emissive.setHex( 0x0000cc ); 91 | }, 92 | onGazeClick: function(){ 93 | // have the object react when user clicks / taps on targeted object 94 | this.material.emissive.setHex( 0x0000cc ); 95 | } 96 | }); 97 | ``` 98 | 99 | You can also remove targeted objects. 100 | ```javascript 101 | Reticulum.remove( object ); 102 | ``` 103 | 104 | 105 | ### 4. Add to animation loop 106 | 107 | Add Reticulum to your animation loop 108 | 109 | ```javascript 110 | Reticulum.update() 111 | ``` 112 | 113 | 114 | ### 5. Add Camera to scene 115 | 116 | If you require to display the reticle you will need to add the `camera` to the `scene`. 117 | 118 | **Note:** See Known Issues below if ghosting occurs. 119 | 120 | ```javascript 121 | scene.add(camera); 122 | ``` 123 | 124 | ## Demos 125 | 126 | - [Basic](https://skezo.github.io/Reticulum/examples/basic.html) 127 | - [Proximity](https://skezo.github.io/Reticulum/examples/proximity.html) - only display reticle if targeted object is visible 128 | - [Depth Test](https://skezo.github.io/Reticulum/examples/depth.html) - hit moving targets 129 | - [Objects in Groups](https://skezo.github.io/Reticulum/examples/groups.html) - hit object in group, get world values 130 | - [Fuse](https://skezo.github.io/Reticulum/examples/fuse.html) - selective objects have fuse 131 | - [Visibility](https://skezo.github.io/Reticulum/examples/visibility.html) - test for hitting only visible objects 132 | - [Gazeable](https://skezo.github.io/Reticulum/examples/gazeable.html) - test for hitting only gazeable objects 133 | 134 | ## Known Issues 135 | - Ghosting occurs to the reticle and fuse when in VR mode. More details on the issue can found [here](https://github.com/mrdoob/three.js/issues/7041). **A quick workaround** to this issue is adding `camera.updateMatrixWorld();` before the render call (e.g. `manager.render(scene, camera, timestamp);` to the callback function of the `requestAnimationFrame()` method. 136 | 137 | 138 | ## Acknowledgements: 139 | Reticulum was inspired by the work done by [neuman](https://github.com/neuman/vreticle) 140 | 141 | ## License 142 | The MIT License (MIT) -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reticulum", 3 | "version": "1.0.0", 4 | "main": "reticulum.js", 5 | "ignore": [ 6 | "example" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
301 | * Allows to compute the original undistorted radius from a distorted one.
302 | * See also getApproximateInverseDistortion() for a faster but potentially
303 | * less accurate method.
304 | *
305 | * @param {Number} radius Distorted radius from the lens center in tan-angle units.
306 | * @return {Number} The undistorted radius in tan-angle units.
307 | */
308 | Distortion.prototype.distortInverse = function(radius) {
309 | // Secant method.
310 | var r0 = radius / 0.9;
311 | var r1 = radius * 0.9;
312 | var dr0 = radius - this.distort(r0);
313 | while (Math.abs(r1 - r0) > 0.0001 /** 0.1mm */) {
314 | var dr1 = radius - this.distort(r1);
315 | var r2 = r1 - dr1 * ((r1 - r0) / (dr1 - dr0));
316 | r0 = r1;
317 | r1 = r2;
318 | dr0 = dr1;
319 | }
320 | return r1;
321 | }
322 |
323 |
324 | /**
325 | * Distorts a radius by its distortion factor from the center of the lenses.
326 | *
327 | * @param {Number} radius Radius from the lens center in tan-angle units.
328 | * @return {Number} The distorted radius in tan-angle units.
329 | */
330 | Distortion.prototype.distort = function(radius) {
331 | return radius * this.distortionFactor_(radius);
332 | }
333 |
334 | /**
335 | * Returns the distortion factor of a point.
336 | *
337 | * @param {Number} radius Radius of the point from the lens center in tan-angle units.
338 | * @return {Number} The distortion factor. Multiply by this factor to distort points.
339 | */
340 | Distortion.prototype.distortionFactor_ = function(radius) {
341 | var result = 1.0;
342 | var rFactor = 1.0;
343 | var rSquared = radius * radius;
344 |
345 | for (var i = 0; i < this.coefficients.length; i++) {
346 | var ki = this.coefficients[i];
347 | rFactor *= rSquared;
348 | result += ki * rFactor;
349 | }
350 |
351 | return result;
352 | }
353 |
354 | module.exports = Distortion;
355 |
356 | },{}],3:[function(require,module,exports){
357 | /*
358 | * Copyright 2015 Google Inc. All Rights Reserved.
359 | * Licensed under the Apache License, Version 2.0 (the "License");
360 | * you may not use this file except in compliance with the License.
361 | * You may obtain a copy of the License at
362 | *
363 | * http://www.apache.org/licenses/LICENSE-2.0
364 | *
365 | * Unless required by applicable law or agreed to in writing, software
366 | * distributed under the License is distributed on an "AS IS" BASIS,
367 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
368 | * See the License for the specific language governing permissions and
369 | * limitations under the License.
370 | */
371 |
372 | var Util = {};
373 |
374 | Util.base64 = function(mimeType, base64) {
375 | return 'data:' + mimeType + ';base64,' + base64;
376 | };
377 |
378 | Util.isMobile = function() {
379 | var check = false;
380 | (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
381 | return check;
382 | };
383 |
384 | Util.isFirefox = function() {
385 | return /firefox/i.test(navigator.userAgent);
386 | };
387 |
388 | Util.isIOS = function() {
389 | return /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
390 | };
391 |
392 | Util.isIFrame = function() {
393 | try {
394 | return window.self !== window.top;
395 | } catch (e) {
396 | return true;
397 | }
398 | };
399 |
400 | Util.appendQueryParameter = function(url, key, value) {
401 | // Determine delimiter based on if the URL already GET parameters in it.
402 | var delimiter = (url.indexOf('?') < 0 ? '?' : '&');
403 | url += delimiter + key + '=' + value;
404 | return url;
405 | };
406 |
407 | // From http://goo.gl/4WX3tg
408 | Util.getQueryParameter = function(name) {
409 | name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
410 | var regex = new RegExp("[\\?&]" + name + "=([^]*)"),
411 | results = regex.exec(location.search);
412 | return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
413 | };
414 |
415 | Util.isLandscapeMode = function() {
416 | return (window.orientation == 90 || window.orientation == -90);
417 | };
418 |
419 | Util.getScreenWidth = function() {
420 | return Math.max(window.screen.width, window.screen.height) *
421 | window.devicePixelRatio;
422 | };
423 |
424 | Util.getScreenHeight = function() {
425 | return Math.min(window.screen.width, window.screen.height) *
426 | window.devicePixelRatio;
427 | };
428 |
429 | /**
430 | * Utility to convert the projection matrix to a vector accepted by the shader.
431 | *
432 | * @param {Object} opt_params A rectangle to scale this vector by.
433 | */
434 | Util.projectionMatrixToVector_ = function(matrix, opt_params) {
435 | var params = opt_params || {};
436 | var xScale = params.xScale || 1;
437 | var yScale = params.yScale || 1;
438 | var xTrans = params.xTrans || 0;
439 | var yTrans = params.yTrans || 0;
440 |
441 | var elements = matrix.elements;
442 | var vec = new THREE.Vector4();
443 | vec.set(elements[4*0 + 0] * xScale,
444 | elements[4*1 + 1] * yScale,
445 | elements[4*2 + 0] - 1 - xTrans,
446 | elements[4*2 + 1] - 1 - yTrans).divideScalar(2);
447 | return vec;
448 | };
449 |
450 | Util.leftProjectionVectorToRight_ = function(left) {
451 | //projectionLeft + vec4(0.0, 0.0, 1.0, 0.0)) * vec4(1.0, 1.0, -1.0, 1.0);
452 | var out = new THREE.Vector4(0, 0, 1, 0);
453 | out.add(left); // out = left + (0, 0, 1, 0).
454 | out.z *= -1; // Flip z.
455 |
456 | return out;
457 | };
458 |
459 |
460 | module.exports = Util;
461 |
462 | },{}],4:[function(require,module,exports){
463 | var DeviceInfo = require('../src/device-info.js');
464 |
465 | var di = new DeviceInfo();
466 | var centroid = di.getLeftEyeCenter();
467 |
468 | // Size the canvas. Render the centroid.
469 | var canvas = document.querySelector('canvas');
470 | var w = window.innerWidth;
471 | var h = window.innerHeight;
472 | var x = centroid.x * w/2;
473 | var y = centroid.y * h;
474 | var size = 10;
475 |
476 | canvas.width = w;
477 | canvas.height = h;
478 |
479 | var ctx = canvas.getContext('2d');
480 | ctx.clearRect(0, 0, w, h);
481 | ctx.fillStyle = 'black';
482 | ctx.fillRect(x - size/2, y - size/2, size, size);
483 |
484 | console.log('Placing eye at (%d, %d).', x, y);
485 |
486 | },{"../src/device-info.js":1}]},{},[4]);
487 |
--------------------------------------------------------------------------------
/examples/js/lib/webvr-manager.js:
--------------------------------------------------------------------------------
1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.WebVRManager = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o VR Reticulum attempts to follow Google's interactive pattern for the display reticle. It creates the illusion of depth by projecting spatially onto targeted objects while maintaining a fixed size so that it is easy to see at all times.VR Reticulum
23 |
25 |
27 |
28 | Examples
29 |
30 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reticulum",
3 | "title": "Reticulum",
4 | "description": "A gaze interaction manager for VR with three.js, supporting depth.",
5 | "version": "1.0.0",
6 | "homepage": "https://github.com/skezo/Reticulum",
7 | "license": "MIT",
8 | "keywords": [
9 | "vr",
10 | "reticle",
11 | "gaze",
12 | "cardboard"
13 | ],
14 | "author": {
15 | "name": "Skezo",
16 | "email": "admin@gqpbj.com"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git://github.com/GQPBJ/Reticulum.git"
21 | },
22 | "bugs": {
23 | "url": "https://github.com/skezo/Reticulum/issues"
24 | },
25 | "dependencies": {},
26 | "devDependencies": {},
27 | "main": "reticulum.js",
28 | "engines": {
29 | "node": ">=0.8.0"
30 | },
31 | "scripts": {}
32 | }
33 |
--------------------------------------------------------------------------------
/reticulum.js:
--------------------------------------------------------------------------------
1 | /*! Reticulum - v2.1.2
2 | * http://skezo.github.io/examples/basic.html
3 | *
4 | * Copyright (c) 2015 Skezo;
5 | * Licensed under the MIT license */
6 |
7 | var Reticulum = (function () {
8 | var INTERSECTED = null;
9 |
10 | var collisionList = [];
11 | var raycaster;
12 | var vector;
13 | var clock;
14 | var reticle = {};
15 | var fuse = {};
16 |
17 | var frustum;
18 | var cameraViewProjectionMatrix;
19 |
20 | var parentContainer
21 |
22 | //Settings from user
23 | var settings = {
24 | camera: null, //Required
25 | proximity: false,
26 | isClickEnabled: true,
27 | lockDistance: false
28 | };
29 |
30 | //Utilities
31 | var utilities = {
32 | clampBottom: function ( x, a ) {
33 | return x < a ? a : x;
34 | }
35 | }
36 |
37 | //Vibrate
38 | var vibrate = navigator.vibrate ? navigator.vibrate.bind(navigator) : function(){};
39 |
40 | //Fuse
41 | fuse.initiate = function( options ) {
42 | var parameters = options || {};
43 |
44 | this.visible = parameters.visible !== false; //default to true;
45 | this.globalDuration = parameters.duration || 2.5;
46 | this.vibratePattern = parameters.vibrate || 100;
47 | this.color = parameters.color || 0x00fff6;
48 | this.innerRadius = parameters.innerRadius || reticle.innerRadiusTo;
49 | this.outerRadius = parameters.outerRadius || reticle.outerRadiusTo;
50 | this.clickCancel = parameters.clickCancelFuse === undefined ? false : parameters.clickCancelFuse; //default to false;
51 | this.phiSegments = 3;
52 | this.thetaSegments = 32;
53 | this.thetaStart = Math.PI/2;
54 | this.duration = this.globalDuration;
55 | this.timeDone = false;
56 | //var geometry = new THREE.CircleGeometry( reticle.outerRadiusTo, 32, Math.PI/2, 0 );
57 | var geometry = new THREE.RingGeometry( this.innerRadius, this.outerRadius, this.thetaSegments, this.phiSegments, this.thetaStart, Math.PI/2 );
58 |
59 | //Make Mesh
60 | this.mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( {
61 | color: this.color,
62 | side: THREE.BackSide,
63 | fog: false
64 | //depthWrite: false,
65 | //depthTest: false
66 | }));
67 |
68 | //Set mesh visibility
69 | this.mesh.visible = this.visible;
70 |
71 | //Change position and rotation of fuse
72 | this.mesh.position.z = 0.0001; // Keep in front of reticle
73 | this.mesh.rotation.y = 180*(Math.PI/180); //Make it clockwise
74 |
75 | //Add to reticle
76 | //reticle.mesh.add( this.mesh );
77 | parentContainer.add( this.mesh );
78 | //geometry.dispose();
79 | };
80 |
81 | fuse.out = function() {
82 | this.active = false;
83 | this.mesh.visible = false;
84 | this.timeDone = false;
85 | this.update(0);
86 | }
87 |
88 | fuse.over = function(duration, visible) {
89 | this.duration = duration || this.globalDuration;
90 | this.active = true;
91 | this.update(0);
92 | this.mesh.visible = visible || this.visible;
93 | }
94 |
95 | fuse.update = function(elapsed) {
96 |
97 | if(!this.active || fuse.timeDone) return;
98 |
99 | //--RING
100 | var gazedTime = elapsed/this.duration;
101 | var thetaLength = gazedTime * (Math.PI*2);
102 |
103 | var vertices = this.mesh.geometry.vertices;
104 | var radius = this.innerRadius;
105 | var radiusStep = ( ( this.outerRadius - this.innerRadius ) / this.phiSegments );
106 | var count = 0;
107 |
108 | for ( var i = 0; i <= this.phiSegments; i ++ ) {
109 |
110 | for ( var o = 0; o <= this.thetaSegments; o++ ) {
111 | var vertex = vertices[ count ];
112 | var segment = this.thetaStart + o / this.thetaSegments * thetaLength;
113 | vertex.x = radius * Math.cos( segment );
114 | vertex.y = radius * Math.sin( segment );
115 | count++;
116 | }
117 | radius += radiusStep;
118 | }
119 |
120 | this.mesh.geometry.verticesNeedUpdate = true;
121 |
122 | //Disable fuse if reached 100%
123 | if(gazedTime >= 1) {
124 | this.active = false;
125 | }
126 | //--RING EOF
127 |
128 |
129 | }
130 |
131 | //Reticle
132 | reticle.initiate = function( options ) {
133 | var parameters = options || {};
134 |
135 | parameters.hover = parameters.hover || {};
136 | parameters.click = parameters.click || {};
137 |
138 | this.active = true;
139 | this.visible = parameters.visible !== false; //default to true;
140 | this.restPoint = parameters.restPoint || settings.camera.far-10.0;
141 | this.globalColor = parameters.color || 0xcc0000;
142 | this.innerRadius = parameters.innerRadius || 0.0004;
143 | this.outerRadius = parameters.outerRadius || 0.003;
144 | this.worldPosition = new THREE.Vector3();
145 | this.ignoreInvisible = parameters.ignoreInvisible !== false; //default to true;
146 |
147 | //Hover
148 | this.innerRadiusTo = parameters.hover.innerRadius || 0.02;
149 | this.outerRadiusTo = parameters.hover.outerRadius || 0.024;
150 | this.globalColorTo = parameters.hover.color || this.color;
151 | this.vibrateHover = parameters.hover.vibrate || 50;
152 | this.hit = false;
153 | //Click
154 | this.vibrateClick = parameters.click.vibrate || 50;
155 | //Animation options
156 | this.speed = parameters.hover.speed || 5;
157 | this.moveSpeed = 0;
158 |
159 | //Colors
160 | this.globalColor = new THREE.Color( this.globalColor );
161 | this.color = this.globalColor.clone();
162 | this.globalColorTo = new THREE.Color( this.globalColorTo );
163 | this.colorTo = this.globalColorTo.clone();
164 |
165 | //Geometry
166 | var geometry = new THREE.RingGeometry( this.innerRadius, this.outerRadius, 32, 3, 0, Math.PI * 2 );
167 | var geometryScale = new THREE.RingGeometry( this.innerRadiusTo, this.outerRadiusTo, 32, 3, 0, Math.PI * 2 );
168 |
169 | //Add Morph Targets for scale animation
170 | geometry.morphTargets.push( { name: "target1", vertices: geometryScale.vertices } );
171 |
172 | //Make Mesh
173 | this.mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( {
174 | color: this.color,
175 | morphTargets: true,
176 | fog: false
177 | //depthWrite: false,
178 | //depthTest: false
179 | }));
180 | this.mesh.visible = this.visible;
181 |
182 | //set depth and scale
183 | this.setDepthAndScale();
184 |
185 | //Add to camera
186 | //settings.camera.add( this.mesh );
187 | parentContainer.add( this.mesh );
188 |
189 | };
190 |
191 | //Sets the depth and scale of the reticle - reduces eyestrain and depth issues
192 | reticle.setDepthAndScale = function( depth ) {
193 | //var crosshair = this.mesh;
194 | var crosshair = parentContainer;
195 | var z = Math.abs( depth || this.restPoint ); //Default to user far setting
196 | var cameraZ = settings.camera.position.z;
197 | //Force reticle to appear the same size - scale
198 | //http://answers.unity3d.com/questions/419342/make-gameobject-size-always-be-the-same.html
199 | var scale = Math.abs( cameraZ - z ) - Math.abs( cameraZ );
200 |
201 | //Set Depth
202 | crosshair.position.x = 0;
203 | crosshair.position.y = 0;
204 | crosshair.position.z = utilities.clampBottom( z, settings.camera.near+0.1 ) * -1;
205 |
206 | //Set Scale
207 | crosshair.scale.set( scale, scale, scale );
208 | };
209 |
210 | reticle.update = function(delta) {
211 | //If not active
212 | if(!this.active) return;
213 |
214 | var accel = delta * this.speed;
215 |
216 | if( this.hit ) {
217 | this.moveSpeed += accel;
218 | this.moveSpeed = Math.min(this.moveSpeed, 1);
219 | } else {
220 | this.moveSpeed -= accel;
221 | this.moveSpeed = Math.max(this.moveSpeed, 0);
222 | }
223 | //Morph
224 | this.mesh.morphTargetInfluences[ 0 ] = this.moveSpeed;
225 | //Set Color
226 | this.color = this.globalColor.clone();
227 | //console.log( this.color.lerp( this.colorTo, this.moveSpeed ) )
228 | this.mesh.material.color = this.color.lerp( this.colorTo, this.moveSpeed );
229 | };
230 |
231 | var initiate = function (camera, options) {
232 | //Update Settings:
233 | options = options || {};
234 |
235 | settings.camera = camera; //required
236 | settings.proximity = options.proximity || settings.proximity;
237 | settings.lockDistance = options.lockDistance || settings.lockDistance;
238 | settings.isClickEnabled = options.clickevents || settings.isClickEnabled;
239 | options.reticle = options.reticle || {};
240 | options.fuse = options.fuse || {};
241 |
242 | //Raycaster Setup
243 | raycaster = new THREE.Raycaster();
244 | vector = new THREE.Vector2(0, 0);
245 | //Update Raycaster
246 | if(options.near && options.near >= 0 ) {
247 | raycaster.near = options.near;
248 | }
249 | if(options.far && options.far >= 0 ) {
250 | raycaster.far = options.far;
251 | }
252 |
253 | //Create Parent Object for reticle and fuse
254 | parentContainer = new THREE.Object3D();
255 | settings.camera.add( parentContainer );
256 |
257 | //Proximity Setup
258 | if( settings.proximity ) {
259 | frustum = new THREE.Frustum();
260 | cameraViewProjectionMatrix = new THREE.Matrix4();
261 | }
262 |
263 | //Enable Click / Tap Events
264 | if( settings.isClickEnabled ) {
265 | document.body.addEventListener('touchend', touchClickHandler, false);
266 | document.body.addEventListener('click', touchClickHandler, false);
267 | }
268 |
269 | //Clock Setup
270 | clock = new THREE.Clock(true);
271 |
272 | //Initiate Reticle
273 | reticle.initiate(options.reticle);
274 |
275 | //Initiate Fuse
276 | fuse.initiate(options.fuse);
277 | };
278 |
279 | var proximity = function() {
280 | var camera = settings.camera;
281 | var showReticle = false;
282 |
283 | //Use frustum to see if any targetable object is visible
284 | //http://stackoverflow.com/questions/17624021/determine-if-a-mesh-is-visible-on-the-viewport-according-to-current-camera
285 | camera.updateMatrixWorld();
286 | camera.matrixWorldInverse.getInverse( camera.matrixWorld );
287 | cameraViewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
288 |
289 | frustum.setFromMatrix( cameraViewProjectionMatrix );
290 |
291 |
292 | for( var i =0, l=collisionList.length; i