Clicking on the Sculpture

In order to allow interaction with the sculpture, users are going to have to be able to click on it in some way.

Luckily, A-Frame provides the Cursor component to allow this. In order to familiarise myself with the component, I decided to follow the example provided in the docs, which allows the user to click to change the colour of entities that they click on.

To begin with, I created a new pug file (collidingWithTerrainModelComponent.pug) to create my demo html page, using the example from the docs and my previously developed component pug file:

doctype html
html
 head
 meta(charset='utf-8')
 title Colliding with the Terrain Model Component Demo
 meta(name='description', content='Colliding with the Terrain Model Component Demo')
 script(src='build.js')
 link(href="style.css", rel="stylesheet", media="all")
 body
 a-scene(antialias='true')
 //Assets
 a-assets
 img(id='sky' src='firstPanorama/2016_10_18_FirstPanorama.JPG')
 //Camera
 a-entity(position='0 0 0' rotation='0 0 0')
 a-entity(camera look-controls wasd-controls)
 a-entity(cursor='fuse: true; fuseTimeout: 500'
 position='0 0 -1'
 geometry='primitive: ring'
 material='color: black; shader: flat')
 //Terrain
 a-entity(id='landscape' position='0 100 0' terrain-model='DEM: url(aframe-terrain-model-component/docs/Olympic-Peninsula/data/clipped-envi.bin); texture: url(aframe-terrain-model-component/docs/Olympic-Peninsula/data/olympic-texture.jpg); planeWidth: 287; planeHeight: 151; segmentsWidth: 287; segmentsHeight: 151; zPosition: 50;' material-side-modifier cursor-listener)
 //Sky
 a-sky(src='#sky')
 //Light
 a-entity(light='type: ambient;')

I created a new js file (cursor-listener.js), with the example code from the Cursor docs:

// Component to change to random colour on click.
AFRAME.registerComponent('cursor-listener', {
 init: function () {
 var COLOURS = ['red', 'green', 'blue'];
 this.el.addEventListener('click', function () {
 var randomIndex = Math.floor(Math.random() * COLOURS.length);
 this.setAttribute('material', 'color', COLOURS[randomIndex]);
 console.log('I was clicked!');
 });
 }
});

I also had to add the cursor-listener.js file to my main.js as a requirement:

require('cursor-listener.js');

I found that the demo was reporting a click, but it wasn’t changing the colour correctly, so I edited the cursor-listener.js file as follows, drawing upon the material-side-modifier.js component that I had developed previously. I renamed the file to cursor-listener-terrain.js as it was apparent that this component wasn’t going to work well with the way that the Terrain Model component was set up:

// Component to change to random colour on click, not functioning presently
AFRAME.registerComponent('cursor-listener-terrain', {

init: function () {
 this.el.addEventListener('click', function () {
 console.log('I was clicked!');
 if(this.el.getObject3D('terrain')){
 var COLOURS = ['red', 'green', 'blue'];
 var randomIndex = Math.floor(Math.random() * COLOURS.length);
 //this.setAttribute('material', 'color', COLOURS[randomIndex]);
 var terrain = this.el.getObject3D('terrain');
 terrain.material.color = COLOURS[randomIndex];
 console.log('I changed the colour!');
 }
 });
 }
});

In order to keep making progress, I decided to work with the Mountain Component that I had previously been working with. I renamed my previously made components to:

material-side-modifier-terrain-model.js
cursor-listener-terrain.js

And updated both the intial component demo and the cursor interaction demo’s to reflect the name changes.

I could then continue developing my colliding with Mountain Component demo and the component that would allow the initial colour changing interaction demo to occur. Because of more standard way that the Mountain component mesh and materials were set up, I found I could use the example code for the cursor listener exactly as in the documentation:

// Component to change to random color on click.
AFRAME.registerComponent('cursor-listener', {
  init: function () {
    var COLORS = ['red', 'green', 'blue'];
    this.el.addEventListener('click', function () {
      var randomIndex = Math.floor(Math.random() * COLORS.length);
      this.setAttribute('material', 'color', COLORS[randomIndex]);
      console.log('I was clicked!');
    });
  }
});

For the sake of completeness I created a demo of a similar interaction but using the Ocean component – I had to create a new modifier component to do that, but tried reuse my cursor-listener that I had used successfully with the Mountain component, unfortunately the Ocean component doesn’t seem to be identifying clicks. I filled the problem as an issue on the Ocean Component Github, and decided to keep working with the Mountain component.

A-Frame also has the Raycaster Component, so I decided to follow it’s tutorial to create a collider-check component, with the mountain component as it was the only component that had proved to be compatible with the cursor-listener component.

I created a new pug file (mountainComponentWithColliderCheck.pug) to to demonstrate the the newly created collider-check component. I also added the collider-check component to my main.js to that Browserify could do it’s magic. The resulting collider-check with Mountain component functioned as expected, as it output to the Javascript Console successfully when the cursor was over the Mountain component.

As the collider-check component was attaching a new listener to the Mountain Component itself:

AFRAME.registerComponent('collider-check', {
 dependencies: ['raycaster'],
 init: function () {
 this.el.addEventListener('raycaster-intersected', function () {
 console.log('Player hit something!');
 });
 }
});

I wanted to see if it would be possible to trigger the update() method of the Mountain Component. I created a new pug file (mountainComponentWithColliderCheckAndUpdate.pug) and component (collider-check-and-update.js):

AFRAME.registerComponent('collider-check-and-update', {
 dependencies: ['raycaster'],
 init: function () {
 this.el.addEventListener('raycaster-intersected', function () {
 console.log('Player hit something!');
 //console.log(this);
 //trying too call the update() method of the Mountain component to which this event listener is now attached
 update(); //this errors with Uncaught ReferenceError: update is not defined
 this.update(); //this doesn't error, but doesn't do anything
 this.el.update(); //this errors with Cannot read property 'update' of undefined
 });
 }
});

As can be seen in the comments above, none of my three methods for calling the update method of the Mountain component are currently functioning. I received a tip from DonMcCurdy on the A-Frame Slack:

@jgl: `el.components['component-name'].update()` should do the trick.

I also had to store a reference to the element before calling the callback, the complete code for collider-check-and-update.js is now:

AFRAME.registerComponent('collider-check-and-update', {
 dependencies: ['raycaster'],
 init: function () {
 var el = this.el; //tip from Don McCurdy, needed to store the element that this was called from
 this.el.addEventListener('raycaster-intersected', function () {
 el.components['mountain'].update(); //tip from Don McCurdy
 });
 }
});

You can try the demo for yourself here.

The interaction was rather clumsy, so I created a new .pug file (mountainComponentWithMouseAndTouchInteraction.pug) and added Touch and Mouse interaction to the entire page, so that clicking or touching anywhere would generate a new terrain – as well as adding a random colour generator for the color and shadowColor attributes of the Mountain component, and finally making the sunPosition attribute random too.

You can try that demo for yourself here.

The frankly amazing Dietrich Ayala created my first ever pull request for the project and added his getUserMedia() work to the project, to make the background panorama live on compatible (i.e. Firefox on Android) devices.

Leave a Reply