Skip to content

vanilla touch UI demo of touching button with indexfinger #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions example/touch.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<html>

<head>
<meta charset="utf-8" />
<title>Basic Example — AFrame HTML</title>
<script src="./../aframe-master.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/aframe-environment-component.min.js"></script>
<script src="./../build/aframe-html.js"></script>
</head>

<body>

<script>
// 'pressable' makes objects clickable via indexfingers (by touching it)
// by putting a tiny raycaster on the indexfingers
AFRAME.registerComponent('pressable', {
schema: {
pressDistance: { default: 0.005 },
pressDuration: { default: 300 },
immersiveOnly: { default: true }
},
init: function() {
this.worldPosition = new THREE.Vector3();
this.raycaster = new THREE.Raycaster()
this.handEls = document.querySelectorAll('[hand-tracking-controls]');
this.pressed = false;
this.distance = -1
// we throttle by distance, to support scenes with loads of clickable objects (far away)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clickable -> pressable

this.tick = this.throttleByDistance( () => this.detectPress() )
},
throttleByDistance: function(f){
return function(){
if( this.distance < 0 ) return f() // first call
if( !f.tid ){
let x = this.distance
let y = x*(x*0.05)*1000 // parabolic curve
f.tid = setTimeout( function(){
f.tid = null
f()
}, y )
}
}
},
detectPress: function(){
if( !this.el.sceneEl.renderer.xr.isPresenting ) return // ignore events in desktop mode
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!this.el.sceneEl.is("vr-mode")) is a more common way to check if we're in not vr.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in that case we need to add a check for "ar-mode" too then (I can do that, not the end of the world)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can keep isPresenting. I see that the ar-hit-test component is using it like this as well.


if( this.handEls.length == 0 ){
this.handEls = document.querySelectorAll('[hand-tracking-controls]');
}
var handEls = this.handEls;
var handEl;
let minDistance = 5

// compensate for an object inside a group
let object3D = this.el.object3D.type == "Group" ? this.el.object3D.children[0] : this.el.object3D
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what case this.el.object3D is not a group?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem here is twofold:

  • raycasters cannot cast with object3D type 'Group'
  • AFRAME uses type 'Group' for this.el.object3D

Perhaps better would be object3D.getObjectByProperty("Mesh") (gets the first object3D of type Mesh).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes so the check is unneeded, this is what I'm saying. It could have just been

let object3D = this.el.object3D.children[0];

but yes better use here:

let object3D = this.el.getObject3D('mesh')

Copy link
Author

@coderofsalvation coderofsalvation Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps object3D.getObjectByProperty("Mesh") would be a safer/flexible bet.
'mesh' is AFRAME-only (ArrayMap), and is empty quite often, depending on the implementation of certain components, or when dealing with an object from an glTF.
Another approach could be, what obb-collider does, it tries to mitigate this by offering an object3Dpath (or something similar).
We could also do both, to avoid silent false positives:

let object3D = this.getMesh(this.data.trackedObject3D) || this.el.object3D.getObjectByProperty("Mesh")

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, it's "html" not "mesh" we want here

this.el.setObject3D('html', mesh);

and I guess you want that pressable component to work not only with htmlmesh entities.

We can do something similar to flattenObject3DMaps in aframe raycaster component https://github.com/aframevr/aframe/blob/96c37a38dbe1be9df477adb9b9bf2be4d69d9682/src/components/raycaster.js#L411
getting all meshes from el.object3DMap.

if( !object3D ) return

for (var i = 0; i < handEls.length; i++) {
handEl = handEls[i];
let indexTip = handEl.components['hand-tracking-controls'] ?
handEl.components['hand-tracking-controls'].indexTipPosition :
false
if( ! indexTip ) return // nothing to do here

this.raycaster.far = this.data.pressDistance

// Create a direction vector to negative Z
const direction = new THREE.Vector3(0,0,-1.0);
direction.normalize()
this.raycaster.set(indexTip, direction)
intersects = this.raycaster.intersectObjects([object3D],true)

object3D.getWorldPosition(this.worldPosition)
distance = indexTip.distanceTo(this.worldPosition)
minDistance = distance < minDistance ? distance : minDistance

if (intersects.length ){
this.i = this.i || 0;
if( !this.pressed ){
this.el.emit('pressedstarted', intersects);
this.el.emit('click', {intersection: intersects[0]});
this.pressed = setTimeout( () => {
this.el.emit('pressedended', intersects);
this.pressed = null
}, this.data.pressDuration )
}
}
}
this.distance = minDistance
},

});


function onTouch(){
let $msg = document.querySelector('#msg')
msg.innerText = String(new Date()).substr(15,10)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the dollar in the variable. The code is working because in javascript all ids are variables in the global scope.

msg.style.background = (onTouch.bg = onTouch.bg == 'cyan' ? 'magenta' : 'cyan')
}
onTouch.bg = 'cyan'

</script>

<a-scene webxr="overlayElement:#dom-overlay;">
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
<a-sky color="#ECECEC"></a-sky>
<a-entity html="cursor:#cursor;html:#my-interface" shadow position="0.25 1.5 -0.5" pressable></a-entity>

<a-entity id="leftHand" hand-tracking-controls="hand: left;"></a-entity>
<a-entity id="rightHand" hand-tracking-controls="hand: right;"></a-entity>

</a-scene>

<div id="dom-overlay">
<div id="my-interface" style="width:200px; height:240px; background:#FFF; padding: 10px; z-index:1000; font-family:monospace;">
<h1 id="foo">Hello</h1>
<br>
Touch the button with<br>
your indexfinger<br>
(requires handtracking)
<div id="msg" style="margin-top:10px; text-align:center; padding:20px;"></div>
<br>
<button onclick="onTouch()">touch me</button>
</div>
</div>


</body>

</html>