21. Custom Components
A-Frame is internally structured around the Entity-Component-System architecture. In this architecture, entities are containers to which we can add components that give entities specific characteristics (appearance, functionality, behaviour).
Entities are represented by the HTML elements <a-*
. Components are represented by the attributes in those elements.
For example, if I write:
<a-entity position="1 2 3"></a-entity>
I'm creating an entity, and giving it the position
component so that the entity can be placed in 3D space.
Components are written in JavaScript and depending on what we want the component to do, we may need to know about other libraries such as Three.js. Three.js is the 3D rendering library used internally by A-Frame to render the 3D scene.
The purpose of this chapter is not talk about Three.js or even JavaScript in detail, but simply to show the high-level structure of a custom component in A-Frame.
Structure of a custom component
Let's say we wish to create a custom component, that triggers toggle_on
and toggle_off
events when users click on the entity:
- first click: trigger
toggle_on
- second click: trigger
toggle_off
- third click: trigger
toggle_on
- fourth click: trigger
toggle_off
- …
To use this component, which we will call toggle
, in our scene we will write something like:
<a-box
toggle
event-set__toggle_on="color: red"
event-set__toggle_off="color: blue"
color="blue"
position="0 1.6 -2"
></a-box>
Notice how we add the toggle
attribute/component but also make use of the event-set
. Our toggle
component simply generates new events, the event-set
than uses those events to create something useful (toggle the color of the box).
So how do we write our custom component? We need to add a <script>
and provide our own JavaScript code inside of it. The skeleton of a component is something like:
<script>
AFRAME.registerComponent('toggle', {
schema: {},
init: function () {},
update: function () {},
tick: function () {},
remove: function () {},
pause: function () {},
play: function () {}
});
</script>
With this, we call AFRAME.registerComponent()
function, which will cause A-Frame to know about our new component. We also define the name of the component -- toggle
-- and provide the implementation for the component. The functions init
, update
, etc., are part of A-Frame's component execution lifecycle.
In our toggle
component, we will make use of the init
function only. This function is called by A-Frame when it detects that the component has been attached to an entity. This function is called only once, to initialize the component.
<script>
AFRAME.registerComponent("toggle", {
schema: {},
init: function () {
this._clickCount = 0;
this.el.addEventListener("click", this.onClick.bind(this));
},
update: function () {},
tick: function () {},
remove: function () {},
pause: function () {},
play: function () {},
onClick: function () {
this._clickCount = this._clickCount + 1;
if (this._clickCount % 2 == 0) {
this.el.emit('toggle_off');
} else {
this.el.emit('toggle_on');
}
},
});
</script>
In the init
function, we simply initialize a variable this._clickCount = 0;
which will keep track of how many clicks the user has done over the entity. We then add a click
event listener to the entity.
The entity to which the component is attached is accessible through the this.el
object. So with this we call tha JavaScript function addEventListener
which needs two parameters: the name of th event, and the function that will handle the event. The function that handles the event is the onClick
, which we define at the bottom. (The onClick.bind(this)
is needed to make sure that the event handler can access the varible that counts the clicks.)
So, every time the user clicks on the entity which has our toggle
component, the onClick
function will run. This function simply increments the click counter and checks whether the click is an odd or even click. Depending on this, it will emit a toggle_off
or toggle_on
events.
We can, of course, use the same toggle
component on multiple entities:
<a-box
toggle
event-set__toggle_on="color: red"
event-set__toggle_off="color: blue"
color="blue"
position="-1 1.6 -2"
></a-box>
<a-box
toggle
event-set__toggle_on="color: green"
event-set__toggle_off="color: yellow"
color="yellow"
position="1 1.6 -2"
></a-box>
Or change multiple characteristics of the entity by applying multiple event-set
s:
<a-box
toggle
event-set__color_on="_event: toggle_on; color: red"
event-set__color_off="_event: toggle_off; color: blue"
event-set__size_on="_event: toggle_on; height: 0.5"
event-set__size_off="_event: toggle_off; height: 1"
color="blue"
position="-1 1.6 -2"
></a-box>
Counting clicks on different entities
To demonstrate a few more details about the use of components, let's now implement a component that is able to count clicks on two different entities and trigger and event when both entities have been clicked twice by the user.
We will use this custom component, which I called my-count
, in our scene, like this:
<a-box
my-count="entity1: #box1; entity2: #box2"
animation="property: rotation; to: 0 360 0; startEvents: myevent; loop:true"
position="0 1.6 -3"
color="yellow"
></a-box>
<a-box
id="box1"
color="red"
position="-1 1.6 -1"
width="0.1"
height="0.1"
depth="0.1"
></a-box>
<a-box
id="box2"
color="blue"
position="1 1.6 -1"
width="0.1"
height="0.1"
depth="0.1"
></a-box>
Notice how this component can be parametrized: my-count="entity1: #box1; entity2: #box2"
. Our are saying to the component that entity one will be the red box (#box1
) and entity two will be the blue box (#box2
) -- these are the entities on which our component will count the clicks. Our component will then emit a myevent
event that is used to start the animation. The end result is that when a user clicks twice on the red box and twice on the blue box, the yellow box will start rotating.
How do we implement this component? The first part is defining the parameters of the component. This is done in the schema
:
AFRAME.registerComponent("my-count", {
schema: {
entity1: { type: "selector" },
entity2: { type: "selector" },
event: { type: "string", default: "myevent" },
},
. . .
In the schema
, we specify the parameters that the component knows about. We specify an entity1
and entity2
parameters of type selector
. A selector
type makes A-Frame fetch the entity from our scene automatically. When we pass the string #box1
as the value for entity1
, A-Frame will search for the entity the the id box1
and assign it to the parameter.
We also specify an event
parameter. Notice how this parameter has a default value of myevent
so that, even if we don't pass a value for this, the component will automatically use myevent
as the value. The values of the parameters can be accessed within the component by using this.data.<parametername>
. For example:
this.data.entity1
So now, in the init
function, we can simply attach the event listeners to the specified entities:
init: function () {
this.clicksOnEntity1 = 0;
this.clicksOnEntity2 = 0;
this.data.entity1.addEventListener("click", this.onClickEntity1.bind(this) );
this.data.entity2.addEventListener("click", this.onClickEntity2.bind(this) );
},
onClickEntity1: function () {
this.clicksOnEntity1++;
this.check();
},
onClickEntity2: function () {
this.clicksOnEntity2++;
this.check();
},
check: function () {
console.log(this.clicksOnEntity1, this.clicksOnEntity2);
if (this.clicksOnEntity1 > 1 && this.clicksOnEntity2 > 1) {
console.log("emitting event myevent");
this.el.emit(this.data.event);
}
},
Time-based media events
The next example custom component shows how we can create components to deal with time-based media, namely videos.
In this example (1800-customcomponents-05-time-based-media)
, we want to be able to trigger the play of a video by clicking on an entity in our scene (a green box).
To do this, we create a custom component video-play
that we can parametrize with the video element that will be played (the video element in the <a-assets>
) and the event that we want to use to trigger it (click
):
AFRAME.registerComponent("video-play", {
schema: {
event: { type: "string" },
video: { type: "selector" }
},
init: function () {
this.el.addEventListener(this.data.event, this.playVideo.bind(this));
},
playVideo: function () {
console.log("Playing video")
this.data.video.play();
}
});
The above component simply adds an event listener to the entity that has the component and, when the event is triggered, calls the play()
function on the video element.
To use it, we attach the video-play
component to the green box.
<a-box
video-play="event: click; video: #countdown"
position="0 1 -1.5"
width="0.5"
height="0.5"
depth="0.5"
color="green">
<a-text value="Click to \nStart"
width="2"
position="0 0 0.25"
align="center"></a-text>
</a-box>
In example 1800-customcomponents-05-time-based-media
, we also want to be able to start a second video when the first one ends. To do this, we create another custom component -- video-events
. This component listens for standard HTML video events and triggers them on the A-Frame entities:
AFRAME.registerComponent("video-events", {
schema: {
video: { type: "selector" }
},
init: function () {
this.data.video.addEventListener("play", this.onVideoPlay.bind(this));
this.data.video.addEventListener("pause", this.onVideoPause.bind(this));
this.data.video.addEventListener("ended", this.onVideoEnded.bind(this));
},
onVideoPlay: function () {
console.log("Video play");
this.el.emit("video_play");
},
onVideoPause: function () {
console.log("Video pause");
this.el.emit("video_pause");
},
onVideoEnded: function () {
console.log("Video ended");
this.el.emit("video_ended");
},
});
This component has a single parameter video
which identifies the video that we want to monitor for events. It then adds event listeners for the standard play
, pause
, and ended
events and emits those events to the entity (their names are changed to video_play
, video_pause
, and video_ended
simply to make it easier to know these are our custom events).
We these two custom components, we can then listen for the video events from the first video and use those events to start the second video:
<a-plane
video-events="video: #countdown"
video-play="event: video_ended; video: #closeup"
src="#countdown"
position="-1 1.6 -2"
></a-plane>
Constantly updating entity's position
The last component is taken from the A-Frame documentation. The follow
component causes an entity in the scene to follow another entity.
In example 1800-customcomponents-06-follow
, a sphere is made to follow the camera (the user).
This is accomplished by using the tick
function of the component, which runs on every frame. Inside this function, you will see a lot of Three.js methods which are outside of the scope of this course.
Exercises
CustomComponents-01
Create a custom component that is able to track a sequence of clicks on three coloured boxes. If the sequence of clicks (for example 3 clicks) is the same as a pre-defined sequence, than the component should trigger a securitypass
event that causes an animation:
(Make a click sequence blue, gree, red, to see the animation in the following example)