How to make A-Frame components talk to each other? - aframe

I want some components to respond to the user's position and orientation in the scene. I have little experience with interactive a-frame scenes and haven't written a component myself.
Generally, I'd want components to be able to provide callbacks for other components to call, or if that's not possible then some kind of inter-component data handoff. The "receiving" component would change its contents (children), appearance and/or behavior.
If we were to take a really simple example, let's say that I want the scene to include either a box if the user is at x>0, or a sphere if they're at x<=0.
Breaking this down, I'll be happy to understand how to...:
Read user position and make it available for others. I found how to read the position; I guess I could just take the <a-scene> element and set some attribute, such as user-position="1 2 3".
Write some code, somewhere, that runs a function when this position changes (I'll debounce it, I imagine) and makes changes to a scene. I think that if I wrote my own component to include the whole scene, I'd need to...:
Set the user position as an attribute on that element;
Define an update method;
In the update method, compare current vs previous user location.
...but I'm wondering if maybe this is overkill and I can just hook somehow into a-scene, or something else entirely.
If I take the approach I mentioned above, I guess the missing piece is how to "declare" what to render? For example, using ReactJS I'd just do return x > 0 ? <a-box/> : <a-sphere/>;. Is there an equivalent, or would I need to reach into the DOM and manually add/remove <a-box> child and such?
Thank you!
EDIT: I sort of got my box/sphere working (glitch), but it feels quite strange, would love to improve this.

How to make A-Frame components talk to each other?
0. setAttribute
You can change any property in any component with
element.setAttribute("component_name", "value");
but I assume you want more than reacting to update calls. Something more flexible than the component schema and a bit more performant when used 60 times per second/
1. events
component 1 emits an event
components 2 - x listen for an event, and react accordingly.
Not dependant on hard-coded component names, you can easily have multiple recipients, and a possibly stable API:
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("position-reader", {
tick: function() {
// read the position and broadcast it around
const pos = this.el.object3D.position;
const positionString = "x: " + pos.x.toFixed(2) +
", z: " + pos.z.toFixed(2)
this.el.emit("position-update", {text: positionString})
}
})
AFRAME.registerComponent("position-renderer", {
init: function() {
const textEl = document.querySelector("a-text");
this.el.addEventListener("position-update", (evt) => {
textEl.setAttribute("value", evt.detail.text);
})
}
})
</script>
<a-scene>
<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-camera position-renderer position-reader>
<a-text position="-0.5 0 -0.75" color="black" value="test"></a-text>
</a-camera>
</a-scene>
2. Directly
Taking this literally, you can grab the component "object" reference with
entity.components["componentName"]
and call its functions:
entity.components["componentName"].function();
For example - one component grabs the current position, and tells the other one to print it:
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("position-reader", {
init: function() {
// wait until the entity is loaded and grab the other component reference
this.el.addEventListener("loaded", evt => {
this.rendererComp = this.el.components["position-renderer"];
})
},
tick: function() {
if (!this.rendererComp) return;
// read the position and call 'updateText' in the 'position-renderer'
const pos = this.el.object3D.position;
const positionString = "x: " + pos.x.toFixed(2) +
", z: " + pos.z.toFixed(2)
this.rendererComp.updateText(positionString)
}
})
AFRAME.registerComponent("position-renderer", {
init: function() {
this.textEl = document.querySelector("a-text");
},
updateText: function(string) {
this.textEl.setAttribute("value", string);
}
})
</script>
<a-scene>
<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-camera position-renderer position-reader>
<a-text position="-0.5 0 -0.75" color="black" value="test"></a-text>
</a-camera>
</a-scene>
In Your case I'd check the position, and manage the elements in one component. Or use one to determine if the position.x > 0 || < 0, and the other one for visibility changes.
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("position-check", {
schema: {
z: {default: 0}
},
tick: function() {
const pos = this.el.object3D.position;
// check if we're 'inside', or outside
if (pos.z >= this.data.z) {
// emit an event only once per occurence
if (!this.inside) this.el.emit("got-inside");
this.inside = true
} else {
// emit an event only once per occurence
if (this.inside) this.el.emit("got-outside");
this.inside = false
}
}
})
AFRAME.registerComponent("manager", {
init: function() {
const box = this.el.querySelector("a-box");
const sphere = this.el.querySelector("a-sphere")
//react to the changes
this.el.sceneEl.camera.el.addEventListener("got-inside", e => {
box.setAttribute("visible", true);
sphere.setAttribute("visible", false);
})
this.el.sceneEl.camera.el.addEventListener("got-outside", e => {
box.setAttribute("visible", false);
sphere.setAttribute("visible", true);
})
}
})
</script>
<a-scene>
<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-entity manager>
<a-box position="0 1 -3" visible="false"></a-box>
<a-sphere position="0 1 -3" visible="false"></a-sphere>
</a-entity>
<a-camera position-check="z: 0"></a-camera>
</a-scene>

Related

In Aframe, how to point directional light to a certain position instead to an entity?

I want my Directional Light to point to [10,5,5]. I don't have any object there. However, in Aframe documentation, target attributed points only to a certain object.
<a-light type="directional" position="0 0 0" rotation="-90 0 0" target="#directionaltarget">
<a-entity id="directionaltarget" position="0 0 -1"></a-entity>
</a-light>
The target doesn't have to be a "physical" object with a mesh. In this case, it's just an empty dummy object which entire purpose is to provide an anchor point where the light should be directed at.
If you want it to be at 10 5 5, then just change the dummys position:
<a-light type="directional" position="0 0 0" rotation="-90 0 0" target="#directionaltarget">
<a-entity id="directionaltarget" position="10 5 5"></a-entity>
</a-light>
Keep in mind, that here 10 5 5 is in regard to the parents position, because the target is nested.
If you don't want to create an entity, you can create the light in threejs - but still it requires a dummy object:
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("left-light", {
init: function() {
const directionalLight = new THREE.DirectionalLight(0xff0000, 1.5); // red light
const target = new THREE.Object3D(); // dummy target
directionalLight.target = target; // direct the light onto the target
directionalLight.position.set(0, 0, 0) // move light to the center
target.position.set(1, 0, 0); // move the target to the right
// add the target and the light
this.el.sceneEl.object3D.add(target);
this.el.sceneEl.object3D.add(directionalLight);
}
})
</script>
<a-scene left-light>
<a-light></a-light>
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#FFFFFF"></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#FFFFFF"></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFFFFF"></a-cylinder>
</a-scene>

AFRAME mouse auto following

Is it possible to make AFRAME cursor to automatically follow the movement?
I readed trough documentation but still didn't find any ideas.
I found this package but this is out-of-date: https://www.npmjs.com/package/aframe-no-click-look-controls
Expected result should be same: https://alexrkass.github.io/aframe-thetarestricted-example/
To "lock" the mouse and move like in an FPS game, you can use the look-controls pointerLockEnabled property:
look-controls="pointerLockEnabled: true"
Check it out in this glitch
If you want to customize it a bit (like that website with angle limiters), I'd try creating a custom component like this one:
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script>
// register component
AFRAME.registerComponent("foo", {
init: function() {
const max_y_angle = 45; // max left right angle
const max_x_angle = 15; // max up down angle
document.body.addEventListener("mousemove", evt => {
// get the mouse position normalized to <-1,1>
const x = -(evt.clientY / window.innerHeight) * 2 + 1;
const y = -(evt.clientX / window.innerWidth) * 2 + 1;
// set the camera entity rotation values
this.el.camera.el.setAttribute("rotation", {
x: max_x_angle * x,
y: max_y_angle * y,
z: 0
})
})
}
})
</script>
<a-scene foo>
<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-entity camera position="0 1.6 0"></a-entity>
</a-scene>
Here's a glitch with a react example.
I got also stuck with react. Can't get it working with react.
The error im getting is: The component foo has been already registered. Check that you are not loading two versions of the same component or two different components of the same name.
import React from "react";
import { graphql, Link } from "gatsby";
import 'swiper/css';
import Layout from "../components/layout";
import "../assets/css/main.css";
import Img from "gatsby-image";
import { Scene } from "aframe-react";
import "aframe";
const IndexPage = ({data}) => {
var AFRAME = require('aframe');
AFRAME.registerComponent('foo', {
init: function() {
const max_y_angle = 45; // max left right angle
const max_x_angle = 15; // max up down angle
document.body.addEventListener("mousemove", evt => {
// get the mouse position normalized to <-1,1>
const x = -(evt.clientY / window.innerHeight) * 2 + 1;
const y = -(evt.clientX / window.innerWidth) * 2 + 1;
this.el.camera.el.setAttribute("rotation", {
x: max_x_angle * x,
y: max_y_angle * y,
z: 0
})
})
}
});
return (
<Layout seo={data.strapiHomepage.seo}>
<div className="full-width loader-view">
<div className="content-before-start">
<Img className="logo-image" fluid={data.logo.childImageSharp.fluid} />
<h1>{data.strapiHomepage.loading_title_homepage}</h1>
<p>{data.strapiHomepage.loading_description_homepage}</p>
<div className="button"><Link to="/main">{data.strapiHomepage.start_vr_homepage}</Link></div>
</div>
<div className="three-six-zero-image">
<Scene foo>
<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-entity camera position="0 1.6 0" ></a-entity>
</Scene>
</div>
</div>
<div>
</div>
</Layout>
);
};

How do I use checkpoint controls in A-Frame?

I am new to A-Frame and still trying to figure everything out! I'm currently constructing a 3D space and would like to create a guided experience for visitors by providing dots on the floor for them to click and be transported to that position. I found this code online which is perfect but I can't get it to work.
Here is the link to my project on Glitch: https://glitch.com/~museum-exhibit-demo
This is the code for my camera:
<a-entity position="1.8 -1.1 3" rotation="0 90 0" id="pov">
<a-camera universal-controls="movementControls: checkpoint" checkpoint-controls="mode: animate">
<a-entity cursor position="0 0 -1" geometry="primitive: ring; radiusInner: 0.01; radiusOuter: 0.015;" material="color: #CCC; shader: flat;"> </a-entity>
</a-camera>
</a-entity>
And this is the code for the cylinder:
<a-cylinder checkpoint radius="0.1.5" height="0.01" position="-0.164 0.111 2.363" color="#39BB82"></a-cylinder>
Can anyone spot where I'm going wrong?
UPDATE:
I just read the current source of aframe-extra and it seems that nothing is broken! In fact there was a backward-incompatible change in the new versions. Rather than the old syntax of:
universal-controls="movementControls: checkpoint;"
Now this new syntax should be used:
movement-controls="controls: checkpoint;"
But keep in mind that since version 3.2.7, the movement offset is calculated on all 3 XYZ axis and therefore the camera will move to the center of the checkpoint. If you want to preserve the height (y) then simply add the code below above line 83:
targetPosition.y = position.y;
Here is a complete working example:
<html>
<head>
<meta charset="utf-8">
<title>Checkpoint Control with AFrame 1.2.0</title>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/donmccurdy/aframe-extras#v6.1.1/dist/aframe-extras.min.js"></script>
</head>
<body>
<a-scene stats>
<!-- CAMERA -->
<a-entity position="0 0 0" id="pov">
<a-camera camera="active: true; spectator: false;" look-controls="pointerLockEnabled:true" movement-controls="controls: checkpoint;" checkpoint-controls="mode: animate; animateSpeed: 10" wasd-controls="enabled: false;" position="0 1.6 22">
<a-cursor></a-cursor>
</a-camera>
</a-entity>
<!-- CHECKPOINTS -->
<a-cylinder checkpoint radius="0.5" height="0.01" position="0 0 20" color="#FF0000"></a-cylinder>
<a-cylinder checkpoint radius="0.5" height="0.01" position="0 0 16" color="#FF0000"></a-cylinder>
<a-cylinder checkpoint radius="0.5" height="0.01" position="0 0 12" color="#FF0000"></a-cylinder>
<a-cylinder checkpoint radius="0.5" height="0.01" position="0 0 8" color="#FF0000"></a-cylinder>
</a-scene>
</body>
</html>
Information below this line is not valid anymore.
As Piotr already mentioned, the new releases of Aframe-Extra are somehow broken!
Using an older version everything will work again.
Below is a working example with Aframe-extra version 3.2.7.
Once the page is fully loaded, click on the screen to lock the cursor, then point the cursor (tiny ring) at a red circle and click.
I also noted few additional options just in case:
spectator: false (switch between 1st and 3rd person view)
pointerLockEnabled:true (hide the mouse)
mode: animate (the other option is teleport)
animateSpeed: 10 (well... adjusts the animation speed)
wasd-controls="enabled: false;" (otherwise user can move around via WASD/arrow keys)
Code:
<html>
<head>
<meta charset="utf-8">
<title>Checkpoint Control with AFrame 1.2.0</title>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/donmccurdy/aframe-extras#v3.2.7/dist/aframe-extras.min.js"></script>
</head>
<body>
<a-scene stats>
<!-- CAMERA -->
<a-entity position="0 0 0" id="pov">
<a-camera camera="active: true; spectator: false;" look-controls="pointerLockEnabled:true" universal-controls="movementControls: checkpoint;" checkpoint-controls="mode: animate; animateSpeed: 10" wasd-controls="enabled: false;" position="0 1.6 22">
<a-cursor></a-cursor>
</a-camera>
</a-entity>
<!-- CHECKPOINTS -->
<a-cylinder checkpoint radius="0.5" height="0.01" position="0 0 20" color="#FF0000"></a-cylinder>
<a-cylinder checkpoint radius="0.5" height="0.01" position="0 0 16" color="#FF0000"></a-cylinder>
<a-cylinder checkpoint radius="0.5" height="0.01" position="0 0 12" color="#FF0000"></a-cylinder>
<a-cylinder checkpoint radius="0.5" height="0.01" position="0 0 8" color="#FF0000"></a-cylinder>
</a-scene>
</body>
</html>
This won't answer the question, but should solve your problem.
You can substitute the checkpoint-controls with a simple animation system:
you click on a cylinder
you animate the camera from the current position to the cylinder
Which could be implemented like this:
// use a system to keep a global track if we are already moving
AFRAME.registerSystem('goto', {
init: function() {
this.isMoving = false
}
})
// this component will have the actual logic
AFRAME.registerComponent('goto', {
init: function() {
let camRig = document.querySelector('#rig')
// upon click - move the camera
this.el.addEventListener('click', e => {
// check if we are already moving
if (this.system.isMoving) return;
// lock other attempts to move
this.system.isMoving = true
// grab the positions
let targetPos = this.el.getAttribute("position")
let rigPos = camRig.getAttribute("position")
// set the animation attributes.
camRig.setAttribute("animation", {
"from": rigPos,
"to": AFRAME.utils.coordinates.stringify({x: targetPos.x, y: rigPos.y, z: targetPos.z}),
"dur": targetPos.distanceTo(rigPos) * 750
})
camRig.emit('go')
})
// when the animation is finished - update the "shared" variable
camRig.addEventListener('animationcomplete', e=> {
this.system.isMoving = false
})
}
})
with a setup like this:
<!-- Camera with locked movement --/>
<a-entity id="rig" animation="property: position; startEvents: go">
<a-camera look-controls wasd-controls-enabled="false"></a-camera>
<a-entity>
<!-- Cylinder node --/>
<a-cylinder goto></a-cylinder>
You can see it working in this glitch.

Raycaster camera with a-sky intersects with cursor

I just started developing with a-frame, please excuse if the answer is obvious.
In my project I would like to get the position of a-sky where the user is looking at. Therefor I implemented a raycaster within the camera, which works fine so far.
HTML
<a-camera listener>
<a-entity raycaster="far: 1000" position="0 -0.9 0" rotation="0 0 0"></a-entity>
</a-camera>
<a-sky follow-intersection
id="sky"
src="#skybox-image">
</a-sky>
TS
AFRAME.registerComponent("follow-intersection", {
init: function() {
this.el.addEventListener("raycaster-intersected", evt => {
this.intersectingRaycaster = evt.detail.el.components.raycaster;
});
this.el.addEventListener("raycaster-intersected-cleared", () => {
this.intersectingRaycaster = null;
});
},
tick: function(t) {
if (!this.intersectingRaycaster) {
return;
}
const intersection = this.intersectingRaycaster.getIntersection(this.el);
if (intersection) {
let point = intersection.uv;
console.log(point.x, point.y);
}
So far this works fine, the problem is that after I set the cursor in the scene (which is needed for the project)
<a-scene
cursor="rayOrigin: listener"
>
I always get the intersections with the cursor, which are not wanted.
How can I only get the intersections of the camera? Thank you!
I don't think the cursor in a-scene is needed. You could just do:
<a-camera listener>
<a-entity cursor raycaster="far: 1000" position="0 -0.9 0" rotation="0 0 0"></a-entity>
</a-camera>

How to stop A-frame from rendering?

I read the documentation about the scene entity that's responsible for rendering the scene, but I didn't find anything regarding stopping the rendering to happen.
Question: Is there a way to halt the rendering?
I'm not sure you can completely stop the rendering, however you can pause specific entities or the scene:
https://aframe.io/docs/0.7.0/core/entity.html#pause
Here's a demo, it waits for the scene to load, and then calls the pause function on the scene, which pauses all components and entities in the scene.
https://glitch.com/edit/#!/a-frame-pause-scene-on-load
<a-scene>
<a-node id="waitOnMe"></a-node>
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" shadow>
<a-animation attribute="rotation"
dur="10000"
fill="forwards"
to="0 360 0"
repeat="indefinite"></a-animation>
</a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E" shadow></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" shadow></a-cylinder>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" shadow></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
Note: I've added an a-node element to guarantee it catches the load event at the end of the page.
Then the js to pause:
<script>
var scene = document.querySelector('a-scene')
scene.addEventListener('loaded', function () {
scene.pause();
});
document.getElementById('waitOnMe').emit('loaded');
</script>
Related info about waiting for load can be found here:
https://stackoverflow.com/a/47363072/8005106
I did not find any satisfactory answer to this, so here's how I managed to stop it (and also start it when needed):
let sceneEl = document.querySelector('a-scene');
let renderer = sceneEl.renderer;
// Stop rendering
renderer.setAnimationLoop(null);
// Start rendering, personally i set this inside a button click
renderer.setAnimationLoop(sceneEl.render);

Resources