diff --git a/examples/mixed-reality/watch/button.js b/examples/mixed-reality/watch/button.js
new file mode 100644
index 00000000000..cf6087b3dd4
--- /dev/null
+++ b/examples/mixed-reality/watch/button.js
@@ -0,0 +1,52 @@
+/* global AFRAME */
+AFRAME.registerComponent('button', {
+ init: function () {
+ var buttonContainerEl = this.buttonContainerEl = document.createElement('div');
+ var buttonWristEl = this.buttonWristEl = document.createElement('button');
+ var buttonPalmEl = this.buttonPalmEl = document.createElement('button');
+
+ var style = document.createElement('style');
+ var css =
+ '.a-button-container {box-sizing: border-box; display: inline-block; height: 34px; padding: 0;;' +
+ 'bottom: 20px; width: 200px; left: calc(50% - 75px); position: absolute; color: white;' +
+ 'font-size: 20px; line-height: 20px; border: none;' +
+ 'border-radius: 5px}' +
+ '.a-button {cursor: pointer; padding: 0px 10px 0 10px; font-weight: bold; color: #666; border: 3px solid #666; box-sizing: border-box; vertical-align: middle; max-width: 200px; border-radius: 10px; height: 40px; background-color: white; margin: 0; margin-right: 10px;}' +
+ '.a-button:hover {border-color: #ef2d5e; color: #ef2d5e}' +
+ '.a-button.selected {color: white; background-color: #ef2d5e; border-color: #ef2d5e}';
+
+ if (style.styleSheet) {
+ style.styleSheet.cssText = css;
+ } else {
+ style.appendChild(document.createTextNode(css));
+ }
+ document.getElementsByTagName('head')[0].appendChild(style);
+
+ buttonContainerEl.classList.add('a-button-container');
+
+ buttonPalmEl.classList.add('a-button');
+ buttonPalmEl.classList.add('selected');
+ buttonPalmEl.addEventListener('click', this.onClick.bind(this));
+ buttonContainerEl.appendChild(buttonPalmEl);
+
+ buttonWristEl.classList.add('a-button');
+ buttonWristEl.addEventListener('click', this.onClick.bind(this));
+ buttonContainerEl.appendChild(buttonWristEl);
+
+ this.el.sceneEl.appendChild(buttonContainerEl);
+ buttonWristEl.innerHTML = 'WRIST';
+ buttonPalmEl.innerHTML = 'PALM';
+ },
+
+ onClick: function (evt) {
+ if (evt.target === this.buttonPalmEl) {
+ this.el.querySelector('[hand-menu]').setAttribute('hand-menu', 'location', 'palm');
+ this.buttonPalmEl.classList.add('selected');
+ this.buttonWristEl.classList.remove('selected');
+ } else {
+ this.el.querySelector('[hand-menu]').setAttribute('hand-menu', 'location', 'wrist');
+ this.buttonPalmEl.classList.remove('selected');
+ this.buttonWristEl.classList.add('selected');
+ }
+ }
+});
diff --git a/examples/mixed-reality/watch/hand-menu-button.js b/examples/mixed-reality/watch/hand-menu-button.js
new file mode 100644
index 00000000000..98cc08fd972
--- /dev/null
+++ b/examples/mixed-reality/watch/hand-menu-button.js
@@ -0,0 +1,153 @@
+/* global AFRAME, THREE */
+AFRAME.registerComponent('hand-menu-button', {
+ schema: {
+ src: {default: ''},
+ srcHover: {default: ''},
+ mixin: {default: ''}
+ },
+
+ init: function () {
+ this.onWatchButtonHovered = this.onWatchButtonHovered.bind(this);
+ this.onAnimationComplete = this.onAnimationComplete.bind(this);
+ this.onCollisionStarted = this.onCollisionStarted.bind(this);
+ this.onCollisionEnded = this.onCollisionEnded.bind(this);
+ this.onAnimationBegin = this.onAnimationBegin.bind(this);
+ this.onPinchEnded = this.onPinchEnded.bind(this);
+
+ this.el.addEventListener('obbcollisionstarted', this.onCollisionStarted);
+ this.el.addEventListener('obbcollisionended', this.onCollisionEnded);
+ this.el.object3D.renderOrder = 1000;
+
+ this.menuEl = this.el.parentEl;
+ this.handMenuEl = this.el.sceneEl.querySelector('[hand-menu]');
+
+ this.menuEl.addEventListener('animationbegin', this.onAnimationBegin);
+ this.menuEl.addEventListener('animationcomplete', this.onAnimationComplete);
+ },
+
+ onAnimationBegin: function (evt) {
+ // To prevent menu activations while animation is running.
+ if (evt.detail.name === 'animation__open') { this.menuOpen = false; }
+ },
+
+ onAnimationComplete: function (evt) {
+ if (evt.detail.name === 'animation__open') { this.menuOpen = true; }
+ if (evt.detail.name === 'animation__close') { this.menuOpen = false; }
+ },
+
+ onCollisionStarted: function (evt) {
+ var withEl = evt.detail.withEl;
+ if (this.handMenuEl === withEl ||
+ !withEl.components['hand-tracking-controls']) { return; }
+ if (!this.menuOpen) { return; }
+ this.handHoveringEl = withEl;
+ this.el.emit('watchbuttonhoverstarted');
+ },
+
+ onCollisionEnded: function (evt) {
+ var withEl = evt.detail.withEl;
+ if (this.handMenuEl === withEl ||
+ !withEl.components['hand-tracking-controls']) { return; }
+ this.disableHover();
+ this.handHoveringEl = undefined;
+ this.el.emit('watchbuttonhoverended');
+ },
+
+ enableHover: function () {
+ this.handHoveringEl.addEventListener('pinchended', this.onPinchEnded);
+ this.el.setAttribute('material', 'src', this.data.srcHover);
+ },
+
+ disableHover: function () {
+ if (!this.handHoveringEl) { return; }
+ this.handHoveringEl.removeEventListener('pinchended', this.onPinchEnded);
+ this.el.setAttribute('material', 'src', this.data.src);
+ },
+
+ onPinchEnded: (function () {
+ var spawnPosition = new THREE.Vector3(0, 1, 0);
+ return function () {
+ var cubeEl;
+ var newEntityEl;
+ if (!this.menuOpen) { return; }
+ this.menuOpen = false;
+ if (!this.handHoveringEl || !this.data.mixin) { return; }
+ // Spawn shape a little above the menu.
+ spawnPosition.set(0, 1, 0);
+ // Apply rotation of the menu.
+ spawnPosition.applyQuaternion(this.el.parentEl.object3D.quaternion);
+ // 20cm above the menu.
+ spawnPosition.normalize().multiplyScalar(0.2);
+ spawnPosition.add(this.el.parentEl.object3D.position);
+
+ newEntityEl = document.createElement('a-entity');
+ newEntityEl.setAttribute('mixin', this.data.mixin);
+ newEntityEl.setAttribute('position', spawnPosition);
+ this.el.sceneEl.appendChild(newEntityEl);
+ this.handHoveringEl.removeEventListener('pinchended', this.onPinchEnded);
+ };
+ })(),
+
+ onWatchButtonHovered: function (evt) {
+ if (evt.target === this.el || !this.handHoveringEl) { return; }
+ this.disableHover();
+ this.handHoveringEl = undefined;
+ }
+});
+
+/*
+ User's hand can collide with multiple buttons simulatenously but only want one in a hovered state.
+ This system keeps track of all the collided buttons, keeping just the closest to the hand in a hovered state.
+*/
+AFRAME.registerSystem('hand-menu-button', {
+ init: function () {
+ this.onWatchButtonHovered = this.onWatchButtonHovered.bind(this);
+ this.el.parentEl.addEventListener('watchbuttonhoverended', this.onWatchButtonHovered);
+ this.el.parentEl.addEventListener('watchbuttonhoverstarted', this.onWatchButtonHovered);
+ this.hoveredButtonEls = [];
+ },
+
+ tick: function () {
+ var buttonWorldPosition = new THREE.Vector3();
+ var thumbPosition;
+ var smallestDistance = 1000000;
+ var currentDistance;
+ var closestButtonEl;
+ if (this.hoveredButtonEls.length < 2) { return; }
+ thumbPosition = this.hoveredButtonEls[0].components['hand-menu-button'].handHoveringEl.components['obb-collider'].trackedObject3D.position;
+ for (var i = 0; i < this.hoveredButtonEls.length; ++i) {
+ this.hoveredButtonEls[i].object3D.getWorldPosition(buttonWorldPosition);
+ currentDistance = buttonWorldPosition.distanceTo(thumbPosition);
+ if (currentDistance < smallestDistance) {
+ closestButtonEl = this.hoveredButtonEls[i];
+ smallestDistance = currentDistance;
+ }
+ }
+
+ if (this.hoveredButtonEl === closestButtonEl) { return; }
+
+ this.hoveredButtonEl = closestButtonEl;
+
+ for (i = 0; i < this.hoveredButtonEls.length; ++i) {
+ if (!this.hoveredButtonEls[i].components['hand-menu-button'].handHoveringEl) { continue; }
+ if (this.hoveredButtonEls[i] === closestButtonEl) {
+ this.hoveredButtonEls[i].components['hand-menu-button'].enableHover();
+ continue;
+ }
+ this.hoveredButtonEls[i].components['hand-menu-button'].disableHover();
+ }
+ },
+
+ onWatchButtonHovered: function (evt) {
+ this.buttonEls = this.el.sceneEl.querySelectorAll('[hand-menu-button]');
+ this.hoveredButtonEls = [];
+ for (var i = 0; i < this.buttonEls.length; ++i) {
+ if (!this.buttonEls[i].components['hand-menu-button'].handHoveringEl) { continue; }
+ this.hoveredButtonEls.push(this.buttonEls[i]);
+ }
+ if (this.hoveredButtonEls.length === 1) {
+ this.hoveredButtonEl = this.hoveredButtonEls[0];
+ this.hoveredButtonEls[0].components['hand-menu-button'].enableHover();
+ }
+ }
+});
diff --git a/examples/mixed-reality/watch/hand-menu.js b/examples/mixed-reality/watch/hand-menu.js
new file mode 100644
index 00000000000..22a93ea33fa
--- /dev/null
+++ b/examples/mixed-reality/watch/hand-menu.js
@@ -0,0 +1,243 @@
+/* global AFRAME, THREE */
+AFRAME.registerComponent('hand-menu', {
+ schema: {
+ location: {default: 'palm', oneOf: ['wrist', 'palm']}
+ },
+
+ menuHTML: /* syntax: html */ `
+
+It requires a headset and browser with hand tracking support (e.g Meta browser on Meta headsets). Starting point for hand attached menus. Only 400 lines of code. +
+ ++Pinch to open the menu and release to close or select an option. A wrist menu more familiar but often one hand to occludes the other resulting in tracking loss. A palm menu is less intuitive but more robust against occlusion. +
+ ++Grid shader by onirenaud +Starry Night shader by sneha-belkhale +Watch model by Socksthecat +
\ No newline at end of file diff --git a/examples/mixed-reality/watch/time.js b/examples/mixed-reality/watch/time.js new file mode 100644 index 00000000000..23fe5322e88 --- /dev/null +++ b/examples/mixed-reality/watch/time.js @@ -0,0 +1,9 @@ +/* global AFRAME */ +AFRAME.registerComponent('time', { + tick: function () { + var date = new Date(); + this.el.setAttribute('text', 'value', + date.getHours().toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping: false}) + + ':' + date.getMinutes().toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping: false})); + } +});