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.
Related
I have a small A-frame based program to render text on a marker. When user click on the marker, I would like to scale the text, so it will be more visible. I can capture the click-event on the marker but how can I get it's child entities to scale?
My code can be found in https://codepen.io/asatrash/pen/rNLMgpa
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>
<script src="https://raw.githack.com/AR-js-org/AR.js/master/aframe/build/aframe-ar.js"></script>
</head>
<body>
<script>
//Multi Markers WebAR-AR.js and Aframe - Playing the Archive - Connected Environment CASA-UCL
//Global Variable
var markersURLArray=[];
var markersNameArray=[];
var allow_clicks = false;
AFRAME.registerComponent('markers_start',{
init:function(){
console.log('Add markers to the scene');
var sceneEl = document.querySelector('a-scene');
//list of the markers
for(var i=1; i<6; i++)`enter code here`
{
var url="https://raw.githubusercontent.com/asatrash/ARInWeb/main/resources/markers/pattern-Individual_Blocks-3.patt";
markersURLArray.push(url);
markersNameArray.push('Marker_'+i);
//console.log(url);
}
for(var k=0; k<5; k++)
{
var markerEl = document.createElement('a-marker');
markerEl.setAttribute('type','pattern');
markerEl.setAttribute('url',markersURLArray[k]);
markerEl.setAttribute('id',markersNameArray[k]);
markerEl.setAttribute('registerevents','');
sceneEl.appendChild(markerEl);
var boxEl = document.createElement('a-box');
boxEl.setAttribute('id','box');
boxEl.setAttribute('depth','0.001');
boxEl.setAttribute('width','6');
boxEl.setAttribute('height','4');
boxEl.setAttribute('opacity','0.25');
boxEl.setAttribute('position','0 -2 -4');
boxEl.setAttribute('rotation','0 0 0');
boxEl.setAttribute('material', {color: '#000000'});
markerEl.appendChild(boxEl);
var textEl = document.createElement('a-text');
textEl.setAttribute('id','text');
textEl.setAttribute('text',{color: '#ffff00', align: 'left', value:'This is a very log text /b \n line which might or might not wrap', width: '2' });
//textEl.setAttribute('position', '-1.4 1.5 0');
textEl.setAttribute('position', '-2.5 1.5 0')
textEl.setAttribute('scale', '2 2 0');
boxEl.appendChild(textEl);
}
}
});
//Detect marker found and lost
AFRAME.registerComponent('registerevents', {
init: function () {
const marker = this.el;
marker.addEventListener("markerFound", ()=> {
var markerId = marker.id;
console.log('Marker Found: ', markerId);
allow_clicks = true;
});
marker.addEventListener("markerLost",() =>{
var markerId = marker.id;
console.log('Marker Lost: ', markerId);
allow_clicks = false;
});
marker.addEventListener('click', (evt) => {
if (allow_clicks){
console.log("CLICKED!!!" + marker.id);
//I want to scale the box and the text attached to the marker jere
}
});
},
});
</script>
<a-scene markers_start vr-mode-ui="enabled: false" color-space="sRGB" renderer="gammaOutput: true"
embedded arjs='debugUIEnabled: false; sourceType: webcam; patternRatio: 0.85; trackingMethod: best;'>
<a-entity id='userCamera' camera look-controls >
<a-cursor> </a-cursor>
</a-entity>
</a-scene>
</body>
</html>
Did bit more testing and found out the following.
The 'click' event doesn't work as I expected on the marker. It is registered to the whole scene and it get invoked irrespective of the place I click. Which is not what I want. I am looking at using intersection Points to get it working.
However the getting access to the child can be easily done if you put unique ids. We can use the normal document.getElementById("boxelementId"); and change the attributes using the setAttribute method.
Ex: To change the text
var markerTextEl = document.getElementById(markerTextxId);
markerTextEl.setAttribute("value", "Current Temperature is: " + data);
I used above inside the 'markerFound' event to show different/current value when user point to a particular marker.
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.
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"
/>
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
I am trying to place an image in a-frame camera view. please share an example.
A quick way to do this is to add an invisible "marker" as a child of the camera, and use its position as the spawn point when adding an object.
HTML
<a-scene>
<a-camera>
<a-entity id="marker" position="0 0 -5"></a-entity>
</a-camera>
<a-cylinder id="floor" height="0.1" radius="10" color="green"></a-cylinder>
</a-scene>
JS
var sceneEl = document.querySelector('a-scene');
var markerEl = document.querySelector('#marker');
// Add boxe when spacebar is pressed.
document.addEventListener('keyup', function (e) {
if (e.keyCode !== 32) return;
var newEl = document.createElement('a-box');
newEl.setAttribute('color', 'red');
sceneEl.appendChild(newEl);
var position = markerEl.object3D.getWorldPosition();
position.y = 0.5;
newEl.setAttribute('position', position);
});
Codepen: https://codepen.io/donmccurdy/pen/QOOXbK?editors=1010