A-frame score counter with multiple objects - aframe

I've looked at this answer: Implementing a score counter in A-Frame
... but cannot figure out how to write a similar function that would work for multiple objects, since querySelector only finds the first element.
I'm trying to create a scene with 5-8 objects, and each one can only be clicked once. When an object is clicked, the counter will increment by 1.

Either use querySelectorAll:
var targets = querySelectorAll('a-box')`
for (var i = 0; i < targets.length; i++) { // Add event listeners... }
But better, use a container entity that listens to the click events that bubble up.
<a-entity listener>
<a-box data-raycastable></a-box>
<a-box data-raycastable></a-box>
</a-entity>
<a-entity cursor="rayOrigin: mouse" raycaster="objects: [data-raycastable]"></a-entity>
Component:
AFRAME.registerComponent('listener', {
init: function () {
this.el.addEventListener('click', evt => {
if (evt.target.dataset.wasClicked) { return; }
// evt.target tells you which was clicked.
evt.target.dataset.wasClicked = true;
// Score up.
});
}
});
As a side note, state component is a great place to store your score. https://www.npmjs.com/package/aframe-state-component

Related

A-Frame: parent's raycaster-intersected-cleared triggered if no intersection with children

I'm implementing a feature to get coordinate of an a-sky while moving the VR controller.
<a-scene scenelistener>
<a-sky
id="map-sky"
color="#222"
radius="700"
rotation="0 45 0"
rotation-control
raycaster-listen
class="ray-castable"
>
<a-entity
id="country-tiles"
country-tiles
scale="1 1 1"
rotation="0 90 0"
></a-entity>
</a-sky>
... // Oculus entities
</a-scene>
a-sky and its children is listening for raycasting, and print out the console logging
AFRAME.registerComponent("raycaster-listen", {
init: function() {
this.el.addEventListener("raycaster-intersected", evt => {
console.log(`raycaster ${this.el.id} intersected!!!`);
});
this.el.addEventListener("raycaster-intersected-cleared", evt => {
console.log(`raycaster ${this.el.id} cleared!!!`);
});
}
});
RESULT: while moving in and out the children country-tiles, raycaster-intersected-cleared event is triggered from the children and also its parent map-sky, mean A-Frame cannot get the intersections between a-sky and the raycasting VR controller.
raycaster intersected!!!
raycaster map-sky intersected!!!
// Moving out the tile
raycaster cleared!!!
raycaster map-sky cleared!!!
You can check on this Glitch
NOTE: since I am developing for VR controller, therefore please use the WebXR emulator extension for interaction
It turns out that raycaster-intersected-cleared event from parent element is fired but it does not mean that it is not intersected anymore. To confirm if it has been still intersected I have to check if getIntersection result is NULL.
AFRAME.registerComponent("raycaster-listen", {
init: function() {
this.el.addEventListener("raycaster-intersected", evt => {
console.log(`raycaster ${this.el.id} intersected!!!`);
this.raycaster = evt.detail.el;
});
this.el.addEventListener("raycaster-intersected-cleared", evt => {
if (this.raycaster) {
const intersection = this.raycaster.components.raycaster.getIntersection(this.el);
if (!intersection) {
console.log(`raycaster ${this.el.id} cleared!!!`);
}
}
});
}
});

Add link to GLTF object in aframe

Is it possible to add a link to an GLTF 3d object (which is triggered with a marker?)
I've tried the usual 'a-link' method, the onClick method, even applying an id and using jQuery - all without luck - any help would be appreciated.
<a-scene embedded arjs>
<a-marker id="dragon" preset="custom" type="pattern" url="pattern-dragonfly.patt">
<a-entity animation-mixer="clip: *;" scale="1.5 1.5 1.5" gltf-model-next="src: url(dragon.gltf);"></a-entity>
</a-marker>
<a-entity camera></a-entity>
</a-scene>
To make this work, you need to create a cursor with a raycaster, and a custom component for the gltf.
<a-entity id="mouseCursor" cursor="rayOrigin: mouse" raycaster="objects: .clickable"></a-entity>
<a-entity id="tree" gltf-model="#gltftree" scale="5 5 5" treeman class="clickable" ></a-entity>
Inside the custom component, first you traverse the gltf and store references to the models that you want to be interactive, like this
init: function(){
let el = this.el;
let self = this;
self.trees = [];
el.addEventListener("model-loaded", e =>{
let tree3D = el.getObject3D('mesh');
if (!tree3D){return;}
console.log('tree3D', tree3D);
tree3D.traverse(function(node){
if (node.isMesh){
console.log(node);
self.trees.push(node);
node.material = new THREE.MeshStandardMaterial({color: 0x33aa00});
}
});
});
Then you make event listeners that detect intersection events, and save which object has been intersected, and highlight it, so users know it is live, like this
el.addEventListener('raycaster-intersected', e =>{
self.raycaster = e.detail.el;
let intersection = self.raycaster.components.raycaster.getIntersection(el);
console.log('click', intersection.object.name, self.mouseOverObject,
intersection.object.name != self.mouseOverObject );
if (self.mouseOverObject != intersection.object.name){
intersection.object.material.emissive = new THREE.Color(0xFFFF00);
intersection.object.material.emissiveIntensity = 0.5;
} else {
intersection.object.material.emissive = new THREE.Color(0x000000);
intersection.object.material.emissiveIntensity = 0.0;
}
self.mouseOverObject = intersection.object.name;
});
el.addEventListener('raycaster-intersected-cleared', e =>{
self.trees.forEach(function(tree){
tree.material.emissive = new THREE.Color(0x000000);
tree.material.emissiveIntensity = 0.0;
});
self.mouseOverObject = null;
});
Finally add a click listener that operate the hyperlink, like this
el.addEventListener('click', function(){
console.log(self.mouseOverObject);
if(self.mouseOverObject === "Trunk_A"){
console.log('link');
let url = 'https://supermedium.com/supercraft/';
let win = window.open(url, '_blank');
win.focus();
}
});
glitch here
Click the trunk to activate the hyperlink.

animating a custom attribute in a custom component

I am attempting to animate a material property that is buried inside of a gltf-model (that has many objects). I can find the parameter and animate it with a tick event, and it looks like this
https://glitch.com/~uv-reveal-wave
It almost works, but animating with formulas is a nightmare. Instead I want to control it with the animation component. Unfortunately, exposing the parameter directly seems impossible, as I have to use getObject3D.traverse() just to locate the object, and then get the parameter.
Instead I have made a custom attribute (in schema) and and animating that attribute. And in the update function, I'm using that attribute to drive the buried parameter. It should work, but I can't seem to get a response in the update function. Iow, my custom attribute is not animating.
AFRAME.registerComponent('matclipplane', { // attached to gltf-model tree
schema:{
clipHeightA:{type: 'number', default: 0}
},
init: function () {
let el = this.el;
let comp = this;
comp.treeModels = [];
el.addEventListener('model-loaded', function(ev){
let mesh = el.getObject3D('mesh');
mesh.traverse(function(node){
if (node.isMesh){
comp.treeModels.push(node);
}
comp.modelLoaded = true;
});
...
update: function(){
console.log("update"); // returns nothing. no update from
animating attributes clipHeightA
let comp = this;
if (comp.modelLoaded){
comp.treeModels[1].material.map.offset.y =
this.data.clipHeightA;
}
}
...
AFRAME.registerComponent("click-listener", { // attached to box button
schema:{
dir:{type:"boolean", default: false}
},
init: function(){
let el=this.el;
let data = this.data;
el.addEventListener("click", function(evt){
let tree = document.querySelector('#tree');
data.dir = !data.dir;
if(data.dir){
el.setAttribute("material",'color', 'orange');
tree.emit("grow");
} else {
el.setAttribute('material','color','#332211');
tree.emit('shrink');
}
});
}
});
<a-entity id="tree" gltf-model="#tree" scale="5 5 5"
animation__grow="property: clipHeightA; from: 0; to: 1;
startEvents: grow; dur: 500"
matclipplane></a-entity>
<a-entity id="button" geometry="primitive: box" material="color:orange;
metalness: 0.5" scale="0.2 0.2 0.2" class="clickable" click-listener></a-
entity>
<a-entity id="mouseCursor" cursor="rayOrigin: mouse" raycaster="objects:
.clickable"></a-entity>
here is the glitch in progress
https://glitch.com/~uv-reveal
Clicking on the orange cube should launch the uv reveal animation. I expect that calling a custom component attribute should trigger an update event, but the update does not log in the console.
Not sure how to animate a parameter inside a gltf.
Do I have to implement the animejs component directly inside my custom component?
Or can this be done with the animate component?
According to the docs you can animate properties through
object3D like animation="property: object3D.x"
components like animation="property: components.myComp.myProp"
I don't see it mentioned (although it's used in the docs) but it also works when you provide the property as the components attribute:
animation__grow="property: matclipplane.clipHeightA;"
glitch here. You can see update is being called.
Also tree was an id of both the asset and the component, therefore grow was emitted on the asset item.

How can you detect a double click in a frame?

This event seems to be missing from the standard events. Lick click, I would need to set up a listener for it in a component.
Thanks to Piotr for getting me on the right track. As of this post, there is no double click event for aframe, but we can use js/jquery to get around this. Here is the start of my function:
el.addEventListener('click', function(evt) {
\$('#myEmbeddedSce').on('dblclick', function(event) {
event.stopPropagation();
event.stopImmediatePropagation();
.....
The key was to add the jquery listener inside the aframe listener, and add the stop propagations so the doubleclicks only registered once.
use stopPropagation() to detect the double click in the frame.
The stopPropagation() method prevents propagation of the same event from being called. Propagation means bubbling up to parent elements or capturing down to child elements.
Syntax
event.stopPropagation()
I've solved the issue of detecting by taking tips from existing answers. Ie, tracking the difference of time and distance between clicks. For whatever reason the click listener did not give me clientX and clientY coordinates, so I used the distance vector instead.
This is my solution:
let prevClickTime = Date.now();
let prevVector = new Vector3(0, 0, 0);
// let pointString;
AFRAME.registerComponent('bar', {
init: function () {
this.el.addEventListener('click', (e) => {
const currClickTime = Date.now();
const currVector = e.detail.intersection.point;
const distance = currVector.distanceTo(prevVector);
const timeDiff = currClickTime - prevClickTime;
if (timeDiff < 260 && distance < 0.4) {
console.log('double click');
}
prevClickTime = currClickTime;
prevVector = currVector;
});
},
});
This was pointed at this aframe-react entity.
<Entity
class="interactable"
id="arrow"
bar
geometry={{ primitive: 'cylinder', width: 1, height: 0.05 }}
material={{ color: 'blue', opacity: 0.6 }}
position={`2 0.5 ${adj}`}
rotation="0 0 0"
/>

Handling GearVR Controller events in AFrame

So, I should start out with the warning that I'm new to AFrame, (though not to programming, game programming, or Javascript). I'm currently trying to put together a simple scene with basic GearVR controller interaction.
When I say "basic interaction", I mean I have the little starter scene that you get when you follow the official tutorial, and I'm just trying to expand that out, such that when you press the trigger, (and specifically the trigger, not just any button), the little text component has it's text value changed.
To make a long story short -- my code isn't working. Despite the fact that I add the event listener in the component init function, (and I've confirmed that init is being called), the listener callback never seems to be invoked. If I had to guess, I'd say that the entity I've attached 'laser-controls' to isn't emitting the 'triggerdown' event, but I'm not sure how to confirm that. Here are the relevant pieces:
AFRAME.registerComponent('gearvr-trigger', {
schema: {
textValue: {
default: "Hello WebVR!"
}
},
init: function() {
var message_box = document.querySelector('#message');
var el = this.el;
el.addEventListener('triggerdown', function(evt) {
message_box.setAttribute('value', this.data.textValue);
});
}
});
<a-text id='message' value="Hello, A-Frame!" color="#BBB"
position="-0.9 0.2 -3" scale="1.5 1.5 1.5">
</a-text>
<a-box gearvr-trigger='textValue: "New Text Value";' src='#boxTexture' position='0 2 -5' rotation='0 45 45' scale='2 2 2'>
<a-animation attribute='position' to='0 2.5 -5' direction='alternate' dur='2000' repeat='indefinite'></a-animation>
<a-animation attribute="scale" begin="mouseenter" dur="300" to="2.3 2.3 2.3"></a-animation>
<a-animation attribute="scale" begin="mouseleave" dur="300" to="2 2 2"></a-animation>
<a-animation attribute="rotation" begin="click" dur="2000" to="360 405 45"></a-animation>
</a-box>
<a-entity raycaster='far: 100; showLine: true;' line='color: red;' laser-controls></a-entity>
If someone with more experience than me sees what I'm doing wrong, that would be great, but I'd also be happy if you could just point me at some decent code examples of responding to GearVR controller events.
As others have noted, the reason you're not picking up the 'triggerdown' events in your custom component is that these events are emitted by the controller entity (a-entity[laser-controls] in this case), and they bubble just as in the standard DOM, (which would be next to a-scene in this case), so a-box is not seeing them.
When you combine information about controller buttons and the controller's relation to other entities in the scene (e.g. pointing with a laser or collisions), I like to call that a gesture. The laser-controls component provides basic gesture interpretation, but I have created the super-hands package for rich gesture interpretation including what you require: button discrimination.
<html>
<head>
<script src="https://aframe.io/releases/0.7.1/aframe.min.js"></script>
<script src="https://unpkg.com/super-hands/dist/super-hands.min.js"></script>
<script>
AFRAME.registerComponent('gearvr-trigger', {
schema: {
textValue: {
default: "Hello WebVR!"
}
},
init: function() {
var message_box = document.querySelector('#message');
var el = this.el;
var triggerResponse = function (evt) {
if (evt.detail.state === 'clicked') {
message_box.setAttribute('value', this.data.textValue);
}
}
el.addEventListener('stateadded', triggerResponse.bind(this));
}
});
</script>
</head>
<body>
<a-scene>
<a-assets>
<a-mixin id="point-laser" raycaster='far: 100; showLine: true;' line='color: red;'></a-mixin>
</a-assets>
<a-text id='message' value="Hello, A-Frame!" color="#BBB"
position="-0.9 0.2 -3" scale="1.5 1.5 1.5">
</a-text>
<a-box clickable="startButtons: triggerdown, mousedown; endButtons: triggerup, mouseup" gearvr-trigger='textValue: "New Text Value";' src='#boxTexture' position='0 2 -5' rotation='0 45 45' scale='2 2 2'>
<a-animation attribute='position' to='0 2.5 -5' direction='alternate' dur='2000' repeat='indefinite'></a-animation>
<a-animation attribute="scale" begin="hover-start" dur="300" to="2.3 2.3 2.3"></a-animation>
<a-animation attribute="scale" begin="hover-end" dur="300" to="2 2 2"></a-animation>
<a-animation attribute="rotation" begin="grab-end" dur="2000" to="360 405 45"></a-animation>
</a-box>
<a-entity progressive-controls="maxLevel: point; pointMixin: point-laser" ></a-entity>
</a-scene>
</body>
</html>
For the controller, progressive-controls auto-detects the controller and sets up the laser pointer like laser-controls, but using super-hands gesture interpretation (it also provides cursor-like input for desktop/cardboard users).
<a-entity progressive-controls="maxLevel: point; pointMixin: point-laser"></a-entity>
For the target entity, clickable activates it as a gesture receiving component and defines which button events it will accept (you can remove the mouse ones if you don't want cross-platform support). The animation trigger events were also updatedaa to work with super-hands.
<a-box clickable="startButtons: triggerdown, mousedown; endButtons: triggerup, mouseup" gearvr-trigger='textValue: "New Text Value";' ...>
...
<a-animation attribute="scale" begin="hover-start" dur="300" to="2.3 2.3 2.3"></a-animation>
<a-animation attribute="scale" begin="hover-end" dur="300" to="2 2 2"></a-animation>
<a-animation attribute="rotation" begin="grab-end" dur="2000" to="360 405 45"></a-animation>
For your custom reaction component, I changed it to react to state changes, which is how clickable communicates that is has received an acceptable gesture. I also resolved a scoping issue that you would have caught once you had the events triggering this.
var triggerResponse = function (evt) {
if (evt.detail.state === 'clicked') {
message_box.setAttribute('value', this.data.textValue);
}
}
el.addEventListener('stateadded', triggerResponse.bind(this));
Try setAttribute('text', 'value, 'whatever you wanted') ?
In case it's not the trigger code, it could be a scope issue:
'this' is referring to the element in the handler as opposed to the component, so this.data is undefined during the event
A potential fix:
AFRAME.registerComponent('gearvr-trigger', {
schema: {
textValue: {
default: "Hello WebVR!"
}
},
init: function() {
var message_box = document.querySelector('#message');
var el = this.el;
var self = this;
el.addEventListener('triggerdown', function(evt) {
message_box.setAttribute('value', self.data.textValue);
});
}
});
or using an arrow function
AFRAME.registerComponent('gearvr-trigger', {
schema: {
textValue: {
default: "Hello WebVR!"
}
},
init: function() {
var message_box = document.querySelector('#message');
var el = this.el;
el.addEventListener('triggerdown', (evt) => {
message_box.setAttribute('value', this.data.textValue);
});
}
});
The triggerdown event fires on the entity with the laser-controls attribute.
My code uses the click event and responds to either button press (unlike what you want) but implementing this pattern may nudge you forward. (It's also more on the path to cross-device operation.) The click event may contain which button was pressed.
In the scene, I have:
<a-entity laser-controls="hand: right"></a-entity>
In a script, I register
AFRAME.registerComponent('cursor-listener', {
init: function () {
var lastIndex = -1;
var COLORS = ['purple', 'orange', 'white'];
this.el.addEventListener('click', function (evt) {
lastIndex = (lastIndex + 1) % COLORS.length;
this.setAttribute('material', 'color', COLORS[lastIndex]);
console.log('I was clicked at: ', evt.detail.intersection.point);
});
}
});
Then I set the cursor-listener attribute on the target boxes:
boxEl.setAttribute('cursor-listener', true);

Resources